fullscreen: modified widget rebuild logic

This commit is contained in:
Thibault Deckers 2020-06-08 17:07:37 +09:00
parent 5fe985537f
commit be664f0967
3 changed files with 286 additions and 198 deletions

View file

@ -38,7 +38,7 @@ class FullscreenBody extends StatefulWidget {
} }
class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProviderStateMixin, WidgetsBindingObserver { class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProviderStateMixin, WidgetsBindingObserver {
ImageEntry _entry; final ValueNotifier<ImageEntry> _entryNotifier = ValueNotifier(null);
int _currentHorizontalPage; int _currentHorizontalPage;
ValueNotifier<int> _currentVerticalPage; ValueNotifier<int> _currentVerticalPage;
PageController _horizontalPager, _verticalPager; PageController _horizontalPager, _verticalPager;
@ -57,19 +57,18 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
List<ImageEntry> get entries => hasCollection ? collection.sortedEntries : [widget.initialEntry]; List<ImageEntry> get entries => hasCollection ? collection.sortedEntries : [widget.initialEntry];
List<String> get pages => ['transition', 'image', 'info']; static const int transitionPage = 0;
int get transitionPage => pages.indexOf('transition'); static const int imagePage = 1;
int get imagePage => pages.indexOf('image'); static const int infoPage = 2;
int get infoPage => pages.indexOf('info');
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_entry = widget.initialEntry; final entry = widget.initialEntry;
_currentHorizontalPage = max(0, entries.indexOf(_entry)); _entryNotifier.value = entry;
_currentHorizontalPage = max(0, entries.indexOf(entry));
_currentVerticalPage = ValueNotifier(imagePage); _currentVerticalPage = ValueNotifier(imagePage);
_horizontalPager = PageController(initialPage: _currentHorizontalPage); _horizontalPager = PageController(initialPage: _currentHorizontalPage);
_verticalPager = PageController(initialPage: _currentVerticalPage.value)..addListener(_onVerticalPageControllerChange); _verticalPager = PageController(initialPage: _currentVerticalPage.value)..addListener(_onVerticalPageControllerChange);
@ -138,76 +137,6 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final topOverlay = ValueListenableBuilder<int>(
valueListenable: _currentVerticalPage,
builder: (context, page, child) {
final showOverlay = _entry != null && page == imagePage;
return showOverlay
? FullscreenTopOverlay(
entries: entries,
index: _currentHorizontalPage,
scale: _topOverlayScale,
canToggleFavourite: hasCollection,
viewInsets: _frozenViewInsets,
viewPadding: _frozenViewPadding,
onActionSelected: (action) => _actionDelegate.onActionSelected(context, _entry, action),
)
: const SizedBox.shrink();
},
);
final videoController = _entry != null && _entry.isVideo ? _videoControllers.firstWhere((kv) => kv.item1 == _entry.uri, orElse: () => null)?.item2 : null;
Widget bottomOverlay = Column(
children: [
if (videoController != null)
VideoControlOverlay(
entry: _entry,
controller: videoController,
scale: _bottomOverlayScale,
viewInsets: _frozenViewInsets,
viewPadding: _frozenViewPadding,
),
SlideTransition(
position: _bottomOverlayOffset,
child: FullscreenBottomOverlay(
entries: entries,
index: _currentHorizontalPage,
showPosition: hasCollection,
viewInsets: _frozenViewInsets,
viewPadding: _frozenViewPadding,
),
),
],
);
bottomOverlay = ValueListenableBuilder<double>(
valueListenable: _overlayAnimationController,
builder: (context, animation, child) {
return Visibility(
visible: _entry != null && _overlayAnimationController.status != AnimationStatus.dismissed,
child: child,
);
},
child: bottomOverlay,
);
bottomOverlay = Selector<MediaQueryData, double>(
selector: (c, mq) => mq.size.height,
builder: (c, mqHeight, child) {
// when orientation change, the `PageController` offset is not updated right away
// and it does not trigger its listeners when it does, so we force a refresh in the next frame
WidgetsBinding.instance.addPostFrameCallback((_) => _onVerticalPageControllerChange());
return AnimatedBuilder(
animation: _verticalScrollNotifier,
builder: (context, child) => Positioned(
bottom: (_verticalPager.offset ?? 0) - mqHeight,
child: child,
),
child: child,
);
},
child: bottomOverlay,
);
return WillPopScope( return WillPopScope(
onWillPop: () { onWillPop: () {
if (_currentVerticalPage.value == infoPage) { if (_currentVerticalPage.value == infoPage) {
@ -227,7 +156,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
children: [ children: [
FullscreenVerticalPageView( FullscreenVerticalPageView(
collection: collection, collection: collection,
entry: _entry, entryNotifier: _entryNotifier,
videoControllers: _videoControllers, videoControllers: _videoControllers,
verticalPager: _verticalPager, verticalPager: _verticalPager,
horizontalPager: _horizontalPager, horizontalPager: _horizontalPager,
@ -236,14 +165,106 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
onImageTap: () => _overlayVisible.value = !_overlayVisible.value, onImageTap: () => _overlayVisible.value = !_overlayVisible.value,
onImagePageRequested: () => _goToVerticalPage(imagePage), onImagePageRequested: () => _goToVerticalPage(imagePage),
), ),
topOverlay, _buildTopOverlay(),
bottomOverlay, _buildBottomOverlay(),
], ],
), ),
), ),
); );
} }
Widget _buildTopOverlay() {
final child = ValueListenableBuilder<ImageEntry>(
valueListenable: _entryNotifier,
builder: (context, entry, child) {
if (entry == null) return const SizedBox.shrink();
return FullscreenTopOverlay(
entry: entry,
scale: _topOverlayScale,
canToggleFavourite: hasCollection,
viewInsets: _frozenViewInsets,
viewPadding: _frozenViewPadding,
onActionSelected: (action) => _actionDelegate.onActionSelected(context, entry, action),
);
},
);
return ValueListenableBuilder<int>(
valueListenable: _currentVerticalPage,
builder: (context, page, child) {
return Visibility(
visible: page == imagePage,
child: child,
);
},
child: child,
);
}
Widget _buildBottomOverlay() {
Widget bottomOverlay = ValueListenableBuilder<ImageEntry>(
valueListenable: _entryNotifier,
builder: (context, entry, child) {
Widget videoOverlay;
if (entry != null) {
final videoController = entry.isVideo ? _videoControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2 : null;
if (videoController != null) {
videoOverlay = VideoControlOverlay(
entry: entry,
controller: videoController,
scale: _bottomOverlayScale,
viewInsets: _frozenViewInsets,
viewPadding: _frozenViewPadding,
);
}
}
final child = Column(
children: [
if (videoOverlay != null) videoOverlay,
SlideTransition(
position: _bottomOverlayOffset,
child: FullscreenBottomOverlay(
entries: entries,
index: _currentHorizontalPage,
showPosition: hasCollection,
viewInsets: _frozenViewInsets,
viewPadding: _frozenViewPadding,
),
),
],
);
return ValueListenableBuilder<double>(
valueListenable: _overlayAnimationController,
builder: (context, animation, child) {
return Visibility(
visible: entry != null && _overlayAnimationController.status != AnimationStatus.dismissed,
child: child,
);
},
child: child,
);
},
);
bottomOverlay = Selector<MediaQueryData, double>(
selector: (c, mq) => mq.size.height,
builder: (c, mqHeight, child) {
// when orientation change, the `PageController` offset is not updated right away
// and it does not trigger its listeners when it does, so we force a refresh in the next frame
WidgetsBinding.instance.addPostFrameCallback((_) => _onVerticalPageControllerChange());
return AnimatedBuilder(
animation: _verticalScrollNotifier,
builder: (context, child) => Positioned(
bottom: (_verticalPager.offset ?? 0) - mqHeight,
child: child,
),
child: child,
);
},
child: bottomOverlay,
);
return bottomOverlay;
}
void _onVerticalPageControllerChange() { void _onVerticalPageControllerChange() {
_verticalScrollNotifier.notifyListeners(); _verticalScrollNotifier.notifyListeners();
} }
@ -286,11 +307,10 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
void _updateEntry() { void _updateEntry() {
final newEntry = _currentHorizontalPage != null && _currentHorizontalPage < entries.length ? entries[_currentHorizontalPage] : null; final newEntry = _currentHorizontalPage != null && _currentHorizontalPage < entries.length ? entries[_currentHorizontalPage] : null;
if (_entry == newEntry) return; if (_entryNotifier.value == newEntry) return;
_entry = newEntry; _entryNotifier.value = newEntry;
_pauseVideoControllers(); _pauseVideoControllers();
_initVideoController(); _initVideoController();
setState(() {});
} }
void _onLeave() { void _onLeave() {
@ -348,9 +368,10 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
void _pauseVideoControllers() => _videoControllers.forEach((e) => e.item2.pause()); void _pauseVideoControllers() => _videoControllers.forEach((e) => e.item2.pause());
Future<void> _initVideoController() async { Future<void> _initVideoController() async {
if (_entry == null || !_entry.isVideo) return; final entry = _entryNotifier.value;
if (entry == null || !entry.isVideo) return;
final uri = _entry.uri; final uri = entry.uri;
var controllerEntry = _videoControllers.firstWhere((kv) => kv.item1 == uri, orElse: () => null); var controllerEntry = _videoControllers.firstWhere((kv) => kv.item1 == uri, orElse: () => null);
if (controllerEntry != null) { if (controllerEntry != null) {
_videoControllers.remove(controllerEntry); _videoControllers.remove(controllerEntry);
@ -362,12 +383,13 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
while (_videoControllers.length > 3) { while (_videoControllers.length > 3) {
_videoControllers.removeLast().item2.dispose(); _videoControllers.removeLast().item2.dispose();
} }
setState(() {});
} }
} }
class FullscreenVerticalPageView extends StatefulWidget { class FullscreenVerticalPageView extends StatefulWidget {
final CollectionLens collection; final CollectionLens collection;
final ImageEntry entry; final ValueNotifier<ImageEntry> entryNotifier;
final List<Tuple2<String, IjkMediaController>> videoControllers; final List<Tuple2<String, IjkMediaController>> videoControllers;
final PageController horizontalPager, verticalPager; final PageController horizontalPager, verticalPager;
final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged; final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged;
@ -375,7 +397,7 @@ class FullscreenVerticalPageView extends StatefulWidget {
const FullscreenVerticalPageView({ const FullscreenVerticalPageView({
@required this.collection, @required this.collection,
@required this.entry, @required this.entryNotifier,
@required this.videoControllers, @required this.videoControllers,
@required this.verticalPager, @required this.verticalPager,
@required this.horizontalPager, @required this.horizontalPager,
@ -392,12 +414,13 @@ class FullscreenVerticalPageView extends StatefulWidget {
class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView> { class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView> {
final ValueNotifier<Color> _backgroundColorNotifier = ValueNotifier(Colors.black); final ValueNotifier<Color> _backgroundColorNotifier = ValueNotifier(Colors.black);
final ValueNotifier<bool> _infoPageVisibleNotifier = ValueNotifier(false); final ValueNotifier<bool> _infoPageVisibleNotifier = ValueNotifier(false);
ImageEntry _oldEntry;
CollectionLens get collection => widget.collection; CollectionLens get collection => widget.collection;
bool get hasCollection => collection != null; bool get hasCollection => collection != null;
ImageEntry get entry => widget.entry; ImageEntry get entry => widget.entryNotifier.value;
@override @override
void initState() { void initState() {
@ -420,12 +443,12 @@ class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView>
void _registerWidget(FullscreenVerticalPageView widget) { void _registerWidget(FullscreenVerticalPageView widget) {
widget.verticalPager.addListener(_onVerticalPageControllerChanged); widget.verticalPager.addListener(_onVerticalPageControllerChanged);
widget.entry?.imageChangeNotifier?.addListener(_onImageChanged); widget.entryNotifier.addListener(_onEntryChanged);
} }
void _unregisterWidget(FullscreenVerticalPageView widget) { void _unregisterWidget(FullscreenVerticalPageView widget) {
widget.verticalPager.removeListener(_onVerticalPageControllerChanged); widget.verticalPager.removeListener(_onVerticalPageControllerChanged);
widget.entry?.imageChangeNotifier?.removeListener(_onImageChanged); widget.entryNotifier.removeListener(_onEntryChanged);
} }
@override @override
@ -453,7 +476,7 @@ class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView>
}, },
child: InfoPage( child: InfoPage(
collection: collection, collection: collection,
entry: entry, entryNotifier: widget.entryNotifier,
visibleNotifier: _infoPageVisibleNotifier, visibleNotifier: _infoPageVisibleNotifier,
), ),
), ),
@ -482,6 +505,14 @@ class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView>
_backgroundColorNotifier.value = _backgroundColorNotifier.value.withOpacity(opacity * opacity); _backgroundColorNotifier.value = _backgroundColorNotifier.value.withOpacity(opacity * opacity);
} }
// when the entry changed (e.g. by scrolling through the PageView, or if the entry got deleted)
void _onEntryChanged() {
_oldEntry?.imageChangeNotifier?.removeListener(_onImageChanged);
entry?.imageChangeNotifier?.addListener(_onImageChanged);
_oldEntry = entry;
}
// when the entry image itself changed (e.g. after rotation)
void _onImageChanged() async { void _onImageChanged() async {
await UriImage(uri: entry.uri, mimeType: entry.mimeType).evict(); await UriImage(uri: entry.uri, mimeType: entry.mimeType).evict();
// TODO TLAD also evict `ThumbnailProvider` with specified extents // TODO TLAD also evict `ThumbnailProvider` with specified extents

View file

@ -14,14 +14,14 @@ import 'package:tuple/tuple.dart';
class InfoPage extends StatefulWidget { class InfoPage extends StatefulWidget {
final CollectionLens collection; final CollectionLens collection;
final ImageEntry entry; final ValueNotifier<ImageEntry> entryNotifier;
final ValueNotifier<bool> visibleNotifier; final ValueNotifier<bool> visibleNotifier;
const InfoPage({ const InfoPage({
Key key, Key key,
@required this.collection, @required this.collection,
@required this.entry, @required this.entryNotifier,
this.visibleNotifier, @required this.visibleNotifier,
}) : super(key: key); }) : super(key: key);
@override @override
@ -34,12 +34,31 @@ class InfoPageState extends State<InfoPage> {
CollectionLens get collection => widget.collection; CollectionLens get collection => widget.collection;
ImageEntry get entry => widget.entry;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
entry.locate(); _registerWidget(widget);
}
@override
void didUpdateWidget(InfoPage oldWidget) {
super.didUpdateWidget(oldWidget);
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
@override
void dispose() {
_unregisterWidget(widget);
super.dispose();
}
void _registerWidget(InfoPage widget) {
widget.entryNotifier.addListener(_onEntryChanged);
}
void _unregisterWidget(InfoPage widget) {
widget.entryNotifier.removeListener(_onEntryChanged);
} }
@override @override
@ -67,8 +86,11 @@ class InfoPageState extends State<InfoPage> {
final mqWidth = mq.item1; final mqWidth = mq.item1;
final mqViewInsetsBottom = mq.item2; final mqViewInsetsBottom = mq.item2;
final split = mqWidth > 400; final split = mqWidth > 400;
final locationAtTop = split && entry.hasGps;
return ValueListenableBuilder(
valueListenable: widget.entryNotifier,
builder: (context, entry, child) {
final locationAtTop = split && entry.hasGps;
final locationSection = LocationSection( final locationSection = LocationSection(
collection: collection, collection: collection,
entry: entry, entry: entry,
@ -115,6 +137,8 @@ class InfoPageState extends State<InfoPage> {
], ],
); );
}, },
);
},
), ),
), ),
), ),
@ -123,6 +147,10 @@ class InfoPageState extends State<InfoPage> {
); );
} }
void _onEntryChanged() {
widget.entryNotifier.value?.locate();
}
bool _handleTopScroll(Notification notification) { bool _handleTopScroll(Notification notification) {
if (notification is ScrollNotification) { if (notification is ScrollNotification) {
if (notification is ScrollStartNotification) { if (notification is ScrollStartNotification) {

View file

@ -9,17 +9,15 @@ import 'package:aves/widgets/fullscreen/overlay/common.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
class FullscreenTopOverlay extends StatelessWidget { class FullscreenTopOverlay extends StatelessWidget {
final List<ImageEntry> entries; final ImageEntry entry;
final int index;
final Animation<double> scale; final Animation<double> scale;
final EdgeInsets viewInsets, viewPadding; final EdgeInsets viewInsets, viewPadding;
final Function(EntryAction value) onActionSelected; final Function(EntryAction value) onActionSelected;
final bool canToggleFavourite; final bool canToggleFavourite;
ImageEntry get entry => entries[index];
static const double padding = 8; static const double padding = 8;
static const int landscapeActionCount = 3; static const int landscapeActionCount = 3;
@ -28,13 +26,12 @@ class FullscreenTopOverlay extends StatelessWidget {
const FullscreenTopOverlay({ const FullscreenTopOverlay({
Key key, Key key,
@required this.entries, @required this.entry,
@required this.index,
@required this.scale, @required this.scale,
this.canToggleFavourite = false, @required this.canToggleFavourite,
this.viewInsets, @required this.viewInsets,
this.viewPadding, @required this.viewPadding,
this.onActionSelected, @required this.onActionSelected,
}) : super(key: key); }) : super(key: key);
@override @override
@ -43,49 +40,31 @@ class FullscreenTopOverlay extends StatelessWidget {
minimum: (viewInsets ?? EdgeInsets.zero) + (viewPadding ?? EdgeInsets.zero), minimum: (viewInsets ?? EdgeInsets.zero) + (viewPadding ?? EdgeInsets.zero),
child: Padding( child: Padding(
padding: const EdgeInsets.all(padding), padding: const EdgeInsets.all(padding),
child: Selector<MediaQueryData, Orientation>( child: Selector<MediaQueryData, Tuple2<double, Orientation>>(
selector: (c, mq) => mq.orientation, selector: (c, mq) => Tuple2(mq.size.width, mq.orientation),
builder: (c, orientation, child) { builder: (c, mq, child) {
final targetCount = orientation == Orientation.landscape ? landscapeActionCount : portraitActionCount; final mqWidth = mq.item1;
return LayoutBuilder( final mqOrientation = mq.item2;
builder: (context, constraints) {
final availableCount = (constraints.maxWidth / (kMinInteractiveDimension + padding)).floor() - 2; final targetCount = mqOrientation == Orientation.landscape ? landscapeActionCount : portraitActionCount;
final availableCount = (mqWidth / (kMinInteractiveDimension + padding)).floor() - 2;
final quickActionCount = min(targetCount, availableCount); final quickActionCount = min(targetCount, availableCount);
final quickActions = [ final quickActions = [
EntryAction.toggleFavourite, EntryAction.toggleFavourite,
EntryAction.share, EntryAction.share,
EntryAction.delete, EntryAction.delete,
].where(_canDo).take(quickActionCount); ].where(_canDo).take(quickActionCount).toList();
final inAppActions = EntryActions.inApp.where((action) => !quickActions.contains(action)).where(_canDo); final inAppActions = EntryActions.inApp.where((action) => !quickActions.contains(action)).where(_canDo).toList();
final externalAppActions = EntryActions.externalApp.where(_canDo); final externalAppActions = EntryActions.externalApp.where(_canDo).toList();
return Row( return _TopOverlayRow(
children: [ quickActions: quickActions,
OverlayButton( inAppActions: inAppActions,
externalAppActions: externalAppActions,
scale: scale, scale: scale,
child: ModalRoute.of(context)?.canPop ?? true ? const BackButton() : const CloseButton(), isFavouriteNotifier: entry.isFavouriteNotifier,
), onActionSelected: onActionSelected,
const Spacer(),
...quickActions.map(_buildOverlayButton),
OverlayButton(
scale: scale,
child: PopupMenuButton<EntryAction>(
itemBuilder: (context) => [
...inAppActions.map(_buildPopupMenuItem),
const PopupMenuDivider(),
...externalAppActions.map(_buildPopupMenuItem),
if (kDebugMode) ...[
const PopupMenuDivider(),
_buildPopupMenuItem(EntryAction.debug),
]
],
onSelected: onActionSelected,
),
),
],
);
},
); );
}, },
), ),
@ -118,6 +97,56 @@ class FullscreenTopOverlay extends StatelessWidget {
} }
return false; return false;
} }
}
class _TopOverlayRow extends StatelessWidget {
final List<EntryAction> quickActions;
final List<EntryAction> inAppActions;
final List<EntryAction> externalAppActions;
final Animation<double> scale;
final ValueNotifier<bool> isFavouriteNotifier;
final Function(EntryAction value) onActionSelected;
const _TopOverlayRow({
Key key,
@required this.quickActions,
@required this.inAppActions,
@required this.externalAppActions,
@required this.scale,
@required this.isFavouriteNotifier,
@required this.onActionSelected,
}) : super(key: key);
static const double padding = 8;
@override
Widget build(BuildContext context) {
return Row(
children: [
OverlayButton(
scale: scale,
child: ModalRoute.of(context)?.canPop ?? true ? const BackButton() : const CloseButton(),
),
const Spacer(),
...quickActions.map(_buildOverlayButton),
OverlayButton(
scale: scale,
child: PopupMenuButton<EntryAction>(
itemBuilder: (context) => [
...inAppActions.map(_buildPopupMenuItem),
const PopupMenuDivider(),
...externalAppActions.map(_buildPopupMenuItem),
if (kDebugMode) ...[
const PopupMenuDivider(),
_buildPopupMenuItem(EntryAction.debug),
]
],
onSelected: onActionSelected,
),
),
],
);
}
Widget _buildOverlayButton(EntryAction action) { Widget _buildOverlayButton(EntryAction action) {
Widget child; Widget child;
@ -125,7 +154,7 @@ class FullscreenTopOverlay extends StatelessWidget {
switch (action) { switch (action) {
case EntryAction.toggleFavourite: case EntryAction.toggleFavourite:
child = ValueListenableBuilder<bool>( child = ValueListenableBuilder<bool>(
valueListenable: entry.isFavouriteNotifier, valueListenable: isFavouriteNotifier,
builder: (context, isFavourite, child) => Stack( builder: (context, isFavourite, child) => Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
@ -136,7 +165,7 @@ class FullscreenTopOverlay extends StatelessWidget {
), ),
Sweeper( Sweeper(
builder: (context) => const Icon(AIcons.favourite, color: Colors.redAccent), builder: (context) => const Icon(AIcons.favourite, color: Colors.redAccent),
toggledNotifier: entry.isFavouriteNotifier, toggledNotifier: isFavouriteNotifier,
), ),
], ],
), ),
@ -178,7 +207,7 @@ class FullscreenTopOverlay extends StatelessWidget {
switch (action) { switch (action) {
// in app actions // in app actions
case EntryAction.toggleFavourite: case EntryAction.toggleFavourite:
child = entry.isFavouriteNotifier.value child = isFavouriteNotifier.value
? const MenuRow( ? const MenuRow(
text: 'Remove from favourites', text: 'Remove from favourites',
icon: AIcons.favouriteActive, icon: AIcons.favouriteActive,