#149 fav: toggle multiple items, thumbnail overlay icon
This commit is contained in:
parent
862a8003fa
commit
aa6a00b080
18 changed files with 251 additions and 151 deletions
|
@ -524,6 +524,7 @@
|
||||||
"settingsNavigationDrawerAddAlbum": "Add album",
|
"settingsNavigationDrawerAddAlbum": "Add album",
|
||||||
|
|
||||||
"settingsSectionThumbnails": "Thumbnails",
|
"settingsSectionThumbnails": "Thumbnails",
|
||||||
|
"settingsThumbnailShowFavouriteIcon": "Show favourite icon",
|
||||||
"settingsThumbnailShowLocationIcon": "Show location icon",
|
"settingsThumbnailShowLocationIcon": "Show location icon",
|
||||||
"settingsThumbnailShowMotionPhotoIcon": "Show motion photo icon",
|
"settingsThumbnailShowMotionPhotoIcon": "Show motion photo icon",
|
||||||
"settingsThumbnailShowRating": "Show rating",
|
"settingsThumbnailShowRating": "Show rating",
|
||||||
|
|
|
@ -371,6 +371,7 @@
|
||||||
"settingsNavigationDrawerAddAlbum": "Ajouter un album",
|
"settingsNavigationDrawerAddAlbum": "Ajouter un album",
|
||||||
|
|
||||||
"settingsSectionThumbnails": "Vignettes",
|
"settingsSectionThumbnails": "Vignettes",
|
||||||
|
"settingsThumbnailShowFavouriteIcon": "Afficher l’icône de favori",
|
||||||
"settingsThumbnailShowLocationIcon": "Afficher l’icône de lieu",
|
"settingsThumbnailShowLocationIcon": "Afficher l’icône de lieu",
|
||||||
"settingsThumbnailShowMotionPhotoIcon": "Afficher l’icône de photo animée",
|
"settingsThumbnailShowMotionPhotoIcon": "Afficher l’icône de photo animée",
|
||||||
"settingsThumbnailShowRating": "Afficher la notation",
|
"settingsThumbnailShowRating": "Afficher la notation",
|
||||||
|
|
|
@ -371,6 +371,7 @@
|
||||||
"settingsNavigationDrawerAddAlbum": "앨범 추가",
|
"settingsNavigationDrawerAddAlbum": "앨범 추가",
|
||||||
|
|
||||||
"settingsSectionThumbnails": "섬네일",
|
"settingsSectionThumbnails": "섬네일",
|
||||||
|
"settingsThumbnailShowFavouriteIcon": "즐겨찾기 아이콘 표시",
|
||||||
"settingsThumbnailShowLocationIcon": "위치 아이콘 표시",
|
"settingsThumbnailShowLocationIcon": "위치 아이콘 표시",
|
||||||
"settingsThumbnailShowMotionPhotoIcon": "모션 포토 아이콘 표시",
|
"settingsThumbnailShowMotionPhotoIcon": "모션 포토 아이콘 표시",
|
||||||
"settingsThumbnailShowRating": "별점 표시",
|
"settingsThumbnailShowRating": "별점 표시",
|
||||||
|
|
|
@ -21,6 +21,7 @@ enum EntrySetAction {
|
||||||
copy,
|
copy,
|
||||||
move,
|
move,
|
||||||
rescan,
|
rescan,
|
||||||
|
toggleFavourite,
|
||||||
rotateCCW,
|
rotateCCW,
|
||||||
rotateCW,
|
rotateCW,
|
||||||
flip,
|
flip,
|
||||||
|
@ -51,6 +52,7 @@ class EntrySetActions {
|
||||||
EntrySetAction.delete,
|
EntrySetAction.delete,
|
||||||
EntrySetAction.copy,
|
EntrySetAction.copy,
|
||||||
EntrySetAction.move,
|
EntrySetAction.move,
|
||||||
|
EntrySetAction.toggleFavourite,
|
||||||
EntrySetAction.rescan,
|
EntrySetAction.rescan,
|
||||||
EntrySetAction.map,
|
EntrySetAction.map,
|
||||||
EntrySetAction.stats,
|
EntrySetAction.stats,
|
||||||
|
@ -94,6 +96,9 @@ extension ExtraEntrySetAction on EntrySetAction {
|
||||||
return context.l10n.collectionActionMove;
|
return context.l10n.collectionActionMove;
|
||||||
case EntrySetAction.rescan:
|
case EntrySetAction.rescan:
|
||||||
return context.l10n.collectionActionRescan;
|
return context.l10n.collectionActionRescan;
|
||||||
|
case EntrySetAction.toggleFavourite:
|
||||||
|
// different data depending on toggle state
|
||||||
|
return context.l10n.entryActionAddFavourite;
|
||||||
case EntrySetAction.rotateCCW:
|
case EntrySetAction.rotateCCW:
|
||||||
return context.l10n.entryActionRotateCCW;
|
return context.l10n.entryActionRotateCCW;
|
||||||
case EntrySetAction.rotateCW:
|
case EntrySetAction.rotateCW:
|
||||||
|
@ -150,6 +155,9 @@ extension ExtraEntrySetAction on EntrySetAction {
|
||||||
return AIcons.move;
|
return AIcons.move;
|
||||||
case EntrySetAction.rescan:
|
case EntrySetAction.rescan:
|
||||||
return AIcons.refresh;
|
return AIcons.refresh;
|
||||||
|
case EntrySetAction.toggleFavourite:
|
||||||
|
// different data depending on toggle state
|
||||||
|
return AIcons.favourite;
|
||||||
case EntrySetAction.rotateCCW:
|
case EntrySetAction.rotateCCW:
|
||||||
return AIcons.rotateLeft;
|
return AIcons.rotateLeft;
|
||||||
case EntrySetAction.rotateCW:
|
case EntrySetAction.rotateCW:
|
||||||
|
|
|
@ -685,13 +685,13 @@ class AvesEntry {
|
||||||
|
|
||||||
Future<void> addToFavourites() async {
|
Future<void> addToFavourites() async {
|
||||||
if (!isFavourite) {
|
if (!isFavourite) {
|
||||||
await favourites.add([this]);
|
await favourites.add({this});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeFromFavourites() async {
|
Future<void> removeFromFavourites() async {
|
||||||
if (isFavourite) {
|
if (isFavourite) {
|
||||||
await favourites.remove([this]);
|
await favourites.remove({this});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ class Favourites with ChangeNotifier {
|
||||||
|
|
||||||
FavouriteRow _entryToRow(AvesEntry entry) => FavouriteRow(contentId: entry.contentId!, path: entry.path!);
|
FavouriteRow _entryToRow(AvesEntry entry) => FavouriteRow(contentId: entry.contentId!, path: entry.path!);
|
||||||
|
|
||||||
Future<void> add(Iterable<AvesEntry> entries) async {
|
Future<void> add(Set<AvesEntry> entries) async {
|
||||||
final newRows = entries.map(_entryToRow);
|
final newRows = entries.map(_entryToRow);
|
||||||
|
|
||||||
await metadataDb.addFavourites(newRows);
|
await metadataDb.addFavourites(newRows);
|
||||||
|
@ -30,7 +30,7 @@ class Favourites with ChangeNotifier {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> remove(Iterable<AvesEntry> entries) async {
|
Future<void> remove(Set<AvesEntry> entries) async {
|
||||||
final contentIds = entries.map((entry) => entry.contentId).toSet();
|
final contentIds = entries.map((entry) => entry.contentId).toSet();
|
||||||
final removedRows = _rows.where((row) => contentIds.contains(row.contentId)).toSet();
|
final removedRows = _rows.where((row) => contentIds.contains(row.contentId)).toSet();
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@ class SettingsDefaults {
|
||||||
EntrySetAction.share,
|
EntrySetAction.share,
|
||||||
EntrySetAction.delete,
|
EntrySetAction.delete,
|
||||||
];
|
];
|
||||||
|
static const showThumbnailFavourite = true;
|
||||||
static const showThumbnailLocation = true;
|
static const showThumbnailLocation = true;
|
||||||
static const showThumbnailMotionPhoto = true;
|
static const showThumbnailMotionPhoto = true;
|
||||||
static const showThumbnailRating = true;
|
static const showThumbnailRating = true;
|
||||||
|
|
|
@ -61,6 +61,7 @@ class Settings extends ChangeNotifier {
|
||||||
static const collectionSortFactorKey = 'collection_sort_factor';
|
static const collectionSortFactorKey = 'collection_sort_factor';
|
||||||
static const collectionBrowsingQuickActionsKey = 'collection_browsing_quick_actions';
|
static const collectionBrowsingQuickActionsKey = 'collection_browsing_quick_actions';
|
||||||
static const collectionSelectionQuickActionsKey = 'collection_selection_quick_actions';
|
static const collectionSelectionQuickActionsKey = 'collection_selection_quick_actions';
|
||||||
|
static const showThumbnailFavouriteKey = 'show_thumbnail_favourite';
|
||||||
static const showThumbnailLocationKey = 'show_thumbnail_location';
|
static const showThumbnailLocationKey = 'show_thumbnail_location';
|
||||||
static const showThumbnailMotionPhotoKey = 'show_thumbnail_motion_photo';
|
static const showThumbnailMotionPhotoKey = 'show_thumbnail_motion_photo';
|
||||||
static const showThumbnailRatingKey = 'show_thumbnail_rating';
|
static const showThumbnailRatingKey = 'show_thumbnail_rating';
|
||||||
|
@ -303,6 +304,10 @@ class Settings extends ChangeNotifier {
|
||||||
|
|
||||||
set collectionSelectionQuickActions(List<EntrySetAction> newValue) => setAndNotify(collectionSelectionQuickActionsKey, newValue.map((v) => v.toString()).toList());
|
set collectionSelectionQuickActions(List<EntrySetAction> newValue) => setAndNotify(collectionSelectionQuickActionsKey, newValue.map((v) => v.toString()).toList());
|
||||||
|
|
||||||
|
bool get showThumbnailFavourite => getBoolOrDefault(showThumbnailFavouriteKey, SettingsDefaults.showThumbnailFavourite);
|
||||||
|
|
||||||
|
set showThumbnailFavourite(bool newValue) => setAndNotify(showThumbnailFavouriteKey, newValue);
|
||||||
|
|
||||||
bool get showThumbnailLocation => getBoolOrDefault(showThumbnailLocationKey, SettingsDefaults.showThumbnailLocation);
|
bool get showThumbnailLocation => getBoolOrDefault(showThumbnailLocationKey, SettingsDefaults.showThumbnailLocation);
|
||||||
|
|
||||||
set showThumbnailLocation(bool newValue) => setAndNotify(showThumbnailLocationKey, newValue);
|
set showThumbnailLocation(bool newValue) => setAndNotify(showThumbnailLocationKey, newValue);
|
||||||
|
@ -622,6 +627,7 @@ class Settings extends ChangeNotifier {
|
||||||
case isInstalledAppAccessAllowedKey:
|
case isInstalledAppAccessAllowedKey:
|
||||||
case isErrorReportingAllowedKey:
|
case isErrorReportingAllowedKey:
|
||||||
case mustBackTwiceToExitKey:
|
case mustBackTwiceToExitKey:
|
||||||
|
case showThumbnailFavouriteKey:
|
||||||
case showThumbnailLocationKey:
|
case showThumbnailLocationKey:
|
||||||
case showThumbnailMotionPhotoKey:
|
case showThumbnailMotionPhotoKey:
|
||||||
case showThumbnailRatingKey:
|
case showThumbnailRatingKey:
|
||||||
|
|
|
@ -20,6 +20,7 @@ import 'package:aves/widgets/common/app_bar_subtitle.dart';
|
||||||
import 'package:aves/widgets/common/app_bar_title.dart';
|
import 'package:aves/widgets/common/app_bar_title.dart';
|
||||||
import 'package:aves/widgets/common/basic/menu.dart';
|
import 'package:aves/widgets/common/basic/menu.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
import 'package:aves/widgets/common/favourite_toggler.dart';
|
||||||
import 'package:aves/widgets/common/sliver_app_bar_title.dart';
|
import 'package:aves/widgets/common/sliver_app_bar_title.dart';
|
||||||
import 'package:aves/widgets/dialogs/tile_view_dialog.dart';
|
import 'package:aves/widgets/dialogs/tile_view_dialog.dart';
|
||||||
import 'package:aves/widgets/search/search_delegate.dart';
|
import 'package:aves/widgets/search/search_delegate.dart';
|
||||||
|
@ -102,50 +103,42 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
||||||
return Selector<Selection<AvesEntry>, Tuple2<bool, int>>(
|
final selection = context.watch<Selection<AvesEntry>>();
|
||||||
selector: (context, selection) => Tuple2(selection.isSelecting, selection.selectedItems.length),
|
final isSelecting = selection.isSelecting;
|
||||||
builder: (context, s, child) {
|
_isSelectingNotifier.value = isSelecting;
|
||||||
final isSelecting = s.item1;
|
return AnimatedBuilder(
|
||||||
final selectedItemCount = s.item2;
|
animation: collection.filterChangeNotifier,
|
||||||
_isSelectingNotifier.value = isSelecting;
|
builder: (context, child) {
|
||||||
return AnimatedBuilder(
|
final removableFilters = appMode != AppMode.pickInternal;
|
||||||
animation: collection.filterChangeNotifier,
|
return Selector<Query, bool>(
|
||||||
builder: (context, child) {
|
selector: (context, query) => query.enabled,
|
||||||
final removableFilters = appMode != AppMode.pickInternal;
|
builder: (context, queryEnabled, child) {
|
||||||
return Selector<Query, bool>(
|
return SliverAppBar(
|
||||||
selector: (context, query) => query.enabled,
|
leading: appMode.hasDrawer ? _buildAppBarLeading(isSelecting) : null,
|
||||||
builder: (context, queryEnabled, child) {
|
title: SliverAppBarTitleWrapper(
|
||||||
return SliverAppBar(
|
child: _buildAppBarTitle(isSelecting),
|
||||||
leading: appMode.hasDrawer ? _buildAppBarLeading(isSelecting) : null,
|
),
|
||||||
title: SliverAppBarTitleWrapper(
|
actions: _buildActions(selection),
|
||||||
child: _buildAppBarTitle(isSelecting),
|
bottom: PreferredSize(
|
||||||
),
|
preferredSize: Size.fromHeight(appBarBottomHeight),
|
||||||
actions: _buildActions(
|
child: Column(
|
||||||
isSelecting: isSelecting,
|
children: [
|
||||||
selectedItemCount: selectedItemCount,
|
if (showFilterBar)
|
||||||
),
|
FilterBar(
|
||||||
bottom: PreferredSize(
|
filters: collection.filters.where((v) => !(v is QueryFilter && v.live)).toSet(),
|
||||||
preferredSize: Size.fromHeight(appBarBottomHeight),
|
removable: removableFilters,
|
||||||
child: Column(
|
onTap: removableFilters ? collection.removeFilter : null,
|
||||||
children: [
|
),
|
||||||
if (showFilterBar)
|
if (queryEnabled)
|
||||||
FilterBar(
|
EntryQueryBar(
|
||||||
filters: collection.filters.where((v) => !(v is QueryFilter && v.live)).toSet(),
|
queryNotifier: context.select<Query, ValueNotifier<String>>((query) => query.queryNotifier),
|
||||||
removable: removableFilters,
|
focusNode: _queryBarFocusNode,
|
||||||
onTap: removableFilters ? collection.removeFilter : null,
|
)
|
||||||
),
|
],
|
||||||
if (queryEnabled)
|
),
|
||||||
EntryQueryBar(
|
),
|
||||||
queryNotifier: context.select<Query, ValueNotifier<String>>((query) => query.queryNotifier),
|
titleSpacing: 0,
|
||||||
focusNode: _queryBarFocusNode,
|
floating: true,
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
titleSpacing: 0,
|
|
||||||
floating: true,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -204,10 +197,10 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildActions({
|
List<Widget> _buildActions(Selection<AvesEntry> selection) {
|
||||||
required bool isSelecting,
|
final isSelecting = selection.isSelecting;
|
||||||
required int selectedItemCount,
|
final selectedItemCount = selection.selectedItems.length;
|
||||||
}) {
|
|
||||||
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
||||||
bool isVisible(EntrySetAction action) => _actionDelegate.isVisible(
|
bool isVisible(EntrySetAction action) => _actionDelegate.isVisible(
|
||||||
action,
|
action,
|
||||||
|
@ -227,7 +220,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
final browsingQuickActions = settings.collectionBrowsingQuickActions;
|
final browsingQuickActions = settings.collectionBrowsingQuickActions;
|
||||||
final selectionQuickActions = settings.collectionSelectionQuickActions;
|
final selectionQuickActions = settings.collectionSelectionQuickActions;
|
||||||
final quickActionButtons = (isSelecting ? selectionQuickActions : browsingQuickActions).where(isVisible).map(
|
final quickActionButtons = (isSelecting ? selectionQuickActions : browsingQuickActions).where(isVisible).map(
|
||||||
(action) => _toActionButton(action, enabled: canApply(action)),
|
(action) => _toActionButton(action, enabled: canApply(action), selection: selection),
|
||||||
);
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
@ -238,14 +231,14 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
key: const Key('appbar-menu-button'),
|
key: const Key('appbar-menu-button'),
|
||||||
itemBuilder: (context) {
|
itemBuilder: (context) {
|
||||||
final generalMenuItems = EntrySetActions.general.where(isVisible).map(
|
final generalMenuItems = EntrySetActions.general.where(isVisible).map(
|
||||||
(action) => _toMenuItem(action, enabled: canApply(action)),
|
(action) => _toMenuItem(action, enabled: canApply(action), selection: selection),
|
||||||
);
|
);
|
||||||
|
|
||||||
final browsingMenuActions = EntrySetActions.browsing.where((v) => !browsingQuickActions.contains(v));
|
final browsingMenuActions = EntrySetActions.browsing.where((v) => !browsingQuickActions.contains(v));
|
||||||
final selectionMenuActions = EntrySetActions.selection.where((v) => !selectionQuickActions.contains(v));
|
final selectionMenuActions = EntrySetActions.selection.where((v) => !selectionQuickActions.contains(v));
|
||||||
final contextualMenuItems = [
|
final contextualMenuItems = [
|
||||||
...(isSelecting ? selectionMenuActions : browsingMenuActions).where(isVisible).map(
|
...(isSelecting ? selectionMenuActions : browsingMenuActions).where(isVisible).map(
|
||||||
(action) => _toMenuItem(action, enabled: canApply(action)),
|
(action) => _toMenuItem(action, enabled: canApply(action), selection: selection),
|
||||||
),
|
),
|
||||||
if (isSelecting)
|
if (isSelecting)
|
||||||
PopupMenuItem<EntrySetAction>(
|
PopupMenuItem<EntrySetAction>(
|
||||||
|
@ -262,7 +255,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
EntrySetAction.editRating,
|
EntrySetAction.editRating,
|
||||||
EntrySetAction.editTags,
|
EntrySetAction.editTags,
|
||||||
EntrySetAction.removeMetadata,
|
EntrySetAction.removeMetadata,
|
||||||
].map((action) => _toMenuItem(action, enabled: canApply(action))),
|
].map((action) => _toMenuItem(action, enabled: canApply(action), selection: selection)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -286,10 +279,14 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Set<AvesEntry> _getExpandedSelectedItems(Selection<AvesEntry> selection) {
|
||||||
|
return selection.selectedItems.expand((entry) => entry.burstEntries ?? {entry}).toSet();
|
||||||
|
}
|
||||||
|
|
||||||
// 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}) {
|
Widget _toActionButton(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:
|
||||||
|
@ -302,6 +299,11 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
case EntrySetAction.toggleFavourite:
|
||||||
|
return FavouriteToggler(
|
||||||
|
entries: _getExpandedSelectedItems(selection),
|
||||||
|
onPressed: onPressed,
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return IconButton(
|
return IconButton(
|
||||||
key: _getActionKey(action),
|
key: _getActionKey(action),
|
||||||
|
@ -312,7 +314,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PopupMenuItem<EntrySetAction> _toMenuItem(EntrySetAction action, {required bool enabled}) {
|
PopupMenuItem<EntrySetAction> _toMenuItem(EntrySetAction action, {required bool enabled, required Selection<AvesEntry> selection}) {
|
||||||
late Widget child;
|
late Widget child;
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case EntrySetAction.toggleTitleSearch:
|
case EntrySetAction.toggleTitleSearch:
|
||||||
|
@ -321,6 +323,12 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
isMenuItem: true,
|
isMenuItem: true,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case EntrySetAction.toggleFavourite:
|
||||||
|
child = FavouriteToggler(
|
||||||
|
entries: _getExpandedSelectedItems(selection),
|
||||||
|
isMenuItem: true,
|
||||||
|
);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
child = MenuRow(text: action.getText(context), icon: action.getIcon());
|
child = MenuRow(text: action.getText(context), icon: action.getIcon());
|
||||||
break;
|
break;
|
||||||
|
@ -424,6 +432,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
case EntrySetAction.copy:
|
case EntrySetAction.copy:
|
||||||
case EntrySetAction.move:
|
case EntrySetAction.move:
|
||||||
case EntrySetAction.rescan:
|
case EntrySetAction.rescan:
|
||||||
|
case EntrySetAction.toggleFavourite:
|
||||||
case EntrySetAction.rotateCCW:
|
case EntrySetAction.rotateCCW:
|
||||||
case EntrySetAction.rotateCW:
|
case EntrySetAction.rotateCW:
|
||||||
case EntrySetAction.flip:
|
case EntrySetAction.flip:
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/app_mode.dart';
|
import 'package:aves/app_mode.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
|
import 'package:aves/model/favourites.dart';
|
||||||
import 'package:aves/model/filters/favourite.dart';
|
import 'package:aves/model/filters/favourite.dart';
|
||||||
import 'package:aves/model/filters/mime.dart';
|
import 'package:aves/model/filters/mime.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
|
@ -103,13 +104,18 @@ class _CollectionGridContent extends StatelessWidget {
|
||||||
columnCount: columnCount,
|
columnCount: columnCount,
|
||||||
spacing: tileSpacing,
|
spacing: tileSpacing,
|
||||||
tileExtent: thumbnailExtent,
|
tileExtent: thumbnailExtent,
|
||||||
tileBuilder: (entry) => InteractiveTile(
|
tileBuilder: (entry) => AnimatedBuilder(
|
||||||
key: ValueKey(entry.contentId),
|
animation: favourites,
|
||||||
collection: collection,
|
builder: (context, child) {
|
||||||
entry: entry,
|
return InteractiveTile(
|
||||||
thumbnailExtent: thumbnailExtent,
|
key: ValueKey(entry.contentId),
|
||||||
tileLayout: tileLayout,
|
collection: collection,
|
||||||
isScrollingNotifier: _isScrollingNotifier,
|
entry: entry,
|
||||||
|
thumbnailExtent: thumbnailExtent,
|
||||||
|
tileLayout: tileLayout,
|
||||||
|
isScrollingNotifier: _isScrollingNotifier,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
tileAnimationDelay: tileAnimationDelay,
|
tileAnimationDelay: tileAnimationDelay,
|
||||||
child: child!,
|
child: child!,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import 'package:aves/model/actions/move_type.dart';
|
||||||
import 'package:aves/model/device.dart';
|
import 'package:aves/model/device.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/entry_metadata_edition.dart';
|
import 'package:aves/model/entry_metadata_edition.dart';
|
||||||
|
import 'package:aves/model/favourites.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/highlight.dart';
|
import 'package:aves/model/highlight.dart';
|
||||||
|
@ -73,6 +74,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
||||||
case EntrySetAction.copy:
|
case EntrySetAction.copy:
|
||||||
case EntrySetAction.move:
|
case EntrySetAction.move:
|
||||||
case EntrySetAction.rescan:
|
case EntrySetAction.rescan:
|
||||||
|
case EntrySetAction.toggleFavourite:
|
||||||
case EntrySetAction.rotateCCW:
|
case EntrySetAction.rotateCCW:
|
||||||
case EntrySetAction.rotateCW:
|
case EntrySetAction.rotateCW:
|
||||||
case EntrySetAction.flip:
|
case EntrySetAction.flip:
|
||||||
|
@ -115,6 +117,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
||||||
case EntrySetAction.copy:
|
case EntrySetAction.copy:
|
||||||
case EntrySetAction.move:
|
case EntrySetAction.move:
|
||||||
case EntrySetAction.rescan:
|
case EntrySetAction.rescan:
|
||||||
|
case EntrySetAction.toggleFavourite:
|
||||||
case EntrySetAction.rotateCCW:
|
case EntrySetAction.rotateCCW:
|
||||||
case EntrySetAction.rotateCW:
|
case EntrySetAction.rotateCW:
|
||||||
case EntrySetAction.flip:
|
case EntrySetAction.flip:
|
||||||
|
@ -167,6 +170,9 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
||||||
case EntrySetAction.rescan:
|
case EntrySetAction.rescan:
|
||||||
_rescan(context);
|
_rescan(context);
|
||||||
break;
|
break;
|
||||||
|
case EntrySetAction.toggleFavourite:
|
||||||
|
_toggleFavourite(context);
|
||||||
|
break;
|
||||||
case EntrySetAction.rotateCCW:
|
case EntrySetAction.rotateCCW:
|
||||||
_rotate(context, clockwise: false);
|
_rotate(context, clockwise: false);
|
||||||
break;
|
break;
|
||||||
|
@ -214,6 +220,18 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
||||||
selection.browse();
|
selection.browse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _toggleFavourite(BuildContext context) async {
|
||||||
|
final selection = context.read<Selection<AvesEntry>>();
|
||||||
|
final selectedItems = _getExpandedSelectedItems(selection);
|
||||||
|
if (selectedItems.every((entry) => entry.isFavourite)) {
|
||||||
|
await favourites.remove(selectedItems);
|
||||||
|
} else {
|
||||||
|
await favourites.add(selectedItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
selection.browse();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _delete(BuildContext context) async {
|
Future<void> _delete(BuildContext context) async {
|
||||||
final source = context.read<CollectionSource>();
|
final source = context.read<CollectionSource>();
|
||||||
final selection = context.read<Selection<AvesEntry>>();
|
final selection = context.read<Selection<AvesEntry>>();
|
||||||
|
|
87
lib/widgets/common/favourite_toggler.dart
Normal file
87
lib/widgets/common/favourite_toggler.dart
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import 'package:aves/model/entry.dart';
|
||||||
|
import 'package:aves/model/favourites.dart';
|
||||||
|
import 'package:aves/theme/icons.dart';
|
||||||
|
import 'package:aves/widgets/common/basic/menu.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
import 'package:aves/widgets/common/fx/sweeper.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class FavouriteToggler extends StatefulWidget {
|
||||||
|
final Set<AvesEntry> entries;
|
||||||
|
final bool isMenuItem;
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
|
||||||
|
const FavouriteToggler({
|
||||||
|
Key? key,
|
||||||
|
required this.entries,
|
||||||
|
this.isMenuItem = false,
|
||||||
|
this.onPressed,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_FavouriteTogglerState createState() => _FavouriteTogglerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FavouriteTogglerState extends State<FavouriteToggler> {
|
||||||
|
final ValueNotifier<bool> isFavouriteNotifier = ValueNotifier(false);
|
||||||
|
|
||||||
|
Set<AvesEntry> get entries => widget.entries;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
favourites.addListener(_onChanged);
|
||||||
|
_onChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant FavouriteToggler oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
_onChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
favourites.removeListener(_onChanged);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ValueListenableBuilder<bool>(
|
||||||
|
valueListenable: isFavouriteNotifier,
|
||||||
|
builder: (context, isFavourite, child) {
|
||||||
|
if (widget.isMenuItem) {
|
||||||
|
return isFavourite
|
||||||
|
? MenuRow(
|
||||||
|
text: context.l10n.entryActionRemoveFavourite,
|
||||||
|
icon: const Icon(AIcons.favouriteActive),
|
||||||
|
)
|
||||||
|
: MenuRow(
|
||||||
|
text: context.l10n.entryActionAddFavourite,
|
||||||
|
icon: const Icon(AIcons.favourite),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(isFavourite ? AIcons.favouriteActive : AIcons.favourite),
|
||||||
|
onPressed: widget.onPressed,
|
||||||
|
tooltip: isFavourite ? context.l10n.entryActionRemoveFavourite : context.l10n.entryActionAddFavourite,
|
||||||
|
),
|
||||||
|
Sweeper(
|
||||||
|
key: ValueKey(entries.length == 1 ? entries.first : entries.length),
|
||||||
|
builder: (context) => const Icon(AIcons.favourite, color: Colors.redAccent),
|
||||||
|
toggledNotifier: isFavouriteNotifier,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onChanged() {
|
||||||
|
isFavouriteNotifier.value = entries.isNotEmpty && entries.every((entry) => entry.isFavourite);
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,6 +28,7 @@ class GridTheme extends StatelessWidget {
|
||||||
iconSize: iconSize,
|
iconSize: iconSize,
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
highlightBorderWidth: highlightBorderWidth,
|
highlightBorderWidth: highlightBorderWidth,
|
||||||
|
showFavourite: settings.showThumbnailFavourite,
|
||||||
showLocation: showLocation ?? settings.showThumbnailLocation,
|
showLocation: showLocation ?? settings.showThumbnailLocation,
|
||||||
showMotionPhoto: settings.showThumbnailMotionPhoto,
|
showMotionPhoto: settings.showThumbnailMotionPhoto,
|
||||||
showRating: settings.showThumbnailRating,
|
showRating: settings.showThumbnailRating,
|
||||||
|
@ -42,12 +43,13 @@ class GridTheme extends StatelessWidget {
|
||||||
|
|
||||||
class GridThemeData {
|
class GridThemeData {
|
||||||
final double iconSize, fontSize, highlightBorderWidth;
|
final double iconSize, fontSize, highlightBorderWidth;
|
||||||
final bool showLocation, showMotionPhoto, showRating, showRaw, showVideoDuration;
|
final bool showFavourite, showLocation, showMotionPhoto, showRating, showRaw, showVideoDuration;
|
||||||
|
|
||||||
const GridThemeData({
|
const GridThemeData({
|
||||||
required this.iconSize,
|
required this.iconSize,
|
||||||
required this.fontSize,
|
required this.fontSize,
|
||||||
required this.highlightBorderWidth,
|
required this.highlightBorderWidth,
|
||||||
|
required this.showFavourite,
|
||||||
required this.showLocation,
|
required this.showLocation,
|
||||||
required this.showMotionPhoto,
|
required this.showMotionPhoto,
|
||||||
required this.showRating,
|
required this.showRating,
|
||||||
|
|
|
@ -72,6 +72,20 @@ class SphericalImageIcon extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FavouriteIcon extends StatelessWidget {
|
||||||
|
const FavouriteIcon({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
static const scale = .9;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const OverlayIcon(
|
||||||
|
icon: AIcons.favourite,
|
||||||
|
iconScale: scale,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class GpsIcon extends StatelessWidget {
|
class GpsIcon extends StatelessWidget {
|
||||||
const GpsIcon({Key? key}) : super(key: key);
|
const GpsIcon({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ class ThumbnailEntryOverlay extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final children = [
|
final children = [
|
||||||
|
if (entry.isFavourite && context.select<GridThemeData, bool>((t) => t.showFavourite)) const FavouriteIcon(),
|
||||||
if (entry.hasGps && context.select<GridThemeData, bool>((t) => t.showLocation)) const GpsIcon(),
|
if (entry.hasGps && context.select<GridThemeData, bool>((t) => t.showLocation)) const GpsIcon(),
|
||||||
if (entry.rating != 0 && context.select<GridThemeData, bool>((t) => t.showRating)) RatingIcon(entry: entry),
|
if (entry.rating != 0 && context.select<GridThemeData, bool>((t) => t.showRating)) RatingIcon(entry: entry),
|
||||||
if (entry.isVideo)
|
if (entry.isVideo)
|
||||||
|
|
|
@ -33,6 +33,29 @@ class ThumbnailsSection extends StatelessWidget {
|
||||||
showHighlight: false,
|
showHighlight: false,
|
||||||
children: [
|
children: [
|
||||||
const CollectionActionsTile(),
|
const CollectionActionsTile(),
|
||||||
|
Selector<Settings, bool>(
|
||||||
|
selector: (context, s) => s.showThumbnailFavourite,
|
||||||
|
builder: (context, current, child) => SwitchListTile(
|
||||||
|
value: current,
|
||||||
|
onChanged: (v) => settings.showThumbnailFavourite = v,
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(context.l10n.settingsThumbnailShowFavouriteIcon)),
|
||||||
|
AnimatedOpacity(
|
||||||
|
opacity: opacityFor(current),
|
||||||
|
duration: Durations.toggleableTransitionAnimation,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: iconSize * (1 - FavouriteIcon.scale) / 2),
|
||||||
|
child: Icon(
|
||||||
|
AIcons.favourite,
|
||||||
|
size: iconSize * FavouriteIcon.scale,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
Selector<Settings, bool>(
|
Selector<Settings, bool>(
|
||||||
selector: (context, s) => s.showThumbnailLocation,
|
selector: (context, s) => s.showThumbnailLocation,
|
||||||
builder: (context, current, child) => SwitchListTile(
|
builder: (context, current, child) => SwitchListTile(
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
import 'package:aves/model/actions/entry_actions.dart';
|
import 'package:aves/model/actions/entry_actions.dart';
|
||||||
import 'package:aves/model/device.dart';
|
import 'package:aves/model/device.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/favourites.dart';
|
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
|
||||||
import 'package:aves/widgets/common/basic/menu.dart';
|
import 'package:aves/widgets/common/basic/menu.dart';
|
||||||
import 'package:aves/widgets/common/basic/popup_menu_button.dart';
|
import 'package:aves/widgets/common/basic/popup_menu_button.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/favourite_toggler.dart';
|
||||||
import 'package:aves/widgets/common/fx/sweeper.dart';
|
|
||||||
import 'package:aves/widgets/viewer/action/entry_action_delegate.dart';
|
import 'package:aves/widgets/viewer/action/entry_action_delegate.dart';
|
||||||
import 'package:aves/widgets/viewer/multipage/conductor.dart';
|
import 'package:aves/widgets/viewer/multipage/conductor.dart';
|
||||||
import 'package:aves/widgets/viewer/overlay/common.dart';
|
import 'package:aves/widgets/viewer/overlay/common.dart';
|
||||||
|
@ -204,8 +201,8 @@ class _TopOverlayRow extends StatelessWidget {
|
||||||
void onPressed() => _onActionSelected(context, action);
|
void onPressed() => _onActionSelected(context, action);
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case EntryAction.toggleFavourite:
|
case EntryAction.toggleFavourite:
|
||||||
child = _FavouriteToggler(
|
child = FavouriteToggler(
|
||||||
entry: favouriteTargetEntry,
|
entries: {favouriteTargetEntry},
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
@ -251,8 +248,8 @@ class _TopOverlayRow extends StatelessWidget {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
// in app actions
|
// in app actions
|
||||||
case EntryAction.toggleFavourite:
|
case EntryAction.toggleFavourite:
|
||||||
child = _FavouriteToggler(
|
child = FavouriteToggler(
|
||||||
entry: favouriteTargetEntry,
|
entries: {favouriteTargetEntry},
|
||||||
isMenuItem: true,
|
isMenuItem: true,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
@ -315,80 +312,3 @@ class _TopOverlayRow extends StatelessWidget {
|
||||||
EntryActionDelegate(targetEntry).onActionSelected(context, action);
|
EntryActionDelegate(targetEntry).onActionSelected(context, action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FavouriteToggler extends StatefulWidget {
|
|
||||||
final AvesEntry entry;
|
|
||||||
final bool isMenuItem;
|
|
||||||
final VoidCallback? onPressed;
|
|
||||||
|
|
||||||
const _FavouriteToggler({
|
|
||||||
required this.entry,
|
|
||||||
this.isMenuItem = false,
|
|
||||||
this.onPressed,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
_FavouriteTogglerState createState() => _FavouriteTogglerState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FavouriteTogglerState extends State<_FavouriteToggler> {
|
|
||||||
final ValueNotifier<bool> isFavouriteNotifier = ValueNotifier(false);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
favourites.addListener(_onChanged);
|
|
||||||
_onChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant _FavouriteToggler oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
_onChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
favourites.removeListener(_onChanged);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ValueListenableBuilder<bool>(
|
|
||||||
valueListenable: isFavouriteNotifier,
|
|
||||||
builder: (context, isFavourite, child) {
|
|
||||||
if (widget.isMenuItem) {
|
|
||||||
return isFavourite
|
|
||||||
? MenuRow(
|
|
||||||
text: context.l10n.entryActionRemoveFavourite,
|
|
||||||
icon: const Icon(AIcons.favouriteActive),
|
|
||||||
)
|
|
||||||
: MenuRow(
|
|
||||||
text: context.l10n.entryActionAddFavourite,
|
|
||||||
icon: const Icon(AIcons.favourite),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Stack(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(isFavourite ? AIcons.favouriteActive : AIcons.favourite),
|
|
||||||
onPressed: widget.onPressed,
|
|
||||||
tooltip: isFavourite ? context.l10n.entryActionRemoveFavourite : context.l10n.entryActionAddFavourite,
|
|
||||||
),
|
|
||||||
Sweeper(
|
|
||||||
key: ValueKey(widget.entry),
|
|
||||||
builder: (context) => const Icon(AIcons.favourite, color: Colors.redAccent),
|
|
||||||
toggledNotifier: isFavouriteNotifier,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onChanged() {
|
|
||||||
isFavouriteNotifier.value = widget.entry.isFavourite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
"editEntryRatingDialogTitle",
|
"editEntryRatingDialogTitle",
|
||||||
"collectionSortRating",
|
"collectionSortRating",
|
||||||
"searchSectionRating",
|
"searchSectionRating",
|
||||||
|
"settingsThumbnailShowFavouriteIcon",
|
||||||
"settingsThumbnailShowRating"
|
"settingsThumbnailShowRating"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -42,6 +43,7 @@
|
||||||
"editEntryRatingDialogTitle",
|
"editEntryRatingDialogTitle",
|
||||||
"collectionSortRating",
|
"collectionSortRating",
|
||||||
"searchSectionRating",
|
"searchSectionRating",
|
||||||
|
"settingsThumbnailShowFavouriteIcon",
|
||||||
"settingsThumbnailShowRating"
|
"settingsThumbnailShowRating"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue