#197 preview button when selecting items

This commit is contained in:
Thibault Deckers 2023-03-08 19:56:56 +01:00
parent 09cf4fef3e
commit 433ac537dd
17 changed files with 225 additions and 165 deletions

View file

@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
### Added ### Added
- Collection: preview button when selecting items
- Vaults: custom pattern lock - Vaults: custom pattern lock
- Video: picture-in-picture - Video: picture-in-picture
- Video: handle skip next/previous media buttons - Video: handle skip next/previous media buttons

View file

@ -19,6 +19,11 @@ extension ExtraAppMode on AppMode {
AppMode.pickMultipleMediaExternal, AppMode.pickMultipleMediaExternal,
}.contains(this); }.contains(this);
bool get canEditEntry => {
AppMode.main,
AppMode.view,
}.contains(this);
bool get canSelectMedia => { bool get canSelectMedia => {
AppMode.main, AppMode.main,
AppMode.pickMultipleMediaExternal, AppMode.pickMultipleMediaExternal,

View file

@ -254,8 +254,11 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
); );
if (selection.isSelecting) { if (selection.isSelecting) {
child = ChangeNotifierProvider<Selection<AvesEntry>>.value( child = MultiProvider(
value: selection, providers: [
ListenableProvider<ValueNotifier<AppMode>>.value(value: ValueNotifier(AppMode.pickMediaInternal)),
ChangeNotifierProvider<Selection<AvesEntry>>.value(value: selection),
],
child: child, child: child,
); );
} }

View file

