map: create shortcut to custom region and filters

This commit is contained in:
Thibault Deckers 2024-10-23 01:09:17 +02:00
parent 5ce8bef9cc
commit b9327db44b
25 changed files with 350 additions and 137 deletions

View file

@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased] ## <a id="unreleased"></a>[Unreleased]
### Added
- Map: create shortcut to custom region and filters
### Fixed ### Fixed
- crash when loading large collection - crash when loading large collection

View file

@ -314,6 +314,7 @@ open class MainActivity : FlutterFragmentActivity() {
return hashMapOf( return hashMapOf(
INTENT_DATA_KEY_ACTION to INTENT_ACTION_VIEW_GEO, INTENT_DATA_KEY_ACTION to INTENT_ACTION_VIEW_GEO,
INTENT_DATA_KEY_URI to uri.toString(), INTENT_DATA_KEY_URI to uri.toString(),
INTENT_DATA_KEY_FILTERS to extractFiltersFromIntent(intent),
) )
} }
@ -584,6 +585,8 @@ open class MainActivity : FlutterFragmentActivity() {
// dart page routes // dart page routes
const val COLLECTION_PAGE_ROUTE_NAME = "/collection" const val COLLECTION_PAGE_ROUTE_NAME = "/collection"
const val ENTRY_VIEWER_PAGE_ROUTE_NAME = "/viewer"
const val EXPLORER_PAGE_ROUTE_NAME = "/explorer"
const val MAP_PAGE_ROUTE_NAME = "/map" const val MAP_PAGE_ROUTE_NAME = "/map"
const val SEARCH_PAGE_ROUTE_NAME = "/search" const val SEARCH_PAGE_ROUTE_NAME = "/search"

View file

@ -23,11 +23,15 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.MainActivity import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.MainActivity.Companion.COLLECTION_PAGE_ROUTE_NAME
import deckers.thibault.aves.MainActivity.Companion.ENTRY_VIEWER_PAGE_ROUTE_NAME
import deckers.thibault.aves.MainActivity.Companion.EXPLORER_PAGE_ROUTE_NAME
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_EXPLORER_PATH import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_EXPLORER_PATH
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_ARRAY import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_ARRAY
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_STRING import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_STRING
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_PAGE import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_PAGE
import deckers.thibault.aves.MainActivity.Companion.EXTRA_STRING_ARRAY_SEPARATOR import deckers.thibault.aves.MainActivity.Companion.EXTRA_STRING_ARRAY_SEPARATOR
import deckers.thibault.aves.MainActivity.Companion.MAP_PAGE_ROUTE_NAME
import deckers.thibault.aves.R import deckers.thibault.aves.R
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
@ -354,12 +358,17 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
// shortcuts // shortcuts
private fun pinShortcut(call: MethodCall, result: MethodChannel.Result) { private fun pinShortcut(call: MethodCall, result: MethodChannel.Result) {
// common arguments
val label = call.argument<String>("label") val label = call.argument<String>("label")
val iconBytes = call.argument<ByteArray>("iconBytes") val iconBytes = call.argument<ByteArray>("iconBytes")
val route = call.argument<String>("route")
// route dependent arguments
val filters = call.argument<List<String>>("filters") val filters = call.argument<List<String>>("filters")
val explorerPath = call.argument<String>("explorerPath") val explorerPath = call.argument<String>("path")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val viewUri = call.argument<String>("viewUri")?.let { Uri.parse(it) }
if (label == null) { val geoUri = call.argument<String>("geoUri")?.let { Uri.parse(it) }
if (label == null || route == null) {
result.error("pin-args", "missing arguments", null) result.error("pin-args", "missing arguments", null)
return return
} }
@ -383,24 +392,60 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
// so that foreground is rendered at the intended scale // so that foreground is rendered at the intended scale
val supportAdaptiveIcon = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O val supportAdaptiveIcon = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
icon = IconCompat.createWithResource(context, if (supportAdaptiveIcon) R.mipmap.ic_shortcut_collection else R.drawable.ic_shortcut_collection) val resId = when (route) {
MAP_PAGE_ROUTE_NAME -> if (supportAdaptiveIcon) R.mipmap.ic_shortcut_map else R.drawable.ic_shortcut_map
else -> if (supportAdaptiveIcon) R.mipmap.ic_shortcut_collection else R.drawable.ic_shortcut_collection
}
icon = IconCompat.createWithResource(context, resId)
} }
val intent = when { val intent: Intent = when (route) {
filters != null -> Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java) COLLECTION_PAGE_ROUTE_NAME -> {
.putExtra(EXTRA_KEY_PAGE, "/collection") if (filters == null) {
result.error("pin-filters", "collection shortcut requires filters", null)
return
}
Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
.putExtra(EXTRA_KEY_PAGE, route)
.putExtra(EXTRA_KEY_FILTERS_ARRAY, filters.toTypedArray()) .putExtra(EXTRA_KEY_FILTERS_ARRAY, filters.toTypedArray())
// on API 25, `String[]` or `ArrayList` extras are null when using the shortcut // on API 25, `String[]` or `ArrayList` extras are null when using the shortcut
// so we use a joined `String` as fallback // so we use a joined `String` as fallback
.putExtra(EXTRA_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR)) .putExtra(EXTRA_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR))
}
explorerPath != null -> Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java) ENTRY_VIEWER_PAGE_ROUTE_NAME -> {
.putExtra(EXTRA_KEY_PAGE, "/explorer") if (viewUri == null) {
result.error("pin-viewUri", "viewer shortcut requires URI", null)
return
}
Intent(Intent.ACTION_VIEW, viewUri, context, MainActivity::class.java)
}
EXPLORER_PAGE_ROUTE_NAME -> {
Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
.putExtra(EXTRA_KEY_PAGE, route)
.putExtra(EXTRA_KEY_EXPLORER_PATH, explorerPath) .putExtra(EXTRA_KEY_EXPLORER_PATH, explorerPath)
}
MAP_PAGE_ROUTE_NAME -> {
if (geoUri == null) {
result.error("pin-geoUri", "map shortcut requires URI", null)
return
}
Intent(Intent.ACTION_VIEW, geoUri, context, MainActivity::class.java).apply {
putExtra(EXTRA_KEY_PAGE, route)
// filters are optional
filters?.let {
putExtra(EXTRA_KEY_FILTERS_ARRAY, it.toTypedArray())
// on API 25, `String[]` or `ArrayList` extras are null when using the shortcut
// so we use a joined `String` as fallback
putExtra(EXTRA_KEY_FILTERS_STRING, it.joinToString(EXTRA_STRING_ARRAY_SEPARATOR))
}
}
}
uri != null -> Intent(Intent.ACTION_VIEW, uri, context, MainActivity::class.java)
else -> { else -> {
result.error("pin-intent", "failed to build intent", null) result.error("pin-route", "unsupported shortcut route=$route", null)
return return
} }
} }

