export: support embedded images

This commit is contained in:
Thibault Deckers 2021-01-25 12:43:04 +09:00
parent c4fdd38850
commit 218db5d091
18 changed files with 468 additions and 438 deletions

View file

@ -2,18 +2,27 @@ package deckers.thibault.aves.model.provider
import android.content.ContentUris import android.content.ContentUris
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.media.MediaScannerConnection import android.media.MediaScannerConnection
import android.net.Uri import android.net.Uri
import android.os.Build
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log import android.util.Log
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import com.commonsware.cwac.document.DocumentFileCompat import com.commonsware.cwac.document.DocumentFileCompat
import deckers.thibault.aves.decoder.MultiTrackImage
import deckers.thibault.aves.decoder.TiffImage
import deckers.thibault.aves.model.AvesEntry import deckers.thibault.aves.model.AvesEntry
import deckers.thibault.aves.model.ExifOrientationOp import deckers.thibault.aves.model.ExifOrientationOp
import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes.isImage import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.StorageUtils.copyFileToTemp import deckers.thibault.aves.utils.StorageUtils.copyFileToTemp
import deckers.thibault.aves.utils.StorageUtils.createDirectoryIfAbsent
import deckers.thibault.aves.utils.StorageUtils.getDocumentFile import deckers.thibault.aves.utils.StorageUtils.getDocumentFile
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
@ -36,8 +45,145 @@ abstract class ImageProvider {
callback.onFailure(UnsupportedOperationException()) callback.onFailure(UnsupportedOperationException())
} }
open suspend fun exportMultiple(context: Context, mimeType: String, destinationDir: String, entries: List<AvesEntry>, callback: ImageOpCallback) { suspend fun exportMultiple(
callback.onFailure(UnsupportedOperationException()) context: Context,
mimeType: String,
destinationDir: String,
entries: List<AvesEntry>,
callback: ImageOpCallback,
) {
val destinationDirDocFile = createDirectoryIfAbsent(context, destinationDir)
if (destinationDirDocFile == null) {
callback.onFailure(Exception("failed to create directory at path=$destinationDir"))
return
}
for (entry in entries) {
val sourceUri = entry.uri
val sourcePath = entry.path
val pageId = entry.pageId
val result = hashMapOf<String, Any?>(
"uri" to sourceUri.toString(),
"pageId" to pageId,
"success" to false,
)
try {
val newFields = exportSingleByTreeDocAndScan(
context = context,
sourceEntry = entry,
destinationDir = destinationDir,
destinationDirDocFile = destinationDirDocFile,
exportMimeType = mimeType,
)
result["newFields"] = newFields
result["success"] = true
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to export to destinationDir=$destinationDir entry with sourcePath=$sourcePath pageId=$pageId", e)
}
callback.onSuccess(result)
}
}
private suspend fun exportSingleByTreeDocAndScan(
context: Context,
sourceEntry: AvesEntry,
destinationDir: String,
destinationDirDocFile: DocumentFileCompat,
exportMimeType: String,
): FieldMap {
val sourceMimeType = sourceEntry.mimeType
val sourceUri = sourceEntry.uri
val pageId = sourceEntry.pageId
var desiredNameWithoutExtension = if (sourceEntry.path != null) {
val sourcePath = sourceEntry.path
val sourceFile = File(sourcePath)
val sourceFileName = sourceFile.name
sourceFileName.replaceFirst("[.][^.]+$".toRegex(), "")
} else {
sourceUri.lastPathSegment!!
}
if (pageId != null) {
val page = if (sourceMimeType == MimeTypes.TIFF) pageId + 1 else pageId
desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}"
}
val desiredFileName = desiredNameWithoutExtension + when (exportMimeType) {
MimeTypes.JPEG -> ".jpg"
MimeTypes.PNG -> ".png"
MimeTypes.WEBP -> ".webp"
else -> throw Exception("unsupported export MIME type=$exportMimeType")
}
if (File(destinationDir, desiredFileName).exists()) {
throw Exception("file with name=$desiredFileName already exists in destination directory")
}
// the file created from a `TreeDocumentFile` is also a `TreeDocumentFile`
// but in order to open an output stream to it, we need to use a `SingleDocumentFile`
// through a document URI, not a tree URI
// note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first
@Suppress("BlockingMethodInNonBlockingContext")
val destinationTreeFile = destinationDirDocFile.createFile(exportMimeType, desiredNameWithoutExtension)
val destinationDocFile = DocumentFileCompat.fromSingleUri(context, destinationTreeFile.uri)
val model: Any = if (MimeTypes.isHeifLike(sourceMimeType) && pageId != null) {
MultiTrackImage(context, sourceUri, pageId)
} else if (sourceMimeType == MimeTypes.TIFF) {
TiffImage(context, sourceUri, pageId)
} else {
sourceUri
}
// request a fresh image with the highest quality format
val glideOptions = RequestOptions()
.format(DecodeFormat.PREFER_ARGB_8888)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
val target = Glide.with(context)
.asBitmap()
.apply(glideOptions)
.load(model)
.submit()
try {
@Suppress("BlockingMethodInNonBlockingContext")
var bitmap = target.get()
if (MimeTypes.needRotationAfterGlide(sourceMimeType)) {
bitmap = BitmapUtils.applyExifOrientation(context, bitmap, sourceEntry.rotationDegrees, sourceEntry.isFlipped)
}
bitmap ?: throw Exception("failed to get image from uri=$sourceUri page=$pageId")
val quality = 100
val format = when (exportMimeType) {
MimeTypes.JPEG -> Bitmap.CompressFormat.JPEG
MimeTypes.PNG -> Bitmap.CompressFormat.PNG
MimeTypes.WEBP -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (quality == 100) {
Bitmap.CompressFormat.WEBP_LOSSLESS
} else {
Bitmap.CompressFormat.WEBP_LOSSY
}
} else {
@Suppress("DEPRECATION")
Bitmap.CompressFormat.WEBP
}
else -> throw Exception("unsupported export MIME type=$exportMimeType")
}
@Suppress("BlockingMethodInNonBlockingContext")
destinationDocFile.openOutputStream().use {
bitmap.compress(format, quality, it)
}
} finally {
Glide.with(context).clear(target)
}
val fileName = destinationDocFile.name
val destinationFullPath = destinationDir + fileName
return scanNewPath(context, destinationFullPath, exportMimeType)
} }
suspend fun rename(context: Context, oldPath: String, oldMediaUri: Uri, mimeType: String, newFilename: String, callback: ImageOpCallback) { suspend fun rename(context: Context, oldPath: String, oldMediaUri: Uri, mimeType: String, newFilename: String, callback: ImageOpCallback) {
@ -151,9 +297,9 @@ abstract class ImageProvider {
// `newURI` is possibly a file media URI (e.g. "content://media/12a9-8b42/file/62872") // `newURI` is possibly a file media URI (e.g. "content://media/12a9-8b42/file/62872")
// but we need an image/video media URI (e.g. "content://media/external/images/media/62872") // but we need an image/video media URI (e.g. "content://media/external/images/media/62872")
contentId = ContentUris.parseId(newUri) contentId = ContentUris.parseId(newUri)
if (isImage(mimeType)) { if (MimeTypes.isImage(mimeType)) {
contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId) contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId)
} else if (isVideo(mimeType)) { } else if (MimeTypes.isVideo(mimeType)) {
contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId) contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId)
} }
} }

View file

@ -3,21 +3,13 @@ package deckers.thibault.aves.model.provider
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.ContentUris import android.content.ContentUris
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log import android.util.Log
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import com.commonsware.cwac.document.DocumentFileCompat import com.commonsware.cwac.document.DocumentFileCompat
import deckers.thibault.aves.decoder.MultiTrackImage
import deckers.thibault.aves.decoder.TiffImage
import deckers.thibault.aves.model.AvesEntry import deckers.thibault.aves.model.AvesEntry
import deckers.thibault.aves.model.SourceEntry import deckers.thibault.aves.model.SourceEntry
import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.isImage import deckers.thibault.aves.utils.MimeTypes.isImage
@ -319,145 +311,6 @@ class MediaStoreImageProvider : ImageProvider() {
} }
} }
override suspend fun exportMultiple(
context: Context,
mimeType: String,
destinationDir: String,
entries: List<AvesEntry>,
callback: ImageOpCallback,
) {
val destinationDirDocFile = createDirectoryIfAbsent(context, destinationDir)
if (destinationDirDocFile == null) {
callback.onFailure(Exception("failed to create directory at path=$destinationDir"))
return
}
for (entry in entries) {
val sourceUri = entry.uri
val sourcePath = entry.path
val pageId = entry.pageId
val result = hashMapOf<String, Any?>(
"uri" to sourceUri.toString(),
"pageId" to pageId,
"success" to false,
)
if (sourcePath != null) {
try {
val newFields = exportSingleByTreeDocAndScan(
context = context,
sourceEntry = entry,
destinationDir = destinationDir,
destinationDirDocFile = destinationDirDocFile,
exportMimeType = mimeType,
)
result["newFields"] = newFields
result["success"] = true
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to export to destinationDir=$destinationDir entry with sourcePath=$sourcePath pageId=$pageId", e)
}
}
callback.onSuccess(result)
}
}
private suspend fun exportSingleByTreeDocAndScan(
context: Context,
sourceEntry: AvesEntry,
destinationDir: String,
destinationDirDocFile: DocumentFileCompat,
exportMimeType: String,
): FieldMap {
val sourceMimeType = sourceEntry.mimeType
val sourcePath = sourceEntry.path ?: throw Exception("source path is missing")
val sourceFile = File(sourcePath)
val pageId = sourceEntry.pageId
val sourceFileName = sourceFile.name
var desiredNameWithoutExtension = sourceFileName.replaceFirst("[.][^.]+$".toRegex(), "")
if (pageId != null) {
val page = if (sourceMimeType == MimeTypes.TIFF) pageId + 1 else pageId
desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}"
}
val desiredFileName = desiredNameWithoutExtension + when (exportMimeType) {
MimeTypes.JPEG -> ".jpg"
MimeTypes.PNG -> ".png"
MimeTypes.WEBP -> ".webp"
else -> throw Exception("unsupported export MIME type=$exportMimeType")
}
if (File(destinationDir, desiredFileName).exists()) {
throw Exception("file with name=$desiredFileName already exists in destination directory")
}
// the file created from a `TreeDocumentFile` is also a `TreeDocumentFile`
// but in order to open an output stream to it, we need to use a `SingleDocumentFile`
// through a document URI, not a tree URI
// note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first
@Suppress("BlockingMethodInNonBlockingContext")
val destinationTreeFile = destinationDirDocFile.createFile(exportMimeType, desiredNameWithoutExtension)
val destinationDocFile = DocumentFileCompat.fromSingleUri(context, destinationTreeFile.uri)
val sourceUri = sourceEntry.uri
val model: Any = if (MimeTypes.isHeifLike(sourceMimeType) && pageId != null) {
MultiTrackImage(context, sourceUri, pageId)
} else if (sourceMimeType == MimeTypes.TIFF) {
TiffImage(context, sourceUri, pageId)
} else {
sourceUri
}
// request a fresh image with the highest quality format
val glideOptions = RequestOptions()
.format(DecodeFormat.PREFER_ARGB_8888)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
val target = Glide.with(context)
.asBitmap()
.apply(glideOptions)
.load(model)
.submit()
try {
@Suppress("BlockingMethodInNonBlockingContext")
var bitmap = target.get()
if (MimeTypes.needRotationAfterGlide(sourceMimeType)) {
bitmap = BitmapUtils.applyExifOrientation(context, bitmap, sourceEntry.rotationDegrees, sourceEntry.isFlipped)
}
bitmap ?: throw Exception("failed to get image from uri=$sourceUri page=$pageId")
val quality = 100
val format = when (exportMimeType) {
MimeTypes.JPEG -> Bitmap.CompressFormat.JPEG
MimeTypes.PNG -> Bitmap.CompressFormat.PNG
MimeTypes.WEBP -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (quality == 100) {
Bitmap.CompressFormat.WEBP_LOSSLESS
} else {
Bitmap.CompressFormat.WEBP_LOSSY
}
} else {
@Suppress("DEPRECATION")
Bitmap.CompressFormat.WEBP
}
else -> throw Exception("unsupported export MIME type=$exportMimeType")
}
@Suppress("BlockingMethodInNonBlockingContext")
destinationDocFile.openOutputStream().use {
bitmap.compress(format, quality, it)
}
} finally {
Glide.with(context).clear(target)
}
val fileName = destinationDocFile.name
val destinationFullPath = destinationDir + fileName
return scanNewPath(context, destinationFullPath, exportMimeType)
}
companion object { companion object {
private val LOG_TAG = LogUtils.createTag(MediaStoreImageProvider::class.java) private val LOG_TAG = LogUtils.createTag(MediaStoreImageProvider::class.java)

View file

@ -2,10 +2,11 @@ import 'dart:isolate';
import 'dart:ui'; import 'dart:ui';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/media_store_source.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/behaviour/route_tracker.dart'; import 'package:aves/widgets/common/behaviour/route_tracker.dart';
import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/common/behaviour/routes.dart';
import 'package:aves/widgets/common/providers/settings_provider.dart';
import 'package:aves/widgets/home_page.dart'; import 'package:aves/widgets/home_page.dart';
import 'package:aves/widgets/welcome_page.dart'; import 'package:aves/widgets/welcome_page.dart';
import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_analytics/firebase_analytics.dart';
@ -16,6 +17,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:overlay_support/overlay_support.dart'; import 'package:overlay_support/overlay_support.dart';
import 'package:provider/provider.dart';
void main() { void main() {
// HttpClient.enableTimelineLogging = true; // enable network traffic logging // HttpClient.enableTimelineLogging = true; // enable network traffic logging
@ -137,25 +139,29 @@ class _AvesAppState extends State<AvesApp> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
// place the settings provider above `MaterialApp` // place the settings provider above `MaterialApp`
// so it can be used during navigation transitions // so it can be used during navigation transitions
return SettingsProvider( return ChangeNotifierProvider<Settings>.value(
child: OverlaySupport( value: settings,
child: FutureBuilder<void>( child: Provider<CollectionSource>(
future: _appSetup, create: (context) => MediaStoreSource(),
builder: (context, snapshot) { child: OverlaySupport(
final home = (!snapshot.hasError && snapshot.connectionState == ConnectionState.done) child: FutureBuilder<void>(
? getFirstPage() future: _appSetup,
: Scaffold( builder: (context, snapshot) {
body: snapshot.hasError ? _buildError(snapshot.error) : SizedBox.shrink(), final home = (!snapshot.hasError && snapshot.connectionState == ConnectionState.done)
); ? getFirstPage()
return MaterialApp( : Scaffold(
navigatorKey: _navigatorKey, body: snapshot.hasError ? _buildError(snapshot.error) : SizedBox.shrink(),
home: home, );
navigatorObservers: _navigatorObservers, return MaterialApp(
title: 'Aves', navigatorKey: _navigatorKey,
darkTheme: darkTheme, home: home,
themeMode: ThemeMode.dark, navigatorObservers: _navigatorObservers,
); title: 'Aves',
}, darkTheme: darkTheme,
themeMode: ThemeMode.dark,
);
},
),
), ),
), ),
); );

View file

@ -164,6 +164,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
return _filterEntryCountMap.putIfAbsent(filter, () => _rawEntries.where((entry) => filter.filter(entry)).length); return _filterEntryCountMap.putIfAbsent(filter, () => _rawEntries.where((entry) => filter.filter(entry)).length);
} }
Future<void> init();
Future<void> refresh(); Future<void> refresh();
Future<void> refreshMetadata(Set<AvesEntry> entries); Future<void> refreshMetadata(Set<AvesEntry> entries);

View file

@ -13,6 +13,7 @@ import 'package:flutter_native_timezone/flutter_native_timezone.dart';
import 'package:pedantic/pedantic.dart'; import 'package:pedantic/pedantic.dart';
class MediaStoreSource extends CollectionSource { class MediaStoreSource extends CollectionSource {
@override
Future<void> init() async { Future<void> init() async {
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
stateNotifier.value = SourceState.loading; stateNotifier.value = SourceState.loading;

View file

@ -53,8 +53,8 @@ class InteractiveThumbnail extends StatelessWidget {
Navigator.push( Navigator.push(
context, context,
TransparentMaterialPageRoute( TransparentMaterialPageRoute(
settings: RouteSettings(name: MultiEntryViewerPage.routeName), settings: RouteSettings(name: EntryViewerPage.routeName),
pageBuilder: (c, a, sa) => MultiEntryViewerPage( pageBuilder: (c, a, sa) => EntryViewerPage(
collection: collection, collection: collection,
initialEntry: entry, initialEntry: entry,
), ),

View file

@ -82,7 +82,7 @@ mixin FeedbackMixin {
Future<void> _hideOpReportOverlay() async { Future<void> _hideOpReportOverlay() async {
await Future.delayed(Durations.collectionOpOverlayAnimation * timeDilation); await Future.delayed(Durations.collectionOpOverlayAnimation * timeDilation);
_opReportOverlayEntry.remove(); _opReportOverlayEntry?.remove();
_opReportOverlayEntry = null; _opReportOverlayEntry = null;
} }
} }

View file

@ -1,17 +0,0 @@
import 'package:aves/model/settings/settings.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
class SettingsProvider extends StatelessWidget {
final Widget child;
const SettingsProvider({@required this.child});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<Settings>.value(
value: settings,
child: child,
);
}
}

View file

@ -5,7 +5,7 @@ import 'package:aves/model/settings/home_page.dart';
import 'package:aves/model/settings/screen_on.dart'; import 'package:aves/model/settings/screen_on.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/media_store_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/services/image_file_service.dart'; import 'package:aves/services/image_file_service.dart';
import 'package:aves/services/viewer_service.dart'; import 'package:aves/services/viewer_service.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
@ -20,6 +20,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pedantic/pedantic.dart'; import 'package:pedantic/pedantic.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
static const routeName = '/'; static const routeName = '/';
@ -34,7 +35,6 @@ class HomePage extends StatefulWidget {
} }
class _HomePageState extends State<HomePage> { class _HomePageState extends State<HomePage> {
MediaStoreSource _mediaStore;
AvesEntry _viewerEntry; AvesEntry _viewerEntry;
String _shortcutRouteName; String _shortcutRouteName;
List<String> _shortcutFilters; List<String> _shortcutFilters;
@ -100,9 +100,9 @@ class _HomePageState extends State<HomePage> {
unawaited(FirebaseCrashlytics.instance.setCustomKey('app_mode', AvesApp.mode.toString())); unawaited(FirebaseCrashlytics.instance.setCustomKey('app_mode', AvesApp.mode.toString()));
if (AvesApp.mode != AppMode.view) { if (AvesApp.mode != AppMode.view) {
_mediaStore = MediaStoreSource(); final source = context.read<CollectionSource>();
await _mediaStore.init(); await source.init();
unawaited(_mediaStore.refresh()); unawaited(source.refresh());
} }
unawaited(Navigator.pushReplacement(context, _getRedirectRoute())); unawaited(Navigator.pushReplacement(context, _getRedirectRoute()));
@ -121,8 +121,10 @@ class _HomePageState extends State<HomePage> {
Route _getRedirectRoute() { Route _getRedirectRoute() {
if (AvesApp.mode == AppMode.view) { if (AvesApp.mode == AppMode.view) {
return DirectMaterialPageRoute( return DirectMaterialPageRoute(
settings: RouteSettings(name: SingleEntryViewerPage.routeName), settings: RouteSettings(name: EntryViewerPage.routeName),
builder: (_) => SingleEntryViewerPage(entry: _viewerEntry), builder: (_) => EntryViewerPage(
initialEntry: _viewerEntry,
),
); );
} }
@ -134,15 +136,16 @@ class _HomePageState extends State<HomePage> {
routeName = _shortcutRouteName ?? settings.homePage.routeName; routeName = _shortcutRouteName ?? settings.homePage.routeName;
filters = (_shortcutFilters ?? []).map(CollectionFilter.fromJson); filters = (_shortcutFilters ?? []).map(CollectionFilter.fromJson);
} }
final source = context.read<CollectionSource>();
switch (routeName) { switch (routeName) {
case AlbumListPage.routeName: case AlbumListPage.routeName:
return DirectMaterialPageRoute( return DirectMaterialPageRoute(
settings: RouteSettings(name: AlbumListPage.routeName), settings: RouteSettings(name: AlbumListPage.routeName),
builder: (_) => AlbumListPage(source: _mediaStore), builder: (_) => AlbumListPage(source: source),
); );
case SearchPage.routeName: case SearchPage.routeName:
return SearchPageRoute( return SearchPageRoute(
delegate: CollectionSearchDelegate(source: _mediaStore), delegate: CollectionSearchDelegate(source: source),
); );
case CollectionPage.routeName: case CollectionPage.routeName:
default: default:
@ -150,7 +153,7 @@ class _HomePageState extends State<HomePage> {
settings: RouteSettings(name: CollectionPage.routeName), settings: RouteSettings(name: CollectionPage.routeName),
builder: (_) => CollectionPage( builder: (_) => CollectionPage(
CollectionLens( CollectionLens(
source: _mediaStore, source: source,
filters: filters, filters: filters,
groupFactor: settings.collectionGroupFactor, groupFactor: settings.collectionGroupFactor,
sortFactor: settings.collectionSortFactor, sortFactor: settings.collectionSortFactor,

View file

@ -4,6 +4,7 @@ import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/actions/move_type.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/android_app_service.dart';
import 'package:aves/services/image_file_service.dart'; import 'package:aves/services/image_file_service.dart';
import 'package:aves/services/image_op_events.dart'; import 'package:aves/services/image_op_events.dart';
@ -22,6 +23,7 @@ import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:pedantic/pedantic.dart'; import 'package:pedantic/pedantic.dart';
import 'package:provider/provider.dart';
class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
final CollectionLens collection; final CollectionLens collection;
@ -150,19 +152,14 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
} }
Future<void> _showExportDialog(BuildContext context, AvesEntry entry) async { Future<void> _showExportDialog(BuildContext context, AvesEntry entry) async {
String destinationAlbum; final source = context.read<CollectionSource>();
if (hasCollection) { final destinationAlbum = await Navigator.push(
final source = collection.source; context,
destinationAlbum = await Navigator.push( MaterialPageRoute<String>(
context, settings: RouteSettings(name: AlbumPickPage.routeName),
MaterialPageRoute<String>( builder: (context) => AlbumPickPage(source: source, moveType: MoveType.export),
settings: RouteSettings(name: AlbumPickPage.routeName), ),
builder: (context) => AlbumPickPage(source: source, moveType: MoveType.export), );
),
);
} else {
destinationAlbum = entry.directory;
}
if (destinationAlbum == null || destinationAlbum.isEmpty) return; if (destinationAlbum == null || destinationAlbum.isEmpty) return;
if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return; if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return;
@ -198,9 +195,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
} else { } else {
showFeedback(context, 'Done!'); showFeedback(context, 'Done!');
} }
if (hasCollection) { source.refresh();
collection.source.refresh();
}
}, },
); );
} }

View file

@ -0,0 +1,165 @@
import 'dart:math';
import 'package:aves/model/entry.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/widgets/common/magnifier/pan/scroll_physics.dart';
import 'package:aves/widgets/viewer/entry_horizontal_pager.dart';
import 'package:aves/widgets/viewer/info/info_page.dart';
import 'package:aves/widgets/viewer/info/notifications.dart';
import 'package:aves/widgets/viewer/multipage.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
import 'package:tuple/tuple.dart';
class ViewerVerticalPageView extends StatefulWidget {
final CollectionLens collection;
final ValueNotifier<AvesEntry> entryNotifier;
final List<Tuple2<String, IjkMediaController>> videoControllers;
final List<Tuple2<String, MultiPageController>> multiPageControllers;
final PageController horizontalPager, verticalPager;
final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged;
final VoidCallback onImageTap, onImagePageRequested;
final void Function(String uri) onViewDisposed;
const ViewerVerticalPageView({
@required this.collection,
@required this.entryNotifier,
@required this.videoControllers,
@required this.multiPageControllers,
@required this.verticalPager,
@required this.horizontalPager,
@required this.onVerticalPageChanged,
@required this.onHorizontalPageChanged,
@required this.onImageTap,
@required this.onImagePageRequested,
@required this.onViewDisposed,
});
@override
_ViewerVerticalPageViewState createState() => _ViewerVerticalPageViewState();
}
class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
final ValueNotifier<Color> _backgroundColorNotifier = ValueNotifier(Colors.black);
final ValueNotifier<bool> _infoPageVisibleNotifier = ValueNotifier(false);
AvesEntry _oldEntry;
CollectionLens get collection => widget.collection;
bool get hasCollection => collection != null;
AvesEntry get entry => widget.entryNotifier.value;
@override
void initState() {
super.initState();
_registerWidget(widget);
}
@override
void didUpdateWidget(covariant ViewerVerticalPageView oldWidget) {
super.didUpdateWidget(oldWidget);
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
@override
void dispose() {
_unregisterWidget(widget);
super.dispose();
}
void _registerWidget(ViewerVerticalPageView widget) {
widget.verticalPager.addListener(_onVerticalPageControllerChanged);
widget.entryNotifier.addListener(_onEntryChanged);
if (_oldEntry != entry) _onEntryChanged();
}
void _unregisterWidget(ViewerVerticalPageView widget) {
widget.verticalPager.removeListener(_onVerticalPageControllerChanged);
widget.entryNotifier.removeListener(_onEntryChanged);
_oldEntry?.imageChangeNotifier?.removeListener(_onImageChanged);
}
@override
Widget build(BuildContext context) {
final pages = [
// fake page for opacity transition between collection and viewer
SizedBox(),
hasCollection
? MultiEntryScroller(
collection: collection,
pageController: widget.horizontalPager,
onTap: widget.onImageTap,
onPageChanged: widget.onHorizontalPageChanged,
videoControllers: widget.videoControllers,
multiPageControllers: widget.multiPageControllers,
onViewDisposed: widget.onViewDisposed,
)
: SingleEntryScroller(
entry: entry,
onTap: widget.onImageTap,
videoControllers: widget.videoControllers,
multiPageControllers: widget.multiPageControllers,
),
NotificationListener(
onNotification: (notification) {
if (notification is BackUpNotification) widget.onImagePageRequested();
return false;
},
child: InfoPage(
collection: collection,
entryNotifier: widget.entryNotifier,
visibleNotifier: _infoPageVisibleNotifier,
),
),
];
return ValueListenableBuilder<Color>(
valueListenable: _backgroundColorNotifier,
builder: (context, backgroundColor, child) => Container(
color: backgroundColor,
child: child,
),
child: PageView(
key: Key('vertical-pageview'),
scrollDirection: Axis.vertical,
controller: widget.verticalPager,
physics: MagnifierScrollerPhysics(parent: PageScrollPhysics()),
onPageChanged: (page) {
widget.onVerticalPageChanged(page);
_infoPageVisibleNotifier.value = page == pages.length - 1;
},
children: pages,
),
);
}
void _onVerticalPageControllerChanged() {
final opacity = min(1.0, widget.verticalPager.page);
_backgroundColorNotifier.value = _backgroundColorNotifier.value.withOpacity(opacity * opacity);
}
// when the entry changed (e.g. by scrolling through the PageView, or if the entry got deleted)
void _onEntryChanged() {
_oldEntry?.imageChangeNotifier?.removeListener(_onImageChanged);
_oldEntry = entry;
if (entry != null) {
entry.imageChangeNotifier.addListener(_onImageChanged);
// make sure to locate the entry,
// so that we can display the address instead of coordinates
// even when background locating has not reached this entry yet
entry.locate();
} else {
Navigator.pop(context);
}
}
// when the entry image itself changed (e.g. after rotation)
void _onImageChanged() async {
// rebuild to refresh the Image inside ImagePage
setState(() {});
}
}

View file

@ -4,50 +4,33 @@ import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/viewer/entry_viewer_stack.dart'; import 'package:aves/widgets/viewer/entry_viewer_stack.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class MultiEntryViewerPage extends AnimatedWidget { class EntryViewerPage extends StatelessWidget {
static const routeName = '/viewer'; static const routeName = '/viewer';
final CollectionLens collection; final CollectionLens collection;
final AvesEntry initialEntry; final AvesEntry initialEntry;
const MultiEntryViewerPage({ const EntryViewerPage({
Key key, Key key,
this.collection, this.collection,
this.initialEntry, this.initialEntry,
}) : super(key: key, listenable: collection);
@override
Widget build(BuildContext context) {
return MediaQueryDataProvider(
child: Scaffold(
body: EntryViewerStack(
collection: collection,
initialEntry: initialEntry,
),
backgroundColor: Colors.transparent,
resizeToAvoidBottomInset: false,
),
);
}
}
class SingleEntryViewerPage extends StatelessWidget {
static const routeName = '/viewer';
final AvesEntry entry;
const SingleEntryViewerPage({
Key key,
this.entry,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MediaQueryDataProvider( return MediaQueryDataProvider(
child: Scaffold( child: Scaffold(
body: EntryViewerStack( body: collection != null
initialEntry: entry, ? AnimatedBuilder(
), animation: collection,
builder: (context, child) => EntryViewerStack(
collection: collection,
initialEntry: initialEntry,
),
)
: EntryViewerStack(
initialEntry: initialEntry,
),
backgroundColor: Navigator.canPop(context) ? Colors.transparent : Colors.black, backgroundColor: Navigator.canPop(context) ? Colors.transparent : Colors.black,
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
), ),

View file

@ -1,7 +1,7 @@
import 'dart:math'; import 'dart:math';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/screen_on.dart'; import 'package:aves/model/settings/screen_on.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
@ -9,10 +9,8 @@ import 'package:aves/theme/durations.dart';
import 'package:aves/utils/change_notifier.dart'; import 'package:aves/utils/change_notifier.dart';
import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/magnifier/pan/scroll_physics.dart';
import 'package:aves/widgets/viewer/entry_action_delegate.dart'; import 'package:aves/widgets/viewer/entry_action_delegate.dart';
import 'package:aves/widgets/viewer/entry_scroller.dart'; import 'package:aves/widgets/viewer/entry_vertical_pager.dart';
import 'package:aves/widgets/viewer/info/info_page.dart';
import 'package:aves/widgets/viewer/info/notifications.dart'; import 'package:aves/widgets/viewer/info/notifications.dart';
import 'package:aves/widgets/viewer/multipage.dart'; import 'package:aves/widgets/viewer/multipage.dart';
import 'package:aves/widgets/viewer/overlay/bottom.dart'; import 'package:aves/widgets/viewer/overlay/bottom.dart';
@ -36,7 +34,7 @@ class EntryViewerStack extends StatefulWidget {
const EntryViewerStack({ const EntryViewerStack({
Key key, Key key,
this.collection, this.collection,
this.initialEntry, @required this.initialEntry,
}) : super(key: key); }) : super(key: key);
@override @override
@ -475,154 +473,3 @@ class _EntryViewerStackState extends State<EntryViewerStack> with SingleTickerPr
void _pauseVideoControllers() => _videoControllers.forEach((e) => e.item2.pause()); void _pauseVideoControllers() => _videoControllers.forEach((e) => e.item2.pause());
} }
class ViewerVerticalPageView extends StatefulWidget {
final CollectionLens collection;
final ValueNotifier<AvesEntry> entryNotifier;
final List<Tuple2<String, IjkMediaController>> videoControllers;
final List<Tuple2<String, MultiPageController>> multiPageControllers;
final PageController horizontalPager, verticalPager;
final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged;
final VoidCallback onImageTap, onImagePageRequested;
final void Function(String uri) onViewDisposed;
const ViewerVerticalPageView({
@required this.collection,
@required this.entryNotifier,
@required this.videoControllers,
@required this.multiPageControllers,
@required this.verticalPager,
@required this.horizontalPager,
@required this.onVerticalPageChanged,
@required this.onHorizontalPageChanged,
@required this.onImageTap,
@required this.onImagePageRequested,
@required this.onViewDisposed,
});
@override
_ViewerVerticalPageViewState createState() => _ViewerVerticalPageViewState();
}
class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
final ValueNotifier<Color> _backgroundColorNotifier = ValueNotifier(Colors.black);
final ValueNotifier<bool> _infoPageVisibleNotifier = ValueNotifier(false);
AvesEntry _oldEntry;
CollectionLens get collection => widget.collection;
bool get hasCollection => collection != null;
AvesEntry get entry => widget.entryNotifier.value;
@override
void initState() {
super.initState();
_registerWidget(widget);
}
@override
void didUpdateWidget(covariant ViewerVerticalPageView oldWidget) {
super.didUpdateWidget(oldWidget);
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
@override
void dispose() {
_unregisterWidget(widget);
super.dispose();
}
void _registerWidget(ViewerVerticalPageView widget) {
widget.verticalPager.addListener(_onVerticalPageControllerChanged);
widget.entryNotifier.addListener(_onEntryChanged);
if (_oldEntry != entry) _onEntryChanged();
}
void _unregisterWidget(ViewerVerticalPageView widget) {
widget.verticalPager.removeListener(_onVerticalPageControllerChanged);
widget.entryNotifier.removeListener(_onEntryChanged);
_oldEntry?.imageChangeNotifier?.removeListener(_onImageChanged);
}
@override
Widget build(BuildContext context) {
final pages = [
// fake page for opacity transition between collection and viewer
SizedBox(),
hasCollection
? MultiEntryScroller(
collection: collection,
pageController: widget.horizontalPager,
onTap: widget.onImageTap,
onPageChanged: widget.onHorizontalPageChanged,
videoControllers: widget.videoControllers,
multiPageControllers: widget.multiPageControllers,
onViewDisposed: widget.onViewDisposed,
)
: SingleEntryScroller(
entry: entry,
onTap: widget.onImageTap,
videoControllers: widget.videoControllers,
multiPageControllers: widget.multiPageControllers,
),
NotificationListener(
onNotification: (notification) {
if (notification is BackUpNotification) widget.onImagePageRequested();
return false;
},
child: InfoPage(
collection: collection,
entryNotifier: widget.entryNotifier,
visibleNotifier: _infoPageVisibleNotifier,
),
),
];
return ValueListenableBuilder<Color>(
valueListenable: _backgroundColorNotifier,
builder: (context, backgroundColor, child) => Container(
color: backgroundColor,
child: child,
),
child: PageView(
key: Key('vertical-pageview'),
scrollDirection: Axis.vertical,
controller: widget.verticalPager,
physics: MagnifierScrollerPhysics(parent: PageScrollPhysics()),
onPageChanged: (page) {
widget.onVerticalPageChanged(page);
_infoPageVisibleNotifier.value = page == pages.length - 1;
},
children: pages,
),
);
}
void _onVerticalPageControllerChanged() {
final opacity = min(1.0, widget.verticalPager.page);
_backgroundColorNotifier.value = _backgroundColorNotifier.value.withOpacity(opacity * opacity);
}
// when the entry changed (e.g. by scrolling through the PageView, or if the entry got deleted)
void _onEntryChanged() {
_oldEntry?.imageChangeNotifier?.removeListener(_onImageChanged);
_oldEntry = entry;
if (entry != null) {
entry.imageChangeNotifier.addListener(_onImageChanged);
// make sure to locate the entry,
// so that we can display the address instead of coordinates
// even when background locating has not reached this entry yet
entry.locate();
} else {
Navigator.pop(context);
}
}
// when the entry image itself changed (e.g. after rotation)
void _onImageChanged() async {
// rebuild to refresh the Image inside ImagePage
setState(() {});
}
}

View file

@ -1,9 +1,11 @@
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/behaviour/routes.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
import 'package:aves/widgets/viewer/info/basic_section.dart'; import 'package:aves/widgets/viewer/info/basic_section.dart';
import 'package:aves/widgets/viewer/info/info_app_bar.dart'; import 'package:aves/widgets/viewer/info/info_app_bar.dart';
import 'package:aves/widgets/viewer/info/location_section.dart'; import 'package:aves/widgets/viewer/info/location_section.dart';
@ -45,25 +47,31 @@ class _InfoPageState extends State<InfoPage> {
bottom: false, bottom: false,
child: NotificationListener( child: NotificationListener(
onNotification: _handleTopScroll, onNotification: _handleTopScroll,
child: Selector<MediaQueryData, double>( child: NotificationListener<OpenTempEntryNotification>(
selector: (c, mq) => mq.size.width, onNotification: (notification) {
builder: (c, mqWidth, child) { _openTempEntry(notification.entry);
return ValueListenableBuilder<AvesEntry>( return true;
valueListenable: widget.entryNotifier,
builder: (context, entry, child) {
return entry != null
? _InfoPageContent(
collection: collection,
entry: entry,
visibleNotifier: widget.visibleNotifier,
scrollController: _scrollController,
split: mqWidth > 400,
goToViewer: _goToViewer,
)
: SizedBox.shrink();
},
);
}, },
child: Selector<MediaQueryData, double>(
selector: (c, mq) => mq.size.width,
builder: (c, mqWidth, child) {
return ValueListenableBuilder<AvesEntry>(
valueListenable: widget.entryNotifier,
builder: (context, entry, child) {
return entry != null
? _InfoPageContent(
collection: collection,
entry: entry,
visibleNotifier: widget.visibleNotifier,
scrollController: _scrollController,
split: mqWidth > 400,
goToViewer: _goToViewer,
)
: SizedBox.shrink();
},
);
},
),
), ),
), ),
), ),
@ -103,6 +111,18 @@ class _InfoPageState extends State<InfoPage> {
curve: Curves.easeInOut, curve: Curves.easeInOut,
); );
} }
void _openTempEntry(AvesEntry tempEntry) {
Navigator.push(
context,
TransparentMaterialPageRoute(
settings: RouteSettings(name: EntryViewerPage.routeName),
pageBuilder: (c, a, sa) => EntryViewerPage(
initialEntry: tempEntry,
),
),
);
}
} }
class _InfoPageContent extends StatefulWidget { class _InfoPageContent extends StatefulWidget {

View file

@ -1,8 +1,11 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/collection/empty.dart'; import 'package:aves/widgets/collection/empty.dart';
import 'package:aves/widgets/common/behaviour/routes.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
import 'package:aves/widgets/viewer/info/metadata/metadata_dir_tile.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_dir_tile.dart';
import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart';
import 'package:aves/widgets/viewer/info/notifications.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -109,10 +112,28 @@ class InfoSearchDelegate extends SearchDelegate {
icon: AIcons.info, icon: AIcons.info,
text: 'No matching keys', text: 'No matching keys',
) )
: ListView.builder( : NotificationListener<OpenTempEntryNotification>(
padding: EdgeInsets.all(8), onNotification: (notification) {
itemBuilder: (context, index) => tiles[index], _openTempEntry(context, notification.entry);
itemCount: tiles.length, return true;
},
child: ListView.builder(
padding: EdgeInsets.all(8),
itemBuilder: (context, index) => tiles[index],
itemCount: tiles.length,
),
); );
} }
void _openTempEntry(BuildContext context, AvesEntry tempEntry) {
Navigator.push(
context,
TransparentMaterialPageRoute(
settings: RouteSettings(name: EntryViewerPage.routeName),
pageBuilder: (c, a, sa) => EntryViewerPage(
initialEntry: tempEntry,
),
),
);
}
} }

View file

@ -6,10 +6,8 @@ import 'package:aves/ref/xmp.dart';
import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/android_app_service.dart';
import 'package:aves/services/metadata_service.dart'; import 'package:aves/services/metadata_service.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/behaviour/routes.dart';
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/exif.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/exif.dart';
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/google.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/google.dart';
@ -17,6 +15,7 @@ import 'package:aves/widgets/viewer/info/metadata/xmp_ns/iptc.dart';
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/photoshop.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/photoshop.dart';
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/tiff.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/tiff.dart';
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/xmp.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/xmp.dart';
import 'package:aves/widgets/viewer/info/notifications.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pedantic/pedantic.dart'; import 'package:pedantic/pedantic.dart';
@ -123,13 +122,6 @@ class _XmpDirTileState extends State<XmpDirTile> with FeedbackMixin {
return; return;
} }
final embedEntry = AvesEntry.fromMap(fields); OpenTempEntryNotification(entry: AvesEntry.fromMap(fields)).dispatch(context);
unawaited(Navigator.push(
context,
TransparentMaterialPageRoute(
settings: RouteSettings(name: SingleEntryViewerPage.routeName),
pageBuilder: (c, a, sa) => SingleEntryViewerPage(entry: embedEntry),
),
));
} }
} }

View file

@ -1,4 +1,6 @@
import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class BackUpNotification extends Notification {} class BackUpNotification extends Notification {}
@ -8,3 +10,14 @@ class FilterNotification extends Notification {
const FilterNotification(this.filter); const FilterNotification(this.filter);
} }
class OpenTempEntryNotification extends Notification {
final AvesEntry entry;
const OpenTempEntryNotification({
@required this.entry,
});
@override
String toString() => '$runtimeType#${shortHash(this)}{entry=$entry}';
}