From 433ac537dd91ccbabe7132b3669d69bb9e19d3b0 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 8 Mar 2023 19:56:56 +0100 Subject: [PATCH] #197 preview button when selecting items --- CHANGELOG.md | 1 + lib/app_mode.dart | 5 + lib/widgets/collection/collection_grid.dart | 7 +- .../common/basic/text/animated_diff.dart | 130 ++---------------- lib/widgets/common/grid/overlay.dart | 2 +- lib/widgets/common/map/buttons/panel.dart | 2 + lib/widgets/common/thumbnail/decorated.dart | 7 +- .../viewer/action/entry_action_delegate.dart | 13 +- .../action/entry_info_action_delegate.dart | 16 ++- lib/widgets/viewer/entry_viewer_stack.dart | 19 ++- lib/widgets/viewer/info/basic_section.dart | 11 +- lib/widgets/viewer/info/info_app_bar.dart | 12 +- lib/widgets/viewer/info/location_section.dart | 7 +- lib/widgets/viewer/overlay/bottom.dart | 54 ++++---- .../viewer/overlay/selection_button.dart | 83 +++++++++++ .../viewer/overlay/viewer_buttons.dart | 19 ++- plugins/aves_map/lib/src/theme.dart | 2 +- 17 files changed, 225 insertions(+), 165 deletions(-) create mode 100644 lib/widgets/viewer/overlay/selection_button.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 401ba61df..3ca45d2f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added +- Collection: preview button when selecting items - Vaults: custom pattern lock - Video: picture-in-picture - Video: handle skip next/previous media buttons diff --git a/lib/app_mode.dart b/lib/app_mode.dart index 925c3d967..99dd00ef4 100644 --- a/lib/app_mode.dart +++ b/lib/app_mode.dart @@ -19,6 +19,11 @@ extension ExtraAppMode on AppMode { AppMode.pickMultipleMediaExternal, }.contains(this); + bool get canEditEntry => { + AppMode.main, + AppMode.view, + }.contains(this); + bool get canSelectMedia => { AppMode.main, AppMode.pickMultipleMediaExternal, diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart index ab7d37a03..5909acd79 100644 --- a/lib/widgets/collection/collection_grid.dart +++ b/lib/widgets/collection/collection_grid.dart @@ -254,8 +254,11 @@ class _CollectionGridContentState extends State<_CollectionGridContent> { ); if (selection.isSelecting) { - child = ChangeNotifierProvider>.value( - value: selection, + child = MultiProvider( + providers: [ + ListenableProvider>.value(value: ValueNotifier(AppMode.pickMediaInternal)), + ChangeNotifierProvider>.value(value: selection), + ], child: child, ); } diff --git a/lib/widgets/common/basic/text/animated_diff.dart b/lib/widgets/common/basic/text/animated_diff.dart index 624ed7d3b..8ac807e14 100644 --- a/lib/widgets/common/basic/text/animated_diff.dart +++ b/lib/widgets/common/basic/text/animated_diff.dart @@ -73,8 +73,8 @@ class _AnimatedDiffTextState extends State with SingleTickerPr children: _diffs.map((diff) { final oldText = diff.item1; final newText = diff.item2; - final oldWidth = diff.item3; - final newWidth = diff.item4; + final oldSize = diff.item3; + final newSize = diff.item4; final text = (_animation.value == 0 ? oldText : newText) ?? ''; return WidgetSpan( child: AnimatedSize( @@ -91,9 +91,10 @@ class _AnimatedDiffTextState extends State with SingleTickerPr children: [ ...previousChildren.map( (child) => ConstrainedBox( - constraints: BoxConstraints( - maxWidth: min(oldWidth, newWidth), - ), + constraints: BoxConstraints.tight(Size( + min(oldSize.width, newSize.width), + min(oldSize.height, newSize.height), + )), child: child, ), ), @@ -116,14 +117,16 @@ class _AnimatedDiffTextState extends State with SingleTickerPr ); } - double textWidth(String text) { + Size textSize(String text) { final para = RenderParagraph( TextSpan(text: text, style: widget.textStyle), textDirection: Directionality.of(context), textScaleFactor: MediaQuery.textScaleFactorOf(context), strutStyle: widget.strutStyle, )..layout(const BoxConstraints(), parentUsesSize: true); - return para.getMaxIntrinsicWidth(double.infinity); + final width = para.getMaxIntrinsicWidth(double.infinity); + final height = para.getMaxIntrinsicHeight(double.infinity); + return Size(width, height); } // use an adaptation of Google's `Diff Match and Patch` @@ -140,15 +143,15 @@ class _AnimatedDiffTextState extends State with SingleTickerPr ..clear() ..addAll(d.map((diff) { final text = diff.text; + final size = textSize(text); switch (diff.operation) { case Operation.delete: - return Tuple4(text, null, textWidth(text), .0); + return Tuple4(text, null, size, Size.zero); case Operation.insert: - return Tuple4(null, text, .0, textWidth(text)); + return Tuple4(null, text, Size.zero, size); case Operation.equal: default: - final width = textWidth(text); - return Tuple4(text, text, width, width); + return Tuple4(text, text, size, size); } }).fold>([], (prev, v) { if (prev.isNotEmpty) { @@ -168,109 +171,6 @@ class _AnimatedDiffTextState extends State with SingleTickerPr return [...prev, v]; })); } - -// void _computeDiff(String oldText, String newText) { -// final oldCharacters = oldText.characters.toList(); -// final newCharacters = newText.characters.toList(); -// final diffResult = calculateListDiff(oldCharacters, newCharacters, detectMoves: false); -// final updates = diffResult.getUpdatesWithData().toList(); -// List diffs = []; -// DataDiffUpdate? pendingUpdate; -// int lastPos = oldCharacters.length; -// void addKeep(int pos) { -// if (pos < lastPos) { -// final text = oldCharacters.sublist(pos, lastPos).join(); -// final width = textWidth(text); -// diffs.insert(0, Tuple4(text, text, width, width)); -// lastPos = pos; -// } -// } -// -// void commit(DataDiffUpdate? update) { -// update?.when( -// insert: (pos, data) { -// addKeep(pos); -// diffs.insert(0, Tuple4(null, data, 0, textWidth(data))); -// lastPos = pos; -// }, -// remove: (pos, data) { -// addKeep(pos + data.length); -// diffs.insert(0, Tuple4(data, null, textWidth(data), 0)); -// lastPos = pos; -// }, -// change: (pos, oldData, newData) { -// addKeep(pos + oldData.length); -// diffs.insert(0, Tuple4(oldData, newData, textWidth(oldData), textWidth(newData))); -// lastPos = pos; -// }, -// move: (from, to, data) { -// assert(false, '`move` update: from=$from, to=$from, data=$data'); -// }, -// ); -// } -// -// for (var update in updates) { -// update.when( -// insert: (pos, data) { -// if (pendingUpdate == null) { -// pendingUpdate = update; -// return; -// } -// if (pendingUpdate is DataInsert) { -// final pendingInsert = pendingUpdate as DataInsert; -// if (pendingInsert.position == pos) { -// // merge insertions -// pendingUpdate = DataInsert(position: pos, data: data + pendingInsert.data); -// return; -// } -// } else if (pendingUpdate is DataRemove) { -// final pendingRemove = pendingUpdate as DataRemove; -// if (pendingRemove.position == pos) { -// // convert to change -// pendingUpdate = DataChange(position: pos, oldData: pendingRemove.data, newData: data); -// return; -// } -// } else if (pendingUpdate is DataChange) { -// final pendingChange = pendingUpdate as DataChange; -// if (pendingChange.position == pos) { -// // merge changes -// pendingUpdate = DataChange(position: pos, oldData: pendingChange.oldData, newData: data + pendingChange.newData); -// return; -// } -// } -// commit(pendingUpdate); -// pendingUpdate = update; -// }, -// remove: (pos, data) { -// if (pendingUpdate == null) { -// pendingUpdate = update; -// return; -// } -// if (pendingUpdate is DataRemove) { -// final pendingRemove = pendingUpdate as DataRemove; -// if (pendingRemove.position == pos + data.length) { -// // merge removals -// pendingUpdate = DataRemove(position: pos, data: data + pendingRemove.data); -// return; -// } -// } -// commit(pendingUpdate); -// pendingUpdate = update; -// }, -// change: (pos, oldData, newData) { -// assert(false, '`change` update: from=$pos, oldData=$oldData, newData=$newData'); -// }, -// move: (from, to, data) { -// assert(false, '`move` update: from=$from, to=$from, data=$data'); -// }, -// ); -// } -// commit(pendingUpdate); -// addKeep(0); -// _diffs -// ..clear() -// ..addAll(diffs); -// } } -typedef _TextDiff = Tuple4; +typedef _TextDiff = Tuple4; diff --git a/lib/widgets/common/grid/overlay.dart b/lib/widgets/common/grid/overlay.dart index 28501f925..2961dff2b 100644 --- a/lib/widgets/common/grid/overlay.dart +++ b/lib/widgets/common/grid/overlay.dart @@ -26,7 +26,7 @@ class GridItemSelectionOverlay extends StatelessWidget { duration: duration, child: isSelecting ? Selector, bool>( - selector: (context, selection) => selection.isSelected([item]), + selector: (context, selection) => selection.isSelected({item}), builder: (context, isSelected, child) { return AnimatedContainer( alignment: AlignmentDirectional.topEnd, diff --git a/lib/widgets/common/map/buttons/panel.dart b/lib/widgets/common/map/buttons/panel.dart index 7e37cb822..9d72ae6e7 100644 --- a/lib/widgets/common/map/buttons/panel.dart +++ b/lib/widgets/common/map/buttons/panel.dart @@ -51,6 +51,8 @@ class MapButtonPanel extends StatelessWidget { ); } break; + case MapNavigationButton.none: + break; } final showCoordinateFilter = context.select((v) => v.showCoordinateFilter); diff --git a/lib/widgets/common/thumbnail/decorated.dart b/lib/widgets/common/thumbnail/decorated.dart index 9871d3502..93ccf66ac 100644 --- a/lib/widgets/common/thumbnail/decorated.dart +++ b/lib/widgets/common/thumbnail/decorated.dart @@ -2,6 +2,7 @@ import 'package:aves/model/entry.dart'; import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/common/grid/overlay.dart'; import 'package:aves/widgets/common/thumbnail/image.dart'; +import 'package:aves/widgets/common/thumbnail/notifications.dart'; import 'package:aves/widgets/common/thumbnail/overlay.dart'; import 'package:flutter/material.dart'; @@ -44,11 +45,15 @@ class DecoratedThumbnail extends StatelessWidget { children: [ child, ThumbnailEntryOverlay(entry: entry), - if (selectable) + if (selectable) ...[ GridItemSelectionOverlay( item: entry, padding: const EdgeInsets.all(2), ), + ThumbnailZoomOverlay( + onZoom: () => OpenViewerNotification(entry).dispatch(context), + ), + ], if (highlightable) ThumbnailHighlightOverlay(entry: entry), ], ); diff --git a/lib/widgets/viewer/action/entry_action_delegate.dart b/lib/widgets/viewer/action/entry_action_delegate.dart index e984f6e32..ca4322671 100644 --- a/lib/widgets/viewer/action/entry_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_action_delegate.dart @@ -47,7 +47,10 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix EntryActionDelegate(this.mainEntry, this.pageEntry, this.collection); - bool isVisible(EntryAction action) { + bool isVisible({ + required AppMode appMode, + required EntryAction action, + }) { if (mainEntry.trashed) { switch (action) { case EntryAction.delete: @@ -60,7 +63,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix } } else { final targetEntry = EntryActions.pageActions.contains(action) ? pageEntry : mainEntry; - final canWrite = !settings.isReadOnly; + final canWrite = appMode.canEditEntry && !settings.isReadOnly; switch (action) { case EntryAction.toggleFavourite: return collection != null; @@ -120,7 +123,11 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix case EntryAction.showGeoTiffOnMap: case EntryAction.convertMotionPhotoToStillImage: case EntryAction.viewMotionPhotoVideo: - return _metadataActionDelegate.isVisible(targetEntry, action); + return _metadataActionDelegate.isVisible( + appMode: appMode, + targetEntry: targetEntry, + action: action, + ); case EntryAction.debug: return kDebugMode; } diff --git a/lib/widgets/viewer/action/entry_info_action_delegate.dart b/lib/widgets/viewer/action/entry_info_action_delegate.dart index 59d1cbb74..78dfe9374 100644 --- a/lib/widgets/viewer/action/entry_info_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_info_action_delegate.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/events.dart'; import 'package:aves/model/entry.dart'; @@ -29,8 +30,12 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi Stream> get eventStream => _eventStreamController.stream; - bool isVisible(AvesEntry targetEntry, EntryAction action) { - final canWrite = !settings.isReadOnly; + bool isVisible({ + required AppMode appMode, + required AvesEntry targetEntry, + required EntryAction action, + }) { + final canWrite = appMode.canEditEntry && !settings.isReadOnly; switch (action) { // general case EntryAction.editDate: @@ -39,16 +44,17 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi case EntryAction.editRating: case EntryAction.editTags: case EntryAction.removeMetadata: - case EntryAction.exportMetadata: return canWrite; + case EntryAction.exportMetadata: + return true; // GeoTIFF case EntryAction.showGeoTiffOnMap: - return targetEntry.isGeotiff; + return appMode.canNavigate && targetEntry.isGeotiff; // motion photo case EntryAction.convertMotionPhotoToStillImage: return canWrite && targetEntry.isMotionPhoto; case EntryAction.viewMotionPhotoVideo: - return targetEntry.isMotionPhoto; + return appMode.canNavigate && targetEntry.isMotionPhoto; default: return false; } diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index b00ef23d8..9cf35d55d 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -562,12 +562,19 @@ class _EntryViewerStackState extends State with EntryViewContr void _onVerticalPageChanged(int page) { _currentVerticalPage.value = page; - if (page == transitionPage) { - dismissFeedback(context); - _popVisual(); - } else if (page == infoPage) { - // prevent hero when viewer is offscreen - _heroInfoNotifier.value = null; + switch (page) { + case transitionPage: + dismissFeedback(context); + _popVisual(); + break; + case imagePage: + reportService.log('Nav move to Image page'); + break; + case infoPage: + reportService.log('Nav move to Info page'); + // prevent hero when viewer is offscreen + _heroInfoNotifier.value = null; + break; } } diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index 1f3d8e7e4..4f081f3d2 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -150,11 +150,20 @@ class _BasicSectionState extends State { } Widget _buildEditButtons(BuildContext context) { + final appMode = context.watch>().value; final entry = widget.entry; final children = [ EntryAction.editRating, EntryAction.editTags, - ].where((v) => actionDelegate.canApply(entry, v)).map((v) => _buildEditMetadataButton(context, v)).toList(); + ] + .where((v) => actionDelegate.isVisible( + appMode: appMode, + targetEntry: entry, + action: v, + )) + .where((v) => actionDelegate.canApply(entry, v)) + .map((v) => _buildEditMetadataButton(context, v)) + .toList(); return children.isEmpty ? const SizedBox() diff --git a/lib/widgets/viewer/info/info_app_bar.dart b/lib/widgets/viewer/info/info_app_bar.dart index 1b2bb745e..c306074ee 100644 --- a/lib/widgets/viewer/info/info_app_bar.dart +++ b/lib/widgets/viewer/info/info_app_bar.dart @@ -1,3 +1,4 @@ +import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/settings.dart'; @@ -14,6 +15,7 @@ import 'package:aves/widgets/viewer/info/metadata/metadata_dir.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +import 'package:provider/provider.dart'; class InfoAppBar extends StatelessWidget { final AvesEntry entry; @@ -33,8 +35,14 @@ class InfoAppBar extends StatelessWidget { @override Widget build(BuildContext context) { - final commonActions = EntryActions.commonMetadataActions.where((v) => actionDelegate.isVisible(entry, v)); - final formatSpecificActions = EntryActions.formatSpecificMetadataActions.where((v) => actionDelegate.isVisible(entry, v)); + final appMode = context.watch>().value; + bool isVisible(EntryAction action) => actionDelegate.isVisible( + appMode: appMode, + targetEntry: entry, + action: action, + ); + final commonActions = EntryActions.commonMetadataActions.where(isVisible); + final formatSpecificActions = EntryActions.formatSpecificMetadataActions.where(isVisible); final useTvLayout = settings.useTvLayout; return SliverAppBar( leading: useTvLayout diff --git a/lib/widgets/viewer/info/location_section.dart b/lib/widgets/viewer/info/location_section.dart index 2742cd701..208d270fe 100644 --- a/lib/widgets/viewer/info/location_section.dart +++ b/lib/widgets/viewer/info/location_section.dart @@ -1,3 +1,4 @@ +import 'package:aves/app_mode.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/location.dart'; import 'package:aves/model/settings/enums/coordinate_format.dart'; @@ -13,6 +14,7 @@ import 'package:aves/widgets/map/map_page.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves_map/aves_map.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class LocationSection extends StatefulWidget { final CollectionLens? collection; @@ -72,6 +74,7 @@ class _LocationSectionState extends State { Widget build(BuildContext context) { if (!entry.hasGps) return const SizedBox(); + final canNavigate = context.select, bool>((v) => v.value.canNavigate); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -79,7 +82,7 @@ class _LocationSectionState extends State { MapTheme( interactive: false, showCoordinateFilter: false, - navigationButton: MapNavigationButton.map, + navigationButton: canNavigate ? MapNavigationButton.map : MapNavigationButton.none, visualDensity: VisualDensity.compact, mapHeight: 200, child: GeoMap( @@ -87,7 +90,7 @@ class _LocationSectionState extends State { entries: [entry], isAnimatingNotifier: widget.isScrollingNotifier, onUserZoomChange: (zoom) => settings.infoMapZoom = zoom.roundToDouble(), - onMarkerTap: collection != null ? (location, entry) => _openMapPage(context) : null, + onMarkerTap: collection != null && canNavigate ? (location, entry) => _openMapPage(context) : null, openMapPage: collection != null ? _openMapPage : null, ), ), diff --git a/lib/widgets/viewer/overlay/bottom.dart b/lib/widgets/viewer/overlay/bottom.dart index a48dbb36e..e23aa29a7 100644 --- a/lib/widgets/viewer/overlay/bottom.dart +++ b/lib/widgets/viewer/overlay/bottom.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:aves/app_mode.dart'; import 'package:aves/model/entry.dart'; +import 'package:aves/model/selection.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/common/extensions/media_query.dart'; @@ -9,6 +10,7 @@ import 'package:aves/widgets/viewer/entry_vertical_pager.dart'; import 'package:aves/widgets/viewer/multipage/controller.dart'; import 'package:aves/widgets/viewer/notifications.dart'; import 'package:aves/widgets/viewer/overlay/multipage.dart'; +import 'package:aves/widgets/viewer/overlay/selection_button.dart'; import 'package:aves/widgets/viewer/overlay/thumbnail_preview.dart'; import 'package:aves/widgets/viewer/overlay/viewer_buttons.dart'; import 'package:aves/widgets/viewer/overlay/wallpaper_buttons.dart'; @@ -183,30 +185,36 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> { ]), builder: (context, child) { final viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero); - final viewerButtonRow = FocusableActionDetector( - focusNode: _buttonRowFocusScopeNode, - shortcuts: settings.useTvLayout ? const {SingleActivator(LogicalKeyboardKey.arrowUp): TvShowLessInfoIntent()} : null, - actions: {TvShowLessInfoIntent: CallbackAction(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context))}, - child: SafeArea( - top: false, - bottom: false, - minimum: EdgeInsets.only( - left: viewInsetsPadding.left, - right: viewInsetsPadding.right, - ), - child: isWallpaperMode - ? WallpaperButtons( - entry: pageEntry, - scale: _buttonScale, - ) - : ViewerButtons( - mainEntry: mainEntry, - pageEntry: pageEntry, - collection: widget.collection, - scale: _buttonScale, + final selection = context.read?>(); + final viewerButtonRow = (selection?.isSelecting ?? false) + ? SelectionButton( + mainEntry: mainEntry, + scale: _buttonScale, + ) + : FocusableActionDetector( + focusNode: _buttonRowFocusScopeNode, + shortcuts: settings.useTvLayout ? const {SingleActivator(LogicalKeyboardKey.arrowUp): TvShowLessInfoIntent()} : null, + actions: {TvShowLessInfoIntent: CallbackAction(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context))}, + child: SafeArea( + top: false, + bottom: false, + minimum: EdgeInsets.only( + left: viewInsetsPadding.left, + right: viewInsetsPadding.right, ), - ), - ); + child: isWallpaperMode + ? WallpaperButtons( + entry: pageEntry, + scale: _buttonScale, + ) + : ViewerButtons( + mainEntry: mainEntry, + pageEntry: pageEntry, + collection: widget.collection, + scale: _buttonScale, + ), + ), + ); final showMultiPageOverlay = mainEntry.isMultiPage && multiPageController != null; final collapsedPageScroller = mainEntry.isMotionPhoto; diff --git a/lib/widgets/viewer/overlay/selection_button.dart b/lib/widgets/viewer/overlay/selection_button.dart new file mode 100644 index 000000000..ad2f1b899 --- /dev/null +++ b/lib/widgets/viewer/overlay/selection_button.dart @@ -0,0 +1,83 @@ +import 'package:aves/model/entry.dart'; +import 'package:aves/model/selection.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/common/basic/text/animated_diff.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/buttons/overlay_button.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class SelectionButton extends StatelessWidget { + final AvesEntry mainEntry; + final Animation scale; + + static const double padding = 8; + static const duration = Durations.thumbnailOverlayAnimation; + + const SelectionButton({ + super.key, + required this.mainEntry, + required this.scale, + }); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final selection = context.read>(); + return SafeArea( + top: false, + bottom: false, + child: Padding( + padding: const EdgeInsets.only(left: padding, right: padding, bottom: padding), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Spacer(), + ScalingOverlayTextButton( + scale: scale, + onPressed: () => selection.toggleSelection(mainEntry), + child: Selector?, int>( + selector: (context, selection) => selection?.selectedItems.length ?? 0, + builder: (context, count, child) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedDiffText( + count == 0 ? l10n.collectionSelectPageTitle : l10n.itemCount(count), + duration: duration, + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: Text(Constants.separator), + ), + Selector, bool>( + selector: (context, selection) => selection.isSelected({mainEntry}), + builder: (context, isSelected, child) { + return AnimatedSwitcher( + duration: duration, + switchInCurve: Curves.easeOutBack, + switchOutCurve: Curves.easeOutBack, + transitionBuilder: (child, animation) => ScaleTransition( + scale: animation, + child: child, + ), + child: Icon( + isSelected ? AIcons.selected : AIcons.unselected, + key: ValueKey(isSelected), + ), + ); + }, + ), + ], + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/viewer/overlay/viewer_buttons.dart b/lib/widgets/viewer/overlay/viewer_buttons.dart index bd1342a86..41208da65 100644 --- a/lib/widgets/viewer/overlay/viewer_buttons.dart +++ b/lib/widgets/viewer/overlay/viewer_buttons.dart @@ -1,3 +1,4 @@ +import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/settings.dart'; @@ -61,6 +62,12 @@ class ViewerButtons extends StatelessWidget { ); } + final appMode = context.watch>().value; + bool isVisible(EntryAction action) => actionDelegate.isVisible( + appMode: appMode, + action: action, + ); + final trashed = mainEntry.trashed; return SafeArea( top: false, @@ -72,9 +79,9 @@ class ViewerButtons extends StatelessWidget { return Selector( selector: (context, s) => s.isRotationLocked, builder: (context, s, child) { - final quickActions = (trashed ? EntryActions.trashed : settings.viewerQuickActions).where(actionDelegate.isVisible).where(actionDelegate.canApply).take(availableCount - 1).toList(); + final quickActions = (trashed ? EntryActions.trashed : settings.viewerQuickActions).where(isVisible).where(actionDelegate.canApply).take(availableCount - 1).toList(); List getMenuActions(List categoryActions) { - return categoryActions.where((action) => !quickActions.contains(action)).where(actionDelegate.isVisible).toList(); + return categoryActions.where((action) => !quickActions.contains(action)).where(isVisible).toList(); } return ViewerButtonRowContent( @@ -109,6 +116,7 @@ class _TvButtonRowContent extends StatelessWidget { @override Widget build(BuildContext context) { + final appMode = context.watch>().value; return Selector( selector: (context, vc) => vc.getController(pageEntry), builder: (context, videoController, child) { @@ -120,7 +128,12 @@ class _TvButtonRowContent extends StatelessWidget { ...EntryActions.export, ...EntryActions.videoPlayback, ...EntryActions.video, - ].where(actionDelegate.isVisible).map((action) { + ] + .where((action) => actionDelegate.isVisible( + appMode: appMode, + action: action, + )) + .map((action) { final enabled = actionDelegate.canApply(action); return CaptionedButton( scale: scale, diff --git a/plugins/aves_map/lib/src/theme.dart b/plugins/aves_map/lib/src/theme.dart index e0daabd63..b05e60ad1 100644 --- a/plugins/aves_map/lib/src/theme.dart +++ b/plugins/aves_map/lib/src/theme.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -enum MapNavigationButton { back, map } +enum MapNavigationButton { back, map, none } class MapThemeData { final bool interactive, showCoordinateFilter;