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(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
@@ -63,41 +64,43 @@ class _LeaderboardPanelState extends State<LeaderboardPanel> {
|
||||
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),
|
||||
|
||||
@@ -28,6 +28,8 @@ class _LegCardState extends State<LegCard> {
|
||||
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<DistanceUnitService>();
|
||||
final routeSegments = _parseRouteSegments(leg.route);
|
||||
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),
|
||||
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<LegCard> {
|
||||
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 List<String> route;
|
||||
final String? legShareId;
|
||||
final LegShareMeta? sharedFrom;
|
||||
final List<LegShareMeta> 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<String, dynamic> 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<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(
|
||||
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<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 {
|
||||
final String id;
|
||||
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
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user