Compare commits

...

2 Commits

Author SHA1 Message Date
5b94ab263b move transfer button, hide delete button better
All checks were successful
Release / meta (push) Successful in 28s
Release / linux-build (push) Successful in 54s
Release / web-build (push) Successful in 4m48s
Release / android-build (push) Successful in 15m12s
Release / release-master (push) Successful in 31s
Release / release-dev (push) Successful in 34s
2026-01-06 12:04:34 +00:00
06bed86a49 Add accent colour picker, fix empty user card when accepting friend request, add button to transfer allocations
All checks were successful
Release / meta (push) Successful in 8s
Release / linux-build (push) Successful in 56s
Release / web-build (push) Successful in 2m15s
Release / android-build (push) Successful in 6m47s
Release / release-master (push) Successful in 19s
Release / release-dev (push) Successful in 21s
2026-01-06 00:21:19 +00:00
11 changed files with 512 additions and 95 deletions

View File

@@ -2,8 +2,10 @@ import 'package:flutter/material.dart';
import 'package:mileograph_flutter/services/api_service.dart'; import 'package:mileograph_flutter/services/api_service.dart';
import 'package:mileograph_flutter/services/authservice.dart'; import 'package:mileograph_flutter/services/authservice.dart';
import 'package:mileograph_flutter/services/data_service.dart'; import 'package:mileograph_flutter/services/data_service.dart';
import 'package:mileograph_flutter/services/accent_color_service.dart';
import 'package:mileograph_flutter/services/distance_unit_service.dart'; import 'package:mileograph_flutter/services/distance_unit_service.dart';
import 'package:mileograph_flutter/services/endpoint_service.dart'; import 'package:mileograph_flutter/services/endpoint_service.dart';
import 'package:mileograph_flutter/services/theme_mode_service.dart';
import 'package:mileograph_flutter/ui/app_shell.dart'; import 'package:mileograph_flutter/ui/app_shell.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -17,9 +19,15 @@ class App extends StatelessWidget {
ChangeNotifierProvider<EndpointService>( ChangeNotifierProvider<EndpointService>(
create: (_) => EndpointService(), create: (_) => EndpointService(),
), ),
ChangeNotifierProvider<AccentColorService>(
create: (_) => AccentColorService(),
),
ChangeNotifierProvider<DistanceUnitService>( ChangeNotifierProvider<DistanceUnitService>(
create: (_) => DistanceUnitService(), create: (_) => DistanceUnitService(),
), ),
ChangeNotifierProvider<ThemeModeService>(
create: (_) => ThemeModeService(),
),
ProxyProvider<EndpointService, ApiService>( ProxyProvider<EndpointService, ApiService>(
update: (_, endpoint, api) { update: (_, endpoint, api) {
final service = api ?? ApiService(baseUrl: endpoint.baseUrl); final service = api ?? ApiService(baseUrl: endpoint.baseUrl);

View File

@@ -127,6 +127,9 @@ class _ProfilePageState extends State<ProfilePage> {
); );
if (!mounted) return; if (!mounted) return;
setState(() => _status = status); setState(() => _status = status);
final data = context.read<DataService>();
await data.fetchFriendships();
await data.fetchPendingFriendships();
_showSnack('Friend request sent'); _showSnack('Friend request sent');
} catch (e) { } catch (e) {
_showSnack('Failed to send request: $e'); _showSnack('Failed to send request: $e');
@@ -159,6 +162,7 @@ class _ProfilePageState extends State<ProfilePage> {
final updated = await context.read<DataService>().acceptFriendship(id); final updated = await context.read<DataService>().acceptFriendship(id);
if (!mounted) return; if (!mounted) return;
setState(() => _status = updated); setState(() => _status = updated);
await context.read<DataService>().fetchFriendships();
_showSnack('Friend request accepted'); _showSnack('Friend request accepted');
} catch (e) { } catch (e) {
_showSnack('Failed to accept: $e'); _showSnack('Failed to accept: $e');
@@ -175,6 +179,7 @@ class _ProfilePageState extends State<ProfilePage> {
final updated = await context.read<DataService>().rejectFriendship(id); final updated = await context.read<DataService>().rejectFriendship(id);
if (!mounted) return; if (!mounted) return;
setState(() => _status = updated); setState(() => _status = updated);
await context.read<DataService>().fetchPendingFriendships();
_showSnack('Friend request rejected'); _showSnack('Friend request rejected');
} catch (e) { } catch (e) {
_showSnack('Failed to reject: $e'); _showSnack('Failed to reject: $e');

View File

@@ -3,8 +3,10 @@ import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:mileograph_flutter/services/accent_color_service.dart';
import 'package:mileograph_flutter/services/distance_unit_service.dart'; import 'package:mileograph_flutter/services/distance_unit_service.dart';
import 'package:mileograph_flutter/services/endpoint_service.dart'; import 'package:mileograph_flutter/services/endpoint_service.dart';
import 'package:mileograph_flutter/services/theme_mode_service.dart';
import 'package:mileograph_flutter/services/data_service.dart'; import 'package:mileograph_flutter/services/data_service.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -18,6 +20,18 @@ class SettingsPage extends StatefulWidget {
class _SettingsPageState extends State<SettingsPage> { class _SettingsPageState extends State<SettingsPage> {
late final TextEditingController _endpointController; late final TextEditingController _endpointController;
bool _saving = false; bool _saving = false;
static const List<Color> _accentPalette = [
Colors.red,
Colors.pink,
Colors.orange,
Colors.amber,
Colors.green,
Colors.teal,
Colors.blue,
Colors.indigo,
Colors.purple,
Colors.cyan,
];
@override @override
void initState() { void initState() {
@@ -130,7 +144,12 @@ class _SettingsPageState extends State<SettingsPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final endpointService = context.watch<EndpointService>(); final endpointService = context.watch<EndpointService>();
final distanceUnitService = context.watch<DistanceUnitService>(); final distanceUnitService = context.watch<DistanceUnitService>();
if (!endpointService.isLoaded || !distanceUnitService.isLoaded) { final accentService = context.watch<AccentColorService>();
final themeModeService = context.watch<ThemeModeService>();
if (!endpointService.isLoaded ||
!distanceUnitService.isLoaded ||
!accentService.isLoaded ||
!themeModeService.isLoaded) {
return const Scaffold( return const Scaffold(
body: Center(child: CircularProgressIndicator()), body: Center(child: CircularProgressIndicator()),
); );
@@ -184,6 +203,73 @@ class _SettingsPageState extends State<SettingsPage> {
}, },
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Text(
'Accent colour',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
Text(
'Choose your preferred accent colour or use system colours.',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 12),
Wrap(
spacing: 12,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
OutlinedButton.icon(
onPressed:
accentService.useSystem ? null : () => accentService.setUseSystem(true),
icon: const Icon(Icons.phone_android),
label: const Text('Use system colours'),
),
..._accentPalette.map(
(color) => _AccentSwatchButton(
color: color,
selected:
!accentService.useSystem &&
accentService.seedColor.toARGB32() == color.toARGB32(),
onTap: () => accentService.setSeedColor(color),
),
),
],
),
const SizedBox(height: 24),
Text(
'Theme mode',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
SegmentedButton<ThemeMode>(
segments: const [
ButtonSegment(
value: ThemeMode.system,
icon: Icon(Icons.settings_suggest),
label: Text('System'),
),
ButtonSegment(
value: ThemeMode.light,
icon: Icon(Icons.light_mode),
label: Text('Light'),
),
ButtonSegment(
value: ThemeMode.dark,
icon: Icon(Icons.dark_mode),
label: Text('Dark'),
),
],
selected: {themeModeService.mode},
onSelectionChanged: (selection) {
final mode = selection.first;
themeModeService.setMode(mode);
},
),
const SizedBox(height: 24),
Text( Text(
'API endpoint', 'API endpoint',
style: Theme.of(context).textTheme.titleMedium?.copyWith( style: Theme.of(context).textTheme.titleMedium?.copyWith(
@@ -241,3 +327,54 @@ class _SettingsPageState extends State<SettingsPage> {
); );
} }
} }
class _AccentSwatchButton extends StatelessWidget {
const _AccentSwatchButton({
required this.color,
required this.selected,
required this.onTap,
});
final Color color;
final bool selected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final borderColor = selected
? Theme.of(context).colorScheme.onSurface
: Colors.black26;
return InkWell(
onTap: onTap,
customBorder: const CircleBorder(),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(
color: borderColor,
width: selected ? 3 : 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.15),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: selected
? const Center(
child: Icon(
Icons.check,
size: 18,
color: Colors.white,
),
)
: null,
),
);
}
}

View File

@@ -12,6 +12,7 @@ class TractionPage extends StatefulWidget {
this.selectionMode = false, this.selectionMode = false,
this.selectionSingle = false, this.selectionSingle = false,
this.replacementPendingLocoId, this.replacementPendingLocoId,
this.transferFromLocoId,
this.onSelect, this.onSelect,
this.selectedKeys = const {}, this.selectedKeys = const {},
}); });
@@ -19,6 +20,7 @@ class TractionPage extends StatefulWidget {
final bool selectionMode; final bool selectionMode;
final bool selectionSingle; final bool selectionSingle;
final int? replacementPendingLocoId; final int? replacementPendingLocoId;
final int? transferFromLocoId;
final ValueChanged<LocoSummary>? onSelect; final ValueChanged<LocoSummary>? onSelect;
final Set<String> selectedKeys; final Set<String> selectedKeys;
@@ -33,6 +35,7 @@ class _TractionPageState extends State<TractionPage> {
final _nameController = TextEditingController(); final _nameController = TextEditingController();
bool _mileageFirst = true; bool _mileageFirst = true;
bool _initialised = false; bool _initialised = false;
int? get _transferFromLocoId => widget.transferFromLocoId;
bool _showAdvancedFilters = false; bool _showAdvancedFilters = false;
String? _selectedClass; String? _selectedClass;
late Set<String> _selectedKeys; late Set<String> _selectedKeys;
@@ -1200,6 +1203,53 @@ class _TractionPageState extends State<TractionPage> {
} }
} }
Future<void> _confirmTransfer(LocoSummary target) async {
final fromId = _transferFromLocoId;
if (fromId == null) return;
final navContext = context;
final messenger = ScaffoldMessenger.of(navContext);
final confirmed = await showDialog<bool>(
context: navContext,
builder: (dialogContext) {
return AlertDialog(
title: const Text('Transfer allocations?'),
content: Text(
'Transfer all allocations from this loco to ${target.locoClass} ${target.number}?',
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text('Transfer'),
),
],
);
},
);
if (confirmed != true) return;
if (!navContext.mounted) return;
try {
final data = navContext.read<DataService>();
await data.transferAllocations(fromLocoId: fromId, toLocoId: target.id);
if (navContext.mounted) {
messenger.showSnackBar(
const SnackBar(content: Text('Allocations transferred')),
);
}
await _refreshTraction(preservePosition: true);
if (navContext.mounted) navContext.pop();
} catch (e) {
if (navContext.mounted) {
messenger.showSnackBar(
SnackBar(content: Text('Failed to transfer allocations: $e')),
);
}
}
}
bool _isSelected(LocoSummary loco) { bool _isSelected(LocoSummary loco) {
final keyVal = '${loco.locoClass}-${loco.number}'; final keyVal = '${loco.locoClass}-${loco.number}';
return _selectedKeys.contains(keyVal); return _selectedKeys.contains(keyVal);
@@ -1353,6 +1403,12 @@ class _TractionPageState extends State<TractionPage> {
widget.replacementPendingLocoId != null widget.replacementPendingLocoId != null
? () => _confirmReplacePending(loco) ? () => _confirmReplacePending(loco)
: null, : null,
onTransferAllocations: widget.selectionMode &&
widget.selectionSingle &&
widget.transferFromLocoId != null &&
widget.transferFromLocoId != loco.id
? () => _confirmTransfer(loco)
: null,
); );
} }

View File

@@ -20,6 +20,7 @@ class TractionCard extends StatelessWidget {
this.onToggleSelect, this.onToggleSelect,
this.onReplacePending, this.onReplacePending,
this.onActionComplete, this.onActionComplete,
this.onTransferAllocations,
}); });
final LocoSummary loco; final LocoSummary loco;
@@ -31,6 +32,7 @@ class TractionCard extends StatelessWidget {
final VoidCallback? onToggleSelect; final VoidCallback? onToggleSelect;
final VoidCallback? onReplacePending; final VoidCallback? onReplacePending;
final Future<void> Function()? onActionComplete; final Future<void> Function()? onActionComplete;
final VoidCallback? onTransferAllocations;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -145,7 +147,13 @@ class TractionCard extends StatelessWidget {
]; ];
// Prefer replace action when picking a replacement loco. // Prefer replace action when picking a replacement loco.
final addButton = onReplacePending != null final addButton = onTransferAllocations != null
? TextButton.icon(
onPressed: onTransferAllocations,
icon: const Icon(Icons.swap_horiz),
label: const Text('Transfer'),
)
: onReplacePending != null
? TextButton.icon( ? TextButton.icon(
onPressed: onReplacePending, onPressed: onReplacePending,
icon: const Icon(Icons.swap_horiz), icon: const Icon(Icons.swap_horiz),
@@ -159,7 +167,8 @@ class TractionCard extends StatelessWidget {
? Icons.remove_circle_outline ? Icons.remove_circle_outline
: Icons.add_circle_outline, : Icons.add_circle_outline,
), ),
label: Text(isSelected ? 'Remove' : 'Add to entry'), label:
Text(isSelected ? 'Remove' : 'Add to entry'),
) )
: null; : null;
@@ -551,6 +560,7 @@ Future<void> showTractionDetails(
LocoSummary loco, { LocoSummary loco, {
Future<void> Function()? onActionComplete, Future<void> Function()? onActionComplete,
}) async { }) async {
final navContext = context;
final hasMileageOrTrips = _hasMileageOrTrips(loco); final hasMileageOrTrips = _hasMileageOrTrips(loco);
final isVisibilityPending = final isVisibilityPending =
(loco.visibility ?? '').toLowerCase().trim() == 'pending'; (loco.visibility ?? '').toLowerCase().trim() == 'pending';
@@ -625,6 +635,27 @@ Future<void> showTractionDetails(
child: ListView( child: ListView(
controller: controller, controller: controller,
children: [ children: [
FilledButton.icon(
onPressed: () {
Navigator.of(ctx).pop();
navContext.push(
Uri(
path: '/traction',
queryParameters: {
'selection': 'single',
'transferFromLocoId': loco.id.toString(),
},
).toString(),
extra: {
'selection': 'single',
'transferFromLocoId': loco.id,
},
);
},
icon: const Icon(Icons.swap_horiz),
label: const Text('Transfer allocations'),
),
const SizedBox(height: 12),
if (isRejected && rejectedReason.isNotEmpty) if (isRejected && rejectedReason.isNotEmpty)
...[ ...[
_detailRow( _detailRow(
@@ -713,8 +744,37 @@ Future<void> showTractionDetails(
); );
}, },
), ),
if (auth.isElevated || canDeleteAsOwner) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
FilledButton.icon(
onPressed: () {
Navigator.of(ctx).pop();
navContext.push(
Uri(
path: '/traction',
queryParameters: {
'selection': 'single',
'transferFromLocoId': loco.id.toString(),
},
).toString(),
extra: {
'selection': 'single',
'transferFromLocoId': loco.id,
},
);
},
icon: const Icon(Icons.swap_horiz),
label: const Text('Transfer allocations'),
),
if (auth.isElevated || canDeleteAsOwner) ...[
const SizedBox(height: 8),
ExpansionTile(
tilePadding: EdgeInsets.zero,
childrenPadding: EdgeInsets.zero,
title: Text(
'More',
style: Theme.of(context).textTheme.titleSmall,
),
children: [
FilledButton.tonal( FilledButton.tonal(
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
backgroundColor: backgroundColor:
@@ -733,11 +793,13 @@ Future<void> showTractionDetails(
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(false), onPressed: () =>
Navigator.of(context).pop(false),
child: const Text('Cancel'), child: const Text('Cancel'),
), ),
FilledButton( FilledButton(
onPressed: () => Navigator.of(context).pop(true), onPressed: () =>
Navigator.of(context).pop(true),
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
backgroundColor: backgroundColor:
Theme.of(context).colorScheme.error, Theme.of(context).colorScheme.error,
@@ -759,12 +821,16 @@ Future<void> showTractionDetails(
Navigator.of(ctx).pop(); Navigator.of(ctx).pop();
} catch (e) { } catch (e) {
messenger.showSnackBar( messenger.showSnackBar(
SnackBar(content: Text('Failed to delete loco: $e')), SnackBar(
content: Text('Failed to delete loco: $e')),
); );
} }
}, },
child: const Text('Delete loco'), child: const Text('Delete loco'),
), ),
const SizedBox(height: 8),
],
),
], ],
], ],
), ),

