ente/lib/ui/map/map_screen.dart

242 lines
6.9 KiB
Dart
Raw Normal View History

import "dart:async";
import "dart:isolate";
import "package:collection/collection.dart";
import "package:flutter/foundation.dart";
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import "package:latlong2/latlong.dart";
import "package:logging/logging.dart";
2023-07-06 04:12:17 +00:00
import "package:photos/generated/l10n.dart";
2023-08-25 04:39:30 +00:00
import 'package:photos/models/file/file.dart';
import "package:photos/models/location/location.dart";
import "package:photos/theme/ente_theme.dart";
2023-06-12 11:19:02 +00:00
import "package:photos/ui/common/loading_widget.dart";
import "package:photos/ui/map/image_marker.dart";
2023-06-14 11:38:47 +00:00
import "package:photos/ui/map/map_isolate.dart";
import "package:photos/ui/map/map_pull_up_gallery.dart";
import "package:photos/ui/map/map_view.dart";
2023-06-05 13:11:39 +00:00
import "package:photos/utils/toast_util.dart";
class MapScreen extends StatefulWidget {
// Add a function parameter where the function returns a Future<List<File>>
2023-08-24 16:56:24 +00:00
final Future<List<EnteFile>> Function() filesFutureFn;
const MapScreen({
super.key,
required this.filesFutureFn,
});
@override
State<StatefulWidget> createState() {
return _MapScreenState();
}
}
class _MapScreenState extends State<MapScreen> {
List<ImageMarker> imageMarkers = [];
2023-08-24 16:56:24 +00:00
List<EnteFile> allImages = [];
StreamController<List<EnteFile>> visibleImages =
StreamController<List<EnteFile>>.broadcast();
MapController mapController = MapController();
bool isLoading = true;
2023-06-22 05:59:24 +00:00
double initialZoom = 4.5;
2023-06-14 17:39:16 +00:00
double maxZoom = 18.0;
double minZoom = 2.8;
int debounceDuration = 500;
LatLng center = const LatLng(46.7286, 4.8614);
final Logger _logger = Logger("_MapScreenState");
StreamSubscription? _mapMoveSubscription;
Isolate? isolate;
2023-06-21 17:19:56 +00:00
static const bottomSheetDraggableAreaHeight = 32.0;
2023-08-24 16:56:24 +00:00
List<EnteFile>? prevMessage;
@override
void initState() {
super.initState();
initialize();
}
@override
void dispose() {
super.dispose();
visibleImages.close();
_mapMoveSubscription?.cancel();
}
Future<void> initialize() async {
try {
allImages = await widget.filesFutureFn();
2023-12-21 07:34:06 +00:00
unawaited(processFiles(allImages));
} catch (e, s) {
_logger.severe("Error initializing map screen", e, s);
}
}
2023-08-24 16:56:24 +00:00
Future<void> processFiles(List<EnteFile> files) async {
final List<ImageMarker> tempMarkers = [];
bool hasAnyLocation = false;
2023-08-24 16:56:24 +00:00
EnteFile? mostRecentFile;
for (var file in files) {
if (file.hasLocation) {
if (!Location.isValidRange(
latitude: file.location!.latitude!,
longitude: file.location!.longitude!,
)) {
_logger.warning(
'Skipping file with invalid location ${file.toString()}',
);
continue;
}
hasAnyLocation = true;
if (mostRecentFile == null) {
mostRecentFile = file;
2023-06-14 11:34:33 +00:00
} else {
if ((mostRecentFile.creationTime ?? 0) < (file.creationTime ?? 0)) {
mostRecentFile = file;
}
2023-06-14 11:34:33 +00:00
}
2023-06-14 11:34:33 +00:00
tempMarkers.add(
ImageMarker(
latitude: file.location!.latitude!,
longitude: file.location!.longitude!,
imageFile: file,
),
);
}
2023-06-14 12:14:19 +00:00
}
if (hasAnyLocation) {
center = LatLng(
mostRecentFile!.location!.latitude!,
mostRecentFile.location!.longitude!,
);
if (kDebugMode) {
debugPrint("Info for map: center $center, initialZoom $initialZoom");
}
2023-06-05 13:11:39 +00:00
} else {
2023-07-06 04:12:17 +00:00
showShortToast(context, S.of(context).noImagesWithLocation);
}
setState(() {
imageMarkers = tempMarkers;
});
2023-06-14 18:14:58 +00:00
mapController.move(
center,
initialZoom,
);
Timer(Duration(milliseconds: debounceDuration), () {
2023-06-14 18:14:58 +00:00
calculateVisibleMarkers(mapController.bounds!);
setState(() {
isLoading = false;
});
});
}
void calculateVisibleMarkers(LatLngBounds bounds) async {
final ReceivePort receivePort = ReceivePort();
isolate = await Isolate.spawn<MapIsolate>(
_calculateMarkersIsolate,
2023-06-14 11:38:47 +00:00
MapIsolate(
bounds: bounds,
imageMarkers: imageMarkers,
sendPort: receivePort.sendPort,
),
);
_mapMoveSubscription = receivePort.listen((dynamic message) async {
2023-08-24 16:56:24 +00:00
if (message is List<EnteFile>) {
if (!message.equals(prevMessage ?? [])) {
visibleImages.sink.add(message);
}
prevMessage = message;
} else {
2023-12-21 07:34:06 +00:00
await _mapMoveSubscription?.cancel();
isolate?.kill();
}
});
}
@pragma('vm:entry-point')
2023-06-14 11:38:47 +00:00
static void _calculateMarkersIsolate(MapIsolate message) async {
final bounds = message.bounds;
final imageMarkers = message.imageMarkers;
final SendPort sendPort = message.sendPort;
try {
2023-08-24 16:56:24 +00:00
final List<EnteFile> visibleFiles = [];
2023-06-14 11:34:33 +00:00
for (var imageMarker in imageMarkers) {
final point = LatLng(imageMarker.latitude, imageMarker.longitude);
if (bounds.contains(point)) {
visibleFiles.add(imageMarker.imageFile);
}
2023-06-14 11:34:33 +00:00
}
sendPort.send(visibleFiles);
} catch (e) {
sendPort.send(e.toString());
}
}
@override
Widget build(BuildContext context) {
2023-06-12 11:19:02 +00:00
final colorScheme = getEnteColorScheme(context);
final bottomUnsafeArea = MediaQuery.of(context).padding.bottom;
return Container(
2023-06-12 11:19:02 +00:00
color: colorScheme.backgroundBase,
2023-06-24 11:17:31 +00:00
child: Theme(
data: Theme.of(context).copyWith(
bottomSheetTheme: const BottomSheetThemeData(
backgroundColor: Colors.transparent,
2023-06-21 17:19:56 +00:00
),
2023-06-24 11:17:31 +00:00
),
child: Scaffold(
body: Stack(
children: [
LayoutBuilder(
builder: (context, constrains) {
return SizedBox(
height: constrains.maxHeight * 0.75 +
bottomSheetDraggableAreaHeight -
bottomUnsafeArea,
2023-06-24 11:17:31 +00:00
child: MapView(
key: ValueKey(
'image-marker-count-${imageMarkers.length}',
2023-06-22 05:50:03 +00:00
),
2023-06-24 11:17:31 +00:00
controller: mapController,
imageMarkers: imageMarkers,
updateVisibleImages: calculateVisibleMarkers,
center: center,
initialZoom: initialZoom,
minZoom: minZoom,
maxZoom: maxZoom,
debounceDuration: debounceDuration,
bottomSheetDraggableAreaHeight:
bottomSheetDraggableAreaHeight,
),
);
},
),
isLoading
? EnteLoadingWidget(
size: 28,
color: getEnteColorScheme(context).primary700,
)
: const SizedBox.shrink(),
],
),
bottomSheet: MapPullUpGallery(
visibleImages,
bottomSheetDraggableAreaHeight,
bottomUnsafeArea,
),
),
),
);
}
}