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