Add high quality WebGPU renderer (#3388)
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import coil3.Canvas
|
||||
import coil3.Image
|
||||
import coil3.ImageLoader
|
||||
import coil3.asImage
|
||||
import coil3.decode.DecodeResult
|
||||
@@ -62,7 +64,9 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
|
||||
class Factory : Decoder.Factory {
|
||||
|
||||
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)
|
||||
} else {
|
||||
null
|
||||
@@ -88,3 +92,27 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
|
||||
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)
|
||||
|
||||
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.viewer.ReaderProgressIndicator
|
||||
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.util.system.isNightMode
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
@@ -479,7 +480,7 @@ class ReaderActivity : BaseActivity() {
|
||||
onShare = ::shareChapter.takeIf { isHttpSource },
|
||||
|
||||
chapterNavigatorType = if (!verticalNavigator) {
|
||||
if (state.viewer is R2LPagerViewer) {
|
||||
if (state.viewer is R2LPagerViewer || (state.viewer as? WebGpuViewer)?.isReversed ?: false) {
|
||||
ChapterNavigatorType.HORIZONTAL_RTL
|
||||
} else {
|
||||
ChapterNavigatorType.HORIZONTAL_LTR
|
||||
|
||||
@@ -132,6 +132,11 @@ class ReaderPreferences(
|
||||
false,
|
||||
)
|
||||
|
||||
val dualPageView: Preference<DualPageView> = preferenceStore.getEnum(
|
||||
"pref_dual_page_view",
|
||||
DualPageView.NEVER,
|
||||
)
|
||||
|
||||
// endregion
|
||||
|
||||
// region Color filter
|
||||
@@ -189,6 +194,15 @@ class ReaderPreferences(
|
||||
|
||||
// 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 {
|
||||
BLACK,
|
||||
WHITE,
|
||||
@@ -201,8 +215,14 @@ class ReaderPreferences(
|
||||
val shouldInvertVertical: Boolean = false,
|
||||
) {
|
||||
NONE(MR.strings.tapping_inverted_none),
|
||||
HORIZONTAL(MR.strings.tapping_inverted_horizontal, shouldInvertHorizontal = true),
|
||||
VERTICAL(MR.strings.tapping_inverted_vertical, shouldInvertVertical = true),
|
||||
HORIZONTAL(
|
||||
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),
|
||||
}
|
||||
|
||||
@@ -213,6 +233,41 @@ class ReaderPreferences(
|
||||
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 {
|
||||
const val WEBTOON_PADDING_MIN = 0
|
||||
const val WEBTOON_PADDING_MAX = 25
|
||||
|
||||
@@ -2,14 +2,19 @@ package eu.kanade.tachiyomi.ui.reader.setting
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
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.R2LPagerViewer
|
||||
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 tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
enum class ReadingMode(
|
||||
val stringRes: StringResource,
|
||||
@@ -67,6 +72,16 @@ enum class ReadingMode(
|
||||
}
|
||||
|
||||
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)) {
|
||||
LEFT_TO_RIGHT -> L2RPagerViewer(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
+26
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user