mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-02-13 01:03:03 +00:00
fix loading on startup
This commit is contained in:
@@ -11,9 +11,9 @@ const double kDirectionConeHalfAngle = 20.0; // degrees
|
||||
const double kDirectionConeBaseLength = 0.0012; // multiplier
|
||||
|
||||
// Marker/camera interaction
|
||||
const int kCameraMinZoomLevel = 10; // Minimum zoom to show cameras or warning
|
||||
const Duration kMarkerTapTimeout = Duration(milliseconds: 250);
|
||||
const Duration kDebounceCameraRefresh = Duration(milliseconds: 500);
|
||||
const Duration kDebounceTileLayerUpdate = Duration(milliseconds: 50);
|
||||
|
||||
// Tile/OSM fetch retry parameters (for tunable backoff)
|
||||
const int kTileFetchMaxAttempts = 3;
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:latlong2/latlong.dart';
|
||||
import 'package:flock_map_app/dev_config.dart';
|
||||
import '../app_state.dart';
|
||||
import '../widgets/map_view.dart';
|
||||
import '../widgets/tile_provider_with_cache.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import '../services/offline_area_service.dart';
|
||||
import '../widgets/add_camera_sheet.dart';
|
||||
@@ -36,54 +37,57 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final appState = context.watch<AppState>();
|
||||
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: const Text('Flock Map'),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: _followMe ? 'Disable follow‑me' : 'Enable follow‑me',
|
||||
icon: Icon(_followMe ? Icons.gps_fixed : Icons.gps_off),
|
||||
onPressed: () => setState(() => _followMe = !_followMe),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
onPressed: () => Navigator.pushNamed(context, '/settings'),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: MapView(
|
||||
controller: _mapController,
|
||||
followMe: _followMe,
|
||||
onUserGesture: () {
|
||||
if (_followMe) setState(() => _followMe = false);
|
||||
},
|
||||
),
|
||||
floatingActionButton: appState.session == null
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
FloatingActionButton.extended(
|
||||
onPressed: _openAddCameraSheet,
|
||||
icon: const Icon(Icons.add_location_alt),
|
||||
label: const Text('Tag Camera'),
|
||||
heroTag: 'tag_camera_fab',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
FloatingActionButton.extended(
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => DownloadAreaDialog(controller: _mapController),
|
||||
return ChangeNotifierProvider<TileProviderWithCache>(
|
||||
create: (_) => TileProviderWithCache(),
|
||||
child: Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: const Text('Flock Map'),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: _followMe ? 'Disable follow‑me' : 'Enable follow‑me',
|
||||
icon: Icon(_followMe ? Icons.gps_fixed : Icons.gps_off),
|
||||
onPressed: () => setState(() => _followMe = !_followMe),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
onPressed: () => Navigator.pushNamed(context, '/settings'),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: MapView(
|
||||
controller: _mapController,
|
||||
followMe: _followMe,
|
||||
onUserGesture: () {
|
||||
if (_followMe) setState(() => _followMe = false);
|
||||
},
|
||||
),
|
||||
floatingActionButton: appState.session == null
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
FloatingActionButton.extended(
|
||||
onPressed: _openAddCameraSheet,
|
||||
icon: const Icon(Icons.add_location_alt),
|
||||
label: const Text('Tag Camera'),
|
||||
heroTag: 'tag_camera_fab',
|
||||
),
|
||||
icon: const Icon(Icons.download_for_offline),
|
||||
label: const Text('Download'),
|
||||
heroTag: 'download_fab',
|
||||
),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
|
||||
const SizedBox(height: 12),
|
||||
FloatingActionButton.extended(
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => DownloadAreaDialog(controller: _mapController),
|
||||
),
|
||||
icon: const Icon(Icons.download_for_offline),
|
||||
label: const Text('Download'),
|
||||
heroTag: 'download_fab',
|
||||
),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,6 @@ class _MapViewState extends State<MapView> {
|
||||
late final MapController _controller;
|
||||
final MapDataProvider _mapDataProvider = MapDataProvider();
|
||||
final Debouncer _debounce = Debouncer(kDebounceCameraRefresh);
|
||||
Debouncer? _debounceTileLayerUpdate;
|
||||
|
||||
StreamSubscription<Position>? _positionSub;
|
||||
LatLng? _currentLatLng;
|
||||
@@ -99,7 +98,7 @@ class _MapViewState extends State<MapView> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_debounceTileLayerUpdate = Debouncer(kDebounceTileLayerUpdate);
|
||||
// _debounceTileLayerUpdate removed
|
||||
OfflineAreaService();
|
||||
_controller = widget.controller;
|
||||
_initLocation();
|
||||
@@ -130,13 +129,13 @@ class _MapViewState extends State<MapView> {
|
||||
return;
|
||||
}
|
||||
final zoom = _controller.camera.zoom;
|
||||
if (zoom < 10) {
|
||||
if (zoom < kCameraMinZoomLevel) {
|
||||
// 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),
|
||||
SnackBar(
|
||||
content: Text('Cameras not drawn below zoom level $kCameraMinZoomLevel'),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -149,8 +148,6 @@ class _MapViewState extends State<MapView> {
|
||||
);
|
||||
}
|
||||
|
||||
// Duplicate dispose in _MapViewState removed. Only one dispose() remains with all proper teardown.
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant MapView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
@@ -182,37 +179,6 @@ class _MapViewState extends State<MapView> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _refreshCameras() async {
|
||||
final appState = context.read<AppState>();
|
||||
LatLngBounds? bounds;
|
||||
try {
|
||||
bounds = _controller.camera.visibleBounds;
|
||||
} catch (_) {
|
||||
return; // controller not ready yet
|
||||
}
|
||||
// If too zoomed out, do NOT fetch cameras; show info
|
||||
final zoom = _controller.camera.zoom;
|
||||
if (zoom < 10) {
|
||||
// No-op: camera overlays handled via provider and cache, no local _cameras assignment needed.
|
||||
// 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 {
|
||||
// (Legacy _cameras assignment removed—now handled via provider and cache updates)
|
||||
} on OfflineModeException catch (_) {
|
||||
// Swallow the error in offline mode
|
||||
// (Legacy _cameras assignment removed—handled via provider)
|
||||
}
|
||||
}
|
||||
|
||||
double _safeZoom() {
|
||||
try {
|
||||
return _controller.camera.zoom;
|
||||
@@ -303,15 +269,7 @@ class _MapViewState extends State<MapView> {
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
tileProvider: TileProviderWithCache(
|
||||
onTileCacheUpdated: () {
|
||||
print('[MapView] onTileCacheUpdated fired (tile loaded)');
|
||||
if (_debounceTileLayerUpdate != null) _debounceTileLayerUpdate!(() {
|
||||
print('[MapView] Running debounced setState due to tile cache update');
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
},
|
||||
),
|
||||
tileProvider: Provider.of<TileProviderWithCache>(context),
|
||||
urlTemplate: 'unused-{z}-{x}-{y}',
|
||||
tileSize: 256,
|
||||
tileBuilder: (ctx, tileWidget, tileImage) {
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../services/map_data_provider.dart';
|
||||
import '../app_state.dart';
|
||||
|
||||
/// Singleton in-memory tile cache and async provider for custom tiles.
|
||||
class TileProviderWithCache extends TileProvider {
|
||||
class TileProviderWithCache extends TileProvider with ChangeNotifier {
|
||||
static final Map<String, Uint8List> _tileCache = {};
|
||||
static Map<String, Uint8List> get tileCache => _tileCache;
|
||||
final VoidCallback? onTileCacheUpdated;
|
||||
|
||||
TileProviderWithCache({this.onTileCacheUpdated});
|
||||
TileProviderWithCache();
|
||||
|
||||
@override
|
||||
ImageProvider getImage(TileCoordinates coords, TileLayer options, {MapSource source = MapSource.auto}) {
|
||||
final key = '${coords.z}/${coords.x}/${coords.y}';
|
||||
if (_tileCache.containsKey(key)) {
|
||||
return MemoryImage(_tileCache[key]!);
|
||||
final bytes = _tileCache[key]!;
|
||||
return MemoryImage(bytes);
|
||||
} else {
|
||||
_fetchAndCacheTile(coords, key, source: source);
|
||||
// Always return a placeholder until the real tile is cached, regardless of source/offline/online.
|
||||
// Always return a placeholder until the real tile is cached
|
||||
return const AssetImage('assets/transparent_1x1.png');
|
||||
}
|
||||
}
|
||||
@@ -41,10 +40,7 @@ class TileProviderWithCache extends TileProvider {
|
||||
if (bytes.isNotEmpty) {
|
||||
_tileCache[key] = Uint8List.fromList(bytes);
|
||||
print('[TileProviderWithCache] Cached tile $key, bytes=${bytes.length}');
|
||||
if (onTileCacheUpdated != null) {
|
||||
print('[TileProviderWithCache] Calling onTileCacheUpdated for $key');
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) => onTileCacheUpdated!());
|
||||
}
|
||||
notifyListeners(); // This updates any listening widgets
|
||||
}
|
||||
// If bytes were empty, don't cache (will re-attempt next time)
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user