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":
|
case "rotate":
|
||||||
new Thread(() -> rotate(call, new MethodResultWrapper(result))).start();
|
new Thread(() -> rotate(call, new MethodResultWrapper(result))).start();
|
||||||
break;
|
break;
|
||||||
|
case "renameDirectory":
|
||||||
|
new Thread(() -> renameDirectory(call, new MethodResultWrapper(result))).start();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
result.notImplemented();
|
result.notImplemented();
|
||||||
break;
|
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.FileNotFoundException;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -86,6 +87,91 @@ public abstract class ImageProvider {
|
||||||
scanNewPath(context, newFile.getPath(), mimeType, callback);
|
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) {
|
public void rotate(final Context context, final String path, final Uri uri, final String mimeType, final boolean clockwise, final ImageOpCallback callback) {
|
||||||
switch (mimeType) {
|
switch (mimeType) {
|
||||||
case MimeTypes.JPEG:
|
case MimeTypes.JPEG:
|
||||||
|
@ -261,6 +347,7 @@ public abstract class ImageProvider {
|
||||||
Map<String, Object> newFields = new HashMap<>();
|
Map<String, Object> newFields = new HashMap<>();
|
||||||
// we retrieve updated fields as the renamed file became a new entry in the Media Store
|
// we retrieve updated fields as the renamed file became a new entry in the Media Store
|
||||||
String[] projection = {
|
String[] projection = {
|
||||||
|
MediaStore.MediaColumns.DISPLAY_NAME,
|
||||||
MediaStore.MediaColumns.TITLE,
|
MediaStore.MediaColumns.TITLE,
|
||||||
MediaStore.MediaColumns.DATE_MODIFIED,
|
MediaStore.MediaColumns.DATE_MODIFIED,
|
||||||
};
|
};
|
||||||
|
@ -271,6 +358,7 @@ public abstract class ImageProvider {
|
||||||
newFields.put("uri", contentUri.toString());
|
newFields.put("uri", contentUri.toString());
|
||||||
newFields.put("contentId", contentId);
|
newFields.put("contentId", contentId);
|
||||||
newFields.put("path", path);
|
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("title", cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.TITLE)));
|
||||||
newFields.put("dateModifiedSecs", cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)));
|
newFields.put("dateModifiedSecs", cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)));
|
||||||
}
|
}
|
||||||
|
@ -294,4 +382,10 @@ public abstract class ImageProvider {
|
||||||
|
|
||||||
void onFailure(Throwable throwable);
|
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 class MimeTypes {
|
||||||
public static final String IMAGE = "image";
|
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 GIF = "image/gif";
|
||||||
public static final String HEIC = "image/heic";
|
public static final String HEIC = "image/heic";
|
||||||
public static final String HEIF = "image/heif";
|
public static final String HEIF = "image/heif";
|
||||||
public static final String JPEG = "image/jpeg";
|
public static final String JPEG = "image/jpeg";
|
||||||
public static final String PNG = "image/png";
|
public static final String PNG = "image/png";
|
||||||
public static final String PSD = "image/x-photoshop";
|
public static final String PSD = "image/x-photoshop"; // .psd
|
||||||
public static final String SVG = "image/svg+xml";
|
public static final String SVG = "image/svg+xml"; // .svg
|
||||||
public static final String WEBP = "image/webp";
|
public static final String WEBP = "image/webp";
|
||||||
|
|
||||||
public static final String VIDEO = "video";
|
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 {
|
try {
|
||||||
final result = await platform.invokeMethod('getObsoleteEntries', <String, dynamic>{
|
final result = await platform.invokeMethod('getObsoleteEntries', <String, dynamic>{
|
||||||
'knownContentIds': knownContentIds,
|
'knownContentIds': knownContentIds,
|
||||||
|
@ -194,6 +194,19 @@ class ImageFileService {
|
||||||
}
|
}
|
||||||
return {};
|
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
|
@immutable
|
||||||
|
|
|
@ -132,9 +132,11 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _validate() async {
|
Future<void> _validate() async {
|
||||||
final path = _buildAlbumPath(_nameController.text);
|
final newName = _nameController.text ?? '';
|
||||||
_existsNotifier.value = path.isEmpty ? false : await Directory(path).exists();
|
final path = _buildAlbumPath(newName);
|
||||||
_isValidNotifier.value = (_nameController.text ?? '').isNotEmpty;
|
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));
|
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:aves/model/image_entry.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
import '../aves_dialog.dart';
|
import '../aves_dialog.dart';
|
||||||
|
|
||||||
|
@ -65,13 +65,17 @@ class _RenameEntryDialogState extends State<RenameEntryDialog> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _validate() async {
|
String _buildEntryPath(String name) {
|
||||||
var newName = _nameController.text ?? '';
|
if (name == null || name.isEmpty) return '';
|
||||||
if (newName.isNotEmpty) {
|
return path.join(entry.directory, name + entry.extension);
|
||||||
newName += 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);
|
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/filters/filters.dart';
|
||||||
import 'package:aves/model/settings/settings.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/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: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:pedantic/pedantic.dart';
|
||||||
|
|
||||||
class ChipActionDelegate {
|
class ChipActionDelegate {
|
||||||
Future<void> onActionSelected(BuildContext context, CollectionFilter filter, ChipAction action) async {
|
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
|
@override
|
||||||
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);
|
||||||
|
final album = (filter as AlbumFilter).album;
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case ChipAction.rename:
|
case ChipAction.rename:
|
||||||
// TODO TLAD rename album
|
unawaited(_showRenameDialog(context, album));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
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