Add high quality WebGPU renderer (#3388)
This commit is contained in:
@@ -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() }
|
||||||
|
|
||||||
|
|||||||
+4
@@ -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
+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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user