View File

@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AccentColorService extends ChangeNotifier {
static const _prefsKeyUseSystem = 'accent_use_system';
static const _prefsKeySeed = 'accent_seed';
static const Color defaultSeed = Colors.red;
bool _useSystem = true;
Color _seedColor = defaultSeed;
bool _hasSavedSeed = false;
bool _loaded = false;
bool get useSystem => _useSystem;
Color get seedColor => _seedColor;
bool get hasSavedSeed => _hasSavedSeed;
bool get isLoaded => _loaded;
AccentColorService() {
_load();
}
Future<void> _load() async {
final prefs = await SharedPreferences.getInstance();
_useSystem = prefs.getBool(_prefsKeyUseSystem) ?? true;
final seedValue = prefs.getInt(_prefsKeySeed);
if (seedValue != null) {
_seedColor = Color(seedValue);
_hasSavedSeed = true;
}
_loaded = true;
notifyListeners();
}
Future<void> setUseSystem(bool value) async {
_useSystem = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_prefsKeyUseSystem, _useSystem);
notifyListeners();
}
Future<void> setSeedColor(Color color) async {
_seedColor = color;
_useSystem = false;
_hasSavedSeed = true;
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_prefsKeySeed, color.toARGB32());
await prefs.setBool(_prefsKeyUseSystem, _useSystem);
notifyListeners();
}
}

