more localization

This commit is contained in:
stopflock
2025-08-31 14:01:03 -05:00
parent 3150297bb0
commit f05a31f40b
21 changed files with 1961 additions and 1182 deletions
@@ -1,68 +1,75 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../app_state.dart';
import '../../services/localization_service.dart';
class AuthSection extends StatelessWidget {
const AuthSection({super.key});
@override
Widget build(BuildContext context) {
final appState = context.watch<AppState>();
return AnimatedBuilder(
animation: LocalizationService.instance,
builder: (context, child) {
final locService = LocalizationService.instance;
final appState = context.watch<AppState>();
return Column(
children: [
ListTile(
leading: Icon(
appState.isLoggedIn ? Icons.person : Icons.login,
color: appState.isLoggedIn ? Colors.green : null,
),
title: Text(appState.isLoggedIn
? 'Logged in as ${appState.username}'
: 'Log in to OpenStreetMap'),
subtitle: appState.isLoggedIn
? const Text('Tap to logout')
: const Text('Required to submit camera data'),
onTap: () async {
if (appState.isLoggedIn) {
await appState.logout();
} else {
await appState.forceLogin();
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(appState.isLoggedIn
? 'Logged in as ${appState.username}'
: 'Logged out'),
backgroundColor: appState.isLoggedIn ? Colors.green : Colors.grey,
),
);
}
},
),
if (appState.isLoggedIn)
ListTile(
leading: const Icon(Icons.wifi_protected_setup),
title: const Text('Test Connection'),
subtitle: const Text('Verify OSM credentials are working'),
onTap: () async {
final isValid = await appState.validateToken();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(isValid
? 'Connection OK - credentials are valid'
: 'Connection failed - please re-login'),
backgroundColor: isValid ? Colors.green : Colors.red,
),
);
}
if (!isValid) {
await appState.logout();
}
},
),
],
return Column(
children: [
ListTile(
leading: Icon(
appState.isLoggedIn ? Icons.person : Icons.login,
color: appState.isLoggedIn ? Colors.green : null,
),
title: Text(appState.isLoggedIn
? locService.t('auth.loggedInAs', params: [appState.username])
: locService.t('auth.loginToOSM')),
subtitle: appState.isLoggedIn
? Text(locService.t('auth.tapToLogout'))
: Text(locService.t('auth.requiredToSubmit')),
onTap: () async {
if (appState.isLoggedIn) {
await appState.logout();
} else {
await appState.forceLogin();
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(appState.isLoggedIn
? locService.t('auth.loggedInAs', params: [appState.username])
: locService.t('auth.loggedOut')),
backgroundColor: appState.isLoggedIn ? Colors.green : Colors.grey,
),
);
}
},
),
if (appState.isLoggedIn)
ListTile(
leading: const Icon(Icons.wifi_protected_setup),
title: Text(locService.t('auth.testConnection')),
subtitle: Text(locService.t('auth.testConnectionSubtitle')),
onTap: () async {
final isValid = await appState.validateToken();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(isValid
? locService.t('auth.connectionOK')
: locService.t('auth.connectionFailed')),
backgroundColor: isValid ? Colors.green : Colors.red,
),
);
}
if (!isValid) {
await appState.logout();
}
},
),
],
);
},
);
}
}
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../app_state.dart';
import '../../services/localization_service.dart';
class MaxNodesSection extends StatefulWidget {
const MaxNodesSection({super.key});
@@ -27,54 +28,61 @@ class _MaxNodesSectionState extends State<MaxNodesSection> {
@override
Widget build(BuildContext context) {
final appState = context.watch<AppState>();
final current = appState.maxCameras;
final showWarning = current > 1000;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
leading: const Icon(Icons.filter_alt),
title: const Text('Max nodes fetched/drawn'),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Set an upper limit for the number of nodes on the map (default: 250).'),
if (showWarning)
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
children: const [
Icon(Icons.warning, color: Colors.orange, size: 18),
SizedBox(width: 6),
Expanded(child: Text(
'You probably don\'t want to do that unless you are absolutely sure you have a good reason for it.',
style: TextStyle(color: Colors.orange),
)),
],
return AnimatedBuilder(
animation: LocalizationService.instance,
builder: (context, child) {
final locService = LocalizationService.instance;
final appState = context.watch<AppState>();
final current = appState.maxCameras;
final showWarning = current > 1000;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
leading: const Icon(Icons.filter_alt),
title: Text(locService.t('settings.maxNodes')),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(locService.t('settings.maxNodesSubtitle')),
if (showWarning)
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
children: [
const Icon(Icons.warning, color: Colors.orange, size: 18),
const SizedBox(width: 6),
Expanded(child: Text(
locService.t('settings.maxNodesWarning'),
style: const TextStyle(color: Colors.orange),
)),
],
),
),
],
),
trailing: SizedBox(
width: 80,
child: TextFormField(
controller: _controller,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
isDense: true,
contentPadding: EdgeInsets.symmetric(vertical: 6, horizontal: 8),
border: OutlineInputBorder(),
),
onFieldSubmitted: (value) {
final n = int.tryParse(value) ?? 10;
appState.maxCameras = n;
_controller.text = appState.maxCameras.toString();
},
),
],
),
trailing: SizedBox(
width: 80,
child: TextFormField(
controller: _controller,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
isDense: true,
contentPadding: EdgeInsets.symmetric(vertical: 6, horizontal: 8),
border: OutlineInputBorder(),
),
onFieldSubmitted: (value) {
final n = int.tryParse(value) ?? 10;
appState.maxCameras = n;
_controller.text = appState.maxCameras.toString();
},
),
),
),
],
],
);
},
);
}
}
@@ -2,11 +2,14 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../app_state.dart';
import '../../services/offline_area_service.dart';
import '../../services/localization_service.dart';
class OfflineModeSection extends StatelessWidget {
const OfflineModeSection({super.key});
Future<void> _handleOfflineModeChange(BuildContext context, AppState appState, bool value) async {
final locService = LocalizationService.instance;
// If enabling offline mode, check for active downloads
if (value && !appState.offlineMode) {
final offlineService = OfflineAreaService();
@@ -16,19 +19,17 @@ class OfflineModeSection extends StatelessWidget {
context: context,
builder: (context) => AlertDialog(
title: Row(
children: const [
Icon(Icons.warning, color: Colors.orange),
SizedBox(width: 8),
Text('Active Downloads'),
children: [
const Icon(Icons.warning, color: Colors.orange),
const SizedBox(width: 8),
Text(locService.t('settings.offlineModeWarningTitle')),
],
),
content: const Text(
'Enabling offline mode will cancel any active area downloads. Do you want to continue?',
),
content: Text(locService.t('settings.offlineModeWarningMessage')),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
child: Text(locService.cancel),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
@@ -36,7 +37,7 @@ class OfflineModeSection extends StatelessWidget {
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
),
child: const Text('Enable Offline Mode'),
child: Text(locService.t('settings.enableOfflineMode')),
),
],
),
@@ -54,15 +55,22 @@ class OfflineModeSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final appState = context.watch<AppState>();
return ListTile(
leading: const Icon(Icons.wifi_off),
title: const Text('Offline Mode'),
subtitle: const Text('Disable all network requests except for local/offline areas.'),
trailing: Switch(
value: appState.offlineMode,
onChanged: (value) => _handleOfflineModeChange(context, appState, value),
),
return AnimatedBuilder(
animation: LocalizationService.instance,
builder: (context, child) {
final locService = LocalizationService.instance;
final appState = context.watch<AppState>();
return ListTile(
leading: const Icon(Icons.wifi_off),
title: Text(locService.t('settings.offlineMode')),
subtitle: Text(locService.t('settings.offlineModeSubtitle')),
trailing: Switch(
value: appState.offlineMode,
onChanged: (value) => _handleOfflineModeChange(context, appState, value),
),
);
},
);
}
}
@@ -3,6 +3,7 @@ import 'package:uuid/uuid.dart';
import 'package:provider/provider.dart';
import '../../app_state.dart';
import '../../models/node_profile.dart';
import '../../services/localization_service.dart';
import '../profile_editor.dart';
class ProfileListSection extends StatelessWidget {
@@ -10,129 +11,136 @@ class ProfileListSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final appState = context.watch<AppState>();
return AnimatedBuilder(
animation: LocalizationService.instance,
builder: (context, child) {
final locService = LocalizationService.instance;
final appState = context.watch<AppState>();
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
return Column(
children: [
const Text('Node Profiles', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
TextButton.icon(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ProfileEditor(
profile: NodeProfile(
id: const Uuid().v4(),
name: '',
tags: const {},
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(locService.t('profiles.nodeProfiles'), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
TextButton.icon(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ProfileEditor(
profile: NodeProfile(
id: const Uuid().v4(),
name: '',
tags: const {},
),
),
),
),
icon: const Icon(Icons.add),
label: Text(locService.t('profiles.newProfile')),
),
],
),
...appState.profiles.map(
(p) => ListTile(
leading: Checkbox(
value: appState.isEnabled(p),
onChanged: (v) => appState.toggleProfile(p, v ?? false),
),
title: Text(p.name),
subtitle: Text(p.builtin ? locService.t('profiles.builtIn') : locService.t('profiles.custom')),
trailing: !p.editable
? PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
value: 'view',
child: Row(
children: [
const Icon(Icons.visibility),
const SizedBox(width: 8),
Text(locService.t('profiles.view')),
],
),
),
],
onSelected: (value) {
if (value == 'view') {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ProfileEditor(profile: p),
),
);
}
},
)
: PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
value: 'edit',
child: Row(
children: [
const Icon(Icons.edit),
const SizedBox(width: 8),
Text(locService.t('actions.edit')),
],
),
),
PopupMenuItem(
value: 'delete',
child: Row(
children: [
const Icon(Icons.delete, color: Colors.red),
const SizedBox(width: 8),
Text(locService.t('profiles.deleteProfile'), style: const TextStyle(color: Colors.red)),
],
),
),
],
onSelected: (value) {
if (value == 'edit') {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ProfileEditor(profile: p),
),
);
} else if (value == 'delete') {
_showDeleteProfileDialog(context, p);
}
},
),
),
icon: const Icon(Icons.add),
label: const Text('New Profile'),
),
],
),
...appState.profiles.map(
(p) => ListTile(
leading: Checkbox(
value: appState.isEnabled(p),
onChanged: (v) => appState.toggleProfile(p, v ?? false),
),
title: Text(p.name),
subtitle: Text(p.builtin ? 'Built-in' : 'Custom'),
trailing: !p.editable
? PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
value: 'view',
child: const Row(
children: [
Icon(Icons.visibility),
SizedBox(width: 8),
Text('View'),
],
),
),
],
onSelected: (value) {
if (value == 'view') {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ProfileEditor(profile: p),
),
);
}
},
)
: PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
value: 'edit',
child: const Row(
children: [
Icon(Icons.edit),
SizedBox(width: 8),
Text('Edit'),
],
),
),
PopupMenuItem(
value: 'delete',
child: const Row(
children: [
Icon(Icons.delete, color: Colors.red),
SizedBox(width: 8),
Text('Delete', style: TextStyle(color: Colors.red)),
],
),
),
],
onSelected: (value) {
if (value == 'edit') {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ProfileEditor(profile: p),
),
);
} else if (value == 'delete') {
_showDeleteProfileDialog(context, p);
}
},
),
),
),
],
);
},
);
}
void _showDeleteProfileDialog(BuildContext context, NodeProfile profile) {
final locService = LocalizationService.instance;
final appState = context.read<AppState>();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete Profile'),
content: Text('Are you sure you want to delete "${profile.name}"?'),
title: Text(locService.t('profiles.deleteProfile')),
content: Text(locService.t('profiles.deleteProfileConfirm', params: [profile.name])),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
child: Text(locService.t('actions.cancel')),
),
TextButton(
onPressed: () {
appState.deleteProfile(profile);
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Profile deleted')),
SnackBar(content: Text(locService.t('profiles.profileDeleted'))),
);
},
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Delete'),
child: Text(locService.t('profiles.deleteProfile')),
),
],
),
@@ -1,19 +1,21 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../app_state.dart';
import '../../services/localization_service.dart';
import '../../state/settings_state.dart';
class QueueSection extends StatelessWidget {
const QueueSection({super.key});
String _getUploadModeDisplayName(UploadMode mode) {
final locService = LocalizationService.instance;
switch (mode) {
case UploadMode.production:
return 'Production';
return locService.t('uploadMode.production');
case UploadMode.sandbox:
return 'Sandbox';
return locService.t('uploadMode.sandbox');
case UploadMode.simulate:
return 'Simulate';
return locService.t('uploadMode.simulate');
}
}
@@ -30,67 +32,75 @@ class QueueSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final appState = context.watch<AppState>();
return Column(
children: [
ListTile(
leading: const Icon(Icons.queue),
title: Text('Pending uploads: ${appState.pendingCount}'),
subtitle: appState.uploadMode == UploadMode.simulate
? const Text('Simulate mode enabled uploads simulated')
: appState.uploadMode == UploadMode.sandbox
? const Text('Sandbox mode uploads go to OSM Sandbox')
: const Text('Tap to view queue'),
onTap: appState.pendingCount > 0
? () => _showQueueDialog(context)
: null,
),
if (appState.pendingCount > 0)
ListTile(
leading: const Icon(Icons.clear_all),
title: const Text('Clear Upload Queue'),
subtitle: Text('Remove all ${appState.pendingCount} pending uploads'),
onTap: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Clear Queue'),
content: Text('Remove all ${appState.pendingCount} pending uploads?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
return AnimatedBuilder(
animation: LocalizationService.instance,
builder: (context, child) {
final locService = LocalizationService.instance;
final appState = context.watch<AppState>();
return Column(
children: [
ListTile(
leading: const Icon(Icons.queue),
title: Text(locService.t('queue.pendingUploads', params: [appState.pendingCount.toString()])),
subtitle: appState.uploadMode == UploadMode.simulate
? Text(locService.t('queue.simulateModeEnabled'))
: appState.uploadMode == UploadMode.sandbox
? Text(locService.t('queue.sandboxMode'))
: Text(locService.t('queue.tapToViewQueue')),
onTap: appState.pendingCount > 0
? () => _showQueueDialog(context)
: null,
),
if (appState.pendingCount > 0)
ListTile(
leading: const Icon(Icons.clear_all),
title: Text(locService.t('queue.clearUploadQueue')),
subtitle: Text(locService.t('queue.removeAllPending', params: [appState.pendingCount.toString()])),
onTap: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(locService.t('queue.clearQueueTitle')),
content: Text(locService.t('queue.clearQueueConfirm', params: [appState.pendingCount.toString()])),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(locService.cancel),
),
TextButton(
onPressed: () {
appState.clearQueue();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(locService.t('queue.queueCleared'))),
);
},
child: Text(locService.t('actions.clear')),
),
],
),
TextButton(
onPressed: () {
appState.clearQueue();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Queue cleared')),
);
},
child: const Text('Clear'),
),
],
),
);
},
),
],
);
},
),
],
);
},
);
}
void _showQueueDialog(BuildContext context) {
final locService = LocalizationService.instance;
showDialog(
context: context,
builder: (context) => Consumer<AppState>(
builder: (context, appState, child) => AlertDialog(
title: Text('Upload Queue (${appState.pendingCount} items)'),
title: Text(locService.t('queue.uploadQueueTitle', params: [appState.pendingCount.toString()])),
content: SizedBox(
width: double.maxFinite,
height: 300,
child: appState.pendingUploads.isEmpty
? const Center(child: Text('Queue is empty'))
? Center(child: Text(locService.t('queue.queueIsEmpty')))
: ListView.builder(
itemCount: appState.pendingUploads.length,
itemBuilder: (context, index) {
@@ -102,16 +112,16 @@ class QueueSection extends StatelessWidget {
? Colors.red
: _getUploadModeColor(upload.uploadMode),
),
title: Text('Camera ${index + 1}'
'${upload.error ? " (Error)" : ""}'
'${upload.completing ? " (Completing...)" : ""}'),
title: Text(locService.t('queue.cameraWithIndex', params: [(index + 1).toString()]) +
(upload.error ? locService.t('queue.error') : "") +
(upload.completing ? locService.t('queue.completing') : "")),
subtitle: Text(
'Dest: ${_getUploadModeDisplayName(upload.uploadMode)}\n'
'Lat: ${upload.coord.latitude.toStringAsFixed(6)}\n'
'Lon: ${upload.coord.longitude.toStringAsFixed(6)}\n'
'Direction: ${upload.direction.round()}°\n'
'Attempts: ${upload.attempts}' +
(upload.error ? "\nUpload failed. Tap retry to try again." : "")
locService.t('queue.destination', params: [_getUploadModeDisplayName(upload.uploadMode)]) + '\n' +
locService.t('queue.latitude', params: [upload.coord.latitude.toStringAsFixed(6)]) + '\n' +
locService.t('queue.longitude', params: [upload.coord.longitude.toStringAsFixed(6)]) + '\n' +
locService.t('queue.direction', params: [upload.direction.round().toString()]) + '\n' +
locService.t('queue.attempts', params: [upload.attempts.toString()]) +
(upload.error ? "\n${locService.t('queue.uploadFailedRetry')}" : "")
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
@@ -120,7 +130,7 @@ class QueueSection extends StatelessWidget {
IconButton(
icon: const Icon(Icons.refresh),
color: Colors.orange,
tooltip: 'Retry upload',
tooltip: locService.t('queue.retryUpload'),
onPressed: () {
appState.retryUpload(upload);
},
@@ -146,7 +156,7 @@ class QueueSection extends StatelessWidget {
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
child: Text(locService.t('actions.close')),
),
if (appState.pendingCount > 1)
TextButton(
@@ -154,10 +164,10 @@ class QueueSection extends StatelessWidget {
appState.clearQueue();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Queue cleared')),
SnackBar(content: Text(locService.t('queue.queueCleared'))),
);
},
child: const Text('Clear All'),
child: Text(locService.t('queue.clearAll')),
),
],
),
@@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
import '../../app_state.dart';
import '../../models/tile_provider.dart';
import '../../services/localization_service.dart';
import '../tile_provider_management_screen.dart';
class TileProviderSection extends StatelessWidget {
@@ -10,29 +11,36 @@ class TileProviderSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Map Tiles',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const TileProviderManagementScreen(),
),
);
},
icon: const Icon(Icons.settings),
label: const Text('Manage Providers'),
),
),
],
return AnimatedBuilder(
animation: LocalizationService.instance,
builder: (context, child) {
final locService = LocalizationService.instance;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
locService.t('mapTiles.title'),
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const TileProviderManagementScreen(),
),
);
},
icon: const Icon(Icons.settings),
label: Text(locService.t('mapTiles.manageProviders')),
),
),
],
);
},
);
}
}
@@ -1,71 +1,85 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../app_state.dart';
import '../../services/localization_service.dart';
class UploadModeSection extends StatelessWidget {
const UploadModeSection({super.key});
@override
Widget build(BuildContext context) {
final appState = context.watch<AppState>();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
leading: const Icon(Icons.cloud_upload),
title: const Text('Upload Destination'),
subtitle: const Text('Choose where cameras are uploaded'),
trailing: DropdownButton<UploadMode>(
value: appState.uploadMode,
items: const [
DropdownMenuItem(
value: UploadMode.production,
child: Text('Production'),
return AnimatedBuilder(
animation: LocalizationService.instance,
builder: (context, child) {
final locService = LocalizationService.instance;
final appState = context.watch<AppState>();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
leading: const Icon(Icons.cloud_upload),
title: Text(locService.t('uploadMode.title')),
subtitle: Text(locService.t('uploadMode.subtitle')),
trailing: DropdownButton<UploadMode>(
value: appState.uploadMode,
items: [
DropdownMenuItem(
value: UploadMode.production,
child: Text(locService.t('uploadMode.production')),
),
DropdownMenuItem(
value: UploadMode.sandbox,
child: Text(locService.t('uploadMode.sandbox')),
),
DropdownMenuItem(
value: UploadMode.simulate,
child: Text(locService.t('uploadMode.simulate')),
),
],
onChanged: (mode) {
if (mode != null) appState.setUploadMode(mode);
},
),
DropdownMenuItem(
value: UploadMode.sandbox,
child: Text('Sandbox'),
),
Padding(
padding: const EdgeInsets.only(left: 56, top: 2, right: 16, bottom: 12),
child: Builder(
builder: (context) {
switch (appState.uploadMode) {
case UploadMode.production:
return Text(
locService.t('uploadMode.productionDescription'),
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7))
);
case UploadMode.sandbox:
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
locService.t('uploadMode.sandboxDescription'),
style: const TextStyle(fontSize: 12, color: Colors.orange),
),
const SizedBox(height: 2),
Text(
locService.t('uploadMode.sandboxNote'),
style: const TextStyle(fontSize: 11, color: Colors.redAccent),
),
],
);
case UploadMode.simulate:
default:
return Text(
locService.t('uploadMode.simulateDescription'),
style: const TextStyle(fontSize: 12, color: Colors.deepPurple)
);
}
},
),
DropdownMenuItem(
value: UploadMode.simulate,
child: Text('Simulate'),
),
],
onChanged: (mode) {
if (mode != null) appState.setUploadMode(mode);
},
),
),
Padding(
padding: const EdgeInsets.only(left: 56, top: 2, right: 16, bottom: 12),
child: Builder(
builder: (context) {
switch (appState.uploadMode) {
case UploadMode.production:
return Text('Upload to the live OSM database (visible to all users)', style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7)));
case UploadMode.sandbox:
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Uploads go to the OSM Sandbox (safe for testing, resets regularly).',
style: TextStyle(fontSize: 12, color: Colors.orange),
),
SizedBox(height: 2),
Text(
'NOTE: Due to OpenStreetMap limitations, cameras submitted to the sandbox will NOT appear on the map in this app.',
style: TextStyle(fontSize: 11, color: Colors.redAccent),
),
],
);
case UploadMode.simulate:
default:
return const Text('Simulate uploads (does not contact OSM servers)', style: TextStyle(fontSize: 12, color: Colors.deepPurple));
}
},
),
),
],
),
],
);
},
);
}
}