Add friends leaderboard, reverse button in add page
All checks were successful
Release / meta (push) Successful in 8s
Release / linux-build (push) Successful in 7m11s
Release / web-build (push) Successful in 5m3s
Release / android-build (push) Successful in 18m23s
Release / release-master (push) Successful in 22s
Release / release-dev (push) Successful in 25s
All checks were successful
Release / meta (push) Successful in 8s
Release / linux-build (push) Successful in 7m11s
Release / web-build (push) Successful in 5m3s
Release / android-build (push) Successful in 18m23s
Release / release-master (push) Successful in 22s
Release / release-dev (push) Successful in 25s
This commit is contained in:
@@ -366,6 +366,16 @@ class _RouteCalculatorState extends State<RouteCalculator> {
|
|||||||
spacing: 12,
|
spacing: 12,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
children: [
|
children: [
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.swap_horiz),
|
||||||
|
label: const Text('Reverse route'),
|
||||||
|
onPressed: () async {
|
||||||
|
setState(() {
|
||||||
|
data.stations = data.stations.reversed.toList();
|
||||||
|
});
|
||||||
|
await _calculateRoute(data.stations);
|
||||||
|
},
|
||||||
|
),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
label: const Text('Add Station'),
|
label: const Text('Add Station'),
|
||||||
|
|||||||
@@ -1,19 +1,31 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:mileograph_flutter/services/data_service.dart';
|
import 'package:mileograph_flutter/services/data_service.dart';
|
||||||
import 'package:mileograph_flutter/services/distance_unit_service.dart';
|
import 'package:mileograph_flutter/services/distance_unit_service.dart';
|
||||||
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
enum _LeaderboardScope { global, friends }
|
||||||
|
|
||||||
class LeaderboardPanel extends StatelessWidget {
|
class LeaderboardPanel extends StatefulWidget {
|
||||||
const LeaderboardPanel({super.key});
|
const LeaderboardPanel({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LeaderboardPanel> createState() => _LeaderboardPanelState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LeaderboardPanelState extends State<LeaderboardPanel> {
|
||||||
|
_LeaderboardScope _scope = _LeaderboardScope.global;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final data = context.watch<DataService>();
|
final data = context.watch<DataService>();
|
||||||
final distanceUnits = context.watch<DistanceUnitService>();
|
final distanceUnits = context.watch<DistanceUnitService>();
|
||||||
final leaderboard = data.homepageStats?.leaderboard ?? [];
|
final leaderboard = _scope == _LeaderboardScope.global
|
||||||
|
? (data.homepageStats?.leaderboard ?? [])
|
||||||
|
: data.friendsLeaderboard;
|
||||||
|
final loading = _scope == _LeaderboardScope.global
|
||||||
|
? data.isHomepageLoading
|
||||||
|
: data.isFriendsLeaderboardLoading;
|
||||||
final textTheme = Theme.of(context).textTheme;
|
final textTheme = Theme.of(context).textTheme;
|
||||||
if (data.isHomepageLoading && leaderboard.isEmpty) {
|
if (loading && leaderboard.isEmpty) {
|
||||||
return const Padding(
|
return const Padding(
|
||||||
padding: EdgeInsets.all(16.0),
|
padding: EdgeInsets.all(16.0),
|
||||||
child: Center(child: CircularProgressIndicator()),
|
child: Center(child: CircularProgressIndicator()),
|
||||||
@@ -51,6 +63,38 @@ class LeaderboardPanel extends StatelessWidget {
|
|||||||
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),
|
||||||
|
|||||||
@@ -9,10 +9,19 @@ extension _NewEntryDraftLogic on _NewEntryPageState {
|
|||||||
if (_activeDraftId != null && !_draftChangedFromBaseline()) {
|
if (_activeDraftId != null && !_draftChangedFromBaseline()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
final currentSnapshot = _currentSubmissionSnapshot();
|
||||||
|
if (_lastSubmittedSnapshot != null &&
|
||||||
|
_snapshotEquality.equals(_lastSubmittedSnapshot, currentSnapshot)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
final choice = await _promptSaveDraft();
|
final choice = await _promptSaveDraft();
|
||||||
if (choice == _ExitChoice.cancel) return false;
|
if (choice == _ExitChoice.cancel) return false;
|
||||||
if (choice == _ExitChoice.save) {
|
if (choice == _ExitChoice.save) {
|
||||||
|
try {
|
||||||
await _saveDraftEntry(draftId: _activeDraftId);
|
await _saveDraftEntry(draftId: _activeDraftId);
|
||||||
|
} catch (_) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
} else if (choice == _ExitChoice.discard) {
|
} else if (choice == _ExitChoice.discard) {
|
||||||
// Delay reset to avoid setState during the dialog/build phase.
|
// Delay reset to avoid setState during the dialog/build phase.
|
||||||
await Future<void>.delayed(Duration.zero);
|
await Future<void>.delayed(Duration.zero);
|
||||||
@@ -54,6 +63,7 @@ extension _NewEntryDraftLogic on _NewEntryPageState {
|
|||||||
|
|
||||||
Future<_ExitChoice> _promptSaveDraft() async {
|
Future<_ExitChoice> _promptSaveDraft() async {
|
||||||
if (!mounted) return _ExitChoice.cancel;
|
if (!mounted) return _ExitChoice.cancel;
|
||||||
|
try {
|
||||||
final result = await showDialog<_ExitChoice>(
|
final result = await showDialog<_ExitChoice>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
@@ -79,7 +89,11 @@ extension _NewEntryDraftLogic on _NewEntryPageState {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
if (!mounted) return _ExitChoice.cancel;
|
||||||
return result ?? _ExitChoice.cancel;
|
return result ?? _ExitChoice.cancel;
|
||||||
|
} catch (_) {
|
||||||
|
return _ExitChoice.cancel;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _openDrafts() async {
|
Future<void> _openDrafts() async {
|
||||||
@@ -174,6 +188,14 @@ extension _NewEntryDraftLogic on _NewEntryPageState {
|
|||||||
"distance": _routeResult!.distance,
|
"distance": _routeResult!.distance,
|
||||||
},
|
},
|
||||||
"tractionItems": _serializeTractionItems(),
|
"tractionItems": _serializeTractionItems(),
|
||||||
|
"shareUserIds": _shareUserIds.toList(),
|
||||||
|
"shareUsers": _shareUsers
|
||||||
|
.map((u) => {
|
||||||
|
"user_id": u.userId,
|
||||||
|
"username": u.username,
|
||||||
|
"full_name": u.fullName,
|
||||||
|
})
|
||||||
|
.toList(),
|
||||||
};
|
};
|
||||||
await prefs.setString(_kDraftPrefsKey, jsonEncode(draft));
|
await prefs.setString(_kDraftPrefsKey, jsonEncode(draft));
|
||||||
}
|
}
|
||||||
@@ -449,6 +471,19 @@ extension _NewEntryDraftLogic on _NewEntryPageState {
|
|||||||
..clear()
|
..clear()
|
||||||
..add(_TractionItem.marker());
|
..add(_TractionItem.marker());
|
||||||
}
|
}
|
||||||
|
final shareIdsRaw = data['shareUserIds'];
|
||||||
|
final shareUsersRaw = data['shareUsers'];
|
||||||
|
_shareUserIds = shareIdsRaw is List
|
||||||
|
? shareIdsRaw.map((e) => e.toString()).toSet()
|
||||||
|
: {};
|
||||||
|
_shareUsers = shareUsersRaw is List
|
||||||
|
? shareUsersRaw
|
||||||
|
.whereType<Map>()
|
||||||
|
.map((e) => UserSummary.fromJson(
|
||||||
|
e.map((k, v) => MapEntry(k.toString(), v)),
|
||||||
|
))
|
||||||
|
.toList()
|
||||||
|
: [];
|
||||||
_lastSubmittedSnapshot = null;
|
_lastSubmittedSnapshot = null;
|
||||||
final idRaw = data['id'];
|
final idRaw = data['id'];
|
||||||
if (idRaw != null) {
|
if (idRaw != null) {
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
|||||||
LegShareData? _activeLegShare;
|
LegShareData? _activeLegShare;
|
||||||
String? _sharedFromUser;
|
String? _sharedFromUser;
|
||||||
int? _shareNotificationId;
|
int? _shareNotificationId;
|
||||||
|
bool _routeReversedFlag = false;
|
||||||
|
|
||||||
bool get _isEditing => widget.editLegId != null;
|
bool get _isEditing => widget.editLegId != null;
|
||||||
bool get _draftPersistenceEnabled =>
|
bool get _draftPersistenceEnabled =>
|
||||||
@@ -147,6 +148,48 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _reverseRouteAndEndpoints() {
|
||||||
|
setState(() {
|
||||||
|
// Swap start/end and origin/destination fields
|
||||||
|
final startText = _startController.text;
|
||||||
|
final endText = _endController.text;
|
||||||
|
_startController.text = endText;
|
||||||
|
_endController.text = startText;
|
||||||
|
|
||||||
|
final originText = _originController.text;
|
||||||
|
final destText = _destinationController.text;
|
||||||
|
_originController.text = destText;
|
||||||
|
_destinationController.text = originText;
|
||||||
|
|
||||||
|
// Reverse route result if present
|
||||||
|
if (_routeResult != null) {
|
||||||
|
final reversedInput = _routeResult!.inputRoute.reversed.toList();
|
||||||
|
final reversedCalc = _routeResult!.calculatedRoute.reversed.toList();
|
||||||
|
final reversedCosts = _routeResult!.costs.reversed.toList();
|
||||||
|
_routeResult = RouteResult(
|
||||||
|
inputRoute: reversedInput,
|
||||||
|
calculatedRoute: reversedCalc,
|
||||||
|
costs: reversedCosts,
|
||||||
|
distance: _routeResult!.distance,
|
||||||
|
);
|
||||||
|
final units = _distanceUnits(context);
|
||||||
|
_mileageController.text = _formatDistance(
|
||||||
|
units,
|
||||||
|
_routeResult!.distance,
|
||||||
|
decimals: 2,
|
||||||
|
includeUnit: false,
|
||||||
|
);
|
||||||
|
_useManualMileage = false;
|
||||||
|
} else if (_useManualMileage &&
|
||||||
|
_mileageController.text.trim().isNotEmpty) {
|
||||||
|
// keep manual mileage, just swap endpoints
|
||||||
|
}
|
||||||
|
_routeReversedFlag = !_routeReversedFlag;
|
||||||
|
});
|
||||||
|
_saveDraft();
|
||||||
|
_scheduleMatchUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
double _milesFromInputWithUnit(DistanceUnit unit) {
|
double _milesFromInputWithUnit(DistanceUnit unit) {
|
||||||
return DistanceFormatter(unit)
|
return DistanceFormatter(unit)
|
||||||
.parseInputMiles(_mileageController.text.trim()) ??
|
.parseInputMiles(_mileageController.text.trim()) ??
|
||||||
@@ -1391,17 +1434,53 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
|||||||
_buildTractionList(),
|
_buildTractionList(),
|
||||||
], minHeight: balancedHeight);
|
], minHeight: balancedHeight);
|
||||||
|
|
||||||
|
final routeStart = _routeResult?.calculatedRoute.isNotEmpty == true
|
||||||
|
? _routeResult!.calculatedRoute.first
|
||||||
|
: (_routeResult?.inputRoute.isNotEmpty == true
|
||||||
|
? _routeResult!.inputRoute.first
|
||||||
|
: _startController.text.trim());
|
||||||
|
final routeEnd = _routeResult?.calculatedRoute.isNotEmpty == true
|
||||||
|
? _routeResult!.calculatedRoute.last
|
||||||
|
: (_routeResult?.inputRoute.isNotEmpty == true
|
||||||
|
? _routeResult!.inputRoute.last
|
||||||
|
: _endController.text.trim());
|
||||||
|
|
||||||
final mileagePanel = _section(
|
final mileagePanel = _section(
|
||||||
'Mileage',
|
'Your Journey',
|
||||||
[
|
[
|
||||||
if (!_useManualMileage)
|
if (!_useManualMileage)
|
||||||
Align(
|
Wrap(
|
||||||
alignment: Alignment.centerLeft,
|
spacing: 12,
|
||||||
child: ElevatedButton.icon(
|
runSpacing: 8,
|
||||||
|
children: [
|
||||||
|
ElevatedButton.icon(
|
||||||
onPressed: _openCalculator,
|
onPressed: _openCalculator,
|
||||||
icon: const Icon(Icons.calculate, size: 18),
|
icon: const Icon(Icons.calculate, size: 18),
|
||||||
label: const Text('Open mileage calculator'),
|
label: const Text('Open mileage calculator'),
|
||||||
),
|
),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: _reverseRouteAndEndpoints,
|
||||||
|
icon: const Icon(Icons.swap_horiz),
|
||||||
|
label: const Text('Reverse route'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: _reverseRouteAndEndpoints,
|
||||||
|
icon: const Icon(Icons.swap_horiz),
|
||||||
|
label: const Text('Reverse route'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (routeStart.isNotEmpty || routeEnd.isNotEmpty)
|
||||||
|
ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: const Text('Route'),
|
||||||
|
subtitle: Text(
|
||||||
|
'${routeStart.isEmpty ? 'Start' : routeStart} → ${routeEnd.isEmpty ? 'End' : routeEnd}',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (_useManualMileage)
|
if (_useManualMileage)
|
||||||
TextFormField(
|
TextFormField(
|
||||||
@@ -1467,22 +1546,25 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
|||||||
children: [
|
children: [
|
||||||
entryPanel,
|
entryPanel,
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
if (twoCol) ...[
|
||||||
trainPanel,
|
trainPanel,
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
twoCol
|
Row(
|
||||||
? Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: tractionPanel),
|
Expanded(child: tractionPanel),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(child: mileagePanel),
|
Expanded(child: mileagePanel),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
: Column(
|
] else
|
||||||
|
Column(
|
||||||
children: [
|
children: [
|
||||||
tractionPanel,
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
mileagePanel,
|
mileagePanel,
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
trainPanel,
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
tractionPanel,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|||||||
@@ -71,12 +71,14 @@ extension _NewEntrySubmitLogic on _NewEntryPageState {
|
|||||||
final beginDelay = _parseDelayMinutes(_beginDelayController.text);
|
final beginDelay = _parseDelayMinutes(_beginDelayController.text);
|
||||||
final endDelay =
|
final endDelay =
|
||||||
_hasEndTime ? _parseDelayMinutes(_endDelayController.text) : 0;
|
_hasEndTime ? _parseDelayMinutes(_endDelayController.text) : 0;
|
||||||
final snapshot = _buildSubmissionSnapshot(
|
final snapshot = _currentSubmissionSnapshot(
|
||||||
routeStations: routeStations,
|
routeStations: routeStations,
|
||||||
startVal: startVal,
|
startVal: startVal,
|
||||||
endVal: endVal,
|
endVal: endVal,
|
||||||
mileageVal: mileageVal,
|
mileageVal: mileageVal,
|
||||||
tractionPayload: tractionPayload,
|
tractionPayload: tractionPayload,
|
||||||
|
beginDelay: beginDelay,
|
||||||
|
endDelay: endDelay,
|
||||||
);
|
);
|
||||||
if (_lastSubmittedSnapshot != null &&
|
if (_lastSubmittedSnapshot != null &&
|
||||||
_snapshotEquality.equals(_lastSubmittedSnapshot, snapshot)) {
|
_snapshotEquality.equals(_lastSubmittedSnapshot, snapshot)) {
|
||||||
@@ -161,16 +163,35 @@ extension _NewEntrySubmitLogic on _NewEntryPageState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> _buildSubmissionSnapshot({
|
Map<String, dynamic> _currentSubmissionSnapshot({
|
||||||
required List<String> routeStations,
|
List<String>? routeStations,
|
||||||
required String startVal,
|
String? startVal,
|
||||||
required String endVal,
|
String? endVal,
|
||||||
required double mileageVal,
|
double? mileageVal,
|
||||||
required List<Map<String, dynamic>> tractionPayload,
|
List<Map<String, dynamic>>? tractionPayload,
|
||||||
|
int? beginDelay,
|
||||||
|
int? endDelay,
|
||||||
}) {
|
}) {
|
||||||
final beginDelay = _parseDelayMinutes(_beginDelayController.text);
|
final stations = routeStations ?? (_routeResult?.calculatedRoute ?? []);
|
||||||
final endDelay =
|
final start = startVal ??
|
||||||
_hasEndTime ? _parseDelayMinutes(_endDelayController.text) : 0;
|
(_useManualMileage
|
||||||
|
? _startController.text.trim()
|
||||||
|
: (stations.isNotEmpty ? stations.first : ''));
|
||||||
|
final end = endVal ??
|
||||||
|
(_useManualMileage
|
||||||
|
? _endController.text.trim()
|
||||||
|
: (stations.isNotEmpty ? stations.last : ''));
|
||||||
|
final mileage = mileageVal ??
|
||||||
|
(_useManualMileage
|
||||||
|
? (_distanceUnits(context)
|
||||||
|
.milesFromInput(_mileageController.text.trim()) ??
|
||||||
|
0)
|
||||||
|
: (_routeResult?.distance ?? 0));
|
||||||
|
final traction = tractionPayload ?? _buildTractionPayload();
|
||||||
|
final begin = beginDelay ?? _parseDelayMinutes(_beginDelayController.text);
|
||||||
|
final endDelayVal =
|
||||||
|
endDelay ?? (_hasEndTime ? _parseDelayMinutes(_endDelayController.text) : 0);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"legId": widget.editLegId,
|
"legId": widget.editLegId,
|
||||||
"useManualMileage": _useManualMileage,
|
"useManualMileage": _useManualMileage,
|
||||||
@@ -182,20 +203,20 @@ extension _NewEntrySubmitLogic on _NewEntryPageState {
|
|||||||
"hasOriginTime": _hasOriginTime,
|
"hasOriginTime": _hasOriginTime,
|
||||||
"legDestinationTime": _destinationDateTime?.toIso8601String(),
|
"legDestinationTime": _destinationDateTime?.toIso8601String(),
|
||||||
"hasDestinationTime": _hasDestinationTime,
|
"hasDestinationTime": _hasDestinationTime,
|
||||||
"start": startVal,
|
"start": start,
|
||||||
"end": endVal,
|
"end": end,
|
||||||
"origin": _originController.text.trim(),
|
"origin": _originController.text.trim(),
|
||||||
"destination": _destinationController.text.trim(),
|
"destination": _destinationController.text.trim(),
|
||||||
"routeStations": routeStations,
|
"routeStations": stations,
|
||||||
"mileage": mileageVal,
|
"mileage": mileage,
|
||||||
"network": _networkController.text.trim(),
|
"network": _networkController.text.trim(),
|
||||||
"notes": _notesController.text.trim(),
|
"notes": _notesController.text.trim(),
|
||||||
"headcode": _headcodeController.text.trim(),
|
"headcode": _headcodeController.text.trim(),
|
||||||
"beginDelay": beginDelay,
|
"beginDelay": begin,
|
||||||
"endDelay": endDelay,
|
"endDelay": endDelayVal,
|
||||||
"legShareId": _activeLegShare?.id,
|
"legShareId": _activeLegShare?.id,
|
||||||
"shareUserIds": _shareUserIds.toList(),
|
"shareUserIds": _shareUserIds.toList(),
|
||||||
"locos": tractionPayload,
|
"locos": traction,
|
||||||
"routeResult": _routeResult == null
|
"routeResult": _routeResult == null
|
||||||
? null
|
? null
|
||||||
: {
|
: {
|
||||||
|
|||||||
@@ -108,6 +108,10 @@ class DataService extends ChangeNotifier {
|
|||||||
bool get isHomepageLoading => _isHomepageLoading;
|
bool get isHomepageLoading => _isHomepageLoading;
|
||||||
bool _isOnThisDayLoading = false;
|
bool _isOnThisDayLoading = false;
|
||||||
bool get isOnThisDayLoading => _isOnThisDayLoading;
|
bool get isOnThisDayLoading => _isOnThisDayLoading;
|
||||||
|
List<LeaderboardEntry> _friendsLeaderboard = [];
|
||||||
|
List<LeaderboardEntry> get friendsLeaderboard => _friendsLeaderboard;
|
||||||
|
bool _isFriendsLeaderboardLoading = false;
|
||||||
|
bool get isFriendsLeaderboardLoading => _isFriendsLeaderboardLoading;
|
||||||
|
|
||||||
// Notifications
|
// Notifications
|
||||||
List<UserNotification> _notifications = [];
|
List<UserNotification> _notifications = [];
|
||||||
|
|||||||
@@ -25,4 +25,38 @@ extension DataServiceStats on DataService {
|
|||||||
_notifyAsync();
|
_notifyAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> fetchFriendsLeaderboard() async {
|
||||||
|
if (_isFriendsLeaderboardLoading) return;
|
||||||
|
_isFriendsLeaderboardLoading = true;
|
||||||
|
_notifyAsync();
|
||||||
|
try {
|
||||||
|
final json = await api.get('/stats/leaderboard/friends');
|
||||||
|
List<dynamic>? list;
|
||||||
|
if (json is List) {
|
||||||
|
list = json;
|
||||||
|
} else if (json is Map) {
|
||||||
|
for (final key in ['leaderboard', 'data', 'items', 'results']) {
|
||||||
|
final value = json[key];
|
||||||
|
if (value is List) {
|
||||||
|
list = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_friendsLeaderboard = list
|
||||||
|
?.whereType<Map>()
|
||||||
|
.map((e) => LeaderboardEntry.fromJson(
|
||||||
|
e.map((k, v) => MapEntry(k.toString(), v)),
|
||||||
|
))
|
||||||
|
.toList() ??
|
||||||
|
const [];
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Failed to fetch friends leaderboard: $e');
|
||||||
|
_friendsLeaderboard = [];
|
||||||
|
} finally {
|
||||||
|
_isFriendsLeaderboardLoading = false;
|
||||||
|
_notifyAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.0+3
|
version: 0.6.1+4
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.8.1
|
sdk: ^3.8.1
|
||||||
|
|||||||
Reference in New Issue
Block a user