View File

@@ -151,6 +151,8 @@ extension DataServiceFriendships on DataService {
overrideAddressee: targetUser, overrideAddressee: targetUser,
); );
_pendingOutgoing = [friendship, ..._pendingOutgoing]; _pendingOutgoing = [friendship, ..._pendingOutgoing];
await fetchFriendships();
await fetchPendingFriendships();
_notifyAsync(); _notifyAsync();
return friendship; return friendship;
} }
@@ -159,6 +161,8 @@ extension DataServiceFriendships on DataService {
final json = await api.post('/friendships/$friendshipId/accept', {}); final json = await api.post('/friendships/$friendshipId/accept', {});
final friendship = _parseAndUpsertFriendship(json, fallbackStatus: 'accepted'); final friendship = _parseAndUpsertFriendship(json, fallbackStatus: 'accepted');
_pendingIncoming = _pendingIncoming.where((f) => f.id != friendshipId).toList(); _pendingIncoming = _pendingIncoming.where((f) => f.id != friendshipId).toList();
await fetchFriendships();
await fetchPendingFriendships();
_notifyAsync(); _notifyAsync();
return friendship; return friendship;
} }
@@ -177,6 +181,8 @@ extension DataServiceFriendships on DataService {
_parseAndRemoveFriendship(json, friendshipId, status: 'none'); _parseAndRemoveFriendship(json, friendshipId, status: 'none');
_pendingOutgoing = _pendingOutgoing =
_pendingOutgoing.where((f) => f.id != friendshipId).toList(); _pendingOutgoing.where((f) => f.id != friendshipId).toList();
await fetchFriendships();
await fetchPendingFriendships();
_notifyAsync(); _notifyAsync();
return friendship; return friendship;
} }
@@ -193,6 +199,8 @@ extension DataServiceFriendships on DataService {
_pendingIncoming.where((f) => f.id != friendshipId).toList(); _pendingIncoming.where((f) => f.id != friendshipId).toList();
_pendingOutgoing = _pendingOutgoing =
_pendingOutgoing.where((f) => f.id != friendshipId).toList(); _pendingOutgoing.where((f) => f.id != friendshipId).toList();
await fetchFriendships();
await fetchPendingFriendships();
_notifyAsync(); _notifyAsync();
} }

