Add foldable device support (auto two-page)

devel
skepsun 6 months ago committed by Koitharu
parent e2fcfcc7a8
commit c37458d43a

@ -155,6 +155,9 @@ dependencies {
implementation libs.androidx.work.runtime implementation libs.androidx.work.runtime
implementation libs.guava implementation libs.guava
// Foldable/Window layout
implementation libs.androidx.window
implementation libs.androidx.room.runtime implementation libs.androidx.room.runtime
implementation libs.androidx.room.ktx implementation libs.androidx.room.ktx
ksp libs.androidx.room.compiler ksp libs.androidx.room.compiler

@ -138,6 +138,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getBoolean(KEY_READER_DOUBLE_PAGES, false) get() = prefs.getBoolean(KEY_READER_DOUBLE_PAGES, false)
set(value) = prefs.edit { putBoolean(KEY_READER_DOUBLE_PAGES, value) } 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) @get:FloatRange(0.0, 1.0)
var readerDoublePagesSensitivity: Float var readerDoublePagesSensitivity: Float
get() = prefs.getFloat(KEY_READER_DOUBLE_PAGES_SENSITIVITY, 0.5f) 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_LOCAL_STORAGE = "local_storage"
const val KEY_READER_DOUBLE_PAGES = "reader_double_pages" 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_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_ZOOM_BUTTONS = "reader_zoom_buttons"
const val KEY_READER_CONTROL_LTR = "reader_taps_ltr" const val KEY_READER_CONTROL_LTR = "reader_taps_ltr"
const val KEY_READER_NAVIGATION_INVERTED = "reader_navigation_inverted" const val KEY_READER_NAVIGATION_INVERTED = "reader_navigation_inverted"

