Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2021-06-12 11:44:17 +09:00
commit 47f1e9253f
14 changed files with 411 additions and 208 deletions

View file

@ -3,7 +3,7 @@ All notable changes to this project will be documented in this file.
## [Unreleased] ## [Unreleased]
## [v1.4.2] - 2021-06-10 ## [v1.4.3] - 2021-06-12
### Added ### Added
- Collection: snack bar action to show moved/copied/exported entries - Collection: snack bar action to show moved/copied/exported entries
- Collection / Albums / Countries / Tags: when switching device orientation, keep items in view - Collection / Albums / Countries / Tags: when switching device orientation, keep items in view
@ -23,6 +23,8 @@ All notable changes to this project will be documented in this file.
- fixed opening files shared via content URI with incorrect MIME type - fixed opening files shared via content URI with incorrect MIME type
- refresh collection when entries modified in Viewer no longer match collection filters - refresh collection when entries modified in Viewer no longer match collection filters
## [v1.4.2] - 2021-06-10 [YANKED]
## [v1.4.1] - 2021-04-29 ## [v1.4.1] - 2021-04-29
### Added ### Added
- Motion photo support - Motion photo support

View file

@ -4,20 +4,24 @@ import 'package:flutter/widgets.dart';
enum EntryAction { enum EntryAction {
delete, delete,
edit,
export, export,
flip,
info, info,
open,
openMap,
print, print,
rename, rename,
rotateCCW,
rotateCW,
setAs,
share, share,
toggleFavourite, toggleFavourite,
// raster
rotateCCW,
rotateCW,
flip,
// vector
viewSource, viewSource,
// external
edit,
open,
openMap,
setAs,
// debug
debug, debug,
} }
@ -55,7 +59,6 @@ class EntryActions {
extension ExtraEntryAction on EntryAction { extension ExtraEntryAction on EntryAction {
String getText(BuildContext context) { String getText(BuildContext context) {
switch (this) { switch (this) {
// in app actions
case EntryAction.toggleFavourite: case EntryAction.toggleFavourite:
// different data depending on toggle state // different data depending on toggle state
return context.l10n.entryActionAddFavourite; return context.l10n.entryActionAddFavourite;
@ -67,19 +70,21 @@ extension ExtraEntryAction on EntryAction {
return context.l10n.entryActionInfo; return context.l10n.entryActionInfo;
case EntryAction.rename: case EntryAction.rename:
return context.l10n.entryActionRename; return context.l10n.entryActionRename;
case EntryAction.print:
return context.l10n.entryActionPrint;
case EntryAction.share:
return context.l10n.entryActionShare;
// raster
case EntryAction.rotateCCW: case EntryAction.rotateCCW:
return context.l10n.entryActionRotateCCW; return context.l10n.entryActionRotateCCW;
case EntryAction.rotateCW: case EntryAction.rotateCW:
return context.l10n.entryActionRotateCW; return context.l10n.entryActionRotateCW;
case EntryAction.flip: case EntryAction.flip:
return context.l10n.entryActionFlip; return context.l10n.entryActionFlip;
case EntryAction.print: // vector
return context.l10n.entryActionPrint;
case EntryAction.share:
return context.l10n.entryActionShare;
case EntryAction.viewSource: case EntryAction.viewSource:
return context.l10n.entryActionViewSource; return context.l10n.entryActionViewSource;
// external app actions // external
case EntryAction.edit: case EntryAction.edit:
return context.l10n.entryActionEdit; return context.l10n.entryActionEdit;
case EntryAction.open: case EntryAction.open:
@ -88,6 +93,7 @@ extension ExtraEntryAction on EntryAction {
return context.l10n.entryActionSetAs; return context.l10n.entryActionSetAs;
case EntryAction.openMap: case EntryAction.openMap:
return context.l10n.entryActionOpenMap; return context.l10n.entryActionOpenMap;
// debug
case EntryAction.debug: case EntryAction.debug:
return 'Debug'; return 'Debug';
} }
@ -95,7 +101,6 @@ extension ExtraEntryAction on EntryAction {
IconData? getIcon() { IconData? getIcon() {
switch (this) { switch (this) {
// in app actions
case EntryAction.toggleFavourite: case EntryAction.toggleFavourite:
// different data depending on toggle state // different data depending on toggle state
return AIcons.favourite; return AIcons.favourite;
@ -107,24 +112,27 @@ extension ExtraEntryAction on EntryAction {
return AIcons.info; return AIcons.info;
case EntryAction.rename: case EntryAction.rename:
return AIcons.rename; return AIcons.rename;
case EntryAction.print:
return AIcons.print;
case EntryAction.share:
return AIcons.share;
// raster
case EntryAction.rotateCCW: case EntryAction.rotateCCW:
return AIcons.rotateLeft; return AIcons.rotateLeft;
case EntryAction.rotateCW: case EntryAction.rotateCW:
return AIcons.rotateRight; return AIcons.rotateRight;
case EntryAction.flip: case EntryAction.flip:
return AIcons.flip; return AIcons.flip;
case EntryAction.print: // vector
return AIcons.print;
case EntryAction.share:
return AIcons.share;
case EntryAction.viewSource: case EntryAction.viewSource:
return AIcons.vector; return AIcons.vector;
// external app actions // external
case EntryAction.edit: case EntryAction.edit:
case EntryAction.open: case EntryAction.open:
case EntryAction.setAs: case EntryAction.setAs:
case EntryAction.openMap: case EntryAction.openMap:
return null; return null;
// debug
case EntryAction.debug: case EntryAction.debug:
return AIcons.debug; return AIcons.debug;
} }

View file

@ -5,6 +5,7 @@ import 'package:aves/theme/durations.dart';
import 'package:collection/collection.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:flutter/rendering.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -123,7 +124,8 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
); );
children.add(animate ? _buildAnimation(itemGridIndex, item) : item); children.add(animate ? _buildAnimation(itemGridIndex, item) : item);
} }
return Wrap( return _GridRow(
extent: tileExtent,
spacing: spacing, spacing: spacing,
children: children, children: children,
); );
@ -274,3 +276,128 @@ class SectionLayout {
@override @override
String toString() => '$runtimeType#${shortHash(this)}{sectionKey=$sectionKey, firstIndex=$firstIndex, lastIndex=$lastIndex, minOffset=$minOffset, maxOffset=$maxOffset, headerExtent=$headerExtent, tileExtent=$tileExtent, spacing=$spacing}'; String toString() => '$runtimeType#${shortHash(this)}{sectionKey=$sectionKey, firstIndex=$firstIndex, lastIndex=$lastIndex, minOffset=$minOffset, maxOffset=$maxOffset, headerExtent=$headerExtent, tileExtent=$tileExtent, spacing=$spacing}';
} }
class _GridRow extends MultiChildRenderObjectWidget {
final double extent, spacing;
_GridRow({
Key? key,
required this.extent,
required this.spacing,
required List<Widget> children,
}) : super(key: key, children: children);
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderGridRow(
extent: extent,
spacing: spacing,
);
}
@override
void updateRenderObject(BuildContext context, _RenderGridRow renderObject) {
renderObject.extent = extent;
renderObject.spacing = spacing;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DoubleProperty('extent', extent));
properties.add(DoubleProperty('spacing', spacing));
}
}
class _GridRowParentData extends ContainerBoxParentData<RenderBox> {}
class _RenderGridRow extends RenderBox with ContainerRenderObjectMixin<RenderBox, _GridRowParentData>, RenderBoxContainerDefaultsMixin<RenderBox, _GridRowParentData> {
_RenderGridRow({
List<RenderBox>? children,
required double extent,
required double spacing,
}) : _extent = extent,
_spacing = spacing {
addAll(children);
}
double get extent => _extent;
double _extent;
set extent(double value) {
if (_extent == value) return;
_extent = value;
markNeedsLayout();
}
double get spacing => _spacing;
double _spacing;
set spacing(double value) {
if (_spacing == value) return;
_spacing = value;
markNeedsLayout();
}
@override
void setupParentData(RenderBox child) {
if (child.parentData is! _GridRowParentData) {
child.parentData = _GridRowParentData();
}
}
double get intrinsicWidth => extent * childCount + spacing * (childCount - 1);
@override
double computeMinIntrinsicWidth(double height) => intrinsicWidth;
@override
double computeMaxIntrinsicWidth(double height) => intrinsicWidth;
@override
double computeMinIntrinsicHeight(double width) => extent;
@override
double computeMaxIntrinsicHeight(double width) => extent;
@override
void performLayout() {
var child = firstChild;
if (child == null) {
size = constraints.smallest;
return;
}
size = Size(constraints.maxWidth, extent);
final childConstraints = BoxConstraints.tight(Size(extent, extent));
var offset = Offset.zero;
while (child != null) {
child.layout(childConstraints, parentUsesSize: false);
final childParentData = child.parentData! as _GridRowParentData;
childParentData.offset = offset;
offset += Offset(extent + spacing, 0);
child = childParentData.nextSibling;
}
}
@override
double? computeDistanceToActualBaseline(TextBaseline baseline) {
return defaultComputeDistanceToHighestActualBaseline(baseline);
}
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
return defaultHitTestChildren(result, position: position);
}
@override
void paint(PaintingContext context, Offset offset) {
defaultPaint(context, offset);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DoubleProperty('extent', extent));
properties.add(DoubleProperty('spacing', spacing));
}
}

View file

@ -0,0 +1,55 @@
import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/settings/quick_actions/editor_page.dart';
import 'package:flutter/material.dart';
class QuickEntryActionsTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(context.l10n.settingsViewerQuickActionsTile),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
settings: const RouteSettings(name: QuickEntryActionEditorPage.routeName),
builder: (context) => const QuickEntryActionEditorPage(),
),
);
},
);
}
}
class QuickEntryActionEditorPage extends StatelessWidget {
static const routeName = '/settings/quick_entry_actions';
const QuickEntryActionEditorPage({Key? key}) : super(key: key);
static const allAvailableActions = [
EntryAction.info,
EntryAction.toggleFavourite,
EntryAction.share,
EntryAction.delete,
EntryAction.rename,
EntryAction.export,
EntryAction.print,
EntryAction.viewSource,
EntryAction.flip,
EntryAction.rotateCCW,
EntryAction.rotateCW,
];
@override
Widget build(BuildContext context) {
return QuickActionEditorPage<EntryAction>(
bannerText: context.l10n.settingsViewerQuickActionEditorBanner,
allAvailableActions: allAvailableActions,
actionIcon: (action) => action.getIcon(),
actionText: (context, action) => action.getText(context),
load: () => settings.viewerQuickActions.toList(),
save: (actions) => settings.viewerQuickActions = actions,
);
}
}

View file

@ -0,0 +1,48 @@
import 'package:aves/widgets/viewer/overlay/common.dart';
import 'package:flutter/material.dart';
class ActionButton extends StatelessWidget {
final String text;
final IconData? icon;
final bool enabled, showCaption;
const ActionButton({
required this.text,
required this.icon,
this.enabled = true,
this.showCaption = true,
});
static const padding = 8.0;
@override
Widget build(BuildContext context) {
final textStyle = Theme.of(context).textTheme.caption;
return SizedBox(
width: OverlayButton.getSize(context) + padding * 2,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: padding),
OverlayButton(
child: IconButton(
icon: Icon(icon),
onPressed: enabled ? () {} : null,
),
),
if (showCaption) ...[
const SizedBox(height: padding),
Text(
text,
style: enabled ? textStyle : textStyle!.copyWith(color: textStyle.color!.withOpacity(.2)),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
],
const SizedBox(height: padding),
],
),
);
}
}

View file

@ -0,0 +1,33 @@
import 'package:aves/theme/durations.dart';
import 'package:flutter/material.dart';
class ActionPanel extends StatelessWidget {
final bool highlight;
final Widget child;
const ActionPanel({
this.highlight = false,
required this.child,
});
@override
Widget build(BuildContext context) {
final color = highlight ? Theme.of(context).accentColor : Colors.blueGrey;
return AnimatedContainer(
foregroundDecoration: BoxDecoration(
color: color.withOpacity(.2),
border: Border.fromBorderSide(BorderSide(
color: color,
width: highlight ? 2 : 1,
)),
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
margin: const EdgeInsets.all(16),
duration: Durations.quickActionHighlightAnimation,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: child,
),
);
}
}

View file

@ -1,42 +1,33 @@
import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/settings/quick_actions/common.dart'; import 'package:aves/widgets/settings/quick_actions/action_button.dart';
import 'package:flutter/material.dart'; import 'package:aves/widgets/settings/quick_actions/placeholder.dart';
import 'package:flutter/widgets.dart';
class AvailableActionPanel extends StatelessWidget { class AvailableActionPanel<T extends Object> extends StatelessWidget {
final List<EntryAction> quickActions; final List<T> allActions, quickActions;
final Listenable quickActionsChangeNotifier; final Listenable quickActionsChangeNotifier;
final ValueNotifier<bool> panelHighlight; final ValueNotifier<bool> panelHighlight;
final ValueNotifier<EntryAction?> draggedQuickAction; final ValueNotifier<T?> draggedQuickAction;
final ValueNotifier<EntryAction?> draggedAvailableAction; final ValueNotifier<T?> draggedAvailableAction;
final bool Function(EntryAction? action) removeQuickAction; final bool Function(T? action) removeQuickAction;
final IconData? Function(T action) actionIcon;
final String Function(BuildContext context, T action) actionText;
const AvailableActionPanel({ const AvailableActionPanel({
required this.allActions,
required this.quickActions, required this.quickActions,
required this.quickActionsChangeNotifier, required this.quickActionsChangeNotifier,
required this.panelHighlight, required this.panelHighlight,
required this.draggedQuickAction, required this.draggedQuickAction,
required this.draggedAvailableAction, required this.draggedAvailableAction,
required this.removeQuickAction, required this.removeQuickAction,
required this.actionIcon,
required this.actionText,
}); });
static const allActions = [
EntryAction.info,
EntryAction.toggleFavourite,
EntryAction.share,
EntryAction.delete,
EntryAction.rename,
EntryAction.export,
EntryAction.print,
EntryAction.viewSource,
EntryAction.flip,
EntryAction.rotateCCW,
EntryAction.rotateCW,
];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DragTarget<EntryAction>( return DragTarget<T>(
onWillAccept: (data) { onWillAccept: (data) {
if (draggedQuickAction.value != null) { if (draggedQuickAction.value != null) {
_setPanelHighlight(true); _setPanelHighlight(true);
@ -61,15 +52,12 @@ class AvailableActionPanel extends StatelessWidget {
children: allActions.map((action) { children: allActions.map((action) {
final dragged = action == draggedAvailableAction.value; final dragged = action == draggedAvailableAction.value;
final enabled = dragged || !quickActions.contains(action); final enabled = dragged || !quickActions.contains(action);
Widget child = ActionButton( var child = _buildActionButton(context, action, enabled: enabled);
action: action,
enabled: enabled,
);
if (dragged) { if (dragged) {
child = DraggedPlaceholder(child: child); child = DraggedPlaceholder(child: child);
} }
if (enabled) { if (enabled) {
child = _buildDraggable(action, child); child = _buildDraggable(context, action, child);
} }
return child; return child;
}).toList(), }).toList(),
@ -80,14 +68,20 @@ class AvailableActionPanel extends StatelessWidget {
); );
} }
Widget _buildDraggable(EntryAction action, Widget child) => LongPressDraggable<EntryAction>( Widget _buildDraggable(
BuildContext context,
T action,
Widget child,
) =>
LongPressDraggable<T>(
data: action, data: action,
maxSimultaneousDrags: 1, maxSimultaneousDrags: 1,
onDragStarted: () => _setDraggedAvailableAction(action), onDragStarted: () => _setDraggedAvailableAction(action),
onDragEnd: (details) => _setDraggedAvailableAction(null), onDragEnd: (details) => _setDraggedAvailableAction(null),
feedback: MediaQueryDataProvider( feedback: MediaQueryDataProvider(
child: ActionButton( child: _buildActionButton(
action: action, context,
action,
showCaption: false, showCaption: false,
), ),
), ),
@ -95,9 +89,22 @@ class AvailableActionPanel extends StatelessWidget {
child: child, child: child,
); );
void _setDraggedQuickAction(EntryAction? action) => draggedQuickAction.value = action; Widget _buildActionButton(
BuildContext context,
T action, {
bool enabled = true,
bool showCaption = true,
}) =>
ActionButton(
text: actionText(context, action),
icon: actionIcon(action),
enabled: enabled,
showCaption: showCaption,
);
void _setDraggedAvailableAction(EntryAction? action) => draggedAvailableAction.value = action; void _setDraggedQuickAction(T? action) => draggedQuickAction.value = action;
void _setDraggedAvailableAction(T? action) => draggedAvailableAction.value = action;
void _setPanelHighlight(bool flag) => panelHighlight.value = flag; void _setPanelHighlight(bool flag) => panelHighlight.value = flag;
} }

View file

@ -1,95 +0,0 @@
import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/viewer/overlay/common.dart';
import 'package:flutter/material.dart';
class ActionPanel extends StatelessWidget {
final bool highlight;
final Widget child;
const ActionPanel({
this.highlight = false,
required this.child,
});
@override
Widget build(BuildContext context) {
final color = highlight ? Theme.of(context).accentColor : Colors.blueGrey;
return AnimatedContainer(
foregroundDecoration: BoxDecoration(
color: color.withOpacity(.2),
border: Border.fromBorderSide(BorderSide(
color: color,
width: highlight ? 2 : 1,
)),
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
margin: const EdgeInsets.all(16),
duration: Durations.quickActionHighlightAnimation,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: child,
),
);
}
}
class ActionButton extends StatelessWidget {
final EntryAction action;
final bool enabled, showCaption;
const ActionButton({
required this.action,
this.enabled = true,
this.showCaption = true,
});
static const padding = 8.0;
@override
Widget build(BuildContext context) {
final textStyle = Theme.of(context).textTheme.caption;
return SizedBox(
width: OverlayButton.getSize(context) + padding * 2,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: padding),
OverlayButton(
child: IconButton(
icon: Icon(action.getIcon()),
onPressed: enabled ? () {} : null,
),
),
if (showCaption) ...[
const SizedBox(height: padding),
Text(
action.getText(context),
style: enabled ? textStyle : textStyle!.copyWith(color: textStyle.color!.withOpacity(.2)),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
],
const SizedBox(height: padding),
],
),
);
}
}
class DraggedPlaceholder extends StatelessWidget {
final Widget child;
const DraggedPlaceholder({
required this.child,
});
@override
Widget build(BuildContext context) {
return Opacity(
opacity: .2,
child: child,
);
}
}

View file

@ -1,51 +1,47 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/change_notifier.dart'; import 'package:aves/utils/change_notifier.dart';
import 'package:aves/utils/constants.dart'; import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/settings/quick_actions/action_button.dart';
import 'package:aves/widgets/settings/quick_actions/action_panel.dart';
import 'package:aves/widgets/settings/quick_actions/available_actions.dart'; import 'package:aves/widgets/settings/quick_actions/available_actions.dart';
import 'package:aves/widgets/settings/quick_actions/common.dart'; import 'package:aves/widgets/settings/quick_actions/placeholder.dart';
import 'package:aves/widgets/settings/quick_actions/quick_actions.dart'; import 'package:aves/widgets/settings/quick_actions/quick_actions.dart';
import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:aves/widgets/viewer/overlay/common.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class QuickActionsTile extends StatelessWidget { class QuickActionEditorPage<T extends Object> extends StatefulWidget {
@override final String bannerText;
Widget build(BuildContext context) { final List<T> allAvailableActions;
return ListTile( final IconData? Function(T action) actionIcon;
title: Text(context.l10n.settingsViewerQuickActionsTile), final String Function(BuildContext context, T action) actionText;
onTap: () { final List<T> Function() load;
Navigator.push( final void Function(List<T> actions) save;
context,
MaterialPageRoute(
settings: const RouteSettings(name: QuickActionEditorPage.routeName),
builder: (context) => QuickActionEditorPage(),
),
);
},
);
}
}
class QuickActionEditorPage extends StatefulWidget { const QuickActionEditorPage({
static const routeName = '/settings/quick_actions'; required this.bannerText,
required this.allAvailableActions,
required this.actionIcon,
required this.actionText,
required this.load,
required this.save,
});
@override @override
_QuickActionEditorPageState createState() => _QuickActionEditorPageState(); _QuickActionEditorPageState createState() => _QuickActionEditorPageState<T>();
} }
class _QuickActionEditorPageState extends State<QuickActionEditorPage> { class _QuickActionEditorPageState<T extends Object> extends State<QuickActionEditorPage<T>> {
final GlobalKey<AnimatedListState> _animatedListKey = GlobalKey(debugLabel: 'quick-actions-animated-list'); final GlobalKey<AnimatedListState> _animatedListKey = GlobalKey(debugLabel: 'quick-actions-animated-list');
Timer? _targetLeavingTimer; Timer? _targetLeavingTimer;
late List<EntryAction> _quickActions; late List<T> _quickActions;
final ValueNotifier<EntryAction?> _draggedQuickAction = ValueNotifier(null); final ValueNotifier<T?> _draggedQuickAction = ValueNotifier(null);
final ValueNotifier<EntryAction?> _draggedAvailableAction = ValueNotifier(null); final ValueNotifier<T?> _draggedAvailableAction = ValueNotifier(null);
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();
@ -59,7 +55,7 @@ class _QuickActionEditorPageState extends State<QuickActionEditorPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_quickActions = settings.viewerQuickActions.toList(); _quickActions = widget.load();
} }
@override @override
@ -79,7 +75,7 @@ class _QuickActionEditorPageState extends State<QuickActionEditorPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final header = QuickActionButton( final header = QuickActionButton<T>(
placement: QuickActionPlacement.header, placement: QuickActionPlacement.header,
panelHighlight: _quickActionHighlight, panelHighlight: _quickActionHighlight,
draggedQuickAction: _draggedQuickAction, draggedQuickAction: _draggedQuickAction,
@ -88,7 +84,7 @@ class _QuickActionEditorPageState extends State<QuickActionEditorPage> {
removeAction: _removeQuickAction, removeAction: _removeQuickAction,
onTargetLeave: _onQuickActionTargetLeave, onTargetLeave: _onQuickActionTargetLeave,
); );
final footer = QuickActionButton( final footer = QuickActionButton<T>(
placement: QuickActionPlacement.footer, placement: QuickActionPlacement.footer,
panelHighlight: _quickActionHighlight, panelHighlight: _quickActionHighlight,
draggedQuickAction: _draggedQuickAction, draggedQuickAction: _draggedQuickAction,
@ -104,7 +100,7 @@ class _QuickActionEditorPageState extends State<QuickActionEditorPage> {
), ),
body: WillPopScope( body: WillPopScope(
onWillPop: () { onWillPop: () {
settings.viewerQuickActions = _quickActions; widget.save(_quickActions);
return SynchronousFuture(true); return SynchronousFuture(true);
}, },
child: SafeArea( child: SafeArea(
@ -116,7 +112,7 @@ class _QuickActionEditorPageState extends State<QuickActionEditorPage> {
children: [ children: [
const Icon(AIcons.info), const Icon(AIcons.info),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded(child: Text(context.l10n.settingsViewerQuickActionEditorBanner)), Expanded(child: Text(widget.bannerText)),
], ],
), ),
), ),
@ -163,7 +159,7 @@ class _QuickActionEditorPageState extends State<QuickActionEditorPage> {
itemBuilder: (context, index, animation) { itemBuilder: (context, index, animation) {
if (index >= _quickActions.length) return const SizedBox(); if (index >= _quickActions.length) return const SizedBox();
final action = _quickActions[index]; final action = _quickActions[index];
return QuickActionButton( return QuickActionButton<T>(
placement: QuickActionPlacement.action, placement: QuickActionPlacement.action,
action: action, action: action,
panelHighlight: _quickActionHighlight, panelHighlight: _quickActionHighlight,
@ -172,6 +168,11 @@ class _QuickActionEditorPageState extends State<QuickActionEditorPage> {
insertAction: _insertQuickAction, insertAction: _insertQuickAction,
removeAction: _removeQuickAction, removeAction: _removeQuickAction,
onTargetLeave: _onQuickActionTargetLeave, onTargetLeave: _onQuickActionTargetLeave,
draggableFeedbackBuilder: (action) => ActionButton(
text: widget.actionText(context, action),
icon: widget.actionIcon(action),
showCaption: false,
),
child: _buildQuickActionButton(action, animation), child: _buildQuickActionButton(action, animation),
); );
}, },
@ -205,13 +206,16 @@ class _QuickActionEditorPageState extends State<QuickActionEditorPage> {
highlight: highlight, highlight: highlight,
child: child!, child: child!,
), ),
child: AvailableActionPanel( child: AvailableActionPanel<T>(
allActions: widget.allAvailableActions,
quickActions: _quickActions, quickActions: _quickActions,
quickActionsChangeNotifier: _quickActionsChangeNotifier, quickActionsChangeNotifier: _quickActionsChangeNotifier,
panelHighlight: _availableActionHighlight, panelHighlight: _availableActionHighlight,
draggedQuickAction: _draggedQuickAction, draggedQuickAction: _draggedQuickAction,
draggedAvailableAction: _draggedAvailableAction, draggedAvailableAction: _draggedAvailableAction,
removeQuickAction: _removeQuickAction, removeQuickAction: _removeQuickAction,
actionIcon: widget.actionIcon,
actionText: widget.actionText,
), ),
), ),
], ],
@ -224,7 +228,7 @@ class _QuickActionEditorPageState extends State<QuickActionEditorPage> {
void _stopLeavingTimer() => _targetLeavingTimer?.cancel(); void _stopLeavingTimer() => _targetLeavingTimer?.cancel();
bool _insertQuickAction(EntryAction action, QuickActionPlacement placement, EntryAction? overAction) { bool _insertQuickAction(T action, QuickActionPlacement placement, T? overAction) {
_stopLeavingTimer(); _stopLeavingTimer();
if (_reordering) return false; if (_reordering) return false;
@ -256,7 +260,7 @@ class _QuickActionEditorPageState extends State<QuickActionEditorPage> {
return true; return true;
} }
bool _removeQuickAction(EntryAction? action) { bool _removeQuickAction(T? action) {
if (action == null || !_quickActions.contains(action)) return false; if (action == null || !_quickActions.contains(action)) return false;
final index = _quickActions.indexOf(action); final index = _quickActions.indexOf(action);
@ -270,7 +274,7 @@ class _QuickActionEditorPageState extends State<QuickActionEditorPage> {
return true; return true;
} }
Widget _buildQuickActionButton(EntryAction action, Animation<double> animation) { Widget _buildQuickActionButton(T action, Animation<double> animation) {
animation = animation.drive(CurveTween(curve: Curves.easeInOut)); animation = animation.drive(CurveTween(curve: Curves.easeInOut));
Widget child = FadeTransition( Widget child = FadeTransition(
opacity: animation, opacity: animation,
@ -281,7 +285,7 @@ class _QuickActionEditorPageState extends State<QuickActionEditorPage> {
padding: const EdgeInsets.symmetric(vertical: _QuickActionEditorPageState.quickActionVerticalPadding, horizontal: 4), padding: const EdgeInsets.symmetric(vertical: _QuickActionEditorPageState.quickActionVerticalPadding, horizontal: 4),
child: OverlayButton( child: OverlayButton(
child: IconButton( child: IconButton(
icon: Icon(action.getIcon()), icon: Icon(widget.actionIcon(action)),
onPressed: () {}, onPressed: () {},
), ),
), ),

View file

@ -0,0 +1,17 @@
import 'package:flutter/widgets.dart';
class DraggedPlaceholder extends StatelessWidget {
final Widget child;
const DraggedPlaceholder({
required this.child,
});
@override
Widget build(BuildContext context) {
return Opacity(
opacity: .2,
child: child,
);
}
}

View file

@ -1,19 +1,18 @@
import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/settings/quick_actions/common.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
enum QuickActionPlacement { header, action, footer } enum QuickActionPlacement { header, action, footer }
class QuickActionButton extends StatelessWidget { class QuickActionButton<T extends Object> extends StatelessWidget {
final QuickActionPlacement placement; final QuickActionPlacement placement;
final EntryAction? action; final T? action;
final ValueNotifier<bool> panelHighlight; final ValueNotifier<bool> panelHighlight;
final ValueNotifier<EntryAction?> draggedQuickAction; final ValueNotifier<T?> draggedQuickAction;
final ValueNotifier<EntryAction?> draggedAvailableAction; final ValueNotifier<T?> draggedAvailableAction;
final bool Function(EntryAction action, QuickActionPlacement placement, EntryAction? overAction) insertAction; final bool Function(T action, QuickActionPlacement placement, T? overAction) insertAction;
final bool Function(EntryAction action) removeAction; final bool Function(T action) removeAction;
final VoidCallback onTargetLeave; final VoidCallback onTargetLeave;
final Widget Function(T action)? draggableFeedbackBuilder;
final Widget? child; final Widget? child;
const QuickActionButton({ const QuickActionButton({
@ -25,6 +24,7 @@ class QuickActionButton extends StatelessWidget {
required this.insertAction, required this.insertAction,
required this.removeAction, required this.removeAction,
required this.onTargetLeave, required this.onTargetLeave,
this.draggableFeedbackBuilder,
this.child, this.child,
}); });
@ -38,8 +38,8 @@ class QuickActionButton extends StatelessWidget {
return child; return child;
} }
DragTarget<EntryAction> _buildDragTarget(Widget? child) { DragTarget<T> _buildDragTarget(Widget? child) {
return DragTarget<EntryAction>( return DragTarget<T>(
onWillAccept: (data) { onWillAccept: (data) {
if (draggedQuickAction.value != null) { if (draggedQuickAction.value != null) {
insertAction(draggedQuickAction.value!, placement, action); insertAction(draggedQuickAction.value!, placement, action);
@ -56,7 +56,7 @@ class QuickActionButton extends StatelessWidget {
); );
} }
Widget _buildDraggable(Widget child, EntryAction action) => LongPressDraggable( Widget _buildDraggable(Widget child, T action) => LongPressDraggable(
data: action, data: action,
maxSimultaneousDrags: 1, maxSimultaneousDrags: 1,
onDragStarted: () => _setDraggedQuickAction(action), onDragStarted: () => _setDraggedQuickAction(action),
@ -65,16 +65,13 @@ class QuickActionButton extends StatelessWidget {
onDraggableCanceled: (velocity, offset) => _setDraggedQuickAction(null), onDraggableCanceled: (velocity, offset) => _setDraggedQuickAction(null),
onDragCompleted: () => _setDraggedQuickAction(null), onDragCompleted: () => _setDraggedQuickAction(null),
feedback: MediaQueryDataProvider( feedback: MediaQueryDataProvider(
child: ActionButton( child: draggableFeedbackBuilder!(action),
action: action,
showCaption: false,
),
), ),
childWhenDragging: child, childWhenDragging: child,
child: child, child: child,
); );
void _setDraggedQuickAction(EntryAction? action) => draggedQuickAction.value = action; void _setDraggedQuickAction(T? action) => draggedQuickAction.value = action;
void _setPanelHighlight(bool flag) => panelHighlight.value = flag; void _setPanelHighlight(bool flag) => panelHighlight.value = flag;
} }

View file

@ -19,7 +19,7 @@ import 'package:aves/widgets/settings/access_grants.dart';
import 'package:aves/widgets/settings/entry_background.dart'; import 'package:aves/widgets/settings/entry_background.dart';
import 'package:aves/widgets/settings/hidden_filters.dart'; import 'package:aves/widgets/settings/hidden_filters.dart';
import 'package:aves/widgets/settings/language.dart'; import 'package:aves/widgets/settings/language.dart';
import 'package:aves/widgets/settings/quick_actions/editor.dart'; import 'package:aves/widgets/settings/entry_actions_editor.dart';
import 'package:decorated_icon/decorated_icon.dart'; import 'package:decorated_icon/decorated_icon.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
@ -191,7 +191,7 @@ class _SettingsPageState extends State<SettingsPage> {
expandedNotifier: _expandedNotifier, expandedNotifier: _expandedNotifier,
showHighlight: false, showHighlight: false,
children: [ children: [
QuickActionsTile(), QuickEntryActionsTile(),
SwitchListTile( SwitchListTile(
value: settings.showOverlayMinimap, value: settings.showOverlayMinimap,
onChanged: (v) => settings.showOverlayMinimap = v, onChanged: (v) => settings.showOverlayMinimap = v,

View file

@ -1,7 +1,7 @@
name: aves name: aves
description: A visual media gallery and metadata explorer app. description: A visual media gallery and metadata explorer app.
repository: https://github.com/deckerst/aves repository: https://github.com/deckerst/aves
version: 1.4.2+46 version: 1.4.3+47
publish_to: none publish_to: none
environment: environment:

View file

@ -1,5 +1,5 @@
Thanks for using Aves! Thanks for using Aves!
v1.4.2: v1.4.3:
- improved navigation usability - improved navigation usability
- changed thumbnail layout - changed thumbnail layout
- improved playing videos with non-square pixels - improved playing videos with non-square pixels