Bläddra i källkod

Map in file info (#1689)

Vishnu Mohandas 1 år sedan
förälder
incheckning
4865a085c7

+ 5 - 0
lib/ui/components/info_item_widget.dart

@@ -8,6 +8,7 @@ class InfoItemWidget extends StatelessWidget {
   final IconData leadingIcon;
   final VoidCallback? editOnTap;
   final String? title;
+  final Widget? endSection;
   final Future<List<Widget>> subtitleSection;
   final bool hasChipButtons;
   final VoidCallback? onTap;
@@ -15,6 +16,7 @@ class InfoItemWidget extends StatelessWidget {
     required this.leadingIcon,
     this.editOnTap,
     this.title,
+    this.endSection,
     required this.subtitleSection,
     this.hasChipButtons = false,
     this.onTap,
@@ -70,6 +72,9 @@ class InfoItemWidget extends StatelessWidget {
         ),
       ),
     ]);
+
+    endSection != null ? children.add(endSection!) : null;
+
     return Row(
       mainAxisAlignment: MainAxisAlignment.spaceBetween,
       crossAxisAlignment: CrossAxisAlignment.start,

+ 6 - 0
lib/ui/map/enable_map.dart

@@ -48,3 +48,9 @@ Future<bool> requestForMapEnable(BuildContext context) async {
   }
   return false;
 }
+
+//For debugging.
+void disableMap() {
+  UserRemoteFlagService.instance
+      .setBoolValue(UserRemoteFlagService.mapEnabled, false);
+}

+ 12 - 4
lib/ui/map/map_marker.dart

@@ -2,20 +2,28 @@ import "package:flutter/material.dart";
 import "package:flutter_map/flutter_map.dart";
 import "package:latlong2/latlong.dart";
 import "package:photos/ui/map/image_marker.dart";
+import "package:photos/ui/map/map_view.dart";
 import "package:photos/ui/map/marker_image.dart";
 
