Bump webgpuviewer (#3886)

* Add continuous min width

* Add dual page flip transition, separate some dual config

* Fix progress on sides for dual

* re-organize and minor fixes

* Reverse pan when vertical and zoom start right

* add flip to single

* Simple page size heuristics for dual selection

* zoom minimum wideness

* Fix continuous page progress

* continuous dynamic cache size

* Update libs.versions.toml
This commit is contained in:
w
2026-09-04 08:18:18 -07:00
committed by GitHub
parent 21af65b100
commit f3125ab897
7 changed files with 280 additions and 67 deletions
@@ -64,6 +64,21 @@ internal fun ColumnScope.ReadingModePage(viewModel: ReaderSettingsViewModel) {
}
}
}
if (resolved == ReadingMode.WEBTOON) {
val numberFormat = remember { NumberFormat.getPercentInstance() }
val continuousMinWidth by viewModel.preferences.continuousMinWidth.collectAsState()
SliderItem(
value = continuousMinWidth,
valueRange = ReaderPreferences.let { 1..100 },
label = stringResource(MR.strings.pref_continuous_minwidth),
valueString = numberFormat.format(continuousMinWidth / 100f),
onChange = {
viewModel.preferences.continuousMinWidth.set(it)
},
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
}
}
val orientation = remember(manga) { ReaderOrientation.fromPreference(manga?.readerOrientation?.toInt()) }
@@ -263,6 +278,10 @@ private fun ColumnScope.TapZonesItems(
private fun ColumnScope.WebGpuViewerSettings(viewModel: ReaderSettingsViewModel) {
HeadingItem(MR.strings.webgpu_viewer)
val viewer by viewModel.viewerFlow.collectAsState()
val isDual = (viewer as? WebGpuViewer)?.isDualPageMode() == true
val navigationModePager by viewModel.preferences.navigationModePager.collectAsState()
val pagerNavInverted by viewModel.preferences.pagerNavInverted.collectAsState()
TapZonesItems(
@@ -272,6 +291,33 @@ private fun ColumnScope.WebGpuViewerSettings(viewModel: ReaderSettingsViewModel)
onSelectInvertMode = viewModel.preferences.pagerNavInverted::set,
)
if (isDual) {
val transitionAnimation by viewModel.preferences.transitionAnimationDual.collectAsState()
SettingsChipRow(MR.strings.pref_transition_animation_dual) {
(
ReaderPreferences.TransitionAnimation.entries - ReaderPreferences.TransitionAnimation.FLIP_LEFT -
ReaderPreferences.TransitionAnimation.FLIP_RIGHT
).forEach {
FilterChip(
selected = it == transitionAnimation,
onClick = { viewModel.preferences.transitionAnimationDual.set(it) },
label = { Text(stringResource(it.titleRes)) },
)
}
}
val cutoutMode by viewModel.preferences.cutoutModeDual.collectAsState()
SettingsChipRow(MR.strings.pref_cutout_mode_dual) {
ReaderPreferences.CutoutMode.entries.forEach {
FilterChip(
selected = it == cutoutMode,
onClick = { viewModel.preferences.cutoutModeDual.set(it) },
label = { Text(stringResource(it.titleRes)) },
)
}
}
return
}
val imageScaleType by viewModel.preferences.imageScaleType.collectAsState()
SettingsChipRow(MR.strings.pref_image_scale_type) {
ReaderPreferences.ImageScaleTypeWebGpuViewer.forEach {
@@ -202,10 +202,17 @@ class ReaderPreferences(
// region WebGpu
val transitionAnimation: Preference<TransitionAnimation> =
preferenceStore.getEnum("webgpu_transition_animation", TransitionAnimation.DEFAULT)
preferenceStore.getEnum("webgpu_transition_animation", TransitionAnimation.BASIC)
val transitionAnimationDual: Preference<TransitionAnimation> =
preferenceStore.getEnum("webgpu_dual_transition_animation", TransitionAnimation.BASIC)
val cutoutMode: Preference<CutoutMode> = preferenceStore.getEnum("webgpu_cutout_mode", CutoutMode.AVOID)
val cutoutModeDual: Preference<CutoutMode> = preferenceStore.getEnum("webgpu_dual_cutout_mode", CutoutMode.IGNORE)
val continuousMinWidth: Preference<Int> = preferenceStore.getInt("webgpu_continuous_minwidth", 100)
// endregion
enum class FlashColor {
@@ -239,7 +246,8 @@ class ReaderPreferences(
}
enum class TransitionAnimation(val titleRes: StringResource) {
DEFAULT(MR.strings.transition_animation_default),
BASIC(MR.strings.transition_animation_basic),
FLIP(MR.strings.transition_animation_flip),
FLIP_LEFT(MR.strings.transition_animation_flip_left),
FLIP_RIGHT(
MR.strings.transition_animation_flip_right,
@@ -46,15 +46,24 @@ class WebGpuConfig(
var landscapeZoom = false
private set
var transitionAnimation = ReaderPreferences.TransitionAnimation.DEFAULT
var transitionAnimation = ReaderPreferences.TransitionAnimation.BASIC
private set
var transitionAnimationDual = ReaderPreferences.TransitionAnimation.BASIC
private set
var cutoutMode = ReaderPreferences.CutoutMode.AVOID
private set
var cutoutModeDual = ReaderPreferences.CutoutMode.AVOID
private set
var dualPageView = ReaderPreferences.DualPageView.NEVER
private set
var continuousMinWidth = 1
private set
init {
readerPreferences.readerTheme
.register(
@@ -120,17 +129,35 @@ class WebGpuConfig(
{ imagePropertyChangedListener?.invoke() },
)
readerPreferences.transitionAnimationDual
.register(
{ transitionAnimationDual = it },
{ imagePropertyChangedListener?.invoke() },
)
readerPreferences.cutoutMode
.register(
{ cutoutMode = it },
{ imagePropertyChangedListener?.invoke() },
)
readerPreferences.cutoutModeDual
.register(
{ cutoutModeDual = it },
{ imagePropertyChangedListener?.invoke() },
)
readerPreferences.dualPageView
.register(
{ dualPageView = it },
{ imagePropertyChangedListener?.invoke() },
)
readerPreferences.continuousMinWidth
.register(
{ continuousMinWidth = it },
{ imagePropertyChangedListener?.invoke() },
)
}
private fun zoomTypeFromPreference(value: Int) {
@@ -20,6 +20,7 @@ import ca.mpreg.webgpuviewer.transition.TransitionCube
import ca.mpreg.webgpuviewer.transition.TransitionCubeOuter
import ca.mpreg.webgpuviewer.transition.TransitionFade
import ca.mpreg.webgpuviewer.transition.TransitionFadeWhite
import ca.mpreg.webgpuviewer.transition.TransitionFlip
import ca.mpreg.webgpuviewer.transition.TransitionFlipLeft
import ca.mpreg.webgpuviewer.transition.TransitionFlipRight
import ca.mpreg.webgpuviewer.transition.TransitionNone
@@ -29,6 +30,7 @@ import ca.mpreg.webgpuviewer.transition.TransitionStackLeft
import ca.mpreg.webgpuviewer.transition.TransitionStackRight
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
@@ -55,6 +57,7 @@ import kotlinx.coroutines.launch
import logcat.LogPriority
import mihon.app.di.appGraph
import tachiyomi.core.common.util.system.logcat
import java.util.TreeSet
import java.util.concurrent.Executors
import kotlin.math.abs
import kotlin.math.min
@@ -96,12 +99,24 @@ open class WebGpuViewer(
// Decode queue - pages waiting to be decoded, processed LIFO (last = highest priority)
private val decodeQueue = ArrayDeque<ViewerReaderPage>()
/**
* Indices of the pages that take a spread to themselves, by chapter - see [spreadStartIndex].
* Outlives [pageCache]: every page after one of these depends on it, long since evicted.
*/
private val loneIndices = HashMap<Long?, TreeSet<Int>>()
/**
* Which side of a dual-page spread a [ViewerReaderPage] belongs on - app-level bookkeeping
* for [getSpreadAnchor]/[buildSpreadPage], independent of the decoded image itself.
*/
internal enum class SpreadPosition { LEFT, RIGHT, SINGLE }
/** Above this, an untagged page is a spread already, not half of one. */
private val wideAspect = 1.2f
/** How far two untagged pages' aspect ratios may differ and still pair. */
private val pairAspectTolerance = 0.1f
// Stable key types for page identity - data classes provide correct equals/hashCode
private sealed class PageKey {
data class Reader(val chapterId: Long?, val index: Int) : PageKey()
@@ -232,7 +247,11 @@ open class WebGpuViewer(
open val preloadAhead = 3
open val preloadBehind = 2
open val cacheSize get() = 1 + preloadAhead + preloadBehind
/**
* Everything [preloadPages] reaches, plus slack. Sized exactly, a chapter transition page - or
* in dual mode a spread partner - evicts a page the next fetch asks for, and it decodes again.
*/
open val cacheSize get() = 1 + preloadAhead + preloadBehind + if (isDualPageMode()) 3 else 1
/**
* Page processing state
@@ -254,9 +273,8 @@ open class WebGpuViewer(
*/
private fun evictFarthestPage(reference: ViewerPage? = null): Boolean {
val current = reference ?: currentPage ?: return false
val candidates = pageCache.values
.filter { it !== current && it !== currentPage && !isPinned(it) }
.toMutableSet()
val candidates =
pageCache.values.filter { it !== current && it !== currentPage && !isPinned(it) }.toMutableSet()
if (candidates.isEmpty()) return false
fun findNext(page: ViewerPage): ViewerPage? = when (page) {
@@ -390,11 +408,13 @@ open class WebGpuViewer(
inner class ErrorPage internal constructor(
message: String,
spreadPosition: SpreadPosition = SpreadPosition.SINGLE,
) : ImagePage.Render(
if (spreadPosition == SpreadPosition.SINGLE) pager.state.width else pager.state.width / 2,
pager.state.height,
) {
private val spreadPosition: SpreadPosition = SpreadPosition.SINGLE,
) : ImagePage.Render(0, 0) {
override val width: Int
get() = viewportPageWidth(spreadPosition != SpreadPosition.SINGLE)
override val height: Int
get() = pager.state.height
init {
minScale = 1f
maxScale = 1f
@@ -431,10 +451,12 @@ open class WebGpuViewer(
}
}
inner class ProgressPage(foregroundColor: Int = readerOnBackgroundColor()) : ImagePage.Render(
if (!isDualPageMode()) pager.state.width else pager.state.width / 2,
pager.state.height,
) {
inner class ProgressPage(foregroundColor: Int = readerOnBackgroundColor()) : ImagePage.Render(0, 0) {
override val width: Int
get() = viewportPageWidth(isDualPageMode())
override val height: Int
get() = pager.state.height
init {
minScale = 1f
maxScale = 1f
@@ -456,6 +478,9 @@ open class WebGpuViewer(
override val backgroundColor: Int = readerBackgroundColor()
override fun render(dst: GPUTexture, x: Float, y: Float, scale: Float) {
// Its own footprint, so the page carries its background wherever a transition puts it.
fillPage(dst, x, y, scale, backgroundColor)
val cx = dst.width * (0.5f + scale * x)
val cy = dst.height * (0.5f + scale * y)
@@ -472,10 +497,14 @@ open class WebGpuViewer(
}
}
inner class TransitionPage(val prevChapter: ReaderChapter?, val nextChapter: ReaderChapter?) : ImagePage.Render(
min(pager.state.width, pager.state.height),
min(pager.state.width, pager.state.height),
) {
inner class TransitionPage(val prevChapter: ReaderChapter?, val nextChapter: ReaderChapter?) :
ImagePage.Render(0, 0) {
/** Square, and never a spread side - [buildSpreadPage] hands it back whole. */
override val width: Int
get() = min(pager.state.width, pager.state.height)
override val height: Int
get() = width
init {
minScale = 1f
maxScale = 1f
@@ -485,6 +514,9 @@ open class WebGpuViewer(
override val backgroundColor: Int = readerBackgroundColor()
override fun render(dst: GPUTexture, x: Float, y: Float, scale: Float) {
// Its own footprint, so the page carries its background wherever a transition puts it.
fillPage(dst, x, y, scale, backgroundColor)
val lines: MutableList<String> = mutableListOf()
prevChapter?.chapter?.let { chapter -> lines.add("Previous: " + chapter.name) }
nextChapter?.chapter?.let { chapter -> lines.add("Next: " + chapter.name) }
@@ -544,8 +576,35 @@ open class WebGpuViewer(
/** Cached spread ImagePage when this page is the anchor of a dual-page spread */
var spreadPage: ImagePage.ImageSpread? = null
/** Which side of a dual-page spread this page belongs on - set once decoding tags it. */
internal var spreadPosition: SpreadPosition = SpreadPosition.SINGLE
/** The side the file names, or null for none. Never a value merely derived from the index. */
@Volatile
internal var taggedSpreadPosition: SpreadPosition? = null
/** The decoded image's shape, or null while this page is still a placeholder. */
internal val aspectRatio: Float?
get() = (imagePage as? ImagePage.ImageSingle)?.let {
val height = it.trimHeight
if (it.isDecoded && height > 0) it.trimWidth.toFloat() / height else null
}
/**
* Which half of a spread this page is on - derived until the file tags it. Without that a
* still-loading page stays SINGLE, never pairs, and its ring draws mid-screen; deriving it
* live also re-decides it on a rotation in or out of dual mode.
*
* Untagged goes by [wideAspect] first, then [derivedSpreadPosition].
*/
internal val spreadPosition: SpreadPosition
get() {
taggedSpreadPosition?.let { return it }
if (standsAlone) return SpreadPosition.SINGLE
return derivedSpreadPosition(page)
}
/** True when nothing may share this page's spread - it is one already. */
internal val standsAlone: Boolean
get() = taggedSpreadPosition == SpreadPosition.SINGLE ||
(aspectRatio ?: 0f) > wideAspect
override var imagePage: ImagePage = ProgressPage()
@@ -601,11 +660,14 @@ open class WebGpuViewer(
}
}
/** Read live: these pages are built before the surface has a size, and outlive a rotation. */
private fun viewportPageWidth(half: Boolean): Int = if (half) pager.state.width / 2 else pager.state.width
/**
* Check if dual page mode is currently active based on config and view dimensions.
* Dual page is never active for continuous (scrolling) viewers.
*/
private fun isDualPageMode(): Boolean {
fun isDualPageMode(): Boolean {
if (isContinuous) return false
return when (config.dualPageView) {
ReaderPreferences.DualPageView.NEVER -> false
@@ -618,6 +680,50 @@ open class WebGpuViewer(
}
}
/** The half a spread opens on: right reading right-to-left, left otherwise. */
private val anchorPosition get() = if (isReversed) SpreadPosition.RIGHT else SpreadPosition.LEFT
private val partnerPosition get() = if (isReversed) SpreadPosition.LEFT else SpreadPosition.RIGHT
/**
* Which half a page falls on when nothing tags the file: alternating from its spread's start,
* anchor then partner. SINGLE outside dual page mode, so nothing pairs while one page fills
* the viewer.
*/
private fun derivedSpreadPosition(page: ReaderPage): SpreadPosition {
if (!isDualPageMode()) return SpreadPosition.SINGLE
val offset = page.index - spreadStartIndex(page.chapter.chapter.id, page.index)
return if (offset >= 0 && offset % 2 == 0) anchorPosition else partnerPosition
}
/**
* Where the spread holding [index] starts: just past the last page before it that took one to
* itself, so the page after a detected spread opens the next one instead of inheriting a parity
* that page broke. Defaults to 1 - page 0 is the cover, and pairs with nothing.
*/
private fun spreadStartIndex(chapterId: Long?, index: Int): Int {
val lone = synchronized(lock) { loneIndices[chapterId]?.lower(index) } ?: return 1
return lone + 1
}
/** Registers whether [page] stands alone, for [spreadStartIndex]. Must hold [lock]. */
private fun noteIfLone(page: ViewerReaderPage) {
val indices = loneIndices.getOrPut(page.page.chapter.chapter.id) { TreeSet() }
if (page.standsAlone) indices.add(page.page.index) else indices.remove(page.page.index)
}
/**
* Whether these two may share a spread, beyond their positions agreeing. Both tagged is taken
* as read; a pair resting on page order needs the same shape - halves of one sheet scan alike.
* Undecoded pairs anyway, or a loading page draws its ring mid-screen.
*/
private fun canPairShapes(anchor: ViewerReaderPage, partner: ViewerReaderPage): Boolean {
if (anchor.taggedSpreadPosition != null && partner.taggedSpreadPosition != null) return true
val a = anchor.aspectRatio ?: return true
val b = partner.aspectRatio ?: return true
return abs(a - b) <= pairAspectTolerance
}
/**
* Check if the given page can form a spread with the next page.
* Uses page.spreadPosition to determine: anchor + partner = spread
@@ -626,12 +732,10 @@ open class WebGpuViewer(
*/
private fun canFormSpread(page: ViewerReaderPage): Boolean {
if (!isDualPageMode()) return false
val anchorPosition = if (isReversed) SpreadPosition.RIGHT else SpreadPosition.LEFT
val partnerPosition = if (isReversed) SpreadPosition.LEFT else SpreadPosition.RIGHT
if (page.spreadPosition != anchorPosition) return false
val next = page.next as? ViewerReaderPage ?: return false
if (next.page.chapter != page.page.chapter) return false
return next.spreadPosition == partnerPosition
return next.spreadPosition == partnerPosition && canPairShapes(page, next)
}
/**
@@ -643,13 +747,12 @@ open class WebGpuViewer(
if (!isDualPageMode()) return page
if (page !is ViewerReaderPage) return page
val anchorPosition = if (isReversed) SpreadPosition.RIGHT else SpreadPosition.LEFT
val partnerPosition = if (isReversed) SpreadPosition.LEFT else SpreadPosition.RIGHT
// If this is a partner page, check if previous is anchor
if (page.spreadPosition == partnerPosition) {
val prev = page.prev as? ViewerReaderPage ?: return page
if (prev.page.chapter == page.page.chapter && prev.spreadPosition == anchorPosition) {
if (prev.page.chapter == page.page.chapter && prev.spreadPosition == anchorPosition &&
canPairShapes(prev, page)
) {
return prev
}
}
@@ -679,9 +782,6 @@ open class WebGpuViewer(
return imagePage
}
val anchorPosition = if (isReversed) SpreadPosition.RIGHT else SpreadPosition.LEFT
val partnerPosition = if (isReversed) SpreadPosition.LEFT else SpreadPosition.RIGHT
// Only the anchor side looks for a partner on the next page. A partner-tagged page only
// reaches this function directly (rather than being redirected here via
// [getSpreadAnchor]) when it has no anchor before it - a lone RIGHT with no preceding
@@ -690,7 +790,8 @@ open class WebGpuViewer(
val partnerImagePage = if (page.spreadPosition == anchorPosition) {
val nextReaderPage = (page.next as? ViewerReaderPage)?.takeIf { it.page.chapter == page.page.chapter }
nextReaderPage?.imagePage?.takeIf {
nextReaderPage.spreadPosition == partnerPosition
nextReaderPage.spreadPosition == partnerPosition &&
canPairShapes(page, nextReaderPage)
}
} else {
null
@@ -753,9 +854,11 @@ open class WebGpuViewer(
}
config.imagePropertyChangedListener = {
val isDual = isDualPageMode()
pager.state.apply {
transition = when (config.transitionAnimation) {
TransitionAnimation.DEFAULT -> if (isVertical) TransitionBasic.Vertical else TransitionBasic
transition = when (if (isDual) config.transitionAnimationDual else config.transitionAnimation) {
TransitionAnimation.BASIC -> if (isVertical) TransitionBasic.Vertical else TransitionBasic
TransitionAnimation.FLIP -> TransitionFlip
TransitionAnimation.FLIP_LEFT -> TransitionFlipLeft
TransitionAnimation.FLIP_RIGHT -> TransitionFlipRight
TransitionAnimation.STACK_LEFT -> TransitionStackLeft
@@ -770,7 +873,7 @@ open class WebGpuViewer(
TransitionAnimation.NONE -> TransitionNone
}
when (config.cutoutMode) {
when (if (isDual) config.cutoutModeDual else config.cutoutMode) {
ReaderPreferences.CutoutMode.IGNORE -> avoidCutout = false
ReaderPreferences.CutoutMode.AVOID -> {
avoidCutout = true
@@ -782,6 +885,11 @@ open class WebGpuViewer(
alwaysAvoidCutout = true
}
}
(this as? ImageViewerContinuousState)?.let {
minZoomWidthFraction = config.continuousMinWidth / 100f
scale = minScale
}
}
synchronized(lock) {
@@ -827,6 +935,7 @@ open class WebGpuViewer(
it.imagePage.cleanup()
}
pageCache.clear()
loneIndices.clear()
// Notify in case worker is waiting (though it should be interrupted)
lock.notifyAll()
}
@@ -878,14 +987,12 @@ open class WebGpuViewer(
try {
val downloadProgressJob = launch {
page.page.progressFlow.collect { value ->
// Check if page was evicted or already decoded
synchronized(lock) {
if (!pageInCache(page) || page.imagePage !is ImagePage.Dummy) return@collect
}
(page.imagePage as? ProgressPage)?.apply {
progress = value / 100f
// Still the placeholder? Evicted or decoded, and there is nothing to fill.
val progressPage = synchronized(lock) {
if (!pageInCache(page)) return@collect
page.imagePage as? ProgressPage ?: return@collect
}
progressPage.progress = value / 100f
}
}
@@ -943,29 +1050,24 @@ open class WebGpuViewer(
}
}
// Buffer file to detect spread position tag, then decode.
// When not in dual page mode, skip Kim entirely.
val bytes = if (isDualPageMode()) input.readBytes() else null
// 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.
val bytes = if (config.dualPageView != ReaderPreferences.DualPageView.NEVER) {
input.readBytes()
} else {
null
}
page.spreadPosition = if (bytes != 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)
when (tag) {
page.taggedSpreadPosition = when (tag) {
"Left" -> SpreadPosition.LEFT
"Right" -> SpreadPosition.RIGHT
// Set position for dual page spreads based on reading direction:
// RTL (isReversed): Cover on LEFT, even=LEFT, odd=RIGHT
// LTR (!isReversed): Cover on RIGHT, even=RIGHT, odd=LEFT
null -> if (isReversed) { // TODO: heuristics, use image size
if (page.page.index % 2 == 0) SpreadPosition.LEFT else SpreadPosition.RIGHT
} else {
if (page.page.index % 2 == 0) SpreadPosition.RIGHT else SpreadPosition.LEFT
}
null -> null
else -> SpreadPosition.SINGLE
}
} else {
SpreadPosition.SINGLE
}
val dec = ImageDecoder.new(bytes?.inputStream() ?: input)
@@ -1035,12 +1137,13 @@ open class WebGpuViewer(
if (pageInCache(page) && !page.isDecoded && !page.imagePage.destroyed) {
val oldImagePage = page.imagePage
page.imagePage = imagePage
noteIfLone(page)
page.state = PageState.IDLE
oldImagePage.cleanup()
// 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 (page.spreadPosition == SpreadPosition.SINGLE) {
if (!isDualPageMode()) {
(page.imagePage as? ImagePage.ImageSingle)?.let {
if (!applyWideZoomIfNeeded(it)) {
applyFitModeAnchor(it)
@@ -1073,6 +1176,9 @@ open class WebGpuViewer(
image.width.toFloat() / image.height.toFloat(),
)
// not wide enough
if (aspectRatio < 1.1) return false
// Wide page: half the image width is wider than the screen aspect ratio
if (aspectRatio <= 2f * screenW.toFloat() / screenH) return false
@@ -1334,7 +1440,8 @@ open class WebGpuViewer(
val maxX = page.maxX(page.scale)
val currentX = page.animationJob?.let { page.animationTargetX } ?: page.x
val x = (currentX - 1 / page.scale).coerceIn(minX, maxX)
val c = if (isVertical && config.imageZoomType == ZoomStartPosition.RIGHT) -1 else 1
val x = (currentX - c / page.scale).coerceIn(minX, maxX)
if (!currentX.closeTo(x)) {
page.animateTo(targetX = x, targetY = page.y)
@@ -1356,7 +1463,8 @@ open class WebGpuViewer(
val maxX = page.maxX(page.scale)
val currentX = page.animationJob?.isActive?.let { page.animationTargetX } ?: page.x
val x = (currentX + 1 / page.scale).coerceIn(minX, maxX)
val c = if (isVertical && config.imageZoomType == ZoomStartPosition.RIGHT) -1 else 1
val x = (currentX + c / page.scale).coerceIn(minX, maxX)
if (!currentX.closeTo(x)) {
page.animateTo(targetX = x, targetY = page.y)
@@ -2,19 +2,39 @@ 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 kotlin.math.max
class WebGpuViewerContinuous(activity: ReaderActivity) :
WebGpuViewer(activity, isReversed = false, isVertical = true, pager = ImageViewContinuous(activity)) {
override val isContinuous: Boolean = true
override val preloadAhead = 3
override val preloadBehind = 1
// 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)
// 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
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)
}
}
private fun scrollByHalfPage(direction: Int) {
state.animateScroll(direction * state.height / 2f)
}
+1 -1
View File
@@ -77,7 +77,7 @@ tapmoc = "0.4.2"
unifile = "08f224c8f9"
valkyrie = "0.5.2"
voyager = "2.2.21-1.10.3"
webgpuviewer = "39"
webgpuviewer = "40"
xmlutil = "1.0.2"
[libraries]
@@ -499,7 +499,8 @@
<string name="pref_webtoon_disable_zoom_out">Disable zoom out</string>
<string name="webgpu_viewer">WebGPU</string>
<string name="pref_transition_animation">Transition animation</string>
<string name="transition_animation_default">Default</string>
<string name="pref_transition_animation_dual">Transition animation (dual)</string>
<string name="transition_animation_basic">Basic</string>
<string name="transition_animation_flip_left">Page flip (left)</string>
<string name="transition_animation_flip_right">Page flip (right)</string>
<string name="transition_animation_stack_left">Stack (left)</string>
@@ -512,7 +513,9 @@
<string name="transition_animation_fade">Fade</string>
<string name="transition_animation_fade_white">Fade to white</string>
<string name="transition_animation_none">None</string>
<string name="transition_animation_flip">Page flip</string>
<string name="pref_cutout_mode">Display cutout mode</string>
<string name="pref_cutout_mode_dual">Display cutout mode (dual)</string>
<string name="cutout_mode_ignore">Ignore</string>
<string name="cutout_mode_avoid">Avoid</string>
<string name="cutout_mode_shift">Shift</string>
@@ -520,6 +523,7 @@
<string name="dual_page_view_never">Never</string>
<string name="dual_page_view_always">Always</string>
<string name="dual_page_view_wide">When wide</string>
<string name="pref_continuous_minwidth">Min width</string>
<!-- Downloads section -->
<string name="pref_category_delete_chapters">Delete chapters</string>