diff --git a/app/build.gradle b/app/build.gradle index 541d2759f..8e1e8f6f8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -155,6 +155,9 @@ dependencies { implementation libs.androidx.work.runtime implementation libs.guava + // Foldable/Window layout + implementation libs.androidx.window + implementation libs.androidx.room.runtime implementation libs.androidx.room.ktx ksp libs.androidx.room.compiler diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 43c09daef..dd0555871 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -138,6 +138,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { get() = prefs.getBoolean(KEY_READER_DOUBLE_PAGES, false) set(value) = prefs.edit { putBoolean(KEY_READER_DOUBLE_PAGES, value) } + var isReaderDoubleOnFoldable: Boolean + get() = prefs.getBoolean(KEY_READER_DOUBLE_FOLDABLE, false) + set(value) = prefs.edit { putBoolean(KEY_READER_DOUBLE_FOLDABLE, value) } + @get:FloatRange(0.0, 1.0) var readerDoublePagesSensitivity: Float get() = prefs.getFloat(KEY_READER_DOUBLE_PAGES_SENSITIVITY, 0.5f) @@ -682,6 +686,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_LOCAL_STORAGE = "local_storage" const val KEY_READER_DOUBLE_PAGES = "reader_double_pages" const val KEY_READER_DOUBLE_PAGES_SENSITIVITY = "reader_double_pages_sensitivity" + const val KEY_READER_DOUBLE_FOLDABLE = "reader_double_foldable" const val KEY_READER_ZOOM_BUTTONS = "reader_zoom_buttons" const val KEY_READER_CONTROL_LTR = "reader_taps_ltr" const val KEY_READER_NAVIGATION_INVERTED = "reader_navigation_inverted" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index 8fded84e9..972500def 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -24,10 +24,15 @@ import androidx.transition.Fade import androidx.transition.Slide import androidx.transition.TransitionManager import androidx.transition.TransitionSet +import androidx.window.layout.FoldingFeature +import androidx.window.layout.WindowInfoTracker +import android.content.res.Configuration import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn @@ -107,8 +112,10 @@ class ReaderActivity : private lateinit var touchHelper: TapGridDispatcher private lateinit var controlDelegate: ReaderControlDelegate private var gestureInsets: Insets = Insets.NONE - private lateinit var readerManager: ReaderManager - private val hideUiRunnable = Runnable { setUiIsVisible(false) } + private lateinit var readerManager: ReaderManager + private val hideUiRunnable = Runnable { setUiIsVisible(false) } + // Tracks whether the foldable device is in an unfolded state (half-opened or flat) + private var isFoldUnfolded: Boolean = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -186,8 +193,27 @@ class ReaderActivity : viewModel.isZoomControlsEnabled.observe(this) { viewBinding.zoomControl.isVisible = it } - addMenuProvider(ReaderMenuProvider(viewModel)) - } + addMenuProvider(ReaderMenuProvider(viewModel)) + + // Observe foldable window layout to auto-enable double-page if configured + WindowInfoTracker.getOrCreate(this) + .windowLayoutInfo(this) + .onEach { info -> + val fold = info.displayFeatures.filterIsInstance().firstOrNull() + val unfolded = when (fold?.state) { + FoldingFeature.State.HALF_OPENED, FoldingFeature.State.FLAT -> true + else -> false + } + if (unfolded != isFoldUnfolded) { + isFoldUnfolded = unfolded + applyDoubleModeAuto() + } + } + .launchIn(lifecycleScope) + + // Apply initial double-mode considering foldable setting + applyDoubleModeAuto() + } override fun getParentActivityIntent(): Intent? { val manga = viewModel.getMangaOrNull() ?: return null @@ -340,9 +366,19 @@ class ReaderActivity : viewBinding.timerControl.onReaderModeChanged(mode) } - override fun onDoubleModeChanged(isEnabled: Boolean) { - readerManager.setDoubleReaderMode(isEnabled) - } + override fun onDoubleModeChanged(isEnabled: Boolean) { + // Combine manual toggle with foldable auto setting + applyDoubleModeAuto(isEnabled) + } + + private fun applyDoubleModeAuto(manualEnabled: Boolean? = null) { + val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + // Auto double-page on foldable when device is unfolded (half-opened or flat) + val autoFoldable = settings.isReaderDoubleOnFoldable && isFoldUnfolded + val manualLandscape = (manualEnabled ?: settings.isReaderDoubleOnLandscape) && isLandscape + val autoEnabled = autoFoldable || manualLandscape + readerManager.setDoubleReaderMode(autoEnabled) + } private fun setKeepScreenOn(isKeep: Boolean) { if (isKeep) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderManager.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderManager.kt index 53069dd1b..d263d7fd8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderManager.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderManager.kt @@ -49,7 +49,7 @@ class ReaderManager( fun setDoubleReaderMode(isEnabled: Boolean) { val mode = currentMode val prevReader = currentReader?.javaClass - invalidateTypesMap(isEnabled && isLandscape()) + invalidateTypesMap(isEnabled) val newReader = modeMap[mode] if (mode != null && newReader != prevReader) { replace(mode) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt index 856cdd9c8..968611913 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt @@ -79,21 +79,23 @@ class ReaderConfigSheet : return SheetReaderConfigBinding.inflate(inflater, container, false) } - override fun onViewBindingCreated( - binding: SheetReaderConfigBinding, - savedInstanceState: Bundle?, - ) { - super.onViewBindingCreated(binding, savedInstanceState) - observeScreenOrientation() - binding.buttonStandard.isChecked = mode == ReaderMode.STANDARD - binding.buttonReversed.isChecked = mode == ReaderMode.REVERSED - binding.buttonWebtoon.isChecked = mode == ReaderMode.WEBTOON - binding.buttonVertical.isChecked = mode == ReaderMode.VERTICAL - binding.switchDoubleReader.isChecked = settings.isReaderDoubleOnLandscape - binding.switchDoubleReader.isEnabled = mode == ReaderMode.STANDARD || mode == ReaderMode.REVERSED - binding.sliderDoubleSensitivity.setValueRounded(settings.readerDoublePagesSensitivity * 100f) - binding.sliderDoubleSensitivity.setLabelFormatter(IntPercentLabelFormatter(binding.root.context)) - binding.adjustSensitivitySlider(withAnimation = false) + override fun onViewBindingCreated( + binding: SheetReaderConfigBinding, + savedInstanceState: Bundle?, + ) { + super.onViewBindingCreated(binding, savedInstanceState) + observeScreenOrientation() + binding.buttonStandard.isChecked = mode == ReaderMode.STANDARD + binding.buttonReversed.isChecked = mode == ReaderMode.REVERSED + binding.buttonWebtoon.isChecked = mode == ReaderMode.WEBTOON + binding.buttonVertical.isChecked = mode == ReaderMode.VERTICAL + binding.switchDoubleReader.isChecked = settings.isReaderDoubleOnLandscape + binding.switchDoubleReader.isEnabled = mode == ReaderMode.STANDARD || mode == ReaderMode.REVERSED + binding.switchDoubleFoldable.isChecked = settings.isReaderDoubleOnFoldable + binding.switchDoubleFoldable.isEnabled = binding.switchDoubleReader.isEnabled + binding.sliderDoubleSensitivity.setValueRounded(settings.readerDoublePagesSensitivity * 100f) + binding.sliderDoubleSensitivity.setLabelFormatter(IntPercentLabelFormatter(binding.root.context)) + binding.adjustSensitivitySlider(withAnimation = false) binding.checkableGroup.addOnButtonCheckedListener(this) binding.buttonSavePage.setOnClickListener(this) @@ -102,9 +104,10 @@ class ReaderConfigSheet : binding.buttonImageServer.setOnClickListener(this) binding.buttonColorFilter.setOnClickListener(this) binding.buttonScrollTimer.setOnClickListener(this) - binding.buttonBookmark.setOnClickListener(this) - binding.switchDoubleReader.setOnCheckedChangeListener(this) - binding.sliderDoubleSensitivity.addOnChangeListener(this) + binding.buttonBookmark.setOnClickListener(this) + binding.switchDoubleReader.setOnCheckedChangeListener(this) + binding.switchDoubleFoldable.setOnCheckedChangeListener(this) + binding.sliderDoubleSensitivity.addOnChangeListener(this) viewModel.isBookmarkAdded.observe(viewLifecycleOwner) { binding.buttonBookmark.setText(if (it) R.string.bookmark_remove else R.string.bookmark_add) @@ -171,19 +174,25 @@ class ReaderConfigSheet : } } - override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) { - when (buttonView.id) { + override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) { + when (buttonView.id) { R.id.switch_screen_lock_rotation -> { orientationHelper.isLocked = isChecked } - R.id.switch_double_reader -> { - settings.isReaderDoubleOnLandscape = isChecked - viewBinding?.adjustSensitivitySlider(withAnimation = true) - findParentCallback(Callback::class.java)?.onDoubleModeChanged(isChecked) + R.id.switch_double_reader -> { + settings.isReaderDoubleOnLandscape = isChecked + viewBinding?.adjustSensitivitySlider(withAnimation = true) + findParentCallback(Callback::class.java)?.onDoubleModeChanged(isChecked) + } + + R.id.switch_double_foldable -> { + settings.isReaderDoubleOnFoldable = isChecked + // Re-evaluate double-page considering foldable state and current manual toggle + findParentCallback(Callback::class.java)?.onDoubleModeChanged(settings.isReaderDoubleOnLandscape) + } } } - } override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) { settings.readerDoublePagesSensitivity = value / 100f @@ -205,9 +214,10 @@ class ReaderConfigSheet : else -> return } viewBinding?.run { - switchDoubleReader.isEnabled = newMode == ReaderMode.STANDARD || newMode == ReaderMode.REVERSED - adjustSensitivitySlider(withAnimation = true) - } + switchDoubleReader.isEnabled = newMode == ReaderMode.STANDARD || newMode == ReaderMode.REVERSED + switchDoubleFoldable.isEnabled = switchDoubleReader.isEnabled + adjustSensitivitySlider(withAnimation = true) + } if (newMode == mode) { return } @@ -242,12 +252,18 @@ class ReaderConfigSheet : } private fun SheetReaderConfigBinding.adjustSensitivitySlider(withAnimation: Boolean) { - val isSliderVisible = switchDoubleReader.isEnabled && switchDoubleReader.isChecked - if (isSliderVisible != sliderDoubleSensitivity.isVisible && withAnimation) { + val isSubOptionsVisible = switchDoubleReader.isEnabled && switchDoubleReader.isChecked + val needTransition = withAnimation && ( + (isSubOptionsVisible != sliderDoubleSensitivity.isVisible) || + (isSubOptionsVisible != textDoubleSensitivity.isVisible) || + (isSubOptionsVisible != switchDoubleFoldable.isVisible) + ) + if (needTransition) { TransitionManager.beginDelayedTransition(layoutMain) } - sliderDoubleSensitivity.isVisible = isSliderVisible - textDoubleSensitivity.isVisible = isSliderVisible + sliderDoubleSensitivity.isVisible = isSubOptionsVisible + textDoubleSensitivity.isVisible = isSubOptionsVisible + switchDoubleFoldable.isVisible = isSubOptionsVisible } interface Callback { diff --git a/app/src/main/res/layout/sheet_reader_config.xml b/app/src/main/res/layout/sheet_reader_config.xml index 3aa0662df..24bb3d3ff 100644 --- a/app/src/main/res/layout/sheet_reader_config.xml +++ b/app/src/main/res/layout/sheet_reader_config.xml @@ -130,6 +130,21 @@ android:textColor="?colorOnSurfaceVariant" app:drawableStartCompat="@drawable/ic_split_horizontal" /> + + 到底了 启用推拉手势 条漫模式下使用推拉手势切换章节 + 折叠设备自动双页 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index eb1715fed..59c7b8df5 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -661,4 +661,5 @@ 下載新的漫畫章節 啟用所有漫畫來源 所有漫畫來源已啟用 + 摺疊設備自動雙頁 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 898db1cfc..86eed9617 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -579,6 +579,8 @@ None Reset settings to default values? This action cannot be undone. Use two pages layout on landscape orientation (beta) + Auto Two-Page On Foldable + Enable two-page layout when device has a separating hinge Two-Page Scroll Sensitivity Default webtoon zoom out Fullscreen mode diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4d41b8028..44c997942 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -49,6 +49,7 @@ viewpager2 = "1.1.0" webkit = "1.14.0" workRuntime = "2.10.5" workinspector = "1.2" +window = "1.3.0" [libraries] acra-dialog = { module = "ch.acra:acra-dialog", version.ref = "acra" } @@ -115,6 +116,7 @@ okhttp-tls = { module = "com.squareup.okhttp3:okhttp-tls", version.ref = "okhttp okio = { module = "com.squareup.okio:okio", version.ref = "okio" } ssiv = { module = "com.github.KotatsuApp:subsampling-scale-image-view", version.ref = "ssiv" } workinspector = { module = "com.github.Koitharu:WorkInspector", version.ref = "workinspector" } +androidx-window = { module = "androidx.window:window", version.ref = "window" } [plugins] android-application = { id = "com.android.application", version.ref = "gradle" }