#334 collection: selection edit actions available as quick actions

This commit is contained in:
Thibault Deckers 2022-09-28 09:40:26 +02:00
parent 9ba9ec302e
commit 59473dab64
13 changed files with 184 additions and 67 deletions

View file

@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file.
- mosaic layout - mosaic layout
- reverse filters to filter out/in - reverse filters to filter out/in
- Collection: selection edit actions available as quick actions
- Albums: group by content type - Albums: group by content type
- Stats: top albums - Stats: top albums
- Stats: open full top listings - Stats: open full top listings

View file

@ -83,7 +83,7 @@ class EntrySetActions {
]; ];
// exclude bin related actions // exclude bin related actions
static const collectionEditorSelection = [ static const collectionEditorSelectionRegular = [
EntrySetAction.share, EntrySetAction.share,
EntrySetAction.delete, EntrySetAction.delete,
EntrySetAction.copy, EntrySetAction.copy,
@ -97,6 +97,18 @@ class EntrySetActions {
// editing actions are in their subsection // editing actions are in their subsection
]; ];
static const collectionEditorSelectionEdit = [
EntrySetAction.rotateCCW,
EntrySetAction.rotateCW,
EntrySetAction.flip,
EntrySetAction.editDate,
EntrySetAction.editLocation,
EntrySetAction.editTitleDescription,
EntrySetAction.editRating,
EntrySetAction.editTags,
EntrySetAction.removeMetadata,
];
static const edit = [ static const edit = [
EntrySetAction.editDate, EntrySetAction.editDate,
EntrySetAction.editLocation, EntrySetAction.editLocation,

View file

@ -312,6 +312,11 @@ class Constants {
license: 'MIT', license: 'MIT',
sourceUrl: 'https://github.com/rrousselGit/provider', sourceUrl: 'https://github.com/rrousselGit/provider',
), ),
Dependency(
name: 'Smooth Page Indicator',
license: 'MIT',
sourceUrl: 'https://github.com/Milad-Akarie/smooth_page_indicator',
),
]; ];
static const List<Dependency> dartPackages = [ static const List<Dependency> dartPackages = [

View file

@ -318,7 +318,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
title: context.l10n.collectionActionEdit, title: context.l10n.collectionActionEdit,
items: [ items: [
_buildRotateAndFlipMenuItems(context, canApply: canApply), _buildRotateAndFlipMenuItems(context, canApply: canApply),
...EntrySetActions.edit.where(isVisible).map((action) => _toMenuItem(action, enabled: canApply(action), selection: selection)), ...EntrySetActions.edit.where((v) => isVisible(v) && !selectionQuickActions.contains(v)).map((action) => _toMenuItem(action, enabled: canApply(action), selection: selection)),
], ],
), ),
), ),

View file

@ -15,14 +15,10 @@ class TileExtentControllerProvider extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) => ProxyProvider0<TileExtentController>(
return LayoutBuilder( update: (context, __) => controller..setViewportSize(constraints.biggest),
builder: (context, constraints) => ProxyProvider0<TileExtentController>( child: child,
update: (context, __) => controller..setViewportSize(constraints.biggest), ),
child: child,
),
);
},
); );
} }
} }

View file

@ -1,5 +1,6 @@
import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:aves/widgets/viewer/overlay/common.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class ActionButton extends StatelessWidget { class ActionButton extends StatelessWidget {
final String text; final String text;
@ -14,13 +15,14 @@ class ActionButton extends StatelessWidget {
this.showCaption = true, this.showCaption = true,
}); });
static const padding = 8.0; static const int maxLines = 2;
static const double padding = 8;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final textStyle = Theme.of(context).textTheme.caption; final textStyle = _textStyle(context);
return SizedBox( return SizedBox(
width: OverlayButton.getSize(context) + padding * 2, width: _width(context),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -35,10 +37,10 @@ class ActionButton extends StatelessWidget {
const SizedBox(height: padding), const SizedBox(height: padding),
Text( Text(
text, text,
style: enabled ? textStyle : textStyle!.copyWith(color: textStyle.color!.withOpacity(.2)), style: enabled ? textStyle : textStyle.copyWith(color: textStyle.color!.withOpacity(.2)),
textAlign: TextAlign.center, textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 2, maxLines: maxLines,
), ),
], ],
const SizedBox(height: padding), const SizedBox(height: padding),
@ -46,4 +48,23 @@ class ActionButton extends StatelessWidget {
), ),
); );
} }
static TextStyle _textStyle(BuildContext context) => Theme.of(context).textTheme.caption!;
static double _width(BuildContext context) => OverlayButton.getSize(context) + padding * 2;
static Size getSize(BuildContext context, String text, {required bool showCaption}) {
final width = _width(context);
var height = width;
if (showCaption) {
final para = RenderParagraph(
TextSpan(text: text, style: _textStyle(context)),
textDirection: TextDirection.ltr,
textScaleFactor: MediaQuery.textScaleFactorOf(context),
maxLines: maxLines,
)..layout(const BoxConstraints(), parentUsesSize: true);
height += para.getMaxIntrinsicHeight(width) + padding;
}
return Size(width, height);
}
} }

