media store fetch by stream handler, collection source split in mixins
This commit is contained in:
parent
049840bd73
commit
b170ce0492
49 changed files with 633 additions and 579 deletions
|
@ -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) -> {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package deckers.thibault.aves.channelhandlers;
|
||||
package deckers.thibault.aves.channel.calls;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
|
@ -1,4 +1,4 @@
|
|||
package deckers.thibault.aves.channelhandlers;
|
||||
package deckers.thibault.aves.channel.calls;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
|
@ -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<String, Object> 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<String, Object> 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<String, Object> entryMap = call.argument("entry");
|
||||
Boolean clockwise = call.argument("clockwise");
|
||||
if (entryMap == null || clockwise == null) {
|
||||
result.error("rotate-args", "failed because of missing arguments", null);
|
|
@ -1,4 +1,4 @@
|
|||
package deckers.thibault.aves.channelhandlers;
|
||||
package deckers.thibault.aves.channel.calls;
|
||||
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
|
@ -1,4 +1,4 @@
|
|||
package deckers.thibault.aves.channelhandlers;
|
||||
package deckers.thibault.aves.channel.calls;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
|
@ -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<Map<String, Object>> volumes = null;
|
||||
List<Map<String, Object>> volumes;
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
|
||||
volumes = getStorageVolumes();
|
||||
} else {
|
|
@ -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<String, Object> argMap = (Map<String, Object>) arguments;
|
||||
this.mimeType = (String) argMap.get("mimeType");
|
||||
this.uri = Uri.parse((String) argMap.get("uri"));
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package deckers.thibault.aves.channelhandlers;
|
||||
package deckers.thibault.aves.channel.streams;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
|
@ -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<String, Object> result) {
|
||||
handler.post(() -> eventSink.success(result));
|
||||
}
|
||||
|
||||
private void endOfStream() {
|
||||
handler.post(() -> eventSink.endOfStream());
|
||||
}
|
||||
|
||||
void fetchAll() {
|
||||
new MediaStoreImageProvider().fetchAll(activity, this::success); // 350ms
|
||||
endOfStream();
|
||||
}
|
||||
}
|
|
@ -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<String, Object> argMap = (Map<String, Object>) 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());
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -68,36 +68,13 @@ 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;
|
||||
}
|
||||
|
||||
fetchFrom(activity, newEntryHandler, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, IMAGE_PROJECTION, orderBy);
|
||||
fetchFrom(activity, newEntryHandler, MediaStore.Video.Media.EXTERNAL_CONTENT_URI, VIDEO_PROJECTION, orderBy);
|
||||
|
|
|
@ -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;
|
||||
|
|
111
lib/main.dart
111
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<AvesApp> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage();
|
||||
|
||||
@override
|
||||
_HomePageState createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
MediaStoreSource _mediaStore;
|
||||
ImageEntry _viewerEntry;
|
||||
Future<void> _appSetup;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_appSetup = _setup();
|
||||
imageCache.maximumSizeBytes = 512 * (1 << 20);
|
||||
Screen.keepOn(true);
|
||||
}
|
||||
|
||||
Future<void> _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<ImageEntry> _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<void> 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ImageEntry> _rawEntries;
|
||||
final Set<String> _folderPaths = {};
|
||||
final Map<CollectionFilter, int> _filterEntryCountMap = {};
|
||||
final EventBus _eventBus = EventBus();
|
||||
|
||||
List<String> sortedAlbums = List.unmodifiable([]);
|
||||
List<String> sortedCountries = List.unmodifiable([]);
|
||||
List<String> sortedPlaces = List.unmodifiable([]);
|
||||
List<String> sortedTags = List.unmodifiable([]);
|
||||
ValueNotifier<SourceState> stateNotifier = ValueNotifier<SourceState>(SourceState.ready);
|
||||
|
||||
List<ImageEntry> get entries => List.unmodifiable(_rawEntries);
|
||||
|
||||
EventBus get eventBus => _eventBus;
|
||||
|
||||
int get albumCount => sortedAlbums.length;
|
||||
|
||||
int get tagCount => sortedTags.length;
|
||||
|
||||
CollectionSource({
|
||||
List<ImageEntry> entries,
|
||||
}) : _rawEntries = entries ?? [];
|
||||
|
||||
final List<DateMetadata> savedDates = [];
|
||||
|
||||
Future<void> 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<void> 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<void> 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<void> catalogEntries() async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final uncataloguedEntries = _rawEntries.where((entry) => !entry.isCatalogued).toList();
|
||||
if (uncataloguedEntries.isEmpty) return;
|
||||
|
||||
final newMetadata = <CatalogMetadata>[];
|
||||
await Future.forEach<ImageEntry>(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<void> locateEntries() async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final unlocatedEntries = _rawEntries.where((entry) => entry.hasGps && !entry.isLocated).toList();
|
||||
final newAddresses = <AddressDetails>[];
|
||||
await Future.forEach<ImageEntry>(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<String>.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<ImageEntry> 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<ImageEntry> 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<ImageEntry> entries,
|
||||
@required Set<String> 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<String> 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<ImageEntry> get _sortedEntriesForFilterList => CollectionLens(
|
||||
source: this,
|
||||
groupFactor: GroupFactor.month,
|
||||
sortFactor: SortFactor.date,
|
||||
).sortedEntries;
|
||||
|
||||
Map<String, ImageEntry> getAlbumEntries() {
|
||||
final entries = _sortedEntriesForFilterList;
|
||||
final regularAlbums = <String>[], appAlbums = <String>[], specialAlbums = <String>[];
|
||||
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<String, ImageEntry> 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<String, ImageEntry> 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<ImageEntry> entries;
|
||||
|
||||
const EntryRemovedEvent(this.entries);
|
||||
}
|
||||
|
||||
class EntryMovedEvent {
|
||||
final Iterable<ImageEntry> entries;
|
||||
|
||||
const EntryMovedEvent(this.entries);
|
||||
}
|
|
@ -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';
|
||||
|
|
71
lib/model/source/album.dart
Normal file
71
lib/model/source/album.dart
Normal file
|
@ -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<String> _folderPaths = {};
|
||||
|
||||
List<String> 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<String, ImageEntry> getAlbumEntries() {
|
||||
final entries = sortedEntriesForFilterList;
|
||||
final regularAlbums = <String>[], appAlbums = <String>[], specialAlbums = <String>[];
|
||||
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<String> albums) => _folderPaths.addAll(albums);
|
||||
|
||||
void cleanEmptyAlbums([Set<String> 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 {}
|
|
@ -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))));
|
||||
}
|
||||
|
115
lib/model/source/collection_source.dart
Normal file
115
lib/model/source/collection_source.dart
Normal file
|
@ -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<ImageEntry> _rawEntries = [];
|
||||
|
||||
List<ImageEntry> get rawEntries => List.unmodifiable(_rawEntries);
|
||||
|
||||
final EventBus _eventBus = EventBus();
|
||||
|
||||
EventBus get eventBus => _eventBus;
|
||||
|
||||
List<ImageEntry> get sortedEntriesForFilterList;
|
||||
|
||||
final Map<CollectionFilter, int> _filterEntryCountMap = {};
|
||||
|
||||
void invalidateFilterEntryCounts() => _filterEntryCountMap.clear();
|
||||
}
|
||||
|
||||
class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin {
|
||||
@override
|
||||
List<ImageEntry> get sortedEntriesForFilterList => CollectionLens(
|
||||
source: this,
|
||||
groupFactor: GroupFactor.month,
|
||||
sortFactor: SortFactor.date,
|
||||
).sortedEntries;
|
||||
|
||||
ValueNotifier<SourceState> stateNotifier = ValueNotifier<SourceState>(SourceState.ready);
|
||||
|
||||
List<DateMetadata> _savedDates;
|
||||
|
||||
Future<void> 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<ImageEntry> 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<ImageEntry> 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<ImageEntry> entries,
|
||||
@required Set<String> 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<ImageEntry> entries;
|
||||
|
||||
const EntryRemovedEvent(this.entries);
|
||||
}
|
||||
|
||||
class EntryMovedEvent {
|
||||
final Iterable<ImageEntry> entries;
|
||||
|
||||
const EntryMovedEvent(this.entries);
|
||||
}
|
73
lib/model/source/location.dart
Normal file
73
lib/model/source/location.dart
Normal file
|
@ -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<String> sortedCountries = List.unmodifiable([]);
|
||||
List<String> sortedPlaces = List.unmodifiable([]);
|
||||
|
||||
Future<void> 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<void> locateEntries() async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final unlocatedEntries = rawEntries.where((entry) => entry.hasGps && !entry.isLocated).toList();
|
||||
final newAddresses = <AddressDetails>[];
|
||||
await Future.forEach<ImageEntry>(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<String>.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<String, ImageEntry> 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 {}
|
66
lib/model/source/tag.dart
Normal file
66
lib/model/source/tag.dart
Normal file
|
@ -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<String> sortedTags = List.unmodifiable([]);
|
||||
|
||||
Future<void> 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<void> catalogEntries() async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final uncataloguedEntries = rawEntries.where((entry) => !entry.isCatalogued).toList();
|
||||
if (uncataloguedEntries.isEmpty) return;
|
||||
|
||||
final newMetadata = <CatalogMetadata>[];
|
||||
await Future.forEach<ImageEntry>(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<String, ImageEntry> 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 {}
|
|
@ -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<void> getImageEntries(SortFactor sort, GroupFactor group) async {
|
||||
static Stream<ImageEntry> getImageEntries() {
|
||||
try {
|
||||
await platform.invokeMethod('getImageEntries', <String, dynamic>{
|
||||
'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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,6 +211,11 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
..._buildGroupMenuItems(),
|
||||
if (collection.isBrowsing) ...[
|
||||
if (AvesApp.mode == AppMode.main)
|
||||
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),
|
||||
|
@ -303,6 +309,13 @@ class _CollectionAppBarState extends State<CollectionAppBar> 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<CollectionAppBar> 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<SourceStateSubtitle> {
|
|||
|
||||
SourceState get sourceState => source.stateNotifier.value;
|
||||
|
||||
List<ImageEntry> get entries => source.entries;
|
||||
List<ImageEntry> get entries => source.rawEntries;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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<void> fetch() async {
|
||||
class MediaStoreSource extends CollectionSource {
|
||||
Future<void> 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<void> refresh() async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
stateNotifier.value = SourceState.loading;
|
||||
|
||||
var refreshCount = 10;
|
||||
const refreshCountMax = 1000;
|
||||
final allEntries = <ImageEntry>[];
|
||||
_eventChannel.receiveBroadcastStream().cast<Map>().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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<DebugPage> {
|
|||
Future<List<Tuple2<String, bool>>> _volumePermissionLoader;
|
||||
Future<Map> _envLoader;
|
||||
|
||||
List<ImageEntry> get entries => widget.source.entries;
|
||||
List<ImageEntry> get entries => widget.source.rawEntries;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
114
lib/widgets/home_page.dart
Normal file
114
lib/widgets/home_page.dart
Normal file
|
@ -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<HomePage> {
|
||||
MediaStoreSource _mediaStore;
|
||||
ImageEntry _viewerEntry;
|
||||
Future<void> _appSetup;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_appSetup = _setup();
|
||||
imageCache.maximumSizeBytes = 512 * (1 << 20);
|
||||
Screen.keepOn(true);
|
||||
}
|
||||
|
||||
Future<void> _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<ImageEntry> _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<void> 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();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
Loading…
Reference in a new issue