-Marker mapMarker(ImageMarker imageMarker, String key) {
+Marker mapMarker(
+  ImageMarker imageMarker,
+  String key, {
+  Size markerSize = MapView.defaultMarkerSize,
+}) {
   return Marker(
+    //-6.5 is for taking in the height of the MarkerPointer
+    anchorPos: AnchorPos.exactly(Anchor(markerSize.height / 2, -6.5)),
     key: Key(key),
-    width: 75,
-    height: 75,
+    width: markerSize.width,
+    height: markerSize.height,
     point: LatLng(
       imageMarker.latitude,
       imageMarker.longitude,
     ),
     builder: (context) => MarkerImage(
       file: imageMarker.imageFile,
-      seperator: 85,
+      seperator: (MapView.defaultMarkerSize.height + 10) -
+          (MapView.defaultMarkerSize.height - markerSize.height),
     ),
   );
 }

+ 14 - 8
lib/ui/map/map_screen.dart

@@ -22,10 +22,14 @@ class MapScreen extends StatefulWidget {
   // Add a function parameter where the function returns a Future<List<File>>
 
   final Future<List<EnteFile>> Function() filesFutureFn;
+  final LatLng? center;
+  final double initialZoom;
 
   const MapScreen({
     super.key,
     required this.filesFutureFn,
+    this.center,
+    this.initialZoom = 4.5,
   });
 
   @override
@@ -41,7 +45,6 @@ class _MapScreenState extends State<MapScreen> {
       StreamController<List<EnteFile>>.broadcast();
   MapController mapController = MapController();
   bool isLoading = true;
-  double initialZoom = 4.5;
   double maxZoom = 18.0;
   double minZoom = 2.8;
   int debounceDuration = 500;
@@ -109,13 +112,16 @@ class _MapScreenState extends State<MapScreen> {
     }
 
     if (hasAnyLocation) {
-      center = LatLng(
-        mostRecentFile!.location!.latitude!,
-        mostRecentFile.location!.longitude!,
-      );
+      center = widget.center ??
+          LatLng(
+            mostRecentFile!.location!.latitude!,
+            mostRecentFile.location!.longitude!,
+          );
 
       if (kDebugMode) {
-        debugPrint("Info for map: center $center, initialZoom $initialZoom");
+        debugPrint(
+          "Info for map: center $center, initialZoom ${widget.initialZoom}",
+        );
       }
     } else {
       showShortToast(context, S.of(context).noImagesWithLocation);
@@ -127,7 +133,7 @@ class _MapScreenState extends State<MapScreen> {
 
     mapController.move(
       center,
-      initialZoom,
+      widget.initialZoom,
     );
 
     Timer(Duration(milliseconds: debounceDuration), () {
@@ -211,7 +217,7 @@ class _MapScreenState extends State<MapScreen> {
                       imageMarkers: imageMarkers,
                       updateVisibleImages: calculateVisibleMarkers,
                       center: center,
-                      initialZoom: initialZoom,
+                      initialZoom: widget.initialZoom,
                       minZoom: minZoom,
                       maxZoom: maxZoom,
                       debounceDuration: debounceDuration,

+ 71 - 44
lib/ui/map/map_view.dart

@@ -20,6 +20,12 @@ class MapView extends StatefulWidget {
   final double initialZoom;
   final int debounceDuration;
   final double bottomSheetDraggableAreaHeight;
+  final bool showControls;
+  final int interactiveFlags;
+  final VoidCallback? onTap;
+  final Size markerSize;
+  final MapAttributionOptions mapAttributionOptions;
+  static const defaultMarkerSize = Size(75, 75);
 
   const MapView({
     Key? key,
@@ -32,6 +38,11 @@ class MapView extends StatefulWidget {
     required this.initialZoom,
     required this.debounceDuration,
     required this.bottomSheetDraggableAreaHeight,
+    this.mapAttributionOptions = const MapAttributionOptions(),
+    this.markerSize = MapView.defaultMarkerSize,
+    this.onTap,
+    this.interactiveFlags = InteractiveFlag.all,
+    this.showControls = true,
   }) : super(key: key);
 
   @override
@@ -71,6 +82,11 @@ class _MapViewState extends State<MapView> {
         FlutterMap(
           mapController: widget.controller,
           options: MapOptions(
+            onTap: widget.onTap != null
+                ? (_, __) {
+                    widget.onTap!.call();
+                  }
+                : null,
             center: widget.center,
             minZoom: widget.minZoom,
             maxZoom: widget.maxZoom,
@@ -85,13 +101,16 @@ class _MapViewState extends State<MapView> {
                 onChange(position.bounds!);
               }
             },
+            interactiveFlags: widget.interactiveFlags,
           ),
           nonRotatedChildren: [
             Padding(
               padding: EdgeInsets.only(
                 bottom: widget.bottomSheetDraggableAreaHeight,
               ),
-              child: const OSMFranceTileAttributes(),
+              child: OSMFranceTileAttributes(
+                options: widget.mapAttributionOptions,
+              ),
             ),
           ],
           children: [
@@ -101,7 +120,7 @@ class _MapViewState extends State<MapView> {
                 anchorPos: AnchorPos.align(AnchorAlign.top),
                 maxClusterRadius: 100,
                 showPolygon: false,
-                size: const Size(75, 75),
+                size: widget.markerSize,
                 fitBoundsOptions: const FitBoundsOptions(
                   padding: EdgeInsets.all(80),
                 ),
@@ -133,47 +152,51 @@ class _MapViewState extends State<MapView> {
             ),
           ],
         ),
-        Positioned(
-          top: 4,
-          left: 10,
-          child: SafeArea(
-            child: MapButton(
-              icon: Icons.arrow_back,
-              onPressed: () {
-                Navigator.pop(context);
-              },
-              heroTag: 'back',
-            ),
-          ),
-        ),
-        Positioned(
-          bottom: widget.bottomSheetDraggableAreaHeight + 10,
-          right: 10,
-          child: Column(
-            children: [
-              MapButton(
-                icon: Icons.add,
-                onPressed: () {
-                  widget.controller.move(
-                    widget.controller.center,
-                    widget.controller.zoom + 1,
-                  );
-                },
-                heroTag: 'zoom-in',
-              ),
-              MapButton(
-                icon: Icons.remove,
-                onPressed: () {
-                  widget.controller.move(
-                    widget.controller.center,
-                    widget.controller.zoom - 1,
-                  );
-                },
-                heroTag: 'zoom-out',
-              ),
-            ],
-          ),
-        ),
+        widget.showControls
+            ? Positioned(
+                top: 4,
+                left: 10,
+                child: SafeArea(
+                  child: MapButton(
+                    icon: Icons.arrow_back,
+                    onPressed: () {
+                      Navigator.pop(context);
+                    },
+                    heroTag: 'back',
+                  ),
+                ),
+              )
+            : const SizedBox.shrink(),
+        widget.showControls
+            ? Positioned(
+                bottom: widget.bottomSheetDraggableAreaHeight + 10,
+                right: 10,
+                child: Column(
+                  children: [
+                    MapButton(
+                      icon: Icons.add,
+                      onPressed: () {
+                        widget.controller.move(
+                          widget.controller.center,
+                          widget.controller.zoom + 1,
+                        );
+                      },
+                      heroTag: 'zoom-in',
+                    ),
+                    MapButton(
+                      icon: Icons.remove,
+                      onPressed: () {
+                        widget.controller.move(
+                          widget.controller.center,
+                          widget.controller.zoom - 1,
+                        );
+                      },
+                      heroTag: 'zoom-out',
+                    ),
+                  ],
+                ),
+              )
+            : const SizedBox.shrink(),
       ],
     );
   }
@@ -181,7 +204,11 @@ class _MapViewState extends State<MapView> {
   List<Marker> _buildMakers() {
     return List<Marker>.generate(widget.imageMarkers.length, (index) {
       final imageMarker = widget.imageMarkers[index];
-      return mapMarker(imageMarker, index.toString());
+      return mapMarker(
+        imageMarker,
+        index.toString(),
+        markerSize: widget.markerSize,
+      );
     });
   }
 }

+ 17 - 16
lib/ui/map/tile/attribution/map_attribution.dart

@@ -5,7 +5,9 @@ import "dart:async";
 import "package:flutter/material.dart";
 import "package:flutter_map/plugin_api.dart";
 import "package:photos/extensions/list.dart";
+import "package:photos/theme/colors.dart";
 import "package:photos/theme/ente_theme.dart";
+import "package:photos/ui/components/buttons/icon_button_widget.dart";
 
 // Credit: This code is based on the Rich Attribution widget from the flutter_map
 class MapAttributionWidget extends StatefulWidget {
@@ -87,6 +89,8 @@ class MapAttributionWidget extends StatefulWidget {
   ///
   /// Read the documentation on the individual properties for more information
   /// and customizability.
+
+  final double iconSize;
   const MapAttributionWidget({
     super.key,
     required this.attributions,
@@ -99,6 +103,7 @@ class MapAttributionWidget extends StatefulWidget {
     this.showFlutterMapAttribution = true,
     this.animationConfig = const FadeRAWA(),
     this.popupInitialDisplayDuration = Duration.zero,
+    this.iconSize = 20,
   });
 
   @override
@@ -168,27 +173,23 @@ class MapAttributionWidgetState extends State<MapAttributionWidget> {
         duration: widget.animationConfig.buttonDuration,
         child: popupExpanded
             ? (widget.closeButton ??
-                (context, close) => IconButton(
-                      onPressed: close,
-                      icon: Icon(
-                        Icons.cancel_outlined,
-                        color: Theme.of(context).textTheme.titleSmall?.color ??
-                            Colors.black,
-                        size: widget.permanentHeight,
-                      ),
+                (context, close) => IconButtonWidget(
+                      size: widget.iconSize,
+                      onTap: close,
+                      icon: Icons.cancel_outlined,
+                      iconButtonType: IconButtonType.primary,
+                      iconColor: getEnteColorScheme(context).strokeBase,
                     ))(
                 context,
                 () => setState(() => popupExpanded = false),
               )
             : (widget.openButton ??
-                (context, open) => IconButton(
-                      onPressed: open,
-                      tooltip: 'Attributions',
-                      icon: Icon(
-                        Icons.info_outlined,
-                        size: widget.permanentHeight,
-                        color: getEnteColorScheme(context).backgroundElevated,
-                      ),
+                (context, open) => IconButtonWidget(
+                      size: widget.iconSize,
+                      onTap: open,
+                      icon: Icons.info_outlined,
+                      iconButtonType: IconButtonType.primary,
+                      iconColor: strokeBaseLight,
                     ))(
                 context,
                 () {

+ 25 - 4
lib/ui/map/tile/layers.dart

@@ -9,6 +9,18 @@ import "package:url_launcher/url_launcher_string.dart";
 
 const String _userAgent = "io.ente.photos";
 
+class MapAttributionOptions {
+  final double permanentHeight;
+  final BorderRadius popupBorderRadius;
+  final double iconSize;
+
+  const MapAttributionOptions({
+    this.permanentHeight = 24,
+    this.popupBorderRadius = const BorderRadius.all(Radius.circular(12)),
+    this.iconSize = 20,
+  });
+}
+
 class OSMTileLayer extends StatelessWidget {
   const OSMTileLayer({super.key});
 
@@ -42,28 +54,37 @@ class OSMFranceTileLayer extends StatelessWidget {
 }
 
 class OSMFranceTileAttributes extends StatelessWidget {
-  const OSMFranceTileAttributes({super.key});
+  final MapAttributionOptions options;
+  const OSMFranceTileAttributes({
+    this.options = const MapAttributionOptions(),
+    super.key,
+  });
 
   @override
   Widget build(BuildContext context) {
+    final textTheme = getEnteTextTheme(context).tinyBold;
     return MapAttributionWidget(
       alignment: AttributionAlignment.bottomLeft,
       showFlutterMapAttribution: false,
+      permanentHeight: options.permanentHeight,
+      popupBackgroundColor: getEnteColorScheme(context).backgroundElevated,
+      popupBorderRadius: options.popupBorderRadius,
+      iconSize: options.iconSize,
       attributions: [
         TextSourceAttribution(
           S.of(context).openstreetmapContributors,
-          textStyle: getEnteTextTheme(context).smallBold,
+          textStyle: textTheme,
           onTap: () => launchUrlString('https://openstreetmap.org/copyright'),
         ),
         TextSourceAttribution(
           'HOT Tiles',
-          textStyle: getEnteTextTheme(context).smallBold,
+          textStyle: textTheme,
           onTap: () => launchUrl(Uri.parse('https://www.hotosm.org/')),
         ),
         TextSourceAttribution(
           S.of(context).hostedAtOsmFrance,
+          textStyle: textTheme,
           onTap: () => launchUrl(Uri.parse('https://www.openstreetmap.fr/')),
-          textStyle: getEnteTextTheme(context).smallBold,
         ),
       ],
     );

+ 1 - 0
lib/ui/viewer/file/file_details_widget.dart

@@ -145,6 +145,7 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
         },
       ),
     );
+
     fileDetailsTiles.addAll([
       ValueListenableBuilder(
         valueListenable: hasLocationData,

+ 183 - 0
lib/ui/viewer/file_details/location_tags_widget.dart

@@ -1,15 +1,27 @@
 import "dart:async";
+import "dart:ui";
 
 import "package:flutter/material.dart";
+import "package:flutter_map/flutter_map.dart";
+import "package:latlong2/latlong.dart";
+
 import "package:photos/core/event_bus.dart";
 import "package:photos/events/location_tag_updated_event.dart";
 import "package:photos/generated/l10n.dart";
 import "package:photos/models/file/file.dart";
 import "package:photos/services/location_service.dart";
+import "package:photos/services/search_service.dart";
+import "package:photos/services/user_remote_flag_service.dart";
 import "package:photos/states/location_screen_state.dart";
 import "package:photos/theme/ente_theme.dart";
 import "package:photos/ui/components/buttons/chip_button_widget.dart";
 import "package:photos/ui/components/info_item_widget.dart";
+import "package:photos/ui/map/enable_map.dart";
+import "package:photos/ui/map/image_marker.dart";
+import "package:photos/ui/map/map_screen.dart";
+import "package:photos/ui/map/map_view.dart";
+import "package:photos/ui/map/tile/layers.dart";
+
 import 'package:photos/ui/viewer/location/add_location_sheet.dart';
 import "package:photos/ui/viewer/location/location_screen.dart";
 import "package:photos/utils/navigation_util.dart";
@@ -36,6 +48,7 @@ class _LocationTagsWidgetState extends State<LocationTagsWidget> {
         Bus.instance.on<LocationTagUpdatedEvent>().listen((event) {
       locationTagChips = _getLocationTags();
     });
+
     super.initState();
   }
 
@@ -58,6 +71,7 @@ class _LocationTagsWidgetState extends State<LocationTagsWidget> {
         subtitleSection: locationTagChips,
         hasChipButtons: hasChipButtons ?? true,
         onTap: onTap,
+        endSection: InfoMap(widget.file),
 
         /// to be used when state issues are fixed when location is updated
         // editOnTap: widget.file.ownerID == Configuration.instance.getUserID()!
@@ -139,3 +153,172 @@ class _LocationTagsWidgetState extends State<LocationTagsWidget> {
     }
   }
 }
+
+class InfoMap extends StatefulWidget {
+  final EnteFile file;
+  const InfoMap(this.file, {super.key});
+
+  @override
+  State<InfoMap> createState() => _InfoMapState();
+}
+
+class _InfoMapState extends State<InfoMap> {
+  final _mapController = MapController();
+  late bool _hasEnabledMap;
+  late double _fileLat;
+  late double _fileLng;
+  static const _enabledMapZoom = 12.0;
+  static const _disabledMapZoom = 9.0;
+
+  @override
+  void initState() {
+    super.initState();
+    _hasEnabledMap = UserRemoteFlagService.instance
+        .getCachedBoolValue(UserRemoteFlagService.mapEnabled);
+    _fileLat = widget.file.location!.latitude!;
+    _fileLng = widget.file.location!.longitude!;
+  }
+
+  @override
+  void dispose() {
+    _mapController.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.only(top: 8),
+      child: ClipRRect(
+        clipBehavior: Clip.antiAliasWithSaveLayer,
+        borderRadius: const BorderRadius.all(Radius.circular(8)),
+        child: SizedBox(
+          height: 124,
+          child: _hasEnabledMap
+              ? Stack(
+                  clipBehavior: Clip.none,
+                  key: ValueKey(_hasEnabledMap),
+                  children: [
+                    MapView(
+                      updateVisibleImages: () {},
+                      imageMarkers: [
+                        ImageMarker(
+                          imageFile: widget.file,
+                          latitude: _fileLat,
+                          longitude: _fileLng,
+                        ),
+                      ],
+                      controller: _mapController,
+                      center: LatLng(
+                        _fileLat,
+                        _fileLng,
+                      ),
+                      minZoom: _enabledMapZoom,
+                      maxZoom: _enabledMapZoom,
+                      initialZoom: _enabledMapZoom,
+                      debounceDuration: 0,
+                      bottomSheetDraggableAreaHeight: 0,
+                      showControls: false,
+                      interactiveFlags: InteractiveFlag.none,
+                      mapAttributionOptions: MapAttributionOptions(
+                        permanentHeight: 16,
+                        popupBorderRadius: BorderRadius.circular(4),
+                        iconSize: 16,
+                      ),
+                      onTap: () {
+                        Navigator.of(context).push(
+                          MaterialPageRoute(
+                            builder: (context) => MapScreen(
+                              filesFutureFn: SearchService.instance.getAllFiles,
+                              center: LatLng(
+                                _fileLat,
+                                _fileLng,
+                              ),
+                              initialZoom: 16,
+                            ),
+                          ),
+                        );
+                      },
+                      markerSize: const Size(45, 45),
+                    ),
+                    IgnorePointer(
+                      child: Container(
+                        decoration: BoxDecoration(
+                          borderRadius: BorderRadius.circular(8),
+                          border: Border.all(
+                            color: getEnteColorScheme(context).strokeFaint,
+                          ),
+                        ),
+                      ),
+                    ),
+                  ],
+                )
+              : Stack(
+                  key: ValueKey(_hasEnabledMap),
+                  clipBehavior: Clip.none,
+                  children: [
+                    MapView(
+                      updateVisibleImages: () {},
+                      imageMarkers: const [],
+                      controller: _mapController,
+                      center: const LatLng(
+                        13.041599,
+                        77.594566,
+                      ),
+                      minZoom: _disabledMapZoom,
+                      maxZoom: _disabledMapZoom,
+                      initialZoom: _disabledMapZoom,
+                      debounceDuration: 0,
+                      bottomSheetDraggableAreaHeight: 0,
+                      showControls: false,
+                      interactiveFlags: InteractiveFlag.none,
+                      mapAttributionOptions: const MapAttributionOptions(
+                        iconSize: 0,
+                      ),
+                    ),
+                    BackdropFilter(
+                      filter: ImageFilter.blur(
+                        sigmaX: 2.8,
+                        sigmaY: 2.8,
+                      ),
+                      child: Container(
+                        color: getEnteColorScheme(context)
+                            .backgroundElevated
+                            .withOpacity(0.5),
+                      ),
+                    ),
+                    Container(
+                      decoration: BoxDecoration(
+                        borderRadius: BorderRadius.circular(8),
+                        border: Border.all(
+                          color: getEnteColorScheme(context).strokeFaint,
+                        ),
+                      ),
+                    ),
+                    GestureDetector(
+                      behavior: HitTestBehavior.opaque,
+                      onTap: () async {
+                        unawaited(
+                          requestForMapEnable(context).then((value) {
+                            if (value) {
+                              setState(() {
+                                _hasEnabledMap = true;
+                              });
+                            }
+                          }),
+                        );
+                      },
+                      child: Center(
+                        child: Text(
+                          S.of(context).enableMaps,
+                          style: getEnteTextTheme(context).small,
+                        ),
+                      ),
+                    ),
+                  ],
+                ),
+        ),
+      ),
+    );
+  }
+}

+ 1 - 1
pubspec.yaml

@@ -12,7 +12,7 @@ description: ente photos application
 # Read more about iOS versioning at
 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 
-version: 0.8.49+569
+version: 0.8.50+570
 
 environment:
   sdk: ">=3.0.0 <4.0.0"