#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
- 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

View file

@ -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,

View file

@ -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<Dependency> dartPackages = [

View file

@ -318,7 +318,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> 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)),
],
),
),

View file

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

View file

@ -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);
}
}

View file

@ -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),

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/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<T extends Object> 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,12 +50,13 @@ class AvailableActionPanel<T extends Object> extends StatelessWidget {
builder: (context, accepted, rejected) {
return AnimatedBuilder(
animation: Listenable.merge([quickActionsChangeNotifier, draggedAvailableAction]),
builder: (context, child) => Padding(
padding: const EdgeInsets.all(8),
builder: (context, child) {
return Padding(
padding: padding,
child: Wrap(
alignment: WrapAlignment.spaceEvenly,
spacing: 8,
runSpacing: 8,
spacing: spacing,
runSpacing: spacing,
children: allActions.map((action) {
final dragged = action == draggedAvailableAction.value;
final enabled = dragged || !quickActions.contains(action);
@ -65,7 +70,8 @@ class AvailableActionPanel<T extends Object> extends StatelessWidget {
return child;
}).toList(),
),
),
);
},
);
},
);
@ -113,4 +119,16 @@ class AvailableActionPanel<T extends Object> extends StatelessWidget {
void _setDraggedAvailableAction(T? action) => draggedAvailableAction.value = action;
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/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<T extends Object> extends StatelessWidget {
final String title, bannerText;
final List<T> allAvailableActions;
final List<List<T>> allAvailableActions;
final Widget? Function(T action) actionIcon;
final String Function(BuildContext context, T action) actionText;
final List<T> Function() load;
@ -58,7 +60,7 @@ class QuickActionEditorPage<T extends Object> extends StatelessWidget {
class QuickActionEditorBody<T extends Object> extends StatefulWidget {
final String bannerText;
final List<T> allAvailableActions;
final List<List<T>> allAvailableActions;
final Widget? Function(T action) actionIcon;
final String Function(BuildContext context, T action) actionText;
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> _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<T extends Object> extends State<QuickActionEdi
Widget build(BuildContext context) {
super.build(context);
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final header = QuickActionButton<T>(
placement: QuickActionPlacement.header,
panelHighlight: _quickActionHighlight,
@ -222,7 +227,7 @@ class _QuickActionEditorBodyState<T extends Object> extends State<QuickActionEdi
? Center(
child: Text(
context.l10n.settingsViewerQuickActionEmpty,
style: Theme.of(context).textTheme.caption,
style: theme.textTheme.caption,
),
)
: const SizedBox(),
@ -244,8 +249,40 @@ class _QuickActionEditorBodyState<T extends Object> extends State<QuickActionEdi
highlight: highlight,
child: child!,
),
child: AvailableActionPanel<T>(
allActions: widget.allAvailableActions,
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<T>(
allActions: allActions,
quickActions: _quickActions,
quickActionsChangeNotifier: _quickActionsChangeNotifier,
panelHighlight: _availableActionHighlight,
@ -254,6 +291,13 @@ class _QuickActionEditorBodyState<T extends Object> extends State<QuickActionEdi
removeQuickAction: _removeQuickAction,
actionIcon: widget.actionIcon,
actionText: widget.actionText,
))
.toList(),
),
),
],
);
},
),
),
],

View file

@ -19,7 +19,7 @@ class CollectionActionEditorPage extends StatelessWidget {
Tab(text: l10n.settingsCollectionQuickActionTabBrowsing),
QuickActionEditorBody<EntrySetAction>(
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<EntrySetAction>(
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,

View file

@ -10,6 +10,7 @@ class ViewerActionEditorPage extends StatelessWidget {
const ViewerActionEditorPage({super.key});
static const allAvailableActions = [
[
EntryAction.share,
EntryAction.edit,
EntryAction.rename,
@ -18,14 +19,17 @@ class ViewerActionEditorPage extends StatelessWidget {
EntryAction.move,
EntryAction.toggleFavourite,
EntryAction.rotateScreen,
EntryAction.videoCaptureFrame,
EntryAction.videoToggleMute,
EntryAction.videoSetSpeed,
EntryAction.videoSelectStreams,
EntryAction.viewSource,
EntryAction.rotateCCW,
EntryAction.rotateCW,
EntryAction.flip,
],
[
EntryAction.videoCaptureFrame,
EntryAction.videoToggleMute,
EntryAction.videoSetSpeed,
EntryAction.videoSelectStreams,
],
];
@override

View file

@ -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:

View file

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