View file

@ -1,3 +1,4 @@
import 'package:aves/utils/math_utils.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
// e.g. `geo:44.4361283,26.1027248?z=4.0(Bucharest)` // e.g. `geo:44.4361283,26.1027248?z=4.0(Bucharest)`
@ -24,3 +25,13 @@ import 'package:latlong2/latlong.dart';
} }
return null; return null;
} }
String toGeoUri(LatLng latLng, {double? zoom}) {
final latitude = roundToPrecision(latLng.latitude, decimals: 6);
final longitude = roundToPrecision(latLng.longitude, decimals: 6);
var uri = 'geo:$latitude,$longitude?q=$latitude,$longitude';
if (zoom != null) {
uri += '&z=$zoom';
}
return uri;
}

View file

@ -106,6 +106,7 @@ class Contributors {
Contributor('splice11', 'trenchedgrandpa@protonmail.com'), Contributor('splice11', 'trenchedgrandpa@protonmail.com'),
Contributor('Ihor Hordiichuk', 'igor_ck@outlook.com'), Contributor('Ihor Hordiichuk', 'igor_ck@outlook.com'),
Contributor('João Palmeiro', 'joaommpalmeiro@gmail.com'), Contributor('João Palmeiro', 'joaommpalmeiro@gmail.com'),
Contributor('Whoever4976', 'wolffjonas47@gmail.com'),
// Contributor('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali // Contributor('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali
// Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese // Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese
// Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese // Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese

View file

@ -1,11 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/geo/uri.dart';
import 'package:aves/model/app_inventory.dart'; import 'package:aves/model/app_inventory.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/utils/math_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
@ -30,7 +30,15 @@ abstract class AppService {
Future<bool> shareSingle(String uri, String mimeType); Future<bool> shareSingle(String uri, String mimeType);
Future<void> pinToHomeScreen(String label, AvesEntry? coverEntry, {Set<CollectionFilter>? filters, String? explorerPath, String? uri}); Future<void> pinToHomeScreen(
String label,
AvesEntry? coverEntry, {
required String route,
Set<CollectionFilter>? filters,
String? path,
String? viewUri,
String? geoUri,
});
} }
class PlatformAppService implements AppService { class PlatformAppService implements AppService {
@ -138,13 +146,9 @@ class PlatformAppService implements AppService {
@override @override
Future<bool> openMap(LatLng latLng) async { Future<bool> openMap(LatLng latLng) async {
final latitude = roundToPrecision(latLng.latitude, decimals: 6);
final longitude = roundToPrecision(latLng.longitude, decimals: 6);
final geoUri = 'geo:$latitude,$longitude?q=$latitude,$longitude';
try { try {
final result = await _platform.invokeMethod('openMap', <String, dynamic>{ final result = await _platform.invokeMethod('openMap', <String, dynamic>{
'geoUri': geoUri, 'geoUri': toGeoUri(latLng),
}); });
if (result != null) return result as bool; if (result != null) return result as bool;
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
@ -203,7 +207,15 @@ class PlatformAppService implements AppService {
// app shortcuts // app shortcuts
@override @override
Future<void> pinToHomeScreen(String label, AvesEntry? coverEntry, {Set<CollectionFilter>? filters, String? explorerPath, String? uri}) async { Future<void> pinToHomeScreen(
String label,
AvesEntry? coverEntry, {
required String route,
Set<CollectionFilter>? filters,
String? path,
String? viewUri,
String? geoUri,
}) async {
Uint8List? iconBytes; Uint8List? iconBytes;
if (coverEntry != null) { if (coverEntry != null) {
final size = coverEntry.isVideo ? 0.0 : 256.0; final size = coverEntry.isVideo ? 0.0 : 256.0;
@ -221,9 +233,11 @@ class PlatformAppService implements AppService {
await _platform.invokeMethod('pinShortcut', <String, dynamic>{ await _platform.invokeMethod('pinShortcut', <String, dynamic>{
'label': label, 'label': label,
'iconBytes': iconBytes, 'iconBytes': iconBytes,
'route': route,
'filters': filters?.map((filter) => filter.toJson()).toList(), 'filters': filters?.map((filter) => filter.toJson()).toList(),
'explorerPath': explorerPath, 'path': path,
'uri': uri, 'viewUri': viewUri,
'geoUri': geoUri,
}); });
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);

View file

@ -11,6 +11,7 @@ extension ExtraMapActionView on MapAction {
MapAction.openMapApp => l10n.entryActionOpenMap, MapAction.openMapApp => l10n.entryActionOpenMap,
MapAction.zoomIn => l10n.mapZoomInTooltip, MapAction.zoomIn => l10n.mapZoomInTooltip,
MapAction.zoomOut => l10n.mapZoomOutTooltip, MapAction.zoomOut => l10n.mapZoomOutTooltip,
MapAction.addShortcut => l10n.collectionActionAddShortcut,
}; };
} }
@ -22,6 +23,7 @@ extension ExtraMapActionView on MapAction {
MapAction.openMapApp => AIcons.openOutside, MapAction.openMapApp => AIcons.openOutside,
MapAction.zoomIn => AIcons.zoomIn, MapAction.zoomIn => AIcons.zoomIn,
MapAction.zoomOut => AIcons.zoomOut, MapAction.zoomOut => AIcons.zoomOut,
MapAction.addShortcut => AIcons.addShortcut,
}; };
} }
} }

View file

@ -26,6 +26,7 @@ import 'package:aves/theme/durations.dart';
import 'package:aves/theme/themes.dart'; import 'package:aves/theme/themes.dart';
import 'package:aves/utils/collection_utils.dart'; import 'package:aves/utils/collection_utils.dart';
import 'package:aves/utils/mime_utils.dart'; import 'package:aves/utils/mime_utils.dart';
import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/action_mixins/entry_editor.dart'; import 'package:aves/widgets/common/action_mixins/entry_editor.dart';
import 'package:aves/widgets/common/action_mixins/entry_storage.dart'; import 'package:aves/widgets/common/action_mixins/entry_storage.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart';
@ -746,7 +747,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
final (coverEntry, name) = result; final (coverEntry, name) = result;
if (name.isEmpty) return; if (name.isEmpty) return;
await appService.pinToHomeScreen(name, coverEntry, filters: filters); await appService.pinToHomeScreen(name, coverEntry, route: CollectionPage.routeName, filters: filters);
if (!device.showPinShortcutFeedback) { if (!device.showPinShortcutFeedback) {
showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback); showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback);
} }

View file

@ -423,7 +423,9 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
); );
final animate = context.select<Settings, bool>((v) => v.animate); final animate = context.select<Settings, bool>((v) => v.animate);
if (animate && (widget.heroType == HeroType.always || widget.heroType == HeroType.onTap && _tapped)) { if (animate) {
final heroType = widget.heroType;
if (heroType == HeroType.always || (heroType == HeroType.onTap && _tapped)) {
chip = Hero( chip = Hero(
tag: filter, tag: filter,
transitionOnUserGestures: true, transitionOnUserGestures: true,
@ -435,6 +437,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
), ),
); );
} }
}
return chip; return chip;
} }
} }

