mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-02-12 16:52:51 +00:00
Add welcome popup, changelog system, and account deletion button
This commit is contained in:
43
DEVELOPER.md
43
DEVELOPER.md
@@ -65,6 +65,49 @@ External APIs (OSM, Overpass, Tile providers)
|
||||
|
||||
---
|
||||
|
||||
## Changelog & First Launch System
|
||||
|
||||
The app includes a comprehensive system for welcoming new users and notifying existing users of updates.
|
||||
|
||||
### Components
|
||||
- **ChangelogService**: Manages version tracking and changelog loading
|
||||
- **WelcomeDialog**: First launch popup with privacy information and quick links
|
||||
- **ChangelogDialog**: Update notification popup for version changes
|
||||
- **ReleaseNotesScreen**: Settings page for viewing all changelog history
|
||||
|
||||
### Content Management
|
||||
Changelog content is stored in `assets/changelog.json`:
|
||||
```json
|
||||
{
|
||||
"1.2.4": {
|
||||
"content": "• New feature description\n• Bug fixes\n• Other improvements"
|
||||
},
|
||||
"1.2.3": {
|
||||
"content": "" // Empty string = skip popup for this version
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Developer Workflow
|
||||
1. **For each release**: Add entry to `changelog.json` with version from `pubspec.yaml`
|
||||
2. **Content required**: Every version must have an entry (can be empty string to skip)
|
||||
3. **Localization**: Welcome dialog supports i18n, changelog content is English-only
|
||||
4. **Testing**: Clear app data to test first launch, change version to test updates
|
||||
|
||||
### User Experience Flow
|
||||
- **First Launch**: Welcome popup with "don't show again" option
|
||||
- **Version Updates**: Changelog popup (only if content exists, no "don't show again")
|
||||
- **Settings Access**: Complete changelog history available in Settings > About > Release Notes
|
||||
|
||||
### Privacy Integration
|
||||
The welcome popup explains that the app:
|
||||
- Runs entirely locally on device
|
||||
- Uses OpenStreetMap API for data storage only
|
||||
- DeFlock collects no user data
|
||||
- DeFlock is not responsible for OSM account management
|
||||
|
||||
---
|
||||
|
||||
## Core Components
|
||||
|
||||
### 1. MapDataProvider
|
||||
|
||||
@@ -54,7 +54,7 @@ A comprehensive Flutter app for mapping public surveillance infrastructure with
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Install** the app on iOS or Android
|
||||
1. **Install** the app on iOS or Android - a welcome popup will guide you through key information
|
||||
2. **Enable location** permissions
|
||||
3. **Log into OpenStreetMap**: Choose upload mode and get OAuth2 credentials
|
||||
4. **Add your first device**: Tap the "New Node" button, position the pin, set direction, select a profile, and tap submit
|
||||
@@ -62,6 +62,8 @@ A comprehensive Flutter app for mapping public surveillance infrastructure with
|
||||
|
||||
**New to OpenStreetMap?** Visit [deflock.me](https://deflock.me) for complete setup instructions and community guidelines.
|
||||
|
||||
**App Updates**: The app will automatically show you what's new when you update. You can always view release notes in Settings > About.
|
||||
|
||||
---
|
||||
|
||||
## For Developers
|
||||
|
||||
17
assets/changelog.json
Normal file
17
assets/changelog.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"1.2.4": {
|
||||
"content": "• New welcome popup for first-time users with essential privacy information\n• Automatic changelog display when app updates (like this one!)\n• Added Release Notes viewer in Settings > About\n• Enhanced user onboarding and transparency about data handling\n• Improved documentation for contributors"
|
||||
},
|
||||
"1.2.3": {
|
||||
"content": "• Enhanced map performance and stability\n• Improved offline sync reliability\n• Added better error handling for uploads\n• Various bug fixes and improvements"
|
||||
},
|
||||
"1.2.2": {
|
||||
"content": "• New surveillance device profiles added\n• Improved tile loading performance\n• Fixed issue with GPS accuracy\n• Updated translations"
|
||||
},
|
||||
"1.2.1": {
|
||||
"content": ""
|
||||
},
|
||||
"1.2.0": {
|
||||
"content": "• Major UI improvements\n• Added proximity alerts\n• Enhanced offline capabilities\n• New suspected locations feature"
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import 'models/search_result.dart';
|
||||
import 'services/offline_area_service.dart';
|
||||
import 'services/node_cache.dart';
|
||||
import 'services/tile_preview_service.dart';
|
||||
import 'services/changelog_service.dart';
|
||||
import 'widgets/camera_provider_with_cache.dart';
|
||||
import 'state/auth_state.dart';
|
||||
import 'state/navigation_state.dart';
|
||||
@@ -160,6 +161,9 @@ class AppState extends ChangeNotifier {
|
||||
// Initialize all state modules
|
||||
await _settingsState.init();
|
||||
|
||||
// Initialize changelog service
|
||||
await ChangelogService().init();
|
||||
|
||||
// Attempt to fetch missing tile type preview tiles (fails silently)
|
||||
_fetchMissingTilePreviews();
|
||||
|
||||
|
||||
@@ -124,7 +124,12 @@
|
||||
"testConnection": "Verbindung Testen",
|
||||
"testConnectionSubtitle": "OSM-Anmeldedaten überprüfen",
|
||||
"connectionOK": "Verbindung OK - Anmeldedaten sind gültig",
|
||||
"connectionFailed": "Verbindung fehlgeschlagen - bitte erneut anmelden"
|
||||
"connectionFailed": "Verbindung fehlgeschlagen - bitte erneut anmelden",
|
||||
"deleteAccount": "OSM-Konto Löschen",
|
||||
"deleteAccountSubtitle": "Ihr OpenStreetMap-Konto verwalten",
|
||||
"deleteAccountExplanation": "Um Ihr OpenStreetMap-Konto zu löschen, müssen Sie die OpenStreetMap-Website besuchen. Dies entfernt dauerhaft Ihr OSM-Konto und alle zugehörigen Daten.",
|
||||
"deleteAccountWarning": "Warnung: Diese Aktion kann nicht rückgängig gemacht werden und löscht Ihr OSM-Konto dauerhaft.",
|
||||
"goToOSM": "Zu OpenStreetMap gehen"
|
||||
},
|
||||
"queue": {
|
||||
"pendingUploads": "Ausstehende Uploads: {}",
|
||||
@@ -306,6 +311,16 @@
|
||||
"initiative": "Teil der breiteren DeFlock-Initiative zur Förderung von Überwachungstransparenz.",
|
||||
"footer": "Besuchen Sie: deflock.me\nGebaut mit Flutter • Open Source"
|
||||
},
|
||||
"welcome": {
|
||||
"title": "Willkommen bei DeFlock",
|
||||
"description": "DeFlock wurde auf der Idee gegründet, dass öffentliche Überwachungsinstrumente transparent sein sollten. In dieser mobilen App, wie auch auf der Website, können Sie die Standorte von ALPRs und anderer Überwachungsinfrastruktur in Ihrer Umgebung und weltweit einsehen.",
|
||||
"mission": "Dieses Projekt ist jedoch nicht automatisiert; es braucht uns alle, um dieses Projekt zu verbessern. Bei der Kartenansicht können Sie auf \"Neuer Knoten\" tippen, um eine bisher unbekannte Installation hinzuzufügen. Mit Ihrer Hilfe können wir unser Ziel erreichen: mehr Transparenz und öffentliches Bewusstsein für Überwachungsinfrastruktur.",
|
||||
"privacy": "Datenschutzhinweis: Diese App läuft vollständig lokal auf Ihrem Gerät und nutzt die OpenStreetMap-API von Drittanbietern nur für Datenspeicherung und Übermittlungen. DeFlock sammelt oder speichert keinerlei Nutzerdaten und ist nicht für die Kontoverwaltung verantwortlich.",
|
||||
"tileNote": "HINWEIS: Die kostenlosen Kartenkacheln von OpenStreetMap können sehr langsam laden. Alternative Kartenanbieter können unter Einstellungen > Erweitert konfiguriert werden.",
|
||||
"moreInfo": "Weitere Links finden Sie unter Einstellungen > Über.",
|
||||
"dontShowAgain": "Diese Willkommensnachricht nicht mehr anzeigen",
|
||||
"getStarted": "Los geht's mit DeFlocking!"
|
||||
},
|
||||
"navigation": {
|
||||
"searchLocation": "Ort suchen",
|
||||
"searchPlaceholder": "Orte oder Koordinaten suchen...",
|
||||
|
||||
@@ -12,6 +12,16 @@
|
||||
"initiative": "Part of the broader DeFlock initiative to promote surveillance transparency.",
|
||||
"footer": "Visit: deflock.me\nBuilt with Flutter • Open Source"
|
||||
},
|
||||
"welcome": {
|
||||
"title": "Welcome to DeFlock",
|
||||
"description": "DeFlock was founded on the idea that public surveillance tools should be transparent. Within this mobile app, as on the website, you will be able to view the location of ALPRs and other surveillance infrastructure in your local area and abroad.",
|
||||
"mission": "However, this project isn't automated; it takes all of us to make this project better. When viewing the map, you can tap \"New Node\" to add a previously unknown installation. With your help, we can achieve our goal of increased transparency and public awareness of surveillance infrastructure.",
|
||||
"privacy": "Privacy Note: This app runs entirely locally on your device and uses the third-party OpenStreetMap API for data storage and submissions. DeFlock does not collect or store any user data of any kind, and is not responsible for account management.",
|
||||
"tileNote": "NOTE: The free map tiles from OpenStreetMap can be very slow to load. Alternate tile providers can be configured in Settings > Advanced.",
|
||||
"moreInfo": "You can find more links under Settings > About.",
|
||||
"dontShowAgain": "Don't show this welcome message again",
|
||||
"getStarted": "Let's Get DeFlocking!"
|
||||
},
|
||||
"actions": {
|
||||
"tagNode": "New Node",
|
||||
"download": "Download",
|
||||
@@ -131,7 +141,12 @@
|
||||
"testConnection": "Test Connection",
|
||||
"testConnectionSubtitle": "Verify OSM credentials are working",
|
||||
"connectionOK": "Connection OK - credentials are valid",
|
||||
"connectionFailed": "Connection failed - please re-login"
|
||||
"connectionFailed": "Connection failed - please re-login",
|
||||
"deleteAccount": "Delete OSM Account",
|
||||
"deleteAccountSubtitle": "Manage your OpenStreetMap account",
|
||||
"deleteAccountExplanation": "To delete your OpenStreetMap account, you'll need to visit the OpenStreetMap website. This will permanently remove your OSM account and all associated data.",
|
||||
"deleteAccountWarning": "Warning: This action cannot be undone and will permanently delete your OSM account.",
|
||||
"goToOSM": "Go to OpenStreetMap"
|
||||
},
|
||||
"queue": {
|
||||
"pendingUploads": "Pending uploads: {}",
|
||||
|
||||
@@ -12,6 +12,16 @@
|
||||
"initiative": "Parte de la iniciativa más amplia DeFlock para promover la transparencia en vigilancia.",
|
||||
"footer": "Visita: deflock.me\nConstruido con Flutter • Código Abierto"
|
||||
},
|
||||
"welcome": {
|
||||
"title": "Bienvenido a DeFlock",
|
||||
"description": "DeFlock fue fundado sobre la idea de que las herramientas de vigilancia pública deben ser transparentes. Dentro de esta aplicación móvil, como en el sitio web, podrás ver la ubicación de ALPRs y otra infraestructura de vigilancia en tu área local y en el extranjero.",
|
||||
"mission": "Sin embargo, este proyecto no es automatizado; todos nosotros somos necesarios para mejorarlo. Al ver el mapa, puedes tocar \"Nuevo Nodo\" para agregar una instalación previamente desconocida. Con tu ayuda, podemos lograr nuestro objetivo de mayor transparencia y conciencia pública sobre la infraestructura de vigilancia.",
|
||||
"privacy": "Nota de Privacidad: Esta aplicación funciona completamente de forma local en tu dispositivo y utiliza la API de terceros de OpenStreetMap solo para almacenamiento y envío de datos. DeFlock no recopila ni almacena ningún tipo de datos de usuario, y no es responsable de la gestión de cuentas.",
|
||||
"tileNote": "NOTA: Los mosaicos gratuitos de mapa de OpenStreetMap pueden tardar mucho en cargar. Se pueden configurar proveedores alternativos de mosaicos en Configuración > Avanzado.",
|
||||
"moreInfo": "Puedes encontrar más enlaces en Configuración > Acerca de.",
|
||||
"dontShowAgain": "No mostrar este mensaje de bienvenida otra vez",
|
||||
"getStarted": "¡Comencemos con DeFlock!"
|
||||
},
|
||||
"actions": {
|
||||
"tagNode": "Nuevo Nodo",
|
||||
"download": "Descargar",
|
||||
@@ -131,7 +141,12 @@
|
||||
"testConnection": "Probar Conexión",
|
||||
"testConnectionSubtitle": "Verificar que las credenciales de OSM funcionen",
|
||||
"connectionOK": "Conexión OK - las credenciales son válidas",
|
||||
"connectionFailed": "Conexión falló - por favor, inicie sesión nuevamente"
|
||||
"connectionFailed": "Conexión falló - por favor, inicie sesión nuevamente",
|
||||
"deleteAccount": "Eliminar Cuenta OSM",
|
||||
"deleteAccountSubtitle": "Gestiona tu cuenta de OpenStreetMap",
|
||||
"deleteAccountExplanation": "Para eliminar tu cuenta de OpenStreetMap, necesitarás visitar el sitio web de OpenStreetMap. Esto eliminará permanentemente tu cuenta OSM y todos los datos asociados.",
|
||||
"deleteAccountWarning": "Advertencia: Esta acción no se puede deshacer y eliminará permanentemente tu cuenta OSM.",
|
||||
"goToOSM": "Ir a OpenStreetMap"
|
||||
},
|
||||
"queue": {
|
||||
"pendingUploads": "Subidas pendientes: {}",
|
||||
|
||||
@@ -12,6 +12,16 @@
|
||||
"initiative": "Partie de l'initiative plus large DeFlock pour promouvoir la transparence de la surveillance.",
|
||||
"footer": "Visitez : deflock.me\nConstruit avec Flutter • Source Ouverte"
|
||||
},
|
||||
"welcome": {
|
||||
"title": "Bienvenue dans DeFlock",
|
||||
"description": "DeFlock a été fondé sur l'idée que les outils de surveillance publique devraient être transparents. Dans cette application mobile, comme sur le site web, vous pourrez voir l'emplacement des ALPRs et autres infrastructures de surveillance dans votre région et à l'étranger.",
|
||||
"mission": "Cependant, ce projet n'est pas automatisé ; il nous faut tous pour améliorer ce projet. En visualisant la carte, vous pouvez appuyer sur \"Nouveau Nœud\" pour ajouter une installation précédemment inconnue. Avec votre aide, nous pouvons atteindre notre objectif d'augmenter la transparence et la sensibilisation du public à l'infrastructure de surveillance.",
|
||||
"privacy": "Note de Confidentialité : Cette application fonctionne entièrement localement sur votre appareil et utilise l'API tierce OpenStreetMap uniquement pour le stockage et la soumission de données. DeFlock ne collecte ni ne stocke aucune donnée utilisateur de quelque nature que ce soit, et n'est pas responsable de la gestion des comptes.",
|
||||
"tileNote": "NOTE : Les tuiles de carte gratuites d'OpenStreetMap peuvent être très lentes à charger. Des fournisseurs de tuiles alternatifs peuvent être configurés dans Paramètres > Avancé.",
|
||||
"moreInfo": "Vous pouvez trouver plus de liens sous Paramètres > À propos.",
|
||||
"dontShowAgain": "Ne plus afficher ce message de bienvenue",
|
||||
"getStarted": "Commençons le DeFlock !"
|
||||
},
|
||||
"actions": {
|
||||
"tagNode": "Nouveau Nœud",
|
||||
"download": "Télécharger",
|
||||
@@ -131,7 +141,12 @@
|
||||
"testConnection": "Tester Connexion",
|
||||
"testConnectionSubtitle": "Vérifier que les identifiants OSM fonctionnent",
|
||||
"connectionOK": "Connexion OK - les identifiants sont valides",
|
||||
"connectionFailed": "Connexion échouée - veuillez vous reconnecter"
|
||||
"connectionFailed": "Connexion échouée - veuillez vous reconnecter",
|
||||
"deleteAccount": "Supprimer Compte OSM",
|
||||
"deleteAccountSubtitle": "Gérez votre compte OpenStreetMap",
|
||||
"deleteAccountExplanation": "Pour supprimer votre compte OpenStreetMap, vous devrez visiter le site web OpenStreetMap. Cela supprimera définitivement votre compte OSM et toutes les données associées.",
|
||||
"deleteAccountWarning": "Attention : Cette action ne peut pas être annulée et supprimera définitivement votre compte OSM.",
|
||||
"goToOSM": "Aller à OpenStreetMap"
|
||||
},
|
||||
"queue": {
|
||||
"pendingUploads": "Téléchargements en attente: {}",
|
||||
|
||||
@@ -12,6 +12,16 @@
|
||||
"initiative": "Parte della più ampia iniziativa DeFlock per promuovere la trasparenza della sorveglianza.",
|
||||
"footer": "Visita: deflock.me\nCostruito con Flutter • Open Source"
|
||||
},
|
||||
"welcome": {
|
||||
"title": "Benvenuto in DeFlock",
|
||||
"description": "DeFlock è stato fondato sull'idea che gli strumenti di sorveglianza pubblica dovrebbero essere trasparenti. All'interno di questa app mobile, come sul sito web, sarai in grado di visualizzare la posizione di ALPR e altre infrastrutture di sorveglianza nella tua zona locale e all'estero.",
|
||||
"mission": "Tuttavia, questo progetto non è automatizzato; servono tutti noi per migliorare questo progetto. Durante la visualizzazione della mappa, puoi toccare \"Nuovo Nodo\" per aggiungere un'installazione precedentemente sconosciuta. Con il tuo aiuto, possiamo raggiungere il nostro obiettivo di maggiore trasparenza e consapevolezza pubblica dell'infrastruttura di sorveglianza.",
|
||||
"privacy": "Nota sulla Privacy: Questa app funziona interamente localmente sul tuo dispositivo e utilizza l'API di terze parti OpenStreetMap solo per l'archiviazione e l'invio dei dati. DeFlock non raccoglie né memorizza alcun tipo di dati utente e non è responsabile della gestione degli account.",
|
||||
"tileNote": "NOTA: Le tessere mappa gratuite di OpenStreetMap possono essere molto lente a caricare. Fornitori di tessere alternativi possono essere configurati in Impostazioni > Avanzate.",
|
||||
"moreInfo": "Puoi trovare altri collegamenti in Impostazioni > Informazioni.",
|
||||
"dontShowAgain": "Non mostrare più questo messaggio di benvenuto",
|
||||
"getStarted": "Iniziamo con DeFlock!"
|
||||
},
|
||||
"actions": {
|
||||
"tagNode": "Nuovo Nodo",
|
||||
"download": "Scarica",
|
||||
@@ -131,7 +141,12 @@
|
||||
"testConnection": "Testa Connessione",
|
||||
"testConnectionSubtitle": "Verifica che le credenziali OSM funzionino",
|
||||
"connectionOK": "Connessione OK - le credenziali sono valide",
|
||||
"connectionFailed": "Connessione fallita - per favore accedi di nuovo"
|
||||
"connectionFailed": "Connessione fallita - per favore accedi di nuovo",
|
||||
"deleteAccount": "Elimina Account OSM",
|
||||
"deleteAccountSubtitle": "Gestisci il tuo account OpenStreetMap",
|
||||
"deleteAccountExplanation": "Per eliminare il tuo account OpenStreetMap, dovrai visitare il sito web di OpenStreetMap. Questo rimuoverà permanentemente il tuo account OSM e tutti i dati associati.",
|
||||
"deleteAccountWarning": "Attenzione: Questa azione non può essere annullata e eliminerà permanentemente il tuo account OSM.",
|
||||
"goToOSM": "Vai a OpenStreetMap"
|
||||
},
|
||||
"queue": {
|
||||
"pendingUploads": "Upload in sospeso: {}",
|
||||
|
||||
@@ -12,6 +12,16 @@
|
||||
"initiative": "Parte da iniciativa mais ampla DeFlock para promover transparência na vigilância.",
|
||||
"footer": "Visite: deflock.me\nConstruído com Flutter • Código Aberto"
|
||||
},
|
||||
"welcome": {
|
||||
"title": "Bem-vindo ao DeFlock",
|
||||
"description": "DeFlock foi fundado na ideia de que ferramentas de vigilância pública devem ser transparentes. Dentro deste aplicativo móvel, como no site, você poderá ver a localização de ALPRs e outras infraestruturas de vigilância em sua área local e no exterior.",
|
||||
"mission": "No entanto, este projeto não é automatizado; precisamos de todos nós para tornar este projeto melhor. Ao visualizar o mapa, você pode tocar em \"Novo Nó\" para adicionar uma instalação anteriormente desconhecida. Com sua ajuda, podemos alcançar nosso objetivo de maior transparência e conscientização pública sobre infraestrutura de vigilância.",
|
||||
"privacy": "Nota de Privacidade: Este aplicativo funciona inteiramente localmente em seu dispositivo e usa a API de terceiros OpenStreetMap apenas para armazenamento e envio de dados. DeFlock não coleta nem armazena qualquer tipo de dados do usuário e não é responsável pelo gerenciamento de contas.",
|
||||
"tileNote": "NOTA: Os tiles gratuitos de mapa do OpenStreetMap podem ser muito lentos para carregar. Provedores alternativos de tiles podem ser configurados em Configurações > Avançado.",
|
||||
"moreInfo": "Você pode encontrar mais links em Configurações > Sobre.",
|
||||
"dontShowAgain": "Não mostrar esta mensagem de boas-vindas novamente",
|
||||
"getStarted": "Vamos começar com o DeFlock!"
|
||||
},
|
||||
"actions": {
|
||||
"tagNode": "Novo Nó",
|
||||
"download": "Baixar",
|
||||
@@ -131,7 +141,12 @@
|
||||
"testConnection": "Testar Conexão",
|
||||
"testConnectionSubtitle": "Verificar se as credenciais OSM estão funcionando",
|
||||
"connectionOK": "Conexão OK - credenciais são válidas",
|
||||
"connectionFailed": "Conexão falhou - por favor, faça login novamente"
|
||||
"connectionFailed": "Conexão falhou - por favor, faça login novamente",
|
||||
"deleteAccount": "Excluir Conta OSM",
|
||||
"deleteAccountSubtitle": "Gerencie sua conta OpenStreetMap",
|
||||
"deleteAccountExplanation": "Para excluir sua conta OpenStreetMap, você precisará visitar o site do OpenStreetMap. Isso removerá permanentemente sua conta OSM e todos os dados associados.",
|
||||
"deleteAccountWarning": "Aviso: Esta ação não pode ser desfeita e excluirá permanentemente sua conta OSM.",
|
||||
"goToOSM": "Ir para OpenStreetMap"
|
||||
},
|
||||
"queue": {
|
||||
"pendingUploads": "Uploads pendentes: {}",
|
||||
|
||||
@@ -12,6 +12,16 @@
|
||||
"initiative": "DeFlock 更广泛倡议的一部分,旨在促进监控透明化。",
|
||||
"footer": "访问:deflock.me\n使用 Flutter 构建 • 开源"
|
||||
},
|
||||
"welcome": {
|
||||
"title": "欢迎使用 DeFlock",
|
||||
"description": "DeFlock 的创立基于公共监控工具应该透明的理念。在这个移动应用程序中,就像在网站上一样,您将能够查看您当地和国外的车牌识别系统和其他监控基础设施的位置。",
|
||||
"mission": "然而,这个项目不是自动化的;需要我们所有人来改善这个项目。在查看地图时,您可以点击\"新建节点\"来添加一个之前未知的装置。在您的帮助下,我们可以实现增强监控基础设施透明度和公众意识的目标。",
|
||||
"privacy": "隐私说明:此应用程序完全在您的设备上本地运行,仅使用第三方 OpenStreetMap API 进行数据存储和提交。DeFlock 不收集或存储任何类型的用户数据,也不负责账户管理。",
|
||||
"tileNote": "注意:来自 OpenStreetMap 的免费地图图块可能加载很慢。可以在设置 > 高级中配置替代图块提供商。",
|
||||
"moreInfo": "您可以在设置 > 关于中找到更多链接。",
|
||||
"dontShowAgain": "不再显示此欢迎消息",
|
||||
"getStarted": "开始使用 DeFlock!"
|
||||
},
|
||||
"actions": {
|
||||
"tagNode": "新建节点",
|
||||
"download": "下载",
|
||||
@@ -131,7 +141,12 @@
|
||||
"testConnection": "测试连接",
|
||||
"testConnectionSubtitle": "验证 OSM 凭据是否有效",
|
||||
"connectionOK": "连接正常 - 凭据有效",
|
||||
"connectionFailed": "连接失败 - 请重新登录"
|
||||
"connectionFailed": "连接失败 - 请重新登录",
|
||||
"deleteAccount": "删除 OSM 账户",
|
||||
"deleteAccountSubtitle": "管理您的 OpenStreetMap 账户",
|
||||
"deleteAccountExplanation": "要删除您的 OpenStreetMap 账户,您需要访问 OpenStreetMap 网站。这将永久删除您的 OSM 账户和所有相关数据。",
|
||||
"deleteAccountWarning": "警告:此操作无法撤销,将永久删除您的 OSM 账户。",
|
||||
"goToOSM": "前往 OpenStreetMap"
|
||||
},
|
||||
"queue": {
|
||||
"pendingUploads": "待上传:{}",
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'screens/offline_settings_screen.dart';
|
||||
import 'screens/advanced_settings_screen.dart';
|
||||
import 'screens/language_settings_screen.dart';
|
||||
import 'screens/about_screen.dart';
|
||||
import 'screens/release_notes_screen.dart';
|
||||
import 'services/localization_service.dart';
|
||||
import 'services/version_service.dart';
|
||||
|
||||
@@ -74,6 +75,7 @@ class DeFlockApp extends StatelessWidget {
|
||||
'/settings/advanced': (context) => const AdvancedSettingsScreen(),
|
||||
'/settings/language': (context) => const LanguageSettingsScreen(),
|
||||
'/settings/about': (context) => const AboutScreen(),
|
||||
'/settings/release-notes': (context) => const ReleaseNotesScreen(),
|
||||
},
|
||||
initialRoute: '/',
|
||||
);
|
||||
|
||||
@@ -68,7 +68,18 @@ class AboutScreen extends StatelessWidget {
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: 24),
|
||||
// Release Notes button
|
||||
Center(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.pushNamed(context, '/settings/release-notes');
|
||||
},
|
||||
icon: const Icon(Icons.article_outlined),
|
||||
label: const Text('View Release Notes'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildHelpLinks(context),
|
||||
],
|
||||
),
|
||||
@@ -93,10 +104,76 @@ class AboutScreen extends StatelessWidget {
|
||||
_buildLinkText(context, 'Contact', 'https://deflock.me/contact'),
|
||||
const SizedBox(height: 8),
|
||||
_buildLinkText(context, 'Donate', 'https://deflock.me/donate'),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Divider for account management section
|
||||
Divider(
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.3),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Account deletion link (less prominent)
|
||||
_buildAccountDeletionLink(context),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAccountDeletionLink(BuildContext context) {
|
||||
final locService = LocalizationService.instance;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => _showDeleteAccountDialog(context, locService),
|
||||
child: Text(
|
||||
locService.t('auth.deleteAccount'),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.error.withOpacity(0.7),
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteAccountDialog(BuildContext context, LocalizationService locService) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(locService.t('auth.deleteAccount')),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(locService.t('auth.deleteAccountExplanation')),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
locService.t('auth.deleteAccountWarning'),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(locService.t('actions.cancel')),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
_launchUrl('https://www.openstreetmap.org/account/deletion', context);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
child: Text(locService.t('auth.goToOSM')),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLinkText(BuildContext context, String text, String url) {
|
||||
return GestureDetector(
|
||||
onTap: () => _launchUrl(url, context),
|
||||
|
||||
@@ -19,9 +19,12 @@ import '../widgets/measured_sheet.dart';
|
||||
import '../widgets/navigation_sheet.dart';
|
||||
import '../widgets/search_bar.dart';
|
||||
import '../widgets/suspected_location_sheet.dart';
|
||||
import '../widgets/welcome_dialog.dart';
|
||||
import '../widgets/changelog_dialog.dart';
|
||||
import '../models/osm_node.dart';
|
||||
import '../models/suspected_location.dart';
|
||||
import '../models/search_result.dart';
|
||||
import '../services/changelog_service.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
@@ -48,6 +51,9 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||
|
||||
// Track selected node for highlighting
|
||||
int? _selectedNodeId;
|
||||
|
||||
// Track popup display to avoid showing multiple times
|
||||
bool _hasCheckedForPopup = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -219,6 +225,52 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||
});
|
||||
}
|
||||
|
||||
// Check for and display welcome/changelog popup
|
||||
Future<void> _checkForPopup() async {
|
||||
if (!mounted) return;
|
||||
|
||||
try {
|
||||
final popupType = await ChangelogService().getPopupType();
|
||||
|
||||
if (!mounted) return; // Check again after async operation
|
||||
|
||||
switch (popupType) {
|
||||
case PopupType.welcome:
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => const WelcomeDialog(),
|
||||
);
|
||||
break;
|
||||
|
||||
case PopupType.changelog:
|
||||
final changelogContent = ChangelogService().getChangelogForCurrentVersion();
|
||||
if (changelogContent != null) {
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => ChangelogDialog(changelogContent: changelogContent),
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case PopupType.none:
|
||||
// No popup needed, but still update version tracking for future launches
|
||||
await ChangelogService().updateLastSeenVersion();
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
// Silently handle errors to avoid breaking the app launch
|
||||
debugPrint('[HomeScreen] Error checking for popup: $e');
|
||||
// Still update version tracking in case of error
|
||||
try {
|
||||
await ChangelogService().updateLastSeenVersion();
|
||||
} catch (e2) {
|
||||
debugPrint('[HomeScreen] Error updating version: $e2');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onStartRoute() {
|
||||
final appState = context.read<AppState>();
|
||||
|
||||
@@ -526,6 +578,12 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for welcome/changelog popup after app is fully initialized
|
||||
if (appState.isInitialized && !_hasCheckedForPopup) {
|
||||
_hasCheckedForPopup = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _checkForPopup());
|
||||
}
|
||||
|
||||
// Pass the active sheet height directly to the map
|
||||
final activeSheetHeight = _addSheetHeight > 0
|
||||
? _addSheetHeight
|
||||
|
||||
191
lib/screens/release_notes_screen.dart
Normal file
191
lib/screens/release_notes_screen.dart
Normal file
@@ -0,0 +1,191 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/changelog_service.dart';
|
||||
import '../services/version_service.dart';
|
||||
|
||||
class ReleaseNotesScreen extends StatefulWidget {
|
||||
const ReleaseNotesScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ReleaseNotesScreen> createState() => _ReleaseNotesScreenState();
|
||||
}
|
||||
|
||||
class _ReleaseNotesScreenState extends State<ReleaseNotesScreen> {
|
||||
Map<String, String>? _changelogs;
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadChangelogs();
|
||||
}
|
||||
|
||||
Future<void> _loadChangelogs() async {
|
||||
try {
|
||||
// Ensure changelog service is initialized
|
||||
if (!ChangelogService().isInitialized) {
|
||||
await ChangelogService().init();
|
||||
}
|
||||
|
||||
final changelogs = ChangelogService().getAllChangelogs();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_changelogs = changelogs;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[ReleaseNotesScreen] Error loading changelogs: $e');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_changelogs = {};
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<String> _sortVersions(List<String> versions) {
|
||||
// Simple version sorting - splits by '.' and compares numerically
|
||||
versions.sort((a, b) {
|
||||
final aParts = a.split('.').map(int.tryParse).where((v) => v != null).cast<int>().toList();
|
||||
final bParts = b.split('.').map(int.tryParse).where((v) => v != null).cast<int>().toList();
|
||||
|
||||
// Compare version parts (reverse order for newest first)
|
||||
for (int i = 0; i < aParts.length && i < bParts.length; i++) {
|
||||
final comparison = bParts[i].compareTo(aParts[i]); // Reverse for desc order
|
||||
if (comparison != 0) return comparison;
|
||||
}
|
||||
|
||||
// If one version has more parts, the longer one is newer
|
||||
return bParts.length.compareTo(aParts.length);
|
||||
});
|
||||
|
||||
return versions;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Release Notes'),
|
||||
),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _changelogs == null || _changelogs!.isEmpty
|
||||
? const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24.0),
|
||||
child: Text(
|
||||
'No release notes available.',
|
||||
style: TextStyle(fontSize: 16),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
// Current version indicator
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Current Version: ${VersionService().version}',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Changelog entries
|
||||
..._buildChangelogEntries(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildChangelogEntries() {
|
||||
if (_changelogs == null || _changelogs!.isEmpty) return [];
|
||||
|
||||
final sortedVersions = _sortVersions(_changelogs!.keys.toList());
|
||||
final currentVersion = VersionService().version;
|
||||
|
||||
return sortedVersions.map((version) {
|
||||
final content = _changelogs![version]!;
|
||||
final isCurrentVersion = version == currentVersion;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: isCurrentVersion
|
||||
? Theme.of(context).colorScheme.primary.withOpacity(0.3)
|
||||
: Theme.of(context).dividerColor.withOpacity(0.3),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: ExpansionTile(
|
||||
title: Row(
|
||||
children: [
|
||||
Text(
|
||||
'Version $version',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isCurrentVersion
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
),
|
||||
if (isCurrentVersion) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'CURRENT',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
content,
|
||||
style: const TextStyle(height: 1.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
154
lib/services/changelog_service.dart
Normal file
154
lib/services/changelog_service.dart
Normal file
@@ -0,0 +1,154 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'version_service.dart';
|
||||
|
||||
/// Service for managing changelog data and first launch detection
|
||||
class ChangelogService {
|
||||
static final ChangelogService _instance = ChangelogService._internal();
|
||||
factory ChangelogService() => _instance;
|
||||
ChangelogService._internal();
|
||||
|
||||
static const String _lastSeenVersionKey = 'last_seen_version';
|
||||
static const String _hasSeenWelcomeKey = 'has_seen_welcome';
|
||||
|
||||
Map<String, dynamic>? _changelogData;
|
||||
bool _initialized = false;
|
||||
|
||||
/// Initialize the service by loading changelog data
|
||||
Future<void> init() async {
|
||||
if (_initialized) return;
|
||||
|
||||
try {
|
||||
final String jsonString = await rootBundle.loadString('assets/changelog.json');
|
||||
_changelogData = json.decode(jsonString);
|
||||
_initialized = true;
|
||||
debugPrint('[ChangelogService] Loaded changelog with ${_changelogData?.keys.length ?? 0} versions');
|
||||
} catch (e) {
|
||||
debugPrint('[ChangelogService] Failed to load changelog: $e');
|
||||
_changelogData = {};
|
||||
_initialized = true; // Mark as initialized even on failure to prevent repeated attempts
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this is the first app launch ever
|
||||
Future<bool> isFirstLaunch() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return !prefs.containsKey(_lastSeenVersionKey);
|
||||
}
|
||||
|
||||
/// Check if user has seen the welcome popup (separate from version tracking)
|
||||
Future<bool> hasSeenWelcome() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_hasSeenWelcomeKey) ?? false;
|
||||
}
|
||||
|
||||
/// Mark that user has seen the welcome popup
|
||||
Future<void> markWelcomeSeen() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_hasSeenWelcomeKey, true);
|
||||
}
|
||||
|
||||
/// Check if app version has changed since last launch
|
||||
Future<bool> hasVersionChanged() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final lastSeenVersion = prefs.getString(_lastSeenVersionKey);
|
||||
final currentVersion = VersionService().version;
|
||||
|
||||
return lastSeenVersion != currentVersion;
|
||||
}
|
||||
|
||||
/// Update the stored version to current version
|
||||
Future<void> updateLastSeenVersion() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final currentVersion = VersionService().version;
|
||||
await prefs.setString(_lastSeenVersionKey, currentVersion);
|
||||
debugPrint('[ChangelogService] Updated last seen version to: $currentVersion');
|
||||
}
|
||||
|
||||
/// Get changelog content for the current version
|
||||
String? getChangelogForCurrentVersion() {
|
||||
if (!_initialized || _changelogData == null) {
|
||||
debugPrint('[ChangelogService] Not initialized or no changelog data');
|
||||
return null;
|
||||
}
|
||||
|
||||
final currentVersion = VersionService().version;
|
||||
final versionData = _changelogData![currentVersion] as Map<String, dynamic>?;
|
||||
|
||||
if (versionData == null) {
|
||||
debugPrint('[ChangelogService] No changelog entry found for version: $currentVersion');
|
||||
return null;
|
||||
}
|
||||
|
||||
final content = versionData['content'] as String?;
|
||||
return (content?.isEmpty == true) ? null : content;
|
||||
}
|
||||
|
||||
/// Get changelog content for a specific version
|
||||
String? getChangelogForVersion(String version) {
|
||||
if (!_initialized || _changelogData == null) return null;
|
||||
|
||||
final versionData = _changelogData![version] as Map<String, dynamic>?;
|
||||
if (versionData == null) return null;
|
||||
|
||||
final content = versionData['content'] as String?;
|
||||
return (content?.isEmpty == true) ? null : content;
|
||||
}
|
||||
|
||||
/// Get all changelog entries (for settings page)
|
||||
Map<String, String> getAllChangelogs() {
|
||||
if (!_initialized || _changelogData == null) return {};
|
||||
|
||||
final Map<String, String> result = {};
|
||||
|
||||
for (final entry in _changelogData!.entries) {
|
||||
final version = entry.key;
|
||||
final versionData = entry.value as Map<String, dynamic>?;
|
||||
final content = versionData?['content'] as String?;
|
||||
|
||||
// Only include versions with non-empty content
|
||||
if (content != null && content.isNotEmpty) {
|
||||
result[version] = content;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Determine what popup (if any) should be shown
|
||||
Future<PopupType> getPopupType() async {
|
||||
// Ensure services are initialized
|
||||
if (!_initialized) await init();
|
||||
|
||||
final isFirstLaunch = await this.isFirstLaunch();
|
||||
final hasSeenWelcome = await this.hasSeenWelcome();
|
||||
final hasVersionChanged = await this.hasVersionChanged();
|
||||
|
||||
// First launch and haven't seen welcome
|
||||
if (isFirstLaunch || !hasSeenWelcome) {
|
||||
return PopupType.welcome;
|
||||
}
|
||||
|
||||
// Version changed and there's changelog content
|
||||
if (hasVersionChanged) {
|
||||
final changelogContent = getChangelogForCurrentVersion();
|
||||
if (changelogContent != null) {
|
||||
return PopupType.changelog;
|
||||
}
|
||||
}
|
||||
|
||||
return PopupType.none;
|
||||
}
|
||||
|
||||
/// Check if the service is properly initialized
|
||||
bool get isInitialized => _initialized;
|
||||
}
|
||||
|
||||
/// Types of popups that can be shown
|
||||
enum PopupType {
|
||||
none,
|
||||
welcome,
|
||||
changelog,
|
||||
}
|
||||
55
lib/widgets/changelog_dialog.dart
Normal file
55
lib/widgets/changelog_dialog.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/changelog_service.dart';
|
||||
import '../services/version_service.dart';
|
||||
|
||||
class ChangelogDialog extends StatelessWidget {
|
||||
final String changelogContent;
|
||||
|
||||
const ChangelogDialog({
|
||||
super.key,
|
||||
required this.changelogContent,
|
||||
});
|
||||
|
||||
void _onClose(BuildContext context) async {
|
||||
// Update version tracking when closing changelog dialog
|
||||
await ChangelogService().updateLastSeenVersion();
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('What\'s New in v${VersionService().version}'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
changelogContent,
|
||||
style: const TextStyle(fontSize: 14, height: 1.4),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Thank you for keeping DeFlock up to date!',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => _onClose(context),
|
||||
child: const Text('Continue'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
142
lib/widgets/welcome_dialog.dart
Normal file
142
lib/widgets/welcome_dialog.dart
Normal file
@@ -0,0 +1,142 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../services/changelog_service.dart';
|
||||
import '../services/localization_service.dart';
|
||||
|
||||
class WelcomeDialog extends StatefulWidget {
|
||||
const WelcomeDialog({super.key});
|
||||
|
||||
@override
|
||||
State<WelcomeDialog> createState() => _WelcomeDialogState();
|
||||
}
|
||||
|
||||
class _WelcomeDialogState extends State<WelcomeDialog> {
|
||||
bool _dontShowAgain = false;
|
||||
|
||||
Future<void> _launchUrl(String url) async {
|
||||
final uri = Uri.parse(url);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
}
|
||||
|
||||
void _onClose() async {
|
||||
if (_dontShowAgain) {
|
||||
await ChangelogService().markWelcomeSeen();
|
||||
}
|
||||
|
||||
// Always update version tracking when closing welcome dialog
|
||||
await ChangelogService().updateLastSeenVersion();
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final locService = LocalizationService.instance;
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: LocalizationService.instance,
|
||||
builder: (context, child) => AlertDialog(
|
||||
title: Text(locService.t('welcome.title')),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
locService.t('welcome.description'),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
locService.t('welcome.mission'),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
locService.t('welcome.privacy'),
|
||||
style: const TextStyle(fontSize: 13, fontStyle: FontStyle.italic),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
locService.t('welcome.tileNote'),
|
||||
style: const TextStyle(fontSize: 13, color: Colors.orange),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
locService.t('welcome.moreInfo'),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Quick links row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildLinkButton('Website', 'https://deflock.me'),
|
||||
_buildLinkButton('GitHub', 'https://github.com/FoggedLens/deflock-app'),
|
||||
_buildLinkButton('Discord', 'https://discord.gg/aV7v4R3sKT'),
|
||||
_buildLinkButton('Donate', 'https://deflock.me/donate'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Don't show again checkbox
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: _dontShowAgain,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_dontShowAgain = value ?? false;
|
||||
});
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
locService.t('welcome.dontShowAgain'),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _onClose,
|
||||
child: Text(locService.t('welcome.getStarted')),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLinkButton(String text, String url) {
|
||||
return Flexible(
|
||||
child: GestureDetector(
|
||||
onTap: () => _launchUrl(url),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
name: deflockapp
|
||||
description: Map public surveillance infrastructure with OpenStreetMap
|
||||
publish_to: "none"
|
||||
version: 1.2.3+4 # The thing after the + is the version code, incremented with each release
|
||||
version: 1.2.4+5 # The thing after the + is the version code, incremented with each release
|
||||
|
||||
environment:
|
||||
sdk: ">=3.5.0 <4.0.0" # oauth2_client 4.x needs Dart 3.5+
|
||||
@@ -45,6 +45,7 @@ flutter:
|
||||
- assets/android_app_icon.png
|
||||
- assets/transparent_1x1.png
|
||||
- assets/deflock-logo.svg
|
||||
- assets/changelog.json
|
||||
- lib/localizations/
|
||||
|
||||
flutter_launcher_icons:
|
||||
|
||||
Reference in New Issue
Block a user