multipage: open with default track
This commit is contained in:
parent
60243a20fd
commit
a6b99e7c2a
24 changed files with 163 additions and 143 deletions
|
@ -58,7 +58,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
|
||||||
val isFlipped = call.argument<Boolean>("isFlipped")
|
val isFlipped = call.argument<Boolean>("isFlipped")
|
||||||
val widthDip = call.argument<Double>("widthDip")
|
val widthDip = call.argument<Double>("widthDip")
|
||||||
val heightDip = call.argument<Double>("heightDip")
|
val heightDip = call.argument<Double>("heightDip")
|
||||||
val page = call.argument<Int>("page")
|
val pageId = call.argument<Int>("pageId")
|
||||||
val defaultSizeDip = call.argument<Double>("defaultSizeDip")
|
val defaultSizeDip = call.argument<Double>("defaultSizeDip")
|
||||||
|
|
||||||
if (uri == null || mimeType == null || dateModifiedSecs == null || rotationDegrees == null || isFlipped == null || widthDip == null || heightDip == null || defaultSizeDip == null) {
|
if (uri == null || mimeType == null || dateModifiedSecs == null || rotationDegrees == null || isFlipped == null || widthDip == null || heightDip == null || defaultSizeDip == null) {
|
||||||
|
@ -76,7 +76,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
|
||||||
isFlipped,
|
isFlipped,
|
||||||
width = (widthDip * density).roundToInt(),
|
width = (widthDip * density).roundToInt(),
|
||||||
height = (heightDip * density).roundToInt(),
|
height = (heightDip * density).roundToInt(),
|
||||||
page = page,
|
pageId = pageId,
|
||||||
defaultSize = (defaultSizeDip * density).roundToInt(),
|
defaultSize = (defaultSizeDip * density).roundToInt(),
|
||||||
result,
|
result,
|
||||||
).fetch()
|
).fetch()
|
||||||
|
@ -85,7 +85,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
|
||||||
private fun getRegion(call: MethodCall, result: MethodChannel.Result) {
|
private fun getRegion(call: MethodCall, result: MethodChannel.Result) {
|
||||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
val mimeType = call.argument<String>("mimeType")
|
val mimeType = call.argument<String>("mimeType")
|
||||||
val page = call.argument<Int>("page")
|
val pageId = call.argument<Int>("pageId")
|
||||||
val sampleSize = call.argument<Int>("sampleSize")
|
val sampleSize = call.argument<Int>("sampleSize")
|
||||||
val x = call.argument<Int>("regionX")
|
val x = call.argument<Int>("regionX")
|
||||||
val y = call.argument<Int>("regionY")
|
val y = call.argument<Int>("regionY")
|
||||||
|
@ -105,7 +105,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
|
||||||
uri,
|
uri,
|
||||||
sampleSize,
|
sampleSize,
|
||||||
regionRect,
|
regionRect,
|
||||||
page = page ?: 0,
|
page = pageId ?: 0,
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
else -> regionFetcher.fetch(
|
else -> regionFetcher.fetch(
|
||||||
|
|
|
@ -524,20 +524,21 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val pages = HashMap<Int, Any>()
|
val pages = ArrayList<Map<String, Any>>()
|
||||||
if (mimeType == MimeTypes.TIFF) {
|
if (mimeType == MimeTypes.TIFF) {
|
||||||
fun toMap(options: TiffBitmapFactory.Options): Map<String, Any> {
|
fun toMap(page: Int, options: TiffBitmapFactory.Options): HashMap<String, Any> {
|
||||||
return hashMapOf(
|
return hashMapOf(
|
||||||
|
KEY_PAGE to page,
|
||||||
KEY_MIME_TYPE to mimeType,
|
KEY_MIME_TYPE to mimeType,
|
||||||
KEY_WIDTH to options.outWidth,
|
KEY_WIDTH to options.outWidth,
|
||||||
KEY_HEIGHT to options.outHeight,
|
KEY_HEIGHT to options.outHeight,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
getTiffPageInfo(uri, 0)?.let { first ->
|
getTiffPageInfo(uri, 0)?.let { first ->
|
||||||
pages[0] = toMap(first)
|
pages.add(toMap(0, first))
|
||||||
val pageCount = first.outDirectoryCount
|
val pageCount = first.outDirectoryCount
|
||||||
for (i in 1 until pageCount) {
|
for (i in 1 until pageCount) {
|
||||||
getTiffPageInfo(uri, i)?.let { pages[i] = toMap(it) }
|
getTiffPageInfo(uri, i)?.let { pages.add(toMap(i, it)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (isHeifLike(mimeType)) {
|
} else if (isHeifLike(mimeType)) {
|
||||||
|
@ -556,14 +557,18 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
val format = extractor.getTrackFormat(i)
|
val format = extractor.getTrackFormat(i)
|
||||||
format.getString(MediaFormat.KEY_MIME)?.let { mime ->
|
format.getString(MediaFormat.KEY_MIME)?.let { mime ->
|
||||||
val trackMime = if (mime == MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC) MimeTypes.HEIC else mime
|
val trackMime = if (mime == MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC) MimeTypes.HEIC else mime
|
||||||
val page = hashMapOf<String, Any>(KEY_MIME_TYPE to trackMime)
|
val page = hashMapOf<String, Any>(
|
||||||
|
KEY_PAGE to i,
|
||||||
|
KEY_MIME_TYPE to trackMime,
|
||||||
|
)
|
||||||
|
format.getSafeInt(MediaFormat.KEY_IS_DEFAULT) { page[KEY_IS_DEFAULT] = it != 0 }
|
||||||
format.getSafeInt(MediaFormat.KEY_TRACK_ID) { page[KEY_TRACK_ID] = it }
|
format.getSafeInt(MediaFormat.KEY_TRACK_ID) { page[KEY_TRACK_ID] = it }
|
||||||
format.getSafeInt(MediaFormat.KEY_WIDTH) { page[KEY_WIDTH] = it }
|
format.getSafeInt(MediaFormat.KEY_WIDTH) { page[KEY_WIDTH] = it }
|
||||||
format.getSafeInt(MediaFormat.KEY_HEIGHT) { page[KEY_HEIGHT] = it }
|
format.getSafeInt(MediaFormat.KEY_HEIGHT) { page[KEY_HEIGHT] = it }
|
||||||
if (isVideo(trackMime)) {
|
if (isVideo(trackMime)) {
|
||||||
format.getSafeLong(MediaFormat.KEY_DURATION) { page[KEY_DURATION] = it / 1000 }
|
format.getSafeLong(MediaFormat.KEY_DURATION) { page[KEY_DURATION] = it / 1000 }
|
||||||
}
|
}
|
||||||
pages[i] = page
|
pages.add(page)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(LOG_TAG, "failed to get track information for uri=$uri, track num=$i", e)
|
Log.w(LOG_TAG, "failed to get track information for uri=$uri, track num=$i", e)
|
||||||
|
@ -778,7 +783,9 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
private const val KEY_XMP_TITLE_DESCRIPTION = "xmpTitleDescription"
|
private const val KEY_XMP_TITLE_DESCRIPTION = "xmpTitleDescription"
|
||||||
private const val KEY_HEIGHT = "height"
|
private const val KEY_HEIGHT = "height"
|
||||||
private const val KEY_WIDTH = "width"
|
private const val KEY_WIDTH = "width"
|
||||||
|
private const val KEY_PAGE = "page"
|
||||||
private const val KEY_TRACK_ID = "trackId"
|
private const val KEY_TRACK_ID = "trackId"
|
||||||
|
private const val KEY_IS_DEFAULT = "isDefault"
|
||||||
private const val KEY_DURATION = "durationMillis"
|
private const val KEY_DURATION = "durationMillis"
|
||||||
|
|
||||||
private const val MASK_IS_ANIMATED = 1 shl 0
|
private const val MASK_IS_ANIMATED = 1 shl 0
|
||||||
|
|
|
@ -34,7 +34,7 @@ class ThumbnailFetcher internal constructor(
|
||||||
private val isFlipped: Boolean,
|
private val isFlipped: Boolean,
|
||||||
width: Int?,
|
width: Int?,
|
||||||
height: Int?,
|
height: Int?,
|
||||||
private val page: Int?,
|
private val pageId: Int?,
|
||||||
private val defaultSize: Int,
|
private val defaultSize: Int,
|
||||||
private val result: MethodChannel.Result,
|
private val result: MethodChannel.Result,
|
||||||
) {
|
) {
|
||||||
|
@ -42,7 +42,7 @@ class ThumbnailFetcher internal constructor(
|
||||||
private val width: Int = if (width?.takeIf { it > 0 } != null) width else defaultSize
|
private val width: Int = if (width?.takeIf { it > 0 } != null) width else defaultSize
|
||||||
private val height: Int = if (height?.takeIf { it > 0 } != null) height else defaultSize
|
private val height: Int = if (height?.takeIf { it > 0 } != null) height else defaultSize
|
||||||
private val tiffFetch = mimeType == MimeTypes.TIFF
|
private val tiffFetch = mimeType == MimeTypes.TIFF
|
||||||
private val multiTrackFetch = isHeifLike(mimeType) && page != null
|
private val multiTrackFetch = isHeifLike(mimeType) && pageId != null
|
||||||
private val customFetch = tiffFetch || multiTrackFetch
|
private val customFetch = tiffFetch || multiTrackFetch
|
||||||
|
|
||||||
fun fetch() {
|
fun fetch() {
|
||||||
|
@ -114,7 +114,7 @@ class ThumbnailFetcher internal constructor(
|
||||||
// add signature to ignore cache for images which got modified but kept the same URI
|
// add signature to ignore cache for images which got modified but kept the same URI
|
||||||
var options = RequestOptions()
|
var options = RequestOptions()
|
||||||
.format(DecodeFormat.PREFER_RGB_565)
|
.format(DecodeFormat.PREFER_RGB_565)
|
||||||
.signature(ObjectKey("$dateModifiedSecs-$rotationDegrees-$isFlipped-$width-$page"))
|
.signature(ObjectKey("$dateModifiedSecs-$rotationDegrees-$isFlipped-$width-$pageId"))
|
||||||
.override(width, height)
|
.override(width, height)
|
||||||
|
|
||||||
val target = if (isVideo(mimeType)) {
|
val target = if (isVideo(mimeType)) {
|
||||||
|
@ -125,11 +125,11 @@ class ThumbnailFetcher internal constructor(
|
||||||
.load(VideoThumbnail(context, uri))
|
.load(VideoThumbnail(context, uri))
|
||||||
.submit(width, height)
|
.submit(width, height)
|
||||||
} else {
|
} else {
|
||||||
val model: Any = if (tiffFetch) {
|
val model: Any = when {
|
||||||
TiffThumbnail(context, uri, page ?: 0)
|
tiffFetch -> TiffThumbnail(context, uri, pageId ?: 0)
|
||||||
} else if (multiTrackFetch) {
|
multiTrackFetch -> MultiTrackImage(context, uri, pageId)
|
||||||
MultiTrackImage(context, uri, page ?: 0)
|
else -> uri
|
||||||
} else uri
|
}
|
||||||
Glide.with(context)
|
Glide.with(context)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.apply(options)
|
.apply(options)
|
||||||
|
|
|
@ -86,7 +86,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
|
||||||
val uri = (arguments["uri"] as String?)?.let { Uri.parse(it) }
|
val uri = (arguments["uri"] as String?)?.let { Uri.parse(it) }
|
||||||
val rotationDegrees = arguments["rotationDegrees"] as Int
|
val rotationDegrees = arguments["rotationDegrees"] as Int
|
||||||
val isFlipped = arguments["isFlipped"] as Boolean
|
val isFlipped = arguments["isFlipped"] as Boolean
|
||||||
val page = arguments["page"] as Int?
|
val pageId = arguments["pageId"] as Int?
|
||||||
|
|
||||||
if (mimeType == null || uri == null) {
|
if (mimeType == null || uri == null) {
|
||||||
error("streamImage-args", "failed because of missing arguments", null)
|
error("streamImage-args", "failed because of missing arguments", null)
|
||||||
|
@ -97,10 +97,10 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
|
||||||
if (isVideo(mimeType)) {
|
if (isVideo(mimeType)) {
|
||||||
streamVideoByGlide(uri)
|
streamVideoByGlide(uri)
|
||||||
} else if (mimeType == MimeTypes.TIFF) {
|
} else if (mimeType == MimeTypes.TIFF) {
|
||||||
streamTiffImage(uri, page)
|
streamTiffImage(uri, pageId)
|
||||||
} else if (!isSupportedByFlutter(mimeType, rotationDegrees, isFlipped)) {
|
} else if (!isSupportedByFlutter(mimeType, rotationDegrees, isFlipped)) {
|
||||||
// decode exotic format on platform side, then encode it in portable format for Flutter
|
// decode exotic format on platform side, then encode it in portable format for Flutter
|
||||||
streamImageByGlide(uri, page, mimeType, rotationDegrees, isFlipped)
|
streamImageByGlide(uri, pageId, mimeType, rotationDegrees, isFlipped)
|
||||||
} else {
|
} else {
|
||||||
// to be decoded by Flutter
|
// to be decoded by Flutter
|
||||||
streamImageAsIs(uri)
|
streamImageAsIs(uri)
|
||||||
|
@ -116,9 +116,9 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun streamImageByGlide(uri: Uri, page: Int?, mimeType: String, rotationDegrees: Int, isFlipped: Boolean) {
|
private fun streamImageByGlide(uri: Uri, pageId: Int?, mimeType: String, rotationDegrees: Int, isFlipped: Boolean) {
|
||||||
val model: Any = if (isHeifLike(mimeType) && page != null) {
|
val model: Any = if (isHeifLike(mimeType) && pageId != null) {
|
||||||
MultiTrackImage(activity, uri, page)
|
MultiTrackImage(activity, uri, pageId)
|
||||||
} else {
|
} else {
|
||||||
uri
|
uri
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ class MultiTrackImageGlideModule : LibraryGlideModule() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MultiTrackImage(val context: Context, val uri: Uri, val trackIndex: Int)
|
class MultiTrackImage(val context: Context, val uri: Uri, val trackId: Int?)
|
||||||
|
|
||||||
internal class MultiTrackThumbnailLoader : ModelLoader<MultiTrackImage, InputStream> {
|
internal class MultiTrackThumbnailLoader : ModelLoader<MultiTrackImage, InputStream> {
|
||||||
override fun buildLoadData(model: MultiTrackImage, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream> {
|
override fun buildLoadData(model: MultiTrackImage, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream> {
|
||||||
|
@ -53,9 +53,9 @@ internal class MultiTrackImageFetcher(val model: MultiTrackImage, val width: Int
|
||||||
|
|
||||||
val context = model.context
|
val context = model.context
|
||||||
val uri = model.uri
|
val uri = model.uri
|
||||||
val trackIndex = model.trackIndex
|
val trackId = model.trackId
|
||||||
|
|
||||||
val bitmap = MultiTrackMedia.getImage(context, uri, trackIndex)
|
val bitmap = MultiTrackMedia.getImage(context, uri, trackId)
|
||||||
if (bitmap == null) {
|
if (bitmap == null) {
|
||||||
callback.onLoadFailed(Exception("null bitmap"))
|
callback.onLoadFailed(Exception("null bitmap"))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -16,14 +16,17 @@ object MultiTrackMedia {
|
||||||
private val LOG_TAG = LogUtils.createTag(MultiTrackMedia::class.java)
|
private val LOG_TAG = LogUtils.createTag(MultiTrackMedia::class.java)
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.P)
|
@RequiresApi(Build.VERSION_CODES.P)
|
||||||
fun getImage(context: Context, uri: Uri, trackIndex: Int): Bitmap? {
|
fun getImage(context: Context, uri: Uri, trackId: Int?): Bitmap? {
|
||||||
val imageIndex = trackIndexToImageIndex(context, uri, trackIndex) ?: return null
|
|
||||||
|
|
||||||
val retriever = StorageUtils.openMetadataRetriever(context, uri) ?: return null
|
val retriever = StorageUtils.openMetadataRetriever(context, uri) ?: return null
|
||||||
try {
|
try {
|
||||||
return retriever.getImageAtIndex(imageIndex)
|
return if (trackId != null) {
|
||||||
|
val imageIndex = trackIdToImageIndex(context, uri, trackId) ?: return null
|
||||||
|
retriever.getImageAtIndex(imageIndex)
|
||||||
|
} else {
|
||||||
|
retriever.primaryImage
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(LOG_TAG, "failed to extract image from uri=$uri trackIndex=$trackIndex imageIndex=$imageIndex", e)
|
Log.w(LOG_TAG, "failed to extract image from uri=$uri trackId=$trackId", e)
|
||||||
} finally {
|
} finally {
|
||||||
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
|
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
|
||||||
retriever.release()
|
retriever.release()
|
||||||
|
@ -31,21 +34,23 @@ object MultiTrackMedia {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun trackIndexToImageIndex(context: Context, uri: Uri, trackIndex: Int): Int? {
|
private fun trackIdToImageIndex(context: Context, uri: Uri, trackId: Int): Int? {
|
||||||
val extractor = MediaExtractor()
|
val extractor = MediaExtractor()
|
||||||
try {
|
try {
|
||||||
extractor.setDataSource(context, uri, null)
|
extractor.setDataSource(context, uri, null)
|
||||||
val trackCount = extractor.trackCount
|
val trackCount = extractor.trackCount
|
||||||
if (trackIndex < trackCount) {
|
|
||||||
var imageIndex = 0
|
var imageIndex = 0
|
||||||
for (i in 0 until trackIndex) {
|
for (i in 0 until trackCount) {
|
||||||
val mimeType = extractor.getTrackFormat(i).getString(MediaFormat.KEY_MIME)
|
val trackFormat = extractor.getTrackFormat(i)
|
||||||
if (MimeTypes.isImage(mimeType)) imageIndex++
|
if (trackId == trackFormat.getInteger(MediaFormat.KEY_TRACK_ID)) {
|
||||||
}
|
|
||||||
return imageIndex
|
return imageIndex
|
||||||
}
|
}
|
||||||
|
if (MimeTypes.isImage(trackFormat.getString(MediaFormat.KEY_MIME))) {
|
||||||
|
imageIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(LOG_TAG, "failed to get image index for uri=$uri, trackIndex=$trackIndex", e)
|
Log.w(LOG_TAG, "failed to get image index for uri=$uri, trackId=$trackId", e)
|
||||||
} finally {
|
} finally {
|
||||||
extractor.release()
|
extractor.release()
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
|
||||||
codec: _loadAsync(key, decode),
|
codec: _loadAsync(key, decode),
|
||||||
scale: key.scale,
|
scale: key.scale,
|
||||||
informationCollector: () sync* {
|
informationCollector: () sync* {
|
||||||
yield ErrorDescription('uri=${key.uri}, page=${key.page}, mimeType=${key.mimeType}, region=${key.region}');
|
yield ErrorDescription('uri=${key.uri}, pageId=${key.pageId}, mimeType=${key.mimeType}, region=${key.region}');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
|
||||||
Future<ui.Codec> _loadAsync(RegionProviderKey key, DecoderCallback decode) async {
|
Future<ui.Codec> _loadAsync(RegionProviderKey key, DecoderCallback decode) async {
|
||||||
final uri = key.uri;
|
final uri = key.uri;
|
||||||
final mimeType = key.mimeType;
|
final mimeType = key.mimeType;
|
||||||
final page = key.page;
|
final pageId = key.pageId;
|
||||||
try {
|
try {
|
||||||
final bytes = await ImageFileService.getRegion(
|
final bytes = await ImageFileService.getRegion(
|
||||||
uri,
|
uri,
|
||||||
|
@ -40,7 +40,7 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
|
||||||
key.sampleSize,
|
key.sampleSize,
|
||||||
key.region,
|
key.region,
|
||||||
key.imageSize,
|
key.imageSize,
|
||||||
page: page,
|
pageId: pageId,
|
||||||
taskKey: key,
|
taskKey: key,
|
||||||
);
|
);
|
||||||
if (bytes == null) {
|
if (bytes == null) {
|
||||||
|
@ -49,7 +49,7 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
|
||||||
return await decode(bytes);
|
return await decode(bytes);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error');
|
debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error');
|
||||||
throw StateError('$mimeType region decoding failed (page $page)');
|
throw StateError('$mimeType region decoding failed (page $pageId)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ class RegionProviderKey {
|
||||||
// do not store the entry as it is, because the key should be constant
|
// do not store the entry as it is, because the key should be constant
|
||||||
// but the entry attributes may change over time
|
// but the entry attributes may change over time
|
||||||
final String uri, mimeType;
|
final String uri, mimeType;
|
||||||
final int page, rotationDegrees, sampleSize;
|
final int pageId, rotationDegrees, sampleSize;
|
||||||
final bool isFlipped;
|
final bool isFlipped;
|
||||||
final Rectangle<int> region;
|
final Rectangle<int> region;
|
||||||
final Size imageSize;
|
final Size imageSize;
|
||||||
|
@ -75,7 +75,7 @@ class RegionProviderKey {
|
||||||
const RegionProviderKey({
|
const RegionProviderKey({
|
||||||
@required this.uri,
|
@required this.uri,
|
||||||
@required this.mimeType,
|
@required this.mimeType,
|
||||||
@required this.page,
|
@required this.pageId,
|
||||||
@required this.rotationDegrees,
|
@required this.rotationDegrees,
|
||||||
@required this.isFlipped,
|
@required this.isFlipped,
|
||||||
@required this.sampleSize,
|
@required this.sampleSize,
|
||||||
|
@ -94,14 +94,14 @@ class RegionProviderKey {
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (other.runtimeType != runtimeType) return false;
|
if (other.runtimeType != runtimeType) return false;
|
||||||
return other is RegionProviderKey && other.uri == uri && other.mimeType == mimeType && other.page == page && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.sampleSize == sampleSize && other.region == region && other.imageSize == imageSize && other.scale == scale;
|
return other is RegionProviderKey && other.uri == uri && other.mimeType == mimeType && other.pageId == pageId && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.sampleSize == sampleSize && other.region == region && other.imageSize == imageSize && other.scale == scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => hashValues(
|
int get hashCode => hashValues(
|
||||||
uri,
|
uri,
|
||||||
mimeType,
|
mimeType,
|
||||||
page,
|
pageId,
|
||||||
rotationDegrees,
|
rotationDegrees,
|
||||||
isFlipped,
|
isFlipped,
|
||||||
sampleSize,
|
sampleSize,
|
||||||
|
@ -111,5 +111,5 @@ class RegionProviderKey {
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, page=$page, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, sampleSize=$sampleSize, region=$region, imageSize=$imageSize, scale=$scale}';
|
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, pageId=$pageId, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, sampleSize=$sampleSize, region=$region, imageSize=$imageSize, scale=$scale}';
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
||||||
codec: _loadAsync(key, decode),
|
codec: _loadAsync(key, decode),
|
||||||
scale: key.scale,
|
scale: key.scale,
|
||||||
informationCollector: () sync* {
|
informationCollector: () sync* {
|
||||||
yield ErrorDescription('uri=${key.uri}, page=${key.page}, mimeType=${key.mimeType}, extent=${key.extent}');
|
yield ErrorDescription('uri=${key.uri}, pageId=${key.pageId}, mimeType=${key.mimeType}, extent=${key.extent}');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -31,12 +31,12 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
||||||
Future<ui.Codec> _loadAsync(ThumbnailProviderKey key, DecoderCallback decode) async {
|
Future<ui.Codec> _loadAsync(ThumbnailProviderKey key, DecoderCallback decode) async {
|
||||||
final uri = key.uri;
|
final uri = key.uri;
|
||||||
final mimeType = key.mimeType;
|
final mimeType = key.mimeType;
|
||||||
final page = key.page;
|
final pageId = key.pageId;
|
||||||
try {
|
try {
|
||||||
final bytes = await ImageFileService.getThumbnail(
|
final bytes = await ImageFileService.getThumbnail(
|
||||||
uri: uri,
|
uri: uri,
|
||||||
mimeType: mimeType,
|
mimeType: mimeType,
|
||||||
page: page,
|
pageId: pageId,
|
||||||
rotationDegrees: key.rotationDegrees,
|
rotationDegrees: key.rotationDegrees,
|
||||||
isFlipped: key.isFlipped,
|
isFlipped: key.isFlipped,
|
||||||
dateModifiedSecs: key.dateModifiedSecs,
|
dateModifiedSecs: key.dateModifiedSecs,
|
||||||
|
@ -49,7 +49,7 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
||||||
return await decode(bytes);
|
return await decode(bytes);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugPrint('$runtimeType _loadAsync failed with uri=$uri, error=$error');
|
debugPrint('$runtimeType _loadAsync failed with uri=$uri, error=$error');
|
||||||
throw StateError('$mimeType decoding failed (page $page)');
|
throw StateError('$mimeType decoding failed (page $pageId)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ class ThumbnailProviderKey {
|
||||||
// do not store the entry as it is, because the key should be constant
|
// do not store the entry as it is, because the key should be constant
|
||||||
// but the entry attributes may change over time
|
// but the entry attributes may change over time
|
||||||
final String uri, mimeType;
|
final String uri, mimeType;
|
||||||
final int page, rotationDegrees;
|
final int pageId, rotationDegrees;
|
||||||
final bool isFlipped;
|
final bool isFlipped;
|
||||||
final int dateModifiedSecs;
|
final int dateModifiedSecs;
|
||||||
final double extent, scale;
|
final double extent, scale;
|
||||||
|
@ -74,7 +74,7 @@ class ThumbnailProviderKey {
|
||||||
const ThumbnailProviderKey({
|
const ThumbnailProviderKey({
|
||||||
@required this.uri,
|
@required this.uri,
|
||||||
@required this.mimeType,
|
@required this.mimeType,
|
||||||
@required this.page,
|
@required this.pageId,
|
||||||
@required this.rotationDegrees,
|
@required this.rotationDegrees,
|
||||||
@required this.isFlipped,
|
@required this.isFlipped,
|
||||||
@required this.dateModifiedSecs,
|
@required this.dateModifiedSecs,
|
||||||
|
@ -91,14 +91,14 @@ class ThumbnailProviderKey {
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (other.runtimeType != runtimeType) return false;
|
if (other.runtimeType != runtimeType) return false;
|
||||||
return other is ThumbnailProviderKey && other.uri == uri && other.mimeType == mimeType && other.page == page && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.dateModifiedSecs == dateModifiedSecs && other.extent == extent && other.scale == scale;
|
return other is ThumbnailProviderKey && other.uri == uri && other.mimeType == mimeType && other.pageId == pageId && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.dateModifiedSecs == dateModifiedSecs && other.extent == extent && other.scale == scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => hashValues(
|
int get hashCode => hashValues(
|
||||||
uri,
|
uri,
|
||||||
mimeType,
|
mimeType,
|
||||||
page,
|
pageId,
|
||||||
rotationDegrees,
|
rotationDegrees,
|
||||||
isFlipped,
|
isFlipped,
|
||||||
dateModifiedSecs,
|
dateModifiedSecs,
|
||||||
|
@ -107,5 +107,5 @@ class ThumbnailProviderKey {
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, page=$page, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, dateModifiedSecs=$dateModifiedSecs, extent=$extent, scale=$scale}';
|
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, pageId=$pageId, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, dateModifiedSecs=$dateModifiedSecs, extent=$extent, scale=$scale}';
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,14 +8,14 @@ import 'package:pedantic/pedantic.dart';
|
||||||
|
|
||||||
class UriImage extends ImageProvider<UriImage> {
|
class UriImage extends ImageProvider<UriImage> {
|
||||||
final String uri, mimeType;
|
final String uri, mimeType;
|
||||||
final int page, rotationDegrees, expectedContentLength;
|
final int pageId, rotationDegrees, expectedContentLength;
|
||||||
final bool isFlipped;
|
final bool isFlipped;
|
||||||
final double scale;
|
final double scale;
|
||||||
|
|
||||||
const UriImage({
|
const UriImage({
|
||||||
@required this.uri,
|
@required this.uri,
|
||||||
@required this.mimeType,
|
@required this.mimeType,
|
||||||
@required this.page,
|
@required this.pageId,
|
||||||
@required this.rotationDegrees,
|
@required this.rotationDegrees,
|
||||||
@required this.isFlipped,
|
@required this.isFlipped,
|
||||||
this.expectedContentLength,
|
this.expectedContentLength,
|
||||||
|
@ -37,7 +37,7 @@ class UriImage extends ImageProvider<UriImage> {
|
||||||
scale: key.scale,
|
scale: key.scale,
|
||||||
chunkEvents: chunkEvents.stream,
|
chunkEvents: chunkEvents.stream,
|
||||||
informationCollector: () sync* {
|
informationCollector: () sync* {
|
||||||
yield ErrorDescription('uri=$uri, page=$page, mimeType=$mimeType');
|
yield ErrorDescription('uri=$uri, pageId=$pageId, mimeType=$mimeType');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ class UriImage extends ImageProvider<UriImage> {
|
||||||
mimeType,
|
mimeType,
|
||||||
rotationDegrees,
|
rotationDegrees,
|
||||||
isFlipped,
|
isFlipped,
|
||||||
page: page,
|
pageId: pageId,
|
||||||
expectedContentLength: expectedContentLength,
|
expectedContentLength: expectedContentLength,
|
||||||
onBytesReceived: (cumulative, total) {
|
onBytesReceived: (cumulative, total) {
|
||||||
chunkEvents.add(ImageChunkEvent(
|
chunkEvents.add(ImageChunkEvent(
|
||||||
|
@ -66,7 +66,7 @@ class UriImage extends ImageProvider<UriImage> {
|
||||||
return await decode(bytes);
|
return await decode(bytes);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error');
|
debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error');
|
||||||
throw StateError('$mimeType decoding failed (page $page)');
|
throw StateError('$mimeType decoding failed (page $pageId)');
|
||||||
} finally {
|
} finally {
|
||||||
unawaited(chunkEvents.close());
|
unawaited(chunkEvents.close());
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,7 @@ class UriImage extends ImageProvider<UriImage> {
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (other.runtimeType != runtimeType) return false;
|
if (other.runtimeType != runtimeType) return false;
|
||||||
return other is UriImage && other.uri == uri && other.mimeType == mimeType && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.page == page && other.scale == scale;
|
return other is UriImage && other.uri == uri && other.mimeType == mimeType && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.pageId == pageId && other.scale == scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -84,10 +84,10 @@ class UriImage extends ImageProvider<UriImage> {
|
||||||
mimeType,
|
mimeType,
|
||||||
rotationDegrees,
|
rotationDegrees,
|
||||||
isFlipped,
|
isFlipped,
|
||||||
page,
|
pageId,
|
||||||
scale,
|
scale,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, page=$page, scale=$scale}';
|
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, pageId=$pageId, scale=$scale}';
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ import '../ref/mime_types.dart';
|
||||||
class AvesEntry {
|
class AvesEntry {
|
||||||
String uri;
|
String uri;
|
||||||
String _path, _directory, _filename, _extension;
|
String _path, _directory, _filename, _extension;
|
||||||
int page, contentId;
|
int pageId, contentId;
|
||||||
final String sourceMimeType;
|
final String sourceMimeType;
|
||||||
int width;
|
int width;
|
||||||
int height;
|
int height;
|
||||||
|
@ -49,7 +49,7 @@ class AvesEntry {
|
||||||
this.uri,
|
this.uri,
|
||||||
String path,
|
String path,
|
||||||
this.contentId,
|
this.contentId,
|
||||||
this.page,
|
this.pageId,
|
||||||
this.sourceMimeType,
|
this.sourceMimeType,
|
||||||
@required this.width,
|
@required this.width,
|
||||||
@required this.height,
|
@required this.height,
|
||||||
|
@ -96,18 +96,14 @@ class AvesEntry {
|
||||||
return copied;
|
return copied;
|
||||||
}
|
}
|
||||||
|
|
||||||
AvesEntry getPageEntry({
|
AvesEntry getPageEntry(SinglePageInfo pageInfo) {
|
||||||
@required MultiPageInfo multiPageInfo,
|
|
||||||
@required int page,
|
|
||||||
}) {
|
|
||||||
final pageInfo = (multiPageInfo?.pages ?? {})[page];
|
|
||||||
if (pageInfo == null) return this;
|
if (pageInfo == null) return this;
|
||||||
return AvesPageEntry(
|
return AvesPageEntry(
|
||||||
pageInfo: pageInfo,
|
pageInfo: pageInfo,
|
||||||
uri: uri,
|
uri: uri,
|
||||||
path: path,
|
path: path,
|
||||||
contentId: contentId,
|
contentId: contentId,
|
||||||
page: page,
|
pageId: pageInfo.pageId,
|
||||||
sourceMimeType: sourceMimeType,
|
sourceMimeType: sourceMimeType,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
|
@ -168,7 +164,7 @@ class AvesEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, path=$path}';
|
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, path=$path, pageId=$pageId}';
|
||||||
|
|
||||||
set path(String path) {
|
set path(String path) {
|
||||||
_path = path;
|
_path = path;
|
||||||
|
@ -227,7 +223,7 @@ class AvesEntry {
|
||||||
MimeTypes.srw,
|
MimeTypes.srw,
|
||||||
].contains(mimeType) &&
|
].contains(mimeType) &&
|
||||||
!isAnimated &&
|
!isAnimated &&
|
||||||
page == null;
|
pageId == null;
|
||||||
|
|
||||||
bool get supportTiling => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff;
|
bool get supportTiling => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff;
|
||||||
|
|
||||||
|
|
|
@ -12,14 +12,14 @@ class EntryCache {
|
||||||
int oldRotationDegrees,
|
int oldRotationDegrees,
|
||||||
bool oldIsFlipped,
|
bool oldIsFlipped,
|
||||||
) async {
|
) async {
|
||||||
// TODO TLAD provide page parameter for multipage items, if someday image editing features are added for them
|
// TODO TLAD provide pageId parameter for multipage items, if someday image editing features are added for them
|
||||||
int page;
|
int pageId;
|
||||||
|
|
||||||
// evict fullscreen image
|
// evict fullscreen image
|
||||||
await UriImage(
|
await UriImage(
|
||||||
uri: uri,
|
uri: uri,
|
||||||
mimeType: mimeType,
|
mimeType: mimeType,
|
||||||
page: page,
|
pageId: pageId,
|
||||||
rotationDegrees: oldRotationDegrees,
|
rotationDegrees: oldRotationDegrees,
|
||||||
isFlipped: oldIsFlipped,
|
isFlipped: oldIsFlipped,
|
||||||
).evict();
|
).evict();
|
||||||
|
@ -28,7 +28,7 @@ class EntryCache {
|
||||||
await ThumbnailProvider(ThumbnailProviderKey(
|
await ThumbnailProvider(ThumbnailProviderKey(
|
||||||
uri: uri,
|
uri: uri,
|
||||||
mimeType: mimeType,
|
mimeType: mimeType,
|
||||||
page: page,
|
pageId: pageId,
|
||||||
dateModifiedSecs: dateModifiedSecs,
|
dateModifiedSecs: dateModifiedSecs,
|
||||||
rotationDegrees: oldRotationDegrees,
|
rotationDegrees: oldRotationDegrees,
|
||||||
isFlipped: oldIsFlipped,
|
isFlipped: oldIsFlipped,
|
||||||
|
@ -41,7 +41,7 @@ class EntryCache {
|
||||||
(extent) => ThumbnailProvider(ThumbnailProviderKey(
|
(extent) => ThumbnailProvider(ThumbnailProviderKey(
|
||||||
uri: uri,
|
uri: uri,
|
||||||
mimeType: mimeType,
|
mimeType: mimeType,
|
||||||
page: page,
|
pageId: pageId,
|
||||||
dateModifiedSecs: dateModifiedSecs,
|
dateModifiedSecs: dateModifiedSecs,
|
||||||
rotationDegrees: oldRotationDegrees,
|
rotationDegrees: oldRotationDegrees,
|
||||||
isFlipped: oldIsFlipped,
|
isFlipped: oldIsFlipped,
|
||||||
|
|
|
@ -20,7 +20,7 @@ extension ExtraAvesEntry on AvesEntry {
|
||||||
return ThumbnailProviderKey(
|
return ThumbnailProviderKey(
|
||||||
uri: uri,
|
uri: uri,
|
||||||
mimeType: mimeType,
|
mimeType: mimeType,
|
||||||
page: page,
|
pageId: pageId,
|
||||||
rotationDegrees: rotationDegrees,
|
rotationDegrees: rotationDegrees,
|
||||||
isFlipped: isFlipped,
|
isFlipped: isFlipped,
|
||||||
dateModifiedSecs: dateModifiedSecs ?? -1,
|
dateModifiedSecs: dateModifiedSecs ?? -1,
|
||||||
|
@ -34,7 +34,7 @@ extension ExtraAvesEntry on AvesEntry {
|
||||||
return RegionProviderKey(
|
return RegionProviderKey(
|
||||||
uri: uri,
|
uri: uri,
|
||||||
mimeType: mimeType,
|
mimeType: mimeType,
|
||||||
page: page,
|
pageId: pageId,
|
||||||
rotationDegrees: rotationDegrees,
|
rotationDegrees: rotationDegrees,
|
||||||
isFlipped: isFlipped,
|
isFlipped: isFlipped,
|
||||||
sampleSize: sampleSize,
|
sampleSize: sampleSize,
|
||||||
|
@ -46,7 +46,7 @@ extension ExtraAvesEntry on AvesEntry {
|
||||||
UriImage get uriImage => UriImage(
|
UriImage get uriImage => UriImage(
|
||||||
uri: uri,
|
uri: uri,
|
||||||
mimeType: mimeType,
|
mimeType: mimeType,
|
||||||
page: page,
|
pageId: pageId,
|
||||||
rotationDegrees: rotationDegrees,
|
rotationDegrees: rotationDegrees,
|
||||||
isFlipped: isFlipped,
|
isFlipped: isFlipped,
|
||||||
expectedContentLength: sizeBytes,
|
expectedContentLength: sizeBytes,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import 'package:aves/model/entry.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
class MultiPageInfo {
|
class MultiPageInfo {
|
||||||
final Map<int, SinglePageInfo> pages;
|
final List<SinglePageInfo> pages;
|
||||||
|
|
||||||
int get pageCount => pages.length;
|
int get pageCount => pages.length;
|
||||||
|
|
||||||
|
@ -10,44 +10,49 @@ class MultiPageInfo {
|
||||||
this.pages,
|
this.pages,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory MultiPageInfo.fromMap(Map map) {
|
factory MultiPageInfo.fromPageMaps(List<Map> pageMaps) {
|
||||||
final pages = <int, SinglePageInfo>{};
|
return MultiPageInfo(pages: pageMaps.map((page) => SinglePageInfo.fromMap(page)).toList());
|
||||||
map.keys.forEach((key) {
|
|
||||||
final index = key as int;
|
|
||||||
pages.putIfAbsent(index, () => SinglePageInfo.fromMap(map[key]));
|
|
||||||
});
|
|
||||||
return MultiPageInfo(pages: pages);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SinglePageInfo getByIndex(int index) => pages.firstWhere((page) => page.index == index, orElse: () => null);
|
||||||
|
|
||||||
|
SinglePageInfo getById(int pageId) => pages.firstWhere((page) => page.pageId == pageId, orElse: () => null);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '$runtimeType#${shortHash(this)}{pages=$pages}';
|
String toString() => '$runtimeType#${shortHash(this)}{pages=$pages}';
|
||||||
}
|
}
|
||||||
|
|
||||||
class SinglePageInfo {
|
class SinglePageInfo {
|
||||||
|
final int index, pageId;
|
||||||
final String mimeType;
|
final String mimeType;
|
||||||
final int width, height;
|
final bool isDefault;
|
||||||
final int trackId, durationMillis;
|
final int width, height, durationMillis;
|
||||||
|
|
||||||
SinglePageInfo({
|
SinglePageInfo({
|
||||||
|
this.index,
|
||||||
|
this.pageId,
|
||||||
this.mimeType,
|
this.mimeType,
|
||||||
|
this.isDefault,
|
||||||
this.width,
|
this.width,
|
||||||
this.height,
|
this.height,
|
||||||
this.trackId,
|
|
||||||
this.durationMillis,
|
this.durationMillis,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory SinglePageInfo.fromMap(Map map) {
|
factory SinglePageInfo.fromMap(Map map) {
|
||||||
|
final index = map['page'] as int;
|
||||||
return SinglePageInfo(
|
return SinglePageInfo(
|
||||||
|
index: index,
|
||||||
|
pageId: map['trackId'] as int ?? index,
|
||||||
mimeType: map['mimeType'] as String,
|
mimeType: map['mimeType'] as String,
|
||||||
width: map['width'] as int,
|
isDefault: map['isDefault'] as bool ?? false,
|
||||||
height: map['height'] as int,
|
width: map['width'] as int ?? 0,
|
||||||
trackId: map['trackId'] as int,
|
height: map['height'] as int ?? 0,
|
||||||
durationMillis: map['durationMillis'] as int,
|
durationMillis: map['durationMillis'] as int,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '$runtimeType#${shortHash(this)}{mimeType=$mimeType, width=$width, height=$height, trackId=$trackId, durationMillis=$durationMillis}';
|
String toString() => '$runtimeType#${shortHash(this)}{index=$index, pageId=$pageId, mimeType=$mimeType, isDefault=$isDefault, width=$width, height=$height, durationMillis=$durationMillis}';
|
||||||
}
|
}
|
||||||
|
|
||||||
class AvesPageEntry extends AvesEntry {
|
class AvesPageEntry extends AvesEntry {
|
||||||
|
@ -58,7 +63,7 @@ class AvesPageEntry extends AvesEntry {
|
||||||
String uri,
|
String uri,
|
||||||
String path,
|
String path,
|
||||||
int contentId,
|
int contentId,
|
||||||
int page,
|
int pageId,
|
||||||
String sourceMimeType,
|
String sourceMimeType,
|
||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
|
@ -72,7 +77,7 @@ class AvesPageEntry extends AvesEntry {
|
||||||
uri: uri,
|
uri: uri,
|
||||||
path: path,
|
path: path,
|
||||||
contentId: contentId,
|
contentId: contentId,
|
||||||
page: page,
|
pageId: pageId,
|
||||||
sourceMimeType: pageInfo.mimeType ?? sourceMimeType,
|
sourceMimeType: pageInfo.mimeType ?? sourceMimeType,
|
||||||
width: pageInfo.width ?? width,
|
width: pageInfo.width ?? width,
|
||||||
height: pageInfo.height ?? height,
|
height: pageInfo.height ?? height,
|
||||||
|
|
|
@ -33,7 +33,7 @@ class AppShortcutService {
|
||||||
iconBytes = await ImageFileService.getThumbnail(
|
iconBytes = await ImageFileService.getThumbnail(
|
||||||
uri: entry.uri,
|
uri: entry.uri,
|
||||||
mimeType: entry.mimeType,
|
mimeType: entry.mimeType,
|
||||||
page: entry.page,
|
pageId: entry.pageId,
|
||||||
rotationDegrees: entry.rotationDegrees,
|
rotationDegrees: entry.rotationDegrees,
|
||||||
isFlipped: entry.isFlipped,
|
isFlipped: entry.isFlipped,
|
||||||
dateModifiedSecs: entry.dateModifiedSecs,
|
dateModifiedSecs: entry.dateModifiedSecs,
|
||||||
|
|
|
@ -89,7 +89,7 @@ class ImageFileService {
|
||||||
String mimeType,
|
String mimeType,
|
||||||
int rotationDegrees,
|
int rotationDegrees,
|
||||||
bool isFlipped, {
|
bool isFlipped, {
|
||||||
int page,
|
int pageId,
|
||||||
int expectedContentLength,
|
int expectedContentLength,
|
||||||
BytesReceivedCallback onBytesReceived,
|
BytesReceivedCallback onBytesReceived,
|
||||||
}) {
|
}) {
|
||||||
|
@ -102,7 +102,7 @@ class ImageFileService {
|
||||||
'mimeType': mimeType,
|
'mimeType': mimeType,
|
||||||
'rotationDegrees': rotationDegrees ?? 0,
|
'rotationDegrees': rotationDegrees ?? 0,
|
||||||
'isFlipped': isFlipped ?? false,
|
'isFlipped': isFlipped ?? false,
|
||||||
'page': page,
|
'pageId': pageId,
|
||||||
}).listen(
|
}).listen(
|
||||||
(data) {
|
(data) {
|
||||||
final chunk = data as Uint8List;
|
final chunk = data as Uint8List;
|
||||||
|
@ -140,7 +140,7 @@ class ImageFileService {
|
||||||
int sampleSize,
|
int sampleSize,
|
||||||
Rectangle<int> regionRect,
|
Rectangle<int> regionRect,
|
||||||
Size imageSize, {
|
Size imageSize, {
|
||||||
int page,
|
int pageId,
|
||||||
Object taskKey,
|
Object taskKey,
|
||||||
int priority,
|
int priority,
|
||||||
}) {
|
}) {
|
||||||
|
@ -150,7 +150,7 @@ class ImageFileService {
|
||||||
final result = await platform.invokeMethod('getRegion', <String, dynamic>{
|
final result = await platform.invokeMethod('getRegion', <String, dynamic>{
|
||||||
'uri': uri,
|
'uri': uri,
|
||||||
'mimeType': mimeType,
|
'mimeType': mimeType,
|
||||||
'page': page,
|
'pageId': pageId,
|
||||||
'sampleSize': sampleSize,
|
'sampleSize': sampleSize,
|
||||||
'regionX': regionRect.left,
|
'regionX': regionRect.left,
|
||||||
'regionY': regionRect.top,
|
'regionY': regionRect.top,
|
||||||
|
@ -174,7 +174,7 @@ class ImageFileService {
|
||||||
@required String uri,
|
@required String uri,
|
||||||
@required String mimeType,
|
@required String mimeType,
|
||||||
@required int rotationDegrees,
|
@required int rotationDegrees,
|
||||||
@required int page,
|
@required int pageId,
|
||||||
@required bool isFlipped,
|
@required bool isFlipped,
|
||||||
@required int dateModifiedSecs,
|
@required int dateModifiedSecs,
|
||||||
@required double extent,
|
@required double extent,
|
||||||
|
@ -195,7 +195,7 @@ class ImageFileService {
|
||||||
'isFlipped': isFlipped,
|
'isFlipped': isFlipped,
|
||||||
'widthDip': extent,
|
'widthDip': extent,
|
||||||
'heightDip': extent,
|
'heightDip': extent,
|
||||||
'page': page,
|
'pageId': pageId,
|
||||||
'defaultSizeDip': thumbnailDefaultSize,
|
'defaultSizeDip': thumbnailDefaultSize,
|
||||||
});
|
});
|
||||||
return result as Uint8List;
|
return result as Uint8List;
|
||||||
|
|
|
@ -87,8 +87,9 @@ class MetadataService {
|
||||||
final result = await platform.invokeMethod('getMultiPageInfo', <String, dynamic>{
|
final result = await platform.invokeMethod('getMultiPageInfo', <String, dynamic>{
|
||||||
'mimeType': entry.mimeType,
|
'mimeType': entry.mimeType,
|
||||||
'uri': entry.uri,
|
'uri': entry.uri,
|
||||||
}) as Map;
|
});
|
||||||
return MultiPageInfo.fromMap(result);
|
final pageMaps = (result as List).cast<Map>();
|
||||||
|
return MultiPageInfo.fromPageMaps(pageMaps);
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
debugPrint('getMultiPageInfo failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
debugPrint('getMultiPageInfo failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ import 'package:flutter/material.dart';
|
||||||
class RasterImageThumbnail extends StatefulWidget {
|
class RasterImageThumbnail extends StatefulWidget {
|
||||||
final AvesEntry entry;
|
final AvesEntry entry;
|
||||||
final double extent;
|
final double extent;
|
||||||
final int page;
|
|
||||||
final ValueNotifier<bool> isScrollingNotifier;
|
final ValueNotifier<bool> isScrollingNotifier;
|
||||||
final Object heroTag;
|
final Object heroTag;
|
||||||
|
|
||||||
|
@ -17,7 +16,6 @@ class RasterImageThumbnail extends StatefulWidget {
|
||||||
Key key,
|
Key key,
|
||||||
@required this.entry,
|
@required this.entry,
|
||||||
@required this.extent,
|
@required this.extent,
|
||||||
this.page,
|
|
||||||
this.isScrollingNotifier,
|
this.isScrollingNotifier,
|
||||||
this.heroTag,
|
this.heroTag,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
|
@ -62,7 +62,7 @@ class _MultiEntryScrollerState extends State<MultiEntryScroller> with AutomaticK
|
||||||
return ValueListenableBuilder<int>(
|
return ValueListenableBuilder<int>(
|
||||||
valueListenable: multiPageController.pageNotifier,
|
valueListenable: multiPageController.pageNotifier,
|
||||||
builder: (context, page, child) {
|
builder: (context, page, child) {
|
||||||
return _buildViewer(entry, multiPageInfo: multiPageInfo, page: page);
|
return _buildViewer(entry, page: multiPageInfo?.getByIndex(page));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -80,14 +80,13 @@ class _MultiEntryScrollerState extends State<MultiEntryScroller> with AutomaticK
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildViewer(AvesEntry entry, {MultiPageInfo multiPageInfo, int page}) {
|
Widget _buildViewer(AvesEntry entry, {SinglePageInfo page}) {
|
||||||
return Selector<MediaQueryData, Size>(
|
return Selector<MediaQueryData, Size>(
|
||||||
selector: (c, mq) => mq.size,
|
selector: (c, mq) => mq.size,
|
||||||
builder: (c, mqSize, child) {
|
builder: (c, mqSize, child) {
|
||||||
return EntryPageView(
|
return EntryPageView(
|
||||||
key: Key('imageview'),
|
key: Key('imageview'),
|
||||||
mainEntry: entry,
|
mainEntry: entry,
|
||||||
multiPageInfo: multiPageInfo,
|
|
||||||
page: page,
|
page: page,
|
||||||
viewportSize: mqSize,
|
viewportSize: mqSize,
|
||||||
heroTag: widget.collection.heroTag(entry),
|
heroTag: widget.collection.heroTag(entry),
|
||||||
|
@ -142,7 +141,7 @@ class _SingleEntryScrollerState extends State<SingleEntryScroller> with Automati
|
||||||
return ValueListenableBuilder<int>(
|
return ValueListenableBuilder<int>(
|
||||||
valueListenable: multiPageController.pageNotifier,
|
valueListenable: multiPageController.pageNotifier,
|
||||||
builder: (context, page, child) {
|
builder: (context, page, child) {
|
||||||
return _buildViewer(multiPageInfo: multiPageInfo, page: page);
|
return _buildViewer(page: multiPageInfo?.getByIndex(page));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -157,13 +156,12 @@ class _SingleEntryScrollerState extends State<SingleEntryScroller> with Automati
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildViewer({MultiPageInfo multiPageInfo, int page}) {
|
Widget _buildViewer({SinglePageInfo page}) {
|
||||||
return Selector<MediaQueryData, Size>(
|
return Selector<MediaQueryData, Size>(
|
||||||
selector: (c, mq) => mq.size,
|
selector: (c, mq) => mq.size,
|
||||||
builder: (c, mqSize, child) {
|
builder: (c, mqSize, child) {
|
||||||
return EntryPageView(
|
return EntryPageView(
|
||||||
mainEntry: entry,
|
mainEntry: entry,
|
||||||
multiPageInfo: multiPageInfo,
|
|
||||||
page: page,
|
page: page,
|
||||||
viewportSize: mqSize,
|
viewportSize: mqSize,
|
||||||
onTap: (_) => widget.onTap?.call(),
|
onTap: (_) => widget.onTap?.call(),
|
||||||
|
|
|
@ -7,10 +7,16 @@ import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class MultiPageController extends ChangeNotifier {
|
class MultiPageController extends ChangeNotifier {
|
||||||
final Future<MultiPageInfo> info;
|
Future<MultiPageInfo> info;
|
||||||
final ValueNotifier<int> pageNotifier = ValueNotifier(0);
|
final ValueNotifier<int> pageNotifier = ValueNotifier(null);
|
||||||
|
|
||||||
MultiPageController(AvesEntry entry) : info = MetadataService.getMultiPageInfo(entry);
|
MultiPageController(AvesEntry entry) {
|
||||||
|
info = MetadataService.getMultiPageInfo(entry).then((value) {
|
||||||
|
final defaultPage = value.pages.firstWhere((page) => page.isDefault, orElse: () => null);
|
||||||
|
pageNotifier.value = defaultPage?.index ?? 0;
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
int get page => pageNotifier.value;
|
int get page => pageNotifier.value;
|
||||||
|
|
||||||
|
|
|
@ -101,8 +101,7 @@ class _ViewerBottomOverlayState extends State<ViewerBottomOverlay> {
|
||||||
|
|
||||||
Widget _buildContent({MultiPageInfo multiPageInfo, int page}) => _BottomOverlayContent(
|
Widget _buildContent({MultiPageInfo multiPageInfo, int page}) => _BottomOverlayContent(
|
||||||
mainEntry: _lastEntry,
|
mainEntry: _lastEntry,
|
||||||
multiPageInfo: multiPageInfo,
|
page: multiPageInfo?.getByIndex(page),
|
||||||
page: page,
|
|
||||||
details: _lastDetails,
|
details: _lastDetails,
|
||||||
position: widget.showPosition ? '${widget.index + 1}/${widget.entries.length}' : null,
|
position: widget.showPosition ? '${widget.index + 1}/${widget.entries.length}' : null,
|
||||||
availableWidth: availableWidth,
|
availableWidth: availableWidth,
|
||||||
|
@ -139,8 +138,7 @@ const double _subRowMinWidth = 300.0;
|
||||||
|
|
||||||
class _BottomOverlayContent extends AnimatedWidget {
|
class _BottomOverlayContent extends AnimatedWidget {
|
||||||
final AvesEntry mainEntry, entry;
|
final AvesEntry mainEntry, entry;
|
||||||
final MultiPageInfo multiPageInfo;
|
final SinglePageInfo page;
|
||||||
final int page;
|
|
||||||
final OverlayMetadata details;
|
final OverlayMetadata details;
|
||||||
final String position;
|
final String position;
|
||||||
final double availableWidth;
|
final double availableWidth;
|
||||||
|
@ -151,13 +149,12 @@ class _BottomOverlayContent extends AnimatedWidget {
|
||||||
_BottomOverlayContent({
|
_BottomOverlayContent({
|
||||||
Key key,
|
Key key,
|
||||||
this.mainEntry,
|
this.mainEntry,
|
||||||
this.multiPageInfo,
|
|
||||||
this.page,
|
this.page,
|
||||||
this.details,
|
this.details,
|
||||||
this.position,
|
this.position,
|
||||||
this.availableWidth,
|
this.availableWidth,
|
||||||
this.multiPageController,
|
this.multiPageController,
|
||||||
}) : entry = mainEntry.getPageEntry(multiPageInfo: multiPageInfo, page: page),
|
}) : entry = mainEntry.getPageEntry(page),
|
||||||
super(key: key, listenable: mainEntry.metadataChangeNotifier);
|
super(key: key, listenable: mainEntry.metadataChangeNotifier);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -342,6 +339,8 @@ class _PositionTitleRow extends StatelessWidget {
|
||||||
|
|
||||||
bool get isNotEmpty => collectionPosition != null || multiPageController != null || title != null;
|
bool get isNotEmpty => collectionPosition != null || multiPageController != null || title != null;
|
||||||
|
|
||||||
|
static const separator = ' • ';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Text toText({String pagePosition}) => Text(
|
Text toText({String pagePosition}) => Text(
|
||||||
|
@ -349,7 +348,7 @@ class _PositionTitleRow extends StatelessWidget {
|
||||||
if (collectionPosition != null) collectionPosition,
|
if (collectionPosition != null) collectionPosition,
|
||||||
if (pagePosition != null) pagePosition,
|
if (pagePosition != null) pagePosition,
|
||||||
if (title != null) title,
|
if (title != null) title,
|
||||||
].join(' • '),
|
].join(separator),
|
||||||
strutStyle: Constants.overflowStrutStyle);
|
strutStyle: Constants.overflowStrutStyle);
|
||||||
|
|
||||||
if (multiPageController == null) return toText();
|
if (multiPageController == null) return toText();
|
||||||
|
@ -358,11 +357,17 @@ class _PositionTitleRow extends StatelessWidget {
|
||||||
future: multiPageController.info,
|
future: multiPageController.info,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final multiPageInfo = snapshot.data;
|
final multiPageInfo = snapshot.data;
|
||||||
final pageCount = multiPageInfo?.pageCount;
|
String pagePosition;
|
||||||
|
if (multiPageInfo != null) {
|
||||||
// page count may be 0 when we know an entry to have multiple pages
|
// page count may be 0 when we know an entry to have multiple pages
|
||||||
// but fail to get information about these pages
|
// but fail to get information about these pages
|
||||||
final missingInfo = pageCount == 0;
|
final pageCount = multiPageInfo.pageCount;
|
||||||
return toText(pagePosition: missingInfo ? null : '${(entry.page ?? 0) + 1}/${pageCount ?? '?'}');
|
if (pageCount > 0) {
|
||||||
|
final page = multiPageInfo.getById(entry.pageId);
|
||||||
|
pagePosition = '${(page?.index ?? 0) + 1}/$pageCount';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return toText(pagePosition: pagePosition);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ class Minimap extends StatelessWidget {
|
||||||
return ValueListenableBuilder<int>(
|
return ValueListenableBuilder<int>(
|
||||||
valueListenable: multiPageController.pageNotifier,
|
valueListenable: multiPageController.pageNotifier,
|
||||||
builder: (context, page, child) {
|
builder: (context, page, child) {
|
||||||
final pageEntry = mainEntry.getPageEntry(multiPageInfo: multiPageInfo, page: page);
|
final pageEntry = mainEntry.getPageEntry(multiPageInfo?.getByIndex(page));
|
||||||
return _buildForEntrySize(pageEntry.displaySize);
|
return _buildForEntrySize(pageEntry.displaySize);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -62,7 +62,8 @@ class _MultiPageOverlayState extends State<MultiPageOverlay> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _registerWidget() {
|
void _registerWidget() {
|
||||||
final scrollOffset = pageToScrollOffset(controller.page);
|
final page = controller.page ?? 0;
|
||||||
|
final scrollOffset = pageToScrollOffset(page);
|
||||||
_scrollController = ScrollController(initialScrollOffset: scrollOffset);
|
_scrollController = ScrollController(initialScrollOffset: scrollOffset);
|
||||||
_scrollController.addListener(_onScrollChange);
|
_scrollController.addListener(_onScrollChange);
|
||||||
}
|
}
|
||||||
|
@ -108,7 +109,7 @@ class _MultiPageOverlayState extends State<MultiPageOverlay> {
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index == 0 || index == multiPageInfo.pageCount + 1) return horizontalMargin;
|
if (index == 0 || index == multiPageInfo.pageCount + 1) return horizontalMargin;
|
||||||
final page = index - 1;
|
final page = index - 1;
|
||||||
final pageEntry = mainEntry.getPageEntry(multiPageInfo: multiPageInfo, page: page);
|
final pageEntry = mainEntry.getPageEntry(multiPageInfo.getByIndex(page));
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
|
|
|
@ -46,8 +46,8 @@ class EntryPrinter {
|
||||||
if (entry.isMultipage) {
|
if (entry.isMultipage) {
|
||||||
final multiPageInfo = await MetadataService.getMultiPageInfo(entry);
|
final multiPageInfo = await MetadataService.getMultiPageInfo(entry);
|
||||||
if (multiPageInfo.pageCount > 1) {
|
if (multiPageInfo.pageCount > 1) {
|
||||||
for (final kv in multiPageInfo.pages.entries) {
|
for (final page in multiPageInfo.pages) {
|
||||||
final pageEntry = entry.getPageEntry(multiPageInfo: multiPageInfo, page: kv.key);
|
final pageEntry = entry.getPageEntry(page);
|
||||||
_addPdfPage(await _buildPageImage(pageEntry));
|
_addPdfPage(await _buildPageImage(pageEntry));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,8 +24,7 @@ import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
class EntryPageView extends StatefulWidget {
|
class EntryPageView extends StatefulWidget {
|
||||||
final AvesEntry entry;
|
final AvesEntry entry;
|
||||||
final MultiPageInfo multiPageInfo;
|
final SinglePageInfo page;
|
||||||
final int page;
|
|
||||||
final Size viewportSize;
|
final Size viewportSize;
|
||||||
final Object heroTag;
|
final Object heroTag;
|
||||||
final MagnifierTapCallback onTap;
|
final MagnifierTapCallback onTap;
|
||||||
|
@ -37,14 +36,13 @@ class EntryPageView extends StatefulWidget {
|
||||||
EntryPageView({
|
EntryPageView({
|
||||||
Key key,
|
Key key,
|
||||||
AvesEntry mainEntry,
|
AvesEntry mainEntry,
|
||||||
this.multiPageInfo,
|
|
||||||
this.page,
|
this.page,
|
||||||
this.viewportSize,
|
this.viewportSize,
|
||||||
this.heroTag,
|
this.heroTag,
|
||||||
@required this.onTap,
|
@required this.onTap,
|
||||||
@required this.videoControllers,
|
@required this.videoControllers,
|
||||||
this.onDisposed,
|
this.onDisposed,
|
||||||
}) : entry = mainEntry.getPageEntry(multiPageInfo: multiPageInfo, page: page) ?? mainEntry,
|
}) : entry = mainEntry.getPageEntry(page) ?? mainEntry,
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -198,7 +196,7 @@ class _EntryPageViewState extends State<EntryPageView> {
|
||||||
}) {
|
}) {
|
||||||
return Magnifier(
|
return Magnifier(
|
||||||
// key includes size and orientation to refresh when the image is rotated
|
// key includes size and orientation to refresh when the image is rotated
|
||||||
key: ValueKey('${entry.page}_${entry.rotationDegrees}_${entry.isFlipped}_${entry.width}_${entry.height}_${entry.path}'),
|
key: ValueKey('${entry.pageId}_${entry.rotationDegrees}_${entry.isFlipped}_${entry.width}_${entry.height}_${entry.path}'),
|
||||||
controller: _magnifierController,
|
controller: _magnifierController,
|
||||||
childSize: entry.displaySize,
|
childSize: entry.displaySize,
|
||||||
minScale: minScale,
|
minScale: minScale,
|
||||||
|
|
Loading…
Reference in a new issue