fix leaderboard formatting, save shared users to drafts, display shared legs
All checks were successful
Release / meta (push) Successful in 8s
Release / linux-build (push) Successful in 6m59s
Release / web-build (push) Successful in 6m37s
Release / android-build (push) Successful in 18m3s
Release / release-master (push) Successful in 23s
Release / release-dev (push) Successful in 25s
All checks were successful
Release / meta (push) Successful in 8s
Release / linux-build (push) Successful in 6m59s
Release / web-build (push) Successful in 6m37s
Release / android-build (push) Successful in 18m3s
Release / release-master (push) Successful in 23s
Release / release-dev (push) Successful in 25s
This commit is contained in:
@@ -50,7 +50,8 @@ class _LeaderboardPanelState extends State<LeaderboardPanel> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (leaderboard.isNotEmpty)
|
if (leaderboard.isNotEmpty &&
|
||||||
|
MediaQuery.of(context).size.width > 600)
|
||||||
Container(
|
Container(
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
@@ -63,41 +64,43 @@ class _LeaderboardPanelState extends State<LeaderboardPanel> {
|
|||||||
style: textTheme.labelSmall,
|
style: textTheme.labelSmall,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
|
||||||
SegmentedButton<_LeaderboardScope>(
|
|
||||||
segments: const [
|
|
||||||
ButtonSegment(
|
|
||||||
value: _LeaderboardScope.global,
|
|
||||||
label: Text('Global'),
|
|
||||||
),
|
|
||||||
ButtonSegment(
|
|
||||||
value: _LeaderboardScope.friends,
|
|
||||||
label: Text('Friends'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
selected: {_scope},
|
|
||||||
onSelectionChanged: (vals) async {
|
|
||||||
if (vals.isEmpty) return;
|
|
||||||
final selected = vals.first;
|
|
||||||
setState(() => _scope = selected);
|
|
||||||
if (selected == _LeaderboardScope.friends &&
|
|
||||||
data.friendsLeaderboard.isEmpty &&
|
|
||||||
!data.isFriendsLeaderboardLoading) {
|
|
||||||
await data.fetchFriendsLeaderboard();
|
|
||||||
} else if (selected == _LeaderboardScope.global &&
|
|
||||||
(data.homepageStats?.leaderboard.isEmpty ?? true) &&
|
|
||||||
!data.isHomepageLoading) {
|
|
||||||
await data.fetchHomepageStats();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
style: SegmentedButton.styleFrom(
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
Center(
|
||||||
|
child: SegmentedButton<_LeaderboardScope>(
|
||||||
|
segments: const [
|
||||||
|
ButtonSegment(
|
||||||
|
value: _LeaderboardScope.global,
|
||||||
|
label: Text('Global'),
|
||||||
|
),
|
||||||
|
ButtonSegment(
|
||||||
|
value: _LeaderboardScope.friends,
|
||||||
|
label: Text('Friends'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
selected: {_scope},
|
||||||
|
onSelectionChanged: (vals) async {
|
||||||
|
if (vals.isEmpty) return;
|
||||||
|
final selected = vals.first;
|
||||||
|
setState(() => _scope = selected);
|
||||||
|
if (selected == _LeaderboardScope.friends &&
|
||||||
|
data.friendsLeaderboard.isEmpty &&
|
||||||
|
!data.isFriendsLeaderboardLoading) {
|
||||||
|
await data.fetchFriendsLeaderboard();
|
||||||
|
} else if (selected == _LeaderboardScope.global &&
|
||||||
|
(data.homepageStats?.leaderboard.isEmpty ?? true) &&
|
||||||
|
!data.isHomepageLoading) {
|
||||||
|
await data.fetchHomepageStats();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: SegmentedButton.styleFrom(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
if (leaderboard.isEmpty)
|
if (leaderboard.isEmpty)
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.all(8.0),
|
padding: EdgeInsets.all(8.0),
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ class _LegCardState extends State<LegCard> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final leg = widget.leg;
|
final leg = widget.leg;
|
||||||
final isShared = leg.legShareId != null && leg.legShareId!.isNotEmpty;
|
final isShared = leg.legShareId != null && leg.legShareId!.isNotEmpty;
|
||||||
|
final sharedFrom = leg.sharedFrom;
|
||||||
|
final sharedTo = leg.sharedTo;
|
||||||
final distanceUnits = context.watch<DistanceUnitService>();
|
final distanceUnits = context.watch<DistanceUnitService>();
|
||||||
final routeSegments = _parseRouteSegments(leg.route);
|
final routeSegments = _parseRouteSegments(leg.route);
|
||||||
final textTheme = Theme.of(context).textTheme;
|
final textTheme = Theme.of(context).textTheme;
|
||||||
@@ -198,16 +200,9 @@ class _LegCardState extends State<LegCard> {
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (isShared) ...[
|
if (isShared || sharedFrom != null || (sharedTo.isNotEmpty)) ...[
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Tooltip(
|
_SharedIcons(sharedFrom: sharedFrom, sharedTo: sharedTo, isShared: isShared),
|
||||||
message: 'Shared entry',
|
|
||||||
child: Icon(
|
|
||||||
Icons.share,
|
|
||||||
size: 18,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
if (widget.showEditButton) ...[
|
if (widget.showEditButton) ...[
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
@@ -461,3 +456,61 @@ class _LegCardState extends State<LegCard> {
|
|||||||
return route.map((e) => e.toString()).where((e) => e.trim().isNotEmpty).toList();
|
return route.map((e) => e.toString()).where((e) => e.trim().isNotEmpty).toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _SharedIcons extends StatelessWidget {
|
||||||
|
const _SharedIcons({
|
||||||
|
required this.sharedFrom,
|
||||||
|
required this.sharedTo,
|
||||||
|
required this.isShared,
|
||||||
|
});
|
||||||
|
|
||||||
|
final LegShareMeta? sharedFrom;
|
||||||
|
final List<LegShareMeta> sharedTo;
|
||||||
|
final bool isShared;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final icons = <Widget>[];
|
||||||
|
if (isShared || sharedFrom != null) {
|
||||||
|
final fromName = sharedFrom?.sharedFromDisplay ?? '';
|
||||||
|
icons.add(
|
||||||
|
Tooltip(
|
||||||
|
message: fromName.isNotEmpty ? 'Shared from $fromName' : 'Shared entry',
|
||||||
|
child: Icon(
|
||||||
|
Icons.share,
|
||||||
|
size: 18,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (sharedTo.isNotEmpty) {
|
||||||
|
final names = sharedTo
|
||||||
|
.map((s) => s.sharedToDisplay)
|
||||||
|
.where((name) => name.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
final tooltip = names.isEmpty
|
||||||
|
? 'Shared to others'
|
||||||
|
: 'Shared to: ${names.join(', ')}';
|
||||||
|
icons.add(
|
||||||
|
Tooltip(
|
||||||
|
message: tooltip,
|
||||||
|
child: Icon(
|
||||||
|
Icons.group,
|
||||||
|
size: 18,
|
||||||
|
color: Theme.of(context).colorScheme.tertiary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
for (int i = 0; i < icons.length; i++) ...[
|
||||||
|
if (i > 0) const SizedBox(width: 6),
|
||||||
|
icons[i],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1061,6 +1061,8 @@ class Leg {
|
|||||||
final String origin, destination;
|
final String origin, destination;
|
||||||
final List<String> route;
|
final List<String> route;
|
||||||
final String? legShareId;
|
final String? legShareId;
|
||||||
|
final LegShareMeta? sharedFrom;
|
||||||
|
final List<LegShareMeta> sharedTo;
|
||||||
final DateTime beginTime;
|
final DateTime beginTime;
|
||||||
final DateTime? endTime;
|
final DateTime? endTime;
|
||||||
final DateTime? originTime;
|
final DateTime? originTime;
|
||||||
@@ -1092,6 +1094,8 @@ class Leg {
|
|||||||
this.origin = '',
|
this.origin = '',
|
||||||
this.destination = '',
|
this.destination = '',
|
||||||
this.legShareId,
|
this.legShareId,
|
||||||
|
this.sharedFrom,
|
||||||
|
this.sharedTo = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Leg.fromJson(Map<String, dynamic> json) {
|
factory Leg.fromJson(Map<String, dynamic> json) {
|
||||||
@@ -1099,6 +1103,25 @@ class Leg {
|
|||||||
final parsedEndTime = (endTimeRaw == null || '$endTimeRaw'.isEmpty)
|
final parsedEndTime = (endTimeRaw == null || '$endTimeRaw'.isEmpty)
|
||||||
? null
|
? null
|
||||||
: _asDateTime(endTimeRaw);
|
: _asDateTime(endTimeRaw);
|
||||||
|
LegShareMeta? sharedFrom;
|
||||||
|
final sharedFromJson = json['shared_from'];
|
||||||
|
if (sharedFromJson is Map) {
|
||||||
|
sharedFrom = LegShareMeta.fromJson(
|
||||||
|
sharedFromJson.map((k, v) => MapEntry(k.toString(), v)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
List<LegShareMeta> sharedTo = const [];
|
||||||
|
final sharedToJson = json['shared_to'];
|
||||||
|
if (sharedToJson is List) {
|
||||||
|
sharedTo = sharedToJson
|
||||||
|
.whereType<Map>()
|
||||||
|
.map(
|
||||||
|
(e) => LegShareMeta.fromJson(
|
||||||
|
e.map((k, v) => MapEntry(k.toString(), v)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
return Leg(
|
return Leg(
|
||||||
id: _asInt(json['leg_id']),
|
id: _asInt(json['leg_id']),
|
||||||
tripId: _asInt(json['leg_trip']),
|
tripId: _asInt(json['leg_trip']),
|
||||||
@@ -1133,10 +1156,73 @@ class Leg {
|
|||||||
origin: _asString(json['leg_origin']),
|
origin: _asString(json['leg_origin']),
|
||||||
destination: _asString(json['leg_destination']),
|
destination: _asString(json['leg_destination']),
|
||||||
legShareId: _asString(json['leg_share_id']),
|
legShareId: _asString(json['leg_share_id']),
|
||||||
|
sharedFrom: sharedFrom,
|
||||||
|
sharedTo: sharedTo,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LegShareMeta {
|
||||||
|
final int? legShareId;
|
||||||
|
final int? legId;
|
||||||
|
final String sharedToUserId;
|
||||||
|
final String sharedByUserId;
|
||||||
|
final String status;
|
||||||
|
final DateTime? respondedAt;
|
||||||
|
final bool? acceptedEdits;
|
||||||
|
final DateTime? sharedAt;
|
||||||
|
final String sharedToUsername;
|
||||||
|
final String sharedToFullName;
|
||||||
|
final String sharedByUsername;
|
||||||
|
final String sharedByFullName;
|
||||||
|
|
||||||
|
LegShareMeta({
|
||||||
|
this.legShareId,
|
||||||
|
this.legId,
|
||||||
|
required this.sharedToUserId,
|
||||||
|
required this.sharedByUserId,
|
||||||
|
required this.status,
|
||||||
|
this.respondedAt,
|
||||||
|
this.acceptedEdits,
|
||||||
|
this.sharedAt,
|
||||||
|
this.sharedToUsername = '',
|
||||||
|
this.sharedToFullName = '',
|
||||||
|
this.sharedByUsername = '',
|
||||||
|
this.sharedByFullName = '',
|
||||||
|
});
|
||||||
|
|
||||||
|
factory LegShareMeta.fromJson(Map<String, dynamic> json) {
|
||||||
|
DateTime? parseDate(dynamic raw) {
|
||||||
|
if (raw == null) return null;
|
||||||
|
if (raw is DateTime) return raw;
|
||||||
|
return DateTime.tryParse(raw.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return LegShareMeta(
|
||||||
|
legShareId: _asInt(json['leg_share_id']),
|
||||||
|
legId: _asInt(json['leg_id']),
|
||||||
|
sharedToUserId: _asString(json['shared_to_user_id']),
|
||||||
|
sharedByUserId: _asString(json['shared_by_user_id']),
|
||||||
|
status: _asString(json['status'], 'pending'),
|
||||||
|
respondedAt: parseDate(json['responded_at']),
|
||||||
|
acceptedEdits: json['accepted_edits'] == null
|
||||||
|
? null
|
||||||
|
: _asBool(json['accepted_edits'], false),
|
||||||
|
sharedAt: parseDate(json['shared_at']),
|
||||||
|
sharedToUsername: _asString(json['shared_to_username']),
|
||||||
|
sharedToFullName: _asString(json['shared_to_full_name']),
|
||||||
|
sharedByUsername: _asString(json['shared_by_username']),
|
||||||
|
sharedByFullName: _asString(json['shared_by_full_name']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String get sharedFromDisplay =>
|
||||||
|
sharedByFullName.isNotEmpty ? sharedByFullName : sharedByUsername;
|
||||||
|
|
||||||
|
String get sharedToDisplay =>
|
||||||
|
sharedToFullName.isNotEmpty ? sharedToFullName : sharedToUsername;
|
||||||
|
}
|
||||||
|
|
||||||
class LegShareData {
|
class LegShareData {
|
||||||
final String id;
|
final String id;
|
||||||
final Leg entry;
|
final Leg entry;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 0.6.1+4
|
version: 0.6.2+5
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.8.1
|
sdk: ^3.8.1
|
||||||
|
|||||||
Reference in New Issue
Block a user