album: rename (WIP)

This commit is contained in:
Thibault Deckers 2020-09-21 18:44:58 +09:00
parent 2edf04b6f5
commit 917b14ce6d
8 changed files with 263 additions and 16 deletions

View file

@ -58,6 +58,9 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
case "rotate":
new Thread(() -> rotate(call, new MethodResultWrapper(result))).start();
break;
case "renameDirectory":
new Thread(() -> renameDirectory(call, new MethodResultWrapper(result))).start();
break;
default:
result.notImplemented();
break;
@ -179,4 +182,26 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
}
});
}
private void renameDirectory(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
String dirPath = call.argument("path");
String newName = call.argument("newName");
if (dirPath == null || newName == null) {
result.error("renameDirectory-args", "failed because of missing arguments", null);
return;
}
ImageProvider provider = new MediaStoreImageProvider();
provider.renameDirectory(activity, dirPath, newName, new ImageProvider.AlbumRenameOpCallback() {
@Override
public void onSuccess(List<Map<String, Object>> fieldsByEntry) {
result.success(fieldsByEntry);
}
@Override
public void onFailure(Throwable throwable) {
result.error("renameDirectory-failure", "failed to rename directory", throwable.getMessage());
}
});
}
}

View file

@ -22,6 +22,7 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -86,6 +87,91 @@ public abstract class ImageProvider {
scanNewPath(context, newFile.getPath(), mimeType, callback);
}
public void renameDirectory(Context context, String oldDirPath, String newDirName, final AlbumRenameOpCallback callback) {
if (!oldDirPath.endsWith(File.separator)) {
oldDirPath += File.separator;
}
DocumentFileCompat destinationDirDocFile = StorageUtils.createDirectoryIfAbsent(context, oldDirPath);
if (destinationDirDocFile == null) {
callback.onFailure(new Exception("failed to find directory at path=" + oldDirPath));
return;
}
final ArrayList<Map<String, Object>> entries = new ArrayList<>();
entries.addAll(listContentEntries(context, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, oldDirPath));
entries.addAll(listContentEntries(context, MediaStore.Video.Media.EXTERNAL_CONTENT_URI, oldDirPath));
boolean renamed;
try {
renamed = destinationDirDocFile.renameTo(newDirName);
} catch (FileNotFoundException e) {
callback.onFailure(new Exception("failed to rename to name=" + newDirName + " directory at path=" + oldDirPath, e));
return;
}
if (!renamed) {
callback.onFailure(new Exception("failed to rename to name=" + newDirName + " directory at path=" + oldDirPath));
return;
}
String newDirPath = new File(oldDirPath).getParent() + File.separator + newDirName + File.separator;
for (Map<String, Object> entry : entries) {
String displayName = (String) entry.get("displayName");
String mimeType = (String) entry.get("mimeType");
String oldEntryPath = oldDirPath + displayName;
MediaScannerConnection.scanFile(context, new String[]{oldEntryPath}, new String[]{mimeType}, null);
String newEntryPath = newDirPath + displayName;
scanNewPath(context, newEntryPath, mimeType, new ImageProvider.ImageOpCallback() {
@Override
public void onSuccess(Map<String, Object> newFields) {
// TODO TLAD process ID and report success
entry.putAll(newFields);
Log.d(LOG_TAG, "success with entry=" + entry);
}
@Override
public void onFailure(Throwable throwable) {
// TODO TLAD report failure
}
});
}
callback.onSuccess(entries);
}
private List<Map<String, Object>> listContentEntries(Context context, Uri contentUri, String dirPath) {
final ArrayList<Map<String, Object>> entries = new ArrayList<>();
String[] projection = {
MediaStore.MediaColumns._ID,
MediaStore.MediaColumns.DISPLAY_NAME,
MediaStore.MediaColumns.MIME_TYPE,
};
String selection = MediaStore.MediaColumns.DATA + " like ?";
try {
Cursor cursor = context.getContentResolver().query(contentUri, projection, selection, new String[]{dirPath + "%"}, null);
if (cursor != null) {
int idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID);
int displayNameColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME);
int mimeTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE);
while (cursor.moveToNext()) {
entries.add(new HashMap<String, Object>() {{
put("oldContentId", cursor.getInt(idColumn));
put("displayName", cursor.getString(displayNameColumn));
put("mimeType", cursor.getString(mimeTypeColumn));
}});
}
cursor.close();
}
} catch (Exception e) {
Log.e(LOG_TAG, "failed to list entries in contentUri=" + contentUri, e);
}
return entries;
}
public void rotate(final Context context, final String path, final Uri uri, final String mimeType, final boolean clockwise, final ImageOpCallback callback) {
switch (mimeType) {
case MimeTypes.JPEG:
@ -261,6 +347,7 @@ public abstract class ImageProvider {
Map<String, Object> newFields = new HashMap<>();
// we retrieve updated fields as the renamed file became a new entry in the Media Store
String[] projection = {
MediaStore.MediaColumns.DISPLAY_NAME,
MediaStore.MediaColumns.TITLE,
MediaStore.MediaColumns.DATE_MODIFIED,
};
@ -271,6 +358,7 @@ public abstract class ImageProvider {
newFields.put("uri", contentUri.toString());
newFields.put("contentId", contentId);
newFields.put("path", path);
newFields.put("displayName", cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)));
newFields.put("title", cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.TITLE)));
newFields.put("dateModifiedSecs", cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)));
}
@ -294,4 +382,10 @@ public abstract class ImageProvider {
void onFailure(Throwable throwable);
}
public interface AlbumRenameOpCallback {
void onSuccess(List<Map<String, Object>> fieldsByEntry);
void onFailure(Throwable throwable);
}
}

