From 7739be4e21365147affce9e88ec40ca8dc17898d Mon Sep 17 00:00:00 2001 From: ashilkn Date: Wed, 29 May 2024 20:42:05 +0530 Subject: [PATCH] [mob][photos] Migrating to flutter_map v6 (4): Fix attribution --- mobile/lib/ui/map/map_view.dart | 8 + .../map/tile/attribution/map_attribution.dart | 549 +++++++++--------- mobile/lib/ui/map/tile/layers.dart | 58 +- 3 files changed, 316 insertions(+), 299 deletions(-) diff --git a/mobile/lib/ui/map/map_view.dart b/mobile/lib/ui/map/map_view.dart index 5286924c95..3ff86f2ae1 100644 --- a/mobile/lib/ui/map/map_view.dart +++ b/mobile/lib/ui/map/map_view.dart @@ -135,6 +135,14 @@ class _MapViewState extends State { }, ), ), + Padding( + padding: EdgeInsets.only( + bottom: widget.bottomSheetDraggableAreaHeight, + ), + child: OSMFranceTileAttributes( + options: widget.mapAttributionOptions, + ), + ), ], ), widget.showControls diff --git a/mobile/lib/ui/map/tile/attribution/map_attribution.dart b/mobile/lib/ui/map/tile/attribution/map_attribution.dart index 370883bf08..524d8b1e5d 100644 --- a/mobile/lib/ui/map/tile/attribution/map_attribution.dart +++ b/mobile/lib/ui/map/tile/attribution/map_attribution.dart @@ -1,296 +1,301 @@ -// // ignore_for_file: invalid_use_of_internal_member +// ignore_for_file: invalid_use_of_internal_member -// import "dart:async"; +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"; +import "package:flutter/material.dart"; +import "package:flutter_map/flutter_map.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 { -// /// List of attributions to display -// /// -// /// [TextSourceAttribution]s are shown in a popup box (toggled by a tap/click -// /// on the [openButton]/[closeButton]), unlike [LogoSourceAttribution], which -// /// are visible permanently adjacent to the open/close button. -// final List attributions; +// Credit: This code is based on the Rich Attribution widget from the flutter_map +class MapAttributionWidget extends StatefulWidget { + /// List of attributions to display + /// + /// [TextSourceAttribution]s are shown in a popup box (toggled by a tap/click + /// on the [openButton]/[closeButton]), unlike [LogoSourceAttribution], which + /// are visible permanently adjacent to the open/close button. + final List attributions; -// /// The position in which to anchor this widget -// final AttributionAlignment alignment; + /// The position in which to anchor this widget + final AttributionAlignment alignment; -// /// The widget (usually an [IconButton]) to display when the popup box is -// /// closed, that opens the popup box via the `open` callback -// final Widget Function(BuildContext context, VoidCallback open)? openButton; + /// The widget (usually an [IconButton]) to display when the popup box is + /// closed, that opens the popup box via the `open` callback + final Widget Function(BuildContext context, VoidCallback open)? openButton; -// /// The widget (usually an [IconButton]) to display when the popup box is open, -// /// that closes the popup box via the `close` callback -// final Widget Function(BuildContext context, VoidCallback close)? closeButton; + /// The widget (usually an [IconButton]) to display when the popup box is open, + /// that closes the popup box via the `close` callback + final Widget Function(BuildContext context, VoidCallback close)? closeButton; -// /// The color to use as the popup box's background color, defaulting to the -// /// [Theme]s background color -// final Color? popupBackgroundColor; + /// The color to use as the popup box's background color, defaulting to the + /// [Theme]s background color + final Color? popupBackgroundColor; -// /// The radius of the edges of the popup box -// final BorderRadius? popupBorderRadius; + /// The radius of the edges of the popup box + final BorderRadius? popupBorderRadius; -// /// The height of the permanent row in which is found the popup menu toggle -// /// button -// /// -// /// Also determines spacing between the items within the row. -// /// -// /// Also set [LogoSourceAttribution.height] to the same value, if adjusted. -// final double permanentHeight; + /// The height of the permanent row in which is found the popup menu toggle + /// button + /// + /// Also determines spacing between the items within the row. + /// + /// Also set [LogoSourceAttribution.height] to the same value, if adjusted. + final double permanentHeight; -// /// Whether to add an additional attribution logo and text for 'flutter_map' -// final bool showFlutterMapAttribution; + /// Whether to add an additional attribution logo and text for 'flutter_map' + final bool showFlutterMapAttribution; -// /// Animation configuration, through the properties and handler/builder -// /// defined by a [RichAttributionWidgetAnimation] implementation -// /// -// /// Can be extensivley customized by implementing a custom -// /// [RichAttributionWidgetAnimation], or the prebuilt [FadeRAWA] and -// /// [ScaleRAWA] animations can be used with limited customization. -// final RichAttributionWidgetAnimation animationConfig; + /// Animation configuration, through the properties and handler/builder + /// defined by a [RichAttributionWidgetAnimation] implementation + /// + /// Can be extensivley customized by implementing a custom + /// [RichAttributionWidgetAnimation], or the prebuilt [FadeRAWA] and + /// [ScaleRAWA] animations can be used with limited customization. + final RichAttributionWidgetAnimation animationConfig; -// /// If not [Duration.zero] (default), the popup box will be open by default and -// /// hidden this long after the map is initialised -// /// -// /// This is useful with certain sources/tile servers that make immediate -// /// attribution mandatory and are not attributed with a permanently visible -// /// [LogoSourceAttribution]. -// final Duration popupInitialDisplayDuration; + /// If not [Duration.zero] (default), the popup box will be open by default and + /// hidden this long after the map is initialised + /// + /// This is useful with certain sources/tile servers that make immediate + /// attribution mandatory and are not attributed with a permanently visible + /// [LogoSourceAttribution]. + final Duration popupInitialDisplayDuration; -// /// A prebuilt dynamic attribution layer that supports both logos and text -// /// through [SourceAttribution]s -// /// -// /// [TextSourceAttribution]s are shown in a popup box that can be visible or -// /// invisible. Its state is toggled by a tri-state [openButton]/[closeButton] : -// /// 1. Not hovered, not opened: faded button, invisible box -// /// 2. Hovered, not opened: full opacity button, invisible box -// /// 3. Opened: full opacity button, visible box -// /// -// /// The hover state on mobile devices is unspecified, but the behaviour is -// /// usually inconsequential on mobile devices anyway, due to the fingertip -// /// covering the entire button. -// /// -// /// [LogoSourceAttribution]s are shown adjacent to the open/close button, to -// /// comply with some stricter tile server requirements (such as Mapbox). These -// /// are usually supplemented with a [TextSourceAttribution]. -// /// -// /// The popup box also closes automatically on any interaction with the map. -// /// -// /// Animations are built in by default, and configured/handled through -// /// [RichAttributionWidgetAnimation] - see that class and the [animationConfig] -// /// property for more information. By default, a simple fade/opacity animation -// /// is provided by [FadeRAWA]. [ScaleRAWA] is also available. -// /// -// /// Read the documentation on the individual properties for more information -// /// and customizability. + /// A prebuilt dynamic attribution layer that supports both logos and text + /// through [SourceAttribution]s + /// + /// [TextSourceAttribution]s are shown in a popup box that can be visible or + /// invisible. Its state is toggled by a tri-state [openButton]/[closeButton] : + /// 1. Not hovered, not opened: faded button, invisible box + /// 2. Hovered, not opened: full opacity button, invisible box + /// 3. Opened: full opacity button, visible box + /// + /// The hover state on mobile devices is unspecified, but the behaviour is + /// usually inconsequential on mobile devices anyway, due to the fingertip + /// covering the entire button. + /// + /// [LogoSourceAttribution]s are shown adjacent to the open/close button, to + /// comply with some stricter tile server requirements (such as Mapbox). These + /// are usually supplemented with a [TextSourceAttribution]. + /// + /// The popup box also closes automatically on any interaction with the map. + /// + /// Animations are built in by default, and configured/handled through + /// [RichAttributionWidgetAnimation] - see that class and the [animationConfig] + /// property for more information. By default, a simple fade/opacity animation + /// is provided by [FadeRAWA]. [ScaleRAWA] is also available. + /// + /// Read the documentation on the individual properties for more information + /// and customizability. -// final double iconSize; -// const MapAttributionWidget({ -// super.key, -// required this.attributions, -// this.alignment = AttributionAlignment.bottomRight, -// this.openButton, -// this.closeButton, -// this.popupBackgroundColor, -// this.popupBorderRadius, -// this.permanentHeight = 24, -// this.showFlutterMapAttribution = true, -// this.animationConfig = const FadeRAWA(), -// this.popupInitialDisplayDuration = Duration.zero, -// this.iconSize = 20, -// }); + final double iconSize; + const MapAttributionWidget({ + super.key, + required this.attributions, + this.alignment = AttributionAlignment.bottomRight, + this.openButton, + this.closeButton, + this.popupBackgroundColor, + this.popupBorderRadius, + this.permanentHeight = 24, + this.showFlutterMapAttribution = true, + this.animationConfig = const FadeRAWA(), + this.popupInitialDisplayDuration = Duration.zero, + this.iconSize = 20, + }); -// @override -// State createState() => MapAttributionWidgetState(); -// } + @override + State createState() => MapAttributionWidgetState(); +} -// class MapAttributionWidgetState extends State { -// StreamSubscription? mapEventSubscription; +class MapAttributionWidgetState extends State { + StreamSubscription? mapEventSubscription; -// final persistentAttributionKey = GlobalKey(); -// Size? persistentAttributionSize; + final persistentAttributionKey = GlobalKey(); + Size? persistentAttributionSize; -// late bool popupExpanded = widget.popupInitialDisplayDuration != Duration.zero; -// bool persistentHovered = false; + late bool popupExpanded = widget.popupInitialDisplayDuration != Duration.zero; + bool persistentHovered = false; -// @override -// void initState() { -// super.initState(); + @override + void initState() { + super.initState(); -// if (popupExpanded) { -// Future.delayed( -// widget.popupInitialDisplayDuration, -// () => setState(() => popupExpanded = false), -// ); -// } + if (popupExpanded) { + Future.delayed( + widget.popupInitialDisplayDuration, + () => setState(() => popupExpanded = false), + ); + } -// WidgetsBinding.instance.addPostFrameCallback( -// (_) => WidgetsBinding.instance.addPostFrameCallback((_) { -// if (mounted) { -// setState( -// () => persistentAttributionSize = -// (persistentAttributionKey.currentContext!.findRenderObject() -// as RenderBox) -// .size, -// ); -// } -// }), -// ); -// } + WidgetsBinding.instance.addPostFrameCallback( + (_) => WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState( + () => persistentAttributionSize = + (persistentAttributionKey.currentContext!.findRenderObject() + as RenderBox) + .size, + ); + } + }), + ); + } -// @override -// void dispose() { -// mapEventSubscription?.cancel(); -// super.dispose(); -// } + @override + void dispose() { + mapEventSubscription?.cancel(); + super.dispose(); + } -// @override -// Widget build(BuildContext context) { -// final persistentAttributionItems = [ -// ...List.from( -// widget.attributions.whereType(), -// growable: false, -// ).interleave(SizedBox(width: widget.permanentHeight / 1.5)), -// if (widget.showFlutterMapAttribution) -// LogoSourceAttribution( -// Image.asset( -// 'lib/assets/flutter_map_logo.png', -// package: 'flutter_map', -// ), -// tooltip: 'flutter_map', -// height: widget.permanentHeight, -// ), -// SizedBox(width: widget.permanentHeight * 0.1), -// AnimatedSwitcher( -// switchInCurve: widget.animationConfig.buttonCurve, -// switchOutCurve: widget.animationConfig.buttonCurve, -// duration: widget.animationConfig.buttonDuration, -// child: popupExpanded -// ? (widget.closeButton ?? -// (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) => IconButtonWidget( -// size: widget.iconSize, -// onTap: open, -// icon: Icons.info_outlined, -// iconButtonType: IconButtonType.primary, -// iconColor: strokeBaseLight, -// ))( -// context, -// () { -// setState(() => popupExpanded = true); -// mapEventSubscription = FlutterMapState.of(context) -// .mapController -// .mapEventStream -// .listen((e) { -// setState(() => popupExpanded = false); -// mapEventSubscription?.cancel(); -// }); -// }, -// ), -// ), -// ]; + @override + Widget build(BuildContext context) { + final persistentAttributionItems = [ + ...List.from( + widget.attributions.whereType(), + growable: false, + ).interleave(SizedBox(width: widget.permanentHeight / 1.5)), + if (widget.showFlutterMapAttribution) + LogoSourceAttribution( + Image.asset( + 'lib/assets/flutter_map_logo.png', + package: 'flutter_map', + ), + tooltip: 'flutter_map', + height: widget.permanentHeight, + ), + SizedBox(width: widget.permanentHeight * 0.1), + AnimatedSwitcher( + switchInCurve: widget.animationConfig.buttonCurve, + switchOutCurve: widget.animationConfig.buttonCurve, + duration: widget.animationConfig.buttonDuration, + child: popupExpanded + ? (widget.closeButton ?? + (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) => IconButtonWidget( + size: widget.iconSize, + onTap: open, + icon: Icons.info_outlined, + iconButtonType: IconButtonType.primary, + iconColor: strokeBaseLight, + ))( + context, + () { + setState(() => popupExpanded = true); + // mapEventSubscription = FlutterMapState.of(context) + // .mapController + // .mapEventStream + // .listen((e) { + // setState(() => popupExpanded = false); + // mapEventSubscription?.cancel(); + // }); + mapEventSubscription = + MapController().mapEventStream.listen((e) { + setState(() => popupExpanded = false); + mapEventSubscription?.cancel(); + }); + }, + ), + ), + ]; -// return LayoutBuilder( -// builder: (context, constraints) => Align( -// alignment: widget.alignment.real, -// child: Stack( -// alignment: widget.alignment.real, -// children: [ -// if (persistentAttributionSize != null) -// Padding( -// padding: const EdgeInsets.all(6), -// child: AnimatedScale( -// scale: popupExpanded ? 1 : 0, -// duration: const Duration(milliseconds: 200), -// curve: popupExpanded ? Curves.easeOut : Curves.easeIn, -// alignment: widget.alignment.real, -// child: Container( -// decoration: BoxDecoration( -// color: widget.popupBackgroundColor ?? -// Theme.of(context).colorScheme.background, -// border: Border.all(width: 0, style: BorderStyle.none), -// borderRadius: widget.popupBorderRadius ?? -// BorderRadius.only( -// topLeft: const Radius.circular(10), -// topRight: const Radius.circular(10), -// bottomLeft: widget.alignment == -// AttributionAlignment.bottomLeft -// ? Radius.zero -// : const Radius.circular(10), -// bottomRight: widget.alignment == -// AttributionAlignment.bottomRight -// ? Radius.zero -// : const Radius.circular(10), -// ), -// ), -// constraints: BoxConstraints( -// minWidth: constraints.maxWidth < 420 -// ? constraints.maxWidth -// : persistentAttributionSize!.width, -// ), -// child: Padding( -// padding: const EdgeInsets.all(8), -// child: SizedBox( -// height: widget.attributions.length * 32, -// child: Column( -// mainAxisSize: MainAxisSize.max, -// mainAxisAlignment: MainAxisAlignment.spaceBetween, -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// ...widget.attributions -// .whereType(), -// SizedBox( -// height: (widget.permanentHeight - 24) + 32, -// ), -// ], -// ), -// ), -// ), -// ), -// ), -// ), -// MouseRegion( -// key: persistentAttributionKey, -// onEnter: (_) => setState(() => persistentHovered = true), -// onExit: (_) => setState(() => persistentHovered = false), -// cursor: SystemMouseCursors.click, -// child: AnimatedOpacity( -// opacity: persistentHovered || popupExpanded ? 1 : 0.5, -// curve: widget.animationConfig.buttonCurve, -// duration: widget.animationConfig.buttonDuration, -// child: Padding( -// padding: const EdgeInsets.all(4), -// child: FittedBox( -// child: Row( -// mainAxisSize: MainAxisSize.min, -// children: -// widget.alignment == AttributionAlignment.bottomLeft -// ? persistentAttributionItems.reversed.toList() -// : persistentAttributionItems, -// ), -// ), -// ), -// ), -// ), -// ], -// ), -// ), -// ); -// } -// } + return LayoutBuilder( + builder: (context, constraints) => Align( + alignment: widget.alignment.real, + child: Stack( + alignment: widget.alignment.real, + children: [ + if (persistentAttributionSize != null) + Padding( + padding: const EdgeInsets.all(6), + child: AnimatedScale( + scale: popupExpanded ? 1 : 0, + duration: const Duration(milliseconds: 200), + curve: popupExpanded ? Curves.easeOut : Curves.easeIn, + alignment: widget.alignment.real, + child: Container( + decoration: BoxDecoration( + color: widget.popupBackgroundColor ?? + Theme.of(context).colorScheme.background, + border: Border.all(width: 0, style: BorderStyle.none), + borderRadius: widget.popupBorderRadius ?? + BorderRadius.only( + topLeft: const Radius.circular(10), + topRight: const Radius.circular(10), + bottomLeft: widget.alignment == + AttributionAlignment.bottomLeft + ? Radius.zero + : const Radius.circular(10), + bottomRight: widget.alignment == + AttributionAlignment.bottomRight + ? Radius.zero + : const Radius.circular(10), + ), + ), + constraints: BoxConstraints( + minWidth: constraints.maxWidth < 420 + ? constraints.maxWidth + : persistentAttributionSize!.width, + ), + child: Padding( + padding: const EdgeInsets.all(8), + child: SizedBox( + height: widget.attributions.length * 32, + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...widget.attributions + .whereType(), + SizedBox( + height: (widget.permanentHeight - 24) + 32, + ), + ], + ), + ), + ), + ), + ), + ), + MouseRegion( + key: persistentAttributionKey, + onEnter: (_) => setState(() => persistentHovered = true), + onExit: (_) => setState(() => persistentHovered = false), + cursor: SystemMouseCursors.click, + child: AnimatedOpacity( + opacity: persistentHovered || popupExpanded ? 1 : 0.5, + curve: widget.animationConfig.buttonCurve, + duration: widget.animationConfig.buttonDuration, + child: Padding( + padding: const EdgeInsets.all(4), + child: FittedBox( + child: Row( + mainAxisSize: MainAxisSize.min, + children: + widget.alignment == AttributionAlignment.bottomLeft + ? persistentAttributionItems.reversed.toList() + : persistentAttributionItems, + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/mobile/lib/ui/map/tile/layers.dart b/mobile/lib/ui/map/tile/layers.dart index c57ba2c639..2597dddea2 100644 --- a/mobile/lib/ui/map/tile/layers.dart +++ b/mobile/lib/ui/map/tile/layers.dart @@ -1,6 +1,11 @@ import "package:flutter/material.dart"; import "package:flutter_map/flutter_map.dart"; +import "package:photos/generated/l10n.dart"; +import "package:photos/theme/ente_theme.dart"; +import "package:photos/ui/map/tile/attribution/map_attribution.dart"; import "package:photos/ui/map/tile/cache.dart"; +import "package:url_launcher/url_launcher.dart"; +import "package:url_launcher/url_launcher_string.dart"; const String _userAgent = "io.ente.photos"; @@ -57,33 +62,32 @@ class OSMFranceTileAttributes extends StatelessWidget { @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: textTheme, - // onTap: () => launchUrlString('https://openstreetmap.org/copyright'), - // ), - // TextSourceAttribution( - // 'HOT Tiles', - // 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/')), - // ), - // ], - // ); - return const SizedBox.shrink(); + 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: textTheme, + onTap: () => launchUrlString('https://openstreetmap.org/copyright'), + ), + TextSourceAttribution( + 'HOT Tiles', + 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/')), + ), + ], + ); } }