various fixes for copy/move/fav
This commit is contained in:
parent
e26f2b4fb6
commit
b92545f059
9 changed files with 169 additions and 78 deletions
|
@ -31,6 +31,7 @@ public class ImageOpStreamHandler implements EventChannel.StreamHandler {
|
||||||
private List<Map<String, Object>> entryMapList;
|
private List<Map<String, Object>> entryMapList;
|
||||||
private String op;
|
private String op;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
public ImageOpStreamHandler(Activity activity, Object arguments) {
|
public ImageOpStreamHandler(Activity activity, Object arguments) {
|
||||||
this.activity = activity;
|
this.activity = activity;
|
||||||
if (arguments instanceof Map) {
|
if (arguments instanceof Map) {
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/image_metadata.dart';
|
import 'package:aves/model/image_metadata.dart';
|
||||||
import 'package:aves/model/metadata_db.dart';
|
import 'package:aves/model/metadata_db.dart';
|
||||||
|
import 'package:aves/utils/change_notifier.dart';
|
||||||
|
|
||||||
final FavouriteRepo favourites = FavouriteRepo._private();
|
final FavouriteRepo favourites = FavouriteRepo._private();
|
||||||
|
|
||||||
class FavouriteRepo {
|
class FavouriteRepo {
|
||||||
List<FavouriteRow> _rows = [];
|
List<FavouriteRow> _rows = [];
|
||||||
|
|
||||||
|
final AChangeNotifier changeNotifier = AChangeNotifier();
|
||||||
|
|
||||||
FavouriteRepo._private();
|
FavouriteRepo._private();
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
|
@ -23,16 +26,31 @@ class FavouriteRepo {
|
||||||
final newRows = entries.map(_entryToRow);
|
final newRows = entries.map(_entryToRow);
|
||||||
await metadataDb.addFavourites(newRows);
|
await metadataDb.addFavourites(newRows);
|
||||||
_rows.addAll(newRows);
|
_rows.addAll(newRows);
|
||||||
|
changeNotifier.notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> remove(Iterable<ImageEntry> entries) async {
|
Future<void> remove(Iterable<ImageEntry> entries) async {
|
||||||
final removedRows = entries.map(_entryToRow);
|
final removedRows = entries.map(_entryToRow);
|
||||||
await metadataDb.removeFavourites(removedRows);
|
await metadataDb.removeFavourites(removedRows);
|
||||||
removedRows.forEach(_rows.remove);
|
removedRows.forEach(_rows.remove);
|
||||||
|
changeNotifier.notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> move(int oldContentId, ImageEntry entry) async {
|
||||||
|
final oldRow = _rows.firstWhere((row) => row.contentId == oldContentId, orElse: () => null);
|
||||||
|
if (oldRow != null) {
|
||||||
|
_rows.remove(oldRow);
|
||||||
|
|
||||||
|
final newRow = _entryToRow(entry);
|
||||||
|
await metadataDb.updateFavouriteId(oldContentId, newRow);
|
||||||
|
_rows.add(newRow);
|
||||||
|
changeNotifier.notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> clear() async {
|
Future<void> clear() async {
|
||||||
await metadataDb.clearFavourites();
|
await metadataDb.clearFavourites();
|
||||||
_rows.clear();
|
_rows.clear();
|
||||||
|
changeNotifier.notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,6 @@ class ImageEntry {
|
||||||
AddressDetails _addressDetails;
|
AddressDetails _addressDetails;
|
||||||
|
|
||||||
final AChangeNotifier imageChangeNotifier = AChangeNotifier(), metadataChangeNotifier = AChangeNotifier(), addressChangeNotifier = AChangeNotifier();
|
final AChangeNotifier imageChangeNotifier = AChangeNotifier(), metadataChangeNotifier = AChangeNotifier(), addressChangeNotifier = AChangeNotifier();
|
||||||
final ValueNotifier<bool> isFavouriteNotifier = ValueNotifier(false);
|
|
||||||
|
|
||||||
ImageEntry({
|
ImageEntry({
|
||||||
this.uri,
|
this.uri,
|
||||||
|
@ -52,7 +51,6 @@ class ImageEntry {
|
||||||
this.durationMillis,
|
this.durationMillis,
|
||||||
}) {
|
}) {
|
||||||
this.path = path;
|
this.path = path;
|
||||||
isFavouriteNotifier.value = isFavourite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageEntry copyWith({
|
ImageEntry copyWith({
|
||||||
|
@ -120,7 +118,6 @@ class ImageEntry {
|
||||||
imageChangeNotifier.dispose();
|
imageChangeNotifier.dispose();
|
||||||
metadataChangeNotifier.dispose();
|
metadataChangeNotifier.dispose();
|
||||||
addressChangeNotifier.dispose();
|
addressChangeNotifier.dispose();
|
||||||
isFavouriteNotifier.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -362,14 +359,12 @@ class ImageEntry {
|
||||||
void addToFavourites() {
|
void addToFavourites() {
|
||||||
if (!isFavourite) {
|
if (!isFavourite) {
|
||||||
favourites.add([this]);
|
favourites.add([this]);
|
||||||
isFavouriteNotifier.value = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeFromFavourites() {
|
void removeFromFavourites() {
|
||||||
if (isFavourite) {
|
if (isFavourite) {
|
||||||
favourites.remove([this]);
|
favourites.remove([this]);
|
||||||
isFavouriteNotifier.value = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,8 @@ class SectionedListLayoutProvider extends StatelessWidget {
|
||||||
@required this.tileExtent,
|
@required this.tileExtent,
|
||||||
@required this.thumbnailBuilder,
|
@required this.thumbnailBuilder,
|
||||||
@required this.child,
|
@required this.child,
|
||||||
}) : columnCount = max((scrollableWidth / tileExtent).round(), TileExtentManager.columnCountMin);
|
}) : assert(scrollableWidth != 0),
|
||||||
|
columnCount = max((scrollableWidth / tileExtent).round(), TileExtentManager.columnCountMin);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
|
@ -33,6 +33,9 @@ class ThumbnailCollection extends StatelessWidget {
|
||||||
builder: (context, mq, child) {
|
builder: (context, mq, child) {
|
||||||
final mqSize = mq.item1;
|
final mqSize = mq.item1;
|
||||||
final mqHorizontalPadding = mq.item2;
|
final mqHorizontalPadding = mq.item2;
|
||||||
|
|
||||||
|
if (mqSize.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
TileExtentManager.applyTileExtent(mqSize, mqHorizontalPadding, _tileExtentNotifier);
|
TileExtentManager.applyTileExtent(mqSize, mqHorizontalPadding, _tileExtentNotifier);
|
||||||
final cacheExtent = TileExtentManager.extentMaxForSize(mqSize) * 2;
|
final cacheExtent = TileExtentManager.extentMaxForSize(mqSize) * 2;
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/model/collection_lens.dart';
|
import 'package:aves/model/collection_lens.dart';
|
||||||
|
import 'package:aves/model/favourite_repo.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/image_metadata.dart';
|
|
||||||
import 'package:aves/model/metadata_db.dart';
|
import 'package:aves/model/metadata_db.dart';
|
||||||
import 'package:aves/services/android_app_service.dart';
|
import 'package:aves/services/android_app_service.dart';
|
||||||
import 'package:aves/services/image_file_service.dart';
|
import 'package:aves/services/image_file_service.dart';
|
||||||
|
@ -103,7 +103,7 @@ class SelectionActionDelegate with PermissionAwareMixin {
|
||||||
context: context,
|
context: context,
|
||||||
selection: selection,
|
selection: selection,
|
||||||
opStream: ImageFileService.move(selection, copy: copy, destinationAlbum: destinationAlbum),
|
opStream: ImageFileService.move(selection, copy: copy, destinationAlbum: destinationAlbum),
|
||||||
onDone: (Set<MoveOpEvent> processed) {
|
onDone: (Set<MoveOpEvent> processed) async {
|
||||||
debugPrint('$runtimeType _moveSelection onDone');
|
debugPrint('$runtimeType _moveSelection onDone');
|
||||||
final movedOps = processed.where((e) => e.success);
|
final movedOps = processed.where((e) => e.success);
|
||||||
final movedCount = movedOps.length;
|
final movedCount = movedOps.length;
|
||||||
|
@ -130,10 +130,10 @@ class SelectionActionDelegate with PermissionAwareMixin {
|
||||||
contentId: newFields['contentId'] as int,
|
contentId: newFields['contentId'] as int,
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
metadataDb.saveMetadata(movedEntries.map((entry) => entry.catalogMetadata));
|
await metadataDb.saveMetadata(movedEntries.map((entry) => entry.catalogMetadata));
|
||||||
metadataDb.saveAddresses(movedEntries.map((entry) => entry.addressDetails));
|
await metadataDb.saveAddresses(movedEntries.map((entry) => entry.addressDetails));
|
||||||
} else {
|
} else {
|
||||||
movedOps.forEach((movedOp) {
|
await Future.forEach(movedOps, (movedOp) async {
|
||||||
final sourceUri = movedOp.uri;
|
final sourceUri = movedOp.uri;
|
||||||
final newFields = movedOp.newFields;
|
final newFields = movedOp.newFields;
|
||||||
final entry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null);
|
final entry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null);
|
||||||
|
@ -146,9 +146,9 @@ class SelectionActionDelegate with PermissionAwareMixin {
|
||||||
entry.contentId = newContentId;
|
entry.contentId = newContentId;
|
||||||
movedEntries.add(entry);
|
movedEntries.add(entry);
|
||||||
|
|
||||||
metadataDb.updateMetadataId(oldContentId, entry.catalogMetadata);
|
await metadataDb.updateMetadataId(oldContentId, entry.catalogMetadata);
|
||||||
metadataDb.updateAddressId(oldContentId, entry.addressDetails);
|
await metadataDb.updateAddressId(oldContentId, entry.addressDetails);
|
||||||
metadataDb.updateFavouriteId(oldContentId, FavouriteRow(contentId: entry.contentId, path: entry.path));
|
await favourites.move(oldContentId, entry);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -234,17 +234,17 @@ class SelectionActionDelegate with PermissionAwareMixin {
|
||||||
// do not handle completion inside `StreamBuilder`
|
// do not handle completion inside `StreamBuilder`
|
||||||
// as it could be called multiple times
|
// as it could be called multiple times
|
||||||
final onComplete = () => _hideOpReportOverlay().then((_) => onDone(processed));
|
final onComplete = () => _hideOpReportOverlay().then((_) => onDone(processed));
|
||||||
opStream.listen(null, onError: (error) => onComplete(), onDone: onComplete);
|
opStream.listen(
|
||||||
|
(event) => processed.add(event),
|
||||||
|
onError: (error) => onComplete(),
|
||||||
|
onDone: onComplete,
|
||||||
|
);
|
||||||
|
|
||||||
_opReportOverlayEntry = OverlayEntry(
|
_opReportOverlayEntry = OverlayEntry(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return StreamBuilder<T>(
|
return StreamBuilder<T>(
|
||||||
stream: opStream,
|
stream: opStream,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasData) {
|
|
||||||
processed.add(snapshot.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget child = const SizedBox.shrink();
|
Widget child = const SizedBox.shrink();
|
||||||
if (!snapshot.hasError && snapshot.connectionState == ConnectionState.active) {
|
if (!snapshot.hasError && snapshot.connectionState == ConnectionState.active) {
|
||||||
final percent = processed.length.toDouble() / selection.length;
|
final percent = processed.length.toDouble() / selection.length;
|
||||||
|
|
|
@ -68,6 +68,7 @@ class _SweeperState extends State<Sweeper> with SingleTickerProviderStateMixin {
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_angleAnimationController.removeStatusListener(_onAnimationStatusChange);
|
_angleAnimationController.removeStatusListener(_onAnimationStatusChange);
|
||||||
|
_angleAnimationController.dispose();
|
||||||
_unregisterWidget(widget);
|
_unregisterWidget(widget);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
@ -114,10 +115,14 @@ class _SweeperState extends State<Sweeper> with SingleTickerProviderStateMixin {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
await Future.delayed(Duration(milliseconds: (opacityAnimationDurationMillis * timeDilation).toInt()));
|
await Future.delayed(Duration(milliseconds: (opacityAnimationDurationMillis * timeDilation).toInt()));
|
||||||
_isAppearing = false;
|
_isAppearing = false;
|
||||||
_angleAnimationController.reset();
|
if (mounted) {
|
||||||
_angleAnimationController.forward();
|
_angleAnimationController.reset();
|
||||||
|
_angleAnimationController.forward();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
}
|
}
|
||||||
setState(() {});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/model/collection_lens.dart';
|
import 'package:aves/model/collection_lens.dart';
|
||||||
|
import 'package:aves/model/favourite_repo.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.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';
|
||||||
|
@ -31,7 +32,6 @@ class BasicSection extends StatelessWidget {
|
||||||
final showMegaPixels = entry.isPhoto && entry.megaPixels != null && entry.megaPixels > 0;
|
final showMegaPixels = entry.isPhoto && entry.megaPixels != null && entry.megaPixels > 0;
|
||||||
final resolutionText = '${entry.width ?? '?'} × ${entry.height ?? '?'}${showMegaPixels ? ' (${entry.megaPixels} MP)' : ''}';
|
final resolutionText = '${entry.width ?? '?'} × ${entry.height ?? '?'}${showMegaPixels ? ' (${entry.megaPixels} MP)' : ''}';
|
||||||
|
|
||||||
final tags = entry.xmpSubjects..sort(compareAsciiUpperCase);
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
@ -44,37 +44,45 @@ class BasicSection extends StatelessWidget {
|
||||||
'URI': entry.uri ?? '?',
|
'URI': entry.uri ?? '?',
|
||||||
if (entry.path != null) 'Path': entry.path,
|
if (entry.path != null) 'Path': entry.path,
|
||||||
}),
|
}),
|
||||||
ValueListenableBuilder<bool>(
|
_buildChips(),
|
||||||
valueListenable: entry.isFavouriteNotifier,
|
|
||||||
builder: (context, isFavourite, child) {
|
|
||||||
final album = entry.directory;
|
|
||||||
final filters = [
|
|
||||||
if (entry.isVideo) MimeFilter(MimeTypes.ANY_VIDEO),
|
|
||||||
if (entry.isAnimated) MimeFilter(MimeFilter.animated),
|
|
||||||
if (isFavourite) FavouriteFilter(),
|
|
||||||
if (album != null) AlbumFilter(album, collection?.source?.getUniqueAlbumName(album)),
|
|
||||||
...tags.map((tag) => TagFilter(tag)),
|
|
||||||
]..sort();
|
|
||||||
if (filters.isEmpty) return const SizedBox.shrink();
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: AvesFilterChip.outlineWidth / 2) + const EdgeInsets.only(top: 8),
|
|
||||||
child: Wrap(
|
|
||||||
spacing: 8,
|
|
||||||
runSpacing: 8,
|
|
||||||
children: filters
|
|
||||||
.map((filter) => AvesFilterChip(
|
|
||||||
filter: filter,
|
|
||||||
onPressed: onFilter,
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildChips() {
|
||||||
|
final tags = entry.xmpSubjects..sort(compareAsciiUpperCase);
|
||||||
|
final album = entry.directory;
|
||||||
|
final filters = [
|
||||||
|
if (entry.isVideo) MimeFilter(MimeTypes.ANY_VIDEO),
|
||||||
|
if (entry.isAnimated) MimeFilter(MimeFilter.animated),
|
||||||
|
if (album != null) AlbumFilter(album, collection?.source?.getUniqueAlbumName(album)),
|
||||||
|
...tags.map((tag) => TagFilter(tag)),
|
||||||
|
];
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: favourites.changeNotifier,
|
||||||
|
builder: (context, child) {
|
||||||
|
final effectiveFilters = [
|
||||||
|
...filters,
|
||||||
|
if (entry.isFavourite) FavouriteFilter(),
|
||||||
|
]..sort();
|
||||||
|
if (effectiveFilters.isEmpty) return const SizedBox.shrink();
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: AvesFilterChip.outlineWidth / 2) + const EdgeInsets.only(top: 8),
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: effectiveFilters
|
||||||
|
.map((filter) => AvesFilterChip(
|
||||||
|
filter: filter,
|
||||||
|
onPressed: onFilter,
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, String> _buildVideoRows() {
|
Map<String, String> _buildVideoRows() {
|
||||||
final rotation = entry.catalogMetadata?.videoRotation;
|
final rotation = entry.catalogMetadata?.videoRotation;
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:aves/model/favourite_repo.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/widgets/common/entry_actions.dart';
|
import 'package:aves/widgets/common/entry_actions.dart';
|
||||||
import 'package:aves/widgets/common/fx/sweeper.dart';
|
import 'package:aves/widgets/common/fx/sweeper.dart';
|
||||||
|
@ -63,7 +64,7 @@ class FullscreenTopOverlay extends StatelessWidget {
|
||||||
inAppActions: inAppActions,
|
inAppActions: inAppActions,
|
||||||
externalAppActions: externalAppActions,
|
externalAppActions: externalAppActions,
|
||||||
scale: scale,
|
scale: scale,
|
||||||
isFavouriteNotifier: entry.isFavouriteNotifier,
|
entry: entry,
|
||||||
onActionSelected: onActionSelected,
|
onActionSelected: onActionSelected,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -104,7 +105,7 @@ class _TopOverlayRow extends StatelessWidget {
|
||||||
final List<EntryAction> inAppActions;
|
final List<EntryAction> inAppActions;
|
||||||
final List<EntryAction> externalAppActions;
|
final List<EntryAction> externalAppActions;
|
||||||
final Animation<double> scale;
|
final Animation<double> scale;
|
||||||
final ValueNotifier<bool> isFavouriteNotifier;
|
final ImageEntry entry;
|
||||||
final Function(EntryAction value) onActionSelected;
|
final Function(EntryAction value) onActionSelected;
|
||||||
|
|
||||||
const _TopOverlayRow({
|
const _TopOverlayRow({
|
||||||
|
@ -113,7 +114,7 @@ class _TopOverlayRow extends StatelessWidget {
|
||||||
@required this.inAppActions,
|
@required this.inAppActions,
|
||||||
@required this.externalAppActions,
|
@required this.externalAppActions,
|
||||||
@required this.scale,
|
@required this.scale,
|
||||||
@required this.isFavouriteNotifier,
|
@required this.entry,
|
||||||
@required this.onActionSelected,
|
@required this.onActionSelected,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@ -153,22 +154,9 @@ class _TopOverlayRow extends StatelessWidget {
|
||||||
final onPressed = () => onActionSelected?.call(action);
|
final onPressed = () => onActionSelected?.call(action);
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case EntryAction.toggleFavourite:
|
case EntryAction.toggleFavourite:
|
||||||
child = ValueListenableBuilder<bool>(
|
child = _FavouriteToggler(
|
||||||
valueListenable: isFavouriteNotifier,
|
entry: entry,
|
||||||
builder: (context, isFavourite, child) => Stack(
|
onPressed: onPressed,
|
||||||
alignment: Alignment.center,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(isFavourite ? AIcons.favouriteActive : AIcons.favourite),
|
|
||||||
onPressed: onPressed,
|
|
||||||
tooltip: isFavourite ? 'Remove from favourites' : 'Add to favourites',
|
|
||||||
),
|
|
||||||
Sweeper(
|
|
||||||
builder: (context) => const Icon(AIcons.favourite, color: Colors.redAccent),
|
|
||||||
toggledNotifier: isFavouriteNotifier,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case EntryAction.info:
|
case EntryAction.info:
|
||||||
|
@ -207,15 +195,10 @@ class _TopOverlayRow extends StatelessWidget {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
// in app actions
|
// in app actions
|
||||||
case EntryAction.toggleFavourite:
|
case EntryAction.toggleFavourite:
|
||||||
child = isFavouriteNotifier.value
|
child = _FavouriteToggler(
|
||||||
? const MenuRow(
|
entry: entry,
|
||||||
text: 'Remove from favourites',
|
isMenuItem: true,
|
||||||
icon: AIcons.favouriteActive,
|
);
|
||||||
)
|
|
||||||
: const MenuRow(
|
|
||||||
text: 'Add to favourites',
|
|
||||||
icon: AIcons.favourite,
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case EntryAction.info:
|
case EntryAction.info:
|
||||||
case EntryAction.share:
|
case EntryAction.share:
|
||||||
|
@ -241,3 +224,80 @@ class _TopOverlayRow extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _FavouriteToggler extends StatefulWidget {
|
||||||
|
final ImageEntry 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(null);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
favourites.changeNotifier.addListener(_onChanged);
|
||||||
|
_onChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(_FavouriteToggler oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
_onChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
favourites.changeNotifier.removeListener(_onChanged);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ValueListenableBuilder<bool>(
|
||||||
|
valueListenable: isFavouriteNotifier,
|
||||||
|
builder: (context, isFavourite, child) {
|
||||||
|
if (widget.isMenuItem) {
|
||||||
|
return isFavourite
|
||||||
|
? const MenuRow(
|
||||||
|
text: 'Remove from favourites',
|
||||||
|
icon: AIcons.favouriteActive,
|
||||||
|
)
|
||||||
|
: const MenuRow(
|
||||||
|
text: 'Add to favourites',
|
||||||
|
icon: AIcons.favourite,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(isFavourite ? AIcons.favouriteActive : AIcons.favourite),
|
||||||
|
onPressed: widget.onPressed,
|
||||||
|
tooltip: isFavourite ? 'Remove from favourites' : 'Add to favourites',
|
||||||
|
),
|
||||||
|
Sweeper(
|
||||||
|
key: ValueKey(widget.entry),
|
||||||
|
builder: (context) => const Icon(AIcons.favourite, color: Colors.redAccent),
|
||||||
|
toggledNotifier: isFavouriteNotifier,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onChanged() {
|
||||||
|
isFavouriteNotifier.value = widget.entry.isFavourite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue