[mob][photos] Migrating to flutter_map v6 (4): Fix attribution

This commit is contained in:
ashilkn
2024-05-29 20:42:05 +05:30
parent 588df2c346
commit 7739be4e21
3 changed files with 316 additions and 299 deletions

View File

@@ -135,6 +135,14 @@ class _MapViewState extends State<MapView> {
},
),
),
Padding(
padding: EdgeInsets.only(
bottom: widget.bottomSheetDraggableAreaHeight,
),
child: OSMFranceTileAttributes(
options: widget.mapAttributionOptions,
),
),
],
),
widget.showControls

View File

@@ -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<SourceAttribution> 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<SourceAttribution> 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<StatefulWidget> createState() => MapAttributionWidgetState();
// }
@override
State<StatefulWidget> createState() => MapAttributionWidgetState();
}
// class MapAttributionWidgetState extends State<MapAttributionWidget> {
// StreamSubscription<MapEvent>? mapEventSubscription;
class MapAttributionWidgetState extends State<MapAttributionWidget> {
StreamSubscription<MapEvent>? 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<Widget>.from(
// widget.attributions.whereType<LogoSourceAttribution>(),
// 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<Widget>.from(
widget.attributions.whereType<LogoSourceAttribution>(),
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<TextSourceAttribution>(),
// 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<TextSourceAttribution>(),
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,
),
),
),
),
),
],
),
),
);
}
}

View File

@@ -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/')),
),
],
);
}
}