Add high quality WebGPU renderer (#3388)

This commit is contained in:
w
2026-08-14 10:01:00 -07:00
committed by GitHub
parent 581a89b7b0
commit 5c3ae37d52
16 changed files with 1731 additions and 4 deletions
+4
View File
@@ -292,6 +292,10 @@ dependencies {
} }
implementation(libs.image.decoder) implementation(libs.image.decoder)
implementation(libs.image.decoder2)
implementation(libs.webgpuviewer)
implementation(libs.kim)
// UI libraries // UI libraries
implementation(libs.material) implementation(libs.material)
implementation(libs.flexibleAdapter) implementation(libs.flexibleAdapter)
@@ -33,6 +33,8 @@ class BasePreferences(
PRIVATE(MR.strings.ext_installer_private, false), PRIVATE(MR.strings.ext_installer_private, false),
} }
val highQualityRenderer: Preference<Boolean> = preferenceStore.getBoolean("pref_high_quality_renderer_key", false)
val displayProfile: Preference<String> = preferenceStore.getString("pref_display_profile_key", "") val displayProfile: Preference<String> = preferenceStore.getString("pref_display_profile_key", "")
val hardwareBitmapThreshold: Preference<Int> = preferenceStore.getInt( val hardwareBitmapThreshold: Preference<Int> = preferenceStore.getInt(
@@ -31,6 +31,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
@@ -38,21 +39,31 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.core.graphics.drawable.toDrawable import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import ca.mpreg.webgpuviewer.renderer.WebGpuRenderer
import ca.mpreg.webgpuviewer.viewer.ImagePage
import ca.mpreg.webgpuviewer.viewer.ImageViewer
import ca.mpreg.webgpuviewer.viewer.ImageViewerState
import coil3.asDrawable import coil3.asDrawable
import coil3.imageLoader import coil3.imageLoader
import coil3.request.CachePolicy import coil3.request.CachePolicy
import coil3.request.ImageRequest import coil3.request.ImageRequest
import coil3.size.Size import coil3.size.Size
import eu.kanade.domain.base.BasePreferences
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.DropdownMenu import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.manga.EditCoverAction import eu.kanade.presentation.manga.EditCoverAction
import eu.kanade.tachiyomi.data.coil.ImageDecoder2
import eu.kanade.tachiyomi.data.coil.newDecoder
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
import kotlinx.coroutines.runBlocking
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.clickableNoIndication import tachiyomi.presentation.core.util.clickableNoIndication
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable @Composable
fun MangaCoverDialog( fun MangaCoverDialog(
@@ -64,6 +75,9 @@ fun MangaCoverDialog(
onEditClick: ((EditCoverAction) -> Unit)?, onEditClick: ((EditCoverAction) -> Unit)?,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
) { ) {
val useNewRenderer = Injekt.get<BasePreferences>().highQualityRenderer.get()
val view = LocalView.current
Dialog( Dialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
properties = DialogProperties( properties = DialogProperties(
@@ -148,6 +162,41 @@ fun MangaCoverDialog(
} }
}, },
) { contentPadding -> ) { contentPadding ->
if (useNewRenderer) {
val state = ImageViewerState()
state.dpi = view.resources.displayMetrics.densityDpi / 100f
ImageRequest.Builder(view.context)
.data(manga)
.size(Size.ORIGINAL)
.memoryCachePolicy(CachePolicy.DISABLED)
.newDecoder(true)
.target { result ->
val res = (result as ImageDecoder2.DecodeResultImage).res
val page = runBlocking(WebGpuRenderer.dispatcher) {
ImagePage(res.image, res.width, res.height)
}.apply {
image?.backgroundColor = 0
}
state.apply {
fetchPage = { index ->
if (index == 0) {
page
} else {
null
}
}
invalidate()
}
}
.build()
.let(view.context.imageLoader::enqueue)
ImageViewer(state = state)
return@Scaffold
}
val statusBarPaddingPx = with(LocalDensity.current) { contentPadding.calculateTopPadding().roundToPx() } val statusBarPaddingPx = with(LocalDensity.current) { contentPadding.calculateTopPadding().roundToPx() }
val bottomPaddingPx = with(LocalDensity.current) { contentPadding.calculateBottomPadding().roundToPx() } val bottomPaddingPx = with(LocalDensity.current) { contentPadding.calculateBottomPadding().roundToPx() }
@@ -346,6 +346,10 @@ object SettingsAdvancedScreen : SearchableSettings {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_reader), title = stringResource(MR.strings.pref_category_reader),
preferenceItems = listOf( preferenceItems = listOf(
Preference.PreferenceItem.SwitchPreference(
preference = basePreferences.highQualityRenderer,
title = stringResource(MR.strings.pref_high_quality_renderer),
),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = basePreferences.hardwareBitmapThreshold, preference = basePreferences.hardwareBitmapThreshold,
entries = GLUtil.CUSTOM_TEXTURE_LIMIT_OPTIONS entries = GLUtil.CUSTOM_TEXTURE_LIMIT_OPTIONS
@@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsViewModel import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsViewModel
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import eu.kanade.tachiyomi.ui.reader.viewer.webgpu.WebGpuViewer
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.CheckboxItem import tachiyomi.presentation.core.components.CheckboxItem
@@ -22,6 +23,8 @@ import tachiyomi.presentation.core.components.SettingsChipRow
import tachiyomi.presentation.core.components.SliderItem import tachiyomi.presentation.core.components.SliderItem
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.NumberFormat import java.text.NumberFormat
@Composable @Composable
@@ -40,6 +43,26 @@ internal fun ColumnScope.ReadingModePage(viewModel: ReaderSettingsViewModel) {
} }
} }
val default = Injekt.get<ReaderPreferences>().defaultReadingMode.get()
val resolved = ReadingMode.fromPreference(
when {
readingMode == ReadingMode.DEFAULT -> default
else -> manga?.readingMode?.toInt() ?: default
},
)
if (resolved == ReadingMode.LEFT_TO_RIGHT || resolved == ReadingMode.RIGHT_TO_LEFT) {
val dualPageView by viewModel.preferences.dualPageView.collectAsState()
SettingsChipRow(MR.strings.pref_dual_page_view) {
ReaderPreferences.DualPageView.entries.map {
FilterChip(
selected = it == dualPageView,
onClick = { viewModel.preferences.dualPageView.set(it) },
label = { Text(stringResource(it.titleRes)) },
)
}
}
}
val orientation = remember(manga) { ReaderOrientation.fromPreference(manga?.readerOrientation?.toInt()) } val orientation = remember(manga) { ReaderOrientation.fromPreference(manga?.readerOrientation?.toInt()) }
SettingsChipRow(MR.strings.rotation_type) { SettingsChipRow(MR.strings.rotation_type) {
ReaderOrientation.entries.map { ReaderOrientation.entries.map {
@@ -56,6 +79,9 @@ internal fun ColumnScope.ReadingModePage(viewModel: ReaderSettingsViewModel) {
WebtoonViewerSettings(viewModel) WebtoonViewerSettings(viewModel)
} else { } else {
PagerViewerSettings(viewModel) PagerViewerSettings(viewModel)
if (viewer is WebGpuViewer) {
WebGpuViewerSettings(viewModel)
}
} }
} }
@@ -233,3 +259,30 @@ private fun ColumnScope.TapZonesItems(
} }
} }
} }
@Composable
private fun ColumnScope.WebGpuViewerSettings(viewModel: ReaderSettingsViewModel) {
HeadingItem(MR.strings.webgpu_viewer)
val transitionAnimation by viewModel.preferences.transitionAnimation.collectAsState()
SettingsChipRow(MR.strings.pref_transition_animation) {
ReaderPreferences.TransitionAnimation.entries.map {
FilterChip(
selected = it == transitionAnimation,
onClick = { viewModel.preferences.transitionAnimation.set(it) },
label = { Text(stringResource(it.titleRes)) },
)
}
}
val cutoutMode by viewModel.preferences.cutoutMode.collectAsState()
SettingsChipRow(MR.strings.pref_cutout_mode) {
ReaderPreferences.CutoutMode.entries.map {
FilterChip(
selected = it == cutoutMode,
onClick = { viewModel.preferences.cutoutMode.set(it) },
label = { Text(stringResource(it.titleRes)) },
)
}
}
}
@@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.data.coil package eu.kanade.tachiyomi.data.coil
import android.graphics.Bitmap import android.graphics.Bitmap
import coil3.Canvas
import coil3.Image
import coil3.ImageLoader import coil3.ImageLoader
import coil3.asImage import coil3.asImage
import coil3.decode.DecodeResult import coil3.decode.DecodeResult
@@ -62,7 +64,9 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
class Factory : Decoder.Factory { class Factory : Decoder.Factory {
override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? { override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? {
return if (options.customDecoder || isApplicable(result.source.source())) { return if (options.newDecoder) {
ImageDecoder2(result.source, options)
} else if (options.customDecoder || isApplicable(result.source.source())) {
TachiyomiImageDecoder(result.source, options) TachiyomiImageDecoder(result.source, options)
} else { } else {
null null
@@ -88,3 +92,27 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
var displayProfile: ByteArray? = null var displayProfile: ByteArray? = null
} }
} }
class ImageDecoder2(private val resources: ImageSource, private val options: Options) : Decoder {
class DecodeResultImage(val res: ca.mpreg.imagedecoder.ImageDecoder.DecodeResult) : Image {
override val size: Long get() = res.image.capacity().toLong()
override val width: Int get() = res.width
override val height: Int get() = res.height
override val shareable: Boolean get() = true
override fun draw(canvas: Canvas) {}
}
override suspend fun decode(): DecodeResult {
val source = resources.source()
val decoder = ca.mpreg.imagedecoder.ImageDecoder.new(source.inputStream())
val res = decoder.decode()
return DecodeResult(
image = DecodeResultImage(res),
isSampled = false,
)
}
}
@@ -42,3 +42,11 @@ val Options.customDecoder: Boolean
get() = getExtra(customDecoderKey) get() = getExtra(customDecoderKey)
private val customDecoderKey = Extras.Key(default = false) private val customDecoderKey = Extras.Key(default = false)
fun ImageRequest.Builder.newDecoder(enable: Boolean) = apply {
extras[newDecoderKey] = enable
}
val Options.newDecoder: Boolean
get() = getExtra(newDecoderKey)
private val newDecoderKey = Extras.Key(default = false)
@@ -78,6 +78,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsViewModel
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.webgpu.WebGpuViewer
import eu.kanade.tachiyomi.ui.webview.WebViewActivity import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.system.isNightMode import eu.kanade.tachiyomi.util.system.isNightMode
import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.openInBrowser
@@ -479,7 +480,7 @@ class ReaderActivity : BaseActivity() {
onShare = ::shareChapter.takeIf { isHttpSource }, onShare = ::shareChapter.takeIf { isHttpSource },
chapterNavigatorType = if (!verticalNavigator) { chapterNavigatorType = if (!verticalNavigator) {
if (state.viewer is R2LPagerViewer) { if (state.viewer is R2LPagerViewer || (state.viewer as? WebGpuViewer)?.isReversed ?: false) {
ChapterNavigatorType.HORIZONTAL_RTL ChapterNavigatorType.HORIZONTAL_RTL
} else { } else {
ChapterNavigatorType.HORIZONTAL_LTR ChapterNavigatorType.HORIZONTAL_LTR
@@ -132,6 +132,11 @@ class ReaderPreferences(
false, false,
) )
val dualPageView: Preference<DualPageView> = preferenceStore.getEnum(
"pref_dual_page_view",
DualPageView.NEVER,
)
// endregion // endregion
// region Color filter // region Color filter
@@ -189,6 +194,15 @@ class ReaderPreferences(
// endregion // endregion
// region WebGpu
val transitionAnimation: Preference<TransitionAnimation> =
preferenceStore.getEnum("webgpu_transition_animation", TransitionAnimation.DEFAULT)
val cutoutMode: Preference<CutoutMode> = preferenceStore.getEnum("webgpu_cutout_mode", CutoutMode.AVOID)
// endregion
enum class FlashColor { enum class FlashColor {
BLACK, BLACK,
WHITE, WHITE,
@@ -201,8 +215,14 @@ class ReaderPreferences(
val shouldInvertVertical: Boolean = false, val shouldInvertVertical: Boolean = false,
) { ) {
NONE(MR.strings.tapping_inverted_none), NONE(MR.strings.tapping_inverted_none),
HORIZONTAL(MR.strings.tapping_inverted_horizontal, shouldInvertHorizontal = true), HORIZONTAL(
VERTICAL(MR.strings.tapping_inverted_vertical, shouldInvertVertical = true), MR.strings.tapping_inverted_horizontal,
shouldInvertHorizontal = true,
),
VERTICAL(
MR.strings.tapping_inverted_vertical,
shouldInvertVertical = true,
),
BOTH(MR.strings.tapping_inverted_both, shouldInvertHorizontal = true, shouldInvertVertical = true), BOTH(MR.strings.tapping_inverted_both, shouldInvertHorizontal = true, shouldInvertVertical = true),
} }
@@ -213,6 +233,41 @@ class ReaderPreferences(
LOWEST(47), LOWEST(47),
} }
enum class TransitionAnimation(val titleRes: StringResource) {
DEFAULT(MR.strings.transition_animation_default),
FLIP_LEFT(MR.strings.transition_animation_flip_left),
FLIP_RIGHT(
MR.strings.transition_animation_flip_right,
),
STACK_LEFT(MR.strings.transition_animation_stack_left),
STACK_RIGHT(MR.strings.transition_animation_stack_right),
STACK_UP(
MR.strings.transition_animation_stack_up,
),
STACK_DOWN(MR.strings.transition_animation_stack_down),
SPHERE(MR.strings.transition_animation_sphere),
CUBE_INSIDE(
MR.strings.transition_animation_cube_inside,
),
CUBE_OUTSIDE(MR.strings.transition_animation_cube_outside),
FADE(MR.strings.transition_animation_fade),
FADE_WHITE(
MR.strings.transition_animation_fade_white,
),
}
enum class CutoutMode(val titleRes: StringResource) {
IGNORE(MR.strings.cutout_mode_ignore),
AVOID(MR.strings.cutout_mode_avoid),
SHIFT(MR.strings.cutout_mode_shift),
}
enum class DualPageView(val titleRes: StringResource) {
NEVER(MR.strings.dual_page_view_never),
ALWAYS(MR.strings.dual_page_view_always),
WIDE(MR.strings.dual_page_view_wide),
}
companion object { companion object {
const val WEBTOON_PADDING_MIN = 0 const val WEBTOON_PADDING_MIN = 0
const val WEBTOON_PADDING_MAX = 25 const val WEBTOON_PADDING_MAX = 25
@@ -2,14 +2,19 @@ package eu.kanade.tachiyomi.ui.reader.setting
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.viewer.Viewer import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.L2RPagerViewer import eu.kanade.tachiyomi.ui.reader.viewer.pager.L2RPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.webgpu.WebGpuViewer
import eu.kanade.tachiyomi.ui.reader.viewer.webgpu.WebGpuViewerContinuous
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
enum class ReadingMode( enum class ReadingMode(
val stringRes: StringResource, val stringRes: StringResource,
@@ -67,6 +72,16 @@ enum class ReadingMode(
} }
fun toViewer(preference: Int?, activity: ReaderActivity): Viewer { fun toViewer(preference: Int?, activity: ReaderActivity): Viewer {
if (Injekt.get<BasePreferences>().highQualityRenderer.get()) {
return when (fromPreference(preference)) {
LEFT_TO_RIGHT -> WebGpuViewer(activity, isReversed = false, isVertical = false)
RIGHT_TO_LEFT -> WebGpuViewer(activity, isReversed = true, isVertical = false)
VERTICAL -> WebGpuViewer(activity, isReversed = false, isVertical = true)
WEBTOON -> WebGpuViewerContinuous(activity)
CONTINUOUS_VERTICAL -> WebGpuViewerContinuous(activity)
DEFAULT -> throw IllegalStateException("Preference value must be resolved: $preference")
}
}
return when (fromPreference(preference)) { return when (fromPreference(preference)) {
LEFT_TO_RIGHT -> L2RPagerViewer(activity) LEFT_TO_RIGHT -> L2RPagerViewer(activity)
RIGHT_TO_LEFT -> R2LPagerViewer(activity) RIGHT_TO_LEFT -> R2LPagerViewer(activity)
@@ -0,0 +1,180 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webgpu
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerConfig
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.DisabledNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.EdgeNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.KindlishNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.LNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.RightAndLeftNavigation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Configuration used by pager viewers.
*/
class WebGpuConfig(
private val viewer: WebGpuViewer,
scope: CoroutineScope,
readerPreferences: ReaderPreferences = Injekt.get(),
) : ViewerConfig(readerPreferences, scope) {
var theme = readerPreferences.readerTheme.get()
private set
var automaticBackground = false
private set
var dualPageSplitChangedListener: ((Boolean) -> Unit)? = null
var imageScaleType = 1
private set
var imageZoomType = ReaderPageImageView.ZoomStartPosition.LEFT
private set
var imageCropBorders = false
private set
var navigateToPan = false
private set
var landscapeZoom = false
private set
var transitionAnimation = ReaderPreferences.TransitionAnimation.DEFAULT
private set
var cutoutMode = ReaderPreferences.CutoutMode.AVOID
private set
var dualPageView = ReaderPreferences.DualPageView.NEVER
private set
init {
readerPreferences.readerTheme
.register(
{
theme = it
automaticBackground = it == 3
},
{ imagePropertyChangedListener?.invoke() },
)
readerPreferences.imageScaleType
.register({ imageScaleType = it }, { imagePropertyChangedListener?.invoke() })
readerPreferences.zoomStart
.register({ zoomTypeFromPreference(it) }, { imagePropertyChangedListener?.invoke() })
readerPreferences.cropBorders
.register({ imageCropBorders = it }, { imagePropertyChangedListener?.invoke() })
readerPreferences.navigateToPan
.register({ navigateToPan = it })
readerPreferences.landscapeZoom
.register({ landscapeZoom = it }, { imagePropertyChangedListener?.invoke() })
readerPreferences.navigationModePager
.register({ navigationMode = it }, { updateNavigation(navigationMode) })
readerPreferences.pagerNavInverted
.register({ tappingInverted = it }, { navigator.invertMode = it })
readerPreferences.pagerNavInverted.changes()
.drop(1)
.onEach { navigationModeChangedListener?.invoke() }
.launchIn(scope)
readerPreferences.dualPageSplitPaged
.register(
{ dualPageSplit = it },
{
imagePropertyChangedListener?.invoke()
dualPageSplitChangedListener?.invoke(it)
},
)
readerPreferences.dualPageInvertPaged
.register({ dualPageInvert = it }, { imagePropertyChangedListener?.invoke() })
readerPreferences.dualPageRotateToFit
.register(
{ dualPageRotateToFit = it },
{ imagePropertyChangedListener?.invoke() },
)
readerPreferences.dualPageRotateToFitInvert
.register(
{ dualPageRotateToFitInvert = it },
{ imagePropertyChangedListener?.invoke() },
)
readerPreferences.transitionAnimation
.register(
{ transitionAnimation = it },
{ imagePropertyChangedListener?.invoke() },
)
readerPreferences.cutoutMode
.register(
{ cutoutMode = it },
{ imagePropertyChangedListener?.invoke() },
)
readerPreferences.dualPageView
.register(
{ dualPageView = it },
{ imagePropertyChangedListener?.invoke() },
)
}
private fun zoomTypeFromPreference(value: Int) {
imageZoomType = when (value) {
// Auto
1 -> if (viewer.isReversed) {
ReaderPageImageView.ZoomStartPosition.RIGHT
} else {
ReaderPageImageView.ZoomStartPosition.LEFT
}
// Left
2 -> ReaderPageImageView.ZoomStartPosition.LEFT
// Right
3 -> ReaderPageImageView.ZoomStartPosition.RIGHT
// Center
else -> ReaderPageImageView.ZoomStartPosition.CENTER
}
}
override var navigator: ViewerNavigation = defaultNavigation()
set(value) {
field = value.also { it.invertMode = this.tappingInverted }
}
override fun defaultNavigation(): ViewerNavigation {
return if (viewer.isVertical) {
LNavigation()
} else {
RightAndLeftNavigation()
}
}
override fun updateNavigation(navigationMode: Int) {
navigator = when (navigationMode) {
0 -> defaultNavigation()
1 -> LNavigation()
2 -> KindlishNavigation()
3 -> EdgeNavigation()
4 -> RightAndLeftNavigation()
5 -> DisabledNavigation()
else -> defaultNavigation()
}
navigationModeChangedListener?.invoke()
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,26 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webgpu
import ca.mpreg.webgpuviewer.ImageViewContinuous
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
class WebGpuViewerContinuous(activity: ReaderActivity) :
WebGpuViewer(activity, isReversed = false, isVertical = true, pager = ImageViewContinuous(activity)) {
override val isContinuous: Boolean = true
private fun scrollByHalfPage(direction: Int) {
val state = (pager as ImageViewContinuous).state
val totalDistance = direction * state.height / 2f
state.animateScroll(totalDistance)
}
override fun moveRight() = scrollByHalfPage(1)
override fun moveLeft() = scrollByHalfPage(-1)
override fun moveToPage(page: ReaderPage) {
super.moveToPage(page)
(pager as ImageViewContinuous).state.scrollY = 0f
}
}
+1
View File
@@ -32,6 +32,7 @@ dependencies {
api(libs.okio) api(libs.okio)
implementation(libs.image.decoder) implementation(libs.image.decoder)
implementation(libs.image.decoder2)
implementation(libs.unifile) implementation(libs.unifile)
implementation(libs.archive) implementation(libs.archive)
+6
View File
@@ -40,9 +40,11 @@ firebase-crashlytics = "3.0.7"
flexibleAdapter = "c8013533" flexibleAdapter = "c8013533"
google-services = "4.5.0" google-services = "4.5.0"
image-decoder = "e03b81e18a" image-decoder = "e03b81e18a"
imagedecoder2 = "6"
injekt = "91edab2317" injekt = "91edab2317"
jsoup = "1.23.1" jsoup = "1.23.1"
junit = "6.1.3" junit = "6.1.3"
kim = "0.34.0"
kotest-assertions = "6.2.4" kotest-assertions = "6.2.4"
kotlin-gradle = "2.4.10" kotlin-gradle = "2.4.10"
kotlinx-coroutines = "1.11.0" kotlinx-coroutines = "1.11.0"
@@ -72,6 +74,7 @@ swipe = "1.3.0"
tapmoc = "0.4.2" tapmoc = "0.4.2"
unifile = "08f224c8f9" unifile = "08f224c8f9"
voyager = "2.2.21-1.10.3" voyager = "2.2.21-1.10.3"
webgpuviewer = "25"
xmlutil = "1.0.2" xmlutil = "1.0.2"
[libraries] [libraries]
@@ -133,10 +136,12 @@ firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "fir
firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" } firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" }
flexibleAdapter = { module = "com.github.arkon.FlexibleAdapter:flexible-adapter", version.ref = "flexibleAdapter" } flexibleAdapter = { module = "com.github.arkon.FlexibleAdapter:flexible-adapter", version.ref = "flexibleAdapter" }
image-decoder = { module = "com.github.mihonapp:image-decoder", version.ref = "image-decoder" } image-decoder = { module = "com.github.mihonapp:image-decoder", version.ref = "image-decoder" }
image-decoder2 = { module = "ca.mpreg:imagedecoder", version.ref = "imagedecoder2" }
injekt = { module = "com.github.mihonapp:injekt", version.ref = "injekt" } injekt = { module = "com.github.mihonapp:injekt", version.ref = "injekt" }
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" }
kim = { module = "de.stefan-oltmann:kim", version.ref = "kim" }
kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest-assertions" } kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest-assertions" }
kotlin-compose-compiler-gradle = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin-gradle" } kotlin-compose-compiler-gradle = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin-gradle" }
kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin-gradle" } kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin-gradle" }
@@ -186,6 +191,7 @@ unifile = { module = "com.github.mihon:unifile", version.ref = "unifile" }
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
voyager-tabNavigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" } voyager-tabNavigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" }
voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" } voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" }
webgpuviewer = { module = "ca.mpreg:webgpuviewer", version.ref = "webgpuviewer" }
xmlutil-core = { module = "io.github.pdvrieze.xmlutil:core-android", version.ref = "xmlutil" } xmlutil-core = { module = "io.github.pdvrieze.xmlutil:core-android", version.ref = "xmlutil" }
xmlutil-serialization = { module = "io.github.pdvrieze.xmlutil:serialization", version.ref = "xmlutil" } xmlutil-serialization = { module = "io.github.pdvrieze.xmlutil:serialization", version.ref = "xmlutil" }
@@ -407,6 +407,7 @@
<string name="pref_webtoon_vertical_navigator_on_left">Place vertical navigator on the left side</string> <string name="pref_webtoon_vertical_navigator_on_left">Place vertical navigator on the left side</string>
<string name="pref_show_reading_mode">Show reading mode</string> <string name="pref_show_reading_mode">Show reading mode</string>
<string name="pref_show_reading_mode_summary">Briefly show current mode when reader is opened</string> <string name="pref_show_reading_mode_summary">Briefly show current mode when reader is opened</string>
<string name="pref_high_quality_renderer">Use high quality renderer</string>
<string name="pref_hardware_bitmap_threshold">Custom hardware bitmap threshold</string> <string name="pref_hardware_bitmap_threshold">Custom hardware bitmap threshold</string>
<string name="pref_hardware_bitmap_threshold_default">Default (%d)</string> <string name="pref_hardware_bitmap_threshold_default">Default (%d)</string>
<string name="pref_hardware_bitmap_threshold_summary">If reader loads a blank image incrementally reduce the threshold.\nSelected: %s</string> <string name="pref_hardware_bitmap_threshold_summary">If reader loads a blank image incrementally reduce the threshold.\nSelected: %s</string>
@@ -501,6 +502,28 @@
<string name="pref_low">Low</string> <string name="pref_low">Low</string>
<string name="pref_lowest">Lowest</string> <string name="pref_lowest">Lowest</string>
<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="pref_transition_animation">Transition animation</string>
<string name="transition_animation_default">Default</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>
<string name="transition_animation_stack_right">Stack (right)</string>
<string name="transition_animation_stack_up">Stack (up)</string>
<string name="transition_animation_stack_down">Stack (down)</string>
<string name="transition_animation_sphere">Sphere</string>
<string name="transition_animation_cube_inside">Cube (Inside)</string>
<string name="transition_animation_cube_outside">Cube (Outside)</string>
<string name="transition_animation_fade">Fade</string>
<string name="transition_animation_fade_white">Fade to white</string>
<string name="pref_cutout_mode">Display cutout mode</string>
<string name="cutout_mode_ignore">Ignore</string>
<string name="cutout_mode_avoid">Avoid</string>
<string name="cutout_mode_shift">Shift</string>
<string name="pref_dual_page_view">Dual page view</string>
<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>
<!-- Downloads section --> <!-- Downloads section -->
<string name="pref_category_delete_chapters">Delete chapters</string> <string name="pref_category_delete_chapters">Delete chapters</string>