copy/move: album creation
This commit is contained in:
parent
a437c2fe9a
commit
97e3fe62c0
15 changed files with 323 additions and 133 deletions
|
@ -85,8 +85,8 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure() {
|
public void onFailure(Throwable throwable) {
|
||||||
result.error("getImageEntry-failure", "failed to get entry for uri=" + uriString, null);
|
result.error("getImageEntry-failure", "failed to get entry for uri=" + uriString, throwable);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -114,8 +114,8 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure() {
|
public void onFailure(Throwable throwable) {
|
||||||
new Handler(Looper.getMainLooper()).post(() -> result.error("rename-failure", "failed to rename", null));
|
new Handler(Looper.getMainLooper()).post(() -> result.error("rename-failure", "failed to rename", throwable));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -143,8 +143,8 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure() {
|
public void onFailure(Throwable throwable) {
|
||||||
new Handler(Looper.getMainLooper()).post(() -> result.error("rotate-failure", "failed to rotate", null));
|
new Handler(Looper.getMainLooper()).post(() -> result.error("rotate-failure", "failed to rotate", throwable));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,9 @@ import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import deckers.thibault.aves.model.ImageEntry;
|
||||||
import deckers.thibault.aves.model.provider.ImageProvider;
|
import deckers.thibault.aves.model.provider.ImageProvider;
|
||||||
import deckers.thibault.aves.model.provider.ImageProviderFactory;
|
import deckers.thibault.aves.model.provider.ImageProviderFactory;
|
||||||
import deckers.thibault.aves.utils.Utils;
|
import deckers.thibault.aves.utils.Utils;
|
||||||
|
@ -91,25 +93,18 @@ public class ImageOpStreamHandler implements EventChannel.StreamHandler {
|
||||||
String destinationDir = (String) argMap.get("destinationPath");
|
String destinationDir = (String) argMap.get("destinationPath");
|
||||||
if (copy == null || destinationDir == null) return;
|
if (copy == null || destinationDir == null) return;
|
||||||
|
|
||||||
for (Map<String, Object> entryMap : entryMapList) {
|
ArrayList<ImageEntry> entries = entryMapList.stream().map(ImageEntry::new).collect(Collectors.toCollection(ArrayList::new));
|
||||||
String uriString = (String) entryMap.get("uri");
|
provider.moveMultiple(activity, copy, destinationDir, entries, new ImageProvider.ImageOpCallback() {
|
||||||
Uri sourceUri = Uri.parse(uriString);
|
@Override
|
||||||
String sourcePath = (String) entryMap.get("path");
|
public void onSuccess(Map<String, Object> fields) {
|
||||||
String mimeType = (String) entryMap.get("mimeType");
|
success(fields);
|
||||||
|
|
||||||
Map<String, Object> result = new HashMap<String, Object>() {{
|
|
||||||
put("uri", uriString);
|
|
||||||
}};
|
|
||||||
try {
|
|
||||||
Map<String, Object> newFields = provider.move(activity, sourcePath, sourceUri, destinationDir, mimeType, copy).get();
|
|
||||||
result.put("success", true);
|
|
||||||
result.put("newFields", newFields);
|
|
||||||
} catch (ExecutionException | InterruptedException e) {
|
|
||||||
Log.w(LOG_TAG, "failed to move to destinationDir=" + destinationDir + " entry with sourcePath=" + sourcePath, e);
|
|
||||||
result.put("success", false);
|
|
||||||
}
|
}
|
||||||
success(result);
|
|
||||||
}
|
@Override
|
||||||
|
public void onFailure(Throwable throwable) {
|
||||||
|
error("move-failure", "failed to move entries", throwable);
|
||||||
|
}
|
||||||
|
});
|
||||||
endOfStream();
|
endOfStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ class ContentImageProvider extends ImageProvider {
|
||||||
if (entry.hasSize() || entry.isSvg()) {
|
if (entry.hasSize() || entry.isSvg()) {
|
||||||
callback.onSuccess(entry.toMap());
|
callback.onSuccess(entry.toMap());
|
||||||
} else {
|
} else {
|
||||||
callback.onFailure();
|
callback.onFailure(new Exception("entry has no size"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package deckers.thibault.aves.model.provider;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
@ -10,11 +9,8 @@ import java.io.File;
|
||||||
|
|
||||||
import deckers.thibault.aves.model.ImageEntry;
|
import deckers.thibault.aves.model.ImageEntry;
|
||||||
import deckers.thibault.aves.utils.FileUtils;
|
import deckers.thibault.aves.utils.FileUtils;
|
||||||
import deckers.thibault.aves.utils.Utils;
|
|
||||||
|
|
||||||
class FileImageProvider extends ImageProvider {
|
class FileImageProvider extends ImageProvider {
|
||||||
private static final String LOG_TAG = Utils.createLogTag(FileImageProvider.class);
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void fetchSingle(@NonNull final Context context, @NonNull final Uri uri, @NonNull final String mimeType, @NonNull final ImageOpCallback callback) {
|
public void fetchSingle(@NonNull final Context context, @NonNull final Uri uri, @NonNull final String mimeType, @NonNull final ImageOpCallback callback) {
|
||||||
ImageEntry entry = new ImageEntry();
|
ImageEntry entry = new ImageEntry();
|
||||||
|
@ -32,8 +28,7 @@ class FileImageProvider extends ImageProvider {
|
||||||
entry.dateModifiedSecs = file.lastModified() / 1000;
|
entry.dateModifiedSecs = file.lastModified() / 1000;
|
||||||
}
|
}
|
||||||
} catch (SecurityException e) {
|
} catch (SecurityException e) {
|
||||||
Log.w(LOG_TAG, "failed to get path from file at uri=" + uri);
|
callback.onFailure(e);
|
||||||
callback.onFailure();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entry.fillPreCatalogMetadata(context);
|
entry.fillPreCatalogMetadata(context);
|
||||||
|
@ -41,7 +36,7 @@ class FileImageProvider extends ImageProvider {
|
||||||
if (entry.hasSize() || entry.isSvg()) {
|
if (entry.hasSize() || entry.isSvg()) {
|
||||||
callback.onSuccess(entry.toMap());
|
callback.onSuccess(entry.toMap());
|
||||||
} else {
|
} else {
|
||||||
callback.onFailure();
|
callback.onFailure(new Exception("entry has no size"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -33,9 +33,11 @@ import java.io.FileNotFoundException;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import deckers.thibault.aves.model.ImageEntry;
|
||||||
import deckers.thibault.aves.utils.Env;
|
import deckers.thibault.aves.utils.Env;
|
||||||
import deckers.thibault.aves.utils.MetadataHelper;
|
import deckers.thibault.aves.utils.MetadataHelper;
|
||||||
import deckers.thibault.aves.utils.MimeTypes;
|
import deckers.thibault.aves.utils.MimeTypes;
|
||||||
|
@ -56,21 +58,20 @@ public abstract class ImageProvider {
|
||||||
private static final String LOG_TAG = Utils.createLogTag(ImageProvider.class);
|
private static final String LOG_TAG = Utils.createLogTag(ImageProvider.class);
|
||||||
|
|
||||||
public void fetchSingle(@NonNull final Context context, @NonNull final Uri uri, @NonNull final String mimeType, @NonNull final ImageOpCallback callback) {
|
public void fetchSingle(@NonNull final Context context, @NonNull final Uri uri, @NonNull final String mimeType, @NonNull final ImageOpCallback callback) {
|
||||||
callback.onFailure();
|
callback.onFailure(new UnsupportedOperationException());
|
||||||
}
|
}
|
||||||
|
|
||||||
public ListenableFuture<Object> delete(final Activity activity, final String path, final Uri uri) {
|
public ListenableFuture<Object> delete(final Activity activity, final String path, final Uri uri) {
|
||||||
return Futures.immediateFailedFuture(new UnsupportedOperationException());
|
return Futures.immediateFailedFuture(new UnsupportedOperationException());
|
||||||
}
|
}
|
||||||
|
|
||||||
public ListenableFuture<Map<String, Object>> move(final Activity activity, final String sourcePath, final Uri sourceUri, String destinationDir, String mimeType, boolean copy) {
|
public void moveMultiple(final Activity activity, Boolean copy, String destinationDir, ArrayList<ImageEntry> entries, @NonNull ImageOpCallback callback) {
|
||||||
return Futures.immediateFailedFuture(new UnsupportedOperationException());
|
callback.onFailure(new UnsupportedOperationException());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void rename(final Activity activity, final String oldPath, final Uri oldMediaUri, final String mimeType, final String newFilename, final ImageOpCallback callback) {
|
public void rename(final Activity activity, final String oldPath, final Uri oldMediaUri, final String mimeType, final String newFilename, final ImageOpCallback callback) {
|
||||||
if (oldPath == null) {
|
if (oldPath == null) {
|
||||||
Log.w(LOG_TAG, "entry does not have a path, uri=" + oldMediaUri);
|
callback.onFailure(new IllegalArgumentException("entry does not have a path, uri=" + oldMediaUri));
|
||||||
callback.onFailure();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,13 +97,11 @@ public abstract class ImageProvider {
|
||||||
try {
|
try {
|
||||||
boolean renamed = df != null && df.renameTo(newFilename);
|
boolean renamed = df != null && df.renameTo(newFilename);
|
||||||
if (!renamed) {
|
if (!renamed) {
|
||||||
Log.w(LOG_TAG, "failed to rename entry at path=" + oldPath);
|
callback.onFailure(new Exception("failed to rename entry at path=" + oldPath));
|
||||||
callback.onFailure();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
Log.w(LOG_TAG, "failed to rename entry at path=" + oldPath, e);
|
callback.onFailure(e);
|
||||||
callback.onFailure();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,8 +124,7 @@ public abstract class ImageProvider {
|
||||||
cursor.close();
|
cursor.close();
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.w(LOG_TAG, "failed to update Media Store after renaming entry at path=" + oldPath, e);
|
callback.onFailure(e);
|
||||||
callback.onFailure();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -157,8 +155,11 @@ public abstract class ImageProvider {
|
||||||
public void rotate(final Activity activity, final String path, final Uri uri, final String mimeType, final boolean clockwise, final ImageOpCallback callback) {
|
public void rotate(final Activity activity, final String path, final Uri uri, final String mimeType, final boolean clockwise, final ImageOpCallback callback) {
|
||||||
// the reported `mimeType` (e.g. from Media Store) is sometimes incorrect
|
// the reported `mimeType` (e.g. from Media Store) is sometimes incorrect
|
||||||
// so we retrieve it again from the file metadata
|
// so we retrieve it again from the file metadata
|
||||||
String metadataMimeType = getMimeType(activity, uri);
|
String actualMimeType = getMimeType(activity, uri);
|
||||||
switch (metadataMimeType != null ? metadataMimeType : mimeType) {
|
if (actualMimeType == null) {
|
||||||
|
actualMimeType = mimeType;
|
||||||
|
}
|
||||||
|
switch (actualMimeType) {
|
||||||
case MimeTypes.JPEG:
|
case MimeTypes.JPEG:
|
||||||
rotateJpeg(activity, path, uri, clockwise, callback);
|
rotateJpeg(activity, path, uri, clockwise, callback);
|
||||||
break;
|
break;
|
||||||
|
@ -166,7 +167,7 @@ public abstract class ImageProvider {
|
||||||
rotatePng(activity, path, uri, clockwise, callback);
|
rotatePng(activity, path, uri, clockwise, callback);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
callback.onFailure();
|
callback.onFailure(new UnsupportedOperationException("unsupported mimeType=" + actualMimeType));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,8 +183,7 @@ public abstract class ImageProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean rotated = false;
|
int newOrientationCode;
|
||||||
int newOrientationCode = 0;
|
|
||||||
try {
|
try {
|
||||||
ExifInterface exif = new ExifInterface(editablePath);
|
ExifInterface exif = new ExifInterface(editablePath);
|
||||||
switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)) {
|
switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)) {
|
||||||
|
@ -205,12 +205,8 @@ public abstract class ImageProvider {
|
||||||
|
|
||||||
// copy the edited temporary file to the original DocumentFile
|
// copy the edited temporary file to the original DocumentFile
|
||||||
DocumentFileCompat.fromFile(new File(editablePath)).copyTo(DocumentFileCompat.fromSingleUri(activity, uri));
|
DocumentFileCompat.fromFile(new File(editablePath)).copyTo(DocumentFileCompat.fromSingleUri(activity, uri));
|
||||||
rotated = true;
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.w(LOG_TAG, "failed to edit EXIF to rotate image at path=" + path, e);
|
callback.onFailure(e);
|
||||||
}
|
|
||||||
if (!rotated) {
|
|
||||||
callback.onFailure();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,8 +249,7 @@ public abstract class ImageProvider {
|
||||||
|
|
||||||
Bitmap originalImage = BitmapFactory.decodeFile(path);
|
Bitmap originalImage = BitmapFactory.decodeFile(path);
|
||||||
if (originalImage == null) {
|
if (originalImage == null) {
|
||||||
Log.e(LOG_TAG, "failed to decode image at path=" + path);
|
callback.onFailure(new Exception("failed to decode image at path=" + path));
|
||||||
callback.onFailure();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Matrix matrix = new Matrix();
|
Matrix matrix = new Matrix();
|
||||||
|
@ -263,18 +258,13 @@ public abstract class ImageProvider {
|
||||||
matrix.setRotate(clockwise ? 90 : -90, originalWidth >> 1, originalHeight >> 1);
|
matrix.setRotate(clockwise ? 90 : -90, originalWidth >> 1, originalHeight >> 1);
|
||||||
Bitmap rotatedImage = Bitmap.createBitmap(originalImage, 0, 0, originalWidth, originalHeight, matrix, true);
|
Bitmap rotatedImage = Bitmap.createBitmap(originalImage, 0, 0, originalWidth, originalHeight, matrix, true);
|
||||||
|
|
||||||
boolean rotated = false;
|
|
||||||
try (FileOutputStream fos = new FileOutputStream(editablePath)) {
|
try (FileOutputStream fos = new FileOutputStream(editablePath)) {
|
||||||
rotatedImage.compress(Bitmap.CompressFormat.PNG, 100, fos);
|
rotatedImage.compress(Bitmap.CompressFormat.PNG, 100, fos);
|
||||||
|
|
||||||
// copy the edited temporary file to the original DocumentFile
|
// copy the edited temporary file to the original DocumentFile
|
||||||
DocumentFileCompat.fromFile(new File(editablePath)).copyTo(DocumentFileCompat.fromSingleUri(activity, uri));
|
DocumentFileCompat.fromFile(new File(editablePath)).copyTo(DocumentFileCompat.fromSingleUri(activity, uri));
|
||||||
rotated = true;
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e(LOG_TAG, "failed to save rotated image to path=" + path, e);
|
callback.onFailure(e);
|
||||||
}
|
|
||||||
if (!rotated) {
|
|
||||||
callback.onFailure();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -306,8 +296,8 @@ public abstract class ImageProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface ImageOpCallback {
|
public interface ImageOpCallback {
|
||||||
void onSuccess(Map<String, Object> newFields);
|
void onSuccess(Map<String, Object> fields);
|
||||||
|
|
||||||
void onFailure();
|
void onFailure(Throwable throwable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import com.google.common.util.concurrent.SettableFuture;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
@ -88,7 +89,7 @@ public class MediaStoreImageProvider extends ImageProvider {
|
||||||
entryCount = fetchFrom(context, onSuccess, contentUri, VIDEO_PROJECTION);
|
entryCount = fetchFrom(context, onSuccess, contentUri, VIDEO_PROJECTION);
|
||||||
}
|
}
|
||||||
if (entryCount == 0) {
|
if (entryCount == 0) {
|
||||||
callback.onFailure();
|
callback.onFailure(new Exception("failed to fetch entry at uri=" + uri));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,9 +225,7 @@ public class MediaStoreImageProvider extends ImageProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ListenableFuture<Map<String, Object>> move(final Activity activity, final String sourcePath, final Uri sourceUri, String destinationDir, String mimeType, boolean copy) {
|
public void moveMultiple(final Activity activity, Boolean copy, String destinationDir, ArrayList<ImageEntry> entries, ImageOpCallback callback) {
|
||||||
SettableFuture<Map<String, Object>> future = SettableFuture.create();
|
|
||||||
|
|
||||||
String volumeName = "external";
|
String volumeName = "external";
|
||||||
StorageManager sm = activity.getSystemService(StorageManager.class);
|
StorageManager sm = activity.getSystemService(StorageManager.class);
|
||||||
if (sm != null) {
|
if (sm != null) {
|
||||||
|
@ -241,6 +240,34 @@ public class MediaStoreImageProvider extends ImageProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!StorageUtils.createDirectoryIfAbsent(activity, destinationDir)) {
|
||||||
|
callback.onFailure(new Exception("failed to create directory at path=" + destinationDir));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ImageEntry entry : entries) {
|
||||||
|
Uri sourceUri = entry.uri;
|
||||||
|
String sourcePath = entry.path;
|
||||||
|
String mimeType = entry.mimeType;
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<String, Object>() {{
|
||||||
|
put("uri", sourceUri.toString());
|
||||||
|
}};
|
||||||
|
try {
|
||||||
|
Map<String, Object> newFields = moveSingle(activity, volumeName, sourcePath, sourceUri, destinationDir, mimeType, copy).get();
|
||||||
|
result.put("success", true);
|
||||||
|
result.put("newFields", newFields);
|
||||||
|
} catch (ExecutionException | InterruptedException e) {
|
||||||
|
Log.w(LOG_TAG, "failed to move to destinationDir=" + destinationDir + " entry with sourcePath=" + sourcePath, e);
|
||||||
|
result.put("success", false);
|
||||||
|
}
|
||||||
|
callback.onSuccess(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListenableFuture<Map<String, Object>> moveSingle(final Activity activity, final String volumeName, final String sourcePath, final Uri sourceUri, String destinationDir, String mimeType, boolean copy) {
|
||||||
|
SettableFuture<Map<String, Object>> future = SettableFuture.create();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// from API 29, changing MediaColumns.RELATIVE_PATH can move files on disk (same storage device)
|
// from API 29, changing MediaColumns.RELATIVE_PATH can move files on disk (same storage device)
|
||||||
// from API 26, retrieve document URI from mediastore URI with `MediaStore.getDocumentUri(...)`
|
// from API 26, retrieve document URI from mediastore URI with `MediaStore.getDocumentUri(...)`
|
||||||
|
|
|
@ -59,20 +59,24 @@ public class StorageUtils {
|
||||||
public static MediaMetadataRetriever openMetadataRetriever(Context context, Uri uri, String path) {
|
public static MediaMetadataRetriever openMetadataRetriever(Context context, Uri uri, String path) {
|
||||||
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
try {
|
||||||
// we get a permission denial if we require original from a provider other than the media store
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
if (isMediaStoreContentUri(uri)) {
|
// we get a permission denial if we require original from a provider other than the media store
|
||||||
uri = MediaStore.setRequireOriginal(uri);
|
if (isMediaStoreContentUri(uri)) {
|
||||||
|
uri = MediaStore.setRequireOriginal(uri);
|
||||||
|
}
|
||||||
|
retriever.setDataSource(context, uri);
|
||||||
|
return retriever;
|
||||||
}
|
}
|
||||||
retriever.setDataSource(context, uri);
|
|
||||||
return retriever;
|
|
||||||
}
|
|
||||||
|
|
||||||
// on Android <Q, we directly work with file paths if possible
|
// on Android <Q, we directly work with file paths if possible
|
||||||
if (path != null) {
|
if (path != null) {
|
||||||
retriever.setDataSource(path);
|
retriever.setDataSource(path);
|
||||||
} else {
|
} else {
|
||||||
retriever.setDataSource(context, uri);
|
retriever.setDataSource(context, uri);
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
Log.w(LOG_TAG, "failed to open MediaMetadataRetriever for uri=" + uri + ", path=" + path);
|
||||||
}
|
}
|
||||||
return retriever;
|
return retriever;
|
||||||
}
|
}
|
||||||
|
@ -189,17 +193,13 @@ public class StorageUtils {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
PathSegments pathSegments = new PathSegments(path, storageVolumeRoots);
|
|
||||||
ArrayList<String> pathSteps = Lists.newArrayList(Splitter.on(File.separatorChar)
|
|
||||||
.trimResults().omitEmptyStrings().split(pathSegments.getRelativePath()));
|
|
||||||
pathSteps.add(pathSegments.getFilename());
|
|
||||||
Iterator<String> pathIterator = pathSteps.iterator();
|
|
||||||
|
|
||||||
DocumentFileCompat documentFile = DocumentFileCompat.fromTreeUri(context, rootTreeUri);
|
DocumentFileCompat documentFile = DocumentFileCompat.fromTreeUri(context, rootTreeUri);
|
||||||
if (documentFile == null) {
|
if (documentFile == null) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
// follow the entry path down the document tree
|
// follow the entry path down the document tree
|
||||||
|
Iterator<String> pathIterator = getPathStepIterator(storageVolumeRoots, path);
|
||||||
while (pathIterator.hasNext()) {
|
while (pathIterator.hasNext()) {
|
||||||
documentFile = documentFile.findFile(pathIterator.next());
|
documentFile = documentFile.findFile(pathIterator.next());
|
||||||
if (documentFile == null) {
|
if (documentFile == null) {
|
||||||
|
@ -209,9 +209,20 @@ public class StorageUtils {
|
||||||
return Optional.of(documentFile);
|
return Optional.of(documentFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Iterator<String> getPathStepIterator(String[] storageVolumeRoots, String path) {
|
||||||
|
PathSegments pathSegments = new PathSegments(path, storageVolumeRoots);
|
||||||
|
ArrayList<String> pathSteps = Lists.newArrayList(Splitter.on(File.separatorChar)
|
||||||
|
.trimResults().omitEmptyStrings().split(pathSegments.getRelativePath()));
|
||||||
|
String filename = pathSegments.getFilename();
|
||||||
|
if (filename != null && filename.length() > 0) {
|
||||||
|
pathSteps.add(filename);
|
||||||
|
}
|
||||||
|
return pathSteps.iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static DocumentFileCompat getDocumentFile(Activity activity, @NonNull String path, @NonNull Uri mediaUri) {
|
public static DocumentFileCompat getDocumentFile(@NonNull Activity activity, @NonNull String path, @NonNull Uri mediaUri) {
|
||||||
if (Env.requireAccessPermission(path)) {
|
if (Env.requireAccessPermission(path)) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
Uri docUri = MediaStore.getDocumentUri(activity, mediaUri);
|
Uri docUri = MediaStore.getDocumentUri(activity, mediaUri);
|
||||||
|
@ -227,6 +238,40 @@ public class StorageUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean createDirectoryIfAbsent(@NonNull Activity activity, @NonNull String directoryPath) {
|
||||||
|
if (Env.requireAccessPermission(directoryPath)) {
|
||||||
|
Uri rootTreeUri = PermissionManager.getSdCardTreeUri(activity);
|
||||||
|
DocumentFileCompat parentFile = DocumentFileCompat.fromTreeUri(activity, rootTreeUri);
|
||||||
|
if (parentFile == null) return false;
|
||||||
|
|
||||||
|
String[] storageVolumeRoots = Env.getStorageVolumeRoots(activity);
|
||||||
|
if (!directoryPath.endsWith(File.separator)) {
|
||||||
|
directoryPath += File.separator;
|
||||||
|
}
|
||||||
|
Iterator<String> pathIterator = getPathStepIterator(storageVolumeRoots, directoryPath);
|
||||||
|
while (pathIterator.hasNext()) {
|
||||||
|
String dirName = pathIterator.next();
|
||||||
|
DocumentFileCompat dirFile = parentFile.findFile(dirName);
|
||||||
|
if (dirFile == null || !dirFile.exists()) {
|
||||||
|
try {
|
||||||
|
dirFile = parentFile.createDirectory(dirName);
|
||||||
|
if (dirFile != null) {
|
||||||
|
parentFile = dirFile;
|
||||||
|
}
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
Log.e(LOG_TAG, "failed to create directory with name=" + dirName + " from parent=" + parentFile, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
File directory = new File(directoryPath);
|
||||||
|
if (directory.exists()) return true;
|
||||||
|
return directory.mkdirs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static String copyFileToTemp(String path) {
|
public static String copyFileToTemp(String path) {
|
||||||
try {
|
try {
|
||||||
String extension = MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(new File(path)).toString());
|
String extension = MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(new File(path)).toString());
|
||||||
|
|
|
@ -121,6 +121,7 @@ class MetadataDb {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _batchInsertMetadata(Batch batch, CatalogMetadata metadata) {
|
void _batchInsertMetadata(Batch batch, CatalogMetadata metadata) {
|
||||||
|
if (metadata == null) return;
|
||||||
if (metadata.dateMillis != 0) {
|
if (metadata.dateMillis != 0) {
|
||||||
batch.insert(
|
batch.insert(
|
||||||
dateTakenTable,
|
dateTakenTable,
|
||||||
|
@ -171,6 +172,7 @@ class MetadataDb {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _batchInsertAddress(Batch batch, AddressDetails address) {
|
void _batchInsertAddress(Batch batch, AddressDetails address) {
|
||||||
|
if (address == null) return;
|
||||||
batch.insert(
|
batch.insert(
|
||||||
addressTable,
|
addressTable,
|
||||||
address.toMap(),
|
address.toMap(),
|
||||||
|
@ -214,6 +216,7 @@ class MetadataDb {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _batchInsertFavourite(Batch batch, FavouriteRow row) {
|
void _batchInsertFavourite(Batch batch, FavouriteRow row) {
|
||||||
|
if (row == null) return;
|
||||||
batch.insert(
|
batch.insert(
|
||||||
favouriteTable,
|
favouriteTable,
|
||||||
row.toMap(),
|
row.toMap(),
|
||||||
|
|
|
@ -5,21 +5,20 @@ import 'package:path/path.dart';
|
||||||
final AndroidFileUtils androidFileUtils = AndroidFileUtils._private();
|
final AndroidFileUtils androidFileUtils = AndroidFileUtils._private();
|
||||||
|
|
||||||
class AndroidFileUtils {
|
class AndroidFileUtils {
|
||||||
String externalStorage, dcimPath, downloadPath, moviesPath, picturesPath;
|
String primaryStorage, dcimPath, downloadPath, moviesPath, picturesPath;
|
||||||
|
Set<StorageVolume> storageVolumes = {};
|
||||||
static List<StorageVolume> storageVolumes = [];
|
Map appNameMap = {};
|
||||||
static Map appNameMap = {};
|
|
||||||
|
|
||||||
AndroidFileUtils._private();
|
AndroidFileUtils._private();
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
storageVolumes = (await AndroidFileService.getStorageVolumes()).map((map) => StorageVolume.fromMap(map)).toList();
|
storageVolumes = (await AndroidFileService.getStorageVolumes()).map((map) => StorageVolume.fromMap(map)).toSet();
|
||||||
// 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'
|
||||||
externalStorage = storageVolumes.firstWhere((volume) => volume.isPrimary).path;
|
primaryStorage = storageVolumes.firstWhere((volume) => volume.isPrimary).path;
|
||||||
dcimPath = join(externalStorage, 'DCIM');
|
dcimPath = join(primaryStorage, 'DCIM');
|
||||||
downloadPath = join(externalStorage, 'Download');
|
downloadPath = join(primaryStorage, 'Download');
|
||||||
moviesPath = join(externalStorage, 'Movies');
|
moviesPath = join(primaryStorage, 'Movies');
|
||||||
picturesPath = join(externalStorage, 'Pictures');
|
picturesPath = join(primaryStorage, 'Pictures');
|
||||||
appNameMap = await AndroidAppService.getAppNames()
|
appNameMap = await AndroidAppService.getAppNames()
|
||||||
..addAll({'KakaoTalkDownload': 'com.kakao.talk'});
|
..addAll({'KakaoTalkDownload': 'com.kakao.talk'});
|
||||||
}
|
}
|
||||||
|
@ -38,20 +37,20 @@ class AndroidFileUtils {
|
||||||
|
|
||||||
AlbumType getAlbumType(String albumDirectory) {
|
AlbumType getAlbumType(String albumDirectory) {
|
||||||
if (albumDirectory != null) {
|
if (albumDirectory != null) {
|
||||||
if (androidFileUtils.isCameraPath(albumDirectory)) return AlbumType.camera;
|
if (isCameraPath(albumDirectory)) return AlbumType.camera;
|
||||||
if (androidFileUtils.isDownloadPath(albumDirectory)) return AlbumType.download;
|
if (isDownloadPath(albumDirectory)) return AlbumType.download;
|
||||||
if (androidFileUtils.isScreenRecordingsPath(albumDirectory)) return AlbumType.screenRecordings;
|
if (isScreenRecordingsPath(albumDirectory)) return AlbumType.screenRecordings;
|
||||||
if (androidFileUtils.isScreenshotsPath(albumDirectory)) return AlbumType.screenshots;
|
if (isScreenshotsPath(albumDirectory)) return AlbumType.screenshots;
|
||||||
|
|
||||||
final parts = albumDirectory.split(separator);
|
final parts = albumDirectory.split(separator);
|
||||||
if (albumDirectory.startsWith(androidFileUtils.externalStorage) && appNameMap.keys.contains(parts.last)) return AlbumType.app;
|
if (albumDirectory.startsWith(primaryStorage) && appNameMap.keys.contains(parts.last)) return AlbumType.app;
|
||||||
}
|
}
|
||||||
return AlbumType.regular;
|
return AlbumType.regular;
|
||||||
}
|
}
|
||||||
|
|
||||||
String getAlbumAppPackageName(String albumDirectory) {
|
String getAlbumAppPackageName(String albumDirectory) {
|
||||||
final parts = albumDirectory.split(separator);
|
final parts = albumDirectory.split(separator);
|
||||||
return AndroidFileUtils.appNameMap[parts.last];
|
return appNameMap[parts.last];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_unregisterWidget(widget);
|
_unregisterWidget(widget);
|
||||||
_browseToSelectAnimation.dispose();
|
_browseToSelectAnimation.dispose();
|
||||||
|
_searchFieldController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
92
lib/widgets/common/action_delegates/create_album_dialog.dart
Normal file
92
lib/widgets/common/action_delegates/create_album_dialog.dart
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
|
||||||
|
class CreateAlbumDialog extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_CreateAlbumDialogState createState() => _CreateAlbumDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
||||||
|
final TextEditingController _nameController = TextEditingController();
|
||||||
|
Set<StorageVolume> allVolumes;
|
||||||
|
StorageVolume primaryVolume, selectedVolume;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
// TODO TLAD improve new album default name
|
||||||
|
_nameController.text = 'Album 1';
|
||||||
|
|
||||||
|
allVolumes = androidFileUtils.storageVolumes;
|
||||||
|
primaryVolume = allVolumes.firstWhere((volume) => volume.isPrimary, orElse: () => allVolumes.first);
|
||||||
|
selectedVolume = primaryVolume;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_nameController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('New Album'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _nameController,
|
||||||
|
// autofocus: true,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text('Storage:'),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: DropdownButton<StorageVolume>(
|
||||||
|
isExpanded: true,
|
||||||
|
items: allVolumes
|
||||||
|
.map((volume) => DropdownMenuItem(
|
||||||
|
value: volume,
|
||||||
|
child: Text(
|
||||||
|
volume.description,
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
value: selectedVolume,
|
||||||
|
onChanged: (volume) => setState(() => selectedVolume = volume),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 0),
|
||||||
|
actions: [
|
||||||
|
FlatButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text('Cancel'.toUpperCase()),
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
onPressed: () => Navigator.pop(context, _buildAlbumPath()),
|
||||||
|
child: Text('Create'.toUpperCase()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _buildAlbumPath() {
|
||||||
|
final newName = _nameController.text;
|
||||||
|
if (newName == null || newName.isEmpty) return null;
|
||||||
|
return join(selectedVolume == primaryVolume ? androidFileUtils.dcimPath : selectedVolume.path, newName);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import 'package:aves/model/collection_lens.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/services/android_app_service.dart';
|
import 'package:aves/services/android_app_service.dart';
|
||||||
import 'package:aves/services/image_file_service.dart';
|
import 'package:aves/services/image_file_service.dart';
|
||||||
|
import 'package:aves/widgets/common/action_delegates/rename_entry_dialog.dart';
|
||||||
import 'package:aves/widgets/common/action_delegates/permission_aware.dart';
|
import 'package:aves/widgets/common/action_delegates/permission_aware.dart';
|
||||||
import 'package:aves/widgets/common/entry_actions.dart';
|
import 'package:aves/widgets/common/entry_actions.dart';
|
||||||
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
|
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
|
||||||
|
@ -167,28 +168,10 @@ class EntryActionDelegate with PermissionAwareMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showRenameDialog(BuildContext context, ImageEntry entry) async {
|
Future<void> _showRenameDialog(BuildContext context, ImageEntry entry) async {
|
||||||
final currentName = entry.filenameWithoutExtension ?? entry.sourceTitle;
|
|
||||||
final controller = TextEditingController(text: currentName);
|
|
||||||
final newName = await showDialog<String>(
|
final newName = await showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (context) => RenameEntryDialog(entry),
|
||||||
return AlertDialog(
|
);
|
||||||
content: TextField(
|
|
||||||
controller: controller,
|
|
||||||
autofocus: true,
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
FlatButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: Text('Cancel'.toUpperCase()),
|
|
||||||
),
|
|
||||||
FlatButton(
|
|
||||||
onPressed: () => Navigator.pop(context, controller.text),
|
|
||||||
child: Text('Apply'.toUpperCase()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
if (newName == null || newName.isEmpty) return;
|
if (newName == null || newName.isEmpty) return;
|
||||||
|
|
||||||
if (!await checkStoragePermission(context, [entry])) return;
|
if (!await checkStoragePermission(context, [entry])) return;
|
||||||
|
|
48
lib/widgets/common/action_delegates/rename_entry_dialog.dart
Normal file
48
lib/widgets/common/action_delegates/rename_entry_dialog.dart
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class RenameEntryDialog extends StatefulWidget {
|
||||||
|
final ImageEntry entry;
|
||||||
|
|
||||||
|
const RenameEntryDialog(this.entry);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_RenameEntryDialogState createState() => _RenameEntryDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RenameEntryDialogState extends State<RenameEntryDialog> {
|
||||||
|
final TextEditingController _nameController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final entry = widget.entry;
|
||||||
|
_nameController.text = entry.filenameWithoutExtension ?? entry.sourceTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_nameController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
content: TextField(
|
||||||
|
controller: _nameController,
|
||||||
|
autofocus: true,
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
FlatButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text('Cancel'.toUpperCase()),
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
onPressed: () => Navigator.pop(context, _nameController.text),
|
||||||
|
child: Text('Apply'.toUpperCase()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import 'package:aves/model/metadata_db.dart';
|
||||||
import 'package:aves/services/android_app_service.dart';
|
import 'package:aves/services/android_app_service.dart';
|
||||||
import 'package:aves/services/image_file_service.dart';
|
import 'package:aves/services/image_file_service.dart';
|
||||||
import 'package:aves/widgets/album/app_bar.dart';
|
import 'package:aves/widgets/album/app_bar.dart';
|
||||||
|
import 'package:aves/widgets/common/action_delegates/create_album_dialog.dart';
|
||||||
import 'package:aves/widgets/common/action_delegates/permission_aware.dart';
|
import 'package:aves/widgets/common/action_delegates/permission_aware.dart';
|
||||||
import 'package:aves/widgets/common/entry_actions.dart';
|
import 'package:aves/widgets/common/entry_actions.dart';
|
||||||
import 'package:aves/widgets/common/icons.dart';
|
import 'package:aves/widgets/common/icons.dart';
|
||||||
|
@ -16,6 +17,7 @@ import 'package:collection/collection.dart';
|
||||||
import 'package:flushbar/flushbar.dart';
|
import 'package:flushbar/flushbar.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:percent_indicator/circular_percent_indicator.dart';
|
import 'package:percent_indicator/circular_percent_indicator.dart';
|
||||||
|
|
||||||
|
@ -53,9 +55,9 @@ class SelectionActionDelegate with PermissionAwareMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _moveSelection(BuildContext context, {@required bool copy}) async {
|
Future _moveSelection(BuildContext context, {@required bool copy}) async {
|
||||||
final filter = await Navigator.push(
|
final destinationAlbum = await Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute<AlbumFilter>(
|
MaterialPageRoute<String>(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final source = collection.source;
|
final source = collection.source;
|
||||||
return FilterGridPage(
|
return FilterGridPage(
|
||||||
|
@ -64,10 +66,17 @@ class SelectionActionDelegate with PermissionAwareMixin {
|
||||||
leading: const BackButton(),
|
leading: const BackButton(),
|
||||||
title: Text(copy ? 'Copy to Album' : 'Move to Album'),
|
title: Text(copy ? 'Copy to Album' : 'Move to Album'),
|
||||||
actions: [
|
actions: [
|
||||||
const IconButton(
|
IconButton(
|
||||||
icon: Icon(AIcons.createAlbum),
|
icon: Icon(AIcons.createAlbum),
|
||||||
// TODO TLAD album creation
|
onPressed: () async {
|
||||||
onPressed: null,
|
final newAlbum = await showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => CreateAlbumDialog(),
|
||||||
|
);
|
||||||
|
if (newAlbum != null && newAlbum.isNotEmpty) {
|
||||||
|
Navigator.pop<String>(context, newAlbum);
|
||||||
|
}
|
||||||
|
},
|
||||||
tooltip: 'Create album',
|
tooltip: 'Create album',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -75,12 +84,12 @@ class SelectionActionDelegate with PermissionAwareMixin {
|
||||||
),
|
),
|
||||||
filterEntries: source.getAlbumEntries(),
|
filterEntries: source.getAlbumEntries(),
|
||||||
filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)),
|
filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)),
|
||||||
onPressed: (filter) => Navigator.pop<AlbumFilter>(context, filter),
|
onPressed: (filter) => Navigator.pop<String>(context, (filter as AlbumFilter)?.album),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (filter == null) return;
|
if (destinationAlbum == null || destinationAlbum.isEmpty) return;
|
||||||
|
|
||||||
final selection = collection.selection.toList();
|
final selection = collection.selection.toList();
|
||||||
if (!await checkStoragePermission(context, selection)) return;
|
if (!await checkStoragePermission(context, selection)) return;
|
||||||
|
@ -88,7 +97,7 @@ class SelectionActionDelegate with PermissionAwareMixin {
|
||||||
_showOpReport(
|
_showOpReport(
|
||||||
context: context,
|
context: context,
|
||||||
selection: selection,
|
selection: selection,
|
||||||
opStream: ImageFileService.move(selection, copy: copy, destinationAlbum: filter.album),
|
opStream: ImageFileService.move(selection, copy: copy, destinationAlbum: destinationAlbum),
|
||||||
onDone: (Set<MoveOpEvent> processed) {
|
onDone: (Set<MoveOpEvent> processed) {
|
||||||
debugPrint('$runtimeType _moveSelection onDone');
|
debugPrint('$runtimeType _moveSelection onDone');
|
||||||
final movedOps = processed.where((e) => e.success);
|
final movedOps = processed.where((e) => e.success);
|
||||||
|
@ -97,6 +106,9 @@ class SelectionActionDelegate with PermissionAwareMixin {
|
||||||
if (movedCount < selectionCount) {
|
if (movedCount < selectionCount) {
|
||||||
final count = selectionCount - movedCount;
|
final count = selectionCount - movedCount;
|
||||||
_showFeedback(context, 'Failed to move ${Intl.plural(count, one: '${count} item', other: '${count} items')}');
|
_showFeedback(context, 'Failed to move ${Intl.plural(count, one: '${count} item', other: '${count} items')}');
|
||||||
|
} else {
|
||||||
|
final count = movedCount;
|
||||||
|
_showFeedback(context, '${copy ? 'Copied' : 'Moved'} ${Intl.plural(count, one: '${count} item', other: '${count} items')}');
|
||||||
}
|
}
|
||||||
if (movedCount > 0) {
|
if (movedCount > 0) {
|
||||||
final source = collection.source;
|
final source = collection.source;
|
||||||
|
|
|
@ -43,7 +43,7 @@ class DebugPageState extends State<DebugPage> {
|
||||||
super.initState();
|
super.initState();
|
||||||
_startDbReport();
|
_startDbReport();
|
||||||
_volumePermissionLoader = Future.wait<Tuple2<String, bool>>(
|
_volumePermissionLoader = Future.wait<Tuple2<String, bool>>(
|
||||||
AndroidFileUtils.storageVolumes.map(
|
androidFileUtils.storageVolumes.map(
|
||||||
(volume) => AndroidFileService.hasGrantedPermissionToVolumeRoot(volume.path).then(
|
(volume) => AndroidFileService.hasGrantedPermissionToVolumeRoot(volume.path).then(
|
||||||
(value) => Tuple2(volume.path, value),
|
(value) => Tuple2(volume.path, value),
|
||||||
),
|
),
|
||||||
|
@ -259,7 +259,7 @@ class DebugPageState extends State<DebugPage> {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
...AndroidFileUtils.storageVolumes.expand((v) => [
|
...androidFileUtils.storageVolumes.expand((v) => [
|
||||||
Text(v.path),
|
Text(v.path),
|
||||||
InfoRowGroup({
|
InfoRowGroup({
|
||||||
'description': '${v.description}',
|
'description': '${v.description}',
|
||||||
|
|
Loading…
Reference in a new issue