mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-10-08 00:51:13 +02:00
Merge pull request #198 from FoggedLens/osm-user-block-detection
Osm user block detection
This commit is contained in:
29 files changed
+968
-52
No files matched your search
+141
-10
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart' show LatLngBounds;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'services/http_client.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@@ -20,10 +21,14 @@ import 'services/tile_preview_service.dart';
|
||||
import 'services/changelog_service.dart';
|
||||
import 'services/operator_profile_service.dart';
|
||||
import 'services/deep_link_service.dart';
|
||||
import 'services/auth_service.dart' show AccountBlockedException;
|
||||
import 'services/localization_service.dart';
|
||||
import 'widgets/node_provider_with_cache.dart';
|
||||
import 'services/profile_service.dart';
|
||||
import 'widgets/reauth_messages_dialog.dart';
|
||||
import 'widgets/active_block_dialog.dart';
|
||||
import 'dev_config.dart';
|
||||
import 'state/account_block_state.dart';
|
||||
import 'state/auth_state.dart';
|
||||
import 'state/messages_state.dart';
|
||||
import 'state/navigation_state.dart';
|
||||
@@ -46,6 +51,7 @@ class AppState extends ChangeNotifier {
|
||||
static late AppState instance;
|
||||
|
||||
// State modules
|
||||
late final AccountBlockState _accountBlockState;
|
||||
late final AuthState _authState;
|
||||
late final MessagesState _messagesState;
|
||||
late final NavigationState _navigationState;
|
||||
@@ -72,6 +78,7 @@ class AppState extends ChangeNotifier {
|
||||
|
||||
AppState() {
|
||||
instance = this;
|
||||
_accountBlockState = AccountBlockState();
|
||||
_authState = AuthState();
|
||||
_messagesState = MessagesState();
|
||||
_navigationState = NavigationState();
|
||||
@@ -84,6 +91,7 @@ class AppState extends ChangeNotifier {
|
||||
_uploadQueueState = UploadQueueState();
|
||||
|
||||
// Set up state change listeners
|
||||
_accountBlockState.addListener(_onStateChanged);
|
||||
_authState.addListener(_onStateChanged);
|
||||
_messagesState.addListener(_onStateChanged);
|
||||
_navigationState.addListener(_onStateChanged);
|
||||
@@ -179,6 +187,9 @@ class AppState extends ChangeNotifier {
|
||||
bool get hasUnreadChangesetComments => _messagesState.hasUnreadChangesetComments;
|
||||
bool get hasUnreadNotifications => _messagesState.hasUnreadNotifications;
|
||||
bool get isCheckingMessages => _messagesState.isChecking;
|
||||
|
||||
// Account block state
|
||||
bool get hasActiveBlock => _accountBlockState.isBlocked;
|
||||
|
||||
// Tile provider state
|
||||
List<TileProvider> get tileProviders => _settingsState.tileProviders;
|
||||
@@ -196,6 +207,10 @@ class AppState extends ChangeNotifier {
|
||||
/// queue so in-flight submissions can finish first.
|
||||
bool get hasInFlightUploads => pendingUploads.any((u) => u.isActivelyProcessing);
|
||||
|
||||
/// True if queue processing is currently paused because an active-block
|
||||
/// check found the user's account blocked.
|
||||
bool get isQueuePausedDueToBlock => _uploadQueueState.isPausedDueToBlock;
|
||||
|
||||
|
||||
// Suspected location state
|
||||
SuspectedLocation? get selectedSuspectedLocation => _suspectedLocationState.selectedLocation;
|
||||
@@ -245,15 +260,17 @@ class AppState extends ChangeNotifier {
|
||||
await _suspectedLocationState.init(offlineMode: _settingsState.offlineMode);
|
||||
await _uploadQueueState.init();
|
||||
await _authState.init(_settingsState.uploadMode);
|
||||
await _accountBlockState.init(_settingsState.uploadMode);
|
||||
|
||||
// Set up callback to repopulate pending nodes after cache clears
|
||||
NodeProviderWithCache.instance.setOnCacheClearedCallback(() {
|
||||
_uploadQueueState.repopulateCacheFromQueue();
|
||||
});
|
||||
|
||||
// Check for messages on app launch if user is already logged in
|
||||
// Check for messages and active blocks on app launch if user is already logged in
|
||||
if (isLoggedIn) {
|
||||
checkMessages();
|
||||
checkActiveBlock();
|
||||
}
|
||||
|
||||
// Note: Re-auth check will be triggered from home screen after init
|
||||
@@ -298,34 +315,131 @@ class AppState extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// ---------- Auth Methods ----------
|
||||
Future<void> login() async {
|
||||
await _authState.login();
|
||||
// Check for messages after successful login
|
||||
Future<void> login({BuildContext? context}) async {
|
||||
try {
|
||||
await _authState.login();
|
||||
} catch (e) {
|
||||
await _recordLoginFailure(e);
|
||||
if (context == null || !context.mounted) return;
|
||||
_showLoginFailureUi(e, context);
|
||||
return;
|
||||
}
|
||||
// Check for messages and active blocks after successful login
|
||||
if (isLoggedIn) {
|
||||
checkMessages();
|
||||
checkActiveBlock();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
await _authState.logout();
|
||||
// Clear message state when logging out
|
||||
// Clear message and block state when logging out
|
||||
clearMessages();
|
||||
_accountBlockState.clear();
|
||||
}
|
||||
|
||||
Future<void> refreshAuthState() async {
|
||||
await _authState.refreshAuthState();
|
||||
}
|
||||
|
||||
Future<void> forceLogin() async {
|
||||
await _authState.forceLogin();
|
||||
// Check for messages after successful login
|
||||
Future<void> forceLogin({BuildContext? context}) async {
|
||||
try {
|
||||
await _authState.forceLogin();
|
||||
} catch (e) {
|
||||
await _recordLoginFailure(e);
|
||||
if (context == null || !context.mounted) return;
|
||||
_showLoginFailureUi(e, context);
|
||||
return;
|
||||
}
|
||||
// Check for messages and active blocks after successful login
|
||||
if (isLoggedIn) {
|
||||
checkMessages();
|
||||
checkActiveBlock();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> validateToken() async {
|
||||
return await _authState.validateToken();
|
||||
/// Record any state changes resulting from a login/forceLogin failure.
|
||||
/// Never touches the UI - safe to call before a BuildContext mounted check.
|
||||
Future<void> _recordLoginFailure(Object error) async {
|
||||
debugPrint('AppState: Login failed: $error');
|
||||
if (error is AccountBlockedException) {
|
||||
// We got a conclusive answer straight from the API during login, so
|
||||
// record it like any other successful check.
|
||||
await _accountBlockState.check(
|
||||
accessToken: await _authState.getAccessToken(),
|
||||
uploadMode: uploadMode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Show the appropriate UI for a login/forceLogin failure. Callers must
|
||||
/// check `context.mounted` immediately before calling this (no further
|
||||
/// awaits in between) since it's used synchronously here.
|
||||
void _showLoginFailureUi(Object error, BuildContext context) {
|
||||
if (error is AccountBlockedException) {
|
||||
_showActiveBlockDialog(context);
|
||||
return;
|
||||
}
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(LocalizationService.instance.t('auth.loginFailed')),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- Account Block Methods ----------
|
||||
|
||||
/// Silently check whether the user currently has an active OSM block.
|
||||
/// Returns the conclusive result (true/false), or null if the check was
|
||||
/// inconclusive (offline, error, not logged in, simulate mode).
|
||||
Future<bool?> checkActiveBlock() async {
|
||||
final accessToken = await _authState.getAccessToken();
|
||||
final result = await _accountBlockState.check(
|
||||
accessToken: accessToken,
|
||||
uploadMode: uploadMode,
|
||||
);
|
||||
// If the block was just lifted, resume queue processing.
|
||||
if (result == false && isQueuePausedDueToBlock) {
|
||||
_startUploader();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Check for an active block and, if found, show [ActiveBlockDialog].
|
||||
Future<void> checkAndPromptForActiveBlock(BuildContext context) async {
|
||||
final result = await checkActiveBlock();
|
||||
if (result == true) {
|
||||
if (!context.mounted) return;
|
||||
_showActiveBlockDialog(context);
|
||||
}
|
||||
}
|
||||
|
||||
void _showActiveBlockDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ActiveBlockDialog(
|
||||
onViewDetails: () async {
|
||||
final url = Uri.parse(getBlockDetailsUrl());
|
||||
await launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
},
|
||||
onDismiss: () {
|
||||
// Just dismiss - will show again on the next check that finds a block.
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Test the OSM connection using the same endpoint as the active-block
|
||||
/// check. A conclusive result (blocked or not) means the credentials are
|
||||
/// valid; null means the connection/credentials failed. Also surfaces the
|
||||
/// [ActiveBlockDialog] if the user turns out to be blocked.
|
||||
Future<bool> testConnection(BuildContext context) async {
|
||||
final result = await checkActiveBlock();
|
||||
if (result == true) {
|
||||
if (context.mounted) _showActiveBlockDialog(context);
|
||||
}
|
||||
return result != null;
|
||||
}
|
||||
|
||||
// ---------- Messages Methods ----------
|
||||
@@ -342,6 +456,18 @@ class AppState extends ChangeNotifier {
|
||||
String getMessagesUrl() {
|
||||
return _messagesState.getMessagesUrl(uploadMode);
|
||||
}
|
||||
|
||||
/// URL to view the current user's block details on OSM's website, or -
|
||||
/// if the username isn't known for some reason - the messages inbox,
|
||||
/// since OSM also sends a message when a block is issued.
|
||||
String getBlockDetailsUrl() {
|
||||
if (username.isEmpty) return getMessagesUrl();
|
||||
final host = switch (uploadMode) {
|
||||
UploadMode.sandbox => 'https://master.apis.dev.openstreetmap.org',
|
||||
UploadMode.production || UploadMode.simulate => 'https://www.openstreetmap.org',
|
||||
};
|
||||
return '$host/user/${Uri.encodeComponent(username)}/blocks';
|
||||
}
|
||||
|
||||
/// URL of the specific changeset with unread comments, on OSM's website,
|
||||
/// or null if there are no unread changeset comments.
|
||||
@@ -783,9 +909,12 @@ class AppState extends ChangeNotifier {
|
||||
|
||||
// Clear and re-check messages for new mode
|
||||
clearMessages();
|
||||
// Load the persisted block state for the new mode, then re-check it
|
||||
await _accountBlockState.init(mode);
|
||||
if (isLoggedIn) {
|
||||
// Don't await - let it run in background
|
||||
checkMessages();
|
||||
checkActiveBlock();
|
||||
|
||||
// Note: Re-auth check will be triggered from the settings screen after mode change
|
||||
}
|
||||
@@ -958,12 +1087,14 @@ class AppState extends ChangeNotifier {
|
||||
pauseQueueProcessing: pauseQueueProcessing,
|
||||
uploadMode: uploadMode,
|
||||
getAccessToken: _authState.getAccessToken,
|
||||
checkActiveBlock: checkActiveBlock,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_messageCheckTimer?.cancel();
|
||||
_accountBlockState.removeListener(_onStateChanged);
|
||||
_authState.removeListener(_onStateChanged);
|
||||
_messagesState.removeListener(_onStateChanged);
|
||||
_navigationState.removeListener(_onStateChanged);
|
||||
|
||||
@@ -49,6 +49,16 @@ double topPositionWithSafeArea(double baseTop, EdgeInsets safeArea) {
|
||||
return baseTop + safeArea.top;
|
||||
}
|
||||
|
||||
// Whether to proactively check blocks/active *before* attempting to fetch
|
||||
// the username during login. Current OSM behavior: user/details returns 403
|
||||
// for blocked accounts, so we check blocks/active first (it's documented as
|
||||
// accessible even while blocked) to detect this reliably instead of getting
|
||||
// an unexplained 403. Flip to false once OSM allows user/details to succeed
|
||||
// for blocked accounts too, so login can complete normally and rely on the
|
||||
// regular checkActiveBlock() triggers (launch/resume/queue/test connection)
|
||||
// to surface the block warning instead.
|
||||
const bool kCheckBlockBeforeUsernameFetch = true;
|
||||
|
||||
// Client name for OSM uploads ("created_by" tag)
|
||||
const String kClientName = 'DeFlock';
|
||||
// Note: Version is now dynamically retrieved from VersionService
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"edit": "Bearbeiten",
|
||||
"delete": "Löschen",
|
||||
"cancel": "Abbrechen",
|
||||
"dismiss": "Verwerfen",
|
||||
"ok": "OK",
|
||||
"close": "Schließen",
|
||||
"submit": "Senden",
|
||||
@@ -124,6 +125,7 @@
|
||||
"deleteReasonCustom": "Benutzerdefiniert..",
|
||||
"deleteReasonHint": "Bitte Grund beschreiben...",
|
||||
"mustBeLoggedInToDelete": "Sie müssen mit einem OpenStreetMap-Konto angemeldet sein, um Kartendaten beizutragen.",
|
||||
"accountBlockedCannotDelete": "Ihr OpenStreetMap-Konto ist derzeit gesperrt und kann keine Knoten löschen.",
|
||||
"uploadPending": "Ausstehender Upload",
|
||||
"uploadCreatingChangeset": "Changeset wird erstellt...",
|
||||
"uploadUploading": "Wird hochgeladen...",
|
||||
@@ -140,6 +142,7 @@
|
||||
"direction": "Richtung {}°",
|
||||
"profileNoDirectionInfo": "Dieses Profil benötigt keine Richtung.",
|
||||
"mustBeLoggedIn": "Sie müssen angemeldet sein, um neue Knoten zu übertragen. Bitte melden Sie sich über die Einstellungen an.",
|
||||
"accountBlocked": "Ihr OpenStreetMap-Konto ist derzeit gesperrt und kann keine neuen Knoten einreichen.",
|
||||
"enableSubmittableProfile": "Aktivieren Sie ein übertragbares Profil in den Einstellungen, um neue Knoten zu übertragen.",
|
||||
"profileViewOnlyWarning": "Dieses Profil ist nur zum Anzeigen der Karte gedacht. Bitte wählen Sie ein übertragbares Profil aus, um neue Knoten zu übertragen.",
|
||||
"loadingAreaData": "Lade Bereichsdaten... Bitte warten Sie vor dem Übertragen.",
|
||||
@@ -154,6 +157,7 @@
|
||||
"profileNoDirectionInfo": "Dieses Profil benötigt keine Richtung.",
|
||||
"temporarilyDisabled": "Bearbeitungen wurden vorübergehend deaktiviert, während wir einen Fehler beheben - Entschuldigung - schauen Sie bald wieder vorbei.",
|
||||
"mustBeLoggedIn": "Sie müssen angemeldet sein, um Knoten zu bearbeiten. Bitte melden Sie sich über die Einstellungen an.",
|
||||
"accountBlocked": "Ihr OpenStreetMap-Konto ist derzeit gesperrt und kann keine Knoten bearbeiten.",
|
||||
"sandboxModeWarning": "Bearbeitungen von Produktionsknoten können nicht an die Sandbox übertragen werden. Wechseln Sie in den Produktionsmodus in den Einstellungen, um Knoten zu bearbeiten.",
|
||||
"enableSubmittableProfile": "Aktivieren Sie ein übertragbares Profil in den Einstellungen, um Knoten zu bearbeiten.",
|
||||
"profileViewOnlyWarning": "Dieses Profil ist nur zum Anzeigen der Karte gedacht. Bitte wählen Sie ein übertragbares Profil aus, um Knoten zu bearbeiten.",
|
||||
@@ -210,6 +214,7 @@
|
||||
"tapToLogout": "Zum Abmelden antippen",
|
||||
"requiredToSubmit": "Erforderlich, um Kameradaten zu übertragen",
|
||||
"loggedOut": "Abgemeldet",
|
||||
"loginFailed": "Anmeldung fehlgeschlagen. Bitte versuchen Sie es erneut.",
|
||||
"testConnection": "Verbindung Testen",
|
||||
"testConnectionSubtitle": "OSM-Anmeldedaten überprüfen",
|
||||
"connectionOK": "Verbindung OK - Anmeldedaten sind gültig",
|
||||
@@ -241,7 +246,10 @@
|
||||
"reauthLater": "Später",
|
||||
"unreadNotificationsTitle": "Sie haben ungelesene Benachrichtigungen",
|
||||
"unreadNotificationsMessage": "Sie haben ungelesene OSM-Nachrichten und/oder neue Kommentare zu Ihren Changesets.",
|
||||
"unreadNotificationsView": "Jetzt ansehen"
|
||||
"unreadNotificationsView": "Jetzt ansehen",
|
||||
"activeBlockTitle": "Ihr OSM-Konto hat eine aktive Sperre",
|
||||
"activeBlockMessage": "Ihr OpenStreetMap-Konto ist derzeit gesperrt und kann keine Beiträge einreichen. Sie sollten eine Nachricht mit weiteren Details erhalten haben; bitte prüfen Sie Ihren OSM-Posteingang, um mehr zu erfahren.",
|
||||
"viewBlockDetails": "Sperrdetails ansehen"
|
||||
},
|
||||
"queue": {
|
||||
"title": "Upload-Warteschlange",
|
||||
@@ -277,6 +285,7 @@
|
||||
"processingPaused": "Warteschlangenverarbeitung pausiert",
|
||||
"pausedDueToOffline": "Upload-Verarbeitung ist pausiert, da der Offline-Modus aktiviert ist.",
|
||||
"pausedByUser": "Upload-Verarbeitung ist manuell pausiert.",
|
||||
"pausedDueToBlock": "Die Upload-Verarbeitung ist pausiert, da Ihr OSM-Konto derzeit gesperrt ist.",
|
||||
"inFlightWaitTitle": "Übermittlung wird abgeschlossen",
|
||||
"inFlightWaitMessage": "Eine Übermittlung läuft gerade und muss abgeschlossen werden, bevor diese Einstellung angewendet werden kann. Dies sollte nur einen Moment dauern.",
|
||||
"inFlightCompleteTitle": "Übermittlung Abgeschlossen",
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"cancel": "Cancel",
|
||||
"dismiss": "Dismiss",
|
||||
"ok": "OK",
|
||||
"close": "Close",
|
||||
"submit": "Submit",
|
||||
@@ -161,6 +162,7 @@
|
||||
"deleteReasonCustom": "Custom..",
|
||||
"deleteReasonHint": "Please describe the reason...",
|
||||
"mustBeLoggedInToDelete": "You must be logged in with an OpenStreetMap account to contribute map data.",
|
||||
"accountBlockedCannotDelete": "Your OpenStreetMap account has an active block and cannot delete nodes.",
|
||||
"uploadPending": "Pending upload",
|
||||
"uploadCreatingChangeset": "Creating changeset...",
|
||||
"uploadUploading": "Uploading...",
|
||||
@@ -177,6 +179,7 @@
|
||||
"direction": "Direction {}°",
|
||||
"profileNoDirectionInfo": "This profile does not require a direction.",
|
||||
"mustBeLoggedIn": "You must be logged in to submit new nodes. Please log in via Settings.",
|
||||
"accountBlocked": "Your OpenStreetMap account has an active block and cannot submit new nodes.",
|
||||
"enableSubmittableProfile": "Enable a submittable profile in Settings to submit new nodes.",
|
||||
"profileViewOnlyWarning": "This profile is for map viewing only. Please select a submittable profile to submit new nodes.",
|
||||
"loadingAreaData": "Loading area data... Please wait before submitting.",
|
||||
@@ -191,6 +194,7 @@
|
||||
"profileNoDirectionInfo": "This profile does not require a direction.",
|
||||
"temporarilyDisabled": "Edits have been temporarily disabled while we sort out a bug - apologies - check back soon.",
|
||||
"mustBeLoggedIn": "You must be logged in to edit nodes. Please log in via Settings.",
|
||||
"accountBlocked": "Your OpenStreetMap account has an active block and cannot edit nodes.",
|
||||
"sandboxModeWarning": "Cannot submit edits on production nodes to sandbox. Switch to Production mode in Settings to edit nodes.",
|
||||
"enableSubmittableProfile": "Enable a submittable profile in Settings to edit nodes.",
|
||||
"profileViewOnlyWarning": "This profile is for map viewing only. Please select a submittable profile to edit nodes.",
|
||||
@@ -247,6 +251,7 @@
|
||||
"tapToLogout": "Tap to logout",
|
||||
"requiredToSubmit": "Required to submit camera data",
|
||||
"loggedOut": "Logged out",
|
||||
"loginFailed": "Login failed. Please try again.",
|
||||
"testConnection": "Test Connection",
|
||||
"testConnectionSubtitle": "Verify OSM credentials are working",
|
||||
"connectionOK": "Connection OK - credentials are valid",
|
||||
@@ -278,7 +283,10 @@
|
||||
"reauthLater": "Later",
|
||||
"unreadNotificationsTitle": "You Have Unread Notifications",
|
||||
"unreadNotificationsMessage": "You have unread OSM messages and/or new comments on your changesets.",
|
||||
"unreadNotificationsView": "View Now"
|
||||
"unreadNotificationsView": "View Now",
|
||||
"activeBlockTitle": "Your OSM Account Has an Active Block",
|
||||
"activeBlockMessage": "Your OpenStreetMap account currently has an active block and is restricted from making submissions. You should have received a message with more details; please check your OSM inbox to learn more.",
|
||||
"viewBlockDetails": "View Block Details"
|
||||
},
|
||||
"queue": {
|
||||
"title": "Upload Queue",
|
||||
@@ -314,6 +322,7 @@
|
||||
"processingPaused": "Queue Processing Paused",
|
||||
"pausedDueToOffline": "Upload processing is paused because offline mode is enabled.",
|
||||
"pausedByUser": "Upload processing is manually paused.",
|
||||
"pausedDueToBlock": "Upload processing is paused because your OSM account has an active block.",
|
||||
"inFlightWaitTitle": "Finishing Submission",
|
||||
"inFlightWaitMessage": "A submission is currently in progress and needs to finish before this setting can be applied. This should only take a moment.",
|
||||
"inFlightCompleteTitle": "Submission Complete",
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"edit": "Editar",
|
||||
"delete": "Eliminar",
|
||||
"cancel": "Cancelar",
|
||||
"dismiss": "Descartar",
|
||||
"ok": "Aceptar",
|
||||
"close": "Cerrar",
|
||||
"submit": "Enviar",
|
||||
@@ -161,6 +162,7 @@
|
||||
"deleteReasonCustom": "Personalizado..",
|
||||
"deleteReasonHint": "Por favor describa la razón...",
|
||||
"mustBeLoggedInToDelete": "Debes iniciar sesión con una cuenta de OpenStreetMap para contribuir con datos del mapa.",
|
||||
"accountBlockedCannotDelete": "Su cuenta de OpenStreetMap tiene un bloqueo activo y no puede eliminar nodos.",
|
||||
"uploadPending": "Subida pendiente",
|
||||
"uploadCreatingChangeset": "Creando changeset...",
|
||||
"uploadUploading": "Subiendo...",
|
||||
@@ -177,6 +179,7 @@
|
||||
"direction": "Dirección {}°",
|
||||
"profileNoDirectionInfo": "Este perfil no requiere una dirección.",
|
||||
"mustBeLoggedIn": "Debe estar conectado para enviar nuevos nodos. Por favor, inicie sesión a través de Configuración.",
|
||||
"accountBlocked": "Su cuenta de OpenStreetMap tiene un bloqueo activo y no puede enviar nodos nuevos.",
|
||||
"enableSubmittableProfile": "Habilite un perfil envíable en Configuración para enviar nuevos nodos.",
|
||||
"profileViewOnlyWarning": "Este perfil es solo para visualización del mapa. Por favor, seleccione un perfil envíable para enviar nuevos nodos.",
|
||||
"loadingAreaData": "Cargando datos del área... Por favor espere antes de enviar.",
|
||||
@@ -191,6 +194,7 @@
|
||||
"profileNoDirectionInfo": "Este perfil no requiere una dirección.",
|
||||
"temporarilyDisabled": "Las ediciones han sido temporalmente deshabilitadas mientras solucionamos un error - disculpas - regrese pronto.",
|
||||
"mustBeLoggedIn": "Debe estar conectado para editar nodos. Por favor, inicie sesión a través de Configuración.",
|
||||
"accountBlocked": "Su cuenta de OpenStreetMap tiene un bloqueo activo y no puede editar nodos.",
|
||||
"sandboxModeWarning": "No se pueden enviar ediciones de nodos de producción al sandbox. Cambie al modo Producción en Configuración para editar nodos.",
|
||||
"enableSubmittableProfile": "Habilite un perfil envíable en Configuración para editar nodos.",
|
||||
"profileViewOnlyWarning": "Este perfil es solo para visualización del mapa. Por favor, seleccione un perfil envíable para editar nodos.",
|
||||
@@ -247,6 +251,7 @@
|
||||
"tapToLogout": "Toque para cerrar sesión",
|
||||
"requiredToSubmit": "Requerido para enviar datos de cámaras",
|
||||
"loggedOut": "Sesión cerrada",
|
||||
"loginFailed": "Error al iniciar sesión. Inténtelo de nuevo.",
|
||||
"testConnection": "Probar Conexión",
|
||||
"testConnectionSubtitle": "Verificar que las credenciales de OSM funcionen",
|
||||
"connectionOK": "Conexión OK - las credenciales son válidas",
|
||||
@@ -278,7 +283,10 @@
|
||||
"reauthLater": "Más Tarde",
|
||||
"unreadNotificationsTitle": "Tienes Notificaciones Sin Leer",
|
||||
"unreadNotificationsMessage": "Tienes mensajes de OSM sin leer y/o nuevos comentarios en tus changesets.",
|
||||
"unreadNotificationsView": "Ver Ahora"
|
||||
"unreadNotificationsView": "Ver Ahora",
|
||||
"activeBlockTitle": "Su cuenta de OSM tiene un bloqueo activo",
|
||||
"activeBlockMessage": "Su cuenta de OpenStreetMap tiene actualmente un bloqueo activo y no puede realizar envíos. Debería haber recibido un mensaje con más detalles; revise su bandeja de entrada de OSM para obtener más información.",
|
||||
"viewBlockDetails": "Ver Detalles del Bloqueo"
|
||||
},
|
||||
"queue": {
|
||||
"title": "Cola de Subida",
|
||||
@@ -314,6 +322,7 @@
|
||||
"processingPaused": "Procesamiento de Cola Pausado",
|
||||
"pausedDueToOffline": "El procesamiento de subida está pausado porque el modo sin conexión está habilitado.",
|
||||
"pausedByUser": "El procesamiento de subida está pausado manualmente.",
|
||||
"pausedDueToBlock": "El procesamiento de subidas está pausado porque su cuenta de OSM tiene un bloqueo activo.",
|
||||
"inFlightWaitTitle": "Finalizando Envío",
|
||||
"inFlightWaitMessage": "Actualmente hay un envío en curso que debe finalizar antes de que se pueda aplicar esta configuración. Esto solo debería tomar un momento.",
|
||||
"inFlightCompleteTitle": "Envío Completado",
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"edit": "Modifier",
|
||||
"delete": "Supprimer",
|
||||
"cancel": "Annuler",
|
||||
"dismiss": "Ignorer",
|
||||
"ok": "OK",
|
||||
"close": "Fermer",
|
||||
"submit": "Soumettre",
|
||||
@@ -161,6 +162,7 @@
|
||||
"deleteReasonCustom": "Personnalisé..",
|
||||
"deleteReasonHint": "Veuillez décrire la raison...",
|
||||
"mustBeLoggedInToDelete": "Vous devez être connecté avec un compte OpenStreetMap pour contribuer aux données cartographiques.",
|
||||
"accountBlockedCannotDelete": "Votre compte OpenStreetMap a un blocage actif et ne peut pas supprimer de nœuds.",
|
||||
"uploadPending": "Téléchargement en attente",
|
||||
"uploadCreatingChangeset": "Création du changeset...",
|
||||
"uploadUploading": "Téléchargement en cours...",
|
||||
@@ -177,6 +179,7 @@
|
||||
"direction": "Direction {}°",
|
||||
"profileNoDirectionInfo": "Ce profil ne nécessite pas de direction.",
|
||||
"mustBeLoggedIn": "Vous devez être connecté pour soumettre de nouveaux nœuds. Veuillez vous connecter via les Paramètres.",
|
||||
"accountBlocked": "Votre compte OpenStreetMap a un blocage actif et ne peut pas soumettre de nouveaux nœuds.",
|
||||
"enableSubmittableProfile": "Activez un profil soumissible dans les Paramètres pour soumettre de nouveaux nœuds.",
|
||||
"profileViewOnlyWarning": "Ce profil est uniquement pour la visualisation de la carte. Veuillez sélectionner un profil soumissible pour soumettre de nouveaux nœuds.",
|
||||
"loadingAreaData": "Chargement des données de zone... Veuillez patienter avant de soumettre.",
|
||||
@@ -191,6 +194,7 @@
|
||||
"profileNoDirectionInfo": "Ce profil ne nécessite pas de direction.",
|
||||
"temporarilyDisabled": "Les modifications ont été temporairement désactivées pendant que nous résolvons un bug - désolés - revenez bientôt.",
|
||||
"mustBeLoggedIn": "Vous devez être connecté pour modifier les nœuds. Veuillez vous connecter via les Paramètres.",
|
||||
"accountBlocked": "Votre compte OpenStreetMap a un blocage actif et ne peut pas modifier de nœuds.",
|
||||
"sandboxModeWarning": "Impossible de soumettre des modifications de nœuds de production au sandbox. Passez au mode Production dans les Paramètres pour modifier les nœuds.",
|
||||
"enableSubmittableProfile": "Activez un profil soumissible dans les Paramètres pour modifier les nœuds.",
|
||||
"profileViewOnlyWarning": "Ce profil est uniquement pour la visualisation de la carte. Veuillez sélectionner un profil soumissible pour modifier les nœuds.",
|
||||
@@ -247,6 +251,7 @@
|
||||
"tapToLogout": "Appuyer pour se déconnecter",
|
||||
"requiredToSubmit": "Requis pour soumettre des données de caméras",
|
||||
"loggedOut": "Déconnecté",
|
||||
"loginFailed": "Échec de la connexion. Veuillez réessayer.",
|
||||
"testConnection": "Tester Connexion",
|
||||
"testConnectionSubtitle": "Vérifier que les identifiants OSM fonctionnent",
|
||||
"connectionOK": "Connexion OK - les identifiants sont valides",
|
||||
@@ -278,7 +283,10 @@
|
||||
"reauthLater": "Plus Tard",
|
||||
"unreadNotificationsTitle": "Vous Avez des Notifications Non Lues",
|
||||
"unreadNotificationsMessage": "Vous avez des messages OSM non lus et/ou de nouveaux commentaires sur vos changesets.",
|
||||
"unreadNotificationsView": "Voir Maintenant"
|
||||
"unreadNotificationsView": "Voir Maintenant",
|
||||
"activeBlockTitle": "Votre compte OSM a un blocage actif",
|
||||
"activeBlockMessage": "Votre compte OpenStreetMap a actuellement un blocage actif et ne peut pas effectuer de soumissions. Vous devriez avoir reçu un message avec plus de détails ; veuillez consulter votre boîte de réception OSM pour en savoir plus.",
|
||||
"viewBlockDetails": "Voir les Détails du Blocage"
|
||||
},
|
||||
"queue": {
|
||||
"title": "File de Téléchargement",
|
||||
@@ -314,6 +322,7 @@
|
||||
"processingPaused": "Traitement de la File d'Attente Interrompu",
|
||||
"pausedDueToOffline": "Le traitement des téléversements est interrompu car le mode hors ligne est activé.",
|
||||
"pausedByUser": "Le traitement des téléversements est interrompu manuellement.",
|
||||
"pausedDueToBlock": "Le traitement des envois est en pause car votre compte OSM a un blocage actif.",
|
||||
"inFlightWaitTitle": "Finalisation de la Soumission",
|
||||
"inFlightWaitMessage": "Une soumission est actuellement en cours et doit se terminer avant que ce paramètre puisse être appliqué. Cela ne devrait prendre qu'un instant.",
|
||||
"inFlightCompleteTitle": "Soumission Terminée",
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"edit": "Modifica",
|
||||
"delete": "Elimina",
|
||||
"cancel": "Annulla",
|
||||
"dismiss": "Ignora",
|
||||
"ok": "OK",
|
||||
"close": "Chiudi",
|
||||
"submit": "Invia",
|
||||
@@ -161,6 +162,7 @@
|
||||
"deleteReasonCustom": "Personalizzato..",
|
||||
"deleteReasonHint": "Descrivi il motivo...",
|
||||
"mustBeLoggedInToDelete": "Devi aver effettuato l'accesso con un account OpenStreetMap per contribuire ai dati della mappa.",
|
||||
"accountBlockedCannotDelete": "Il tuo account OpenStreetMap ha un blocco attivo e non può eliminare i nodi.",
|
||||
"uploadPending": "Caricamento in sospeso",
|
||||
"uploadCreatingChangeset": "Creazione changeset...",
|
||||
"uploadUploading": "Caricamento...",
|
||||
@@ -177,6 +179,7 @@
|
||||
"direction": "Direzione {}°",
|
||||
"profileNoDirectionInfo": "Questo profilo non richiede una direzione.",
|
||||
"mustBeLoggedIn": "Devi essere loggato per inviare nuovi nodi. Per favore accedi tramite Impostazioni.",
|
||||
"accountBlocked": "Il tuo account OpenStreetMap ha un blocco attivo e non può inviare nuovi nodi.",
|
||||
"enableSubmittableProfile": "Abilita un profilo inviabile nelle Impostazioni per inviare nuovi nodi.",
|
||||
"profileViewOnlyWarning": "Questo profilo è solo per la visualizzazione della mappa. Per favore seleziona un profilo inviabile per inviare nuovi nodi.",
|
||||
"loadingAreaData": "Caricamento dati area... Per favore attendi prima di inviare.",
|
||||
@@ -191,6 +194,7 @@
|
||||
"profileNoDirectionInfo": "Questo profilo non richiede una direzione.",
|
||||
"temporarilyDisabled": "Le modifiche sono state temporaneamente disabilitate mentre risolviamo un bug - scuse - torna presto.",
|
||||
"mustBeLoggedIn": "Devi essere loggato per modificare i nodi. Per favore accedi tramite Impostazioni.",
|
||||
"accountBlocked": "Il tuo account OpenStreetMap ha un blocco attivo e non può modificare i nodi.",
|
||||
"sandboxModeWarning": "Impossibile inviare modifiche di nodi di produzione alla sandbox. Passa alla modalità Produzione nelle Impostazioni per modificare i nodi.",
|
||||
"enableSubmittableProfile": "Abilita un profilo inviabile nelle Impostazioni per modificare i nodi.",
|
||||
"profileViewOnlyWarning": "Questo profilo è solo per la visualizzazione della mappa. Per favore seleziona un profilo inviabile per modificare i nodi.",
|
||||
@@ -247,6 +251,7 @@
|
||||
"tapToLogout": "Tocca per disconnetterti",
|
||||
"requiredToSubmit": "Richiesto per inviare dati delle telecamere",
|
||||
"loggedOut": "Disconnesso",
|
||||
"loginFailed": "Accesso non riuscito. Riprova.",
|
||||
"testConnection": "Testa Connessione",
|
||||
"testConnectionSubtitle": "Verifica che le credenziali OSM funzionino",
|
||||
"connectionOK": "Connessione OK - le credenziali sono valide",
|
||||
@@ -278,7 +283,10 @@
|
||||
"reauthLater": "Più Tardi",
|
||||
"unreadNotificationsTitle": "Hai Notifiche Non Lette",
|
||||
"unreadNotificationsMessage": "Hai messaggi OSM non letti e/o nuovi commenti sui tuoi changeset.",
|
||||
"unreadNotificationsView": "Visualizza Ora"
|
||||
"unreadNotificationsView": "Visualizza Ora",
|
||||
"activeBlockTitle": "Il tuo account OSM ha un blocco attivo",
|
||||
"activeBlockMessage": "Il tuo account OpenStreetMap ha attualmente un blocco attivo e non può effettuare invii. Dovresti aver ricevuto un messaggio con maggiori dettagli; controlla la tua casella di posta OSM per saperne di più.",
|
||||
"viewBlockDetails": "Visualizza Dettagli Blocco"
|
||||
},
|
||||
"queue": {
|
||||
"title": "Coda di Upload",
|
||||
@@ -314,6 +322,7 @@
|
||||
"processingPaused": "Elaborazione Coda Sospesa",
|
||||
"pausedDueToOffline": "L'elaborazione dei caricamenti è sospesa perché la modalità offline è abilitata.",
|
||||
"pausedByUser": "L'elaborazione dei caricamenti è sospesa manualmente.",
|
||||
"pausedDueToBlock": "L'elaborazione della coda di caricamento è in pausa perché il tuo account OSM ha un blocco attivo.",
|
||||
"inFlightWaitTitle": "Completamento Invio",
|
||||
"inFlightWaitMessage": "Un invio è attualmente in corso e deve terminare prima che questa impostazione possa essere applicata. Questo dovrebbe richiedere solo un momento.",
|
||||
"inFlightCompleteTitle": "Invio Completato",
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"edit": "Bewerken",
|
||||
"delete": "Verwijderen",
|
||||
"cancel": "Annuleren",
|
||||
"dismiss": "Negeren",
|
||||
"ok": "OK",
|
||||
"close": "Sluiten",
|
||||
"submit": "Indienen",
|
||||
@@ -161,6 +162,7 @@
|
||||
"deleteReasonCustom": "Aangepast..",
|
||||
"deleteReasonHint": "Beschrijf de reden...",
|
||||
"mustBeLoggedInToDelete": "U moet ingelogd zijn met een OpenStreetMap-account om kaartgegevens bij te dragen.",
|
||||
"accountBlockedCannotDelete": "Uw OpenStreetMap-account heeft een actieve blokkade en kan geen knooppunten verwijderen.",
|
||||
"uploadPending": "Upload in behandeling",
|
||||
"uploadCreatingChangeset": "Changeset maken...",
|
||||
"uploadUploading": "Uploaden...",
|
||||
@@ -177,6 +179,7 @@
|
||||
"direction": "Richting {}°",
|
||||
"profileNoDirectionInfo": "Dit profiel vereist geen richting.",
|
||||
"mustBeLoggedIn": "U moet ingelogd zijn om nieuwe nodes in te dienen. Log in via Instellingen.",
|
||||
"accountBlocked": "Uw OpenStreetMap-account heeft een actieve blokkade en kan geen nieuwe knooppunten indienen.",
|
||||
"enableSubmittableProfile": "Schakel een indienbaar profiel in via Instellingen om nieuwe nodes in te dienen.",
|
||||
"profileViewOnlyWarning": "Dit profiel is alleen voor kaart weergave. Selecteer een indienbaar profiel om nieuwe nodes in te dienen.",
|
||||
"loadingAreaData": "Gebied gegevens laden... Wacht even voordat u indient.",
|
||||
@@ -191,6 +194,7 @@
|
||||
"profileNoDirectionInfo": "Dit profiel vereist geen richting.",
|
||||
"temporarilyDisabled": "Bewerkingen zijn tijdelijk uitgeschakeld terwijl we een bug oplossen - excuses - controleer binnenkort opnieuw.",
|
||||
"mustBeLoggedIn": "U moet ingelogd zijn om nodes te bewerken. Log in via Instellingen.",
|
||||
"accountBlocked": "Uw OpenStreetMap-account heeft een actieve blokkade en kan geen knooppunten bewerken.",
|
||||
"sandboxModeWarning": "Kan geen bewerkingen op productie nodes naar sandbox versturen. Schakel naar Productie modus in Instellingen om nodes te bewerken.",
|
||||
"enableSubmittableProfile": "Schakel een indienbaar profiel in via Instellingen om nodes te bewerken.",
|
||||
"profileViewOnlyWarning": "Dit profiel is alleen voor kaart weergave. Selecteer een indienbaar profiel om nodes te bewerken.",
|
||||
@@ -247,6 +251,7 @@
|
||||
"tapToLogout": "Tik om uit te loggen",
|
||||
"requiredToSubmit": "Vereist om camera gegevens in te dienen",
|
||||
"loggedOut": "Uitgelogd",
|
||||
"loginFailed": "Inloggen mislukt. Probeer het opnieuw.",
|
||||
"testConnection": "Test Verbinding",
|
||||
"testConnectionSubtitle": "Verifieer dat OSM credentials werken",
|
||||
"connectionOK": "Verbinding OK - credentials zijn geldig",
|
||||
@@ -278,7 +283,10 @@
|
||||
"reauthLater": "Later",
|
||||
"unreadNotificationsTitle": "U Heeft Ongelezen Meldingen",
|
||||
"unreadNotificationsMessage": "U heeft ongelezen OSM-berichten en/of nieuwe opmerkingen op uw changesets.",
|
||||
"unreadNotificationsView": "Nu Bekijken"
|
||||
"unreadNotificationsView": "Nu Bekijken",
|
||||
"activeBlockTitle": "Uw OSM-account heeft een actieve blokkade",
|
||||
"activeBlockMessage": "Uw OpenStreetMap-account heeft momenteel een actieve blokkade en kan geen inzendingen doen. U zou een bericht met meer details moeten hebben ontvangen; controleer uw OSM-inbox voor meer informatie.",
|
||||
"viewBlockDetails": "Blokkadedetails Bekijken"
|
||||
},
|
||||
"queue": {
|
||||
"title": "Upload Wachtrij",
|
||||
@@ -314,6 +322,7 @@
|
||||
"processingPaused": "Wachtrij Verwerking Gepauzeerd",
|
||||
"pausedDueToOffline": "Upload verwerking is gepauzeerd omdat offline modus is ingeschakeld.",
|
||||
"pausedByUser": "Upload verwerking is handmatig gepauzeerd.",
|
||||
"pausedDueToBlock": "Het uploadproces is gepauzeerd omdat uw OSM-account een actieve blokkade heeft.",
|
||||
"inFlightWaitTitle": "Indiening Wordt Afgerond",
|
||||
"inFlightWaitMessage": "Er is momenteel een indiening bezig die moet worden afgerond voordat deze instelling kan worden toegepast. Dit zou maar een moment moeten duren.",
|
||||
"inFlightCompleteTitle": "Indiening Voltooid",
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"edit": "Edytuj",
|
||||
"delete": "Usuń",
|
||||
"cancel": "Anuluj",
|
||||
"dismiss": "Odrzuć",
|
||||
"ok": "OK",
|
||||
"close": "Zamknij",
|
||||
"submit": "Zgłoś",
|
||||
@@ -161,6 +162,7 @@
|
||||
"deleteReasonCustom": "Niestandardowy..",
|
||||
"deleteReasonHint": "Opisz powód...",
|
||||
"mustBeLoggedInToDelete": "Musisz być zalogowany na konto OpenStreetMap, aby dodawać dane do mapy.",
|
||||
"accountBlockedCannotDelete": "Twoje konto OpenStreetMap ma aktywną blokadę i nie może usuwać węzłów.",
|
||||
"uploadPending": "Przesyłanie oczekujące",
|
||||
"uploadCreatingChangeset": "Tworzenie zestawu zmian...",
|
||||
"uploadUploading": "Przesyłanie...",
|
||||
@@ -177,6 +179,7 @@
|
||||
"direction": "Kierunek {}°",
|
||||
"profileNoDirectionInfo": "Ten profil nie wymaga kierunku.",
|
||||
"mustBeLoggedIn": "Musisz być zalogowany, aby zgłaszać nowe węzły. Zaloguj się przez Ustawienia.",
|
||||
"accountBlocked": "Twoje konto OpenStreetMap ma aktywną blokadę i nie może zgłaszać nowych węzłów.",
|
||||
"enableSubmittableProfile": "Włącz profil możliwy do zgłoszenia w Ustawieniach, aby zgłaszać nowe węzły.",
|
||||
"profileViewOnlyWarning": "Ten profil służy tylko do przeglądania mapy. Proszę wybrać profil możliwy do zgłoszenia, aby zgłaszać nowe węzły.",
|
||||
"loadingAreaData": "Ładowanie danych obszaru... Poczekaj przed zgłoszeniem.",
|
||||
@@ -191,6 +194,7 @@
|
||||
"profileNoDirectionInfo": "Ten profil nie wymaga kierunku.",
|
||||
"temporarilyDisabled": "Edycje zostały tymczasowo wyłączone, gdy rozwiązujemy błąd - przepraszamy - sprawdź wkrótce.",
|
||||
"mustBeLoggedIn": "Musisz być zalogowany, aby edytować węzły. Zaloguj się przez Ustawienia.",
|
||||
"accountBlocked": "Twoje konto OpenStreetMap ma aktywną blokadę i nie może edytować węzłów.",
|
||||
"sandboxModeWarning": "Nie można przesyłać edycji węzłów produkcyjnych do piaskownicy. Przełącz na tryb Produkcyjny w Ustawieniach, aby edytować węzły.",
|
||||
"enableSubmittableProfile": "Włącz profil możliwy do zgłoszenia w Ustawieniach, aby edytować węzły.",
|
||||
"profileViewOnlyWarning": "Ten profil służy tylko do przeglądania mapy. Proszę wybrać profil możliwy do zgłoszenia, aby edytować węzły.",
|
||||
@@ -247,6 +251,7 @@
|
||||
"tapToLogout": "Stuknij aby się wylogować",
|
||||
"requiredToSubmit": "Wymagane do zgłaszania danych kamer",
|
||||
"loggedOut": "Wylogowany",
|
||||
"loginFailed": "Logowanie nie powiodło się. Spróbuj ponownie.",
|
||||
"testConnection": "Testuj Połączenie",
|
||||
"testConnectionSubtitle": "Sprawdź czy dane logowania OSM działają",
|
||||
"connectionOK": "Połączenie OK - dane logowania są ważne",
|
||||
@@ -278,7 +283,10 @@
|
||||
"reauthLater": "Później",
|
||||
"unreadNotificationsTitle": "Masz Nieprzeczytane Powiadomienia",
|
||||
"unreadNotificationsMessage": "Masz nieprzeczytane wiadomości OSM i/lub nowe komentarze do Twoich zestawów zmian.",
|
||||
"unreadNotificationsView": "Zobacz Teraz"
|
||||
"unreadNotificationsView": "Zobacz Teraz",
|
||||
"activeBlockTitle": "Twoje konto OSM ma aktywną blokadę",
|
||||
"activeBlockMessage": "Twoje konto OpenStreetMap ma obecnie aktywną blokadę i nie może dokonywać zgłoszeń. Powinieneś był otrzymać wiadomość z dalszymi szczegółami; sprawdź swoją skrzynkę odbiorczą OSM, aby dowiedzieć się więcej.",
|
||||
"viewBlockDetails": "Zobacz Szczegóły Blokady"
|
||||
},
|
||||
"queue": {
|
||||
"title": "Kolejka Przesyłania",
|
||||
@@ -314,6 +322,7 @@
|
||||
"processingPaused": "Przetwarzanie Kolejki Wstrzymane",
|
||||
"pausedDueToOffline": "Przetwarzanie przesyłania jest wstrzymane, ponieważ tryb offline jest włączony.",
|
||||
"pausedByUser": "Przetwarzanie przesyłania jest ręcznie wstrzymane.",
|
||||
"pausedDueToBlock": "Przetwarzanie kolejki przesyłania jest wstrzymane, ponieważ Twoje konto OSM ma aktywną blokadę.",
|
||||
"inFlightWaitTitle": "Kończenie Przesyłania",
|
||||
"inFlightWaitMessage": "Przesyłanie jest obecnie w toku i musi zostać zakończone, zanim to ustawienie będzie mogło zostać zastosowane. To powinno zająć tylko chwilę.",
|
||||
"inFlightCompleteTitle": "Przesyłanie Zakończone",
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"edit": "Editar",
|
||||
"delete": "Excluir",
|
||||
"cancel": "Cancelar",
|
||||
"dismiss": "Dispensar",
|
||||
"ok": "OK",
|
||||
"close": "Fechar",
|
||||
"submit": "Enviar",
|
||||
@@ -161,6 +162,7 @@
|
||||
"deleteReasonCustom": "Personalizado..",
|
||||
"deleteReasonHint": "Descreva o motivo...",
|
||||
"mustBeLoggedInToDelete": "Você precisa estar conectado com uma conta do OpenStreetMap para contribuir com dados do mapa.",
|
||||
"accountBlockedCannotDelete": "Sua conta OpenStreetMap tem um bloqueio ativo e não pode excluir nós.",
|
||||
"uploadPending": "Envio pendente",
|
||||
"uploadCreatingChangeset": "Criando changeset...",
|
||||
"uploadUploading": "Enviando...",
|
||||
@@ -177,6 +179,7 @@
|
||||
"direction": "Direção {}°",
|
||||
"profileNoDirectionInfo": "Este perfil não requer uma direção.",
|
||||
"mustBeLoggedIn": "Você deve estar logado para enviar novos nós. Por favor, faça login via Configurações.",
|
||||
"accountBlocked": "Sua conta OpenStreetMap tem um bloqueio ativo e não pode enviar novos nós.",
|
||||
"enableSubmittableProfile": "Ative um perfil enviável nas Configurações para enviar novos nós.",
|
||||
"profileViewOnlyWarning": "Este perfil é apenas para visualização do mapa. Por favor, selecione um perfil enviável para enviar novos nós.",
|
||||
"loadingAreaData": "Carregando dados da área... Por favor aguarde antes de enviar.",
|
||||
@@ -191,6 +194,7 @@
|
||||
"profileNoDirectionInfo": "Este perfil não requer uma direção.",
|
||||
"temporarilyDisabled": "As edições foram temporariamente desabilitadas enquanto resolvemos um bug - desculpe - volte em breve.",
|
||||
"mustBeLoggedIn": "Você deve estar logado para editar nós. Por favor, faça login via Configurações.",
|
||||
"accountBlocked": "Sua conta OpenStreetMap tem um bloqueio ativo e não pode editar nós.",
|
||||
"sandboxModeWarning": "Não é possível enviar edições de nós de produção para o sandbox. Mude para o modo Produção nas Configurações para editar nós.",
|
||||
"enableSubmittableProfile": "Ative um perfil enviável nas Configurações para editar nós.",
|
||||
"profileViewOnlyWarning": "Este perfil é apenas para visualização do mapa. Por favor, selecione um perfil enviável para editar nós.",
|
||||
@@ -247,6 +251,7 @@
|
||||
"tapToLogout": "Toque para sair",
|
||||
"requiredToSubmit": "Necessário para enviar dados de câmeras",
|
||||
"loggedOut": "Deslogado",
|
||||
"loginFailed": "Falha no login. Tente novamente.",
|
||||
"testConnection": "Testar Conexão",
|
||||
"testConnectionSubtitle": "Verificar se as credenciais OSM estão funcionando",
|
||||
"connectionOK": "Conexão OK - credenciais são válidas",
|
||||
@@ -278,7 +283,10 @@
|
||||
"reauthLater": "Mais Tarde",
|
||||
"unreadNotificationsTitle": "Você Tem Notificações Não Lidas",
|
||||
"unreadNotificationsMessage": "Você tem mensagens OSM não lidas e/ou novos comentários nos seus changesets.",
|
||||
"unreadNotificationsView": "Ver Agora"
|
||||
"unreadNotificationsView": "Ver Agora",
|
||||
"activeBlockTitle": "Sua conta OSM tem um bloqueio ativo",
|
||||
"activeBlockMessage": "Sua conta OpenStreetMap atualmente tem um bloqueio ativo e está impedida de fazer envios. Você deve ter recebido uma mensagem com mais detalhes; verifique sua caixa de entrada do OSM para saber mais.",
|
||||
"viewBlockDetails": "Ver Detalhes do Bloqueio"
|
||||
},
|
||||
"queue": {
|
||||
"title": "Fila de Upload",
|
||||
@@ -314,6 +322,7 @@
|
||||
"processingPaused": "Processamento da Fila Pausado",
|
||||
"pausedDueToOffline": "O processamento de upload está pausado porque o modo offline está habilitado.",
|
||||
"pausedByUser": "O processamento de upload está pausado manualmente.",
|
||||
"pausedDueToBlock": "O processamento da fila de upload está pausado porque sua conta OSM tem um bloqueio ativo.",
|
||||
"inFlightWaitTitle": "Finalizando Envio",
|
||||
"inFlightWaitMessage": "Atualmente há um envio em andamento que precisa terminar antes que esta configuração possa ser aplicada. Isso deve levar apenas um momento.",
|
||||
"inFlightCompleteTitle": "Envio Concluído",
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"edit": "Düzenle",
|
||||
"delete": "Sil",
|
||||
"cancel": "İptal",
|
||||
"dismiss": "Kapat",
|
||||
"ok": "Tamam",
|
||||
"close": "Kapat",
|
||||
"submit": "Gönder",
|
||||
@@ -161,6 +162,7 @@
|
||||
"deleteReasonCustom": "Özel..",
|
||||
"deleteReasonHint": "Lütfen nedeni açıklayın...",
|
||||
"mustBeLoggedInToDelete": "Harita verilerine katkıda bulunmak için bir OpenStreetMap hesabıyla oturum açmış olmanız gerekir.",
|
||||
"accountBlockedCannotDelete": "OpenStreetMap hesabınızda aktif bir engelleme var ve düğüm silemez.",
|
||||
"uploadPending": "Yükleme bekliyor",
|
||||
"uploadCreatingChangeset": "Değişiklik seti oluşturuluyor...",
|
||||
"uploadUploading": "Yükleniyor...",
|
||||
@@ -177,6 +179,7 @@
|
||||
"direction": "Yön {}°",
|
||||
"profileNoDirectionInfo": "Bu profil bir yön gerektirmez.",
|
||||
"mustBeLoggedIn": "Yeni düğümler göndermek için giriş yapmalısınız. Lütfen Ayarlar üzerinden giriş yapın.",
|
||||
"accountBlocked": "OpenStreetMap hesabınızda aktif bir engelleme var ve yeni düğüm gönderemez.",
|
||||
"enableSubmittableProfile": "Yeni düğümler göndermek için Ayarlarda gönderilebilir bir profili etkinleştirin.",
|
||||
"profileViewOnlyWarning": "Bu profil sadece harita görüntüleme içindir. Yeni düğümler göndermek için lütfen gönderilebilir bir profil seçin.",
|
||||
"loadingAreaData": "Alan verisi yükleniyor... Göndermeden önce lütfen bekleyin.",
|
||||
@@ -191,6 +194,7 @@
|
||||
"profileNoDirectionInfo": "Bu profil bir yön gerektirmez.",
|
||||
"temporarilyDisabled": "Bir hatayı çözmemiz sırasında düzenlemeler geçici olarak devre dışı bırakıldı - özür dileriz - yakında tekrar kontrol edin.",
|
||||
"mustBeLoggedIn": "Düğümleri düzenlemek için giriş yapmalısınız. Lütfen Ayarlar üzerinden giriş yapın.",
|
||||
"accountBlocked": "OpenStreetMap hesabınızda aktif bir engelleme var ve düğüm düzenleyemez.",
|
||||
"sandboxModeWarning": "Üretim düğümlerinde düzenlemeleri sandbox'a gönderemezsiniz. Düğümleri düzenlemek için Ayarlar'da Üretim moduna geçin.",
|
||||
"enableSubmittableProfile": "Düğümleri düzenlemek için Ayarlarda gönderilebilir bir profili etkinleştirin.",
|
||||
"profileViewOnlyWarning": "Bu profil sadece harita görüntüleme içindir. Düğümleri düzenlemek için lütfen gönderilebilir bir profil seçin.",
|
||||
@@ -247,6 +251,7 @@
|
||||
"tapToLogout": "Çıkış yapmak için dokun",
|
||||
"requiredToSubmit": "Kamera verisi göndermek için gerekli",
|
||||
"loggedOut": "Çıkış yapıldı",
|
||||
"loginFailed": "Giriş başarısız oldu. Lütfen tekrar deneyin.",
|
||||
"testConnection": "Bağlantıyı Test Et",
|
||||
"testConnectionSubtitle": "OSM kimlik bilgilerinin çalışıp çalışmadığını doğrulayın",
|
||||
"connectionOK": "Bağlantı Tamam - kimlik bilgileri geçerli",
|
||||
@@ -278,7 +283,10 @@
|
||||
"reauthLater": "Sonra",
|
||||
"unreadNotificationsTitle": "Okunmamış Bildirimleriniz Var",
|
||||
"unreadNotificationsMessage": "Okunmamış OSM mesajlarınız ve/veya değişiklik kümelerinize yapılan yeni yorumlar var.",
|
||||
"unreadNotificationsView": "Şimdi Görüntüle"
|
||||
"unreadNotificationsView": "Şimdi Görüntüle",
|
||||
"activeBlockTitle": "OSM Hesabınızda Aktif Bir Engelleme Var",
|
||||
"activeBlockMessage": "OpenStreetMap hesabınızda şu anda aktif bir engelleme var ve gönderim yapmanız kısıtlanmış durumda. Daha fazla ayrıntı içeren bir mesaj almış olmalısınız; daha fazla bilgi için lütfen OSM gelen kutunuzu kontrol edin.",
|
||||
"viewBlockDetails": "Engelleme Ayrıntılarını Görüntüle"
|
||||
},
|
||||
"queue": {
|
||||
"title": "Yükleme Kuyruğu",
|
||||
@@ -314,6 +322,7 @@
|
||||
"processingPaused": "Kuyruk İşleme Duraklatıldı",
|
||||
"pausedDueToOffline": "Çevrimdışı mod etkin olduğu için yükleme işleme duraklatıldı.",
|
||||
"pausedByUser": "Yükleme işleme manuel olarak duraklatıldı.",
|
||||
"pausedDueToBlock": "OSM hesabınızda aktif bir engelleme olduğu için yükleme işlemi duraklatıldı.",
|
||||
"inFlightWaitTitle": "Gönderim Tamamlanıyor",
|
||||
"inFlightWaitMessage": "Şu anda devam eden bir gönderim var ve bu ayar uygulanmadan önce tamamlanması gerekiyor. Bu sadece bir an sürmelidir.",
|
||||
"inFlightCompleteTitle": "Gönderim Tamamlandı",
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"edit": "Редагувати",
|
||||
"delete": "Видалити",
|
||||
"cancel": "Скасувати",
|
||||
"dismiss": "Відхилити",
|
||||
"ok": "ОК",
|
||||
"close": "Закрити",
|
||||
"submit": "Подати",
|
||||
@@ -161,6 +162,7 @@
|
||||
"deleteReasonCustom": "Власний..",
|
||||
"deleteReasonHint": "Опишіть причину...",
|
||||
"mustBeLoggedInToDelete": "Ви повинні увійти в обліковий запис OpenStreetMap, щоб надавати дані карти.",
|
||||
"accountBlockedCannotDelete": "Ваш обліковий запис OpenStreetMap має активне блокування і не може видаляти вузли.",
|
||||
"uploadPending": "Завантаження очікує",
|
||||
"uploadCreatingChangeset": "Створення набору змін...",
|
||||
"uploadUploading": "Завантаження...",
|
||||
@@ -177,6 +179,7 @@
|
||||
"direction": "Напрямок {}°",
|
||||
"profileNoDirectionInfo": "Цей профіль не потребує напрямку.",
|
||||
"mustBeLoggedIn": "Ви повинні увійти в систему, щоб подавати нові вузли. Будь ласка, увійдіть через Налаштування.",
|
||||
"accountBlocked": "Ваш обліковий запис OpenStreetMap має активне блокування і не може подавати нові вузли.",
|
||||
"enableSubmittableProfile": "Увімкніть профіль, який можна подавати, в Налаштуваннях для подання нових вузлів.",
|
||||
"profileViewOnlyWarning": "Цей профіль призначений лише для перегляду карти. Будь ласка, оберіть профіль, який можна подавати, для подання нових вузлів.",
|
||||
"loadingAreaData": "Завантаження даних області... Будь ласка, зачекайте перед поданням.",
|
||||
@@ -191,6 +194,7 @@
|
||||
"profileNoDirectionInfo": "Цей профіль не потребує напрямку.",
|
||||
"temporarilyDisabled": "Редагування тимчасово вимкнено, поки ми розбираємося з помилкою - вибачте - перевірте пізніше.",
|
||||
"mustBeLoggedIn": "Ви повинні увійти в систему, щоб редагувати вузли. Будь ласка, увійдіть через Налаштування.",
|
||||
"accountBlocked": "Ваш обліковий запис OpenStreetMap має активне блокування і не може редагувати вузли.",
|
||||
"sandboxModeWarning": "Не можна подавати редагування виробничих вузлів в sandbox. Перейдіть в режим Виробництва в Налаштуваннях для редагування вузлів.",
|
||||
"enableSubmittableProfile": "Увімкніть профіль, який можна подавати, в Налаштуваннях для редагування вузлів.",
|
||||
"profileViewOnlyWarning": "Цей профіль призначений лише для перегляду карти. Будь ласка, оберіть профіль, який можна подавати, для редагування вузлів.",
|
||||
@@ -247,6 +251,7 @@
|
||||
"tapToLogout": "Натисніть для виходу",
|
||||
"requiredToSubmit": "Потрібно для подання даних камер",
|
||||
"loggedOut": "Вихід здійснено",
|
||||
"loginFailed": "Не вдалося увійти. Спробуйте ще раз.",
|
||||
"testConnection": "Тестувати З'єднання",
|
||||
"testConnectionSubtitle": "Перевірити, що облікові дані OSM працюють",
|
||||
"connectionOK": "З'єднання в порядку - облікові дані дійсні",
|
||||
@@ -278,7 +283,10 @@
|
||||
"reauthLater": "Пізніше",
|
||||
"unreadNotificationsTitle": "У Вас Є Непрочитані Сповіщення",
|
||||
"unreadNotificationsMessage": "У вас є непрочитані повідомлення OSM та/або нові коментарі до ваших наборів змін.",
|
||||
"unreadNotificationsView": "Переглянути Зараз"
|
||||
"unreadNotificationsView": "Переглянути Зараз",
|
||||
"activeBlockTitle": "Ваш обліковий запис OSM має активне блокування",
|
||||
"activeBlockMessage": "Ваш обліковий запис OpenStreetMap наразі має активне блокування і не може надсилати дані. Ви мали отримати повідомлення з додатковими деталями; перевірте свою поштову скриньку OSM, щоб дізнатися більше.",
|
||||
"viewBlockDetails": "Переглянути Деталі Блокування"
|
||||
},
|
||||
"queue": {
|
||||
"title": "Черга Завантаження",
|
||||
@@ -314,6 +322,7 @@
|
||||
"processingPaused": "Обробка Черги Призупинена",
|
||||
"pausedDueToOffline": "Обробка завантаження призупинена, оскільки увімкнено офлайн режим.",
|
||||
"pausedByUser": "Обробка завантаження призупинена вручну.",
|
||||
"pausedDueToBlock": "Обробку черги завантаження призупинено, оскільки ваш обліковий запис OSM має активне блокування.",
|
||||
"inFlightWaitTitle": "Завершення Подання",
|
||||
"inFlightWaitMessage": "Наразі триває подання, яке потрібно завершити перед застосуванням цього налаштування. Це має зайняти лише мить.",
|
||||
"inFlightCompleteTitle": "Подання Завершено",
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"cancel": "取消",
|
||||
"dismiss": "关闭",
|
||||
"ok": "确定",
|
||||
"close": "关闭",
|
||||
"submit": "提交",
|
||||
@@ -161,6 +162,7 @@
|
||||
"deleteReasonCustom": "自定义..",
|
||||
"deleteReasonHint": "请描述原因...",
|
||||
"mustBeLoggedInToDelete": "您必须使用 OpenStreetMap 账户登录才能贡献地图数据。",
|
||||
"accountBlockedCannotDelete": "您的 OpenStreetMap 账户有一个有效封禁,无法删除节点。",
|
||||
"uploadPending": "上传待处理",
|
||||
"uploadCreatingChangeset": "正在创建变更集...",
|
||||
"uploadUploading": "正在上传...",
|
||||
@@ -177,6 +179,7 @@
|
||||
"direction": "方向 {}°",
|
||||
"profileNoDirectionInfo": "此配置文件不需要方向。",
|
||||
"mustBeLoggedIn": "您必须登录才能提交新节点。请通过设置登录。",
|
||||
"accountBlocked": "您的 OpenStreetMap 账户有一个有效封禁,无法提交新节点。",
|
||||
"enableSubmittableProfile": "在设置中启用可提交的配置文件以提交新节点。",
|
||||
"profileViewOnlyWarning": "此配置文件仅用于地图查看。请选择可提交的配置文件来提交新节点。",
|
||||
"loadingAreaData": "正在加载区域数据...提交前请稍候。",
|
||||
@@ -191,6 +194,7 @@
|
||||
"profileNoDirectionInfo": "此配置文件不需要方向。",
|
||||
"temporarilyDisabled": "编辑功能已暂时禁用,我们正在修复一个错误 - 抱歉 - 请稍后再试。",
|
||||
"mustBeLoggedIn": "您必须登录才能编辑节点。请通过设置登录。",
|
||||
"accountBlocked": "您的 OpenStreetMap 账户有一个有效封禁,无法编辑节点。",
|
||||
"sandboxModeWarning": "无法将生产节点的编辑提交到沙盒。在设置中切换到生产模式以编辑节点。",
|
||||
"enableSubmittableProfile": "在设置中启用可提交的配置文件以编辑节点。",
|
||||
"profileViewOnlyWarning": "此配置文件仅用于地图查看。请选择可提交的配置文件来编辑节点。",
|
||||
@@ -247,6 +251,7 @@
|
||||
"tapToLogout": "点击登出",
|
||||
"requiredToSubmit": "提交摄像头数据所需",
|
||||
"loggedOut": "已登出",
|
||||
"loginFailed": "登录失败,请重试。",
|
||||
"testConnection": "测试连接",
|
||||
"testConnectionSubtitle": "验证 OSM 凭据是否有效",
|
||||
"connectionOK": "连接正常 - 凭据有效",
|
||||
@@ -278,7 +283,10 @@
|
||||
"reauthLater": "稍后",
|
||||
"unreadNotificationsTitle": "您有未读通知",
|
||||
"unreadNotificationsMessage": "您有未读的 OSM 消息和/或对您变更集的新评论。",
|
||||
"unreadNotificationsView": "立即查看"
|
||||
"unreadNotificationsView": "立即查看",
|
||||
"activeBlockTitle": "您的 OSM 账户有一个有效封禁",
|
||||
"activeBlockMessage": "您的 OpenStreetMap 账户目前有有效封禁,无法提交内容。您应该已收到一条包含更多详情的消息;请查看您的 OSM 收件箱以了解更多信息。",
|
||||
"viewBlockDetails": "查看封禁详情"
|
||||
},
|
||||
"queue": {
|
||||
"title": "上传队列",
|
||||
@@ -314,6 +322,7 @@
|
||||
"processingPaused": "队列处理已暂停",
|
||||
"pausedDueToOffline": "因为离线模式已启用,上传处理已暂停。",
|
||||
"pausedByUser": "上传处理已手动暂停。",
|
||||
"pausedDueToBlock": "由于您的 OSM 账户有一个有效封禁,上传处理已暂停。",
|
||||
"inFlightWaitTitle": "正在完成提交",
|
||||
"inFlightWaitMessage": "当前有一个提交正在进行中,需要先完成才能应用此设置。这应该只需要一点时间。",
|
||||
"inFlightCompleteTitle": "提交完成",
|
||||
|
||||
@@ -37,7 +37,7 @@ class HomeScreen extends StatefulWidget {
|
||||
State<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||
class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin, WidgetsBindingObserver {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
final GlobalKey<MapViewState> _mapViewKey = GlobalKey<MapViewState>();
|
||||
late final AnimatedMapController _mapController;
|
||||
@@ -62,6 +62,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_mapController = AnimatedMapController(vsync: this);
|
||||
_sheetCoordinator = SheetCoordinator();
|
||||
_navigationCoordinator = NavigationCoordinator();
|
||||
@@ -71,11 +72,24 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
DeepLinkService().onNodeDeepLink = null;
|
||||
_mapController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
// Re-check for an active OSM block when the user returns to the app,
|
||||
// in case they resolved it (or got blocked) while away.
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
final appState = context.read<AppState>();
|
||||
if (appState.isLoggedIn) {
|
||||
appState.checkAndPromptForActiveBlock(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _getFollowMeTooltip(FollowMeMode mode) {
|
||||
final locService = LocalizationService.instance;
|
||||
switch (mode) {
|
||||
@@ -474,6 +488,10 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||
_checkForPopup();
|
||||
// Check if re-authentication is needed for message notifications
|
||||
appState.checkAndPromptReauthForMessages(context);
|
||||
// Check for an active OSM block on app launch
|
||||
if (appState.isLoggedIn) {
|
||||
appState.checkAndPromptForActiveBlock(context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -544,7 +562,9 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||
return IconButton(
|
||||
tooltip: LocalizationService.instance.settings,
|
||||
icon: Badge(
|
||||
isLabelVisible: appState.hasUnreadNotifications,
|
||||
isLabelVisible: appState.hasActiveBlock || appState.hasUnreadNotifications,
|
||||
backgroundColor: appState.hasActiveBlock ? Colors.red : null,
|
||||
label: appState.hasActiveBlock ? const Icon(Icons.block, size: 10, color: Colors.white) : null,
|
||||
child: const Icon(Icons.settings),
|
||||
),
|
||||
onPressed: () => Navigator.pushNamed(context, '/settings'),
|
||||
|
||||
@@ -17,11 +17,12 @@ class _OSMAccountScreenState extends State<OSMAccountScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Check for messages when screen loads
|
||||
// Check for messages and active blocks when screen loads
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final appState = context.read<AppState>();
|
||||
if (appState.isLoggedIn) {
|
||||
appState.checkMessages();
|
||||
appState.checkAndPromptForActiveBlock(context);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -74,22 +75,51 @@ class _OSMAccountScreenState extends State<OSMAccountScreen> {
|
||||
}
|
||||
} else {
|
||||
// Start login flow - the user will be redirected to browser
|
||||
await appState.forceLogin();
|
||||
await appState.forceLogin(context: context);
|
||||
|
||||
// Don't show immediate feedback - the UI will update automatically
|
||||
// when the OAuth callback completes and notifyListeners() is called
|
||||
// Don't show immediate feedback on success - the UI will update
|
||||
// automatically when the OAuth callback completes and
|
||||
// notifyListeners() is called. Failures are surfaced directly
|
||||
// by forceLogin() via a snackbar/dialog.
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
if (appState.isLoggedIn) ...[
|
||||
// Active block warning - shown whenever a check has
|
||||
// found the user's OSM account currently blocked.
|
||||
if (appState.hasActiveBlock) ...[
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: Badge(
|
||||
isLabelVisible: true,
|
||||
backgroundColor: Colors.red,
|
||||
child: const Icon(Icons.block, color: Colors.red),
|
||||
),
|
||||
title: Text(locService.t('auth.activeBlockTitle')),
|
||||
subtitle: Text(locService.t('auth.activeBlockMessage')),
|
||||
trailing: const Icon(Icons.open_in_new),
|
||||
onTap: () async {
|
||||
final url = Uri.parse(appState.getBlockDetailsUrl());
|
||||
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 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();
|
||||
final isValid = await appState.testConnection(context);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
|
||||
@@ -40,10 +40,12 @@ class AuthSection extends StatelessWidget {
|
||||
}
|
||||
} else {
|
||||
// Start login flow - the user will be redirected to browser
|
||||
await appState.forceLogin();
|
||||
await appState.forceLogin(context: context);
|
||||
|
||||
// Don't show immediate feedback - the UI will update automatically
|
||||
// when the OAuth callback completes and notifyListeners() is called
|
||||
// Don't show immediate feedback on success - the UI will update
|
||||
// automatically when the OAuth callback completes and
|
||||
// notifyListeners() is called. Failures are surfaced directly
|
||||
// by forceLogin() via a snackbar/dialog.
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -53,7 +55,7 @@ class AuthSection extends StatelessWidget {
|
||||
title: Text(locService.t('auth.testConnection')),
|
||||
subtitle: Text(locService.t('auth.testConnectionSubtitle')),
|
||||
onTap: () async {
|
||||
final isValid = await appState.validateToken();
|
||||
final isValid = await appState.testConnection(context);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
|
||||
@@ -118,7 +118,9 @@ class SettingsScreen extends StatelessWidget {
|
||||
|
||||
return ListTile(
|
||||
leading: Badge(
|
||||
isLabelVisible: appState.hasUnreadNotifications,
|
||||
isLabelVisible: appState.hasActiveBlock || appState.hasUnreadNotifications,
|
||||
backgroundColor: appState.hasActiveBlock ? Colors.red : null,
|
||||
label: appState.hasActiveBlock ? const Icon(Icons.block, size: 10, color: Colors.white) : null,
|
||||
child: const Icon(Icons.account_circle),
|
||||
),
|
||||
title: Text(locService.t('auth.osmAccountTitle')),
|
||||
|
||||
@@ -111,7 +111,7 @@ class UploadQueueScreen extends StatelessWidget {
|
||||
final appState = context.watch<AppState>();
|
||||
|
||||
// Check if queue processing is paused
|
||||
final isQueuePaused = appState.offlineMode || appState.pauseQueueProcessing;
|
||||
final isQueuePaused = appState.offlineMode || appState.pauseQueueProcessing || appState.isQueuePausedDueToBlock;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -151,9 +151,11 @@ class UploadQueueScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
appState.offlineMode
|
||||
? locService.t('queue.pausedDueToOffline')
|
||||
: locService.t('queue.pausedByUser'),
|
||||
appState.isQueuePausedDueToBlock
|
||||
? locService.t('queue.pausedDueToBlock')
|
||||
: appState.offlineMode
|
||||
? locService.t('queue.pausedDueToOffline')
|
||||
: locService.t('queue.pausedByUser'),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.orange.shade700,
|
||||
|
||||
+107
-13
@@ -2,14 +2,42 @@ import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:oauth2_client/oauth2_client.dart';
|
||||
import 'package:oauth2_client/oauth2_helper.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Handles PKCE OAuth login with OpenStreetMap.
|
||||
import '../keys.dart';
|
||||
import '../dev_config.dart';
|
||||
import '../app_state.dart' show UploadMode;
|
||||
import 'http_client.dart';
|
||||
import 'osm_block_service.dart';
|
||||
|
||||
/// Thrown when the OAuth token exchange succeeds but a subsequent
|
||||
/// authenticated API call (e.g. fetching the username) fails. Carries the
|
||||
/// HTTP status code so callers can distinguish e.g. a 403 (often indicating
|
||||
/// an active account block) from other failures.
|
||||
class AuthApiException implements Exception {
|
||||
final String message;
|
||||
final int? statusCode;
|
||||
final String? body;
|
||||
|
||||
AuthApiException(this.message, {this.statusCode, this.body});
|
||||
|
||||
@override
|
||||
String toString() => 'AuthApiException: $message (status: $statusCode)';
|
||||
}
|
||||
|
||||
/// Thrown when the OAuth token exchange succeeds but the account turns out
|
||||
/// to have an active OSM DWG block. Detected by checking the
|
||||
/// `user/blocks/active` endpoint (which is accessible even while blocked)
|
||||
/// immediately after obtaining the token, before attempting any other
|
||||
/// authenticated call that would otherwise fail with a 403.
|
||||
class AccountBlockedException implements Exception {
|
||||
@override
|
||||
String toString() => 'AccountBlockedException: account has an active OSM block';
|
||||
}
|
||||
|
||||
class AuthService {
|
||||
// Both client IDs from keys.dart
|
||||
@@ -18,8 +46,10 @@ class AuthService {
|
||||
late OAuth2Helper _helper;
|
||||
String? _displayName;
|
||||
UploadMode _mode = UploadMode.production;
|
||||
final OSMBlockService _blockService;
|
||||
|
||||
AuthService({UploadMode mode = UploadMode.production}) {
|
||||
AuthService({UploadMode mode = UploadMode.production, OSMBlockService? blockService})
|
||||
: _blockService = blockService ?? OSMBlockService() {
|
||||
setUploadMode(mode);
|
||||
}
|
||||
|
||||
@@ -87,8 +117,12 @@ class AuthService {
|
||||
try {
|
||||
final token = await _helper.getToken();
|
||||
if (token.accessToken == null) {
|
||||
debugPrint('AuthService: OAuth error: token null or missing accessToken (status: ${token.httpStatusCode}, error: ${token.error})');
|
||||
log('OAuth error: token null or missing accessToken');
|
||||
return null;
|
||||
throw AuthApiException(
|
||||
'OAuth token exchange did not return an access token',
|
||||
statusCode: token.httpStatusCode,
|
||||
);
|
||||
}
|
||||
final tokenMap = {
|
||||
'accessToken': token.accessToken,
|
||||
@@ -97,8 +131,55 @@ class AuthService {
|
||||
final tokenJson = jsonEncode(tokenMap);
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_tokenKey, tokenJson); // Save token for current mode
|
||||
_displayName = await _fetchUsername(token.accessToken!);
|
||||
|
||||
if (kCheckBlockBeforeUsernameFetch) {
|
||||
// Check for an active block *before* fetching the username or
|
||||
// anything else. As of writing, a blocked account's OAuth token
|
||||
// exchange succeeds normally, but nearly every other authenticated
|
||||
// endpoint (including user/details) returns 403. The blocks/active
|
||||
// endpoint is explicitly documented as accessible even while
|
||||
// blocked, so checking it first lets us detect this case reliably
|
||||
// instead of getting a mysterious 403 from _fetchUsername with no
|
||||
// way to tell why.
|
||||
final isBlocked = await _blockService.checkActiveBlock(
|
||||
accessToken: token.accessToken,
|
||||
uploadMode: _mode,
|
||||
);
|
||||
if (isBlocked == true) {
|
||||
debugPrint('AuthService: Login succeeded but account has an active block');
|
||||
throw AccountBlockedException();
|
||||
}
|
||||
}
|
||||
|
||||
// Fetching the username can still fail even though the OAuth token
|
||||
// exchange succeeded (e.g. if OSM ever allows this call to succeed for
|
||||
// blocked accounts, kCheckBlockBeforeUsernameFetch would be false and
|
||||
// we'd only find out here). Don't swallow that - the token is already
|
||||
// saved, so the caller needs to know the login isn't actually usable
|
||||
// rather than silently treating it as "not logged in".
|
||||
try {
|
||||
_displayName = await _fetchUsername(token.accessToken!);
|
||||
} on AuthApiException {
|
||||
// When we skipped the proactive check above, a 403 here could still
|
||||
// mean the account is blocked - check blocks/active now so we can
|
||||
// report that specifically instead of a generic auth failure.
|
||||
if (!kCheckBlockBeforeUsernameFetch) {
|
||||
final isBlocked = await _blockService.checkActiveBlock(
|
||||
accessToken: token.accessToken,
|
||||
uploadMode: _mode,
|
||||
);
|
||||
if (isBlocked == true) {
|
||||
debugPrint('AuthService: user/details failed and account has an active block');
|
||||
throw AccountBlockedException();
|
||||
}
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
return _displayName;
|
||||
} on AccountBlockedException {
|
||||
rethrow;
|
||||
} on AuthApiException {
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
debugPrint('AuthService: OAuth login failed: $e');
|
||||
log('OAuth login failed: $e');
|
||||
@@ -184,24 +265,37 @@ class AuthService {
|
||||
final _client = UserAgentClient();
|
||||
|
||||
Future<String?> _fetchUsername(String accessToken) async {
|
||||
final http.Response resp;
|
||||
try {
|
||||
final resp = await _client.get(
|
||||
resp = await _client.get(
|
||||
Uri.parse('$_apiHost/api/0.6/user/details.json'),
|
||||
headers: {'Authorization': 'Bearer $accessToken'},
|
||||
);
|
||||
|
||||
if (resp.statusCode != 200) {
|
||||
log('fetchUsername response ${resp.statusCode}: ${resp.body}');
|
||||
return null;
|
||||
}
|
||||
final userData = jsonDecode(resp.body);
|
||||
final displayName = userData['user']?['display_name'];
|
||||
return displayName;
|
||||
} catch (e) {
|
||||
// Network-level failure (no connection, timeout, etc.) - not
|
||||
// indicative of a block, just a connectivity problem.
|
||||
debugPrint('AuthService: Error fetching username: $e');
|
||||
log('Error fetching username: $e');
|
||||
return null;
|
||||
rethrow;
|
||||
}
|
||||
|
||||
if (resp.statusCode != 200) {
|
||||
// A non-200 here (very commonly 403) after a successful OAuth token
|
||||
// exchange is a strong signal of an active account block. Surface it
|
||||
// loudly via debugPrint (visible in a normal release/profile console,
|
||||
// unlike dart:developer's log()) and throw instead of silently
|
||||
// returning null, so the caller can show a real error to the user.
|
||||
debugPrint('AuthService: fetchUsername failed - HTTP ${resp.statusCode}: ${resp.body}');
|
||||
log('fetchUsername response ${resp.statusCode}: ${resp.body}');
|
||||
throw AuthApiException(
|
||||
'Failed to fetch user details after login',
|
||||
statusCode: resp.statusCode,
|
||||
body: resp.body,
|
||||
);
|
||||
}
|
||||
final userData = jsonDecode(resp.body);
|
||||
final displayName = userData['user']?['display_name'];
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../state/settings_state.dart';
|
||||
import 'http_client.dart';
|
||||
|
||||
/// Service for checking whether the current user has an active OSM DWG block.
|
||||
///
|
||||
/// See: https://wiki.openstreetmap.org/wiki/API_v0.6#List_active_blocks:_GET_/api/0.6/user/blocks/active
|
||||
class OSMBlockService {
|
||||
final http.Client _client;
|
||||
|
||||
OSMBlockService({http.Client? client}) : _client = client ?? UserAgentClient();
|
||||
|
||||
/// Returns whether the user currently has an active block.
|
||||
///
|
||||
/// Returns null if not logged in, in simulate mode, or on any error
|
||||
/// (including no network connection) - callers should treat null as
|
||||
/// "unknown, assume unchanged" rather than "not blocked".
|
||||
Future<bool?> checkActiveBlock({
|
||||
required String? accessToken,
|
||||
required UploadMode uploadMode,
|
||||
}) async {
|
||||
if (uploadMode == UploadMode.simulate) return null;
|
||||
if (accessToken == null || accessToken.isEmpty) return null;
|
||||
|
||||
try {
|
||||
final apiHost = _getApiHost(uploadMode);
|
||||
final response = await _client.get(
|
||||
Uri.parse('$apiHost/api/0.6/user/blocks/active.json'),
|
||||
headers: {'Authorization': 'Bearer $accessToken'},
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) return null;
|
||||
|
||||
final data = jsonDecode(response.body);
|
||||
final userBlocks = data['user_blocks'];
|
||||
if (userBlocks is! List) return null;
|
||||
|
||||
return userBlocks.isNotEmpty;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String _getApiHost(UploadMode uploadMode) {
|
||||
switch (uploadMode) {
|
||||
case UploadMode.production:
|
||||
return 'https://api.openstreetmap.org';
|
||||
case UploadMode.sandbox:
|
||||
return 'https://api06.dev.openstreetmap.org';
|
||||
case UploadMode.simulate:
|
||||
return 'https://api.openstreetmap.org';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../services/osm_block_service.dart';
|
||||
import 'settings_state.dart';
|
||||
|
||||
/// Tracks whether the current user has an active OSM DWG block.
|
||||
///
|
||||
/// Persisted per upload mode so the last known state survives app restarts
|
||||
/// (e.g. launching offline) - a failed/inconclusive check leaves the
|
||||
/// persisted value untouched rather than assuming "not blocked".
|
||||
class AccountBlockState extends ChangeNotifier {
|
||||
final OSMBlockService _blockService;
|
||||
|
||||
AccountBlockState({OSMBlockService? blockService})
|
||||
: _blockService = blockService ?? OSMBlockService();
|
||||
|
||||
bool _isBlocked = false;
|
||||
|
||||
bool get isBlocked => _isBlocked;
|
||||
|
||||
/// Load the last known block state for the given upload mode.
|
||||
Future<void> init(UploadMode uploadMode) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_isBlocked = prefs.getBool(_prefsKey(uploadMode)) ?? false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Check for an active block. Returns the conclusive result (true/false),
|
||||
/// or null if the check was inconclusive (offline, error, not logged in,
|
||||
/// simulate mode). On null, the previously known state is left unchanged.
|
||||
Future<bool?> check({
|
||||
required String? accessToken,
|
||||
required UploadMode uploadMode,
|
||||
}) async {
|
||||
final result = await _blockService.checkActiveBlock(
|
||||
accessToken: accessToken,
|
||||
uploadMode: uploadMode,
|
||||
);
|
||||
|
||||
if (result != null && result != _isBlocked) {
|
||||
_isBlocked = result;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_prefsKey(uploadMode), result);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Clear block state (called on logout).
|
||||
void clear() {
|
||||
if (_isBlocked) {
|
||||
_isBlocked = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
String _prefsKey(UploadMode uploadMode) => 'account_blocked_${uploadMode.name}';
|
||||
}
|
||||
@@ -28,11 +28,16 @@ class AuthState extends ChangeNotifier {
|
||||
Future<void> login() async {
|
||||
try {
|
||||
_username = await _auth.login();
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint("AuthState: Login error: $e");
|
||||
_username = null;
|
||||
notifyListeners();
|
||||
// Don't swallow this - the caller (AppState) needs to know login
|
||||
// failed so it can tell the user, rather than silently staying
|
||||
// logged out with no explanation.
|
||||
rethrow;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
@@ -58,11 +63,16 @@ class AuthState extends ChangeNotifier {
|
||||
Future<void> forceLogin() async {
|
||||
try {
|
||||
_username = await _auth.forceLogin();
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint("AuthState: Forced login error: $e");
|
||||
_username = null;
|
||||
notifyListeners();
|
||||
// Don't swallow this - the caller (AppState) needs to know login
|
||||
// failed so it can tell the user, rather than silently staying
|
||||
// logged out with no explanation.
|
||||
rethrow;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<bool> validateToken() async {
|
||||
|
||||
@@ -20,11 +20,13 @@ class UploadQueueState extends ChangeNotifier {
|
||||
final List<PendingUpload> _queue = [];
|
||||
Timer? _uploadTimer;
|
||||
int _activeUploadCount = 0;
|
||||
bool _isPausedDueToBlock = false;
|
||||
|
||||
// Getters
|
||||
int get pendingCount => _queue.length;
|
||||
List<PendingUpload> get pendingUploads => List.unmodifiable(_queue);
|
||||
int get activeUploadCount => _activeUploadCount;
|
||||
bool get isPausedDueToBlock => _isPausedDueToBlock;
|
||||
|
||||
// Initialize by loading queue from storage and repopulate cache with pending nodes
|
||||
Future<void> init() async {
|
||||
@@ -326,8 +328,10 @@ class UploadQueueState extends ChangeNotifier {
|
||||
required bool pauseQueueProcessing,
|
||||
required UploadMode uploadMode,
|
||||
required Future<String?> Function() getAccessToken,
|
||||
required Future<bool?> Function() checkActiveBlock,
|
||||
}) {
|
||||
_uploadTimer?.cancel();
|
||||
_isPausedDueToBlock = false;
|
||||
|
||||
// No uploads if queue is empty, offline mode is enabled, or queue processing is paused
|
||||
if (_queue.isEmpty || offlineMode || pauseQueueProcessing) return;
|
||||
@@ -386,6 +390,16 @@ class UploadQueueState extends ChangeNotifier {
|
||||
final access = await getAccessToken();
|
||||
if (access == null) return; // not logged in
|
||||
|
||||
// Don't start new uploads if the user has an active OSM block.
|
||||
// Leave the item pending - it'll be retried next time the uploader starts.
|
||||
if (await checkActiveBlock() == true) {
|
||||
debugPrint('[UploadQueue] Active block detected, pausing queue processing');
|
||||
_isPausedDueToBlock = true;
|
||||
_uploadTimer?.cancel();
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
// Start processing the next pending upload
|
||||
final item = pendingItems.first;
|
||||
debugPrint('[UploadQueue] Starting new upload processing for item at ${item.coord} ($_activeUploadCount/$kMaxConcurrentUploads active)');
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/localization_service.dart';
|
||||
|
||||
/// Dialog shown whenever a check finds that the user's OSM account has an
|
||||
/// active block. Unlike the unread-notifications dialog, this has no
|
||||
/// "don't show again" option and is shown every time a check finds a block.
|
||||
class ActiveBlockDialog extends StatelessWidget {
|
||||
final VoidCallback onViewDetails;
|
||||
final VoidCallback onDismiss;
|
||||
|
||||
const ActiveBlockDialog({
|
||||
super.key,
|
||||
required this.onViewDetails,
|
||||
required this.onDismiss,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final locService = LocalizationService.instance;
|
||||
|
||||
return AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.block,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(locService.t('auth.activeBlockTitle')),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Text(locService.t('auth.activeBlockMessage')),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
onDismiss();
|
||||
},
|
||||
child: Text(locService.t('actions.dismiss')),
|
||||
),
|
||||
FilledButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
onViewDetails();
|
||||
},
|
||||
icon: const Icon(Icons.open_in_new, size: 18),
|
||||
label: Text(locService.t('auth.viewBlockDetails')),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -421,6 +421,7 @@ class _AddNodeSheetState extends State<AddNodeSheet> {
|
||||
}
|
||||
|
||||
final allowSubmit = appState.isLoggedIn &&
|
||||
!appState.hasActiveBlock &&
|
||||
submittableProfiles.isNotEmpty &&
|
||||
session.profile != null &&
|
||||
session.profile!.isSubmittable &&
|
||||
@@ -493,6 +494,22 @@ class _AddNodeSheetState extends State<AddNodeSheet> {
|
||||
],
|
||||
),
|
||||
)
|
||||
else if (appState.hasActiveBlock)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.block, color: Colors.red, size: 20),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
locService.t('addNode.accountBlocked'),
|
||||
style: const TextStyle(color: Colors.red, fontSize: 13),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else if (submittableProfiles.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||
|
||||
@@ -377,6 +377,7 @@ class _EditNodeSheetState extends State<EditNodeSheet> {
|
||||
|
||||
final allowSubmit = kEnableNodeEdits &&
|
||||
appState.isLoggedIn &&
|
||||
!appState.hasActiveBlock &&
|
||||
submittableProfiles.isNotEmpty &&
|
||||
session.profile != null &&
|
||||
session.profile!.isSubmittable &&
|
||||
@@ -526,6 +527,22 @@ class _EditNodeSheetState extends State<EditNodeSheet> {
|
||||
],
|
||||
),
|
||||
)
|
||||
else if (appState.hasActiveBlock)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.block, color: Colors.red, size: 20),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
locService.t('editNode.accountBlocked'),
|
||||
style: const TextStyle(color: Colors.red, fontSize: 13),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else if (submittableProfiles.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||
|
||||
@@ -184,6 +184,23 @@ class _NodeTagSheetState extends State<NodeTagSheet> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (appState.hasActiveBlock) {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
title: Text(locService.t('node.confirmDeleteTitle')),
|
||||
content: Text(locService.t('node.accountBlockedCannotDelete')),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(locService.ok),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await showDialog<({bool confirmed, String comment})>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => _DeleteNodeDialog(
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
|
||||
import 'package:deflockapp/services/osm_block_service.dart';
|
||||
import 'package:deflockapp/state/settings_state.dart';
|
||||
|
||||
void main() {
|
||||
group('OSMBlockService', () {
|
||||
test('returns null in simulate mode without making a request', () async {
|
||||
var requested = false;
|
||||
final client = MockClient((request) async {
|
||||
requested = true;
|
||||
return http.Response('{"user_blocks":[]}', 200);
|
||||
});
|
||||
final service = OSMBlockService(client: client);
|
||||
|
||||
final result = await service.checkActiveBlock(
|
||||
accessToken: 'token',
|
||||
uploadMode: UploadMode.simulate,
|
||||
);
|
||||
|
||||
expect(result, isNull);
|
||||
expect(requested, isFalse);
|
||||
});
|
||||
|
||||
test('returns null when access token is null', () async {
|
||||
final client = MockClient((request) async => http.Response('{"user_blocks":[]}', 200));
|
||||
final service = OSMBlockService(client: client);
|
||||
|
||||
final result = await service.checkActiveBlock(
|
||||
accessToken: null,
|
||||
uploadMode: UploadMode.production,
|
||||
);
|
||||
|
||||
expect(result, isNull);
|
||||
});
|
||||
|
||||
test('returns null when access token is empty', () async {
|
||||
final client = MockClient((request) async => http.Response('{"user_blocks":[]}', 200));
|
||||
final service = OSMBlockService(client: client);
|
||||
|
||||
final result = await service.checkActiveBlock(
|
||||
accessToken: '',
|
||||
uploadMode: UploadMode.production,
|
||||
);
|
||||
|
||||
expect(result, isNull);
|
||||
});
|
||||
|
||||
test('returns false when user_blocks is empty', () async {
|
||||
final client = MockClient((request) async => http.Response('{"user_blocks":[]}', 200));
|
||||
final service = OSMBlockService(client: client);
|
||||
|
||||
final result = await service.checkActiveBlock(
|
||||
accessToken: 'token',
|
||||
uploadMode: UploadMode.production,
|
||||
);
|
||||
|
||||
expect(result, isFalse);
|
||||
});
|
||||
|
||||
test('returns true when user_blocks is non-empty', () async {
|
||||
final client = MockClient((request) async => http.Response(
|
||||
'{"user_blocks":[{"id":101,"needs_view":true}]}',
|
||||
200,
|
||||
));
|
||||
final service = OSMBlockService(client: client);
|
||||
|
||||
final result = await service.checkActiveBlock(
|
||||
accessToken: 'token',
|
||||
uploadMode: UploadMode.production,
|
||||
);
|
||||
|
||||
expect(result, isTrue);
|
||||
});
|
||||
|
||||
test('sends the request to the correct production host with auth header', () async {
|
||||
Uri? capturedUri;
|
||||
String? capturedAuth;
|
||||
final client = MockClient((request) async {
|
||||
capturedUri = request.url;
|
||||
capturedAuth = request.headers['Authorization'];
|
||||
return http.Response('{"user_blocks":[]}', 200);
|
||||
});
|
||||
final service = OSMBlockService(client: client);
|
||||
|
||||
await service.checkActiveBlock(
|
||||
accessToken: 'my-token',
|
||||
uploadMode: UploadMode.production,
|
||||
);
|
||||
|
||||
expect(capturedUri?.host, 'api.openstreetmap.org');
|
||||
expect(capturedUri?.path, '/api/0.6/user/blocks/active.json');
|
||||
expect(capturedAuth, 'Bearer my-token');
|
||||
});
|
||||
|
||||
test('sends the request to the sandbox host in sandbox mode', () async {
|
||||
Uri? capturedUri;
|
||||
final client = MockClient((request) async {
|
||||
capturedUri = request.url;
|
||||
return http.Response('{"user_blocks":[]}', 200);
|
||||
});
|
||||
final service = OSMBlockService(client: client);
|
||||
|
||||
await service.checkActiveBlock(
|
||||
accessToken: 'my-token',
|
||||
uploadMode: UploadMode.sandbox,
|
||||
);
|
||||
|
||||
expect(capturedUri?.host, 'api06.dev.openstreetmap.org');
|
||||
});
|
||||
|
||||
test('returns null on non-200 response', () async {
|
||||
final client = MockClient((request) async => http.Response('Unauthorized', 401));
|
||||
final service = OSMBlockService(client: client);
|
||||
|
||||
final result = await service.checkActiveBlock(
|
||||
accessToken: 'token',
|
||||
uploadMode: UploadMode.production,
|
||||
);
|
||||
|
||||
expect(result, isNull);
|
||||
});
|
||||
|
||||
test('returns null on malformed JSON response', () async {
|
||||
final client = MockClient((request) async => http.Response('not json', 200));
|
||||
final service = OSMBlockService(client: client);
|
||||
|
||||
final result = await service.checkActiveBlock(
|
||||
accessToken: 'token',
|
||||
uploadMode: UploadMode.production,
|
||||
);
|
||||
|
||||
expect(result, isNull);
|
||||
});
|
||||
|
||||
test('returns null when user_blocks field is missing', () async {
|
||||
final client = MockClient((request) async => http.Response('{}', 200));
|
||||
final service = OSMBlockService(client: client);
|
||||
|
||||
final result = await service.checkActiveBlock(
|
||||
accessToken: 'token',
|
||||
uploadMode: UploadMode.production,
|
||||
);
|
||||
|
||||
expect(result, isNull);
|
||||
});
|
||||
|
||||
test('returns null when the client throws (e.g. no network)', () async {
|
||||
final client = MockClient((request) async {
|
||||
throw Exception('network error');
|
||||
});
|
||||
final service = OSMBlockService(client: client);
|
||||
|
||||
final result = await service.checkActiveBlock(
|
||||
accessToken: 'token',
|
||||
uploadMode: UploadMode.production,
|
||||
);
|
||||
|
||||
expect(result, isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:deflockapp/services/osm_block_service.dart';
|
||||
import 'package:deflockapp/state/account_block_state.dart';
|
||||
import 'package:deflockapp/state/settings_state.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
group('AccountBlockState', () {
|
||||
test('defaults to not blocked before init()', () {
|
||||
final state = AccountBlockState();
|
||||
expect(state.isBlocked, isFalse);
|
||||
});
|
||||
|
||||
test('init() loads persisted blocked state for the given upload mode', () async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'account_blocked_production': true,
|
||||
});
|
||||
|
||||
final state = AccountBlockState();
|
||||
await state.init(UploadMode.production);
|
||||
|
||||
expect(state.isBlocked, isTrue);
|
||||
});
|
||||
|
||||
test('init() defaults to false when nothing is persisted', () async {
|
||||
final state = AccountBlockState();
|
||||
await state.init(UploadMode.production);
|
||||
|
||||
expect(state.isBlocked, isFalse);
|
||||
});
|
||||
|
||||
test('init() is scoped per upload mode', () async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'account_blocked_production': true,
|
||||
});
|
||||
|
||||
final state = AccountBlockState();
|
||||
await state.init(UploadMode.sandbox);
|
||||
|
||||
expect(state.isBlocked, isFalse);
|
||||
});
|
||||
|
||||
test('check() updates and persists state on a conclusive true result', () async {
|
||||
final client = MockClient((request) async => http.Response(
|
||||
'{"user_blocks":[{"id":1}]}',
|
||||
200,
|
||||
));
|
||||
final state = AccountBlockState(blockService: OSMBlockService(client: client));
|
||||
await state.init(UploadMode.production);
|
||||
|
||||
final result = await state.check(accessToken: 'token', uploadMode: UploadMode.production);
|
||||
|
||||
expect(result, isTrue);
|
||||
expect(state.isBlocked, isTrue);
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
expect(prefs.getBool('account_blocked_production'), isTrue);
|
||||
});
|
||||
|
||||
test('check() updates and persists state on a conclusive false result', () async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'account_blocked_production': true,
|
||||
});
|
||||
final client = MockClient((request) async => http.Response('{"user_blocks":[]}', 200));
|
||||
final state = AccountBlockState(blockService: OSMBlockService(client: client));
|
||||
await state.init(UploadMode.production);
|
||||
expect(state.isBlocked, isTrue);
|
||||
|
||||
final result = await state.check(accessToken: 'token', uploadMode: UploadMode.production);
|
||||
|
||||
expect(result, isFalse);
|
||||
expect(state.isBlocked, isFalse);
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
expect(prefs.getBool('account_blocked_production'), isFalse);
|
||||
});
|
||||
|
||||
test('check() leaves state unchanged on an inconclusive (null) result', () async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'account_blocked_production': true,
|
||||
});
|
||||
final client = MockClient((request) async {
|
||||
throw Exception('offline');
|
||||
});
|
||||
final state = AccountBlockState(blockService: OSMBlockService(client: client));
|
||||
await state.init(UploadMode.production);
|
||||
expect(state.isBlocked, isTrue);
|
||||
|
||||
final result = await state.check(accessToken: 'token', uploadMode: UploadMode.production);
|
||||
|
||||
expect(result, isNull);
|
||||
expect(state.isBlocked, isTrue); // unchanged
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
expect(prefs.getBool('account_blocked_production'), isTrue); // unchanged
|
||||
});
|
||||
|
||||
test('clear() resets in-memory blocked state', () async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'account_blocked_production': true,
|
||||
});
|
||||
final state = AccountBlockState();
|
||||
await state.init(UploadMode.production);
|
||||
expect(state.isBlocked, isTrue);
|
||||
|
||||
state.clear();
|
||||
|
||||
expect(state.isBlocked, isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user