#334 collection: selection edit actions available as quick actions
This commit is contained in:
parent
9ba9ec302e
commit
59473dab64
13 changed files with 184 additions and 67 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -75,6 +75,7 @@ dependencies:
|
|||
provider:
|
||||
screen_brightness:
|
||||
shared_preferences:
|
||||
smooth_page_indicator:
|
||||
sqflite:
|
||||
streams_channel:
|
||||
git:
|
||||
|
|
Loading…
Reference in a new issue