#27 albums: action to create empty album
This commit is contained in:
parent
bcbb7d5994
commit
2f34c3c48d
14 changed files with 105 additions and 6 deletions
|
@ -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": {},
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
"chipActionUnpin": "고정 해제",
|
||||
"chipActionRename": "이름 변경",
|
||||
"chipActionSetCover": "대표 이미지 변경",
|
||||
"chipActionCreateAlbum": "앨범 만들기",
|
||||
|
||||
"entryActionDelete": "삭제",
|
||||
"entryActionExport": "내보내기",
|
||||
|
@ -232,8 +233,9 @@
|
|||
|
||||
"albumPageTitle": "앨범",
|
||||
"albumEmpty": "앨범이 없습니다",
|
||||
"createAlbumTooltip": "새 앨범 만들기",
|
||||
"createAlbumTooltip": "앨범 만들기",
|
||||
"createAlbumButtonLabel": "추가",
|
||||
"newFilterBanner": "신규",
|
||||
|
||||
"countryPageTitle": "국가",
|
||||
"countryEmpty": "국가가 없습니다",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -205,6 +205,7 @@ class _FilterGridAppBarState<T extends CollectionFilter> extends State<FilterGri
|
|||
enabled: !widget.isEmpty,
|
||||
),
|
||||
toMenuItem(ChipSetAction.stats),
|
||||
toMenuItem(ChipSetAction.createAlbum),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue