album: rename (WIP)
This commit is contained in:
parent
2edf04b6f5
commit
917b14ce6d
8 changed files with 263 additions and 16 deletions
|
@ -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());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
|
|
88
lib/widgets/common/action_delegates/rename_album_dialog.dart
Normal file
88
lib/widgets/common/action_delegates/rename_album_dialog.dart
Normal 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);
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue