Refactor theme system and remove unused platforms
- Overhaul theme system with Svrnty design and WCAG AAA compliance - Remove android, macos, and web platform files (iOS-only focus) - Update components with improved dark mode map and UI refinements - Enhance settings page with additional configuration options - Add theme system documentation in lib/theme/README.md - Update CLAUDE.md with comprehensive theme guidelines Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -318,6 +318,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
||||
endpoint: 'completeDelivery',
|
||||
command: CompleteDeliveryCommand(
|
||||
deliveryId: delivery.id,
|
||||
deliveredAt: DateTime.now().toUtc().toIso8601String(),
|
||||
),
|
||||
);
|
||||
result.when(
|
||||
|
||||
+65
-78
@@ -6,9 +6,7 @@ import 'package:image_picker/image_picker.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../models/delivery.dart';
|
||||
import '../models/delivery_route.dart';
|
||||
import '../models/delivery_commands.dart';
|
||||
import '../providers/providers.dart';
|
||||
import '../api/client.dart';
|
||||
import '../utils/toast_helper.dart';
|
||||
import '../api/openapi_config.dart';
|
||||
import '../utils/http_client_factory.dart';
|
||||
@@ -76,7 +74,7 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||
void _backToRoutes() {
|
||||
setState(() {
|
||||
_selectedRoute = null;
|
||||
_selectedDelivery = null;
|
||||
// Keep _selectedDelivery to preserve selection when returning to map
|
||||
});
|
||||
}
|
||||
|
||||
@@ -98,11 +96,8 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create API client with auth service for automatic token refresh
|
||||
final authClient = CqrsApiClient(
|
||||
config: ApiClientConfig.development,
|
||||
authService: authService,
|
||||
);
|
||||
// Use gRPC client for commands
|
||||
final grpcClient = ref.read(grpcClientProvider);
|
||||
|
||||
switch (action) {
|
||||
case 'complete':
|
||||
@@ -110,11 +105,8 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||
LoadingDialog.show(context, message: l10n.completingDelivery);
|
||||
}
|
||||
|
||||
final result = await authClient.executeCommand(
|
||||
endpoint: 'completeDelivery',
|
||||
command: CompleteDeliveryCommand(
|
||||
deliveryId: delivery.id,
|
||||
),
|
||||
final result = await grpcClient.completeDelivery(
|
||||
deliveryId: delivery.id,
|
||||
);
|
||||
result.when(
|
||||
success: (_) async {
|
||||
@@ -123,17 +115,13 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
// Invalidate both providers to force refresh
|
||||
ref.invalidate(deliveriesProvider(routeFragmentId));
|
||||
ref.invalidate(allDeliveriesProvider);
|
||||
ref.invalidate(deliveryRoutesProvider);
|
||||
|
||||
// Wait for providers to refresh
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
// Refresh providers to force fresh data fetch
|
||||
await ref.refresh(deliveriesProvider(routeFragmentId).future);
|
||||
await ref.refresh(allDeliveriesProvider.future);
|
||||
|
||||
if (mounted) {
|
||||
// Get refreshed deliveries
|
||||
final allDeliveries = await ref.read(allDeliveriesProvider.future);
|
||||
final allDeliveries = ref.read(allDeliveriesProvider).value ?? [];
|
||||
final routeDeliveries = allDeliveries
|
||||
.where((d) => d.routeFragmentId == routeFragmentId)
|
||||
.toList();
|
||||
@@ -172,12 +160,17 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||
|
||||
debugPrint('Complete delivery failed - Type: ${error.type}, Message: ${error.message}');
|
||||
debugPrint('Error details: ${error.details}');
|
||||
debugPrint('Status code: ${error.statusCode}');
|
||||
if (mounted) {
|
||||
String errorMessage = l10n.error(error.message);
|
||||
if (error.statusCode == 500) {
|
||||
errorMessage = l10n.serverError;
|
||||
} else if (error.statusCode == 401) {
|
||||
errorMessage = 'Unauthorized: You may not have permission to complete this delivery. Please check with your administrator.';
|
||||
} else if (error.details != null) {
|
||||
errorMessage = 'Error: ${error.details}';
|
||||
}
|
||||
ToastHelper.showError(context, errorMessage);
|
||||
ToastHelper.showError(context, errorMessage, duration: const Duration(seconds: 5));
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -188,9 +181,8 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||
LoadingDialog.show(context, message: l10n.markingAsUncompleted);
|
||||
}
|
||||
|
||||
final uncompleteResult = await authClient.executeCommand(
|
||||
endpoint: 'markDeliveryAsUncompleted',
|
||||
command: MarkDeliveryAsUncompletedCommand(deliveryId: delivery.id),
|
||||
final uncompleteResult = await grpcClient.markDeliveryAsUncompleted(
|
||||
deliveryId: delivery.id,
|
||||
);
|
||||
uncompleteResult.when(
|
||||
success: (_) async {
|
||||
@@ -199,17 +191,13 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
// Invalidate both providers to force refresh
|
||||
ref.invalidate(deliveriesProvider(routeFragmentId));
|
||||
ref.invalidate(allDeliveriesProvider);
|
||||
ref.invalidate(deliveryRoutesProvider);
|
||||
|
||||
// Wait for providers to refresh
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
// Refresh providers to force fresh data fetch
|
||||
await ref.refresh(deliveriesProvider(routeFragmentId).future);
|
||||
await ref.refresh(allDeliveriesProvider.future);
|
||||
|
||||
if (mounted) {
|
||||
// Get refreshed deliveries
|
||||
final allDeliveries = await ref.read(allDeliveriesProvider.future);
|
||||
final allDeliveries = ref.read(allDeliveriesProvider).value ?? [];
|
||||
final updatedDelivery = allDeliveries.firstWhere(
|
||||
(d) => d.id == delivery.id,
|
||||
orElse: () => delivery,
|
||||
@@ -299,12 +287,12 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||
|
||||
try {
|
||||
final Uri uploadUrl = Uri.parse(
|
||||
'${ApiClientConfig.development.baseUrl}/api/delivery/uploadDeliveryPicture?deliveryId=${delivery.id}',
|
||||
'${ApiClientConfig.production.baseUrl}/api/delivery/uploadDeliveryPicture?deliveryId=${delivery.id}',
|
||||
);
|
||||
|
||||
// Create HTTP client that accepts self-signed certificates
|
||||
final client = HttpClientFactory.createClient(
|
||||
allowSelfSigned: ApiClientConfig.development.allowSelfSignedCertificate,
|
||||
allowSelfSigned: ApiClientConfig.production.allowSelfSignedCertificate,
|
||||
);
|
||||
|
||||
final http.MultipartRequest request = http.MultipartRequest('POST', uploadUrl);
|
||||
@@ -385,28 +373,40 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||
final userProfile = ref.watch(userProfileProvider);
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
final isMobile = context.isMobile;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: (isMobile && _selectedRoute != null)
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: _backToRoutes,
|
||||
tooltip: 'Back to routes',
|
||||
)
|
||||
: null,
|
||||
title: Text(l10n.deliveryRoutes),
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: (routesData.isLoading || allDeliveriesData.isLoading)
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.refresh),
|
||||
onPressed: (routesData.isLoading || allDeliveriesData.isLoading)
|
||||
? null
|
||||
: () {
|
||||
ref.invalidate(deliveryRoutesProvider);
|
||||
ref.invalidate(allDeliveriesProvider);
|
||||
},
|
||||
tooltip: 'Refresh',
|
||||
),
|
||||
// Hide refresh button when on map view (mobile + route selected)
|
||||
// Google Maps Navigation has its own built-in volume controls
|
||||
if (!(isMobile && _selectedRoute != null))
|
||||
IconButton(
|
||||
icon: (routesData.isLoading || allDeliveriesData.isLoading)
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.refresh),
|
||||
onPressed: (routesData.isLoading || allDeliveriesData.isLoading)
|
||||
? null
|
||||
: () {
|
||||
ref.invalidate(deliveryRoutesProvider);
|
||||
ref.invalidate(allDeliveriesProvider);
|
||||
},
|
||||
tooltip: 'Refresh',
|
||||
),
|
||||
userProfile.when(
|
||||
data: (profile) {
|
||||
String getInitials(String? fullName) {
|
||||
@@ -431,12 +431,13 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||
child: CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
child: Text(
|
||||
getInitials(profile?.fullName),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -496,32 +497,18 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||
// ignore: unused_result
|
||||
ref.refresh(allDeliveriesProvider);
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
MobileMapWithOverlay(
|
||||
deliveries: routeDeliveries,
|
||||
selectedDelivery: _selectedDelivery,
|
||||
onDeliverySelected: (delivery) {
|
||||
setState(() {
|
||||
_selectedDelivery = delivery;
|
||||
});
|
||||
_autoShowNotesIfNeeded(delivery);
|
||||
},
|
||||
onDeliveryAction: (delivery, action) {
|
||||
_handleDeliveryAction(action, delivery, _selectedRoute!.id);
|
||||
},
|
||||
),
|
||||
// Back button to return to routes list
|
||||
Positioned(
|
||||
top: 16,
|
||||
left: 16,
|
||||
child: FloatingActionButton.small(
|
||||
onPressed: _backToRoutes,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
child: const Icon(Icons.arrow_back),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: MobileMapWithOverlay(
|
||||
deliveries: routeDeliveries,
|
||||
selectedDelivery: _selectedDelivery,
|
||||
onDeliverySelected: (delivery) {
|
||||
setState(() {
|
||||
_selectedDelivery = delivery;
|
||||
});
|
||||
_autoShowNotesIfNeeded(delivery);
|
||||
},
|
||||
onDeliveryAction: (delivery, action) {
|
||||
_handleDeliveryAction(action, delivery, _selectedRoute!.id);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+171
-63
@@ -31,7 +31,10 @@ class SettingsPage extends ConsumerWidget {
|
||||
children: [
|
||||
Text(
|
||||
l10n.profile,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
userProfile.when(
|
||||
@@ -49,9 +52,14 @@ class SettingsPage extends ConsumerWidget {
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 32,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
child: Text(
|
||||
profile.firstName[0].toUpperCase(),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
@@ -61,34 +69,53 @@ class SettingsPage extends ConsumerWidget {
|
||||
children: [
|
||||
Text(
|
||||
profile.fullName,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
profile.email,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton.filled(
|
||||
icon: const Icon(Icons.logout),
|
||||
onPressed: () async {
|
||||
final authService = ref.read(authServiceProvider);
|
||||
await authService.logout();
|
||||
if (context.mounted) {
|
||||
// ignore: unused_result
|
||||
ref.refresh(isAuthenticatedProvider);
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pushReplacementNamed('/');
|
||||
}
|
||||
}
|
||||
},
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
tooltip: l10n.logout,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.tonalIcon(
|
||||
onPressed: () async {
|
||||
final authService = ref.read(authServiceProvider);
|
||||
await authService.logout();
|
||||
if (context.mounted) {
|
||||
// ignore: unused_result
|
||||
ref.refresh(isAuthenticatedProvider);
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pushReplacementNamed('/');
|
||||
}
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.logout),
|
||||
label: Text(
|
||||
l10n.logout,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.errorContainer,
|
||||
foregroundColor: Theme.of(context).colorScheme.onErrorContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -108,17 +135,32 @@ class SettingsPage extends ConsumerWidget {
|
||||
children: [
|
||||
Text(
|
||||
l10n.preferences,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ListTile(
|
||||
title: Text(l10n.language),
|
||||
title: Text(
|
||||
l10n.language,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
language == 'system'
|
||||
? l10n.systemLanguage
|
||||
: language == 'fr'
|
||||
? l10n.french
|
||||
: l10n.english
|
||||
: l10n.english,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
trailing: DropdownButton<String>(
|
||||
value: language,
|
||||
@@ -130,52 +172,105 @@ class SettingsPage extends ConsumerWidget {
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: 'system',
|
||||
child: Text(l10n.systemLanguage),
|
||||
child: Text(
|
||||
l10n.systemLanguage,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'en',
|
||||
child: Text(l10n.english),
|
||||
child: Text(
|
||||
l10n.english,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'fr',
|
||||
child: Text(l10n.french),
|
||||
child: Text(
|
||||
l10n.french,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.theme,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
ListTile(
|
||||
title: Text(
|
||||
l10n.theme,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 16,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SegmentedButton<ThemeMode>(
|
||||
selected: {themeMode},
|
||||
onSelectionChanged: (Set<ThemeMode> newSelection) {
|
||||
ref.read(themeModeProvider.notifier).setThemeMode(newSelection.first);
|
||||
},
|
||||
segments: [
|
||||
ButtonSegment<ThemeMode>(
|
||||
value: ThemeMode.light,
|
||||
label: Text(l10n.themeLight),
|
||||
icon: const Icon(Icons.light_mode),
|
||||
),
|
||||
ButtonSegment<ThemeMode>(
|
||||
value: ThemeMode.dark,
|
||||
label: Text(l10n.themeDark),
|
||||
icon: const Icon(Icons.dark_mode),
|
||||
),
|
||||
ButtonSegment<ThemeMode>(
|
||||
value: ThemeMode.system,
|
||||
label: Text(l10n.themeSystem),
|
||||
icon: const Icon(Icons.brightness_auto),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Text(
|
||||
themeMode == ThemeMode.light
|
||||
? l10n.themeLight
|
||||
: themeMode == ThemeMode.dark
|
||||
? l10n.themeDark
|
||||
: 'Device',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: DropdownButton<ThemeMode>(
|
||||
value: themeMode,
|
||||
onChanged: (ThemeMode? newValue) {
|
||||
if (newValue != null) {
|
||||
ref.read(themeModeProvider.notifier).setThemeMode(newValue);
|
||||
}
|
||||
},
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: ThemeMode.light,
|
||||
child: Text(
|
||||
l10n.themeLight,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: ThemeMode.dark,
|
||||
child: Text(
|
||||
l10n.themeDark,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: ThemeMode.system,
|
||||
child: Text(
|
||||
'Device',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -188,16 +283,29 @@ class SettingsPage extends ConsumerWidget {
|
||||
children: [
|
||||
Text(
|
||||
l10n.about,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ListTile(
|
||||
title: Text(l10n.appVersion),
|
||||
subtitle: const Text('1.0.0'),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(l10n.builtWithFlutter),
|
||||
subtitle: Text(l10n.appDescription),
|
||||
title: Text(
|
||||
l10n.appVersion,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'1.0.0',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user