Bump webgpuviewer (#3949)
* Continuous background color * Use ImageDecoder instead of Kim * Only get tag when in dual mode * Update libs.versions.toml * Make progress circle smaller and animate progress * Fix long strip showing the menu at ends
This commit is contained in:
@@ -298,7 +298,6 @@ dependencies {
|
||||
implementation(libs.image.decoder)
|
||||
|
||||
implementation(libs.webgpuviewer)
|
||||
implementation(libs.kim)
|
||||
|
||||
// UI libraries
|
||||
implementation(libs.material)
|
||||
|
||||
@@ -13,10 +13,8 @@ import coil3.decode.Decoder
|
||||
import coil3.decode.ImageSource
|
||||
import coil3.fetch.SourceFetchResult
|
||||
import coil3.request.Options
|
||||
import logcat.LogPriority
|
||||
import okio.BufferedSource
|
||||
import tachiyomi.core.common.util.system.ImageUtil
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
|
||||
/**
|
||||
* A [Decoder] that uses [ImageDecoder] (libvips-based) to decode image formats not supported
|
||||
@@ -37,19 +35,10 @@ class ImageDecoder(private val resources: ImageSource, private val options: Opti
|
||||
}
|
||||
|
||||
override suspend fun decode(): DecodeResult {
|
||||
val decoder = resources.source().use {
|
||||
try {
|
||||
ImageDecoder.new(it.inputStream())
|
||||
} catch (e: ImageDecoder.DecodeException) {
|
||||
logcat(LogPriority.ERROR, e) { "ImageDecoder.new failed: ${e.message}" }
|
||||
null
|
||||
}
|
||||
val res = resources.source().use {
|
||||
ImageDecoder.new(it.inputStream()).use { dec -> dec.decode() }
|
||||
}
|
||||
|
||||
check(decoder != null && decoder.pages > 0) { "Failed to initialize decoder" }
|
||||
|
||||
val res = decoder.decode()
|
||||
|
||||
val srcWidth = res.width
|
||||
val srcHeight = res.height
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ import android.view.InputDevice
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.animate
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastCoerceIn
|
||||
@@ -33,9 +36,6 @@ import ca.mpreg.webgpuviewer.transition.TransitionStackUp
|
||||
import ca.mpreg.webgpuviewer.viewer.ImagePage
|
||||
import ca.mpreg.webgpuviewer.viewer.ImageViewerContinuousState
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import de.stefan_oltmann.kim.Kim
|
||||
import de.stefan_oltmann.kim.android.readMetadata
|
||||
import de.stefan_oltmann.kim.format.tiff.constant.TiffTag
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
@@ -50,6 +50,7 @@ import eu.kanade.tachiyomi.util.system.createReaderThemeContext
|
||||
import eu.kanade.tachiyomi.util.system.readerBackgroundColor
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.cancel
|
||||
@@ -83,7 +84,7 @@ open class WebGpuViewer(
|
||||
@Volatile
|
||||
private var cachedOnBackgroundColor: Int? = null
|
||||
|
||||
private fun readerBackgroundColor(): Int =
|
||||
protected fun readerBackgroundColor(): Int =
|
||||
cachedBackgroundColor ?: activity.baseContext.readerBackgroundColor(config.theme)
|
||||
.also { cachedBackgroundColor = it }
|
||||
|
||||
@@ -93,7 +94,7 @@ open class WebGpuViewer(
|
||||
Color.WHITE,
|
||||
).also { cachedOnBackgroundColor = it }
|
||||
|
||||
private val scope = MainScope()
|
||||
protected val scope = MainScope()
|
||||
|
||||
// Dedicated thread for decode worker to avoid blocking Dispatchers.Default pool
|
||||
private val decodeExecutor = Executors.newSingleThreadExecutor { r ->
|
||||
@@ -147,6 +148,10 @@ open class WebGpuViewer(
|
||||
|
||||
private fun findInCache(key: PageKey): ViewerPage? = pageCache[key]
|
||||
|
||||
protected fun viewerPageFor(imagePage: ImagePage): ViewerPage? = synchronized(lock) {
|
||||
pageCache.values.firstOrNull { it.imagePage === imagePage }
|
||||
}
|
||||
|
||||
/** Check if a page is in the cache by identity. O(1) via key lookup. */
|
||||
private fun pageInCache(page: ViewerPage): Boolean = pageCache[pageKey(page)] === page
|
||||
|
||||
@@ -513,12 +518,47 @@ open class WebGpuViewer(
|
||||
homeScale = 1f
|
||||
}
|
||||
|
||||
var progress: Float = 0f
|
||||
@Volatile
|
||||
private var progressValue: Float = 0f
|
||||
private var progressJob: Job? = null
|
||||
var progress: Float
|
||||
get() = progressValue
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
val target = value.fastCoerceIn(0f, 1f)
|
||||
synchronized(this) {
|
||||
progressJob?.cancel()
|
||||
progressJob = null
|
||||
|
||||
if (destroyed || progressValue == target) return
|
||||
|
||||
val scope = scope ?: run {
|
||||
progressValue = target
|
||||
invalidate()
|
||||
return
|
||||
}
|
||||
|
||||
val start = progressValue
|
||||
progressJob = scope.launch {
|
||||
animate(
|
||||
start,
|
||||
target,
|
||||
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
|
||||
) { current, _ ->
|
||||
progressValue = current
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
super.cleanup()
|
||||
synchronized(this) {
|
||||
progressJob?.cancel()
|
||||
progressJob = null
|
||||
}
|
||||
}
|
||||
|
||||
var foregroundColor: Int = foregroundColor
|
||||
set(value) {
|
||||
field = value
|
||||
@@ -534,9 +574,7 @@ open class WebGpuViewer(
|
||||
val cx = dst.width * (0.5f + scale * x)
|
||||
val cy = dst.height * (0.5f + scale * y)
|
||||
|
||||
// Off this page's own width, not dst's: a spread half would otherwise draw a ring
|
||||
// sized for the whole screen, straight over its partner.
|
||||
val full = width * 0.5f * scale
|
||||
val full = min(width, height) * 0.25f * scale
|
||||
|
||||
circle(cx, cy, full / 2f, 0xAAAAAAAA.toInt())
|
||||
|
||||
@@ -925,6 +963,7 @@ open class WebGpuViewer(
|
||||
}
|
||||
|
||||
(this as? ImageViewerContinuousState)?.let {
|
||||
backgroundColor = readerBackgroundColor()
|
||||
homeScale = config.continuousMinWidth / 100f
|
||||
scale = homeScale
|
||||
minScale = if (config.zoomOutDisabled) 0f else 0.1f
|
||||
@@ -1089,27 +1128,6 @@ open class WebGpuViewer(
|
||||
}
|
||||
}
|
||||
|
||||
// Buffered to read the spread tag, then decoded from the buffer. On the preference,
|
||||
// not isDualPageMode(): WIDE is portrait-off, and a rotate never re-decodes. Never in
|
||||
// continuous, where nothing pairs - that mode reads the stream instead of holding it.
|
||||
val bytes = if (!isContinuous && config.dualPageView != ReaderPreferences.DualPageView.NEVER) {
|
||||
input.readBytes()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// Left untouched for a file that names no side - [spreadPosition] then derives one.
|
||||
if (bytes != null) {
|
||||
val tag = Kim.readMetadata(bytes.inputStream(), bytes.size.toLong())
|
||||
?.findStringValue(TiffTag.TIFF_TAG_PAGE_NAME)
|
||||
page.taggedSpreadPosition = when (tag) {
|
||||
"Left" -> SpreadPosition.LEFT
|
||||
"Right" -> SpreadPosition.RIGHT
|
||||
null -> null
|
||||
else -> SpreadPosition.SINGLE
|
||||
}
|
||||
}
|
||||
|
||||
// The decoder hands the map over unapplied - see ImageDecoder.Gainmap - because how
|
||||
// much of it to use depends on the display, so the viewer applies it.
|
||||
fun ImageDecoder.DecodeResult.gainmapInput(): GainmapInput? = gainmap?.let {
|
||||
@@ -1126,121 +1144,130 @@ open class WebGpuViewer(
|
||||
)
|
||||
}
|
||||
|
||||
val dec = ImageDecoder.new(bytes?.inputStream() ?: input)
|
||||
ImageDecoder.new(input).use { dec ->
|
||||
if (isDualPageMode()) {
|
||||
page.taggedSpreadPosition = when (dec.getTag("PageName")) {
|
||||
"Left" -> SpreadPosition.LEFT
|
||||
"Right" -> SpreadPosition.RIGHT
|
||||
null -> null
|
||||
else -> SpreadPosition.SINGLE
|
||||
}
|
||||
}
|
||||
|
||||
val pageCount = dec.pages
|
||||
val pageCount = dec.pages
|
||||
|
||||
if (pageCount == 0) throw Exception("No frames decoded")
|
||||
if (pageCount == 0) throw Exception("No frames decoded")
|
||||
|
||||
val backgroundColor = if (config.automaticBackground) null else readerBackgroundColor()
|
||||
val backgroundColor = if (config.automaticBackground) null else readerBackgroundColor()
|
||||
|
||||
val firstFrame = dec.decodeNext()
|
||||
val firstFrame = dec.decodeNext()
|
||||
|
||||
val imagePage = if (pageCount == 1) {
|
||||
// Only trim when not animated and not in dual page mode
|
||||
val trimColors = if (config.imageCropBorders && !isDualPageMode()) {
|
||||
listOf(
|
||||
floatArrayOf(1f, 1f, 1f),
|
||||
floatArrayOf(0f, 0f, 0f),
|
||||
val imagePage = if (pageCount == 1) {
|
||||
// Only trim when not animated and not in dual page mode
|
||||
val trimColors = if (config.imageCropBorders && !isDualPageMode()) {
|
||||
listOf(
|
||||
floatArrayOf(1f, 1f, 1f),
|
||||
floatArrayOf(0f, 0f, 0f),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val firstImage = Image(
|
||||
firstFrame.image,
|
||||
firstFrame.width,
|
||||
firstFrame.height,
|
||||
createMipMaps = true,
|
||||
trimColors = trimColors,
|
||||
trimThreshold = 0.15f,
|
||||
backgroundColor = backgroundColor,
|
||||
hdr = firstFrame.isHdr,
|
||||
hdrHeadroom = firstFrame.hdrHeadroom,
|
||||
gainmap = firstFrame.gainmapInput(),
|
||||
)
|
||||
|
||||
ImagePage.ImageSingle(firstImage)
|
||||
} else {
|
||||
null
|
||||
val frames = ArrayList<Pair<Image, Int>>(pageCount)
|
||||
|
||||
// Built frames hold uploaded textures, and ImageSingle owns the only teardown.
|
||||
fun discardFrames() {
|
||||
if (frames.isNotEmpty()) ImagePage.ImageSingle(frames).cleanup()
|
||||
}
|
||||
|
||||
val firstImage = Image(
|
||||
firstFrame.image,
|
||||
firstFrame.width,
|
||||
firstFrame.height,
|
||||
createMipMaps = false,
|
||||
backgroundColor = backgroundColor,
|
||||
hdr = firstFrame.isHdr,
|
||||
hdrHeadroom = firstFrame.hdrHeadroom,
|
||||
gainmap = firstFrame.gainmapInput(),
|
||||
)
|
||||
|
||||
frames.add(Pair(firstImage, firstFrame.duration))
|
||||
|
||||
try {
|
||||
for (i in 1 until pageCount) {
|
||||
// Under lock: a decode this long gives an eviction's cleanup() time to land.
|
||||
val stillWanted = synchronized(lock) {
|
||||
pageInCache(page).also { inCache ->
|
||||
if (inCache) {
|
||||
(page.imagePage as? ProgressPage)?.progress = i.toFloat() / pageCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scrolled past: the frames left are work nothing will draw.
|
||||
if (!stillWanted) {
|
||||
discardFrames()
|
||||
return
|
||||
}
|
||||
|
||||
val frame = dec.decodeNext()
|
||||
val image = Image(
|
||||
frame.image,
|
||||
frame.width,
|
||||
frame.height,
|
||||
createMipMaps = false,
|
||||
backgroundColor = firstImage.backgroundColor,
|
||||
hdr = frame.isHdr,
|
||||
hdrHeadroom = frame.hdrHeadroom,
|
||||
gainmap = frame.gainmapInput(),
|
||||
)
|
||||
frames.add(Pair(image, frame.duration))
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
discardFrames()
|
||||
throw e
|
||||
}
|
||||
|
||||
ImagePage.ImageSingle(frames)
|
||||
}
|
||||
|
||||
val firstImage = Image(
|
||||
firstFrame.image,
|
||||
firstFrame.width,
|
||||
firstFrame.height,
|
||||
createMipMaps = true,
|
||||
trimColors = trimColors,
|
||||
trimThreshold = 0.15f,
|
||||
backgroundColor = backgroundColor,
|
||||
hdr = firstFrame.isHdr,
|
||||
hdrHeadroom = firstFrame.hdrHeadroom,
|
||||
gainmap = firstFrame.gainmapInput(),
|
||||
)
|
||||
|
||||
ImagePage.ImageSingle(firstImage)
|
||||
} else {
|
||||
val frames = ArrayList<Pair<Image, Int>>(pageCount)
|
||||
|
||||
// Built frames hold uploaded textures, and ImageSingle owns the only teardown.
|
||||
fun discardFrames() {
|
||||
if (frames.isNotEmpty()) ImagePage.ImageSingle(frames).cleanup()
|
||||
}
|
||||
|
||||
val firstImage = Image(
|
||||
firstFrame.image,
|
||||
firstFrame.width,
|
||||
firstFrame.height,
|
||||
createMipMaps = false,
|
||||
backgroundColor = backgroundColor,
|
||||
hdr = firstFrame.isHdr,
|
||||
hdrHeadroom = firstFrame.hdrHeadroom,
|
||||
gainmap = firstFrame.gainmapInput(),
|
||||
)
|
||||
|
||||
frames.add(Pair(firstImage, firstFrame.duration))
|
||||
|
||||
try {
|
||||
for (i in 1 until pageCount) {
|
||||
// Under lock: a decode this long gives an eviction's cleanup() time to land.
|
||||
val stillWanted = synchronized(lock) {
|
||||
pageInCache(page).also { inCache ->
|
||||
if (inCache) {
|
||||
(page.imagePage as? ProgressPage)?.progress = i.toFloat() / pageCount
|
||||
synchronized(lock) {
|
||||
if (pageInCache(page) && !page.isDecoded && !page.imagePage.destroyed) {
|
||||
val oldImagePage = page.imagePage
|
||||
page.imagePage = imagePage
|
||||
noteIfLone(page)
|
||||
page.state = PageState.IDLE
|
||||
cleanupImage(oldImagePage)
|
||||
// Fade up from the placeholder's colour, if that placeholder was on screen -
|
||||
// one that decoded out of view has nothing left to fade from.
|
||||
if (oldImagePage.isOnScreen) imagePage.fadeIn()
|
||||
if (!isDualPageMode()) {
|
||||
(page.imagePage as? ImagePage.ImageSingle)?.let {
|
||||
if (!applyWideZoomIfNeeded(it)) {
|
||||
applyFitModeAnchor(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scrolled past: the frames left are work nothing will draw.
|
||||
if (!stillWanted) {
|
||||
discardFrames()
|
||||
return
|
||||
}
|
||||
|
||||
val frame = dec.decodeNext()
|
||||
val image = Image(
|
||||
frame.image,
|
||||
frame.width,
|
||||
frame.height,
|
||||
createMipMaps = false,
|
||||
backgroundColor = firstImage.backgroundColor,
|
||||
hdr = frame.isHdr,
|
||||
hdrHeadroom = frame.hdrHeadroom,
|
||||
gainmap = frame.gainmapInput(),
|
||||
)
|
||||
frames.add(Pair(image, frame.duration))
|
||||
pager.state.invalidate()
|
||||
} else {
|
||||
if (pageInCache(page)) page.state = PageState.IDLE
|
||||
imagePage.cleanup()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
discardFrames()
|
||||
throw e
|
||||
}
|
||||
|
||||
ImagePage.ImageSingle(frames)
|
||||
}
|
||||
|
||||
synchronized(lock) {
|
||||
if (pageInCache(page) && !page.isDecoded && !page.imagePage.destroyed) {
|
||||
val oldImagePage = page.imagePage
|
||||
page.imagePage = imagePage
|
||||
noteIfLone(page)
|
||||
page.state = PageState.IDLE
|
||||
cleanupImage(oldImagePage)
|
||||
// Fade up from the placeholder's colour, if that placeholder was on screen -
|
||||
// one that decoded out of view has nothing left to fade from.
|
||||
if (oldImagePage.isOnScreen) imagePage.fadeIn()
|
||||
if (!isDualPageMode()) {
|
||||
(page.imagePage as? ImagePage.ImageSingle)?.let {
|
||||
if (!applyWideZoomIfNeeded(it)) {
|
||||
applyFitModeAnchor(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
pager.state.invalidate()
|
||||
} else {
|
||||
if (pageInCache(page)) page.state = PageState.IDLE
|
||||
imagePage.cleanup()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1405,13 +1432,17 @@ open class WebGpuViewer(
|
||||
// MainScope, not the state's: that one dispatches inside the frame callback.
|
||||
val settled = page
|
||||
this@WebGpuViewer.scope.launch {
|
||||
activity.hideMenu()
|
||||
progressPage(settled)?.let { activity.onPageSelected(it.page) }
|
||||
if (!isContinuous) {
|
||||
activity.hideMenu()
|
||||
progressPage(settled)?.let { activity.onPageSelected(it.page) }
|
||||
}
|
||||
preloadPages(settled)
|
||||
|
||||
(settled as? ViewerTransitionPage)?.let { transitionPage ->
|
||||
if (transitionPage.prevChapter == null || transitionPage.nextChapter == null) {
|
||||
activity.showMenu()
|
||||
if (!isContinuous) {
|
||||
(settled as? ViewerTransitionPage)?.let { transitionPage ->
|
||||
if (transitionPage.prevChapter == null || transitionPage.nextChapter == null) {
|
||||
activity.showMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+60
-13
@@ -2,9 +2,9 @@ package eu.kanade.tachiyomi.ui.reader.viewer.webgpu
|
||||
|
||||
import ca.mpreg.webgpuviewer.ImageViewContinuous
|
||||
import ca.mpreg.webgpuviewer.viewer.ImagePage
|
||||
import ca.mpreg.webgpuviewer.viewer.ImageViewerContinuousState
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.max
|
||||
|
||||
class WebGpuViewerContinuous(activity: ReaderActivity, val useGap: Boolean = false) :
|
||||
@@ -15,26 +15,71 @@ class WebGpuViewerContinuous(activity: ReaderActivity, val useGap: Boolean = fal
|
||||
// How many pages the viewport shows depends on the zoom, and a page on screen has to be
|
||||
// decoded rather than merely reserved - so the window follows what the last frame reached.
|
||||
override val preloadAhead get() = max(3, state.pagesBelow)
|
||||
override val preloadBehind get() = max(1, state.pagesAbove)
|
||||
override val preloadBehind get() = max(2, state.pagesAbove)
|
||||
|
||||
// The state reaches MAX_VISIBLE_PAGES either side of the current page whatever the zoom - to
|
||||
// measure the document's end as well as to draw - and every page in that reach is created on
|
||||
// demand here. Sized under it, each frame evicts exactly what the next one asks for.
|
||||
override val cacheSize get() = 2 + 2 * ImageViewerContinuousState.MAX_VISIBLE_PAGES
|
||||
override val cacheSize get() = 2 + 2 * max(4, state.pagesAbove + state.pagesBelow)
|
||||
|
||||
private val state get() = (pager as ImageViewContinuous).state
|
||||
|
||||
init {
|
||||
// Scrolling clear of a transition page is the only point this mode can call the chapter
|
||||
// before it finished - reaching a page's top comes a screen too early. Reported on every
|
||||
// change, so scrolling back up over it and down again selects that last page again.
|
||||
state.onPageScrolledThrough = onScrolledThrough@{ imagePage ->
|
||||
val chapter = (imagePage as? TransitionPage)?.prevChapter ?: return@onScrolledThrough
|
||||
val lastPage = chapter.pages?.lastOrNull() ?: return@onScrolledThrough
|
||||
activity.onPageSelected(lastPage)
|
||||
state.backgroundColor = readerBackgroundColor()
|
||||
|
||||
state.onViewport = { readThrough ->
|
||||
val pageChanged = readThrough !== lastReadThrough
|
||||
if (pageChanged) {
|
||||
lastReadThrough = readThrough
|
||||
readThrough?.let { select(it) }
|
||||
}
|
||||
|
||||
val page0 = state.getPage(0)
|
||||
val edge = (page0 is TransitionPage && page0.prevChapter == null) ||
|
||||
(readThrough is TransitionPage && readThrough.nextChapter == null)
|
||||
|
||||
val first = wasAtEdge == null
|
||||
val edgeChanged = edge != wasAtEdge
|
||||
wasAtEdge = edge
|
||||
if (edge) {
|
||||
if (edgeChanged) scope.launch { activity.showMenu() }
|
||||
} else if (!first && (edgeChanged || pageChanged)) {
|
||||
scope.launch { activity.hideMenu() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Null until the first frame, so opening mid-document neither shows nor hides. */
|
||||
@Volatile
|
||||
private var wasAtEdge: Boolean? = null
|
||||
|
||||
@Volatile
|
||||
private var lastReadThrough: ImagePage? = null
|
||||
|
||||
/**
|
||||
* Mark [imagePage] read - its bottom has cleared the viewport, which is later than
|
||||
* onPageChange's "top arrived" and the reason this mode selects from here: scrolling up to
|
||||
* reveal the page above would otherwise walk progress back past pages still on screen.
|
||||
*/
|
||||
private fun select(imagePage: ImagePage) {
|
||||
val page = readerPageFor(imagePage) ?: return
|
||||
scope.launch { activity.onPageSelected(page) }
|
||||
}
|
||||
|
||||
private fun readerPageFor(imagePage: ImagePage): ReaderPage? {
|
||||
// Off the page itself: a transition page is never currentPage (the scroll walk stops
|
||||
// before it), so it is the first thing a preload evicts.
|
||||
if (imagePage is TransitionPage) return imagePage.prevChapter?.pages?.lastOrNull()
|
||||
|
||||
// Only an image page needs the lookup - nothing on it names its ReaderPage. Evicted
|
||||
// means scrolled well clear of, so there is no progress left to report.
|
||||
return (viewerPageFor(imagePage) as? ViewerReaderPage)?.page
|
||||
}
|
||||
|
||||
override fun destroy() {
|
||||
state.onViewport = null
|
||||
lastReadThrough = null
|
||||
wasAtEdge = null
|
||||
super.destroy()
|
||||
}
|
||||
|
||||
private fun scrollByHalfPage(direction: Int) {
|
||||
state.animateScroll(direction * state.height / 2f)
|
||||
}
|
||||
@@ -45,6 +90,8 @@ class WebGpuViewerContinuous(activity: ReaderActivity, val useGap: Boolean = fal
|
||||
|
||||
override fun moveToPage(page: ReaderPage) {
|
||||
super.moveToPage(page)
|
||||
wasAtEdge = null
|
||||
lastReadThrough = null
|
||||
// Also for a jump to the page already showing, which turns nothing to slide in.
|
||||
state.resetScroll()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user