diff --git a/lib/components/dashboard/leaderboard_panel.dart b/lib/components/dashboard/leaderboard_panel.dart index a431fd6..9ef1672 100644 --- a/lib/components/dashboard/leaderboard_panel.dart +++ b/lib/components/dashboard/leaderboard_panel.dart @@ -50,7 +50,8 @@ class _LeaderboardPanelState extends State { ), ), ), - if (leaderboard.isNotEmpty) + if (leaderboard.isNotEmpty && + MediaQuery.of(context).size.width > 600) Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), @@ -63,41 +64,43 @@ class _LeaderboardPanelState extends State { 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), + 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) const Padding( padding: EdgeInsets.all(8.0), diff --git a/lib/components/legs/leg_card.dart b/lib/components/legs/leg_card.dart index d67756c..4227753 100644 --- a/lib/components/legs/leg_card.dart +++ b/lib/components/legs/leg_card.dart @@ -28,6 +28,8 @@ class _LegCardState extends State { Widget build(BuildContext context) { final leg = widget.leg; final isShared = leg.legShareId != null && leg.legShareId!.isNotEmpty; + final sharedFrom = leg.sharedFrom; + final sharedTo = leg.sharedTo; final distanceUnits = context.watch(); final routeSegments = _parseRouteSegments(leg.route); final textTheme = Theme.of(context).textTheme; @@ -198,16 +200,9 @@ class _LegCardState extends State { ], ], ), - if (isShared) ...[ + if (isShared || sharedFrom != null || (sharedTo.isNotEmpty)) ...[ const SizedBox(width: 8), - Tooltip( - message: 'Shared entry', - child: Icon( - Icons.share, - size: 18, - color: Theme.of(context).colorScheme.primary, - ), - ), + _SharedIcons(sharedFrom: sharedFrom, sharedTo: sharedTo, isShared: isShared), ], if (widget.showEditButton) ...[ const SizedBox(width: 8), @@ -461,3 +456,61 @@ class _LegCardState extends State { 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 sharedTo; + final bool isShared; + + @override + Widget build(BuildContext context) { + final icons = []; + 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], + ], + ], + ); + } +} diff --git a/lib/objects/objects.dart b/lib/objects/objects.dart index dd24782..3a8d274 100644 --- a/lib/objects/objects.dart +++ b/lib/objects/objects.dart @@ -1061,6 +1061,8 @@ class Leg { final String origin, destination; final List route; final String? legShareId; + final LegShareMeta? sharedFrom; + final List sharedTo; final DateTime beginTime; final DateTime? endTime; final DateTime? originTime; @@ -1092,6 +1094,8 @@ class Leg { this.origin = '', this.destination = '', this.legShareId, + this.sharedFrom, + this.sharedTo = const [], }); factory Leg.fromJson(Map json) { @@ -1099,6 +1103,25 @@ class Leg { final parsedEndTime = (endTimeRaw == null || '$endTimeRaw'.isEmpty) ? null : _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 sharedTo = const []; + final sharedToJson = json['shared_to']; + if (sharedToJson is List) { + sharedTo = sharedToJson + .whereType() + .map( + (e) => LegShareMeta.fromJson( + e.map((k, v) => MapEntry(k.toString(), v)), + ), + ) + .toList(); + } return Leg( id: _asInt(json['leg_id']), tripId: _asInt(json['leg_trip']), @@ -1133,10 +1156,73 @@ class Leg { origin: _asString(json['leg_origin']), destination: _asString(json['leg_destination']), 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 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 { final String id; final Leg entry; diff --git a/pubspec.yaml b/pubspec.yaml index 046a224..8ab566f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 # 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. -version: 0.6.1+4 +version: 0.6.2+5 environment: sdk: ^3.8.1