Add foldable device support (auto two-page)

This commit is contained in:
skepsun
2025-10-28 17:40:31 +08:00
committed by Koitharu
parent e2fcfcc7a8
commit c37458d43a
10 changed files with 121 additions and 40 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -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<FoldingFeature>().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) {

View File

@@ -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)

View File

@@ -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 {

View File

@@ -130,6 +130,21 @@
android:textColor="?colorOnSurfaceVariant"
app:drawableStartCompat="@drawable/ic_split_horizontal" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_double_foldable"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_small"
android:drawablePadding="?android:listPreferredItemPaddingStart"
android:minHeight="?android:listPreferredItemHeightSmall"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd"
android:text="@string/auto_double_foldable"
android:textAppearance="@style/TextAppearance.Kotatsu.GridTitle"
android:textColor="?colorOnSurfaceVariant"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/text_double_sensitivity"
android:layout_width="match_parent"

View File

@@ -866,4 +866,5 @@
<string name="pull_bottom_no_next">到底了</string>
<string name="enable_pull_gesture_title">启用推拉手势</string>
<string name="enable_pull_gesture_summary">条漫模式下使用推拉手势切换章节</string>
<string name="auto_double_foldable">折叠设备自动双页</string>
</resources>

View File

@@ -661,4 +661,5 @@
<string name="download_new_chapters">下載新的漫畫章節</string>
<string name="enable_all_sources">啟用所有漫畫來源</string>
<string name="all_sources_enabled">所有漫畫來源已啟用</string>
<string name="auto_double_foldable">摺疊設備自動雙頁</string>
</resources>

View File

@@ -579,6 +579,8 @@
<string name="none">None</string>
<string name="config_reset_confirm">Reset settings to default values? This action cannot be undone.</string>
<string name="use_two_pages_landscape">Use two pages layout on landscape orientation (beta)</string>
<string name="auto_double_foldable">Auto Two-Page On Foldable</string>
<string name="auto_double_foldable_summary">Enable two-page layout when device has a separating hinge</string>
<string name="two_page_scroll_sensitivity">Two-Page Scroll Sensitivity</string>
<string name="default_webtoon_zoom_out">Default webtoon zoom out</string>
<string name="fullscreen_mode">Fullscreen mode</string>

View File

@@ -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" }