From b170ce04928e7e54468872ba50ebc7eec54cd169 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 11 Jun 2020 18:06:30 +0900 Subject: [PATCH] media store fetch by stream handler, collection source split in mixins --- .../deckers/thibault/aves/MainActivity.java | 37 +-- .../calls}/AppAdapterHandler.java | 2 +- .../calls}/ImageDecodeTask.java | 2 +- .../calls}/ImageFileHandler.java | 20 +- .../calls}/MetadataHandler.java | 2 +- .../calls}/MethodResultWrapper.java | 2 +- .../calls}/StorageHandler.java | 4 +- .../streams}/ImageByteStreamHandler.java | 5 +- .../streams}/ImageOpStreamHandler.java | 2 +- .../streams/MediaStoreStreamHandler.java | 47 +++ .../streams}/StorageAccessStreamHandler.java | 9 +- .../MediaStoreStreamHandler.java | 30 -- .../provider/MediaStoreImageProvider.java | 33 +- .../thibault/aves/utils/StorageUtils.java | 1 - lib/main.dart | 111 +------ lib/model/collection_source.dart | 284 ------------------ lib/model/settings.dart | 2 +- lib/model/source/album.dart | 71 +++++ lib/model/{ => source}/collection_lens.dart | 5 +- lib/model/source/collection_source.dart | 115 +++++++ lib/model/source/location.dart | 73 +++++ lib/model/source/tag.dart | 66 ++++ lib/services/image_file_service.dart | 10 +- lib/widgets/album/app_bar.dart | 43 ++- lib/widgets/album/collection_page.dart | 2 +- lib/widgets/album/grid/header_generic.dart | 4 +- .../album/grid/list_section_layout.dart | 2 +- lib/widgets/album/grid/list_sliver.dart | 2 +- lib/widgets/album/search/search_delegate.dart | 5 +- lib/widgets/album/thumbnail/decorated.dart | 2 +- lib/widgets/album/thumbnail/overlay.dart | 2 +- lib/widgets/album/thumbnail_collection.dart | 4 +- lib/widgets/app_drawer.dart | 7 +- .../entry_action_delegate.dart | 2 +- .../selection_action_delegate.dart | 2 +- .../media_store_collection_provider.dart | 61 ++-- lib/widgets/common/icons.dart | 1 + lib/widgets/debug_page.dart | 4 +- lib/widgets/filter_grid_page.dart | 4 +- lib/widgets/fullscreen/fullscreen_body.dart | 2 +- lib/widgets/fullscreen/fullscreen_page.dart | 2 +- lib/widgets/fullscreen/image_page.dart | 2 +- .../fullscreen/info/basic_section.dart | 2 +- lib/widgets/fullscreen/info/info_page.dart | 2 +- .../fullscreen/info/location_section.dart | 2 +- lib/widgets/home_page.dart | 114 +++++++ lib/widgets/stats/filter_table.dart | 2 +- lib/widgets/stats/stats.dart | 2 +- .../{welcome.dart => welcome_page.dart} | 2 +- 49 files changed, 633 insertions(+), 579 deletions(-) rename android/app/src/main/java/deckers/thibault/aves/{channelhandlers => channel/calls}/AppAdapterHandler.java (99%) rename android/app/src/main/java/deckers/thibault/aves/{channelhandlers => channel/calls}/ImageDecodeTask.java (99%) rename android/app/src/main/java/deckers/thibault/aves/{channelhandlers => channel/calls}/ImageFileHandler.java (89%) rename android/app/src/main/java/deckers/thibault/aves/{channelhandlers => channel/calls}/MetadataHandler.java (99%) rename android/app/src/main/java/deckers/thibault/aves/{channelhandlers => channel/calls}/MethodResultWrapper.java (95%) rename android/app/src/main/java/deckers/thibault/aves/{channelhandlers => channel/calls}/StorageHandler.java (96%) rename android/app/src/main/java/deckers/thibault/aves/{channelhandlers => channel/streams}/ImageByteStreamHandler.java (97%) rename android/app/src/main/java/deckers/thibault/aves/{channelhandlers => channel/streams}/ImageOpStreamHandler.java (99%) create mode 100644 android/app/src/main/java/deckers/thibault/aves/channel/streams/MediaStoreStreamHandler.java rename android/app/src/main/java/deckers/thibault/aves/{channelhandlers => channel/streams}/StorageAccessStreamHandler.java (85%) delete mode 100644 android/app/src/main/java/deckers/thibault/aves/channelhandlers/MediaStoreStreamHandler.java delete mode 100644 lib/model/collection_source.dart create mode 100644 lib/model/source/album.dart rename lib/model/{ => source}/collection_lens.dart (98%) create mode 100644 lib/model/source/collection_source.dart create mode 100644 lib/model/source/location.dart create mode 100644 lib/model/source/tag.dart create mode 100644 lib/widgets/home_page.dart rename lib/widgets/{welcome.dart => welcome_page.dart} (99%) diff --git a/android/app/src/main/java/deckers/thibault/aves/MainActivity.java b/android/app/src/main/java/deckers/thibault/aves/MainActivity.java index 600701f39..56520b9a4 100644 --- a/android/app/src/main/java/deckers/thibault/aves/MainActivity.java +++ b/android/app/src/main/java/deckers/thibault/aves/MainActivity.java @@ -10,21 +10,20 @@ import java.util.Map; import java.util.Objects; import app.loup.streams_channel.StreamsChannel; -import deckers.thibault.aves.channelhandlers.AppAdapterHandler; -import deckers.thibault.aves.channelhandlers.ImageByteStreamHandler; -import deckers.thibault.aves.channelhandlers.ImageFileHandler; -import deckers.thibault.aves.channelhandlers.ImageOpStreamHandler; -import deckers.thibault.aves.channelhandlers.MediaStoreStreamHandler; -import deckers.thibault.aves.channelhandlers.MetadataHandler; -import deckers.thibault.aves.channelhandlers.StorageAccessStreamHandler; -import deckers.thibault.aves.channelhandlers.StorageHandler; +import deckers.thibault.aves.channel.calls.AppAdapterHandler; +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.MediaStoreStreamHandler; +import deckers.thibault.aves.channel.streams.StorageAccessStreamHandler; import deckers.thibault.aves.utils.Constants; import deckers.thibault.aves.utils.Env; 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 { @@ -40,23 +39,17 @@ public class MainActivity extends FlutterActivity { handleIntent(getIntent()); - MediaStoreStreamHandler mediaStoreStreamHandler = new MediaStoreStreamHandler(); - BinaryMessenger messenger = Objects.requireNonNull(getFlutterEngine()).getDartExecutor().getBinaryMessenger(); - new MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(new StorageHandler(this)); + new MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(new AppAdapterHandler(this)); - new MethodChannel(messenger, ImageFileHandler.CHANNEL).setMethodCallHandler(new ImageFileHandler(this, mediaStoreStreamHandler)); + new MethodChannel(messenger, ImageFileHandler.CHANNEL).setMethodCallHandler(new ImageFileHandler(this)); new MethodChannel(messenger, MetadataHandler.CHANNEL).setMethodCallHandler(new MetadataHandler(this)); - new EventChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandler(mediaStoreStreamHandler); + new MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(new StorageHandler(this)); - final StreamsChannel fileAccessStreamChannel = new StreamsChannel(messenger, StorageAccessStreamHandler.CHANNEL); - fileAccessStreamChannel.setStreamHandlerFactory(arguments -> new StorageAccessStreamHandler(this, arguments)); - - final StreamsChannel imageByteStreamChannel = new StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL); - imageByteStreamChannel.setStreamHandlerFactory(arguments -> new ImageByteStreamHandler(this, arguments)); - - final StreamsChannel imageOpStreamChannel = new StreamsChannel(messenger, ImageOpStreamHandler.CHANNEL); - imageOpStreamChannel.setStreamHandlerFactory(arguments -> new ImageOpStreamHandler(this, arguments)); + 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)); + new StreamsChannel(messenger, StorageAccessStreamHandler.CHANNEL).setStreamHandlerFactory(args -> new StorageAccessStreamHandler(this, args)); new MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler( (call, result) -> { diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/AppAdapterHandler.java b/android/app/src/main/java/deckers/thibault/aves/channel/calls/AppAdapterHandler.java similarity index 99% rename from android/app/src/main/java/deckers/thibault/aves/channelhandlers/AppAdapterHandler.java rename to android/app/src/main/java/deckers/thibault/aves/channel/calls/AppAdapterHandler.java index c5453ecdb..22dd8c1a3 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/AppAdapterHandler.java +++ b/android/app/src/main/java/deckers/thibault/aves/channel/calls/AppAdapterHandler.java @@ -1,4 +1,4 @@ -package deckers.thibault.aves.channelhandlers; +package deckers.thibault.aves.channel.calls; import android.content.ContentResolver; import android.content.Context; diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageDecodeTask.java b/android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageDecodeTask.java similarity index 99% rename from android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageDecodeTask.java rename to android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageDecodeTask.java index 005570fea..f86cea884 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageDecodeTask.java +++ b/android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageDecodeTask.java @@ -1,4 +1,4 @@ -package deckers.thibault.aves.channelhandlers; +package deckers.thibault.aves.channel.calls; import android.annotation.SuppressLint; import android.app.Activity; diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageFileHandler.java b/android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageFileHandler.java similarity index 89% rename from android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageFileHandler.java rename to android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageFileHandler.java index b162bc07a..3dbca6909 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageFileHandler.java +++ b/android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageFileHandler.java @@ -1,4 +1,4 @@ -package deckers.thibault.aves.channelhandlers; +package deckers.thibault.aves.channel.calls; import android.app.Activity; import android.net.Uri; @@ -22,11 +22,9 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler { private Activity activity; private float density; - private MediaStoreStreamHandler mediaStoreStreamHandler; - public ImageFileHandler(Activity activity, MediaStoreStreamHandler mediaStoreStreamHandler) { + public ImageFileHandler(Activity activity) { this.activity = activity; - this.mediaStoreStreamHandler = mediaStoreStreamHandler; } public float getDensity() { @@ -39,14 +37,6 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler { @Override public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { switch (call.method) { - case "getImageEntries": - new Thread(() -> { - String sortBy = call.argument("sort"); - String groupBy = call.argument("group"); - mediaStoreStreamHandler.fetchAll(activity, sortBy, groupBy); - }).start(); - result.success(null); - break; case "getImageEntry": new Thread(() -> getImageEntry(call, new MethodResultWrapper(result))).start(); break; @@ -70,7 +60,7 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler { } private void getThumbnail(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - Map entryMap = call.argument("entry"); + Map entryMap = call.argument("entry"); Double widthDip = call.argument("widthDip"); Double heightDip = call.argument("heightDip"); Double defaultSizeDip = call.argument("defaultSizeDip"); @@ -118,7 +108,7 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler { } private void rename(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - Map entryMap = call.argument("entry"); + Map entryMap = call.argument("entry"); String newName = call.argument("newName"); if (entryMap == null || newName == null) { result.error("rename-args", "failed because of missing arguments", null); @@ -147,7 +137,7 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler { } private void rotate(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - Map entryMap = call.argument("entry"); + Map entryMap = call.argument("entry"); Boolean clockwise = call.argument("clockwise"); if (entryMap == null || clockwise == null) { result.error("rotate-args", "failed because of missing arguments", null); diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MetadataHandler.java b/android/app/src/main/java/deckers/thibault/aves/channel/calls/MetadataHandler.java similarity index 99% rename from android/app/src/main/java/deckers/thibault/aves/channelhandlers/MetadataHandler.java rename to android/app/src/main/java/deckers/thibault/aves/channel/calls/MetadataHandler.java index 40ae50fad..4410a36c3 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MetadataHandler.java +++ b/android/app/src/main/java/deckers/thibault/aves/channel/calls/MetadataHandler.java @@ -1,4 +1,4 @@ -package deckers.thibault.aves.channelhandlers; +package deckers.thibault.aves.channel.calls; import android.content.ContentUris; import android.content.Context; diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MethodResultWrapper.java b/android/app/src/main/java/deckers/thibault/aves/channel/calls/MethodResultWrapper.java similarity index 95% rename from android/app/src/main/java/deckers/thibault/aves/channelhandlers/MethodResultWrapper.java rename to android/app/src/main/java/deckers/thibault/aves/channel/calls/MethodResultWrapper.java index 0033cdc5f..e6aeaacef 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MethodResultWrapper.java +++ b/android/app/src/main/java/deckers/thibault/aves/channel/calls/MethodResultWrapper.java @@ -1,4 +1,4 @@ -package deckers.thibault.aves.channelhandlers; +package deckers.thibault.aves.channel.calls; import android.os.Handler; import android.os.Looper; diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/StorageHandler.java b/android/app/src/main/java/deckers/thibault/aves/channel/calls/StorageHandler.java similarity index 96% rename from android/app/src/main/java/deckers/thibault/aves/channelhandlers/StorageHandler.java rename to android/app/src/main/java/deckers/thibault/aves/channel/calls/StorageHandler.java index b321fbcbe..6d8da6137 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/StorageHandler.java +++ b/android/app/src/main/java/deckers/thibault/aves/channel/calls/StorageHandler.java @@ -1,4 +1,4 @@ -package deckers.thibault.aves.channelhandlers; +package deckers.thibault.aves.channel.calls; import android.app.Activity; import android.os.Build; @@ -32,7 +32,7 @@ public class StorageHandler implements MethodChannel.MethodCallHandler { public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { switch (call.method) { case "getStorageVolumes": { - List> volumes = null; + List> volumes; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { volumes = getStorageVolumes(); } else { diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageByteStreamHandler.java b/android/app/src/main/java/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.java similarity index 97% rename from android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageByteStreamHandler.java rename to android/app/src/main/java/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.java index 929f228e8..5d7fffff7 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageByteStreamHandler.java +++ b/android/app/src/main/java/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.java @@ -1,4 +1,4 @@ -package deckers.thibault.aves.channelhandlers; +package deckers.thibault.aves.channel.streams; import android.app.Activity; import android.content.ContentResolver; @@ -33,10 +33,11 @@ public class ImageByteStreamHandler implements EventChannel.StreamHandler { private EventChannel.EventSink eventSink; private Handler handler; + @SuppressWarnings("unchecked") public ImageByteStreamHandler(Activity activity, Object arguments) { this.activity = activity; if (arguments instanceof Map) { - Map argMap = (Map) arguments; + Map argMap = (Map) arguments; this.mimeType = (String) argMap.get("mimeType"); this.uri = Uri.parse((String) argMap.get("uri")); } diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageOpStreamHandler.java b/android/app/src/main/java/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.java similarity index 99% rename from android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageOpStreamHandler.java rename to android/app/src/main/java/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.java index b21313500..a198d4cdf 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageOpStreamHandler.java +++ b/android/app/src/main/java/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.java @@ -1,4 +1,4 @@ -package deckers.thibault.aves.channelhandlers; +package deckers.thibault.aves.channel.streams; import android.app.Activity; import android.net.Uri; diff --git a/android/app/src/main/java/deckers/thibault/aves/channel/streams/MediaStoreStreamHandler.java b/android/app/src/main/java/deckers/thibault/aves/channel/streams/MediaStoreStreamHandler.java new file mode 100644 index 000000000..1990c568c --- /dev/null +++ b/android/app/src/main/java/deckers/thibault/aves/channel/streams/MediaStoreStreamHandler.java @@ -0,0 +1,47 @@ +package deckers.thibault.aves.channel.streams; + +import android.app.Activity; +import android.os.Handler; +import android.os.Looper; + +import java.util.Map; + +import deckers.thibault.aves.model.provider.MediaStoreImageProvider; +import io.flutter.plugin.common.EventChannel; + +public class MediaStoreStreamHandler implements EventChannel.StreamHandler { + public static final String CHANNEL = "deckers.thibault/aves/mediastorestream"; + + private Activity activity; + private EventChannel.EventSink eventSink; + private Handler handler; + + public MediaStoreStreamHandler(Activity activity) { + this.activity = activity; + } + + @Override + public void onListen(Object args, final EventChannel.EventSink eventSink) { + this.eventSink = eventSink; + this.handler = new Handler(Looper.getMainLooper()); + new Thread(this::fetchAll).start(); + } + + @Override + public void onCancel(Object args) { + // nothing + } + + private void success(final Map result) { + handler.post(() -> eventSink.success(result)); + } + + private void endOfStream() { + handler.post(() -> eventSink.endOfStream()); + } + + void fetchAll() { + new MediaStoreImageProvider().fetchAll(activity, this::success); // 350ms + endOfStream(); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/StorageAccessStreamHandler.java b/android/app/src/main/java/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.java similarity index 85% rename from android/app/src/main/java/deckers/thibault/aves/channelhandlers/StorageAccessStreamHandler.java rename to android/app/src/main/java/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.java index dad6f725d..65dd42b6e 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/StorageAccessStreamHandler.java +++ b/android/app/src/main/java/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.java @@ -1,4 +1,4 @@ -package deckers.thibault.aves.channelhandlers; +package deckers.thibault.aves.channel.streams; import android.app.Activity; import android.os.Handler; @@ -19,10 +19,11 @@ public class StorageAccessStreamHandler implements EventChannel.StreamHandler { private Handler handler; private String volumePath; + @SuppressWarnings("unchecked") public StorageAccessStreamHandler(Activity activity, Object arguments) { this.activity = activity; if (arguments instanceof Map) { - Map argMap = (Map) arguments; + Map argMap = (Map) arguments; this.volumePath = (String) argMap.get("path"); } } @@ -45,10 +46,6 @@ public class StorageAccessStreamHandler implements EventChannel.StreamHandler { endOfStream(); } - private void error(final String errorCode, final String errorMessage, final Object errorDetails) { - handler.post(() -> eventSink.error(errorCode, errorMessage, errorDetails)); - } - private void endOfStream() { handler.post(() -> eventSink.endOfStream()); } diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MediaStoreStreamHandler.java b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MediaStoreStreamHandler.java deleted file mode 100644 index 41a51b97e..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MediaStoreStreamHandler.java +++ /dev/null @@ -1,30 +0,0 @@ -package deckers.thibault.aves.channelhandlers; - -import android.app.Activity; -import android.os.Handler; -import android.os.Looper; - -import deckers.thibault.aves.model.provider.MediaStoreImageProvider; -import io.flutter.plugin.common.EventChannel; - -public class MediaStoreStreamHandler implements EventChannel.StreamHandler { - public static final String CHANNEL = "deckers.thibault/aves/mediastore"; - - private EventChannel.EventSink eventSink; - - @Override - public void onListen(Object args, final EventChannel.EventSink events) { - eventSink = events; - } - - @Override - public void onCancel(Object args) { - // nothing - } - - void fetchAll(Activity activity, String sortBy, String groupBy) { - Handler handler = new Handler(Looper.getMainLooper()); - new MediaStoreImageProvider().fetchAll(activity, sortBy, groupBy, (entry) -> handler.post(() -> eventSink.success(entry))); // 350ms - handler.post(() -> eventSink.endOfStream()); - } -} \ No newline at end of file diff --git a/android/app/src/main/java/deckers/thibault/aves/model/provider/MediaStoreImageProvider.java b/android/app/src/main/java/deckers/thibault/aves/model/provider/MediaStoreImageProvider.java index 60ac722f0..01e31429b 100644 --- a/android/app/src/main/java/deckers/thibault/aves/model/provider/MediaStoreImageProvider.java +++ b/android/app/src/main/java/deckers/thibault/aves/model/provider/MediaStoreImageProvider.java @@ -68,35 +68,12 @@ public class MediaStoreImageProvider extends ImageProvider { MediaStore.Video.Media.ORIENTATION, } : new String[0]).flatMap(Stream::of).toArray(String[]::new); - public void fetchAll(Activity activity, final String sortBy, final String groupBy, NewEntryHandler newEntryHandler) { + public void fetchAll(Activity activity, NewEntryHandler newEntryHandler) { String orderBy; - switch (sortBy) { - case "size": - orderBy = MediaStore.MediaColumns.SIZE + " DESC"; - break; - case "name": - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - orderBy = MediaStore.MediaColumns.RELATIVE_PATH + ", " + MediaStore.MediaColumns.BUCKET_DISPLAY_NAME + ", " + MediaStore.MediaColumns.DISPLAY_NAME; - } else { - orderBy = MediaStore.MediaColumns.DATA; - } - break; - default: - case "date": - switch (groupBy) { - case "album": - // TODO TLAD find album order first - case "month": - case "day": - default: - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - orderBy = MediaStore.MediaColumns.DATE_TAKEN + " DESC"; - } else { - orderBy = MediaStore.MediaColumns.DATE_MODIFIED + " DESC"; - } - break; - } - break; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + orderBy = MediaStore.MediaColumns.DATE_TAKEN + " DESC"; + } else { + orderBy = MediaStore.MediaColumns.DATE_MODIFIED + " DESC"; } fetchFrom(activity, newEntryHandler, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, IMAGE_PROJECTION, orderBy); diff --git a/android/app/src/main/java/deckers/thibault/aves/utils/StorageUtils.java b/android/app/src/main/java/deckers/thibault/aves/utils/StorageUtils.java index 6d2de469b..3ea02d339 100644 --- a/android/app/src/main/java/deckers/thibault/aves/utils/StorageUtils.java +++ b/android/app/src/main/java/deckers/thibault/aves/utils/StorageUtils.java @@ -21,7 +21,6 @@ import com.google.common.base.Splitter; import com.google.common.collect.Lists; import java.io.File; -import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; diff --git a/lib/main.dart b/lib/main.dart index 96a4fe0f3..2090e5063 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,20 +1,10 @@ -import 'package:aves/model/collection_lens.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings.dart'; -import 'package:aves/services/image_file_service.dart'; -import 'package:aves/services/viewer_service.dart'; -import 'package:aves/utils/android_file_utils.dart'; -import 'package:aves/widgets/album/collection_page.dart'; -import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart'; import 'package:aves/widgets/common/icons.dart'; -import 'package:aves/widgets/fullscreen/fullscreen_page.dart'; -import 'package:aves/widgets/welcome.dart'; +import 'package:aves/widgets/home_page.dart'; +import 'package:aves/widgets/welcome_page.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:pedantic/pedantic.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:screen/screen.dart'; void main() { // HttpClient.enableTimelineLogging = true; // enable network traffic logging @@ -79,100 +69,3 @@ class _AvesAppState extends State { ); } } - -class HomePage extends StatefulWidget { - const HomePage(); - - @override - _HomePageState createState() => _HomePageState(); -} - -class _HomePageState extends State { - MediaStoreSource _mediaStore; - ImageEntry _viewerEntry; - Future _appSetup; - - @override - void initState() { - super.initState(); - _appSetup = _setup(); - imageCache.maximumSizeBytes = 512 * (1 << 20); - Screen.keepOn(true); - } - - Future _setup() async { - debugPrint('$runtimeType _setup'); - - // TODO reduce permission check time - final permissions = await [ - Permission.storage, - // to access media with unredacted metadata with scoped storage (Android 10+) - Permission.accessMediaLocation, - ].request(); // 350ms - if (permissions[Permission.storage] != PermissionStatus.granted) { - unawaited(SystemNavigator.pop()); - return; - } - - // TODO notify when icons are ready for drawer and section header refresh - await androidFileUtils.init(); // 170ms - - final intentData = await ViewerService.getIntentData(); - if (intentData != null) { - final action = intentData['action']; - switch (action) { - case 'view': - AvesApp.mode = AppMode.view; - _viewerEntry = await _initViewerEntry( - uri: intentData['uri'], - mimeType: intentData['mimeType'], - ); - if (_viewerEntry == null) { - // fallback to default mode when we fail to retrieve the entry - AvesApp.mode = AppMode.main; - } - break; - case 'pick': - AvesApp.mode = AppMode.pick; - break; - } - } - - if (AvesApp.mode != AppMode.view) { - _mediaStore = MediaStoreSource(); - unawaited(_mediaStore.fetch()); - } - } - - Future _initViewerEntry({@required String uri, @required String mimeType}) async { - final entry = await ImageFileService.getImageEntry(uri, mimeType); - if (entry != null) { - // cataloguing is essential for geolocation and video rotation - await entry.catalog(); - unawaited(entry.locate()); - } - return entry; - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: _appSetup, - builder: (context, AsyncSnapshot snapshot) { - if (snapshot.hasError) return const Icon(AIcons.error); - if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); - debugPrint('$runtimeType app setup future complete'); - if (AvesApp.mode == AppMode.view) { - return SingleFullscreenPage(entry: _viewerEntry); - } - if (_mediaStore != null) { - return CollectionPage(CollectionLens( - source: _mediaStore.source, - groupFactor: settings.collectionGroupFactor, - sortFactor: settings.collectionSortFactor, - )); - } - return const SizedBox.shrink(); - }); - } -} diff --git a/lib/model/collection_source.dart b/lib/model/collection_source.dart deleted file mode 100644 index 2b686e4f0..000000000 --- a/lib/model/collection_source.dart +++ /dev/null @@ -1,284 +0,0 @@ -import 'package:aves/model/collection_lens.dart'; -import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/image_metadata.dart'; -import 'package:aves/model/metadata_db.dart'; -import 'package:aves/utils/android_file_utils.dart'; -import 'package:collection/collection.dart'; -import 'package:event_bus/event_bus.dart'; -import 'package:flutter/foundation.dart'; -import 'package:path/path.dart'; - -class CollectionSource { - final List _rawEntries; - final Set _folderPaths = {}; - final Map _filterEntryCountMap = {}; - final EventBus _eventBus = EventBus(); - - List sortedAlbums = List.unmodifiable([]); - List sortedCountries = List.unmodifiable([]); - List sortedPlaces = List.unmodifiable([]); - List sortedTags = List.unmodifiable([]); - ValueNotifier stateNotifier = ValueNotifier(SourceState.ready); - - List get entries => List.unmodifiable(_rawEntries); - - EventBus get eventBus => _eventBus; - - int get albumCount => sortedAlbums.length; - - int get tagCount => sortedTags.length; - - CollectionSource({ - List entries, - }) : _rawEntries = entries ?? []; - - final List savedDates = []; - - Future loadDates() async { - final stopwatch = Stopwatch()..start(); - savedDates.addAll(await metadataDb.loadDates()); - debugPrint('$runtimeType loadDates complete in ${stopwatch.elapsed.inMilliseconds}ms for ${savedDates.length} saved entries'); - } - - Future loadCatalogMetadata() async { - final stopwatch = Stopwatch()..start(); - final saved = await metadataDb.loadMetadataEntries(); - _rawEntries.forEach((entry) { - final contentId = entry.contentId; - entry.catalogMetadata = saved.firstWhere((metadata) => metadata.contentId == contentId, orElse: () => null); - }); - debugPrint('$runtimeType loadCatalogMetadata complete in ${stopwatch.elapsed.inMilliseconds}ms for ${saved.length} saved entries'); - onCatalogMetadataChanged(); - } - - Future loadAddresses() async { - final stopwatch = Stopwatch()..start(); - final saved = await metadataDb.loadAddresses(); - _rawEntries.forEach((entry) { - final contentId = entry.contentId; - entry.addressDetails = saved.firstWhere((address) => address.contentId == contentId, orElse: () => null); - }); - debugPrint('$runtimeType loadAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms for ${saved.length} saved entries'); - onAddressMetadataChanged(); - } - - Future catalogEntries() async { - final stopwatch = Stopwatch()..start(); - final uncataloguedEntries = _rawEntries.where((entry) => !entry.isCatalogued).toList(); - if (uncataloguedEntries.isEmpty) return; - - final newMetadata = []; - await Future.forEach(uncataloguedEntries, (entry) async { - await entry.catalog(); - if (entry.isCatalogued) { - newMetadata.add(entry.catalogMetadata); - if (newMetadata.length >= 500) { - await metadataDb.saveMetadata(List.unmodifiable(newMetadata)); - newMetadata.clear(); - } - } - }); - await metadataDb.saveMetadata(List.unmodifiable(newMetadata)); - onCatalogMetadataChanged(); - debugPrint('$runtimeType catalogEntries complete in ${stopwatch.elapsed.inSeconds}s with ${newMetadata.length} new entries'); - } - - Future locateEntries() async { - final stopwatch = Stopwatch()..start(); - final unlocatedEntries = _rawEntries.where((entry) => entry.hasGps && !entry.isLocated).toList(); - final newAddresses = []; - await Future.forEach(unlocatedEntries, (entry) async { - await entry.locate(); - if (entry.isLocated) { - newAddresses.add(entry.addressDetails); - if (newAddresses.length >= 50) { - await metadataDb.saveAddresses(List.unmodifiable(newAddresses)); - newAddresses.clear(); - } - } - }); - await metadataDb.saveAddresses(List.unmodifiable(newAddresses)); - onAddressMetadataChanged(); - debugPrint('$runtimeType locateEntries complete in ${stopwatch.elapsed.inMilliseconds}ms'); - } - - void onCatalogMetadataChanged() { - updateTags(); - eventBus.fire(CatalogMetadataChangedEvent()); - } - - void onAddressMetadataChanged() { - updateLocations(); - eventBus.fire(AddressMetadataChangedEvent()); - } - - void updateAlbums() { - final sorted = _folderPaths.toList() - ..sort((a, b) { - final ua = getUniqueAlbumName(a); - final ub = getUniqueAlbumName(b); - return compareAsciiUpperCase(ua, ub); - }); - sortedAlbums = List.unmodifiable(sorted); - _filterEntryCountMap.clear(); - eventBus.fire(AlbumsChangedEvent()); - } - - void updateTags() { - final tags = _rawEntries.expand((entry) => entry.xmpSubjects).toSet().toList()..sort(compareAsciiUpperCase); - sortedTags = List.unmodifiable(tags); - _filterEntryCountMap.clear(); - eventBus.fire(TagsChangedEvent()); - } - - void updateLocations() { - final locations = _rawEntries.where((entry) => entry.isLocated).map((entry) => entry.addressDetails); - final lister = (String Function(AddressDetails a) f) => List.unmodifiable(locations.map(f).where((s) => s != null && s.isNotEmpty).toSet().toList()..sort(compareAsciiUpperCase)); - sortedCountries = lister((address) => '${address.countryName};${address.countryCode}'); - sortedPlaces = lister((address) => address.place); - _filterEntryCountMap.clear(); - eventBus.fire(LocationsChangedEvent()); - } - - void addAll(Iterable entries) { - entries.forEach((entry) { - final contentId = entry.contentId; - entry.catalogDateMillis = savedDates.firstWhere((metadata) => metadata.contentId == contentId, orElse: () => null)?.dateMillis; - }); - _rawEntries.addAll(entries); - _folderPaths.addAll(_rawEntries.map((entry) => entry.directory).toSet()); - _filterEntryCountMap.clear(); - eventBus.fire(const EntryAddedEvent()); - } - - void removeEntries(Iterable entries) async { - entries.forEach((entry) => entry.removeFromFavourites()); - _rawEntries.removeWhere(entries.contains); - _cleanEmptyAlbums(entries.map((entry) => entry.directory).toSet()); - _filterEntryCountMap.clear(); - eventBus.fire(EntryRemovedEvent(entries)); - } - - void applyMove({ - @required Iterable entries, - @required Set fromAlbums, - @required String toAlbum, - @required bool copy, - }) { - if (copy) { - addAll(entries); - } else { - _cleanEmptyAlbums(fromAlbums); - _folderPaths.add(toAlbum); - } - updateAlbums(); - _filterEntryCountMap.clear(); - eventBus.fire(EntryMovedEvent(entries)); - } - - void _cleanEmptyAlbums(Set albums) { - final emptyAlbums = albums.where(_isEmptyAlbum); - if (emptyAlbums.isNotEmpty) { - _folderPaths.removeAll(emptyAlbums); - updateAlbums(); - } - } - - bool _isEmptyAlbum(String album) => !_rawEntries.any((entry) => entry.directory == album); - - String getUniqueAlbumName(String album) { - final volumeRoot = androidFileUtils.getStorageVolume(album)?.path ?? ''; - final otherAlbums = _folderPaths.where((item) => item != album && item.startsWith(volumeRoot)); - final parts = album.split(separator); - var partCount = 0; - String testName; - do { - testName = separator + parts.skip(parts.length - ++partCount).join(separator); - } while (otherAlbums.any((item) => item.endsWith(testName))); - return parts.skip(parts.length - partCount).join(separator); - } - - List get _sortedEntriesForFilterList => CollectionLens( - source: this, - groupFactor: GroupFactor.month, - sortFactor: SortFactor.date, - ).sortedEntries; - - Map getAlbumEntries() { - final entries = _sortedEntriesForFilterList; - final regularAlbums = [], appAlbums = [], specialAlbums = []; - for (var album in sortedAlbums) { - switch (androidFileUtils.getAlbumType(album)) { - case AlbumType.regular: - regularAlbums.add(album); - break; - case AlbumType.app: - appAlbums.add(album); - break; - default: - specialAlbums.add(album); - break; - } - } - return Map.fromEntries([...specialAlbums, ...appAlbums, ...regularAlbums].map((tag) => MapEntry( - tag, - entries.firstWhere((entry) => entry.directory == tag, orElse: () => null), - ))); - } - - Map getCountryEntries() { - final locatedEntries = _sortedEntriesForFilterList.where((entry) => entry.isLocated); - return Map.fromEntries(sortedCountries.map((countryNameAndCode) { - final split = countryNameAndCode.split(';'); - ImageEntry entry; - if (split.length > 1) { - final countryCode = split[1]; - entry = locatedEntries.firstWhere((entry) => entry.addressDetails.countryCode == countryCode, orElse: () => null); - } - return MapEntry(countryNameAndCode, entry); - })); - } - - Map getTagEntries() { - final entries = _sortedEntriesForFilterList; - return Map.fromEntries(sortedTags.map((tag) => MapEntry( - tag, - entries.firstWhere((entry) => entry.xmpSubjects.contains(tag), orElse: () => null), - ))); - } - - int count(CollectionFilter filter) { - return _filterEntryCountMap.putIfAbsent(filter, () => _rawEntries.where((entry) => filter.filter(entry)).length); - } -} - -enum SourceState { loading, cataloguing, locating, ready } - -class AddressMetadataChangedEvent {} - -class CatalogMetadataChangedEvent {} - -class AlbumsChangedEvent {} - -class LocationsChangedEvent {} - -class TagsChangedEvent {} - -class EntryAddedEvent { - final ImageEntry entry; - - const EntryAddedEvent([this.entry]); -} - -class EntryRemovedEvent { - final Iterable entries; - - const EntryRemovedEvent(this.entries); -} - -class EntryMovedEvent { - final Iterable entries; - - const EntryMovedEvent(this.entries); -} diff --git a/lib/model/settings.dart b/lib/model/settings.dart index 2ac161756..0cf95928c 100644 --- a/lib/model/settings.dart +++ b/lib/model/settings.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/collection_lens.dart'; +import 'package:aves/model/source/collection_lens.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:shared_preferences/shared_preferences.dart'; diff --git a/lib/model/source/album.dart b/lib/model/source/album.dart new file mode 100644 index 000000000..f534beb7e --- /dev/null +++ b/lib/model/source/album.dart @@ -0,0 +1,71 @@ +import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/utils/android_file_utils.dart'; +import 'package:collection/collection.dart'; +import 'package:path/path.dart'; + +mixin AlbumMixin on SourceBase { + final Set _folderPaths = {}; + + List sortedAlbums = List.unmodifiable([]); + + void updateAlbums() { + final sorted = _folderPaths.toList() + ..sort((a, b) { + final ua = getUniqueAlbumName(a); + final ub = getUniqueAlbumName(b); + return compareAsciiUpperCase(ua, ub); + }); + sortedAlbums = List.unmodifiable(sorted); + invalidateFilterEntryCounts(); + eventBus.fire(AlbumsChangedEvent()); + } + + String getUniqueAlbumName(String album) { + final volumeRoot = androidFileUtils.getStorageVolume(album)?.path ?? ''; + final otherAlbums = _folderPaths.where((item) => item != album && item.startsWith(volumeRoot)); + final parts = album.split(separator); + var partCount = 0; + String testName; + do { + testName = separator + parts.skip(parts.length - ++partCount).join(separator); + } while (otherAlbums.any((item) => item.endsWith(testName))); + return parts.skip(parts.length - partCount).join(separator); + } + + Map getAlbumEntries() { + final entries = sortedEntriesForFilterList; + final regularAlbums = [], appAlbums = [], specialAlbums = []; + for (var album in sortedAlbums) { + switch (androidFileUtils.getAlbumType(album)) { + case AlbumType.regular: + regularAlbums.add(album); + break; + case AlbumType.app: + appAlbums.add(album); + break; + default: + specialAlbums.add(album); + break; + } + } + return Map.fromEntries([...specialAlbums, ...appAlbums, ...regularAlbums].map((album) => MapEntry( + album, + entries.firstWhere((entry) => entry.directory == album, orElse: () => null), + ))); + } + + void addFolderPath(Iterable albums) => _folderPaths.addAll(albums); + + void cleanEmptyAlbums([Set albums]) { + final emptyAlbums = (albums ?? _folderPaths).where(_isEmptyAlbum).toList(); + if (emptyAlbums.isNotEmpty) { + _folderPaths.removeAll(emptyAlbums); + updateAlbums(); + } + } + + bool _isEmptyAlbum(String album) => !rawEntries.any((entry) => entry.directory == album); +} + +class AlbumsChangedEvent {} diff --git a/lib/model/collection_lens.dart b/lib/model/source/collection_lens.dart similarity index 98% rename from lib/model/collection_lens.dart rename to lib/model/source/collection_lens.dart index 7fe7dfe79..ac62fc887 100644 --- a/lib/model/collection_lens.dart +++ b/lib/model/source/collection_lens.dart @@ -1,11 +1,12 @@ import 'dart:async'; import 'dart:collection'; -import 'package:aves/model/collection_source.dart'; 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.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/source/tag.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/change_notifier.dart'; import 'package:collection/collection.dart'; @@ -125,7 +126,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel } void _applyFilters() { - final rawEntries = source.entries; + final rawEntries = source.rawEntries; _filteredEntries = List.of(filters.isEmpty ? rawEntries : rawEntries.where((entry) => filters.fold(true, (prev, filter) => prev && filter.filter(entry)))); } diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart new file mode 100644 index 000000000..47e52a317 --- /dev/null +++ b/lib/model/source/collection_source.dart @@ -0,0 +1,115 @@ +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/image_metadata.dart'; +import 'package:aves/model/metadata_db.dart'; +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:event_bus/event_bus.dart'; +import 'package:flutter/foundation.dart'; + +mixin SourceBase { + final List _rawEntries = []; + + List get rawEntries => List.unmodifiable(_rawEntries); + + final EventBus _eventBus = EventBus(); + + EventBus get eventBus => _eventBus; + + List get sortedEntriesForFilterList; + + final Map _filterEntryCountMap = {}; + + void invalidateFilterEntryCounts() => _filterEntryCountMap.clear(); +} + +class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin { + @override + List get sortedEntriesForFilterList => CollectionLens( + source: this, + groupFactor: GroupFactor.month, + sortFactor: SortFactor.date, + ).sortedEntries; + + ValueNotifier stateNotifier = ValueNotifier(SourceState.ready); + + List _savedDates; + + Future loadDates() async { + final stopwatch = Stopwatch()..start(); + _savedDates = List.unmodifiable(await metadataDb.loadDates()); + debugPrint('$runtimeType loadDates complete in ${stopwatch.elapsed.inMilliseconds}ms for ${_savedDates.length} saved entries'); + } + + void addAll(Iterable entries) { + entries.forEach((entry) { + final contentId = entry.contentId; + entry.catalogDateMillis = _savedDates.firstWhere((metadata) => metadata.contentId == contentId, orElse: () => null)?.dateMillis; + }); + _rawEntries.addAll(entries); + addFolderPath(_rawEntries.map((entry) => entry.directory)); + invalidateFilterEntryCounts(); + eventBus.fire(const EntryAddedEvent()); + } + + void removeEntries(Iterable entries) { + entries.forEach((entry) => entry.removeFromFavourites()); + _rawEntries.removeWhere(entries.contains); + // TODO TLAD invalidate locations/tags, like cleaning albums + cleanEmptyAlbums(entries.map((entry) => entry.directory).toSet()); + invalidateFilterEntryCounts(); + eventBus.fire(EntryRemovedEvent(entries)); + } + + void clearEntries() { + _rawEntries.clear(); + cleanEmptyAlbums(); + updateAlbums(); + updateLocations(); + updateTags(); + invalidateFilterEntryCounts(); + } + + void applyMove({ + @required Iterable entries, + @required Set fromAlbums, + @required String toAlbum, + @required bool copy, + }) { + if (copy) { + addAll(entries); + } else { + cleanEmptyAlbums(fromAlbums); + addFolderPath({toAlbum}); + } + updateAlbums(); + invalidateFilterEntryCounts(); + eventBus.fire(EntryMovedEvent(entries)); + } + + int count(CollectionFilter filter) { + return _filterEntryCountMap.putIfAbsent(filter, () => _rawEntries.where((entry) => filter.filter(entry)).length); + } +} + +enum SourceState { loading, cataloguing, locating, ready } + +class EntryAddedEvent { + final ImageEntry entry; + + const EntryAddedEvent([this.entry]); +} + +class EntryRemovedEvent { + final Iterable entries; + + const EntryRemovedEvent(this.entries); +} + +class EntryMovedEvent { + final Iterable entries; + + const EntryMovedEvent(this.entries); +} diff --git a/lib/model/source/location.dart b/lib/model/source/location.dart new file mode 100644 index 000000000..87ab77b6a --- /dev/null +++ b/lib/model/source/location.dart @@ -0,0 +1,73 @@ +import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/image_metadata.dart'; +import 'package:aves/model/metadata_db.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; + +mixin LocationMixin on SourceBase { + List sortedCountries = List.unmodifiable([]); + List sortedPlaces = List.unmodifiable([]); + + Future loadAddresses() async { + final stopwatch = Stopwatch()..start(); + final saved = await metadataDb.loadAddresses(); + rawEntries.forEach((entry) { + final contentId = entry.contentId; + entry.addressDetails = saved.firstWhere((address) => address.contentId == contentId, orElse: () => null); + }); + debugPrint('$runtimeType loadAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms for ${saved.length} saved entries'); + onAddressMetadataChanged(); + } + + Future locateEntries() async { + final stopwatch = Stopwatch()..start(); + final unlocatedEntries = rawEntries.where((entry) => entry.hasGps && !entry.isLocated).toList(); + final newAddresses = []; + await Future.forEach(unlocatedEntries, (entry) async { + await entry.locate(); + if (entry.isLocated) { + newAddresses.add(entry.addressDetails); + if (newAddresses.length >= 50) { + await metadataDb.saveAddresses(List.unmodifiable(newAddresses)); + newAddresses.clear(); + } + } + }); + await metadataDb.saveAddresses(List.unmodifiable(newAddresses)); + onAddressMetadataChanged(); + debugPrint('$runtimeType locateEntries complete in ${stopwatch.elapsed.inMilliseconds}ms'); + } + + void onAddressMetadataChanged() { + updateLocations(); + eventBus.fire(AddressMetadataChangedEvent()); + } + + void updateLocations() { + final locations = rawEntries.where((entry) => entry.isLocated).map((entry) => entry.addressDetails).toList(); + final lister = (String Function(AddressDetails a) f) => List.unmodifiable(locations.map(f).where((s) => s != null && s.isNotEmpty).toSet().toList()..sort(compareAsciiUpperCase)); + sortedCountries = lister((address) => '${address.countryName};${address.countryCode}'); + sortedPlaces = lister((address) => address.place); + + invalidateFilterEntryCounts(); + eventBus.fire(LocationsChangedEvent()); + } + + Map getCountryEntries() { + final locatedEntries = sortedEntriesForFilterList.where((entry) => entry.isLocated); + return Map.fromEntries(sortedCountries.map((countryNameAndCode) { + final split = countryNameAndCode.split(';'); + ImageEntry entry; + if (split.length > 1) { + final countryCode = split[1]; + entry = locatedEntries.firstWhere((entry) => entry.addressDetails.countryCode == countryCode, orElse: () => null); + } + return MapEntry(countryNameAndCode, entry); + })); + } +} + +class AddressMetadataChangedEvent {} + +class LocationsChangedEvent {} diff --git a/lib/model/source/tag.dart b/lib/model/source/tag.dart new file mode 100644 index 000000000..ede78d9af --- /dev/null +++ b/lib/model/source/tag.dart @@ -0,0 +1,66 @@ +import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/image_metadata.dart'; +import 'package:aves/model/metadata_db.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; + +mixin TagMixin on SourceBase { + List sortedTags = List.unmodifiable([]); + + Future loadCatalogMetadata() async { + final stopwatch = Stopwatch()..start(); + final saved = await metadataDb.loadMetadataEntries(); + rawEntries.forEach((entry) { + final contentId = entry.contentId; + entry.catalogMetadata = saved.firstWhere((metadata) => metadata.contentId == contentId, orElse: () => null); + }); + debugPrint('$runtimeType loadCatalogMetadata complete in ${stopwatch.elapsed.inMilliseconds}ms for ${saved.length} saved entries'); + onCatalogMetadataChanged(); + } + + Future catalogEntries() async { + final stopwatch = Stopwatch()..start(); + final uncataloguedEntries = rawEntries.where((entry) => !entry.isCatalogued).toList(); + if (uncataloguedEntries.isEmpty) return; + + final newMetadata = []; + await Future.forEach(uncataloguedEntries, (entry) async { + await entry.catalog(); + if (entry.isCatalogued) { + newMetadata.add(entry.catalogMetadata); + if (newMetadata.length >= 500) { + await metadataDb.saveMetadata(List.unmodifiable(newMetadata)); + newMetadata.clear(); + } + } + }); + await metadataDb.saveMetadata(List.unmodifiable(newMetadata)); + onCatalogMetadataChanged(); + debugPrint('$runtimeType catalogEntries complete in ${stopwatch.elapsed.inSeconds}s with ${newMetadata.length} new entries'); + } + + void onCatalogMetadataChanged() { + updateTags(); + eventBus.fire(CatalogMetadataChangedEvent()); + } + + void updateTags() { + final tags = rawEntries.expand((entry) => entry.xmpSubjects).toSet().toList()..sort(compareAsciiUpperCase); + sortedTags = List.unmodifiable(tags); + invalidateFilterEntryCounts(); + eventBus.fire(TagsChangedEvent()); + } + + Map getTagEntries() { + final entries = sortedEntriesForFilterList; + return Map.fromEntries(sortedTags.map((tag) => MapEntry( + tag, + entries.firstWhere((entry) => entry.xmpSubjects.contains(tag), orElse: () => null), + ))); + } +} + +class CatalogMetadataChangedEvent {} + +class TagsChangedEvent {} diff --git a/lib/services/image_file_service.dart b/lib/services/image_file_service.dart index 5cbb68a29..3b94c201e 100644 --- a/lib/services/image_file_service.dart +++ b/lib/services/image_file_service.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; -import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/services/service_policy.dart'; import 'package:flutter/foundation.dart'; @@ -12,18 +11,17 @@ import 'package:streams_channel/streams_channel.dart'; class ImageFileService { static const platform = MethodChannel('deckers.thibault/aves/image'); + static final StreamsChannel mediaStoreChannel = StreamsChannel('deckers.thibault/aves/mediastorestream'); static final StreamsChannel byteChannel = StreamsChannel('deckers.thibault/aves/imagebytestream'); static final StreamsChannel opChannel = StreamsChannel('deckers.thibault/aves/imageopstream'); static const double thumbnailDefaultSize = 64.0; - static Future getImageEntries(SortFactor sort, GroupFactor group) async { + static Stream getImageEntries() { try { - await platform.invokeMethod('getImageEntries', { - 'sort': sort.toString().replaceAll('SortFactor.', ''), - 'group': group.toString().replaceAll('GroupFactor.', ''), - }); + return mediaStoreChannel.receiveBroadcastStream().map((event) => ImageEntry.fromMap(event)); } on PlatformException catch (e) { debugPrint('getImageEntries failed with code=${e.code}, exception=${e.message}, details=${e.details}'); + return Stream.error(e); } } diff --git a/lib/widgets/album/app_bar.dart b/lib/widgets/album/app_bar.dart index efc6cf76a..3710b9ee2 100644 --- a/lib/widgets/album/app_bar.dart +++ b/lib/widgets/album/app_bar.dart @@ -1,14 +1,15 @@ import 'dart:async'; import 'package:aves/main.dart'; -import 'package:aves/model/collection_lens.dart'; -import 'package:aves/model/collection_source.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings.dart'; +import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/model/source/collection_source.dart'; import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/album/filter_bar.dart'; import 'package:aves/widgets/album/search/search_delegate.dart'; import 'package:aves/widgets/common/action_delegates/selection_action_delegate.dart'; +import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart'; import 'package:aves/widgets/common/entry_actions.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/common/menu_row.dart'; @@ -210,10 +211,15 @@ class _CollectionAppBarState extends State with SingleTickerPr ..._buildGroupMenuItems(), if (collection.isBrowsing) ...[ if (AvesApp.mode == AppMode.main) - const PopupMenuItem( - value: CollectionAction.select, - child: MenuRow(text: 'Select', icon: AIcons.select), - ), + if (kDebugMode) + const PopupMenuItem( + value: CollectionAction.refresh, + child: MenuRow(text: 'Refresh', icon: AIcons.refresh), + ), + const PopupMenuItem( + value: CollectionAction.select, + child: MenuRow(text: 'Select', icon: AIcons.select), + ), const PopupMenuItem( value: CollectionAction.stats, child: MenuRow(text: 'Stats', icon: AIcons.stats), @@ -303,6 +309,13 @@ class _CollectionAppBarState extends State with SingleTickerPr case CollectionAction.move: _actionDelegate.onCollectionActionSelected(context, action); break; + case CollectionAction.refresh: + final source = collection.source; + if (source is MediaStoreSource) { + source.clearEntries(); + unawaited(source.refresh()); + } + break; case CollectionAction.select: collection.select(); break; @@ -361,7 +374,21 @@ class _CollectionAppBarState extends State with SingleTickerPr } } -enum CollectionAction { copy, move, select, selectAll, selectNone, stats, groupByAlbum, groupByMonth, groupByDay, sortByDate, sortBySize, sortByName } +enum CollectionAction { + copy, + move, + refresh, + select, + selectAll, + selectNone, + stats, + groupByAlbum, + groupByMonth, + groupByDay, + sortByDate, + sortBySize, + sortByName, +} class SourceStateSubtitle extends StatefulWidget { final CollectionSource source; @@ -379,7 +406,7 @@ class _SourceStateSubtitleState extends State { SourceState get sourceState => source.stateNotifier.value; - List get entries => source.entries; + List get entries => source.rawEntries; @override void initState() { diff --git a/lib/widgets/album/collection_page.dart b/lib/widgets/album/collection_page.dart index 2fd6c628d..d1c942e0a 100644 --- a/lib/widgets/album/collection_page.dart +++ b/lib/widgets/album/collection_page.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/collection_lens.dart'; +import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/album/thumbnail_collection.dart'; import 'package:aves/widgets/app_drawer.dart'; import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart'; diff --git a/lib/widgets/album/grid/header_generic.dart b/lib/widgets/album/grid/header_generic.dart index bca038fd8..f30ff4554 100644 --- a/lib/widgets/album/grid/header_generic.dart +++ b/lib/widgets/album/grid/header_generic.dart @@ -1,7 +1,7 @@ import 'dart:math'; -import 'package:aves/model/collection_lens.dart'; -import 'package:aves/model/collection_source.dart'; +import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/model/source/collection_source.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/utils/durations.dart'; diff --git a/lib/widgets/album/grid/list_section_layout.dart b/lib/widgets/album/grid/list_section_layout.dart index 2f321aa33..93f99fbb8 100644 --- a/lib/widgets/album/grid/list_section_layout.dart +++ b/lib/widgets/album/grid/list_section_layout.dart @@ -1,7 +1,7 @@ import 'dart:math'; -import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/album/grid/header_generic.dart'; import 'package:aves/widgets/album/grid/tile_extent_manager.dart'; import 'package:collection/collection.dart'; diff --git a/lib/widgets/album/grid/list_sliver.dart b/lib/widgets/album/grid/list_sliver.dart index ed565892e..aea48ce9d 100644 --- a/lib/widgets/album/grid/list_sliver.dart +++ b/lib/widgets/album/grid/list_sliver.dart @@ -1,6 +1,6 @@ import 'package:aves/main.dart'; -import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/services/viewer_service.dart'; import 'package:aves/widgets/album/grid/list_known_extent.dart'; import 'package:aves/widgets/album/grid/list_section_layout.dart'; diff --git a/lib/widgets/album/search/search_delegate.dart b/lib/widgets/album/search/search_delegate.dart index 7c3fb7b18..d770779cc 100644 --- a/lib/widgets/album/search/search_delegate.dart +++ b/lib/widgets/album/search/search_delegate.dart @@ -1,4 +1,3 @@ -import 'package:aves/model/collection_source.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/filters.dart'; @@ -7,6 +6,10 @@ import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/mime_types.dart'; +import 'package:aves/model/source/album.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/source/location.dart'; +import 'package:aves/model/source/tag.dart'; import 'package:aves/widgets/album/search/expandable_filter_row.dart'; import 'package:aves/widgets/common/aves_filter_chip.dart'; import 'package:aves/widgets/common/icons.dart'; diff --git a/lib/widgets/album/thumbnail/decorated.dart b/lib/widgets/album/thumbnail/decorated.dart index 728a2b505..c283be3fe 100644 --- a/lib/widgets/album/thumbnail/decorated.dart +++ b/lib/widgets/album/thumbnail/decorated.dart @@ -1,5 +1,5 @@ -import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/album/thumbnail/overlay.dart'; import 'package:aves/widgets/album/thumbnail/raster.dart'; import 'package:aves/widgets/album/thumbnail/vector.dart'; diff --git a/lib/widgets/album/thumbnail/overlay.dart b/lib/widgets/album/thumbnail/overlay.dart index ac6f6801f..a29f8bc72 100644 --- a/lib/widgets/album/thumbnail/overlay.dart +++ b/lib/widgets/album/thumbnail/overlay.dart @@ -1,7 +1,7 @@ import 'dart:math'; -import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/common/fx/sweeper.dart'; import 'package:aves/widgets/common/icons.dart'; diff --git a/lib/widgets/album/thumbnail_collection.dart b/lib/widgets/album/thumbnail_collection.dart index adc6e7cfc..8d0a417e7 100644 --- a/lib/widgets/album/thumbnail_collection.dart +++ b/lib/widgets/album/thumbnail_collection.dart @@ -1,10 +1,10 @@ import 'dart:async'; -import 'package:aves/model/collection_lens.dart'; -import 'package:aves/model/collection_source.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/mime_types.dart'; +import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/model/source/collection_source.dart'; import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/album/app_bar.dart'; import 'package:aves/widgets/album/empty.dart'; diff --git a/lib/widgets/app_drawer.dart b/lib/widgets/app_drawer.dart index 6598801c5..ecd07614a 100644 --- a/lib/widgets/app_drawer.dart +++ b/lib/widgets/app_drawer.dart @@ -1,7 +1,5 @@ import 'dart:ui'; -import 'package:aves/model/collection_lens.dart'; -import 'package:aves/model/collection_source.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/filters.dart'; @@ -10,6 +8,11 @@ import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/mime_types.dart'; import 'package:aves/model/settings.dart'; +import 'package:aves/model/source/album.dart'; +import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/source/location.dart'; +import 'package:aves/model/source/tag.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/about/about_page.dart'; import 'package:aves/widgets/album/collection_page.dart'; diff --git a/lib/widgets/common/action_delegates/entry_action_delegate.dart b/lib/widgets/common/action_delegates/entry_action_delegate.dart index 3baed655e..d64d061f9 100644 --- a/lib/widgets/common/action_delegates/entry_action_delegate.dart +++ b/lib/widgets/common/action_delegates/entry_action_delegate.dart @@ -1,7 +1,7 @@ import 'dart:io'; -import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:aves/widgets/common/action_delegates/feedback.dart'; diff --git a/lib/widgets/common/action_delegates/selection_action_delegate.dart b/lib/widgets/common/action_delegates/selection_action_delegate.dart index 56cda1c5a..0f301f0a9 100644 --- a/lib/widgets/common/action_delegates/selection_action_delegate.dart +++ b/lib/widgets/common/action_delegates/selection_action_delegate.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:aves/model/collection_lens.dart'; +import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/favourite_repo.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/image_entry.dart'; diff --git a/lib/widgets/common/data_providers/media_store_collection_provider.dart b/lib/widgets/common/data_providers/media_store_collection_provider.dart index d43678af1..e32bf0a1d 100644 --- a/lib/widgets/common/data_providers/media_store_collection_provider.dart +++ b/lib/widgets/common/data_providers/media_store_collection_provider.dart @@ -1,23 +1,18 @@ import 'dart:math'; -import 'package:aves/model/collection_source.dart'; import 'package:aves/model/favourite_repo.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/settings.dart'; +import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_native_timezone/flutter_native_timezone.dart'; -class MediaStoreSource { - final CollectionSource source = CollectionSource(); - - static const EventChannel _eventChannel = EventChannel('deckers.thibault/aves/mediastore'); - - Future fetch() async { +class MediaStoreSource extends CollectionSource { + Future init() async { final stopwatch = Stopwatch()..start(); - source.stateNotifier.value = SourceState.loading; + stateNotifier.value = SourceState.loading; await metadataDb.init(); // <20ms await favourites.init(); final currentTimeZone = await FlutterNativeTimezone.getLocalTimezone(); // <20ms @@ -29,40 +24,44 @@ class MediaStoreSource { await metadataDb.clearMetadataEntries(); settings.catalogTimeZone = currentTimeZone; } - await source.loadDates(); // 100ms for 5400 entries + await loadDates(); // 100ms for 5400 entries + debugPrint('$runtimeType init done, elapsed=${stopwatch.elapsed}'); + } + + Future refresh() async { + final stopwatch = Stopwatch()..start(); + stateNotifier.value = SourceState.loading; var refreshCount = 10; const refreshCountMax = 1000; final allEntries = []; - _eventChannel.receiveBroadcastStream().cast().listen( - (entryMap) { - allEntries.add(ImageEntry.fromMap(entryMap)); + + // TODO split image fetch AND/OR cache fetch across sessions + ImageFileService.getImageEntries().listen( + (entry) { + allEntries.add(entry); if (allEntries.length >= refreshCount) { refreshCount = min(refreshCount * 10, refreshCountMax); - source.addAll(allEntries); + addAll(allEntries); allEntries.clear(); -// debugPrint('$runtimeType streamed ${_source.entries.length} entries at ${stopwatch.elapsed.inMilliseconds}ms'); +// debugPrint('$runtimeType streamed ${entries.length} entries at ${stopwatch.elapsed.inMilliseconds}ms'); } }, onDone: () async { - debugPrint('$runtimeType stream complete at ${stopwatch.elapsed.inMilliseconds}ms'); - source.addAll(allEntries); + debugPrint('$runtimeType stream done, elapsed=${stopwatch.elapsed}'); + addAll(allEntries); // TODO reduce setup time until here - source.updateAlbums(); // <50ms - source.stateNotifier.value = SourceState.cataloguing; - await source.loadCatalogMetadata(); // 400ms for 5400 entries - await source.catalogEntries(); // <50ms - source.stateNotifier.value = SourceState.locating; - await source.loadAddresses(); // 350ms - await source.locateEntries(); // <50ms - source.stateNotifier.value = SourceState.ready; - debugPrint('$runtimeType setup end, elapsed=${stopwatch.elapsed}'); + updateAlbums(); // <50ms + stateNotifier.value = SourceState.cataloguing; + await loadCatalogMetadata(); // 400ms for 5400 entries + await catalogEntries(); // <50ms + stateNotifier.value = SourceState.locating; + await loadAddresses(); // 350ms + await locateEntries(); // <50ms + stateNotifier.value = SourceState.ready; + debugPrint('$runtimeType refresh done, elapsed=${stopwatch.elapsed}'); }, - onError: (error) => debugPrint('$runtimeType mediastore stream error=$error'), + onError: (error) => debugPrint('$runtimeType stream error=$error'), ); - - // TODO split image fetch AND/OR cache fetch across sessions - debugPrint('$runtimeType stream start at ${stopwatch.elapsed.inMilliseconds}ms'); - await ImageFileService.getImageEntries(settings.collectionSortFactor, settings.collectionGroupFactor); // 460ms } } diff --git a/lib/widgets/common/icons.dart b/lib/widgets/common/icons.dart index 4ddd2da0f..64ddf399f 100644 --- a/lib/widgets/common/icons.dart +++ b/lib/widgets/common/icons.dart @@ -35,6 +35,7 @@ class AIcons { static const IconData info = OMIcons.info; static const IconData openInNew = OMIcons.openInNew; static const IconData print = OMIcons.print; + static const IconData refresh = OMIcons.refresh; static const IconData rename = OMIcons.title; static const IconData rotateLeft = OMIcons.rotateLeft; static const IconData rotateRight = OMIcons.rotateRight; diff --git a/lib/widgets/debug_page.dart b/lib/widgets/debug_page.dart index aae7bba1b..f9f441739 100644 --- a/lib/widgets/debug_page.dart +++ b/lib/widgets/debug_page.dart @@ -1,6 +1,6 @@ import 'dart:collection'; -import 'package:aves/model/collection_source.dart'; +import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/favourite_repo.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_metadata.dart'; @@ -37,7 +37,7 @@ class DebugPageState extends State { Future>> _volumePermissionLoader; Future _envLoader; - List get entries => widget.source.entries; + List get entries => widget.source.rawEntries; @override void initState() { diff --git a/lib/widgets/filter_grid_page.dart b/lib/widgets/filter_grid_page.dart index 42e7fdbe1..30129ac84 100644 --- a/lib/widgets/filter_grid_page.dart +++ b/lib/widgets/filter_grid_page.dart @@ -1,11 +1,11 @@ import 'dart:ui'; -import 'package:aves/model/collection_lens.dart'; -import 'package:aves/model/collection_source.dart'; 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.dart'; +import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/model/source/collection_source.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/album/collection_page.dart'; diff --git a/lib/widgets/fullscreen/fullscreen_body.dart b/lib/widgets/fullscreen/fullscreen_body.dart index 81c6aea33..4ed9df9a1 100644 --- a/lib/widgets/fullscreen/fullscreen_body.dart +++ b/lib/widgets/fullscreen/fullscreen_body.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'dart:math'; -import 'package:aves/model/collection_lens.dart'; +import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/utils/change_notifier.dart'; diff --git a/lib/widgets/fullscreen/fullscreen_page.dart b/lib/widgets/fullscreen/fullscreen_page.dart index cf7bd78fa..32ddf1507 100644 --- a/lib/widgets/fullscreen/fullscreen_page.dart +++ b/lib/widgets/fullscreen/fullscreen_page.dart @@ -1,5 +1,5 @@ -import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart'; import 'package:aves/widgets/fullscreen/fullscreen_body.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/fullscreen/image_page.dart b/lib/widgets/fullscreen/image_page.dart index fdd606d53..b9ff3042c 100644 --- a/lib/widgets/fullscreen/image_page.dart +++ b/lib/widgets/fullscreen/image_page.dart @@ -1,5 +1,5 @@ -import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/fullscreen/image_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; diff --git a/lib/widgets/fullscreen/info/basic_section.dart b/lib/widgets/fullscreen/info/basic_section.dart index 3c6160b0c..ffb00956d 100644 --- a/lib/widgets/fullscreen/info/basic_section.dart +++ b/lib/widgets/fullscreen/info/basic_section.dart @@ -1,4 +1,3 @@ -import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/favourite_repo.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/favourite.dart'; @@ -6,6 +5,7 @@ import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/mime_types.dart'; +import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/common/aves_filter_chip.dart'; import 'package:aves/widgets/fullscreen/info/info_page.dart'; diff --git a/lib/widgets/fullscreen/info/info_page.dart b/lib/widgets/fullscreen/info/info_page.dart index bb8ce3594..1b10df6bd 100644 --- a/lib/widgets/fullscreen/info/info_page.dart +++ b/lib/widgets/fullscreen/info/info_page.dart @@ -1,6 +1,6 @@ -import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/common/aves_filter_chip.dart'; import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart'; diff --git a/lib/widgets/fullscreen/info/location_section.dart b/lib/widgets/fullscreen/info/location_section.dart index 180f1442d..c0da8ccc8 100644 --- a/lib/widgets/fullscreen/info/location_section.dart +++ b/lib/widgets/fullscreen/info/location_section.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/collection_lens.dart'; +import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/filters/location.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings.dart'; diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart new file mode 100644 index 000000000..bd513f6f0 --- /dev/null +++ b/lib/widgets/home_page.dart @@ -0,0 +1,114 @@ +import 'package:aves/main.dart'; +import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/settings.dart'; +import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/services/image_file_service.dart'; +import 'package:aves/services/viewer_service.dart'; +import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/widgets/album/collection_page.dart'; +import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart'; +import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/widgets/fullscreen/fullscreen_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:pedantic/pedantic.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:screen/screen.dart'; + +class HomePage extends StatefulWidget { + const HomePage(); + + @override + _HomePageState createState() => _HomePageState(); +} + +class _HomePageState extends State { + MediaStoreSource _mediaStore; + ImageEntry _viewerEntry; + Future _appSetup; + + @override + void initState() { + super.initState(); + _appSetup = _setup(); + imageCache.maximumSizeBytes = 512 * (1 << 20); + Screen.keepOn(true); + } + + Future _setup() async { + debugPrint('$runtimeType _setup'); + + // TODO reduce permission check time + final permissions = await [ + Permission.storage, + // to access media with unredacted metadata with scoped storage (Android 10+) + Permission.accessMediaLocation, + ].request(); // 350ms + if (permissions[Permission.storage] != PermissionStatus.granted) { + unawaited(SystemNavigator.pop()); + return; + } + + // TODO notify when icons are ready for drawer and section header refresh + await androidFileUtils.init(); // 170ms + + final intentData = await ViewerService.getIntentData(); + if (intentData != null) { + final action = intentData['action']; + switch (action) { + case 'view': + AvesApp.mode = AppMode.view; + _viewerEntry = await _initViewerEntry( + uri: intentData['uri'], + mimeType: intentData['mimeType'], + ); + if (_viewerEntry == null) { + // fallback to default mode when we fail to retrieve the entry + AvesApp.mode = AppMode.main; + } + break; + case 'pick': + AvesApp.mode = AppMode.pick; + break; + } + } + + if (AvesApp.mode != AppMode.view) { + _mediaStore = MediaStoreSource(); + await _mediaStore.init(); + unawaited(_mediaStore.refresh()); + } + } + + Future _initViewerEntry({@required String uri, @required String mimeType}) async { + final entry = await ImageFileService.getImageEntry(uri, mimeType); + if (entry != null) { + // cataloguing is essential for geolocation and video rotation + await entry.catalog(); + unawaited(entry.locate()); + } + return entry; + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _appSetup, + builder: (context, AsyncSnapshot snapshot) { + if (snapshot.hasError) return const Icon(AIcons.error); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); + debugPrint('$runtimeType app setup future complete'); + if (AvesApp.mode == AppMode.view) { + return SingleFullscreenPage(entry: _viewerEntry); + } + if (_mediaStore != null) { + return CollectionPage(CollectionLens( + source: _mediaStore, + groupFactor: settings.collectionGroupFactor, + sortFactor: settings.collectionSortFactor, + )); + } + return const SizedBox.shrink(); + }); + } +} diff --git a/lib/widgets/stats/filter_table.dart b/lib/widgets/stats/filter_table.dart index 2a747f013..6234c72c7 100644 --- a/lib/widgets/stats/filter_table.dart +++ b/lib/widgets/stats/filter_table.dart @@ -1,5 +1,5 @@ -import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/utils/color_utils.dart'; import 'package:aves/widgets/album/collection_page.dart'; import 'package:aves/widgets/common/aves_filter_chip.dart'; diff --git a/lib/widgets/stats/stats.dart b/lib/widgets/stats/stats.dart index a887d193d..3030a1b4f 100644 --- a/lib/widgets/stats/stats.dart +++ b/lib/widgets/stats/stats.dart @@ -1,6 +1,6 @@ import 'dart:math'; -import 'package:aves/model/collection_lens.dart'; +import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/tag.dart'; diff --git a/lib/widgets/welcome.dart b/lib/widgets/welcome_page.dart similarity index 99% rename from lib/widgets/welcome.dart rename to lib/widgets/welcome_page.dart index 0a536c9a7..4a28a55c6 100644 --- a/lib/widgets/welcome.dart +++ b/lib/widgets/welcome_page.dart @@ -1,9 +1,9 @@ -import 'package:aves/main.dart'; import 'package:aves/model/settings.dart'; import 'package:aves/model/terms.dart'; import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/common/aves_logo.dart'; import 'package:aves/widgets/common/labeled_checkbox.dart'; +import 'package:aves/widgets/home_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';