aves/lib/widgets/dialogs/pick_dialogs/album_pick_page.dart
2024-09-15 23:50:55 +02:00

296 lines
10 KiB
Dart

import 'package:aves/app_mode.dart';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.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/album.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/vaults/details.dart';
import 'package:aves/model/vaults/vaults.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/view/view.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
import 'package:aves/widgets/common/identity/empty.dart';
import 'package:aves/widgets/common/providers/query_provider.dart';
import 'package:aves/widgets/common/providers/selection_provider.dart';
import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart';
import 'package:aves/widgets/dialogs/filter_editors/create_album_dialog.dart';
import 'package:aves/widgets/dialogs/filter_editors/edit_vault_dialog.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/common/action_delegates/album_set.dart';
import 'package:aves/widgets/filter_grids/common/app_bar.dart';
import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart';
import 'package:aves_model/aves_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart';
Future<String?> pickAlbum({
required BuildContext context,
required MoveType? moveType,
}) async {
final source = context.read<CollectionSource>();
if (source.initState != SourceInitializationState.full) {
// source may not be fully initialized in view mode
await source.init();
}
final filter = await Navigator.maybeOf(context)?.push(
MaterialPageRoute<AlbumFilter>(
settings: const RouteSettings(name: _AlbumPickPage.routeName),
builder: (context) => _AlbumPickPage(source: source, moveType: moveType),
),
);
return filter?.album;
}
class _AlbumPickPage extends StatefulWidget {
static const routeName = '/album_pick';
final CollectionSource source;
final MoveType? moveType;
const _AlbumPickPage({
required this.source,
required this.moveType,
});
@override
State<_AlbumPickPage> createState() => _AlbumPickPageState();
}
class _AlbumPickPageState extends State<_AlbumPickPage> {
final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0);
final ValueNotifier<AppMode> _appModeNotifier = ValueNotifier(AppMode.pickFilterInternal);
CollectionSource get source => widget.source;
String get title {
final l10n = context.l10n;
return switch (widget.moveType) {
MoveType.copy => l10n.albumPickPageTitleCopy,
MoveType.move => l10n.albumPickPageTitleMove,
MoveType.export => l10n.albumPickPageTitleExport,
MoveType.toBin || MoveType.fromBin || null => l10n.albumPickPageTitlePick,
};
}
static const _quickActions = [
ChipSetAction.createAlbum,
];
// `null` items are converted to dividers
static const _menuActions = [
...ChipSetActions.general,
null,
ChipSetAction.toggleTitleSearch,
null,
ChipSetAction.createVault,
];
@override
void dispose() {
_appBarHeightNotifier.dispose();
_appModeNotifier.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ListenableProvider<ValueNotifier<AppMode>>.value(
value: _appModeNotifier,
child: Selector<Settings, (AlbumChipGroupFactor, ChipSortFactor)>(
selector: (context, s) => (s.albumGroupFactor, s.albumSortFactor),
builder: (context, s, child) {
return StreamBuilder(
stream: source.eventBus.on<AlbumsChangedEvent>(),
builder: (context, snapshot) {
final gridItems = AlbumListPage.getAlbumGridItems(context, source);
return SelectionProvider<FilterGridItem<AlbumFilter>>(
child: QueryProvider(
enabled: settings.showAlbumPickQuery,
child: FilterGridPage<AlbumFilter>(
settingsRouteKey: AlbumListPage.routeName,
appBar: FilterGridAppBar(
source: source,
title: title,
actionDelegate: _AlbumChipSetPickActionDelegate(gridItems),
actionsBuilder: _buildActions,
isEmpty: false,
appBarHeightNotifier: _appBarHeightNotifier,
),
appBarHeightNotifier: _appBarHeightNotifier,
sections: AlbumListPage.groupToSections(context, source, gridItems),
newFilters: source.getNewAlbumFilters(context),
sortFactor: settings.albumSortFactor,
showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none,
selectable: false,
applyQuery: AlbumListPage.applyQuery,
emptyBuilder: () => EmptyContent(
icon: AIcons.album,
text: context.l10n.albumEmpty,
),
heroType: HeroType.never,
),
),
);
},
);
},
),
);
}
List<Widget> _buildActions(
BuildContext context,
AppMode appMode,
Selection<FilterGridItem<AlbumFilter>> selection,
AlbumChipSetActionDelegate actionDelegate,
) {
final itemCount = actionDelegate.allItems.length;
final isSelecting = selection.isSelecting;
final selectedItems = selection.selectedItems;
final selectedFilters = selectedItems.map((v) => v.filter).toSet();
bool isVisible(ChipSetAction action) => actionDelegate.isVisible(
action,
appMode: appMode,
isSelecting: isSelecting,
itemCount: itemCount,
selectedFilters: selectedFilters,
);
void onActionSelected(ChipSetAction action) {
switch (action) {
case ChipSetAction.createAlbum:
_createAlbum();
case ChipSetAction.createVault:
_createVault();
default:
actionDelegate.onActionSelected(context, action);
}
}
return settings.useTvLayout
? _buildTelevisionActions(
context: context,
isVisible: isVisible,
onActionSelected: onActionSelected,
)
: _buildMobileActions(
context: context,
isVisible: isVisible,
onActionSelected: onActionSelected,
);
}
List<Widget> _buildTelevisionActions({
required BuildContext context,
required bool Function(ChipSetAction action) isVisible,
required void Function(ChipSetAction action) onActionSelected,
}) {
return [
...ChipSetActions.general,
].where(isVisible).map((action) {
return CaptionedButton(
icon: action.getIcon(),
caption: action.getText(context),
onPressed: () => onActionSelected(action),
);
}).toList();
}
List<Widget> _buildMobileActions({
required BuildContext context,
required bool Function(ChipSetAction action) isVisible,
required void Function(ChipSetAction action) onActionSelected,
}) {
final animations = context.select<Settings, AccessibilityAnimations>((s) => s.accessibilityAnimations);
return [
if (widget.moveType != null)
..._quickActions.where(isVisible).map(
(action) => IconButton(
icon: action.getIcon(),
onPressed: () => onActionSelected(action),
tooltip: action.getText(context),
),
),
PopupMenuButton<ChipSetAction>(
itemBuilder: (context) {
return _menuActions.where((v) => v == null || isVisible(v)).map((action) {
if (action == null) return const PopupMenuDivider();
return FilterGridAppBar.toMenuItem(context, action, enabled: true);
}).toList();
},
onSelected: (action) async {
// remove focus, if any, to prevent the keyboard from showing up
// after the user is done with the popup menu
FocusManager.instance.primaryFocus?.unfocus();
// wait for the popup menu to hide before proceeding with the action
await Future.delayed(animations.popUpAnimationDelay * timeDilation);
onActionSelected(action);
},
popUpAnimationStyle: animations.popUpAnimationStyle,
),
];
}
Future<void> _createAlbum() async {
final directory = await showDialog<String>(
context: context,
builder: (context) => const CreateAlbumDialog(),
routeSettings: const RouteSettings(name: CreateAlbumDialog.routeName),
);
if (directory == null) return;
// wait for the dialog to hide as applying the change may block the UI
await Future.delayed(ADurations.dialogTransitionLoose * timeDilation);
_pickAlbum(directory);
}
Future<void> _createVault() async {
final l10n = context.l10n;
if (!await showSkippableConfirmationDialog(
context: context,
type: ConfirmationDialog.createVault,
message: l10n.newVaultWarningDialogMessage,
confirmationButtonLabel: l10n.continueButtonLabel,
)) return;
final details = await showDialog<VaultDetails>(
context: context,
builder: (context) => const EditVaultDialog(),
routeSettings: const RouteSettings(name: EditVaultDialog.routeName),
);
if (details == null) return;
// wait for the dialog to hide as applying the change may block the UI
await Future.delayed(ADurations.dialogTransitionLoose * timeDilation);
await vaults.create(details);
_pickAlbum(details.path);
}
void _pickAlbum(String directory) {
source.createAlbum(directory);
final filter = AlbumFilter(directory, source.getAlbumDisplayName(context, directory));
Navigator.maybeOf(context)?.pop<AlbumFilter>(filter);
}
}
class _AlbumChipSetPickActionDelegate extends AlbumChipSetActionDelegate {
_AlbumChipSetPickActionDelegate(super.items);
@override
void onActionSelected(BuildContext context, ChipSetAction action) {
if (action == ChipSetAction.toggleTitleSearch) {
settings.showAlbumPickQuery = !settings.showAlbumPickQuery;
}
super.onActionSelected(context, action);
}
}