mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-05-20 07:25:13 +02:00
Only show appropriate external editors on each platform, redirect to appstore on error
This commit is contained in:
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"1.4.0": {
|
||||
"content": [
|
||||
"• IMPROVED: Advanced editing options now only show apps available on your platform (iOS/Android)",
|
||||
"• IMPROVED: When an OSM editor app isn't installed, automatically redirect to the appropriate app store",
|
||||
"• IMPROVED: Better error handling for external editor launches with app store fallback",
|
||||
"• Supported editors: Vespucci (Android), StreetComplete (Android), EveryDoor (both), Go Map!! (iOS)",
|
||||
"• Web editors (iD, RapiD) remain available on all platforms as before"
|
||||
]
|
||||
},
|
||||
"1.3.4": {
|
||||
"content": [
|
||||
"• NEW: 'Pause Upload Queue' toggle in Offline Settings - stops uploads while keeping live data access",
|
||||
|
||||
@@ -4,15 +4,106 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
import '../models/osm_node.dart';
|
||||
import '../services/localization_service.dart';
|
||||
|
||||
/// Information about an OSM editor app
|
||||
class EditorInfo {
|
||||
final String name;
|
||||
final String subtitle;
|
||||
final IconData icon;
|
||||
final String? urlScheme; // null means no custom scheme - go straight to store
|
||||
final String? androidStoreUrl;
|
||||
final String? iosStoreUrl;
|
||||
final bool availableOnAndroid;
|
||||
final bool availableOnIOS;
|
||||
|
||||
const EditorInfo({
|
||||
required this.name,
|
||||
required this.subtitle,
|
||||
required this.icon,
|
||||
this.urlScheme, // Made optional
|
||||
this.androidStoreUrl,
|
||||
this.iosStoreUrl,
|
||||
required this.availableOnAndroid,
|
||||
required this.availableOnIOS,
|
||||
});
|
||||
}
|
||||
|
||||
class AdvancedEditOptionsSheet extends StatelessWidget {
|
||||
final OsmNode node;
|
||||
|
||||
const AdvancedEditOptionsSheet({super.key, required this.node});
|
||||
|
||||
/// Mobile editor apps with their platform availability and store URLs
|
||||
List<EditorInfo> get _mobileEditors => [
|
||||
EditorInfo(
|
||||
name: LocalizationService.instance.t('advancedEdit.vespucci'),
|
||||
subtitle: LocalizationService.instance.t('advancedEdit.vespucciSubtitle'),
|
||||
icon: Icons.android,
|
||||
urlScheme: 'josm:/load_and_zoom?select=node${node.id}', // Has documented deep link support
|
||||
androidStoreUrl: 'https://play.google.com/store/apps/details?id=de.blau.android',
|
||||
availableOnAndroid: true,
|
||||
availableOnIOS: false,
|
||||
),
|
||||
EditorInfo(
|
||||
name: LocalizationService.instance.t('advancedEdit.streetComplete'),
|
||||
subtitle: LocalizationService.instance.t('advancedEdit.streetCompleteSubtitle'),
|
||||
icon: Icons.place,
|
||||
urlScheme: null, // No documented deep link support - go straight to store
|
||||
androidStoreUrl: 'https://play.google.com/store/apps/details?id=de.westnordost.streetcomplete',
|
||||
availableOnAndroid: true,
|
||||
availableOnIOS: false,
|
||||
),
|
||||
EditorInfo(
|
||||
name: LocalizationService.instance.t('advancedEdit.everyDoor'),
|
||||
subtitle: LocalizationService.instance.t('advancedEdit.everyDoorSubtitle'),
|
||||
icon: Icons.map,
|
||||
urlScheme: null, // No documented deep link support - go straight to store
|
||||
androidStoreUrl: 'https://play.google.com/store/apps/details?id=info.zverev.ilya.every_door',
|
||||
iosStoreUrl: 'https://apps.apple.com/app/every-door/id1621945342',
|
||||
availableOnAndroid: true,
|
||||
availableOnIOS: true,
|
||||
),
|
||||
EditorInfo(
|
||||
name: LocalizationService.instance.t('advancedEdit.goMap'),
|
||||
subtitle: LocalizationService.instance.t('advancedEdit.goMapSubtitle'),
|
||||
icon: Icons.phone_iphone,
|
||||
urlScheme: null, // No documented deep link support - go straight to store
|
||||
iosStoreUrl: 'https://apps.apple.com/app/go-map/id592990211',
|
||||
availableOnAndroid: false,
|
||||
availableOnIOS: true,
|
||||
),
|
||||
];
|
||||
|
||||
/// Web editor apps (always available on all platforms)
|
||||
List<EditorInfo> get _webEditors => [
|
||||
EditorInfo(
|
||||
name: LocalizationService.instance.t('advancedEdit.iDEditor'),
|
||||
subtitle: LocalizationService.instance.t('advancedEdit.iDEditorSubtitle'),
|
||||
icon: Icons.public,
|
||||
urlScheme: 'https://www.openstreetmap.org/edit?editor=id&node=${node.id}',
|
||||
availableOnAndroid: true,
|
||||
availableOnIOS: true,
|
||||
),
|
||||
EditorInfo(
|
||||
name: LocalizationService.instance.t('advancedEdit.rapidEditor'),
|
||||
subtitle: LocalizationService.instance.t('advancedEdit.rapidEditorSubtitle'),
|
||||
icon: Icons.speed,
|
||||
urlScheme: 'https://rapideditor.org/edit#map=19/0/0&nodes=${node.id}',
|
||||
availableOnAndroid: true,
|
||||
availableOnIOS: true,
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final locService = LocalizationService.instance;
|
||||
|
||||
// Filter mobile editors based on current platform
|
||||
final availableMobileEditors = _mobileEditors.where((editor) {
|
||||
if (Platform.isAndroid) return editor.availableOnAndroid;
|
||||
if (Platform.isIOS) return editor.availableOnIOS;
|
||||
return false; // Other platforms don't have mobile editors
|
||||
}).toList();
|
||||
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
|
||||
@@ -39,62 +130,17 @@ class AdvancedEditOptionsSheet extends StatelessWidget {
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildEditorTile(
|
||||
context: context,
|
||||
icon: Icons.public,
|
||||
title: locService.t('advancedEdit.iDEditor'),
|
||||
subtitle: locService.t('advancedEdit.iDEditorSubtitle'),
|
||||
onTap: () => _launchEditor(context, 'https://www.openstreetmap.org/edit?editor=id&node=${node.id}'),
|
||||
),
|
||||
_buildEditorTile(
|
||||
context: context,
|
||||
icon: Icons.speed,
|
||||
title: locService.t('advancedEdit.rapidEditor'),
|
||||
subtitle: locService.t('advancedEdit.rapidEditorSubtitle'),
|
||||
onTap: () => _launchEditor(context, 'https://rapideditor.org/edit#map=19/0/0&nodes=${node.id}'),
|
||||
),
|
||||
..._webEditors.map((editor) => _buildEditorTile(context, editor)),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Mobile Editors Section
|
||||
Text(
|
||||
locService.t('advancedEdit.mobileEditors'),
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
if (Platform.isAndroid) ...[
|
||||
_buildEditorTile(
|
||||
context: context,
|
||||
icon: Icons.android,
|
||||
title: locService.t('advancedEdit.vespucci'),
|
||||
subtitle: locService.t('advancedEdit.vespucciSubtitle'),
|
||||
onTap: () => _launchEditor(context, 'vespucci://edit?node=${node.id}'),
|
||||
),
|
||||
_buildEditorTile(
|
||||
context: context,
|
||||
icon: Icons.place,
|
||||
title: locService.t('advancedEdit.streetComplete'),
|
||||
subtitle: locService.t('advancedEdit.streetCompleteSubtitle'),
|
||||
onTap: () => _launchEditor(context, 'streetcomplete://quest?node=${node.id}'),
|
||||
),
|
||||
_buildEditorTile(
|
||||
context: context,
|
||||
icon: Icons.map,
|
||||
title: locService.t('advancedEdit.everyDoor'),
|
||||
subtitle: locService.t('advancedEdit.everyDoorSubtitle'),
|
||||
onTap: () => _launchEditor(context, 'everydoor://edit?node=${node.id}'),
|
||||
),
|
||||
],
|
||||
|
||||
if (Platform.isIOS) ...[
|
||||
_buildEditorTile(
|
||||
context: context,
|
||||
icon: Icons.phone_iphone,
|
||||
title: locService.t('advancedEdit.goMap'),
|
||||
subtitle: locService.t('advancedEdit.goMapSubtitle'),
|
||||
onTap: () => _launchEditor(context, 'gomaposm://edit?node=${node.id}'),
|
||||
// Mobile Editors Section (only show if there are available editors)
|
||||
if (availableMobileEditors.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
locService.t('advancedEdit.mobileEditors'),
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...availableMobileEditors.map((editor) => _buildEditorTile(context, editor)),
|
||||
],
|
||||
|
||||
const SizedBox(height: 16),
|
||||
@@ -113,44 +159,80 @@ class AdvancedEditOptionsSheet extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEditorTile({
|
||||
required BuildContext context,
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
Widget _buildEditorTile(BuildContext context, EditorInfo editor) {
|
||||
return ListTile(
|
||||
leading: Icon(icon, size: 24),
|
||||
title: Text(title),
|
||||
subtitle: Text(subtitle),
|
||||
leading: Icon(editor.icon, size: 24),
|
||||
title: Text(editor.name),
|
||||
subtitle: Text(editor.subtitle),
|
||||
trailing: const Icon(Icons.launch, size: 18),
|
||||
onTap: onTap,
|
||||
onTap: () => _launchEditor(context, editor),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 4),
|
||||
);
|
||||
}
|
||||
|
||||
void _launchEditor(BuildContext context, String url) async {
|
||||
final locService = LocalizationService.instance;
|
||||
void _launchEditor(BuildContext context, EditorInfo editor) async {
|
||||
Navigator.pop(context); // Close the sheet first
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(url);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(locService.t('advancedEdit.couldNotOpenEditor'))),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(locService.t('advancedEdit.couldNotOpenEditor'))),
|
||||
);
|
||||
// If app has a custom URL scheme, try to open it
|
||||
if (editor.urlScheme != null) {
|
||||
try {
|
||||
final uri = Uri.parse(editor.urlScheme!);
|
||||
final launched = await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
if (launched) return; // Success - app opened
|
||||
} catch (e) {
|
||||
// App launch failed - continue to app store
|
||||
}
|
||||
}
|
||||
|
||||
// No custom scheme or app launch failed - redirect to app store
|
||||
await _redirectToAppStore(context, editor);
|
||||
}
|
||||
|
||||
Future<void> _redirectToAppStore(BuildContext context, EditorInfo editor) async {
|
||||
final locService = LocalizationService.instance;
|
||||
|
||||
try {
|
||||
if (Platform.isAndroid && editor.androidStoreUrl != null) {
|
||||
// Try native Play Store first, then web fallback
|
||||
final packageName = _extractAndroidPackageName(editor.androidStoreUrl!);
|
||||
if (packageName != null) {
|
||||
final marketUri = Uri.parse('market://details?id=$packageName');
|
||||
try {
|
||||
final launched = await launchUrl(marketUri, mode: LaunchMode.externalApplication);
|
||||
if (launched) return;
|
||||
} catch (e) {
|
||||
// Fall back to web Play Store
|
||||
}
|
||||
}
|
||||
|
||||
// Web Play Store fallback
|
||||
final webStoreUri = Uri.parse(editor.androidStoreUrl!);
|
||||
await launchUrl(webStoreUri, mode: LaunchMode.externalApplication);
|
||||
return;
|
||||
} else if (Platform.isIOS && editor.iosStoreUrl != null) {
|
||||
// iOS App Store
|
||||
final iosStoreUri = Uri.parse(editor.iosStoreUrl!);
|
||||
await launchUrl(iosStoreUri, mode: LaunchMode.externalApplication);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// Fall through to show error message
|
||||
}
|
||||
|
||||
// Could not open app or store - show error message
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(locService.t('advancedEdit.couldNotOpenEditor'))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract Android package name from Play Store URL for market:// scheme
|
||||
String? _extractAndroidPackageName(String playStoreUrl) {
|
||||
final uri = Uri.tryParse(playStoreUrl);
|
||||
if (uri == null) return null;
|
||||
|
||||
// Extract from "id=" parameter in Play Store URLs
|
||||
return uri.queryParameters['id'];
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
name: deflockapp
|
||||
description: Map public surveillance infrastructure with OpenStreetMap
|
||||
publish_to: "none"
|
||||
version: 1.3.4+12 # The thing after the + is the version code, incremented with each release
|
||||
version: 1.4.0+13 # 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+
|
||||
|
||||
Reference in New Issue
Block a user