delete empty directories, if possible, after move/rename file ops
This commit is contained in:
parent
66c1566eeb
commit
0464bd8678
10 changed files with 103 additions and 45 deletions
|
@ -6,6 +6,7 @@ import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.os.storage.StorageManager
|
import android.os.storage.StorageManager
|
||||||
|
import android.util.Log
|
||||||
import androidx.core.os.EnvironmentCompat
|
import androidx.core.os.EnvironmentCompat
|
||||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||||
import deckers.thibault.aves.utils.PermissionManager
|
import deckers.thibault.aves.utils.PermissionManager
|
||||||
|
@ -29,6 +30,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
|
||||||
"getInaccessibleDirectories" -> safe(call, result, ::getInaccessibleDirectories)
|
"getInaccessibleDirectories" -> safe(call, result, ::getInaccessibleDirectories)
|
||||||
"getRestrictedDirectories" -> safe(call, result, ::getRestrictedDirectories)
|
"getRestrictedDirectories" -> safe(call, result, ::getRestrictedDirectories)
|
||||||
"revokeDirectoryAccess" -> safe(call, result, ::revokeDirectoryAccess)
|
"revokeDirectoryAccess" -> safe(call, result, ::revokeDirectoryAccess)
|
||||||
|
"deleteEmptyDirectories" -> safe(call, result, ::deleteEmptyDirectories)
|
||||||
"scanFile" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::scanFile) }
|
"scanFile" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::scanFile) }
|
||||||
else -> result.notImplemented()
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
|
@ -136,6 +138,28 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
|
||||||
result.success(success)
|
result.success(success)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun deleteEmptyDirectories(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val dirPaths = call.argument<List<String>>("dirPaths")
|
||||||
|
if (dirPaths == null) {
|
||||||
|
result.error("deleteEmptyDirectories-args", "failed because of missing arguments", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var deleted = 0
|
||||||
|
dirPaths.forEach {
|
||||||
|
try {
|
||||||
|
val dir = File(it)
|
||||||
|
if (dir.isDirectory && dir.listFiles()?.isEmpty() == true && dir.delete()) {
|
||||||
|
Log.d("TLAD", "deleted empty directory=$dir")
|
||||||
|
deleted++
|
||||||
|
}
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.success(deleted)
|
||||||
|
}
|
||||||
|
|
||||||
private fun scanFile(call: MethodCall, result: MethodChannel.Result) {
|
private fun scanFile(call: MethodCall, result: MethodChannel.Result) {
|
||||||
val path = call.argument<String>("path")
|
val path = call.argument<String>("path")
|
||||||
val mimeType = call.argument<String>("mimeType")
|
val mimeType = call.argument<String>("mimeType")
|
||||||
|
|
|
@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:streams_channel/streams_channel.dart';
|
import 'package:streams_channel/streams_channel.dart';
|
||||||
|
|
||||||
class AndroidFileService {
|
class StorageService {
|
||||||
static const platform = MethodChannel('deckers.thibault/aves/storage');
|
static const platform = MethodChannel('deckers.thibault/aves/storage');
|
||||||
static final StreamsChannel storageAccessChannel = StreamsChannel('deckers.thibault/aves/storageaccessstream');
|
static final StreamsChannel storageAccessChannel = StreamsChannel('deckers.thibault/aves/storageaccessstream');
|
||||||
|
|
||||||
|
@ -95,6 +95,18 @@ class AndroidFileService {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns number of deleted directories
|
||||||
|
static Future<int> deleteEmptyDirectories(Iterable<String> dirPaths) async {
|
||||||
|
try {
|
||||||
|
return await platform.invokeMethod('deleteEmptyDirectories', <String, dynamic>{
|
||||||
|
'dirPaths': dirPaths.toList(),
|
||||||
|
});
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
debugPrint('deleteEmptyDirectories failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// returns media URI
|
// returns media URI
|
||||||
static Future<Uri> scanFile(String path, String mimeType) async {
|
static Future<Uri> scanFile(String path, String mimeType) async {
|
||||||
debugPrint('scanFile with path=$path, mimeType=$mimeType');
|
debugPrint('scanFile with path=$path, mimeType=$mimeType');
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:aves/services/android_app_service.dart';
|
import 'package:aves/services/android_app_service.dart';
|
||||||
import 'package:aves/services/android_file_service.dart';
|
import 'package:aves/services/storage_service.dart';
|
||||||
import 'package:aves/utils/change_notifier.dart';
|
import 'package:aves/utils/change_notifier.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
@ -21,7 +21,7 @@ class AndroidFileUtils {
|
||||||
AndroidFileUtils._private();
|
AndroidFileUtils._private();
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
storageVolumes = await AndroidFileService.getStorageVolumes();
|
storageVolumes = await StorageService.getStorageVolumes();
|
||||||
// path_provider getExternalStorageDirectory() gives '/storage/emulated/0/Android/data/deckers.thibault.aves/files'
|
// path_provider getExternalStorageDirectory() gives '/storage/emulated/0/Android/data/deckers.thibault.aves/files'
|
||||||
primaryStorage = storageVolumes.firstWhere((volume) => volume.isPrimary).path;
|
primaryStorage = storageVolumes.firstWhere((volume) => volume.isPrimary).path;
|
||||||
dcimPath = join(primaryStorage, 'DCIM');
|
dcimPath = join(primaryStorage, 'DCIM');
|
||||||
|
|
|
@ -7,9 +7,9 @@ import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
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/android_file_service.dart';
|
|
||||||
import 'package:aves/services/image_op_events.dart';
|
import 'package:aves/services/image_op_events.dart';
|
||||||
import 'package:aves/services/services.dart';
|
import 'package:aves/services/services.dart';
|
||||||
|
import 'package:aves/services/storage_service.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
||||||
import 'package:aves/widgets/common/action_mixins/permission_aware.dart';
|
import 'package:aves/widgets/common/action_mixins/permission_aware.dart';
|
||||||
|
@ -69,7 +69,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
if (moveType == MoveType.move) {
|
if (moveType == MoveType.move) {
|
||||||
// check whether moving is possible given OS restrictions,
|
// check whether moving is possible given OS restrictions,
|
||||||
// before asking to pick a destination album
|
// before asking to pick a destination album
|
||||||
final restrictedDirs = await AndroidFileService.getRestrictedDirectories();
|
final restrictedDirs = await StorageService.getRestrictedDirectories();
|
||||||
for (final selectionDir in selectionDirs) {
|
for (final selectionDir in selectionDirs) {
|
||||||
final dir = VolumeRelativeDirectory.fromPath(selectionDir);
|
final dir = VolumeRelativeDirectory.fromPath(selectionDir);
|
||||||
if (restrictedDirs.contains(dir)) {
|
if (restrictedDirs.contains(dir)) {
|
||||||
|
@ -124,19 +124,25 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
final count = movedCount;
|
final count = movedCount;
|
||||||
showFeedback(context, copy ? l10n.collectionCopySuccessFeedback(count) : l10n.collectionMoveSuccessFeedback(count));
|
showFeedback(context, copy ? l10n.collectionCopySuccessFeedback(count) : l10n.collectionMoveSuccessFeedback(count));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
if (moveType == MoveType.move) {
|
||||||
|
await StorageService.deleteEmptyDirectories(selectionDirs);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showDeleteDialog(BuildContext context) async {
|
Future<void> _showDeleteDialog(BuildContext context) async {
|
||||||
final count = selection.length;
|
final selectionDirs = selection.where((e) => e.path != null).map((e) => e.directory).toSet();
|
||||||
|
final todoCount = selection.length;
|
||||||
|
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return AvesDialog(
|
return AvesDialog(
|
||||||
context: context,
|
context: context,
|
||||||
content: Text(context.l10n.deleteEntriesConfirmationDialogMessage(count)),
|
content: Text(context.l10n.deleteEntriesConfirmationDialogMessage(todoCount)),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
|
@ -152,14 +158,13 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
);
|
);
|
||||||
if (confirmed == null || !confirmed) return;
|
if (confirmed == null || !confirmed) return;
|
||||||
|
|
||||||
if (!await checkStoragePermission(context, selection)) return;
|
if (!await checkStoragePermissionForAlbums(context, selectionDirs)) return;
|
||||||
|
|
||||||
final selectionCount = selection.length;
|
|
||||||
source.pauseMonitoring();
|
source.pauseMonitoring();
|
||||||
showOpReport<ImageOpEvent>(
|
showOpReport<ImageOpEvent>(
|
||||||
context: context,
|
context: context,
|
||||||
opStream: imageFileService.delete(selection),
|
opStream: imageFileService.delete(selection),
|
||||||
itemCount: selectionCount,
|
itemCount: todoCount,
|
||||||
onDone: (processed) async {
|
onDone: (processed) async {
|
||||||
final deletedUris = processed.where((event) => event.success).map((event) => event.uri).toSet();
|
final deletedUris = processed.where((event) => event.success).map((event) => event.uri).toSet();
|
||||||
await source.removeEntries(deletedUris);
|
await source.removeEntries(deletedUris);
|
||||||
|
@ -167,10 +172,13 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
source.resumeMonitoring();
|
source.resumeMonitoring();
|
||||||
|
|
||||||
final deletedCount = deletedUris.length;
|
final deletedCount = deletedUris.length;
|
||||||
if (deletedCount < selectionCount) {
|
if (deletedCount < todoCount) {
|
||||||
final count = selectionCount - deletedCount;
|
final count = todoCount - deletedCount;
|
||||||
showFeedback(context, context.l10n.collectionDeleteFailureFeedback(count));
|
showFeedback(context, context.l10n.collectionDeleteFailureFeedback(count));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
await StorageService.deleteEmptyDirectories(selectionDirs);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/services/android_file_service.dart';
|
import 'package:aves/services/storage_service.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||||
|
@ -11,9 +11,9 @@ mixin PermissionAwareMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> checkStoragePermissionForAlbums(BuildContext context, Set<String> albumPaths) async {
|
Future<bool> checkStoragePermissionForAlbums(BuildContext context, Set<String> albumPaths) async {
|
||||||
final restrictedDirs = await AndroidFileService.getRestrictedDirectories();
|
final restrictedDirs = await StorageService.getRestrictedDirectories();
|
||||||
while (true) {
|
while (true) {
|
||||||
final dirs = await AndroidFileService.getInaccessibleDirectories(albumPaths);
|
final dirs = await StorageService.getInaccessibleDirectories(albumPaths);
|
||||||
if (dirs == null) return false;
|
if (dirs == null) return false;
|
||||||
if (dirs.isEmpty) return true;
|
if (dirs.isEmpty) return true;
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ mixin PermissionAwareMixin {
|
||||||
// abort if the user cancels in Flutter
|
// abort if the user cancels in Flutter
|
||||||
if (confirmed == null || !confirmed) return false;
|
if (confirmed == null || !confirmed) return false;
|
||||||
|
|
||||||
final granted = await AndroidFileService.requestVolumeAccess(dir.volumePath);
|
final granted = await StorageService.requestVolumeAccess(dir.volumePath);
|
||||||
if (!granted) {
|
if (!granted) {
|
||||||
// abort if the user denies access from the native dialog
|
// abort if the user denies access from the native dialog
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -3,7 +3,7 @@ import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/model/actions/move_type.dart';
|
import 'package:aves/model/actions/move_type.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/services/android_file_service.dart';
|
import 'package:aves/services/storage_service.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:aves/utils/file_utils.dart';
|
import 'package:aves/utils/file_utils.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
@ -20,7 +20,7 @@ mixin SizeAwareMixin {
|
||||||
MoveType moveType,
|
MoveType moveType,
|
||||||
) async {
|
) async {
|
||||||
final destinationVolume = androidFileUtils.getStorageVolume(destinationAlbum);
|
final destinationVolume = androidFileUtils.getStorageVolume(destinationAlbum);
|
||||||
final free = await AndroidFileService.getFreeSpace(destinationVolume);
|
final free = await StorageService.getFreeSpace(destinationVolume);
|
||||||
int needed;
|
int needed;
|
||||||
int sumSize(sum, entry) => sum + entry.sizeBytes;
|
int sumSize(sum, entry) => sum + entry.sizeBytes;
|
||||||
switch (moveType) {
|
switch (moveType) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/services/android_file_service.dart';
|
import 'package:aves/services/storage_service.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:aves/utils/file_utils.dart';
|
import 'package:aves/utils/file_utils.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
||||||
|
@ -17,7 +17,7 @@ class _DebugStorageSectionState extends State<DebugStorageSection> with Automati
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
androidFileUtils.storageVolumes.forEach((volume) async {
|
androidFileUtils.storageVolumes.forEach((volume) async {
|
||||||
final byteCount = await AndroidFileService.getFreeSpace(volume);
|
final byteCount = await StorageService.getFreeSpace(volume);
|
||||||
setState(() => _freeSpaceByVolume[volume.path] = byteCount);
|
setState(() => _freeSpaceByVolume[volume.path] = byteCount);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:aves/model/actions/chip_actions.dart';
|
import 'package:aves/model/actions/chip_actions.dart';
|
||||||
import 'package:aves/model/actions/move_type.dart';
|
import 'package:aves/model/actions/move_type.dart';
|
||||||
import 'package:aves/model/covers.dart';
|
import 'package:aves/model/covers.dart';
|
||||||
|
@ -7,9 +9,9 @@ import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/highlight.dart';
|
import 'package:aves/model/highlight.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/services/android_file_service.dart';
|
|
||||||
import 'package:aves/services/image_op_events.dart';
|
import 'package:aves/services/image_op_events.dart';
|
||||||
import 'package:aves/services/services.dart';
|
import 'package:aves/services/services.dart';
|
||||||
|
import 'package:aves/services/storage_service.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
||||||
import 'package:aves/widgets/common/action_mixins/permission_aware.dart';
|
import 'package:aves/widgets/common/action_mixins/permission_aware.dart';
|
||||||
|
@ -132,16 +134,19 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showDeleteDialog(BuildContext context, AlbumFilter filter) async {
|
Future<void> _showDeleteDialog(BuildContext context, AlbumFilter filter) async {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
final source = context.read<CollectionSource>();
|
final source = context.read<CollectionSource>();
|
||||||
final selection = source.visibleEntries.where(filter.test).toSet();
|
final album = filter.album;
|
||||||
final count = selection.length;
|
final todoEntries = source.visibleEntries.where(filter.test).toSet();
|
||||||
|
final todoCount = todoEntries.length;
|
||||||
|
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return AvesDialog(
|
return AvesDialog(
|
||||||
context: context,
|
context: context,
|
||||||
content: Text(context.l10n.deleteAlbumConfirmationDialogMessage(count)),
|
content: Text(l10n.deleteAlbumConfirmationDialogMessage(todoCount)),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
|
@ -149,7 +154,7 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context, true),
|
onPressed: () => Navigator.pop(context, true),
|
||||||
child: Text(context.l10n.deleteButtonLabel),
|
child: Text(l10n.deleteButtonLabel),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -157,41 +162,47 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per
|
||||||
);
|
);
|
||||||
if (confirmed == null || !confirmed) return;
|
if (confirmed == null || !confirmed) return;
|
||||||
|
|
||||||
if (!await checkStoragePermission(context, selection)) return;
|
if (!await checkStoragePermissionForAlbums(context, {album})) return;
|
||||||
|
|
||||||
final selectionCount = selection.length;
|
|
||||||
source.pauseMonitoring();
|
source.pauseMonitoring();
|
||||||
showOpReport<ImageOpEvent>(
|
showOpReport<ImageOpEvent>(
|
||||||
context: context,
|
context: context,
|
||||||
opStream: imageFileService.delete(selection),
|
opStream: imageFileService.delete(todoEntries),
|
||||||
itemCount: selectionCount,
|
itemCount: todoCount,
|
||||||
onDone: (processed) async {
|
onDone: (processed) async {
|
||||||
final deletedUris = processed.where((event) => event.success).map((event) => event.uri).toSet();
|
final deletedUris = processed.where((event) => event.success).map((event) => event.uri).toSet();
|
||||||
await source.removeEntries(deletedUris);
|
await source.removeEntries(deletedUris);
|
||||||
source.resumeMonitoring();
|
source.resumeMonitoring();
|
||||||
|
|
||||||
final deletedCount = deletedUris.length;
|
final deletedCount = deletedUris.length;
|
||||||
if (deletedCount < selectionCount) {
|
if (deletedCount < todoCount) {
|
||||||
final count = selectionCount - deletedCount;
|
final count = todoCount - deletedCount;
|
||||||
showFeedback(context, context.l10n.collectionDeleteFailureFeedback(count));
|
showFeedbackWithMessenger(messenger, l10n.collectionDeleteFailureFeedback(count));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
await StorageService.deleteEmptyDirectories({album});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showRenameDialog(BuildContext context, AlbumFilter filter) async {
|
Future<void> _showRenameDialog(BuildContext context, AlbumFilter filter) async {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
|
final source = context.read<CollectionSource>();
|
||||||
final album = filter.album;
|
final album = filter.album;
|
||||||
|
final todoEntries = source.visibleEntries.where(filter.test).toSet();
|
||||||
|
final todoCount = todoEntries.length;
|
||||||
|
|
||||||
// check whether renaming is possible given OS restrictions,
|
// check whether renaming is possible given OS restrictions,
|
||||||
// before asking to input a new name
|
// before asking to input a new name
|
||||||
final restrictedDirs = await AndroidFileService.getRestrictedDirectories();
|
final restrictedDirs = await StorageService.getRestrictedDirectories();
|
||||||
final dir = VolumeRelativeDirectory.fromPath(album);
|
final dir = VolumeRelativeDirectory.fromPath(album);
|
||||||
if (restrictedDirs.contains(dir)) {
|
if (restrictedDirs.contains(dir)) {
|
||||||
await showRestrictedDirectoryDialog(context, dir);
|
await showRestrictedDirectoryDialog(context, dir);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final source = context.read<CollectionSource>();
|
|
||||||
final newName = await showDialog<String>(
|
final newName = await showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => RenameAlbumDialog(album),
|
builder: (context) => RenameAlbumDialog(album),
|
||||||
|
@ -200,15 +211,15 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per
|
||||||
|
|
||||||
if (!await checkStoragePermissionForAlbums(context, {album})) return;
|
if (!await checkStoragePermissionForAlbums(context, {album})) return;
|
||||||
|
|
||||||
final todoEntries = source.visibleEntries.where(filter.test).toSet();
|
final destinationAlbumParent = path.dirname(album);
|
||||||
final destinationAlbum = path.join(path.dirname(album), newName);
|
final destinationAlbum = path.join(destinationAlbumParent, newName);
|
||||||
|
|
||||||
if (!await checkFreeSpaceForMove(context, todoEntries, destinationAlbum, MoveType.move)) return;
|
if (!await checkFreeSpaceForMove(context, todoEntries, destinationAlbum, MoveType.move)) return;
|
||||||
|
|
||||||
final l10n = context.l10n;
|
if (!(await File(destinationAlbum).exists())) {
|
||||||
final messenger = ScaffoldMessenger.of(context);
|
// access to the destination parent is required to create the underlying destination folder
|
||||||
|
if (!await checkStoragePermissionForAlbums(context, {destinationAlbumParent})) return;
|
||||||
|
}
|
||||||
|
|
||||||
final todoCount = todoEntries.length;
|
|
||||||
source.pauseMonitoring();
|
source.pauseMonitoring();
|
||||||
showOpReport<MoveOpEvent>(
|
showOpReport<MoveOpEvent>(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -226,6 +237,9 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per
|
||||||
} else {
|
} else {
|
||||||
showFeedbackWithMessenger(messenger, l10n.genericSuccessFeedback);
|
showFeedbackWithMessenger(messenger, l10n.genericSuccessFeedback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
await StorageService.deleteEmptyDirectories({album});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/services/android_file_service.dart';
|
import 'package:aves/services/storage_service.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/identity/empty.dart';
|
import 'package:aves/widgets/common/identity/empty.dart';
|
||||||
|
@ -39,7 +39,7 @@ class _StorageAccessPageState extends State<StorageAccessPage> {
|
||||||
_load();
|
_load();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _load() => _pathLoader = AndroidFileService.getGrantedDirectories();
|
void _load() => _pathLoader = StorageService.getGrantedDirectories();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -87,7 +87,7 @@ class _StorageAccessPageState extends State<StorageAccessPage> {
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: Icon(AIcons.clear),
|
icon: Icon(AIcons.clear),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await AndroidFileService.revokeDirectoryAccess(path);
|
await StorageService.revokeDirectoryAccess(path);
|
||||||
_load();
|
_load();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,7 +3,7 @@ import 'dart:ui';
|
||||||
import 'package:aves/main.dart' as app;
|
import 'package:aves/main.dart' as app;
|
||||||
import 'package:aves/model/settings/enums.dart';
|
import 'package:aves/model/settings/enums.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/services/android_file_service.dart';
|
import 'package:aves/services/storage_service.dart';
|
||||||
import 'package:flutter_driver/driver_extension.dart';
|
import 'package:flutter_driver/driver_extension.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ void main() {
|
||||||
// scan files copied from test assets
|
// scan files copied from test assets
|
||||||
// we do it via the app instead of broadcasting via ADB
|
// we do it via the app instead of broadcasting via ADB
|
||||||
// because `MEDIA_SCANNER_SCAN_FILE` intent got deprecated in API 29
|
// because `MEDIA_SCANNER_SCAN_FILE` intent got deprecated in API 29
|
||||||
AndroidFileService.scanFile(path.join(targetPicturesDir, 'ipse.jpg'), 'image/jpeg');
|
StorageService.scanFile(path.join(targetPicturesDir, 'ipse.jpg'), 'image/jpeg');
|
||||||
|
|
||||||
configureAndLaunch();
|
configureAndLaunch();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue