From f57e2306e2fa15433f8234e6d86ab228cfd8a850 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 30 Nov 2022 12:51:31 +0100 Subject: [PATCH] #407 quick copy/move --- CHANGELOG.md | 2 +- lib/model/settings/settings.dart | 8 + .../collection/entry_set_action_delegate.dart | 2 +- .../common/action_mixins/entry_storage.dart | 88 +++++++---- lib/widgets/common/app_bar/move_button.dart | 67 ++++++++ .../app_bar/quick_choosers/album_chooser.dart | 35 +++++ .../quick_choosers/chooser_button.dart | 2 +- .../quick_choosers/filter_chooser.dart | 146 ++++++++++++++++++ .../app_bar/quick_choosers/rate_chooser.dart | 59 +++---- lib/widgets/common/app_bar/rate_button.dart | 4 +- lib/widgets/common/map/geo_map.dart | 4 +- lib/widgets/common/map/leaflet/map.dart | 2 +- .../common/action_delegates/album_set.dart | 2 +- .../viewer/action/entry_action_delegate.dart | 15 +- .../action/entry_info_action_delegate.dart | 2 +- .../viewer/overlay/viewer_buttons.dart | 19 +++ plugins/aves_map/lib/src/marker/dot.dart | 50 +----- plugins/aves_map/lib/src/theme.dart | 1 + plugins/aves_map/pubspec.lock | 7 + plugins/aves_map/pubspec.yaml | 2 + plugins/aves_services/pubspec.lock | 7 + plugins/aves_services_google/pubspec.lock | 7 + plugins/aves_services_huawei/pubspec.lock | 7 + plugins/aves_services_none/pubspec.lock | 7 + plugins/aves_ui/.gitignore | 30 ++++ plugins/aves_ui/.metadata | 10 ++ plugins/aves_ui/analysis_options.yaml | 1 + plugins/aves_ui/lib/aves_ui.dart | 3 + plugins/aves_ui/lib/src/dot.dart | 62 ++++++++ plugins/aves_ui/pubspec.lock | 64 ++++++++ plugins/aves_ui/pubspec.yaml | 15 ++ pubspec.lock | 9 +- pubspec.yaml | 2 + 33 files changed, 626 insertions(+), 115 deletions(-) create mode 100644 lib/widgets/common/app_bar/move_button.dart create mode 100644 lib/widgets/common/app_bar/quick_choosers/album_chooser.dart create mode 100644 lib/widgets/common/app_bar/quick_choosers/filter_chooser.dart create mode 100644 plugins/aves_ui/.gitignore create mode 100644 plugins/aves_ui/.metadata create mode 100644 plugins/aves_ui/analysis_options.yaml create mode 100644 plugins/aves_ui/lib/aves_ui.dart create mode 100644 plugins/aves_ui/lib/src/dot.dart create mode 100644 plugins/aves_ui/pubspec.lock create mode 100644 plugins/aves_ui/pubspec.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 78dce5151..c3adeacd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ All notable changes to this project will be documented in this file. ### Added - Viewer: optionally show rating & tags on overlay -- Viewer: long press on rating quick action for quicker rating +- Viewer: long press on copy/move/rating quick action for quicker action - Search: missing address, portrait, landscape filters - Lithuanian translation (thanks Gediminas Murauskas) diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index e0e83353e..197123fc7 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -29,6 +29,8 @@ class Settings extends ChangeNotifier { Settings._private(); + static const int moveDestinationAlbumMax = 3; + static const Set _internalKeys = { hasAcceptedTermsKey, catalogTimeZoneKey, @@ -37,6 +39,7 @@ class Settings extends ChangeNotifier { platformAccelerometerRotationKey, platformTransitionAnimationScaleKey, topEntryIdsKey, + moveDestinationAlbumsKey, }; static const _widgetKeyPrefix = 'widget_'; @@ -51,6 +54,7 @@ class Settings extends ChangeNotifier { static const tileLayoutPrefixKey = 'tile_layout_'; static const entryRenamingPatternKey = 'entry_renaming_pattern'; static const topEntryIdsKey = 'top_entry_ids'; + static const moveDestinationAlbumsKey = 'move_destination_albums'; // display static const displayRefreshRateModeKey = 'display_refresh_rate_mode'; @@ -314,6 +318,10 @@ class Settings extends ChangeNotifier { set topEntryIds(List? newValue) => setAndNotify(topEntryIdsKey, newValue?.map((id) => id.toString()).whereNotNull().toList()); + List get moveDestinationAlbums => getStringList(moveDestinationAlbumsKey) ?? []; + + set moveDestinationAlbums(List newValue) => setAndNotify(moveDestinationAlbumsKey, newValue.take(Settings.moveDestinationAlbumMax).toList()); + // display DisplayRefreshRateMode get displayRefreshRateMode => getEnumOrDefault(displayRefreshRateModeKey, SettingsDefaults.displayRefreshRateMode, DisplayRefreshRateMode.values); diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index 32c8bfe5d..5790d0000 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -317,7 +317,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware Future _move(BuildContext context, {required MoveType moveType}) async { final entries = _getTargetItems(context); - await move(context, moveType: moveType, entries: entries); + await doMove(context, moveType: moveType, entries: entries); _leaveSelectionMode(context); } diff --git a/lib/widgets/common/action_mixins/entry_storage.dart b/lib/widgets/common/action_mixins/entry_storage.dart index 03ec5bc2c..69e041d82 100644 --- a/lib/widgets/common/action_mixins/entry_storage.dart +++ b/lib/widgets/common/action_mixins/entry_storage.dart @@ -34,50 +34,20 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { - Future move( + Future doQuickMove( BuildContext context, { required MoveType moveType, - required Set entries, + required Map> entriesByDestination, bool hideShowAction = false, VoidCallback? onSuccess, }) async { + final entries = entriesByDestination.values.expand((v) => v).toSet(); final todoCount = entries.length; assert(todoCount > 0); final toBin = moveType == MoveType.toBin; final copy = moveType == MoveType.copy; - final l10n = context.l10n; - if (toBin) { - if (!await showConfirmationDialog( - context: context, - type: ConfirmationDialog.moveToBin, - message: l10n.binEntriesConfirmationDialogMessage(todoCount), - confirmationButtonLabel: l10n.deleteButtonLabel, - )) return; - } - - final entriesByDestination = >{}; - switch (moveType) { - case MoveType.copy: - case MoveType.move: - case MoveType.export: - final destinationAlbum = await pickAlbum(context: context, moveType: moveType); - if (destinationAlbum == null) return; - entriesByDestination[destinationAlbum] = entries; - break; - case MoveType.toBin: - entriesByDestination[AndroidFileUtils.trashDirPath] = entries; - break; - case MoveType.fromBin: - groupBy(entries, (e) => e.directory).forEach((originAlbum, dirEntries) { - if (originAlbum != null) { - entriesByDestination[originAlbum] = dirEntries.toSet(); - } - }); - break; - } - // permission for modification at destinations final destinationAlbums = entriesByDestination.keys.toSet(); if (!await checkStoragePermissionForAlbums(context, destinationAlbums)) return; @@ -90,6 +60,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { if (!await checkFreeSpaceForMove(context, entries, destinationAlbum, moveType)) return; }); + final l10n = context.l10n; var nameConflictStrategy = NameConflictStrategy.rename; if (!toBin && destinationAlbums.length == 1) { final destinationDirectory = Directory(destinationAlbums.single); @@ -174,7 +145,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { // local context may be deactivated when action is triggered after navigation final context = AvesApp.navigatorKey.currentContext; if (context != null) { - move( + doMove( context, moveType: MoveType.fromBin, entries: movedEntries, @@ -213,6 +184,55 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { ); } + Future doMove( + BuildContext context, { + required MoveType moveType, + required Set entries, + bool hideShowAction = false, + VoidCallback? onSuccess, + }) async { + if (moveType == MoveType.toBin) { + final l10n = context.l10n; + if (!await showConfirmationDialog( + context: context, + type: ConfirmationDialog.moveToBin, + message: l10n.binEntriesConfirmationDialogMessage(entries.length), + confirmationButtonLabel: l10n.deleteButtonLabel, + )) return; + } + + final entriesByDestination = >{}; + switch (moveType) { + case MoveType.copy: + case MoveType.move: + case MoveType.export: + final destinationAlbum = await pickAlbum(context: context, moveType: moveType); + if (destinationAlbum == null) return; + + settings.moveDestinationAlbums = settings.moveDestinationAlbums + ..remove(destinationAlbum) + ..insert(0, destinationAlbum); + entriesByDestination[destinationAlbum] = entries; + break; + case MoveType.toBin: + entriesByDestination[AndroidFileUtils.trashDirPath] = entries; + break; + case MoveType.fromBin: + groupBy(entries, (e) => e.directory).forEach((originAlbum, dirEntries) { + if (originAlbum != null) { + entriesByDestination[originAlbum] = dirEntries.toSet(); + } + }); + break; + } + + await doQuickMove( + context, + moveType: moveType, + entriesByDestination: entriesByDestination, + ); + } + Future rename( BuildContext context, { required Map entriesToNewName, diff --git a/lib/widgets/common/app_bar/move_button.dart b/lib/widgets/common/app_bar/move_button.dart new file mode 100644 index 000000000..c70870001 --- /dev/null +++ b/lib/widgets/common/app_bar/move_button.dart @@ -0,0 +1,67 @@ +import 'package:aves/model/actions/entry_actions.dart'; +import 'package:aves/model/filters/album.dart'; +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/album_chooser.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/chooser_button.dart'; +import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class MoveButton extends ChooserQuickButton { + final bool copy; + + const MoveButton({ + super.key, + required this.copy, + super.chooserPosition, + super.onChooserValue, + required super.onPressed, + }); + + @override + State createState() => _MoveQuickButtonState(); +} + +class _MoveQuickButtonState extends ChooserQuickButtonState { + EntryAction get action => widget.copy ? EntryAction.copy : EntryAction.move; + + @override + Widget get icon => action.getIcon(); + + @override + String get tooltip => action.getText(context); + + @override + String? get defaultValue => null; + + @override + Widget buildChooser(Animation animation) { + final options = settings.moveDestinationAlbums; + final takeCount = Settings.moveDestinationAlbumMax - options.length; + if (takeCount > 0) { + final source = context.read(); + final filters = source.rawAlbums.whereNot(options.contains).map((album) => AlbumFilter(album, null)).toSet(); + final allMapEntries = filters.map((filter) => FilterGridItem(filter, source.recentEntry(filter))).toList(); + allMapEntries.sort(FilterNavigationPage.compareFiltersByDate); + options.addAll(allMapEntries.take(takeCount).map((v) => v.filter.album)); + } + + return MediaQueryDataProvider( + child: FadeTransition( + opacity: animation, + child: ScaleTransition( + scale: animation, + child: AlbumQuickChooser( + valueNotifier: chooserValueNotifier, + pointerGlobalPosition: pointerGlobalPosition, + options: widget.chooserPosition == PopupMenuPosition.over ? options.reversed.toList() : options, + ), + ), + ), + ); + } +} diff --git a/lib/widgets/common/app_bar/quick_choosers/album_chooser.dart b/lib/widgets/common/app_bar/quick_choosers/album_chooser.dart new file mode 100644 index 000000000..db09f8044 --- /dev/null +++ b/lib/widgets/common/app_bar/quick_choosers/album_chooser.dart @@ -0,0 +1,35 @@ +import 'dart:async'; + +import 'package:aves/model/filters/album.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/widgets/common/app_bar/quick_choosers/filter_chooser.dart'; +import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class AlbumQuickChooser extends StatelessWidget { + final ValueNotifier valueNotifier; + final List options; + final Stream pointerGlobalPosition; + + const AlbumQuickChooser({ + super.key, + required this.valueNotifier, + required this.options, + required this.pointerGlobalPosition, + }); + + @override + Widget build(BuildContext context) { + final source = context.read(); + return FilterQuickChooser( + valueNotifier: valueNotifier, + options: options, + pointerGlobalPosition: pointerGlobalPosition, + buildFilterChip: (context, album) => AvesFilterChip( + filter: AlbumFilter(album, source.getAlbumDisplayName(context, album)), + showGenericIcon: false, + ), + ); + } +} diff --git a/lib/widgets/common/app_bar/quick_choosers/chooser_button.dart b/lib/widgets/common/app_bar/quick_choosers/chooser_button.dart index 7b97885a4..a1c2b4acb 100644 --- a/lib/widgets/common/app_bar/quick_choosers/chooser_button.dart +++ b/lib/widgets/common/app_bar/quick_choosers/chooser_button.dart @@ -39,7 +39,7 @@ abstract class ChooserQuickButtonState, U> exten ValueNotifier get chooserValueNotifier => _chooserValueNotifier; - Stream get moveUpdates => _moveUpdateStreamController.stream; + Stream get pointerGlobalPosition => _moveUpdateStreamController.stream.map((event) => event.globalPosition); @override void dispose() { diff --git a/lib/widgets/common/app_bar/quick_choosers/filter_chooser.dart b/lib/widgets/common/app_bar/quick_choosers/filter_chooser.dart new file mode 100644 index 000000000..5c49ddfee --- /dev/null +++ b/lib/widgets/common/app_bar/quick_choosers/filter_chooser.dart @@ -0,0 +1,146 @@ +import 'dart:async'; + +import 'package:aves/widgets/dialogs/aves_dialog.dart'; +import 'package:aves_ui/aves_ui.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; + +class FilterQuickChooser extends StatefulWidget { + final ValueNotifier valueNotifier; + final List options; + final Stream pointerGlobalPosition; + final Widget Function(BuildContext context, T album) buildFilterChip; + + const FilterQuickChooser({ + super.key, + required this.valueNotifier, + required this.options, + required this.pointerGlobalPosition, + required this.buildFilterChip, + }); + + @override + State> createState() => _FilterQuickChooserState(); +} + +class _FilterQuickChooserState extends State> { + final List _subscriptions = []; + final ValueNotifier _selectedRowRect = ValueNotifier(Rect.zero); + + ValueNotifier get valueNotifier => widget.valueNotifier; + + List get options => widget.options; + + static const margin = EdgeInsets.all(8); + static const padding = EdgeInsets.all(8); + static const double intraPadding = 8; + + @override + void initState() { + super.initState(); + _registerWidget(widget); + } + + @override + void didUpdateWidget(covariant FilterQuickChooser oldWidget) { + super.didUpdateWidget(oldWidget); + _unregisterWidget(oldWidget); + _registerWidget(widget); + } + + @override + void dispose() { + _unregisterWidget(widget); + super.dispose(); + } + + void _registerWidget(FilterQuickChooser widget) { + _subscriptions.add(widget.pointerGlobalPosition.listen(_onPointerMove)); + } + + void _unregisterWidget(FilterQuickChooser widget) { + _subscriptions + ..forEach((sub) => sub.cancel()) + ..clear(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: margin, + child: Material( + shape: AvesDialog.shape(context), + child: Padding( + padding: padding, + child: ValueListenableBuilder( + valueListenable: valueNotifier, + builder: (context, selectedValue, child) { + return Stack( + children: [ + ValueListenableBuilder( + valueListenable: _selectedRowRect, + builder: (context, selectedRowRect, child) { + Widget child = const Center(child: AvesDot()); + child = AnimatedOpacity( + opacity: selectedValue != null ? 1 : 0, + curve: Curves.easeInOutCubic, + duration: const Duration(milliseconds: 200), + child: child, + ); + child = AnimatedPositioned( + top: selectedRowRect.top, + height: selectedRowRect.height, + curve: Curves.easeInOutCubic, + duration: const Duration(milliseconds: 200), + child: child, + ); + return child; + }, + ), + Padding( + padding: const EdgeInsetsDirectional.only(start: 24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: options.mapIndexed((index, value) { + return Padding( + padding: index == 0 ? EdgeInsets.zero : const EdgeInsets.only(top: intraPadding), + child: widget.buildFilterChip(context, value), + ); + }).toList(), + ), + ), + ], + ); + }, + ), + ), + ), + ); + } + + void _onPointerMove(Offset globalPosition) { + final chooserBox = context.findRenderObject() as RenderBox; + final chooserSize = chooserBox.size; + final contentWidth = chooserSize.width; + final contentHeight = chooserSize.height - (margin.vertical + padding.vertical); + + final optionCount = options.length; + final itemHeight = (contentHeight - (optionCount - 1) * intraPadding) / optionCount; + + final local = chooserBox.globalToLocal(globalPosition); + final dx = local.dx; + final dy = local.dy - (margin.vertical + padding.vertical) / 2; + + T? selectedValue; + if (0 < dx && dx < contentWidth && 0 < dy && dy < contentHeight) { + final index = (options.length * dy / contentHeight).floor(); + if (0 <= index && index < options.length) { + selectedValue = options[index]; + final top = index * (itemHeight + intraPadding); + _selectedRowRect.value = Rect.fromLTWH(0, top, contentWidth, itemHeight); + } + } + valueNotifier.value = selectedValue; + } +} diff --git a/lib/widgets/common/app_bar/quick_choosers/rate_chooser.dart b/lib/widgets/common/app_bar/quick_choosers/rate_chooser.dart index 0333b4b56..f0559a80a 100644 --- a/lib/widgets/common/app_bar/quick_choosers/rate_chooser.dart +++ b/lib/widgets/common/app_bar/quick_choosers/rate_chooser.dart @@ -5,13 +5,13 @@ import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:flutter/material.dart'; class RateQuickChooser extends StatefulWidget { - final ValueNotifier ratingNotifier; - final Stream moveUpdates; + final ValueNotifier valueNotifier; + final Stream pointerGlobalPosition; const RateQuickChooser({ super.key, - required this.ratingNotifier, - required this.moveUpdates, + required this.valueNotifier, + required this.pointerGlobalPosition, }); @override @@ -21,6 +21,11 @@ class RateQuickChooser extends StatefulWidget { class _RateQuickChooserState extends State { final List _subscriptions = []; + ValueNotifier get valueNotifier => widget.valueNotifier; + + static const margin = EdgeInsets.all(8); + static const padding = EdgeInsets.all(8); + @override void initState() { super.initState(); @@ -41,7 +46,7 @@ class _RateQuickChooserState extends State { } void _registerWidget(RateQuickChooser widget) { - _subscriptions.add(widget.moveUpdates.map((event) => event.globalPosition).listen(_onPointerMove)); + _subscriptions.add(widget.pointerGlobalPosition.listen(_onPointerMove)); } void _unregisterWidget(RateQuickChooser widget) { @@ -53,29 +58,27 @@ class _RateQuickChooserState extends State { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.all(8), + padding: margin, child: Material( shape: AvesDialog.shape(context), child: Padding( - padding: const EdgeInsets.all(8), + padding: padding, child: ValueListenableBuilder( - valueListenable: widget.ratingNotifier, - builder: (context, rating, child) { - final _rating = rating ?? 0; + valueListenable: valueNotifier, + builder: (context, selectedValue, child) { + final _rating = selectedValue ?? 0; return Row( mainAxisSize: MainAxisSize.min, - children: [ - ...List.generate(5, (i) { - final thisRating = i + 1; - return Padding( - padding: const EdgeInsets.all(4), - child: Icon( - _rating < thisRating ? AIcons.rating : AIcons.ratingFull, - color: _rating < thisRating ? Colors.grey : Colors.amber, - ), - ); - }) - ], + children: List.generate(5, (i) { + final thisRating = i + 1; + return Padding( + padding: const EdgeInsets.all(4), + child: Icon( + _rating < thisRating ? AIcons.rating : AIcons.ratingFull, + color: _rating < thisRating ? Colors.grey : Colors.amber, + ), + ); + }).toList(), ); }, ), @@ -85,9 +88,13 @@ class _RateQuickChooserState extends State { } void _onPointerMove(Offset globalPosition) { - final rowBox = context.findRenderObject() as RenderBox; - final rowSize = rowBox.size; - final local = rowBox.globalToLocal(globalPosition); - widget.ratingNotifier.value = (5 * local.dx / rowSize.width).ceil().clamp(0, 5); + final chooserBox = context.findRenderObject() as RenderBox; + final chooserSize = chooserBox.size; + final contentWidth = chooserSize.width - (margin.horizontal + padding.horizontal); + + final local = chooserBox.globalToLocal(globalPosition); + final dx = local.dx - (margin.horizontal + padding.horizontal) / 2; + + valueNotifier.value = (5 * dx / contentWidth).ceil().clamp(0, 5); } } diff --git a/lib/widgets/common/app_bar/rate_button.dart b/lib/widgets/common/app_bar/rate_button.dart index 274145a55..64fcb2dc4 100644 --- a/lib/widgets/common/app_bar/rate_button.dart +++ b/lib/widgets/common/app_bar/rate_button.dart @@ -34,8 +34,8 @@ class _RateQuickButtonState extends ChooserQuickButtonState { child: ScaleTransition( scale: animation, child: RateQuickChooser( - ratingNotifier: chooserValueNotifier, - moveUpdates: moveUpdates, + valueNotifier: chooserValueNotifier, + pointerGlobalPosition: pointerGlobalPosition, ), ), ); diff --git a/lib/widgets/common/map/geo_map.dart b/lib/widgets/common/map/geo_map.dart index 68c70c0d3..7427df121 100644 --- a/lib/widgets/common/map/geo_map.dart +++ b/lib/widgets/common/map/geo_map.dart @@ -213,8 +213,8 @@ class _GeoMapState extends State { MapThemeData.markerImageExtent + MapThemeData.markerOuterBorderWidth * 2 + MapThemeData.markerArrowSize.height, ), dotMarkerSize: const Size( - DotMarker.diameter + MapThemeData.markerOuterBorderWidth * 2, - DotMarker.diameter + MapThemeData.markerOuterBorderWidth * 2, + MapThemeData.markerDotDiameter + MapThemeData.markerOuterBorderWidth * 2, + MapThemeData.markerDotDiameter + MapThemeData.markerOuterBorderWidth * 2, ), overlayOpacityNotifier: widget.overlayOpacityNotifier, overlayEntry: widget.overlayEntry, diff --git a/lib/widgets/common/map/leaflet/map.dart b/lib/widgets/common/map/leaflet/map.dart index d21b97774..1f744e249 100644 --- a/lib/widgets/common/map/leaflet/map.dart +++ b/lib/widgets/common/map/leaflet/map.dart @@ -53,7 +53,7 @@ class EntryLeafletMap extends StatefulWidget { }); @override - State createState() => _EntryLeafletMapState(); + State> createState() => _EntryLeafletMapState(); } class _EntryLeafletMapState extends State> with TickerProviderStateMixin { diff --git a/lib/widgets/filter_grids/common/action_delegates/album_set.dart b/lib/widgets/filter_grids/common/action_delegates/album_set.dart index 7854c88f3..34012661c 100644 --- a/lib/widgets/filter_grids/common/action_delegates/album_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/album_set.dart @@ -212,7 +212,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with final emptyAlbums = todoAlbums.whereNot(filledAlbums.contains).toSet(); if (settings.enableBin && filledAlbums.isNotEmpty) { - await move( + await doMove( context, moveType: MoveType.toBin, entries: todoEntries, diff --git a/lib/widgets/viewer/action/entry_action_delegate.dart b/lib/widgets/viewer/action/entry_action_delegate.dart index 437199ced..d56ea8c8d 100644 --- a/lib/widgets/viewer/action/entry_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_action_delegate.dart @@ -280,6 +280,19 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix } } + void quickMove(BuildContext context, String? album, {required bool copy}) { + final targetEntry = _getTargetEntry(context, EntryAction.editRating); + if (album == null || (!copy && targetEntry.directory == album)) return; + + doQuickMove( + context, + moveType: copy ? MoveType.copy : MoveType.move, + entriesByDestination: { + album: {targetEntry} + }, + ); + } + void quickRate(BuildContext context, int? rating) { final targetEntry = _getTargetEntry(context, EntryAction.editRating); _metadataActionDelegate.quickRate(context, targetEntry, rating); @@ -441,7 +454,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix ); } - Future _move(BuildContext context, AvesEntry targetEntry, {required MoveType moveType}) => move( + Future _move(BuildContext context, AvesEntry targetEntry, {required MoveType moveType}) => doMove( context, moveType: moveType, entries: {targetEntry}, diff --git a/lib/widgets/viewer/action/entry_info_action_delegate.dart b/lib/widgets/viewer/action/entry_info_action_delegate.dart index 66695fce5..29829fe32 100644 --- a/lib/widgets/viewer/action/entry_info_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_info_action_delegate.dart @@ -148,7 +148,7 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi } Future quickRate(BuildContext context, AvesEntry targetEntry, int? rating) async { - if (rating == null) return; + if (rating == null || targetEntry.rating == rating) return; await edit(context, targetEntry, () => targetEntry.editRating(rating)); } diff --git a/lib/widgets/viewer/overlay/viewer_buttons.dart b/lib/widgets/viewer/overlay/viewer_buttons.dart index dcb6b3aef..0b808be2d 100644 --- a/lib/widgets/viewer/overlay/viewer_buttons.dart +++ b/lib/widgets/viewer/overlay/viewer_buttons.dart @@ -5,6 +5,7 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/app_bar/favourite_toggler.dart'; +import 'package:aves/widgets/common/app_bar/move_button.dart'; import 'package:aves/widgets/common/app_bar/rate_button.dart'; import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/basic/popup_menu_button.dart'; @@ -204,6 +205,22 @@ class ViewerButtonRowContent extends StatelessWidget { } switch (action) { + case EntryAction.copy: + child = MoveButton( + copy: true, + chooserPosition: PopupMenuPosition.over, + onChooserValue: (album) => _quickMove(context, album, copy: true), + onPressed: onPressed, + ); + break; + case EntryAction.move: + child = MoveButton( + copy: false, + chooserPosition: PopupMenuPosition.over, + onChooserValue: (album) => _quickMove(context, album, copy: false), + onPressed: onPressed, + ); + break; case EntryAction.toggleFavourite: child = FavouriteToggler( entries: {favouriteTargetEntry}, @@ -365,5 +382,7 @@ class ViewerButtonRowContent extends StatelessWidget { void _onActionSelected(BuildContext context, EntryAction action) => _entryActionDelegate.onActionSelected(context, action); + void _quickMove(BuildContext context, String? album, {required bool copy}) => _entryActionDelegate.quickMove(context, album, copy: copy); + void _quickRate(BuildContext context, int? rating) => _entryActionDelegate.quickRate(context, rating); } diff --git a/plugins/aves_map/lib/src/marker/dot.dart b/plugins/aves_map/lib/src/marker/dot.dart index 7a3477e22..b43b81b77 100644 --- a/plugins/aves_map/lib/src/marker/dot.dart +++ b/plugins/aves_map/lib/src/marker/dot.dart @@ -1,54 +1,18 @@ import 'package:aves_map/src/theme.dart'; +import 'package:aves_ui/aves_ui.dart'; import 'package:flutter/material.dart'; class DotMarker extends StatelessWidget { const DotMarker({super.key}); - static const double diameter = 16; - static const double outerBorderRadiusDim = diameter; - static const double outerBorderWidth = MapThemeData.markerOuterBorderWidth; - static const double innerBorderWidth = MapThemeData.markerInnerBorderWidth; - static const outerBorderRadius = BorderRadius.all(Radius.circular(outerBorderRadiusDim)); - static const innerRadius = Radius.circular(outerBorderRadiusDim - outerBorderWidth); - static const innerBorderRadius = BorderRadius.all(innerRadius); - @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final isDark = theme.brightness == Brightness.dark; - final outerBorderColor = MapThemeData.markerThemedOuterBorderColor(isDark); - final innerBorderColor = MapThemeData.markerThemedInnerBorderColor(isDark); - - final outerDecoration = BoxDecoration( - border: Border.fromBorderSide(BorderSide( - color: outerBorderColor, - width: outerBorderWidth, - )), - borderRadius: outerBorderRadius, - ); - - final innerDecoration = BoxDecoration( - border: Border.fromBorderSide(BorderSide( - color: innerBorderColor, - width: innerBorderWidth, - )), - borderRadius: innerBorderRadius, - ); - - return Container( - decoration: outerDecoration, - child: DecoratedBox( - decoration: innerDecoration, - position: DecorationPosition.foreground, - child: ClipRRect( - borderRadius: innerBorderRadius, - child: Container( - width: diameter, - height: diameter, - color: theme.colorScheme.secondary, - ), - ), - ), + return const AvesDot( + diameter: MapThemeData.markerDotDiameter, + outerBorderWidth: MapThemeData.markerOuterBorderWidth, + innerBorderWidth: MapThemeData.markerInnerBorderWidth, + getOuterBorderColor: MapThemeData.markerThemedOuterBorderColor, + getInnerBorderColor: MapThemeData.markerThemedInnerBorderColor, ); } } diff --git a/plugins/aves_map/lib/src/theme.dart b/plugins/aves_map/lib/src/theme.dart index c0f75e67b..e0daabd63 100644 --- a/plugins/aves_map/lib/src/theme.dart +++ b/plugins/aves_map/lib/src/theme.dart @@ -22,6 +22,7 @@ class MapThemeData { static const double markerInnerBorderWidth = 2; static const double markerImageExtent = 48.0; static const Size markerArrowSize = Size(8, 6); + static const double markerDotDiameter = 16; static Color markerThemedOuterBorderColor(bool isDark) => isDark ? Colors.white30 : Colors.black26; diff --git a/plugins/aves_map/pubspec.lock b/plugins/aves_map/pubspec.lock index daebb0222..a50ca5849 100644 --- a/plugins/aves_map/pubspec.lock +++ b/plugins/aves_map/pubspec.lock @@ -8,6 +8,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.10.0" + aves_ui: + dependency: "direct main" + description: + path: "../aves_ui" + relative: true + source: path + version: "0.0.1" characters: dependency: transitive description: diff --git a/plugins/aves_map/pubspec.yaml b/plugins/aves_map/pubspec.yaml index 2bd1fa095..9f2e682a6 100644 --- a/plugins/aves_map/pubspec.yaml +++ b/plugins/aves_map/pubspec.yaml @@ -8,6 +8,8 @@ environment: dependencies: flutter: sdk: flutter + aves_ui: + path: ../aves_ui collection: # TODO TLAD as of 2022/02/22, null safe version is pre-release custom_rounded_rectangle_border: '>=0.2.0-nullsafety.0' diff --git a/plugins/aves_services/pubspec.lock b/plugins/aves_services/pubspec.lock index 8eb99297a..145474f7d 100644 --- a/plugins/aves_services/pubspec.lock +++ b/plugins/aves_services/pubspec.lock @@ -15,6 +15,13 @@ packages: relative: true source: path version: "0.0.1" + aves_ui: + dependency: transitive + description: + path: "../aves_ui" + relative: true + source: path + version: "0.0.1" characters: dependency: transitive description: diff --git a/plugins/aves_services_google/pubspec.lock b/plugins/aves_services_google/pubspec.lock index 9d60bc455..18f0c0b19 100644 --- a/plugins/aves_services_google/pubspec.lock +++ b/plugins/aves_services_google/pubspec.lock @@ -22,6 +22,13 @@ packages: relative: true source: path version: "0.0.1" + aves_ui: + dependency: transitive + description: + path: "../aves_ui" + relative: true + source: path + version: "0.0.1" characters: dependency: transitive description: diff --git a/plugins/aves_services_huawei/pubspec.lock b/plugins/aves_services_huawei/pubspec.lock index 4386459ce..9a86bbc3e 100644 --- a/plugins/aves_services_huawei/pubspec.lock +++ b/plugins/aves_services_huawei/pubspec.lock @@ -29,6 +29,13 @@ packages: relative: true source: path version: "0.0.1" + aves_ui: + dependency: transitive + description: + path: "../aves_ui" + relative: true + source: path + version: "0.0.1" characters: dependency: transitive description: diff --git a/plugins/aves_services_none/pubspec.lock b/plugins/aves_services_none/pubspec.lock index a23a381b1..708877175 100644 --- a/plugins/aves_services_none/pubspec.lock +++ b/plugins/aves_services_none/pubspec.lock @@ -22,6 +22,13 @@ packages: relative: true source: path version: "0.0.1" + aves_ui: + dependency: transitive + description: + path: "../aves_ui" + relative: true + source: path + version: "0.0.1" characters: dependency: transitive description: diff --git a/plugins/aves_ui/.gitignore b/plugins/aves_ui/.gitignore new file mode 100644 index 000000000..28124a571 --- /dev/null +++ b/plugins/aves_ui/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +#/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/plugins/aves_ui/.metadata b/plugins/aves_ui/.metadata new file mode 100644 index 000000000..c24d00d29 --- /dev/null +++ b/plugins/aves_ui/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 5464c5bac742001448fe4fc0597be939379f88ea + channel: stable + +project_type: package diff --git a/plugins/aves_ui/analysis_options.yaml b/plugins/aves_ui/analysis_options.yaml new file mode 100644 index 000000000..f04c6cf0f --- /dev/null +++ b/plugins/aves_ui/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/plugins/aves_ui/lib/aves_ui.dart b/plugins/aves_ui/lib/aves_ui.dart new file mode 100644 index 000000000..53b2945dc --- /dev/null +++ b/plugins/aves_ui/lib/aves_ui.dart @@ -0,0 +1,3 @@ +library aves_ui; + +export 'src/dot.dart'; diff --git a/plugins/aves_ui/lib/src/dot.dart b/plugins/aves_ui/lib/src/dot.dart new file mode 100644 index 000000000..9ca5e2726 --- /dev/null +++ b/plugins/aves_ui/lib/src/dot.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +class AvesDot extends StatelessWidget { + final double diameter, outerBorderWidth, innerBorderWidth; + final Color Function(bool isDark) getOuterBorderColor, getInnerBorderColor; + + const AvesDot({ + super.key, + this.diameter = 16, + this.outerBorderWidth = 1.5, + this.innerBorderWidth = 2, + this.getOuterBorderColor = themedOuterBorderColor, + this.getInnerBorderColor = themedInnerBorderColor, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + final outerBorderColor = getOuterBorderColor(isDark); + final innerBorderColor = getInnerBorderColor(isDark); + final outerBorderRadius = BorderRadius.all(Radius.circular(diameter)); + final innerRadius = Radius.circular(diameter - outerBorderWidth); + final innerBorderRadius = BorderRadius.all(innerRadius); + + final outerDecoration = BoxDecoration( + border: Border.fromBorderSide(BorderSide( + color: outerBorderColor, + width: outerBorderWidth, + )), + borderRadius: outerBorderRadius, + ); + + final innerDecoration = BoxDecoration( + border: Border.fromBorderSide(BorderSide( + color: innerBorderColor, + width: innerBorderWidth, + )), + borderRadius: innerBorderRadius, + ); + + return Container( + decoration: outerDecoration, + child: DecoratedBox( + decoration: innerDecoration, + position: DecorationPosition.foreground, + child: ClipRRect( + borderRadius: innerBorderRadius, + child: Container( + width: diameter, + height: diameter, + color: theme.colorScheme.secondary, + ), + ), + ), + ); + } + + static Color themedOuterBorderColor(bool isDark) => isDark ? Colors.white30 : Colors.black26; + + static Color themedInnerBorderColor(bool isDark) => isDark ? const Color(0xFF212121) : Colors.white; +} diff --git a/plugins/aves_ui/pubspec.lock b/plugins/aves_ui/pubspec.lock new file mode 100644 index 000000000..9d22b49d3 --- /dev/null +++ b/plugins/aves_ui/pubspec.lock @@ -0,0 +1,64 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.16.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.5" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" +sdks: + dart: ">=2.18.0 <3.0.0" diff --git a/plugins/aves_ui/pubspec.yaml b/plugins/aves_ui/pubspec.yaml new file mode 100644 index 000000000..b7a7e9643 --- /dev/null +++ b/plugins/aves_ui/pubspec.yaml @@ -0,0 +1,15 @@ +name: aves_ui +version: 0.0.1 +publish_to: none + +environment: + sdk: ">=2.18.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_lints: + +flutter: diff --git a/pubspec.lock b/pubspec.lock index 14adfc172..c363b56e0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -85,6 +85,13 @@ packages: relative: true source: path version: "0.0.1" + aves_ui: + dependency: "direct main" + description: + path: "plugins/aves_ui" + relative: true + source: path + version: "0.0.1" barcode: dependency: transitive description: @@ -615,7 +622,7 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.3" motion_sensors: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d2cacf38f..ac542b76f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,8 @@ dependencies: path: plugins/aves_services aves_services_platform: path: plugins/aves_services_google + aves_ui: + path: plugins/aves_ui charts_flutter: collection: connectivity_plus: