From aa6a00b080cd5ad9c01a6f660724235dd976ceb7 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 5 Jan 2022 18:06:21 +0900 Subject: [PATCH] #149 fav: toggle multiple items, thumbnail overlay icon --- lib/l10n/app_en.arb | 1 + lib/l10n/app_fr.arb | 1 + lib/l10n/app_ko.arb | 1 + lib/model/actions/entry_set_actions.dart | 8 ++ lib/model/entry.dart | 4 +- lib/model/favourites.dart | 4 +- lib/model/settings/defaults.dart | 1 + lib/model/settings/settings.dart | 6 + lib/widgets/collection/app_bar.dart | 117 ++++++++++-------- lib/widgets/collection/collection_grid.dart | 20 +-- .../collection/entry_set_action_delegate.dart | 18 +++ lib/widgets/common/favourite_toggler.dart | 87 +++++++++++++ lib/widgets/common/grid/theme.dart | 4 +- lib/widgets/common/identity/aves_icons.dart | 14 +++ lib/widgets/common/thumbnail/overlay.dart | 1 + .../settings/thumbnails/thumbnails.dart | 23 ++++ lib/widgets/viewer/overlay/top.dart | 90 +------------- untranslated.json | 2 + 18 files changed, 251 insertions(+), 151 deletions(-) create mode 100644 lib/widgets/common/favourite_toggler.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b34ff0cf5..8cbfe7f2b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -524,6 +524,7 @@ "settingsNavigationDrawerAddAlbum": "Add album", "settingsSectionThumbnails": "Thumbnails", + "settingsThumbnailShowFavouriteIcon": "Show favourite icon", "settingsThumbnailShowLocationIcon": "Show location icon", "settingsThumbnailShowMotionPhotoIcon": "Show motion photo icon", "settingsThumbnailShowRating": "Show rating", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 87b92dd51..3975fda04 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -371,6 +371,7 @@ "settingsNavigationDrawerAddAlbum": "Ajouter un album", "settingsSectionThumbnails": "Vignettes", + "settingsThumbnailShowFavouriteIcon": "Afficher l’icône de favori", "settingsThumbnailShowLocationIcon": "Afficher l’icône de lieu", "settingsThumbnailShowMotionPhotoIcon": "Afficher l’icône de photo animée", "settingsThumbnailShowRating": "Afficher la notation", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 56955ad05..595e1300f 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -371,6 +371,7 @@ "settingsNavigationDrawerAddAlbum": "앨범 추가", "settingsSectionThumbnails": "섬네일", + "settingsThumbnailShowFavouriteIcon": "즐겨찾기 아이콘 표시", "settingsThumbnailShowLocationIcon": "위치 아이콘 표시", "settingsThumbnailShowMotionPhotoIcon": "모션 포토 아이콘 표시", "settingsThumbnailShowRating": "별점 표시", diff --git a/lib/model/actions/entry_set_actions.dart b/lib/model/actions/entry_set_actions.dart index 6d8ae339c..5b7243b72 100644 --- a/lib/model/actions/entry_set_actions.dart +++ b/lib/model/actions/entry_set_actions.dart @@ -21,6 +21,7 @@ enum EntrySetAction { copy, move, rescan, + toggleFavourite, rotateCCW, rotateCW, flip, @@ -51,6 +52,7 @@ class EntrySetActions { EntrySetAction.delete, EntrySetAction.copy, EntrySetAction.move, + EntrySetAction.toggleFavourite, EntrySetAction.rescan, EntrySetAction.map, EntrySetAction.stats, @@ -94,6 +96,9 @@ extension ExtraEntrySetAction on EntrySetAction { return context.l10n.collectionActionMove; case EntrySetAction.rescan: return context.l10n.collectionActionRescan; + case EntrySetAction.toggleFavourite: + // different data depending on toggle state + return context.l10n.entryActionAddFavourite; case EntrySetAction.rotateCCW: return context.l10n.entryActionRotateCCW; case EntrySetAction.rotateCW: @@ -150,6 +155,9 @@ extension ExtraEntrySetAction on EntrySetAction { return AIcons.move; case EntrySetAction.rescan: return AIcons.refresh; + case EntrySetAction.toggleFavourite: + // different data depending on toggle state + return AIcons.favourite; case EntrySetAction.rotateCCW: return AIcons.rotateLeft; case EntrySetAction.rotateCW: diff --git a/lib/model/entry.dart b/lib/model/entry.dart index 812d3fad0..be81490c3 100644 --- a/lib/model/entry.dart +++ b/lib/model/entry.dart @@ -685,13 +685,13 @@ class AvesEntry { Future addToFavourites() async { if (!isFavourite) { - await favourites.add([this]); + await favourites.add({this}); } } Future removeFromFavourites() async { if (isFavourite) { - await favourites.remove([this]); + await favourites.remove({this}); } } diff --git a/lib/model/favourites.dart b/lib/model/favourites.dart index 4a8e225e8..aef618695 100644 --- a/lib/model/favourites.dart +++ b/lib/model/favourites.dart @@ -21,7 +21,7 @@ class Favourites with ChangeNotifier { FavouriteRow _entryToRow(AvesEntry entry) => FavouriteRow(contentId: entry.contentId!, path: entry.path!); - Future add(Iterable entries) async { + Future add(Set entries) async { final newRows = entries.map(_entryToRow); await metadataDb.addFavourites(newRows); @@ -30,7 +30,7 @@ class Favourites with ChangeNotifier { notifyListeners(); } - Future remove(Iterable entries) async { + Future remove(Set entries) async { final contentIds = entries.map((entry) => entry.contentId).toSet(); final removedRows = _rows.where((row) => contentIds.contains(row.contentId)).toSet(); diff --git a/lib/model/settings/defaults.dart b/lib/model/settings/defaults.dart index cfd155b42..9f7a6f968 100644 --- a/lib/model/settings/defaults.dart +++ b/lib/model/settings/defaults.dart @@ -43,6 +43,7 @@ class SettingsDefaults { EntrySetAction.share, EntrySetAction.delete, ]; + static const showThumbnailFavourite = true; static const showThumbnailLocation = true; static const showThumbnailMotionPhoto = true; static const showThumbnailRating = true; diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 3abbdea7a..1b7f9c872 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -61,6 +61,7 @@ class Settings extends ChangeNotifier { static const collectionSortFactorKey = 'collection_sort_factor'; static const collectionBrowsingQuickActionsKey = 'collection_browsing_quick_actions'; static const collectionSelectionQuickActionsKey = 'collection_selection_quick_actions'; + static const showThumbnailFavouriteKey = 'show_thumbnail_favourite'; static const showThumbnailLocationKey = 'show_thumbnail_location'; static const showThumbnailMotionPhotoKey = 'show_thumbnail_motion_photo'; static const showThumbnailRatingKey = 'show_thumbnail_rating'; @@ -303,6 +304,10 @@ class Settings extends ChangeNotifier { set collectionSelectionQuickActions(List 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); set showThumbnailLocation(bool newValue) => setAndNotify(showThumbnailLocationKey, newValue); @@ -622,6 +627,7 @@ class Settings extends ChangeNotifier { case isInstalledAppAccessAllowedKey: case isErrorReportingAllowedKey: case mustBackTwiceToExitKey: + case showThumbnailFavouriteKey: case showThumbnailLocationKey: case showThumbnailMotionPhotoKey: case showThumbnailRatingKey: diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index 1bdaedae4..e8934726f 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -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/basic/menu.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/dialogs/tile_view_dialog.dart'; import 'package:aves/widgets/search/search_delegate.dart'; @@ -102,50 +103,42 @@ class _CollectionAppBarState extends State with SingleTickerPr @override Widget build(BuildContext context) { final appMode = context.watch>().value; - return Selector, Tuple2>( - selector: (context, selection) => Tuple2(selection.isSelecting, selection.selectedItems.length), - builder: (context, s, child) { - final isSelecting = s.item1; - final selectedItemCount = s.item2; - _isSelectingNotifier.value = isSelecting; - return AnimatedBuilder( - animation: collection.filterChangeNotifier, - builder: (context, child) { - final removableFilters = appMode != AppMode.pickInternal; - return Selector( - selector: (context, query) => query.enabled, - builder: (context, queryEnabled, child) { - return SliverAppBar( - leading: appMode.hasDrawer ? _buildAppBarLeading(isSelecting) : null, - title: SliverAppBarTitleWrapper( - child: _buildAppBarTitle(isSelecting), - ), - actions: _buildActions( - isSelecting: isSelecting, - selectedItemCount: selectedItemCount, - ), - bottom: PreferredSize( - preferredSize: Size.fromHeight(appBarBottomHeight), - child: Column( - children: [ - if (showFilterBar) - FilterBar( - filters: collection.filters.where((v) => !(v is QueryFilter && v.live)).toSet(), - removable: removableFilters, - onTap: removableFilters ? collection.removeFilter : null, - ), - if (queryEnabled) - EntryQueryBar( - queryNotifier: context.select>((query) => query.queryNotifier), - focusNode: _queryBarFocusNode, - ) - ], - ), - ), - titleSpacing: 0, - floating: true, - ); - }, + final selection = context.watch>(); + final isSelecting = selection.isSelecting; + _isSelectingNotifier.value = isSelecting; + return AnimatedBuilder( + animation: collection.filterChangeNotifier, + builder: (context, child) { + final removableFilters = appMode != AppMode.pickInternal; + return Selector( + selector: (context, query) => query.enabled, + builder: (context, queryEnabled, child) { + return SliverAppBar( + leading: appMode.hasDrawer ? _buildAppBarLeading(isSelecting) : null, + title: SliverAppBarTitleWrapper( + child: _buildAppBarTitle(isSelecting), + ), + actions: _buildActions(selection), + bottom: PreferredSize( + preferredSize: Size.fromHeight(appBarBottomHeight), + child: Column( + children: [ + if (showFilterBar) + FilterBar( + filters: collection.filters.where((v) => !(v is QueryFilter && v.live)).toSet(), + removable: removableFilters, + onTap: removableFilters ? collection.removeFilter : null, + ), + if (queryEnabled) + EntryQueryBar( + queryNotifier: context.select>((query) => query.queryNotifier), + focusNode: _queryBarFocusNode, + ) + ], + ), + ), + titleSpacing: 0, + floating: true, ); }, ); @@ -204,10 +197,10 @@ class _CollectionAppBarState extends State with SingleTickerPr } } - List _buildActions({ - required bool isSelecting, - required int selectedItemCount, - }) { + List _buildActions(Selection selection) { + final isSelecting = selection.isSelecting; + final selectedItemCount = selection.selectedItems.length; + final appMode = context.watch>().value; bool isVisible(EntrySetAction action) => _actionDelegate.isVisible( action, @@ -227,7 +220,7 @@ class _CollectionAppBarState extends State with SingleTickerPr final browsingQuickActions = settings.collectionBrowsingQuickActions; final selectionQuickActions = settings.collectionSelectionQuickActions; final quickActionButtons = (isSelecting ? selectionQuickActions : browsingQuickActions).where(isVisible).map( - (action) => _toActionButton(action, enabled: canApply(action)), + (action) => _toActionButton(action, enabled: canApply(action), selection: selection), ); return [ @@ -238,14 +231,14 @@ class _CollectionAppBarState extends State with SingleTickerPr key: const Key('appbar-menu-button'), itemBuilder: (context) { 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 selectionMenuActions = EntrySetActions.selection.where((v) => !selectionQuickActions.contains(v)); final contextualMenuItems = [ ...(isSelecting ? selectionMenuActions : browsingMenuActions).where(isVisible).map( - (action) => _toMenuItem(action, enabled: canApply(action)), + (action) => _toMenuItem(action, enabled: canApply(action), selection: selection), ), if (isSelecting) PopupMenuItem( @@ -262,7 +255,7 @@ class _CollectionAppBarState extends State with SingleTickerPr EntrySetAction.editRating, EntrySetAction.editTags, 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 with SingleTickerPr ]; } + Set _getExpandedSelectedItems(Selection selection) { + return selection.selectedItems.expand((entry) => entry.burstEntries ?? {entry}).toSet(); + } + // key is expected by test driver (e.g. 'menu-configureView', 'menu-map') Key _getActionKey(EntrySetAction action) => Key('menu-${action.name}'); - Widget _toActionButton(EntrySetAction action, {required bool enabled}) { + Widget _toActionButton(EntrySetAction action, {required bool enabled, required Selection selection}) { final onPressed = enabled ? () => _onActionSelected(action) : null; switch (action) { case EntrySetAction.toggleTitleSearch: @@ -302,6 +299,11 @@ class _CollectionAppBarState extends State with SingleTickerPr ); }, ); + case EntrySetAction.toggleFavourite: + return FavouriteToggler( + entries: _getExpandedSelectedItems(selection), + onPressed: onPressed, + ); default: return IconButton( key: _getActionKey(action), @@ -312,7 +314,7 @@ class _CollectionAppBarState extends State with SingleTickerPr } } - PopupMenuItem _toMenuItem(EntrySetAction action, {required bool enabled}) { + PopupMenuItem _toMenuItem(EntrySetAction action, {required bool enabled, required Selection selection}) { late Widget child; switch (action) { case EntrySetAction.toggleTitleSearch: @@ -321,6 +323,12 @@ class _CollectionAppBarState extends State with SingleTickerPr isMenuItem: true, ); break; + case EntrySetAction.toggleFavourite: + child = FavouriteToggler( + entries: _getExpandedSelectedItems(selection), + isMenuItem: true, + ); + break; default: child = MenuRow(text: action.getText(context), icon: action.getIcon()); break; @@ -424,6 +432,7 @@ class _CollectionAppBarState extends State with SingleTickerPr case EntrySetAction.copy: case EntrySetAction.move: case EntrySetAction.rescan: + case EntrySetAction.toggleFavourite: case EntrySetAction.rotateCCW: case EntrySetAction.rotateCW: case EntrySetAction.flip: diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart index 4df61a326..b1d647b86 100644 --- a/lib/widgets/collection/collection_grid.dart +++ b/lib/widgets/collection/collection_grid.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:aves/app_mode.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/mime.dart'; import 'package:aves/model/settings/settings.dart'; @@ -103,13 +104,18 @@ class _CollectionGridContent extends StatelessWidget { columnCount: columnCount, spacing: tileSpacing, tileExtent: thumbnailExtent, - tileBuilder: (entry) => InteractiveTile( - key: ValueKey(entry.contentId), - collection: collection, - entry: entry, - thumbnailExtent: thumbnailExtent, - tileLayout: tileLayout, - isScrollingNotifier: _isScrollingNotifier, + tileBuilder: (entry) => AnimatedBuilder( + animation: favourites, + builder: (context, child) { + return InteractiveTile( + key: ValueKey(entry.contentId), + collection: collection, + entry: entry, + thumbnailExtent: thumbnailExtent, + tileLayout: tileLayout, + isScrollingNotifier: _isScrollingNotifier, + ); + }, ), tileAnimationDelay: tileAnimationDelay, child: child!, diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index ab8211a1b..dc5b75c6f 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -7,6 +7,7 @@ import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/device.dart'; import 'package:aves/model/entry.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/filters.dart'; import 'package:aves/model/highlight.dart'; @@ -73,6 +74,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa case EntrySetAction.copy: case EntrySetAction.move: case EntrySetAction.rescan: + case EntrySetAction.toggleFavourite: case EntrySetAction.rotateCCW: case EntrySetAction.rotateCW: case EntrySetAction.flip: @@ -115,6 +117,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa case EntrySetAction.copy: case EntrySetAction.move: case EntrySetAction.rescan: + case EntrySetAction.toggleFavourite: case EntrySetAction.rotateCCW: case EntrySetAction.rotateCW: case EntrySetAction.flip: @@ -167,6 +170,9 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa case EntrySetAction.rescan: _rescan(context); break; + case EntrySetAction.toggleFavourite: + _toggleFavourite(context); + break; case EntrySetAction.rotateCCW: _rotate(context, clockwise: false); break; @@ -214,6 +220,18 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa selection.browse(); } + Future _toggleFavourite(BuildContext context) async { + final selection = context.read>(); + final selectedItems = _getExpandedSelectedItems(selection); + if (selectedItems.every((entry) => entry.isFavourite)) { + await favourites.remove(selectedItems); + } else { + await favourites.add(selectedItems); + } + + selection.browse(); + } + Future _delete(BuildContext context) async { final source = context.read(); final selection = context.read>(); diff --git a/lib/widgets/common/favourite_toggler.dart b/lib/widgets/common/favourite_toggler.dart new file mode 100644 index 000000000..caec381e2 --- /dev/null +++ b/lib/widgets/common/favourite_toggler.dart @@ -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 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 { + final ValueNotifier isFavouriteNotifier = ValueNotifier(false); + + Set 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( + 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); + } +} diff --git a/lib/widgets/common/grid/theme.dart b/lib/widgets/common/grid/theme.dart index 442754db6..2422ab2bd 100644 --- a/lib/widgets/common/grid/theme.dart +++ b/lib/widgets/common/grid/theme.dart @@ -28,6 +28,7 @@ class GridTheme extends StatelessWidget { iconSize: iconSize, fontSize: fontSize, highlightBorderWidth: highlightBorderWidth, + showFavourite: settings.showThumbnailFavourite, showLocation: showLocation ?? settings.showThumbnailLocation, showMotionPhoto: settings.showThumbnailMotionPhoto, showRating: settings.showThumbnailRating, @@ -42,12 +43,13 @@ class GridTheme extends StatelessWidget { class GridThemeData { final double iconSize, fontSize, highlightBorderWidth; - final bool showLocation, showMotionPhoto, showRating, showRaw, showVideoDuration; + final bool showFavourite, showLocation, showMotionPhoto, showRating, showRaw, showVideoDuration; const GridThemeData({ required this.iconSize, required this.fontSize, required this.highlightBorderWidth, + required this.showFavourite, required this.showLocation, required this.showMotionPhoto, required this.showRating, diff --git a/lib/widgets/common/identity/aves_icons.dart b/lib/widgets/common/identity/aves_icons.dart index a2f85c220..4527a50ac 100644 --- a/lib/widgets/common/identity/aves_icons.dart +++ b/lib/widgets/common/identity/aves_icons.dart @@ -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 { const GpsIcon({Key? key}) : super(key: key); diff --git a/lib/widgets/common/thumbnail/overlay.dart b/lib/widgets/common/thumbnail/overlay.dart index 795a7e814..388604013 100644 --- a/lib/widgets/common/thumbnail/overlay.dart +++ b/lib/widgets/common/thumbnail/overlay.dart @@ -19,6 +19,7 @@ class ThumbnailEntryOverlay extends StatelessWidget { @override Widget build(BuildContext context) { final children = [ + if (entry.isFavourite && context.select((t) => t.showFavourite)) const FavouriteIcon(), if (entry.hasGps && context.select((t) => t.showLocation)) const GpsIcon(), if (entry.rating != 0 && context.select((t) => t.showRating)) RatingIcon(entry: entry), if (entry.isVideo) diff --git a/lib/widgets/settings/thumbnails/thumbnails.dart b/lib/widgets/settings/thumbnails/thumbnails.dart index ed8c8fb1a..1ff395e94 100644 --- a/lib/widgets/settings/thumbnails/thumbnails.dart +++ b/lib/widgets/settings/thumbnails/thumbnails.dart @@ -33,6 +33,29 @@ class ThumbnailsSection extends StatelessWidget { showHighlight: false, children: [ const CollectionActionsTile(), + Selector( + 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( selector: (context, s) => s.showThumbnailLocation, builder: (context, current, child) => SwitchListTile( diff --git a/lib/widgets/viewer/overlay/top.dart b/lib/widgets/viewer/overlay/top.dart index b8b8087cb..846f258d5 100644 --- a/lib/widgets/viewer/overlay/top.dart +++ b/lib/widgets/viewer/overlay/top.dart @@ -1,14 +1,11 @@ import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; -import 'package:aves/model/favourites.dart'; import 'package:aves/model/settings/settings.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/popup_menu_button.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/common/fx/sweeper.dart'; +import 'package:aves/widgets/common/favourite_toggler.dart'; import 'package:aves/widgets/viewer/action/entry_action_delegate.dart'; import 'package:aves/widgets/viewer/multipage/conductor.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; @@ -204,8 +201,8 @@ class _TopOverlayRow extends StatelessWidget { void onPressed() => _onActionSelected(context, action); switch (action) { case EntryAction.toggleFavourite: - child = _FavouriteToggler( - entry: favouriteTargetEntry, + child = FavouriteToggler( + entries: {favouriteTargetEntry}, onPressed: onPressed, ); break; @@ -251,8 +248,8 @@ class _TopOverlayRow extends StatelessWidget { switch (action) { // in app actions case EntryAction.toggleFavourite: - child = _FavouriteToggler( - entry: favouriteTargetEntry, + child = FavouriteToggler( + entries: {favouriteTargetEntry}, isMenuItem: true, ); break; @@ -315,80 +312,3 @@ class _TopOverlayRow extends StatelessWidget { 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 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( - 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; - } -} diff --git a/untranslated.json b/untranslated.json index e9cb2d571..4789f58a8 100644 --- a/untranslated.json +++ b/untranslated.json @@ -12,6 +12,7 @@ "editEntryRatingDialogTitle", "collectionSortRating", "searchSectionRating", + "settingsThumbnailShowFavouriteIcon", "settingsThumbnailShowRating" ], @@ -42,6 +43,7 @@ "editEntryRatingDialogTitle", "collectionSortRating", "searchSectionRating", + "settingsThumbnailShowFavouriteIcon", "settingsThumbnailShowRating" ] }