Add foldable device support (auto two-page)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user