2023-06-04 18:29:03 +00:00
|
|
|
import "dart:async";
|
2023-06-14 10:00:30 +00:00
|
|
|
import "dart:isolate";
|
2023-06-04 18:29:03 +00:00
|
|
|
|
2023-06-05 03:12:43 +00:00
|
|
|
import "package:flutter/foundation.dart";
|
2023-06-04 18:29:03 +00:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter_map/flutter_map.dart';
|
2023-06-21 14:20:36 +00:00
|
|
|
import "package:interactive_bottom_sheet/interactive_bottom_sheet.dart";
|
2023-06-04 18:29:03 +00:00
|
|
|
import "package:latlong2/latlong.dart";
|
2023-06-04 23:22:21 +00:00
|
|
|
import "package:logging/logging.dart";
|
2023-06-04 18:29:03 +00:00
|
|
|
import "package:photos/models/file.dart";
|
2023-06-20 16:21:14 +00:00
|
|
|
// import "package:photos/models/location/location.dart";
|
2023-06-10 09:52:54 +00:00
|
|
|
import "package:photos/theme/ente_theme.dart";
|
2023-06-12 11:19:02 +00:00
|
|
|
import "package:photos/ui/common/loading_widget.dart";
|
2023-06-04 18:29:03 +00:00
|
|
|
import "package:photos/ui/map/image_marker.dart";
|
2023-06-05 11:47:38 +00:00
|
|
|
import 'package:photos/ui/map/image_tile.dart';
|
2023-06-14 11:38:47 +00:00
|
|
|
import "package:photos/ui/map/map_isolate.dart";
|
2023-06-04 18:29:03 +00:00
|
|
|
import "package:photos/ui/map/map_view.dart";
|
2023-06-05 13:11:39 +00:00
|
|
|
import "package:photos/utils/toast_util.dart";
|
2023-06-04 18:29:03 +00:00
|
|
|
|
|
|
|
class MapScreen extends StatefulWidget {
|
2023-06-05 03:40:25 +00:00
|
|
|
// Add a function parameter where the function returns a Future<List<File>>
|
|
|
|
|
|
|
|
final Future<List<File>> Function() filesFutureFn;
|
|
|
|
|
|
|
|
const MapScreen({
|
|
|
|
super.key,
|
|
|
|
required this.filesFutureFn,
|
|
|
|
});
|
2023-06-04 18:29:03 +00:00
|
|
|
|
|
|
|
@override
|
|
|
|
State<StatefulWidget> createState() {
|
|
|
|
return _MapScreenState();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _MapScreenState extends State<MapScreen> {
|
2023-06-21 14:20:36 +00:00
|
|
|
GlobalKey bottomSheetKey = GlobalKey();
|
2023-06-04 18:29:03 +00:00
|
|
|
List<ImageMarker> imageMarkers = [];
|
|
|
|
List<File> allImages = [];
|
2023-06-14 17:49:47 +00:00
|
|
|
StreamController<List<File>> visibleImages =
|
|
|
|
StreamController<List<File>>.broadcast();
|
2023-06-04 18:29:03 +00:00
|
|
|
MapController mapController = MapController();
|
|
|
|
bool isLoading = true;
|
2023-06-05 03:40:25 +00:00
|
|
|
double initialZoom = 4.0;
|
2023-06-14 17:39:16 +00:00
|
|
|
double maxZoom = 18.0;
|
2023-06-20 16:21:14 +00:00
|
|
|
double minZoom = 2.8;
|
2023-06-14 10:00:30 +00:00
|
|
|
int debounceDuration = 500;
|
2023-06-05 13:11:39 +00:00
|
|
|
LatLng center = LatLng(46.7286, 4.8614);
|
2023-06-04 23:22:21 +00:00
|
|
|
final Logger _logger = Logger("_MapScreenState");
|
2023-06-14 17:49:47 +00:00
|
|
|
StreamSubscription? _mapMoveSubscription;
|
|
|
|
Isolate? isolate;
|
2023-06-21 14:20:36 +00:00
|
|
|
double heightOfBottomSheetContent = 100;
|
|
|
|
static const gridCrossAxisSpacing = 4.0;
|
|
|
|
static const gridMainAxisSpacing = 4.0;
|
|
|
|
static const gridPadding = 4.0;
|
|
|
|
static const gridCrossAxisCount = 4;
|
2023-06-04 18:29:03 +00:00
|
|
|
|
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
super.initState();
|
|
|
|
initialize();
|
|
|
|
}
|
|
|
|
|
2023-06-14 17:49:47 +00:00
|
|
|
@override
|
|
|
|
void dispose() {
|
|
|
|
super.dispose();
|
|
|
|
visibleImages.close();
|
|
|
|
_mapMoveSubscription?.cancel();
|
|
|
|
}
|
|
|
|
|
2023-06-04 23:22:21 +00:00
|
|
|
Future<void> initialize() async {
|
|
|
|
try {
|
2023-06-05 03:40:25 +00:00
|
|
|
allImages = await widget.filesFutureFn();
|
2023-06-04 23:22:21 +00:00
|
|
|
processFiles(allImages);
|
|
|
|
} catch (e, s) {
|
|
|
|
_logger.severe("Error initializing map screen", e, s);
|
|
|
|
}
|
2023-06-04 18:29:03 +00:00
|
|
|
}
|
|
|
|
|
2023-06-14 12:14:19 +00:00
|
|
|
Future<void> processFiles(List<File> files) async {
|
2023-06-04 18:29:03 +00:00
|
|
|
final List<ImageMarker> tempMarkers = [];
|
2023-06-05 03:12:43 +00:00
|
|
|
bool hasAnyLocation = false;
|
2023-06-20 16:21:14 +00:00
|
|
|
File? mostRecentFile;
|
2023-06-14 17:49:47 +00:00
|
|
|
for (var file in files) {
|
2023-06-14 11:38:47 +00:00
|
|
|
if (file.hasLocation && file.location != null) {
|
2023-06-20 16:21:14 +00:00
|
|
|
hasAnyLocation = true;
|
|
|
|
|
|
|
|
if (mostRecentFile == null) {
|
|
|
|
mostRecentFile = file;
|
2023-06-14 11:34:33 +00:00
|
|
|
} else {
|
2023-06-20 16:21:14 +00:00
|
|
|
if ((mostRecentFile.creationTime ?? 0) < (file.creationTime ?? 0)) {
|
|
|
|
mostRecentFile = file;
|
|
|
|
}
|
2023-06-14 11:34:33 +00:00
|
|
|
}
|
2023-06-20 16:21:14 +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-04 23:21:51 +00:00
|
|
|
}
|
2023-06-14 12:14:19 +00:00
|
|
|
}
|
2023-06-14 10:00:30 +00:00
|
|
|
|
2023-06-05 03:12:43 +00:00
|
|
|
if (hasAnyLocation) {
|
|
|
|
center = LatLng(
|
2023-06-20 16:21:14 +00:00
|
|
|
mostRecentFile!.location!.latitude!,
|
|
|
|
mostRecentFile.location!.longitude!,
|
2023-06-05 03:12:43 +00:00
|
|
|
);
|
2023-06-05 11:47:38 +00:00
|
|
|
|
2023-06-05 03:12:43 +00:00
|
|
|
if (kDebugMode) {
|
|
|
|
debugPrint("Info for map: center $center, initialZoom $initialZoom");
|
|
|
|
}
|
2023-06-05 13:11:39 +00:00
|
|
|
} else {
|
2023-06-06 03:45:47 +00:00
|
|
|
showShortToast(context, "No images with location");
|
2023-06-05 03:12:43 +00:00
|
|
|
}
|
|
|
|
|
2023-06-04 18:29:03 +00:00
|
|
|
setState(() {
|
|
|
|
imageMarkers = tempMarkers;
|
|
|
|
});
|
2023-06-05 11:47:38 +00:00
|
|
|
|
2023-06-14 18:14:58 +00:00
|
|
|
mapController.move(
|
|
|
|
center,
|
|
|
|
initialZoom,
|
|
|
|
);
|
|
|
|
|
2023-06-05 11:47:38 +00:00
|
|
|
Timer(Duration(milliseconds: debounceDuration), () {
|
2023-06-14 18:14:58 +00:00
|
|
|
calculateVisibleMarkers(mapController.bounds!);
|
2023-06-05 11:47:38 +00:00
|
|
|
setState(() {
|
|
|
|
isLoading = false;
|
|
|
|
});
|
|
|
|
});
|
2023-06-04 18:29:03 +00:00
|
|
|
}
|
|
|
|
|
2023-06-14 17:49:47 +00:00
|
|
|
void calculateVisibleMarkers(LatLngBounds bounds) async {
|
2023-06-14 10:00:30 +00:00
|
|
|
final ReceivePort receivePort = ReceivePort();
|
2023-06-14 17:49:47 +00:00
|
|
|
isolate = await Isolate.spawn<MapIsolate>(
|
2023-06-14 10:00:30 +00:00
|
|
|
_calculateMarkersIsolate,
|
2023-06-14 11:38:47 +00:00
|
|
|
MapIsolate(
|
2023-06-14 10:00:30 +00:00
|
|
|
bounds: bounds,
|
|
|
|
imageMarkers: imageMarkers,
|
|
|
|
sendPort: receivePort.sendPort,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
2023-06-14 17:49:47 +00:00
|
|
|
_mapMoveSubscription = receivePort.listen((dynamic message) async {
|
2023-06-14 10:00:30 +00:00
|
|
|
if (message is List<File>) {
|
2023-06-14 17:49:47 +00:00
|
|
|
visibleImages.sink.add(message);
|
2023-06-14 10:00:30 +00:00
|
|
|
} else {
|
2023-06-14 17:49:47 +00:00
|
|
|
_mapMoveSubscription?.cancel();
|
|
|
|
isolate?.kill();
|
2023-06-14 10:00:30 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
@pragma('vm:entry-point')
|
2023-06-14 11:38:47 +00:00
|
|
|
static void _calculateMarkersIsolate(MapIsolate message) async {
|
2023-06-14 10:00:30 +00:00
|
|
|
final bounds = message.bounds;
|
|
|
|
final imageMarkers = message.imageMarkers;
|
|
|
|
final SendPort sendPort = message.sendPort;
|
|
|
|
try {
|
2023-06-14 17:49:47 +00:00
|
|
|
final List<File> visibleFiles = [];
|
2023-06-14 11:34:33 +00:00
|
|
|
for (var imageMarker in imageMarkers) {
|
2023-06-14 10:00:30 +00:00
|
|
|
final point = LatLng(imageMarker.latitude, imageMarker.longitude);
|
|
|
|
if (bounds.contains(point)) {
|
2023-06-14 17:49:47 +00:00
|
|
|
visibleFiles.add(imageMarker.imageFile);
|
2023-06-14 10:00:30 +00:00
|
|
|
}
|
2023-06-14 11:34:33 +00:00
|
|
|
}
|
2023-06-14 17:49:47 +00:00
|
|
|
sendPort.send(visibleFiles);
|
2023-06-14 10:00:30 +00:00
|
|
|
} catch (e) {
|
|
|
|
sendPort.send(e.toString());
|
|
|
|
}
|
|
|
|
}
|
2023-06-04 18:29:03 +00:00
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2023-06-12 10:06:39 +00:00
|
|
|
final textTheme = getEnteTextTheme(context);
|
2023-06-12 11:19:02 +00:00
|
|
|
final colorScheme = getEnteColorScheme(context);
|
2023-06-10 09:52:54 +00:00
|
|
|
return Container(
|
2023-06-12 11:19:02 +00:00
|
|
|
color: colorScheme.backgroundBase,
|
2023-06-10 09:52:54 +00:00
|
|
|
child: SafeArea(
|
|
|
|
top: false,
|
|
|
|
child: Scaffold(
|
|
|
|
body: Stack(
|
|
|
|
children: [
|
|
|
|
Column(
|
|
|
|
children: [
|
|
|
|
Expanded(
|
2023-06-10 11:15:32 +00:00
|
|
|
child: ClipRRect(
|
|
|
|
borderRadius: const BorderRadius.only(
|
|
|
|
bottomLeft: Radius.circular(6),
|
|
|
|
bottomRight: Radius.circular(6),
|
|
|
|
),
|
|
|
|
child: MapView(
|
2023-06-14 17:39:16 +00:00
|
|
|
key: ValueKey(
|
|
|
|
'image-marker-count-${imageMarkers.length}',
|
|
|
|
),
|
2023-06-10 11:15:32 +00:00
|
|
|
controller: mapController,
|
|
|
|
imageMarkers: imageMarkers,
|
2023-06-14 17:49:47 +00:00
|
|
|
updateVisibleImages: calculateVisibleMarkers,
|
2023-06-10 11:15:32 +00:00
|
|
|
center: center,
|
|
|
|
initialZoom: initialZoom,
|
|
|
|
minZoom: minZoom,
|
|
|
|
maxZoom: maxZoom,
|
|
|
|
debounceDuration: debounceDuration,
|
|
|
|
),
|
2023-06-04 18:29:03 +00:00
|
|
|
),
|
|
|
|
),
|
2023-06-10 11:15:32 +00:00
|
|
|
const SizedBox(height: 4),
|
2023-06-10 09:52:54 +00:00
|
|
|
],
|
|
|
|
),
|
|
|
|
isLoading
|
2023-06-12 11:19:02 +00:00
|
|
|
? EnteLoadingWidget(
|
|
|
|
size: 28,
|
|
|
|
color: getEnteColorScheme(context).primary700,
|
2023-06-10 09:52:54 +00:00
|
|
|
)
|
|
|
|
: const SizedBox.shrink(),
|
|
|
|
],
|
|
|
|
),
|
2023-06-21 14:20:36 +00:00
|
|
|
bottomSheet: InteractiveBottomSheet(
|
|
|
|
options: InteractiveBottomSheetOptions(
|
|
|
|
backgroundColor: colorScheme.backgroundBase,
|
|
|
|
maxSize: 0.95,
|
|
|
|
),
|
|
|
|
draggableAreaOptions: DraggableAreaOptions(
|
|
|
|
backgroundColor: colorScheme.backgroundBase,
|
|
|
|
indicatorColor: colorScheme.fillBase,
|
|
|
|
height: 32,
|
|
|
|
indicatorHeight: 4,
|
|
|
|
),
|
|
|
|
child: AnimatedSwitcher(
|
|
|
|
duration: const Duration(milliseconds: 200),
|
|
|
|
switchInCurve: Curves.easeInOutExpo,
|
|
|
|
switchOutCurve: Curves.easeInOutExpo,
|
|
|
|
child: StreamBuilder<List<File>>(
|
|
|
|
stream: visibleImages.stream,
|
|
|
|
builder: (
|
|
|
|
BuildContext context,
|
|
|
|
AsyncSnapshot<List<File>> snapshot,
|
|
|
|
) {
|
|
|
|
if (!snapshot.hasData) {
|
2023-06-21 15:40:20 +00:00
|
|
|
return SizedBox(
|
|
|
|
height: MediaQuery.of(context).size.height * 0.2,
|
|
|
|
child: const EnteLoadingWidget(),
|
|
|
|
);
|
2023-06-21 14:20:36 +00:00
|
|
|
}
|
|
|
|
final images = snapshot.data!;
|
|
|
|
_logger.info("Visible images: ${images.length}");
|
|
|
|
if (images.isEmpty) {
|
2023-06-21 15:40:20 +00:00
|
|
|
return SizedBox(
|
|
|
|
height: MediaQuery.of(context).size.height * 0.2,
|
|
|
|
child: Center(
|
|
|
|
child: Column(
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
children: [
|
|
|
|
Text(
|
|
|
|
"No photos found here",
|
|
|
|
style: textTheme.large,
|
|
|
|
),
|
|
|
|
const SizedBox(height: 4),
|
|
|
|
Text(
|
|
|
|
"Zoom out to see photos",
|
|
|
|
style: textTheme.smallFaint,
|
|
|
|
)
|
|
|
|
],
|
2023-06-21 14:20:36 +00:00
|
|
|
),
|
2023-06-21 15:40:20 +00:00
|
|
|
),
|
2023-06-21 14:20:36 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
//Be very careful when changing the height of the grid. It
|
|
|
|
//the height should be exactly how much the grid occupies.
|
|
|
|
//Do not add padding around the grid.
|
|
|
|
//Doing these will cause unexpected scroll behaviour. This
|
|
|
|
//is an issue with the package that is used here
|
|
|
|
//(InteractiveBottomSheet)
|
|
|
|
return ConstrainedBox(
|
|
|
|
constraints: BoxConstraints(
|
|
|
|
maxHeight: maxHeightOfGrid(images.length),
|
|
|
|
),
|
|
|
|
child: GridView.builder(
|
|
|
|
itemCount: images.length,
|
|
|
|
scrollDirection: Axis.vertical,
|
|
|
|
padding:
|
|
|
|
const EdgeInsets.symmetric(horizontal: gridPadding),
|
|
|
|
physics: const BouncingScrollPhysics(),
|
|
|
|
gridDelegate:
|
|
|
|
const SliverGridDelegateWithFixedCrossAxisCount(
|
|
|
|
crossAxisCount: gridCrossAxisCount,
|
|
|
|
crossAxisSpacing: gridCrossAxisSpacing,
|
|
|
|
mainAxisSpacing: gridMainAxisSpacing,
|
|
|
|
),
|
|
|
|
// shrinkWrap: true,
|
|
|
|
itemBuilder: (context, index) {
|
|
|
|
final image = images[index];
|
|
|
|
return ImageTile(
|
|
|
|
image: image,
|
|
|
|
visibleImages: images,
|
|
|
|
index: index,
|
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
2023-06-04 18:29:03 +00:00
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2023-06-21 14:20:36 +00:00
|
|
|
|
|
|
|
double maxHeightOfGrid(int imageCount) {
|
|
|
|
final rowHeight = ((MediaQuery.of(context).size.width -
|
|
|
|
(gridPadding * 2 +
|
|
|
|
gridCrossAxisSpacing * (gridCrossAxisCount - 1))) /
|
|
|
|
gridCrossAxisCount) +
|
|
|
|
gridMainAxisSpacing;
|
|
|
|
final rowCount = (imageCount / gridCrossAxisCount).ceilToDouble();
|
|
|
|
|
|
|
|
return rowCount * rowHeight;
|
|
|
|
}
|
2023-06-04 18:29:03 +00:00
|
|
|
}
|