export: support embedded images
This commit is contained in:
parent
c4fdd38850
commit
218db5d091
18 changed files with 468 additions and 438 deletions
|
@ -2,18 +2,27 @@ package deckers.thibault.aves.model.provider
|
|||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.media.MediaScannerConnection
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
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 deckers.thibault.aves.decoder.MultiTrackImage
|
||||
import deckers.thibault.aves.decoder.TiffImage
|
||||
import deckers.thibault.aves.model.AvesEntry
|
||||
import deckers.thibault.aves.model.ExifOrientationOp
|
||||
import deckers.thibault.aves.utils.BitmapUtils
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import deckers.thibault.aves.utils.MimeTypes.isImage
|
||||
import deckers.thibault.aves.utils.MimeTypes.isVideo
|
||||
import deckers.thibault.aves.utils.MimeTypes
|
||||
import deckers.thibault.aves.utils.StorageUtils.copyFileToTemp
|
||||
import deckers.thibault.aves.utils.StorageUtils.createDirectoryIfAbsent
|
||||
import deckers.thibault.aves.utils.StorageUtils.getDocumentFile
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
|
@ -36,8 +45,145 @@ abstract class ImageProvider {
|
|||
callback.onFailure(UnsupportedOperationException())
|
||||
}
|
||||
|
||||
open suspend fun exportMultiple(context: Context, mimeType: String, destinationDir: String, entries: List<AvesEntry>, callback: ImageOpCallback) {
|
||||
callback.onFailure(UnsupportedOperationException())
|
||||
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,
|
||||
)
|
||||
|
||||
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) {
|
||||
|
@ -151,9 +297,9 @@ abstract class ImageProvider {
|
|||
// `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")
|
||||
contentId = ContentUris.parseId(newUri)
|
||||
if (isImage(mimeType)) {
|
||||
if (MimeTypes.isImage(mimeType)) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,21 +3,13 @@ package deckers.thibault.aves.model.provider
|
|||
import android.annotation.SuppressLint
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
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 deckers.thibault.aves.decoder.MultiTrackImage
|
||||
import deckers.thibault.aves.decoder.TiffImage
|
||||
import deckers.thibault.aves.model.AvesEntry
|
||||
import deckers.thibault.aves.model.SourceEntry
|
||||
import deckers.thibault.aves.utils.BitmapUtils
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import deckers.thibault.aves.utils.MimeTypes
|
||||
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 {
|
||||
private val LOG_TAG = LogUtils.createTag(MediaStoreImageProvider::class.java)
|
||||
|
||||
|
|
|
@ -2,10 +2,11 @@ import 'dart:isolate';
|
|||
import 'dart:ui';
|
||||
|
||||
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/widgets/common/behaviour/route_tracker.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/welcome_page.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/services.dart';
|
||||
import 'package:overlay_support/overlay_support.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
void main() {
|
||||
// HttpClient.enableTimelineLogging = true; // enable network traffic logging
|
||||
|
@ -137,25 +139,29 @@ class _AvesAppState extends State<AvesApp> {
|
|||
Widget build(BuildContext context) {
|
||||
// place the settings provider above `MaterialApp`
|
||||
// so it can be used during navigation transitions
|
||||
return SettingsProvider(
|
||||
child: OverlaySupport(
|
||||
child: FutureBuilder<void>(
|
||||
future: _appSetup,
|
||||
builder: (context, snapshot) {
|
||||
final home = (!snapshot.hasError && snapshot.connectionState == ConnectionState.done)
|
||||
? getFirstPage()
|
||||
: Scaffold(
|
||||
body: snapshot.hasError ? _buildError(snapshot.error) : SizedBox.shrink(),
|
||||
);
|
||||
return MaterialApp(
|
||||
navigatorKey: _navigatorKey,
|
||||
home: home,
|
||||
navigatorObservers: _navigatorObservers,
|
||||
title: 'Aves',
|
||||
darkTheme: darkTheme,
|
||||
themeMode: ThemeMode.dark,
|
||||
);
|
||||
},
|
||||
return ChangeNotifierProvider<Settings>.value(
|
||||
value: settings,
|
||||
child: Provider<CollectionSource>(
|
||||
create: (context) => MediaStoreSource(),
|
||||
child: OverlaySupport(
|
||||
child: FutureBuilder<void>(
|
||||
future: _appSetup,
|
||||
builder: (context, snapshot) {
|
||||
final home = (!snapshot.hasError && snapshot.connectionState == ConnectionState.done)
|
||||
? getFirstPage()
|
||||
: Scaffold(
|
||||
body: snapshot.hasError ? _buildError(snapshot.error) : SizedBox.shrink(),
|
||||
);
|
||||
return MaterialApp(
|
||||
navigatorKey: _navigatorKey,
|
||||
home: home,
|
||||
navigatorObservers: _navigatorObservers,
|
||||
title: 'Aves',
|
||||
darkTheme: darkTheme,
|
||||
themeMode: ThemeMode.dark,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -164,6 +164,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
return _filterEntryCountMap.putIfAbsent(filter, () => _rawEntries.where((entry) => filter.filter(entry)).length);
|
||||
}
|
||||
|
||||
Future<void> init();
|
||||
|
||||
Future<void> refresh();
|
||||
|
||||
Future<void> refreshMetadata(Set<AvesEntry> entries);
|
||||
|
|
|
@ -13,6 +13,7 @@ import 'package:flutter_native_timezone/flutter_native_timezone.dart';
|
|||
import 'package:pedantic/pedantic.dart';
|
||||
|
||||
class MediaStoreSource extends CollectionSource {
|
||||
@override
|
||||
Future<void> init() async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
stateNotifier.value = SourceState.loading;
|
||||
|
|
|
@ -53,8 +53,8 @@ class InteractiveThumbnail extends StatelessWidget {
|
|||
Navigator.push(
|
||||
context,
|
||||
TransparentMaterialPageRoute(
|
||||
settings: RouteSettings(name: MultiEntryViewerPage.routeName),
|
||||
pageBuilder: (c, a, sa) => MultiEntryViewerPage(
|
||||
settings: RouteSettings(name: EntryViewerPage.routeName),
|
||||
pageBuilder: (c, a, sa) => EntryViewerPage(
|
||||
collection: collection,
|
||||
initialEntry: entry,
|
||||
),
|
||||
|
|
|
@ -82,7 +82,7 @@ mixin FeedbackMixin {
|
|||
|
||||
Future<void> _hideOpReportOverlay() async {
|
||||
await Future.delayed(Durations.collectionOpOverlayAnimation * timeDilation);
|
||||
_opReportOverlayEntry.remove();
|
||||
_opReportOverlayEntry?.remove();
|
||||
_opReportOverlayEntry = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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/settings.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/viewer_service.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:pedantic/pedantic.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
static const routeName = '/';
|
||||
|
@ -34,7 +35,6 @@ class HomePage extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
MediaStoreSource _mediaStore;
|
||||
AvesEntry _viewerEntry;
|
||||
String _shortcutRouteName;
|
||||
List<String> _shortcutFilters;
|
||||
|
@ -100,9 +100,9 @@ class _HomePageState extends State<HomePage> {
|
|||
unawaited(FirebaseCrashlytics.instance.setCustomKey('app_mode', AvesApp.mode.toString()));
|
||||
|
||||
if (AvesApp.mode != AppMode.view) {
|
||||
_mediaStore = MediaStoreSource();
|
||||
await _mediaStore.init();
|
||||
unawaited(_mediaStore.refresh());
|
||||
final source = context.read<CollectionSource>();
|
||||
await source.init();
|
||||
unawaited(source.refresh());
|
||||
}
|
||||
|
||||
unawaited(Navigator.pushReplacement(context, _getRedirectRoute()));
|
||||
|
@ -121,8 +121,10 @@ class _HomePageState extends State<HomePage> {
|
|||
Route _getRedirectRoute() {
|
||||
if (AvesApp.mode == AppMode.view) {
|
||||
return DirectMaterialPageRoute(
|
||||
settings: RouteSettings(name: SingleEntryViewerPage.routeName),
|
||||
builder: (_) => SingleEntryViewerPage(entry: _viewerEntry),
|
||||
settings: RouteSettings(name: EntryViewerPage.routeName),
|
||||
builder: (_) => EntryViewerPage(
|
||||
initialEntry: _viewerEntry,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -134,15 +136,16 @@ class _HomePageState extends State<HomePage> {
|
|||
routeName = _shortcutRouteName ?? settings.homePage.routeName;
|
||||
filters = (_shortcutFilters ?? []).map(CollectionFilter.fromJson);
|
||||
}
|
||||
final source = context.read<CollectionSource>();
|
||||
switch (routeName) {
|
||||
case AlbumListPage.routeName:
|
||||
return DirectMaterialPageRoute(
|
||||
settings: RouteSettings(name: AlbumListPage.routeName),
|
||||
builder: (_) => AlbumListPage(source: _mediaStore),
|
||||
builder: (_) => AlbumListPage(source: source),
|
||||
);
|
||||
case SearchPage.routeName:
|
||||
return SearchPageRoute(
|
||||
delegate: CollectionSearchDelegate(source: _mediaStore),
|
||||
delegate: CollectionSearchDelegate(source: source),
|
||||
);
|
||||
case CollectionPage.routeName:
|
||||
default:
|
||||
|
@ -150,7 +153,7 @@ class _HomePageState extends State<HomePage> {
|
|||
settings: RouteSettings(name: CollectionPage.routeName),
|
||||
builder: (_) => CollectionPage(
|
||||
CollectionLens(
|
||||
source: _mediaStore,
|
||||
source: source,
|
||||
filters: filters,
|
||||
groupFactor: settings.collectionGroupFactor,
|
||||
sortFactor: settings.collectionSortFactor,
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:aves/model/actions/entry_actions.dart';
|
|||
import 'package:aves/model/actions/move_type.dart';
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/services/android_app_service.dart';
|
||||
import 'package:aves/services/image_file_service.dart';
|
||||
import 'package:aves/services/image_op_events.dart';
|
||||
|
@ -22,6 +23,7 @@ import 'package:flutter/scheduler.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
||||
final CollectionLens collection;
|
||||
|
@ -150,19 +152,14 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
}
|
||||
|
||||
Future<void> _showExportDialog(BuildContext context, AvesEntry entry) async {
|
||||
String destinationAlbum;
|
||||
if (hasCollection) {
|
||||
final source = collection.source;
|
||||
destinationAlbum = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<String>(
|
||||
settings: RouteSettings(name: AlbumPickPage.routeName),
|
||||
builder: (context) => AlbumPickPage(source: source, moveType: MoveType.export),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
destinationAlbum = entry.directory;
|
||||
}
|
||||
final source = context.read<CollectionSource>();
|
||||
final destinationAlbum = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<String>(
|
||||
settings: RouteSettings(name: AlbumPickPage.routeName),
|
||||
builder: (context) => AlbumPickPage(source: source, moveType: MoveType.export),
|
||||
),
|
||||
);
|
||||
|
||||
if (destinationAlbum == null || destinationAlbum.isEmpty) return;
|
||||
if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return;
|
||||
|
@ -198,9 +195,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
} else {
|
||||
showFeedback(context, 'Done!');
|
||||
}
|
||||
if (hasCollection) {
|
||||
collection.source.refresh();
|
||||
}
|
||||
source.refresh();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
165
lib/widgets/viewer/entry_vertical_pager.dart
Normal file
165
lib/widgets/viewer/entry_vertical_pager.dart
Normal 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(() {});
|
||||
}
|
||||
}
|
|
@ -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:flutter/material.dart';
|
||||
|
||||
class MultiEntryViewerPage extends AnimatedWidget {
|
||||
class EntryViewerPage extends StatelessWidget {
|
||||
static const routeName = '/viewer';
|
||||
|
||||
final CollectionLens collection;
|
||||
final AvesEntry initialEntry;
|
||||
|
||||
const MultiEntryViewerPage({
|
||||
const EntryViewerPage({
|
||||
Key key,
|
||||
this.collection,
|
||||
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);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
body: EntryViewerStack(
|
||||
initialEntry: entry,
|
||||
),
|
||||
body: collection != null
|
||||
? AnimatedBuilder(
|
||||
animation: collection,
|
||||
builder: (context, child) => EntryViewerStack(
|
||||
collection: collection,
|
||||
initialEntry: initialEntry,
|
||||
),
|
||||
)
|
||||
: EntryViewerStack(
|
||||
initialEntry: initialEntry,
|
||||
),
|
||||
backgroundColor: Navigator.canPop(context) ? Colors.transparent : Colors.black,
|
||||
resizeToAvoidBottomInset: false,
|
||||
),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/model/filters/filters.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/settings.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/widgets/collection/collection_page.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_scroller.dart';
|
||||
import 'package:aves/widgets/viewer/info/info_page.dart';
|
||||
import 'package:aves/widgets/viewer/entry_vertical_pager.dart';
|
||||
import 'package:aves/widgets/viewer/info/notifications.dart';
|
||||
import 'package:aves/widgets/viewer/multipage.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/bottom.dart';
|
||||
|
@ -36,7 +34,7 @@ class EntryViewerStack extends StatefulWidget {
|
|||
const EntryViewerStack({
|
||||
Key key,
|
||||
this.collection,
|
||||
this.initialEntry,
|
||||
@required this.initialEntry,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -475,154 +473,3 @@ class _EntryViewerStackState extends State<EntryViewerStack> with SingleTickerPr
|
|||
|
||||
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(() {});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import 'package:aves/model/filters/filters.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/theme/durations.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/viewer/entry_viewer_page.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/location_section.dart';
|
||||
|
@ -45,25 +47,31 @@ class _InfoPageState extends State<InfoPage> {
|
|||
bottom: false,
|
||||
child: NotificationListener(
|
||||
onNotification: _handleTopScroll,
|
||||
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();
|
||||
},
|
||||
);
|
||||
child: NotificationListener<OpenTempEntryNotification>(
|
||||
onNotification: (notification) {
|
||||
_openTempEntry(notification.entry);
|
||||
return true;
|
||||
},
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
void _openTempEntry(AvesEntry tempEntry) {
|
||||
Navigator.push(
|
||||
context,
|
||||
TransparentMaterialPageRoute(
|
||||
settings: RouteSettings(name: EntryViewerPage.routeName),
|
||||
pageBuilder: (c, a, sa) => EntryViewerPage(
|
||||
initialEntry: tempEntry,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoPageContent extends StatefulWidget {
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/theme/icons.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_section.dart';
|
||||
import 'package:aves/widgets/viewer/info/notifications.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
@ -109,10 +112,28 @@ class InfoSearchDelegate extends SearchDelegate {
|
|||
icon: AIcons.info,
|
||||
text: 'No matching keys',
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: EdgeInsets.all(8),
|
||||
itemBuilder: (context, index) => tiles[index],
|
||||
itemCount: tiles.length,
|
||||
: NotificationListener<OpenTempEntryNotification>(
|
||||
onNotification: (notification) {
|
||||
_openTempEntry(context, notification.entry);
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,10 +6,8 @@ import 'package:aves/ref/xmp.dart';
|
|||
import 'package:aves/services/android_app_service.dart';
|
||||
import 'package:aves/services/metadata_service.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/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_ns/exif.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/tiff.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:flutter/material.dart';
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
|
@ -123,13 +122,6 @@ class _XmpDirTileState extends State<XmpDirTile> with FeedbackMixin {
|
|||
return;
|
||||
}
|
||||
|
||||
final embedEntry = AvesEntry.fromMap(fields);
|
||||
unawaited(Navigator.push(
|
||||
context,
|
||||
TransparentMaterialPageRoute(
|
||||
settings: RouteSettings(name: SingleEntryViewerPage.routeName),
|
||||
pageBuilder: (c, a, sa) => SingleEntryViewerPage(entry: embedEntry),
|
||||
),
|
||||
));
|
||||
OpenTempEntryNotification(entry: AvesEntry.fromMap(fields)).dispatch(context);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BackUpNotification extends Notification {}
|
||||
|
@ -8,3 +10,14 @@ class FilterNotification extends Notification {
|
|||
|
||||
const FilterNotification(this.filter);
|
||||
}
|
||||
|
||||
class OpenTempEntryNotification extends Notification {
|
||||
final AvesEntry entry;
|
||||
|
||||
const OpenTempEntryNotification({
|
||||
@required this.entry,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType#${shortHash(this)}{entry=$entry}';
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue