#26 viewer: custom quick actions
This commit is contained in:
parent
9d2954d717
commit
f7ced5832a
25 changed files with 706 additions and 70 deletions
|
@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
|
|||
- Collection / Albums / Countries / Tags: added label when dragging scrollbar thumb
|
||||
- Albums: localized common album names
|
||||
- Collection: select shortcut icon image
|
||||
- Viewer: customizable quick actions
|
||||
|
||||
### Changed
|
||||
- Upgraded Flutter to beta v2.1.0-12.2.pre
|
||||
|
|
|
@ -525,6 +525,19 @@
|
|||
"settingsViewerShowShootingDetails": "Show shooting details",
|
||||
"@settingsViewerShowShootingDetails": {},
|
||||
|
||||
"settingsViewerQuickActionsTile": "Quick actions",
|
||||
"@settingsViewerQuickActionsTile": {},
|
||||
"settingsViewerQuickActionEditorTitle": "Quick Actions",
|
||||
"@settingsViewerQuickActionEditorTitle": {},
|
||||
"settingsViewerQuickActionEditorBanner": "Touch and hold to move buttons and select which actions are displayed in the viewer.",
|
||||
"@settingsViewerQuickActionEditorBanner": {},
|
||||
"settingsViewerQuickActionEditorDisplayedButtons": "Displayed Buttons",
|
||||
"@settingsViewerQuickActionEditorDisplayedButtons": {},
|
||||
"settingsViewerQuickActionEditorAvailableButtons": "Available Buttons",
|
||||
"@settingsViewerQuickActionEditorAvailableButtons": {},
|
||||
"settingsViewerQuickActionEmpty": "No buttons",
|
||||
"@settingsViewerQuickActionEmpty": {},
|
||||
|
||||
"settingsSectionVideo": "Video",
|
||||
"@settingsSectionVideo": {},
|
||||
"settingsVideoShowVideos": "Show videos",
|
||||
|
|
|
@ -243,6 +243,13 @@
|
|||
"settingsViewerShowInformationSubtitle": "제목, 날짜, 장소 등 표시",
|
||||
"settingsViewerShowShootingDetails": "촬영 정보 표시",
|
||||
|
||||
"settingsViewerQuickActionsTile": "빠른 작업",
|
||||
"settingsViewerQuickActionEditorTitle": "빠른 작업",
|
||||
"settingsViewerQuickActionEditorBanner": "버튼을 길게 누른 후 이동하여 뷰어에 표시될 버튼을 선택하세요.",
|
||||
"settingsViewerQuickActionEditorDisplayedButtons": "표시될 버튼",
|
||||
"settingsViewerQuickActionEditorAvailableButtons": "추가 가능한 버튼",
|
||||
"settingsViewerQuickActionEmpty": "버튼이 없습니다",
|
||||
|
||||
"settingsSectionVideo": "동영상",
|
||||
"settingsVideoShowVideos": "미디어에 동영상 표시",
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/settings/screen_on.dart';
|
||||
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||
|
@ -46,6 +47,7 @@ class Settings extends ChangeNotifier {
|
|||
static const showOverlayMinimapKey = 'show_overlay_minimap';
|
||||
static const showOverlayInfoKey = 'show_overlay_info';
|
||||
static const showOverlayShootingDetailsKey = 'show_overlay_shooting_details';
|
||||
static const viewerQuickActionsKey = 'viewer_quick_actions';
|
||||
|
||||
// info
|
||||
static const infoMapStyleKey = 'info_map_style';
|
||||
|
@ -63,6 +65,12 @@ class Settings extends ChangeNotifier {
|
|||
// version
|
||||
static const lastVersionCheckDateKey = 'last_version_check_date';
|
||||
|
||||
// defaults
|
||||
static const viewerQuickActionsDefault = [
|
||||
EntryAction.toggleFavourite,
|
||||
EntryAction.share,
|
||||
];
|
||||
|
||||
Future<void> init() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
}
|
||||
|
@ -211,6 +219,10 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
set showOverlayShootingDetails(bool newValue) => setAndNotify(showOverlayShootingDetailsKey, newValue);
|
||||
|
||||
List<EntryAction> get viewerQuickActions => getEnumListOrDefault(viewerQuickActionsKey, viewerQuickActionsDefault, EntryAction.values);
|
||||
|
||||
set viewerQuickActions(List<EntryAction> newValue) => setAndNotify(viewerQuickActionsKey, newValue.map((v) => v.toString()).toList());
|
||||
|
||||
// info
|
||||
|
||||
EntryMapStyle get infoMapStyle => getEnumOrDefault(infoMapStyleKey, EntryMapStyle.stamenWatercolor, EntryMapStyle.values);
|
||||
|
@ -258,16 +270,16 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
T getEnumOrDefault<T>(String key, T defaultValue, Iterable<T> values) {
|
||||
final valueString = _prefs.getString(key);
|
||||
for (final element in values) {
|
||||
if (element.toString() == valueString) {
|
||||
return element;
|
||||
for (final v in values) {
|
||||
if (v.toString() == valueString) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
List<T> getEnumListOrDefault<T>(String key, List<T> defaultValue, Iterable<T> values) {
|
||||
return _prefs.getStringList(key)?.map((s) => values.firstWhere((el) => el.toString() == s, orElse: () => null))?.where((el) => el != null)?.toList() ?? defaultValue;
|
||||
return _prefs.getStringList(key)?.map((s) => values.firstWhere((v) => v.toString() == s, orElse: () => null))?.where((v) => v != null)?.toList() ?? defaultValue;
|
||||
}
|
||||
|
||||
void setAndNotify(String key, dynamic newValue, {bool notify = true}) {
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
class Durations {
|
||||
// Flutter animations (with margin)
|
||||
static const popupMenuAnimation = Duration(milliseconds: 300 + 10); // ref `_kMenuDuration` used in `_PopupMenuRoute`
|
||||
static const dialogTransitionAnimation = Duration(milliseconds: 150 + 10); // ref `transitionDuration` used in `DialogRoute`
|
||||
static const toggleableTransitionAnimation = Duration(milliseconds: 200 + 10); // ref `_kToggleDuration` used in `ToggleableStateMixin`
|
||||
|
||||
// common animations
|
||||
static const iconAnimation = Duration(milliseconds: 300);
|
||||
static const sweeperOpacityAnimation = Duration(milliseconds: 150);
|
||||
static const sweepingAnimation = Duration(milliseconds: 650);
|
||||
static const popupMenuAnimation = Duration(milliseconds: 300); // ref _PopupMenuRoute._kMenuDuration
|
||||
static const dialogTransitionAnimation = Duration(milliseconds: 150); // ref `transitionDuration` in `showDialog()`
|
||||
|
||||
static const staggeredAnimation = Duration(milliseconds: 375);
|
||||
static const staggeredAnimationPageTarget = Duration(milliseconds: 900);
|
||||
|
@ -42,6 +45,10 @@ class Durations {
|
|||
static const mapStyleSwitchAnimation = Duration(milliseconds: 300);
|
||||
static const xmpStructArrayCardTransition = Duration(milliseconds: 300);
|
||||
|
||||
// settings animations
|
||||
static const quickActionListAnimation = Duration(milliseconds: 200);
|
||||
static const quickActionHighlightAnimation = Duration(milliseconds: 200);
|
||||
|
||||
// delays & refresh intervals
|
||||
static const opToastDisplay = Duration(seconds: 3);
|
||||
static const collectionScrollMonitoringTimerDelay = Duration(milliseconds: 100);
|
||||
|
|
|
@ -320,6 +320,8 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
title: context.l10n.collectionGroupTitle,
|
||||
),
|
||||
);
|
||||
// wait for the dialog to hide as applying the change may block the UI
|
||||
await Future.delayed(Durations.dialogTransitionAnimation * timeDilation);
|
||||
if (value != null) {
|
||||
settings.collectionGroupFactor = value;
|
||||
collection.group(value);
|
||||
|
@ -338,6 +340,8 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
title: context.l10n.collectionSortTitle,
|
||||
),
|
||||
);
|
||||
// wait for the dialog to hide as applying the change may block the UI
|
||||
await Future.delayed(Durations.dialogTransitionAnimation * timeDilation);
|
||||
if (value != null) {
|
||||
settings.collectionSortFactor = value;
|
||||
collection.sort(value);
|
||||
|
|
|
@ -123,7 +123,7 @@ class ScrollLabel extends StatelessWidget {
|
|||
child: Material(
|
||||
elevation: 4.0,
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.all(Radius.circular(16.0)),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -9,7 +9,7 @@ class LinkChip extends StatelessWidget {
|
|||
final Color color;
|
||||
final TextStyle textStyle;
|
||||
|
||||
static const borderRadius = BorderRadius.all(Radius.circular(8));
|
||||
static final borderRadius = BorderRadius.circular(8);
|
||||
|
||||
const LinkChip({
|
||||
Key key,
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:expansion_tile_card/expansion_tile_card.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class AvesExpansionTile extends StatelessWidget {
|
||||
final String value;
|
||||
final Widget leading;
|
||||
final String title;
|
||||
final Color color;
|
||||
|
@ -11,6 +12,7 @@ class AvesExpansionTile extends StatelessWidget {
|
|||
final List<Widget> children;
|
||||
|
||||
const AvesExpansionTile({
|
||||
String value,
|
||||
this.leading,
|
||||
@required this.title,
|
||||
this.color,
|
||||
|
@ -18,7 +20,7 @@ class AvesExpansionTile extends StatelessWidget {
|
|||
this.initiallyExpanded = false,
|
||||
this.showHighlight = true,
|
||||
@required this.children,
|
||||
});
|
||||
}): value = value ?? title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -44,8 +46,8 @@ class AvesExpansionTile extends StatelessWidget {
|
|||
accentColor: Colors.white,
|
||||
),
|
||||
child: ExpansionTileCard(
|
||||
key: Key('tilecard-$title'),
|
||||
value: title,
|
||||
key: Key('tilecard-$value'),
|
||||
value: value,
|
||||
expandedNotifier: expandedNotifier,
|
||||
title: titleChild,
|
||||
expandable: enabled,
|
||||
|
|
|
@ -44,7 +44,7 @@ class AvesFilterChip extends StatefulWidget {
|
|||
this.showGenericIcon = true,
|
||||
this.background,
|
||||
this.details,
|
||||
this.borderRadius = const BorderRadius.all(Radius.circular(defaultRadius)),
|
||||
this.borderRadius,
|
||||
this.padding = 6.0,
|
||||
this.heroType = HeroType.onTap,
|
||||
this.onTap,
|
||||
|
@ -96,8 +96,6 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
|||
|
||||
CollectionFilter get filter => widget.filter;
|
||||
|
||||
BorderRadius get borderRadius => widget.borderRadius;
|
||||
|
||||
double get padding => widget.padding;
|
||||
|
||||
FilterCallback get onTap => widget.onTap;
|
||||
|
@ -197,6 +195,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
|||
);
|
||||
}
|
||||
|
||||
final borderRadius = widget.borderRadius ?? BorderRadius.circular(AvesFilterChip.defaultRadius);
|
||||
Widget chip = Container(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: AvesFilterChip.minChipWidth,
|
||||
|
|
|
@ -12,7 +12,7 @@ ScrollThumbBuilder avesScrollThumbBuilder({
|
|||
final scrollThumb = Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black26,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
height: height,
|
||||
margin: EdgeInsets.only(right: .5),
|
||||
|
@ -23,7 +23,7 @@ ScrollThumbBuilder avesScrollThumbBuilder({
|
|||
width: 20.0,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -37,6 +37,7 @@ class DebugSettingsSection extends StatelessWidget {
|
|||
'tileExtent - Countries': '${settings.getTileExtent(CountryListPage.routeName)}',
|
||||
'tileExtent - Tags': '${settings.getTileExtent(TagListPage.routeName)}',
|
||||
'infoMapZoom': '${settings.infoMapZoom}',
|
||||
'viewerQuickActions': '${settings.viewerQuickActions}',
|
||||
'pinnedFilters': toMultiline(settings.pinnedFilters),
|
||||
'hiddenFilters': toMultiline(settings.hiddenFilters),
|
||||
'searchHistory': toMultiline(settings.searchHistory),
|
||||
|
|
|
@ -108,7 +108,7 @@ class _AddShortcutDialogState extends State<AddShortcutDialog> {
|
|||
return GestureDetector(
|
||||
onTap: _pickEntry,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.all(Radius.circular(32)),
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
child: SizedBox(
|
||||
width: extent,
|
||||
height: extent,
|
||||
|
|
|
@ -53,11 +53,7 @@ class _AvesSelectionDialogState<T> extends State<AvesSelectionDialog<T>> {
|
|||
key: Key(value.toString()),
|
||||
value: value,
|
||||
groupValue: _selectedValue,
|
||||
onChanged: (v) {
|
||||
_selectedValue = v;
|
||||
Navigator.pop(context, _selectedValue);
|
||||
setState(() {});
|
||||
},
|
||||
onChanged: (v) => Navigator.pop(context, v),
|
||||
reselectable: true,
|
||||
title: Text(
|
||||
title,
|
||||
|
|
|
@ -2,10 +2,12 @@ import 'package:aves/model/actions/chip_actions.dart';
|
|||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
||||
import 'package:aves/widgets/stats/stats.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
abstract class ChipSetActionDelegate {
|
||||
|
@ -39,6 +41,8 @@ abstract class ChipSetActionDelegate {
|
|||
title: context.l10n.chipSortTitle,
|
||||
),
|
||||
);
|
||||
// wait for the dialog to hide as applying the change may block the UI
|
||||
await Future.delayed(Durations.dialogTransitionAnimation * timeDilation);
|
||||
if (factor != null) {
|
||||
sortFactor = factor;
|
||||
}
|
||||
|
@ -90,6 +94,8 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate {
|
|||
title: context.l10n.albumGroupTitle,
|
||||
),
|
||||
);
|
||||
// wait for the dialog to hide as applying the change may block the UI
|
||||
await Future.delayed(Durations.dialogTransitionAnimation * timeDilation);
|
||||
if (factor != null) {
|
||||
settings.albumGroupFactor = factor;
|
||||
}
|
||||
|
|
|
@ -93,7 +93,7 @@ class DecoratedFilterChip extends StatelessWidget {
|
|||
);
|
||||
final radius = min<double>(AvesFilterChip.defaultRadius, extent / 4);
|
||||
final titlePadding = min<double>(4.0, extent / 32);
|
||||
final borderRadius = BorderRadius.all(Radius.circular(radius));
|
||||
final borderRadius = BorderRadius.circular(radius);
|
||||
Widget child = AvesFilterChip(
|
||||
filter: filter,
|
||||
showGenericIcon: false,
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import 'dart:collection';
|
||||
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
|
||||
|
@ -30,6 +32,8 @@ class LanguageTile extends StatelessWidget {
|
|||
title: context.l10n.settingsLanguage,
|
||||
),
|
||||
);
|
||||
// wait for the dialog to hide as applying the change may block the UI
|
||||
await Future.delayed(Durations.dialogTransitionAnimation * timeDilation);
|
||||
if (value != null) {
|
||||
settings.locale = value == _systemLocaleOption ? null : value;
|
||||
}
|
||||
|
|
103
lib/widgets/settings/quick_actions/available_actions.dart
Normal file
103
lib/widgets/settings/quick_actions/available_actions.dart
Normal file
|
@ -0,0 +1,103 @@
|
|||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/settings/quick_actions/common.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AvailableActionPanel extends StatelessWidget {
|
||||
final List<EntryAction> quickActions;
|
||||
final Listenable quickActionsChangeNotifier;
|
||||
final ValueNotifier<bool> panelHighlight;
|
||||
final ValueNotifier<EntryAction> draggedQuickAction;
|
||||
final ValueNotifier<EntryAction> draggedAvailableAction;
|
||||
final bool Function(EntryAction action) removeQuickAction;
|
||||
|
||||
const AvailableActionPanel({
|
||||
@required this.quickActions,
|
||||
@required this.quickActionsChangeNotifier,
|
||||
@required this.panelHighlight,
|
||||
@required this.draggedQuickAction,
|
||||
@required this.draggedAvailableAction,
|
||||
@required this.removeQuickAction,
|
||||
});
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return DragTarget<EntryAction>(
|
||||
onWillAccept: (data) {
|
||||
if (draggedQuickAction.value != null) {
|
||||
_setPanelHighlight(true);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
onAcceptWithDetails: (details) {
|
||||
removeQuickAction(draggedQuickAction.value);
|
||||
_setDraggedQuickAction(null);
|
||||
_setPanelHighlight(false);
|
||||
},
|
||||
onLeave: (data) => _setPanelHighlight(false),
|
||||
builder: (context, accepted, rejected) {
|
||||
return AnimatedBuilder(
|
||||
animation: Listenable.merge([quickActionsChangeNotifier, draggedAvailableAction]),
|
||||
builder: (context, child) => Padding(
|
||||
padding: 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);
|
||||
Widget child = ActionButton(
|
||||
action: action,
|
||||
enabled: enabled,
|
||||
);
|
||||
if (dragged) {
|
||||
child = DraggedPlaceholder(child: child);
|
||||
}
|
||||
if (enabled) {
|
||||
child = _buildDraggable(action, child);
|
||||
}
|
||||
return child;
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDraggable(EntryAction action, Widget child) => LongPressDraggable<EntryAction>(
|
||||
data: action,
|
||||
maxSimultaneousDrags: 1,
|
||||
onDragStarted: () => _setDraggedAvailableAction(action),
|
||||
onDragEnd: (details) => _setDraggedAvailableAction(null),
|
||||
feedback: MediaQueryDataProvider(
|
||||
child: ActionButton(
|
||||
action: action,
|
||||
showCaption: false,
|
||||
),
|
||||
),
|
||||
childWhenDragging: child,
|
||||
child: child,
|
||||
);
|
||||
|
||||
void _setDraggedQuickAction(EntryAction action) => draggedQuickAction.value = action;
|
||||
|
||||
void _setDraggedAvailableAction(EntryAction action) => draggedAvailableAction.value = action;
|
||||
|
||||
void _setPanelHighlight(bool flag) => panelHighlight.value = flag;
|
||||
}
|
95
lib/widgets/settings/quick_actions/common.dart
Normal file
95
lib/widgets/settings/quick_actions/common.dart
Normal file
|
@ -0,0 +1,95 @@
|
|||
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.all(
|
||||
color: color,
|
||||
width: highlight ? 2 : 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
margin: EdgeInsets.all(16),
|
||||
duration: Durations.quickActionHighlightAnimation,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.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: [
|
||||
SizedBox(height: padding),
|
||||
OverlayButton(
|
||||
child: IconButton(
|
||||
icon: Icon(action.getIcon()),
|
||||
onPressed: enabled ? () {} : null,
|
||||
),
|
||||
),
|
||||
if (showCaption) ...[
|
||||
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,
|
||||
),
|
||||
],
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
307
lib/widgets/settings/quick_actions/editor.dart
Normal file
307
lib/widgets/settings/quick_actions/editor.dart
Normal file
|
@ -0,0 +1,307 @@
|
|||
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/icons.dart';
|
||||
import 'package:aves/utils/change_notifier.dart';
|
||||
import 'package:aves/utils/constants.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/settings/quick_actions/available_actions.dart';
|
||||
import 'package:aves/widgets/settings/quick_actions/common.dart';
|
||||
import 'package:aves/widgets/settings/quick_actions/quick_actions.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/common.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class QuickActionsTile extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(context.l10n.settingsViewerQuickActionsTile),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: RouteSettings(name: QuickActionEditorPage.routeName),
|
||||
builder: (context) => QuickActionEditorPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class QuickActionEditorPage extends StatefulWidget {
|
||||
static const routeName = '/settings/quick_actions';
|
||||
|
||||
@override
|
||||
_QuickActionEditorPageState createState() => _QuickActionEditorPageState();
|
||||
}
|
||||
|
||||
class _QuickActionEditorPageState extends State<QuickActionEditorPage> {
|
||||
final GlobalKey<AnimatedListState> _animatedListKey = GlobalKey(debugLabel: 'quick-actions-animated-list');
|
||||
Timer _targetLeavingTimer;
|
||||
List<EntryAction> _quickActions;
|
||||
final ValueNotifier<EntryAction> _draggedQuickAction = ValueNotifier(null);
|
||||
final ValueNotifier<EntryAction> _draggedAvailableAction = ValueNotifier(null);
|
||||
final ValueNotifier<bool> _quickActionHighlight = ValueNotifier(false);
|
||||
final ValueNotifier<bool> _availableActionHighlight = ValueNotifier(false);
|
||||
final AChangeNotifier _quickActionsChangeNotifier = AChangeNotifier();
|
||||
|
||||
// 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
|
||||
bool _reordering = false;
|
||||
|
||||
static const quickActionVerticalPadding = 16.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_quickActions = settings.viewerQuickActions.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stopLeavingTimer();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onQuickActionTargetLeave() {
|
||||
_stopLeavingTimer();
|
||||
final action = _draggedAvailableAction.value;
|
||||
_targetLeavingTimer = Timer(Durations.quickActionListAnimation + Duration(milliseconds: 50), () {
|
||||
_removeQuickAction(action);
|
||||
_quickActionHighlight.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final header = QuickActionButton(
|
||||
placement: QuickActionPlacement.header,
|
||||
panelHighlight: _quickActionHighlight,
|
||||
draggedQuickAction: _draggedQuickAction,
|
||||
draggedAvailableAction: _draggedAvailableAction,
|
||||
insertAction: _insertQuickAction,
|
||||
removeAction: _removeQuickAction,
|
||||
onTargetLeave: _onQuickActionTargetLeave,
|
||||
);
|
||||
final footer = QuickActionButton(
|
||||
placement: QuickActionPlacement.footer,
|
||||
panelHighlight: _quickActionHighlight,
|
||||
draggedQuickAction: _draggedQuickAction,
|
||||
draggedAvailableAction: _draggedAvailableAction,
|
||||
insertAction: _insertQuickAction,
|
||||
removeAction: _removeQuickAction,
|
||||
onTargetLeave: _onQuickActionTargetLeave,
|
||||
);
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.settingsViewerQuickActionEditorTitle),
|
||||
),
|
||||
body: WillPopScope(
|
||||
onWillPop: () {
|
||||
settings.viewerQuickActions = _quickActions;
|
||||
return SynchronousFuture(true);
|
||||
},
|
||||
child: SafeArea(
|
||||
child: ListView(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(AIcons.info),
|
||||
SizedBox(width: 16),
|
||||
Expanded(child: Text(context.l10n.settingsViewerQuickActionEditorBanner)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
context.l10n.settingsViewerQuickActionEditorDisplayedButtons,
|
||||
style: Constants.titleTextStyle,
|
||||
),
|
||||
),
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: _quickActionHighlight,
|
||||
builder: (context, highlight, child) => ActionPanel(
|
||||
highlight: highlight,
|
||||
child: child,
|
||||
),
|
||||
child: Container(
|
||||
height: OverlayButton.getSize(context) + quickActionVerticalPadding * 2,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: FractionallySizedBox(
|
||||
alignment: Alignment.centerLeft,
|
||||
widthFactor: .5,
|
||||
child: header,
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: FractionallySizedBox(
|
||||
alignment: Alignment.centerRight,
|
||||
widthFactor: .5,
|
||||
child: footer,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
child: AnimatedList(
|
||||
key: _animatedListKey,
|
||||
initialItemCount: _quickActions.length,
|
||||
scrollDirection: Axis.horizontal,
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (context, index, animation) {
|
||||
if (index >= _quickActions.length) return null;
|
||||
final action = _quickActions[index];
|
||||
return QuickActionButton(
|
||||
placement: QuickActionPlacement.action,
|
||||
action: action,
|
||||
panelHighlight: _quickActionHighlight,
|
||||
draggedQuickAction: _draggedQuickAction,
|
||||
draggedAvailableAction: _draggedAvailableAction,
|
||||
insertAction: _insertQuickAction,
|
||||
removeAction: _removeQuickAction,
|
||||
onTargetLeave: _onQuickActionTargetLeave,
|
||||
child: _buildQuickActionButton(action, animation),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
AnimatedBuilder(
|
||||
animation: _quickActionsChangeNotifier,
|
||||
builder: (context, child) => _quickActions.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
context.l10n.settingsViewerQuickActionEmpty,
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
),
|
||||
)
|
||||
: SizedBox(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
context.l10n.settingsViewerQuickActionEditorAvailableButtons,
|
||||
style: Constants.titleTextStyle,
|
||||
),
|
||||
),
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: _availableActionHighlight,
|
||||
builder: (context, highlight, child) => ActionPanel(
|
||||
highlight: highlight,
|
||||
child: child,
|
||||
),
|
||||
child: AvailableActionPanel(
|
||||
quickActions: _quickActions,
|
||||
quickActionsChangeNotifier: _quickActionsChangeNotifier,
|
||||
panelHighlight: _availableActionHighlight,
|
||||
draggedQuickAction: _draggedQuickAction,
|
||||
draggedAvailableAction: _draggedAvailableAction,
|
||||
removeQuickAction: _removeQuickAction,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _stopLeavingTimer() => _targetLeavingTimer?.cancel();
|
||||
|
||||
bool _insertQuickAction(EntryAction action, QuickActionPlacement placement, EntryAction overAction) {
|
||||
if (action == null) return false;
|
||||
_stopLeavingTimer();
|
||||
if (_reordering) return false;
|
||||
|
||||
final currentIndex = _quickActions.indexOf(action);
|
||||
final contained = currentIndex != -1;
|
||||
int targetIndex;
|
||||
switch (placement) {
|
||||
case QuickActionPlacement.header:
|
||||
targetIndex = 0;
|
||||
break;
|
||||
case QuickActionPlacement.footer:
|
||||
targetIndex = _quickActions.length - (contained ? 1 : 0);
|
||||
break;
|
||||
case QuickActionPlacement.action:
|
||||
targetIndex = _quickActions.indexOf(overAction);
|
||||
break;
|
||||
}
|
||||
if (currentIndex == targetIndex) return false;
|
||||
|
||||
_reordering = true;
|
||||
_removeQuickAction(action);
|
||||
_quickActions.insert(targetIndex, action);
|
||||
_animatedListKey.currentState.insertItem(
|
||||
targetIndex,
|
||||
duration: Durations.quickActionListAnimation,
|
||||
);
|
||||
_quickActionsChangeNotifier.notifyListeners();
|
||||
Future.delayed(Durations.quickActionListAnimation).then((value) => _reordering = false);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _removeQuickAction(EntryAction action) {
|
||||
if (!_quickActions.contains(action)) return false;
|
||||
|
||||
final index = _quickActions.indexOf(action);
|
||||
_quickActions.removeAt(index);
|
||||
_animatedListKey.currentState.removeItem(
|
||||
index,
|
||||
(context, animation) => DraggedPlaceholder(child: _buildQuickActionButton(action, animation)),
|
||||
duration: Durations.quickActionListAnimation,
|
||||
);
|
||||
_quickActionsChangeNotifier.notifyListeners();
|
||||
return true;
|
||||
}
|
||||
|
||||
Widget _buildQuickActionButton(EntryAction action, Animation<double> animation) {
|
||||
animation = animation.drive(CurveTween(curve: Curves.easeInOut));
|
||||
Widget child = FadeTransition(
|
||||
opacity: animation,
|
||||
child: SizeTransition(
|
||||
axis: Axis.horizontal,
|
||||
sizeFactor: animation,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: _QuickActionEditorPageState.quickActionVerticalPadding, horizontal: 4),
|
||||
child: OverlayButton(
|
||||
child: IconButton(
|
||||
icon: Icon(action.getIcon()),
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
child = AnimatedBuilder(
|
||||
animation: Listenable.merge([_draggedQuickAction, _draggedAvailableAction]),
|
||||
builder: (context, child) {
|
||||
final dragged = _draggedQuickAction.value == action || _draggedAvailableAction.value == action;
|
||||
if (dragged) {
|
||||
child = DraggedPlaceholder(child: child);
|
||||
}
|
||||
return child;
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
|
||||
return child;
|
||||
}
|
||||
}
|
80
lib/widgets/settings/quick_actions/quick_actions.dart
Normal file
80
lib/widgets/settings/quick_actions/quick_actions.dart
Normal file
|
@ -0,0 +1,80 @@
|
|||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/settings/quick_actions/common.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum QuickActionPlacement { header, action, footer }
|
||||
|
||||
class QuickActionButton extends StatelessWidget {
|
||||
final QuickActionPlacement placement;
|
||||
final EntryAction action;
|
||||
final ValueNotifier<bool> panelHighlight;
|
||||
final ValueNotifier<EntryAction> draggedQuickAction;
|
||||
final ValueNotifier<EntryAction> draggedAvailableAction;
|
||||
final bool Function(EntryAction action, QuickActionPlacement placement, EntryAction overAction) insertAction;
|
||||
final bool Function(EntryAction action) removeAction;
|
||||
final VoidCallback onTargetLeave;
|
||||
final Widget child;
|
||||
|
||||
const QuickActionButton({
|
||||
@required this.placement,
|
||||
this.action,
|
||||
@required this.panelHighlight,
|
||||
@required this.draggedQuickAction,
|
||||
@required this.draggedAvailableAction,
|
||||
@required this.insertAction,
|
||||
@required this.removeAction,
|
||||
@required this.onTargetLeave,
|
||||
this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var child = this.child;
|
||||
child = _buildDragTarget(child);
|
||||
if (action != null) {
|
||||
child = _buildDraggable(child);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
DragTarget<EntryAction> _buildDragTarget(Widget child) {
|
||||
return DragTarget<EntryAction>(
|
||||
onWillAccept: (data) {
|
||||
if (draggedQuickAction.value != null) {
|
||||
insertAction(draggedQuickAction.value, placement, action);
|
||||
}
|
||||
if (draggedAvailableAction.value != null) {
|
||||
insertAction(draggedAvailableAction.value, placement, action);
|
||||
_setPanelHighlight(true);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
onAcceptWithDetails: (details) => _setPanelHighlight(false),
|
||||
onLeave: (data) => onTargetLeave(),
|
||||
builder: (context, accepted, rejected) => child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDraggable(Widget child) => LongPressDraggable(
|
||||
data: action,
|
||||
maxSimultaneousDrags: 1,
|
||||
onDragStarted: () => _setDraggedQuickAction(action),
|
||||
// `onDragEnd` is only called when the widget is mounted,
|
||||
// so we rely on `onDraggableCanceled` and `onDragCompleted` instead
|
||||
onDraggableCanceled: (velocity, offset) => _setDraggedQuickAction(null),
|
||||
onDragCompleted: () => _setDraggedQuickAction(null),
|
||||
feedback: MediaQueryDataProvider(
|
||||
child: ActionButton(
|
||||
action: action,
|
||||
showCaption: false,
|
||||
),
|
||||
),
|
||||
childWhenDragging: child,
|
||||
child: child,
|
||||
);
|
||||
|
||||
void _setDraggedQuickAction(EntryAction action) => draggedQuickAction.value = action;
|
||||
|
||||
void _setPanelHighlight(bool flag) => panelHighlight.value = flag;
|
||||
}
|
|
@ -18,6 +18,7 @@ import 'package:aves/widgets/settings/access_grants.dart';
|
|||
import 'package:aves/widgets/settings/entry_background.dart';
|
||||
import 'package:aves/widgets/settings/hidden_filters.dart';
|
||||
import 'package:aves/widgets/settings/language.dart';
|
||||
import 'package:aves/widgets/settings/quick_actions/editor.dart';
|
||||
import 'package:decorated_icon/decorated_icon.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
|
@ -131,8 +132,8 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
}
|
||||
|
||||
Widget _buildThumbnailsSection(BuildContext context) {
|
||||
final iconColor = IconTheme.of(context).color;
|
||||
Color iconColorFor(bool enabled) => iconColor.withOpacity(enabled ? 1 : .12);
|
||||
final iconSize = IconTheme.of(context).size * MediaQuery.of(context).textScaleFactor;
|
||||
double opacityFor(bool enabled) => enabled ? 1 : .2;
|
||||
return AvesExpansionTile(
|
||||
leading: _buildLeading(AIcons.grid, stringToColor('Thumbnails')),
|
||||
title: context.l10n.settingsSectionThumbnails,
|
||||
|
@ -145,9 +146,13 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
title: Row(
|
||||
children: [
|
||||
Expanded(child: Text(context.l10n.settingsThumbnailShowLocationIcon)),
|
||||
Icon(
|
||||
AIcons.location,
|
||||
color: iconColorFor(settings.showThumbnailLocation),
|
||||
AnimatedOpacity(
|
||||
opacity: opacityFor(settings.showThumbnailLocation),
|
||||
duration: Durations.toggleableTransitionAnimation,
|
||||
child: Icon(
|
||||
AIcons.location,
|
||||
size: iconSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -158,9 +163,13 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
title: Row(
|
||||
children: [
|
||||
Expanded(child: Text(context.l10n.settingsThumbnailShowRawIcon)),
|
||||
Icon(
|
||||
AIcons.raw,
|
||||
color: iconColorFor(settings.showThumbnailRaw),
|
||||
AnimatedOpacity(
|
||||
opacity: opacityFor(settings.showThumbnailRaw),
|
||||
duration: Durations.toggleableTransitionAnimation,
|
||||
child: Icon(
|
||||
AIcons.raw,
|
||||
size: iconSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -181,20 +190,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
expandedNotifier: _expandedNotifier,
|
||||
showHighlight: false,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(context.l10n.settingsRasterImageBackground),
|
||||
trailing: EntryBackgroundSelector(
|
||||
getter: () => settings.rasterBackground,
|
||||
setter: (value) => settings.rasterBackground = value,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.l10n.settingsVectorImageBackground),
|
||||
trailing: EntryBackgroundSelector(
|
||||
getter: () => settings.vectorBackground,
|
||||
setter: (value) => settings.vectorBackground = value,
|
||||
),
|
||||
),
|
||||
QuickActionsTile(),
|
||||
SwitchListTile(
|
||||
value: settings.showOverlayMinimap,
|
||||
onChanged: (v) => settings.showOverlayMinimap = v,
|
||||
|
@ -211,6 +207,20 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
onChanged: settings.showOverlayInfo ? (v) => settings.showOverlayShootingDetails = v : null,
|
||||
title: Text(context.l10n.settingsViewerShowShootingDetails),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.l10n.settingsRasterImageBackground),
|
||||
trailing: EntryBackgroundSelector(
|
||||
getter: () => settings.rasterBackground,
|
||||
setter: (value) => settings.rasterBackground = value,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.l10n.settingsVectorImageBackground),
|
||||
trailing: EntryBackgroundSelector(
|
||||
getter: () => settings.vectorBackground,
|
||||
setter: (value) => settings.vectorBackground = value,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -263,6 +273,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
|
||||
Widget _buildLanguageSection(BuildContext context) {
|
||||
return AvesExpansionTile(
|
||||
// use a fixed value instead of the title to identify this expansion tile
|
||||
// so that the tile state is kept when the language is modified
|
||||
value: 'language',
|
||||
leading: _buildLeading(AIcons.language, stringToColor('Language')),
|
||||
title: context.l10n.settingsSectionLanguage,
|
||||
expandedNotifier: _expandedNotifier,
|
||||
|
|
|
@ -17,7 +17,7 @@ import 'package:flutter/scheduler.dart';
|
|||
class MapDecorator extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
static const BorderRadius mapBorderRadius = BorderRadius.all(Radius.circular(24)); // to match button circles
|
||||
static final BorderRadius mapBorderRadius = BorderRadius.circular(24); // to match button circles
|
||||
|
||||
const MapDecorator({@required this.child});
|
||||
|
||||
|
@ -90,7 +90,7 @@ class MapButtonPanel extends StatelessWidget {
|
|||
);
|
||||
},
|
||||
);
|
||||
// wait for the dialog to hide because switching to Google Maps layer may block the UI
|
||||
// wait for the dialog to hide as applying the change may block the UI
|
||||
await Future.delayed(Durations.dialogTransitionAnimation * timeDilation);
|
||||
if (style != null && style != settings.infoMapStyle) {
|
||||
settings.infoMapStyle = style;
|
||||
|
|
|
@ -33,6 +33,9 @@ class OverlayButton extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
// icon (24) + icon padding (8) + button padding (16) + border (2)
|
||||
static double getSize(BuildContext context) => 50.0;
|
||||
}
|
||||
|
||||
class OverlayTextButton extends StatelessWidget {
|
||||
|
@ -66,7 +69,7 @@ class OverlayTextButton extends StatelessWidget {
|
|||
minimumSize: _minSize,
|
||||
side: MaterialStateProperty.all<BorderSide>(AvesCircleBorder.buildSide(context)),
|
||||
shape: MaterialStateProperty.all<OutlinedBorder>(RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(_borderRadius)),
|
||||
borderRadius: BorderRadius.circular(_borderRadius),
|
||||
)),
|
||||
// shape: MaterialStateProperty.all<OutlinedBorder>(CircleBorder()),
|
||||
),
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/favourites.dart';
|
||||
|
@ -17,7 +15,6 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class ViewerTopOverlay extends StatelessWidget {
|
||||
final AvesEntry entry;
|
||||
|
@ -30,10 +27,6 @@ class ViewerTopOverlay extends StatelessWidget {
|
|||
|
||||
static const double padding = 8;
|
||||
|
||||
static const int landscapeActionCount = 3;
|
||||
|
||||
static const int portraitActionCount = 2;
|
||||
|
||||
const ViewerTopOverlay({
|
||||
Key key,
|
||||
@required this.entry,
|
||||
|
@ -52,21 +45,11 @@ class ViewerTopOverlay extends StatelessWidget {
|
|||
minimum: (viewInsets ?? EdgeInsets.zero) + (viewPadding ?? EdgeInsets.zero),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(padding),
|
||||
child: Selector<MediaQueryData, Tuple2<double, Orientation>>(
|
||||
selector: (c, mq) => Tuple2(mq.size.width, mq.orientation),
|
||||
builder: (c, mq, child) {
|
||||
final mqWidth = mq.item1;
|
||||
final mqOrientation = mq.item2;
|
||||
|
||||
final targetCount = mqOrientation == Orientation.landscape ? landscapeActionCount : portraitActionCount;
|
||||
final availableCount = (mqWidth / (kMinInteractiveDimension + padding)).floor() - 2;
|
||||
final quickActionCount = min(targetCount, availableCount);
|
||||
|
||||
final quickActions = [
|
||||
EntryAction.toggleFavourite,
|
||||
EntryAction.share,
|
||||
EntryAction.delete,
|
||||
].where(_canDo).take(quickActionCount).toList();
|
||||
child: Selector<MediaQueryData, double>(
|
||||
selector: (c, mq) => mq.size.width - mq.padding.horizontal,
|
||||
builder: (c, mqWidth, child) {
|
||||
final availableCount = (mqWidth / (OverlayButton.getSize(context) + padding)).floor() - 2;
|
||||
final quickActions = settings.viewerQuickActions.where(_canDo).take(availableCount).toList();
|
||||
final inAppActions = EntryActions.inApp.where((action) => !quickActions.contains(action)).where(_canDo).toList();
|
||||
final externalAppActions = EntryActions.externalApp.where(_canDo).toList();
|
||||
final buttonRow = _TopOverlayRow(
|
||||
|
|
Loading…
Reference in a new issue