View file

@ -2,14 +2,14 @@ package deckers.thibault.aves.utils;
public class MimeTypes {
public static final String IMAGE = "image";
public static final String DNG = "image/x-adobe-dng";
public static final String DNG = "image/x-adobe-dng"; // .dng
public static final String GIF = "image/gif";
public static final String HEIC = "image/heic";
public static final String HEIF = "image/heif";
public static final String JPEG = "image/jpeg";
public static final String PNG = "image/png";
public static final String PSD = "image/x-photoshop";
public static final String SVG = "image/svg+xml";
public static final String PSD = "image/x-photoshop"; // .psd
public static final String SVG = "image/svg+xml"; // .svg
public static final String WEBP = "image/webp";
public static final String VIDEO = "video";

View file

@ -40,7 +40,7 @@ class ImageFileService {
}
}
static Future<List> getObsoleteEntries(List<int> knownContentIds) async {
static Future<List<int>> getObsoleteEntries(List<int> knownContentIds) async {
try {
final result = await platform.invokeMethod('getObsoleteEntries', <String, dynamic>{
'knownContentIds': knownContentIds,
@ -194,6 +194,19 @@ class ImageFileService {
}
return {};
}
static Future<List<Map>> renameDirectory(String path, String newName) async {
try {
final result = await platform.invokeMethod('renameDirectory', <String, dynamic>{
'path': path,
'newName': newName,
});
return (result as List).cast<Map>();
} on PlatformException catch (e) {
debugPrint('renameDirectory failed with code=${e.code}, exception=${e.message}, details=${e.details}');
}
return [];
}
}
@immutable

View file

@ -132,9 +132,11 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
}
Future<void> _validate() async {
final path = _buildAlbumPath(_nameController.text);
_existsNotifier.value = path.isEmpty ? false : await Directory(path).exists();
_isValidNotifier.value = (_nameController.text ?? '').isNotEmpty;
final newName = _nameController.text ?? '';
final path = _buildAlbumPath(newName);
final exists = newName.isNotEmpty && await Directory(path).exists();
_existsNotifier.value = exists;
_isValidNotifier.value = newName.isNotEmpty;
}
void _submit(BuildContext context) => Navigator.pop(context, _buildAlbumPath(_nameController.text));

View file

@ -0,0 +1,88 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:path/path.dart' as path;
import '../aves_dialog.dart';
class RenameAlbumDialog extends StatefulWidget {
final String album;
const RenameAlbumDialog(this.album);
@override
_RenameAlbumDialogState createState() => _RenameAlbumDialogState();
}
class _RenameAlbumDialogState extends State<RenameAlbumDialog> {
final TextEditingController _nameController = TextEditingController();
final ValueNotifier<bool> _existsNotifier = ValueNotifier(false);
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
String get album => widget.album;
String get initialValue => path.basename(album);
@override
void initState() {
super.initState();
_nameController.text = initialValue;
_validate();
}
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AvesDialog(
content: ValueListenableBuilder<bool>(
valueListenable: _existsNotifier,
builder: (context, exists, child) {
return TextField(
controller: _nameController,
decoration: InputDecoration(
labelText: 'New name',
helperText: exists ? 'Album already exists' : '',
),
autofocus: true,
onChanged: (_) => _validate(),
onSubmitted: (_) => _submit(context),
);
}),
actions: [
FlatButton(
onPressed: () => Navigator.pop(context),
child: Text('Cancel'.toUpperCase()),
),
ValueListenableBuilder<bool>(
valueListenable: _isValidNotifier,
builder: (context, isValid, child) {
return FlatButton(
onPressed: isValid ? () => _submit(context) : null,
child: Text('Apply'.toUpperCase()),
);
},
)
],
);
}
String _buildAlbumPath(String name) {
if (name == null || name.isEmpty) return '';
return path.join(path.dirname(album), name);
}
Future<void> _validate() async {
final newName = _nameController.text ?? '';
final path = _buildAlbumPath(newName);
final exists = newName.isNotEmpty && await FileSystemEntity.type(path) != FileSystemEntityType.notFound;
_existsNotifier.value = exists && newName != initialValue;
_isValidNotifier.value = newName.isNotEmpty && !exists;
}
void _submit(BuildContext context) => Navigator.pop(context, _nameController.text);
}

View file

@ -2,7 +2,7 @@ import 'dart:io';
import 'package:aves/model/image_entry.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart';
import 'package:path/path.dart' as path;
import '../aves_dialog.dart';
@ -65,13 +65,17 @@ class _RenameEntryDialogState extends State<RenameEntryDialog> {
);
}
Future<void> _validate() async {
var newName = _nameController.text ?? '';
if (newName.isNotEmpty) {
newName += entry.extension;
String _buildEntryPath(String name) {
if (name == null || name.isEmpty) return '';
return path.join(entry.directory, name + entry.extension);
}
final type = await FileSystemEntity.type(join(entry.directory, newName));
_isValidNotifier.value = type == FileSystemEntityType.notFound;
Future<void> _validate() async {
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;
}
void _submit(BuildContext context) => Navigator.pop(context, _nameController.text);

View file

@ -1,9 +1,15 @@
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/image_file_service.dart';
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/filter_grids/common/chip_actions.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:pedantic/pedantic.dart';
class ChipActionDelegate {
Future<void> onActionSelected(BuildContext context, CollectionFilter filter, ChipAction action) async {
@ -25,16 +31,31 @@ class ChipActionDelegate {
}
}
class AlbumChipActionDelegate extends ChipActionDelegate {
class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, PermissionAwareMixin {
@override
Future<void> onActionSelected(BuildContext context, CollectionFilter filter, ChipAction action) async {
await super.onActionSelected(context, filter, action);
final album = (filter as AlbumFilter).album;
switch (action) {
case ChipAction.rename:
// TODO TLAD rename album
unawaited(_showRenameDialog(context, album));
break;
default:
break;
}
}
Future<void> _showRenameDialog(BuildContext context, String album) async {
final newName = await showDialog<String>(
context: context,
builder: (context) => RenameAlbumDialog(album),
);
if (newName == null || newName.isEmpty) return;
if (!await checkStoragePermissionForAlbums(context, {album})) return;
// TODO TLAD rename album
final result = await ImageFileService.renameDirectory(album, newName);
showFeedback(context, result != null ? 'Done!' : 'Failed');
}
}