From c77ea96eaf2ad38b33aeeab14e09935dc2f68f26 Mon Sep 17 00:00:00 2001 From: stopflock Date: Thu, 20 Nov 2025 20:54:16 -0600 Subject: [PATCH] Move OSM account settings and upload queue into their own sections, add "see my edits" button --- assets/changelog.json | 12 ++ lib/localizations/de.json | 11 ++ lib/localizations/en.json | 11 ++ lib/localizations/es.json | 11 ++ lib/localizations/fr.json | 11 ++ lib/localizations/it.json | 11 ++ lib/localizations/pt.json | 11 ++ lib/localizations/zh.json | 11 ++ lib/main.dart | 4 + lib/screens/osm_account_screen.dart | 176 +++++++++++++++++++++++++ lib/screens/settings_screen.dart | 27 ++-- lib/screens/upload_queue_screen.dart | 189 +++++++++++++++++++++++++++ 12 files changed, 475 insertions(+), 10 deletions(-) create mode 100644 lib/screens/osm_account_screen.dart create mode 100644 lib/screens/upload_queue_screen.dart diff --git a/assets/changelog.json b/assets/changelog.json index 4eda41d..4f6d570 100644 --- a/assets/changelog.json +++ b/assets/changelog.json @@ -1,4 +1,16 @@ { + "1.4.2": { + "content": [ + "• NEW: Dedicated 'Upload Queue' page - queue items are now shown in a proper list view instead of a popup", + "• NEW: 'Clear Upload Queue' button is always visible at the top of queue page, greyed out when empty", + "• NEW: 'OpenStreetMap Account' page for managing OSM login and account settings", + "• NEW: 'View My Edits on OSM' button takes you directly to your edit history on OpenStreetMap", + "• IMPROVED: Settings page organization with dedicated pages for upload management and OSM account", + "• IMPROVED: Better empty queue state with helpful messaging", + "• UX: Cleaner settings page layout with auth and queue sections moved to their own dedicated pages", + "• UX: Added informational content about OpenStreetMap on the account page" + ] + }, "1.4.1": { "content": [ "• NEW: 'Extract node from way/relation' option for constrained nodes", diff --git a/lib/localizations/de.json b/lib/localizations/de.json index c58bf23..2c7d3dc 100644 --- a/lib/localizations/de.json +++ b/lib/localizations/de.json @@ -129,6 +129,8 @@ "simulateDescription": "Uploads simulieren (kontaktiert OSM-Server nicht)" }, "auth": { + "osmAccountTitle": "OpenStreetMap-Konto", + "osmAccountSubtitle": "Ihr OSM-Login verwalten und Ihre Beiträge einsehen", "loggedInAs": "Angemeldet als {}", "loginToOSM": "Bei OpenStreetMap anmelden", "tapToLogout": "Zum Abmelden antippen", @@ -138,6 +140,11 @@ "testConnectionSubtitle": "OSM-Anmeldedaten überprüfen", "connectionOK": "Verbindung OK - Anmeldedaten sind gültig", "connectionFailed": "Verbindung fehlgeschlagen - bitte erneut anmelden", + "viewMyEdits": "Meine Änderungen bei OSM Anzeigen", + "viewMyEditsSubtitle": "Ihr Bearbeitungsverlauf bei OpenStreetMap einsehen", + "aboutOSM": "Über OpenStreetMap", + "aboutOSMDescription": "OpenStreetMap ist ein gemeinschaftliches Open-Source-Kartenprojekt, bei dem Mitwirkende eine kostenlose, bearbeitbare Karte der Welt erstellen und pflegen. Ihre Beiträge zu Überwachungsgeräten helfen dabei, diese Infrastruktur sichtbar und durchsuchbar zu machen.", + "visitOSM": "OpenStreetMap Besuchen", "deleteAccount": "OSM-Konto Löschen", "deleteAccountSubtitle": "Ihr OpenStreetMap-Konto verwalten", "deleteAccountExplanation": "Um Ihr OpenStreetMap-Konto zu löschen, müssen Sie die OpenStreetMap-Website besuchen. Dies entfernt dauerhaft Ihr OSM-Konto und alle zugehörigen Daten.", @@ -145,7 +152,11 @@ "goToOSM": "Zu OpenStreetMap gehen" }, "queue": { + "title": "Upload-Warteschlange", + "subtitle": "Ausstehende Überwachungsgeräte-Uploads verwalten", "pendingUploads": "Ausstehende Uploads: {}", + "pendingItemsCount": "Ausstehende Elemente: {}", + "nothingInQueue": "Warteschlange ist leer", "simulateModeEnabled": "Simulationsmodus aktiviert – Uploads simuliert", "sandboxMode": "Sandbox-Modus – Uploads gehen an OSM Sandbox", "tapToViewQueue": "Zum Anzeigen der Warteschlange antippen", diff --git a/lib/localizations/en.json b/lib/localizations/en.json index 404fcb7..db2f5bd 100644 --- a/lib/localizations/en.json +++ b/lib/localizations/en.json @@ -147,6 +147,8 @@ "simulateDescription": "Simulate uploads (does not contact OSM servers)" }, "auth": { + "osmAccountTitle": "OpenStreetMap Account", + "osmAccountSubtitle": "Manage your OSM login and view your contributions", "loggedInAs": "Logged in as {}", "loginToOSM": "Log in to OpenStreetMap", "tapToLogout": "Tap to logout", @@ -156,6 +158,11 @@ "testConnectionSubtitle": "Verify OSM credentials are working", "connectionOK": "Connection OK - credentials are valid", "connectionFailed": "Connection failed - please re-login", + "viewMyEdits": "View My Edits on OSM", + "viewMyEditsSubtitle": "See your edit history on OpenStreetMap", + "aboutOSM": "About OpenStreetMap", + "aboutOSMDescription": "OpenStreetMap is a collaborative, open-source mapping project where contributors create and maintain a free, editable map of the world. Your surveillance device contributions help make this infrastructure visible and searchable.", + "visitOSM": "Visit OpenStreetMap", "deleteAccount": "Delete OSM Account", "deleteAccountSubtitle": "Manage your OpenStreetMap account", "deleteAccountExplanation": "To delete your OpenStreetMap account, you'll need to visit the OpenStreetMap website. This will permanently remove your OSM account and all associated data.", @@ -163,7 +170,11 @@ "goToOSM": "Go to OpenStreetMap" }, "queue": { + "title": "Upload Queue", + "subtitle": "Manage pending surveillance device uploads", "pendingUploads": "Pending uploads: {}", + "pendingItemsCount": "Pending Items: {}", + "nothingInQueue": "Nothing in queue", "simulateModeEnabled": "Simulate mode enabled – uploads simulated", "sandboxMode": "Sandbox mode – uploads go to OSM Sandbox", "tapToViewQueue": "Tap to view queue", diff --git a/lib/localizations/es.json b/lib/localizations/es.json index 4592c7d..e8b18fb 100644 --- a/lib/localizations/es.json +++ b/lib/localizations/es.json @@ -147,6 +147,8 @@ "simulateDescription": "Simular subidas (no contacta servidores OSM)" }, "auth": { + "osmAccountTitle": "Cuenta de OpenStreetMap", + "osmAccountSubtitle": "Gestionar tu login de OSM y ver tus contribuciones", "loggedInAs": "Conectado como {}", "loginToOSM": "Iniciar sesión en OpenStreetMap", "tapToLogout": "Toque para cerrar sesión", @@ -156,6 +158,11 @@ "testConnectionSubtitle": "Verificar que las credenciales de OSM funcionen", "connectionOK": "Conexión OK - las credenciales son válidas", "connectionFailed": "Conexión falló - por favor, inicie sesión nuevamente", + "viewMyEdits": "Ver Mis Ediciones en OSM", + "viewMyEditsSubtitle": "Ver tu historial de ediciones en OpenStreetMap", + "aboutOSM": "Acerca de OpenStreetMap", + "aboutOSMDescription": "OpenStreetMap es un proyecto de mapeo colaborativo de código abierto donde los contribuyentes crean y mantienen un mapa gratuito y editable del mundo. Tus contribuciones de dispositivos de vigilancia ayudan a hacer visible y buscable esta infraestructura.", + "visitOSM": "Visitar OpenStreetMap", "deleteAccount": "Eliminar Cuenta OSM", "deleteAccountSubtitle": "Gestiona tu cuenta de OpenStreetMap", "deleteAccountExplanation": "Para eliminar tu cuenta de OpenStreetMap, necesitarás visitar el sitio web de OpenStreetMap. Esto eliminará permanentemente tu cuenta OSM y todos los datos asociados.", @@ -163,7 +170,11 @@ "goToOSM": "Ir a OpenStreetMap" }, "queue": { + "title": "Cola de Subida", + "subtitle": "Gestionar subidas pendientes de dispositivos de vigilancia", "pendingUploads": "Subidas pendientes: {}", + "pendingItemsCount": "Elementos Pendientes: {}", + "nothingInQueue": "No hay nada en la cola", "simulateModeEnabled": "Modo simulación activado – subidas simuladas", "sandboxMode": "Modo sandbox – subidas van al Sandbox OSM", "tapToViewQueue": "Toque para ver cola", diff --git a/lib/localizations/fr.json b/lib/localizations/fr.json index 1025e6c..64b22f8 100644 --- a/lib/localizations/fr.json +++ b/lib/localizations/fr.json @@ -147,6 +147,8 @@ "simulateDescription": "Simuler les téléchargements (ne contacte pas les serveurs OSM)" }, "auth": { + "osmAccountTitle": "Compte OpenStreetMap", + "osmAccountSubtitle": "Gérer votre connexion OSM et voir vos contributions", "loggedInAs": "Connecté en tant que {}", "loginToOSM": "Se connecter à OpenStreetMap", "tapToLogout": "Appuyer pour se déconnecter", @@ -156,6 +158,11 @@ "testConnectionSubtitle": "Vérifier que les identifiants OSM fonctionnent", "connectionOK": "Connexion OK - les identifiants sont valides", "connectionFailed": "Connexion échouée - veuillez vous reconnecter", + "viewMyEdits": "Voir Mes Modifications sur OSM", + "viewMyEditsSubtitle": "Voir votre historique de modifications sur OpenStreetMap", + "aboutOSM": "À Propos d'OpenStreetMap", + "aboutOSMDescription": "OpenStreetMap est un projet cartographique collaboratif open source où les contributeurs créent et maintiennent une carte gratuite et modifiable du monde. Vos contributions de dispositifs de surveillance aident à rendre cette infrastructure visible et consultable.", + "visitOSM": "Visiter OpenStreetMap", "deleteAccount": "Supprimer Compte OSM", "deleteAccountSubtitle": "Gérez votre compte OpenStreetMap", "deleteAccountExplanation": "Pour supprimer votre compte OpenStreetMap, vous devrez visiter le site web OpenStreetMap. Cela supprimera définitivement votre compte OSM et toutes les données associées.", @@ -163,7 +170,11 @@ "goToOSM": "Aller à OpenStreetMap" }, "queue": { + "title": "File de Téléchargement", + "subtitle": "Gérer les téléchargements de dispositifs de surveillance en attente", "pendingUploads": "Téléchargements en attente: {}", + "pendingItemsCount": "Éléments en Attente: {}", + "nothingInQueue": "Rien dans la file", "simulateModeEnabled": "Mode simulation activé – téléchargements simulés", "sandboxMode": "Mode sandbox – téléchargements vont vers OSM Sandbox", "tapToViewQueue": "Appuyer pour voir la file", diff --git a/lib/localizations/it.json b/lib/localizations/it.json index 4329154..d24c8aa 100644 --- a/lib/localizations/it.json +++ b/lib/localizations/it.json @@ -147,6 +147,8 @@ "simulateDescription": "Simula upload (non contatta i server OSM)" }, "auth": { + "osmAccountTitle": "Account OpenStreetMap", + "osmAccountSubtitle": "Gestisci il tuo login OSM e visualizza i tuoi contributi", "loggedInAs": "Loggato come {}", "loginToOSM": "Accedi a OpenStreetMap", "tapToLogout": "Tocca per disconnetterti", @@ -156,6 +158,11 @@ "testConnectionSubtitle": "Verifica che le credenziali OSM funzionino", "connectionOK": "Connessione OK - le credenziali sono valide", "connectionFailed": "Connessione fallita - per favore accedi di nuovo", + "viewMyEdits": "Visualizza le Mie Modifiche su OSM", + "viewMyEditsSubtitle": "Visualizza la cronologia delle tue modifiche su OpenStreetMap", + "aboutOSM": "Informazioni su OpenStreetMap", + "aboutOSMDescription": "OpenStreetMap è un progetto cartografico collaborativo open source dove i contributori creano e mantengono una mappa gratuita e modificabile del mondo. I tuoi contributi sui dispositivi di sorveglianza aiutano a rendere visibile e ricercabile questa infrastruttura.", + "visitOSM": "Visita OpenStreetMap", "deleteAccount": "Elimina Account OSM", "deleteAccountSubtitle": "Gestisci il tuo account OpenStreetMap", "deleteAccountExplanation": "Per eliminare il tuo account OpenStreetMap, dovrai visitare il sito web di OpenStreetMap. Questo rimuoverà permanentemente il tuo account OSM e tutti i dati associati.", @@ -163,7 +170,11 @@ "goToOSM": "Vai a OpenStreetMap" }, "queue": { + "title": "Coda di Upload", + "subtitle": "Gestisci gli upload di dispositivi di sorveglianza in sospeso", "pendingUploads": "Upload in sospeso: {}", + "pendingItemsCount": "Elementi in Sospeso: {}", + "nothingInQueue": "Niente in coda", "simulateModeEnabled": "Modalità simulazione abilitata – upload simulati", "sandboxMode": "Modalità sandbox – upload vanno alla Sandbox OSM", "tapToViewQueue": "Tocca per vedere la coda", diff --git a/lib/localizations/pt.json b/lib/localizations/pt.json index 6fb3628..bfe672a 100644 --- a/lib/localizations/pt.json +++ b/lib/localizations/pt.json @@ -147,6 +147,8 @@ "simulateDescription": "Simular uploads (não contacta servidores OSM)" }, "auth": { + "osmAccountTitle": "Conta OpenStreetMap", + "osmAccountSubtitle": "Gerencie seu login OSM e visualize suas contribuições", "loggedInAs": "Logado como {}", "loginToOSM": "Fazer login no OpenStreetMap", "tapToLogout": "Toque para sair", @@ -156,6 +158,11 @@ "testConnectionSubtitle": "Verificar se as credenciais OSM estão funcionando", "connectionOK": "Conexão OK - credenciais são válidas", "connectionFailed": "Conexão falhou - por favor, faça login novamente", + "viewMyEdits": "Ver Minhas Edições no OSM", + "viewMyEditsSubtitle": "Ver seu histórico de edições no OpenStreetMap", + "aboutOSM": "Sobre OpenStreetMap", + "aboutOSMDescription": "OpenStreetMap é um projeto de mapeamento colaborativo de código aberto onde os contribuintes criam e mantêm um mapa gratuito e editável do mundo. Suas contribuições de dispositivos de vigilância ajudam a tornar esta infraestrutura visível e pesquisável.", + "visitOSM": "Visitar OpenStreetMap", "deleteAccount": "Excluir Conta OSM", "deleteAccountSubtitle": "Gerencie sua conta OpenStreetMap", "deleteAccountExplanation": "Para excluir sua conta OpenStreetMap, você precisará visitar o site do OpenStreetMap. Isso removerá permanentemente sua conta OSM e todos os dados associados.", @@ -163,7 +170,11 @@ "goToOSM": "Ir para OpenStreetMap" }, "queue": { + "title": "Fila de Upload", + "subtitle": "Gerenciar uploads pendentes de dispositivos de vigilância", "pendingUploads": "Uploads pendentes: {}", + "pendingItemsCount": "Itens Pendentes: {}", + "nothingInQueue": "Nada na fila", "simulateModeEnabled": "Modo simulação ativado – uploads simulados", "sandboxMode": "Modo sandbox – uploads vão para o Sandbox OSM", "tapToViewQueue": "Toque para ver a fila", diff --git a/lib/localizations/zh.json b/lib/localizations/zh.json index 6985975..5f7243e 100644 --- a/lib/localizations/zh.json +++ b/lib/localizations/zh.json @@ -147,6 +147,8 @@ "simulateDescription": "模拟上传(不联系 OSM 服务器)" }, "auth": { + "osmAccountTitle": "OpenStreetMap 账户", + "osmAccountSubtitle": "管理您的 OSM 登录并查看您的贡献", "loggedInAs": "已登录为 {}", "loginToOSM": "登录 OpenStreetMap", "tapToLogout": "点击登出", @@ -156,6 +158,11 @@ "testConnectionSubtitle": "验证 OSM 凭据是否有效", "connectionOK": "连接正常 - 凭据有效", "connectionFailed": "连接失败 - 请重新登录", + "viewMyEdits": "在 OSM 上查看我的编辑", + "viewMyEditsSubtitle": "查看您在 OpenStreetMap 上的编辑历史", + "aboutOSM": "关于 OpenStreetMap", + "aboutOSMDescription": "OpenStreetMap 是一个协作的开源地图项目,贡献者创建和维护一个免费的、可编辑的世界地图。您的监控设备贡献有助于使这种基础设施可见和可搜索。", + "visitOSM": "访问 OpenStreetMap", "deleteAccount": "删除 OSM 账户", "deleteAccountSubtitle": "管理您的 OpenStreetMap 账户", "deleteAccountExplanation": "要删除您的 OpenStreetMap 账户,您需要访问 OpenStreetMap 网站。这将永久删除您的 OSM 账户和所有相关数据。", @@ -163,7 +170,11 @@ "goToOSM": "前往 OpenStreetMap" }, "queue": { + "title": "上传队列", + "subtitle": "管理待上传的监控设备", "pendingUploads": "待上传:{}", + "pendingItemsCount": "待处理项目:{}", + "nothingInQueue": "队列中没有内容", "simulateModeEnabled": "模拟模式已启用 – 上传已模拟", "sandboxMode": "沙盒模式 – 上传到 OSM 沙盒", "tapToViewQueue": "点击查看队列", diff --git a/lib/main.dart b/lib/main.dart index 6f0c5cc..0adf3a0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,6 +11,8 @@ import 'screens/advanced_settings_screen.dart'; import 'screens/language_settings_screen.dart'; import 'screens/about_screen.dart'; import 'screens/release_notes_screen.dart'; +import 'screens/osm_account_screen.dart'; +import 'screens/upload_queue_screen.dart'; import 'services/localization_service.dart'; import 'services/version_service.dart'; @@ -69,6 +71,8 @@ class DeFlockApp extends StatelessWidget { routes: { '/': (context) => const HomeScreen(), '/settings': (context) => const SettingsScreen(), + '/settings/osm-account': (context) => const OSMAccountScreen(), + '/settings/queue': (context) => const UploadQueueScreen(), '/settings/profiles': (context) => const ProfilesSettingsScreen(), '/settings/navigation': (context) => const NavigationSettingsScreen(), '/settings/offline': (context) => const OfflineSettingsScreen(), diff --git a/lib/screens/osm_account_screen.dart b/lib/screens/osm_account_screen.dart new file mode 100644 index 0000000..8a9c3d7 --- /dev/null +++ b/lib/screens/osm_account_screen.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../app_state.dart'; +import '../services/localization_service.dart'; +import '../dev_config.dart'; +import '../state/settings_state.dart'; +import '../screens/settings/sections/upload_mode_section.dart'; + +class OSMAccountScreen extends StatelessWidget { + const OSMAccountScreen({super.key}); + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: LocalizationService.instance, + builder: (context, child) { + final locService = LocalizationService.instance; + final appState = context.watch(); + + return Scaffold( + appBar: AppBar( + title: Text(locService.t('auth.osmAccountTitle')), + ), + body: ListView( + padding: EdgeInsets.fromLTRB( + 16, + 16, + 16, + 16 + MediaQuery.of(context).padding.bottom, + ), + children: [ + // Login/Account Status Section + Card( + child: 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(); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(locService.t('auth.loggedOut')), + backgroundColor: Colors.grey, + ), + ); + } + } else { + // Start login flow - the user will be redirected to browser + await appState.forceLogin(); + + // Don't show immediate feedback - the UI will update automatically + // when the OAuth callback completes and notifyListeners() is called + } + }, + ), + + if (appState.isLoggedIn) ...[ + const Divider(), + 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(); + } + }, + ), + + const Divider(), + ListTile( + leading: const Icon(Icons.history), + title: Text(locService.t('auth.viewMyEdits')), + subtitle: Text(locService.t('auth.viewMyEditsSubtitle')), + trailing: const Icon(Icons.open_in_new), + onTap: () async { + final url = Uri.parse('https://openstreetmap.org/user/${Uri.encodeComponent(appState.username)}/history'); + if (await canLaunchUrl(url)) { + await launchUrl(url, mode: LaunchMode.externalApplication); + } else { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(locService.t('advancedEdit.couldNotOpenOSMWebsite'))), + ); + } + } + }, + ), + ], + ], + ), + ), + + const SizedBox(height: 16), + + // Upload Mode Section (only show in development builds) + if (kEnableDevelopmentModes) ...[ + Card( + child: const Padding( + padding: EdgeInsets.all(16.0), + child: UploadModeSection(), + ), + ), + const SizedBox(height: 16), + ], + + // Information Section + Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + locService.t('auth.aboutOSM'), + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Text( + locService.t('auth.aboutOSMDescription'), + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () async { + final url = Uri.parse('https://openstreetmap.org'); + if (await canLaunchUrl(url)) { + await launchUrl(url, mode: LaunchMode.externalApplication); + } else { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(locService.t('advancedEdit.couldNotOpenOSMWebsite'))), + ); + } + } + }, + icon: const Icon(Icons.open_in_new), + label: Text(locService.t('auth.visitOSM')), + ), + ), + ], + ), + ), + ), + ], + ), + ); + }, + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 17f5666..1a17105 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,7 +1,4 @@ import 'package:flutter/material.dart'; -import 'settings/sections/auth_section.dart'; -import 'settings/sections/upload_mode_section.dart'; -import 'settings/sections/queue_section.dart'; import '../services/localization_service.dart'; import '../services/version_service.dart'; import '../dev_config.dart'; @@ -25,14 +22,24 @@ class SettingsScreen extends StatelessWidget { 16 + MediaQuery.of(context).padding.bottom, ), children: [ - // Only show upload mode section in development builds - if (kEnableDevelopmentModes) ...[ - const UploadModeSection(), - const Divider(), - ], - const AuthSection(), + // OpenStreetMap Account + _buildNavigationTile( + context, + icon: Icons.account_circle, + title: locService.t('auth.osmAccountTitle'), + subtitle: locService.t('auth.osmAccountSubtitle'), + onTap: () => Navigator.pushNamed(context, '/settings/osm-account'), + ), const Divider(), - const QueueSection(), + + // Upload Queue + _buildNavigationTile( + context, + icon: Icons.queue, + title: locService.t('queue.title'), + subtitle: locService.t('queue.subtitle'), + onTap: () => Navigator.pushNamed(context, '/settings/queue'), + ), const Divider(), // Navigation to sub-pages diff --git a/lib/screens/upload_queue_screen.dart b/lib/screens/upload_queue_screen.dart new file mode 100644 index 0000000..758f497 --- /dev/null +++ b/lib/screens/upload_queue_screen.dart @@ -0,0 +1,189 @@ +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 UploadQueueScreen extends StatelessWidget { + const UploadQueueScreen({super.key}); + + String _getUploadModeDisplayName(UploadMode mode) { + final locService = LocalizationService.instance; + switch (mode) { + case UploadMode.production: + return locService.t('uploadMode.production'); + case UploadMode.sandbox: + return locService.t('uploadMode.sandbox'); + case UploadMode.simulate: + return locService.t('uploadMode.simulate'); + } + } + + Color _getUploadModeColor(UploadMode mode) { + switch (mode) { + case UploadMode.production: + return Colors.green; // Green for production (real) + case UploadMode.sandbox: + return Colors.orange; // Orange for sandbox (testing) + case UploadMode.simulate: + return Colors.grey; // Grey for simulate (fake) + } + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: LocalizationService.instance, + builder: (context, child) { + final locService = LocalizationService.instance; + final appState = context.watch(); + + return Scaffold( + appBar: AppBar( + title: Text(locService.t('queue.title')), + ), + body: ListView( + padding: EdgeInsets.fromLTRB( + 16, + 16, + 16, + 16 + MediaQuery.of(context).padding.bottom, + ), + children: [ + // Clear Upload Queue button - always visible + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: appState.pendingCount > 0 ? () { + 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')), + ), + ], + ), + ); + } : null, + icon: const Icon(Icons.clear_all), + label: Text(locService.t('queue.clearUploadQueue')), + style: ElevatedButton.styleFrom( + backgroundColor: appState.pendingCount > 0 ? null : Theme.of(context).disabledColor.withOpacity(0.1), + ), + ), + ), + + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 8), + + // Queue list or empty message + if (appState.pendingUploads.isEmpty) ...[ + Center( + child: Padding( + padding: const EdgeInsets.all(32.0), + child: Column( + children: [ + Icon( + Icons.check_circle_outline, + size: 64, + color: Theme.of(context).textTheme.bodySmall?.color?.withOpacity(0.4), + ), + const SizedBox(height: 16), + Text( + locService.t('queue.nothingInQueue'), + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).textTheme.bodySmall?.color?.withOpacity(0.6), + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ] else ...[ + Text( + locService.t('queue.pendingItemsCount', params: [appState.pendingCount.toString()]), + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 16), + + // Queue items + ...appState.pendingUploads.asMap().entries.map((entry) { + final index = entry.key; + final upload = entry.value; + + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: ListTile( + leading: Icon( + upload.error ? Icons.error : Icons.camera_alt, + color: upload.error + ? Colors.red + : _getUploadModeColor(upload.uploadMode), + ), + title: Text( + locService.t('queue.cameraWithIndex', params: [(index + 1).toString()]) + + (upload.error ? locService.t('queue.error') : "") + + (upload.completing ? locService.t('queue.completing') : "") + ), + subtitle: Text( + 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 is String + ? upload.direction.toString() + : 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, + children: [ + if (upload.error && !upload.completing) + IconButton( + icon: const Icon(Icons.refresh), + color: Colors.orange, + tooltip: locService.t('queue.retryUpload'), + onPressed: () { + appState.retryUpload(upload); + }, + ), + if (upload.completing) + const Icon(Icons.check_circle, color: Colors.green) + else + IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + appState.removeFromQueue(upload); + }, + ), + ], + ), + ), + ); + }), + ], + ], + ), + ); + }, + ); + } +} \ No newline at end of file