feat(webtoon): Adaptive scale for wider screen size (#1130)

* feat: Add webtoon scale type option

This commit introduces a new setting for webtoon reading mode that allows users to select a scale type for webtoon images.

The available scale types are:
- Fit Screen
- 16:9
- 20:9
- 4:3
- 3:2

This setting can be found in both the reader settings sheet and the global reader settings.

* mutable

* add to Reader's settings page & fix size calculation

* option for long strip gap preference

* fix: ensure recycler scales to default when screen ratio is less than desired

* hide smart scale for long strip gap preference

* Avoid zero division

* using forEach

* improve readability

* better viewer size calculation & optimize list
This commit is contained in:
Cuong-Tran 2025-09-09 13:20:55 +07:00 committed by GitHub
parent 9f7309fd2a
commit a0e2d2942a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 121 additions and 19 deletions

View file

@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderBottomButton
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation 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.ReaderPreferences.Companion.zoomWideImagesAllowedList import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences.Companion.zoomWideImagesAllowedList
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences.WebtoonScaleType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -258,8 +259,10 @@ object SettingsReaderScreen : SearchableSettings {
val rotateToFit by rotateToFitPref.collectAsState() val rotateToFit by rotateToFitPref.collectAsState()
// KMK --> // KMK -->
val pagedDisableZoomIn by readerPreferences.pagedDisableZoomIn().collectAsState() val pagedDisableZoomInPref = readerPreferences.pagedDisableZoomIn()
val landscapeZoom by readerPreferences.landscapeZoom().collectAsState() val landscapeZoomPref = readerPreferences.landscapeZoom()
val pagedDisableZoomIn by pagedDisableZoomInPref.collectAsState()
val landscapeZoom by landscapeZoomPref.collectAsState()
// KMK <-- // KMK <--
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
@ -313,7 +316,7 @@ object SettingsReaderScreen : SearchableSettings {
), ),
// SY <-- // SY <--
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.landscapeZoom(), preference = landscapeZoomPref,
title = stringResource(MR.strings.pref_landscape_zoom), title = stringResource(MR.strings.pref_landscape_zoom),
// KMK --> // KMK -->
enabled = imageScaleType in zoomWideImagesAllowedList, enabled = imageScaleType in zoomWideImagesAllowedList,
@ -322,17 +325,14 @@ object SettingsReaderScreen : SearchableSettings {
// KMK --> // KMK -->
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = readerPreferences.landscapeZoomType(), preference = readerPreferences.landscapeZoomType(),
entries = persistentListOf( entries = ReaderPreferences.LandscapeZoomScaleType.entries
ReaderPreferences.LandscapeZoomScaleType.FIT,
ReaderPreferences.LandscapeZoomScaleType.DOUBLE,
)
.associateWith { stringResource(it.titleRes) } .associateWith { stringResource(it.titleRes) }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(KMR.strings.pref_landscape_zoom_type), title = stringResource(KMR.strings.pref_landscape_zoom_type),
enabled = landscapeZoom && imageScaleType in zoomWideImagesAllowedList, enabled = landscapeZoom && imageScaleType in zoomWideImagesAllowedList,
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.pagedDisableZoomIn(), preference = pagedDisableZoomInPref,
title = stringResource(KMR.strings.pref_paged_disable_zoom_in), title = stringResource(KMR.strings.pref_paged_disable_zoom_in),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
@ -415,6 +415,19 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_read_with_tapping_inverted), title = stringResource(MR.strings.pref_read_with_tapping_inverted),
enabled = navMode != 5, enabled = navMode != 5,
), ),
// KMK -->
Preference.PreferenceItem.ListPreference(
preference = readerPreferences.webtoonScaleType(),
entries = WebtoonScaleType.entries
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
title = stringResource(KMR.strings.pref_webtoon_scale_type),
),
Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.longStripGapSmartScale(),
title = stringResource(KMR.strings.pref_smart_scale_long_strip_gap),
),
// KMK <--
Preference.PreferenceItem.SliderPreference( Preference.PreferenceItem.SliderPreference(
value = webtoonSidePadding, value = webtoonSidePadding,
valueRange = ReaderPreferences.let { valueRange = ReaderPreferences.let {

View file

@ -7,7 +7,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import eu.kanade.domain.manga.model.readerOrientation import eu.kanade.domain.manga.model.readerOrientation
import eu.kanade.domain.manga.model.readingMode import eu.kanade.domain.manga.model.readingMode
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
@ -55,7 +54,12 @@ internal fun ReadingModePage(screenModel: ReaderSettingsScreenModel) {
val viewer by screenModel.viewerFlow.collectAsState() val viewer by screenModel.viewerFlow.collectAsState()
if (viewer is WebtoonViewer) { if (viewer is WebtoonViewer) {
WebtoonViewerSettings(screenModel) WebtoonViewerSettings(
screenModel,
// KMK -->
readingMode,
// KMK <--
)
// SY --> // SY -->
WebtoonWithGapsViewerSettings(screenModel) WebtoonWithGapsViewerSettings(screenModel)
// SY <-- // SY <--
@ -125,11 +129,7 @@ private fun PagerViewerSettings(screenModel: ReaderSettingsScreenModel) {
) )
// KMK --> // KMK -->
if (imageScaleType in listOf( if (imageScaleType in ReaderPreferences.zoomWideImagesAllowedList) {
SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE,
SubsamplingScaleImageView.SCALE_TYPE_ORIGINAL_SIZE,
)
) {
// KMK <-- // KMK <--
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_landscape_zoom), label = stringResource(MR.strings.pref_landscape_zoom),
@ -207,7 +207,12 @@ private fun PagerViewerSettings(screenModel: ReaderSettingsScreenModel) {
} }
@Composable @Composable
private fun WebtoonViewerSettings(screenModel: ReaderSettingsScreenModel) { private fun WebtoonViewerSettings(
screenModel: ReaderSettingsScreenModel,
// KMK -->
readingMode: ReadingMode,
// KMK <--
) {
val numberFormat = remember { NumberFormat.getPercentInstance() } val numberFormat = remember { NumberFormat.getPercentInstance() }
HeadingItem(MR.strings.webtoon_viewer) HeadingItem(MR.strings.webtoon_viewer)
@ -221,6 +226,23 @@ private fun WebtoonViewerSettings(screenModel: ReaderSettingsScreenModel) {
onSelectInvertMode = screenModel.preferences.webtoonNavInverted()::set, onSelectInvertMode = screenModel.preferences.webtoonNavInverted()::set,
) )
// KMK -->
val webtoonScaleTypePref = screenModel.preferences.webtoonScaleType()
val webtoonScaleType by webtoonScaleTypePref.collectAsState()
val webtoonSmartScaleLongStripGap = screenModel.preferences.longStripGapSmartScale().get()
if (readingMode != ReadingMode.CONTINUOUS_VERTICAL || webtoonSmartScaleLongStripGap) {
SettingsChipRow(KMR.strings.pref_webtoon_scale_type) {
ReaderPreferences.WebtoonScaleType.entries.forEach { scaleType ->
FilterChip(
selected = webtoonScaleType == scaleType,
onClick = { webtoonScaleTypePref.set(scaleType) },
label = { Text(stringResource(scaleType.titleRes)) },
)
}
}
}
// KMK <--
val webtoonSidePadding by screenModel.preferences.webtoonSidePadding().collectAsState() val webtoonSidePadding by screenModel.preferences.webtoonSidePadding().collectAsState()
SliderItem( SliderItem(
value = webtoonSidePadding, value = webtoonSidePadding,

View file

@ -63,6 +63,11 @@ class ReaderPreferences(
fun imageScaleType() = preferenceStore.getInt("pref_image_scale_type_key", 1) fun imageScaleType() = preferenceStore.getInt("pref_image_scale_type_key", 1)
// KMK -->
fun webtoonScaleType() = preferenceStore.getEnum("pref_webtoon_scale_type_key", WebtoonScaleType.FIT)
fun longStripGapSmartScale() = preferenceStore.getBoolean("pref_webtoon_smart_scale_long_strip_gap", false)
// KMK <--
fun zoomStart() = preferenceStore.getInt("pref_zoom_start_key", 1) fun zoomStart() = preferenceStore.getInt("pref_zoom_start_key", 1)
fun readerTheme() = preferenceStore.getInt("pref_reader_theme_key", 1) fun readerTheme() = preferenceStore.getInt("pref_reader_theme_key", 1)
@ -237,6 +242,17 @@ class ReaderPreferences(
FIT(MR.strings.scale_type_fit_screen), FIT(MR.strings.scale_type_fit_screen),
DOUBLE(KMR.strings.scale_type_double), DOUBLE(KMR.strings.scale_type_double),
} }
enum class WebtoonScaleType(
val titleRes: StringResource,
val ratio: Float,
) {
FIT(MR.strings.scale_type_fit_screen, 0f),
R4_3(KMR.strings.scale_type_4_3, 3f / 4f),
R3_2(KMR.strings.scale_type_3_2, 2f / 3f),
R16_9(KMR.strings.scale_type_16_9, 9f / 16f),
R20_9(KMR.strings.scale_type_20_9, 9f / 20f),
}
// KMK <-- // KMK <--
object ArchiveReaderMode { object ArchiveReaderMode {

View file

@ -47,6 +47,11 @@ class WebtoonConfig(
private set private set
var pinchToZoomChangedListener: ((Boolean) -> Unit)? = null var pinchToZoomChangedListener: ((Boolean) -> Unit)? = null
var webtoonScaleType = readerPreferences.webtoonScaleType().get()
private set
var webtoonScaleTypeChangedListener: ((ReaderPreferences.WebtoonScaleType) -> Unit)? = null
// KMK <-- // KMK <--
// SY --> // SY -->
@ -115,6 +120,12 @@ class WebtoonConfig(
{ pinchToZoom = it }, { pinchToZoom = it },
{ pinchToZoomChangedListener?.invoke(it) }, { pinchToZoomChangedListener?.invoke(it) },
) )
readerPreferences.webtoonScaleType()
.register(
{ webtoonScaleType = it },
{ webtoonScaleTypeChangedListener?.invoke(it) },
)
// KMK <-- // KMK <--
readerPreferences.readerTheme().changes() readerPreferences.readerTheme().changes()

View file

@ -180,9 +180,13 @@ class WebtoonRecyclerView @JvmOverloads constructor(
fun onScale(scaleFactor: Float) { fun onScale(scaleFactor: Float) {
// KMK -> // KMK ->
if (!detector.isQuickScaling && !pinchToZoom) return if (!detector.isQuickScaling && !pinchToZoom) return
// KMK <--
currentScale *= scaleFactor scaleTo(currentScale * scaleFactor)
}
fun scaleTo(scale: Float) {
// KMK <--
currentScale = scale
currentScale = currentScale.coerceIn( currentScale = currentScale.coerceIn(
minRate, minRate,
MAX_SCALE_RATE, MAX_SCALE_RATE,

View file

@ -21,6 +21,7 @@ import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.viewer.Viewer import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
import exh.util.nullIfZero
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
@ -40,6 +41,7 @@ class WebtoonViewer(
private val tapByPage: Boolean = false, private val tapByPage: Boolean = false,
// KMK --> // KMK -->
@ColorInt private val seedColor: Int? = null, @ColorInt private val seedColor: Int? = null,
private val readerPreferences: ReaderPreferences = Injekt.get(),
// KMK <-- // KMK <--
) : Viewer { ) : Viewer {
@ -89,7 +91,9 @@ class WebtoonViewer(
var currentPage: Any? = null var currentPage: Any? = null
private val threshold: Int = private val threshold: Int =
Injekt.get<ReaderPreferences>() // KMK -->
readerPreferences
// KMK <--
.readerHideThreshold() .readerHideThreshold()
.get() .get()
.threshold .threshold
@ -173,6 +177,32 @@ class WebtoonViewer(
config.pinchToZoomChangedListener = { config.pinchToZoomChangedListener = {
frame.pinchToZoom = it frame.pinchToZoom = it
} }
config.webtoonScaleTypeChangedListener = f@{ scaleType ->
if (!isContinuous && !readerPreferences.longStripGapSmartScale().get()) return@f
recycler.post {
if (scaleType == ReaderPreferences.WebtoonScaleType.FIT) {
recycler.scaleTo(1f)
return@post
}
// Call `scaleTo` after the view is loaded and visible
val currentWidth = recycler.width.takeIf { it > 0 } ?: activity.window.decorView.width.nullIfZero() ?: return@post
val currentHeight = recycler.originalHeight.takeIf { it > 0 } ?: activity.window.decorView.height.nullIfZero() ?: return@post
val desiredRatio = scaleType.ratio
val screenRatio = currentWidth.toFloat() / currentHeight
val desiredWidth = currentHeight * desiredRatio
val desiredScale = desiredWidth / currentWidth
if (screenRatio > desiredRatio) {
recycler.scaleTo(desiredScale)
} else {
recycler.scaleTo(1f)
}
}
}
// KMK <-- // KMK <--
config.zoomPropertyChangedListener = { config.zoomPropertyChangedListener = {

View file

@ -29,6 +29,12 @@
<string name="pref_paged_disable_zoom_in">Disable zoom in</string> <string name="pref_paged_disable_zoom_in">Disable zoom in</string>
<string name="pref_landscape_zoom_type">Wide images zoom mode</string> <string name="pref_landscape_zoom_type">Wide images zoom mode</string>
<string name="scale_type_double">Double zoom</string> <string name="scale_type_double">Double zoom</string>
<string name="pref_webtoon_scale_type">Smart scale on wide screen</string>
<string name="pref_smart_scale_long_strip_gap">Apply Smart scale to Long strip with gaps</string>
<string name="scale_type_16_9">16:9</string>
<string name="scale_type_20_9">20:9</string>
<string name="scale_type_4_3">4:3</string>
<string name="scale_type_3_2">3:2</string>
<!-- Preferences --> <!-- Preferences -->
<!-- Appearance section --> <!-- Appearance section -->