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:flushbar/flushbar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:percent_indicator/circular_percent_indicator.dart';
|
||||
|
||||
mixin FeedbackMixin {
|
||||
Flushbar _flushbar;
|
||||
|
@ -20,4 +24,65 @@ mixin FeedbackMixin {
|
|||
animationDuration: Durations.opToastAnimation,
|
||||
)..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 path = _buildEntryPath(newName);
|
||||
final exists = newName.isNotEmpty && await FileSystemEntity.type(path) != FileSystemEntityType.notFound;
|
||||
debugPrint('TLAD path=$path exists=$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/services/android_app_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/empty.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:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:percent_indicator/circular_percent_indicator.dart';
|
||||
|
||||
class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
||||
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 destinationAlbum = await Navigator.push(
|
||||
context,
|
||||
|
@ -106,7 +103,7 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
|||
final selection = collection.selection.toList();
|
||||
if (!await checkStoragePermission(context, selection)) return;
|
||||
|
||||
_showOpReport<MoveOpEvent>(
|
||||
showOpReport<MoveOpEvent>(
|
||||
context: context,
|
||||
selection: selection,
|
||||
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());
|
||||
final source = collection.source;
|
||||
source.stateNotifier.value = SourceState.cataloguing;
|
||||
|
@ -176,7 +173,7 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
|||
source.stateNotifier.value = SourceState.ready;
|
||||
}
|
||||
|
||||
void _showDeleteDialog(BuildContext context) async {
|
||||
Future<void> _showDeleteDialog(BuildContext context) async {
|
||||
final selection = collection.selection.toList();
|
||||
final count = selection.length;
|
||||
|
||||
|
@ -202,7 +199,7 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
|||
|
||||
if (!await checkStoragePermission(context, selection)) return;
|
||||
|
||||
_showOpReport<ImageOpEvent>(
|
||||
showOpReport<ImageOpEvent>(
|
||||
context: context,
|
||||
selection: 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}',
|
||||
'collectionTileExtent': '${settings.collectionTileExtent}',
|
||||
'infoMapZoom': '${settings.infoMapZoom}',
|
||||
'pinnedFilters': '${settings.pinnedFilters}',
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
|
|
@ -41,6 +41,7 @@ class AlbumListPage extends StatelessWidget {
|
|||
chipActionsBuilder: (filter) => [
|
||||
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
|
||||
ChipAction.rename,
|
||||
ChipAction.delete,
|
||||
],
|
||||
filterEntries: getAlbumEntries(source),
|
||||
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/permission_aware.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:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
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 {
|
||||
await super.onActionSelected(context, filter, action);
|
||||
switch (action) {
|
||||
case ChipAction.delete:
|
||||
unawaited(_showDeleteDialog(context, filter as AlbumFilter));
|
||||
break;
|
||||
case ChipAction.rename:
|
||||
unawaited(_showRenameDialog(context, filter as AlbumFilter));
|
||||
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 {
|
||||
final album = filter.album;
|
||||
final newName = await showDialog<String>(
|
||||
|
|
|
@ -6,6 +6,7 @@ enum ChipSetAction {
|
|||
}
|
||||
|
||||
enum ChipAction {
|
||||
delete,
|
||||
pin,
|
||||
unpin,
|
||||
rename,
|
||||
|
@ -14,6 +15,8 @@ enum ChipAction {
|
|||
extension ExtraChipAction on ChipAction {
|
||||
String getText() {
|
||||
switch (this) {
|
||||
case ChipAction.delete:
|
||||
return 'Delete';
|
||||
case ChipAction.pin:
|
||||
return 'Pin to top';
|
||||
case ChipAction.unpin:
|
||||
|
@ -26,6 +29,8 @@ extension ExtraChipAction on ChipAction {
|
|||
|
||||
IconData getIcon() {
|
||||
switch (this) {
|
||||
case ChipAction.delete:
|
||||
return AIcons.delete;
|
||||
case ChipAction.pin:
|
||||
case ChipAction.unpin:
|
||||
return AIcons.pin;
|
||||
|
|
Loading…
Reference in a new issue