Merge branch 'develop'
|
@ -22,6 +22,7 @@ if (flutterVersionName == null) {
|
|||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
|
@ -43,6 +44,10 @@ if (keystorePropertiesFile.exists()) {
|
|||
android {
|
||||
compileSdkVersion 30 // latest (or latest-1 if the sources of latest SDK are unavailable)
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable 'InvalidPackage'
|
||||
}
|
||||
|
@ -100,11 +105,12 @@ repositories {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
// enable support for Java 8 language APIs (stream, optional, etc.)
|
||||
// coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.9'
|
||||
|
||||
implementation 'androidx.core:core:1.5.0-alpha02' // v1.5.0-alpha02 for ShortcutManagerCompat.setDynamicShortcuts
|
||||
implementation "androidx.exifinterface:exifinterface:1.2.0"
|
||||
implementation 'androidx.core:core:1.5.0-alpha03' // v1.5.0-alpha02+ for ShortcutManagerCompat.setDynamicShortcuts
|
||||
implementation "androidx.exifinterface:exifinterface:1.3.0"
|
||||
implementation 'com.commonsware.cwac:document:0.4.1'
|
||||
implementation 'com.drewnoakes:metadata-extractor:2.14.0'
|
||||
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
||||
|
|
|
@ -1,179 +0,0 @@
|
|||
package deckers.thibault.aves;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.content.pm.ShortcutInfoCompat;
|
||||
import androidx.core.content.pm.ShortcutManagerCompat;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import app.loup.streams_channel.StreamsChannel;
|
||||
import deckers.thibault.aves.channel.calls.AppAdapterHandler;
|
||||
import deckers.thibault.aves.channel.calls.AppShortcutHandler;
|
||||
import deckers.thibault.aves.channel.calls.ImageFileHandler;
|
||||
import deckers.thibault.aves.channel.calls.MetadataHandler;
|
||||
import deckers.thibault.aves.channel.calls.StorageHandler;
|
||||
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler;
|
||||
import deckers.thibault.aves.channel.streams.ImageOpStreamHandler;
|
||||
import deckers.thibault.aves.channel.streams.IntentStreamHandler;
|
||||
import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler;
|
||||
import deckers.thibault.aves.channel.streams.StorageAccessStreamHandler;
|
||||
import deckers.thibault.aves.utils.PermissionManager;
|
||||
import deckers.thibault.aves.utils.Utils;
|
||||
import io.flutter.embedding.android.FlutterActivity;
|
||||
import io.flutter.plugin.common.BinaryMessenger;
|
||||
import io.flutter.plugin.common.EventChannel;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
public class MainActivity extends FlutterActivity {
|
||||
private static final String LOG_TAG = Utils.createLogTag(MainActivity.class);
|
||||
|
||||
public static final String INTENT_CHANNEL = "deckers.thibault/aves/intent";
|
||||
public static final String VIEWER_CHANNEL = "deckers.thibault/aves/viewer";
|
||||
|
||||
private IntentStreamHandler intentStreamHandler;
|
||||
private Map<String, Object> intentDataMap;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
handleIntent(getIntent());
|
||||
|
||||
BinaryMessenger messenger = Objects.requireNonNull(getFlutterEngine()).getDartExecutor().getBinaryMessenger();
|
||||
|
||||
new MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(new AppAdapterHandler(this));
|
||||
new MethodChannel(messenger, AppShortcutHandler.CHANNEL).setMethodCallHandler(new AppShortcutHandler(this));
|
||||
new MethodChannel(messenger, ImageFileHandler.CHANNEL).setMethodCallHandler(new ImageFileHandler(this));
|
||||
new MethodChannel(messenger, MetadataHandler.CHANNEL).setMethodCallHandler(new MetadataHandler(this));
|
||||
new MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(new StorageHandler(this));
|
||||
|
||||
new StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory(args -> new ImageByteStreamHandler(this, args));
|
||||
new StreamsChannel(messenger, ImageOpStreamHandler.CHANNEL).setStreamHandlerFactory(args -> new ImageOpStreamHandler(this, args));
|
||||
new StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory(args -> new MediaStoreStreamHandler(this, args));
|
||||
new StreamsChannel(messenger, StorageAccessStreamHandler.CHANNEL).setStreamHandlerFactory(args -> new StorageAccessStreamHandler(this, args));
|
||||
|
||||
new MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler(
|
||||
(call, result) -> {
|
||||
if (call.method.contentEquals("getIntentData")) {
|
||||
result.success(intentDataMap);
|
||||
intentDataMap = null;
|
||||
} else if (call.method.contentEquals("pick")) {
|
||||
result.success(intentDataMap);
|
||||
intentDataMap = null;
|
||||
String resultUri = call.argument("uri");
|
||||
if (resultUri != null) {
|
||||
Intent intent = new Intent();
|
||||
intent.setData(Uri.parse(resultUri));
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
setResult(RESULT_OK, intent);
|
||||
} else {
|
||||
setResult(RESULT_CANCELED);
|
||||
}
|
||||
finish();
|
||||
}
|
||||
});
|
||||
intentStreamHandler = new IntentStreamHandler();
|
||||
new EventChannel(messenger, INTENT_CHANNEL).setStreamHandler(intentStreamHandler);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
setupShortcuts();
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
||||
private void setupShortcuts() {
|
||||
// do not use 'route' as extra key, as the Flutter framework acts on it
|
||||
|
||||
ShortcutInfoCompat search = new ShortcutInfoCompat.Builder(this, "search")
|
||||
.setShortLabel(getString(R.string.search_shortcut_short_label))
|
||||
.setIcon(IconCompat.createWithResource(this, R.mipmap.ic_shortcut_search))
|
||||
.setIntent(new Intent(Intent.ACTION_MAIN, null, this, MainActivity.class)
|
||||
.putExtra("page", "/search"))
|
||||
.build();
|
||||
|
||||
ShortcutInfoCompat videos = new ShortcutInfoCompat.Builder(this, "videos")
|
||||
.setShortLabel(getString(R.string.videos_shortcut_short_label))
|
||||
.setIcon(IconCompat.createWithResource(this, R.mipmap.ic_shortcut_movie))
|
||||
.setIntent(new Intent(Intent.ACTION_MAIN, null, this, MainActivity.class)
|
||||
.putExtra("page", "/collection")
|
||||
.putExtra("filters", new String[]{"{\"type\":\"mime\",\"mime\":\"video/*\"}"}))
|
||||
.build();
|
||||
|
||||
ShortcutManagerCompat.setDynamicShortcuts(this, Arrays.asList(videos, search));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(@NonNull Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
handleIntent(intent);
|
||||
intentStreamHandler.notifyNewIntent();
|
||||
}
|
||||
|
||||
private void handleIntent(Intent intent) {
|
||||
Log.i(LOG_TAG, "handleIntent intent=" + intent);
|
||||
if (intent == null) return;
|
||||
String action = intent.getAction();
|
||||
if (action == null) return;
|
||||
switch (action) {
|
||||
case Intent.ACTION_MAIN:
|
||||
String page = intent.getStringExtra("page");
|
||||
if (page != null) {
|
||||
intentDataMap = new HashMap<>();
|
||||
intentDataMap.put("page", page);
|
||||
String[] filters = intent.getStringArrayExtra("filters");
|
||||
intentDataMap.put("filters", filters != null ? new ArrayList<>(Arrays.asList(filters)) : null);
|
||||
}
|
||||
break;
|
||||
case Intent.ACTION_VIEW:
|
||||
Uri uri = intent.getData();
|
||||
String mimeType = intent.getType();
|
||||
if (uri != null && mimeType != null) {
|
||||
intentDataMap = new HashMap<>();
|
||||
intentDataMap.put("action", "view");
|
||||
intentDataMap.put("uri", uri.toString());
|
||||
intentDataMap.put("mimeType", mimeType);
|
||||
}
|
||||
break;
|
||||
case Intent.ACTION_GET_CONTENT:
|
||||
case Intent.ACTION_PICK:
|
||||
intentDataMap = new HashMap<>();
|
||||
intentDataMap.put("action", "pick");
|
||||
intentDataMap.put("mimeType", intent.getType());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == PermissionManager.VOLUME_ROOT_PERMISSION_REQUEST_CODE) {
|
||||
if (resultCode != RESULT_OK || data.getData() == null) {
|
||||
PermissionManager.onPermissionResult(requestCode, null);
|
||||
return;
|
||||
}
|
||||
|
||||
Uri treeUri = data.getData();
|
||||
|
||||
// save access permissions across reboots
|
||||
final int takeFlags = data.getFlags()
|
||||
& (Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
|
||||
|
||||
// resume pending action
|
||||
PermissionManager.onPermissionResult(requestCode, treeUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -38,8 +38,6 @@ import deckers.thibault.aves.utils.Utils;
|
|||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
import static com.bumptech.glide.request.RequestOptions.centerCropTransform;
|
||||
|
||||
public class AppAdapterHandler implements MethodChannel.MethodCallHandler {
|
||||
private static final String LOG_TAG = Utils.createLogTag(AppAdapterHandler.class);
|
||||
|
||||
|
@ -184,7 +182,7 @@ public class AppAdapterHandler implements MethodChannel.MethodCallHandler {
|
|||
FutureTarget<Bitmap> target = Glide.with(context)
|
||||
.asBitmap()
|
||||
.apply(options)
|
||||
.apply(centerCropTransform())
|
||||
.centerCrop()
|
||||
.load(uri)
|
||||
.signature(signature)
|
||||
.submit(size, size);
|
||||
|
|
|
@ -2,6 +2,8 @@ package deckers.thibault.aves.channel.calls;
|
|||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
@ -10,6 +12,9 @@ import androidx.core.content.pm.ShortcutInfoCompat;
|
|||
import androidx.core.content.pm.ShortcutManagerCompat;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
|
||||
import com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool;
|
||||
import com.bumptech.glide.load.resource.bitmap.TransformationUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import deckers.thibault.aves.MainActivity;
|
||||
|
@ -35,8 +40,9 @@ public class AppShortcutHandler implements MethodChannel.MethodCallHandler {
|
|||
}
|
||||
case "pin": {
|
||||
String label = call.argument("label");
|
||||
byte[] iconBytes = call.argument("iconBytes");
|
||||
List<String> filters = call.argument("filters");
|
||||
pin(label, filters);
|
||||
new Thread(() -> pin(label, iconBytes, filters)).start();
|
||||
result.success(null);
|
||||
break;
|
||||
}
|
||||
|
@ -46,17 +52,28 @@ public class AppShortcutHandler implements MethodChannel.MethodCallHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private void pin(String label, @Nullable List<String> filters) {
|
||||
private void pin(String label, byte[] iconBytes, @Nullable List<String> filters) {
|
||||
if (!ShortcutManagerCompat.isRequestPinShortcutSupported(context) || filters == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
IconCompat icon;
|
||||
if (iconBytes != null && iconBytes.length > 0) {
|
||||
Bitmap bitmap = BitmapFactory.decodeByteArray(iconBytes, 0, iconBytes.length);
|
||||
bitmap = TransformationUtils.centerCrop(new LruBitmapPool(2 << 24), bitmap, 256, 256);
|
||||
icon = IconCompat.createWithBitmap(bitmap);
|
||||
} else {
|
||||
icon = IconCompat.createWithResource(context, R.mipmap.ic_shortcut_collection);
|
||||
}
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_MAIN, null, context, MainActivity.class)
|
||||
.putExtra("page", "/collection")
|
||||
.putExtra("filters", filters.toArray(new String[0]));
|
||||
|
||||
ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(context, "collection-" + TextUtils.join("-", filters))
|
||||
.setShortLabel(label)
|
||||
.setIcon(IconCompat.createWithResource(context, R.mipmap.ic_shortcut_collection))
|
||||
.setIntent(new Intent(Intent.ACTION_MAIN, null, context, MainActivity.class)
|
||||
.putExtra("page", "/collection")
|
||||
.putExtra("filters", filters.toArray(new String[0])))
|
||||
.setIcon(icon)
|
||||
.setIntent(intent)
|
||||
.build();
|
||||
|
||||
ShortcutManagerCompat.requestPinShortcut(context, shortcut, null);
|
||||
|
|
|
@ -120,10 +120,14 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
|||
AvesImageEntry entry = params.entry;
|
||||
Integer width = params.width;
|
||||
Integer height = params.height;
|
||||
// Log.d(LOG_TAG, "getThumbnailBytesByResolver width=" + width + ", path=" + entry.path);
|
||||
|
||||
ContentResolver resolver = activity.getContentResolver();
|
||||
return resolver.loadThumbnail(entry.uri, new Size(width, height), null);
|
||||
Bitmap bitmap = resolver.loadThumbnail(entry.uri, new Size(width, height), null);
|
||||
String mimeType = entry.mimeType;
|
||||
if (MimeTypes.DNG.equals(mimeType)) {
|
||||
bitmap = rotateBitmap(bitmap, entry.orientationDegrees);
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
private Bitmap getThumbnailBytesByMediaStore(Params params) {
|
||||
|
|
|
@ -58,9 +58,6 @@ 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;
|
||||
|
@ -182,26 +179,4 @@ 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());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -190,6 +190,9 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
|||
case "getContentResolverMetadata":
|
||||
new Thread(() -> getContentResolverMetadata(call, new MethodResultWrapper(result))).start();
|
||||
break;
|
||||
case "getEmbeddedPictures":
|
||||
new Thread(() -> getEmbeddedPictures(call, new MethodResultWrapper(result))).start();
|
||||
break;
|
||||
case "getExifThumbnails":
|
||||
new Thread(() -> getExifThumbnails(call, new MethodResultWrapper(result))).start();
|
||||
break;
|
||||
|
@ -415,7 +418,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
|||
metadataMap.put(KEY_LATITUDE, latitude);
|
||||
metadataMap.put(KEY_LONGITUDE, longitude);
|
||||
}
|
||||
} catch (NumberFormatException ex) {
|
||||
} catch (NumberFormatException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
@ -530,6 +533,26 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private void getEmbeddedPictures(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||
Uri uri = Uri.parse(call.argument("uri"));
|
||||
List<byte[]> pictures = new ArrayList<>();
|
||||
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, uri);
|
||||
if (retriever != null) {
|
||||
try {
|
||||
byte[] picture = retriever.getEmbeddedPicture();
|
||||
if (picture != null) {
|
||||
pictures.add(picture);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
result.error("getVideoEmbeddedPictures-failure", "failed to get embedded picture for uri=" + uri, e);
|
||||
} finally {
|
||||
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
|
||||
retriever.release();
|
||||
}
|
||||
}
|
||||
result.success(pictures);
|
||||
}
|
||||
|
||||
private void getExifThumbnails(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||
Uri uri = Uri.parse(call.argument("uri"));
|
||||
List<byte[]> thumbnails = new ArrayList<>();
|
||||
|
|
|
@ -52,10 +52,17 @@ public class StorageHandler implements MethodChannel.MethodCallHandler {
|
|||
}
|
||||
break;
|
||||
}
|
||||
case "scanFile": {
|
||||
case "revokeDirectoryAccess":
|
||||
String path = call.argument("path");
|
||||
PermissionManager.revokeDirectoryAccess(context, path);
|
||||
result.success(true);
|
||||
break;
|
||||
case "getGrantedDirectories":
|
||||
result.success(new ArrayList<>(PermissionManager.getGrantedDirs(context)));
|
||||
break;
|
||||
case "scanFile":
|
||||
scanFile(call, new MethodResultWrapper(result));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
result.notImplemented();
|
||||
break;
|
||||
|
|
|
@ -40,8 +40,8 @@ class VideoThumbnailFetcher implements DataFetcher<InputStream> {
|
|||
}
|
||||
callback.onDataReady(new ByteArrayInputStream(bos.toByteArray()));
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
callback.onLoadFailed(ex);
|
||||
} catch (Exception e) {
|
||||
callback.onLoadFailed(e);
|
||||
} finally {
|
||||
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
|
||||
retriever.release();
|
||||
|
|
|
@ -17,17 +17,14 @@ import com.bumptech.glide.load.resource.bitmap.TransformationUtils;
|
|||
import com.commonsware.cwac.document.DocumentFileCompat;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
|
||||
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;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import deckers.thibault.aves.model.AvesImageEntry;
|
||||
import deckers.thibault.aves.utils.MetadataHelper;
|
||||
|
@ -89,101 +86,6 @@ public abstract class ImageProvider {
|
|||
scanNewPath(context, newFile.getPath(), mimeType, callback);
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
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;
|
||||
}
|
||||
|
||||
List<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;
|
||||
}
|
||||
|
||||
List<SettableFuture<Map<String, Object>>> scanFutures = new ArrayList<>();
|
||||
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);
|
||||
|
||||
SettableFuture<Map<String, Object>> scanFuture = SettableFuture.create();
|
||||
scanFutures.add(scanFuture);
|
||||
String newEntryPath = newDirPath + displayName;
|
||||
scanNewPath(context, newEntryPath, mimeType, new ImageProvider.ImageOpCallback() {
|
||||
@Override
|
||||
public void onSuccess(Map<String, Object> newFields) {
|
||||
entry.putAll(newFields);
|
||||
entry.put("success", true);
|
||||
scanFuture.set(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable throwable) {
|
||||
Log.w(LOG_TAG, "failed to scan entry=" + displayName + " in new directory=" + newDirPath, throwable);
|
||||
entry.put("success", false);
|
||||
scanFuture.set(entry);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
callback.onSuccess(Futures.allAsList(scanFutures).get());
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
callback.onFailure(e);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> listContentEntries(Context context, Uri contentUri, String dirPath) {
|
||||
List<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:
|
||||
|
@ -355,7 +257,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
|
||||
// we retrieve updated fields as the renamed/moved file became a new entry in the Media Store
|
||||
String[] projection = {
|
||||
MediaStore.MediaColumns.DISPLAY_NAME,
|
||||
MediaStore.MediaColumns.TITLE,
|
||||
|
@ -392,10 +294,4 @@ public abstract class ImageProvider {
|
|||
|
||||
void onFailure(Throwable throwable);
|
||||
}
|
||||
|
||||
public interface AlbumRenameOpCallback {
|
||||
void onSuccess(List<Map<String, Object>> fieldsByEntry);
|
||||
|
||||
void onFailure(Throwable throwable);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
package deckers.thibault.aves.utils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.exifinterface.media.ExifInterface;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class MetadataHelper {
|
||||
|
||||
// interpret EXIF code to angle (0, 90, 180 or 270 degrees)
|
||||
public static int getOrientationDegreesForExifCode(int exifOrientation) {
|
||||
switch (exifOrientation) {
|
||||
case ExifInterface.ORIENTATION_ROTATE_180: // bottom, right side
|
||||
return 180;
|
||||
case ExifInterface.ORIENTATION_ROTATE_90: // right side, top
|
||||
return 90;
|
||||
case ExifInterface.ORIENTATION_ROTATE_270: // left side, bottom
|
||||
return 270;
|
||||
}
|
||||
// all other orientations (regular, flipped...) default to an angle of 0 degree
|
||||
return 0;
|
||||
}
|
||||
|
||||
// yyyyMMddTHHmmss(.sss)?(Z|+/-hhmm)?
|
||||
public static long parseVideoMetadataDate(@Nullable String dateString) {
|
||||
if (dateString == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// optional sub-second
|
||||
String subSecond = null;
|
||||
Matcher subSecondMatcher = Pattern.compile("(\\d{6})(\\.\\d+)").matcher(dateString);
|
||||
if (subSecondMatcher.find()) {
|
||||
subSecond = subSecondMatcher.group(2).substring(1);
|
||||
dateString = subSecondMatcher.replaceAll("$1");
|
||||
}
|
||||
|
||||
// optional time zone
|
||||
TimeZone timeZone = null;
|
||||
Matcher timeZoneMatcher = Pattern.compile("(Z|[+-]\\d{4})$").matcher(dateString);
|
||||
if (timeZoneMatcher.find()) {
|
||||
timeZone = TimeZone.getTimeZone("GMT" + timeZoneMatcher.group().replaceAll("Z", ""));
|
||||
dateString = timeZoneMatcher.replaceAll("");
|
||||
}
|
||||
|
||||
Date date = null;
|
||||
try {
|
||||
DateFormat parser = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.US);
|
||||
parser.setTimeZone((timeZone != null) ? timeZone : TimeZone.getTimeZone("GMT"));
|
||||
date = parser.parse(dateString);
|
||||
} catch (ParseException ex) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (date == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
long dateMillis = date.getTime();
|
||||
if (subSecond != null) {
|
||||
try {
|
||||
int millis = (int) (Double.parseDouble("." + subSecond) * 1000);
|
||||
if (millis >= 0 && millis < 1000) {
|
||||
dateMillis += millis;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return dateMillis;
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
package deckers.thibault.aves.utils;
|
||||
|
||||
public class MimeTypes {
|
||||
public static final String IMAGE = "image";
|
||||
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"; // .psd
|
||||
public static final String SVG = "image/svg+xml"; // .svg
|
||||
public static final String WEBP = "image/webp";
|
||||
|
||||
public static final String VIDEO = "video";
|
||||
public static final String AVI = "video/avi";
|
||||
public static final String MP2T = "video/mp2t"; // .m2ts
|
||||
public static final String MP4 = "video/mp4";
|
||||
}
|
|
@ -71,11 +71,11 @@ public class PermissionManager {
|
|||
}
|
||||
|
||||
public static Optional<String> getGrantedDirForPath(@NonNull Context context, @NonNull String anyPath) {
|
||||
return getGrantedDirs(context).stream().filter(anyPath::startsWith).findFirst();
|
||||
return getAccessibleDirs(context).stream().filter(anyPath::startsWith).findFirst();
|
||||
}
|
||||
|
||||
public static List<Map<String, String>> getInaccessibleDirectories(@NonNull Context context, @NonNull List<String> dirPaths) {
|
||||
Set<String> grantedDirs = getGrantedDirs(context);
|
||||
Set<String> accessibleDirs = getAccessibleDirs(context);
|
||||
|
||||
// find set of inaccessible directories for each volume
|
||||
Map<String, Set<String>> dirsPerVolume = new HashMap<>();
|
||||
|
@ -83,7 +83,7 @@ public class PermissionManager {
|
|||
if (!dirPath.endsWith(File.separator)) {
|
||||
dirPath += File.separator;
|
||||
}
|
||||
if (grantedDirs.stream().noneMatch(dirPath::startsWith)) {
|
||||
if (accessibleDirs.stream().noneMatch(dirPath::startsWith)) {
|
||||
// inaccessible dirs
|
||||
StorageUtils.PathSegments segments = new StorageUtils.PathSegments(context, dirPath);
|
||||
Set<String> dirSet = dirsPerVolume.getOrDefault(segments.volumePath, new HashSet<>());
|
||||
|
@ -135,22 +135,34 @@ public class PermissionManager {
|
|||
return inaccessibleDirs;
|
||||
}
|
||||
|
||||
private static Set<String> getGrantedDirs(Context context) {
|
||||
HashSet<String> accessibleDirs = new HashSet<>();
|
||||
|
||||
// find paths matching URIs granted by the user
|
||||
public static void revokeDirectoryAccess(Context context, String path) {
|
||||
Optional<Uri> uri = StorageUtils.convertDirPathToTreeUri(context, path);
|
||||
if (uri.isPresent()) {
|
||||
int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
|
||||
context.getContentResolver().releasePersistableUriPermission(uri.get(), flags);
|
||||
}
|
||||
}
|
||||
|
||||
// returns paths matching URIs granted by the user
|
||||
public static Set<String> getGrantedDirs(Context context) {
|
||||
Set<String> grantedDirs = new HashSet<>();
|
||||
for (UriPermission uriPermission : context.getContentResolver().getPersistedUriPermissions()) {
|
||||
Optional<String> dirPath = StorageUtils.convertTreeUriToDirPath(context, uriPermission.getUri());
|
||||
dirPath.ifPresent(accessibleDirs::add);
|
||||
dirPath.ifPresent(grantedDirs::add);
|
||||
}
|
||||
return grantedDirs;
|
||||
}
|
||||
|
||||
// returns paths accessible to the app (granted by the user or by default)
|
||||
private static Set<String> getAccessibleDirs(Context context) {
|
||||
Set<String> accessibleDirs = new HashSet<>(getGrantedDirs(context));
|
||||
// from Android R, we no longer have access permission by default on primary volume
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
||||
String primaryPath = StorageUtils.getPrimaryVolumePath();
|
||||
accessibleDirs.add(primaryPath);
|
||||
}
|
||||
|
||||
Log.d(LOG_TAG, "getGrantedDirs accessibleDirs=" + accessibleDirs);
|
||||
Log.d(LOG_TAG, "getAccessibleDirs accessibleDirs=" + accessibleDirs);
|
||||
return accessibleDirs;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
package deckers.thibault.aves.utils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class Utils {
|
||||
private static final int logTagMaxLength = 23;
|
||||
private static final Pattern logTagPackagePattern = Pattern.compile("(\\w)(\\w*)\\.");
|
||||
|
||||
public static String createLogTag(Class<?> clazz) {
|
||||
// shorten class name to "a.b.CccDdd"
|
||||
String logTag = logTagPackagePattern.matcher(clazz.getName()).replaceAll("$1.");
|
||||
if (logTag.length() > logTagMaxLength) {
|
||||
// shorten class name to "a.b.CD"
|
||||
String simpleName = clazz.getSimpleName();
|
||||
String shortSimpleName = simpleName.replaceAll("[a-z]", "");
|
||||
logTag = logTag.replace(simpleName, shortSimpleName);
|
||||
if (logTag.length() > logTagMaxLength) {
|
||||
// shorten class name to "CD"
|
||||
logTag = shortSimpleName;
|
||||
}
|
||||
}
|
||||
return logTag;
|
||||
}
|
||||
|
||||
public static void copyFile(final File source, final FileDescriptor descriptor) throws IOException {
|
||||
try (FileInputStream inStream = new FileInputStream(source); FileOutputStream outStream = new FileOutputStream(descriptor)) {
|
||||
final FileChannel inChannel = inStream.getChannel();
|
||||
final FileChannel outChannel = outStream.getChannel();
|
||||
final long size = inChannel.size();
|
||||
long position = 0;
|
||||
while (position < size) {
|
||||
position += inChannel.transferTo(position, 1024L * 1024L, outChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void copyFile(final File source, final File destination) throws IOException {
|
||||
try (FileInputStream inStream = new FileInputStream(source); FileOutputStream outStream = new FileOutputStream(destination)) {
|
||||
final FileChannel inChannel = inStream.getChannel();
|
||||
final FileChannel outChannel = outStream.getChannel();
|
||||
final long size = inChannel.size();
|
||||
long position = 0;
|
||||
while (position < size) {
|
||||
position += inChannel.transferTo(position, 1024L * 1024L, outChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
package deckers.thibault.aves
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import app.loup.streams_channel.StreamsChannel
|
||||
import deckers.thibault.aves.channel.calls.*
|
||||
import deckers.thibault.aves.channel.streams.*
|
||||
import deckers.thibault.aves.utils.PermissionManager
|
||||
import deckers.thibault.aves.utils.Utils
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
companion object {
|
||||
private val LOG_TAG = Utils.createLogTag(MainActivity::class.java)
|
||||
const val INTENT_CHANNEL = "deckers.thibault/aves/intent"
|
||||
const val VIEWER_CHANNEL = "deckers.thibault/aves/viewer"
|
||||
}
|
||||
|
||||
private val intentStreamHandler = IntentStreamHandler()
|
||||
private var intentDataMap: MutableMap<String, Any?>? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
handleIntent(intent)
|
||||
|
||||
val messenger = flutterEngine!!.dartExecutor.binaryMessenger
|
||||
|
||||
MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(AppAdapterHandler(this))
|
||||
MethodChannel(messenger, AppShortcutHandler.CHANNEL).setMethodCallHandler(AppShortcutHandler(this))
|
||||
MethodChannel(messenger, ImageFileHandler.CHANNEL).setMethodCallHandler(ImageFileHandler(this))
|
||||
MethodChannel(messenger, MetadataHandler.CHANNEL).setMethodCallHandler(MetadataHandler(this))
|
||||
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
|
||||
|
||||
StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(this, args) }
|
||||
StreamsChannel(messenger, ImageOpStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageOpStreamHandler(this, args) }
|
||||
StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(this, args) }
|
||||
StreamsChannel(messenger, StorageAccessStreamHandler.CHANNEL).setStreamHandlerFactory { args -> StorageAccessStreamHandler(this, args) }
|
||||
|
||||
MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"getIntentData" -> {
|
||||
result.success(intentDataMap)
|
||||
intentDataMap = null
|
||||
}
|
||||
"pick" -> {
|
||||
result.success(intentDataMap)
|
||||
intentDataMap = null
|
||||
val resultUri = call.argument<String>("uri")
|
||||
if (resultUri != null) {
|
||||
val intent = Intent().apply {
|
||||
data = Uri.parse(resultUri)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
setResult(RESULT_OK, intent)
|
||||
} else {
|
||||
setResult(RESULT_CANCELED)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
EventChannel(messenger, INTENT_CHANNEL).setStreamHandler(intentStreamHandler)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
setupShortcuts()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
||||
private fun setupShortcuts() {
|
||||
// do not use 'route' as extra key, as the Flutter framework acts on it
|
||||
|
||||
val search = ShortcutInfoCompat.Builder(this, "search")
|
||||
.setShortLabel(getString(R.string.search_shortcut_short_label))
|
||||
.setIcon(IconCompat.createWithResource(this, R.mipmap.ic_shortcut_search))
|
||||
.setIntent(Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java)
|
||||
.putExtra("page", "/search"))
|
||||
.build()
|
||||
|
||||
val videos = ShortcutInfoCompat.Builder(this, "videos")
|
||||
.setShortLabel(getString(R.string.videos_shortcut_short_label))
|
||||
.setIcon(IconCompat.createWithResource(this, R.mipmap.ic_shortcut_movie))
|
||||
.setIntent(Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java)
|
||||
.putExtra("page", "/collection")
|
||||
.putExtra("filters", arrayOf("{\"type\":\"mime\",\"mime\":\"video/*\"}")))
|
||||
.build()
|
||||
|
||||
ShortcutManagerCompat.setDynamicShortcuts(this, listOf(videos, search))
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
handleIntent(intent)
|
||||
intentStreamHandler.notifyNewIntent()
|
||||
}
|
||||
|
||||
private fun handleIntent(intent: Intent?) {
|
||||
Log.i(LOG_TAG, "handleIntent intent=$intent")
|
||||
if (intent == null) return
|
||||
when (intent.action) {
|
||||
Intent.ACTION_MAIN -> {
|
||||
val page = intent.getStringExtra("page")
|
||||
if (page != null) {
|
||||
intentDataMap = hashMapOf(
|
||||
"page" to page,
|
||||
"filters" to intent.getStringArrayExtra("filters")?.toList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Intent.ACTION_VIEW -> {
|
||||
val uri = intent.data
|
||||
val mimeType = intent.type
|
||||
if (uri != null && mimeType != null) {
|
||||
intentDataMap = hashMapOf(
|
||||
"action" to "view",
|
||||
"uri" to uri.toString(),
|
||||
"mimeType" to mimeType,
|
||||
)
|
||||
}
|
||||
}
|
||||
Intent.ACTION_GET_CONTENT, Intent.ACTION_PICK -> {
|
||||
intentDataMap = hashMapOf(
|
||||
"action" to "pick",
|
||||
"mimeType" to intent.type,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
|
||||
if (requestCode == PermissionManager.VOLUME_ROOT_PERMISSION_REQUEST_CODE) {
|
||||
val treeUri = data.data
|
||||
if (resultCode != RESULT_OK || treeUri == null) {
|
||||
PermissionManager.onPermissionResult(requestCode, null)
|
||||
return
|
||||
}
|
||||
|
||||
// save access permissions across reboots
|
||||
val takeFlags = (data.flags
|
||||
and (Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION))
|
||||
contentResolver.takePersistableUriPermission(treeUri, takeFlags)
|
||||
|
||||
// resume pending action
|
||||
PermissionManager.onPermissionResult(requestCode, treeUri)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package deckers.thibault.aves.utils
|
||||
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import java.text.DateFormat
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object MetadataHelper {
|
||||
// interpret EXIF code to angle (0, 90, 180 or 270 degrees)
|
||||
@JvmStatic
|
||||
fun getOrientationDegreesForExifCode(exifOrientation: Int): Int = when (exifOrientation) {
|
||||
ExifInterface.ORIENTATION_ROTATE_180 -> 180
|
||||
ExifInterface.ORIENTATION_ROTATE_90 -> 90
|
||||
ExifInterface.ORIENTATION_ROTATE_270 -> 270
|
||||
else -> 0 // all other orientations (regular, flipped...) default to an angle of 0 degree
|
||||
}
|
||||
|
||||
// yyyyMMddTHHmmss(.sss)?(Z|+/-hhmm)?
|
||||
@JvmStatic
|
||||
fun parseVideoMetadataDate(metadataDate: String?): Long {
|
||||
var dateString = metadataDate ?: return 0
|
||||
|
||||
// optional sub-second
|
||||
var subSecond: String? = null
|
||||
val subSecondMatcher = Pattern.compile("(\\d{6})(\\.\\d+)").matcher(dateString)
|
||||
if (subSecondMatcher.find()) {
|
||||
subSecond = subSecondMatcher.group(2)?.substring(1)
|
||||
dateString = subSecondMatcher.replaceAll("$1")
|
||||
}
|
||||
|
||||
// optional time zone
|
||||
var timeZone: TimeZone? = null
|
||||
val timeZoneMatcher = Pattern.compile("(Z|[+-]\\d{4})$").matcher(dateString)
|
||||
if (timeZoneMatcher.find()) {
|
||||
timeZone = TimeZone.getTimeZone("GMT" + timeZoneMatcher.group().replace("Z".toRegex(), ""))
|
||||
dateString = timeZoneMatcher.replaceAll("")
|
||||
}
|
||||
|
||||
val date: Date = try {
|
||||
val parser = SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.US)
|
||||
parser.timeZone = timeZone ?: TimeZone.getTimeZone("GMT")
|
||||
parser.parse(dateString)
|
||||
} catch (e: ParseException) {
|
||||
// ignore
|
||||
null
|
||||
} ?: return 0
|
||||
|
||||
var dateMillis = date.time
|
||||
if (subSecond != null) {
|
||||
try {
|
||||
val millis = (".$subSecond".toDouble() * 1000).toInt()
|
||||
if (millis in 0..999) {
|
||||
dateMillis += millis.toLong()
|
||||
}
|
||||
} catch (e: NumberFormatException) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return dateMillis
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package deckers.thibault.aves.utils
|
||||
|
||||
object MimeTypes {
|
||||
const val IMAGE = "image"
|
||||
const val DNG = "image/x-adobe-dng" // .dng
|
||||
const val GIF = "image/gif"
|
||||
const val HEIC = "image/heic"
|
||||
const val HEIF = "image/heif"
|
||||
const val JPEG = "image/jpeg"
|
||||
const val PNG = "image/png"
|
||||
const val PSD = "image/x-photoshop" // .psd
|
||||
const val SVG = "image/svg+xml" // .svg
|
||||
const val WEBP = "image/webp"
|
||||
const val VIDEO = "video"
|
||||
const val AVI = "video/avi"
|
||||
const val MP2T = "video/mp2t" // .m2ts
|
||||
const val MP4 = "video/mp4"
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package deckers.thibault.aves.utils
|
||||
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object Utils {
|
||||
private const val LOG_TAG_MAX_LENGTH = 23
|
||||
private val LOG_TAG_PACKAGE_PATTERN = Pattern.compile("(\\w)(\\w*)\\.")
|
||||
|
||||
@JvmStatic
|
||||
fun createLogTag(clazz: Class<*>): String {
|
||||
// shorten class name to "a.b.CccDdd"
|
||||
var logTag = LOG_TAG_PACKAGE_PATTERN.matcher(clazz.name).replaceAll("$1.")
|
||||
if (logTag.length > LOG_TAG_MAX_LENGTH) {
|
||||
// shorten class name to "a.b.CD"
|
||||
val simpleName = clazz.simpleName
|
||||
val shortSimpleName = simpleName.replace("[a-z]".toRegex(), "")
|
||||
logTag = logTag.replace(simpleName, shortSimpleName)
|
||||
if (logTag.length > LOG_TAG_MAX_LENGTH) {
|
||||
// shorten class name to "CD"
|
||||
logTag = shortSimpleName
|
||||
}
|
||||
}
|
||||
return logTag
|
||||
}
|
||||
}
|
|
@ -1,13 +1,15 @@
|
|||
buildscript {
|
||||
ext.kotlin_version = '1.4.10'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.6.3'
|
||||
classpath 'com.android.tools.build:gradle:3.6.3' // do not upgrade to 4+ until this is fixed: https://github.com/flutter/flutter/issues/58247
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'com.google.gms:google-services:4.3.3'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.2.0'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.3.0'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
32
ios/.gitignore
vendored
|
@ -1,32 +0,0 @@
|
|||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
*.perspectivev3
|
||||
**/*sync/
|
||||
.sconsign.dblite
|
||||
.tags*
|
||||
**/.vagrant/
|
||||
**/DerivedData/
|
||||
Icon?
|
||||
**/Pods/
|
||||
**/.symlinks/
|
||||
profile
|
||||
xcuserdata
|
||||
**/.generated/
|
||||
Flutter/App.framework
|
||||
Flutter/Flutter.framework
|
||||
Flutter/Flutter.podspec
|
||||
Flutter/Generated.xcconfig
|
||||
Flutter/app.flx
|
||||
Flutter/app.zip
|
||||
Flutter/flutter_assets/
|
||||
Flutter/flutter_export_environment.sh
|
||||
ServiceDefinitions.json
|
||||
Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
# Exceptions to above rules.
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
!default.pbxuser
|
||||
!default.perspectivev3
|
|
@ -1,26 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>App</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>io.flutter.flutter.app</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>App</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>8.0</string>
|
||||
</dict>
|
||||
</plist>
|
|
@ -1,2 +0,0 @@
|
|||
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
|
@ -1,2 +0,0 @@
|
|||
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
87
ios/Podfile
|
@ -1,87 +0,0 @@
|
|||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '9.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
'Profile' => :release,
|
||||
'Release' => :release,
|
||||
}
|
||||
|
||||
def parse_KV_file(file, separator='=')
|
||||
file_abs_path = File.expand_path(file)
|
||||
if !File.exists? file_abs_path
|
||||
return [];
|
||||
end
|
||||
generated_key_values = {}
|
||||
skip_line_start_symbols = ["#", "/"]
|
||||
File.foreach(file_abs_path) do |line|
|
||||
next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
|
||||
plugin = line.split(pattern=separator)
|
||||
if plugin.length == 2
|
||||
podname = plugin[0].strip()
|
||||
path = plugin[1].strip()
|
||||
podpath = File.expand_path("#{path}", file_abs_path)
|
||||
generated_key_values[podname] = podpath
|
||||
else
|
||||
puts "Invalid plugin specification: #{line}"
|
||||
end
|
||||
end
|
||||
generated_key_values
|
||||
end
|
||||
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
use_modular_headers!
|
||||
|
||||
# Flutter Pod
|
||||
|
||||
copied_flutter_dir = File.join(__dir__, 'Flutter')
|
||||
copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework')
|
||||
copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec')
|
||||
unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path)
|
||||
# Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet.
|
||||
# That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration.
|
||||
# CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist.
|
||||
|
||||
generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig')
|
||||
unless File.exist?(generated_xcode_build_settings_path)
|
||||
raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
||||
end
|
||||
generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path)
|
||||
cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR'];
|
||||
|
||||
unless File.exist?(copied_framework_path)
|
||||
FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir)
|
||||
end
|
||||
unless File.exist?(copied_podspec_path)
|
||||
FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir)
|
||||
end
|
||||
end
|
||||
|
||||
# Keep pod path relative so it can be checked into Podfile.lock.
|
||||
pod 'Flutter', :path => 'Flutter'
|
||||
|
||||
# Plugin Pods
|
||||
|
||||
# Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
|
||||
# referring to absolute paths on developers' machines.
|
||||
system('rm -rf .symlinks')
|
||||
system('mkdir -p .symlinks/plugins')
|
||||
plugin_pods = parse_KV_file('../.flutter-plugins')
|
||||
plugin_pods.each do |name, path|
|
||||
symlink = File.join('.symlinks', 'plugins', name)
|
||||
File.symlink(path, symlink)
|
||||
pod name, :path => File.join(symlink, 'ios')
|
||||
end
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['ENABLE_BITCODE'] = 'NO'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,506 +0,0 @@
|
|||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||
);
|
||||
name = Flutter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146E51CF9000F007C117D = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146EF1CF9000F007C117D /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
97C147021CF9000F007C117D /* Info.plist */,
|
||||
97C146F11CF9000F007C117D /* Supporting Files */,
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146F11CF9000F007C117D /* Supporting Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = "Supporting Files";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1020;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
LastSwiftMigration = 1100;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
97C146ED1CF9000F007C117D /* Runner */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C146FB1CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C147001CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.aves;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
97C147031CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147041CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
97C147061CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.aves;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147071CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.aves;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147031CF9000F007C117D /* Debug */,
|
||||
97C147041CF9000F007C117D /* Release */,
|
||||
249021D3217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147061CF9000F007C117D /* Debug */,
|
||||
97C147071CF9000F007C117D /* Release */,
|
||||
249021D4217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -1,91 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1020"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
7
ios/Runner.xcworkspace/contents.xcworkspacedata
generated
|
@ -1,7 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -1,13 +0,0 @@
|
|||
import UIKit
|
||||
import Flutter
|
||||
|
||||
@UIApplicationMain
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "Icon-App-1024x1024@1x.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 564 B |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 3.5 KiB |
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 68 B |
Before Width: | Height: | Size: 68 B |
Before Width: | Height: | Size: 68 B |
|
@ -1,5 +0,0 @@
|
|||
# Launch Screen Assets
|
||||
|
||||
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||
|
||||
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
|
@ -1,37 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="185"/>
|
||||
</resources>
|
||||
</document>
|
|
@ -1,26 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Flutter View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
|
@ -1,45 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>aves</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -1 +0,0 @@
|
|||
#import "GeneratedPluginRegistrant.h"
|
|
@ -1,4 +1,6 @@
|
|||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
@ -67,6 +69,10 @@ mixin AlbumMixin on SourceBase {
|
|||
if (emptyAlbums.isNotEmpty) {
|
||||
_folderPaths.removeAll(emptyAlbums);
|
||||
updateAlbums();
|
||||
|
||||
final pinnedFilters = settings.pinnedFilters;
|
||||
emptyAlbums.forEach((album) => pinnedFilters.remove(AlbumFilter(album, getUniqueAlbumName(album))));
|
||||
settings.pinnedFilters = pinnedFilters;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import 'package:aves/model/source/album.dart';
|
|||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/location.dart';
|
||||
import 'package:aves/model/source/tag.dart';
|
||||
import 'package:aves/services/image_file_service.dart';
|
||||
import 'package:event_bus/event_bus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
|
@ -69,7 +70,7 @@ class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin {
|
|||
eventBus.fire(EntryAddedEvent());
|
||||
}
|
||||
|
||||
void removeEntries(Iterable<ImageEntry> entries) {
|
||||
void removeEntries(List<ImageEntry> entries) {
|
||||
entries.forEach((entry) => entry.removeFromFavourites());
|
||||
_rawEntries.removeWhere(entries.contains);
|
||||
cleanEmptyAlbums(entries.map((entry) => entry.directory).toSet());
|
||||
|
@ -88,12 +89,15 @@ class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin {
|
|||
invalidateFilterEntryCounts();
|
||||
}
|
||||
|
||||
// `dateModifiedSecs` changes when moving entries to another directory,
|
||||
// but it does not change when renaming the containing directory
|
||||
Future<void> moveEntry(ImageEntry entry, Map newFields) async {
|
||||
final oldContentId = entry.contentId;
|
||||
final newContentId = newFields['contentId'] as int;
|
||||
entry.uri = newFields['uri'] as String;
|
||||
final newDateModifiedSecs = newFields['dateModifiedSecs'] as int;
|
||||
if (newDateModifiedSecs != null) entry.dateModifiedSecs = newDateModifiedSecs;
|
||||
entry.path = newFields['path'] as String;
|
||||
entry.dateModifiedSecs = newFields['dateModifiedSecs'] as int;
|
||||
entry.uri = newFields['uri'] as String;
|
||||
entry.contentId = newContentId;
|
||||
entry.catalogMetadata = entry.catalogMetadata?.copyWith(contentId: newContentId);
|
||||
entry.addressDetails = entry.addressDetails?.copyWith(contentId: newContentId);
|
||||
|
@ -105,20 +109,53 @@ class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin {
|
|||
}
|
||||
|
||||
void updateAfterMove({
|
||||
@required Iterable<ImageEntry> entries,
|
||||
@required Set<String> fromAlbums,
|
||||
@required String toAlbum,
|
||||
@required List<ImageEntry> selection,
|
||||
@required bool copy,
|
||||
}) {
|
||||
@required String destinationAlbum,
|
||||
@required Iterable<MoveOpEvent> movedOps,
|
||||
}) async {
|
||||
if (movedOps.isEmpty) return;
|
||||
|
||||
final fromAlbums = <String>{};
|
||||
final movedEntries = <ImageEntry>[];
|
||||
if (copy) {
|
||||
addAll(entries);
|
||||
movedOps.forEach((movedOp) {
|
||||
final sourceUri = movedOp.uri;
|
||||
final newFields = movedOp.newFields;
|
||||
final sourceEntry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null);
|
||||
fromAlbums.add(sourceEntry.directory);
|
||||
movedEntries.add(sourceEntry?.copyWith(
|
||||
uri: newFields['uri'] as String,
|
||||
path: newFields['path'] as String,
|
||||
contentId: newFields['contentId'] as int,
|
||||
dateModifiedSecs: newFields['dateModifiedSecs'] as int,
|
||||
));
|
||||
});
|
||||
await metadataDb.saveEntries(movedEntries);
|
||||
await metadataDb.saveMetadata(movedEntries.map((entry) => entry.catalogMetadata));
|
||||
await metadataDb.saveAddresses(movedEntries.map((entry) => entry.addressDetails));
|
||||
} else {
|
||||
await Future.forEach<MoveOpEvent>(movedOps, (movedOp) async {
|
||||
final sourceUri = movedOp.uri;
|
||||
final newFields = movedOp.newFields;
|
||||
final entry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null);
|
||||
if (entry != null) {
|
||||
fromAlbums.add(entry.directory);
|
||||
movedEntries.add(entry);
|
||||
await moveEntry(entry, newFields);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (copy) {
|
||||
addAll(movedEntries);
|
||||
} else {
|
||||
cleanEmptyAlbums(fromAlbums);
|
||||
addFolderPath({toAlbum});
|
||||
addFolderPath({destinationAlbum});
|
||||
}
|
||||
updateAlbums();
|
||||
invalidateFilterEntryCounts();
|
||||
eventBus.fire(EntryMovedEvent(entries));
|
||||
eventBus.fire(EntryMovedEvent(movedEntries));
|
||||
}
|
||||
|
||||
int count(CollectionFilter filter) {
|
||||
|
|
|
@ -18,6 +18,27 @@ class AndroidFileService {
|
|||
return [];
|
||||
}
|
||||
|
||||
static Future<List<String>> getGrantedDirectories() async {
|
||||
try {
|
||||
final result = await platform.invokeMethod('getGrantedDirectories');
|
||||
return (result as List).cast<String>();
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getGrantedDirectories failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
static Future<void> revokeDirectoryAccess(String path) async {
|
||||
try {
|
||||
await platform.invokeMethod('revokeDirectoryAccess', <String, dynamic>{
|
||||
'path': path,
|
||||
});
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('revokeDirectoryAccess failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// returns a list of directories,
|
||||
// each directory is a map with "volumePath", "volumeDescription", "relativeDir"
|
||||
static Future<List<Map>> getInaccessibleDirectories(Iterable<String> dirPaths) async {
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/services/image_file_service.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
|
@ -22,10 +26,16 @@ class AppShortcutService {
|
|||
return false;
|
||||
}
|
||||
|
||||
static Future<void> pin(String label, Set<CollectionFilter> filters) async {
|
||||
static Future<void> pin(String label, ImageEntry iconEntry, Set<CollectionFilter> filters) async {
|
||||
Uint8List iconBytes;
|
||||
if (iconEntry != null) {
|
||||
final size = iconEntry.isVideo ? 0.0 : 256.0;
|
||||
iconBytes = await ImageFileService.getThumbnail(iconEntry, size, size);
|
||||
}
|
||||
try {
|
||||
await platform.invokeMethod('pin', <String, dynamic>{
|
||||
'label': label,
|
||||
'iconBytes': iconBytes,
|
||||
'filters': filters.map((filter) => filter.toJson()).toList(),
|
||||
});
|
||||
} on PlatformException catch (e) {
|
||||
|
|
|
@ -194,19 +194,6 @@ 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
|
||||
|
|
|
@ -89,6 +89,18 @@ class MetadataService {
|
|||
return {};
|
||||
}
|
||||
|
||||
static Future<List<Uint8List>> getEmbeddedPictures(String uri) async {
|
||||
try {
|
||||
final result = await platform.invokeMethod('getEmbeddedPictures', <String, dynamic>{
|
||||
'uri': uri,
|
||||
});
|
||||
return (result as List).cast<Uint8List>();
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getEmbeddedPictures failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
static Future<List<Uint8List>> getExifThumbnails(String uri) async {
|
||||
try {
|
||||
final result = await platform.invokeMethod('getExifThumbnails', <String, dynamic>{
|
||||
|
|
|
@ -200,33 +200,33 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
PopupMenuItem(
|
||||
key: Key('menu-sort'),
|
||||
value: CollectionAction.sort,
|
||||
child: MenuRow(text: 'Sort...', icon: AIcons.sort),
|
||||
child: MenuRow(text: 'Sort…', icon: AIcons.sort),
|
||||
),
|
||||
if (collection.sortFactor == EntrySortFactor.date)
|
||||
PopupMenuItem(
|
||||
key: Key('menu-group'),
|
||||
value: CollectionAction.group,
|
||||
child: MenuRow(text: 'Group...', icon: AIcons.group),
|
||||
child: MenuRow(text: 'Group…', icon: AIcons.group),
|
||||
),
|
||||
if (collection.isBrowsing) ...[
|
||||
if (kDebugMode)
|
||||
PopupMenuItem(
|
||||
value: CollectionAction.refresh,
|
||||
child: MenuRow(text: 'Refresh', icon: AIcons.refresh),
|
||||
),
|
||||
if (AvesApp.mode == AppMode.main)
|
||||
if (kDebugMode)
|
||||
PopupMenuItem(
|
||||
value: CollectionAction.refresh,
|
||||
child: MenuRow(text: 'Refresh', icon: AIcons.refresh),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: CollectionAction.select,
|
||||
child: MenuRow(text: 'Select', icon: AIcons.select),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: CollectionAction.select,
|
||||
child: MenuRow(text: 'Select', icon: AIcons.select),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: CollectionAction.stats,
|
||||
child: MenuRow(text: 'Stats', icon: AIcons.stats),
|
||||
),
|
||||
if (canAddShortcuts)
|
||||
if (AvesApp.mode == AppMode.main && canAddShortcuts)
|
||||
PopupMenuItem(
|
||||
value: CollectionAction.addShortcut,
|
||||
child: MenuRow(text: 'Add shortcut', icon: AIcons.addShortcut),
|
||||
child: MenuRow(text: 'Add shortcut…', icon: AIcons.addShortcut),
|
||||
),
|
||||
],
|
||||
if (collection.isSelecting) ...[
|
||||
|
@ -356,7 +356,8 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
);
|
||||
if (name == null || name.isEmpty) return;
|
||||
|
||||
unawaited(AppShortcutService.pin(name, collection.filters));
|
||||
final iconEntry = collection.sortedEntries.isNotEmpty ? collection.sortedEntries.first : null;
|
||||
unawaited(AppShortcutService.pin(name, iconEntry, collection.filters));
|
||||
}
|
||||
|
||||
void _goToSearch() {
|
||||
|
|
|
@ -87,8 +87,7 @@ class SectionHeader extends StatelessWidget {
|
|||
// force a higher first line to match leading icon/selector dimension
|
||||
style: TextStyle(height: 2.3 * textScaleFactor),
|
||||
), // 23 hair spaces match a width of 40.0
|
||||
if (hasTrailing)
|
||||
TextSpan(text: '\u200A' * 17),
|
||||
if (hasTrailing) TextSpan(text: '\u200A' * 17),
|
||||
TextSpan(
|
||||
text: text,
|
||||
style: Constants.titleTextStyle,
|
||||
|
|
|
@ -219,7 +219,6 @@ class _CollectionScrollViewState extends State<CollectionScrollView> {
|
|||
text: 'No favourites',
|
||||
);
|
||||
}
|
||||
debugPrint('collection.filters=${collection.filters}');
|
||||
if (collection.filters.any((filter) => filter is MimeFilter && filter.mime == MimeTypes.anyVideo)) {
|
||||
return EmptyContent(
|
||||
icon: AIcons.video,
|
||||
|
|
|
@ -43,7 +43,7 @@ class _AddShortcutDialogState extends State<AddShortcutDialog> {
|
|||
labelText: 'Shortcut label',
|
||||
),
|
||||
autofocus: true,
|
||||
maxLength: 10,
|
||||
maxLength: 25,
|
||||
onChanged: (_) => _validate(),
|
||||
onSubmitted: (_) => _submit(context),
|
||||
),
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/services/image_file_service.dart';
|
||||
import 'package:aves/utils/durations.dart';
|
||||
import 'package:flushbar/flushbar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:percent_indicator/circular_percent_indicator.dart';
|
||||
|
||||
mixin FeedbackMixin {
|
||||
Flushbar _flushbar;
|
||||
|
@ -20,4 +24,65 @@ mixin FeedbackMixin {
|
|||
animationDuration: Durations.opToastAnimation,
|
||||
)..show(context);
|
||||
}
|
||||
|
||||
// report overlay for multiple operations
|
||||
|
||||
OverlayEntry _opReportOverlayEntry;
|
||||
|
||||
void showOpReport<T extends ImageOpEvent>({
|
||||
@required BuildContext context,
|
||||
@required List<ImageEntry> selection,
|
||||
@required Stream<T> opStream,
|
||||
@required void Function(Set<T> processed) onDone,
|
||||
}) {
|
||||
final processed = <T>{};
|
||||
|
||||
// do not handle completion inside `StreamBuilder`
|
||||
// as it could be called multiple times
|
||||
Future<void> onComplete() => _hideOpReportOverlay().then((_) => onDone(processed));
|
||||
opStream.listen(
|
||||
processed.add,
|
||||
onError: (error) {
|
||||
debugPrint('_showOpReport error=$error');
|
||||
onComplete();
|
||||
},
|
||||
onDone: onComplete,
|
||||
);
|
||||
|
||||
_opReportOverlayEntry = OverlayEntry(
|
||||
builder: (context) {
|
||||
return AbsorbPointer(
|
||||
child: StreamBuilder<T>(
|
||||
stream: opStream,
|
||||
builder: (context, snapshot) {
|
||||
Widget child = SizedBox.shrink();
|
||||
if (!snapshot.hasError && snapshot.connectionState == ConnectionState.active) {
|
||||
final percent = processed.length.toDouble() / selection.length;
|
||||
child = CircularPercentIndicator(
|
||||
percent: percent,
|
||||
lineWidth: 16,
|
||||
radius: 160,
|
||||
backgroundColor: Colors.white24,
|
||||
progressColor: Theme.of(context).accentColor,
|
||||
animation: true,
|
||||
center: Text(NumberFormat.percentPattern().format(percent)),
|
||||
animateFromLastPercent: true,
|
||||
);
|
||||
}
|
||||
return AnimatedSwitcher(
|
||||
duration: Durations.collectionOpOverlayAnimation,
|
||||
child: child,
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
Overlay.of(context).insert(_opReportOverlayEntry);
|
||||
}
|
||||
|
||||
Future<void> _hideOpReportOverlay() async {
|
||||
await Future.delayed(Durations.collectionOpOverlayAnimation * timeDilation);
|
||||
_opReportOverlayEntry.remove();
|
||||
_opReportOverlayEntry = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,7 +74,6 @@ class _RenameEntryDialogState extends State<RenameEntryDialog> {
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/metadata_db.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/services/android_app_service.dart';
|
||||
import 'package:aves/services/image_file_service.dart';
|
||||
import 'package:aves/utils/durations.dart';
|
||||
import 'package:aves/widgets/collection/collection_actions.dart';
|
||||
import 'package:aves/widgets/collection/empty.dart';
|
||||
import 'package:aves/widgets/common/action_delegates/create_album_dialog.dart';
|
||||
|
@ -20,10 +17,8 @@ import 'package:aves/widgets/filter_grids/albums_page.dart';
|
|||
import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:percent_indicator/circular_percent_indicator.dart';
|
||||
|
||||
class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
||||
final CollectionLens collection;
|
||||
|
@ -61,7 +56,7 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
|||
}
|
||||
}
|
||||
|
||||
Future _moveSelection(BuildContext context, {@required bool copy}) async {
|
||||
Future<void> _moveSelection(BuildContext context, {@required bool copy}) async {
|
||||
final source = collection.source;
|
||||
final destinationAlbum = await Navigator.push(
|
||||
context,
|
||||
|
@ -106,12 +101,11 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
|||
final selection = collection.selection.toList();
|
||||
if (!await checkStoragePermission(context, selection)) return;
|
||||
|
||||
_showOpReport<MoveOpEvent>(
|
||||
showOpReport<MoveOpEvent>(
|
||||
context: context,
|
||||
selection: selection,
|
||||
opStream: ImageFileService.move(selection, copy: copy, destinationAlbum: destinationAlbum),
|
||||
onDone: (processed) async {
|
||||
debugPrint('$runtimeType _moveSelection onDone');
|
||||
final movedOps = processed.where((e) => e.success);
|
||||
final movedCount = movedOps.length;
|
||||
final selectionCount = selection.length;
|
||||
|
@ -122,51 +116,19 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
|||
final count = movedCount;
|
||||
showFeedback(context, '${copy ? 'Copied' : 'Moved'} ${Intl.plural(count, one: '$count item', other: '$count items')}');
|
||||
}
|
||||
if (movedCount > 0) {
|
||||
final fromAlbums = <String>{};
|
||||
final movedEntries = <ImageEntry>[];
|
||||
if (copy) {
|
||||
movedOps.forEach((movedOp) {
|
||||
final sourceUri = movedOp.uri;
|
||||
final newFields = movedOp.newFields;
|
||||
final sourceEntry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null);
|
||||
fromAlbums.add(sourceEntry.directory);
|
||||
movedEntries.add(sourceEntry?.copyWith(
|
||||
uri: newFields['uri'] as String,
|
||||
path: newFields['path'] as String,
|
||||
contentId: newFields['contentId'] as int,
|
||||
dateModifiedSecs: newFields['dateModifiedSecs'] as int,
|
||||
));
|
||||
});
|
||||
await metadataDb.saveEntries(movedEntries);
|
||||
await metadataDb.saveMetadata(movedEntries.map((entry) => entry.catalogMetadata));
|
||||
await metadataDb.saveAddresses(movedEntries.map((entry) => entry.addressDetails));
|
||||
} else {
|
||||
await Future.forEach<MoveOpEvent>(movedOps, (movedOp) async {
|
||||
final sourceUri = movedOp.uri;
|
||||
final newFields = movedOp.newFields;
|
||||
final entry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null);
|
||||
if (entry != null) {
|
||||
fromAlbums.add(entry.directory);
|
||||
movedEntries.add(entry);
|
||||
await source.moveEntry(entry, newFields);
|
||||
}
|
||||
});
|
||||
}
|
||||
source.updateAfterMove(
|
||||
entries: movedEntries,
|
||||
fromAlbums: fromAlbums,
|
||||
toAlbum: destinationAlbum,
|
||||
copy: copy,
|
||||
);
|
||||
}
|
||||
await source.updateAfterMove(
|
||||
selection: selection,
|
||||
copy: copy,
|
||||
destinationAlbum: destinationAlbum,
|
||||
movedOps: movedOps,
|
||||
);
|
||||
collection.clearSelection();
|
||||
collection.browse();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _refreshSelectionMetadata() async {
|
||||
Future<void> _refreshSelectionMetadata() async {
|
||||
collection.selection.forEach((entry) => entry.clearMetadata());
|
||||
final source = collection.source;
|
||||
source.stateNotifier.value = SourceState.cataloguing;
|
||||
|
@ -176,7 +138,7 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
|||
source.stateNotifier.value = SourceState.ready;
|
||||
}
|
||||
|
||||
void _showDeleteDialog(BuildContext context) async {
|
||||
Future<void> _showDeleteDialog(BuildContext context) async {
|
||||
final selection = collection.selection.toList();
|
||||
final count = selection.length;
|
||||
|
||||
|
@ -202,12 +164,12 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
|||
|
||||
if (!await checkStoragePermission(context, selection)) return;
|
||||
|
||||
_showOpReport<ImageOpEvent>(
|
||||
showOpReport<ImageOpEvent>(
|
||||
context: context,
|
||||
selection: selection,
|
||||
opStream: ImageFileService.delete(selection),
|
||||
onDone: (processed) {
|
||||
final deletedUris = processed.where((e) => e.success).map((e) => e.uri);
|
||||
final deletedUris = processed.where((e) => e.success).map((e) => e.uri).toList();
|
||||
final deletedCount = deletedUris.length;
|
||||
final selectionCount = selection.length;
|
||||
if (deletedCount < selectionCount) {
|
||||
|
@ -215,72 +177,11 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
|||
showFeedback(context, 'Failed to delete ${Intl.plural(count, one: '$count item', other: '$count items')}');
|
||||
}
|
||||
if (deletedCount > 0) {
|
||||
collection.source.removeEntries(selection.where((e) => deletedUris.contains(e.uri)));
|
||||
collection.source.removeEntries(selection.where((e) => deletedUris.contains(e.uri)).toList());
|
||||
}
|
||||
collection.clearSelection();
|
||||
collection.browse();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// selection action report overlay
|
||||
|
||||
OverlayEntry _opReportOverlayEntry;
|
||||
|
||||
void _showOpReport<T extends ImageOpEvent>({
|
||||
@required BuildContext context,
|
||||
@required List<ImageEntry> selection,
|
||||
@required Stream<T> opStream,
|
||||
@required void Function(Set<T> processed) onDone,
|
||||
}) {
|
||||
final processed = <T>{};
|
||||
|
||||
// do not handle completion inside `StreamBuilder`
|
||||
// as it could be called multiple times
|
||||
Future<void> onComplete() => _hideOpReportOverlay().then((_) => onDone(processed));
|
||||
opStream.listen(
|
||||
processed.add,
|
||||
onError: (error) {
|
||||
debugPrint('_showOpReport error=$error');
|
||||
onComplete();
|
||||
},
|
||||
onDone: onComplete,
|
||||
);
|
||||
|
||||
_opReportOverlayEntry = OverlayEntry(
|
||||
builder: (context) {
|
||||
return AbsorbPointer(
|
||||
child: StreamBuilder<T>(
|
||||
stream: opStream,
|
||||
builder: (context, snapshot) {
|
||||
Widget child = SizedBox.shrink();
|
||||
if (!snapshot.hasError && snapshot.connectionState == ConnectionState.active) {
|
||||
final percent = processed.length.toDouble() / selection.length;
|
||||
child = CircularPercentIndicator(
|
||||
percent: percent,
|
||||
lineWidth: 16,
|
||||
radius: 160,
|
||||
backgroundColor: Colors.white24,
|
||||
progressColor: Theme.of(context).accentColor,
|
||||
animation: true,
|
||||
center: Text(NumberFormat.percentPattern().format(percent)),
|
||||
animateFromLastPercent: true,
|
||||
);
|
||||
}
|
||||
return AnimatedSwitcher(
|
||||
duration: Durations.collectionOpOverlayAnimation,
|
||||
child: child,
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
Overlay.of(context).insert(_opReportOverlayEntry);
|
||||
}
|
||||
|
||||
Future<void> _hideOpReportOverlay() async {
|
||||
await Future.delayed(Durations.collectionOpOverlayAnimation * timeDilation);
|
||||
_opReportOverlayEntry.remove();
|
||||
_opReportOverlayEntry = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -146,7 +146,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
|||
child: Stack(
|
||||
fit: StackFit.passthrough,
|
||||
children: [
|
||||
if (widget.background != null)
|
||||
if (hasBackground)
|
||||
ClipRRect(
|
||||
borderRadius: borderRadius,
|
||||
child: widget.background,
|
||||
|
|
|
@ -66,6 +66,7 @@ class ThumbnailProviderKey {
|
|||
final ImageEntry entry;
|
||||
final double extent;
|
||||
final double scale;
|
||||
|
||||
// do not access `contentId` via `entry` for hashCode and equality purposes
|
||||
// as an entry is not constant and its contentId can change
|
||||
final int contentId;
|
||||
|
|
|
@ -299,6 +299,7 @@ class DebugPageState extends State<DebugPage> {
|
|||
'collectionSortFactor': '${settings.collectionSortFactor}',
|
||||
'collectionTileExtent': '${settings.collectionTileExtent}',
|
||||
'infoMapZoom': '${settings.infoMapZoom}',
|
||||
'pinnedFilters': '${settings.pinnedFilters}',
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
|
|
@ -36,11 +36,12 @@ class AlbumListPage extends StatelessWidget {
|
|||
builder: (context, snapshot) => FilterNavigationPage(
|
||||
source: source,
|
||||
title: 'Albums',
|
||||
chipSetActionDelegate: AlbumChipSetActionDelegate(),
|
||||
chipSetActionDelegate: AlbumChipSetActionDelegate(source: source),
|
||||
chipActionDelegate: AlbumChipActionDelegate(source: source),
|
||||
chipActionsBuilder: (filter) => [
|
||||
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
|
||||
ChipAction.rename,
|
||||
ChipAction.delete,
|
||||
],
|
||||
filterEntries: getAlbumEntries(source),
|
||||
filterBuilder: (album) => AlbumFilter(album, source.getUniqueAlbumName(album)),
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/services/image_file_service.dart';
|
||||
|
@ -8,8 +7,8 @@ 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/common/aves_dialog.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/chip_actions.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
@ -45,6 +44,9 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per
|
|||
Future<void> onActionSelected(BuildContext context, CollectionFilter filter, ChipAction action) async {
|
||||
await super.onActionSelected(context, filter, action);
|
||||
switch (action) {
|
||||
case ChipAction.delete:
|
||||
unawaited(_showDeleteDialog(context, filter as AlbumFilter));
|
||||
break;
|
||||
case ChipAction.rename:
|
||||
unawaited(_showRenameDialog(context, filter as AlbumFilter));
|
||||
break;
|
||||
|
@ -53,6 +55,51 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _showDeleteDialog(BuildContext context, AlbumFilter filter) async {
|
||||
final selection = source.rawEntries.where(filter.filter).toList();
|
||||
final count = selection.length;
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AvesDialog(
|
||||
content: Text('Are you sure you want to delete this album and its ${Intl.plural(count, one: 'item', other: '$count items')}?'),
|
||||
actions: [
|
||||
FlatButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text('Cancel'.toUpperCase()),
|
||||
),
|
||||
FlatButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: Text('Delete'.toUpperCase()),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
if (confirmed == null || !confirmed) return;
|
||||
|
||||
if (!await checkStoragePermission(context, selection)) return;
|
||||
|
||||
showOpReport<ImageOpEvent>(
|
||||
context: context,
|
||||
selection: selection,
|
||||
opStream: ImageFileService.delete(selection),
|
||||
onDone: (processed) {
|
||||
final deletedUris = processed.where((e) => e.success).map((e) => e.uri).toList();
|
||||
final deletedCount = deletedUris.length;
|
||||
final selectionCount = selection.length;
|
||||
if (deletedCount < selectionCount) {
|
||||
final count = selectionCount - deletedCount;
|
||||
showFeedback(context, 'Failed to delete ${Intl.plural(count, one: '$count item', other: '$count items')}');
|
||||
}
|
||||
if (deletedCount > 0) {
|
||||
source.removeEntries(selection.where((e) => deletedUris.contains(e.uri)).toList());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showRenameDialog(BuildContext context, AlbumFilter filter) async {
|
||||
final album = filter.album;
|
||||
final newName = await showDialog<String>(
|
||||
|
@ -63,36 +110,36 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per
|
|||
|
||||
if (!await checkStoragePermissionForAlbums(context, {album})) return;
|
||||
|
||||
final result = await ImageFileService.renameDirectory(album, newName);
|
||||
final bySuccess = groupBy<Map, bool>(result, (fields) => fields['success']);
|
||||
final selection = source.rawEntries.where(filter.filter).toList();
|
||||
final destinationAlbum = path.join(path.dirname(album), newName);
|
||||
|
||||
final albumEntries = source.rawEntries.where(filter.filter);
|
||||
final movedEntries = <ImageEntry>[];
|
||||
await Future.forEach<Map>(bySuccess[true], (newFields) async {
|
||||
final oldContentId = newFields['oldContentId'];
|
||||
final entry = albumEntries.firstWhere((entry) => entry.contentId == oldContentId, orElse: () => null);
|
||||
if (entry != null) {
|
||||
movedEntries.add(entry);
|
||||
await source.moveEntry(entry, newFields);
|
||||
}
|
||||
});
|
||||
final newAlbum = path.join(path.dirname(album), newName);
|
||||
source.updateAfterMove(
|
||||
entries: movedEntries,
|
||||
fromAlbums: {album},
|
||||
toAlbum: newAlbum,
|
||||
copy: false,
|
||||
showOpReport<MoveOpEvent>(
|
||||
context: context,
|
||||
selection: selection,
|
||||
opStream: ImageFileService.move(selection, copy: false, destinationAlbum: destinationAlbum),
|
||||
onDone: (processed) async {
|
||||
final movedOps = processed.where((e) => e.success);
|
||||
final movedCount = movedOps.length;
|
||||
final selectionCount = selection.length;
|
||||
if (movedCount < selectionCount) {
|
||||
final count = selectionCount - movedCount;
|
||||
showFeedback(context, 'Failed to move ${Intl.plural(count, one: '$count item', other: '$count items')}');
|
||||
} else {
|
||||
showFeedback(context, 'Done!');
|
||||
}
|
||||
final pinned = settings.pinnedFilters.contains(filter);
|
||||
await source.updateAfterMove(
|
||||
selection: selection,
|
||||
copy: false,
|
||||
destinationAlbum: destinationAlbum,
|
||||
movedOps: movedOps,
|
||||
);
|
||||
// repin new album after obsolete album got removed and unpinned
|
||||
if (pinned) {
|
||||
final newFilter = AlbumFilter(destinationAlbum, source.getUniqueAlbumName(destinationAlbum));
|
||||
settings.pinnedFilters = settings.pinnedFilters..add(newFilter);
|
||||
}
|
||||
},
|
||||
);
|
||||
final newFilter = AlbumFilter(newAlbum, source.getUniqueAlbumName(newAlbum));
|
||||
settings.pinnedFilters = settings.pinnedFilters
|
||||
..remove(filter)
|
||||
..add(newFilter);
|
||||
|
||||
final failed = bySuccess[false]?.length ?? 0;
|
||||
if (failed > 0) {
|
||||
showFeedback(context, 'Failed to move ${Intl.plural(failed, one: '$failed item', other: '$failed items')}');
|
||||
} else {
|
||||
showFeedback(context, 'Done!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,12 @@ import 'package:flutter/widgets.dart';
|
|||
|
||||
enum ChipSetAction {
|
||||
sort,
|
||||
refresh,
|
||||
stats,
|
||||
}
|
||||
|
||||
enum ChipAction {
|
||||
delete,
|
||||
pin,
|
||||
unpin,
|
||||
rename,
|
||||
|
@ -14,6 +17,8 @@ enum ChipAction {
|
|||
extension ExtraChipAction on ChipAction {
|
||||
String getText() {
|
||||
switch (this) {
|
||||
case ChipAction.delete:
|
||||
return 'Delete';
|
||||
case ChipAction.pin:
|
||||
return 'Pin to top';
|
||||
case ChipAction.unpin:
|
||||
|
@ -26,6 +31,8 @@ extension ExtraChipAction on ChipAction {
|
|||
|
||||
IconData getIcon() {
|
||||
switch (this) {
|
||||
case ChipAction.delete:
|
||||
return AIcons.delete;
|
||||
case ChipAction.pin:
|
||||
case ChipAction.unpin:
|
||||
return AIcons.pin;
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/utils/durations.dart';
|
||||
import 'package:aves/widgets/common/aves_selection_dialog.dart';
|
||||
import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/chip_actions.dart';
|
||||
import 'package:aves/widgets/stats/stats.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
|
||||
abstract class ChipSetActionDelegate {
|
||||
CollectionSource get source;
|
||||
|
||||
ChipSortFactor get sortFactor;
|
||||
|
||||
set sortFactor(ChipSortFactor factor);
|
||||
|
@ -19,6 +26,15 @@ abstract class ChipSetActionDelegate {
|
|||
case ChipSetAction.sort:
|
||||
await _showSortDialog(context);
|
||||
break;
|
||||
case ChipSetAction.refresh:
|
||||
if (source is MediaStoreSource) {
|
||||
source.clearEntries();
|
||||
unawaited((source as MediaStoreSource).refresh());
|
||||
}
|
||||
break;
|
||||
case ChipSetAction.stats:
|
||||
_goToStats(context);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,9 +54,32 @@ abstract class ChipSetActionDelegate {
|
|||
sortFactor = factor;
|
||||
}
|
||||
}
|
||||
|
||||
void _goToStats(BuildContext context) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: RouteSettings(name: StatsPage.routeName),
|
||||
builder: (context) => StatsPage(
|
||||
collection: CollectionLens(
|
||||
source: source,
|
||||
groupFactor: settings.collectionGroupFactor,
|
||||
sortFactor: settings.collectionSortFactor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumChipSetActionDelegate extends ChipSetActionDelegate {
|
||||
@override
|
||||
final CollectionSource source;
|
||||
|
||||
AlbumChipSetActionDelegate({
|
||||
@required this.source,
|
||||
});
|
||||
|
||||
@override
|
||||
ChipSortFactor get sortFactor => settings.albumSortFactor;
|
||||
|
||||
|
@ -49,6 +88,13 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate {
|
|||
}
|
||||
|
||||
class CountryChipSetActionDelegate extends ChipSetActionDelegate {
|
||||
@override
|
||||
final CollectionSource source;
|
||||
|
||||
CountryChipSetActionDelegate({
|
||||
@required this.source,
|
||||
});
|
||||
|
||||
@override
|
||||
ChipSortFactor get sortFactor => settings.countrySortFactor;
|
||||
|
||||
|
@ -57,6 +103,13 @@ class CountryChipSetActionDelegate extends ChipSetActionDelegate {
|
|||
}
|
||||
|
||||
class TagChipSetActionDelegate extends ChipSetActionDelegate {
|
||||
@override
|
||||
final CollectionSource source;
|
||||
|
||||
TagChipSetActionDelegate({
|
||||
@required this.source,
|
||||
});
|
||||
|
||||
@override
|
||||
ChipSortFactor get sortFactor => settings.tagSortFactor;
|
||||
|
||||
|
|
|
@ -34,18 +34,17 @@ class DecoratedFilterChip extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget backgroundImage;
|
||||
if (entry != null) {
|
||||
backgroundImage = entry.isSvg
|
||||
? ThumbnailVectorImage(
|
||||
entry: entry,
|
||||
extent: FilterGridPage.maxCrossAxisExtent,
|
||||
)
|
||||
: ThumbnailRasterImage(
|
||||
entry: entry,
|
||||
extent: FilterGridPage.maxCrossAxisExtent,
|
||||
);
|
||||
}
|
||||
final backgroundImage = entry == null
|
||||
? Container(color: Colors.white)
|
||||
: entry.isSvg
|
||||
? ThumbnailVectorImage(
|
||||
entry: entry,
|
||||
extent: FilterGridPage.maxCrossAxisExtent,
|
||||
)
|
||||
: ThumbnailRasterImage(
|
||||
entry: entry,
|
||||
extent: FilterGridPage.maxCrossAxisExtent,
|
||||
);
|
||||
return AvesFilterChip(
|
||||
filter: filter,
|
||||
showGenericIcon: false,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/main.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
|
@ -24,6 +25,7 @@ import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart';
|
|||
import 'package:aves/widgets/filter_grids/common/decorated_filter_chip.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
|
@ -87,7 +89,7 @@ class FilterNavigationPage extends StatelessWidget {
|
|||
),
|
||||
settings.navRemoveRoutePredicate(CollectionPage.routeName),
|
||||
),
|
||||
onLongPress: (filter, tapPosition) => _showMenu(context, filter, tapPosition),
|
||||
onLongPress: AvesApp.mode == AppMode.main ? (filter, tapPosition) => _showMenu(context, filter, tapPosition) : null,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -119,7 +121,16 @@ class FilterNavigationPage extends StatelessWidget {
|
|||
PopupMenuItem(
|
||||
key: Key('menu-sort'),
|
||||
value: ChipSetAction.sort,
|
||||
child: MenuRow(text: 'Sort...', icon: AIcons.sort),
|
||||
child: MenuRow(text: 'Sort…', icon: AIcons.sort),
|
||||
),
|
||||
if (kDebugMode)
|
||||
PopupMenuItem(
|
||||
value: ChipSetAction.refresh,
|
||||
child: MenuRow(text: 'Refresh', icon: AIcons.refresh),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: ChipSetAction.stats,
|
||||
child: MenuRow(text: 'Stats', icon: AIcons.stats),
|
||||
),
|
||||
];
|
||||
},
|
||||
|
|
|
@ -33,7 +33,7 @@ class CountryListPage extends StatelessWidget {
|
|||
builder: (context, snapshot) => FilterNavigationPage(
|
||||
source: source,
|
||||
title: 'Countries',
|
||||
chipSetActionDelegate: CountryChipSetActionDelegate(),
|
||||
chipSetActionDelegate: CountryChipSetActionDelegate(source: source),
|
||||
chipActionDelegate: ChipActionDelegate(),
|
||||
chipActionsBuilder: (filter) => [
|
||||
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
|
||||
|
|
|
@ -33,7 +33,7 @@ class TagListPage extends StatelessWidget {
|
|||
builder: (context, snapshot) => FilterNavigationPage(
|
||||
source: source,
|
||||
title: 'Tags',
|
||||
chipSetActionDelegate: TagChipSetActionDelegate(),
|
||||
chipSetActionDelegate: TagChipSetActionDelegate(source: source),
|
||||
chipActionDelegate: ChipActionDelegate(),
|
||||
chipActionsBuilder: (filter) => [
|
||||
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
|
||||
|
|
|
@ -5,8 +5,12 @@ import 'package:aves/model/image_entry.dart';
|
|||
import 'package:aves/model/image_metadata.dart';
|
||||
import 'package:aves/model/metadata_db.dart';
|
||||
import 'package:aves/services/metadata_service.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart';
|
||||
import 'package:aves/widgets/common/image_providers/uri_picture_provider.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/info_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class FullscreenDebugPage extends StatefulWidget {
|
||||
|
@ -41,7 +45,8 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
|
|||
final tabs = <Tuple2<Tab, Widget>>[
|
||||
Tuple2(Tab(text: 'Entry'), _buildEntryTabView()),
|
||||
Tuple2(Tab(text: 'DB'), _buildDbTabView()),
|
||||
Tuple2(Tab(text: 'Content Resolver'), _buildContentResolverTabView()),
|
||||
Tuple2(Tab(icon: Icon(AIcons.android)), _buildContentResolverTabView()),
|
||||
Tuple2(Tab(icon: Icon(AIcons.image)), _buildThumbnailsTabView()),
|
||||
];
|
||||
return DefaultTabController(
|
||||
length: tabs.length,
|
||||
|
@ -132,6 +137,33 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildThumbnailsTabView() {
|
||||
const extent = 128.0;
|
||||
return ListView(
|
||||
padding: EdgeInsets.all(16),
|
||||
children: [
|
||||
if (entry.isSvg) ...[
|
||||
Text('SVG ($extent)'),
|
||||
SvgPicture(
|
||||
UriPicture(
|
||||
uri: entry.uri,
|
||||
mimeType: entry.mimeType,
|
||||
),
|
||||
width: extent,
|
||||
height: extent,
|
||||
)
|
||||
],
|
||||
if (!entry.isSvg) ...[
|
||||
Text('Raster (fast)'),
|
||||
Center(child: Image(image: ThumbnailProvider(entry: entry))),
|
||||
SizedBox(height: 16),
|
||||
Text('Raster ($extent)'),
|
||||
Center(child: Image(image: ThumbnailProvider(entry: entry, extent: extent))),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDbTabView() {
|
||||
final catalog = entry.catalogMetadata;
|
||||
return ListView(
|
||||
|
@ -230,6 +262,7 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
|
|||
return ListView(
|
||||
padding: EdgeInsets.all(16),
|
||||
children: [
|
||||
Text('Content Resolver (Media Store):'),
|
||||
FutureBuilder<Map>(
|
||||
future: _contentResolverMetadataLoader,
|
||||
builder: (context, snapshot) {
|
||||
|
|
|
@ -9,6 +9,7 @@ import 'package:aves/widgets/fullscreen/info/info_page.dart';
|
|||
import 'package:aves/widgets/fullscreen/info/maps/common.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/maps/google_map.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/maps/leaflet_map.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/maps/marker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LocationSection extends StatefulWidget {
|
||||
|
@ -34,6 +35,9 @@ class LocationSection extends StatefulWidget {
|
|||
class _LocationSectionState extends State<LocationSection> {
|
||||
String _loadedUri;
|
||||
|
||||
static const extent = 48.0;
|
||||
static const pointerSize = Size(8.0, 6.0);
|
||||
|
||||
CollectionLens get collection => widget.collection;
|
||||
|
||||
ImageEntry get entry => widget.entry;
|
||||
|
@ -85,6 +89,14 @@ class _LocationSectionState extends State<LocationSection> {
|
|||
if (place != null && place.isNotEmpty) filters.add(LocationFilter(LocationLevel.place, place));
|
||||
}
|
||||
|
||||
Widget buildMarker(BuildContext context) {
|
||||
return ImageMarker(
|
||||
entry: entry,
|
||||
extent: extent,
|
||||
pointerSize: pointerSize,
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
@ -96,16 +108,19 @@ class _LocationSectionState extends State<LocationSection> {
|
|||
},
|
||||
child: settings.infoMapStyle.isGoogleMaps
|
||||
? EntryGoogleMap(
|
||||
markerId: entry.uri ?? entry.path,
|
||||
latLng: entry.latLng,
|
||||
geoUri: entry.geoUri,
|
||||
initialZoom: settings.infoMapZoom,
|
||||
markerId: entry.uri ?? entry.path,
|
||||
markerBuilder: buildMarker,
|
||||
)
|
||||
: EntryLeafletMap(
|
||||
latLng: entry.latLng,
|
||||
geoUri: entry.geoUri,
|
||||
initialZoom: settings.infoMapZoom,
|
||||
style: settings.infoMapStyle,
|
||||
markerSize: Size(extent, extent + pointerSize.height),
|
||||
markerBuilder: buildMarker,
|
||||
),
|
||||
),
|
||||
if (entry.hasGps)
|
||||
|
|
|
@ -62,7 +62,7 @@ class MapButtonPanel extends StatelessWidget {
|
|||
MapOverlayButton(
|
||||
icon: AIcons.openInNew,
|
||||
onPressed: () => AndroidAppService.openMap(geoUri),
|
||||
tooltip: 'Show on map...',
|
||||
tooltip: 'Show on map…',
|
||||
),
|
||||
SizedBox(height: padding),
|
||||
MapOverlayButton(
|
||||
|
@ -81,7 +81,7 @@ class MapButtonPanel extends StatelessWidget {
|
|||
MapStyleChangedNotification().dispatch(context);
|
||||
}
|
||||
},
|
||||
tooltip: 'Style map...',
|
||||
tooltip: 'Style map…',
|
||||
),
|
||||
Spacer(),
|
||||
MapOverlayButton(
|
||||
|
|
|
@ -1,22 +1,28 @@
|
|||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/location_section.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/maps/common.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/maps/marker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class EntryGoogleMap extends StatefulWidget {
|
||||
final String markerId;
|
||||
final LatLng latLng;
|
||||
final String geoUri;
|
||||
final double initialZoom;
|
||||
final String markerId;
|
||||
final WidgetBuilder markerBuilder;
|
||||
|
||||
EntryGoogleMap({
|
||||
Key key,
|
||||
this.markerId,
|
||||
Tuple2<double, double> latLng,
|
||||
this.geoUri,
|
||||
this.initialZoom,
|
||||
this.markerId,
|
||||
this.markerBuilder,
|
||||
}) : latLng = LatLng(latLng.item1, latLng.item2),
|
||||
super(key: key);
|
||||
|
||||
|
@ -26,6 +32,13 @@ class EntryGoogleMap extends StatefulWidget {
|
|||
|
||||
class EntryGoogleMapState extends State<EntryGoogleMap> with AutomaticKeepAliveClientMixin {
|
||||
GoogleMapController _controller;
|
||||
Completer<Uint8List> _markerLoaderCompleter;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_markerLoaderCompleter = Completer<Uint8List>();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(EntryGoogleMap oldWidget) {
|
||||
|
@ -33,6 +46,9 @@ class EntryGoogleMapState extends State<EntryGoogleMap> with AutomaticKeepAliveC
|
|||
if (widget.latLng != oldWidget.latLng && _controller != null) {
|
||||
_controller.moveCamera(CameraUpdate.newLatLng(widget.latLng));
|
||||
}
|
||||
if (widget.markerId != oldWidget.markerId) {
|
||||
_markerLoaderCompleter = Completer<Uint8List>();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -46,6 +62,11 @@ class EntryGoogleMapState extends State<EntryGoogleMap> with AutomaticKeepAliveC
|
|||
super.build(context);
|
||||
return Stack(
|
||||
children: [
|
||||
MarkerGeneratorWidget(
|
||||
key: Key(widget.markerId),
|
||||
markers: [widget.markerBuilder(context)],
|
||||
onComplete: (bitmaps) => _markerLoaderCompleter.complete(bitmaps.first),
|
||||
),
|
||||
MapDecorator(
|
||||
child: _buildMap(),
|
||||
),
|
||||
|
@ -58,34 +79,40 @@ class EntryGoogleMapState extends State<EntryGoogleMap> with AutomaticKeepAliveC
|
|||
}
|
||||
|
||||
Widget _buildMap() {
|
||||
final accentHue = HSVColor.fromColor(Theme.of(context).accentColor).hue;
|
||||
return GoogleMap(
|
||||
// GoogleMap init perf issue: https://github.com/flutter/flutter/issues/28493
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: widget.latLng,
|
||||
zoom: widget.initialZoom,
|
||||
),
|
||||
onMapCreated: (controller) => setState(() => _controller = controller),
|
||||
compassEnabled: false,
|
||||
mapToolbarEnabled: false,
|
||||
mapType: _toMapStyle(settings.infoMapStyle),
|
||||
rotateGesturesEnabled: false,
|
||||
scrollGesturesEnabled: false,
|
||||
zoomControlsEnabled: false,
|
||||
zoomGesturesEnabled: false,
|
||||
liteModeEnabled: false,
|
||||
// no camera animation in lite mode
|
||||
tiltGesturesEnabled: false,
|
||||
myLocationEnabled: false,
|
||||
myLocationButtonEnabled: false,
|
||||
markers: {
|
||||
Marker(
|
||||
markerId: MarkerId(widget.markerId),
|
||||
icon: BitmapDescriptor.defaultMarkerWithHue(accentHue),
|
||||
position: widget.latLng,
|
||||
)
|
||||
},
|
||||
);
|
||||
return FutureBuilder<Uint8List>(
|
||||
future: _markerLoaderCompleter.future,
|
||||
builder: (context, snapshot) {
|
||||
final markers = <Marker>{};
|
||||
if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done) {
|
||||
final markerBytes = snapshot.data;
|
||||
markers.add(Marker(
|
||||
markerId: MarkerId(widget.markerId),
|
||||
icon: BitmapDescriptor.fromBytes(markerBytes),
|
||||
position: widget.latLng,
|
||||
));
|
||||
}
|
||||
return GoogleMap(
|
||||
// GoogleMap init perf issue: https://github.com/flutter/flutter/issues/28493
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: widget.latLng,
|
||||
zoom: widget.initialZoom,
|
||||
),
|
||||
onMapCreated: (controller) => setState(() => _controller = controller),
|
||||
compassEnabled: false,
|
||||
mapToolbarEnabled: false,
|
||||
mapType: _toMapStyle(settings.infoMapStyle),
|
||||
rotateGesturesEnabled: false,
|
||||
scrollGesturesEnabled: false,
|
||||
zoomControlsEnabled: false,
|
||||
zoomGesturesEnabled: false,
|
||||
liteModeEnabled: false,
|
||||
// no camera animation in lite mode
|
||||
tiltGesturesEnabled: false,
|
||||
myLocationEnabled: false,
|
||||
myLocationButtonEnabled: false,
|
||||
markers: markers,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _zoomBy(double amount) {
|
||||
|
|
|
@ -15,6 +15,8 @@ class EntryLeafletMap extends StatefulWidget {
|
|||
final String geoUri;
|
||||
final double initialZoom;
|
||||
final EntryMapStyle style;
|
||||
final Size markerSize;
|
||||
final WidgetBuilder markerBuilder;
|
||||
|
||||
EntryLeafletMap({
|
||||
Key key,
|
||||
|
@ -22,6 +24,8 @@ class EntryLeafletMap extends StatefulWidget {
|
|||
this.geoUri,
|
||||
this.initialZoom,
|
||||
this.style,
|
||||
this.markerBuilder,
|
||||
this.markerSize,
|
||||
}) : latLng = LatLng(latLng.item1, latLng.item2),
|
||||
super(key: key);
|
||||
|
||||
|
@ -32,8 +36,6 @@ class EntryLeafletMap extends StatefulWidget {
|
|||
class EntryLeafletMapState extends State<EntryLeafletMap> with AutomaticKeepAliveClientMixin, TickerProviderStateMixin {
|
||||
final MapController _mapController = MapController();
|
||||
|
||||
static const markerSize = 40.0;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(EntryLeafletMap oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
@ -80,16 +82,10 @@ class EntryLeafletMapState extends State<EntryLeafletMap> with AutomaticKeepAliv
|
|||
options: MarkerLayerOptions(
|
||||
markers: [
|
||||
Marker(
|
||||
width: markerSize,
|
||||
height: markerSize,
|
||||
width: widget.markerSize.width,
|
||||
height: widget.markerSize.height,
|
||||
point: widget.latLng,
|
||||
builder: (ctx) {
|
||||
return Icon(
|
||||
Icons.place,
|
||||
size: markerSize,
|
||||
color: Theme.of(context).accentColor,
|
||||
);
|
||||
},
|
||||
builder: widget.markerBuilder,
|
||||
anchorPos: AnchorPos.align(AnchorAlign.top),
|
||||
),
|
||||
],
|
||||
|
|
182
lib/widgets/fullscreen/info/maps/marker.dart
Normal file
|
@ -0,0 +1,182 @@
|
|||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/raster.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/vector.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class ImageMarker extends StatelessWidget {
|
||||
final ImageEntry entry;
|
||||
final double extent;
|
||||
final Size pointerSize;
|
||||
|
||||
static const double outerBorderRadiusDim = 8;
|
||||
static const double outerBorderWidth = 1.5;
|
||||
static const double innerBorderWidth = 2;
|
||||
static const outerBorderColor = Colors.white30;
|
||||
static final innerBorderColor = Colors.grey[900];
|
||||
|
||||
const ImageMarker({
|
||||
@required this.entry,
|
||||
@required this.extent,
|
||||
this.pointerSize = Size.zero,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final thumbnail = entry.isSvg
|
||||
? ThumbnailVectorImage(
|
||||
entry: entry,
|
||||
extent: extent,
|
||||
)
|
||||
: ThumbnailRasterImage(
|
||||
entry: entry,
|
||||
extent: extent,
|
||||
);
|
||||
|
||||
final outerBorderRadius = BorderRadius.circular(outerBorderRadiusDim);
|
||||
final innerBorderRadius = BorderRadius.circular(outerBorderRadiusDim - outerBorderWidth);
|
||||
|
||||
return CustomPaint(
|
||||
foregroundPainter: MarkerPointerPainter(
|
||||
color: innerBorderColor,
|
||||
outlineColor: outerBorderColor,
|
||||
outlineWidth: outerBorderWidth,
|
||||
size: pointerSize,
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: pointerSize.height),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: outerBorderColor,
|
||||
width: outerBorderWidth,
|
||||
),
|
||||
borderRadius: outerBorderRadius,
|
||||
),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: innerBorderColor,
|
||||
width: innerBorderWidth,
|
||||
),
|
||||
borderRadius: innerBorderRadius,
|
||||
),
|
||||
position: DecorationPosition.foreground,
|
||||
child: ClipRRect(
|
||||
borderRadius: innerBorderRadius,
|
||||
child: thumbnail,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MarkerPointerPainter extends CustomPainter {
|
||||
final Color color, outlineColor;
|
||||
final double outlineWidth;
|
||||
final Size size;
|
||||
|
||||
const MarkerPointerPainter({
|
||||
this.color,
|
||||
this.outlineColor,
|
||||
this.outlineWidth,
|
||||
this.size,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final pointerWidth = this.size.width;
|
||||
final pointerHeight = this.size.height;
|
||||
|
||||
final bottomCenter = Offset(size.width / 2, size.height);
|
||||
final topLeft = bottomCenter + Offset(-pointerWidth / 2, -pointerHeight);
|
||||
final topRight = bottomCenter + Offset(pointerWidth / 2, -pointerHeight);
|
||||
|
||||
canvas.drawPath(
|
||||
Path()
|
||||
..moveTo(bottomCenter.dx, bottomCenter.dy)
|
||||
..lineTo(topRight.dx, topRight.dy)
|
||||
..lineTo(topLeft.dx, topLeft.dy)
|
||||
..close(),
|
||||
Paint()..color = outlineColor);
|
||||
|
||||
canvas.translate(0, -outlineWidth.ceilToDouble());
|
||||
canvas.drawPath(
|
||||
Path()
|
||||
..moveTo(bottomCenter.dx, bottomCenter.dy)
|
||||
..lineTo(topRight.dx, topRight.dy)
|
||||
..lineTo(topLeft.dx, topLeft.dy)
|
||||
..close(),
|
||||
Paint()..color = color);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
// generate bitmap from widget, for Google Maps
|
||||
class MarkerGeneratorWidget extends StatefulWidget {
|
||||
final List<Widget> markers;
|
||||
final Duration delay;
|
||||
final Function(List<Uint8List> bitmaps) onComplete;
|
||||
|
||||
const MarkerGeneratorWidget({
|
||||
Key key,
|
||||
@required this.markers,
|
||||
this.delay = Duration.zero,
|
||||
@required this.onComplete,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_MarkerGeneratorWidgetState createState() => _MarkerGeneratorWidgetState();
|
||||
}
|
||||
|
||||
class _MarkerGeneratorWidgetState extends State<MarkerGeneratorWidget> {
|
||||
final _globalKeys = <GlobalKey>[];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
if (widget.delay > Duration.zero) {
|
||||
await Future.delayed(widget.delay);
|
||||
}
|
||||
widget.onComplete(await _getBitmaps(context));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Transform.translate(
|
||||
offset: Offset(MediaQuery.of(context).size.width, 0),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Stack(
|
||||
children: widget.markers.map((i) {
|
||||
final key = GlobalKey();
|
||||
_globalKeys.add(key);
|
||||
return RepaintBoundary(
|
||||
key: key,
|
||||
child: i,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Uint8List>> _getBitmaps(BuildContext context) async {
|
||||
final pixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
return Future.wait(_globalKeys.map((key) async {
|
||||
RenderRepaintBoundary boundary = key.currentContext.findRenderObject();
|
||||
final image = await boundary.toImage(pixelRatio: pixelRatio);
|
||||
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
return byteData.buffer.asUint8List();
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -75,7 +75,12 @@ class ScaleLayer extends StatelessWidget {
|
|||
builder: (context, snapshot) {
|
||||
final center = map.center;
|
||||
final latitude = center.latitude.abs();
|
||||
final level = map.zoom.round() + (latitude > 80 ? 4 : latitude > 60 ? 3 : 2);
|
||||
final level = map.zoom.round() +
|
||||
(latitude > 80
|
||||
? 4
|
||||
: latitude > 60
|
||||
? 3
|
||||
: 2);
|
||||
final distance = scale[max(0, min(20, level))].toDouble();
|
||||
final start = map.project(center);
|
||||
final targetPoint = util.calculateEndingGlobalCoordinates(center, 90, distance);
|
||||
|
|
|
@ -36,8 +36,9 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
|
|||
static const int maxValueLength = 140;
|
||||
|
||||
// directory names from metadata-extractor
|
||||
static const exifThumbnailDirectory = 'Exif Thumbnail';
|
||||
static const xmpDirectory = 'XMP';
|
||||
static const exifThumbnailDirectory = 'Exif Thumbnail'; // from metadata-extractor
|
||||
static const xmpDirectory = 'XMP'; // from metadata-extractor
|
||||
static const videoDirectory = 'Video'; // additional generic video directory
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -106,6 +107,7 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
|
|||
SizedBox(height: 4),
|
||||
if (dir.name == exifThumbnailDirectory) MetadataThumbnails(source: MetadataThumbnailSource.exif, entry: entry),
|
||||
if (dir.name == xmpDirectory) MetadataThumbnails(source: MetadataThumbnailSource.xmp, entry: entry),
|
||||
if (dir.name == videoDirectory) MetadataThumbnails(source: MetadataThumbnailSource.embedded, entry: entry),
|
||||
Container(
|
||||
alignment: Alignment.topLeft,
|
||||
padding: EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
||||
|
|
|
@ -5,7 +5,7 @@ import 'package:aves/model/image_entry.dart';
|
|||
import 'package:aves/services/metadata_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum MetadataThumbnailSource { exif, xmp }
|
||||
enum MetadataThumbnailSource { embedded, exif, xmp }
|
||||
|
||||
class MetadataThumbnails extends StatefulWidget {
|
||||
final MetadataThumbnailSource source;
|
||||
|
@ -24,15 +24,22 @@ class MetadataThumbnails extends StatefulWidget {
|
|||
class _MetadataThumbnailsState extends State<MetadataThumbnails> {
|
||||
Future<List<Uint8List>> _loader;
|
||||
|
||||
ImageEntry get entry => widget.entry;
|
||||
|
||||
String get uri => entry.uri;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
switch (widget.source) {
|
||||
case MetadataThumbnailSource.embedded:
|
||||
_loader = MetadataService.getEmbeddedPictures(uri);
|
||||
break;
|
||||
case MetadataThumbnailSource.exif:
|
||||
_loader = MetadataService.getExifThumbnails(widget.entry.uri);
|
||||
_loader = MetadataService.getExifThumbnails(uri);
|
||||
break;
|
||||
case MetadataThumbnailSource.xmp:
|
||||
_loader = MetadataService.getXmpThumbnails(widget.entry.uri);
|
||||
_loader = MetadataService.getXmpThumbnails(uri);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +50,7 @@ class _MetadataThumbnailsState extends State<MetadataThumbnails> {
|
|||
future: _loader,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done && snapshot.data.isNotEmpty) {
|
||||
final turns = (widget.entry.orientationDegrees / 90).round();
|
||||
final turns = (entry.orientationDegrees / 90).round();
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
return Container(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
|
|
65
lib/widgets/settings/access_grants.dart
Normal file
|
@ -0,0 +1,65 @@
|
|||
import 'package:aves/services/android_file_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class GrantedDirectories extends StatefulWidget {
|
||||
@override
|
||||
_GrantedDirectoriesState createState() => _GrantedDirectoriesState();
|
||||
}
|
||||
|
||||
class _GrantedDirectoriesState extends State<GrantedDirectories> {
|
||||
Future<List<String>> _pathLoader;
|
||||
List<String> _lastPaths;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
void _load() => _pathLoader = AndroidFileService.getGrantedDirectories();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: FutureBuilder<List<String>>(
|
||||
future: _pathLoader,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Text(snapshot.error.toString());
|
||||
}
|
||||
if (snapshot.connectionState != ConnectionState.done && _lastPaths == null) {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
_lastPaths = snapshot.data..sort();
|
||||
final count = _lastPaths.length;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Aves has access to ${Intl.plural(count, zero: 'no directories.', one: 'one directory:', other: '$count directories:')}',
|
||||
style: textTheme.subtitle1,
|
||||
),
|
||||
..._lastPaths.map((path) => Row(
|
||||
children: [
|
||||
Expanded(child: Text(path, style: textTheme.caption)),
|
||||
SizedBox(width: 8),
|
||||
OutlineButton(
|
||||
onPressed: () async {
|
||||
await AndroidFileService.revokeDirectoryAccess(path);
|
||||
_load();
|
||||
setState(() {});
|
||||
},
|
||||
child: Text('Revoke'),
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import 'package:aves/utils/constants.dart';
|
|||
import 'package:aves/widgets/common/aves_selection_dialog.dart';
|
||||
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/common/highlight_title.dart';
|
||||
import 'package:aves/widgets/settings/access_grants.dart';
|
||||
import 'package:aves/widgets/settings/svg_background.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -116,6 +117,7 @@ class SettingsPage extends StatelessWidget {
|
|||
onChanged: (v) => settings.isCrashlyticsEnabled = v,
|
||||
title: Text('Allow anonymous crash reporting'),
|
||||
),
|
||||
GrantedDirectories(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 1.1.13+25
|
||||
version: 1.1.14+26
|
||||
|
||||
# video_player (as of v0.10.8+2, backed by ExoPlayer):
|
||||
# - does not support content URIs (by default, but trivial by fork)
|
||||
|
|