album: delete
This commit is contained in:
parent
2809f976e4
commit
f32c3f1154
7 changed files with 129 additions and 70 deletions
|
@ -1,7 +1,11 @@
|
||||||
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:aves/services/image_file_service.dart';
|
||||||
import 'package:aves/utils/durations.dart';
|
import 'package:aves/utils/durations.dart';
|
||||||
import 'package:flushbar/flushbar.dart';
|
import 'package:flushbar/flushbar.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:percent_indicator/circular_percent_indicator.dart';
|
||||||
|
|
||||||
mixin FeedbackMixin {
|
mixin FeedbackMixin {
|
||||||
Flushbar _flushbar;
|
Flushbar _flushbar;
|
||||||
|
@ -20,4 +24,65 @@ mixin FeedbackMixin {
|
||||||
animationDuration: Durations.opToastAnimation,
|
animationDuration: Durations.opToastAnimation,
|
||||||
)..show(context);
|
)..show(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// report overlay for multiple operations
|
||||||
|
|
||||||
|
OverlayEntry _opReportOverlayEntry;
|
||||||
|
|
||||||
|
void showOpReport<T extends ImageOpEvent>({
|
||||||
|
@required BuildContext context,
|
||||||
|
@required List<ImageEntry> selection,
|
||||||
|
@required Stream<T> opStream,
|
||||||
|
@required void Function(Set<T> processed) onDone,
|
||||||
|
}) {
|
||||||
|
final processed = <T>{};
|
||||||
|
|
||||||
|
// do not handle completion inside `StreamBuilder`
|
||||||
|
// as it could be called multiple times
|
||||||
|
Future<void> onComplete() => _hideOpReportOverlay().then((_) => onDone(processed));
|
||||||
|
opStream.listen(
|
||||||
|
processed.add,
|
||||||
|
onError: (error) {
|
||||||
|
debugPrint('_showOpReport error=$error');
|
||||||
|
onComplete();
|
||||||
|
},
|
||||||
|
onDone: onComplete,
|
||||||
|
);
|
||||||
|
|
||||||
|
_opReportOverlayEntry = OverlayEntry(
|
||||||
|
builder: (context) {
|
||||||
|
return AbsorbPointer(
|
||||||
|
child: StreamBuilder<T>(
|
||||||
|
stream: opStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
Widget child = SizedBox.shrink();
|
||||||
|
if (!snapshot.hasError && snapshot.connectionState == ConnectionState.active) {
|
||||||
|
final percent = processed.length.toDouble() / selection.length;
|
||||||
|
child = CircularPercentIndicator(
|
||||||
|
percent: percent,
|
||||||
|
lineWidth: 16,
|
||||||
|
radius: 160,
|
||||||
|
backgroundColor: Colors.white24,
|
||||||
|
progressColor: Theme.of(context).accentColor,
|
||||||
|
animation: true,
|
||||||
|
center: Text(NumberFormat.percentPattern().format(percent)),
|
||||||
|
animateFromLastPercent: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return AnimatedSwitcher(
|
||||||
|
duration: Durations.collectionOpOverlayAnimation,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Overlay.of(context).insert(_opReportOverlayEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _hideOpReportOverlay() async {
|
||||||
|
await Future.delayed(Durations.collectionOpOverlayAnimation * timeDilation);
|
||||||
|
_opReportOverlayEntry.remove();
|
||||||
|
_opReportOverlayEntry = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,7 +74,6 @@ class _RenameEntryDialogState extends State<RenameEntryDialog> {
|
||||||
final newName = _nameController.text ?? '';
|
final newName = _nameController.text ?? '';
|
||||||
final path = _buildEntryPath(newName);
|
final path = _buildEntryPath(newName);
|
||||||
final exists = newName.isNotEmpty && await FileSystemEntity.type(path) != FileSystemEntityType.notFound;
|
final exists = newName.isNotEmpty && await FileSystemEntity.type(path) != FileSystemEntityType.notFound;
|
||||||
debugPrint('TLAD path=$path exists=$exists');
|
|
||||||
_isValidNotifier.value = newName.isNotEmpty && !exists;
|
_isValidNotifier.value = newName.isNotEmpty && !exists;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.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';
|
||||||
import 'package:aves/utils/durations.dart';
|
|
||||||
import 'package:aves/widgets/collection/collection_actions.dart';
|
import 'package:aves/widgets/collection/collection_actions.dart';
|
||||||
import 'package:aves/widgets/collection/empty.dart';
|
import 'package:aves/widgets/collection/empty.dart';
|
||||||
import 'package:aves/widgets/common/action_delegates/create_album_dialog.dart';
|
import 'package:aves/widgets/common/action_delegates/create_album_dialog.dart';
|
||||||
|
@ -20,10 +19,8 @@ import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||||
import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart';
|
import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:percent_indicator/circular_percent_indicator.dart';
|
|
||||||
|
|
||||||
class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
||||||
final CollectionLens collection;
|
final CollectionLens collection;
|
||||||
|
@ -61,7 +58,7 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _moveSelection(BuildContext context, {@required bool copy}) async {
|
Future<void> _moveSelection(BuildContext context, {@required bool copy}) async {
|
||||||
final source = collection.source;
|
final source = collection.source;
|
||||||
final destinationAlbum = await Navigator.push(
|
final destinationAlbum = await Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
@ -106,7 +103,7 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
||||||
final selection = collection.selection.toList();
|
final selection = collection.selection.toList();
|
||||||
if (!await checkStoragePermission(context, selection)) return;
|
if (!await checkStoragePermission(context, selection)) return;
|
||||||
|
|
||||||
_showOpReport<MoveOpEvent>(
|
showOpReport<MoveOpEvent>(
|
||||||
context: context,
|
context: context,
|
||||||
selection: selection,
|
selection: selection,
|
||||||
opStream: ImageFileService.move(selection, copy: copy, destinationAlbum: destinationAlbum),
|
opStream: ImageFileService.move(selection, copy: copy, destinationAlbum: destinationAlbum),
|
||||||
|
@ -166,7 +163,7 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _refreshSelectionMetadata() async {
|
Future<void> _refreshSelectionMetadata() async {
|
||||||
collection.selection.forEach((entry) => entry.clearMetadata());
|
collection.selection.forEach((entry) => entry.clearMetadata());
|
||||||
final source = collection.source;
|
final source = collection.source;
|
||||||
source.stateNotifier.value = SourceState.cataloguing;
|
source.stateNotifier.value = SourceState.cataloguing;
|
||||||
|
@ -176,7 +173,7 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
||||||
source.stateNotifier.value = SourceState.ready;
|
source.stateNotifier.value = SourceState.ready;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showDeleteDialog(BuildContext context) async {
|
Future<void> _showDeleteDialog(BuildContext context) async {
|
||||||
final selection = collection.selection.toList();
|
final selection = collection.selection.toList();
|
||||||
final count = selection.length;
|
final count = selection.length;
|
||||||
|
|
||||||
|
@ -202,7 +199,7 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
||||||
|
|
||||||
if (!await checkStoragePermission(context, selection)) return;
|
if (!await checkStoragePermission(context, selection)) return;
|
||||||
|
|
||||||
_showOpReport<ImageOpEvent>(
|
showOpReport<ImageOpEvent>(
|
||||||
context: context,
|
context: context,
|
||||||
selection: selection,
|
selection: selection,
|
||||||
opStream: ImageFileService.delete(selection),
|
opStream: ImageFileService.delete(selection),
|
||||||
|
@ -222,65 +219,4 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// selection action report overlay
|
|
||||||
|
|
||||||
OverlayEntry _opReportOverlayEntry;
|
|
||||||
|
|
||||||
void _showOpReport<T extends ImageOpEvent>({
|
|
||||||
@required BuildContext context,
|
|
||||||
@required List<ImageEntry> selection,
|
|
||||||
@required Stream<T> opStream,
|
|
||||||
@required void Function(Set<T> processed) onDone,
|
|
||||||
}) {
|
|
||||||
final processed = <T>{};
|
|
||||||
|
|
||||||
// do not handle completion inside `StreamBuilder`
|
|
||||||
// as it could be called multiple times
|
|
||||||
Future<void> onComplete() => _hideOpReportOverlay().then((_) => onDone(processed));
|
|
||||||
opStream.listen(
|
|
||||||
processed.add,
|
|
||||||
onError: (error) {
|
|
||||||
debugPrint('_showOpReport error=$error');
|
|
||||||
onComplete();
|
|
||||||
},
|
|
||||||
onDone: onComplete,
|
|
||||||
);
|
|
||||||
|
|
||||||
_opReportOverlayEntry = OverlayEntry(
|
|
||||||
builder: (context) {
|
|
||||||
return AbsorbPointer(
|
|
||||||
child: StreamBuilder<T>(
|
|
||||||
stream: opStream,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
Widget child = SizedBox.shrink();
|
|
||||||
if (!snapshot.hasError && snapshot.connectionState == ConnectionState.active) {
|
|
||||||
final percent = processed.length.toDouble() / selection.length;
|
|
||||||
child = CircularPercentIndicator(
|
|
||||||
percent: percent,
|
|
||||||
lineWidth: 16,
|
|
||||||
radius: 160,
|
|
||||||
backgroundColor: Colors.white24,
|
|
||||||
progressColor: Theme.of(context).accentColor,
|
|
||||||
animation: true,
|
|
||||||
center: Text(NumberFormat.percentPattern().format(percent)),
|
|
||||||
animateFromLastPercent: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return AnimatedSwitcher(
|
|
||||||
duration: Durations.collectionOpOverlayAnimation,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
Overlay.of(context).insert(_opReportOverlayEntry);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _hideOpReportOverlay() async {
|
|
||||||
await Future.delayed(Durations.collectionOpOverlayAnimation * timeDilation);
|
|
||||||
_opReportOverlayEntry.remove();
|
|
||||||
_opReportOverlayEntry = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -299,6 +299,7 @@ class DebugPageState extends State<DebugPage> {
|
||||||
'collectionSortFactor': '${settings.collectionSortFactor}',
|
'collectionSortFactor': '${settings.collectionSortFactor}',
|
||||||
'collectionTileExtent': '${settings.collectionTileExtent}',
|
'collectionTileExtent': '${settings.collectionTileExtent}',
|
||||||
'infoMapZoom': '${settings.infoMapZoom}',
|
'infoMapZoom': '${settings.infoMapZoom}',
|
||||||
|
'pinnedFilters': '${settings.pinnedFilters}',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
@ -41,6 +41,7 @@ class AlbumListPage extends StatelessWidget {
|
||||||
chipActionsBuilder: (filter) => [
|
chipActionsBuilder: (filter) => [
|
||||||
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
|
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
|
||||||
ChipAction.rename,
|
ChipAction.rename,
|
||||||
|
ChipAction.delete,
|
||||||
],
|
],
|
||||||
filterEntries: getAlbumEntries(source),
|
filterEntries: getAlbumEntries(source),
|
||||||
filterBuilder: (album) => AlbumFilter(album, source.getUniqueAlbumName(album)),
|
filterBuilder: (album) => AlbumFilter(album, source.getUniqueAlbumName(album)),
|
||||||
|
|
|
@ -8,9 +8,11 @@ import 'package:aves/utils/durations.dart';
|
||||||
import 'package:aves/widgets/common/action_delegates/feedback.dart';
|
import 'package:aves/widgets/common/action_delegates/feedback.dart';
|
||||||
import 'package:aves/widgets/common/action_delegates/permission_aware.dart';
|
import 'package:aves/widgets/common/action_delegates/permission_aware.dart';
|
||||||
import 'package:aves/widgets/common/action_delegates/rename_album_dialog.dart';
|
import 'package:aves/widgets/common/action_delegates/rename_album_dialog.dart';
|
||||||
|
import 'package:aves/widgets/common/aves_dialog.dart';
|
||||||
import 'package:aves/widgets/filter_grids/common/chip_actions.dart';
|
import 'package:aves/widgets/filter_grids/common/chip_actions.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:pedantic/pedantic.dart';
|
import 'package:pedantic/pedantic.dart';
|
||||||
|
|
||||||
|
@ -43,6 +45,9 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per
|
||||||
Future<void> onActionSelected(BuildContext context, CollectionFilter filter, ChipAction action) async {
|
Future<void> onActionSelected(BuildContext context, CollectionFilter filter, ChipAction action) async {
|
||||||
await super.onActionSelected(context, filter, action);
|
await super.onActionSelected(context, filter, action);
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
case ChipAction.delete:
|
||||||
|
unawaited(_showDeleteDialog(context, filter as AlbumFilter));
|
||||||
|
break;
|
||||||
case ChipAction.rename:
|
case ChipAction.rename:
|
||||||
unawaited(_showRenameDialog(context, filter as AlbumFilter));
|
unawaited(_showRenameDialog(context, filter as AlbumFilter));
|
||||||
break;
|
break;
|
||||||
|
@ -51,6 +56,53 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _showDeleteDialog(BuildContext context, AlbumFilter filter) async {
|
||||||
|
final selection = source.rawEntries.where(filter.filter).toList();
|
||||||
|
final count = selection.length;
|
||||||
|
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AvesDialog(
|
||||||
|
content: Text('Are you sure you want to delete this album and its ${Intl.plural(count, one: 'item', other: '$count items')}?'),
|
||||||
|
actions: [
|
||||||
|
FlatButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text('Cancel'.toUpperCase()),
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
child: Text('Delete'.toUpperCase()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (confirmed == null || !confirmed) return;
|
||||||
|
|
||||||
|
if (!await checkStoragePermission(context, selection)) return;
|
||||||
|
|
||||||
|
showOpReport<ImageOpEvent>(
|
||||||
|
context: context,
|
||||||
|
selection: selection,
|
||||||
|
opStream: ImageFileService.delete(selection),
|
||||||
|
onDone: (processed) {
|
||||||
|
final deletedUris = processed.where((e) => e.success).map((e) => e.uri);
|
||||||
|
final deletedCount = deletedUris.length;
|
||||||
|
final selectionCount = selection.length;
|
||||||
|
if (deletedCount < selectionCount) {
|
||||||
|
final count = selectionCount - deletedCount;
|
||||||
|
showFeedback(context, 'Failed to delete ${Intl.plural(count, one: '$count item', other: '$count items')}');
|
||||||
|
} else {
|
||||||
|
settings.pinnedFilters = settings.pinnedFilters..remove(filter);
|
||||||
|
}
|
||||||
|
if (deletedCount > 0) {
|
||||||
|
source.removeEntries(selection.where((e) => deletedUris.contains(e.uri)));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _showRenameDialog(BuildContext context, AlbumFilter filter) async {
|
Future<void> _showRenameDialog(BuildContext context, AlbumFilter filter) async {
|
||||||
final album = filter.album;
|
final album = filter.album;
|
||||||
final newName = await showDialog<String>(
|
final newName = await showDialog<String>(
|
||||||
|
|
|
@ -6,6 +6,7 @@ enum ChipSetAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ChipAction {
|
enum ChipAction {
|
||||||
|
delete,
|
||||||
pin,
|
pin,
|
||||||
unpin,
|
unpin,
|
||||||
rename,
|
rename,
|
||||||
|
@ -14,6 +15,8 @@ enum ChipAction {
|
||||||
extension ExtraChipAction on ChipAction {
|
extension ExtraChipAction on ChipAction {
|
||||||
String getText() {
|
String getText() {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
|
case ChipAction.delete:
|
||||||
|
return 'Delete';
|
||||||
case ChipAction.pin:
|
case ChipAction.pin:
|
||||||
return 'Pin to top';
|
return 'Pin to top';
|
||||||
case ChipAction.unpin:
|
case ChipAction.unpin:
|
||||||
|
@ -26,6 +29,8 @@ extension ExtraChipAction on ChipAction {
|
||||||
|
|
||||||
IconData getIcon() {
|
IconData getIcon() {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
|
case ChipAction.delete:
|
||||||
|
return AIcons.delete;
|
||||||
case ChipAction.pin:
|
case ChipAction.pin:
|
||||||
case ChipAction.unpin:
|
case ChipAction.unpin:
|
||||||
return AIcons.pin;
|
return AIcons.pin;
|
||||||
|
|
Loading…
Reference in a new issue