Compare commits
6 Commits
v0.1.3-dev
...
v0.2.0-dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a6aee8a15 | |||
| 411e82807b | |||
| 2b4d2623fc | |||
| 80c315866f | |||
| da70dce369 | |||
| 603e117af8 |
@@ -19,6 +19,7 @@ jobs:
|
||||
- mileograph
|
||||
outputs:
|
||||
base_version: ${{ steps.meta.outputs.base }}
|
||||
release_tag: ${{ steps.meta.outputs.release_tag }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -29,6 +30,7 @@ jobs:
|
||||
RAW_VERSION=$(awk '/^version:/{print $2}' pubspec.yaml)
|
||||
BASE_VERSION=${RAW_VERSION%%+*}
|
||||
echo "base=${BASE_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "release_tag=v${BASE_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
android-build:
|
||||
runs-on:
|
||||
@@ -308,7 +310,7 @@ jobs:
|
||||
id: bundle
|
||||
run: |
|
||||
BASE="${{ needs.meta.outputs.base_version }}"
|
||||
TAG="v${BASE}"
|
||||
TAG="${{ needs.meta.outputs.release_tag }}"
|
||||
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "apk=artifacts/mileograph-${BASE}.apk" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -97,10 +97,16 @@ class _StationAutocompleteState extends State<StationAutocomplete> {
|
||||
}
|
||||
|
||||
class RouteCalculator extends StatefulWidget {
|
||||
const RouteCalculator({super.key, this.onDistanceComputed, this.onApplyRoute});
|
||||
const RouteCalculator({
|
||||
super.key,
|
||||
this.onDistanceComputed,
|
||||
this.onApplyRoute,
|
||||
this.initialStations,
|
||||
});
|
||||
|
||||
final ValueChanged<double>? onDistanceComputed;
|
||||
final ValueChanged<RouteResult>? onApplyRoute;
|
||||
final List<String>? initialStations;
|
||||
|
||||
@override
|
||||
State<RouteCalculator> createState() => _RouteCalculatorState();
|
||||
@@ -122,6 +128,9 @@ class _RouteCalculatorState extends State<RouteCalculator> {
|
||||
super.didChangeDependencies();
|
||||
if (!_fetched) {
|
||||
_fetched = true;
|
||||
if (widget.initialStations != null && widget.initialStations!.isNotEmpty) {
|
||||
context.read<DataService>().stations = List.from(widget.initialStations!);
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final data = context.read<DataService>();
|
||||
final result = await data.fetchStations();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:mileograph_flutter/objects/objects.dart';
|
||||
import 'package:mileograph_flutter/services/dataService.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -279,6 +280,11 @@ class _LegsPageState extends State<LegsPage> {
|
||||
trailing: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: 'Edit entry',
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () => context.push('/legs/edit/${leg.id}'),
|
||||
),
|
||||
Text(
|
||||
'${leg.mileage.toStringAsFixed(1)} mi',
|
||||
style:
|
||||
|
||||
1263
lib/components/pages/loco_timeline.dart
Normal file
1263
lib/components/pages/loco_timeline.dart
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -423,57 +423,68 @@ class _TractionPageState extends State<TractionPage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (data.isTractionLoading && traction.isEmpty)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 24.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
else if (traction.isEmpty)
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'No traction found',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text('Try relaxing the filters or sync again.'),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Column(
|
||||
children: [
|
||||
...traction.map((loco) => _buildTractionCard(context, loco)),
|
||||
if (data.tractionHasMore || data.isTractionLoading)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: data.isTractionLoading
|
||||
? null
|
||||
: () => _refreshTraction(append: true),
|
||||
icon: data.isTractionLoading
|
||||
? const SizedBox(
|
||||
height: 14,
|
||||
width: 14,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.expand_more),
|
||||
label: Text(
|
||||
data.isTractionLoading ? 'Loading...' : 'Load more',
|
||||
),
|
||||
Stack(
|
||||
children: [
|
||||
if (data.isTractionLoading && traction.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 32.0),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
else if (traction.isEmpty)
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'No traction found',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text('Try relaxing the filters or sync again.'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Column(
|
||||
children: [
|
||||
...traction.map((loco) => _buildTractionCard(context, loco)),
|
||||
if (data.tractionHasMore || data.isTractionLoading)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: data.isTractionLoading
|
||||
? null
|
||||
: () => _refreshTraction(append: true),
|
||||
icon: data.isTractionLoading
|
||||
? const SizedBox(
|
||||
height: 14,
|
||||
width: 14,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.expand_more),
|
||||
label: Text(
|
||||
data.isTractionLoading ? 'Loading...' : 'Load more',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (data.isTractionLoading)
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surface.withOpacity(0.6),
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -574,6 +585,12 @@ class _TractionPageState extends State<TractionPage> {
|
||||
icon: const Icon(Icons.info_outline),
|
||||
label: const Text('Details'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
TextButton.icon(
|
||||
onPressed: () => _openTimeline(loco),
|
||||
icon: const Icon(Icons.timeline),
|
||||
label: const Text('Timeline'),
|
||||
),
|
||||
const Spacer(),
|
||||
if (widget.selectionMode)
|
||||
TextButton.icon(
|
||||
@@ -692,6 +709,14 @@ class _TractionPageState extends State<TractionPage> {
|
||||
return (background, foreground);
|
||||
}
|
||||
|
||||
void _openTimeline(LocoSummary loco) {
|
||||
final label = '${loco.locoClass} ${loco.number}'.trim();
|
||||
context.push(
|
||||
'/traction/${loco.id}/timeline',
|
||||
extra: {'label': label},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showLocoInfo(LocoSummary loco) async {
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
|
||||
@@ -301,6 +301,25 @@ class _TripsPageState extends State<TripsPage> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (!loading && items.isNotEmpty) ...[
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
Chip(
|
||||
avatar: const Icon(Icons.train, size: 16),
|
||||
label: Text('Total had: ${items.length}'),
|
||||
),
|
||||
Chip(
|
||||
avatar: const Icon(Icons.star, size: 16),
|
||||
label: Text(
|
||||
'Winners: ${items.where((e) => e.won == true).length}',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
if (loading)
|
||||
const Center(
|
||||
child: Padding(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:mileograph_flutter/components/pages/calculator.dart';
|
||||
import 'package:mileograph_flutter/components/pages/loco_timeline.dart';
|
||||
import 'package:mileograph_flutter/components/pages/new_entry.dart';
|
||||
import 'package:mileograph_flutter/components/pages/new_traction.dart';
|
||||
import 'package:mileograph_flutter/components/pages/traction.dart';
|
||||
@@ -11,6 +12,7 @@ import 'package:mileograph_flutter/components/pages/legs.dart';
|
||||
import 'package:mileograph_flutter/services/apiService.dart';
|
||||
import 'package:mileograph_flutter/services/authservice.dart';
|
||||
import 'package:mileograph_flutter/services/dataService.dart';
|
||||
import 'package:mileograph_flutter/services/navigation_guard.dart';
|
||||
|
||||
import 'components/login/login.dart';
|
||||
import 'components/pages/dashboard.dart';
|
||||
@@ -94,12 +96,41 @@ class MyApp extends StatelessWidget {
|
||||
),
|
||||
GoRoute(path: '/legs', builder: (_, __) => LegsPage()),
|
||||
GoRoute(path: '/traction', builder: (_, __) => TractionPage()),
|
||||
GoRoute(
|
||||
path: '/traction/:id/timeline',
|
||||
builder: (_, state) {
|
||||
final idParam = state.pathParameters['id'];
|
||||
final locoId = int.tryParse(idParam ?? '') ?? 0;
|
||||
final extra = state.extra;
|
||||
String label = state.uri.queryParameters['label'] ?? '';
|
||||
if (extra is Map && extra['label'] is String) {
|
||||
label = extra['label'] as String;
|
||||
} else if (extra is String && extra.isNotEmpty) {
|
||||
label = extra;
|
||||
}
|
||||
if (label.trim().isEmpty) {
|
||||
label = 'Loco $locoId';
|
||||
}
|
||||
return LocoTimelinePage(
|
||||
locoId: locoId,
|
||||
locoLabel: label,
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/traction/new',
|
||||
builder: (_, __) => const NewTractionPage(),
|
||||
),
|
||||
GoRoute(path: '/trips', builder: (_, __) => TripsPage()),
|
||||
GoRoute(path: '/add', builder: (_, __) => NewEntryPage()),
|
||||
GoRoute(
|
||||
path: '/legs/edit/:id',
|
||||
builder: (_, state) {
|
||||
final idParam = state.pathParameters['id'];
|
||||
final legId = idParam == null ? null : int.tryParse(idParam);
|
||||
return NewEntryPage(editLegId: legId);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(path: '/login', builder: (_, __) => const LoginScreen()),
|
||||
@@ -180,12 +211,14 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
return newIndex;
|
||||
}
|
||||
|
||||
void _onItemTapped(int index, int currentIndex) {
|
||||
Future<void> _onItemTapped(int index, int currentIndex) async {
|
||||
if (index < 0 || index >= contentPages.length || index == currentIndex) {
|
||||
return;
|
||||
}
|
||||
context.push(contentPages[index]);
|
||||
_getIndexFromLocation(contentPages[index]);
|
||||
await NavigationGuard.attemptNavigation(() async {
|
||||
if (!mounted) return;
|
||||
context.go(contentPages[index]);
|
||||
});
|
||||
}
|
||||
|
||||
bool loggedIn = false;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class DestinationObject {
|
||||
const DestinationObject(
|
||||
@@ -190,6 +191,143 @@ class LocoSummary extends Loco {
|
||||
);
|
||||
}
|
||||
|
||||
class LocoAttrVersion {
|
||||
final String attrCode;
|
||||
final int? versionId;
|
||||
final int locoId;
|
||||
final int? attrTypeId;
|
||||
final String? valueStr;
|
||||
final int? valueInt;
|
||||
final DateTime? valueDate;
|
||||
final bool? valueBool;
|
||||
final String? valueEnum;
|
||||
final DateTime? validFrom;
|
||||
final DateTime? validTo;
|
||||
final DateTime? txnFrom;
|
||||
final DateTime? txnTo;
|
||||
final String? suggestedBy;
|
||||
final String? approvedBy;
|
||||
final DateTime? approvedAt;
|
||||
final int? sourceEventId;
|
||||
final String? precisionLevel;
|
||||
final String? maskedValidFrom;
|
||||
final dynamic valueNorm;
|
||||
|
||||
const LocoAttrVersion({
|
||||
required this.attrCode,
|
||||
required this.locoId,
|
||||
this.versionId,
|
||||
this.attrTypeId,
|
||||
this.valueStr,
|
||||
this.valueInt,
|
||||
this.valueDate,
|
||||
this.valueBool,
|
||||
this.valueEnum,
|
||||
this.validFrom,
|
||||
this.validTo,
|
||||
this.txnFrom,
|
||||
this.txnTo,
|
||||
this.suggestedBy,
|
||||
this.approvedBy,
|
||||
this.approvedAt,
|
||||
this.sourceEventId,
|
||||
this.precisionLevel,
|
||||
this.maskedValidFrom,
|
||||
this.valueNorm,
|
||||
});
|
||||
|
||||
factory LocoAttrVersion.fromJson(Map<String, dynamic> json) {
|
||||
return LocoAttrVersion(
|
||||
attrCode: json['attr_code']?.toString() ?? '',
|
||||
locoId: (json['loco_id'] as num?)?.toInt() ?? 0,
|
||||
versionId: (json['loco_attr_v_id'] as num?)?.toInt(),
|
||||
attrTypeId: (json['attr_type_id'] as num?)?.toInt(),
|
||||
valueStr: json['value_str']?.toString(),
|
||||
valueInt: (json['value_int'] as num?)?.toInt(),
|
||||
valueDate: _parseDate(json['value_date']),
|
||||
valueBool: _parseBool(json['value_bool']),
|
||||
valueEnum: json['value_enum']?.toString(),
|
||||
validFrom: _parseDate(json['valid_from']),
|
||||
validTo: _parseDate(json['valid_to']),
|
||||
txnFrom: _parseDate(json['txn_from']),
|
||||
txnTo: _parseDate(json['txn_to']),
|
||||
suggestedBy: json['suggested_by']?.toString(),
|
||||
approvedBy: json['approved_by']?.toString(),
|
||||
approvedAt: _parseDate(json['approved_at']),
|
||||
sourceEventId: (json['source_event_id'] as num?)?.toInt(),
|
||||
precisionLevel: json['precision_level']?.toString(),
|
||||
maskedValidFrom: json['masked_valid_from']?.toString(),
|
||||
valueNorm: json['value_norm'],
|
||||
);
|
||||
}
|
||||
|
||||
static DateTime? _parseDate(dynamic value) {
|
||||
if (value == null) return null;
|
||||
if (value is DateTime) return value;
|
||||
return DateTime.tryParse(value.toString());
|
||||
}
|
||||
|
||||
static bool? _parseBool(dynamic value) {
|
||||
if (value == null) return null;
|
||||
if (value is bool) return value;
|
||||
if (value is num) return value != 0;
|
||||
final str = value.toString().toLowerCase();
|
||||
if (['true', '1', 'yes'].contains(str)) return true;
|
||||
if (['false', '0', 'no'].contains(str)) return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<LocoAttrVersion> fromGroupedJson(dynamic json) {
|
||||
final List<LocoAttrVersion> items = [];
|
||||
if (json is Map) {
|
||||
json.forEach((key, value) {
|
||||
if (value is List) {
|
||||
for (final entry in value) {
|
||||
if (entry is Map<String, dynamic>) {
|
||||
final merged = Map<String, dynamic>.from(entry);
|
||||
merged.putIfAbsent('attr_code', () => key);
|
||||
items.add(LocoAttrVersion.fromJson(merged));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
items.sort(
|
||||
(a, b) {
|
||||
final aDate = a.validFrom ?? a.txnFrom ?? DateTime.fromMillisecondsSinceEpoch(0);
|
||||
final bDate = b.validFrom ?? b.txnFrom ?? DateTime.fromMillisecondsSinceEpoch(0);
|
||||
final dateCompare = aDate.compareTo(bDate);
|
||||
if (dateCompare != 0) return dateCompare;
|
||||
return a.attrCode.compareTo(b.attrCode);
|
||||
},
|
||||
);
|
||||
return items;
|
||||
}
|
||||
|
||||
String get valueLabel {
|
||||
if (valueStr != null && valueStr!.isNotEmpty) return valueStr!;
|
||||
if (valueEnum != null && valueEnum!.isNotEmpty) return valueEnum!;
|
||||
if (valueInt != null) return valueInt!.toString();
|
||||
if (valueBool != null) return valueBool! ? 'Yes' : 'No';
|
||||
if (valueDate != null) return DateFormat('yyyy-MM-dd').format(valueDate!);
|
||||
if (valueNorm != null && valueNorm.toString().isNotEmpty) {
|
||||
return valueNorm.toString();
|
||||
}
|
||||
return '—';
|
||||
}
|
||||
|
||||
String get validityRange {
|
||||
final start = maskedValidFrom ?? _formatDate(validFrom) ?? 'Unknown';
|
||||
final end = _formatDate(validTo, fallback: 'Present') ?? 'Present';
|
||||
return '$start → $end';
|
||||
}
|
||||
|
||||
String? _formatDate(DateTime? value, {String? fallback}) {
|
||||
if (value == null) return fallback;
|
||||
return DateFormat('yyyy-MM-dd').format(value);
|
||||
}
|
||||
}
|
||||
|
||||
class LeaderboardEntry {
|
||||
final String userId, username, userFullName;
|
||||
final double mileage;
|
||||
|
||||
@@ -97,10 +97,7 @@ class ApiService {
|
||||
return body;
|
||||
}
|
||||
|
||||
if (res.statusCode == 401 &&
|
||||
body is Map<String, dynamic> &&
|
||||
body['detail'] == 'Not authenticated' &&
|
||||
_onUnauthorized != null) {
|
||||
if (res.statusCode == 401 && _onUnauthorized != null) {
|
||||
await _onUnauthorized!();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
@@ -49,6 +50,12 @@ class DataService extends ChangeNotifier {
|
||||
bool get isTractionLoading => _isTractionLoading;
|
||||
bool _tractionHasMore = false;
|
||||
bool get tractionHasMore => _tractionHasMore;
|
||||
final Map<int, List<LocoAttrVersion>> _locoTimelines = {};
|
||||
final Map<int, bool> _isLocoTimelineLoading = {};
|
||||
List<LocoAttrVersion> timelineForLoco(int locoId) =>
|
||||
_locoTimelines[locoId] ?? [];
|
||||
bool isLocoTimelineLoading(int locoId) =>
|
||||
_isLocoTimelineLoading[locoId] ?? false;
|
||||
|
||||
// Trips
|
||||
List<TripSummary> _trips = [];
|
||||
@@ -235,6 +242,24 @@ class DataService extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<LocoAttrVersion>> fetchLocoTimeline(int locoId) async {
|
||||
_isLocoTimelineLoading[locoId] = true;
|
||||
_notifyAsync();
|
||||
try {
|
||||
final json = await api.get('/loco/get-timeline/$locoId');
|
||||
final timeline = LocoAttrVersion.fromGroupedJson(json);
|
||||
_locoTimelines[locoId] = timeline;
|
||||
return timeline;
|
||||
} catch (e) {
|
||||
debugPrint('Failed to fetch loco timeline for $locoId: $e');
|
||||
_locoTimelines[locoId] = [];
|
||||
return [];
|
||||
} finally {
|
||||
_isLocoTimelineLoading[locoId] = false;
|
||||
_notifyAsync();
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> createLoco(Map<String, dynamic> payload) async {
|
||||
try {
|
||||
final response = await api.put('/loco/new', payload);
|
||||
@@ -417,6 +442,30 @@ class DataService extends ChangeNotifier {
|
||||
return _locoClasses;
|
||||
}
|
||||
|
||||
Future<void> createLocoEvent({
|
||||
required int locoId,
|
||||
required String eventDate,
|
||||
required Map<String, dynamic> values,
|
||||
required String details,
|
||||
String eventType = 'other',
|
||||
}) async {
|
||||
try {
|
||||
await api.put(
|
||||
'/event/new',
|
||||
{
|
||||
'loco_id': locoId,
|
||||
'loco_event_type': eventType,
|
||||
'loco_event_date': eventDate,
|
||||
'loco_event_value': jsonEncode(values),
|
||||
'loco_event_details': details,
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Failed to create loco event: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_homepageStats = null;
|
||||
_legs = [];
|
||||
@@ -424,6 +473,8 @@ class DataService extends ChangeNotifier {
|
||||
_trips = [];
|
||||
_tripDetails = [];
|
||||
_eventFields = [];
|
||||
_locoTimelines.clear();
|
||||
_isLocoTimelineLoading.clear();
|
||||
_notifyAsync();
|
||||
}
|
||||
|
||||
|
||||
40
lib/services/navigation_guard.dart
Normal file
40
lib/services/navigation_guard.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
typedef NavigationGuardCallback = Future<bool> Function();
|
||||
|
||||
class NavigationGuard {
|
||||
static NavigationGuardCallback? _callback;
|
||||
|
||||
static void register(NavigationGuardCallback callback) {
|
||||
_callback = callback;
|
||||
}
|
||||
|
||||
static void unregister(NavigationGuardCallback callback) {
|
||||
if (_callback == callback) {
|
||||
_callback = null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> attemptNavigation(
|
||||
Future<void> Function() performNavigation,
|
||||
) async {
|
||||
if (_promptActive) return;
|
||||
final cb = _callback;
|
||||
if (cb == null) {
|
||||
await performNavigation();
|
||||
return;
|
||||
}
|
||||
_promptActive = true;
|
||||
bool allow = false;
|
||||
try {
|
||||
allow = await cb();
|
||||
} catch (_) {
|
||||
allow = false;
|
||||
} finally {
|
||||
_promptActive = false;
|
||||
}
|
||||
if (allow) {
|
||||
await performNavigation();
|
||||
}
|
||||
}
|
||||
|
||||
static bool _promptActive = false;
|
||||
}
|
||||
@@ -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.1.3+1
|
||||
version: 0.2.0+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.8.1
|
||||
|
||||
Reference in New Issue
Block a user