#197 preview button when selecting items
This commit is contained in:
parent
09cf4fef3e
commit
433ac537dd
17 changed files with 225 additions and 165 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -254,8 +254,11 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
|
|||
);
|
||||
|
||||
if (selection.isSelecting) {
|
||||
child = ChangeNotifierProvider<Selection<AvesEntry>>.value(
|
||||
value: selection,
|
||||
child = MultiProvider(
|
||||
providers: [
|
||||
ListenableProvider<ValueNotifier<AppMode>>.value(value: ValueNotifier(AppMode.pickMediaInternal)),
|
||||
ChangeNotifierProvider<Selection<AvesEntry>>.value(value: selection),
|
||||
],
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -73,8 +73,8 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> 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<AnimatedDiffText> 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<AnimatedDiffText> 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<AnimatedDiffText> 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<List<_TextDiff>>([], (prev, v) {
|
||||
if (prev.isNotEmpty) {
|
||||
|
@ -168,109 +171,6 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
|
|||
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>;
|
||||
|
|
|
@ -26,7 +26,7 @@ class GridItemSelectionOverlay<T> extends StatelessWidget {
|
|||
duration: duration,
|
||||
child: isSelecting
|
||||
? Selector<Selection<T>, bool>(
|
||||
selector: (context, selection) => selection.isSelected([item]),
|
||||
selector: (context, selection) => selection.isSelected({item}),
|
||||
builder: (context, isSelected, child) {
|
||||
return AnimatedContainer(
|
||||
alignment: AlignmentDirectional.topEnd,
|
||||
|
|
|
@ -51,6 +51,8 @@ class MapButtonPanel extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
break;
|
||||
case MapNavigationButton.none:
|
||||
break;
|
||||
}
|
||||
|
||||
final showCoordinateFilter = context.select<MapThemeData, bool>((v) => v.showCoordinateFilter);
|
||||
|
|
|
@ -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<AvesEntry>(
|
||||
item: entry,
|
||||
padding: const EdgeInsets.all(2),
|
||||
),
|
||||
ThumbnailZoomOverlay(
|
||||
onZoom: () => OpenViewerNotification(entry).dispatch(context),
|
||||
),
|
||||
],
|
||||
if (highlightable) ThumbnailHighlightOverlay(entry: entry),
|
||||
],
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<ActionEvent<EntryAction>> 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;
|
||||
}
|
||||
|
|
|
@ -562,12 +562,19 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
|
||||
void _onVerticalPageChanged(int page) {
|
||||
_currentVerticalPage.value = page;
|
||||
if (page == transitionPage) {
|
||||
switch (page) {
|
||||
case transitionPage:
|
||||
dismissFeedback(context);
|
||||
_popVisual();
|
||||
} else if (page == infoPage) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -150,11 +150,20 @@ class _BasicSectionState extends State<BasicSection> {
|
|||
}
|
||||
|
||||
Widget _buildEditButtons(BuildContext context) {
|
||||
final appMode = context.watch<ValueNotifier<AppMode>>().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()
|
||||
|
|
|
@ -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<ValueNotifier<AppMode>>().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
|
||||
|
|
|
@ -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<LocationSection> {
|
|||
Widget build(BuildContext context) {
|
||||
if (!entry.hasGps) return const SizedBox();
|
||||
|
||||
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
@ -79,7 +82,7 @@ class _LocationSectionState extends State<LocationSection> {
|
|||
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<LocationSection> {
|
|||
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,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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,7 +185,13 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> {
|
|||
]),
|
||||
builder: (context, child) {
|
||||
final viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero);
|
||||
final viewerButtonRow = FocusableActionDetector(
|
||||
final selection = context.read<Selection<AvesEntry>?>();
|
||||
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<Intent>(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context))},
|
||||
|
|
83
lib/widgets/viewer/overlay/selection_button.dart
Normal file
83
lib/widgets/viewer/overlay/selection_button.dart
Normal 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),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<ValueNotifier<AppMode>>().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<Settings, bool>(
|
||||
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<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(
|
||||
|
@ -109,6 +116,7 @@ class _TvButtonRowContent extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
||||
return Selector<VideoConductor, AvesVideoController?>(
|
||||
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,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
enum MapNavigationButton { back, map }
|
||||
enum MapNavigationButton { back, map, none }
|
||||
|
||||
class MapThemeData {
|
||||
final bool interactive, showCoordinateFilter;
|
||||
|
|
Loading…
Reference in a new issue