various fixes for copy/move/fav

This commit is contained in:
Thibault Deckers 2020-06-10 11:53:33 +09:00
parent e26f2b4fb6
commit b92545f059
9 changed files with 169 additions and 78 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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(() {});
}
}

View file

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

View file

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