@ -24,10 +24,15 @@ import androidx.transition.Fade
import androidx.transition.Slide import androidx.transition.Slide
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import androidx.transition.TransitionSet 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 com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
@ -109,6 +114,8 @@ class ReaderActivity :
private var gestureInsets: Insets = Insets.NONE private var gestureInsets: Insets = Insets.NONE
private lateinit var readerManager: ReaderManager private lateinit var readerManager: ReaderManager
private val hideUiRunnable = Runnable { setUiIsVisible(false) } 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -187,6 +194,25 @@ class ReaderActivity :
viewBinding.zoomControl.isVisible = it 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? { override fun getParentActivityIntent(): Intent? {
@ -341,7 +367,17 @@ class ReaderActivity :
} }
override fun onDoubleModeChanged(isEnabled: Boolean) { override fun onDoubleModeChanged(isEnabled: Boolean) {
readerManager.setDoubleReaderMode(isEnabled) // 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) { private fun setKeepScreenOn(isKeep: Boolean) {

@ -49,7 +49,7 @@ class ReaderManager(
fun setDoubleReaderMode(isEnabled: Boolean) { fun setDoubleReaderMode(isEnabled: Boolean) {
val mode = currentMode val mode = currentMode
val prevReader = currentReader?.javaClass val prevReader = currentReader?.javaClass
invalidateTypesMap(isEnabled && isLandscape()) invalidateTypesMap(isEnabled)
val newReader = modeMap[mode] val newReader = modeMap[mode]
if (mode != null && newReader != prevReader) { if (mode != null && newReader != prevReader) {
replace(mode) replace(mode)

@ -91,6 +91,8 @@ class ReaderConfigSheet :
binding.buttonVertical.isChecked = mode == ReaderMode.VERTICAL binding.buttonVertical.isChecked = mode == ReaderMode.VERTICAL
binding.switchDoubleReader.isChecked = settings.isReaderDoubleOnLandscape binding.switchDoubleReader.isChecked = settings.isReaderDoubleOnLandscape
binding.switchDoubleReader.isEnabled = mode == ReaderMode.STANDARD || mode == ReaderMode.REVERSED 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.setValueRounded(settings.readerDoublePagesSensitivity * 100f)
binding.sliderDoubleSensitivity.setLabelFormatter(IntPercentLabelFormatter(binding.root.context)) binding.sliderDoubleSensitivity.setLabelFormatter(IntPercentLabelFormatter(binding.root.context))
binding.adjustSensitivitySlider(withAnimation = false) binding.adjustSensitivitySlider(withAnimation = false)
@ -104,6 +106,7 @@ class ReaderConfigSheet :
binding.buttonScrollTimer.setOnClickListener(this) binding.buttonScrollTimer.setOnClickListener(this)
binding.buttonBookmark.setOnClickListener(this) binding.buttonBookmark.setOnClickListener(this)
binding.switchDoubleReader.setOnCheckedChangeListener(this) binding.switchDoubleReader.setOnCheckedChangeListener(this)
binding.switchDoubleFoldable.setOnCheckedChangeListener(this)
binding.sliderDoubleSensitivity.addOnChangeListener(this) binding.sliderDoubleSensitivity.addOnChangeListener(this)
viewModel.isBookmarkAdded.observe(viewLifecycleOwner) { viewModel.isBookmarkAdded.observe(viewLifecycleOwner) {
@ -182,6 +185,12 @@ class ReaderConfigSheet :
viewBinding?.adjustSensitivitySlider(withAnimation = true) viewBinding?.adjustSensitivitySlider(withAnimation = true)
findParentCallback(Callback::class.java)?.onDoubleModeChanged(isChecked) 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)
}
} }
} }
@ -206,6 +215,7 @@ class ReaderConfigSheet :
} }
viewBinding?.run { viewBinding?.run {
switchDoubleReader.isEnabled = newMode == ReaderMode.STANDARD || newMode == ReaderMode.REVERSED switchDoubleReader.isEnabled = newMode == ReaderMode.STANDARD || newMode == ReaderMode.REVERSED
switchDoubleFoldable.isEnabled = switchDoubleReader.isEnabled
adjustSensitivitySlider(withAnimation = true) adjustSensitivitySlider(withAnimation = true)
} }
if (newMode == mode) { if (newMode == mode) {
@ -242,12 +252,18 @@ class ReaderConfigSheet :
} }
private fun SheetReaderConfigBinding.adjustSensitivitySlider(withAnimation: Boolean) { private fun SheetReaderConfigBinding.adjustSensitivitySlider(withAnimation: Boolean) {
val isSliderVisible = switchDoubleReader.isEnabled && switchDoubleReader.isChecked val isSubOptionsVisible = switchDoubleReader.isEnabled && switchDoubleReader.isChecked
if (isSliderVisible != sliderDoubleSensitivity.isVisible && withAnimation) { val needTransition = withAnimation && (
(isSubOptionsVisible != sliderDoubleSensitivity.isVisible) ||
(isSubOptionsVisible != textDoubleSensitivity.isVisible) ||
(isSubOptionsVisible != switchDoubleFoldable.isVisible)
)
if (needTransition) {
TransitionManager.beginDelayedTransition(layoutMain) TransitionManager.beginDelayedTransition(layoutMain)
} }
sliderDoubleSensitivity.isVisible = isSliderVisible sliderDoubleSensitivity.isVisible = isSubOptionsVisible
textDoubleSensitivity.isVisible = isSliderVisible textDoubleSensitivity.isVisible = isSubOptionsVisible
switchDoubleFoldable.isVisible = isSubOptionsVisible
} }
interface Callback { interface Callback {

@ -130,6 +130,21 @@
android:textColor="?colorOnSurfaceVariant" android:textColor="?colorOnSurfaceVariant"
app:drawableStartCompat="@drawable/ic_split_horizontal" /> 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 <TextView
android:id="@+id/text_double_sensitivity" android:id="@+id/text_double_sensitivity"
android:layout_width="match_parent" android:layout_width="match_parent"

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

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

@ -579,6 +579,8 @@
<string name="none">None</string> <string name="none">None</string>
<string name="config_reset_confirm">Reset settings to default values? This action cannot be undone.</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="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="two_page_scroll_sensitivity">Two-Page Scroll Sensitivity</string>
<string name="default_webtoon_zoom_out">Default webtoon zoom out</string> <string name="default_webtoon_zoom_out">Default webtoon zoom out</string>
<string name="fullscreen_mode">Fullscreen mode</string> <string name="fullscreen_mode">Fullscreen mode</string>

@ -49,6 +49,7 @@ viewpager2 = "1.1.0"
webkit = "1.14.0" webkit = "1.14.0"
workRuntime = "2.10.5" workRuntime = "2.10.5"
workinspector = "1.2" workinspector = "1.2"
window = "1.3.0"
[libraries] [libraries]
acra-dialog = { module = "ch.acra:acra-dialog", version.ref = "acra" } 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" } okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
ssiv = { module = "com.github.KotatsuApp:subsampling-scale-image-view", version.ref = "ssiv" } ssiv = { module = "com.github.KotatsuApp:subsampling-scale-image-view", version.ref = "ssiv" }
workinspector = { module = "com.github.Koitharu:WorkInspector", version.ref = "workinspector" } workinspector = { module = "com.github.Koitharu:WorkInspector", version.ref = "workinspector" }
androidx-window = { module = "androidx.window:window", version.ref = "window" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "gradle" } android-application = { id = "com.android.application", version.ref = "gradle" }

Loading…
Cancel
Save