diff --git a/CHANGELOG.md b/CHANGELOG.md
index 48eae08ac..07fbaf824 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
+### Added
+
+- Map: create shortcut to custom region and filters
+
### Fixed
- crash when loading large collection
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt
index 253f9c770..0bf7591ee 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt
@@ -314,6 +314,7 @@ open class MainActivity : FlutterFragmentActivity() {
return hashMapOf(
INTENT_DATA_KEY_ACTION to INTENT_ACTION_VIEW_GEO,
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
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 SEARCH_PAGE_ROUTE_NAME = "/search"
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt
index 65c3f38a3..c29b8718f 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt
@@ -23,11 +23,15 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.request.RequestOptions
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_FILTERS_ARRAY
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_STRING_ARRAY_SEPARATOR
+import deckers.thibault.aves.MainActivity.Companion.MAP_PAGE_ROUTE_NAME
import deckers.thibault.aves.R
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
@@ -354,12 +358,17 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
// shortcuts
private fun pinShortcut(call: MethodCall, result: MethodChannel.Result) {
+ // common arguments
val label = call.argument("label")
val iconBytes = call.argument("iconBytes")
+ val route = call.argument("route")
+ // route dependent arguments
val filters = call.argument>("filters")
- val explorerPath = call.argument("explorerPath")
- val uri = call.argument("uri")?.let { Uri.parse(it) }
- if (label == null) {
+ val explorerPath = call.argument("path")
+ val viewUri = call.argument("viewUri")?.let { Uri.parse(it) }
+ val geoUri = call.argument("geoUri")?.let { Uri.parse(it) }
+
+ if (label == null || route == null) {
result.error("pin-args", "missing arguments", null)
return
}
@@ -383,24 +392,60 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
// so that foreground is rendered at the intended scale
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 {
- filters != null -> Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
- .putExtra(EXTRA_KEY_PAGE, "/collection")
- .putExtra(EXTRA_KEY_FILTERS_ARRAY, filters.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, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR))
+ val intent: Intent = when (route) {
+ COLLECTION_PAGE_ROUTE_NAME -> {
+ 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())
+ // 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, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR))
+ }
- explorerPath != null -> Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
- .putExtra(EXTRA_KEY_PAGE, "/explorer")
- .putExtra(EXTRA_KEY_EXPLORER_PATH, explorerPath)
+ ENTRY_VIEWER_PAGE_ROUTE_NAME -> {
+ 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)
+ }
+
+ 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 -> {
- result.error("pin-intent", "failed to build intent", null)
+ result.error("pin-route", "unsupported shortcut route=$route", null)
return
}
}
diff --git a/lib/geo/uri.dart b/lib/geo/uri.dart
index 5374bc9cd..3241d0a39 100644
--- a/lib/geo/uri.dart
+++ b/lib/geo/uri.dart
@@ -1,3 +1,4 @@
+import 'package:aves/utils/math_utils.dart';
import 'package:latlong2/latlong.dart';
// e.g. `geo:44.4361283,26.1027248?z=4.0(Bucharest)`
@@ -24,3 +25,13 @@ import 'package:latlong2/latlong.dart';
}
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;
+}
diff --git a/lib/model/app/contributors.dart b/lib/model/app/contributors.dart
index c56ab4269..36f4a4522 100644
--- a/lib/model/app/contributors.dart
+++ b/lib/model/app/contributors.dart
@@ -106,6 +106,7 @@ class Contributors {
Contributor('splice11', 'trenchedgrandpa@protonmail.com'),
Contributor('Ihor Hordiichuk', 'igor_ck@outlook.com'),
Contributor('João Palmeiro', 'joaommpalmeiro@gmail.com'),
+ Contributor('Whoever4976', 'wolffjonas47@gmail.com'),
// Contributor('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali
// Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese
// Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese
diff --git a/lib/services/app_service.dart b/lib/services/app_service.dart
index 705cc9b2d..eee552b1c 100644
--- a/lib/services/app_service.dart
+++ b/lib/services/app_service.dart
@@ -1,11 +1,11 @@
import 'dart:async';
+import 'package:aves/geo/uri.dart';
import 'package:aves/model/app_inventory.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/services/common/services.dart';
-import 'package:aves/utils/math_utils.dart';
import 'package:collection/collection.dart';
import 'package:flutter/services.dart';
import 'package:latlong2/latlong.dart';
@@ -30,7 +30,15 @@ abstract class AppService {
Future shareSingle(String uri, String mimeType);
- Future pinToHomeScreen(String label, AvesEntry? coverEntry, {Set? filters, String? explorerPath, String? uri});
+ Future pinToHomeScreen(
+ String label,
+ AvesEntry? coverEntry, {
+ required String route,
+ Set? filters,
+ String? path,
+ String? viewUri,
+ String? geoUri,
+ });
}
class PlatformAppService implements AppService {
@@ -138,13 +146,9 @@ class PlatformAppService implements AppService {
@override
Future 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 {
final result = await _platform.invokeMethod('openMap', {
- 'geoUri': geoUri,
+ 'geoUri': toGeoUri(latLng),
});
if (result != null) return result as bool;
} on PlatformException catch (e, stack) {
@@ -203,7 +207,15 @@ class PlatformAppService implements AppService {
// app shortcuts
@override
- Future pinToHomeScreen(String label, AvesEntry? coverEntry, {Set? filters, String? explorerPath, String? uri}) async {
+ Future pinToHomeScreen(
+ String label,
+ AvesEntry? coverEntry, {
+ required String route,
+ Set? filters,
+ String? path,
+ String? viewUri,
+ String? geoUri,
+ }) async {
Uint8List? iconBytes;
if (coverEntry != null) {
final size = coverEntry.isVideo ? 0.0 : 256.0;
@@ -221,9 +233,11 @@ class PlatformAppService implements AppService {
await _platform.invokeMethod('pinShortcut', {
'label': label,
'iconBytes': iconBytes,
+ 'route': route,
'filters': filters?.map((filter) => filter.toJson()).toList(),
- 'explorerPath': explorerPath,
- 'uri': uri,
+ 'path': path,
+ 'viewUri': viewUri,
+ 'geoUri': geoUri,
});
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
diff --git a/lib/view/src/actions/map.dart b/lib/view/src/actions/map.dart
index c4d22a684..5e521a09f 100644
--- a/lib/view/src/actions/map.dart
+++ b/lib/view/src/actions/map.dart
@@ -11,6 +11,7 @@ extension ExtraMapActionView on MapAction {
MapAction.openMapApp => l10n.entryActionOpenMap,
MapAction.zoomIn => l10n.mapZoomInTooltip,
MapAction.zoomOut => l10n.mapZoomOutTooltip,
+ MapAction.addShortcut => l10n.collectionActionAddShortcut,
};
}
@@ -22,6 +23,7 @@ extension ExtraMapActionView on MapAction {
MapAction.openMapApp => AIcons.openOutside,
MapAction.zoomIn => AIcons.zoomIn,
MapAction.zoomOut => AIcons.zoomOut,
+ MapAction.addShortcut => AIcons.addShortcut,
};
}
}
diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart
index e9cf454bd..e6eb16c1d 100644
--- a/lib/widgets/collection/entry_set_action_delegate.dart
+++ b/lib/widgets/collection/entry_set_action_delegate.dart
@@ -26,6 +26,7 @@ import 'package:aves/theme/durations.dart';
import 'package:aves/theme/themes.dart';
import 'package:aves/utils/collection_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_storage.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart';
@@ -746,7 +747,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
final (coverEntry, name) = result;
if (name.isEmpty) return;
- await appService.pinToHomeScreen(name, coverEntry, filters: filters);
+ await appService.pinToHomeScreen(name, coverEntry, route: CollectionPage.routeName, filters: filters);
if (!device.showPinShortcutFeedback) {
showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback);
}
diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart
index 9f0108004..357a3b759 100644
--- a/lib/widgets/common/identity/aves_filter_chip.dart
+++ b/lib/widgets/common/identity/aves_filter_chip.dart
@@ -423,17 +423,20 @@ class _AvesFilterChipState extends State {
);
final animate = context.select((v) => v.animate);
- if (animate && (widget.heroType == HeroType.always || widget.heroType == HeroType.onTap && _tapped)) {
- chip = Hero(
- tag: filter,
- transitionOnUserGestures: true,
- child: MediaQueryDataProvider(
- child: DefaultTextStyle(
- style: const TextStyle(),
- child: chip,
+ if (animate) {
+ final heroType = widget.heroType;
+ if (heroType == HeroType.always || (heroType == HeroType.onTap && _tapped)) {
+ chip = Hero(
+ tag: filter,
+ transitionOnUserGestures: true,
+ child: MediaQueryDataProvider(
+ child: DefaultTextStyle(
+ style: const TextStyle(),
+ child: chip,
+ ),
),
- ),
- );
+ );
+ }
}
return chip;
}
diff --git a/lib/widgets/common/map/buttons/button.dart b/lib/widgets/common/map/buttons/button.dart
index f939cf96b..0d3f6da7a 100644
--- a/lib/widgets/common/map/buttons/button.dart
+++ b/lib/widgets/common/map/buttons/button.dart
@@ -4,19 +4,31 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class MapOverlayButton extends StatelessWidget {
- final Key? buttonKey;
- final Widget icon;
- final String tooltip;
- final VoidCallback? onPressed;
+ final ValueWidgetBuilder builder;
const MapOverlayButton({
super.key,
- this.buttonKey,
- required this.icon,
- required this.tooltip,
- required this.onPressed,
+ required this.builder,
});
+ 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
Widget build(BuildContext context) {
return Selector>(
@@ -27,15 +39,10 @@ class MapOverlayButton extends StatelessWidget {
),
child: Selector(
selector: (context, v) => v.visualDensity,
- builder: (context, visualDensity, child) => IconButton(
- key: buttonKey,
- iconSize: 20 + 1.5 * visualDensity.horizontal,
- visualDensity: visualDensity,
- icon: icon,
- onPressed: onPressed,
- tooltip: tooltip,
- ),
+ builder: builder,
),
);
}
+
+ static double iconSize(VisualDensity visualDensity) => 20 + 1.5 * visualDensity.horizontal;
}
diff --git a/lib/widgets/common/map/buttons/panel.dart b/lib/widgets/common/map/buttons/panel.dart
index b57ce502f..52ded17e4 100644
--- a/lib/widgets/common/map/buttons/panel.dart
+++ b/lib/widgets/common/map/buttons/panel.dart
@@ -1,7 +1,9 @@
+import 'package:aves/model/settings/enums/accessibility_animations.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.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/map/buttons/button.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_model/aves_model.dart';
import 'package:flutter/material.dart';
+import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart';
-class MapButtonPanel extends StatelessWidget {
- final AvesMapController? controller;
+class MapButtonPanel extends StatefulWidget {
+ final AvesMapController controller;
final ValueNotifier boundsNotifier;
final void Function(BuildContext context)? openMapPage;
- final VoidCallback? resetRotation;
const MapButtonPanel({
super.key,
required this.controller,
required this.boundsNotifier,
this.openMapPage,
- this.resetRotation,
});
+ @override
+ State createState() => _MapButtonPanelState();
+}
+
+class _MapButtonPanelState extends State {
+ 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
Widget build(BuildContext context) {
final iconTheme = IconTheme.of(context);
@@ -37,23 +59,24 @@ class MapButtonPanel extends StatelessWidget {
switch (context.select((v) => v.navigationButton)) {
case MapNavigationButton.back:
if (!settings.useTvLayout) {
- navigationButton = MapOverlayButton(
+ navigationButton = MapOverlayButton.icon(
icon: const BackButtonIcon(),
onPressed: () => Navigator.maybeOf(context)?.pop(),
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
);
}
case MapNavigationButton.close:
- navigationButton = MapOverlayButton(
+ navigationButton = MapOverlayButton.icon(
icon: const CloseButtonIcon(),
onPressed: SystemNavigator.pop,
tooltip: MaterialLocalizations.of(context).closeButtonTooltip,
);
case MapNavigationButton.map:
- if (openMapPage != null) {
- navigationButton = MapOverlayButton(
+ final _openMapPage = widget.openMapPage;
+ if (_openMapPage != null) {
+ navigationButton = MapOverlayButton.icon(
icon: const Icon(AIcons.showFullscreenCorners),
- onPressed: () => openMapPage?.call(context),
+ onPressed: () => _openMapPage.call(context),
tooltip: context.l10n.openMapPageTooltip,
);
}
@@ -65,6 +88,11 @@ class MapButtonPanel extends StatelessWidget {
final visualDensity = context.select((v) => v.visualDensity);
final double padding = 8 + visualDensity.horizontal * 2;
+ final actions = [
+ MapAction.openMapApp,
+ MapAction.addShortcut,
+ ].where((action) => _actionDelegate.isVisible(context, action)).toList();
+
return Positioned.fill(
child: TooltipTheme(
data: TooltipTheme.of(context).copyWith(
@@ -90,7 +118,7 @@ class MapButtonPanel extends StatelessWidget {
SizedBox(height: padding),
],
ValueListenableBuilder(
- valueListenable: boundsNotifier,
+ valueListenable: widget.boundsNotifier,
builder: (context, bounds, child) {
final degrees = bounds.rotation;
final opacity = degrees == 0 ? .0 : 1.0;
@@ -99,7 +127,7 @@ class MapButtonPanel extends StatelessWidget {
child: AnimatedOpacity(
opacity: opacity,
duration: context.select((v) => v.viewerOverlayAnimation),
- child: MapOverlayButton(
+ child: MapOverlayButton.icon(
icon: Transform(
origin: iconSize.center(Offset.zero),
transform: Matrix4.rotationZ(degToRadian(degrees)),
@@ -110,7 +138,7 @@ class MapButtonPanel extends StatelessWidget {
size: iconSize,
),
),
- onPressed: () => resetRotation?.call(),
+ onPressed: widget.controller.resetRotation,
tooltip: context.l10n.mapPointNorthUpTooltip,
),
),
@@ -123,7 +151,7 @@ class MapButtonPanel extends StatelessWidget {
showCoordinateFilter
? Expanded(
child: OverlayCoordinateFilterChip(
- boundsNotifier: boundsNotifier,
+ boundsNotifier: widget.boundsNotifier,
padding: padding,
),
)
@@ -133,8 +161,31 @@ class MapButtonPanel extends StatelessWidget {
// key is expected by test driver
child: Column(
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().accessibilityAnimations;
+ return PopupMenuButton(
+ 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),
+ // key is expected by test driver
_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,
icon: action.getIcon(),
- onPressed: () => MapActionDelegate(controller).onActionSelected(context, action),
+ onPressed: () => _actionDelegate.onActionSelected(context, action),
tooltip: action.getText(context),
);
}
diff --git a/lib/widgets/common/map/geo_map.dart b/lib/widgets/common/map/geo_map.dart
index 486a13a9e..7e61d2963 100644
--- a/lib/widgets/common/map/geo_map.dart
+++ b/lib/widgets/common/map/geo_map.dart
@@ -33,7 +33,7 @@ import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart';
class GeoMap extends StatefulWidget {
- final AvesMapController? controller;
+ final AvesMapController controller;
final CollectionLens? collection;
final List? entries;
final Size availableSize;
@@ -60,7 +60,7 @@ class GeoMap extends StatefulWidget {
const GeoMap({
super.key,
- this.controller,
+ required this.controller,
this.collection,
this.entries,
required this.availableSize,
@@ -124,10 +124,7 @@ class _GeoMapState extends State {
void _registerWidget(GeoMap widget) {
widget.collection?.addListener(_onCollectionChanged);
- final controller = widget.controller;
- if (controller != null) {
- _subscriptions.add(controller.markerLocationChanges.listen((event) => _onCollectionChanged()));
- }
+ _subscriptions.add(widget.controller.markerLocationChanges.listen((event) => _onCollectionChanged()));
}
void _unregisterWidget(GeoMap widget) {
@@ -164,7 +161,6 @@ class _GeoMapState extends State {
);
bool _isMarkerImageReady(MarkerKey key) => key.entry.isThumbnailReady(extent: MapThemeData.markerImageExtent);
- final controller = widget.controller;
Widget child = const SizedBox();
if (mapStyle != null) {
switch (mapStyle) {
@@ -172,7 +168,7 @@ class _GeoMapState extends State {
case EntryMapStyle.googleHybrid:
case EntryMapStyle.googleTerrain:
child = mobileServices.buildMap(
- controller: controller,
+ controller: widget.controller,
clusterListenable: _clusterChangeNotifier,
boundsNotifier: _boundsNotifier,
style: mapStyle,
@@ -194,7 +190,7 @@ class _GeoMapState extends State {
case EntryMapStyle.osmHot:
case EntryMapStyle.stamenWatercolor:
child = EntryLeafletMap(
- controller: controller,
+ controller: widget.controller,
clusterListenable: _clusterChangeNotifier,
boundsNotifier: _boundsNotifier,
minZoom: 2,
@@ -324,11 +320,7 @@ class _GeoMapState extends State {
Widget replacement = Stack(
children: [
const MapDecorator(),
- MapButtonPanel(
- controller: controller,
- boundsNotifier: _boundsNotifier,
- openMapPage: widget.openMapPage,
- ),
+ _buildButtonPanel(context),
],
);
if (mapHeight != null) {
@@ -560,13 +552,12 @@ class _GeoMapState extends State {
Widget _decorateMap(BuildContext context, Widget? child) => MapDecorator(child: child);
- Widget _buildButtonPanel(VoidCallback resetRotation) {
+ Widget _buildButtonPanel(BuildContext context) {
if (settings.useTvLayout) return const SizedBox();
return MapButtonPanel(
controller: widget.controller,
boundsNotifier: _boundsNotifier,
openMapPage: widget.openMapPage,
- resetRotation: resetRotation,
);
}
}
diff --git a/lib/widgets/common/map/leaflet/map.dart b/lib/widgets/common/map/leaflet/map.dart
index 96f775e2b..10800040f 100644
--- a/lib/widgets/common/map/leaflet/map.dart
+++ b/lib/widgets/common/map/leaflet/map.dart
@@ -14,13 +14,13 @@ import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart';
class EntryLeafletMap extends StatefulWidget {
- final AvesMapController? controller;
+ final AvesMapController controller;
final Listenable clusterListenable;
final ValueNotifier boundsNotifier;
final double minZoom, maxZoom;
final EntryMapStyle style;
final TransitionBuilder decoratorBuilder;
- final ButtonPanelBuilder buttonPanelBuilder;
+ final WidgetBuilder buttonPanelBuilder;
final MarkerClusterBuilder markerClusterBuilder;
final MarkerWidgetBuilder markerWidgetBuilder;
final ValueNotifier? dotLocationNotifier;
@@ -34,7 +34,7 @@ class EntryLeafletMap extends StatefulWidget {
const EntryLeafletMap({
super.key,
- this.controller,
+ required this.controller,
required this.clusterListenable,
required this.boundsNotifier,
this.minZoom = 0,
@@ -94,11 +94,10 @@ class _EntryLeafletMapState extends State> with TickerProv
void _registerWidget(EntryLeafletMap widget) {
final avesMapController = widget.controller;
- if (avesMapController != null) {
- _subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(event.latLng)));
- _subscriptions.add(avesMapController.zoomCommands.listen((event) => _zoomBy(event.delta)));
- }
- _subscriptions.add(_leafletMapController.mapEventStream.listen((event) => _updateVisibleRegion()));
+ _subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(event.latLng)));
+ _subscriptions.add(avesMapController.zoomCommands.listen((event) => _zoomBy(event.delta)));
+ _subscriptions.add(avesMapController.rotationResetCommands.listen((_) => _resetRotation()));
+ _subscriptions.add(_leafletMapController.mapEventStream.listen((_) => _updateVisibleRegion()));
widget.clusterListenable.addListener(_updateMarkers);
widget.boundsNotifier.addListener(_onBoundsChanged);
}
@@ -116,7 +115,7 @@ class _EntryLeafletMapState extends State> with TickerProv
return Stack(
children: [
widget.decoratorBuilder(context, _buildMap()),
- widget.buttonPanelBuilder(_resetRotation),
+ widget.buttonPanelBuilder(context),
],
);
}
@@ -240,7 +239,7 @@ class _EntryLeafletMapState extends State> with TickerProv
void _onIdle() {
if (!mounted) return;
- widget.controller?.notifyIdle(bounds);
+ widget.controller.notifyIdle(bounds);
_updateMarkers();
}
diff --git a/lib/widgets/common/map/map_action_delegate.dart b/lib/widgets/common/map/map_action_delegate.dart
index 5c3728e05..b2c1499a6 100644
--- a/lib/widgets/common/map/map_action_delegate.dart
+++ b/lib/widgets/common/map/map_action_delegate.dart
@@ -1,37 +1,94 @@
+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/source/collection_lens.dart';
import 'package:aves/services/common/services.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/dialogs/add_shortcut_dialog.dart';
import 'package:aves/widgets/dialogs/selection_dialogs/common.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_model/aves_model.dart';
import 'package:flutter/material.dart';
-import 'package:flutter/widgets.dart';
+import 'package:provider/provider.dart';
-class MapActionDelegate {
- final AvesMapController? controller;
+class MapActionDelegate with FeedbackMixin {
+ final AvesMapController 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) {
switch (action) {
case MapAction.selectStyle:
- showSelectionDialog(
- context: context,
- builder: (context) => AvesSingleSelectionDialog(
- initialValue: settings.mapStyle,
- options: Map.fromEntries(availability.mapStyles.map((v) => MapEntry(v, v.getName(context)))),
- title: context.l10n.mapStyleDialogTitle,
- ),
- onSelection: (v) => settings.mapStyle = v,
- );
+ _selectStyle(context);
case MapAction.openMapApp:
OpenMapAppNotification().dispatch(context);
case MapAction.zoomIn:
- controller?.zoomBy(1);
+ controller.zoomBy(1);
case MapAction.zoomOut:
- controller?.zoomBy(-1);
+ controller.zoomBy(-1);
+ case MapAction.addShortcut:
+ _addShortcut(context);
+ }
+ }
+
+ Future _selectStyle(BuildContext context) => showSelectionDialog(
+ context: context,
+ builder: (context) => AvesSingleSelectionDialog(
+ initialValue: settings.mapStyle,
+ options: Map.fromEntries(availability.mapStyles.map((v) => MapEntry(v, v.getName(context)))),
+ title: context.l10n.mapStyleDialogTitle,
+ ),
+ onSelection: (v) => settings.mapStyle = v,
+ );
+
+ Future _addShortcut(BuildContext context) async {
+ final idleBounds = controller.idleBounds;
+ if (idleBounds == null) {
+ showFeedback(context, FeedbackType.warn, context.l10n.genericFailureFeedback);
+ return;
+ }
+
+ final collection = context.read();
+ 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);
}
}
}
diff --git a/lib/widgets/explorer/explorer_action_delegate.dart b/lib/widgets/explorer/explorer_action_delegate.dart
index eb98352e5..3942ac2dd 100644
--- a/lib/widgets/explorer/explorer_action_delegate.dart
+++ b/lib/widgets/explorer/explorer_action_delegate.dart
@@ -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/extensions/build_context.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/stats/stats_page.dart';
import 'package:aves_model/aves_model.dart';
@@ -84,7 +85,7 @@ class ExplorerActionDelegate with FeedbackMixin {
final (coverEntry, name) = result;
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) {
showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback);
}
diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart
index 8bbe51b26..9335f675f 100644
--- a/lib/widgets/home_page.dart
+++ b/lib/widgets/home_page.dart
@@ -377,7 +377,10 @@ class _HomePageState extends State {
return buildRoute((context) {
final mapCollection = CollectionLens(
source: source,
- filters: {LocationFilter.located},
+ filters: {
+ LocationFilter.located,
+ if (filters != null) ...filters,
+ },
);
return MapPage(
collection: mapCollection,
diff --git a/lib/widgets/map/map_page.dart b/lib/widgets/map/map_page.dart
index 388612406..0639e3739 100644
--- a/lib/widgets/map/map_page.dart
+++ b/lib/widgets/map/map_page.dart
@@ -5,8 +5,9 @@ import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/location.dart';
import 'package:aves/model/filters/coordinate.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/media/geotiff.dart';
import 'package:aves/model/settings/enums/accessibility_animations.dart';
import 'package:aves/model/settings/enums/map_style.dart';
import 'package:aves/model/settings/settings.dart';
@@ -62,10 +63,15 @@ class MapPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
- // do not rely on the `HighlightInfoProvider` app level
- // as the map can be stacked on top of other pages
- // that catch highlight events and will not let it bubble up
- return HighlightInfoProvider(
+ return MultiProvider(
+ providers: [
+ // do not rely on the `HighlightInfoProvider` app level
+ // as the map can be stacked on top of other pages
+ // that catch highlight events and will not let it bubble up
+ HighlightInfoProvider(),
+ // opening collection can be used by map actions
+ ChangeNotifierProvider.value(value: collection),
+ ],
child: AvesScaffold(
body: SafeArea(
left: false,
@@ -442,10 +448,16 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
Navigator.maybeOf(context)?.pushAndRemoveUntil(
MaterialPageRoute(
settings: const RouteSettings(name: CollectionPage.routeName),
- builder: (context) => CollectionPage(
- source: openingCollection.source,
- filters: {...openingCollection.filters, filter},
- ),
+ 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,
+ filters: filters,
+ );
+ },
),
(route) => false,
);
diff --git a/lib/widgets/viewer/action/entry_action_delegate.dart b/lib/widgets/viewer/action/entry_action_delegate.dart
index ed0fb320c..688b3c64b 100644
--- a/lib/widgets/viewer/action/entry_action_delegate.dart
+++ b/lib/widgets/viewer/action/entry_action_delegate.dart
@@ -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/controls/notifications.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/source_viewer_page.dart';
import 'package:aves/widgets/viewer/video/conductor.dart';
@@ -395,7 +396,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
final name = result.$2;
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) {
showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback);
}
diff --git a/plugins/aves_map/lib/src/controller.dart b/plugins/aves_map/lib/src/controller.dart
index 26d2b56c4..10cdffa0f 100644
--- a/plugins/aves_map/lib/src/controller.dart
+++ b/plugins/aves_map/lib/src/controller.dart
@@ -16,6 +16,8 @@ class AvesMapController {
Stream get zoomCommands => _events.where((event) => event is MapControllerZoomEvent).cast();
+ Stream get rotationResetCommands => _events.where((event) => event is MapControllerRotationResetEvent).cast();
+
Stream get idleUpdates => _events.where((event) => event is MapIdleUpdate).cast();
Stream get markerLocationChanges => _events.where((event) => event is MapMarkerLocationChangeEvent).cast();
@@ -41,6 +43,8 @@ class AvesMapController {
void zoomBy(double delta) => _streamController.add(MapControllerZoomEvent(delta));
+ void resetRotation() => _streamController.add(MapControllerRotationResetEvent());
+
void notifyIdle(ZoomedBounds bounds) {
_idleBounds = bounds;
_streamController.add(MapIdleUpdate(bounds));
@@ -61,6 +65,8 @@ class MapControllerZoomEvent {
MapControllerZoomEvent(this.delta);
}
+class MapControllerRotationResetEvent {}
+
class MapIdleUpdate {
final ZoomedBounds bounds;
diff --git a/plugins/aves_map/lib/src/interface.dart b/plugins/aves_map/lib/src/interface.dart
index 6aedd5397..3f2cc1756 100644
--- a/plugins/aves_map/lib/src/interface.dart
+++ b/plugins/aves_map/lib/src/interface.dart
@@ -3,7 +3,6 @@ import 'package:aves_map/src/marker/key.dart';
import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart';
-typedef ButtonPanelBuilder = Widget Function(VoidCallback resetRotation);
typedef MarkerClusterBuilder = Map, GeoEntry> Function();
typedef MarkerWidgetBuilder = Widget Function(MarkerKey key);
typedef MarkerImageReadyChecker = bool Function(MarkerKey key);
diff --git a/plugins/aves_model/lib/src/actions/map.dart b/plugins/aves_model/lib/src/actions/map.dart
index 6d4838cf3..1c71363e9 100644
--- a/plugins/aves_model/lib/src/actions/map.dart
+++ b/plugins/aves_model/lib/src/actions/map.dart
@@ -1,6 +1,9 @@
enum MapAction {
+ // any map panel
selectStyle,
openMapApp,
zoomIn,
zoomOut,
+ // full page only
+ addShortcut,
}
diff --git a/plugins/aves_services/lib/aves_services.dart b/plugins/aves_services/lib/aves_services.dart
index 5fa5b9381..236de5895 100644
--- a/plugins/aves_services/lib/aves_services.dart
+++ b/plugins/aves_services/lib/aves_services.dart
@@ -12,12 +12,12 @@ abstract class MobileServices {
List get mapStyles;
Widget buildMap({
- required AvesMapController? controller,
+ required AvesMapController controller,
required Listenable clusterListenable,
required ValueNotifier boundsNotifier,
required EntryMapStyle style,
required TransitionBuilder decoratorBuilder,
- required ButtonPanelBuilder buttonPanelBuilder,
+ required WidgetBuilder buttonPanelBuilder,
required MarkerClusterBuilder markerClusterBuilder,
required MarkerWidgetBuilder markerWidgetBuilder,
required MarkerImageReadyChecker markerImageReadyChecker,
diff --git a/plugins/aves_services_google/lib/aves_services_platform.dart b/plugins/aves_services_google/lib/aves_services_platform.dart
index 23a5ddbfe..9e665c84f 100644
--- a/plugins/aves_services_google/lib/aves_services_platform.dart
+++ b/plugins/aves_services_google/lib/aves_services_platform.dart
@@ -46,12 +46,12 @@ class PlatformMobileServices extends MobileServices {
@override
Widget buildMap({
- required AvesMapController? controller,
+ required AvesMapController controller,
required Listenable clusterListenable,
required ValueNotifier boundsNotifier,
required EntryMapStyle style,
required TransitionBuilder decoratorBuilder,
- required ButtonPanelBuilder buttonPanelBuilder,
+ required WidgetBuilder buttonPanelBuilder,
required MarkerClusterBuilder markerClusterBuilder,
required MarkerWidgetBuilder markerWidgetBuilder,
required MarkerImageReadyChecker markerImageReadyChecker,
diff --git a/plugins/aves_services_google/lib/src/map.dart b/plugins/aves_services_google/lib/src/map.dart
index 26b29f345..ef3e47ad5 100644
--- a/plugins/aves_services_google/lib/src/map.dart
+++ b/plugins/aves_services_google/lib/src/map.dart
@@ -9,13 +9,13 @@ import 'package:latlong2/latlong.dart' as ll;
import 'package:provider/provider.dart';
class EntryGoogleMap extends StatefulWidget {
- final AvesMapController? controller;
+ final AvesMapController controller;
final Listenable clusterListenable;
final ValueNotifier boundsNotifier;
final double? minZoom, maxZoom;
final EntryMapStyle style;
final TransitionBuilder decoratorBuilder;
- final ButtonPanelBuilder buttonPanelBuilder;
+ final WidgetBuilder buttonPanelBuilder;
final MarkerClusterBuilder markerClusterBuilder;
final MarkerWidgetBuilder markerWidgetBuilder;
final MarkerImageReadyChecker markerImageReadyChecker;
@@ -29,7 +29,7 @@ class EntryGoogleMap extends StatefulWidget {
const EntryGoogleMap({
super.key,
- this.controller,
+ required this.controller,
required this.clusterListenable,
required this.boundsNotifier,
this.minZoom,
@@ -93,10 +93,9 @@ class _EntryGoogleMapState extends State> {
void _registerWidget(EntryGoogleMap widget) {
final avesMapController = widget.controller;
- if (avesMapController != null) {
- _subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(_toServiceLatLng(event.latLng))));
- _subscriptions.add(avesMapController.zoomCommands.listen((event) => _zoomBy(event.delta)));
- }
+ _subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(_toServiceLatLng(event.latLng))));
+ _subscriptions.add(avesMapController.zoomCommands.listen((event) => _zoomBy(event.delta)));
+ _subscriptions.add(avesMapController.rotationResetCommands.listen((_) => _resetRotation()));
widget.clusterListenable.addListener(_updateMarkers);
}
@@ -125,7 +124,7 @@ class _EntryGoogleMapState extends State> {
},
),
widget.decoratorBuilder(context, _buildMap()),
- widget.buttonPanelBuilder(_resetRotation),
+ widget.buttonPanelBuilder(context),
],
);
}
@@ -241,7 +240,7 @@ class _EntryGoogleMapState extends State> {
void _onIdle() {
if (!mounted) return;
- widget.controller?.notifyIdle(bounds);
+ widget.controller.notifyIdle(bounds);
_updateMarkers();
}
diff --git a/plugins/aves_services_none/lib/aves_services_platform.dart b/plugins/aves_services_none/lib/aves_services_platform.dart
index f7069f805..8f95b7364 100644
--- a/plugins/aves_services_none/lib/aves_services_platform.dart
+++ b/plugins/aves_services_none/lib/aves_services_platform.dart
@@ -18,12 +18,12 @@ class PlatformMobileServices extends MobileServices {
@override
Widget buildMap({
- required AvesMapController? controller,
+ required AvesMapController controller,
required Listenable clusterListenable,
required ValueNotifier boundsNotifier,
required EntryMapStyle style,
required TransitionBuilder decoratorBuilder,
- required ButtonPanelBuilder buttonPanelBuilder,
+ required WidgetBuilder buttonPanelBuilder,
required MarkerClusterBuilder markerClusterBuilder,
required MarkerWidgetBuilder markerWidgetBuilder,
required MarkerImageReadyChecker markerImageReadyChecker,