#27 albums: action to create empty album

This commit is contained in:
Thibault Deckers 2021-07-13 18:51:09 +09:00
parent bcbb7d5994
commit 2f34c3c48d
14 changed files with 105 additions and 6 deletions

View file

@ -59,6 +59,8 @@
"@chipActionRename": {},
"chipActionSetCover": "Set cover",
"@chipActionSetCover": {},
"chipActionCreateAlbum": "Create album",
"@chipActionCreateAlbum": {},
"entryActionDelete": "Delete",
"@entryActionDelete": {},
@ -515,6 +517,8 @@
"@createAlbumTooltip": {},
"createAlbumButtonLabel": "CREATE",
"@createAlbumButtonLabel": {},
"newFilterBanner": "new",
"@newFilterBanner": {},
"countryPageTitle": "Countries",
"@countryPageTitle": {},

View file

@ -29,6 +29,7 @@
"chipActionUnpin": "고정 해제",
"chipActionRename": "이름 변경",
"chipActionSetCover": "대표 이미지 변경",
"chipActionCreateAlbum": "앨범 만들기",
"entryActionDelete": "삭제",
"entryActionExport": "내보내기",
@ -232,8 +233,9 @@
"albumPageTitle": "앨범",
"albumEmpty": "앨범이 없습니다",
"createAlbumTooltip": "앨범 만들기",
"createAlbumTooltip": "앨범 만들기",
"createAlbumButtonLabel": "추가",
"newFilterBanner": "신규",
"countryPageTitle": "국가",
"countryEmpty": "국가가 없습니다",

View file