@ -73,8 +73,8 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
children: _diffs.map((diff) { children: _diffs.map((diff) {
final oldText = diff.item1; final oldText = diff.item1;
final newText = diff.item2; final newText = diff.item2;
final oldWidth = diff.item3; final oldSize = diff.item3;
final newWidth = diff.item4; final newSize = diff.item4;
final text = (_animation.value == 0 ? oldText : newText) ?? ''; final text = (_animation.value == 0 ? oldText : newText) ?? '';
return WidgetSpan( return WidgetSpan(
child: AnimatedSize( child: AnimatedSize(
@ -91,9 +91,10 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
children: [ children: [
...previousChildren.map( ...previousChildren.map(
(child) => ConstrainedBox( (child) => ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints.tight(Size(
maxWidth: min(oldWidth, newWidth), min(oldSize.width, newSize.width),
), min(oldSize.height, newSize.height),
)),
child: child, child: child,
), ),
), ),
@ -116,14 +117,16 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
); );
} }
double textWidth(String text) { Size textSize(String text) {
final para = RenderParagraph( final para = RenderParagraph(
TextSpan(text: text, style: widget.textStyle), TextSpan(text: text, style: widget.textStyle),
textDirection: Directionality.of(context), textDirection: Directionality.of(context),
textScaleFactor: MediaQuery.textScaleFactorOf(context), textScaleFactor: MediaQuery.textScaleFactorOf(context),
strutStyle: widget.strutStyle, strutStyle: widget.strutStyle,
)..layout(const BoxConstraints(), parentUsesSize: true); )..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` // use an adaptation of Google's `Diff Match and Patch`
@ -140,15 +143,15 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
..clear() ..clear()
..addAll(d.map((diff) { ..addAll(d.map((diff) {
final text = diff.text; final text = diff.text;
final size = textSize(text);
switch (diff.operation) { switch (diff.operation) {
case Operation.delete: case Operation.delete:
return Tuple4(text, null, textWidth(text), .0); return Tuple4(text, null, size, Size.zero);
case Operation.insert: case Operation.insert:
return Tuple4(null, text, .0, textWidth(text)); return Tuple4(null, text, Size.zero, size);
case Operation.equal: case Operation.equal:
default: default:
final width = textWidth(text); return Tuple4(text, text, size, size);
return Tuple4(text, text, width, width);
} }
}).fold<List<_TextDiff>>([], (prev, v) { }).fold<List<_TextDiff>>([], (prev, v) {
if (prev.isNotEmpty) { if (prev.isNotEmpty) {
@ -168,109 +171,6 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
return [...prev, v]; return [...prev, v];
})); }));
} }
// void _computeDiff(String oldText, String newText) {
// final oldCharacters = oldText.characters.toList();
// final newCharacters = newText.characters.toList();
// final diffResult = calculateListDiff<String>(oldCharacters, newCharacters, detectMoves: false);
// final updates = diffResult.getUpdatesWithData().toList();
// List<TextDiff> diffs = [];
// DataDiffUpdate<String>? 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<String>? 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<String?, String?, double, double>; typedef _TextDiff = Tuple4<String?, String?, Size, Size>;

View file

@ -26,7 +26,7 @@ class GridItemSelectionOverlay<T> extends StatelessWidget {
duration: duration, duration: duration,
child: isSelecting child: isSelecting
? Selector<Selection<T>, bool>( ? Selector<Selection<T>, bool>(
selector: (context, selection) => selection.isSelected([item]), selector: (context, selection) => selection.isSelected({item}),
builder: (context, isSelected, child) { builder: (context, isSelected, child) {
return AnimatedContainer( return AnimatedContainer(
alignment: AlignmentDirectional.topEnd, alignment: AlignmentDirectional.topEnd,

View file

@ -51,6 +51,8 @@ class MapButtonPanel extends StatelessWidget {
); );
} }
break; break;
case MapNavigationButton.none:
break;
} }
final showCoordinateFilter = context.select<MapThemeData, bool>((v) => v.showCoordinateFilter); final showCoordinateFilter = context.select<MapThemeData, bool>((v) => v.showCoordinateFilter);

View file

@ -2,6 +2,7 @@ import 'package:aves/model/entry.dart';
import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/common/fx/borders.dart';
import 'package:aves/widgets/common/grid/overlay.dart'; import 'package:aves/widgets/common/grid/overlay.dart';
import 'package:aves/widgets/common/thumbnail/image.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:aves/widgets/common/thumbnail/overlay.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -44,11 +45,15 @@ class DecoratedThumbnail extends StatelessWidget {
children: [ children: [
child, child,
ThumbnailEntryOverlay(entry: entry), ThumbnailEntryOverlay(entry: entry),
if (selectable) if (selectable) ...[
GridItemSelectionOverlay<AvesEntry>( GridItemSelectionOverlay<AvesEntry>(
item: entry, item: entry,
padding: const EdgeInsets.all(2), padding: const EdgeInsets.all(2),
), ),
ThumbnailZoomOverlay(
onZoom: () => OpenViewerNotification(entry).dispatch(context),
),
],
if (highlightable) ThumbnailHighlightOverlay(entry: entry), if (highlightable) ThumbnailHighlightOverlay(entry: entry),
], ],
); );

View file

@ -47,7 +47,10 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
EntryActionDelegate(this.mainEntry, this.pageEntry, this.collection); EntryActionDelegate(this.mainEntry, this.pageEntry, this.collection);
bool isVisible(EntryAction action) { bool isVisible({
required AppMode appMode,
required EntryAction action,
}) {
if (mainEntry.trashed) { if (mainEntry.trashed) {
switch (action) { switch (action) {
case EntryAction.delete: case EntryAction.delete:
@ -60,7 +63,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
} }
} else { } else {
final targetEntry = EntryActions.pageActions.contains(action) ? pageEntry : mainEntry; final targetEntry = EntryActions.pageActions.contains(action) ? pageEntry : mainEntry;
final canWrite = !settings.isReadOnly; final canWrite = appMode.canEditEntry && !settings.isReadOnly;
switch (action) { switch (action) {
case EntryAction.toggleFavourite: case EntryAction.toggleFavourite:
return collection != null; return collection != null;
@ -120,7 +123,11 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
case EntryAction.showGeoTiffOnMap: case EntryAction.showGeoTiffOnMap:
case EntryAction.convertMotionPhotoToStillImage: case EntryAction.convertMotionPhotoToStillImage:
case EntryAction.viewMotionPhotoVideo: case EntryAction.viewMotionPhotoVideo:
return _metadataActionDelegate.isVisible(targetEntry, action); return _metadataActionDelegate.isVisible(
appMode: appMode,
targetEntry: targetEntry,
action: action,
);
case EntryAction.debug: case EntryAction.debug:
return kDebugMode; return kDebugMode;
} }

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/model/actions/events.dart'; import 'package:aves/model/actions/events.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
@ -29,8 +30,12 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi
Stream<ActionEvent<EntryAction>> get eventStream => _eventStreamController.stream; Stream<ActionEvent<EntryAction>> get eventStream => _eventStreamController.stream;
bool isVisible(AvesEntry targetEntry, EntryAction action) { bool isVisible({
final canWrite = !settings.isReadOnly; required AppMode appMode,
required AvesEntry targetEntry,
required EntryAction action,
}) {
final canWrite = appMode.canEditEntry && !settings.isReadOnly;
switch (action) { switch (action) {
// general // general
case EntryAction.editDate: case EntryAction.editDate:
@ -39,16 +44,17 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi
case EntryAction.editRating: case EntryAction.editRating:
case EntryAction.editTags: case EntryAction.editTags:
case EntryAction.removeMetadata: case EntryAction.removeMetadata:
case EntryAction.exportMetadata:
return canWrite; return canWrite;
case EntryAction.exportMetadata:
return true;
// GeoTIFF // GeoTIFF
case EntryAction.showGeoTiffOnMap: case EntryAction.showGeoTiffOnMap:
return targetEntry.isGeotiff; return appMode.canNavigate && targetEntry.isGeotiff;
// motion photo // motion photo
case EntryAction.convertMotionPhotoToStillImage: case EntryAction.convertMotionPhotoToStillImage:
return canWrite && targetEntry.isMotionPhoto; return canWrite && targetEntry.isMotionPhoto;
case EntryAction.viewMotionPhotoVideo: case EntryAction.viewMotionPhotoVideo:
return targetEntry.isMotionPhoto; return appMode.canNavigate && targetEntry.isMotionPhoto;
default: default:
return false; return false;
} }

View file

@ -562,12 +562,19 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
void _onVerticalPageChanged(int page) { void _onVerticalPageChanged(int page) {
_currentVerticalPage.value = page; _currentVerticalPage.value = page;
if (page == transitionPage) { switch (page) {
dismissFeedback(context); case transitionPage:
_popVisual(); dismissFeedback(context);
} else if (page == infoPage) { _popVisual();
// prevent hero when viewer is offscreen break;
_heroInfoNotifier.value = null; 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;
} }
} }

View file

@ -150,11 +150,20 @@ class _BasicSectionState extends State<BasicSection> {
} }
Widget _buildEditButtons(BuildContext context) { Widget _buildEditButtons(BuildContext context) {
final appMode = context.watch<ValueNotifier<AppMode>>().value;
final entry = widget.entry; final entry = widget.entry;
final children = [ final children = [
EntryAction.editRating, EntryAction.editRating,
EntryAction.editTags, 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 return children.isEmpty
? const SizedBox() ? const SizedBox()

View file

@ -1,3 +1,4 @@
import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/settings/settings.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/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart';
class InfoAppBar extends StatelessWidget { class InfoAppBar extends StatelessWidget {
final AvesEntry entry; final AvesEntry entry;
@ -33,8 +35,14 @@ class InfoAppBar extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final commonActions = EntryActions.commonMetadataActions.where((v) => actionDelegate.isVisible(entry, v)); final appMode = context.watch<ValueNotifier<AppMode>>().value;
final formatSpecificActions = EntryActions.formatSpecificMetadataActions.where((v) => actionDelegate.isVisible(entry, v)); 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; final useTvLayout = settings.useTvLayout;
return SliverAppBar( return SliverAppBar(
leading: useTvLayout leading: useTvLayout

View file

@ -1,3 +1,4 @@
import 'package:aves/app_mode.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/location.dart';
import 'package:aves/model/settings/enums/coordinate_format.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/widgets/viewer/info/common.dart';
import 'package:aves_map/aves_map.dart'; import 'package:aves_map/aves_map.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class LocationSection extends StatefulWidget { class LocationSection extends StatefulWidget {
final CollectionLens? collection; final CollectionLens? collection;
@ -72,6 +74,7 @@ class _LocationSectionState extends State<LocationSection> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (!entry.hasGps) return const SizedBox(); if (!entry.hasGps) return const SizedBox();
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -79,7 +82,7 @@ class _LocationSectionState extends State<LocationSection> {
MapTheme( MapTheme(
interactive: false, interactive: false,
showCoordinateFilter: false, showCoordinateFilter: false,
navigationButton: MapNavigationButton.map, navigationButton: canNavigate ? MapNavigationButton.map : MapNavigationButton.none,
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
mapHeight: 200, mapHeight: 200,
child: GeoMap( child: GeoMap(
@ -87,7 +90,7 @@ class _LocationSectionState extends State<LocationSection> {
entries: [entry], entries: [entry],
isAnimatingNotifier: widget.isScrollingNotifier, isAnimatingNotifier: widget.isScrollingNotifier,
onUserZoomChange: (zoom) => settings.infoMapZoom = zoom.roundToDouble(), 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, openMapPage: collection != null ? _openMapPage : null,
), ),
), ),

View file

@ -2,6 +2,7 @@ import 'dart:math';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/selection.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/widgets/common/extensions/media_query.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/multipage/controller.dart';
import 'package:aves/widgets/viewer/notifications.dart'; import 'package:aves/widgets/viewer/notifications.dart';
import 'package:aves/widgets/viewer/overlay/multipage.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/thumbnail_preview.dart';
import 'package:aves/widgets/viewer/overlay/viewer_buttons.dart'; import 'package:aves/widgets/viewer/overlay/viewer_buttons.dart';
import 'package:aves/widgets/viewer/overlay/wallpaper_buttons.dart'; import 'package:aves/widgets/viewer/overlay/wallpaper_buttons.dart';
@ -183,30 +185,36 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> {
]), ]),
builder: (context, child) { builder: (context, child) {
final viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero); final viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero);
final viewerButtonRow = FocusableActionDetector( final selection = context.read<Selection<AvesEntry>?>();
focusNode: _buttonRowFocusScopeNode, final viewerButtonRow = (selection?.isSelecting ?? false)
shortcuts: settings.useTvLayout ? const {SingleActivator(LogicalKeyboardKey.arrowUp): TvShowLessInfoIntent()} : null, ? SelectionButton(
actions: {TvShowLessInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context))}, mainEntry: mainEntry,
child: SafeArea( scale: _buttonScale,
top: false, )
bottom: false, : FocusableActionDetector(
minimum: EdgeInsets.only( focusNode: _buttonRowFocusScopeNode,
left: viewInsetsPadding.left, shortcuts: settings.useTvLayout ? const {SingleActivator(LogicalKeyboardKey.arrowUp): TvShowLessInfoIntent()} : null,
right: viewInsetsPadding.right, actions: {TvShowLessInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context))},
), child: SafeArea(
child: isWallpaperMode top: false,
? WallpaperButtons( bottom: false,
entry: pageEntry, minimum: EdgeInsets.only(
scale: _buttonScale, left: viewInsetsPadding.left,
) right: viewInsetsPadding.right,
: ViewerButtons(
mainEntry: mainEntry,
pageEntry: pageEntry,
collection: widget.collection,
scale: _buttonScale,
), ),
), child: isWallpaperMode
); ? WallpaperButtons(
entry: pageEntry,
scale: _buttonScale,
)
: ViewerButtons(
mainEntry: mainEntry,
pageEntry: pageEntry,
collection: widget.collection,
scale: _buttonScale,
),
),
);
final showMultiPageOverlay = mainEntry.isMultiPage && multiPageController != null; final showMultiPageOverlay = mainEntry.isMultiPage && multiPageController != null;
final collapsedPageScroller = mainEntry.isMotionPhoto; final collapsedPageScroller = mainEntry.isMotionPhoto;

View file

@ -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<double> 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<Selection<AvesEntry>>();
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<Selection<AvesEntry>?, 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<Selection<AvesEntry>, 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),
),
);
},
),
],
);
},
),
),
],
),
),
);
}
}

View file

@ -1,3 +1,4 @@
import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
@ -61,6 +62,12 @@ class ViewerButtons extends StatelessWidget {
); );
} }
final appMode = context.watch<ValueNotifier<AppMode>>().value;
bool isVisible(EntryAction action) => actionDelegate.isVisible(
appMode: appMode,
action: action,
);
final trashed = mainEntry.trashed; final trashed = mainEntry.trashed;
return SafeArea( return SafeArea(
top: false, top: false,
@ -72,9 +79,9 @@ class ViewerButtons extends StatelessWidget {
return Selector<Settings, bool>( return Selector<Settings, bool>(
selector: (context, s) => s.isRotationLocked, selector: (context, s) => s.isRotationLocked,
builder: (context, s, child) { 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<EntryAction> getMenuActions(List<EntryAction> categoryActions) { List<EntryAction> getMenuActions(List<EntryAction> categoryActions) {
return categoryActions.where((action) => !quickActions.contains(action)).where(actionDelegate.isVisible).toList(); return categoryActions.where((action) => !quickActions.contains(action)).where(isVisible).toList();
} }
return ViewerButtonRowContent( return ViewerButtonRowContent(
@ -109,6 +116,7 @@ class _TvButtonRowContent extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final appMode = context.watch<ValueNotifier<AppMode>>().value;
return Selector<VideoConductor, AvesVideoController?>( return Selector<VideoConductor, AvesVideoController?>(
selector: (context, vc) => vc.getController(pageEntry), selector: (context, vc) => vc.getController(pageEntry),
builder: (context, videoController, child) { builder: (context, videoController, child) {
@ -120,7 +128,12 @@ class _TvButtonRowContent extends StatelessWidget {
...EntryActions.export, ...EntryActions.export,
...EntryActions.videoPlayback, ...EntryActions.videoPlayback,
...EntryActions.video, ...EntryActions.video,
].where(actionDelegate.isVisible).map((action) { ]
.where((action) => actionDelegate.isVisible(
appMode: appMode,
action: action,
))
.map((action) {
final enabled = actionDelegate.canApply(action); final enabled = actionDelegate.canApply(action);
return CaptionedButton( return CaptionedButton(
scale: scale, scale: scale,

View file

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
enum MapNavigationButton { back, map } enum MapNavigationButton { back, map, none }
class MapThemeData { class MapThemeData {
final bool interactive, showCoordinateFilter; final bool interactive, showCoordinateFilter;