From 59473dab642eaa764f6f46a4285b05eacb1ea1a2 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 28 Sep 2022 09:40:26 +0200 Subject: [PATCH] #334 collection: selection edit actions available as quick actions --- CHANGELOG.md | 1 + lib/model/actions/entry_set_actions.dart | 14 +++- lib/utils/constants.dart | 5 ++ lib/widgets/collection/app_bar.dart | 2 +- .../tile_extent_controller_provider.dart | 12 ++-- .../common/quick_actions/action_button.dart | 31 ++++++-- .../common/quick_actions/action_panel.dart | 7 +- .../quick_actions/available_actions.dart | 58 +++++++++------ .../common/quick_actions/editor_page.dart | 70 +++++++++++++++---- .../thumbnails/collection_actions_editor.dart | 7 +- .../viewer/viewer_actions_editor.dart | 36 +++++----- pubspec.lock | 7 ++ pubspec.yaml | 1 + 13 files changed, 184 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cc0bdafd..ec2afa58d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. - mosaic layout - reverse filters to filter out/in +- Collection: selection edit actions available as quick actions - Albums: group by content type - Stats: top albums - Stats: open full top listings diff --git a/lib/model/actions/entry_set_actions.dart b/lib/model/actions/entry_set_actions.dart index 0efb48962..92af82d5c 100644 --- a/lib/model/actions/entry_set_actions.dart +++ b/lib/model/actions/entry_set_actions.dart @@ -83,7 +83,7 @@ class EntrySetActions { ]; // exclude bin related actions - static const collectionEditorSelection = [ + static const collectionEditorSelectionRegular = [ EntrySetAction.share, EntrySetAction.delete, EntrySetAction.copy, @@ -97,6 +97,18 @@ class EntrySetActions { // 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 = [ EntrySetAction.editDate, EntrySetAction.editLocation, diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 13b362449..3d11b7beb 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -312,6 +312,11 @@ class Constants { license: 'MIT', 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 dartPackages = [ diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index 05a3bbd15..9d8658651 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -318,7 +318,7 @@ class _CollectionAppBarState extends State with SingleTickerPr title: context.l10n.collectionActionEdit, items: [ _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)), ], ), ), diff --git a/lib/widgets/common/providers/tile_extent_controller_provider.dart b/lib/widgets/common/providers/tile_extent_controller_provider.dart index c2e53f309..0a5d45b99 100644 --- a/lib/widgets/common/providers/tile_extent_controller_provider.dart +++ b/lib/widgets/common/providers/tile_extent_controller_provider.dart @@ -15,14 +15,10 @@ class TileExtentControllerProvider extends StatelessWidget { @override Widget build(BuildContext context) { return LayoutBuilder( - builder: (context, constraints) { - return LayoutBuilder( - builder: (context, constraints) => ProxyProvider0( - update: (context, __) => controller..setViewportSize(constraints.biggest), - child: child, - ), - ); - }, + builder: (context, constraints) => ProxyProvider0( + update: (context, __) => controller..setViewportSize(constraints.biggest), + child: child, + ), ); } } diff --git a/lib/widgets/settings/common/quick_actions/action_button.dart b/lib/widgets/settings/common/quick_actions/action_button.dart index 9e43a23d6..2d6731f20 100644 --- a/lib/widgets/settings/common/quick_actions/action_button.dart +++ b/lib/widgets/settings/common/quick_actions/action_button.dart @@ -1,5 +1,6 @@ import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; class ActionButton extends StatelessWidget { final String text; @@ -14,13 +15,14 @@ class ActionButton extends StatelessWidget { this.showCaption = true, }); - static const padding = 8.0; + static const int maxLines = 2; + static const double padding = 8; @override Widget build(BuildContext context) { - final textStyle = Theme.of(context).textTheme.caption; + final textStyle = _textStyle(context); return SizedBox( - width: OverlayButton.getSize(context) + padding * 2, + width: _width(context), child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -35,10 +37,10 @@ class ActionButton extends StatelessWidget { const SizedBox(height: padding), 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, overflow: TextOverflow.ellipsis, - maxLines: 2, + maxLines: maxLines, ), ], 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); + } } diff --git a/lib/widgets/settings/common/quick_actions/action_panel.dart b/lib/widgets/settings/common/quick_actions/action_panel.dart index d2b9cd621..2b099bbbd 100644 --- a/lib/widgets/settings/common/quick_actions/action_panel.dart +++ b/lib/widgets/settings/common/quick_actions/action_panel.dart @@ -13,7 +13,12 @@ class ActionPanel extends StatelessWidget { @override 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( foregroundDecoration: BoxDecoration( color: color.withOpacity(.2), diff --git a/lib/widgets/settings/common/quick_actions/available_actions.dart b/lib/widgets/settings/common/quick_actions/available_actions.dart index 307ba6fb3..9d5112d99 100644 --- a/lib/widgets/settings/common/quick_actions/available_actions.dart +++ b/lib/widgets/settings/common/quick_actions/available_actions.dart @@ -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/placeholder.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; @@ -15,6 +16,9 @@ class AvailableActionPanel extends StatelessWidget { final Widget? Function(T action) actionIcon; final String Function(BuildContext context, T action) actionText; + static const double spacing = 8; + static const padding = EdgeInsets.all(spacing); + const AvailableActionPanel({ super.key, required this.allActions, @@ -46,26 +50,28 @@ class AvailableActionPanel extends StatelessWidget { builder: (context, accepted, rejected) { return AnimatedBuilder( animation: Listenable.merge([quickActionsChangeNotifier, draggedAvailableAction]), - builder: (context, child) => Padding( - padding: const EdgeInsets.all(8), - child: Wrap( - alignment: WrapAlignment.spaceEvenly, - spacing: 8, - runSpacing: 8, - children: allActions.map((action) { - final dragged = action == draggedAvailableAction.value; - final enabled = dragged || !quickActions.contains(action); - var child = _buildActionButton(context, action, enabled: enabled); - if (dragged) { - child = DraggedPlaceholder(child: child); - } - if (enabled) { - child = _buildDraggable(context, action, child); - } - return child; - }).toList(), - ), - ), + builder: (context, child) { + return Padding( + padding: padding, + child: Wrap( + alignment: WrapAlignment.spaceEvenly, + spacing: spacing, + runSpacing: spacing, + children: allActions.map((action) { + final dragged = action == draggedAvailableAction.value; + final enabled = dragged || !quickActions.contains(action); + var child = _buildActionButton(context, action, enabled: enabled); + if (dragged) { + child = DraggedPlaceholder(child: child); + } + if (enabled) { + child = _buildDraggable(context, action, child); + } + return child; + }).toList(), + ), + ); + }, ); }, ); @@ -113,4 +119,16 @@ class AvailableActionPanel extends StatelessWidget { void _setDraggedAvailableAction(T? action) => draggedAvailableAction.value = action; void _setPanelHighlight(bool flag) => panelHighlight.value = flag; + + static double heightFor(BuildContext context, List 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; + } } diff --git a/lib/widgets/settings/common/quick_actions/editor_page.dart b/lib/widgets/settings/common/quick_actions/editor_page.dart index 367bb1408..c24cfcfdc 100644 --- a/lib/widgets/settings/common/quick_actions/editor_page.dart +++ b/lib/widgets/settings/common/quick_actions/editor_page.dart @@ -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/quick_actions.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:smooth_page_indicator/smooth_page_indicator.dart'; class QuickActionEditorPage extends StatelessWidget { final String title, bannerText; - final List allAvailableActions; + final List> allAvailableActions; final Widget? Function(T action) actionIcon; final String Function(BuildContext context, T action) actionText; final List Function() load; @@ -58,7 +60,7 @@ class QuickActionEditorPage extends StatelessWidget { class QuickActionEditorBody extends StatefulWidget { final String bannerText; - final List allAvailableActions; + final List> allAvailableActions; final Widget? Function(T action) actionIcon; final String Function(BuildContext context, T action) actionText; final List Function() load; @@ -87,6 +89,7 @@ class _QuickActionEditorBodyState extends State _quickActionHighlight = ValueNotifier(false); final ValueNotifier _availableActionHighlight = ValueNotifier(false); final AChangeNotifier _quickActionsChangeNotifier = AChangeNotifier(); + final PageController _availableActionPageController = PageController(); // 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 @@ -119,6 +122,8 @@ class _QuickActionEditorBodyState extends State( placement: QuickActionPlacement.header, panelHighlight: _quickActionHighlight, @@ -222,7 +227,7 @@ class _QuickActionEditorBodyState extends State extends State( - allActions: widget.allAvailableActions, - quickActions: _quickActions, - quickActionsChangeNotifier: _quickActionsChangeNotifier, - panelHighlight: _availableActionHighlight, - draggedQuickAction: _draggedQuickAction, - draggedAvailableAction: _draggedAvailableAction, - removeQuickAction: _removeQuickAction, - actionIcon: widget.actionIcon, - actionText: widget.actionText, + child: LayoutBuilder( + builder: (context, constraints) { + final allAvailableActions = widget.allAvailableActions; + final maxWidth = constraints.maxWidth; + final maxHeight = allAvailableActions + .map((page) => AvailableActionPanel.heightFor( + context, + page.map((v) => widget.actionText(context, v)).toList(), + maxWidth, + )) + .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( + allActions: allActions, + quickActions: _quickActions, + quickActionsChangeNotifier: _quickActionsChangeNotifier, + panelHighlight: _availableActionHighlight, + draggedQuickAction: _draggedQuickAction, + draggedAvailableAction: _draggedAvailableAction, + removeQuickAction: _removeQuickAction, + actionIcon: widget.actionIcon, + actionText: widget.actionText, + )) + .toList(), + ), + ), + ], + ); + }, ), ), ], diff --git a/lib/widgets/settings/thumbnails/collection_actions_editor.dart b/lib/widgets/settings/thumbnails/collection_actions_editor.dart index 5bacb4ce5..38303fe73 100644 --- a/lib/widgets/settings/thumbnails/collection_actions_editor.dart +++ b/lib/widgets/settings/thumbnails/collection_actions_editor.dart @@ -19,7 +19,7 @@ class CollectionActionEditorPage extends StatelessWidget { Tab(text: l10n.settingsCollectionQuickActionTabBrowsing), QuickActionEditorBody( bannerText: context.l10n.settingsCollectionBrowsingQuickActionEditorBanner, - allAvailableActions: EntrySetActions.collectionEditorBrowsing, + allAvailableActions: const [EntrySetActions.collectionEditorBrowsing], actionIcon: (action) => action.getIcon(), actionText: (context, action) => action.getText(context), load: () => settings.collectionBrowsingQuickActions, @@ -30,7 +30,10 @@ class CollectionActionEditorPage extends StatelessWidget { Tab(text: l10n.settingsCollectionQuickActionTabSelecting), QuickActionEditorBody( bannerText: context.l10n.settingsCollectionSelectionQuickActionEditorBanner, - allAvailableActions: EntrySetActions.collectionEditorSelection, + allAvailableActions: const [ + EntrySetActions.collectionEditorSelectionRegular, + EntrySetActions.collectionEditorSelectionEdit, + ], actionIcon: (action) => action.getIcon(), actionText: (context, action) => action.getText(context), load: () => settings.collectionSelectionQuickActions, diff --git a/lib/widgets/settings/viewer/viewer_actions_editor.dart b/lib/widgets/settings/viewer/viewer_actions_editor.dart index b70b10344..6c18e529d 100644 --- a/lib/widgets/settings/viewer/viewer_actions_editor.dart +++ b/lib/widgets/settings/viewer/viewer_actions_editor.dart @@ -10,22 +10,26 @@ class ViewerActionEditorPage extends StatelessWidget { const ViewerActionEditorPage({super.key}); static const allAvailableActions = [ - EntryAction.share, - EntryAction.edit, - EntryAction.rename, - EntryAction.delete, - EntryAction.copy, - EntryAction.move, - EntryAction.toggleFavourite, - EntryAction.rotateScreen, - EntryAction.videoCaptureFrame, - EntryAction.videoToggleMute, - EntryAction.videoSetSpeed, - EntryAction.videoSelectStreams, - EntryAction.viewSource, - EntryAction.rotateCCW, - EntryAction.rotateCW, - EntryAction.flip, + [ + EntryAction.share, + EntryAction.edit, + EntryAction.rename, + EntryAction.delete, + EntryAction.copy, + EntryAction.move, + EntryAction.toggleFavourite, + EntryAction.rotateScreen, + EntryAction.viewSource, + EntryAction.rotateCCW, + EntryAction.rotateCW, + EntryAction.flip, + ], + [ + EntryAction.videoCaptureFrame, + EntryAction.videoToggleMute, + EntryAction.videoSetSpeed, + EntryAction.videoSelectStreams, + ], ]; @override diff --git a/pubspec.lock b/pubspec.lock index ccfae1781..7bcd93fcd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1045,6 +1045,13 @@ packages: description: flutter source: sdk 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: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0864cf230..9826dac70 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -75,6 +75,7 @@ dependencies: provider: screen_brightness: shared_preferences: + smooth_page_indicator: sqflite: streams_channel: git: