diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/AppAdapterHandler.java b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/AppAdapterHandler.java index 3e9fe33f0..0ce02be0a 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/AppAdapterHandler.java +++ b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/AppAdapterHandler.java @@ -3,7 +3,8 @@ package deckers.thibault.aves.channelhandlers; import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.support.annotation.NonNull; + +import androidx.annotation.NonNull; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageDecodeHandler.java b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageDecodeHandler.java index 457dd81a7..eab47245c 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageDecodeHandler.java +++ b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageDecodeHandler.java @@ -6,7 +6,8 @@ import android.app.AlertDialog; import android.content.Intent; import android.net.Uri; import android.provider.Settings; -import android.support.annotation.NonNull; + +import androidx.annotation.NonNull; import com.karumi.dexter.Dexter; import com.karumi.dexter.PermissionToken; diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageDecodeTask.java b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageDecodeTask.java index 5623b0958..c852e5adb 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageDecodeTask.java +++ b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageDecodeTask.java @@ -8,12 +8,15 @@ import android.util.Log; import com.bumptech.glide.Glide; import com.bumptech.glide.load.Key; +import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.request.FutureTarget; +import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.signature.ObjectKey; import java.io.ByteArrayOutputStream; import java.util.function.Consumer; +import deckers.thibault.aves.decoder.VideoThumbnail; import deckers.thibault.aves.model.ImageEntry; import deckers.thibault.aves.utils.Utils; import io.flutter.plugin.common.MethodChannel; @@ -61,11 +64,28 @@ public class ImageDecodeTask extends AsyncTask target = Glide.with(activity) - .asBitmap() - .load(entry.getUri()) + RequestOptions options = new RequestOptions() .signature(signature) - .submit(p.width, p.height); + .override(p.width, p.height); + + FutureTarget target; + if (entry.isVideo()) { + options = options.diskCacheStrategy(DiskCacheStrategy.RESOURCE); + target = Glide.with(activity) + .asBitmap() + .apply(options) + .load(new VideoThumbnail(activity, entry.getUri())) + .signature(signature) + .submit(p.width, p.height); + } else { + target = Glide.with(activity) + .asBitmap() + .apply(options) + .load(entry.getUri()) + .signature(signature) + .submit(p.width, p.height); + } + try { Bitmap bmp = target.get(); if (bmp != null) { diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MetadataHandler.java b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MetadataHandler.java index 6875358e6..c45c77173 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MetadataHandler.java +++ b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MetadataHandler.java @@ -1,6 +1,6 @@ package deckers.thibault.aves.channelhandlers; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; import com.adobe.xmp.XMPException; import com.adobe.xmp.XMPIterator; diff --git a/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnail.java b/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnail.java new file mode 100644 index 000000000..ff76284be --- /dev/null +++ b/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnail.java @@ -0,0 +1,22 @@ +package deckers.thibault.aves.decoder; + +import android.content.Context; +import android.net.Uri; + +public class VideoThumbnail { + private Context mContext; + private Uri mUri; + + public VideoThumbnail(Context context, Uri uri) { + mContext = context; + mUri = uri; + } + + public Context getContext() { + return mContext; + } + + Uri getUri() { + return mUri; + } +} \ No newline at end of file diff --git a/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailFetcher.java b/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailFetcher.java new file mode 100644 index 000000000..3c5c8bd20 --- /dev/null +++ b/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailFetcher.java @@ -0,0 +1,67 @@ +package deckers.thibault.aves.decoder; + +import android.graphics.Bitmap; +import android.media.MediaMetadataRetriever; + +import androidx.annotation.NonNull; + +import com.bumptech.glide.Priority; +import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.data.DataFetcher; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; + +class VideoThumbnailFetcher implements DataFetcher { + private final VideoThumbnail model; + + VideoThumbnailFetcher(VideoThumbnail model) { + this.model = model; + } + + @Override + public void loadData(@NonNull Priority priority, @NonNull DataCallback callback) { + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + try { + retriever.setDataSource(model.getContext(), model.getUri()); + byte[] picture = retriever.getEmbeddedPicture(); + if (picture != null) { + callback.onDataReady(new ByteArrayInputStream(picture)); + } else { + // not ideal: bitmap -> byte[] -> bitmap + // but simple fallback and we cache result + Bitmap bitmap = retriever.getFrameAtTime(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.PNG, 0, bos); + callback.onDataReady(new ByteArrayInputStream(bos.toByteArray())); + } + } catch (Exception ex) { + callback.onLoadFailed(ex); + } finally { + retriever.release(); + } + } + + @Override + public void cleanup() { + // already cleaned up in loadData and ByteArrayInputStream will be GC'd + } + + @Override + public void cancel() { + // cannot cancel + } + + @NonNull + @Override + public Class getDataClass() { + return InputStream.class; + } + + @NonNull + @Override + public DataSource getDataSource() { + return DataSource.LOCAL; + } +} \ No newline at end of file diff --git a/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.java b/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.java new file mode 100644 index 000000000..ccabbfade --- /dev/null +++ b/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.java @@ -0,0 +1,33 @@ +package deckers.thibault.aves.decoder; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.GlideBuilder; +import com.bumptech.glide.Registry; +import com.bumptech.glide.annotation.GlideModule; +import com.bumptech.glide.load.DecodeFormat; +import com.bumptech.glide.module.AppGlideModule; +import com.bumptech.glide.request.RequestOptions; + +import java.io.InputStream; + +@GlideModule +public class VideoThumbnailGlideModule extends AppGlideModule { + @Override + public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) { + builder.setDefaultRequestOptions(new RequestOptions().format(DecodeFormat.PREFER_RGB_565)); + } + + @Override + public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { + registry.append(VideoThumbnail.class, InputStream.class, new VideoThumbnailLoader.Factory()); + } + + @Override + public boolean isManifestParsingEnabled() { + return false; + } +} diff --git a/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailLoader.java b/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailLoader.java new file mode 100644 index 000000000..6b818eefa --- /dev/null +++ b/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailLoader.java @@ -0,0 +1,37 @@ +package deckers.thibault.aves.decoder; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.bumptech.glide.load.Options; +import com.bumptech.glide.load.model.ModelLoader; +import com.bumptech.glide.load.model.ModelLoaderFactory; +import com.bumptech.glide.load.model.MultiModelLoaderFactory; +import com.bumptech.glide.signature.ObjectKey; + +import java.io.InputStream; + +class VideoThumbnailLoader implements ModelLoader { + @Nullable + @Override + public LoadData buildLoadData(@NonNull VideoThumbnail model, int width, int height, @NonNull Options options) { + return new LoadData<>(new ObjectKey(model.getUri()), new VideoThumbnailFetcher(model)); + } + + @Override + public boolean handles(@NonNull VideoThumbnail videoThumbnail) { + return true; + } + + static class Factory implements ModelLoaderFactory { + @NonNull + @Override + public ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { + return new VideoThumbnailLoader(); + } + + @Override + public void teardown() { + } + } +} diff --git a/android/app/src/main/java/deckers/thibault/aves/model/ImageEntry.java b/android/app/src/main/java/deckers/thibault/aves/model/ImageEntry.java index 805394763..fa39f1fd6 100644 --- a/android/app/src/main/java/deckers/thibault/aves/model/ImageEntry.java +++ b/android/app/src/main/java/deckers/thibault/aves/model/ImageEntry.java @@ -1,8 +1,8 @@ package deckers.thibault.aves.model; import android.net.Uri; -import android.support.annotation.Nullable; -import android.text.format.DateUtils; + +import androidx.annotation.Nullable; import java.io.File; import java.util.HashMap; @@ -21,14 +21,11 @@ public class ImageEntry { private String title, bucketDisplayName; private long dateModifiedSecs, sourceDateTakenMillis; private long durationMillis; - // derived - private boolean isVideo; public ImageEntry(Uri uri, String mimeType) { this.contentId = -1; this.uri = uri; this.mimeType = mimeType; - init(); } // uri: content provider uri @@ -48,7 +45,6 @@ public class ImageEntry { this.sourceDateTakenMillis = dateTakenMillis; this.bucketDisplayName = bucketDisplayName; this.durationMillis = durationMillis; - init(); } public ImageEntry(Map map) { @@ -87,10 +83,6 @@ public class ImageEntry { }}; } - private void init() { - isVideo = mimeType.startsWith(Constants.MIME_VIDEO); - } - public Uri getUri() { return uri; } @@ -105,7 +97,7 @@ public class ImageEntry { } public boolean isVideo() { - return isVideo; + return mimeType.startsWith(Constants.MIME_VIDEO); } public boolean isEditable() { @@ -132,56 +124,14 @@ public class ImageEntry { return orientationDegrees; } - public long getSizeBytes() { - return sizeBytes; - } - - public String getTitle() { - return title; - } - public long getDateModifiedSecs() { return dateModifiedSecs; } - String getBucketDisplayName() { - return bucketDisplayName; - } - - public String getDuration() { - return DateUtils.formatElapsedTime(durationMillis / 1000); - } - - public int getMegaPixels() { - return Math.round((width * height) / 1000000f); - } - - // setters - - public void setContentId(long contentId) { - this.contentId = contentId; - } - - public void setPath(String path) { - this.path = path; - } - - public void setSizeBytes(long sizeBytes) { - this.sizeBytes = sizeBytes; - } - - public void setDateModifiedSecs(long dateModifiedSecs) { - this.dateModifiedSecs = dateModifiedSecs; - } - - public void setTitle(String title) { - this.title = title; - } - // convenience method private static long toLong(Object o) { - if (o instanceof Integer) return Long.valueOf((Integer)o); - return (long)o; + if (o instanceof Integer) return Long.valueOf((Integer) o); + return (long) o; } } diff --git a/android/app/src/main/java/deckers/thibault/aves/model/provider/MediaStoreImageProvider.java b/android/app/src/main/java/deckers/thibault/aves/model/provider/MediaStoreImageProvider.java index bf4580e2b..192808ca9 100644 --- a/android/app/src/main/java/deckers/thibault/aves/model/provider/MediaStoreImageProvider.java +++ b/android/app/src/main/java/deckers/thibault/aves/model/provider/MediaStoreImageProvider.java @@ -8,7 +8,6 @@ import android.provider.MediaStore; import android.util.Log; import java.util.ArrayList; -import java.util.List; import java.util.stream.Stream; import deckers.thibault.aves.model.ImageEntry; diff --git a/android/app/src/main/java/deckers/thibault/aves/utils/FileUtils.java b/android/app/src/main/java/deckers/thibault/aves/utils/FileUtils.java index f65b525d3..8ff209539 100644 --- a/android/app/src/main/java/deckers/thibault/aves/utils/FileUtils.java +++ b/android/app/src/main/java/deckers/thibault/aves/utils/FileUtils.java @@ -32,6 +32,7 @@ import android.provider.DocumentsContract; import android.provider.MediaStore; import android.text.TextUtils; import android.webkit.MimeTypeMap; + import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -40,172 +41,174 @@ import java.io.OutputStream; public class FileUtils { - public String getPathFromUri(final Context context, final Uri uri) { - String path = getPathFromLocalUri(context, uri); - if (path == null) { - path = getPathFromRemoteUri(context, uri); + public String getPathFromUri(final Context context, final Uri uri) { + String path = getPathFromLocalUri(context, uri); + if (path == null) { + path = getPathFromRemoteUri(context, uri); + } + return path; } - return path; - } - @SuppressLint("NewApi") - private String getPathFromLocalUri(final Context context, final Uri uri) { - final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + @SuppressLint("NewApi") + private String getPathFromLocalUri(final Context context, final Uri uri) { + final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; - if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { - if (isExternalStorageDocument(uri)) { - final String docId = DocumentsContract.getDocumentId(uri); - final String[] split = docId.split(":"); - final String type = split[0]; + if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { + if (isExternalStorageDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; - if ("primary".equalsIgnoreCase(type)) { - return Environment.getExternalStorageDirectory() + "/" + split[1]; - } - } else if (isDownloadsDocument(uri)) { - final String id = DocumentsContract.getDocumentId(uri); + if ("primary".equalsIgnoreCase(type)) { + return Environment.getExternalStorageDirectory() + "/" + split[1]; + } + } else if (isDownloadsDocument(uri)) { + final String id = DocumentsContract.getDocumentId(uri); - if (!TextUtils.isEmpty(id)) { - try { - final Uri contentUri = - ContentUris.withAppendedId( - Uri.parse(Environment.DIRECTORY_DOWNLOADS), Long.valueOf(id)); - return getDataColumn(context, contentUri, null, null); - } catch (NumberFormatException e) { - return null; - } + if (!TextUtils.isEmpty(id)) { + try { + final Uri contentUri = + ContentUris.withAppendedId( + Uri.parse(Environment.DIRECTORY_DOWNLOADS), Long.valueOf(id)); + return getDataColumn(context, contentUri, null, null); + } catch (NumberFormatException e) { + return null; + } + } + + } else if (isMediaDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + Uri contentUri = null; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + + final String selection = "_id=?"; + final String[] selectionArgs = new String[]{split[1]}; + + return getDataColumn(context, contentUri, selection, selectionArgs); + } + } else if ("content".equalsIgnoreCase(uri.getScheme())) { + + // Return the remote address + if (isGooglePhotosUri(uri)) { + return null; + } + + return getDataColumn(context, uri, null, null); + } else if ("file".equalsIgnoreCase(uri.getScheme())) { + return uri.getPath(); } - } else if (isMediaDocument(uri)) { - final String docId = DocumentsContract.getDocumentId(uri); - final String[] split = docId.split(":"); - final String type = split[0]; - - Uri contentUri = null; - if ("image".equals(type)) { - contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; - } else if ("video".equals(type)) { - contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; - } else if ("audio".equals(type)) { - contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; - } - - final String selection = "_id=?"; - final String[] selectionArgs = new String[] {split[1]}; - - return getDataColumn(context, contentUri, selection, selectionArgs); - } - } else if ("content".equalsIgnoreCase(uri.getScheme())) { - - // Return the remote address - if (isGooglePhotosUri(uri)) { return null; - } - - return getDataColumn(context, uri, null, null); - } else if ("file".equalsIgnoreCase(uri.getScheme())) { - return uri.getPath(); } - return null; - } + private static String getDataColumn( + Context context, Uri uri, String selection, String[] selectionArgs) { - private static String getDataColumn( - Context context, Uri uri, String selection, String[] selectionArgs) { + final String column = "_data"; + final String[] projection = {column}; + try (Cursor cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null)) { + if (cursor != null && cursor.moveToFirst()) { + final int column_index = cursor.getColumnIndex(column); - final String column = "_data"; - final String[] projection = {column}; - try (Cursor cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null)) { - if (cursor != null && cursor.moveToFirst()) { - final int column_index = cursor.getColumnIndex(column); + //yandex.disk and dropbox do not have _data column + if (column_index == -1) { + return null; + } - //yandex.disk and dropbox do not have _data column - if (column_index == -1) { - return null; + return cursor.getString(column_index); + } + } + return null; + } + + private static String getPathFromRemoteUri(final Context context, final Uri uri) { + // The code below is why Java now has try-with-resources and the Files utility. + File file = null; + InputStream inputStream = null; + OutputStream outputStream = null; + boolean success = false; + try { + String extension = getImageExtension(context, uri); + inputStream = context.getContentResolver().openInputStream(uri); + file = File.createTempFile("image_picker", extension, context.getCacheDir()); + outputStream = new FileOutputStream(file); + if (inputStream != null) { + copy(inputStream, outputStream); + success = true; + } + } catch (IOException ignored) { + } finally { + try { + if (inputStream != null) inputStream.close(); + } catch (IOException ignored) { + } + try { + if (outputStream != null) outputStream.close(); + } catch (IOException ignored) { + // If closing the output stream fails, we cannot be sure that the + // target file was written in full. Flushing the stream merely moves + // the bytes into the OS, not necessarily to the file. + success = false; + } + } + return success ? file.getPath() : null; + } + + /** + * @return extension of image with dot, or default .jpg if it none. + */ + private static String getImageExtension(Context context, Uri uriImage) { + String extension = null; + + try (Cursor cursor = context + .getContentResolver() + .query(uriImage, new String[]{MediaStore.MediaColumns.MIME_TYPE}, null, null, null)) { + + if (cursor != null && cursor.moveToNext()) { + String mimeType = cursor.getString(0); + + extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); + } } - return cursor.getString(column_index); - } - } - return null; - } - - private static String getPathFromRemoteUri(final Context context, final Uri uri) { - // The code below is why Java now has try-with-resources and the Files utility. - File file = null; - InputStream inputStream = null; - OutputStream outputStream = null; - boolean success = false; - try { - String extension = getImageExtension(context, uri); - inputStream = context.getContentResolver().openInputStream(uri); - file = File.createTempFile("image_picker", extension, context.getCacheDir()); - outputStream = new FileOutputStream(file); - if (inputStream != null) { - copy(inputStream, outputStream); - success = true; - } - } catch (IOException ignored) { - } finally { - try { - if (inputStream != null) inputStream.close(); - } catch (IOException ignored) { - } - try { - if (outputStream != null) outputStream.close(); - } catch (IOException ignored) { - // If closing the output stream fails, we cannot be sure that the - // target file was written in full. Flushing the stream merely moves - // the bytes into the OS, not necessarily to the file. - success = false; - } - } - return success ? file.getPath() : null; - } - - /** @return extension of image with dot, or default .jpg if it none. */ - private static String getImageExtension(Context context, Uri uriImage) { - String extension = null; - - try (Cursor cursor = context - .getContentResolver() - .query(uriImage, new String[]{MediaStore.MediaColumns.MIME_TYPE}, null, null, null)) { - - if (cursor != null && cursor.moveToNext()) { - String mimeType = cursor.getString(0); - - extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); - } + if (extension == null) { + //default extension for matches the previous behavior of the plugin + extension = "jpg"; + } + return "." + extension; } - if (extension == null) { - //default extension for matches the previous behavior of the plugin - extension = "jpg"; + private static void copy(InputStream in, OutputStream out) throws IOException { + final byte[] buffer = new byte[4 * 1024]; + int bytesRead; + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + } + out.flush(); } - return "." + extension; - } - private static void copy(InputStream in, OutputStream out) throws IOException { - final byte[] buffer = new byte[4 * 1024]; - int bytesRead; - while ((bytesRead = in.read(buffer)) != -1) { - out.write(buffer, 0, bytesRead); + private static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); } - out.flush(); - } - private static boolean isExternalStorageDocument(Uri uri) { - return "com.android.externalstorage.documents".equals(uri.getAuthority()); - } + private static boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } - private static boolean isDownloadsDocument(Uri uri) { - return "com.android.providers.downloads.documents".equals(uri.getAuthority()); - } + private static boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } - private static boolean isMediaDocument(Uri uri) { - return "com.android.providers.media.documents".equals(uri.getAuthority()); - } - - private static boolean isGooglePhotosUri(Uri uri) { - return "com.google.android.apps.photos.contentprovider".equals(uri.getAuthority()); - } + private static boolean isGooglePhotosUri(Uri uri) { + return "com.google.android.apps.photos.contentprovider".equals(uri.getAuthority()); + } } diff --git a/android/gradle.properties b/android/gradle.properties index 8bd86f680..755300e3a 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1 +1,4 @@ org.gradle.jvmargs=-Xmx1536M + +android.useAndroidX=true +android.enableJetifier=true diff --git a/lib/model/image_decode_service.dart b/lib/model/image_decode_service.dart index 4b9ee0836..387059459 100644 --- a/lib/model/image_decode_service.dart +++ b/lib/model/image_decode_service.dart @@ -16,7 +16,7 @@ class ImageDecodeService { } static Future getImageBytes(ImageEntry entry, int width, int height) async { - debugPrint('getImageBytes with uri=${entry.uri}'); + debugPrint('getImageBytes with path=${entry.path} contentId=${entry.contentId}'); if (width > 0 && height > 0) { try { final result = await platform.invokeMethod('getImageBytes', { diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart index 031f5b478..c57ad1311 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/image_entry.dart @@ -1,3 +1,5 @@ +import 'mime_types.dart'; + class ImageEntry { String uri; String path; @@ -65,6 +67,8 @@ class ImageEntry { }; } + bool get isVideo => mimeType.startsWith(MimeTypes.MIME_VIDEO); + int getMegaPixels() { return ((width * height) / 1000000).round(); } diff --git a/lib/widgets/fullscreen/info_page.dart b/lib/widgets/fullscreen/info_page.dart index 153c599ea..f078ea160 100644 --- a/lib/widgets/fullscreen/info_page.dart +++ b/lib/widgets/fullscreen/info_page.dart @@ -38,6 +38,8 @@ class InfoPageState extends State { @override Widget build(BuildContext context) { final date = entry.getBestDate(); + final dateText = '${DateFormat.yMMMd().format(date)} – ${DateFormat.Hm().format(date)}'; + final resolutionText = '${entry.width} × ${entry.height}${entry.isVideo ? '': ' (${entry.getMegaPixels()} MP)'}'; return Scaffold( appBar: AppBar( leading: IconButton( @@ -72,8 +74,8 @@ class InfoPageState extends State { children: [ SectionRow('File'), InfoRow('Title', entry.title), - InfoRow('Date', '${DateFormat.yMMMd().format(date)} – ${DateFormat.Hm().format(date)}'), - InfoRow('Resolution', '${entry.width} × ${entry.height} (${entry.getMegaPixels()} MP)'), + InfoRow('Date', dateText), + InfoRow('Resolution', resolutionText), InfoRow('Size', formatFilesize(entry.sizeBytes)), InfoRow('Path', entry.path), SectionRow('Metadata'),