View File

@@ -466,6 +466,21 @@ extension DataServiceTraction on DataService {
} }
} }
Future<void> transferAllocations({
required int fromLocoId,
required int toLocoId,
}) async {
try {
await api.post('/loco/alloc/transfer', {
'from_loco_id': fromLocoId,
'to_loco_id': toLocoId,
});
} catch (e) {
debugPrint('Failed to transfer allocations $fromLocoId -> $toLocoId: $e');
rethrow;
}
}
Future<void> adminDeleteLoco({required int locoId}) async { Future<void> adminDeleteLoco({required int locoId}) async {
try { try {
await api.delete('/loco/admin/delete/$locoId'); await api.delete('/loco/admin/delete/$locoId');

View File

@@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class ThemeModeService extends ChangeNotifier {
static const _prefsKey = 'theme_mode_preference';
ThemeMode _mode = ThemeMode.system;
bool _loaded = false;
ThemeMode get mode => _mode;
bool get isLoaded => _loaded;
ThemeModeService() {
_load();
}
Future<void> _load() async {
final prefs = await SharedPreferences.getInstance();
final saved = prefs.getString(_prefsKey);
if (saved != null) {
switch (saved) {
case 'light':
_mode = ThemeMode.light;
break;
case 'dark':
_mode = ThemeMode.dark;
break;
default:
_mode = ThemeMode.system;
}
}
_loaded = true;
notifyListeners();
}
Future<void> setMode(ThemeMode mode) async {
_mode = mode;
final prefs = await SharedPreferences.getInstance();
final value = switch (mode) {
ThemeMode.light => 'light',
ThemeMode.dark => 'dark',
_ => 'system',
};
await prefs.setString(_prefsKey, value);
notifyListeners();
}
}

View File

@@ -27,8 +27,10 @@ import 'package:mileograph_flutter/components/widgets/leg_share_edit_notificatio
import 'package:mileograph_flutter/components/widgets/leg_share_notification_card.dart'; import 'package:mileograph_flutter/components/widgets/leg_share_notification_card.dart';
import 'package:mileograph_flutter/objects/objects.dart'; import 'package:mileograph_flutter/objects/objects.dart';
import 'package:mileograph_flutter/services/authservice.dart'; import 'package:mileograph_flutter/services/authservice.dart';
import 'package:mileograph_flutter/services/accent_color_service.dart';
import 'package:mileograph_flutter/services/data_service.dart'; import 'package:mileograph_flutter/services/data_service.dart';
import 'package:mileograph_flutter/services/navigation_guard.dart'; import 'package:mileograph_flutter/services/navigation_guard.dart';
import 'package:mileograph_flutter/services/theme_mode_service.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
final GlobalKey<NavigatorState> _shellNavigatorKey = final GlobalKey<NavigatorState> _shellNavigatorKey =
@@ -103,12 +105,6 @@ class _MyAppState extends State<MyApp> {
late final GoRouter _router; late final GoRouter _router;
bool _routerInitialized = false; bool _routerInitialized = false;
final ColorScheme defaultLight = ColorScheme.fromSeed(seedColor: Colors.red);
final ColorScheme defaultDark = ColorScheme.fromSeed(
seedColor: Colors.red,
brightness: Brightness.dark,
);
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
@@ -188,10 +184,23 @@ class _MyAppState extends State<MyApp> {
'', '',
) )
: null; : null;
final transferFromLocoIdStr =
state.uri.queryParameters['transferFromLocoId'];
final transferFromLocoId = transferFromLocoIdStr != null
? int.tryParse(transferFromLocoIdStr)
: state.extra is Map
? int.tryParse(
(state.extra as Map)['transferFromLocoId']
?.toString() ??
'',
)
: null;
final selectionMode = final selectionMode =
(selectionParam != null && selectionParam.isNotEmpty) || (selectionParam != null && selectionParam.isNotEmpty) ||
replacementPendingLocoId != null; replacementPendingLocoId != null ||
transferFromLocoId != null;
final selectionSingle = replacementPendingLocoId != null || final selectionSingle = replacementPendingLocoId != null ||
transferFromLocoId != null ||
selectionParam?.toLowerCase() == 'single' || selectionParam?.toLowerCase() == 'single' ||
selectionParam == '1' || selectionParam == '1' ||
selectionParam?.toLowerCase() == 'true'; selectionParam?.toLowerCase() == 'true';
@@ -199,6 +208,7 @@ class _MyAppState extends State<MyApp> {
selectionMode: selectionMode, selectionMode: selectionMode,
selectionSingle: selectionSingle, selectionSingle: selectionSingle,
replacementPendingLocoId: replacementPendingLocoId, replacementPendingLocoId: replacementPendingLocoId,
transferFromLocoId: transferFromLocoId,
); );
}, },
), ),
@@ -325,20 +335,34 @@ class _MyAppState extends State<MyApp> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final accent = context.watch<AccentColorService>();
final themeModeService = context.watch<ThemeModeService>();
final seedColor =
accent.hasSavedSeed ? accent.seedColor : AccentColorService.defaultSeed;
final useSystemColors = accent.useSystem;
return DynamicColorBuilder( return DynamicColorBuilder(
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) { builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
final colorSchemeLight = useSystemColors && lightDynamic != null
? lightDynamic
: ColorScheme.fromSeed(seedColor: seedColor);
final colorSchemeDark = useSystemColors && darkDynamic != null
? darkDynamic
: ColorScheme.fromSeed(
seedColor: seedColor,
brightness: Brightness.dark,
);
return MaterialApp.router( return MaterialApp.router(
title: 'Mileograph', title: 'Mileograph',
routerConfig: _router, routerConfig: _router,
theme: ThemeData( theme: ThemeData(
useMaterial3: true, useMaterial3: true,
colorScheme: lightDynamic ?? defaultLight, colorScheme: colorSchemeLight,
), ),
darkTheme: ThemeData( darkTheme: ThemeData(
useMaterial3: true, useMaterial3: true,
colorScheme: darkDynamic ?? defaultDark, colorScheme: colorSchemeDark,
), ),
themeMode: ThemeMode.system, themeMode: themeModeService.mode,
); );
}, },
); );

View File

@@ -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.7.0+8 version: 0.7.1+9
environment: environment:
sdk: ^3.8.1 sdk: ^3.8.1