#149 fav: toggle multiple items, thumbnail overlay icon

This commit is contained in:
Thibault Deckers 2022-01-05 18:06:21 +09:00
parent 862a8003fa
commit aa6a00b080
18 changed files with 251 additions and 151 deletions

View file

@ -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",

View file

@ -371,6 +371,7 @@
"settingsNavigationDrawerAddAlbum": "Ajouter un album",
"settingsSectionThumbnails": "Vignettes",
"settingsThumbnailShowFavouriteIcon": "Afficher licône de favori",
"settingsThumbnailShowLocationIcon": "Afficher licône de lieu",
"settingsThumbnailShowMotionPhotoIcon": "Afficher licône de photo animée",
"settingsThumbnailShowRating": "Afficher la notation",

View file

@ -371,6 +371,7 @@
"settingsNavigationDrawerAddAlbum": "앨범 추가",
"settingsSectionThumbnails": "섬네일",
"settingsThumbnailShowFavouriteIcon": "즐겨찾기 아이콘 표시",
"settingsThumbnailShowLocationIcon": "위치 아이콘 표시",
"settingsThumbnailShowMotionPhotoIcon": "모션 포토 아이콘 표시",
"settingsThumbnailShowRating": "별점 표시",

View file

@ -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:

View file

@ -685,13 +685,13 @@ class AvesEntry {
Future<void> addToFavourites() async {
if (!isFavourite) {
await favourites.add([this]);
await favourites.add({this});
}
}
Future<void> removeFromFavourites() async {
if (isFavourite) {
await favourites.remove([this]);
await favourites.remove({this});
}
}

View file

@ -21,7 +21,7 @@ class Favourites with ChangeNotifier {
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);
await metadataDb.addFavourites(newRows);
@ -30,7 +30,7 @@ class Favourites with ChangeNotifier {
notifyListeners();
}
Future<void> remove(Iterable<AvesEntry> entries) async {
Future<void> remove(Set<AvesEntry> entries) async {
final contentIds = entries.map((entry) => entry.contentId).toSet();
final removedRows = _rows.where((row) => contentIds.contains(row.contentId)).toSet();

View file

@ -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;

View file

@ -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<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);
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:

View file

@ -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<CollectionAppBar> with SingleTickerPr
@override
Widget build(BuildContext context) {
final appMode = context.watch<ValueNotifier<AppMode>>().value;
return Selector<Selection<AvesEntry>, Tuple2<bool, int>>(
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<Query, bool>(
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, ValueNotifier<String>>((query) => query.queryNotifier),
focusNode: _queryBarFocusNode,
)
],
),
),
titleSpacing: 0,
floating: true,
);
},
final selection = context.watch<Selection<AvesEntry>>();
final isSelecting = selection.isSelecting;
_isSelectingNotifier.value = isSelecting;
return AnimatedBuilder(
animation: collection.filterChangeNotifier,
builder: (context, child) {
final removableFilters = appMode != AppMode.pickInternal;
return Selector<Query, bool>(
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, ValueNotifier<String>>((query) => query.queryNotifier),
focusNode: _queryBarFocusNode,
)
],
),
),
titleSpacing: 0,
floating: true,
);
},
);
@ -204,10 +197,10 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
}
}
List<Widget> _buildActions({
required bool isSelecting,
required int selectedItemCount,
}) {
List<Widget> _buildActions(Selection<AvesEntry> selection) {
final isSelecting = selection.isSelecting;
final selectedItemCount = selection.selectedItems.length;
final appMode = context.watch<ValueNotifier<AppMode>>().value;
bool isVisible(EntrySetAction action) => _actionDelegate.isVisible(
action,
@ -227,7 +220,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> 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<CollectionAppBar> 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<EntrySetAction>(
@ -262,7 +255,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> 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<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 _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;
switch (action) {
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:
return IconButton(
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;
switch (action) {
case EntrySetAction.toggleTitleSearch:
@ -321,6 +323,12 @@ class _CollectionAppBarState extends State<CollectionAppBar> 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<CollectionAppBar> with SingleTickerPr
case EntrySetAction.copy:
case EntrySetAction.move:
case EntrySetAction.rescan:
case EntrySetAction.toggleFavourite:
case EntrySetAction.rotateCCW:
case EntrySetAction.rotateCW:
case EntrySetAction.flip:

View file

@ -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!,

View file

@ -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<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 {
final source = context.read<CollectionSource>();
final selection = context.read<Selection<AvesEntry>>();

View 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);
}
}

View file

@ -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,

View file

@ -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);

View file

@ -19,6 +19,7 @@ class ThumbnailEntryOverlay extends StatelessWidget {
@override
Widget build(BuildContext context) {
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.rating != 0 && context.select<GridThemeData, bool>((t) => t.showRating)) RatingIcon(entry: entry),
if (entry.isVideo)

View file

@ -33,6 +33,29 @@ class ThumbnailsSection extends StatelessWidget {
showHighlight: false,
children: [
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: (context, s) => s.showThumbnailLocation,
builder: (context, current, child) => SwitchListTile(

View file

@ -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<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;
}
}

View file

@ -12,6 +12,7 @@
"editEntryRatingDialogTitle",
"collectionSortRating",
"searchSectionRating",
"settingsThumbnailShowFavouriteIcon",
"settingsThumbnailShowRating"
],
@ -42,6 +43,7 @@
"editEntryRatingDialogTitle",
"collectionSortRating",
"searchSectionRating",
"settingsThumbnailShowFavouriteIcon",
"settingsThumbnailShowRating"
]
}