View file

@ -4,19 +4,31 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class MapOverlayButton extends StatelessWidget { class MapOverlayButton extends StatelessWidget {
final Key? buttonKey; final ValueWidgetBuilder<VisualDensity> builder;
final Widget icon;
final String tooltip;
final VoidCallback? onPressed;
const MapOverlayButton({ const MapOverlayButton({
super.key, super.key,
this.buttonKey, required this.builder,
required this.icon,
required this.tooltip,
required this.onPressed,
}); });
factory MapOverlayButton.icon({
Key? buttonKey,
required Widget icon,
required String tooltip,
VoidCallback? onPressed,
}) {
return MapOverlayButton(
builder: (context, visualDensity, child) => IconButton(
key: buttonKey,
iconSize: iconSize(visualDensity),
visualDensity: visualDensity,
icon: icon,
onPressed: onPressed,
tooltip: tooltip,
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector<MapThemeData, Animation<double>>( return Selector<MapThemeData, Animation<double>>(
@ -27,15 +39,10 @@ class MapOverlayButton extends StatelessWidget {
), ),
child: Selector<MapThemeData, VisualDensity>( child: Selector<MapThemeData, VisualDensity>(
selector: (context, v) => v.visualDensity, selector: (context, v) => v.visualDensity,
builder: (context, visualDensity, child) => IconButton( builder: builder,
key: buttonKey,
iconSize: 20 + 1.5 * visualDensity.horizontal,
visualDensity: visualDensity,
icon: icon,
onPressed: onPressed,
tooltip: tooltip,
),
), ),
); );
} }
static double iconSize(VisualDensity visualDensity) => 20 + 1.5 * visualDensity.horizontal;
} }

View file

@ -1,7 +1,9 @@
import 'package:aves/model/settings/enums/accessibility_animations.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/view/view.dart'; import 'package:aves/view/view.dart';
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/map/buttons/button.dart'; import 'package:aves/widgets/common/map/buttons/button.dart';
import 'package:aves/widgets/common/map/buttons/coordinate_filter.dart'; import 'package:aves/widgets/common/map/buttons/coordinate_filter.dart';
@ -10,24 +12,44 @@ import 'package:aves/widgets/common/map/map_action_delegate.dart';
import 'package:aves_map/aves_map.dart'; import 'package:aves_map/aves_map.dart';
import 'package:aves_model/aves_model.dart'; import 'package:aves_model/aves_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class MapButtonPanel extends StatelessWidget { class MapButtonPanel extends StatefulWidget {
final AvesMapController? controller; final AvesMapController controller;
final ValueNotifier<ZoomedBounds> boundsNotifier; final ValueNotifier<ZoomedBounds> boundsNotifier;
final void Function(BuildContext context)? openMapPage; final void Function(BuildContext context)? openMapPage;
final VoidCallback? resetRotation;
const MapButtonPanel({ const MapButtonPanel({
super.key, super.key,
required this.controller, required this.controller,
required this.boundsNotifier, required this.boundsNotifier,
this.openMapPage, this.openMapPage,
this.resetRotation,
}); });
@override
State<MapButtonPanel> createState() => _MapButtonPanelState();
}
class _MapButtonPanelState extends State<MapButtonPanel> {
late MapActionDelegate _actionDelegate;
@override
void initState() {
super.initState();
_updateDelegate();
}
@override
void didUpdateWidget(covariant MapButtonPanel oldWidget) {
super.didUpdateWidget(oldWidget);
_updateDelegate();
}
void _updateDelegate() => _actionDelegate = MapActionDelegate(widget.controller);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final iconTheme = IconTheme.of(context); final iconTheme = IconTheme.of(context);
@ -37,23 +59,24 @@ class MapButtonPanel extends StatelessWidget {
switch (context.select<MapThemeData, MapNavigationButton>((v) => v.navigationButton)) { switch (context.select<MapThemeData, MapNavigationButton>((v) => v.navigationButton)) {
case MapNavigationButton.back: case MapNavigationButton.back:
if (!settings.useTvLayout) { if (!settings.useTvLayout) {
navigationButton = MapOverlayButton( navigationButton = MapOverlayButton.icon(
icon: const BackButtonIcon(), icon: const BackButtonIcon(),
onPressed: () => Navigator.maybeOf(context)?.pop(), onPressed: () => Navigator.maybeOf(context)?.pop(),
tooltip: MaterialLocalizations.of(context).backButtonTooltip, tooltip: MaterialLocalizations.of(context).backButtonTooltip,
); );
} }
case MapNavigationButton.close: case MapNavigationButton.close:
navigationButton = MapOverlayButton( navigationButton = MapOverlayButton.icon(
icon: const CloseButtonIcon(), icon: const CloseButtonIcon(),
onPressed: SystemNavigator.pop, onPressed: SystemNavigator.pop,
tooltip: MaterialLocalizations.of(context).closeButtonTooltip, tooltip: MaterialLocalizations.of(context).closeButtonTooltip,
); );
case MapNavigationButton.map: case MapNavigationButton.map:
if (openMapPage != null) { final _openMapPage = widget.openMapPage;
navigationButton = MapOverlayButton( if (_openMapPage != null) {
navigationButton = MapOverlayButton.icon(
icon: const Icon(AIcons.showFullscreenCorners), icon: const Icon(AIcons.showFullscreenCorners),
onPressed: () => openMapPage?.call(context), onPressed: () => _openMapPage.call(context),
tooltip: context.l10n.openMapPageTooltip, tooltip: context.l10n.openMapPageTooltip,
); );
} }
@ -65,6 +88,11 @@ class MapButtonPanel extends StatelessWidget {
final visualDensity = context.select<MapThemeData, VisualDensity>((v) => v.visualDensity); final visualDensity = context.select<MapThemeData, VisualDensity>((v) => v.visualDensity);
final double padding = 8 + visualDensity.horizontal * 2; final double padding = 8 + visualDensity.horizontal * 2;
final actions = [
MapAction.openMapApp,
MapAction.addShortcut,
].where((action) => _actionDelegate.isVisible(context, action)).toList();
return Positioned.fill( return Positioned.fill(
child: TooltipTheme( child: TooltipTheme(
data: TooltipTheme.of(context).copyWith( data: TooltipTheme.of(context).copyWith(
@ -90,7 +118,7 @@ class MapButtonPanel extends StatelessWidget {
SizedBox(height: padding), SizedBox(height: padding),
], ],
ValueListenableBuilder<ZoomedBounds>( ValueListenableBuilder<ZoomedBounds>(
valueListenable: boundsNotifier, valueListenable: widget.boundsNotifier,
builder: (context, bounds, child) { builder: (context, bounds, child) {
final degrees = bounds.rotation; final degrees = bounds.rotation;
final opacity = degrees == 0 ? .0 : 1.0; final opacity = degrees == 0 ? .0 : 1.0;
@ -99,7 +127,7 @@ class MapButtonPanel extends StatelessWidget {
child: AnimatedOpacity( child: AnimatedOpacity(
opacity: opacity, opacity: opacity,
duration: context.select<DurationsData, Duration>((v) => v.viewerOverlayAnimation), duration: context.select<DurationsData, Duration>((v) => v.viewerOverlayAnimation),
child: MapOverlayButton( child: MapOverlayButton.icon(
icon: Transform( icon: Transform(
origin: iconSize.center(Offset.zero), origin: iconSize.center(Offset.zero),
transform: Matrix4.rotationZ(degToRadian(degrees)), transform: Matrix4.rotationZ(degToRadian(degrees)),
@ -110,7 +138,7 @@ class MapButtonPanel extends StatelessWidget {
size: iconSize, size: iconSize,
), ),
), ),
onPressed: () => resetRotation?.call(), onPressed: widget.controller.resetRotation,
tooltip: context.l10n.mapPointNorthUpTooltip, tooltip: context.l10n.mapPointNorthUpTooltip,
), ),
), ),
@ -123,7 +151,7 @@ class MapButtonPanel extends StatelessWidget {
showCoordinateFilter showCoordinateFilter
? Expanded( ? Expanded(
child: OverlayCoordinateFilterChip( child: OverlayCoordinateFilterChip(
boundsNotifier: boundsNotifier, boundsNotifier: widget.boundsNotifier,
padding: padding, padding: padding,
), ),
) )
@ -133,8 +161,31 @@ class MapButtonPanel extends StatelessWidget {
// key is expected by test driver // key is expected by test driver
child: Column( child: Column(
children: [ children: [
_buildActionButton(context, MapAction.openMapApp), if (actions.length == 1) _buildActionButton(context, actions.first),
if (actions.length > 1)
MapOverlayButton(builder: (context, visualDensity, child) {
final animations = context.read<Settings>().accessibilityAnimations;
return PopupMenuButton<MapAction>(
itemBuilder: (context) => actions
.map((action) => PopupMenuItem(
value: action,
child: MenuRow(
text: action.getText(context),
icon: action.getIcon(),
),
))
.toList(),
onSelected: (action) async {
// wait for the popup menu to hide before proceeding with the action
await Future.delayed(animations.popUpAnimationDelay * timeDilation);
_actionDelegate.onActionSelected(context, action);
},
iconSize: MapOverlayButton.iconSize(visualDensity),
popUpAnimationStyle: animations.popUpAnimationStyle,
);
}),
SizedBox(height: padding), SizedBox(height: padding),
// key is expected by test driver
_buildActionButton(context, MapAction.selectStyle, buttonKey: const Key('map-menu-layers')), _buildActionButton(context, MapAction.selectStyle, buttonKey: const Key('map-menu-layers')),
], ],
), ),
@ -161,10 +212,10 @@ class MapButtonPanel extends StatelessWidget {
); );
} }
Widget _buildActionButton(BuildContext context, MapAction action, {Key? buttonKey}) => MapOverlayButton( Widget _buildActionButton(BuildContext context, MapAction action, {Key? buttonKey}) => MapOverlayButton.icon(
buttonKey: buttonKey, buttonKey: buttonKey,
icon: action.getIcon(), icon: action.getIcon(),
onPressed: () => MapActionDelegate(controller).onActionSelected(context, action), onPressed: () => _actionDelegate.onActionSelected(context, action),
tooltip: action.getText(context), tooltip: action.getText(context),
); );
} }

View file

@ -33,7 +33,7 @@ import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class GeoMap extends StatefulWidget { class GeoMap extends StatefulWidget {
final AvesMapController? controller; final AvesMapController controller;
final CollectionLens? collection; final CollectionLens? collection;
final List<AvesEntry>? entries; final List<AvesEntry>? entries;
final Size availableSize; final Size availableSize;
@ -60,7 +60,7 @@ class GeoMap extends StatefulWidget {
const GeoMap({ const GeoMap({
super.key, super.key,
this.controller, required this.controller,
this.collection, this.collection,
this.entries, this.entries,
required this.availableSize, required this.availableSize,
@ -124,10 +124,7 @@ class _GeoMapState extends State<GeoMap> {
void _registerWidget(GeoMap widget) { void _registerWidget(GeoMap widget) {
widget.collection?.addListener(_onCollectionChanged); widget.collection?.addListener(_onCollectionChanged);
final controller = widget.controller; _subscriptions.add(widget.controller.markerLocationChanges.listen((event) => _onCollectionChanged()));
if (controller != null) {
_subscriptions.add(controller.markerLocationChanges.listen((event) => _onCollectionChanged()));
}
} }
void _unregisterWidget(GeoMap widget) { void _unregisterWidget(GeoMap widget) {
@ -164,7 +161,6 @@ class _GeoMapState extends State<GeoMap> {
); );
bool _isMarkerImageReady(MarkerKey<AvesEntry> key) => key.entry.isThumbnailReady(extent: MapThemeData.markerImageExtent); bool _isMarkerImageReady(MarkerKey<AvesEntry> key) => key.entry.isThumbnailReady(extent: MapThemeData.markerImageExtent);
final controller = widget.controller;
Widget child = const SizedBox(); Widget child = const SizedBox();
if (mapStyle != null) { if (mapStyle != null) {
switch (mapStyle) { switch (mapStyle) {
@ -172,7 +168,7 @@ class _GeoMapState extends State<GeoMap> {
case EntryMapStyle.googleHybrid: case EntryMapStyle.googleHybrid:
case EntryMapStyle.googleTerrain: case EntryMapStyle.googleTerrain:
child = mobileServices.buildMap<AvesEntry>( child = mobileServices.buildMap<AvesEntry>(
controller: controller, controller: widget.controller,
clusterListenable: _clusterChangeNotifier, clusterListenable: _clusterChangeNotifier,
boundsNotifier: _boundsNotifier, boundsNotifier: _boundsNotifier,
style: mapStyle, style: mapStyle,
@ -194,7 +190,7 @@ class _GeoMapState extends State<GeoMap> {
case EntryMapStyle.osmHot: case EntryMapStyle.osmHot:
case EntryMapStyle.stamenWatercolor: case EntryMapStyle.stamenWatercolor:
child = EntryLeafletMap<AvesEntry>( child = EntryLeafletMap<AvesEntry>(
controller: controller, controller: widget.controller,
clusterListenable: _clusterChangeNotifier, clusterListenable: _clusterChangeNotifier,
boundsNotifier: _boundsNotifier, boundsNotifier: _boundsNotifier,
minZoom: 2, minZoom: 2,
@ -324,11 +320,7 @@ class _GeoMapState extends State<GeoMap> {
Widget replacement = Stack( Widget replacement = Stack(
children: [ children: [
const MapDecorator(), const MapDecorator(),
MapButtonPanel( _buildButtonPanel(context),
controller: controller,
boundsNotifier: _boundsNotifier,
openMapPage: widget.openMapPage,
),
], ],
); );
if (mapHeight != null) { if (mapHeight != null) {
@ -560,13 +552,12 @@ class _GeoMapState extends State<GeoMap> {
Widget _decorateMap(BuildContext context, Widget? child) => MapDecorator(child: child); Widget _decorateMap(BuildContext context, Widget? child) => MapDecorator(child: child);
Widget _buildButtonPanel(VoidCallback resetRotation) { Widget _buildButtonPanel(BuildContext context) {
if (settings.useTvLayout) return const SizedBox(); if (settings.useTvLayout) return const SizedBox();
return MapButtonPanel( return MapButtonPanel(
controller: widget.controller, controller: widget.controller,
boundsNotifier: _boundsNotifier, boundsNotifier: _boundsNotifier,
openMapPage: widget.openMapPage, openMapPage: widget.openMapPage,
resetRotation: resetRotation,
); );
} }
} }

View file

@ -14,13 +14,13 @@ import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class EntryLeafletMap<T> extends StatefulWidget { class EntryLeafletMap<T> extends StatefulWidget {
final AvesMapController? controller; final AvesMapController controller;
final Listenable clusterListenable; final Listenable clusterListenable;
final ValueNotifier<ZoomedBounds> boundsNotifier; final ValueNotifier<ZoomedBounds> boundsNotifier;
final double minZoom, maxZoom; final double minZoom, maxZoom;
final EntryMapStyle style; final EntryMapStyle style;
final TransitionBuilder decoratorBuilder; final TransitionBuilder decoratorBuilder;
final ButtonPanelBuilder buttonPanelBuilder; final WidgetBuilder buttonPanelBuilder;
final MarkerClusterBuilder<T> markerClusterBuilder; final MarkerClusterBuilder<T> markerClusterBuilder;
final MarkerWidgetBuilder<T> markerWidgetBuilder; final MarkerWidgetBuilder<T> markerWidgetBuilder;
final ValueNotifier<LatLng?>? dotLocationNotifier; final ValueNotifier<LatLng?>? dotLocationNotifier;
@ -34,7 +34,7 @@ class EntryLeafletMap<T> extends StatefulWidget {
const EntryLeafletMap({ const EntryLeafletMap({
super.key, super.key,
this.controller, required this.controller,
required this.clusterListenable, required this.clusterListenable,
required this.boundsNotifier, required this.boundsNotifier,
this.minZoom = 0, this.minZoom = 0,
@ -94,11 +94,10 @@ class _EntryLeafletMapState<T> extends State<EntryLeafletMap<T>> with TickerProv
void _registerWidget(EntryLeafletMap<T> widget) { void _registerWidget(EntryLeafletMap<T> widget) {
final avesMapController = widget.controller; final avesMapController = widget.controller;
if (avesMapController != null) {
_subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(event.latLng))); _subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(event.latLng)));
_subscriptions.add(avesMapController.zoomCommands.listen((event) => _zoomBy(event.delta))); _subscriptions.add(avesMapController.zoomCommands.listen((event) => _zoomBy(event.delta)));
} _subscriptions.add(avesMapController.rotationResetCommands.listen((_) => _resetRotation()));
_subscriptions.add(_leafletMapController.mapEventStream.listen((event) => _updateVisibleRegion())); _subscriptions.add(_leafletMapController.mapEventStream.listen((_) => _updateVisibleRegion()));
widget.clusterListenable.addListener(_updateMarkers); widget.clusterListenable.addListener(_updateMarkers);
widget.boundsNotifier.addListener(_onBoundsChanged); widget.boundsNotifier.addListener(_onBoundsChanged);
} }
@ -116,7 +115,7 @@ class _EntryLeafletMapState<T> extends State<EntryLeafletMap<T>> with TickerProv
return Stack( return Stack(
children: [ children: [
widget.decoratorBuilder(context, _buildMap()), widget.decoratorBuilder(context, _buildMap()),
widget.buttonPanelBuilder(_resetRotation), widget.buttonPanelBuilder(context),
], ],
); );
} }
@ -240,7 +239,7 @@ class _EntryLeafletMapState<T> extends State<EntryLeafletMap<T>> with TickerProv
void _onIdle() { void _onIdle() {
if (!mounted) return; if (!mounted) return;
widget.controller?.notifyIdle(bounds); widget.controller.notifyIdle(bounds);
_updateMarkers(); _updateMarkers();
} }

View file

@ -1,23 +1,54 @@
import 'package:aves/geo/uri.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/view/view.dart'; import 'package:aves/view/view.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/add_shortcut_dialog.dart';
import 'package:aves/widgets/dialogs/selection_dialogs/common.dart'; import 'package:aves/widgets/dialogs/selection_dialogs/common.dart';
import 'package:aves/widgets/dialogs/selection_dialogs/single_selection.dart'; import 'package:aves/widgets/dialogs/selection_dialogs/single_selection.dart';
import 'package:aves/widgets/map/map_page.dart';
import 'package:aves_map/aves_map.dart'; import 'package:aves_map/aves_map.dart';
import 'package:aves_model/aves_model.dart'; import 'package:aves_model/aves_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart';
class MapActionDelegate { class MapActionDelegate with FeedbackMixin {
final AvesMapController? controller; final AvesMapController controller;
const MapActionDelegate(this.controller); const MapActionDelegate(this.controller);
bool isVisible(BuildContext context, MapAction action) {
switch (action) {
case MapAction.selectStyle:
case MapAction.openMapApp:
case MapAction.zoomIn:
case MapAction.zoomOut:
return true;
case MapAction.addShortcut:
return device.canPinShortcut && context.currentRouteName == MapPage.routeName;
}
}
void onActionSelected(BuildContext context, MapAction action) { void onActionSelected(BuildContext context, MapAction action) {
switch (action) { switch (action) {
case MapAction.selectStyle: case MapAction.selectStyle:
showSelectionDialog<EntryMapStyle>( _selectStyle(context);
case MapAction.openMapApp:
OpenMapAppNotification().dispatch(context);
case MapAction.zoomIn:
controller.zoomBy(1);
case MapAction.zoomOut:
controller.zoomBy(-1);
case MapAction.addShortcut:
_addShortcut(context);
}
}
Future<void> _selectStyle(BuildContext context) => showSelectionDialog<EntryMapStyle>(
context: context, context: context,
builder: (context) => AvesSingleSelectionDialog<EntryMapStyle?>( builder: (context) => AvesSingleSelectionDialog<EntryMapStyle?>(
initialValue: settings.mapStyle, initialValue: settings.mapStyle,
@ -26,12 +57,38 @@ class MapActionDelegate {
), ),
onSelection: (v) => settings.mapStyle = v, onSelection: (v) => settings.mapStyle = v,
); );
case MapAction.openMapApp:
OpenMapAppNotification().dispatch(context); Future<void> _addShortcut(BuildContext context) async {
case MapAction.zoomIn: final idleBounds = controller.idleBounds;
controller?.zoomBy(1); if (idleBounds == null) {
case MapAction.zoomOut: showFeedback(context, FeedbackType.warn, context.l10n.genericFailureFeedback);
controller?.zoomBy(-1); return;
}
final collection = context.read<CollectionLens>();
final result = await showDialog<(AvesEntry?, String)>(
context: context,
builder: (context) => AddShortcutDialog(
defaultName: '',
collection: collection,
),
routeSettings: const RouteSettings(name: AddShortcutDialog.routeName),
);
if (result == null) return;
final (coverEntry, name) = result;
if (name.isEmpty) return;
final geoUri = toGeoUri(idleBounds.projectedCenter, zoom: idleBounds.zoom);
await appService.pinToHomeScreen(
name,
coverEntry,
route: MapPage.routeName,
filters: collection.filters,
geoUri: geoUri,
);
if (!device.showPinShortcutFeedback) {
showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback);
} }
} }
} }

View file

@ -9,6 +9,7 @@ import 'package:aves/services/common/services.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/add_shortcut_dialog.dart'; import 'package:aves/widgets/dialogs/add_shortcut_dialog.dart';
import 'package:aves/widgets/explorer/explorer_page.dart';
import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart';
import 'package:aves/widgets/stats/stats_page.dart'; import 'package:aves/widgets/stats/stats_page.dart';
import 'package:aves_model/aves_model.dart'; import 'package:aves_model/aves_model.dart';
@ -84,7 +85,7 @@ class ExplorerActionDelegate with FeedbackMixin {
final (coverEntry, name) = result; final (coverEntry, name) = result;
if (name.isEmpty) return; if (name.isEmpty) return;
await appService.pinToHomeScreen(name, coverEntry, explorerPath: filter.path); await appService.pinToHomeScreen(name, coverEntry, route: ExplorerPage.routeName, path: filter.path);
if (!device.showPinShortcutFeedback) { if (!device.showPinShortcutFeedback) {
showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback); showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback);
} }

View file

@ -377,7 +377,10 @@ class _HomePageState extends State<HomePage> {
return buildRoute((context) { return buildRoute((context) {
final mapCollection = CollectionLens( final mapCollection = CollectionLens(
source: source, source: source,
filters: {LocationFilter.located}, filters: {
LocationFilter.located,
if (filters != null) ...filters,
},
); );
return MapPage( return MapPage(
collection: mapCollection, collection: mapCollection,

View file

@ -5,8 +5,9 @@ import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/location.dart'; import 'package:aves/model/entry/extensions/location.dart';
import 'package:aves/model/filters/coordinate.dart'; import 'package:aves/model/filters/coordinate.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/media/geotiff.dart'; import 'package:aves/model/filters/location.dart';
import 'package:aves/model/highlight.dart'; import 'package:aves/model/highlight.dart';
import 'package:aves/model/media/geotiff.dart';
import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/enums/accessibility_animations.dart';
import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/settings/enums/map_style.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
@ -62,10 +63,15 @@ class MapPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiProvider(
providers: [
// do not rely on the `HighlightInfoProvider` app level // do not rely on the `HighlightInfoProvider` app level
// as the map can be stacked on top of other pages // as the map can be stacked on top of other pages
// that catch highlight events and will not let it bubble up // that catch highlight events and will not let it bubble up
return HighlightInfoProvider( HighlightInfoProvider(),
// opening collection can be used by map actions
ChangeNotifierProvider<CollectionLens>.value(value: collection),
],
child: AvesScaffold( child: AvesScaffold(
body: SafeArea( body: SafeArea(
left: false, left: false,
@ -442,10 +448,16 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
Navigator.maybeOf(context)?.pushAndRemoveUntil( Navigator.maybeOf(context)?.pushAndRemoveUntil(
MaterialPageRoute( MaterialPageRoute(
settings: const RouteSettings(name: CollectionPage.routeName), settings: const RouteSettings(name: CollectionPage.routeName),
builder: (context) => CollectionPage( builder: (context) {
final filters = {...openingCollection.filters, filter};
if (filter is CoordinateFilter) {
filters.removeWhere((v) => (v is CoordinateFilter && v != filter) || v == LocationFilter.located);
}
return CollectionPage(
source: openingCollection.source, source: openingCollection.source,
filters: {...openingCollection.filters, filter}, filters: filters,
), );
},
), ),
(route) => false, (route) => false,
); );

View file

@ -34,6 +34,7 @@ import 'package:aves/widgets/viewer/action/printer.dart';
import 'package:aves/widgets/viewer/action/single_entry_editor.dart'; import 'package:aves/widgets/viewer/action/single_entry_editor.dart';
import 'package:aves/widgets/viewer/controls/notifications.dart'; import 'package:aves/widgets/viewer/controls/notifications.dart';
import 'package:aves/widgets/viewer/debug/debug_page.dart'; import 'package:aves/widgets/viewer/debug/debug_page.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
import 'package:aves/widgets/viewer/multipage/conductor.dart'; import 'package:aves/widgets/viewer/multipage/conductor.dart';
import 'package:aves/widgets/viewer/source_viewer_page.dart'; import 'package:aves/widgets/viewer/source_viewer_page.dart';
import 'package:aves/widgets/viewer/video/conductor.dart'; import 'package:aves/widgets/viewer/video/conductor.dart';
@ -395,7 +396,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
final name = result.$2; final name = result.$2;
if (name.isEmpty) return; if (name.isEmpty) return;
await appService.pinToHomeScreen(name, targetEntry, uri: targetEntry.uri); await appService.pinToHomeScreen(name, targetEntry, route: EntryViewerPage.routeName, viewUri: targetEntry.uri);
if (!device.showPinShortcutFeedback) { if (!device.showPinShortcutFeedback) {
showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback); showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback);
} }

View file

@ -16,6 +16,8 @@ class AvesMapController {
Stream<MapControllerZoomEvent> get zoomCommands => _events.where((event) => event is MapControllerZoomEvent).cast<MapControllerZoomEvent>(); Stream<MapControllerZoomEvent> get zoomCommands => _events.where((event) => event is MapControllerZoomEvent).cast<MapControllerZoomEvent>();
Stream<MapControllerRotationResetEvent> get rotationResetCommands => _events.where((event) => event is MapControllerRotationResetEvent).cast<MapControllerRotationResetEvent>();
Stream<MapIdleUpdate> get idleUpdates => _events.where((event) => event is MapIdleUpdate).cast<MapIdleUpdate>(); Stream<MapIdleUpdate> get idleUpdates => _events.where((event) => event is MapIdleUpdate).cast<MapIdleUpdate>();
Stream<MapMarkerLocationChangeEvent> get markerLocationChanges => _events.where((event) => event is MapMarkerLocationChangeEvent).cast<MapMarkerLocationChangeEvent>(); Stream<MapMarkerLocationChangeEvent> get markerLocationChanges => _events.where((event) => event is MapMarkerLocationChangeEvent).cast<MapMarkerLocationChangeEvent>();
@ -41,6 +43,8 @@ class AvesMapController {
void zoomBy(double delta) => _streamController.add(MapControllerZoomEvent(delta)); void zoomBy(double delta) => _streamController.add(MapControllerZoomEvent(delta));
void resetRotation() => _streamController.add(MapControllerRotationResetEvent());
void notifyIdle(ZoomedBounds bounds) { void notifyIdle(ZoomedBounds bounds) {
_idleBounds = bounds; _idleBounds = bounds;
_streamController.add(MapIdleUpdate(bounds)); _streamController.add(MapIdleUpdate(bounds));
@ -61,6 +65,8 @@ class MapControllerZoomEvent {
MapControllerZoomEvent(this.delta); MapControllerZoomEvent(this.delta);
} }
class MapControllerRotationResetEvent {}
class MapIdleUpdate { class MapIdleUpdate {
final ZoomedBounds bounds; final ZoomedBounds bounds;

View file

@ -3,7 +3,6 @@ import 'package:aves_map/src/marker/key.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
typedef ButtonPanelBuilder = Widget Function(VoidCallback resetRotation);
typedef MarkerClusterBuilder<T> = Map<MarkerKey<T>, GeoEntry<T>> Function(); typedef MarkerClusterBuilder<T> = Map<MarkerKey<T>, GeoEntry<T>> Function();
typedef MarkerWidgetBuilder<T> = Widget Function(MarkerKey<T> key); typedef MarkerWidgetBuilder<T> = Widget Function(MarkerKey<T> key);
typedef MarkerImageReadyChecker<T> = bool Function(MarkerKey<T> key); typedef MarkerImageReadyChecker<T> = bool Function(MarkerKey<T> key);

View file

@ -1,6 +1,9 @@
enum MapAction { enum MapAction {
// any map panel
selectStyle, selectStyle,
openMapApp, openMapApp,
zoomIn, zoomIn,
zoomOut, zoomOut,
// full page only
addShortcut,
} }

View file

@ -12,12 +12,12 @@ abstract class MobileServices {
List<EntryMapStyle> get mapStyles; List<EntryMapStyle> get mapStyles;
Widget buildMap<T>({ Widget buildMap<T>({
required AvesMapController? controller, required AvesMapController controller,
required Listenable clusterListenable, required Listenable clusterListenable,
required ValueNotifier<ZoomedBounds> boundsNotifier, required ValueNotifier<ZoomedBounds> boundsNotifier,
required EntryMapStyle style, required EntryMapStyle style,
required TransitionBuilder decoratorBuilder, required TransitionBuilder decoratorBuilder,
required ButtonPanelBuilder buttonPanelBuilder, required WidgetBuilder buttonPanelBuilder,
required MarkerClusterBuilder<T> markerClusterBuilder, required MarkerClusterBuilder<T> markerClusterBuilder,
required MarkerWidgetBuilder<T> markerWidgetBuilder, required MarkerWidgetBuilder<T> markerWidgetBuilder,
required MarkerImageReadyChecker<T> markerImageReadyChecker, required MarkerImageReadyChecker<T> markerImageReadyChecker,

View file

@ -46,12 +46,12 @@ class PlatformMobileServices extends MobileServices {
@override @override
Widget buildMap<T>({ Widget buildMap<T>({
required AvesMapController? controller, required AvesMapController controller,
required Listenable clusterListenable, required Listenable clusterListenable,
required ValueNotifier<ZoomedBounds> boundsNotifier, required ValueNotifier<ZoomedBounds> boundsNotifier,
required EntryMapStyle style, required EntryMapStyle style,
required TransitionBuilder decoratorBuilder, required TransitionBuilder decoratorBuilder,
required ButtonPanelBuilder buttonPanelBuilder, required WidgetBuilder buttonPanelBuilder,
required MarkerClusterBuilder<T> markerClusterBuilder, required MarkerClusterBuilder<T> markerClusterBuilder,
required MarkerWidgetBuilder<T> markerWidgetBuilder, required MarkerWidgetBuilder<T> markerWidgetBuilder,
required MarkerImageReadyChecker<T> markerImageReadyChecker, required MarkerImageReadyChecker<T> markerImageReadyChecker,

View file

@ -9,13 +9,13 @@ import 'package:latlong2/latlong.dart' as ll;
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class EntryGoogleMap<T> extends StatefulWidget { class EntryGoogleMap<T> extends StatefulWidget {
final AvesMapController? controller; final AvesMapController controller;
final Listenable clusterListenable; final Listenable clusterListenable;
final ValueNotifier<ZoomedBounds> boundsNotifier; final ValueNotifier<ZoomedBounds> boundsNotifier;
final double? minZoom, maxZoom; final double? minZoom, maxZoom;
final EntryMapStyle style; final EntryMapStyle style;
final TransitionBuilder decoratorBuilder; final TransitionBuilder decoratorBuilder;
final ButtonPanelBuilder buttonPanelBuilder; final WidgetBuilder buttonPanelBuilder;
final MarkerClusterBuilder<T> markerClusterBuilder; final MarkerClusterBuilder<T> markerClusterBuilder;
final MarkerWidgetBuilder<T> markerWidgetBuilder; final MarkerWidgetBuilder<T> markerWidgetBuilder;
final MarkerImageReadyChecker<T> markerImageReadyChecker; final MarkerImageReadyChecker<T> markerImageReadyChecker;
@ -29,7 +29,7 @@ class EntryGoogleMap<T> extends StatefulWidget {
const EntryGoogleMap({ const EntryGoogleMap({
super.key, super.key,
this.controller, required this.controller,
required this.clusterListenable, required this.clusterListenable,
required this.boundsNotifier, required this.boundsNotifier,
this.minZoom, this.minZoom,
@ -93,10 +93,9 @@ class _EntryGoogleMapState<T> extends State<EntryGoogleMap<T>> {
void _registerWidget(EntryGoogleMap<T> widget) { void _registerWidget(EntryGoogleMap<T> widget) {
final avesMapController = widget.controller; final avesMapController = widget.controller;
if (avesMapController != null) {
_subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(_toServiceLatLng(event.latLng)))); _subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(_toServiceLatLng(event.latLng))));
_subscriptions.add(avesMapController.zoomCommands.listen((event) => _zoomBy(event.delta))); _subscriptions.add(avesMapController.zoomCommands.listen((event) => _zoomBy(event.delta)));
} _subscriptions.add(avesMapController.rotationResetCommands.listen((_) => _resetRotation()));
widget.clusterListenable.addListener(_updateMarkers); widget.clusterListenable.addListener(_updateMarkers);
} }
@ -125,7 +124,7 @@ class _EntryGoogleMapState<T> extends State<EntryGoogleMap<T>> {
}, },
), ),
widget.decoratorBuilder(context, _buildMap()), widget.decoratorBuilder(context, _buildMap()),
widget.buttonPanelBuilder(_resetRotation), widget.buttonPanelBuilder(context),
], ],
); );
} }
@ -241,7 +240,7 @@ class _EntryGoogleMapState<T> extends State<EntryGoogleMap<T>> {
void _onIdle() { void _onIdle() {
if (!mounted) return; if (!mounted) return;
widget.controller?.notifyIdle(bounds); widget.controller.notifyIdle(bounds);
_updateMarkers(); _updateMarkers();
} }

View file

@ -18,12 +18,12 @@ class PlatformMobileServices extends MobileServices {
@override @override
Widget buildMap<T>({ Widget buildMap<T>({
required AvesMapController? controller, required AvesMapController controller,
required Listenable clusterListenable, required Listenable clusterListenable,
required ValueNotifier<ZoomedBounds> boundsNotifier, required ValueNotifier<ZoomedBounds> boundsNotifier,
required EntryMapStyle style, required EntryMapStyle style,
required TransitionBuilder decoratorBuilder, required TransitionBuilder decoratorBuilder,
required ButtonPanelBuilder buttonPanelBuilder, required WidgetBuilder buttonPanelBuilder,
required MarkerClusterBuilder<T> markerClusterBuilder, required MarkerClusterBuilder<T> markerClusterBuilder,
required MarkerWidgetBuilder<T> markerWidgetBuilder, required MarkerWidgetBuilder<T> markerWidgetBuilder,
required MarkerImageReadyChecker<T> markerImageReadyChecker, required MarkerImageReadyChecker<T> markerImageReadyChecker,