View file

@ -13,7 +13,12 @@ class ActionPanel extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final color = highlight ? Theme.of(context).colorScheme.secondary : Colors.blueGrey; final theme = Theme.of(context);
final color = highlight
? theme.colorScheme.secondary
: theme.brightness == Brightness.dark
? Colors.blueGrey
: Colors.blueGrey.shade100;
return AnimatedContainer( return AnimatedContainer(
foregroundDecoration: BoxDecoration( foregroundDecoration: BoxDecoration(
color: color.withOpacity(.2), color: color.withOpacity(.2),

View file

@ -2,6 +2,7 @@ import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/settings/common/quick_actions/action_button.dart'; import 'package:aves/widgets/settings/common/quick_actions/action_button.dart';
import 'package:aves/widgets/settings/common/quick_actions/placeholder.dart'; import 'package:aves/widgets/settings/common/quick_actions/placeholder.dart';
import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:aves/widgets/viewer/overlay/common.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -15,6 +16,9 @@ class AvailableActionPanel<T extends Object> extends StatelessWidget {
final Widget? Function(T action) actionIcon; final Widget? Function(T action) actionIcon;
final String Function(BuildContext context, T action) actionText; final String Function(BuildContext context, T action) actionText;
static const double spacing = 8;
static const padding = EdgeInsets.all(spacing);
const AvailableActionPanel({ const AvailableActionPanel({
super.key, super.key,
required this.allActions, required this.allActions,
@ -46,26 +50,28 @@ class AvailableActionPanel<T extends Object> extends StatelessWidget {
builder: (context, accepted, rejected) { builder: (context, accepted, rejected) {
return AnimatedBuilder( return AnimatedBuilder(
animation: Listenable.merge([quickActionsChangeNotifier, draggedAvailableAction]), animation: Listenable.merge([quickActionsChangeNotifier, draggedAvailableAction]),
builder: (context, child) => Padding( builder: (context, child) {
padding: const EdgeInsets.all(8), return Padding(
child: Wrap( padding: padding,
alignment: WrapAlignment.spaceEvenly, child: Wrap(
spacing: 8, alignment: WrapAlignment.spaceEvenly,
runSpacing: 8, spacing: spacing,
children: allActions.map((action) { runSpacing: spacing,
final dragged = action == draggedAvailableAction.value; children: allActions.map((action) {
final enabled = dragged || !quickActions.contains(action); final dragged = action == draggedAvailableAction.value;
var child = _buildActionButton(context, action, enabled: enabled); final enabled = dragged || !quickActions.contains(action);
if (dragged) { var child = _buildActionButton(context, action, enabled: enabled);
child = DraggedPlaceholder(child: child); if (dragged) {
} child = DraggedPlaceholder(child: child);
if (enabled) { }
child = _buildDraggable(context, action, child); if (enabled) {
} child = _buildDraggable(context, action, child);
return child; }
}).toList(), return child;
), }).toList(),
), ),
);
},
); );
}, },
); );
@ -113,4 +119,16 @@ class AvailableActionPanel<T extends Object> extends StatelessWidget {
void _setDraggedAvailableAction(T? action) => draggedAvailableAction.value = action; void _setDraggedAvailableAction(T? action) => draggedAvailableAction.value = action;
void _setPanelHighlight(bool flag) => panelHighlight.value = flag; void _setPanelHighlight(bool flag) => panelHighlight.value = flag;
static double heightFor(BuildContext context, List<String> captions, double width) {
final buttonSizes = captions.map((v) => ActionButton.getSize(context, v, showCaption: true));
final actionsPerRun = (width - padding.horizontal + spacing) ~/ (buttonSizes.first.width + spacing);
final runCount = (captions.length / actionsPerRun).ceil();
var height = .0;
for (var i = 0; i < runCount; i++) {
height += buttonSizes.skip(i * actionsPerRun).take(actionsPerRun).map((v) => v.height).max;
}
height += spacing * (runCount - 1) + padding.vertical;
return height;
}
} }

View file

@ -12,12 +12,14 @@ import 'package:aves/widgets/settings/common/quick_actions/available_actions.dar
import 'package:aves/widgets/settings/common/quick_actions/placeholder.dart'; import 'package:aves/widgets/settings/common/quick_actions/placeholder.dart';
import 'package:aves/widgets/settings/common/quick_actions/quick_actions.dart'; import 'package:aves/widgets/settings/common/quick_actions/quick_actions.dart';
import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:aves/widgets/viewer/overlay/common.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:smooth_page_indicator/smooth_page_indicator.dart';
class QuickActionEditorPage<T extends Object> extends StatelessWidget { class QuickActionEditorPage<T extends Object> extends StatelessWidget {
final String title, bannerText; final String title, bannerText;
final List<T> allAvailableActions; final List<List<T>> allAvailableActions;
final Widget? Function(T action) actionIcon; final Widget? Function(T action) actionIcon;
final String Function(BuildContext context, T action) actionText; final String Function(BuildContext context, T action) actionText;
final List<T> Function() load; final List<T> Function() load;
@ -58,7 +60,7 @@ class QuickActionEditorPage<T extends Object> extends StatelessWidget {
class QuickActionEditorBody<T extends Object> extends StatefulWidget { class QuickActionEditorBody<T extends Object> extends StatefulWidget {
final String bannerText; final String bannerText;
final List<T> allAvailableActions; final List<List<T>> allAvailableActions;
final Widget? Function(T action) actionIcon; final Widget? Function(T action) actionIcon;
final String Function(BuildContext context, T action) actionText; final String Function(BuildContext context, T action) actionText;
final List<T> Function() load; final List<T> Function() load;
@ -87,6 +89,7 @@ class _QuickActionEditorBodyState<T extends Object> extends State<QuickActionEdi
final ValueNotifier<bool> _quickActionHighlight = ValueNotifier(false); final ValueNotifier<bool> _quickActionHighlight = ValueNotifier(false);
final ValueNotifier<bool> _availableActionHighlight = ValueNotifier(false); final ValueNotifier<bool> _availableActionHighlight = ValueNotifier(false);
final AChangeNotifier _quickActionsChangeNotifier = AChangeNotifier(); final AChangeNotifier _quickActionsChangeNotifier = AChangeNotifier();
final PageController _availableActionPageController = PageController();
// use a flag to prevent quick action target accept/leave when already animating reorder // use a flag to prevent quick action target accept/leave when already animating reorder
// as dragging a button against axis direction messes index resolution while items pop in and out // as dragging a button against axis direction messes index resolution while items pop in and out
@ -119,6 +122,8 @@ class _QuickActionEditorBodyState<T extends Object> extends State<QuickActionEdi
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final header = QuickActionButton<T>( final header = QuickActionButton<T>(
placement: QuickActionPlacement.header, placement: QuickActionPlacement.header,
panelHighlight: _quickActionHighlight, panelHighlight: _quickActionHighlight,
@ -222,7 +227,7 @@ class _QuickActionEditorBodyState<T extends Object> extends State<QuickActionEdi
? Center( ? Center(
child: Text( child: Text(
context.l10n.settingsViewerQuickActionEmpty, context.l10n.settingsViewerQuickActionEmpty,
style: Theme.of(context).textTheme.caption, style: theme.textTheme.caption,
), ),
) )
: const SizedBox(), : const SizedBox(),
@ -244,16 +249,55 @@ class _QuickActionEditorBodyState<T extends Object> extends State<QuickActionEdi
highlight: highlight, highlight: highlight,
child: child!, child: child!,
), ),
child: AvailableActionPanel<T>( child: LayoutBuilder(
allActions: widget.allAvailableActions, builder: (context, constraints) {
quickActions: _quickActions, final allAvailableActions = widget.allAvailableActions;
quickActionsChangeNotifier: _quickActionsChangeNotifier, final maxWidth = constraints.maxWidth;
panelHighlight: _availableActionHighlight, final maxHeight = allAvailableActions
draggedQuickAction: _draggedQuickAction, .map((page) => AvailableActionPanel.heightFor(
draggedAvailableAction: _draggedAvailableAction, context,
removeQuickAction: _removeQuickAction, page.map((v) => widget.actionText(context, v)).toList(),
actionIcon: widget.actionIcon, maxWidth,
actionText: widget.actionText, ))
.max;
return Column(
children: [
if (allAvailableActions.length > 1)
Padding(
padding: const EdgeInsets.only(top: 8),
child: SmoothPageIndicator(
controller: _availableActionPageController,
count: allAvailableActions.length,
effect: WormEffect(
dotWidth: 8,
dotHeight: 8,
dotColor: colorScheme.onPrimary.withOpacity(.3),
activeDotColor: colorScheme.secondary,
),
),
),
SizedBox(
height: maxHeight,
child: PageView(
controller: _availableActionPageController,
children: allAvailableActions
.map((allActions) => AvailableActionPanel<T>(
allActions: allActions,
quickActions: _quickActions,
quickActionsChangeNotifier: _quickActionsChangeNotifier,
panelHighlight: _availableActionHighlight,
draggedQuickAction: _draggedQuickAction,
draggedAvailableAction: _draggedAvailableAction,
removeQuickAction: _removeQuickAction,
actionIcon: widget.actionIcon,
actionText: widget.actionText,
))
.toList(),
),
),
],
);
},
), ),
), ),
], ],

View file

@ -19,7 +19,7 @@ class CollectionActionEditorPage extends StatelessWidget {
Tab(text: l10n.settingsCollectionQuickActionTabBrowsing), Tab(text: l10n.settingsCollectionQuickActionTabBrowsing),
QuickActionEditorBody<EntrySetAction>( QuickActionEditorBody<EntrySetAction>(
bannerText: context.l10n.settingsCollectionBrowsingQuickActionEditorBanner, bannerText: context.l10n.settingsCollectionBrowsingQuickActionEditorBanner,
allAvailableActions: EntrySetActions.collectionEditorBrowsing, allAvailableActions: const [EntrySetActions.collectionEditorBrowsing],
actionIcon: (action) => action.getIcon(), actionIcon: (action) => action.getIcon(),
actionText: (context, action) => action.getText(context), actionText: (context, action) => action.getText(context),
load: () => settings.collectionBrowsingQuickActions, load: () => settings.collectionBrowsingQuickActions,
@ -30,7 +30,10 @@ class CollectionActionEditorPage extends StatelessWidget {
Tab(text: l10n.settingsCollectionQuickActionTabSelecting), Tab(text: l10n.settingsCollectionQuickActionTabSelecting),
QuickActionEditorBody<EntrySetAction>( QuickActionEditorBody<EntrySetAction>(
bannerText: context.l10n.settingsCollectionSelectionQuickActionEditorBanner, bannerText: context.l10n.settingsCollectionSelectionQuickActionEditorBanner,
allAvailableActions: EntrySetActions.collectionEditorSelection, allAvailableActions: const [
EntrySetActions.collectionEditorSelectionRegular,
EntrySetActions.collectionEditorSelectionEdit,
],
actionIcon: (action) => action.getIcon(), actionIcon: (action) => action.getIcon(),
actionText: (context, action) => action.getText(context), actionText: (context, action) => action.getText(context),
load: () => settings.collectionSelectionQuickActions, load: () => settings.collectionSelectionQuickActions,

View file

@ -10,22 +10,26 @@ class ViewerActionEditorPage extends StatelessWidget {
const ViewerActionEditorPage({super.key}); const ViewerActionEditorPage({super.key});
static const allAvailableActions = [ static const allAvailableActions = [
EntryAction.share, [
EntryAction.edit, EntryAction.share,
EntryAction.rename, EntryAction.edit,
EntryAction.delete, EntryAction.rename,
EntryAction.copy, EntryAction.delete,
EntryAction.move, EntryAction.copy,
EntryAction.toggleFavourite, EntryAction.move,
EntryAction.rotateScreen, EntryAction.toggleFavourite,
EntryAction.videoCaptureFrame, EntryAction.rotateScreen,
EntryAction.videoToggleMute, EntryAction.viewSource,
EntryAction.videoSetSpeed, EntryAction.rotateCCW,
EntryAction.videoSelectStreams, EntryAction.rotateCW,
EntryAction.viewSource, EntryAction.flip,
EntryAction.rotateCCW, ],
EntryAction.rotateCW, [
EntryAction.flip, EntryAction.videoCaptureFrame,
EntryAction.videoToggleMute,
EntryAction.videoSetSpeed,
EntryAction.videoSelectStreams,
],
]; ];
@override @override

View file

@ -1045,6 +1045,13 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.99" version: "0.0.99"
smooth_page_indicator:
dependency: "direct main"
description:
name: smooth_page_indicator
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0+2"
source_map_stack_trace: source_map_stack_trace:
dependency: transitive dependency: transitive
description: description:

View file

@ -75,6 +75,7 @@ dependencies:
provider: provider:
screen_brightness: screen_brightness:
shared_preferences: shared_preferences:
smooth_page_indicator:
sqflite: sqflite:
streams_channel: streams_channel:
git: git: