#11 viewer: multipage TIFF support
This commit is contained in:
parent
9ca5f7b492
commit
a121d21ca2
51 changed files with 919 additions and 261 deletions
|
@ -251,7 +251,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
|
||||||
TiffBitmapFactory.decodeFileDescriptor(fd, options)
|
TiffBitmapFactory.decodeFileDescriptor(fd, options)
|
||||||
metadataMap["0"] = tiffOptionsToMap(options)
|
metadataMap["0"] = tiffOptionsToMap(options)
|
||||||
val dirCount = options.outDirectoryCount
|
val dirCount = options.outDirectoryCount
|
||||||
for (i in 1 until dirCount) {
|
for (page in 1 until dirCount) {
|
||||||
fd = context.contentResolver.openFileDescriptor(uri, "r")?.detachFd()
|
fd = context.contentResolver.openFileDescriptor(uri, "r")?.detachFd()
|
||||||
if (fd == null) {
|
if (fd == null) {
|
||||||
result.error("getTiffStructure-fd", "failed to get file descriptor", null)
|
result.error("getTiffStructure-fd", "failed to get file descriptor", null)
|
||||||
|
@ -259,10 +259,10 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
options = TiffBitmapFactory.Options().apply {
|
options = TiffBitmapFactory.Options().apply {
|
||||||
inJustDecodeBounds = true
|
inJustDecodeBounds = true
|
||||||
inDirectoryNumber = i
|
inDirectoryNumber = page
|
||||||
}
|
}
|
||||||
TiffBitmapFactory.decodeFileDescriptor(fd, options)
|
TiffBitmapFactory.decodeFileDescriptor(fd, options)
|
||||||
metadataMap["$i"] = tiffOptionsToMap(options)
|
metadataMap["$page"] = tiffOptionsToMap(options)
|
||||||
}
|
}
|
||||||
result.success(metadataMap)
|
result.success(metadataMap)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
|
@ -58,6 +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 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) {
|
||||||
|
@ -75,6 +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,
|
||||||
defaultSize = (defaultSizeDip * density).roundToInt(),
|
defaultSize = (defaultSizeDip * density).roundToInt(),
|
||||||
result,
|
result,
|
||||||
).fetch()
|
).fetch()
|
||||||
|
@ -83,6 +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 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")
|
||||||
|
@ -102,7 +105,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
|
||||||
uri,
|
uri,
|
||||||
sampleSize,
|
sampleSize,
|
||||||
regionRect,
|
regionRect,
|
||||||
page = 0,
|
page = page ?: 0,
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
else -> regionFetcher.fetch(
|
else -> regionFetcher.fetch(
|
||||||
|
|
|
@ -11,10 +11,7 @@ import com.adobe.internal.xmp.properties.XMPPropertyInfo
|
||||||
import com.bumptech.glide.load.resource.bitmap.TransformationUtils
|
import com.bumptech.glide.load.resource.bitmap.TransformationUtils
|
||||||
import com.drew.imaging.ImageMetadataReader
|
import com.drew.imaging.ImageMetadataReader
|
||||||
import com.drew.lang.Rational
|
import com.drew.lang.Rational
|
||||||
import com.drew.metadata.exif.ExifDirectoryBase
|
import com.drew.metadata.exif.*
|
||||||
import com.drew.metadata.exif.ExifIFD0Directory
|
|
||||||
import com.drew.metadata.exif.ExifSubIFDDirectory
|
|
||||||
import com.drew.metadata.exif.GpsDirectory
|
|
||||||
import com.drew.metadata.file.FileTypeDirectory
|
import com.drew.metadata.file.FileTypeDirectory
|
||||||
import com.drew.metadata.gif.GifAnimationDirectory
|
import com.drew.metadata.gif.GifAnimationDirectory
|
||||||
import com.drew.metadata.iptc.IptcDirectory
|
import com.drew.metadata.iptc.IptcDirectory
|
||||||
|
@ -72,6 +69,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
"getAllMetadata" -> GlobalScope.launch(Dispatchers.IO) { getAllMetadata(call, Coresult(result)) }
|
"getAllMetadata" -> GlobalScope.launch(Dispatchers.IO) { getAllMetadata(call, Coresult(result)) }
|
||||||
"getCatalogMetadata" -> GlobalScope.launch(Dispatchers.IO) { getCatalogMetadata(call, Coresult(result)) }
|
"getCatalogMetadata" -> GlobalScope.launch(Dispatchers.IO) { getCatalogMetadata(call, Coresult(result)) }
|
||||||
"getOverlayMetadata" -> GlobalScope.launch(Dispatchers.IO) { getOverlayMetadata(call, Coresult(result)) }
|
"getOverlayMetadata" -> GlobalScope.launch(Dispatchers.IO) { getOverlayMetadata(call, Coresult(result)) }
|
||||||
|
"getMultiPageInfo" -> GlobalScope.launch(Dispatchers.IO) { getMultiPageInfo(call, Coresult(result)) }
|
||||||
"getEmbeddedPictures" -> GlobalScope.launch(Dispatchers.IO) { getEmbeddedPictures(call, Coresult(result)) }
|
"getEmbeddedPictures" -> GlobalScope.launch(Dispatchers.IO) { getEmbeddedPictures(call, Coresult(result)) }
|
||||||
"getExifThumbnails" -> GlobalScope.launch(Dispatchers.IO) { getExifThumbnails(call, Coresult(result)) }
|
"getExifThumbnails" -> GlobalScope.launch(Dispatchers.IO) { getExifThumbnails(call, Coresult(result)) }
|
||||||
"extractXmpDataProp" -> GlobalScope.launch(Dispatchers.IO) { extractXmpDataProp(call, Coresult(result)) }
|
"extractXmpDataProp" -> GlobalScope.launch(Dispatchers.IO) { extractXmpDataProp(call, Coresult(result)) }
|
||||||
|
@ -109,7 +107,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
metadataMap[dirName] = dirMap
|
metadataMap[dirName] = dirMap
|
||||||
|
|
||||||
// tags
|
// tags
|
||||||
if (mimeType == MimeTypes.TIFF && dir is ExifIFD0Directory) {
|
if (mimeType == MimeTypes.TIFF && (dir is ExifIFD0Directory || dir is ExifThumbnailDirectory)) {
|
||||||
dirMap.putAll(dir.tags.map {
|
dirMap.putAll(dir.tags.map {
|
||||||
val name = if (it.hasTagName()) {
|
val name = if (it.hasTagName()) {
|
||||||
it.tagName
|
it.tagName
|
||||||
|
@ -397,7 +395,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mimeType == MimeTypes.TIFF && getTiffDirCount(uri) > 1) flags = flags or MASK_IS_MULTIPAGE
|
if (mimeType == MimeTypes.TIFF && isMultiPageTiff(uri)) flags = flags or MASK_IS_MULTIPAGE
|
||||||
|
|
||||||
metadataMap[KEY_FLAGS] = flags
|
metadataMap[KEY_FLAGS] = flags
|
||||||
}
|
}
|
||||||
|
@ -514,6 +512,33 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
result.success(metadataMap)
|
result.success(metadataMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getMultiPageInfo(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val mimeType = call.argument<String>("mimeType")
|
||||||
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
|
if (mimeType == null || uri == null) {
|
||||||
|
result.error("getMultiPageInfo-args", "failed because of missing arguments", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val pages = HashMap<Int, Any>()
|
||||||
|
if (mimeType == MimeTypes.TIFF) {
|
||||||
|
fun toMap(options: TiffBitmapFactory.Options): Map<String, Any> {
|
||||||
|
return hashMapOf(
|
||||||
|
"width" to options.outWidth,
|
||||||
|
"height" to options.outHeight,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
getTiffPageInfo(uri, 0)?.let { first ->
|
||||||
|
pages[0] = toMap(first)
|
||||||
|
val pageCount = first.outDirectoryCount
|
||||||
|
for (i in 1 until pageCount) {
|
||||||
|
getTiffPageInfo(uri, i)?.let { pages[i] = toMap(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.success(pages)
|
||||||
|
}
|
||||||
|
|
||||||
private fun getEmbeddedPictures(call: MethodCall, result: MethodChannel.Result) {
|
private fun getEmbeddedPictures(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) }
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
|
@ -642,23 +667,25 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
result.error("extractXmpDataProp-empty", "failed to extract file from XMP uri=$uri prop=$dataPropPath", null)
|
result.error("extractXmpDataProp-empty", "failed to extract file from XMP uri=$uri prop=$dataPropPath", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getTiffDirCount(uri: Uri): Int {
|
private fun isMultiPageTiff(uri: Uri) = getTiffPageInfo(uri, 0)?.outDirectoryCount ?: 1 > 1
|
||||||
var dirCount = 1
|
|
||||||
|
private fun getTiffPageInfo(uri: Uri, page: Int): TiffBitmapFactory.Options? {
|
||||||
try {
|
try {
|
||||||
val fd = context.contentResolver.openFileDescriptor(uri, "r")?.detachFd()
|
val fd = context.contentResolver.openFileDescriptor(uri, "r")?.detachFd()
|
||||||
if (fd == null) {
|
if (fd == null) {
|
||||||
Log.w(LOG_TAG, "failed to get file descriptor for uri=$uri")
|
Log.w(LOG_TAG, "failed to get file descriptor for uri=$uri")
|
||||||
} else {
|
return null
|
||||||
val options = TiffBitmapFactory.Options().apply {
|
|
||||||
inJustDecodeBounds = true
|
|
||||||
}
|
|
||||||
TiffBitmapFactory.decodeFileDescriptor(fd, options)
|
|
||||||
dirCount = options.outDirectoryCount
|
|
||||||
}
|
}
|
||||||
|
val options = TiffBitmapFactory.Options().apply {
|
||||||
|
inJustDecodeBounds = true
|
||||||
|
inDirectoryNumber = page
|
||||||
|
}
|
||||||
|
TiffBitmapFactory.decodeFileDescriptor(fd, options)
|
||||||
|
return options
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(LOG_TAG, "failed to get TIFF dir count for uri=$uri", e)
|
Log.w(LOG_TAG, "failed to get TIFF page info for uri=$uri page=$page", e)
|
||||||
}
|
}
|
||||||
return dirCount
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -32,12 +32,14 @@ class ThumbnailFetcher internal constructor(
|
||||||
private val isFlipped: Boolean,
|
private val isFlipped: Boolean,
|
||||||
width: Int?,
|
width: Int?,
|
||||||
height: Int?,
|
height: Int?,
|
||||||
|
page: Int?,
|
||||||
private val defaultSize: Int,
|
private val defaultSize: Int,
|
||||||
private val result: MethodChannel.Result,
|
private val result: MethodChannel.Result,
|
||||||
) {
|
) {
|
||||||
val uri: Uri = Uri.parse(uri)
|
private val uri: Uri = Uri.parse(uri)
|
||||||
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
|
||||||
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 page = page ?: 0
|
||||||
|
|
||||||
fun fetch() {
|
fun fetch() {
|
||||||
var bitmap: Bitmap? = null
|
var bitmap: Bitmap? = null
|
||||||
|
@ -108,7 +110,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"))
|
.signature(ObjectKey("$dateModifiedSecs-$rotationDegrees-$isFlipped-$width-$page"))
|
||||||
.override(width, height)
|
.override(width, height)
|
||||||
|
|
||||||
val target = if (isVideo(mimeType)) {
|
val target = if (isVideo(mimeType)) {
|
||||||
|
@ -119,7 +121,7 @@ class ThumbnailFetcher internal constructor(
|
||||||
.load(VideoThumbnail(context, uri))
|
.load(VideoThumbnail(context, uri))
|
||||||
.submit(width, height)
|
.submit(width, height)
|
||||||
} else {
|
} else {
|
||||||
val model: Any = if (mimeType == MimeTypes.TIFF) TiffThumbnail(context, uri) else uri
|
val model: Any = if (mimeType == MimeTypes.TIFF) TiffThumbnail(context, uri, page) else uri
|
||||||
Glide.with(context)
|
Glide.with(context)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.apply(options)
|
.apply(options)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.app.Activity
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.DecodeFormat
|
import com.bumptech.glide.load.DecodeFormat
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
@ -11,6 +12,7 @@ import com.bumptech.glide.request.RequestOptions
|
||||||
import deckers.thibault.aves.decoder.VideoThumbnail
|
import deckers.thibault.aves.decoder.VideoThumbnail
|
||||||
import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation
|
import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation
|
||||||
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
||||||
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
import deckers.thibault.aves.utils.MimeTypes
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
import deckers.thibault.aves.utils.MimeTypes.isSupportedByFlutter
|
import deckers.thibault.aves.utils.MimeTypes.isSupportedByFlutter
|
||||||
import deckers.thibault.aves.utils.MimeTypes.isVideo
|
import deckers.thibault.aves.utils.MimeTypes.isVideo
|
||||||
|
@ -39,15 +41,33 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
|
||||||
override fun onCancel(o: Any) {}
|
override fun onCancel(o: Any) {}
|
||||||
|
|
||||||
private fun success(bytes: ByteArray) {
|
private fun success(bytes: ByteArray) {
|
||||||
handler.post { eventSink.success(bytes) }
|
handler.post {
|
||||||
|
try {
|
||||||
|
eventSink.success(bytes)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "failed to use event sink", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun error(errorCode: String, errorMessage: String, errorDetails: Any?) {
|
private fun error(errorCode: String, errorMessage: String, errorDetails: Any?) {
|
||||||
handler.post { eventSink.error(errorCode, errorMessage, errorDetails) }
|
handler.post {
|
||||||
|
try {
|
||||||
|
eventSink.error(errorCode, errorMessage, errorDetails)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "failed to use event sink", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun endOfStream() {
|
private fun endOfStream() {
|
||||||
handler.post { eventSink.endOfStream() }
|
handler.post {
|
||||||
|
try {
|
||||||
|
eventSink.endOfStream()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "failed to use event sink", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Supported image formats:
|
// Supported image formats:
|
||||||
|
@ -64,6 +84,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
|
||||||
|
|
||||||
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)
|
||||||
|
@ -74,7 +95,7 @@ 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)
|
streamTiffImage(uri, page)
|
||||||
} 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, mimeType, rotationDegrees, isFlipped)
|
streamImageByGlide(uri, mimeType, rotationDegrees, isFlipped)
|
||||||
|
@ -139,34 +160,19 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
|
||||||
private fun streamTiffImage(uri: Uri, page: Int = 0) {
|
private fun streamTiffImage(uri: Uri, page: Int = 0) {
|
||||||
val resolver = activity.contentResolver
|
val resolver = activity.contentResolver
|
||||||
try {
|
try {
|
||||||
var fd = resolver.openFileDescriptor(uri, "r")?.detachFd()
|
val fd = resolver.openFileDescriptor(uri, "r")?.detachFd()
|
||||||
if (fd == null) {
|
if (fd == null) {
|
||||||
error("streamImage-tiff-fd", "failed to get file descriptor for uri=$uri", null)
|
error("streamImage-tiff-fd", "failed to get file descriptor for uri=$uri", null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var options = TiffBitmapFactory.Options().apply {
|
val options = TiffBitmapFactory.Options().apply {
|
||||||
inJustDecodeBounds = true
|
inDirectoryNumber = page
|
||||||
}
|
}
|
||||||
TiffBitmapFactory.decodeFileDescriptor(fd, options)
|
val bitmap = TiffBitmapFactory.decodeFileDescriptor(fd, options)
|
||||||
val dirCount = options.outDirectoryCount
|
if (bitmap != null) {
|
||||||
|
success(bitmap.getBytes(canHaveAlpha = true, recycle = true))
|
||||||
// TODO TLAD handle multipage TIFF
|
} else {
|
||||||
if (dirCount > page) {
|
error("streamImage-tiff-null", "failed to get tiff image (dir=$page) from uri=$uri", null)
|
||||||
fd = resolver.openFileDescriptor(uri, "r")?.detachFd()
|
|
||||||
if (fd == null) {
|
|
||||||
error("streamImage-tiff-fd", "failed to get file descriptor for uri=$uri", null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
options = TiffBitmapFactory.Options().apply {
|
|
||||||
inJustDecodeBounds = false
|
|
||||||
inDirectoryNumber = page
|
|
||||||
}
|
|
||||||
val bitmap = TiffBitmapFactory.decodeFileDescriptor(fd, options)
|
|
||||||
if (bitmap != null) {
|
|
||||||
success(bitmap.getBytes(canHaveAlpha = true, recycle = true))
|
|
||||||
} else {
|
|
||||||
error("streamImage-tiff-null", "failed to get tiff image (dir=$page) from uri=$uri", null)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
error("streamImage-tiff-exception", "failed to get image from uri=$uri", toErrorDetails(e))
|
error("streamImage-tiff-exception", "failed to get image from uri=$uri", toErrorDetails(e))
|
||||||
|
@ -192,6 +198,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private val LOG_TAG = LogUtils.createTag(ImageByteStreamHandler::class.java)
|
||||||
const val CHANNEL = "deckers.thibault/aves/imagebytestream"
|
const val CHANNEL = "deckers.thibault/aves/imagebytestream"
|
||||||
|
|
||||||
const val bufferSize = 2 shl 17 // 256kB
|
const val bufferSize = 2 shl 17 // 256kB
|
||||||
|
|
|
@ -51,15 +51,33 @@ class ImageOpStreamHandler(private val context: Context, private val arguments:
|
||||||
|
|
||||||
// {String uri, bool success, [Map<String, Object> newFields]}
|
// {String uri, bool success, [Map<String, Object> newFields]}
|
||||||
private fun success(result: Map<String, *>) {
|
private fun success(result: Map<String, *>) {
|
||||||
handler.post { eventSink.success(result) }
|
handler.post {
|
||||||
|
try {
|
||||||
|
eventSink.success(result)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "failed to use event sink", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun error(errorCode: String, errorMessage: String, errorDetails: Any?) {
|
private fun error(errorCode: String, errorMessage: String, errorDetails: Any?) {
|
||||||
handler.post { eventSink.error(errorCode, errorMessage, errorDetails) }
|
handler.post {
|
||||||
|
try {
|
||||||
|
eventSink.error(errorCode, errorMessage, errorDetails)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "failed to use event sink", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun endOfStream() {
|
private fun endOfStream() {
|
||||||
handler.post { eventSink.endOfStream() }
|
handler.post {
|
||||||
|
try {
|
||||||
|
eventSink.endOfStream()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "failed to use event sink", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun move() {
|
private suspend fun move() {
|
||||||
|
|
|
@ -3,8 +3,10 @@ package deckers.thibault.aves.channel.streams
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
import deckers.thibault.aves.model.provider.FieldMap
|
import deckers.thibault.aves.model.provider.FieldMap
|
||||||
import deckers.thibault.aves.model.provider.MediaStoreImageProvider
|
import deckers.thibault.aves.model.provider.MediaStoreImageProvider
|
||||||
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
import io.flutter.plugin.common.EventChannel
|
import io.flutter.plugin.common.EventChannel
|
||||||
import io.flutter.plugin.common.EventChannel.EventSink
|
import io.flutter.plugin.common.EventChannel.EventSink
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -34,11 +36,23 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E
|
||||||
override fun onCancel(arguments: Any?) {}
|
override fun onCancel(arguments: Any?) {}
|
||||||
|
|
||||||
private fun success(result: FieldMap) {
|
private fun success(result: FieldMap) {
|
||||||
handler.post { eventSink.success(result) }
|
handler.post {
|
||||||
|
try {
|
||||||
|
eventSink.success(result)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "failed to use event sink", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun endOfStream() {
|
private fun endOfStream() {
|
||||||
handler.post { eventSink.endOfStream() }
|
handler.post {
|
||||||
|
try {
|
||||||
|
eventSink.endOfStream()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "failed to use event sink", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun fetchAll() {
|
private suspend fun fetchAll() {
|
||||||
|
@ -47,6 +61,7 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private val LOG_TAG = LogUtils.createTag(MediaStoreStreamHandler::class.java)
|
||||||
const val CHANNEL = "deckers.thibault/aves/mediastorestream"
|
const val CHANNEL = "deckers.thibault/aves/mediastorestream"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,6 +3,8 @@ package deckers.thibault.aves.channel.streams
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
import deckers.thibault.aves.utils.PermissionManager.requestVolumeAccess
|
import deckers.thibault.aves.utils.PermissionManager.requestVolumeAccess
|
||||||
import io.flutter.plugin.common.EventChannel
|
import io.flutter.plugin.common.EventChannel
|
||||||
import io.flutter.plugin.common.EventChannel.EventSink
|
import io.flutter.plugin.common.EventChannel.EventSink
|
||||||
|
@ -30,15 +32,28 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
|
||||||
override fun onCancel(arguments: Any?) {}
|
override fun onCancel(arguments: Any?) {}
|
||||||
|
|
||||||
private fun success(result: Boolean) {
|
private fun success(result: Boolean) {
|
||||||
handler.post { eventSink.success(result) }
|
handler.post {
|
||||||
|
try {
|
||||||
|
eventSink.success(result)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "failed to use event sink", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
endOfStream()
|
endOfStream()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun endOfStream() {
|
private fun endOfStream() {
|
||||||
handler.post { eventSink.endOfStream() }
|
handler.post {
|
||||||
|
try {
|
||||||
|
eventSink.endOfStream()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "failed to use event sink", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private val LOG_TAG = LogUtils.createTag(StorageAccessStreamHandler::class.java)
|
||||||
const val CHANNEL = "deckers.thibault/aves/storageaccessstream"
|
const val CHANNEL = "deckers.thibault/aves/storageaccessstream"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -26,7 +26,7 @@ class TiffThumbnailGlideModule : LibraryGlideModule() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TiffThumbnail(val context: Context, val uri: Uri)
|
class TiffThumbnail(val context: Context, val uri: Uri, val page: Int)
|
||||||
|
|
||||||
internal class TiffThumbnailLoader : ModelLoader<TiffThumbnail, InputStream> {
|
internal class TiffThumbnailLoader : ModelLoader<TiffThumbnail, InputStream> {
|
||||||
override fun buildLoadData(model: TiffThumbnail, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream> {
|
override fun buildLoadData(model: TiffThumbnail, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream> {
|
||||||
|
@ -46,6 +46,7 @@ internal class TiffThumbnailFetcher(val model: TiffThumbnail, val width: Int, va
|
||||||
override fun loadData(priority: Priority, callback: DataCallback<in InputStream>) {
|
override fun loadData(priority: Priority, callback: DataCallback<in InputStream>) {
|
||||||
val context = model.context
|
val context = model.context
|
||||||
val uri = model.uri
|
val uri = model.uri
|
||||||
|
val page = model.page
|
||||||
|
|
||||||
// determine sample size
|
// determine sample size
|
||||||
var fd = context.contentResolver.openFileDescriptor(uri, "r")?.detachFd()
|
var fd = context.contentResolver.openFileDescriptor(uri, "r")?.detachFd()
|
||||||
|
@ -56,6 +57,7 @@ internal class TiffThumbnailFetcher(val model: TiffThumbnail, val width: Int, va
|
||||||
var sampleSize = 1
|
var sampleSize = 1
|
||||||
var options = TiffBitmapFactory.Options().apply {
|
var options = TiffBitmapFactory.Options().apply {
|
||||||
inJustDecodeBounds = true
|
inJustDecodeBounds = true
|
||||||
|
inDirectoryNumber = page
|
||||||
}
|
}
|
||||||
TiffBitmapFactory.decodeFileDescriptor(fd, options)
|
TiffBitmapFactory.decodeFileDescriptor(fd, options)
|
||||||
val imageWidth = options.outWidth
|
val imageWidth = options.outWidth
|
||||||
|
@ -74,6 +76,7 @@ internal class TiffThumbnailFetcher(val model: TiffThumbnail, val width: Int, va
|
||||||
}
|
}
|
||||||
options = TiffBitmapFactory.Options().apply {
|
options = TiffBitmapFactory.Options().apply {
|
||||||
inJustDecodeBounds = false
|
inJustDecodeBounds = false
|
||||||
|
inDirectoryNumber = page
|
||||||
inSampleSize = sampleSize
|
inSampleSize = sampleSize
|
||||||
}
|
}
|
||||||
val bitmap = TiffBitmapFactory.decodeFileDescriptor(fd, options)
|
val bitmap = TiffBitmapFactory.decodeFileDescriptor(fd, options)
|
||||||
|
|
|
@ -40,6 +40,7 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
|
||||||
key.sampleSize,
|
key.sampleSize,
|
||||||
key.regionRect,
|
key.regionRect,
|
||||||
key.imageSize,
|
key.imageSize,
|
||||||
|
page: key.page,
|
||||||
taskKey: key,
|
taskKey: key,
|
||||||
);
|
);
|
||||||
if (bytes == null) {
|
if (bytes == null) {
|
||||||
|
@ -63,7 +64,7 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
|
||||||
|
|
||||||
class RegionProviderKey {
|
class RegionProviderKey {
|
||||||
final String uri, mimeType;
|
final String uri, mimeType;
|
||||||
final int rotationDegrees, sampleSize;
|
final int rotationDegrees, sampleSize, page;
|
||||||
final bool isFlipped;
|
final bool isFlipped;
|
||||||
final Rectangle<int> regionRect;
|
final Rectangle<int> regionRect;
|
||||||
final Size imageSize;
|
final Size imageSize;
|
||||||
|
@ -74,6 +75,7 @@ class RegionProviderKey {
|
||||||
@required this.mimeType,
|
@required this.mimeType,
|
||||||
@required this.rotationDegrees,
|
@required this.rotationDegrees,
|
||||||
@required this.isFlipped,
|
@required this.isFlipped,
|
||||||
|
this.page = 0,
|
||||||
@required this.sampleSize,
|
@required this.sampleSize,
|
||||||
@required this.regionRect,
|
@required this.regionRect,
|
||||||
@required this.imageSize,
|
@required this.imageSize,
|
||||||
|
@ -91,6 +93,7 @@ class RegionProviderKey {
|
||||||
// but the entry attributes may change over time
|
// but the entry attributes may change over time
|
||||||
factory RegionProviderKey.fromEntry(
|
factory RegionProviderKey.fromEntry(
|
||||||
ImageEntry entry, {
|
ImageEntry entry, {
|
||||||
|
int page = 0,
|
||||||
@required int sampleSize,
|
@required int sampleSize,
|
||||||
@required Rectangle<int> rect,
|
@required Rectangle<int> rect,
|
||||||
}) {
|
}) {
|
||||||
|
@ -99,6 +102,7 @@ class RegionProviderKey {
|
||||||
mimeType: entry.mimeType,
|
mimeType: entry.mimeType,
|
||||||
rotationDegrees: entry.rotationDegrees,
|
rotationDegrees: entry.rotationDegrees,
|
||||||
isFlipped: entry.isFlipped,
|
isFlipped: entry.isFlipped,
|
||||||
|
page: page,
|
||||||
sampleSize: sampleSize,
|
sampleSize: sampleSize,
|
||||||
regionRect: rect,
|
regionRect: rect,
|
||||||
imageSize: Size(entry.width.toDouble(), entry.height.toDouble()),
|
imageSize: Size(entry.width.toDouble(), entry.height.toDouble()),
|
||||||
|
@ -108,7 +112,7 @@ 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.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.sampleSize == sampleSize && other.regionRect == regionRect && other.imageSize == imageSize && other.scale == scale;
|
return other is RegionProviderKey && other.uri == uri && other.mimeType == mimeType && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.page == page && other.sampleSize == sampleSize && other.regionRect == regionRect && other.imageSize == imageSize && other.scale == scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -117,7 +121,7 @@ class RegionProviderKey {
|
||||||
mimeType,
|
mimeType,
|
||||||
rotationDegrees,
|
rotationDegrees,
|
||||||
isFlipped,
|
isFlipped,
|
||||||
mimeType,
|
page,
|
||||||
sampleSize,
|
sampleSize,
|
||||||
regionRect,
|
regionRect,
|
||||||
imageSize,
|
imageSize,
|
||||||
|
@ -125,5 +129,5 @@ class RegionProviderKey {
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, sampleSize=$sampleSize, regionRect=$regionRect, imageSize=$imageSize, scale=$scale}';
|
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, page=$page, sampleSize=$sampleSize, regionRect=$regionRect, imageSize=$imageSize, scale=$scale}';
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,7 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
||||||
key.isFlipped,
|
key.isFlipped,
|
||||||
key.extent,
|
key.extent,
|
||||||
key.extent,
|
key.extent,
|
||||||
|
page: key.page,
|
||||||
taskKey: key,
|
taskKey: key,
|
||||||
);
|
);
|
||||||
if (bytes == null) {
|
if (bytes == null) {
|
||||||
|
@ -64,7 +65,7 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
||||||
|
|
||||||
class ThumbnailProviderKey {
|
class ThumbnailProviderKey {
|
||||||
final String uri, mimeType;
|
final String uri, mimeType;
|
||||||
final int dateModifiedSecs, rotationDegrees;
|
final int dateModifiedSecs, rotationDegrees, page;
|
||||||
final bool isFlipped;
|
final bool isFlipped;
|
||||||
final double extent, scale;
|
final double extent, scale;
|
||||||
|
|
||||||
|
@ -74,6 +75,7 @@ class ThumbnailProviderKey {
|
||||||
@required this.dateModifiedSecs,
|
@required this.dateModifiedSecs,
|
||||||
@required this.rotationDegrees,
|
@required this.rotationDegrees,
|
||||||
@required this.isFlipped,
|
@required this.isFlipped,
|
||||||
|
this.page = 0,
|
||||||
this.extent = 0,
|
this.extent = 0,
|
||||||
this.scale = 1,
|
this.scale = 1,
|
||||||
}) : assert(uri != null),
|
}) : assert(uri != null),
|
||||||
|
@ -86,7 +88,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
|
||||||
factory ThumbnailProviderKey.fromEntry(ImageEntry entry, {double extent = 0}) {
|
factory ThumbnailProviderKey.fromEntry(ImageEntry entry, {int page = 0, double extent = 0}) {
|
||||||
return ThumbnailProviderKey(
|
return ThumbnailProviderKey(
|
||||||
uri: entry.uri,
|
uri: entry.uri,
|
||||||
mimeType: entry.mimeType,
|
mimeType: entry.mimeType,
|
||||||
|
@ -94,6 +96,7 @@ class ThumbnailProviderKey {
|
||||||
dateModifiedSecs: entry.dateModifiedSecs ?? -1,
|
dateModifiedSecs: entry.dateModifiedSecs ?? -1,
|
||||||
rotationDegrees: entry.rotationDegrees,
|
rotationDegrees: entry.rotationDegrees,
|
||||||
isFlipped: entry.isFlipped,
|
isFlipped: entry.isFlipped,
|
||||||
|
page: page,
|
||||||
extent: extent,
|
extent: extent,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -101,7 +104,7 @@ 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.extent == extent && other.mimeType == mimeType && other.dateModifiedSecs == dateModifiedSecs && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.scale == scale;
|
return other is ThumbnailProviderKey && other.uri == uri && other.extent == extent && other.mimeType == mimeType && other.dateModifiedSecs == dateModifiedSecs && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.page == page && other.scale == scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -111,10 +114,11 @@ class ThumbnailProviderKey {
|
||||||
dateModifiedSecs,
|
dateModifiedSecs,
|
||||||
rotationDegrees,
|
rotationDegrees,
|
||||||
isFlipped,
|
isFlipped,
|
||||||
|
page,
|
||||||
extent,
|
extent,
|
||||||
scale,
|
scale,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, dateModifiedSecs=$dateModifiedSecs, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, extent=$extent, scale=$scale}';
|
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, dateModifiedSecs=$dateModifiedSecs, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, page=$page, extent=$extent, scale=$scale}';
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,15 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:pedantic/pedantic.dart';
|
import 'package:pedantic/pedantic.dart';
|
||||||
|
|
||||||
class UriImage extends ImageProvider<UriImage> {
|
class UriImage extends ImageProvider<UriImage> {
|
||||||
|
final String uri, mimeType;
|
||||||
|
final int page, rotationDegrees, expectedContentLength;
|
||||||
|
final bool isFlipped;
|
||||||
|
final double scale;
|
||||||
|
|
||||||
const UriImage({
|
const UriImage({
|
||||||
@required this.uri,
|
@required this.uri,
|
||||||
@required this.mimeType,
|
@required this.mimeType,
|
||||||
|
this.page = 0,
|
||||||
@required this.rotationDegrees,
|
@required this.rotationDegrees,
|
||||||
@required this.isFlipped,
|
@required this.isFlipped,
|
||||||
this.expectedContentLength,
|
this.expectedContentLength,
|
||||||
|
@ -17,11 +23,6 @@ class UriImage extends ImageProvider<UriImage> {
|
||||||
}) : assert(uri != null),
|
}) : assert(uri != null),
|
||||||
assert(scale != null);
|
assert(scale != null);
|
||||||
|
|
||||||
final String uri, mimeType;
|
|
||||||
final int rotationDegrees, expectedContentLength;
|
|
||||||
final bool isFlipped;
|
|
||||||
final double scale;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<UriImage> obtainKey(ImageConfiguration configuration) {
|
Future<UriImage> obtainKey(ImageConfiguration configuration) {
|
||||||
return SynchronousFuture<UriImage>(this);
|
return SynchronousFuture<UriImage>(this);
|
||||||
|
@ -50,6 +51,7 @@ class UriImage extends ImageProvider<UriImage> {
|
||||||
mimeType,
|
mimeType,
|
||||||
rotationDegrees,
|
rotationDegrees,
|
||||||
isFlipped,
|
isFlipped,
|
||||||
|
page: page,
|
||||||
expectedContentLength: expectedContentLength,
|
expectedContentLength: expectedContentLength,
|
||||||
onBytesReceived: (cumulative, total) {
|
onBytesReceived: (cumulative, total) {
|
||||||
chunkEvents.add(ImageChunkEvent(
|
chunkEvents.add(ImageChunkEvent(
|
||||||
|
@ -73,12 +75,19 @@ 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.scale == scale;
|
return other is UriImage && other.uri == uri && other.mimeType == mimeType && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.page == page && other.scale == scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => hashValues(uri, scale);
|
int get hashCode => hashValues(
|
||||||
|
uri,
|
||||||
|
mimeType,
|
||||||
|
rotationDegrees,
|
||||||
|
isFlipped,
|
||||||
|
page,
|
||||||
|
scale,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, scale=$scale}';
|
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, page=$page, scale=$scale}';
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,10 +12,14 @@ class EntryCache {
|
||||||
int oldRotationDegrees,
|
int oldRotationDegrees,
|
||||||
bool oldIsFlipped,
|
bool oldIsFlipped,
|
||||||
) async {
|
) async {
|
||||||
|
// TODO TLAD revisit this for multipage items, if someday image editing features are added for them
|
||||||
|
const page = 0;
|
||||||
|
|
||||||
// evict fullscreen image
|
// evict fullscreen image
|
||||||
await UriImage(
|
await UriImage(
|
||||||
uri: uri,
|
uri: uri,
|
||||||
mimeType: mimeType,
|
mimeType: mimeType,
|
||||||
|
page: page,
|
||||||
rotationDegrees: oldRotationDegrees,
|
rotationDegrees: oldRotationDegrees,
|
||||||
isFlipped: oldIsFlipped,
|
isFlipped: oldIsFlipped,
|
||||||
).evict();
|
).evict();
|
||||||
|
@ -27,6 +31,7 @@ class EntryCache {
|
||||||
dateModifiedSecs: dateModifiedSecs,
|
dateModifiedSecs: dateModifiedSecs,
|
||||||
rotationDegrees: oldRotationDegrees,
|
rotationDegrees: oldRotationDegrees,
|
||||||
isFlipped: oldIsFlipped,
|
isFlipped: oldIsFlipped,
|
||||||
|
page: page,
|
||||||
)).evict();
|
)).evict();
|
||||||
|
|
||||||
// evict higher quality thumbnails (with powers of 2 from 32 to 1024 as specified extents)
|
// evict higher quality thumbnails (with powers of 2 from 32 to 1024 as specified extents)
|
||||||
|
@ -39,6 +44,7 @@ class EntryCache {
|
||||||
dateModifiedSecs: dateModifiedSecs,
|
dateModifiedSecs: dateModifiedSecs,
|
||||||
rotationDegrees: oldRotationDegrees,
|
rotationDegrees: oldRotationDegrees,
|
||||||
isFlipped: oldIsFlipped,
|
isFlipped: oldIsFlipped,
|
||||||
|
page: page,
|
||||||
extent: extent,
|
extent: extent,
|
||||||
)).evict());
|
)).evict());
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:aves/model/entry_cache.dart';
|
||||||
import 'package:aves/model/favourite_repo.dart';
|
import 'package:aves/model/favourite_repo.dart';
|
||||||
import 'package:aves/model/image_metadata.dart';
|
import 'package:aves/model/image_metadata.dart';
|
||||||
import 'package:aves/model/metadata_db.dart';
|
import 'package:aves/model/metadata_db.dart';
|
||||||
|
import 'package:aves/model/multipage.dart';
|
||||||
import 'package:aves/services/image_file_service.dart';
|
import 'package:aves/services/image_file_service.dart';
|
||||||
import 'package:aves/services/metadata_service.dart';
|
import 'package:aves/services/metadata_service.dart';
|
||||||
import 'package:aves/services/service_policy.dart';
|
import 'package:aves/services/service_policy.dart';
|
||||||
|
@ -242,10 +243,19 @@ class ImageEntry {
|
||||||
static const ratioSeparator = '\u2236';
|
static const ratioSeparator = '\u2236';
|
||||||
static const resolutionSeparator = ' \u00D7 ';
|
static const resolutionSeparator = ' \u00D7 ';
|
||||||
|
|
||||||
String get resolutionText {
|
String getResolutionText({MultiPageInfo multiPageInfo, int page}) {
|
||||||
final w = width ?? '?';
|
int w;
|
||||||
final h = height ?? '?';
|
int h;
|
||||||
return isPortrait ? '$h$resolutionSeparator$w' : '$w$resolutionSeparator$h';
|
if (multiPageInfo != null && page != null) {
|
||||||
|
final pageInfo = multiPageInfo.pages[page];
|
||||||
|
w = pageInfo?.width;
|
||||||
|
h = pageInfo?.height;
|
||||||
|
}
|
||||||
|
w ??= width;
|
||||||
|
h ??= height;
|
||||||
|
final ws = w ?? '?';
|
||||||
|
final hs = h ?? '?';
|
||||||
|
return isPortrait ? '$hs$resolutionSeparator$ws' : '$ws$resolutionSeparator$hs';
|
||||||
}
|
}
|
||||||
|
|
||||||
String get aspectRatioText {
|
String get aspectRatioText {
|
||||||
|
@ -264,7 +274,18 @@ class ImageEntry {
|
||||||
return isPortrait ? height / width : width / height;
|
return isPortrait ? height / width : width / height;
|
||||||
}
|
}
|
||||||
|
|
||||||
Size get displaySize => isPortrait ? Size(height.toDouble(), width.toDouble()) : Size(width.toDouble(), height.toDouble());
|
Size getDisplaySize({MultiPageInfo multiPageInfo, int page}) {
|
||||||
|
int w;
|
||||||
|
int h;
|
||||||
|
if (multiPageInfo != null && page != null) {
|
||||||
|
final pageInfo = multiPageInfo.pages[page];
|
||||||
|
w = pageInfo?.width;
|
||||||
|
h = pageInfo?.height;
|
||||||
|
}
|
||||||
|
w ??= width;
|
||||||
|
h ??= height;
|
||||||
|
return isPortrait ? Size(h.toDouble(), w.toDouble()) : Size(w.toDouble(), h.toDouble());
|
||||||
|
}
|
||||||
|
|
||||||
int get megaPixels => width != null && height != null ? (width * height / 1000000).round() : null;
|
int get megaPixels => width != null && height != null ? (width * height / 1000000).round() : null;
|
||||||
|
|
||||||
|
|
42
lib/model/multipage.dart
Normal file
42
lib/model/multipage.dart
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
class SinglePageInfo {
|
||||||
|
final int width, height;
|
||||||
|
|
||||||
|
SinglePageInfo({
|
||||||
|
this.width,
|
||||||
|
this.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory SinglePageInfo.fromMap(Map map) {
|
||||||
|
return SinglePageInfo(
|
||||||
|
width: map['width'] as int,
|
||||||
|
height: map['height'] as int,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => '$runtimeType#${shortHash(this)}{width=$width, height=$height}';
|
||||||
|
}
|
||||||
|
|
||||||
|
class MultiPageInfo {
|
||||||
|
final Map<int, SinglePageInfo> pages;
|
||||||
|
|
||||||
|
int get pageCount => pages.length;
|
||||||
|
|
||||||
|
MultiPageInfo({
|
||||||
|
this.pages,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory MultiPageInfo.fromMap(Map map) {
|
||||||
|
final pages = <int, SinglePageInfo>{};
|
||||||
|
map.keys.forEach((key) {
|
||||||
|
final index = key as int;
|
||||||
|
pages.putIfAbsent(index, () => SinglePageInfo.fromMap(map[key]));
|
||||||
|
});
|
||||||
|
return MultiPageInfo(pages: pages);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => '$runtimeType#${shortHash(this)}{pages=$pages}';
|
||||||
|
}
|
|
@ -74,6 +74,7 @@ class ImageFileService {
|
||||||
String mimeType,
|
String mimeType,
|
||||||
int rotationDegrees,
|
int rotationDegrees,
|
||||||
bool isFlipped, {
|
bool isFlipped, {
|
||||||
|
int page = 0,
|
||||||
int expectedContentLength,
|
int expectedContentLength,
|
||||||
BytesReceivedCallback onBytesReceived,
|
BytesReceivedCallback onBytesReceived,
|
||||||
}) {
|
}) {
|
||||||
|
@ -86,6 +87,7 @@ class ImageFileService {
|
||||||
'mimeType': mimeType,
|
'mimeType': mimeType,
|
||||||
'rotationDegrees': rotationDegrees ?? 0,
|
'rotationDegrees': rotationDegrees ?? 0,
|
||||||
'isFlipped': isFlipped ?? false,
|
'isFlipped': isFlipped ?? false,
|
||||||
|
'page': page ?? 0,
|
||||||
}).listen(
|
}).listen(
|
||||||
(data) {
|
(data) {
|
||||||
final chunk = data as Uint8List;
|
final chunk = data as Uint8List;
|
||||||
|
@ -123,6 +125,7 @@ class ImageFileService {
|
||||||
int sampleSize,
|
int sampleSize,
|
||||||
Rectangle<int> regionRect,
|
Rectangle<int> regionRect,
|
||||||
Size imageSize, {
|
Size imageSize, {
|
||||||
|
int page = 0,
|
||||||
Object taskKey,
|
Object taskKey,
|
||||||
int priority,
|
int priority,
|
||||||
}) {
|
}) {
|
||||||
|
@ -132,6 +135,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,
|
||||||
'sampleSize': sampleSize,
|
'sampleSize': sampleSize,
|
||||||
'regionX': regionRect.left,
|
'regionX': regionRect.left,
|
||||||
'regionY': regionRect.top,
|
'regionY': regionRect.top,
|
||||||
|
@ -159,6 +163,7 @@ class ImageFileService {
|
||||||
bool isFlipped,
|
bool isFlipped,
|
||||||
double width,
|
double width,
|
||||||
double height, {
|
double height, {
|
||||||
|
int page,
|
||||||
Object taskKey,
|
Object taskKey,
|
||||||
int priority,
|
int priority,
|
||||||
}) {
|
}) {
|
||||||
|
@ -176,6 +181,7 @@ class ImageFileService {
|
||||||
'isFlipped': isFlipped,
|
'isFlipped': isFlipped,
|
||||||
'widthDip': width,
|
'widthDip': width,
|
||||||
'heightDip': height,
|
'heightDip': height,
|
||||||
|
'page': page,
|
||||||
'defaultSizeDip': thumbnailDefaultSize,
|
'defaultSizeDip': thumbnailDefaultSize,
|
||||||
});
|
});
|
||||||
return result as Uint8List;
|
return result as Uint8List;
|
||||||
|
@ -217,7 +223,6 @@ class ImageFileService {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Stream<MoveOpEvent> move(Iterable<ImageEntry> entries, {@required bool copy, @required String destinationAlbum}) {
|
static Stream<MoveOpEvent> move(Iterable<ImageEntry> entries, {@required bool copy, @required String destinationAlbum}) {
|
||||||
debugPrint('move ${entries.length} entries');
|
|
||||||
try {
|
try {
|
||||||
return opChannel.receiveBroadcastStream(<String, dynamic>{
|
return opChannel.receiveBroadcastStream(<String, dynamic>{
|
||||||
'op': 'move',
|
'op': 'move',
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/image_metadata.dart';
|
import 'package:aves/model/image_metadata.dart';
|
||||||
|
import 'package:aves/model/multipage.dart';
|
||||||
import 'package:aves/services/service_policy.dart';
|
import 'package:aves/services/service_policy.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
@ -80,6 +81,19 @@ class MetadataService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<MultiPageInfo> getMultiPageInfo(ImageEntry entry) async {
|
||||||
|
try {
|
||||||
|
final result = await platform.invokeMethod('getMultiPageInfo', <String, dynamic>{
|
||||||
|
'mimeType': entry.mimeType,
|
||||||
|
'uri': entry.uri,
|
||||||
|
}) as Map;
|
||||||
|
return MultiPageInfo.fromMap(result);
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
debugPrint('getMultiPageInfo failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
static Future<List<Uint8List>> getEmbeddedPictures(String uri) async {
|
static Future<List<Uint8List>> getEmbeddedPictures(String uri) async {
|
||||||
try {
|
try {
|
||||||
final result = await platform.invokeMethod('getEmbeddedPictures', <String, dynamic>{
|
final result = await platform.invokeMethod('getEmbeddedPictures', <String, dynamic>{
|
||||||
|
|
|
@ -29,10 +29,11 @@ class Durations {
|
||||||
// search animations
|
// search animations
|
||||||
static const filterRowExpandAnimation = Duration(milliseconds: 300);
|
static const filterRowExpandAnimation = Duration(milliseconds: 300);
|
||||||
|
|
||||||
// fullscreen animations
|
// viewer animations
|
||||||
static const fullscreenPageAnimation = Duration(milliseconds: 300);
|
static const viewerPageAnimation = Duration(milliseconds: 300);
|
||||||
static const fullscreenOverlayAnimation = Duration(milliseconds: 200);
|
static const viewerOverlayAnimation = Duration(milliseconds: 200);
|
||||||
static const fullscreenOverlayChangeAnimation = Duration(milliseconds: 150);
|
static const viewerOverlayChangeAnimation = Duration(milliseconds: 150);
|
||||||
|
static const viewerOverlayPageChooserAnimation = Duration(milliseconds: 200);
|
||||||
|
|
||||||
// info
|
// info
|
||||||
static const mapStyleSwitchAnimation = Duration(milliseconds: 300);
|
static const mapStyleSwitchAnimation = Duration(milliseconds: 300);
|
||||||
|
|
|
@ -68,7 +68,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(CollectionAppBar oldWidget) {
|
void didUpdateWidget(covariant CollectionAppBar oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
_unregisterWidget(oldWidget);
|
_unregisterWidget(oldWidget);
|
||||||
_registerWidget(widget);
|
_registerWidget(widget);
|
||||||
|
|
|
@ -29,7 +29,7 @@ class _FilterBarState extends State<FilterBar> {
|
||||||
CollectionFilter _userRemovedFilter;
|
CollectionFilter _userRemovedFilter;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(FilterBar oldWidget) {
|
void didUpdateWidget(covariant FilterBar oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
final current = widget.filters;
|
final current = widget.filters;
|
||||||
final existing = oldWidget.filters;
|
final existing = oldWidget.filters;
|
||||||
|
|
|
@ -33,7 +33,6 @@ class SectionedListLayoutProvider extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
SectionedListLayout _updateLayouts(BuildContext context) {
|
SectionedListLayout _updateLayouts(BuildContext context) {
|
||||||
// debugPrint('$runtimeType _updateLayouts entries=${collection.entryCount} columnCount=$columnCount tileExtent=$tileExtent');
|
|
||||||
final sectionLayouts = <SectionLayout>[];
|
final sectionLayouts = <SectionLayout>[];
|
||||||
final showHeaders = collection.showHeaders;
|
final showHeaders = collection.showHeaders;
|
||||||
final source = collection.source;
|
final source = collection.source;
|
||||||
|
|
|
@ -11,6 +11,7 @@ import 'package:flutter/material.dart';
|
||||||
class ThumbnailRasterImage extends StatefulWidget {
|
class ThumbnailRasterImage extends StatefulWidget {
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
final double extent;
|
final double extent;
|
||||||
|
final int page;
|
||||||
final ValueNotifier<bool> isScrollingNotifier;
|
final ValueNotifier<bool> isScrollingNotifier;
|
||||||
final Object heroTag;
|
final Object heroTag;
|
||||||
|
|
||||||
|
@ -18,6 +19,7 @@ class ThumbnailRasterImage extends StatefulWidget {
|
||||||
Key key,
|
Key key,
|
||||||
@required this.entry,
|
@required this.entry,
|
||||||
@required this.extent,
|
@required this.extent,
|
||||||
|
this.page = 0,
|
||||||
this.isScrollingNotifier,
|
this.isScrollingNotifier,
|
||||||
this.heroTag,
|
this.heroTag,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
@ -31,6 +33,8 @@ class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
|
||||||
|
|
||||||
ImageEntry get entry => widget.entry;
|
ImageEntry get entry => widget.entry;
|
||||||
|
|
||||||
|
int get page => widget.page;
|
||||||
|
|
||||||
double get extent => widget.extent;
|
double get extent => widget.extent;
|
||||||
|
|
||||||
Object get heroTag => widget.heroTag;
|
Object get heroTag => widget.heroTag;
|
||||||
|
@ -47,7 +51,7 @@ class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(ThumbnailRasterImage oldWidget) {
|
void didUpdateWidget(covariant ThumbnailRasterImage oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (oldWidget.entry != entry) {
|
if (oldWidget.entry != entry) {
|
||||||
_unregisterWidget(oldWidget);
|
_unregisterWidget(oldWidget);
|
||||||
|
@ -75,11 +79,11 @@ class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
|
||||||
if (!entry.canDecode) return;
|
if (!entry.canDecode) return;
|
||||||
|
|
||||||
_fastThumbnailProvider = ThumbnailProvider(
|
_fastThumbnailProvider = ThumbnailProvider(
|
||||||
ThumbnailProviderKey.fromEntry(entry),
|
ThumbnailProviderKey.fromEntry(entry, page: page),
|
||||||
);
|
);
|
||||||
if (!entry.isVideo) {
|
if (!entry.isVideo) {
|
||||||
_sizedThumbnailProvider = ThumbnailProvider(
|
_sizedThumbnailProvider = ThumbnailProvider(
|
||||||
ThumbnailProviderKey.fromEntry(entry, extent: requestExtent),
|
ThumbnailProviderKey.fromEntry(entry, page: page, extent: requestExtent),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -149,6 +153,7 @@ class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
|
||||||
final imageProvider = UriImage(
|
final imageProvider = UriImage(
|
||||||
uri: entry.uri,
|
uri: entry.uri,
|
||||||
mimeType: entry.mimeType,
|
mimeType: entry.mimeType,
|
||||||
|
page: page,
|
||||||
rotationDegrees: entry.rotationDegrees,
|
rotationDegrees: entry.rotationDegrees,
|
||||||
isFlipped: entry.isFlipped,
|
isFlipped: entry.isFlipped,
|
||||||
expectedContentLength: entry.sizeBytes,
|
expectedContentLength: entry.sizeBytes,
|
||||||
|
|
|
@ -29,7 +29,7 @@ class ThumbnailVectorImage extends StatelessWidget {
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final availableSize = constraints.biggest;
|
final availableSize = constraints.biggest;
|
||||||
final fitSize = applyBoxFit(fit, entry.displaySize, availableSize).destination;
|
final fitSize = applyBoxFit(fit, entry.getDisplaySize(), availableSize).destination;
|
||||||
final offset = fitSize / 2 - availableSize / 2;
|
final offset = fitSize / 2 - availableSize / 2;
|
||||||
final child = DecoratedBox(
|
final child = DecoratedBox(
|
||||||
decoration: CheckeredDecoration(checkSize: extent / 8, offset: offset),
|
decoration: CheckeredDecoration(checkSize: extent / 8, offset: offset),
|
||||||
|
|
|
@ -173,7 +173,7 @@ class _CollectionScrollViewState extends State<CollectionScrollView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(CollectionScrollView oldWidget) {
|
void didUpdateWidget(covariant CollectionScrollView oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
_unregisterWidget(oldWidget);
|
_unregisterWidget(oldWidget);
|
||||||
_registerWidget(widget);
|
_registerWidget(widget);
|
||||||
|
|
|
@ -30,7 +30,7 @@ class _MultiCrossFaderState extends State<MultiCrossFader> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(MultiCrossFader oldWidget) {
|
void didUpdateWidget(covariant MultiCrossFader oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (_first == oldWidget.child) {
|
if (_first == oldWidget.child) {
|
||||||
_second = widget.child;
|
_second = widget.child;
|
||||||
|
|
|
@ -57,7 +57,7 @@ class _SweeperState extends State<Sweeper> with SingleTickerProviderStateMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(Sweeper oldWidget) {
|
void didUpdateWidget(covariant Sweeper oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
_unregisterWidget(oldWidget);
|
_unregisterWidget(oldWidget);
|
||||||
_registerWidget(widget);
|
_registerWidget(widget);
|
||||||
|
|
|
@ -57,7 +57,7 @@ class _TransitionImageState extends State<TransitionImage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(TransitionImage oldWidget) {
|
void didUpdateWidget(covariant TransitionImage oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (_isListeningToStream) {
|
if (_isListeningToStream) {
|
||||||
_imageStream.removeListener(_getListener());
|
_imageStream.removeListener(_getListener());
|
||||||
|
|
|
@ -66,7 +66,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(AvesFilterChip oldWidget) {
|
void didUpdateWidget(covariant AvesFilterChip oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (oldWidget.filter != filter) {
|
if (oldWidget.filter != filter) {
|
||||||
_initColorLoader();
|
_initColorLoader();
|
||||||
|
|
|
@ -58,19 +58,14 @@ class Magnifier extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MagnifierState extends State<Magnifier> {
|
class _MagnifierState extends State<Magnifier> {
|
||||||
Size _childSize;
|
|
||||||
|
|
||||||
bool _controlledController;
|
bool _controlledController;
|
||||||
MagnifierController _controller;
|
MagnifierController _controller;
|
||||||
|
|
||||||
void _setChildSize(Size childSize) {
|
Size get childSize => widget.childSize;
|
||||||
_childSize = childSize.isEmpty ? null : childSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_setChildSize(widget.childSize);
|
|
||||||
if (widget.controller == null) {
|
if (widget.controller == null) {
|
||||||
_controlledController = true;
|
_controlledController = true;
|
||||||
_controller = MagnifierController();
|
_controller = MagnifierController();
|
||||||
|
@ -81,12 +76,8 @@ class _MagnifierState extends State<Magnifier> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(Magnifier oldWidget) {
|
void didUpdateWidget(covariant Magnifier oldWidget) {
|
||||||
if (oldWidget.childSize != widget.childSize && widget.childSize != null) {
|
super.didUpdateWidget(oldWidget);
|
||||||
setState(() {
|
|
||||||
_setChildSize(widget.childSize);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (widget.controller == null) {
|
if (widget.controller == null) {
|
||||||
if (!_controlledController) {
|
if (!_controlledController) {
|
||||||
_controlledController = true;
|
_controlledController = true;
|
||||||
|
@ -96,7 +87,6 @@ class _MagnifierState extends State<Magnifier> {
|
||||||
_controlledController = false;
|
_controlledController = false;
|
||||||
_controller = widget.controller;
|
_controller = widget.controller;
|
||||||
}
|
}
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -116,7 +106,7 @@ class _MagnifierState extends State<Magnifier> {
|
||||||
widget.maxScale ?? ScaleLevel(factor: double.infinity),
|
widget.maxScale ?? ScaleLevel(factor: double.infinity),
|
||||||
widget.initialScale ?? ScaleLevel(ref: ScaleReference.contained),
|
widget.initialScale ?? ScaleLevel(ref: ScaleReference.contained),
|
||||||
constraints.biggest,
|
constraints.biggest,
|
||||||
_childSize ?? constraints.biggest,
|
widget.childSize?.isEmpty == true ? constraints.biggest: widget.childSize,
|
||||||
));
|
));
|
||||||
|
|
||||||
return MagnifierCore(
|
return MagnifierCore(
|
||||||
|
|
|
@ -109,6 +109,8 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
||||||
UriImage(
|
UriImage(
|
||||||
uri: uri,
|
uri: uri,
|
||||||
mimeType: mimeType,
|
mimeType: mimeType,
|
||||||
|
// TODO TLAD multipage print
|
||||||
|
page: 0,
|
||||||
rotationDegrees: rotationDegrees,
|
rotationDegrees: rotationDegrees,
|
||||||
isFlipped: isFlipped,
|
isFlipped: isFlipped,
|
||||||
),
|
),
|
||||||
|
|
|
@ -14,6 +14,7 @@ import 'package:aves/widgets/fullscreen/image_page.dart';
|
||||||
import 'package:aves/widgets/fullscreen/image_view.dart';
|
import 'package:aves/widgets/fullscreen/image_view.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/info_page.dart';
|
import 'package:aves/widgets/fullscreen/info/info_page.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/notifications.dart';
|
import 'package:aves/widgets/fullscreen/info/notifications.dart';
|
||||||
|
import 'package:aves/widgets/fullscreen/multipage_controller.dart';
|
||||||
import 'package:aves/widgets/fullscreen/overlay/bottom.dart';
|
import 'package:aves/widgets/fullscreen/overlay/bottom.dart';
|
||||||
import 'package:aves/widgets/fullscreen/overlay/panorama.dart';
|
import 'package:aves/widgets/fullscreen/overlay/panorama.dart';
|
||||||
import 'package:aves/widgets/fullscreen/overlay/top.dart';
|
import 'package:aves/widgets/fullscreen/overlay/top.dart';
|
||||||
|
@ -54,6 +55,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
EdgeInsets _frozenViewInsets, _frozenViewPadding;
|
EdgeInsets _frozenViewInsets, _frozenViewPadding;
|
||||||
EntryActionDelegate _actionDelegate;
|
EntryActionDelegate _actionDelegate;
|
||||||
final List<Tuple2<String, IjkMediaController>> _videoControllers = [];
|
final List<Tuple2<String, IjkMediaController>> _videoControllers = [];
|
||||||
|
final List<Tuple2<String, MultiPageController>> _multiPageControllers = [];
|
||||||
final List<Tuple2<String, ValueNotifier<ViewState>>> _viewStateNotifiers = [];
|
final List<Tuple2<String, ValueNotifier<ViewState>>> _viewStateNotifiers = [];
|
||||||
|
|
||||||
CollectionLens get collection => widget.collection;
|
CollectionLens get collection => widget.collection;
|
||||||
|
@ -78,7 +80,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
_horizontalPager = PageController(initialPage: _currentHorizontalPage);
|
_horizontalPager = PageController(initialPage: _currentHorizontalPage);
|
||||||
_verticalPager = PageController(initialPage: _currentVerticalPage.value)..addListener(_onVerticalPageControllerChange);
|
_verticalPager = PageController(initialPage: _currentVerticalPage.value)..addListener(_onVerticalPageControllerChange);
|
||||||
_overlayAnimationController = AnimationController(
|
_overlayAnimationController = AnimationController(
|
||||||
duration: Durations.fullscreenOverlayAnimation,
|
duration: Durations.viewerOverlayAnimation,
|
||||||
vsync: this,
|
vsync: this,
|
||||||
);
|
);
|
||||||
_topOverlayScale = CurvedAnimation(
|
_topOverlayScale = CurvedAnimation(
|
||||||
|
@ -110,7 +112,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(FullscreenBody oldWidget) {
|
void didUpdateWidget(covariant FullscreenBody oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
_unregisterWidget(oldWidget);
|
_unregisterWidget(oldWidget);
|
||||||
_registerWidget(widget);
|
_registerWidget(widget);
|
||||||
|
@ -122,6 +124,8 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
_overlayVisible.removeListener(_onOverlayVisibleChange);
|
_overlayVisible.removeListener(_onOverlayVisibleChange);
|
||||||
_videoControllers.forEach((kv) => kv.item2.dispose());
|
_videoControllers.forEach((kv) => kv.item2.dispose());
|
||||||
_videoControllers.clear();
|
_videoControllers.clear();
|
||||||
|
_multiPageControllers.forEach((kv) => kv.item2.dispose());
|
||||||
|
_multiPageControllers.clear();
|
||||||
_verticalPager.removeListener(_onVerticalPageControllerChange);
|
_verticalPager.removeListener(_onVerticalPageControllerChange);
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
_unregisterWidget(widget);
|
_unregisterWidget(widget);
|
||||||
|
@ -170,6 +174,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
collection: collection,
|
collection: collection,
|
||||||
entryNotifier: _entryNotifier,
|
entryNotifier: _entryNotifier,
|
||||||
videoControllers: _videoControllers,
|
videoControllers: _videoControllers,
|
||||||
|
multiPageControllers: _multiPageControllers,
|
||||||
verticalPager: _verticalPager,
|
verticalPager: _verticalPager,
|
||||||
horizontalPager: _horizontalPager,
|
horizontalPager: _horizontalPager,
|
||||||
onVerticalPageChanged: _onVerticalPageChanged,
|
onVerticalPageChanged: _onVerticalPageChanged,
|
||||||
|
@ -196,6 +201,9 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
valueListenable: _entryNotifier,
|
valueListenable: _entryNotifier,
|
||||||
builder: (context, entry, child) {
|
builder: (context, entry, child) {
|
||||||
if (entry == null) return SizedBox.shrink();
|
if (entry == null) return SizedBox.shrink();
|
||||||
|
|
||||||
|
final multiPageController = _getMultiPageController(entry);
|
||||||
|
|
||||||
final viewStateNotifier = _viewStateNotifiers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2;
|
final viewStateNotifier = _viewStateNotifiers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2;
|
||||||
return FullscreenTopOverlay(
|
return FullscreenTopOverlay(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
|
@ -205,6 +213,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
viewPadding: _frozenViewPadding,
|
viewPadding: _frozenViewPadding,
|
||||||
onActionSelected: (action) => _actionDelegate.onActionSelected(context, entry, action),
|
onActionSelected: (action) => _actionDelegate.onActionSelected(context, entry, action),
|
||||||
viewStateNotifier: viewStateNotifier,
|
viewStateNotifier: viewStateNotifier,
|
||||||
|
multiPageController: multiPageController,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -226,6 +235,8 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
builder: (context, entry, child) {
|
builder: (context, entry, child) {
|
||||||
if (entry == null) return SizedBox.shrink();
|
if (entry == null) return SizedBox.shrink();
|
||||||
|
|
||||||
|
final multiPageController = _getMultiPageController(entry);
|
||||||
|
|
||||||
Widget extraBottomOverlay;
|
Widget extraBottomOverlay;
|
||||||
if (entry.isVideo) {
|
if (entry.isVideo) {
|
||||||
final videoController = _videoControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2;
|
final videoController = _videoControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2;
|
||||||
|
@ -259,6 +270,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
showPosition: hasCollection,
|
showPosition: hasCollection,
|
||||||
viewInsets: _frozenViewInsets,
|
viewInsets: _frozenViewInsets,
|
||||||
viewPadding: _frozenViewPadding,
|
viewPadding: _frozenViewPadding,
|
||||||
|
multiPageController: multiPageController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -296,6 +308,10 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
return bottomOverlay;
|
return bottomOverlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MultiPageController _getMultiPageController(ImageEntry entry) {
|
||||||
|
return entry.isMultipage ? _multiPageControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2 : null;
|
||||||
|
}
|
||||||
|
|
||||||
void _onVerticalPageControllerChange() {
|
void _onVerticalPageControllerChange() {
|
||||||
_verticalScrollNotifier.notifyListeners();
|
_verticalScrollNotifier.notifyListeners();
|
||||||
}
|
}
|
||||||
|
@ -315,7 +331,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
Future<void> _goToVerticalPage(int page) {
|
Future<void> _goToVerticalPage(int page) {
|
||||||
return _verticalPager.animateToPage(
|
return _verticalPager.animateToPage(
|
||||||
page,
|
page,
|
||||||
duration: Durations.fullscreenPageAnimation,
|
duration: Durations.viewerPageAnimation,
|
||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -428,6 +444,14 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
(_) => _.dispose(),
|
(_) => _.dispose(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (entry.isMultipage) {
|
||||||
|
_initViewSpecificController<MultiPageController>(
|
||||||
|
uri,
|
||||||
|
_multiPageControllers,
|
||||||
|
() => MultiPageController(entry),
|
||||||
|
(_) => _.dispose(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
@ -452,6 +476,7 @@ class FullscreenVerticalPageView extends StatefulWidget {
|
||||||
final CollectionLens collection;
|
final CollectionLens collection;
|
||||||
final ValueNotifier<ImageEntry> entryNotifier;
|
final ValueNotifier<ImageEntry> entryNotifier;
|
||||||
final List<Tuple2<String, IjkMediaController>> videoControllers;
|
final List<Tuple2<String, IjkMediaController>> videoControllers;
|
||||||
|
final List<Tuple2<String, MultiPageController>> multiPageControllers;
|
||||||
final PageController horizontalPager, verticalPager;
|
final PageController horizontalPager, verticalPager;
|
||||||
final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged;
|
final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged;
|
||||||
final VoidCallback onImageTap, onImagePageRequested;
|
final VoidCallback onImageTap, onImagePageRequested;
|
||||||
|
@ -461,6 +486,7 @@ class FullscreenVerticalPageView extends StatefulWidget {
|
||||||
@required this.collection,
|
@required this.collection,
|
||||||
@required this.entryNotifier,
|
@required this.entryNotifier,
|
||||||
@required this.videoControllers,
|
@required this.videoControllers,
|
||||||
|
@required this.multiPageControllers,
|
||||||
@required this.verticalPager,
|
@required this.verticalPager,
|
||||||
@required this.horizontalPager,
|
@required this.horizontalPager,
|
||||||
@required this.onVerticalPageChanged,
|
@required this.onVerticalPageChanged,
|
||||||
|
@ -492,7 +518,7 @@ class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView>
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(FullscreenVerticalPageView oldWidget) {
|
void didUpdateWidget(covariant FullscreenVerticalPageView oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
_unregisterWidget(oldWidget);
|
_unregisterWidget(oldWidget);
|
||||||
_registerWidget(widget);
|
_registerWidget(widget);
|
||||||
|
@ -528,12 +554,14 @@ class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView>
|
||||||
onTap: widget.onImageTap,
|
onTap: widget.onImageTap,
|
||||||
onPageChanged: widget.onHorizontalPageChanged,
|
onPageChanged: widget.onHorizontalPageChanged,
|
||||||
videoControllers: widget.videoControllers,
|
videoControllers: widget.videoControllers,
|
||||||
|
multiPageControllers: widget.multiPageControllers,
|
||||||
onViewDisposed: widget.onViewDisposed,
|
onViewDisposed: widget.onViewDisposed,
|
||||||
)
|
)
|
||||||
: SingleImagePage(
|
: SingleImagePage(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
onTap: widget.onImageTap,
|
onTap: widget.onImageTap,
|
||||||
videoControllers: widget.videoControllers,
|
videoControllers: widget.videoControllers,
|
||||||
|
multiPageControllers: widget.multiPageControllers,
|
||||||
),
|
),
|
||||||
NotificationListener(
|
NotificationListener(
|
||||||
onNotification: (notification) {
|
onNotification: (notification) {
|
||||||
|
|
|
@ -80,7 +80,7 @@ class FullscreenDebugPage extends StatelessWidget {
|
||||||
'isFlipped': '${entry.isFlipped}',
|
'isFlipped': '${entry.isFlipped}',
|
||||||
'portrait': '${entry.isPortrait}',
|
'portrait': '${entry.isPortrait}',
|
||||||
'displayAspectRatio': '${entry.displayAspectRatio}',
|
'displayAspectRatio': '${entry.displayAspectRatio}',
|
||||||
'displaySize': '${entry.displaySize}',
|
'displaySize': '${entry.getDisplaySize()}',
|
||||||
}),
|
}),
|
||||||
Divider(),
|
Divider(),
|
||||||
InfoRowGroup({
|
InfoRowGroup({
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:aves/model/multipage.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/widgets/common/magnifier/pan/gesture_detector_scope.dart';
|
import 'package:aves/widgets/common/magnifier/pan/gesture_detector_scope.dart';
|
||||||
import 'package:aves/widgets/common/magnifier/pan/scroll_physics.dart';
|
import 'package:aves/widgets/common/magnifier/pan/scroll_physics.dart';
|
||||||
import 'package:aves/widgets/fullscreen/image_view.dart';
|
import 'package:aves/widgets/fullscreen/image_view.dart';
|
||||||
|
import 'package:aves/widgets/fullscreen/multipage_controller.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
|
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
@ -13,6 +15,7 @@ class MultiImagePage extends StatefulWidget {
|
||||||
final ValueChanged<int> onPageChanged;
|
final ValueChanged<int> onPageChanged;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
final List<Tuple2<String, IjkMediaController>> videoControllers;
|
final List<Tuple2<String, IjkMediaController>> videoControllers;
|
||||||
|
final List<Tuple2<String, MultiPageController>> multiPageControllers;
|
||||||
final void Function(String uri) onViewDisposed;
|
final void Function(String uri) onViewDisposed;
|
||||||
|
|
||||||
const MultiImagePage({
|
const MultiImagePage({
|
||||||
|
@ -21,6 +24,7 @@ class MultiImagePage extends StatefulWidget {
|
||||||
this.onPageChanged,
|
this.onPageChanged,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.videoControllers,
|
this.videoControllers,
|
||||||
|
this.multiPageControllers,
|
||||||
this.onViewDisposed,
|
this.onViewDisposed,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -45,15 +49,29 @@ class MultiImagePageState extends State<MultiImagePage> with AutomaticKeepAliveC
|
||||||
onPageChanged: widget.onPageChanged,
|
onPageChanged: widget.onPageChanged,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final entry = entries[index];
|
final entry = entries[index];
|
||||||
|
|
||||||
|
Widget child;
|
||||||
|
if (entry.isMultipage) {
|
||||||
|
final multiPageController = _getMultiPageController(entry);
|
||||||
|
if (multiPageController != null) {
|
||||||
|
child = FutureBuilder<MultiPageInfo>(
|
||||||
|
future: multiPageController.info,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final multiPageInfo = snapshot.data;
|
||||||
|
return ValueListenableBuilder<int>(
|
||||||
|
valueListenable: multiPageController.pageNotifier,
|
||||||
|
builder: (context, page, child) {
|
||||||
|
return _buildViewer(entry, multiPageInfo: multiPageInfo, page: page);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
child ??= _buildViewer(entry);
|
||||||
|
|
||||||
return ClipRect(
|
return ClipRect(
|
||||||
child: ImageView(
|
child: child,
|
||||||
key: Key('imageview'),
|
|
||||||
entry: entry,
|
|
||||||
heroTag: widget.collection.heroTag(entry),
|
|
||||||
onTap: (_) => widget.onTap?.call(),
|
|
||||||
videoControllers: widget.videoControllers,
|
|
||||||
onDisposed: () => widget.onViewDisposed?.call(entry.uri),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
itemCount: entries.length,
|
itemCount: entries.length,
|
||||||
|
@ -61,6 +79,23 @@ class MultiImagePageState extends State<MultiImagePage> with AutomaticKeepAliveC
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImageView _buildViewer(ImageEntry entry, {MultiPageInfo multiPageInfo, int page = 0}) {
|
||||||
|
return ImageView(
|
||||||
|
key: Key('imageview'),
|
||||||
|
entry: entry,
|
||||||
|
multiPageInfo: multiPageInfo,
|
||||||
|
page: page,
|
||||||
|
heroTag: widget.collection.heroTag(entry),
|
||||||
|
onTap: (_) => widget.onTap?.call(),
|
||||||
|
videoControllers: widget.videoControllers,
|
||||||
|
onDisposed: () => widget.onViewDisposed?.call(entry.uri),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiPageController _getMultiPageController(ImageEntry entry) {
|
||||||
|
return widget.multiPageControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get wantKeepAlive => true;
|
bool get wantKeepAlive => true;
|
||||||
}
|
}
|
||||||
|
@ -69,11 +104,13 @@ class SingleImagePage extends StatefulWidget {
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
final List<Tuple2<String, IjkMediaController>> videoControllers;
|
final List<Tuple2<String, IjkMediaController>> videoControllers;
|
||||||
|
final List<Tuple2<String, MultiPageController>> multiPageControllers;
|
||||||
|
|
||||||
const SingleImagePage({
|
const SingleImagePage({
|
||||||
this.entry,
|
this.entry,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.videoControllers,
|
this.videoControllers,
|
||||||
|
this.multiPageControllers,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -81,20 +118,52 @@ class SingleImagePage extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class SingleImagePageState extends State<SingleImagePage> with AutomaticKeepAliveClientMixin {
|
class SingleImagePageState extends State<SingleImagePage> with AutomaticKeepAliveClientMixin {
|
||||||
|
ImageEntry get entry => widget.entry;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
|
|
||||||
|
Widget child;
|
||||||
|
if (entry.isMultipage) {
|
||||||
|
final multiPageController = _getMultiPageController(entry);
|
||||||
|
if (multiPageController != null) {
|
||||||
|
child = FutureBuilder<MultiPageInfo>(
|
||||||
|
future: multiPageController.info,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final multiPageInfo = snapshot.data;
|
||||||
|
return ValueListenableBuilder<int>(
|
||||||
|
valueListenable: multiPageController.pageNotifier,
|
||||||
|
builder: (context, page, child) {
|
||||||
|
return _buildViewer(multiPageInfo: multiPageInfo, page: page);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
child ??= _buildViewer();
|
||||||
|
|
||||||
return MagnifierGestureDetectorScope(
|
return MagnifierGestureDetectorScope(
|
||||||
axis: [Axis.vertical],
|
axis: [Axis.vertical],
|
||||||
child: ImageView(
|
child: child,
|
||||||
entry: widget.entry,
|
|
||||||
onTap: (_) => widget.onTap?.call(),
|
|
||||||
videoControllers: widget.videoControllers,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImageView _buildViewer({MultiPageInfo multiPageInfo, int page = 0}) {
|
||||||
|
return ImageView(
|
||||||
|
entry: entry,
|
||||||
|
multiPageInfo: multiPageInfo,
|
||||||
|
page: page,
|
||||||
|
onTap: (_) => widget.onTap?.call(),
|
||||||
|
videoControllers: widget.videoControllers,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiPageController _getMultiPageController(ImageEntry entry) {
|
||||||
|
return widget.multiPageControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get wantKeepAlive => true;
|
bool get wantKeepAlive => true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/image_providers/uri_picture_provider.dart';
|
import 'package:aves/image_providers/uri_picture_provider.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:aves/model/multipage.dart';
|
||||||
import 'package:aves/model/settings/entry_background.dart';
|
import 'package:aves/model/settings/entry_background.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
|
@ -23,6 +24,8 @@ import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
class ImageView extends StatefulWidget {
|
class ImageView extends StatefulWidget {
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
|
final MultiPageInfo multiPageInfo;
|
||||||
|
final int page;
|
||||||
final Object heroTag;
|
final Object heroTag;
|
||||||
final MagnifierTapCallback onTap;
|
final MagnifierTapCallback onTap;
|
||||||
final List<Tuple2<String, IjkMediaController>> videoControllers;
|
final List<Tuple2<String, IjkMediaController>> videoControllers;
|
||||||
|
@ -33,6 +36,8 @@ class ImageView extends StatefulWidget {
|
||||||
const ImageView({
|
const ImageView({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.entry,
|
@required this.entry,
|
||||||
|
this.multiPageInfo,
|
||||||
|
this.page = 0,
|
||||||
this.heroTag,
|
this.heroTag,
|
||||||
@required this.onTap,
|
@required this.onTap,
|
||||||
@required this.videoControllers,
|
@required this.videoControllers,
|
||||||
|
@ -54,8 +59,14 @@ class _ImageViewState extends State<ImageView> {
|
||||||
|
|
||||||
ImageEntry get entry => widget.entry;
|
ImageEntry get entry => widget.entry;
|
||||||
|
|
||||||
|
MultiPageInfo get multiPageInfo => widget.multiPageInfo;
|
||||||
|
|
||||||
|
int get page => widget.page;
|
||||||
|
|
||||||
MagnifierTapCallback get onTap => widget.onTap;
|
MagnifierTapCallback get onTap => widget.onTap;
|
||||||
|
|
||||||
|
Size get pageDisplaySize => entry.getDisplaySize(multiPageInfo: multiPageInfo, page: page);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
@ -99,13 +110,15 @@ class _ImageViewState extends State<ImageView> {
|
||||||
Widget _buildRasterView() {
|
Widget _buildRasterView() {
|
||||||
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.rotationDegrees}_${entry.isFlipped}_${entry.width}_${entry.height}_${entry.path}'),
|
key: ValueKey('${page}_${entry.rotationDegrees}_${entry.isFlipped}_${entry.width}_${entry.height}_${entry.path}'),
|
||||||
child: TiledImageView(
|
child: TiledImageView(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
|
multiPageInfo: multiPageInfo,
|
||||||
|
page: page,
|
||||||
viewStateNotifier: _viewStateNotifier,
|
viewStateNotifier: _viewStateNotifier,
|
||||||
errorBuilder: (context, error, stackTrace) => ErrorChild(onTap: () => onTap?.call(null)),
|
errorBuilder: (context, error, stackTrace) => ErrorChild(onTap: () => onTap?.call(null)),
|
||||||
),
|
),
|
||||||
childSize: entry.displaySize,
|
childSize: pageDisplaySize,
|
||||||
controller: _magnifierController,
|
controller: _magnifierController,
|
||||||
maxScale: maxScale,
|
maxScale: maxScale,
|
||||||
minScale: minScale,
|
minScale: minScale,
|
||||||
|
@ -127,7 +140,7 @@ class _ImageViewState extends State<ImageView> {
|
||||||
colorFilter: colorFilter,
|
colorFilter: colorFilter,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
childSize: entry.displaySize,
|
childSize: pageDisplaySize,
|
||||||
controller: _magnifierController,
|
controller: _magnifierController,
|
||||||
minScale: minScale,
|
minScale: minScale,
|
||||||
initialScale: initialScale,
|
initialScale: initialScale,
|
||||||
|
@ -145,7 +158,7 @@ class _ImageViewState extends State<ImageView> {
|
||||||
final side = viewportSize.shortestSide;
|
final side = viewportSize.shortestSide;
|
||||||
final checkSize = side / ((side / ImageView.decorationCheckSize).round());
|
final checkSize = side / ((side / ImageView.decorationCheckSize).round());
|
||||||
|
|
||||||
final viewSize = entry.displaySize * viewState.scale;
|
final viewSize = pageDisplaySize * viewState.scale;
|
||||||
final decorationSize = applyBoxFit(BoxFit.none, viewSize, viewportSize).source;
|
final decorationSize = applyBoxFit(BoxFit.none, viewSize, viewportSize).source;
|
||||||
final offset = ((decorationSize - viewportSize) as Offset) / 2;
|
final offset = ((decorationSize - viewportSize) as Offset) / 2;
|
||||||
|
|
||||||
|
@ -181,7 +194,7 @@ class _ImageViewState extends State<ImageView> {
|
||||||
controller: videoController,
|
controller: videoController,
|
||||||
)
|
)
|
||||||
: SizedBox(),
|
: SizedBox(),
|
||||||
childSize: entry.displaySize,
|
childSize: pageDisplaySize,
|
||||||
controller: _magnifierController,
|
controller: _magnifierController,
|
||||||
maxScale: maxScale,
|
maxScale: maxScale,
|
||||||
minScale: minScale,
|
minScale: minScale,
|
||||||
|
|
|
@ -26,12 +26,16 @@ class BasicSection extends StatelessWidget {
|
||||||
@required this.onFilter,
|
@required this.onFilter,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
int get megaPixels => entry.megaPixels;
|
||||||
|
|
||||||
|
bool get showMegaPixels => entry.isPhoto && megaPixels != null && megaPixels > 0;
|
||||||
|
|
||||||
|
String get rasterResolutionText => '${entry.getResolutionText()}${showMegaPixels ? ' ($megaPixels MP)' : ''}';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final date = entry.bestDate;
|
final date = entry.bestDate;
|
||||||
final dateText = date != null ? '${DateFormat.yMMMd().format(date)} • ${DateFormat.Hm().format(date)}' : Constants.infoUnknown;
|
final dateText = date != null ? '${DateFormat.yMMMd().format(date)} • ${DateFormat.Hm().format(date)}' : Constants.infoUnknown;
|
||||||
final showMegaPixels = entry.isPhoto && entry.megaPixels != null && entry.megaPixels > 0;
|
|
||||||
final resolutionText = '${entry.resolutionText}${showMegaPixels ? ' (${entry.megaPixels} MP)' : ''}';
|
|
||||||
|
|
||||||
// TODO TLAD line break on all characters for the following fields when this is fixed: https://github.com/flutter/flutter/issues/61081
|
// TODO TLAD line break on all characters for the following fields when this is fixed: https://github.com/flutter/flutter/issues/61081
|
||||||
// inserting ZWSP (\u200B) between characters does help, but it messes with width and height computation (another Flutter issue)
|
// inserting ZWSP (\u200B) between characters does help, but it messes with width and height computation (another Flutter issue)
|
||||||
|
@ -46,7 +50,7 @@ class BasicSection extends StatelessWidget {
|
||||||
'Title': title,
|
'Title': title,
|
||||||
'Date': dateText,
|
'Date': dateText,
|
||||||
if (entry.isVideo) ..._buildVideoRows(),
|
if (entry.isVideo) ..._buildVideoRows(),
|
||||||
if (!entry.isSvg) 'Resolution': resolutionText,
|
if (!entry.isSvg) 'Resolution': rasterResolutionText,
|
||||||
'Size': entry.sizeBytes != null ? formatFilesize(entry.sizeBytes) : Constants.infoUnknown,
|
'Size': entry.sizeBytes != null ? formatFilesize(entry.sizeBytes) : Constants.infoUnknown,
|
||||||
'URI': uri,
|
'URI': uri,
|
||||||
if (path != null) 'Path': path,
|
if (path != null) 'Path': path,
|
||||||
|
|
|
@ -99,7 +99,7 @@ class InfoPageState extends State<InfoPage> {
|
||||||
BackUpNotification().dispatch(context);
|
BackUpNotification().dispatch(context);
|
||||||
_scrollController.animateTo(
|
_scrollController.animateTo(
|
||||||
0,
|
0,
|
||||||
duration: Durations.fullscreenPageAnimation,
|
duration: Durations.viewerPageAnimation,
|
||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,7 @@ class _LocationSectionState extends State<LocationSection> with TickerProviderSt
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(LocationSection oldWidget) {
|
void didUpdateWidget(covariant LocationSection oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
_unregisterWidget(oldWidget);
|
_unregisterWidget(oldWidget);
|
||||||
_registerWidget(widget);
|
_registerWidget(widget);
|
||||||
|
|
|
@ -41,7 +41,7 @@ class EntryGoogleMapState extends State<EntryGoogleMap> with AutomaticKeepAliveC
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(EntryGoogleMap oldWidget) {
|
void didUpdateWidget(covariant EntryGoogleMap oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (widget.latLng != oldWidget.latLng && _controller != null) {
|
if (widget.latLng != oldWidget.latLng && _controller != null) {
|
||||||
_controller.moveCamera(CameraUpdate.newLatLng(widget.latLng));
|
_controller.moveCamera(CameraUpdate.newLatLng(widget.latLng));
|
||||||
|
|
|
@ -35,7 +35,7 @@ class EntryLeafletMapState extends State<EntryLeafletMap> with AutomaticKeepAliv
|
||||||
final MapController _mapController = MapController();
|
final MapController _mapController = MapController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(EntryLeafletMap oldWidget) {
|
void didUpdateWidget(covariant EntryLeafletMap oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (widget.latLng != oldWidget.latLng && _mapController != null) {
|
if (widget.latLng != oldWidget.latLng && _mapController != null) {
|
||||||
_mapController.move(widget.latLng, settings.infoMapZoom);
|
_mapController.move(widget.latLng, settings.infoMapZoom);
|
||||||
|
|
|
@ -116,7 +116,7 @@ class MarkerPointerPainter extends CustomPainter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// generate bitmap from widget, for Google Maps
|
// generate bitmap from widget, for Google Maps
|
||||||
|
|
|
@ -52,7 +52,7 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(MetadataSectionSliver oldWidget) {
|
void didUpdateWidget(covariant MetadataSectionSliver oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
_unregisterWidget(oldWidget);
|
_unregisterWidget(oldWidget);
|
||||||
_registerWidget(widget);
|
_registerWidget(widget);
|
||||||
|
|
24
lib/widgets/fullscreen/multipage_controller.dart
Normal file
24
lib/widgets/fullscreen/multipage_controller.dart
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:aves/model/multipage.dart';
|
||||||
|
import 'package:aves/services/metadata_service.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class MultiPageController extends ChangeNotifier {
|
||||||
|
final Future<MultiPageInfo> info;
|
||||||
|
final ValueNotifier<int> pageNotifier = ValueNotifier(0);
|
||||||
|
|
||||||
|
MultiPageController(ImageEntry entry) : info = MetadataService.getMultiPageInfo(entry);
|
||||||
|
|
||||||
|
int get page => pageNotifier.value;
|
||||||
|
|
||||||
|
set page(int page) => pageNotifier.value = page;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
pageNotifier.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/image_metadata.dart';
|
import 'package:aves/model/image_metadata.dart';
|
||||||
|
import 'package:aves/model/multipage.dart';
|
||||||
import 'package:aves/model/settings/coordinate_format.dart';
|
import 'package:aves/model/settings/coordinate_format.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/services/metadata_service.dart';
|
import 'package:aves/services/metadata_service.dart';
|
||||||
|
@ -9,7 +10,9 @@ import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/utils/constants.dart';
|
import 'package:aves/utils/constants.dart';
|
||||||
import 'package:aves/widgets/common/fx/blurred.dart';
|
import 'package:aves/widgets/common/fx/blurred.dart';
|
||||||
|
import 'package:aves/widgets/fullscreen/multipage_controller.dart';
|
||||||
import 'package:aves/widgets/fullscreen/overlay/common.dart';
|
import 'package:aves/widgets/fullscreen/overlay/common.dart';
|
||||||
|
import 'package:aves/widgets/fullscreen/overlay/multipage.dart';
|
||||||
import 'package:decorated_icon/decorated_icon.dart';
|
import 'package:decorated_icon/decorated_icon.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
@ -21,6 +24,7 @@ class FullscreenBottomOverlay extends StatefulWidget {
|
||||||
final int index;
|
final int index;
|
||||||
final bool showPosition;
|
final bool showPosition;
|
||||||
final EdgeInsets viewInsets, viewPadding;
|
final EdgeInsets viewInsets, viewPadding;
|
||||||
|
final MultiPageController multiPageController;
|
||||||
|
|
||||||
const FullscreenBottomOverlay({
|
const FullscreenBottomOverlay({
|
||||||
Key key,
|
Key key,
|
||||||
|
@ -29,6 +33,7 @@ class FullscreenBottomOverlay extends StatefulWidget {
|
||||||
@required this.showPosition,
|
@required this.showPosition,
|
||||||
this.viewInsets,
|
this.viewInsets,
|
||||||
this.viewPadding,
|
this.viewPadding,
|
||||||
|
@required this.multiPageController,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -40,8 +45,6 @@ class _FullscreenBottomOverlayState extends State<FullscreenBottomOverlay> {
|
||||||
ImageEntry _lastEntry;
|
ImageEntry _lastEntry;
|
||||||
OverlayMetadata _lastDetails;
|
OverlayMetadata _lastDetails;
|
||||||
|
|
||||||
static const innerPadding = EdgeInsets.symmetric(vertical: 4, horizontal: 8);
|
|
||||||
|
|
||||||
ImageEntry get entry {
|
ImageEntry get entry {
|
||||||
final entries = widget.entries;
|
final entries = widget.entries;
|
||||||
final index = widget.index;
|
final index = widget.index;
|
||||||
|
@ -55,7 +58,7 @@ class _FullscreenBottomOverlayState extends State<FullscreenBottomOverlay> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(FullscreenBottomOverlay oldWidget) {
|
void didUpdateWidget(covariant FullscreenBottomOverlay oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (entry != _lastEntry) {
|
if (entry != _lastEntry) {
|
||||||
_initDetailLoader();
|
_initDetailLoader();
|
||||||
|
@ -68,46 +71,41 @@ class _FullscreenBottomOverlayState extends State<FullscreenBottomOverlay> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IgnorePointer(
|
return BlurredRect(
|
||||||
child: BlurredRect(
|
child: Selector<MediaQueryData, Tuple3<double, EdgeInsets, EdgeInsets>>(
|
||||||
child: Selector<MediaQueryData, Tuple3<double, EdgeInsets, EdgeInsets>>(
|
selector: (c, mq) => Tuple3(mq.size.width, mq.viewInsets, mq.viewPadding),
|
||||||
selector: (c, mq) => Tuple3(mq.size.width, mq.viewInsets, mq.viewPadding),
|
builder: (c, mq, child) {
|
||||||
builder: (c, mq, child) {
|
final mqWidth = mq.item1;
|
||||||
final mqWidth = mq.item1;
|
final mqViewInsets = mq.item2;
|
||||||
final mqViewInsets = mq.item2;
|
final mqViewPadding = mq.item3;
|
||||||
final mqViewPadding = mq.item3;
|
|
||||||
|
|
||||||
final viewInsets = widget.viewInsets ?? mqViewInsets;
|
final viewInsets = widget.viewInsets ?? mqViewInsets;
|
||||||
final viewPadding = widget.viewPadding ?? mqViewPadding;
|
final viewPadding = widget.viewPadding ?? mqViewPadding;
|
||||||
final overlayContentMaxWidth = mqWidth - viewPadding.horizontal - innerPadding.horizontal;
|
final availableWidth = mqWidth - viewPadding.horizontal;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
color: kOverlayBackgroundColor,
|
color: kOverlayBackgroundColor,
|
||||||
padding: viewInsets + viewPadding.copyWith(top: 0),
|
padding: viewInsets + viewPadding.copyWith(top: 0),
|
||||||
child: FutureBuilder<OverlayMetadata>(
|
child: FutureBuilder<OverlayMetadata>(
|
||||||
future: _detailLoader,
|
future: _detailLoader,
|
||||||
builder: (futureContext, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) {
|
if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) {
|
||||||
_lastDetails = snapshot.data;
|
_lastDetails = snapshot.data;
|
||||||
_lastEntry = entry;
|
_lastEntry = entry;
|
||||||
}
|
}
|
||||||
return _lastEntry == null
|
return _lastEntry == null
|
||||||
? SizedBox.shrink()
|
? SizedBox.shrink()
|
||||||
: Padding(
|
: _BottomOverlayContent(
|
||||||
// keep padding inside `FutureBuilder` so that overlay takes no space until data is ready
|
entry: _lastEntry,
|
||||||
padding: innerPadding,
|
details: _lastDetails,
|
||||||
child: _FullscreenBottomOverlayContent(
|
position: widget.showPosition ? '${widget.index + 1}/${widget.entries.length}' : null,
|
||||||
entry: _lastEntry,
|
availableWidth: availableWidth,
|
||||||
details: _lastDetails,
|
multiPageController: widget.multiPageController,
|
||||||
position: widget.showPosition ? '${widget.index + 1}/${widget.entries.length}' : null,
|
);
|
||||||
maxWidth: overlayContentMaxWidth,
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -118,22 +116,28 @@ const double _iconSize = 16.0;
|
||||||
const double _interRowPadding = 2.0;
|
const double _interRowPadding = 2.0;
|
||||||
const double _subRowMinWidth = 300.0;
|
const double _subRowMinWidth = 300.0;
|
||||||
|
|
||||||
class _FullscreenBottomOverlayContent extends AnimatedWidget {
|
class _BottomOverlayContent extends AnimatedWidget {
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
final OverlayMetadata details;
|
final OverlayMetadata details;
|
||||||
final String position;
|
final String position;
|
||||||
final double maxWidth;
|
final double availableWidth;
|
||||||
|
final MultiPageController multiPageController;
|
||||||
|
|
||||||
_FullscreenBottomOverlayContent({
|
static const infoPadding = EdgeInsets.symmetric(vertical: 4, horizontal: 8);
|
||||||
|
|
||||||
|
_BottomOverlayContent({
|
||||||
Key key,
|
Key key,
|
||||||
this.entry,
|
this.entry,
|
||||||
this.details,
|
this.details,
|
||||||
this.position,
|
this.position,
|
||||||
this.maxWidth,
|
this.availableWidth,
|
||||||
|
this.multiPageController,
|
||||||
}) : super(key: key, listenable: entry.metadataChangeNotifier);
|
}) : super(key: key, listenable: entry.metadataChangeNotifier);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final infoMaxWidth = availableWidth - infoPadding.horizontal;
|
||||||
|
|
||||||
return DefaultTextStyle(
|
return DefaultTextStyle(
|
||||||
style: Theme.of(context).textTheme.bodyText2.copyWith(
|
style: Theme.of(context).textTheme.bodyText2.copyWith(
|
||||||
shadows: [Constants.embossShadow],
|
shadows: [Constants.embossShadow],
|
||||||
|
@ -142,43 +146,69 @@ class _FullscreenBottomOverlayContent extends AnimatedWidget {
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: maxWidth,
|
width: availableWidth,
|
||||||
child: Selector<MediaQueryData, Orientation>(
|
child: Selector<MediaQueryData, Orientation>(
|
||||||
selector: (c, mq) => mq.orientation,
|
selector: (c, mq) => mq.orientation,
|
||||||
builder: (c, orientation, child) {
|
builder: (c, orientation, child) {
|
||||||
final twoColumns = orientation == Orientation.landscape && maxWidth / 2 > _subRowMinWidth;
|
final twoColumns = orientation == Orientation.landscape && infoMaxWidth / 2 > _subRowMinWidth;
|
||||||
final subRowWidth = twoColumns ? min(_subRowMinWidth, maxWidth / 2) : maxWidth;
|
final subRowWidth = twoColumns ? min(_subRowMinWidth, infoMaxWidth / 2) : infoMaxWidth;
|
||||||
final positionTitle = [
|
final positionTitle = _PositionTitleRow(entry: entry, collectionPosition: position, multiPageController: multiPageController);
|
||||||
if (position != null) position,
|
|
||||||
if (entry.bestTitle != null) entry.bestTitle,
|
|
||||||
].join(' • ');
|
|
||||||
final hasShootingDetails = details != null && !details.isEmpty && settings.showOverlayShootingDetails;
|
final hasShootingDetails = details != null && !details.isEmpty && settings.showOverlayShootingDetails;
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
Widget infoColumn = Padding(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
padding: infoPadding,
|
||||||
children: [
|
child: Column(
|
||||||
if (positionTitle.isNotEmpty) Text(positionTitle, strutStyle: Constants.overflowStrutStyle),
|
mainAxisSize: MainAxisSize.min,
|
||||||
_buildSoloLocationRow(),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
if (twoColumns)
|
children: [
|
||||||
Padding(
|
if (positionTitle.isNotEmpty) positionTitle,
|
||||||
padding: EdgeInsets.only(top: _interRowPadding),
|
_buildSoloLocationRow(),
|
||||||
child: Row(
|
if (twoColumns)
|
||||||
children: [
|
Padding(
|
||||||
Container(width: subRowWidth, child: _DateRow(entry)),
|
padding: EdgeInsets.only(top: _interRowPadding),
|
||||||
_buildDuoShootingRow(subRowWidth, hasShootingDetails),
|
child: Row(
|
||||||
],
|
children: [
|
||||||
|
Container(
|
||||||
|
width: subRowWidth,
|
||||||
|
child: _DateRow(
|
||||||
|
entry: entry,
|
||||||
|
multiPageController: multiPageController,
|
||||||
|
)),
|
||||||
|
_buildDuoShootingRow(subRowWidth, hasShootingDetails),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else ...[
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.only(top: _interRowPadding),
|
||||||
|
width: subRowWidth,
|
||||||
|
child: _DateRow(
|
||||||
|
entry: entry,
|
||||||
|
multiPageController: multiPageController,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
_buildSoloShootingRow(subRowWidth, hasShootingDetails),
|
||||||
else ...[
|
],
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.only(top: _interRowPadding),
|
|
||||||
width: subRowWidth,
|
|
||||||
child: _DateRow(entry),
|
|
||||||
),
|
|
||||||
_buildSoloShootingRow(subRowWidth, hasShootingDetails),
|
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (multiPageController != null) {
|
||||||
|
infoColumn = Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
MultiPageOverlay(
|
||||||
|
entry: entry,
|
||||||
|
controller: multiPageController,
|
||||||
|
availableWidth: availableWidth,
|
||||||
|
),
|
||||||
|
infoColumn,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return infoColumn;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -186,7 +216,7 @@ class _FullscreenBottomOverlayContent extends AnimatedWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSoloLocationRow() => AnimatedSwitcher(
|
Widget _buildSoloLocationRow() => AnimatedSwitcher(
|
||||||
duration: Durations.fullscreenOverlayChangeAnimation,
|
duration: Durations.viewerOverlayChangeAnimation,
|
||||||
switchInCurve: Curves.easeInOutCubic,
|
switchInCurve: Curves.easeInOutCubic,
|
||||||
switchOutCurve: Curves.easeInOutCubic,
|
switchOutCurve: Curves.easeInOutCubic,
|
||||||
transitionBuilder: _soloTransition,
|
transitionBuilder: _soloTransition,
|
||||||
|
@ -199,7 +229,7 @@ class _FullscreenBottomOverlayContent extends AnimatedWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSoloShootingRow(double subRowWidth, bool hasShootingDetails) => AnimatedSwitcher(
|
Widget _buildSoloShootingRow(double subRowWidth, bool hasShootingDetails) => AnimatedSwitcher(
|
||||||
duration: Durations.fullscreenOverlayChangeAnimation,
|
duration: Durations.viewerOverlayChangeAnimation,
|
||||||
switchInCurve: Curves.easeInOutCubic,
|
switchInCurve: Curves.easeInOutCubic,
|
||||||
switchOutCurve: Curves.easeInOutCubic,
|
switchOutCurve: Curves.easeInOutCubic,
|
||||||
transitionBuilder: _soloTransition,
|
transitionBuilder: _soloTransition,
|
||||||
|
@ -213,7 +243,7 @@ class _FullscreenBottomOverlayContent extends AnimatedWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildDuoShootingRow(double subRowWidth, bool hasShootingDetails) => AnimatedSwitcher(
|
Widget _buildDuoShootingRow(double subRowWidth, bool hasShootingDetails) => AnimatedSwitcher(
|
||||||
duration: Durations.fullscreenOverlayChangeAnimation,
|
duration: Durations.viewerOverlayChangeAnimation,
|
||||||
switchInCurve: Curves.easeInOutCubic,
|
switchInCurve: Curves.easeInOutCubic,
|
||||||
switchOutCurve: Curves.easeInOutCubic,
|
switchOutCurve: Curves.easeInOutCubic,
|
||||||
transitionBuilder: (child, animation) => FadeTransition(
|
transitionBuilder: (child, animation) => FadeTransition(
|
||||||
|
@ -264,21 +294,96 @@ class _LocationRow extends AnimatedWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _PositionTitleRow extends StatelessWidget {
|
||||||
|
final ImageEntry entry;
|
||||||
|
final String collectionPosition;
|
||||||
|
final MultiPageController multiPageController;
|
||||||
|
|
||||||
|
const _PositionTitleRow({
|
||||||
|
@required this.entry,
|
||||||
|
@required this.collectionPosition,
|
||||||
|
@required this.multiPageController,
|
||||||
|
});
|
||||||
|
|
||||||
|
String get title => entry.bestTitle;
|
||||||
|
|
||||||
|
bool get isNotEmpty => collectionPosition != null || multiPageController != null || title != null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Text toText({String pagePosition}) => Text(
|
||||||
|
[
|
||||||
|
if (collectionPosition != null) collectionPosition,
|
||||||
|
if (pagePosition != null) pagePosition,
|
||||||
|
if (title != null) title,
|
||||||
|
].join(' • '),
|
||||||
|
strutStyle: Constants.overflowStrutStyle);
|
||||||
|
|
||||||
|
if (multiPageController == null) return toText();
|
||||||
|
|
||||||
|
return FutureBuilder<MultiPageInfo>(
|
||||||
|
future: multiPageController.info,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final multiPageInfo = snapshot.data;
|
||||||
|
final pageCount = multiPageInfo?.pageCount ?? '?';
|
||||||
|
return ValueListenableBuilder<int>(
|
||||||
|
valueListenable: multiPageController.pageNotifier,
|
||||||
|
builder: (context, page, child) {
|
||||||
|
return toText(pagePosition: '${page + 1}/$pageCount');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _DateRow extends StatelessWidget {
|
class _DateRow extends StatelessWidget {
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
|
final MultiPageController multiPageController;
|
||||||
|
|
||||||
const _DateRow(this.entry);
|
const _DateRow({
|
||||||
|
@required this.entry,
|
||||||
|
@required this.multiPageController,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final date = entry.bestDate;
|
final date = entry.bestDate;
|
||||||
final dateText = date != null ? '${DateFormat.yMMMd().format(date)} • ${DateFormat.Hm().format(date)}' : Constants.overlayUnknown;
|
final dateText = date != null ? '${DateFormat.yMMMd().format(date)} • ${DateFormat.Hm().format(date)}' : Constants.overlayUnknown;
|
||||||
|
|
||||||
|
Text toText({MultiPageInfo multiPageInfo, int page}) => Text(
|
||||||
|
entry.isSvg
|
||||||
|
? entry.aspectRatioText
|
||||||
|
: entry.getResolutionText(
|
||||||
|
multiPageInfo: multiPageInfo,
|
||||||
|
page: page,
|
||||||
|
),
|
||||||
|
strutStyle: Constants.overflowStrutStyle,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget resolutionText;
|
||||||
|
if (multiPageController != null) {
|
||||||
|
resolutionText = FutureBuilder<MultiPageInfo>(
|
||||||
|
future: multiPageController.info,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final multiPageInfo = snapshot.data;
|
||||||
|
return ValueListenableBuilder<int>(
|
||||||
|
valueListenable: multiPageController.pageNotifier,
|
||||||
|
builder: (context, page, child) {
|
||||||
|
return toText(multiPageInfo: multiPageInfo, page: page);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
resolutionText = toText();
|
||||||
|
}
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
DecoratedIcon(AIcons.date, shadows: [Constants.embossShadow], size: _iconSize),
|
DecoratedIcon(AIcons.date, shadows: [Constants.embossShadow], size: _iconSize),
|
||||||
SizedBox(width: _iconPadding),
|
SizedBox(width: _iconPadding),
|
||||||
Expanded(flex: 3, child: Text(dateText, strutStyle: Constants.overflowStrutStyle)),
|
Expanded(flex: 3, child: Text(dateText, strutStyle: Constants.overflowStrutStyle)),
|
||||||
Expanded(flex: 2, child: Text(entry.isSvg ? entry.aspectRatioText : entry.resolutionText, strutStyle: Constants.overflowStrutStyle)),
|
Expanded(flex: 2, child: resolutionText),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:aves/model/multipage.dart';
|
||||||
import 'package:aves/widgets/fullscreen/image_view.dart';
|
import 'package:aves/widgets/fullscreen/image_view.dart';
|
||||||
|
import 'package:aves/widgets/fullscreen/multipage_controller.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class Minimap extends StatelessWidget {
|
class Minimap extends StatelessWidget {
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
final ValueNotifier<ViewState> viewStateNotifier;
|
final ValueNotifier<ViewState> viewStateNotifier;
|
||||||
|
final MultiPageController multiPageController;
|
||||||
final Size size;
|
final Size size;
|
||||||
|
|
||||||
static const defaultSize = Size(96, 96);
|
static const defaultSize = Size(96, 96);
|
||||||
|
@ -15,30 +18,48 @@ class Minimap extends StatelessWidget {
|
||||||
const Minimap({
|
const Minimap({
|
||||||
@required this.entry,
|
@required this.entry,
|
||||||
@required this.viewStateNotifier,
|
@required this.viewStateNotifier,
|
||||||
|
@required this.multiPageController,
|
||||||
this.size = defaultSize,
|
this.size = defaultSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
child: ValueListenableBuilder<ViewState>(
|
child: multiPageController != null
|
||||||
valueListenable: viewStateNotifier,
|
? FutureBuilder<MultiPageInfo>(
|
||||||
builder: (context, viewState, child) {
|
future: multiPageController.info,
|
||||||
final viewportSize = viewState.viewportSize;
|
builder: (context, snapshot) {
|
||||||
if (viewportSize == null) return SizedBox.shrink();
|
final multiPageInfo = snapshot.data;
|
||||||
return CustomPaint(
|
if (multiPageInfo == null) return SizedBox.shrink();
|
||||||
painter: MinimapPainter(
|
return ValueListenableBuilder<int>(
|
||||||
viewportSize: viewportSize,
|
valueListenable: multiPageController.pageNotifier,
|
||||||
entrySize: entry.displaySize,
|
builder: (context, page, child) {
|
||||||
viewCenterOffset: viewState.position,
|
return _buildForEntrySize(entry.getDisplaySize(multiPageInfo: multiPageInfo, page: page));
|
||||||
viewScale: viewState.scale,
|
},
|
||||||
minimapBorderColor: Colors.white30,
|
);
|
||||||
),
|
})
|
||||||
size: size,
|
: _buildForEntrySize(entry.getDisplaySize()),
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildForEntrySize(Size entrySize) {
|
||||||
|
return ValueListenableBuilder<ViewState>(
|
||||||
|
valueListenable: viewStateNotifier,
|
||||||
|
builder: (context, viewState, child) {
|
||||||
|
final viewportSize = viewState.viewportSize;
|
||||||
|
if (viewportSize == null) return SizedBox.shrink();
|
||||||
|
return CustomPaint(
|
||||||
|
painter: MinimapPainter(
|
||||||
|
viewportSize: viewportSize,
|
||||||
|
entrySize: entrySize,
|
||||||
|
viewCenterOffset: viewState.position,
|
||||||
|
viewScale: viewState.scale,
|
||||||
|
minimapBorderColor: Colors.white30,
|
||||||
|
),
|
||||||
|
size: size,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MinimapPainter extends CustomPainter {
|
class MinimapPainter extends CustomPainter {
|
||||||
|
|
177
lib/widgets/fullscreen/overlay/multipage.dart
Normal file
177
lib/widgets/fullscreen/overlay/multipage.dart
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:aves/model/multipage.dart';
|
||||||
|
import 'package:aves/theme/durations.dart';
|
||||||
|
import 'package:aves/widgets/collection/thumbnail/raster.dart';
|
||||||
|
import 'package:aves/widgets/fullscreen/multipage_controller.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class MultiPageOverlay extends StatefulWidget {
|
||||||
|
final ImageEntry entry;
|
||||||
|
final MultiPageController controller;
|
||||||
|
final double availableWidth;
|
||||||
|
|
||||||
|
const MultiPageOverlay({
|
||||||
|
Key key,
|
||||||
|
@required this.entry,
|
||||||
|
@required this.controller,
|
||||||
|
@required this.availableWidth,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_MultiPageOverlayState createState() => _MultiPageOverlayState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MultiPageOverlayState extends State<MultiPageOverlay> {
|
||||||
|
ScrollController _scrollController;
|
||||||
|
bool _syncScroll = true;
|
||||||
|
|
||||||
|
static const double extent = 48;
|
||||||
|
static const double separatorWidth = 2;
|
||||||
|
|
||||||
|
ImageEntry get entry => widget.entry;
|
||||||
|
|
||||||
|
MultiPageController get controller => widget.controller;
|
||||||
|
|
||||||
|
double get availableWidth => widget.availableWidth;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_registerWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant MultiPageOverlay oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
|
||||||
|
if (oldWidget.controller != controller) {
|
||||||
|
_unregisterWidget();
|
||||||
|
_registerWidget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_unregisterWidget();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _registerWidget() {
|
||||||
|
final scrollOffset = pageToScrollOffset(controller.page);
|
||||||
|
_scrollController = ScrollController(initialScrollOffset: scrollOffset);
|
||||||
|
_scrollController.addListener(_onScrollChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _unregisterWidget() {
|
||||||
|
_scrollController.removeListener(_onScrollChange);
|
||||||
|
_scrollController.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final marginWidth = max(0, (availableWidth - extent) / 2 - separatorWidth);
|
||||||
|
final horizontalMargin = SizedBox(width: marginWidth);
|
||||||
|
final separator = SizedBox(width: separatorWidth);
|
||||||
|
final shade = IgnorePointer(
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black38,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return FutureBuilder<MultiPageInfo>(
|
||||||
|
future: controller.info,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final multiPageInfo = snapshot.data;
|
||||||
|
if ((multiPageInfo?.pageCount ?? 0) <= 1) return SizedBox.shrink();
|
||||||
|
return Container(
|
||||||
|
height: extent + separatorWidth * 2,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
top: separatorWidth,
|
||||||
|
width: availableWidth,
|
||||||
|
height: extent,
|
||||||
|
child: ListView.separated(
|
||||||
|
key: ValueKey(entry),
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
controller: _scrollController,
|
||||||
|
// default padding in scroll direction matches `MediaQuery.viewPadding`,
|
||||||
|
// but we already accommodate for it, so make sure horizontal padding is 0
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (index == 0 || index == multiPageInfo.pageCount + 1) return horizontalMargin;
|
||||||
|
final page = index - 1;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
_syncScroll = false;
|
||||||
|
controller.page = page;
|
||||||
|
await _scrollController.animateTo(
|
||||||
|
pageToScrollOffset(page),
|
||||||
|
duration: Durations.viewerOverlayPageChooserAnimation,
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
);
|
||||||
|
_syncScroll = true;
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: extent,
|
||||||
|
height: extent,
|
||||||
|
child: ThumbnailRasterImage(
|
||||||
|
entry: entry,
|
||||||
|
extent: extent,
|
||||||
|
page: page,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (context, index) => separator,
|
||||||
|
itemCount: multiPageInfo.pageCount + 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
top: separatorWidth,
|
||||||
|
width: marginWidth + separatorWidth,
|
||||||
|
height: extent,
|
||||||
|
child: shade,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: separatorWidth,
|
||||||
|
right: 0,
|
||||||
|
width: marginWidth + separatorWidth,
|
||||||
|
height: extent,
|
||||||
|
child: shade,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
width: availableWidth,
|
||||||
|
height: separatorWidth,
|
||||||
|
child: shade,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
width: availableWidth,
|
||||||
|
height: separatorWidth,
|
||||||
|
child: shade,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onScrollChange() {
|
||||||
|
if (_syncScroll) {
|
||||||
|
controller.page = scrollOffsetToPage(_scrollController.offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double pageToScrollOffset(int page) => page * (extent + separatorWidth);
|
||||||
|
|
||||||
|
int scrollOffsetToPage(double offset) => (offset / (extent + separatorWidth)).round();
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/common/basic/menu_row.dart';
|
import 'package:aves/widgets/common/basic/menu_row.dart';
|
||||||
import 'package:aves/widgets/common/fx/sweeper.dart';
|
import 'package:aves/widgets/common/fx/sweeper.dart';
|
||||||
import 'package:aves/widgets/fullscreen/image_view.dart';
|
import 'package:aves/widgets/fullscreen/image_view.dart';
|
||||||
|
import 'package:aves/widgets/fullscreen/multipage_controller.dart';
|
||||||
import 'package:aves/widgets/fullscreen/overlay/common.dart';
|
import 'package:aves/widgets/fullscreen/overlay/common.dart';
|
||||||
import 'package:aves/widgets/fullscreen/overlay/minimap.dart';
|
import 'package:aves/widgets/fullscreen/overlay/minimap.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
@ -24,6 +25,7 @@ class FullscreenTopOverlay extends StatelessWidget {
|
||||||
final Function(EntryAction value) onActionSelected;
|
final Function(EntryAction value) onActionSelected;
|
||||||
final bool canToggleFavourite;
|
final bool canToggleFavourite;
|
||||||
final ValueNotifier<ViewState> viewStateNotifier;
|
final ValueNotifier<ViewState> viewStateNotifier;
|
||||||
|
final MultiPageController multiPageController;
|
||||||
|
|
||||||
static const double padding = 8;
|
static const double padding = 8;
|
||||||
|
|
||||||
|
@ -39,7 +41,8 @@ class FullscreenTopOverlay extends StatelessWidget {
|
||||||
@required this.viewInsets,
|
@required this.viewInsets,
|
||||||
@required this.viewPadding,
|
@required this.viewPadding,
|
||||||
@required this.onActionSelected,
|
@required this.onActionSelected,
|
||||||
this.viewStateNotifier,
|
@required this.viewStateNotifier,
|
||||||
|
@required this.multiPageController,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -85,6 +88,7 @@ class FullscreenTopOverlay extends StatelessWidget {
|
||||||
child: Minimap(
|
child: Minimap(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
viewStateNotifier: viewStateNotifier,
|
viewStateNotifier: viewStateNotifier,
|
||||||
|
multiPageController: multiPageController,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -320,7 +324,7 @@ class _FavouriteTogglerState extends State<_FavouriteToggler> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(_FavouriteToggler oldWidget) {
|
void didUpdateWidget(covariant _FavouriteToggler oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
_onChanged();
|
_onChanged();
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(VideoControlOverlay oldWidget) {
|
void didUpdateWidget(covariant VideoControlOverlay oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
_unregisterWidget(oldWidget);
|
_unregisterWidget(oldWidget);
|
||||||
_registerWidget(widget);
|
_registerWidget(widget);
|
||||||
|
|
|
@ -8,7 +8,12 @@ class PanoramaPage extends StatelessWidget {
|
||||||
|
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
|
|
||||||
const PanoramaPage({@required this.entry});
|
final int page;
|
||||||
|
|
||||||
|
const PanoramaPage({
|
||||||
|
@required this.entry,
|
||||||
|
this.page = 0,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -18,6 +23,7 @@ class PanoramaPage extends StatelessWidget {
|
||||||
image: UriImage(
|
image: UriImage(
|
||||||
uri: entry.uri,
|
uri: entry.uri,
|
||||||
mimeType: entry.mimeType,
|
mimeType: entry.mimeType,
|
||||||
|
page: page,
|
||||||
rotationDegrees: entry.rotationDegrees,
|
rotationDegrees: entry.rotationDegrees,
|
||||||
isFlipped: entry.isFlipped,
|
isFlipped: entry.isFlipped,
|
||||||
expectedContentLength: entry.sizeBytes,
|
expectedContentLength: entry.sizeBytes,
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:aves/image_providers/region_provider.dart';
|
||||||
import 'package:aves/image_providers/thumbnail_provider.dart';
|
import 'package:aves/image_providers/thumbnail_provider.dart';
|
||||||
import 'package:aves/image_providers/uri_image_provider.dart';
|
import 'package:aves/image_providers/uri_image_provider.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:aves/model/multipage.dart';
|
||||||
import 'package:aves/model/settings/entry_background.dart';
|
import 'package:aves/model/settings/entry_background.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/utils/math_utils.dart';
|
import 'package:aves/utils/math_utils.dart';
|
||||||
|
@ -15,11 +16,15 @@ import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
class TiledImageView extends StatefulWidget {
|
class TiledImageView extends StatefulWidget {
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
|
final MultiPageInfo multiPageInfo;
|
||||||
|
final int page;
|
||||||
final ValueNotifier<ViewState> viewStateNotifier;
|
final ValueNotifier<ViewState> viewStateNotifier;
|
||||||
final ImageErrorWidgetBuilder errorBuilder;
|
final ImageErrorWidgetBuilder errorBuilder;
|
||||||
|
|
||||||
const TiledImageView({
|
const TiledImageView({
|
||||||
@required this.entry,
|
@required this.entry,
|
||||||
|
this.multiPageInfo,
|
||||||
|
this.page = 0,
|
||||||
@required this.viewStateNotifier,
|
@required this.viewStateNotifier,
|
||||||
@required this.errorBuilder,
|
@required this.errorBuilder,
|
||||||
});
|
});
|
||||||
|
@ -29,6 +34,7 @@ class TiledImageView extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TiledImageViewState extends State<TiledImageView> {
|
class _TiledImageViewState extends State<TiledImageView> {
|
||||||
|
Size _displaySize;
|
||||||
bool _isTilingInitialized = false;
|
bool _isTilingInitialized = false;
|
||||||
int _maxSampleSize;
|
int _maxSampleSize;
|
||||||
double _tileSide;
|
double _tileSide;
|
||||||
|
@ -39,19 +45,21 @@ class _TiledImageViewState extends State<TiledImageView> {
|
||||||
|
|
||||||
ImageEntry get entry => widget.entry;
|
ImageEntry get entry => widget.entry;
|
||||||
|
|
||||||
|
int get page => widget.page;
|
||||||
|
|
||||||
ValueNotifier<ViewState> get viewStateNotifier => widget.viewStateNotifier;
|
ValueNotifier<ViewState> get viewStateNotifier => widget.viewStateNotifier;
|
||||||
|
|
||||||
bool get useBackground => entry.canHaveAlpha && settings.rasterBackground != EntryBackground.transparent;
|
bool get useBackground => entry.canHaveAlpha && settings.rasterBackground != EntryBackground.transparent;
|
||||||
|
|
||||||
bool get useTiles => entry.canTile && (entry.width > 4096 || entry.height > 4096);
|
bool get useTiles => entry.canTile && (entry.width > 4096 || entry.height > 4096);
|
||||||
|
|
||||||
ImageProvider get thumbnailProvider => ThumbnailProvider(ThumbnailProviderKey.fromEntry(entry));
|
ImageProvider get thumbnailProvider => ThumbnailProvider(ThumbnailProviderKey.fromEntry(entry, page: page));
|
||||||
|
|
||||||
ImageProvider get fullImageProvider {
|
ImageProvider get fullImageProvider {
|
||||||
if (useTiles) {
|
if (useTiles) {
|
||||||
assert(_isTilingInitialized);
|
assert(_isTilingInitialized);
|
||||||
final displayWidth = entry.displaySize.width.round();
|
final displayWidth = _displaySize.width.round();
|
||||||
final displayHeight = entry.displaySize.height.round();
|
final displayHeight = _displaySize.height.round();
|
||||||
final viewState = viewStateNotifier.value;
|
final viewState = viewStateNotifier.value;
|
||||||
final regionRect = _getTileRects(
|
final regionRect = _getTileRects(
|
||||||
x: 0,
|
x: 0,
|
||||||
|
@ -62,9 +70,10 @@ class _TiledImageViewState extends State<TiledImageView> {
|
||||||
displayHeight: displayHeight,
|
displayHeight: displayHeight,
|
||||||
scale: viewState.scale,
|
scale: viewState.scale,
|
||||||
viewRect: _getViewRect(viewState, displayWidth, displayHeight),
|
viewRect: _getViewRect(viewState, displayWidth, displayHeight),
|
||||||
).item2;
|
)?.item2;
|
||||||
return RegionProvider(RegionProviderKey.fromEntry(
|
return RegionProvider(RegionProviderKey.fromEntry(
|
||||||
entry,
|
entry,
|
||||||
|
page: page,
|
||||||
sampleSize: _maxSampleSize,
|
sampleSize: _maxSampleSize,
|
||||||
rect: regionRect,
|
rect: regionRect,
|
||||||
));
|
));
|
||||||
|
@ -72,6 +81,7 @@ class _TiledImageViewState extends State<TiledImageView> {
|
||||||
return UriImage(
|
return UriImage(
|
||||||
uri: entry.uri,
|
uri: entry.uri,
|
||||||
mimeType: entry.mimeType,
|
mimeType: entry.mimeType,
|
||||||
|
page: page,
|
||||||
rotationDegrees: entry.rotationDegrees,
|
rotationDegrees: entry.rotationDegrees,
|
||||||
isFlipped: entry.isFlipped,
|
isFlipped: entry.isFlipped,
|
||||||
expectedContentLength: entry.sizeBytes,
|
expectedContentLength: entry.sizeBytes,
|
||||||
|
@ -85,17 +95,18 @@ class _TiledImageViewState extends State<TiledImageView> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_displaySize = entry.getDisplaySize(multiPageInfo: widget.multiPageInfo, page: page);
|
||||||
_fullImageListener = ImageStreamListener(_onFullImageCompleted);
|
_fullImageListener = ImageStreamListener(_onFullImageCompleted);
|
||||||
if (!useTiles) _registerFullImage();
|
if (!useTiles) _registerFullImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(TiledImageView oldWidget) {
|
void didUpdateWidget(covariant TiledImageView oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
|
|
||||||
final oldViewState = oldWidget.viewStateNotifier.value;
|
final oldViewState = oldWidget.viewStateNotifier.value;
|
||||||
final viewState = widget.viewStateNotifier.value;
|
final viewState = widget.viewStateNotifier.value;
|
||||||
if (oldWidget.entry != widget.entry || oldViewState.viewportSize != viewState.viewportSize) {
|
if (oldWidget.entry != widget.entry || oldViewState.viewportSize != viewState.viewportSize || oldWidget.page != page) {
|
||||||
_isTilingInitialized = false;
|
_isTilingInitialized = false;
|
||||||
_fullImageLoaded.value = false;
|
_fullImageLoaded.value = false;
|
||||||
_unregisterFullImage();
|
_unregisterFullImage();
|
||||||
|
@ -135,7 +146,7 @@ class _TiledImageViewState extends State<TiledImageView> {
|
||||||
if (viewportSized && useTiles && !_isTilingInitialized) _initTiling(viewportSize);
|
if (viewportSized && useTiles && !_isTilingInitialized) _initTiling(viewportSize);
|
||||||
|
|
||||||
return SizedBox.fromSize(
|
return SizedBox.fromSize(
|
||||||
size: entry.displaySize * viewState.scale,
|
size: _displaySize * viewState.scale,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
@ -147,7 +158,7 @@ class _TiledImageViewState extends State<TiledImageView> {
|
||||||
image: fullImageProvider,
|
image: fullImageProvider,
|
||||||
gaplessPlayback: true,
|
gaplessPlayback: true,
|
||||||
errorBuilder: widget.errorBuilder,
|
errorBuilder: widget.errorBuilder,
|
||||||
width: (entry.displaySize * viewState.scale).width,
|
width: (_displaySize * viewState.scale).width,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
filterQuality: FilterQuality.medium,
|
filterQuality: FilterQuality.medium,
|
||||||
),
|
),
|
||||||
|
@ -159,10 +170,9 @@ class _TiledImageViewState extends State<TiledImageView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initTiling(Size viewportSize) {
|
void _initTiling(Size viewportSize) {
|
||||||
final displaySize = entry.displaySize;
|
|
||||||
_tileSide = viewportSize.shortestSide * scaleFactor;
|
_tileSide = viewportSize.shortestSide * scaleFactor;
|
||||||
// scale for initial state `contained`
|
// scale for initial state `contained`
|
||||||
final containedScale = min(viewportSize.width / displaySize.width, viewportSize.height / displaySize.height);
|
final containedScale = min(viewportSize.width / _displaySize.width, viewportSize.height / _displaySize.height);
|
||||||
_maxSampleSize = _sampleSizeForScale(containedScale);
|
_maxSampleSize = _sampleSizeForScale(containedScale);
|
||||||
|
|
||||||
final rotationDegrees = entry.rotationDegrees;
|
final rotationDegrees = entry.rotationDegrees;
|
||||||
|
@ -173,7 +183,7 @@ class _TiledImageViewState extends State<TiledImageView> {
|
||||||
..translate(entry.width / 2.0, entry.height / 2.0)
|
..translate(entry.width / 2.0, entry.height / 2.0)
|
||||||
..scale(isFlipped ? -1.0 : 1.0, 1.0, 1.0)
|
..scale(isFlipped ? -1.0 : 1.0, 1.0, 1.0)
|
||||||
..rotateZ(-toRadians(rotationDegrees.toDouble()))
|
..rotateZ(-toRadians(rotationDegrees.toDouble()))
|
||||||
..translate(-displaySize.width / 2.0, -displaySize.height / 2.0);
|
..translate(-_displaySize.width / 2.0, -_displaySize.height / 2.0);
|
||||||
}
|
}
|
||||||
_isTilingInitialized = true;
|
_isTilingInitialized = true;
|
||||||
_registerFullImage();
|
_registerFullImage();
|
||||||
|
@ -203,7 +213,7 @@ class _TiledImageViewState extends State<TiledImageView> {
|
||||||
final viewportSize = viewState.viewportSize;
|
final viewportSize = viewState.viewportSize;
|
||||||
assert(viewportSize != null);
|
assert(viewportSize != null);
|
||||||
|
|
||||||
final viewSize = entry.displaySize * viewState.scale;
|
final viewSize = _displaySize * viewState.scale;
|
||||||
final decorationOffset = ((viewSize - viewportSize) as Offset) / 2 - viewState.position;
|
final decorationOffset = ((viewSize - viewportSize) as Offset) / 2 - viewState.position;
|
||||||
final decorationSize = applyBoxFit(BoxFit.none, viewSize, viewportSize).source;
|
final decorationSize = applyBoxFit(BoxFit.none, viewSize, viewportSize).source;
|
||||||
|
|
||||||
|
@ -236,8 +246,8 @@ class _TiledImageViewState extends State<TiledImageView> {
|
||||||
List<Widget> _getTiles(ViewState viewState) {
|
List<Widget> _getTiles(ViewState viewState) {
|
||||||
if (!_isTilingInitialized) return [];
|
if (!_isTilingInitialized) return [];
|
||||||
|
|
||||||
final displayWidth = entry.displaySize.width.round();
|
final displayWidth = _displaySize.width.round();
|
||||||
final displayHeight = entry.displaySize.height.round();
|
final displayHeight = _displaySize.height.round();
|
||||||
final viewRect = _getViewRect(viewState, displayWidth, displayHeight);
|
final viewRect = _getViewRect(viewState, displayWidth, displayHeight);
|
||||||
final scale = viewState.scale;
|
final scale = viewState.scale;
|
||||||
|
|
||||||
|
@ -265,6 +275,7 @@ class _TiledImageViewState extends State<TiledImageView> {
|
||||||
if (rects != null) {
|
if (rects != null) {
|
||||||
tiles.add(RegionTile(
|
tiles.add(RegionTile(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
|
page: page,
|
||||||
tileRect: rects.item1,
|
tileRect: rects.item1,
|
||||||
regionRect: rects.item2,
|
regionRect: rects.item2,
|
||||||
sampleSize: sampleSize,
|
sampleSize: sampleSize,
|
||||||
|
@ -333,6 +344,7 @@ class _TiledImageViewState extends State<TiledImageView> {
|
||||||
|
|
||||||
class RegionTile extends StatefulWidget {
|
class RegionTile extends StatefulWidget {
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
|
final int page;
|
||||||
|
|
||||||
// `tileRect` uses Flutter view coordinates
|
// `tileRect` uses Flutter view coordinates
|
||||||
// `regionRect` uses the raw image pixel coordinates
|
// `regionRect` uses the raw image pixel coordinates
|
||||||
|
@ -342,6 +354,7 @@ class RegionTile extends StatefulWidget {
|
||||||
|
|
||||||
const RegionTile({
|
const RegionTile({
|
||||||
@required this.entry,
|
@required this.entry,
|
||||||
|
@required this.page,
|
||||||
@required this.tileRect,
|
@required this.tileRect,
|
||||||
@required this.regionRect,
|
@required this.regionRect,
|
||||||
@required this.sampleSize,
|
@required this.sampleSize,
|
||||||
|
@ -363,7 +376,7 @@ class _RegionTileState extends State<RegionTile> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(RegionTile oldWidget) {
|
void didUpdateWidget(covariant RegionTile oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (oldWidget.entry != widget.entry || oldWidget.tileRect != widget.tileRect || oldWidget.sampleSize != widget.sampleSize || oldWidget.sampleSize != widget.sampleSize) {
|
if (oldWidget.entry != widget.entry || oldWidget.tileRect != widget.tileRect || oldWidget.sampleSize != widget.sampleSize || oldWidget.sampleSize != widget.sampleSize) {
|
||||||
_unregisterWidget(oldWidget);
|
_unregisterWidget(oldWidget);
|
||||||
|
@ -390,6 +403,7 @@ class _RegionTileState extends State<RegionTile> {
|
||||||
|
|
||||||
_provider = RegionProvider(RegionProviderKey.fromEntry(
|
_provider = RegionProvider(RegionProviderKey.fromEntry(
|
||||||
entry,
|
entry,
|
||||||
|
page: widget.page,
|
||||||
sampleSize: widget.sampleSize,
|
sampleSize: widget.sampleSize,
|
||||||
rect: widget.regionRect,
|
rect: widget.regionRect,
|
||||||
));
|
));
|
||||||
|
|
|
@ -34,7 +34,7 @@ class AvesVideoState extends State<AvesVideo> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(AvesVideo oldWidget) {
|
void didUpdateWidget(covariant AvesVideo oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
_unregisterWidget(oldWidget);
|
_unregisterWidget(oldWidget);
|
||||||
_registerWidget(widget);
|
_registerWidget(widget);
|
||||||
|
@ -101,6 +101,7 @@ class AvesVideoState extends State<AvesVideo> {
|
||||||
image: UriImage(
|
image: UriImage(
|
||||||
uri: entry.uri,
|
uri: entry.uri,
|
||||||
mimeType: entry.mimeType,
|
mimeType: entry.mimeType,
|
||||||
|
page: 0,
|
||||||
rotationDegrees: entry.rotationDegrees,
|
rotationDegrees: entry.rotationDegrees,
|
||||||
isFlipped: entry.isFlipped,
|
isFlipped: entry.isFlipped,
|
||||||
expectedContentLength: entry.sizeBytes,
|
expectedContentLength: entry.sizeBytes,
|
||||||
|
|
|
@ -52,7 +52,7 @@ class _SearchPageState extends State<SearchPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(SearchPage oldWidget) {
|
void didUpdateWidget(covariant SearchPage oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (widget.delegate != oldWidget.delegate) {
|
if (widget.delegate != oldWidget.delegate) {
|
||||||
oldWidget.delegate.queryTextController.removeListener(_onQueryChanged);
|
oldWidget.delegate.queryTextController.removeListener(_onQueryChanged);
|
||||||
|
|
Loading…
Reference in a new issue