mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-05-14 21:28:08 +02:00
generic providers
This commit is contained in:
@@ -66,6 +66,14 @@ class AppState extends ChangeNotifier {
|
||||
bool get offlineMode => _settingsState.offlineMode;
|
||||
int get maxCameras => _settingsState.maxCameras;
|
||||
UploadMode get uploadMode => _settingsState.uploadMode;
|
||||
|
||||
// Tile provider state
|
||||
List<TileProvider> get tileProviders => _settingsState.tileProviders;
|
||||
TileType? get selectedTileType => _settingsState.selectedTileType;
|
||||
TileProvider? get selectedTileProvider => _settingsState.selectedTileProvider;
|
||||
|
||||
/// Legacy getter for backward compatibility
|
||||
@Deprecated('Use selectedTileType instead')
|
||||
TileProviderType get tileProvider => _settingsState.tileProvider;
|
||||
|
||||
// Upload queue state
|
||||
@@ -179,6 +187,23 @@ class AppState extends ChangeNotifier {
|
||||
_startUploader(); // Restart uploader with new mode
|
||||
}
|
||||
|
||||
/// Select a tile type by ID
|
||||
Future<void> setSelectedTileType(String tileTypeId) async {
|
||||
await _settingsState.setSelectedTileType(tileTypeId);
|
||||
}
|
||||
|
||||
/// Add or update a tile provider
|
||||
Future<void> addOrUpdateTileProvider(TileProvider provider) async {
|
||||
await _settingsState.addOrUpdateTileProvider(provider);
|
||||
}
|
||||
|
||||
/// Delete a tile provider
|
||||
Future<void> deleteTileProvider(String providerId) async {
|
||||
await _settingsState.deleteTileProvider(providerId);
|
||||
}
|
||||
|
||||
/// Legacy setter for backward compatibility
|
||||
@Deprecated('Use setSelectedTileType instead')
|
||||
Future<void> setTileProvider(TileProviderType provider) async {
|
||||
await _settingsState.setTileProvider(provider);
|
||||
}
|
||||
|
||||
+217
-94
@@ -1,100 +1,223 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
/// A specific tile type within a provider
|
||||
class TileType {
|
||||
final String id;
|
||||
final String name;
|
||||
final String urlTemplate;
|
||||
final String attribution;
|
||||
final Uint8List? previewTile; // Single tile image data for preview
|
||||
|
||||
const TileType({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.urlTemplate,
|
||||
required this.attribution,
|
||||
this.previewTile,
|
||||
});
|
||||
|
||||
/// Create URL for a specific tile, replacing template variables
|
||||
String getTileUrl(int z, int x, int y, {String? apiKey}) {
|
||||
String url = urlTemplate
|
||||
.replaceAll('{z}', z.toString())
|
||||
.replaceAll('{x}', x.toString())
|
||||
.replaceAll('{y}', y.toString());
|
||||
|
||||
if (apiKey != null && apiKey.isNotEmpty) {
|
||||
url = url.replaceAll('{api_key}', apiKey);
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
/// Check if this tile type needs an API key
|
||||
bool get requiresApiKey => urlTemplate.contains('{api_key}');
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'urlTemplate': urlTemplate,
|
||||
'attribution': attribution,
|
||||
'previewTile': previewTile != null ? base64Encode(previewTile!) : null,
|
||||
};
|
||||
|
||||
static TileType fromJson(Map<String, dynamic> json) => TileType(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
urlTemplate: json['urlTemplate'],
|
||||
attribution: json['attribution'],
|
||||
previewTile: json['previewTile'] != null
|
||||
? base64Decode(json['previewTile'])
|
||||
: null,
|
||||
);
|
||||
|
||||
TileType copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
String? urlTemplate,
|
||||
String? attribution,
|
||||
Uint8List? previewTile,
|
||||
}) => TileType(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
urlTemplate: urlTemplate ?? this.urlTemplate,
|
||||
attribution: attribution ?? this.attribution,
|
||||
previewTile: previewTile ?? this.previewTile,
|
||||
);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is TileType && runtimeType == other.runtimeType && id == other.id;
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
}
|
||||
|
||||
/// A tile provider containing multiple tile types
|
||||
class TileProvider {
|
||||
final String id;
|
||||
final String name;
|
||||
final String? apiKey;
|
||||
final List<TileType> tileTypes;
|
||||
|
||||
const TileProvider({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.apiKey,
|
||||
required this.tileTypes,
|
||||
});
|
||||
|
||||
/// Check if this provider is usable (has API key if any tile types need it)
|
||||
bool get isUsable {
|
||||
final needsKey = tileTypes.any((type) => type.requiresApiKey);
|
||||
return !needsKey || (apiKey != null && apiKey!.isNotEmpty);
|
||||
}
|
||||
|
||||
/// Get available tile types (those that don't need API key or have one)
|
||||
List<TileType> get availableTileTypes {
|
||||
return tileTypes.where((type) => !type.requiresApiKey || isUsable).toList();
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'apiKey': apiKey,
|
||||
'tileTypes': tileTypes.map((type) => type.toJson()).toList(),
|
||||
};
|
||||
|
||||
static TileProvider fromJson(Map<String, dynamic> json) => TileProvider(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
apiKey: json['apiKey'],
|
||||
tileTypes: (json['tileTypes'] as List)
|
||||
.map((typeJson) => TileType.fromJson(typeJson))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
TileProvider copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
String? apiKey,
|
||||
List<TileType>? tileTypes,
|
||||
}) => TileProvider(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
apiKey: apiKey ?? this.apiKey,
|
||||
tileTypes: tileTypes ?? this.tileTypes,
|
||||
);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is TileProvider && runtimeType == other.runtimeType && id == other.id;
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
}
|
||||
|
||||
/// Factory for creating default tile providers
|
||||
class DefaultTileProviders {
|
||||
/// Create the default set of tile providers
|
||||
static List<TileProvider> createDefaults() {
|
||||
return [
|
||||
TileProvider(
|
||||
id: 'openstreetmap',
|
||||
name: 'OpenStreetMap',
|
||||
tileTypes: [
|
||||
TileType(
|
||||
id: 'osm_street',
|
||||
name: 'Street Map',
|
||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
attribution: '© OpenStreetMap contributors',
|
||||
),
|
||||
],
|
||||
),
|
||||
TileProvider(
|
||||
id: 'google',
|
||||
name: 'Google',
|
||||
tileTypes: [
|
||||
TileType(
|
||||
id: 'google_hybrid',
|
||||
name: 'Satellite + Roads',
|
||||
urlTemplate: 'https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}',
|
||||
attribution: '© Google',
|
||||
),
|
||||
TileType(
|
||||
id: 'google_satellite',
|
||||
name: 'Satellite Only',
|
||||
urlTemplate: 'https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}',
|
||||
attribution: '© Google',
|
||||
),
|
||||
TileType(
|
||||
id: 'google_roadmap',
|
||||
name: 'Road Map',
|
||||
urlTemplate: 'https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}',
|
||||
attribution: '© Google',
|
||||
),
|
||||
],
|
||||
),
|
||||
TileProvider(
|
||||
id: 'esri',
|
||||
name: 'Esri',
|
||||
tileTypes: [
|
||||
TileType(
|
||||
id: 'esri_satellite',
|
||||
name: 'Satellite Imagery',
|
||||
urlTemplate: 'https://services.arcgisonline.com/ArcGis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}.png',
|
||||
attribution: '© Esri © Maxar',
|
||||
),
|
||||
],
|
||||
),
|
||||
TileProvider(
|
||||
id: 'mapbox',
|
||||
name: 'Mapbox',
|
||||
tileTypes: [
|
||||
TileType(
|
||||
id: 'mapbox_satellite',
|
||||
name: 'Satellite',
|
||||
urlTemplate: 'https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}@2x.jpg90?access_token={api_key}',
|
||||
attribution: '© Mapbox © Maxar',
|
||||
),
|
||||
TileType(
|
||||
id: 'mapbox_streets',
|
||||
name: 'Streets',
|
||||
urlTemplate: 'https://api.mapbox.com/styles/v1/mapbox/streets-v12/tiles/{z}/{x}/{y}?access_token={api_key}',
|
||||
attribution: '© Mapbox © OpenStreetMap',
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/// Legacy enum for backward compatibility during transition
|
||||
/// TODO: Remove once all references are updated
|
||||
@Deprecated('Use TileProvider and TileType instead')
|
||||
enum TileProviderType {
|
||||
osmStreet,
|
||||
googleHybrid,
|
||||
arcgisSatellite,
|
||||
mapboxSatellite,
|
||||
}
|
||||
|
||||
class TileProviderConfig {
|
||||
final TileProviderType type;
|
||||
final String name;
|
||||
final String urlTemplate;
|
||||
final String attribution;
|
||||
final bool requiresApiKey;
|
||||
final String? description;
|
||||
|
||||
const TileProviderConfig({
|
||||
required this.type,
|
||||
required this.name,
|
||||
required this.urlTemplate,
|
||||
required this.attribution,
|
||||
this.requiresApiKey = false,
|
||||
this.description,
|
||||
});
|
||||
|
||||
/// Returns the URL template with API key inserted if needed
|
||||
String getUrlTemplate({String? apiKey}) {
|
||||
if (requiresApiKey && apiKey != null) {
|
||||
return urlTemplate.replaceAll('{api_key}', apiKey);
|
||||
}
|
||||
return urlTemplate;
|
||||
}
|
||||
|
||||
/// Check if this provider is available (has required API key if needed)
|
||||
bool isAvailable({String? apiKey}) {
|
||||
if (requiresApiKey) {
|
||||
return apiKey != null && apiKey.isNotEmpty;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Built-in tile provider configurations
|
||||
class TileProviders {
|
||||
static const osmStreet = TileProviderConfig(
|
||||
type: TileProviderType.osmStreet,
|
||||
name: 'Street Map',
|
||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
attribution: '© OpenStreetMap contributors',
|
||||
description: 'Standard street map with roads, buildings, and labels',
|
||||
);
|
||||
|
||||
static const googleHybrid = TileProviderConfig(
|
||||
type: TileProviderType.googleHybrid,
|
||||
name: 'Satellite + Roads',
|
||||
urlTemplate: 'https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}',
|
||||
attribution: '© Google',
|
||||
description: 'Satellite imagery with road and label overlays',
|
||||
);
|
||||
|
||||
static const arcgisSatellite = TileProviderConfig(
|
||||
type: TileProviderType.arcgisSatellite,
|
||||
name: 'Pure Satellite',
|
||||
urlTemplate: 'https://services.arcgisonline.com/ArcGis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}.png',
|
||||
attribution: '© Esri © Maxar',
|
||||
description: 'High-resolution satellite imagery without overlays',
|
||||
);
|
||||
|
||||
static const mapboxSatellite = TileProviderConfig(
|
||||
type: TileProviderType.mapboxSatellite,
|
||||
name: 'Pure Satellite (Mapbox)',
|
||||
urlTemplate: 'https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}@2x.jpg90?access_token={api_key}',
|
||||
attribution: '© Mapbox © Maxar',
|
||||
requiresApiKey: true,
|
||||
description: 'High-resolution satellite imagery without overlays',
|
||||
);
|
||||
|
||||
/// Get all available tile providers (those with API keys if required)
|
||||
static List<TileProviderConfig> getAvailable({String? mapboxApiKey}) {
|
||||
return [
|
||||
osmStreet,
|
||||
googleHybrid,
|
||||
arcgisSatellite,
|
||||
if (mapboxSatellite.isAvailable(apiKey: mapboxApiKey)) mapboxSatellite,
|
||||
];
|
||||
}
|
||||
|
||||
/// Get provider config by type
|
||||
static TileProviderConfig? getByType(TileProviderType type) {
|
||||
switch (type) {
|
||||
case TileProviderType.osmStreet:
|
||||
return osmStreet;
|
||||
case TileProviderType.googleHybrid:
|
||||
return googleHybrid;
|
||||
case TileProviderType.arcgisSatellite:
|
||||
return arcgisSatellite;
|
||||
case TileProviderType.mapboxSatellite:
|
||||
return mapboxSatellite;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,50 +10,84 @@ class TileProviderSection extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appState = context.watch<AppState>();
|
||||
final currentProvider = appState.tileProvider;
|
||||
|
||||
// Get available providers (for now, all free ones are available)
|
||||
final availableProviders = [
|
||||
TileProviders.osmStreet,
|
||||
TileProviders.googleHybrid,
|
||||
TileProviders.arcgisSatellite,
|
||||
// Don't include Mapbox for now since we don't have API key handling
|
||||
];
|
||||
final selectedTileType = appState.selectedTileType;
|
||||
final allTileTypes = <TileType>[];
|
||||
for (final provider in appState.tileProviders) {
|
||||
allTileTypes.addAll(provider.availableTileTypes);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Map Type',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Map Type',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// TODO: Navigate to provider management screen
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Provider management coming soon!')),
|
||||
);
|
||||
},
|
||||
child: const Text('Manage Providers'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...availableProviders.map((config) {
|
||||
final isSelected = config.type == currentProvider;
|
||||
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Radio<TileProviderType>(
|
||||
value: config.type,
|
||||
groupValue: currentProvider,
|
||||
onChanged: (TileProviderType? value) {
|
||||
if (value != null) {
|
||||
appState.setTileProvider(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
title: Text(config.name),
|
||||
subtitle: config.description != null
|
||||
? Text(
|
||||
config.description!,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
)
|
||||
: null,
|
||||
onTap: () {
|
||||
appState.setTileProvider(config.type);
|
||||
},
|
||||
);
|
||||
}),
|
||||
if (allTileTypes.isEmpty)
|
||||
const Text('No tile providers available')
|
||||
else
|
||||
...allTileTypes.map((tileType) {
|
||||
final provider = appState.tileProviders
|
||||
.firstWhere((p) => p.tileTypes.contains(tileType));
|
||||
final isSelected = selectedTileType?.id == tileType.id;
|
||||
final isUsable = !tileType.requiresApiKey || provider.isUsable;
|
||||
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Radio<String>(
|
||||
value: tileType.id,
|
||||
groupValue: selectedTileType?.id,
|
||||
onChanged: isUsable ? (String? value) {
|
||||
if (value != null) {
|
||||
appState.setSelectedTileType(value);
|
||||
}
|
||||
} : null,
|
||||
),
|
||||
title: Text(
|
||||
'${provider.name} - ${tileType.name}',
|
||||
style: TextStyle(
|
||||
color: isUsable ? null : Theme.of(context).disabledColor,
|
||||
),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
tileType.attribution,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: isUsable ? null : Theme.of(context).disabledColor,
|
||||
),
|
||||
),
|
||||
if (!isUsable)
|
||||
Text(
|
||||
'Requires API key',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: isUsable ? () {
|
||||
appState.setSelectedTileType(tileType.id);
|
||||
} : null,
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
+175
-11
@@ -1,5 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import '../models/tile_provider.dart';
|
||||
|
||||
@@ -10,19 +12,74 @@ class SettingsState extends ChangeNotifier {
|
||||
static const String _offlineModePrefsKey = 'offline_mode';
|
||||
static const String _maxCamerasPrefsKey = 'max_cameras';
|
||||
static const String _uploadModePrefsKey = 'upload_mode';
|
||||
static const String _tileProviderPrefsKey = 'tile_provider';
|
||||
static const String _tileProvidersPrefsKey = 'tile_providers';
|
||||
static const String _selectedTileTypePrefsKey = 'selected_tile_type';
|
||||
static const String _legacyTestModePrefsKey = 'test_mode';
|
||||
|
||||
bool _offlineMode = false;
|
||||
int _maxCameras = 250;
|
||||
UploadMode _uploadMode = UploadMode.simulate;
|
||||
TileProviderType _tileProvider = TileProviderType.osmStreet;
|
||||
List<TileProvider> _tileProviders = [];
|
||||
String _selectedTileTypeId = '';
|
||||
|
||||
// Getters
|
||||
bool get offlineMode => _offlineMode;
|
||||
int get maxCameras => _maxCameras;
|
||||
UploadMode get uploadMode => _uploadMode;
|
||||
TileProviderType get tileProvider => _tileProvider;
|
||||
List<TileProvider> get tileProviders => List.unmodifiable(_tileProviders);
|
||||
String get selectedTileTypeId => _selectedTileTypeId;
|
||||
|
||||
/// Get the currently selected tile type
|
||||
TileType? get selectedTileType {
|
||||
for (final provider in _tileProviders) {
|
||||
for (final tileType in provider.tileTypes) {
|
||||
if (tileType.id == _selectedTileTypeId) {
|
||||
return tileType;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Get the provider that contains the selected tile type
|
||||
TileProvider? get selectedTileProvider {
|
||||
for (final provider in _tileProviders) {
|
||||
if (provider.tileTypes.any((type) => type.id == _selectedTileTypeId)) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Get all available tile types from all providers
|
||||
List<TileType> get allAvailableTileTypes {
|
||||
final types = <TileType>[];
|
||||
for (final provider in _tileProviders) {
|
||||
types.addAll(provider.availableTileTypes);
|
||||
}
|
||||
return types;
|
||||
}
|
||||
|
||||
/// Legacy getter for backward compatibility
|
||||
@Deprecated('Use selectedTileType instead')
|
||||
TileProviderType get tileProvider {
|
||||
// Map current selection to legacy enum for compatibility
|
||||
final selected = selectedTileType;
|
||||
if (selected == null) return TileProviderType.osmStreet;
|
||||
|
||||
switch (selected.id) {
|
||||
case 'osm_street':
|
||||
return TileProviderType.osmStreet;
|
||||
case 'google_hybrid':
|
||||
return TileProviderType.googleHybrid;
|
||||
case 'esri_satellite':
|
||||
return TileProviderType.arcgisSatellite;
|
||||
case 'mapbox_satellite':
|
||||
return TileProviderType.mapboxSatellite;
|
||||
default:
|
||||
return TileProviderType.osmStreet;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize settings from preferences
|
||||
Future<void> init() async {
|
||||
@@ -50,15 +107,53 @@ class SettingsState extends ChangeNotifier {
|
||||
await prefs.setInt(_uploadModePrefsKey, _uploadMode.index);
|
||||
}
|
||||
|
||||
// Load tile provider
|
||||
if (prefs.containsKey(_tileProviderPrefsKey)) {
|
||||
final idx = prefs.getInt(_tileProviderPrefsKey) ?? 0;
|
||||
if (idx >= 0 && idx < TileProviderType.values.length) {
|
||||
_tileProvider = TileProviderType.values[idx];
|
||||
// Load tile providers (default to built-in providers if none saved)
|
||||
await _loadTileProviders(prefs);
|
||||
|
||||
// Load selected tile type (default to first available)
|
||||
_selectedTileTypeId = prefs.getString(_selectedTileTypePrefsKey) ?? '';
|
||||
if (_selectedTileTypeId.isEmpty || selectedTileType == null) {
|
||||
final firstType = allAvailableTileTypes.firstOrNull;
|
||||
if (firstType != null) {
|
||||
_selectedTileTypeId = firstType.id;
|
||||
await prefs.setString(_selectedTileTypePrefsKey, _selectedTileTypeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadTileProviders(SharedPreferences prefs) async {
|
||||
if (prefs.containsKey(_tileProvidersPrefsKey)) {
|
||||
try {
|
||||
final providersJson = prefs.getString(_tileProvidersPrefsKey);
|
||||
if (providersJson != null) {
|
||||
final providersList = jsonDecode(providersJson) as List;
|
||||
_tileProviders = providersList
|
||||
.map((json) => TileProvider.fromJson(json))
|
||||
.toList();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error loading tile providers: $e');
|
||||
// Fall back to defaults on error
|
||||
_tileProviders = DefaultTileProviders.createDefaults();
|
||||
}
|
||||
} else {
|
||||
// First time - use defaults
|
||||
_tileProviders = DefaultTileProviders.createDefaults();
|
||||
await _saveTileProviders(prefs);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveTileProviders(SharedPreferences prefs) async {
|
||||
try {
|
||||
final providersJson = jsonEncode(
|
||||
_tileProviders.map((provider) => provider.toJson()).toList(),
|
||||
);
|
||||
await prefs.setString(_tileProvidersPrefsKey, providersJson);
|
||||
} catch (e) {
|
||||
debugPrint('Error saving tile providers: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setOfflineMode(bool enabled) async {
|
||||
_offlineMode = enabled;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
@@ -82,10 +177,79 @@ class SettingsState extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setTileProvider(TileProviderType provider) async {
|
||||
_tileProvider = provider;
|
||||
/// Select a tile type by ID
|
||||
Future<void> setSelectedTileType(String tileTypeId) async {
|
||||
if (_selectedTileTypeId != tileTypeId) {
|
||||
_selectedTileTypeId = tileTypeId;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_selectedTileTypePrefsKey, tileTypeId);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Add or update a tile provider
|
||||
Future<void> addOrUpdateTileProvider(TileProvider provider) async {
|
||||
final existingIndex = _tileProviders.indexWhere((p) => p.id == provider.id);
|
||||
if (existingIndex >= 0) {
|
||||
_tileProviders[existingIndex] = provider;
|
||||
} else {
|
||||
_tileProviders.add(provider);
|
||||
}
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(_tileProviderPrefsKey, provider.index);
|
||||
await _saveTileProviders(prefs);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Delete a tile provider
|
||||
Future<void> deleteTileProvider(String providerId) async {
|
||||
// Don't allow deleting all providers
|
||||
if (_tileProviders.length <= 1) return;
|
||||
|
||||
final providerToDelete = _tileProviders.firstWhereOrNull((p) => p.id == providerId);
|
||||
if (providerToDelete == null) return;
|
||||
|
||||
// If selected tile type belongs to this provider, switch to another
|
||||
if (providerToDelete.tileTypes.any((type) => type.id == _selectedTileTypeId)) {
|
||||
// Find first available tile type from remaining providers
|
||||
final remainingProviders = _tileProviders.where((p) => p.id != providerId).toList();
|
||||
final firstAvailable = remainingProviders
|
||||
.expand((p) => p.availableTileTypes)
|
||||
.firstOrNull;
|
||||
|
||||
if (firstAvailable != null) {
|
||||
_selectedTileTypeId = firstAvailable.id;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_selectedTileTypePrefsKey, _selectedTileTypeId);
|
||||
}
|
||||
}
|
||||
|
||||
_tileProviders.removeWhere((p) => p.id == providerId);
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await _saveTileProviders(prefs);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Legacy setter for backward compatibility
|
||||
@Deprecated('Use setSelectedTileType instead')
|
||||
Future<void> setTileProvider(TileProviderType provider) async {
|
||||
// Map legacy enum to new tile type ID
|
||||
String tileTypeId;
|
||||
switch (provider) {
|
||||
case TileProviderType.osmStreet:
|
||||
tileTypeId = 'osm_street';
|
||||
break;
|
||||
case TileProviderType.googleHybrid:
|
||||
tileTypeId = 'google_hybrid';
|
||||
break;
|
||||
case TileProviderType.arcgisSatellite:
|
||||
tileTypeId = 'esri_satellite';
|
||||
break;
|
||||
case TileProviderType.mapboxSatellite:
|
||||
tileTypeId = 'mapbox_satellite';
|
||||
break;
|
||||
}
|
||||
|
||||
await setSelectedTileType(tileTypeId);
|
||||
}
|
||||
}
|
||||
+36
-17
@@ -5,6 +5,7 @@ import 'package:latlong2/latlong.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import '../app_state.dart';
|
||||
import '../services/offline_area_service.dart';
|
||||
@@ -172,11 +173,30 @@ class MapViewState extends State<MapView> {
|
||||
|
||||
/// Build tile layer based on selected tile provider
|
||||
Widget _buildTileLayer(AppState appState) {
|
||||
final providerConfig = TileProviders.getByType(appState.tileProvider);
|
||||
if (providerConfig == null) {
|
||||
// Fallback to OSM if somehow we have an invalid provider
|
||||
final selectedTileType = appState.selectedTileType;
|
||||
final selectedProvider = appState.selectedTileProvider;
|
||||
|
||||
// Fallback to first available tile type if none selected
|
||||
if (selectedTileType == null || selectedProvider == null) {
|
||||
final allTypes = <TileType>[];
|
||||
for (final provider in appState.tileProviders) {
|
||||
allTypes.addAll(provider.availableTileTypes);
|
||||
}
|
||||
|
||||
final fallback = allTypes.firstOrNull;
|
||||
if (fallback != null) {
|
||||
return TileLayer(
|
||||
urlTemplate: fallback.urlTemplate,
|
||||
userAgentPackageName: 'com.stopflock.flock_map_app',
|
||||
tileProvider: NetworkTileProvider(
|
||||
httpClient: _tileHttpClient,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Ultimate fallback - hardcoded OSM
|
||||
return TileLayer(
|
||||
urlTemplate: TileProviders.osmStreet.urlTemplate,
|
||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
userAgentPackageName: 'com.stopflock.flock_map_app',
|
||||
tileProvider: NetworkTileProvider(
|
||||
httpClient: _tileHttpClient,
|
||||
@@ -184,23 +204,22 @@ class MapViewState extends State<MapView> {
|
||||
);
|
||||
}
|
||||
|
||||
// For OSM tiles, use our custom HTTP client for offline/online routing
|
||||
if (providerConfig.type == TileProviderType.osmStreet) {
|
||||
return TileLayer(
|
||||
urlTemplate: providerConfig.urlTemplate,
|
||||
userAgentPackageName: 'com.stopflock.flock_map_app',
|
||||
tileProvider: NetworkTileProvider(
|
||||
httpClient: _tileHttpClient,
|
||||
),
|
||||
);
|
||||
// Get the URL template with API key if needed
|
||||
String urlTemplate = selectedTileType.urlTemplate;
|
||||
if (selectedTileType.requiresApiKey && selectedProvider.apiKey != null) {
|
||||
urlTemplate = urlTemplate.replaceAll('{api_key}', selectedProvider.apiKey!);
|
||||
}
|
||||
|
||||
// For other providers, use standard HTTP client (no offline support yet)
|
||||
// For now, use our custom HTTP client for all tile requests
|
||||
// This will enable offline support for all providers
|
||||
return TileLayer(
|
||||
urlTemplate: providerConfig.urlTemplate,
|
||||
urlTemplate: urlTemplate,
|
||||
userAgentPackageName: 'com.stopflock.flock_map_app',
|
||||
tileProvider: NetworkTileProvider(
|
||||
httpClient: _tileHttpClient,
|
||||
),
|
||||
additionalOptions: {
|
||||
'attribution': providerConfig.attribution,
|
||||
'attribution': selectedTileType.attribution,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -274,7 +293,7 @@ class MapViewState extends State<MapView> {
|
||||
return Stack(
|
||||
children: [
|
||||
FlutterMap(
|
||||
key: ValueKey('map_offline_${appState.offlineMode}_provider_${appState.tileProvider.name}'),
|
||||
key: ValueKey('map_offline_${appState.offlineMode}_tiletype_${appState.selectedTileType?.id ?? 'none'}'),
|
||||
mapController: _controller,
|
||||
options: MapOptions(
|
||||
initialCenter: _currentLatLng ?? LatLng(37.7749, -122.4194),
|
||||
|
||||
Reference in New Issue
Block a user