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 String op;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public ImageOpStreamHandler(Activity activity, Object arguments) {
|
||||
this.activity = activity;
|
||||
if (arguments instanceof Map) {
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/image_metadata.dart';
|
||||
import 'package:aves/model/metadata_db.dart';
|
||||
import 'package:aves/utils/change_notifier.dart';
|
||||
|
||||
final FavouriteRepo favourites = FavouriteRepo._private();
|
||||
|
||||
class FavouriteRepo {
|
||||
List<FavouriteRow> _rows = [];
|
||||
|
||||
final AChangeNotifier changeNotifier = AChangeNotifier();
|
||||
|
||||
FavouriteRepo._private();
|
||||
|
||||
Future<void> init() async {
|
||||
|
@ -23,16 +26,31 @@ class FavouriteRepo {
|
|||
final newRows = entries.map(_entryToRow);
|
||||
await metadataDb.addFavourites(newRows);
|
||||
_rows.addAll(newRows);
|
||||
changeNotifier.notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> remove(Iterable<ImageEntry> entries) async {
|
||||
final removedRows = entries.map(_entryToRow);
|
||||
await metadataDb.removeFavourites(removedRows);
|
||||
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 {
|
||||
await metadataDb.clearFavourites();
|
||||
_rows.clear();
|
||||
changeNotifier.notifyListeners();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,6 @@ class ImageEntry {
|
|||
AddressDetails _addressDetails;
|
||||
|
||||
final AChangeNotifier imageChangeNotifier = AChangeNotifier(), metadataChangeNotifier = AChangeNotifier(), addressChangeNotifier = AChangeNotifier();
|
||||
final ValueNotifier<bool> isFavouriteNotifier = ValueNotifier(false);
|
||||
|
||||
ImageEntry({
|
||||
this.uri,
|
||||
|
@ -52,7 +51,6 @@ class ImageEntry {
|
|||
this.durationMillis,
|
||||
}) {
|
||||
this.path = path;
|
||||
isFavouriteNotifier.value = isFavourite;
|
||||
}
|
||||
|
||||
ImageEntry copyWith({
|
||||
|
@ -120,7 +118,6 @@ class ImageEntry {
|
|||
imageChangeNotifier.dispose();
|
||||
metadataChangeNotifier.dispose();
|
||||
addressChangeNotifier.dispose();
|
||||
isFavouriteNotifier.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -362,14 +359,12 @@ class ImageEntry {
|
|||
void addToFavourites() {
|
||||
if (!isFavourite) {
|
||||
favourites.add([this]);
|
||||
isFavouriteNotifier.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
void removeFromFavourites() {
|
||||
if (isFavourite) {
|
||||
favourites.remove([this]);
|
||||
isFavouriteNotifier.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,8 @@ class SectionedListLayoutProvider extends StatelessWidget {
|
|||
@required this.tileExtent,
|
||||
@required this.thumbnailBuilder,
|
||||
@required this.child,
|
||||
}) : columnCount = max((scrollableWidth / tileExtent).round(), TileExtentManager.columnCountMin);
|
||||
}) : assert(scrollableWidth != 0),
|
||||
columnCount = max((scrollableWidth / tileExtent).round(), TileExtentManager.columnCountMin);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
@ -33,6 +33,9 @@ class ThumbnailCollection extends StatelessWidget {
|
|||
builder: (context, mq, child) {
|
||||
final mqSize = mq.item1;
|
||||
final mqHorizontalPadding = mq.item2;
|
||||
|
||||
if (mqSize.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
TileExtentManager.applyTileExtent(mqSize, mqHorizontalPadding, _tileExtentNotifier);
|
||||
final cacheExtent = TileExtentManager.extentMaxForSize(mqSize) * 2;
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import 'dart:async';
|
||||
|
||||
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/image_entry.dart';
|
||||
import 'package:aves/model/image_metadata.dart';
|
||||
import 'package:aves/model/metadata_db.dart';
|
||||
import 'package:aves/services/android_app_service.dart';
|
||||
import 'package:aves/services/image_file_service.dart';
|
||||
|
@ -103,7 +103,7 @@ class SelectionActionDelegate with PermissionAwareMixin {
|
|||
context: context,
|
||||
selection: selection,
|
||||
opStream: ImageFileService.move(selection, copy: copy, destinationAlbum: destinationAlbum),
|
||||
onDone: (Set<MoveOpEvent> processed) {
|
||||
onDone: (Set<MoveOpEvent> processed) async {
|
||||
debugPrint('$runtimeType _moveSelection onDone');
|
||||
final movedOps = processed.where((e) => e.success);
|
||||
final movedCount = movedOps.length;
|
||||
|
@ -130,10 +130,10 @@ class SelectionActionDelegate with PermissionAwareMixin {
|
|||
contentId: newFields['contentId'] as int,
|
||||
));
|
||||
});
|
||||
metadataDb.saveMetadata(movedEntries.map((entry) => entry.catalogMetadata));
|
||||
metadataDb.saveAddresses(movedEntries.map((entry) => entry.addressDetails));
|
||||
await metadataDb.saveMetadata(movedEntries.map((entry) => entry.catalogMetadata));
|
||||
await metadataDb.saveAddresses(movedEntries.map((entry) => entry.addressDetails));
|
||||
} else {
|
||||
movedOps.forEach((movedOp) {
|
||||
await Future.forEach(movedOps, (movedOp) async {
|
||||
final sourceUri = movedOp.uri;
|
||||
final newFields = movedOp.newFields;
|
||||
final entry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null);
|
||||
|
@ -146,9 +146,9 @@ class SelectionActionDelegate with PermissionAwareMixin {
|
|||
entry.contentId = newContentId;
|
||||
movedEntries.add(entry);
|
||||
|
||||
metadataDb.updateMetadataId(oldContentId, entry.catalogMetadata);
|
||||
metadataDb.updateAddressId(oldContentId, entry.addressDetails);
|
||||
metadataDb.updateFavouriteId(oldContentId, FavouriteRow(contentId: entry.contentId, path: entry.path));
|
||||
await metadataDb.updateMetadataId(oldContentId, entry.catalogMetadata);
|
||||
await metadataDb.updateAddressId(oldContentId, entry.addressDetails);
|
||||
await favourites.move(oldContentId, entry);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -234,17 +234,17 @@ class SelectionActionDelegate with PermissionAwareMixin {
|
|||
// do not handle completion inside `StreamBuilder`
|
||||
// as it could be called multiple times
|
||||
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(
|
||||
builder: (context) {
|
||||
return StreamBuilder<T>(
|
||||
stream: opStream,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
processed.add(snapshot.data);
|
||||
}
|
||||
|
||||
Widget child = const SizedBox.shrink();
|
||||
if (!snapshot.hasError && snapshot.connectionState == ConnectionState.active) {
|
||||
final percent = processed.length.toDouble() / selection.length;
|
||||
|
|
|
@ -68,6 +68,7 @@ class _SweeperState extends State<Sweeper> with SingleTickerProviderStateMixin {
|
|||
@override
|
||||
void dispose() {
|
||||
_angleAnimationController.removeStatusListener(_onAnimationStatusChange);
|
||||
_angleAnimationController.dispose();
|
||||
_unregisterWidget(widget);
|
||||
super.dispose();
|
||||
}
|
||||
|
@ -114,10 +115,14 @@ class _SweeperState extends State<Sweeper> with SingleTickerProviderStateMixin {
|
|||
setState(() {});
|
||||
await Future.delayed(Duration(milliseconds: (opacityAnimationDurationMillis * timeDilation).toInt()));
|
||||
_isAppearing = false;
|
||||
_angleAnimationController.reset();
|
||||
_angleAnimationController.forward();
|
||||
if (mounted) {
|
||||
_angleAnimationController.reset();
|
||||
_angleAnimationController.forward();
|
||||
}
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
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/favourite.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 resolutionText = '${entry.width ?? '?'} × ${entry.height ?? '?'}${showMegaPixels ? ' (${entry.megaPixels} MP)' : ''}';
|
||||
|
||||
final tags = entry.xmpSubjects..sort(compareAsciiUpperCase);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
@ -44,37 +44,45 @@ class BasicSection extends StatelessWidget {
|
|||
'URI': entry.uri ?? '?',
|
||||
if (entry.path != null) 'Path': entry.path,
|
||||
}),
|
||||
ValueListenableBuilder<bool>(
|
||||
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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_buildChips(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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() {
|
||||
final rotation = entry.catalogMetadata?.videoRotation;
|
||||
return {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/model/favourite_repo.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/widgets/common/entry_actions.dart';
|
||||
import 'package:aves/widgets/common/fx/sweeper.dart';
|
||||
|
@ -63,7 +64,7 @@ class FullscreenTopOverlay extends StatelessWidget {
|
|||
inAppActions: inAppActions,
|
||||
externalAppActions: externalAppActions,
|
||||
scale: scale,
|
||||
isFavouriteNotifier: entry.isFavouriteNotifier,
|
||||
entry: entry,
|
||||
onActionSelected: onActionSelected,
|
||||
);
|
||||
},
|
||||
|
@ -104,7 +105,7 @@ class _TopOverlayRow extends StatelessWidget {
|
|||
final List<EntryAction> inAppActions;
|
||||
final List<EntryAction> externalAppActions;
|
||||
final Animation<double> scale;
|
||||
final ValueNotifier<bool> isFavouriteNotifier;
|
||||
final ImageEntry entry;
|
||||
final Function(EntryAction value) onActionSelected;
|
||||
|
||||
const _TopOverlayRow({
|
||||
|
@ -113,7 +114,7 @@ class _TopOverlayRow extends StatelessWidget {
|
|||
@required this.inAppActions,
|
||||
@required this.externalAppActions,
|
||||
@required this.scale,
|
||||
@required this.isFavouriteNotifier,
|
||||
@required this.entry,
|
||||
@required this.onActionSelected,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -153,22 +154,9 @@ class _TopOverlayRow extends StatelessWidget {
|
|||
final onPressed = () => onActionSelected?.call(action);
|
||||
switch (action) {
|
||||
case EntryAction.toggleFavourite:
|
||||
child = ValueListenableBuilder<bool>(
|
||||
valueListenable: isFavouriteNotifier,
|
||||
builder: (context, isFavourite, child) => Stack(
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
child = _FavouriteToggler(
|
||||
entry: entry,
|
||||
onPressed: onPressed,
|
||||
);
|
||||
break;
|
||||
case EntryAction.info:
|
||||
|
@ -207,15 +195,10 @@ class _TopOverlayRow extends StatelessWidget {
|
|||
switch (action) {
|
||||
// in app actions
|
||||
case EntryAction.toggleFavourite:
|
||||
child = isFavouriteNotifier.value
|
||||
? const MenuRow(
|
||||
text: 'Remove from favourites',
|
||||
icon: AIcons.favouriteActive,
|
||||
)
|
||||
: const MenuRow(
|
||||
text: 'Add to favourites',
|
||||
icon: AIcons.favourite,
|
||||
);
|
||||
child = _FavouriteToggler(
|
||||
entry: entry,
|
||||
isMenuItem: true,
|
||||
);
|
||||
break;
|
||||
case EntryAction.info:
|
||||
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