@ -10,6 +10,7 @@ enum ChipSetAction {
selectAll,
selectNone,
stats,
createAlbum,
// single/multiple filters
delete,
hide,
@ -36,6 +37,8 @@ extension ExtraChipSetAction on ChipSetAction {
return context.l10n.collectionActionSelectNone;
case ChipSetAction.stats:
return context.l10n.menuActionStats;
case ChipSetAction.createAlbum:
return context.l10n.chipActionCreateAlbum;
// single/multiple filters
case ChipSetAction.delete:
return context.l10n.chipActionDelete;
@ -67,6 +70,8 @@ extension ExtraChipSetAction on ChipSetAction {
return null;
case ChipSetAction.stats:
return AIcons.stats;
case ChipSetAction.createAlbum:
return AIcons.createAlbum;
// single/multiple filters
case ChipSetAction.delete:
return AIcons.delete;

View file

@ -10,9 +10,12 @@ import 'package:flutter/widgets.dart';
mixin AlbumMixin on SourceBase {
final Set<String?> _directories = {};
final Set<String> _newAlbums = {};
List<String> get rawAlbums => List.unmodifiable(_directories);
Set<AlbumFilter> getNewAlbumFilters(BuildContext context) => Set.unmodifiable(_newAlbums.map((v) => AlbumFilter(v, getAlbumDisplayName(context, v))));
int compareAlbumsByName(String a, String b) {
final ua = getAlbumDisplayName(null, a);
final ub = getAlbumDisplayName(null, b);
@ -109,7 +112,7 @@ mixin AlbumMixin on SourceBase {
}
void cleanEmptyAlbums([Set<String?>? albums]) {
final emptyAlbums = (albums ?? _directories).where(_isEmptyAlbum).toSet();
final emptyAlbums = (albums ?? _directories).where((v) => _isEmptyAlbum(v) && !_newAlbums.contains(v)).toSet();
if (emptyAlbums.isNotEmpty) {
_directories.removeAll(emptyAlbums);
_notifyAlbumChange();
@ -148,6 +151,22 @@ mixin AlbumMixin on SourceBase {
AvesEntry? albumRecentEntry(AlbumFilter filter) {
return _filterRecentEntryMap.putIfAbsent(filter.album, () => sortedEntriesByDate.firstWhereOrNull(filter.test));
}
void createAlbum(String directory) {
_newAlbums.add(directory);
addDirectories({directory});
}
void renameNewAlbum(String source, String destination) {
if (_newAlbums.remove(source)) {
cleanEmptyAlbums({source});
createAlbum(destination);
}
}
void forgetNewAlbums(Set<String> directories) {
_newAlbums.removeAll(directories);
}
}
class AlbumsChangedEvent {}

View file

@ -162,6 +162,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
final pinned = settings.pinnedFilters.contains(oldFilter);
final oldCoverContentId = covers.coverContentId(oldFilter);
final coverEntry = oldCoverContentId != null ? todoEntries.firstWhereOrNull((entry) => entry.contentId == oldCoverContentId) : null;
renameNewAlbum(sourceAlbum, destinationAlbum);
await updateAfterMove(
todoEntries: todoEntries,
copy: false,

View file

@ -23,6 +23,7 @@ class AvesFilterChip extends StatefulWidget {
final bool removable;
final bool showGenericIcon;
final Widget? background;
final String? banner;
final Widget? details;
final BorderRadius? borderRadius;
final double padding;
@ -43,6 +44,7 @@ class AvesFilterChip extends StatefulWidget {
this.removable = false,
this.showGenericIcon = true,
this.background,
this.banner,
this.details,
this.borderRadius,
this.padding = 6.0,
@ -195,6 +197,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
}
final borderRadius = widget.borderRadius ?? const BorderRadius.all(Radius.circular(AvesFilterChip.defaultRadius));
final banner = widget.banner;
Widget chip = Container(
constraints: const BoxConstraints(
minWidth: AvesFilterChip.minChipWidth,
@ -250,6 +253,21 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
),
),
),
if (banner != null)
LayoutBuilder(builder: (context, constraints) {
return ClipRRect(
borderRadius: borderRadius,
child: Transform(
transform: Matrix4.identity().scaled((constraints.maxHeight / 90 - .4).clamp(.45, 1.0)),
child: Banner(
message: banner.toUpperCase(),
location: BannerLocation.topStart,
color: Theme.of(context).accentColor,
child: const SizedBox(),
),
),
);
}),
],
),
);

View file

@ -66,6 +66,7 @@ class _AlbumPickPageState extends State<AlbumPickPage> {
),
appBarHeight: AlbumPickAppBar.preferredHeight,
sections: AlbumListPage.groupToSections(context, gridItems),
newFilters: source.getNewAlbumFilters(context),
sortFactor: settings.albumSortFactor,
showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none,
selectable: false,

View file

@ -46,6 +46,7 @@ class AlbumListPage extends StatelessWidget {
showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none,
actionDelegate: AlbumChipSetActionDelegate(gridItems),
filterSections: groupToSections(context, gridItems),
newFilters: source.getNewAlbumFilters(context),
emptyBuilder: () => EmptyContent(
icon: AIcons.album,
text: context.l10n.albumEmpty,

View file

@ -4,6 +4,7 @@ import 'package:aves/model/actions/chip_set_actions.dart';
import 'package:aves/model/actions/move_type.dart';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/highlight.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart';
@ -14,8 +15,10 @@ import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
import 'package:aves/widgets/dialogs/create_album_dialog.dart';
import 'package:aves/widgets/dialogs/rename_album_dialog.dart';
import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart';
@ -67,6 +70,9 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
case ChipSetAction.group:
_showGroupDialog(context);
break;
case ChipSetAction.createAlbum:
_createAlbum(context);
break;
// single/multiple filters
case ChipSetAction.delete:
_showDeleteDialog(context, filters);
@ -101,13 +107,35 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
}
}
void _createAlbum(BuildContext context) async {
final newAlbum = await showDialog<String>(
context: context,
builder: (context) => const CreateAlbumDialog(),
);
if (newAlbum != null && newAlbum.isNotEmpty) {
final source = context.read<CollectionSource>();
source.createAlbum(newAlbum);
final showAction = SnackBarAction(
label: context.l10n.showButtonLabel,
onPressed: () async {
final filter = AlbumFilter(newAlbum, source.getAlbumDisplayName(context, newAlbum));
context.read<HighlightInfo>().trackItem(FilterGridItem(filter, null), highlightItem: filter);
},
);
showFeedback(context, context.l10n.genericSuccessFeedback, showAction);
}
}
Future<void> _showDeleteDialog(BuildContext context, Set<AlbumFilter> filters) async {
final l10n = context.l10n;
final messenger = ScaffoldMessenger.of(context);
final source = context.read<CollectionSource>();
final albums = filters.map((v) => v.album).toSet();
final todoEntries = source.visibleEntries.where((entry) => filters.any((f) => f.test(entry))).toSet();
final todoCount = todoEntries.length;
final todoAlbums = filters.map((v) => v.album).toSet();
final filledAlbums = todoEntries.map((e) => e.directory).whereNotNull().toSet();
final emptyAlbums = todoAlbums.whereNot(filledAlbums.contains).toSet();
final confirmed = await showDialog<bool>(
context: context,
@ -130,7 +158,10 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
);
if (confirmed == null || !confirmed) return;
if (!await checkStoragePermissionForAlbums(context, albums)) return;
source.forgetNewAlbums(todoAlbums);
source.cleanEmptyAlbums(emptyAlbums);
if (!await checkStoragePermissionForAlbums(context, filledAlbums)) return;
source.pauseMonitoring();
showOpReport<ImageOpEvent>(
@ -149,7 +180,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
}
// cleanup
await storageService.deleteEmptyDirectories(albums);
await storageService.deleteEmptyDirectories(filledAlbums);
},
);
}

View file

@ -52,6 +52,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
case ChipSetAction.selectAll:
case ChipSetAction.selectNone:
case ChipSetAction.stats:
case ChipSetAction.createAlbum:
return true;
// single/multiple filters
case ChipSetAction.delete:

View file

@ -205,6 +205,7 @@ class _FilterGridAppBarState<T extends CollectionFilter> extends State<FilterGri
enabled: !widget.isEmpty,
),
toMenuItem(ChipSetAction.stats),
toMenuItem(ChipSetAction.createAlbum),
]);
}

View file

@ -25,6 +25,7 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
final double extent, thumbnailExtent;
final AvesEntry? coverEntry;
final bool pinned;
final String? banner;
final FilterCallback? onTap;
const CoveredFilterChip({
@ -34,6 +35,7 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
double? thumbnailExtent,
this.coverEntry,
this.pinned = false,
this.banner,
this.onTap,
}) : thumbnailExtent = thumbnailExtent ?? extent,
super(key: key);
@ -42,7 +44,7 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
Widget build(BuildContext context) {
return Consumer<CollectionSource>(
builder: (context, source, child) {
switch (filter.runtimeType) {
switch (T) {
case AlbumFilter:
{
final album = (filter as AlbumFilter).album;
@ -92,6 +94,7 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
filter: filter,
showGenericIcon: false,
background: backgroundImage,
banner: banner,
details: _buildDetails(source, filter),
borderRadius: BorderRadius.all(radius(extent)),
padding: titlePadding,

View file

@ -44,6 +44,7 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
final Widget appBar;
final double appBarHeight;
final Map<ChipSectionKey, List<FilterGridItem<T>>> sections;
final Set<T> newFilters;
final ChipSortFactor sortFactor;
final bool showHeaders, selectable;
final ValueNotifier<String> queryNotifier;
@ -57,6 +58,7 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
required this.appBar,
this.appBarHeight = kToolbarHeight,
required this.sections,
required this.newFilters,
required this.sortFactor,
required this.showHeaders,
required this.selectable,
@ -92,6 +94,7 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
appBar: appBar,
appBarHeight: appBarHeight,
sections: sections,
newFilters: newFilters,
sortFactor: sortFactor,
showHeaders: showHeaders,
selectable: selectable,
@ -117,6 +120,7 @@ class FilterGrid<T extends CollectionFilter> extends StatefulWidget {
final Widget appBar;
final double appBarHeight;
final Map<ChipSectionKey, List<FilterGridItem<T>>> sections;
final Set<T> newFilters;
final ChipSortFactor sortFactor;
final bool showHeaders, selectable;
final ValueNotifier<String> queryNotifier;
@ -130,6 +134,7 @@ class FilterGrid<T extends CollectionFilter> extends StatefulWidget {
required this.appBar,
required this.appBarHeight,
required this.sections,
required this.newFilters,
required this.sortFactor,
required this.showHeaders,
required this.selectable,
@ -166,6 +171,7 @@ class _FilterGridState<T extends CollectionFilter> extends State<FilterGrid<T>>
appBar: widget.appBar,
appBarHeight: widget.appBarHeight,
sections: widget.sections,
newFilters: widget.newFilters,
sortFactor: widget.sortFactor,
showHeaders: widget.showHeaders,
selectable: widget.selectable,
@ -181,6 +187,7 @@ class _FilterGridState<T extends CollectionFilter> extends State<FilterGrid<T>>
class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
final Widget appBar;
final Map<ChipSectionKey, List<FilterGridItem<T>>> sections;
final Set<T> newFilters;
final ChipSortFactor sortFactor;
final bool showHeaders, selectable;
final ValueNotifier<String> queryNotifier;
@ -195,6 +202,7 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
required this.appBar,
required double appBarHeight,
required this.sections,
required this.newFilters,
required this.sortFactor,
required this.showHeaders,
required this.selectable,
@ -258,6 +266,7 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
filter: filter,
extent: tileExtent,
pinned: pinnedFilters.contains(filter),
banner: newFilters.contains(filter) ? context.l10n.newFilterBanner : null,
onTap: onTap,
),
),

View file

@ -18,6 +18,7 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
final bool groupable, showHeaders;
final ChipSetActionDelegate actionDelegate;
final Map<ChipSectionKey, List<FilterGridItem<T>>> filterSections;
final Set<T>? newFilters;
final Widget Function() emptyBuilder;
const FilterNavigationPage({
@ -29,6 +30,7 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
this.showHeaders = false,
required this.actionDelegate,
required this.filterSections,
this.newFilters,
required this.emptyBuilder,
}) : super(key: key);
@ -46,6 +48,7 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
isEmpty: filterSections.isEmpty,
),
sections: filterSections,
newFilters: newFilters ?? {},
sortFactor: sortFactor,
showHeaders: showHeaders,
selectable: true,