aves_mio0.31/lib/widgets/collection/app_bar/collection_app_bar.dart.ok
2026-07-18 13:39:22 +02:00

1100 lines
34 KiB
Text
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// lib/widgets/collection/app_bar/collection_app_bar.dart
import 'dart:async';
import 'dart:math';
import 'package:aves/app_mode.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/container/dynamic_album.dart';
import 'package:aves/model/filters/container/set_and.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/query.dart';
import 'package:aves/model/filters/trash.dart';
import 'package:aves/model/query.dart';
import 'package:aves/model/selection.dart';
import 'package:aves/model/settings/enums/accessibility_animations.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/theme/themes.dart';
import 'package:aves/view/view.dart';
import 'package:aves/widgets/aves_app.dart';
import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/collection/entry_set_action_delegate.dart';
import 'package:aves/widgets/collection/filter_bar.dart';
import 'package:aves/widgets/collection/query_bar.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/move_button.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/rate_button.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/tag_button.dart';
import 'package:aves/widgets/common/action_controls/togglers/favourite.dart';
import 'package:aves/widgets/common/action_controls/togglers/title_search.dart';
import 'package:aves/widgets/common/app_bar/app_bar_subtitle.dart';
import 'package:aves/widgets/common/app_bar/app_bar_title.dart';
import 'package:aves/widgets/common/basic/popup/container.dart';
import 'package:aves/widgets/common/basic/popup/expansion_panel.dart';
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_app_bar.dart';
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
import 'package:aves/widgets/common/search/route.dart';
import 'package:aves/widgets/common/tile_extent_controller.dart';
import 'package:aves/widgets/dialogs/tile_view_dialog.dart';
import 'package:aves/widgets/search/collection_search_delegate.dart';
import 'package:aves/widgets/viewer/controls/notifications.dart';
import 'package:aves_model/aves_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart';
// Remote
import 'package:aves/widgets/collection/remote_status_button.dart';
// Cast (modulare)
import 'cast/cast_icon_button.dart';
import 'cast/cast_bar_inline.dart';
import 'package:aves/widgets/cast/cast_controller.dart';
import 'package:aves/widgets/activity/activity_files_page.dart';
class CollectionAppBar extends StatefulWidget {
final ValueNotifier<double> appBarHeightNotifier;
final ScrollController scrollController;
final CollectionLens collection;
const CollectionAppBar({
super.key,
required this.appBarHeightNotifier,
required this.scrollController,
required this.collection,
});
@override
State<CollectionAppBar> createState() => _CollectionAppBarState();
}
class _CollectionAppBarState extends State<CollectionAppBar>
with RouteAware, SingleTickerProviderStateMixin, WidgetsBindingObserver {
final Set<StreamSubscription> _subscriptions = {};
final EntrySetActionDelegate _actionDelegate = EntrySetActionDelegate();
late AnimationController _browseToSelectAnimation;
final ValueNotifier<bool> _isSelectingNotifier = ValueNotifier(false);
final FocusNode _queryBarFocusNode = FocusNode();
late final Listenable _queryFocusRequestNotifier;
double _statusBarHeight = 0;
static const double _castBarHeight = 48;
CollectionLens get collection => widget.collection;
bool get isTrash => collection.filters.contains(TrashFilter.instance);
CollectionSource get source => collection.source;
Set<CollectionFilter> get visibleFilters => collection.filters
.where((v) => !(v is QueryFilter && v.live) && v is! TrashFilter)
.toSet();
bool get showFilterBar => visibleFilters.isNotEmpty;
void _onBackendChanged() {
if (!mounted) return;
setState(() {});
}
static const _sortOptions = [
EntrySortFactor.date,
EntrySortFactor.size,
EntrySortFactor.name,
EntrySortFactor.rating,
EntrySortFactor.duration,
EntrySortFactor.path,
];
static const _sectionOptions = [
EntrySectionFactor.album,
EntrySectionFactor.month,
EntrySectionFactor.day,
EntrySectionFactor.none,
];
static const _layoutOptions = [
TileLayout.mosaic,
TileLayout.grid,
TileLayout.list,
];
static const _trashSelectionQuickActions = [
EntrySetAction.delete,
EntrySetAction.restore,
];
@override
void initState() {
super.initState();
CastController.instance.backendNotifier.addListener(_onBackendChanged);
final query = context.read<Query>();
_subscriptions.add(query.enabledStream.listen((e) => _updateAppBarHeight()));
_queryFocusRequestNotifier = query.focusRequestNotifier;
_queryFocusRequestNotifier.addListener(_onQueryFocusRequest);
_queryBarFocusNode.addListener(_onQueryBarFocusChanged);
_browseToSelectAnimation = AnimationController(
duration: context.read<DurationsData>().iconAnimation,
vsync: this,
);
_isSelectingNotifier.addListener(_onActivityChanged);
// Importante: ricostruisce la AppBar quando appare/scompare la cast bar
CastController.instance.isCastingNotifier.addListener(_onCastVisibilityChanged);
_registerWidget(widget);
WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((_) {
_updateStatusBarHeight();
_onFilterChanged();
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final route = ModalRoute.of(context);
if (route is PageRoute) {
AvesApp.pageRouteObserver.subscribe(this, route);
}
}
@override
void didUpdateWidget(covariant CollectionAppBar oldWidget) {
super.didUpdateWidget(oldWidget);
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
@override
void dispose() {
_unregisterWidget(widget);
CastController.instance.isCastingNotifier.removeListener(_onCastVisibilityChanged);
CastController.instance.backendNotifier.removeListener(_onBackendChanged);
_queryBarFocusNode.dispose();
_queryFocusRequestNotifier.removeListener(_onQueryFocusRequest);
_queryBarFocusNode.removeListener(_onQueryBarFocusChanged);
_isSelectingNotifier.dispose();
_browseToSelectAnimation.dispose();
_subscriptions
..forEach((sub) => sub.cancel())
..clear();
WidgetsBinding.instance.removeObserver(this);
AvesApp.pageRouteObserver.unsubscribe(this);
super.dispose();
}
void _registerWidget(CollectionAppBar widget) {
widget.collection.filterChangeNotifier.addListener(_onFilterChanged);
}
void _unregisterWidget(CollectionAppBar widget) {
widget.collection.filterChangeNotifier.removeListener(_onFilterChanged);
}
void _onCastVisibilityChanged() {
if (!mounted) return;
setState(() {});
_updateAppBarHeight();
}
void _onQueryFocusRequest() {
if (_queryFocusRequestNotifier is ValueNotifier<bool> &&
(_queryFocusRequestNotifier as ValueNotifier<bool>).value) {
_queryBarFocusNode.requestFocus();
}
}
void _onQueryBarFocusChanged() {
if (_queryBarFocusNode.hasFocus) {
_scrollToTop();
}
_updateAppBarHeight();
}
void _onActivityChanged() {
if (!mounted) return;
if (context.read<Selection<AvesEntry>>().isSelecting) {
_browseToSelectAnimation.forward();
} else {
_browseToSelectAnimation.reverse();
}
_updateAppBarHeight();
}
void _onFilterChanged() {
_updateAppBarHeight();
final filters = collection.filters;
if (filters.isNotEmpty) {
final selection = context.read<Selection<AvesEntry>>();
if (selection.isSelecting) {
final toRemove = selection.selectedItems
.where((entry) => !filters.every((f) => f.test(entry)))
.toSet();
selection.removeFromSelection(toRemove);
}
}
}
void _scrollToTop() => widget.scrollController.jumpTo(0);
void _updateStatusBarHeight() {
if (!mounted) return;
final padding = MediaQuery.paddingOf(context);
if (_statusBarHeight != padding.top) {
setState(() => _statusBarHeight = padding.top);
_updateAppBarHeight();
}
}
// FIX PRINCIPALE: altezza reale completa della AppBar
void _updateAppBarHeight() {
widget.appBarHeightNotifier.value =
_statusBarHeight +
AvesAppBar.appBarHeightForContentHeight(appBarContentHeight);
}
@override
void didPushNext() {
_queryBarFocusNode.unfocus();
}
@override
void didChangeMetrics() {
WidgetsBinding.instance.addPostFrameCallback((_) => _updateStatusBarHeight());
}
@override
Widget build(BuildContext context) {
final appMode = context.watch<ValueNotifier<AppMode>>().value;
final selection = context.watch<Selection<AvesEntry>>();
final isSelecting = selection.isSelecting;
_isSelectingNotifier.value = isSelecting;
return NotificationListener<ScrollNotification>(
onNotification: (notification) => false,
child: AnimatedBuilder(
animation: collection.filterChangeNotifier,
builder: (context, child) {
final canRemoveFilters = appMode != AppMode.pickFilteredMediaInternal;
return Selector<Query, bool>(
selector: (context, query) => query.enabled,
builder: (context, queryEnabled, child) {
return Selector<Settings, List<EntrySetAction>>(
selector: (context, s) => s.collectionBrowsingQuickActions,
builder: (context, _, child) {
final useTvLayout = settings.useTvLayout;
final onFilterTap =
canRemoveFilters ? collection.removeFilter : null;
return AvesAppBar(
contentHeight: appBarContentHeight,
pinned: context.select<Selection<AvesEntry>, bool>(
(selection) => selection.isSelecting,
),
leading: _buildAppBarLeading(
hasDrawer: appMode.canNavigate,
isSelecting: isSelecting,
),
title: _buildAppBarTitle(isSelecting),
actions: (context, maxWidth) => _buildAppBarActions(
context: context,
selection: selection,
maxWidth: maxWidth,
useTvLayout: useTvLayout,
),
bottom: PreferredSize(
preferredSize: Size.fromHeight(
_computeBottomHeight(context, queryEnabled),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (CastController.instance.isCastingNotifier.value)
const CastBarInline(),
if (useTvLayout)
SizedBox(
height: CaptionedButton.getTelevisionButtonHeight(context),
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 8),
scrollDirection: Axis.horizontal,
children: _buildActions(
context,
selection,
double.infinity,
),
),
),
if (showFilterBar)
NotificationListener(
onNotification: (notification) {
if (notification is SelectFilterNotification) {
collection.addFilters({notification.filter});
return true;
} else if (notification is DecomposeFilterNotification) {
final filter = notification.filter;
if (filter is DynamicAlbumFilter) {
final innerFilter = filter.filter;
final newFilters =
innerFilter is SetAndFilter
? innerFilter.innerFilters
: {innerFilter};
collection.addFilters(newFilters);
collection.removeFilter(filter);
return true;
}
}
return false;
},
child: FilterBar(
filters: visibleFilters,
onTap: onFilterTap,
onRemove: onFilterTap,
),
),
if (queryEnabled)
EntryQueryBar(
queryNotifier:
context.select<Query, ValueNotifier<String>>(
(query) => query.queryNotifier,
),
focusNode: _queryBarFocusNode,
),
],
),
),
transitionKey: isSelecting,
);
},
);
},
);
},
),
);
}
double _computeBottomHeight(BuildContext context, bool queryEnabled) {
double height = 0;
if (CastController.instance.isCastingNotifier.value) {
height += _castBarHeight;
}
if (settings.useTvLayout) {
height += CaptionedButton.getTelevisionButtonHeight(context);
}
if (showFilterBar) {
height += FilterBar.preferredHeight;
}
if (queryEnabled) {
final textScaler = MediaQuery.textScalerOf(context);
height += EntryQueryBar.getPreferredHeight(textScaler);
}
return height;
}
// -------------------------------------------------------------
// ACTIONS
// -------------------------------------------------------------
List<Widget> _buildAppBarActions({
required BuildContext context,
required Selection<AvesEntry> selection,
required double maxWidth,
required bool useTvLayout,
}) {
final statusButton = Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: RemoteStatusButton(source: source),
);
if (useTvLayout) {
return [
statusButton,
const CastIconButton(),
];
}
/*return [
statusButton,
SizedBox(
width: 48,
height: kToolbarHeight,
child: Center(
child: Stack(
alignment: Alignment.center,
children: [
// LETTERA SOPRA, IN ALTO A DESTRA, STESSO COLORE DELLICONA
Positioned(
top: 2,
right: 6,
child: ValueListenableBuilder(
valueListenable: CastController.instance.backendNotifier,
builder: (_, __, ___) {
return ValueListenableBuilder(
valueListenable: CastController.instance.castIconState,
builder: (_, __, ___) {
return Text(
CastController.instance.backendLetter,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: CastController.instance.iconColor, // ← ECCO QUI
//color: Colors.red,
height: 1.0,
),
);
},
);
},
),
),
const CastIconButton(),
],
),
),
),
..._buildActions(context, selection, maxWidth),
];
*/
return [
statusButton,
SizedBox(
width: 48,
height: kToolbarHeight,
child: Center(
child: Stack(
alignment: Alignment.center,
children: [
// SCRITTA IN BASSO, CENTRATA, STESSO COLORE DELLICONA
Positioned(
bottom: 0, // leggermente sotto licona
child: ValueListenableBuilder(
valueListenable: CastController.instance.backendNotifier,
builder: (_, __, ___) {
return ValueListenableBuilder(
valueListenable: CastController.instance.castIconState,
builder: (_, __, ___) {
return Text(
CastController.instance.backendNotifier.value.name,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: CastController.instance.castIconState.value == CastIconState.connected
? Colors.blueAccent
: Colors.black,
height: 1.0,
),
);
},
);
}
),
),
const CastIconButton(),
],
),
),
),
..._buildActions(context, selection, maxWidth),
];
}
double get appBarContentHeight {
final textScaler = MediaQuery.textScalerOf(context);
double height = textScaler.scale(kToolbarHeight);
if (settings.useTvLayout) {
height += CaptionedButton.getTelevisionButtonHeight(context);
}
if (showFilterBar) {
height += FilterBar.preferredHeight;
}
if (context.read<Query>().enabled) {
height += EntryQueryBar.getPreferredHeight(textScaler);
}
if (CastController.instance.isCastingNotifier.value) {
height += _castBarHeight;
}
return height;
}
Widget? _buildAppBarLeading({
required bool hasDrawer,
required bool isSelecting,
}) {
if (settings.useTvLayout) return null;
if (!hasDrawer) {
return const CloseButton();
}
VoidCallback? onPressed;
String? tooltip;
if (isSelecting) {
onPressed = () => context.read<Selection<AvesEntry>>().browse();
tooltip = MaterialLocalizations.of(context).backButtonTooltip;
} else {
onPressed = Scaffold.of(context).openDrawer;
tooltip = MaterialLocalizations.of(context).openAppDrawerTooltip;
}
return IconButton(
key: const Key('appbar-leading-button'),
icon: AnimatedIcon(
icon: AnimatedIcons.menu_arrow,
progress: _browseToSelectAnimation,
),
onPressed: onPressed,
tooltip: tooltip,
);
}
Widget _buildAppBarTitle(bool isSelecting) {
final l10n = context.l10n;
if (isSelecting) {
return Selector<Selection<AvesEntry>?, int>(
selector: (context, selection) => selection?.selectedItems.length ?? 0,
builder: (context, count, child) => Text(
count == 0 ? l10n.collectionSelectPageTitle : l10n.itemCount(count),
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
);
} else {
final appMode = context.watch<ValueNotifier<AppMode>>().value;
Widget title = Text(
appMode.isPickingMedia
? l10n.collectionPickPageTitle
: (isTrash ? l10n.binPageTitle : l10n.collectionPageTitle),
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
);
if (appMode == AppMode.main) {
title = SourceStateAwareAppBarTitle(
title: title,
source: source,
);
}
return InteractiveAppBarTitle(
onTap: appMode.canNavigate ? _goToSearch : null,
child: title,
);
}
}
void _goToSearch() {
Navigator.maybeOf(context)?.push(
SearchPageRoute(
delegate: CollectionSearchDelegate(
searchFieldLabel: context.l10n.searchCollectionFieldHint,
searchFieldStyle: Themes.searchFieldStyle(context),
source: collection.source,
parentCollection: collection,
),
),
);
}
List<Widget> _buildActions(
BuildContext context, Selection<AvesEntry> selection, double maxWidth) {
final appMode = context.watch<ValueNotifier<AppMode>>().value;
final isSelecting = selection.isSelecting;
final selectedItemCount = selection.selectedItems.length;
bool isVisible(EntrySetAction action) => _actionDelegate.isVisible(
action,
appMode: appMode,
isSelecting: isSelecting,
itemCount: collection.entryCount,
selectedItemCount: selectedItemCount,
isTrash: isTrash,
);
bool canApply(EntrySetAction action) => _actionDelegate.canApply(
action,
isSelecting: isSelecting,
collection: collection,
selectedItemCount: selectedItemCount,
);
return settings.useTvLayout
? _buildTelevisionActions(
context: context,
appMode: appMode,
selection: selection,
isVisible: isVisible,
canApply: canApply,
)
: _buildMobileActions(
context: context,
appMode: appMode,
selection: selection,
maxWidth: maxWidth,
isVisible: isVisible,
canApply: canApply,
);
}
List<Widget> _buildTelevisionActions({
required BuildContext context,
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,
].nonNulls.where(isVisible).map((action) {
final enabled = canApply(action);
return CaptionedButton(
iconButtonBuilder: (context, focusNode) => _buildButtonIcon(
context,
action,
enabled: enabled,
selection: selection,
focusNode: focusNode,
),
captionText: _buildButtonCaption(
context,
action,
enabled: enabled,
),
onPressed: enabled ? () => _onActionSelected(action) : null,
);
}).toList();
}
static double _iconButtonWidth(BuildContext context) {
const defaultPadding = EdgeInsets.all(8);
const defaultIconSize = 24.0;
return defaultPadding.horizontal +
MediaQuery.textScalerOf(context).scale(defaultIconSize);
}
List<Widget> _buildMobileActions({
required BuildContext context,
required AppMode appMode,
required Selection<AvesEntry> selection,
required double maxWidth,
required bool Function(EntrySetAction action) isVisible,
required bool Function(EntrySetAction action) canApply,
}) {
final availableCount = (maxWidth / _iconButtonWidth(context)).floor();
final isSelecting = selection.isSelecting;
final selectedItemCount = selection.selectedItems.length;
final hasSelection = selectedItemCount > 0;
final browsingQuickActions = settings.collectionBrowsingQuickActions;
final selectionQuickActions =
isTrash ? _trashSelectionQuickActions : settings.collectionSelectionQuickActions;
final quickActions = (isSelecting ? selectionQuickActions : browsingQuickActions)
.take(max(0, availableCount - 1))
.toList();
final quickActionButtons = quickActions
.where(isVisible)
.map((action) => _buildButtonIcon(
context,
action,
enabled: canApply(action),
selection: selection,
));
final animations =
context.select<Settings, AccessibilityAnimations>((v) => v.accessibilityAnimations);
return [
...quickActionButtons,
PopupMenuButton<EntrySetAction>(
key: const Key('appbar-menu-button'),
itemBuilder: (context) {
bool isValidForMenu(EntrySetAction? v) =>
v == null || (!quickActions.contains(v) && isVisible(v));
final generalMenuItems = EntrySetActions.general
.where(isValidForMenu)
.map((action) =>
_toMenuItem(action, enabled: canApply(action), selection: selection));
final allContextualActions =
isSelecting ? EntrySetActions.pageSelection : EntrySetActions.pageBrowsing;
final contextualMenuActions =
allContextualActions.where(isValidForMenu).fold<List<EntrySetAction?>>(
[],
(prev, v) {
if (v == null && (prev.isEmpty || prev.last == null)) return prev;
return [...prev, v];
},
);
if (contextualMenuActions.isNotEmpty && contextualMenuActions.last == null) {
contextualMenuActions.removeLast();
}
final contextualMenuItems = <PopupMenuEntry<EntrySetAction>>[
...contextualMenuActions.map((action) {
if (action == null) return const PopupMenuDivider();
return _toMenuItem(
action,
enabled: canApply(action),
selection: selection,
);
}),
if (isSelecting &&
!settings.isReadOnly &&
appMode == AppMode.main &&
!isTrash)
PopupMenuExpansionPanel<EntrySetAction>(
enabled: hasSelection,
value: 'edit',
icon: AIcons.edit,
title: context.l10n.collectionActionEdit,
items: [
_buildRotateAndFlipMenuItems(context, canApply: canApply),
...EntrySetActions.edit
.where((v) => isVisible(v) && !quickActions.contains(v))
.map((action) => _toMenuItem(
action,
enabled: canApply(action),
selection: selection,
)),
],
),
];
return [
...generalMenuItems,
if (contextualMenuItems.isNotEmpty) ...[
const PopupMenuDivider(),
...contextualMenuItems,
],
];
},
onSelected: (action) async {
await Future.delayed(animations.popUpAnimationDelay * timeDilation);
await _onActionSelected(action);
},
popUpAnimationStyle: animations.popUpAnimationStyle,
),
];
}
Set<AvesEntry> _getExpandedSelectedItems(Selection<AvesEntry> selection) {
return selection.selectedItems
.expand((entry) => entry.stackedEntries ?? {entry})
.toSet();
}
Key _getActionKey(EntrySetAction action) => Key('menu-${action.name}');
Widget _buildButtonIcon(
BuildContext context,
EntrySetAction action, {
required bool enabled,
FocusNode? focusNode,
required Selection<AvesEntry> selection,
}) {
final blurred = settings.enableBlurEffect;
final onPressed = enabled ? () => _onActionSelected(action) : null;
switch (action) {
case EntrySetAction.toggleTitleSearch:
return Selector<Query?, bool>(
selector: (context, query) => query?.enabled ?? false,
builder: (context, queryEnabled, child) {
return TitleSearchToggler(
queryEnabled: queryEnabled,
onPressed: onPressed,
focusNode: focusNode,
);
},
);
case EntrySetAction.copy:
return MoveButton(
copy: true,
blurred: blurred,
onChooserValue: (album) =>
_actionDelegate.quickMove(context, album, copy: true),
onPressed: onPressed,
);
case EntrySetAction.move:
return MoveButton(
copy: false,
blurred: blurred,
onChooserValue: (album) =>
_actionDelegate.quickMove(context, album, copy: false),
onPressed: onPressed,
);
case EntrySetAction.editRating:
return RateButton(
blurred: blurred,
onChooserValue: (rating) => _actionDelegate.quickRate(context, rating),
focusNode: focusNode,
onPressed: onPressed,
);
case EntrySetAction.editTags:
return TagButton(
blurred: blurred,
onChooserValue: (filter) => _actionDelegate.quickTag(context, filter),
focusNode: focusNode,
onPressed: onPressed,
);
case EntrySetAction.toggleFavourite:
return FavouriteToggler(
entries: _getExpandedSelectedItems(selection),
focusNode: focusNode,
onPressed: onPressed,
);
default:
return IconButton(
key: _getActionKey(action),
icon: action.getIcon(),
onPressed: onPressed,
focusNode: focusNode,
tooltip: action.getText(context),
);
}
}
Widget _buildButtonCaption(
BuildContext context,
EntrySetAction action, {
required bool enabled,
}) {
switch (action) {
case EntrySetAction.toggleTitleSearch:
return TitleSearchTogglerCaption(enabled: enabled);
default:
return CaptionedButtonText(
text: action.getText(context),
enabled: enabled,
);
}
}
PopupMenuItem<EntrySetAction> _toMenuItem(
EntrySetAction action, {
required bool enabled,
required Selection<AvesEntry> selection,
}) {
late Widget child;
switch (action) {
case EntrySetAction.toggleTitleSearch:
child = TitleSearchToggler(
queryEnabled: context.read<Query>().enabled,
isMenuItem: true,
);
break;
case EntrySetAction.toggleFavourite:
child = FavouriteToggler(
entries: _getExpandedSelectedItems(selection),
isMenuItem: true,
);
break;
default:
child = MenuRow(
text: action.getText(context),
icon: action.getIcon(),
);
}
return PopupMenuItem(
key: _getActionKey(action),
value: action,
enabled: enabled,
child: child,
);
}
PopupMenuEntry<EntrySetAction> _buildRotateAndFlipMenuItems(
BuildContext context, {
required bool Function(EntrySetAction action) canApply,
}) {
Widget buildDivider() => const SizedBox(
height: 16,
child: VerticalDivider(width: 1, thickness: 1),
);
Widget buildItem(EntrySetAction action) => Expanded(
child: Material(
color: Colors.transparent,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
clipBehavior: Clip.antiAlias,
child: PopupMenuItem(
value: action,
enabled: canApply(action),
child: Tooltip(
message: action.getText(context),
child: Center(child: action.getIcon()),
),
),
),
);
return PopupMenuItemContainer(
child: Row(
children: [
buildDivider(),
buildItem(EntrySetAction.rotateCCW),
buildDivider(),
buildItem(EntrySetAction.rotateCW),
buildDivider(),
buildItem(EntrySetAction.flip),
buildDivider(),
],
),
);
}
Future<void> _onActionSelected(EntrySetAction action) async {
switch (action) {
case EntrySetAction.configureView:
await _configureView();
break;
case EntrySetAction.select:
context.read<Selection<AvesEntry>>().select();
break;
case EntrySetAction.selectAll:
context.read<Selection<AvesEntry>>().addToSelection(collection.sortedEntries);
break;
case EntrySetAction.selectNone:
context.read<Selection<AvesEntry>>().clearSelection();
break;
case EntrySetAction.openGpx:
await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const GpxFilesPage(),
),
);
break;
default:
_actionDelegate.onActionSelected(context, action);
}
}
Future<void> _configureView() async {
final initialValue = (
settings.collectionSortFactor,
settings.collectionSectionFactor,
settings.getTileLayout(CollectionPage.routeName),
settings.collectionSortReverse,
);
final extentController = context.read<TileExtentController>();
final value =
await showDialog<(EntrySortFactor?, EntrySectionFactor?, TileLayout?, bool)>(
context: context,
builder: (context) {
return TileViewDialog<EntrySortFactor, EntrySectionFactor, TileLayout>(
initialValue: initialValue,
sortOptions: _sortOptions
.map((v) => TileViewDialogOption(
value: v,
title: v.getName(context),
icon: v.icon,
))
.toList(),
sectionOptions: _sectionOptions
.map((v) => TileViewDialogOption(
value: v,
title: v.getName(context),
icon: v.icon,
))
.toList(),
layoutOptions: _layoutOptions
.map((v) => TileViewDialogOption(
value: v,
title: v.getName(context),
icon: v.icon,
))
.toList(),
sortOrder: (factor, reverse) => factor.getOrderName(context, reverse),
canSection: (s, g, l) => s == EntrySortFactor.date,
tileExtentController: extentController,
);
},
routeSettings: const RouteSettings(name: TileViewDialog.routeName),
);
await Future.delayed(ADurations.dialogTransitionLoose * timeDilation);
if (value != null && initialValue != value) {
settings.collectionSortFactor = value.$1!;
settings.collectionSectionFactor = value.$2!;
settings.setTileLayout(CollectionPage.routeName, value.$3!);
settings.collectionSortReverse = value.$4;
}
}
}