From a0e2d2942acec860b7cdf58be0a4fa2f917a1fc8 Mon Sep 17 00:00:00 2001 From: Cuong-Tran Date: Tue, 9 Sep 2025 13:20:55 +0700 Subject: [PATCH] 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 --- .../settings/screen/SettingsReaderScreen.kt | 29 ++++++++++---- .../reader/settings/ReadingModePage.kt | 38 +++++++++++++++---- .../ui/reader/setting/ReaderPreferences.kt | 16 ++++++++ .../ui/reader/viewer/webtoon/WebtoonConfig.kt | 11 ++++++ .../viewer/webtoon/WebtoonRecyclerView.kt | 8 +++- .../ui/reader/viewer/webtoon/WebtoonViewer.kt | 32 +++++++++++++++- .../moko-resources/base/strings.xml | 6 +++ 7 files changed, 121 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt index 4938a5352..0ab84f78b 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt @@ -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.ReaderPreferences 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.viewer.pager.PagerConfig import kotlinx.collections.immutable.persistentListOf @@ -258,8 +259,10 @@ object SettingsReaderScreen : SearchableSettings { val rotateToFit by rotateToFitPref.collectAsState() // KMK --> - val pagedDisableZoomIn by readerPreferences.pagedDisableZoomIn().collectAsState() - val landscapeZoom by readerPreferences.landscapeZoom().collectAsState() + val pagedDisableZoomInPref = readerPreferences.pagedDisableZoomIn() + val landscapeZoomPref = readerPreferences.landscapeZoom() + val pagedDisableZoomIn by pagedDisableZoomInPref.collectAsState() + val landscapeZoom by landscapeZoomPref.collectAsState() // KMK <-- return Preference.PreferenceGroup( @@ -313,7 +316,7 @@ object SettingsReaderScreen : SearchableSettings { ), // SY <-- Preference.PreferenceItem.SwitchPreference( - preference = readerPreferences.landscapeZoom(), + preference = landscapeZoomPref, title = stringResource(MR.strings.pref_landscape_zoom), // KMK --> enabled = imageScaleType in zoomWideImagesAllowedList, @@ -322,17 +325,14 @@ object SettingsReaderScreen : SearchableSettings { // KMK --> Preference.PreferenceItem.ListPreference( preference = readerPreferences.landscapeZoomType(), - entries = persistentListOf( - ReaderPreferences.LandscapeZoomScaleType.FIT, - ReaderPreferences.LandscapeZoomScaleType.DOUBLE, - ) + entries = ReaderPreferences.LandscapeZoomScaleType.entries .associateWith { stringResource(it.titleRes) } .toImmutableMap(), title = stringResource(KMR.strings.pref_landscape_zoom_type), enabled = landscapeZoom && imageScaleType in zoomWideImagesAllowedList, ), Preference.PreferenceItem.SwitchPreference( - preference = readerPreferences.pagedDisableZoomIn(), + preference = pagedDisableZoomInPref, title = stringResource(KMR.strings.pref_paged_disable_zoom_in), ), Preference.PreferenceItem.SwitchPreference( @@ -415,6 +415,19 @@ object SettingsReaderScreen : SearchableSettings { title = stringResource(MR.strings.pref_read_with_tapping_inverted), 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( value = webtoonSidePadding, valueRange = ReaderPreferences.let { diff --git a/app/src/main/java/eu/kanade/presentation/reader/settings/ReadingModePage.kt b/app/src/main/java/eu/kanade/presentation/reader/settings/ReadingModePage.kt index e7547dac4..751c16932 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/settings/ReadingModePage.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/settings/ReadingModePage.kt @@ -7,7 +7,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import eu.kanade.domain.manga.model.readerOrientation import eu.kanade.domain.manga.model.readingMode import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation @@ -55,7 +54,12 @@ internal fun ReadingModePage(screenModel: ReaderSettingsScreenModel) { val viewer by screenModel.viewerFlow.collectAsState() if (viewer is WebtoonViewer) { - WebtoonViewerSettings(screenModel) + WebtoonViewerSettings( + screenModel, + // KMK --> + readingMode, + // KMK <-- + ) // SY --> WebtoonWithGapsViewerSettings(screenModel) // SY <-- @@ -125,11 +129,7 @@ private fun PagerViewerSettings(screenModel: ReaderSettingsScreenModel) { ) // KMK --> - if (imageScaleType in listOf( - SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE, - SubsamplingScaleImageView.SCALE_TYPE_ORIGINAL_SIZE, - ) - ) { + if (imageScaleType in ReaderPreferences.zoomWideImagesAllowedList) { // KMK <-- CheckboxItem( label = stringResource(MR.strings.pref_landscape_zoom), @@ -207,7 +207,12 @@ private fun PagerViewerSettings(screenModel: ReaderSettingsScreenModel) { } @Composable -private fun WebtoonViewerSettings(screenModel: ReaderSettingsScreenModel) { +private fun WebtoonViewerSettings( + screenModel: ReaderSettingsScreenModel, + // KMK --> + readingMode: ReadingMode, + // KMK <-- +) { val numberFormat = remember { NumberFormat.getPercentInstance() } HeadingItem(MR.strings.webtoon_viewer) @@ -221,6 +226,23 @@ private fun WebtoonViewerSettings(screenModel: ReaderSettingsScreenModel) { 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() SliderItem( value = webtoonSidePadding, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt index 9d54ca49f..7871107b6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt @@ -63,6 +63,11 @@ class ReaderPreferences( 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 readerTheme() = preferenceStore.getInt("pref_reader_theme_key", 1) @@ -237,6 +242,17 @@ class ReaderPreferences( FIT(MR.strings.scale_type_fit_screen), 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 <-- object ArchiveReaderMode { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt index 45bf57032..bc1e178e5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt @@ -47,6 +47,11 @@ class WebtoonConfig( private set var pinchToZoomChangedListener: ((Boolean) -> Unit)? = null + + var webtoonScaleType = readerPreferences.webtoonScaleType().get() + private set + + var webtoonScaleTypeChangedListener: ((ReaderPreferences.WebtoonScaleType) -> Unit)? = null // KMK <-- // SY --> @@ -115,6 +120,12 @@ class WebtoonConfig( { pinchToZoom = it }, { pinchToZoomChangedListener?.invoke(it) }, ) + + readerPreferences.webtoonScaleType() + .register( + { webtoonScaleType = it }, + { webtoonScaleTypeChangedListener?.invoke(it) }, + ) // KMK <-- readerPreferences.readerTheme().changes() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt index 8624d51be..8f865b843 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt @@ -180,9 +180,13 @@ class WebtoonRecyclerView @JvmOverloads constructor( fun onScale(scaleFactor: Float) { // KMK -> if (!detector.isQuickScaling && !pinchToZoom) return - // KMK <-- - currentScale *= scaleFactor + scaleTo(currentScale * scaleFactor) + } + + fun scaleTo(scale: Float) { + // KMK <-- + currentScale = scale currentScale = currentScale.coerceIn( minRate, MAX_SCALE_RATE, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt index 86d29399b..c0154e68f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt @@ -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.viewer.Viewer import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion +import exh.util.nullIfZero import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel import tachiyomi.core.common.util.system.logcat @@ -40,6 +41,7 @@ class WebtoonViewer( private val tapByPage: Boolean = false, // KMK --> @ColorInt private val seedColor: Int? = null, + private val readerPreferences: ReaderPreferences = Injekt.get(), // KMK <-- ) : Viewer { @@ -89,7 +91,9 @@ class WebtoonViewer( var currentPage: Any? = null private val threshold: Int = - Injekt.get() + // KMK --> + readerPreferences + // KMK <-- .readerHideThreshold() .get() .threshold @@ -173,6 +177,32 @@ class WebtoonViewer( config.pinchToZoomChangedListener = { 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 <-- config.zoomPropertyChangedListener = { diff --git a/i18n-kmk/src/commonMain/moko-resources/base/strings.xml b/i18n-kmk/src/commonMain/moko-resources/base/strings.xml index 9c2b41d9d..46a3c99aa 100644 --- a/i18n-kmk/src/commonMain/moko-resources/base/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/base/strings.xml @@ -29,6 +29,12 @@ Disable zoom in Wide images zoom mode Double zoom + Smart scale on wide screen + Apply Smart scale to Long strip with gaps + 16:9 + 20:9 + 4:3 + 3:2