tune the app
This commit is contained in:
parent
8b53075a92
commit
58f1f3bbbc
11 changed files with 458 additions and 47 deletions
1
.claude/worktrees/relaxed-wright
Submodule
1
.claude/worktrees/relaxed-wright
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit a3aaf1b6de30f5134d8180dbeee145d598e6e9a8
|
||||
216
backend/initdb/02_tile_functions.sql
Normal file
216
backend/initdb/02_tile_functions.sql
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
-- Zoom-dependent tile functions for Martin.
|
||||
-- These apply geometry simplification and feature filtering so that
|
||||
-- low-zoom tiles contain fewer, simpler features.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- planet_osm_polygon
|
||||
--------------------------------------------------------------------------------
|
||||
CREATE OR REPLACE FUNCTION tile_polygon(z integer, x integer, y integer)
|
||||
RETURNS bytea AS $$
|
||||
DECLARE
|
||||
bounds geometry;
|
||||
tolerance double precision;
|
||||
result bytea;
|
||||
BEGIN
|
||||
bounds := ST_TileEnvelope(z, x, y);
|
||||
|
||||
-- Simplification tolerance scales with zoom: lower zoom = more aggressive
|
||||
tolerance := CASE
|
||||
WHEN z >= 13 THEN 0 -- full detail
|
||||
WHEN z >= 11 THEN 1 -- light simplification
|
||||
WHEN z >= 9 THEN 10 -- moderate
|
||||
ELSE 50 -- heavy
|
||||
END;
|
||||
|
||||
WITH features AS (
|
||||
SELECT
|
||||
osm_id,
|
||||
name,
|
||||
building,
|
||||
landuse,
|
||||
"natural",
|
||||
amenity,
|
||||
leisure,
|
||||
water,
|
||||
waterway,
|
||||
CASE
|
||||
WHEN tolerance = 0 THEN ST_AsMVTGeom(way, bounds)
|
||||
ELSE ST_AsMVTGeom(
|
||||
ST_Simplify(way, tolerance, true),
|
||||
bounds
|
||||
)
|
||||
END AS geom
|
||||
FROM planet_osm_polygon
|
||||
WHERE way && bounds
|
||||
-- Feature filtering by zoom level:
|
||||
AND CASE
|
||||
-- zoom 8-9: only water, large forests/parks
|
||||
WHEN z < 10 THEN
|
||||
"natural" IN ('water', 'wood') OR landuse IN ('forest', 'reservoir')
|
||||
-- zoom 10-11: add more landuse
|
||||
WHEN z < 12 THEN
|
||||
"natural" IN ('water', 'wood', 'wetland', 'heath', 'scrub')
|
||||
OR landuse IN ('forest', 'reservoir', 'residential', 'industrial',
|
||||
'commercial', 'farmland', 'meadow', 'grass')
|
||||
OR leisure IN ('park', 'nature_reserve', 'garden')
|
||||
-- zoom 12: add buildings (large only)
|
||||
WHEN z < 13 THEN
|
||||
"natural" IS NOT NULL OR landuse IS NOT NULL OR leisure IS NOT NULL
|
||||
OR water IS NOT NULL
|
||||
OR (building IS NOT NULL AND ST_Area(way) > 5000)
|
||||
-- zoom 13+: everything
|
||||
ELSE true
|
||||
END
|
||||
)
|
||||
SELECT ST_AsMVT(features, 'planet_osm_polygon') INTO result FROM features;
|
||||
|
||||
RETURN result;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE PARALLEL SAFE;
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- planet_osm_line
|
||||
--------------------------------------------------------------------------------
|
||||
CREATE OR REPLACE FUNCTION tile_line(z integer, x integer, y integer)
|
||||
RETURNS bytea AS $$
|
||||
DECLARE
|
||||
bounds geometry;
|
||||
tolerance double precision;
|
||||
result bytea;
|
||||
BEGIN
|
||||
bounds := ST_TileEnvelope(z, x, y);
|
||||
|
||||
tolerance := CASE
|
||||
WHEN z >= 13 THEN 0
|
||||
WHEN z >= 11 THEN 5
|
||||
ELSE 20
|
||||
END;
|
||||
|
||||
WITH features AS (
|
||||
SELECT
|
||||
osm_id,
|
||||
name,
|
||||
highway,
|
||||
railway,
|
||||
waterway,
|
||||
CASE
|
||||
WHEN tolerance = 0 THEN ST_AsMVTGeom(way, bounds)
|
||||
ELSE ST_AsMVTGeom(
|
||||
ST_Simplify(way, tolerance, true),
|
||||
bounds
|
||||
)
|
||||
END AS geom
|
||||
FROM planet_osm_line
|
||||
WHERE way && bounds
|
||||
AND CASE
|
||||
-- zoom 10-11: only major roads and rivers
|
||||
WHEN z < 12 THEN
|
||||
highway IN ('motorway', 'trunk', 'primary', 'secondary')
|
||||
OR railway IN ('rail')
|
||||
OR waterway IN ('river', 'canal')
|
||||
-- zoom 12: add tertiary roads
|
||||
WHEN z < 13 THEN
|
||||
highway IN ('motorway', 'trunk', 'primary', 'secondary',
|
||||
'tertiary', 'motorway_link', 'trunk_link')
|
||||
OR railway IS NOT NULL
|
||||
OR waterway IS NOT NULL
|
||||
-- zoom 13+: everything
|
||||
ELSE true
|
||||
END
|
||||
)
|
||||
SELECT ST_AsMVT(features, 'planet_osm_line') INTO result FROM features;
|
||||
|
||||
RETURN result;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE PARALLEL SAFE;
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- planet_osm_point
|
||||
--------------------------------------------------------------------------------
|
||||
CREATE OR REPLACE FUNCTION tile_point(z integer, x integer, y integer)
|
||||
RETURNS bytea AS $$
|
||||
DECLARE
|
||||
bounds geometry;
|
||||
result bytea;
|
||||
BEGIN
|
||||
bounds := ST_TileEnvelope(z, x, y);
|
||||
|
||||
WITH features AS (
|
||||
SELECT
|
||||
osm_id,
|
||||
name,
|
||||
place,
|
||||
amenity,
|
||||
shop,
|
||||
tourism,
|
||||
"natural",
|
||||
ST_AsMVTGeom(way, bounds) AS geom
|
||||
FROM planet_osm_point
|
||||
WHERE way && bounds
|
||||
AND CASE
|
||||
-- zoom 12: only cities/towns/villages and major POIs
|
||||
WHEN z < 13 THEN
|
||||
place IN ('city', 'town', 'village')
|
||||
-- zoom 13: add hamlets and named POIs
|
||||
WHEN z < 14 THEN
|
||||
place IS NOT NULL
|
||||
OR (name IS NOT NULL AND (amenity IS NOT NULL OR tourism IS NOT NULL))
|
||||
-- zoom 14+: everything
|
||||
ELSE true
|
||||
END
|
||||
)
|
||||
SELECT ST_AsMVT(features, 'planet_osm_point') INTO result FROM features;
|
||||
|
||||
RETURN result;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE PARALLEL SAFE;
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- planet_osm_roads
|
||||
--------------------------------------------------------------------------------
|
||||
CREATE OR REPLACE FUNCTION tile_roads(z integer, x integer, y integer)
|
||||
RETURNS bytea AS $$
|
||||
DECLARE
|
||||
bounds geometry;
|
||||
tolerance double precision;
|
||||
result bytea;
|
||||
BEGIN
|
||||
bounds := ST_TileEnvelope(z, x, y);
|
||||
|
||||
tolerance := CASE
|
||||
WHEN z >= 10 THEN 0
|
||||
WHEN z >= 8 THEN 10
|
||||
ELSE 50
|
||||
END;
|
||||
|
||||
WITH features AS (
|
||||
SELECT
|
||||
osm_id,
|
||||
name,
|
||||
highway,
|
||||
ref,
|
||||
CASE
|
||||
WHEN tolerance = 0 THEN ST_AsMVTGeom(way, bounds)
|
||||
ELSE ST_AsMVTGeom(
|
||||
ST_Simplify(way, tolerance, true),
|
||||
bounds
|
||||
)
|
||||
END AS geom
|
||||
FROM planet_osm_roads
|
||||
WHERE way && bounds
|
||||
AND CASE
|
||||
-- zoom 6-7: motorways only
|
||||
WHEN z < 8 THEN
|
||||
highway IN ('motorway', 'trunk')
|
||||
-- zoom 8-9: add primary roads
|
||||
WHEN z < 10 THEN
|
||||
highway IN ('motorway', 'trunk', 'primary')
|
||||
-- zoom 10+: everything in the roads table
|
||||
ELSE true
|
||||
END
|
||||
)
|
||||
SELECT ST_AsMVT(features, 'planet_osm_roads') INTO result FROM features;
|
||||
|
||||
RETURN result;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE PARALLEL SAFE;
|
||||
|
|
@ -3,43 +3,31 @@ listen_addresses: '0.0.0.0:3001'
|
|||
postgres:
|
||||
connection_string: ${DATABASE_URL}
|
||||
|
||||
tables:
|
||||
functions:
|
||||
planet_osm_polygon:
|
||||
schema: public
|
||||
table: planet_osm_polygon
|
||||
srid: 3857
|
||||
geometry_column: way
|
||||
geometry_type: GEOMETRY
|
||||
minzoom: 4
|
||||
function: tile_polygon
|
||||
minzoom: 8
|
||||
maxzoom: 14
|
||||
bounds: [-180.0, -85.0511, 180.0, 85.0511]
|
||||
|
||||
planet_osm_line:
|
||||
schema: public
|
||||
table: planet_osm_line
|
||||
srid: 3857
|
||||
geometry_column: way
|
||||
geometry_type: GEOMETRY
|
||||
minzoom: 8
|
||||
function: tile_line
|
||||
minzoom: 10
|
||||
maxzoom: 14
|
||||
bounds: [-180.0, -85.0511, 180.0, 85.0511]
|
||||
|
||||
planet_osm_point:
|
||||
schema: public
|
||||
table: planet_osm_point
|
||||
srid: 3857
|
||||
geometry_column: way
|
||||
geometry_type: GEOMETRY
|
||||
minzoom: 10
|
||||
function: tile_point
|
||||
minzoom: 12
|
||||
maxzoom: 14
|
||||
bounds: [-180.0, -85.0511, 180.0, 85.0511]
|
||||
|
||||
planet_osm_roads:
|
||||
schema: public
|
||||
table: planet_osm_roads
|
||||
srid: 3857
|
||||
geometry_column: way
|
||||
geometry_type: GEOMETRY
|
||||
minzoom: 4
|
||||
function: tile_roads
|
||||
minzoom: 6
|
||||
maxzoom: 14
|
||||
bounds: [-180.0, -85.0511, 180.0, 85.0511]
|
||||
|
|
|
|||
|
|
@ -6,13 +6,16 @@ class AppConstants {
|
|||
/// Default backend URL when none is configured.
|
||||
static const String defaultBackendUrl = 'http://192.168.2.59:8080';
|
||||
|
||||
/// Default Martin tile server URL when none is configured.
|
||||
static const String defaultMartinUrl = 'http://192.168.2.59:3001';
|
||||
|
||||
/// Default map center: Amsterdam.
|
||||
static const double defaultLat = 52.3676;
|
||||
static const double defaultLon = 4.9041;
|
||||
static final LatLng defaultCenter = LatLng(defaultLat, defaultLon);
|
||||
|
||||
/// Zoom levels.
|
||||
static const double minZoom = 0;
|
||||
static const double minZoom = 6;
|
||||
static const double maxZoom = 18;
|
||||
static const double defaultZoom = 13;
|
||||
static const double poiZoom = 16;
|
||||
|
|
@ -26,5 +29,6 @@ class AppConstants {
|
|||
|
||||
/// Settings keys.
|
||||
static const String settingBackendUrl = 'backend_url';
|
||||
static const String settingMartinUrl = 'martin_url';
|
||||
static const String settingThemeMode = 'theme_mode';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:go_router/go_router.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:vector_map_tiles/vector_map_tiles.dart';
|
||||
import '../../../../core/api/api_client.dart';
|
||||
import '../../providers/map_provider.dart';
|
||||
import '../../providers/map_style_provider.dart';
|
||||
import '../widgets/map_controls.dart';
|
||||
|
|
@ -38,8 +37,8 @@ class _MapScreenState extends ConsumerState<MapScreen> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mapState = ref.watch(mapProvider);
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final styleAsync = ref.watch(mapStyleProvider(apiClient.baseUrl));
|
||||
final martinUrl = ref.watch(martinUrlProvider);
|
||||
final styleAsync = ref.watch(mapStyleProvider(martinUrl));
|
||||
|
||||
// Move the map only for programmatic state changes (locateUser,
|
||||
// zoomIn/zoomOut). Gesture-driven pans no longer call updateCamera(),
|
||||
|
|
@ -60,7 +59,7 @@ class _MapScreenState extends ConsumerState<MapScreen> {
|
|||
backgroundColor: Colors.transparent,
|
||||
initialCenter: mapState.center,
|
||||
initialZoom: mapState.zoom,
|
||||
minZoom: 0,
|
||||
minZoom: 6,
|
||||
maxZoom: 18,
|
||||
onPositionChanged: (position, hasGesture) {
|
||||
// Intentionally not syncing gesture-driven position changes
|
||||
|
|
@ -75,19 +74,26 @@ class _MapScreenState extends ConsumerState<MapScreen> {
|
|||
},
|
||||
),
|
||||
children: [
|
||||
styleAsync.when(
|
||||
data: (style) => VectorTileLayer(
|
||||
tileProviders: style.providers,
|
||||
theme: style.theme,
|
||||
concurrency: 4,
|
||||
memoryTileCacheMaxSize: 20 * 1024 * 1024, // 20 MB raw tiles
|
||||
memoryTileDataCacheMaxSize: 50, // 50 parsed tiles
|
||||
fileCacheMaximumSizeInBytes: 50 * 1024 * 1024, // 50 MB on disk
|
||||
cacheFolder: () => getApplicationCacheDirectory(),
|
||||
// Defer tile loading until the initial location is known so
|
||||
// tiles are never fetched for the default center and then
|
||||
// mass-cancelled (CancellationException crash) when the map
|
||||
// jumps to the GPS position.
|
||||
if (!mapState.isLocating)
|
||||
styleAsync.when(
|
||||
data: (style) => VectorTileLayer(
|
||||
tileProviders: style.providers,
|
||||
theme: style.theme,
|
||||
layerMode: VectorTileLayerMode.vector,
|
||||
concurrency: 4,
|
||||
maximumTileSubstitutionDifference: 4,
|
||||
memoryTileCacheMaxSize: 40 * 1024 * 1024,
|
||||
memoryTileDataCacheMaxSize: 100,
|
||||
fileCacheMaximumSizeInBytes: 150 * 1024 * 1024,
|
||||
cacheFolder: () => getApplicationCacheDirectory(),
|
||||
),
|
||||
loading: () => const SizedBox.shrink(),
|
||||
error: (e, _) => const SizedBox.shrink(),
|
||||
),
|
||||
loading: () => const SizedBox.shrink(),
|
||||
error: (e, _) => const SizedBox.shrink(),
|
||||
),
|
||||
if (mapState.currentLocation != null)
|
||||
MarkerLayer(
|
||||
markers: [
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ class MapNotifier extends Notifier<MapState> {
|
|||
MapState build() => MapState(
|
||||
center: AppConstants.defaultCenter,
|
||||
zoom: AppConstants.defaultZoom,
|
||||
isLocating: true,
|
||||
);
|
||||
|
||||
void updateCamera(LatLng center, double zoom) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,141 @@
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:vector_map_tiles/vector_map_tiles.dart';
|
||||
import 'package:vector_tile_renderer/vector_tile_renderer.dart';
|
||||
|
||||
/// Loads and caches the MapLibre style from the backend.
|
||||
/// Keyed by base URL so a URL change triggers a reload.
|
||||
/// Martin URL provider — overridden in main() with the saved value.
|
||||
final martinUrlProvider = Provider<String>((ref) => 'http://localhost:3001');
|
||||
|
||||
/// Builds a Style that fetches tiles directly from Martin.
|
||||
/// Keyed by Martin URL so a URL change triggers a reload.
|
||||
final mapStyleProvider =
|
||||
FutureProvider.family<Style, String>((ref, baseUrl) async {
|
||||
final styleUrl = '$baseUrl/tiles/style.json';
|
||||
return StyleReader(uri: styleUrl).read();
|
||||
});
|
||||
FutureProvider.family<Style, String>((ref, martinUrl) async {
|
||||
final urlTemplate =
|
||||
'$martinUrl/planet_osm_polygon,planet_osm_line,planet_osm_point,planet_osm_roads/{z}/{x}/{y}';
|
||||
|
||||
final tileProvider = NetworkVectorTileProvider(
|
||||
urlTemplate: urlTemplate,
|
||||
maximumZoom: 14,
|
||||
minimumZoom: 0,
|
||||
);
|
||||
|
||||
final providers = TileProviders({
|
||||
'osm_all': tileProvider,
|
||||
});
|
||||
|
||||
final styleJson = <String, dynamic>{
|
||||
"version": 8,
|
||||
"name": "Privacy Maps",
|
||||
"sources": {
|
||||
"osm_all": {
|
||||
"type": "vector",
|
||||
"tiles": [urlTemplate],
|
||||
"minzoom": 0,
|
||||
"maxzoom": 14,
|
||||
}
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"id": "background",
|
||||
"type": "background",
|
||||
"paint": {"background-color": "#f0ebe3"}
|
||||
},
|
||||
{
|
||||
"id": "landuse",
|
||||
"type": "fill",
|
||||
"source": "osm_all",
|
||||
"source-layer": "planet_osm_polygon",
|
||||
"paint": {"fill-color": "#d4e5c9", "fill-opacity": 0.6}
|
||||
},
|
||||
{
|
||||
"id": "water",
|
||||
"type": "fill",
|
||||
"source": "osm_all",
|
||||
"source-layer": "planet_osm_polygon",
|
||||
"filter": ["==", "natural", "water"],
|
||||
"paint": {"fill-color": "#a0c8f0"}
|
||||
},
|
||||
{
|
||||
"id": "roads-minor",
|
||||
"type": "line",
|
||||
"source": "osm_all",
|
||||
"source-layer": "planet_osm_line",
|
||||
"paint": {"line-color": "#ccc", "line-width": 1}
|
||||
},
|
||||
{
|
||||
"id": "roads-main",
|
||||
"type": "line",
|
||||
"source": "osm_all",
|
||||
"source-layer": "planet_osm_roads",
|
||||
"paint": {"line-color": "#f5a623", "line-width": 2}
|
||||
},
|
||||
{
|
||||
"id": "buildings",
|
||||
"type": "fill",
|
||||
"source": "osm_all",
|
||||
"source-layer": "planet_osm_polygon",
|
||||
"filter": ["has", "building"],
|
||||
"paint": {"fill-color": "#d9d0c7", "fill-outline-color": "#bbb"}
|
||||
},
|
||||
{
|
||||
"id": "road-names",
|
||||
"type": "symbol",
|
||||
"source": "osm_all",
|
||||
"source-layer": "planet_osm_line",
|
||||
"minzoom": 13,
|
||||
"filter": ["has", "name"],
|
||||
"layout": {
|
||||
"text-field": ["get", "name"],
|
||||
"text-font": ["Noto Sans Regular", "Open Sans Regular"],
|
||||
"text-size": 11,
|
||||
"symbol-placement": "line",
|
||||
"text-max-angle": 30,
|
||||
"text-padding": 10
|
||||
},
|
||||
"paint": {
|
||||
"text-color": "#444",
|
||||
"text-halo-color": "#fff",
|
||||
"text-halo-width": 1.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "place-names",
|
||||
"type": "symbol",
|
||||
"source": "osm_all",
|
||||
"source-layer": "planet_osm_point",
|
||||
"minzoom": 10,
|
||||
"filter": [
|
||||
"all",
|
||||
["has", "name"],
|
||||
["has", "place"]
|
||||
],
|
||||
"layout": {
|
||||
"text-field": ["get", "name"],
|
||||
"text-font": ["Noto Sans Regular", "Open Sans Regular"],
|
||||
"text-size": [
|
||||
"interpolate",
|
||||
["linear"],
|
||||
["zoom"],
|
||||
10,
|
||||
11,
|
||||
14,
|
||||
14
|
||||
],
|
||||
"text-anchor": "center",
|
||||
"text-padding": 5
|
||||
},
|
||||
"paint": {
|
||||
"text-color": "#333",
|
||||
"text-halo-color": "#fff",
|
||||
"text-halo-width": 1.5
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
final theme = ThemeReader().read(styleJson);
|
||||
|
||||
return Style(
|
||||
theme: theme,
|
||||
providers: providers,
|
||||
);
|
||||
});
|
||||
|
|
@ -51,24 +51,30 @@ class SettingsScreen extends ConsumerStatefulWidget {
|
|||
|
||||
class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
late final TextEditingController _backendUrlController;
|
||||
late final TextEditingController _martinUrlController;
|
||||
bool _urlSaved = false;
|
||||
bool _martinUrlSaved = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_backendUrlController = TextEditingController();
|
||||
_loadBackendUrl();
|
||||
_martinUrlController = TextEditingController();
|
||||
_loadUrls();
|
||||
}
|
||||
|
||||
Future<void> _loadBackendUrl() async {
|
||||
Future<void> _loadUrls() async {
|
||||
final db = ref.read(appDatabaseProvider);
|
||||
final url = await db.getSetting(AppConstants.settingBackendUrl);
|
||||
_backendUrlController.text = url ?? AppConstants.defaultBackendUrl;
|
||||
final martinUrl = await db.getSetting(AppConstants.settingMartinUrl);
|
||||
_martinUrlController.text = martinUrl ?? AppConstants.defaultMartinUrl;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_backendUrlController.dispose();
|
||||
_martinUrlController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -117,6 +123,41 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||
),
|
||||
const Divider(),
|
||||
|
||||
// Martin tile server URL
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Tile Server (Martin)',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _martinUrlController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Martin URL',
|
||||
hintText: AppConstants.defaultMartinUrl,
|
||||
suffixIcon: _martinUrlSaved
|
||||
? const Icon(Icons.check, color: Colors.green)
|
||||
: null,
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
onChanged: (_) {
|
||||
if (_martinUrlSaved) setState(() => _martinUrlSaved = false);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FilledButton(
|
||||
onPressed: _saveMartinUrl,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
|
||||
// Theme
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
|
|
@ -222,6 +263,24 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveMartinUrl() async {
|
||||
final url = _martinUrlController.text.trim();
|
||||
if (url.isEmpty) return;
|
||||
|
||||
final db = ref.read(appDatabaseProvider);
|
||||
await db.setSetting(AppConstants.settingMartinUrl, url);
|
||||
|
||||
setState(() => _martinUrlSaved = true);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Martin URL saved. Restart the app to apply.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _AttributionTile extends StatelessWidget {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'app/app.dart';
|
|||
import 'core/api/api_client.dart';
|
||||
import 'core/constants.dart';
|
||||
import 'core/database/app_database.dart';
|
||||
import 'features/map/providers/map_style_provider.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
|
@ -12,12 +13,15 @@ void main() async {
|
|||
final db = AppDatabase();
|
||||
final savedUrl = await db.getSetting(AppConstants.settingBackendUrl);
|
||||
final backendUrl = savedUrl ?? AppConstants.defaultBackendUrl;
|
||||
final savedMartinUrl = await db.getSetting(AppConstants.settingMartinUrl);
|
||||
final martinUrl = savedMartinUrl ?? AppConstants.defaultMartinUrl;
|
||||
|
||||
runApp(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
appDatabaseProvider.overrideWithValue(db),
|
||||
apiClientProvider.overrideWithValue(ApiClient(baseUrl: backendUrl)),
|
||||
martinUrlProvider.overrideWithValue(martinUrl),
|
||||
],
|
||||
child: const PrivacyMapsApp(),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1062,7 +1062,7 @@ packages:
|
|||
source: hosted
|
||||
version: "2.0.1"
|
||||
vector_tile_renderer:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: vector_tile_renderer
|
||||
sha256: "89746f1108eccbc0b6f33fbbef3fcf394cda3733fc0d5064ea03d53a459b56d3"
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ dependencies:
|
|||
sdk: flutter
|
||||
flutter_map: ^7.0.0
|
||||
vector_map_tiles: ^8.0.0
|
||||
vector_tile_renderer: ^5.2.0
|
||||
flutter_riverpod: ^3.0.0
|
||||
riverpod_annotation: ^4.0.0
|
||||
dio: ^5.4.0
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue