#437 tv: collection app bar
This commit is contained in:
parent
3ca33d0608
commit
6c71752f18
4 changed files with 111 additions and 27 deletions
|
@ -32,6 +32,7 @@ import 'package:aves/widgets/common/search/route.dart';
|
||||||
import 'package:aves/widgets/dialogs/tile_view_dialog.dart';
|
import 'package:aves/widgets/dialogs/tile_view_dialog.dart';
|
||||||
import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart';
|
import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart';
|
||||||
import 'package:aves/widgets/search/search_delegate.dart';
|
import 'package:aves/widgets/search/search_delegate.dart';
|
||||||
|
import 'package:aves/widgets/settings/common/quick_actions/action_button.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -159,6 +160,8 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
return Selector<Settings, List<EntrySetAction>>(
|
return Selector<Settings, List<EntrySetAction>>(
|
||||||
selector: (context, s) => s.collectionBrowsingQuickActions,
|
selector: (context, s) => s.collectionBrowsingQuickActions,
|
||||||
builder: (context, _, child) {
|
builder: (context, _, child) {
|
||||||
|
final isTelevision = device.isTelevision;
|
||||||
|
final actions = _buildActions(context, selection);
|
||||||
return AvesAppBar(
|
return AvesAppBar(
|
||||||
contentHeight: appBarContentHeight,
|
contentHeight: appBarContentHeight,
|
||||||
leading: _buildAppBarLeading(
|
leading: _buildAppBarLeading(
|
||||||
|
@ -166,9 +169,18 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
isSelecting: isSelecting,
|
isSelecting: isSelecting,
|
||||||
),
|
),
|
||||||
title: _buildAppBarTitle(isSelecting),
|
title: _buildAppBarTitle(isSelecting),
|
||||||
actions: _buildActions(context, selection),
|
actions: isTelevision ? [] : actions,
|
||||||
bottom: Column(
|
bottom: Column(
|
||||||
children: [
|
children: [
|
||||||
|
if (isTelevision)
|
||||||
|
SizedBox(
|
||||||
|
height: tvActionButtonHeight,
|
||||||
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
children: actions,
|
||||||
|
),
|
||||||
|
),
|
||||||
if (showFilterBar)
|
if (showFilterBar)
|
||||||
NotificationListener<ReverseFilterNotification>(
|
NotificationListener<ReverseFilterNotification>(
|
||||||
onNotification: (notification) {
|
onNotification: (notification) {
|
||||||
|
@ -198,12 +210,32 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
double get appBarContentHeight {
|
double get tvActionButtonHeight {
|
||||||
final hasQuery = context.read<Query>().enabled;
|
final text = [
|
||||||
return kToolbarHeight + (showFilterBar ? FilterBar.preferredHeight : .0) + (hasQuery ? EntryQueryBar.preferredHeight : .0);
|
...EntrySetActions.general,
|
||||||
|
...EntrySetActions.pageBrowsing,
|
||||||
|
...EntrySetActions.pageSelection,
|
||||||
|
].map((action) => action.getText(context)).fold('', (prev, v) => v.length > prev.length ? v : prev);
|
||||||
|
return ActionButton.getSize(context, text, showCaption: true).height;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAppBarLeading({required bool hasDrawer, required bool isSelecting}) {
|
double get appBarContentHeight {
|
||||||
|
double height = kToolbarHeight;
|
||||||
|
if (device.isTelevision) {
|
||||||
|
height += tvActionButtonHeight;
|
||||||
|
}
|
||||||
|
if (showFilterBar) {
|
||||||
|
height += FilterBar.preferredHeight;
|
||||||
|
}
|
||||||
|
if (context.read<Query>().enabled) {
|
||||||
|
height += EntryQueryBar.preferredHeight;
|
||||||
|
}
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget? _buildAppBarLeading({required bool hasDrawer, required bool isSelecting}) {
|
||||||
|
if (device.isTelevision) return null;
|
||||||
|
|
||||||
if (!hasDrawer) {
|
if (!hasDrawer) {
|
||||||
return const CloseButton();
|
return const CloseButton();
|
||||||
}
|
}
|
||||||
|
@ -265,10 +297,10 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildActions(BuildContext context, Selection<AvesEntry> selection) {
|
List<Widget> _buildActions(BuildContext context, Selection<AvesEntry> selection) {
|
||||||
|
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
||||||
final isSelecting = selection.isSelecting;
|
final isSelecting = selection.isSelecting;
|
||||||
final selectedItemCount = selection.selectedItems.length;
|
final selectedItemCount = selection.selectedItems.length;
|
||||||
|
|
||||||
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
|
||||||
bool isVisible(EntrySetAction action) => _actionDelegate.isVisible(
|
bool isVisible(EntrySetAction action) => _actionDelegate.isVisible(
|
||||||
action,
|
action,
|
||||||
appMode: appMode,
|
appMode: appMode,
|
||||||
|
@ -283,12 +315,58 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
itemCount: collection.entryCount,
|
itemCount: collection.entryCount,
|
||||||
selectedItemCount: selectedItemCount,
|
selectedItemCount: selectedItemCount,
|
||||||
);
|
);
|
||||||
final canApplyEditActions = selectedItemCount > 0;
|
|
||||||
|
return device.isTelevision
|
||||||
|
? _buildTelevisionActions(
|
||||||
|
appMode: appMode,
|
||||||
|
selection: selection,
|
||||||
|
isVisible: isVisible,
|
||||||
|
canApply: canApply,
|
||||||
|
)
|
||||||
|
: _buildMobileActions(
|
||||||
|
appMode: appMode,
|
||||||
|
selection: selection,
|
||||||
|
isVisible: isVisible,
|
||||||
|
canApply: canApply,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildTelevisionActions({
|
||||||
|
required AppMode appMode,
|
||||||
|
required Selection<AvesEntry> selection,
|
||||||
|
required bool Function(EntrySetAction action) isVisible,
|
||||||
|
required bool Function(EntrySetAction action) canApply,
|
||||||
|
}) {
|
||||||
|
final isSelecting = selection.isSelecting;
|
||||||
|
|
||||||
|
return [
|
||||||
|
...EntrySetActions.general,
|
||||||
|
...isSelecting ? EntrySetActions.pageSelection : EntrySetActions.pageBrowsing,
|
||||||
|
].where(isVisible).map((action) {
|
||||||
|
// TODO TLAD [tv] togglers cf `_toIconActionButton`
|
||||||
|
return ActionButton(
|
||||||
|
text: action.getText(context),
|
||||||
|
icon: action.getIcon(),
|
||||||
|
enabled: canApply(action),
|
||||||
|
onPressed: canApply(action) ? () => _onActionSelected(action) : null,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildMobileActions({
|
||||||
|
required AppMode appMode,
|
||||||
|
required Selection<AvesEntry> selection,
|
||||||
|
required bool Function(EntrySetAction action) isVisible,
|
||||||
|
required bool Function(EntrySetAction action) canApply,
|
||||||
|
}) {
|
||||||
|
final isSelecting = selection.isSelecting;
|
||||||
|
final selectedItemCount = selection.selectedItems.length;
|
||||||
|
final hasSelection = selectedItemCount > 0;
|
||||||
|
|
||||||
final browsingQuickActions = settings.collectionBrowsingQuickActions;
|
final browsingQuickActions = settings.collectionBrowsingQuickActions;
|
||||||
final selectionQuickActions = isTrash ? [EntrySetAction.delete, EntrySetAction.restore] : settings.collectionSelectionQuickActions;
|
final selectionQuickActions = isTrash ? [EntrySetAction.delete, EntrySetAction.restore] : settings.collectionSelectionQuickActions;
|
||||||
final quickActionButtons = (isSelecting ? selectionQuickActions : browsingQuickActions).where(isVisible).map(
|
final quickActionButtons = (isSelecting ? selectionQuickActions : browsingQuickActions).where(isVisible).map(
|
||||||
(action) => _toActionButton(action, enabled: canApply(action), selection: selection),
|
(action) => _toIconActionButton(action, enabled: canApply(action), selection: selection),
|
||||||
);
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
@ -310,10 +388,10 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
),
|
),
|
||||||
if (isSelecting && !device.isReadOnly && appMode == AppMode.main && !isTrash)
|
if (isSelecting && !device.isReadOnly && appMode == AppMode.main && !isTrash)
|
||||||
PopupMenuItem<EntrySetAction>(
|
PopupMenuItem<EntrySetAction>(
|
||||||
enabled: canApplyEditActions,
|
enabled: hasSelection,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
child: PopupMenuItemExpansionPanel<EntrySetAction>(
|
child: PopupMenuItemExpansionPanel<EntrySetAction>(
|
||||||
enabled: canApplyEditActions,
|
enabled: hasSelection,
|
||||||
value: 'edit',
|
value: 'edit',
|
||||||
icon: AIcons.edit,
|
icon: AIcons.edit,
|
||||||
title: context.l10n.collectionActionEdit,
|
title: context.l10n.collectionActionEdit,
|
||||||
|
@ -350,7 +428,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
// key is expected by test driver (e.g. 'menu-configureView', 'menu-map')
|
// key is expected by test driver (e.g. 'menu-configureView', 'menu-map')
|
||||||
Key _getActionKey(EntrySetAction action) => Key('menu-${action.name}');
|
Key _getActionKey(EntrySetAction action) => Key('menu-${action.name}');
|
||||||
|
|
||||||
Widget _toActionButton(EntrySetAction action, {required bool enabled, required Selection<AvesEntry> selection}) {
|
Widget _toIconActionButton(EntrySetAction action, {required bool enabled, required Selection<AvesEntry> selection}) {
|
||||||
final onPressed = enabled ? () => _onActionSelected(action) : null;
|
final onPressed = enabled ? () => _onActionSelected(action) : null;
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case EntrySetAction.toggleTitleSearch:
|
case EntrySetAction.toggleTitleSearch:
|
||||||
|
|
|
@ -8,7 +8,7 @@ import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class AvesAppBar extends StatelessWidget {
|
class AvesAppBar extends StatelessWidget {
|
||||||
final double contentHeight;
|
final double contentHeight;
|
||||||
final Widget leading;
|
final Widget? leading;
|
||||||
final Widget title;
|
final Widget title;
|
||||||
final List<Widget> actions;
|
final List<Widget> actions;
|
||||||
final Widget? bottom;
|
final Widget? bottom;
|
||||||
|
@ -33,8 +33,8 @@ class AvesAppBar extends StatelessWidget {
|
||||||
selector: (context, mq) => mq.padding.top,
|
selector: (context, mq) => mq.padding.top,
|
||||||
builder: (context, mqPaddingTop, child) {
|
builder: (context, mqPaddingTop, child) {
|
||||||
return SliverPersistentHeader(
|
return SliverPersistentHeader(
|
||||||
floating: true,
|
floating: !device.isTelevision,
|
||||||
pinned: device.isTelevision,
|
pinned: false,
|
||||||
delegate: _SliverAppBarDelegate(
|
delegate: _SliverAppBarDelegate(
|
||||||
height: mqPaddingTop + appBarHeightForContentHeight(contentHeight),
|
height: mqPaddingTop + appBarHeightForContentHeight(contentHeight),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
|
@ -51,15 +51,17 @@ class AvesAppBar extends StatelessWidget {
|
||||||
height: kToolbarHeight,
|
height: kToolbarHeight,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
leading != null
|
||||||
|
? Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: leadingHeroTag,
|
tag: leadingHeroTag,
|
||||||
flightShuttleBuilder: _flightShuttleBuilder,
|
flightShuttleBuilder: _flightShuttleBuilder,
|
||||||
transitionOnUserGestures: true,
|
transitionOnUserGestures: true,
|
||||||
child: leading,
|
child: leading!,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: titleHeroTag,
|
tag: titleHeroTag,
|
||||||
|
|
|
@ -6,6 +6,7 @@ class ActionButton extends StatelessWidget {
|
||||||
final String text;
|
final String text;
|
||||||
final Widget? icon;
|
final Widget? icon;
|
||||||
final bool enabled, showCaption;
|
final bool enabled, showCaption;
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
|
||||||
const ActionButton({
|
const ActionButton({
|
||||||
super.key,
|
super.key,
|
||||||
|
@ -13,7 +14,8 @@ class ActionButton extends StatelessWidget {
|
||||||
required this.icon,
|
required this.icon,
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
this.showCaption = true,
|
this.showCaption = true,
|
||||||
});
|
this.onPressed,
|
||||||
|
}) : assert(onPressed == null || enabled);
|
||||||
|
|
||||||
static const int maxLines = 2;
|
static const int maxLines = 2;
|
||||||
static const double padding = 8;
|
static const double padding = 8;
|
||||||
|
@ -21,6 +23,7 @@ class ActionButton extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final textStyle = _textStyle(context);
|
final textStyle = _textStyle(context);
|
||||||
|
final _enabled = onPressed != null || enabled;
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: _width(context),
|
width: _width(context),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
@ -30,14 +33,14 @@ class ActionButton extends StatelessWidget {
|
||||||
OverlayButton(
|
OverlayButton(
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: icon ?? const SizedBox(),
|
icon: icon ?? const SizedBox(),
|
||||||
onPressed: enabled ? () {} : null,
|
onPressed: onPressed ?? (_enabled ? () {} : null),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (showCaption) ...[
|
if (showCaption) ...[
|
||||||
const SizedBox(height: padding),
|
const SizedBox(height: padding),
|
||||||
Text(
|
Text(
|
||||||
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,
|
textAlign: TextAlign.center,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:aves/model/device.dart';
|
||||||
import 'package:aves/theme/colors.dart';
|
import 'package:aves/theme/colors.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
@ -24,7 +25,7 @@ class ThumbnailsSection extends SettingsSection {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<SettingsTile> tiles(BuildContext context) => [
|
List<SettingsTile> tiles(BuildContext context) => [
|
||||||
SettingsTileCollectionQuickActions(),
|
if (!device.isTelevision) SettingsTileCollectionQuickActions(),
|
||||||
SettingsTileThumbnailOverlay(),
|
SettingsTileThumbnailOverlay(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue