mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-02-12 16:52:51 +00:00
app launches again, sorta working, map broken, settings page has configurable max cams.
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 72 B |
Binary file not shown.
|
Before Width: | Height: | Size: 95 B |
@@ -50,6 +50,19 @@ class AppState extends ChangeNotifier {
|
||||
final List<CameraProfile> _profiles = [];
|
||||
final Set<CameraProfile> _enabled = {};
|
||||
static const String _enabledPrefsKey = 'enabled_profiles';
|
||||
static const String _maxCamerasPrefsKey = 'max_cameras';
|
||||
|
||||
// Maximum number of cameras fetched/drawn
|
||||
int _maxCameras = 250;
|
||||
int get maxCameras => _maxCameras;
|
||||
set maxCameras(int n) {
|
||||
if (n < 10) n = 10; // minimum
|
||||
_maxCameras = n;
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setInt(_maxCamerasPrefsKey, n);
|
||||
});
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Upload mode: production, sandbox, or simulate (in-memory, no uploads)
|
||||
UploadMode _uploadMode = UploadMode.production;
|
||||
@@ -130,6 +143,10 @@ class AppState extends ChangeNotifier {
|
||||
await prefs.remove(_legacyTestModePrefsKey);
|
||||
await prefs.setInt(_uploadModePrefsKey, _uploadMode.index);
|
||||
}
|
||||
// Max cameras
|
||||
if (prefs.containsKey(_maxCamerasPrefsKey)) {
|
||||
_maxCameras = prefs.getInt(_maxCamerasPrefsKey) ?? 250;
|
||||
}
|
||||
// Offline mode loading
|
||||
if (prefs.containsKey(_offlineModePrefsKey)) {
|
||||
_offlineMode = prefs.getBool(_offlineModePrefsKey) ?? false;
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'settings_screen_sections/queue_section.dart';
|
||||
import 'settings_screen_sections/offline_areas_section.dart';
|
||||
import 'settings_screen_sections/offline_mode_section.dart';
|
||||
import 'settings_screen_sections/about_section.dart';
|
||||
import 'settings_screen_sections/max_cameras_section.dart';
|
||||
|
||||
class SettingsScreen extends StatelessWidget {
|
||||
const SettingsScreen({super.key});
|
||||
@@ -17,14 +18,16 @@ class SettingsScreen extends StatelessWidget {
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: const [
|
||||
AuthSection(),
|
||||
Divider(),
|
||||
UploadModeSection(),
|
||||
Divider(),
|
||||
AuthSection(),
|
||||
Divider(),
|
||||
QueueSection(),
|
||||
Divider(),
|
||||
ProfileListSection(),
|
||||
Divider(),
|
||||
MaxCamerasSection(),
|
||||
Divider(),
|
||||
OfflineModeSection(),
|
||||
Divider(),
|
||||
OfflineAreasSection(),
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../app_state.dart';
|
||||
|
||||
class MaxCamerasSection extends StatefulWidget {
|
||||
const MaxCamerasSection({super.key});
|
||||
|
||||
@override
|
||||
State<MaxCamerasSection> createState() => _MaxCamerasSectionState();
|
||||
}
|
||||
|
||||
class _MaxCamerasSectionState extends State<MaxCamerasSection> {
|
||||
late TextEditingController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final maxCameras = context.read<AppState>().maxCameras;
|
||||
_controller = TextEditingController(text: maxCameras.toString());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appState = context.watch<AppState>();
|
||||
final current = appState.maxCameras;
|
||||
final showWarning = current > 250;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.filter_alt),
|
||||
title: const Text('Max cameras fetched/drawn'),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Set an upper limit for the number of cameras on the map (default: 250).'),
|
||||
if (showWarning)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Row(
|
||||
children: const [
|
||||
Icon(Icons.warning, color: Colors.orange, size: 18),
|
||||
SizedBox(width: 6),
|
||||
Expanded(child: Text(
|
||||
'You probably don\'t want to do that unless you are absolutely sure you have a good reason for it.',
|
||||
style: TextStyle(color: Colors.orange),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: SizedBox(
|
||||
width: 80,
|
||||
child: TextFormField(
|
||||
controller: _controller,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onFieldSubmitted: (value) {
|
||||
final n = int.tryParse(value) ?? 10;
|
||||
appState.maxCameras = n;
|
||||
_controller.text = appState.maxCameras.toString();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,12 @@ class MapDataProvider {
|
||||
throw UnimplementedError('Local camera loading not yet implemented.');
|
||||
} else {
|
||||
// Use Overpass remote fetch, from submodule:
|
||||
return camerasFromOverpass(bounds: bounds, profiles: profiles, uploadMode: uploadMode);
|
||||
return camerasFromOverpass(
|
||||
bounds: bounds,
|
||||
profiles: profiles,
|
||||
uploadMode: uploadMode,
|
||||
maxCameras: AppState.instance.maxCameras,
|
||||
);
|
||||
}
|
||||
}
|
||||
/// Fetch tile image bytes from OSM or local (future). Only fetches, does not save!
|
||||
|
||||
@@ -12,6 +12,7 @@ Future<List<OsmCameraNode>> camerasFromOverpass({
|
||||
required LatLngBounds bounds,
|
||||
required List<CameraProfile> profiles,
|
||||
UploadMode uploadMode = UploadMode.production,
|
||||
int? maxCameras,
|
||||
}) async {
|
||||
if (profiles.isEmpty) return [];
|
||||
|
||||
@@ -24,12 +25,13 @@ Future<List<OsmCameraNode>> camerasFromOverpass({
|
||||
|
||||
const String prodEndpoint = 'https://overpass-api.de/api/interpreter';
|
||||
|
||||
final limit = maxCameras ?? AppState.instance.maxCameras;
|
||||
final query = '''
|
||||
[out:json][timeout:25];
|
||||
(
|
||||
$nodeClauses
|
||||
);
|
||||
out body 250;
|
||||
out body $limit;
|
||||
''';
|
||||
|
||||
try {
|
||||
|
||||
@@ -158,13 +158,33 @@ class _MapViewState extends State<MapView> {
|
||||
} catch (_) {
|
||||
return; // controller not ready yet
|
||||
}
|
||||
final cams = await _mapDataProvider.getCameras(
|
||||
bounds: bounds,
|
||||
profiles: appState.enabledProfiles,
|
||||
uploadMode: appState.uploadMode,
|
||||
// MapSource.auto (default) will prefer Overpass for now
|
||||
);
|
||||
if (mounted) setState(() => _cameras = cams);
|
||||
// If too zoomed out, do NOT fetch cameras; show info
|
||||
final zoom = _controller.camera.zoom;
|
||||
if (zoom < 10) {
|
||||
if (mounted) setState(() => _cameras = []);
|
||||
// Show a snackbar-style bubble, if desired
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Cameras not drawn below zoom level 10'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final cams = await _mapDataProvider.getCameras(
|
||||
bounds: bounds,
|
||||
profiles: appState.enabledProfiles,
|
||||
uploadMode: appState.uploadMode,
|
||||
// MapSource.auto (default) will prefer Overpass for now
|
||||
);
|
||||
if (mounted) setState(() => _cameras = cams);
|
||||
} on OfflineModeException catch (_) {
|
||||
// Swallow the error in offline mode
|
||||
if (mounted) setState(() => _cameras = []);
|
||||
}
|
||||
}
|
||||
|
||||
double _safeZoom() {
|
||||
@@ -199,14 +219,15 @@ class _MapViewState extends State<MapView> {
|
||||
|
||||
// Camera markers first, then GPS dot, so blue dot is always on top
|
||||
final markers = <Marker>[
|
||||
..._cameras.map(
|
||||
(n) => Marker(
|
||||
..._cameras
|
||||
.where((n) => n.coord.latitude != 0 || n.coord.longitude != 0)
|
||||
.where((n) => n.coord.latitude.abs() <= 90 && n.coord.longitude.abs() <= 180)
|
||||
.map((n) => Marker(
|
||||
point: n.coord,
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: _CameraMapMarker(node: n, mapController: _controller),
|
||||
),
|
||||
),
|
||||
)),
|
||||
if (_currentLatLng != null)
|
||||
Marker(
|
||||
point: _currentLatLng!,
|
||||
@@ -221,6 +242,8 @@ class _MapViewState extends State<MapView> {
|
||||
_buildCone(session.target!, session.directionDegrees, zoom),
|
||||
..._cameras
|
||||
.where((n) => n.hasDirection && n.directionDeg != null)
|
||||
.where((n) => n.coord.latitude != 0 || n.coord.longitude != 0)
|
||||
.where((n) => n.coord.latitude.abs() <= 90 && n.coord.longitude.abs() <= 180)
|
||||
.map((n) => _buildCone(n.coord, n.directionDeg!, zoom)),
|
||||
];
|
||||
|
||||
@@ -233,6 +256,7 @@ class _MapViewState extends State<MapView> {
|
||||
initialZoom: 15,
|
||||
maxZoom: 19,
|
||||
onPositionChanged: (pos, gesture) {
|
||||
setState(() {}); // Instant UI update for zoom, etc.
|
||||
if (gesture) widget.onUserGesture();
|
||||
if (session != null) {
|
||||
appState.updateSession(target: pos.center);
|
||||
@@ -264,7 +288,7 @@ class _MapViewState extends State<MapView> {
|
||||
return Image.memory(bytes, gaplessPlayback: true, fit: BoxFit.cover);
|
||||
}
|
||||
}
|
||||
return Image.asset('assets/transparent_1x1.png', gaplessPlayback: true, fit: BoxFit.cover);
|
||||
return tileWidget;
|
||||
} catch (e) {
|
||||
print('tileBuilder error: $e for tileImage: ${tileImage.toString()}');
|
||||
return tileWidget;
|
||||
|
||||
@@ -11,6 +11,7 @@ class TileProviderWithCache extends TileProvider {
|
||||
static final Map<String, Uint8List> _tileCache = {};
|
||||
static Map<String, Uint8List> get tileCache => _tileCache;
|
||||
final VoidCallback? onTileCacheUpdated;
|
||||
|
||||
TileProviderWithCache({this.onTileCacheUpdated});
|
||||
|
||||
@override
|
||||
@@ -20,7 +21,7 @@ class TileProviderWithCache extends TileProvider {
|
||||
return MemoryImage(_tileCache[key]!);
|
||||
} else {
|
||||
_fetchAndCacheTile(coords, key);
|
||||
// Return a transparent PNG until the tile is available.
|
||||
// Use asset (robust, cross-platform) for non-existing tiles.
|
||||
return const AssetImage('assets/transparent_1x1.png');
|
||||
}
|
||||
}
|
||||
@@ -39,9 +40,10 @@ class TileProviderWithCache extends TileProvider {
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) => onTileCacheUpdated!());
|
||||
}
|
||||
}
|
||||
// If bytes were empty, don't cache anything (will re-attempt next time)
|
||||
} catch (e) {
|
||||
print('[TileProviderWithCache] Error fetching tile $key: $e');
|
||||
// Optionally: fall back to a different asset, or record failures
|
||||
// Do NOT cache a failed/placeholder/empty tile!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,9 +36,8 @@ flutter:
|
||||
|
||||
assets:
|
||||
- assets/info.txt
|
||||
- assets/transparent_1x1.png
|
||||
- assets/black_1x1.png
|
||||
- assets/app_icon.png
|
||||
- assets/transparent_1x1.png
|
||||
|
||||
flutter_native_splash:
|
||||
color: "#202020"
|
||||
|
||||
Reference in New Issue
Block a user