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:
@@ -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()) }
|
val orientation = remember(manga) { ReaderOrientation.fromPreference(manga?.readerOrientation?.toInt()) }
|
||||||
@@ -263,6 +278,10 @@ private fun ColumnScope.TapZonesItems(
|
|||||||
private fun ColumnScope.WebGpuViewerSettings(viewModel: ReaderSettingsViewModel) {
|
private fun ColumnScope.WebGpuViewerSettings(viewModel: ReaderSettingsViewModel) {
|
||||||
HeadingItem(MR.strings.webgpu_viewer)
|
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 navigationModePager by viewModel.preferences.navigationModePager.collectAsState()
|
||||||
val pagerNavInverted by viewModel.preferences.pagerNavInverted.collectAsState()
|
val pagerNavInverted by viewModel.preferences.pagerNavInverted.collectAsState()
|
||||||
TapZonesItems(
|
TapZonesItems(
|
||||||
@@ -272,6 +291,33 @@ private fun ColumnScope.WebGpuViewerSettings(viewModel: ReaderSettingsViewModel)
|
|||||||
onSelectInvertMode = viewModel.preferences.pagerNavInverted::set,
|
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()
|
val imageScaleType by viewModel.preferences.imageScaleType.collectAsState()
|
||||||
SettingsChipRow(MR.strings.pref_image_scale_type) {
|
SettingsChipRow(MR.strings.pref_image_scale_type) {
|
||||||
ReaderPreferences.ImageScaleTypeWebGpuViewer.forEach {
|
ReaderPreferences.ImageScaleTypeWebGpuViewer.forEach {
|
||||||
|
|||||||
@@ -202,10 +202,17 @@ class ReaderPreferences(
|
|||||||
// region WebGpu
|
// region WebGpu
|
||||||
|
|
||||||
val transitionAnimation: Preference<TransitionAnimation> =
|
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 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
|
// endregion
|
||||||
|
|
||||||
enum class FlashColor {
|
enum class FlashColor {
|
||||||
@@ -239,7 +246,8 @@ class ReaderPreferences(
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum class TransitionAnimation(val titleRes: StringResource) {
|
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_LEFT(MR.strings.transition_animation_flip_left),
|
||||||
FLIP_RIGHT(
|
FLIP_RIGHT(
|
||||||
MR.strings.transition_animation_flip_right,
|
MR.strings.transition_animation_flip_right,
|
||||||
|
|||||||
@@ -46,15 +46,24 @@ class WebGpuConfig(
|
|||||||
var landscapeZoom = false
|
var landscapeZoom = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var transitionAnimation = ReaderPreferences.TransitionAnimation.DEFAULT
|
var transitionAnimation = ReaderPreferences.TransitionAnimation.BASIC
|
||||||
|
private set
|
||||||
|
|
||||||
|
var transitionAnimationDual = ReaderPreferences.TransitionAnimation.BASIC
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var cutoutMode = ReaderPreferences.CutoutMode.AVOID
|
var cutoutMode = ReaderPreferences.CutoutMode.AVOID
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
var cutoutModeDual = ReaderPreferences.CutoutMode.AVOID
|
||||||
|
private set
|
||||||
|
|
||||||
var dualPageView = ReaderPreferences.DualPageView.NEVER
|
var dualPageView = ReaderPreferences.DualPageView.NEVER
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
var continuousMinWidth = 1
|
||||||
|
private set
|
||||||
|
|
||||||
init {
|
init {
|
||||||
readerPreferences.readerTheme
|
readerPreferences.readerTheme
|
||||||
.register(
|
.register(
|
||||||
@@ -120,17 +129,35 @@ class WebGpuConfig(
|
|||||||
{ imagePropertyChangedListener?.invoke() },
|
{ imagePropertyChangedListener?.invoke() },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
readerPreferences.transitionAnimationDual
|
||||||
|
.register(
|
||||||
|
{ transitionAnimationDual = it },
|
||||||
|
{ imagePropertyChangedListener?.invoke() },
|
||||||
|
)
|
||||||
|
|
||||||
readerPreferences.cutoutMode
|
readerPreferences.cutoutMode
|
||||||
.register(
|
.register(
|
||||||
{ cutoutMode = it },
|
{ cutoutMode = it },
|
||||||
{ imagePropertyChangedListener?.invoke() },
|
{ imagePropertyChangedListener?.invoke() },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
readerPreferences.cutoutModeDual
|
||||||
|
.register(
|
||||||
|
{ cutoutModeDual = it },
|
||||||
|
{ imagePropertyChangedListener?.invoke() },
|
||||||
|
)
|
||||||
|
|
||||||
readerPreferences.dualPageView
|
readerPreferences.dualPageView
|
||||||
.register(
|
.register(
|
||||||
{ dualPageView = it },
|
{ dualPageView = it },
|
||||||
{ imagePropertyChangedListener?.invoke() },
|
{ imagePropertyChangedListener?.invoke() },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
readerPreferences.continuousMinWidth
|
||||||
|
.register(
|
||||||
|
{ continuousMinWidth = it },
|
||||||
|
{ imagePropertyChangedListener?.invoke() },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun zoomTypeFromPreference(value: Int) {
|
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.TransitionCubeOuter
|
||||||
import ca.mpreg.webgpuviewer.transition.TransitionFade
|
import ca.mpreg.webgpuviewer.transition.TransitionFade
|
||||||
import ca.mpreg.webgpuviewer.transition.TransitionFadeWhite
|
import ca.mpreg.webgpuviewer.transition.TransitionFadeWhite
|
||||||
|
import ca.mpreg.webgpuviewer.transition.TransitionFlip
|
||||||
import ca.mpreg.webgpuviewer.transition.TransitionFlipLeft
|
import ca.mpreg.webgpuviewer.transition.TransitionFlipLeft
|
||||||
import ca.mpreg.webgpuviewer.transition.TransitionFlipRight
|
import ca.mpreg.webgpuviewer.transition.TransitionFlipRight
|
||||||
import ca.mpreg.webgpuviewer.transition.TransitionNone
|
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.TransitionStackRight
|
||||||
import ca.mpreg.webgpuviewer.transition.TransitionStackUp
|
import ca.mpreg.webgpuviewer.transition.TransitionStackUp
|
||||||
import ca.mpreg.webgpuviewer.viewer.ImagePage
|
import ca.mpreg.webgpuviewer.viewer.ImagePage
|
||||||
|
import ca.mpreg.webgpuviewer.viewer.ImageViewerContinuousState
|
||||||
import com.google.android.material.color.MaterialColors
|
import com.google.android.material.color.MaterialColors
|
||||||
import de.stefan_oltmann.kim.Kim
|
import de.stefan_oltmann.kim.Kim
|
||||||
import de.stefan_oltmann.kim.android.readMetadata
|
import de.stefan_oltmann.kim.android.readMetadata
|
||||||
@@ -55,6 +57,7 @@ import kotlinx.coroutines.launch
|
|||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import mihon.app.di.appGraph
|
import mihon.app.di.appGraph
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
|
import java.util.TreeSet
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@@ -96,12 +99,24 @@ open class WebGpuViewer(
|
|||||||
// Decode queue - pages waiting to be decoded, processed LIFO (last = highest priority)
|
// Decode queue - pages waiting to be decoded, processed LIFO (last = highest priority)
|
||||||
private val decodeQueue = ArrayDeque<ViewerReaderPage>()
|
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
|
* Which side of a dual-page spread a [ViewerReaderPage] belongs on - app-level bookkeeping
|
||||||
* for [getSpreadAnchor]/[buildSpreadPage], independent of the decoded image itself.
|
* for [getSpreadAnchor]/[buildSpreadPage], independent of the decoded image itself.
|
||||||
*/
|
*/
|
||||||
internal enum class SpreadPosition { LEFT, RIGHT, SINGLE }
|
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
|
// Stable key types for page identity - data classes provide correct equals/hashCode
|
||||||
private sealed class PageKey {
|
private sealed class PageKey {
|
||||||
data class Reader(val chapterId: Long?, val index: Int) : PageKey()
|
data class Reader(val chapterId: Long?, val index: Int) : PageKey()
|
||||||
@@ -232,7 +247,11 @@ open class WebGpuViewer(
|
|||||||
open val preloadAhead = 3
|
open val preloadAhead = 3
|
||||||
open val preloadBehind = 2
|
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
|
* Page processing state
|
||||||
@@ -254,9 +273,8 @@ open class WebGpuViewer(
|
|||||||
*/
|
*/
|
||||||
private fun evictFarthestPage(reference: ViewerPage? = null): Boolean {
|
private fun evictFarthestPage(reference: ViewerPage? = null): Boolean {
|
||||||
val current = reference ?: currentPage ?: return false
|
val current = reference ?: currentPage ?: return false
|
||||||
val candidates = pageCache.values
|
val candidates =
|
||||||
.filter { it !== current && it !== currentPage && !isPinned(it) }
|
pageCache.values.filter { it !== current && it !== currentPage && !isPinned(it) }.toMutableSet()
|
||||||
.toMutableSet()
|
|
||||||
if (candidates.isEmpty()) return false
|
if (candidates.isEmpty()) return false
|
||||||
|
|
||||||
fun findNext(page: ViewerPage): ViewerPage? = when (page) {
|
fun findNext(page: ViewerPage): ViewerPage? = when (page) {
|
||||||
@@ -390,11 +408,13 @@ open class WebGpuViewer(
|
|||||||
|
|
||||||
inner class ErrorPage internal constructor(
|
inner class ErrorPage internal constructor(
|
||||||
message: String,
|
message: String,
|
||||||
spreadPosition: SpreadPosition = SpreadPosition.SINGLE,
|
private val spreadPosition: SpreadPosition = SpreadPosition.SINGLE,
|
||||||
) : ImagePage.Render(
|
) : ImagePage.Render(0, 0) {
|
||||||
if (spreadPosition == SpreadPosition.SINGLE) pager.state.width else pager.state.width / 2,
|
override val width: Int
|
||||||
pager.state.height,
|
get() = viewportPageWidth(spreadPosition != SpreadPosition.SINGLE)
|
||||||
) {
|
override val height: Int
|
||||||
|
get() = pager.state.height
|
||||||
|
|
||||||
init {
|
init {
|
||||||
minScale = 1f
|
minScale = 1f
|
||||||
maxScale = 1f
|
maxScale = 1f
|
||||||
@@ -431,10 +451,12 @@ open class WebGpuViewer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class ProgressPage(foregroundColor: Int = readerOnBackgroundColor()) : ImagePage.Render(
|
inner class ProgressPage(foregroundColor: Int = readerOnBackgroundColor()) : ImagePage.Render(0, 0) {
|
||||||
if (!isDualPageMode()) pager.state.width else pager.state.width / 2,
|
override val width: Int
|
||||||
pager.state.height,
|
get() = viewportPageWidth(isDualPageMode())
|
||||||
) {
|
override val height: Int
|
||||||
|
get() = pager.state.height
|
||||||
|
|
||||||
init {
|
init {
|
||||||
minScale = 1f
|
minScale = 1f
|
||||||
maxScale = 1f
|
maxScale = 1f
|
||||||
@@ -456,6 +478,9 @@ open class WebGpuViewer(
|
|||||||
override val backgroundColor: Int = readerBackgroundColor()
|
override val backgroundColor: Int = readerBackgroundColor()
|
||||||
|
|
||||||
override fun render(dst: GPUTexture, x: Float, y: Float, scale: Float) {
|
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 cx = dst.width * (0.5f + scale * x)
|
||||||
val cy = dst.height * (0.5f + scale * y)
|
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(
|
inner class TransitionPage(val prevChapter: ReaderChapter?, val nextChapter: ReaderChapter?) :
|
||||||
min(pager.state.width, pager.state.height),
|
ImagePage.Render(0, 0) {
|
||||||
min(pager.state.width, pager.state.height),
|
/** 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 {
|
init {
|
||||||
minScale = 1f
|
minScale = 1f
|
||||||
maxScale = 1f
|
maxScale = 1f
|
||||||
@@ -485,6 +514,9 @@ open class WebGpuViewer(
|
|||||||
override val backgroundColor: Int = readerBackgroundColor()
|
override val backgroundColor: Int = readerBackgroundColor()
|
||||||
|
|
||||||
override fun render(dst: GPUTexture, x: Float, y: Float, scale: Float) {
|
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()
|
val lines: MutableList<String> = mutableListOf()
|
||||||
prevChapter?.chapter?.let { chapter -> lines.add("Previous: " + chapter.name) }
|
prevChapter?.chapter?.let { chapter -> lines.add("Previous: " + chapter.name) }
|
||||||
nextChapter?.chapter?.let { chapter -> lines.add("Next: " + 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 */
|
/** Cached spread ImagePage when this page is the anchor of a dual-page spread */
|
||||||
var spreadPage: ImagePage.ImageSpread? = null
|
var spreadPage: ImagePage.ImageSpread? = null
|
||||||
|
|
||||||
/** Which side of a dual-page spread this page belongs on - set once decoding tags it. */
|
/** The side the file names, or null for none. Never a value merely derived from the index. */
|
||||||
internal var spreadPosition: SpreadPosition = SpreadPosition.SINGLE
|
@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()
|
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.
|
* Check if dual page mode is currently active based on config and view dimensions.
|
||||||
* Dual page is never active for continuous (scrolling) viewers.
|
* Dual page is never active for continuous (scrolling) viewers.
|
||||||
*/
|
*/
|
||||||
private fun isDualPageMode(): Boolean {
|
fun isDualPageMode(): Boolean {
|
||||||
if (isContinuous) return false
|
if (isContinuous) return false
|
||||||
return when (config.dualPageView) {
|
return when (config.dualPageView) {
|
||||||
ReaderPreferences.DualPageView.NEVER -> false
|
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.
|
* Check if the given page can form a spread with the next page.
|
||||||
* Uses page.spreadPosition to determine: anchor + partner = spread
|
* Uses page.spreadPosition to determine: anchor + partner = spread
|
||||||
@@ -626,12 +732,10 @@ open class WebGpuViewer(
|
|||||||
*/
|
*/
|
||||||
private fun canFormSpread(page: ViewerReaderPage): Boolean {
|
private fun canFormSpread(page: ViewerReaderPage): Boolean {
|
||||||
if (!isDualPageMode()) return false
|
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
|
if (page.spreadPosition != anchorPosition) return false
|
||||||
val next = page.next as? ViewerReaderPage ?: return false
|
val next = page.next as? ViewerReaderPage ?: return false
|
||||||
if (next.page.chapter != page.page.chapter) 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 (!isDualPageMode()) return page
|
||||||
if (page !is ViewerReaderPage) 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 this is a partner page, check if previous is anchor
|
||||||
if (page.spreadPosition == partnerPosition) {
|
if (page.spreadPosition == partnerPosition) {
|
||||||
val prev = page.prev as? ViewerReaderPage ?: return page
|
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
|
return prev
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -679,9 +782,6 @@ open class WebGpuViewer(
|
|||||||
return imagePage
|
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
|
// 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
|
// reaches this function directly (rather than being redirected here via
|
||||||
// [getSpreadAnchor]) when it has no anchor before it - a lone RIGHT with no preceding
|
// [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 partnerImagePage = if (page.spreadPosition == anchorPosition) {
|
||||||
val nextReaderPage = (page.next as? ViewerReaderPage)?.takeIf { it.page.chapter == page.page.chapter }
|
val nextReaderPage = (page.next as? ViewerReaderPage)?.takeIf { it.page.chapter == page.page.chapter }
|
||||||
nextReaderPage?.imagePage?.takeIf {
|
nextReaderPage?.imagePage?.takeIf {
|
||||||
nextReaderPage.spreadPosition == partnerPosition
|
nextReaderPage.spreadPosition == partnerPosition &&
|
||||||
|
canPairShapes(page, nextReaderPage)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -753,9 +854,11 @@ open class WebGpuViewer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
config.imagePropertyChangedListener = {
|
config.imagePropertyChangedListener = {
|
||||||
|
val isDual = isDualPageMode()
|
||||||
pager.state.apply {
|
pager.state.apply {
|
||||||
transition = when (config.transitionAnimation) {
|
transition = when (if (isDual) config.transitionAnimationDual else config.transitionAnimation) {
|
||||||
TransitionAnimation.DEFAULT -> if (isVertical) TransitionBasic.Vertical else TransitionBasic
|
TransitionAnimation.BASIC -> if (isVertical) TransitionBasic.Vertical else TransitionBasic
|
||||||
|
TransitionAnimation.FLIP -> TransitionFlip
|
||||||
TransitionAnimation.FLIP_LEFT -> TransitionFlipLeft
|
TransitionAnimation.FLIP_LEFT -> TransitionFlipLeft
|
||||||
TransitionAnimation.FLIP_RIGHT -> TransitionFlipRight
|
TransitionAnimation.FLIP_RIGHT -> TransitionFlipRight
|
||||||
TransitionAnimation.STACK_LEFT -> TransitionStackLeft
|
TransitionAnimation.STACK_LEFT -> TransitionStackLeft
|
||||||
@@ -770,7 +873,7 @@ open class WebGpuViewer(
|
|||||||
TransitionAnimation.NONE -> TransitionNone
|
TransitionAnimation.NONE -> TransitionNone
|
||||||
}
|
}
|
||||||
|
|
||||||
when (config.cutoutMode) {
|
when (if (isDual) config.cutoutModeDual else config.cutoutMode) {
|
||||||
ReaderPreferences.CutoutMode.IGNORE -> avoidCutout = false
|
ReaderPreferences.CutoutMode.IGNORE -> avoidCutout = false
|
||||||
ReaderPreferences.CutoutMode.AVOID -> {
|
ReaderPreferences.CutoutMode.AVOID -> {
|
||||||
avoidCutout = true
|
avoidCutout = true
|
||||||
@@ -782,6 +885,11 @@ open class WebGpuViewer(
|
|||||||
alwaysAvoidCutout = true
|
alwaysAvoidCutout = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(this as? ImageViewerContinuousState)?.let {
|
||||||
|
minZoomWidthFraction = config.continuousMinWidth / 100f
|
||||||
|
scale = minScale
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized(lock) {
|
synchronized(lock) {
|
||||||
@@ -827,6 +935,7 @@ open class WebGpuViewer(
|
|||||||
it.imagePage.cleanup()
|
it.imagePage.cleanup()
|
||||||
}
|
}
|
||||||
pageCache.clear()
|
pageCache.clear()
|
||||||
|
loneIndices.clear()
|
||||||
// Notify in case worker is waiting (though it should be interrupted)
|
// Notify in case worker is waiting (though it should be interrupted)
|
||||||
lock.notifyAll()
|
lock.notifyAll()
|
||||||
}
|
}
|
||||||
@@ -878,14 +987,12 @@ open class WebGpuViewer(
|
|||||||
try {
|
try {
|
||||||
val downloadProgressJob = launch {
|
val downloadProgressJob = launch {
|
||||||
page.page.progressFlow.collect { value ->
|
page.page.progressFlow.collect { value ->
|
||||||
// Check if page was evicted or already decoded
|
// Still the placeholder? Evicted or decoded, and there is nothing to fill.
|
||||||
synchronized(lock) {
|
val progressPage = synchronized(lock) {
|
||||||
if (!pageInCache(page) || page.imagePage !is ImagePage.Dummy) return@collect
|
if (!pageInCache(page)) return@collect
|
||||||
}
|
page.imagePage as? ProgressPage ?: return@collect
|
||||||
|
|
||||||
(page.imagePage as? ProgressPage)?.apply {
|
|
||||||
progress = value / 100f
|
|
||||||
}
|
}
|
||||||
|
progressPage.progress = value / 100f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -943,29 +1050,24 @@ open class WebGpuViewer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buffer file to detect spread position tag, then decode.
|
// Buffered to read the spread tag, then decoded from the buffer. On the preference,
|
||||||
// When not in dual page mode, skip Kim entirely.
|
// not isDualPageMode(): WIDE is portrait-off, and a rotate never re-decodes.
|
||||||
val bytes = if (isDualPageMode()) input.readBytes() else null
|
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())
|
val tag = Kim.readMetadata(bytes.inputStream(), bytes.size.toLong())
|
||||||
?.findStringValue(TiffTag.TIFF_TAG_PAGE_NAME)
|
?.findStringValue(TiffTag.TIFF_TAG_PAGE_NAME)
|
||||||
when (tag) {
|
page.taggedSpreadPosition = when (tag) {
|
||||||
"Left" -> SpreadPosition.LEFT
|
"Left" -> SpreadPosition.LEFT
|
||||||
"Right" -> SpreadPosition.RIGHT
|
"Right" -> SpreadPosition.RIGHT
|
||||||
// Set position for dual page spreads based on reading direction:
|
null -> null
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> SpreadPosition.SINGLE
|
else -> SpreadPosition.SINGLE
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
SpreadPosition.SINGLE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val dec = ImageDecoder.new(bytes?.inputStream() ?: input)
|
val dec = ImageDecoder.new(bytes?.inputStream() ?: input)
|
||||||
@@ -1035,12 +1137,13 @@ open class WebGpuViewer(
|
|||||||
if (pageInCache(page) && !page.isDecoded && !page.imagePage.destroyed) {
|
if (pageInCache(page) && !page.isDecoded && !page.imagePage.destroyed) {
|
||||||
val oldImagePage = page.imagePage
|
val oldImagePage = page.imagePage
|
||||||
page.imagePage = imagePage
|
page.imagePage = imagePage
|
||||||
|
noteIfLone(page)
|
||||||
page.state = PageState.IDLE
|
page.state = PageState.IDLE
|
||||||
oldImagePage.cleanup()
|
oldImagePage.cleanup()
|
||||||
// Fade up from the placeholder's colour, if that placeholder was on screen -
|
// 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.
|
// one that decoded out of view has nothing left to fade from.
|
||||||
if (oldImagePage.isOnScreen) imagePage.fadeIn()
|
if (oldImagePage.isOnScreen) imagePage.fadeIn()
|
||||||
if (page.spreadPosition == SpreadPosition.SINGLE) {
|
if (!isDualPageMode()) {
|
||||||
(page.imagePage as? ImagePage.ImageSingle)?.let {
|
(page.imagePage as? ImagePage.ImageSingle)?.let {
|
||||||
if (!applyWideZoomIfNeeded(it)) {
|
if (!applyWideZoomIfNeeded(it)) {
|
||||||
applyFitModeAnchor(it)
|
applyFitModeAnchor(it)
|
||||||
@@ -1073,6 +1176,9 @@ open class WebGpuViewer(
|
|||||||
image.width.toFloat() / image.height.toFloat(),
|
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
|
// Wide page: half the image width is wider than the screen aspect ratio
|
||||||
if (aspectRatio <= 2f * screenW.toFloat() / screenH) return false
|
if (aspectRatio <= 2f * screenW.toFloat() / screenH) return false
|
||||||
|
|
||||||
@@ -1334,7 +1440,8 @@ open class WebGpuViewer(
|
|||||||
val maxX = page.maxX(page.scale)
|
val maxX = page.maxX(page.scale)
|
||||||
val currentX = page.animationJob?.let { page.animationTargetX } ?: page.x
|
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)) {
|
if (!currentX.closeTo(x)) {
|
||||||
page.animateTo(targetX = x, targetY = page.y)
|
page.animateTo(targetX = x, targetY = page.y)
|
||||||
@@ -1356,7 +1463,8 @@ open class WebGpuViewer(
|
|||||||
val maxX = page.maxX(page.scale)
|
val maxX = page.maxX(page.scale)
|
||||||
val currentX = page.animationJob?.isActive?.let { page.animationTargetX } ?: page.x
|
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)) {
|
if (!currentX.closeTo(x)) {
|
||||||
page.animateTo(targetX = x, targetY = page.y)
|
page.animateTo(targetX = x, targetY = page.y)
|
||||||
|
|||||||
+22
-2
@@ -2,19 +2,39 @@ package eu.kanade.tachiyomi.ui.reader.viewer.webgpu
|
|||||||
|
|
||||||
import ca.mpreg.webgpuviewer.ImageViewContinuous
|
import ca.mpreg.webgpuviewer.ImageViewContinuous
|
||||||
import ca.mpreg.webgpuviewer.viewer.ImagePage
|
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.ReaderActivity
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
class WebGpuViewerContinuous(activity: ReaderActivity) :
|
class WebGpuViewerContinuous(activity: ReaderActivity) :
|
||||||
WebGpuViewer(activity, isReversed = false, isVertical = true, pager = ImageViewContinuous(activity)) {
|
WebGpuViewer(activity, isReversed = false, isVertical = true, pager = ImageViewContinuous(activity)) {
|
||||||
|
|
||||||
override val isContinuous: Boolean = true
|
override val isContinuous: Boolean = true
|
||||||
|
|
||||||
override val preloadAhead = 3
|
// How many pages the viewport shows depends on the zoom, and a page on screen has to be
|
||||||
override val preloadBehind = 1
|
// 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
|
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) {
|
private fun scrollByHalfPage(direction: Int) {
|
||||||
state.animateScroll(direction * state.height / 2f)
|
state.animateScroll(direction * state.height / 2f)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ tapmoc = "0.4.2"
|
|||||||
unifile = "08f224c8f9"
|
unifile = "08f224c8f9"
|
||||||
valkyrie = "0.5.2"
|
valkyrie = "0.5.2"
|
||||||
voyager = "2.2.21-1.10.3"
|
voyager = "2.2.21-1.10.3"
|
||||||
webgpuviewer = "39"
|
webgpuviewer = "40"
|
||||||
xmlutil = "1.0.2"
|
xmlutil = "1.0.2"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
|||||||
@@ -499,7 +499,8 @@
|
|||||||
<string name="pref_webtoon_disable_zoom_out">Disable zoom out</string>
|
<string name="pref_webtoon_disable_zoom_out">Disable zoom out</string>
|
||||||
<string name="webgpu_viewer">WebGPU</string>
|
<string name="webgpu_viewer">WebGPU</string>
|
||||||
<string name="pref_transition_animation">Transition animation</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_left">Page flip (left)</string>
|
||||||
<string name="transition_animation_flip_right">Page flip (right)</string>
|
<string name="transition_animation_flip_right">Page flip (right)</string>
|
||||||
<string name="transition_animation_stack_left">Stack (left)</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">Fade</string>
|
||||||
<string name="transition_animation_fade_white">Fade to white</string>
|
<string name="transition_animation_fade_white">Fade to white</string>
|
||||||
<string name="transition_animation_none">None</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">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_ignore">Ignore</string>
|
||||||
<string name="cutout_mode_avoid">Avoid</string>
|
<string name="cutout_mode_avoid">Avoid</string>
|
||||||
<string name="cutout_mode_shift">Shift</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_never">Never</string>
|
||||||
<string name="dual_page_view_always">Always</string>
|
<string name="dual_page_view_always">Always</string>
|
||||||
<string name="dual_page_view_wide">When wide</string>
|
<string name="dual_page_view_wide">When wide</string>
|
||||||
|
<string name="pref_continuous_minwidth">Min width</string>
|
||||||
|
|
||||||
<!-- Downloads section -->
|
<!-- Downloads section -->
|
||||||
<string name="pref_category_delete_chapters">Delete chapters</string>
|
<string name="pref_category_delete_chapters">Delete chapters</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user