mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-08-16 16:10:20 +02:00
Further improve tag views, implement upload queue pause toggle
This commit is contained in:
@@ -1,4 +1,17 @@
|
|||||||
{
|
{
|
||||||
|
"1.3.4": {
|
||||||
|
"content": [
|
||||||
|
"• NEW: 'Pause Upload Queue' toggle in Offline Settings - stops uploads while keeping live data access",
|
||||||
|
"• Useful for metered connections or when you want to batch uploads later",
|
||||||
|
"• Upload queue is now disabled if either full offline mode OR pause queue processing is enabled",
|
||||||
|
"• FIXED: Sheet buttons now remain visible when rotating from portrait to landscape mode",
|
||||||
|
"• FIXED: Sheets now properly resize when rotating between orientations without requiring user interaction",
|
||||||
|
"• IMPROVED: Tag list height adapts automatically for landscape orientation to prevent covering map",
|
||||||
|
"• IMPROVED: Sheets with few tags now shrink to appropriate size rather than maintaining fixed height",
|
||||||
|
"• IMPROVED: More reliable sheet layout using proper flexible height constraints",
|
||||||
|
"• CLEANED: Fixed minor code formatting inconsistencies"
|
||||||
|
]
|
||||||
|
},
|
||||||
"1.3.3": {
|
"1.3.3": {
|
||||||
"content": [
|
"content": [
|
||||||
"• NEW: Added builtin surveillance device profiles for Rekor and Axis Communications ALPR cameras",
|
"• NEW: Added builtin surveillance device profiles for Rekor and Axis Communications ALPR cameras",
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ class AppState extends ChangeNotifier {
|
|||||||
|
|
||||||
// Settings state
|
// Settings state
|
||||||
bool get offlineMode => _settingsState.offlineMode;
|
bool get offlineMode => _settingsState.offlineMode;
|
||||||
|
bool get pauseQueueProcessing => _settingsState.pauseQueueProcessing;
|
||||||
int get maxCameras => _settingsState.maxCameras;
|
int get maxCameras => _settingsState.maxCameras;
|
||||||
UploadMode get uploadMode => _settingsState.uploadMode;
|
UploadMode get uploadMode => _settingsState.uploadMode;
|
||||||
FollowMeMode get followMeMode => _settingsState.followMeMode;
|
FollowMeMode get followMeMode => _settingsState.followMeMode;
|
||||||
@@ -411,6 +412,15 @@ class AppState extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setPauseQueueProcessing(bool enabled) async {
|
||||||
|
await _settingsState.setPauseQueueProcessing(enabled);
|
||||||
|
if (!enabled) {
|
||||||
|
_startUploader(); // Resume upload queue processing
|
||||||
|
} else {
|
||||||
|
_uploadQueueState.stopUploader(); // Stop uploader when paused
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
set maxCameras(int n) {
|
set maxCameras(int n) {
|
||||||
_settingsState.maxCameras = n;
|
_settingsState.maxCameras = n;
|
||||||
}
|
}
|
||||||
@@ -524,6 +534,7 @@ class AppState extends ChangeNotifier {
|
|||||||
void _startUploader() {
|
void _startUploader() {
|
||||||
_uploadQueueState.startUploader(
|
_uploadQueueState.startUploader(
|
||||||
offlineMode: offlineMode,
|
offlineMode: offlineMode,
|
||||||
|
pauseQueueProcessing: pauseQueueProcessing,
|
||||||
uploadMode: uploadMode,
|
uploadMode: uploadMode,
|
||||||
getAccessToken: _authState.getAccessToken,
|
getAccessToken: _authState.getAccessToken,
|
||||||
);
|
);
|
||||||
|
|||||||
+9
-1
@@ -80,7 +80,15 @@ const Duration kFollowMeAnimationDuration = Duration(milliseconds: 600);
|
|||||||
const double kMinSpeedForRotationMps = 1.0; // Minimum speed (m/s) to apply rotation
|
const double kMinSpeedForRotationMps = 1.0; // Minimum speed (m/s) to apply rotation
|
||||||
|
|
||||||
// Sheet content configuration
|
// Sheet content configuration
|
||||||
const double kMaxTagListHeightRatio = 0.4; // Maximum height for tag lists as fraction of screen height
|
const double kMaxTagListHeightRatioPortrait = 0.4; // Maximum height for tag lists in portrait mode
|
||||||
|
const double kMaxTagListHeightRatioLandscape = 0.3; // Maximum height for tag lists in landscape mode
|
||||||
|
|
||||||
|
/// Get appropriate tag list height ratio based on screen orientation
|
||||||
|
double getTagListHeightRatio(BuildContext context) {
|
||||||
|
final size = MediaQuery.of(context).size;
|
||||||
|
final isLandscape = size.width > size.height;
|
||||||
|
return isLandscape ? kMaxTagListHeightRatioLandscape : kMaxTagListHeightRatioPortrait;
|
||||||
|
}
|
||||||
|
|
||||||
// Proximity alerts configuration
|
// Proximity alerts configuration
|
||||||
const int kProximityAlertDefaultDistance = 200; // meters
|
const int kProximityAlertDefaultDistance = 200; // meters
|
||||||
|
|||||||
@@ -39,6 +39,8 @@
|
|||||||
"maxNodesWarning": "Sie möchten das wahrscheinlich nicht tun, es sei denn, Sie sind absolut sicher, dass Sie einen guten Grund dafür haben.",
|
"maxNodesWarning": "Sie möchten das wahrscheinlich nicht tun, es sei denn, Sie sind absolut sicher, dass Sie einen guten Grund dafür haben.",
|
||||||
"offlineMode": "Offline-Modus",
|
"offlineMode": "Offline-Modus",
|
||||||
"offlineModeSubtitle": "Alle Netzwerkanfragen außer für lokale/Offline-Bereiche deaktivieren.",
|
"offlineModeSubtitle": "Alle Netzwerkanfragen außer für lokale/Offline-Bereiche deaktivieren.",
|
||||||
|
"pauseQueueProcessing": "Upload-Warteschlange pausieren",
|
||||||
|
"pauseQueueProcessingSubtitle": "Upload von wartenden Änderungen stoppen, aber Live-Datenzugriff beibehalten.",
|
||||||
"offlineModeWarningTitle": "Aktive Downloads",
|
"offlineModeWarningTitle": "Aktive Downloads",
|
||||||
"offlineModeWarningMessage": "Die Aktivierung des Offline-Modus bricht alle aktiven Bereichsdownloads ab. Möchten Sie fortfahren?",
|
"offlineModeWarningMessage": "Die Aktivierung des Offline-Modus bricht alle aktiven Bereichsdownloads ab. Möchten Sie fortfahren?",
|
||||||
"enableOfflineMode": "Offline-Modus Aktivieren",
|
"enableOfflineMode": "Offline-Modus Aktivieren",
|
||||||
|
|||||||
@@ -57,6 +57,8 @@
|
|||||||
"maxNodesWarning": "You probably don't want to do that unless you are absolutely sure you have a good reason for it.",
|
"maxNodesWarning": "You probably don't want to do that unless you are absolutely sure you have a good reason for it.",
|
||||||
"offlineMode": "Offline Mode",
|
"offlineMode": "Offline Mode",
|
||||||
"offlineModeSubtitle": "Disable all network requests except for local/offline areas.",
|
"offlineModeSubtitle": "Disable all network requests except for local/offline areas.",
|
||||||
|
"pauseQueueProcessing": "Pause Upload Queue",
|
||||||
|
"pauseQueueProcessingSubtitle": "Stop uploading queued changes while keeping live data access.",
|
||||||
"offlineModeWarningTitle": "Active Downloads",
|
"offlineModeWarningTitle": "Active Downloads",
|
||||||
"offlineModeWarningMessage": "Enabling offline mode will cancel any active area downloads. Do you want to continue?",
|
"offlineModeWarningMessage": "Enabling offline mode will cancel any active area downloads. Do you want to continue?",
|
||||||
"enableOfflineMode": "Enable Offline Mode",
|
"enableOfflineMode": "Enable Offline Mode",
|
||||||
|
|||||||
@@ -57,6 +57,8 @@
|
|||||||
"maxNodesWarning": "Probablemente no quieras hacer eso a menos que estés absolutamente seguro de que tienes una buena razón para ello.",
|
"maxNodesWarning": "Probablemente no quieras hacer eso a menos que estés absolutamente seguro de que tienes una buena razón para ello.",
|
||||||
"offlineMode": "Modo Sin Conexión",
|
"offlineMode": "Modo Sin Conexión",
|
||||||
"offlineModeSubtitle": "Deshabilitar todas las solicitudes de red excepto para áreas locales/sin conexión.",
|
"offlineModeSubtitle": "Deshabilitar todas las solicitudes de red excepto para áreas locales/sin conexión.",
|
||||||
|
"pauseQueueProcessing": "Pausar Cola de Subida",
|
||||||
|
"pauseQueueProcessingSubtitle": "Detener la subida de cambios en cola manteniendo acceso a datos en vivo.",
|
||||||
"offlineModeWarningTitle": "Descargas Activas",
|
"offlineModeWarningTitle": "Descargas Activas",
|
||||||
"offlineModeWarningMessage": "Habilitar el modo sin conexión cancelará cualquier descarga de área activa. ¿Desea continuar?",
|
"offlineModeWarningMessage": "Habilitar el modo sin conexión cancelará cualquier descarga de área activa. ¿Desea continuar?",
|
||||||
"enableOfflineMode": "Habilitar Modo Sin Conexión",
|
"enableOfflineMode": "Habilitar Modo Sin Conexión",
|
||||||
|
|||||||
@@ -57,6 +57,8 @@
|
|||||||
"maxNodesWarning": "Vous ne voulez probablement pas faire cela à moins d'être absolument sûr d'avoir une bonne raison de le faire.",
|
"maxNodesWarning": "Vous ne voulez probablement pas faire cela à moins d'être absolument sûr d'avoir une bonne raison de le faire.",
|
||||||
"offlineMode": "Mode Hors Ligne",
|
"offlineMode": "Mode Hors Ligne",
|
||||||
"offlineModeSubtitle": "Désactiver toutes les requêtes réseau sauf pour les zones locales/hors ligne.",
|
"offlineModeSubtitle": "Désactiver toutes les requêtes réseau sauf pour les zones locales/hors ligne.",
|
||||||
|
"pauseQueueProcessing": "Suspendre la File d'Upload",
|
||||||
|
"pauseQueueProcessingSubtitle": "Arrêter l'upload des modifications en attente tout en gardant l'accès aux données en direct.",
|
||||||
"offlineModeWarningTitle": "Téléchargements Actifs",
|
"offlineModeWarningTitle": "Téléchargements Actifs",
|
||||||
"offlineModeWarningMessage": "L'activation du mode hors ligne annulera tous les téléchargements de zone actifs. Voulez-vous continuer?",
|
"offlineModeWarningMessage": "L'activation du mode hors ligne annulera tous les téléchargements de zone actifs. Voulez-vous continuer?",
|
||||||
"enableOfflineMode": "Activer le Mode Hors Ligne",
|
"enableOfflineMode": "Activer le Mode Hors Ligne",
|
||||||
|
|||||||
@@ -57,6 +57,8 @@
|
|||||||
"maxNodesWarning": "Probabilmente non vuoi farlo a meno che non sei assolutamente sicuro di avere una buona ragione per farlo.",
|
"maxNodesWarning": "Probabilmente non vuoi farlo a meno che non sei assolutamente sicuro di avere una buona ragione per farlo.",
|
||||||
"offlineMode": "Modalità Offline",
|
"offlineMode": "Modalità Offline",
|
||||||
"offlineModeSubtitle": "Disabilita tutte le richieste di rete tranne per aree locali/offline.",
|
"offlineModeSubtitle": "Disabilita tutte le richieste di rete tranne per aree locali/offline.",
|
||||||
|
"pauseQueueProcessing": "Pausa Coda Upload",
|
||||||
|
"pauseQueueProcessingSubtitle": "Ferma l'upload delle modifiche in coda mantenendo l'accesso ai dati dal vivo.",
|
||||||
"offlineModeWarningTitle": "Download Attivi",
|
"offlineModeWarningTitle": "Download Attivi",
|
||||||
"offlineModeWarningMessage": "L'attivazione della modalità offline cancellerà qualsiasi download di area attivo. Vuoi continuare?",
|
"offlineModeWarningMessage": "L'attivazione della modalità offline cancellerà qualsiasi download di area attivo. Vuoi continuare?",
|
||||||
"enableOfflineMode": "Attiva Modalità Offline",
|
"enableOfflineMode": "Attiva Modalità Offline",
|
||||||
|
|||||||
@@ -57,6 +57,8 @@
|
|||||||
"maxNodesWarning": "Você provavelmente não quer fazer isso a menos que tenha certeza absoluta de que tem uma boa razão para isso.",
|
"maxNodesWarning": "Você provavelmente não quer fazer isso a menos que tenha certeza absoluta de que tem uma boa razão para isso.",
|
||||||
"offlineMode": "Modo Offline",
|
"offlineMode": "Modo Offline",
|
||||||
"offlineModeSubtitle": "Desabilitar todas as requisições de rede exceto para áreas locais/offline.",
|
"offlineModeSubtitle": "Desabilitar todas as requisições de rede exceto para áreas locais/offline.",
|
||||||
|
"pauseQueueProcessing": "Pausar Fila de Upload",
|
||||||
|
"pauseQueueProcessingSubtitle": "Parar upload de alterações na fila mantendo acesso a dados ao vivo.",
|
||||||
"offlineModeWarningTitle": "Downloads Ativos",
|
"offlineModeWarningTitle": "Downloads Ativos",
|
||||||
"offlineModeWarningMessage": "Ativar o modo offline cancelará qualquer download de área ativo. Deseja continuar?",
|
"offlineModeWarningMessage": "Ativar o modo offline cancelará qualquer download de área ativo. Deseja continuar?",
|
||||||
"enableOfflineMode": "Ativar Modo Offline",
|
"enableOfflineMode": "Ativar Modo Offline",
|
||||||
|
|||||||
@@ -57,6 +57,8 @@
|
|||||||
"maxNodesWarning": "除非您确定有充分的理由,否则您可能不想这样做。",
|
"maxNodesWarning": "除非您确定有充分的理由,否则您可能不想这样做。",
|
||||||
"offlineMode": "离线模式",
|
"offlineMode": "离线模式",
|
||||||
"offlineModeSubtitle": "禁用除本地/离线区域外的所有网络请求。",
|
"offlineModeSubtitle": "禁用除本地/离线区域外的所有网络请求。",
|
||||||
|
"pauseQueueProcessing": "暂停上传队列",
|
||||||
|
"pauseQueueProcessingSubtitle": "停止上传排队的更改,同时保持实时数据访问。",
|
||||||
"offlineModeWarningTitle": "活动下载",
|
"offlineModeWarningTitle": "活动下载",
|
||||||
"offlineModeWarningMessage": "启用离线模式将取消任何活动的区域下载。您要继续吗?",
|
"offlineModeWarningMessage": "启用离线模式将取消任何活动的区域下载。您要继续吗?",
|
||||||
"enableOfflineMode": "启用离线模式",
|
"enableOfflineMode": "启用离线模式",
|
||||||
|
|||||||
@@ -77,6 +77,27 @@ class OfflineModeSection extends StatelessWidget {
|
|||||||
onChanged: (value) => _handleOfflineModeChange(context, appState, value),
|
onChanged: (value) => _handleOfflineModeChange(context, appState, value),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
Icons.pause_circle_outline,
|
||||||
|
color: appState.offlineMode
|
||||||
|
? Theme.of(context).disabledColor
|
||||||
|
: Theme.of(context).iconTheme.color,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
locService.t('settings.pauseQueueProcessingSubtitle'),
|
||||||
|
style: appState.offlineMode
|
||||||
|
? TextStyle(color: Theme.of(context).disabledColor)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
trailing: Switch(
|
||||||
|
value: appState.pauseQueueProcessing,
|
||||||
|
onChanged: appState.offlineMode
|
||||||
|
? null // Disable when offline mode is on
|
||||||
|
: (value) => appState.setPauseQueueProcessing(value),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,8 +28,10 @@ class SettingsState extends ChangeNotifier {
|
|||||||
static const String _proximityAlertDistancePrefsKey = 'proximity_alert_distance';
|
static const String _proximityAlertDistancePrefsKey = 'proximity_alert_distance';
|
||||||
static const String _networkStatusIndicatorEnabledPrefsKey = 'network_status_indicator_enabled';
|
static const String _networkStatusIndicatorEnabledPrefsKey = 'network_status_indicator_enabled';
|
||||||
static const String _suspectedLocationMinDistancePrefsKey = 'suspected_location_min_distance';
|
static const String _suspectedLocationMinDistancePrefsKey = 'suspected_location_min_distance';
|
||||||
|
static const String _pauseQueueProcessingPrefsKey = 'pause_queue_processing';
|
||||||
|
|
||||||
bool _offlineMode = false;
|
bool _offlineMode = false;
|
||||||
|
bool _pauseQueueProcessing = false;
|
||||||
int _maxCameras = 250;
|
int _maxCameras = 250;
|
||||||
UploadMode _uploadMode = kEnableDevelopmentModes ? UploadMode.simulate : UploadMode.production;
|
UploadMode _uploadMode = kEnableDevelopmentModes ? UploadMode.simulate : UploadMode.production;
|
||||||
FollowMeMode _followMeMode = FollowMeMode.follow;
|
FollowMeMode _followMeMode = FollowMeMode.follow;
|
||||||
@@ -42,6 +44,7 @@ class SettingsState extends ChangeNotifier {
|
|||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
bool get offlineMode => _offlineMode;
|
bool get offlineMode => _offlineMode;
|
||||||
|
bool get pauseQueueProcessing => _pauseQueueProcessing;
|
||||||
int get maxCameras => _maxCameras;
|
int get maxCameras => _maxCameras;
|
||||||
UploadMode get uploadMode => _uploadMode;
|
UploadMode get uploadMode => _uploadMode;
|
||||||
FollowMeMode get followMeMode => _followMeMode;
|
FollowMeMode get followMeMode => _followMeMode;
|
||||||
@@ -92,6 +95,9 @@ class SettingsState extends ChangeNotifier {
|
|||||||
// Load offline mode
|
// Load offline mode
|
||||||
_offlineMode = prefs.getBool(_offlineModePrefsKey) ?? false;
|
_offlineMode = prefs.getBool(_offlineModePrefsKey) ?? false;
|
||||||
|
|
||||||
|
// Load queue processing setting
|
||||||
|
_pauseQueueProcessing = prefs.getBool(_pauseQueueProcessingPrefsKey) ?? false;
|
||||||
|
|
||||||
// Load max cameras
|
// Load max cameras
|
||||||
if (prefs.containsKey(_maxCamerasPrefsKey)) {
|
if (prefs.containsKey(_maxCamerasPrefsKey)) {
|
||||||
_maxCameras = prefs.getInt(_maxCamerasPrefsKey) ?? 250;
|
_maxCameras = prefs.getInt(_maxCamerasPrefsKey) ?? 250;
|
||||||
@@ -212,6 +218,13 @@ class SettingsState extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setPauseQueueProcessing(bool enabled) async {
|
||||||
|
_pauseQueueProcessing = enabled;
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setBool(_pauseQueueProcessingPrefsKey, enabled);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
set maxCameras(int n) {
|
set maxCameras(int n) {
|
||||||
if (n < 10) n = 10; // minimum
|
if (n < 10) n = 10; // minimum
|
||||||
_maxCameras = n;
|
_maxCameras = n;
|
||||||
|
|||||||
@@ -163,16 +163,17 @@ class UploadQueueState extends ChangeNotifier {
|
|||||||
// Start the upload processing loop
|
// Start the upload processing loop
|
||||||
void startUploader({
|
void startUploader({
|
||||||
required bool offlineMode,
|
required bool offlineMode,
|
||||||
|
required bool pauseQueueProcessing,
|
||||||
required UploadMode uploadMode,
|
required UploadMode uploadMode,
|
||||||
required Future<String?> Function() getAccessToken,
|
required Future<String?> Function() getAccessToken,
|
||||||
}) {
|
}) {
|
||||||
_uploadTimer?.cancel();
|
_uploadTimer?.cancel();
|
||||||
|
|
||||||
// No uploads without queue, or if offline mode is enabled.
|
// No uploads if queue is empty, offline mode is enabled, or queue processing is paused
|
||||||
if (_queue.isEmpty || offlineMode) return;
|
if (_queue.isEmpty || offlineMode || pauseQueueProcessing) return;
|
||||||
|
|
||||||
_uploadTimer = Timer.periodic(const Duration(seconds: 10), (t) async {
|
_uploadTimer = Timer.periodic(const Duration(seconds: 10), (t) async {
|
||||||
if (_queue.isEmpty || offlineMode) {
|
if (_queue.isEmpty || offlineMode || pauseQueueProcessing) {
|
||||||
_uploadTimer?.cancel();
|
_uploadTimer?.cancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+113
-109
@@ -101,132 +101,136 @@ class NodeTagSheet extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SafeArea(
|
return LayoutBuilder(
|
||||||
child: Padding(
|
builder: (context, constraints) {
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
|
return SafeArea(
|
||||||
child: SingleChildScrollView(
|
child: Padding(
|
||||||
child: Column(
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Text(
|
children: [
|
||||||
locService.t('node.title').replaceAll('{}', node.id.toString()),
|
Text(
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
locService.t('node.title').replaceAll('{}', node.id.toString()),
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Tag list with flexible height constraint
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxHeight: MediaQuery.of(context).size.height * getTagListHeightRatio(context),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
child: SingleChildScrollView(
|
||||||
// Constrain tag list height to keep buttons and map visible
|
child: Column(
|
||||||
ConstrainedBox(
|
mainAxisSize: MainAxisSize.min,
|
||||||
constraints: BoxConstraints(
|
children: [
|
||||||
maxHeight: MediaQuery.of(context).size.height * kMaxTagListHeightRatio,
|
...node.tags.entries.map(
|
||||||
),
|
(e) => Padding(
|
||||||
child: SingleChildScrollView(
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||||
child: Column(
|
child: Row(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
...node.tags.entries.map(
|
children: [
|
||||||
(e) => Padding(
|
Text(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
e.key,
|
||||||
child: Row(
|
style: TextStyle(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
fontWeight: FontWeight.w500,
|
||||||
children: [
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
Text(
|
),
|
||||||
e.key,
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Linkify(
|
||||||
|
onOpen: (link) async {
|
||||||
|
final uri = Uri.parse(link.url);
|
||||||
|
if (await canLaunchUrl(uri)) {
|
||||||
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
|
} else if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('${LocalizationService.instance.t('advancedEdit.couldNotOpenURL')}: ${link.url}')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text: e.value,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w500,
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
),
|
||||||
),
|
linkStyle: TextStyle(
|
||||||
const SizedBox(width: 8),
|
color: Theme.of(context).colorScheme.primary,
|
||||||
Expanded(
|
decoration: TextDecoration.underline,
|
||||||
child: Linkify(
|
|
||||||
onOpen: (link) async {
|
|
||||||
final uri = Uri.parse(link.url);
|
|
||||||
if (await canLaunchUrl(uri)) {
|
|
||||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
|
||||||
} else if (context.mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('${LocalizationService.instance.t('advancedEdit.couldNotOpenURL')}: ${link.url}')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
text: e.value,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
|
|
||||||
),
|
|
||||||
linkStyle: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
),
|
|
||||||
options: const LinkifyOptions(humanize: false),
|
|
||||||
),
|
),
|
||||||
|
options: const LinkifyOptions(humanize: false),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
),
|
||||||
// First row: View and Advanced buttons
|
const SizedBox(height: 16),
|
||||||
Row(
|
// First row: View and Advanced buttons
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
Row(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
TextButton.icon(
|
children: [
|
||||||
onPressed: () => _viewOnOSM(),
|
TextButton.icon(
|
||||||
icon: const Icon(Icons.open_in_new, size: 16),
|
onPressed: () => _viewOnOSM(),
|
||||||
label: Text(locService.t('actions.viewOnOSM')),
|
icon: const Icon(Icons.open_in_new, size: 16),
|
||||||
|
label: Text(locService.t('actions.viewOnOSM')),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
if (isEditable) ...[
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: _openAdvancedEdit,
|
||||||
|
icon: const Icon(Icons.open_in_new, size: 18),
|
||||||
|
label: Text(locService.t('actions.advanced')),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
minimumSize: const Size(0, 36),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
// Second row: Edit, Delete, and Close buttons
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
if (isEditable) ...[
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: _openEditSheet,
|
||||||
|
icon: const Icon(Icons.edit, size: 18),
|
||||||
|
label: Text(locService.edit),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: const Size(0, 36),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
if (isEditable) ...[
|
ElevatedButton.icon(
|
||||||
OutlinedButton.icon(
|
onPressed: _deleteNode,
|
||||||
onPressed: _openAdvancedEdit,
|
icon: const Icon(Icons.delete, size: 18),
|
||||||
icon: const Icon(Icons.open_in_new, size: 18),
|
label: Text(locService.t('actions.delete')),
|
||||||
label: Text(locService.t('actions.advanced')),
|
style: ElevatedButton.styleFrom(
|
||||||
style: OutlinedButton.styleFrom(
|
minimumSize: const Size(0, 36),
|
||||||
minimumSize: const Size(0, 36),
|
foregroundColor: Colors.red,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
// Second row: Edit, Delete, and Close buttons
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
if (isEditable) ...[
|
|
||||||
ElevatedButton.icon(
|
|
||||||
onPressed: _openEditSheet,
|
|
||||||
icon: const Icon(Icons.edit, size: 18),
|
|
||||||
label: Text(locService.edit),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
minimumSize: const Size(0, 36),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
ElevatedButton.icon(
|
|
||||||
onPressed: _deleteNode,
|
|
||||||
icon: const Icon(Icons.delete, size: 18),
|
|
||||||
label: Text(locService.t('actions.delete')),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
minimumSize: const Size(0, 36),
|
|
||||||
foregroundColor: Colors.red,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
],
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: Text(locService.t('actions.close')),
|
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
],
|
],
|
||||||
),
|
TextButton(
|
||||||
],
|
onPressed: () => Navigator.pop(context),
|
||||||
),
|
child: Text(locService.t('actions.close')),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,132 +29,135 @@ class SuspectedLocationSheet extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return SafeArea(
|
return LayoutBuilder(
|
||||||
child: Padding(
|
builder: (context, constraints) {
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
|
return SafeArea(
|
||||||
child: SingleChildScrollView(
|
child: Padding(
|
||||||
child: Column(
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Text(
|
children: [
|
||||||
locService.t('suspectedLocation.title', params: [location.ticketNo]),
|
Text(
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
locService.t('suspectedLocation.title', params: [location.ticketNo]),
|
||||||
),
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
const SizedBox(height: 12),
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
// Constrain field list height to keep buttons visible
|
// Field list with flexible height constraint
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxHeight: MediaQuery.of(context).size.height * kMaxTagListHeightRatio,
|
maxHeight: MediaQuery.of(context).size.height * getTagListHeightRatio(context),
|
||||||
),
|
),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
// Display all fields
|
children: [
|
||||||
...displayData.entries.map(
|
// Display all fields
|
||||||
(e) => Padding(
|
...displayData.entries.map(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
(e) => Padding(
|
||||||
child: Row(
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Row(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Text(
|
children: [
|
||||||
e.key,
|
Text(
|
||||||
style: TextStyle(
|
e.key,
|
||||||
fontWeight: FontWeight.w500,
|
style: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
),
|
||||||
Expanded(
|
const SizedBox(width: 8),
|
||||||
child: e.key.toLowerCase().contains('url') && e.value.isNotEmpty
|
Expanded(
|
||||||
? GestureDetector(
|
child: e.key.toLowerCase().contains('url') && e.value.isNotEmpty
|
||||||
onTap: () async {
|
? GestureDetector(
|
||||||
final uri = Uri.parse(e.value);
|
onTap: () async {
|
||||||
if (await canLaunchUrl(uri)) {
|
final uri = Uri.parse(e.value);
|
||||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
if (await canLaunchUrl(uri)) {
|
||||||
} else {
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
if (context.mounted) {
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (context.mounted) {
|
||||||
SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
content: Text('Could not open URL: ${e.value}'),
|
SnackBar(
|
||||||
),
|
content: Text('Could not open URL: ${e.value}'),
|
||||||
);
|
),
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
child: Text(
|
},
|
||||||
e.value,
|
child: Text(
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
),
|
|
||||||
softWrap: true,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Text(
|
|
||||||
e.value,
|
e.value,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
),
|
),
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
],
|
: Text(
|
||||||
),
|
e.value,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
|
||||||
|
),
|
||||||
|
softWrap: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Coordinates info
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
locService.t('suspectedLocation.coordinates'),
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'${location.centroid.latitude.toStringAsFixed(6)}, ${location.centroid.longitude.toStringAsFixed(6)}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
|
|
||||||
),
|
|
||||||
softWrap: true,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Close button
|
// Coordinates info
|
||||||
Row(
|
Padding(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
TextButton(
|
Text(
|
||||||
onPressed: () => Navigator.pop(context),
|
locService.t('suspectedLocation.coordinates'),
|
||||||
child: Text(locService.t('actions.close')),
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'${location.centroid.latitude.toStringAsFixed(6)}, ${location.centroid.longitude.toStringAsFixed(6)}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
|
||||||
|
),
|
||||||
|
softWrap: true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Close button
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(locService.t('actions.close')),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
name: deflockapp
|
name: deflockapp
|
||||||
description: Map public surveillance infrastructure with OpenStreetMap
|
description: Map public surveillance infrastructure with OpenStreetMap
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 1.3.3+11 # The thing after the + is the version code, incremented with each release
|
version: 1.3.4+12 # The thing after the + is the version code, incremented with each release
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.5.0 <4.0.0" # oauth2_client 4.x needs Dart 3.5+
|
sdk: ">=3.5.0 <4.0.0" # oauth2_client 4.x needs Dart 3.5+
|
||||||
|
|||||||
Reference in New Issue
Block a user