Add vertical chapter navigator for paged reader with height adjustment option (#3531)
This commit is contained in:
@@ -103,12 +103,13 @@ sealed class Preference {
|
||||
* A [PreferenceItem] that displays a list of entries as a dialog.
|
||||
* Multiple entries can be selected at the same time.
|
||||
*/
|
||||
data class MultiSelectListPreference(
|
||||
val preference: PreferenceData<Set<String>>,
|
||||
val entries: Map<String, String>,
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
data class MultiSelectListPreference<T>(
|
||||
val preference: PreferenceData<Set<T>>,
|
||||
val entries: Map<T, String>,
|
||||
override val title: String,
|
||||
override val subtitle: String? = "%s",
|
||||
val subtitleProvider: @Composable (value: Set<String>, entries: Map<String, String>) -> String? =
|
||||
val subtitleProvider: @Composable (value: Set<T>, entries: Map<T, String>) -> String? =
|
||||
{ v, e ->
|
||||
val combined = remember(v, e) {
|
||||
v.mapNotNull { e[it] }
|
||||
@@ -120,8 +121,15 @@ sealed class Preference {
|
||||
},
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (value: Set<String>) -> Boolean = { true },
|
||||
) : PreferenceItem<Set<String>, Boolean>()
|
||||
override val onValueChanged: suspend (value: Set<T>) -> Boolean = { true },
|
||||
) : PreferenceItem<Set<T>, Boolean>() {
|
||||
internal fun internalSet(value: Set<Any?>) = preference.set(value as Set<T>)
|
||||
internal suspend fun internalOnValueChanged(value: Set<Any?>) = onValueChanged(value as Set<T>)
|
||||
|
||||
@Composable
|
||||
internal fun internalSubtitleProvider(value: Set<Any?>, entries: Map<out Any?, String>) =
|
||||
subtitleProvider(value as Set<T>, entries as Map<T, String>)
|
||||
}
|
||||
|
||||
/**
|
||||
* A [PreferenceItem] that shows a EditText in the dialog.
|
||||
|
||||
@@ -128,15 +128,18 @@ internal fun PreferenceItem(
|
||||
onValueChange = { scope.launch { item.onValueChanged(it) } },
|
||||
)
|
||||
}
|
||||
is Preference.PreferenceItem.MultiSelectListPreference -> {
|
||||
is Preference.PreferenceItem.MultiSelectListPreference<*> -> {
|
||||
val values by item.preference.collectAsState()
|
||||
MultiSelectListPreferenceWidget(
|
||||
preference = item,
|
||||
values = values,
|
||||
title = item.title,
|
||||
subtitle = item.internalSubtitleProvider(values, item.entries),
|
||||
icon = item.icon,
|
||||
entries = item.entries,
|
||||
onValuesChange = { newValues ->
|
||||
scope.launch {
|
||||
if (item.onValueChanged(newValues)) {
|
||||
item.preference.set(newValues.toMutableSet())
|
||||
if (item.internalOnValueChanged(newValues)) {
|
||||
item.internalSet(newValues)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -115,7 +115,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
private fun getExcludedCategoriesPreference(
|
||||
downloadPreferences: DownloadPreferences,
|
||||
categories: () -> List<Category>,
|
||||
): Preference.PreferenceItem.MultiSelectListPreference {
|
||||
): Preference.PreferenceItem.MultiSelectListPreference<String> {
|
||||
return Preference.PreferenceItem.MultiSelectListPreference(
|
||||
preference = downloadPreferences.removeExcludeCategories,
|
||||
entries = categories()
|
||||
|
||||
+24
-10
@@ -71,7 +71,6 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
@Composable
|
||||
private fun getDisplayGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||
val fullscreen by readerPreferences.fullscreen.collectAsState()
|
||||
val verticalNavigatorForLongStrip by readerPreferences.verticalNavigatorForLongStrip.collectAsState()
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_display),
|
||||
preferenceItems = listOf(
|
||||
@@ -108,15 +107,6 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
preference = readerPreferences.showPageNumber,
|
||||
title = stringResource(MR.strings.pref_show_page_number),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.verticalNavigatorForLongStrip,
|
||||
title = stringResource(MR.strings.pref_webtoon_vertical_navigator),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.verticalNavigatorOnLeft,
|
||||
title = stringResource(MR.strings.pref_webtoon_vertical_navigator_on_left),
|
||||
enabled = verticalNavigatorForLongStrip,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -392,6 +382,11 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
private fun getNavigationGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||
val readWithVolumeKeysPref = readerPreferences.readWithVolumeKeys
|
||||
val readWithVolumeKeys by readWithVolumeKeysPref.collectAsState()
|
||||
|
||||
val verticalNavigator by readerPreferences.verticalNavigator.collectAsState()
|
||||
val verticalNavigatorHeightPref = readerPreferences.verticalNavigatorHeight
|
||||
val verticalNavigatorHeight by verticalNavigatorHeightPref.collectAsState()
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_reader_navigation),
|
||||
preferenceItems = listOf(
|
||||
@@ -404,6 +399,25 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_read_with_volume_keys_inverted),
|
||||
enabled = readWithVolumeKeys,
|
||||
),
|
||||
Preference.PreferenceItem.MultiSelectListPreference(
|
||||
preference = readerPreferences.verticalNavigator,
|
||||
entries = ReadingMode.entries.filter { it != ReadingMode.DEFAULT }
|
||||
.associate { it to stringResource(it.stringRes) },
|
||||
title = stringResource(MR.strings.pref_vertical_navigator),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.verticalNavigatorOnLeft,
|
||||
title = stringResource(MR.strings.pref_webtoon_vertical_navigator_on_left),
|
||||
enabled = verticalNavigator.isNotEmpty(),
|
||||
),
|
||||
Preference.PreferenceItem.SliderPreference(
|
||||
value = verticalNavigatorHeight,
|
||||
valueRange = 65..100,
|
||||
steps = 6,
|
||||
title = stringResource(MR.strings.pref_vertical_navigator_height),
|
||||
onValueChanged = { verticalNavigatorHeightPref.set(it) },
|
||||
enabled = verticalNavigator.isNotEmpty(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
+14
-11
@@ -10,39 +10,42 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun MultiSelectListPreferenceWidget(
|
||||
preference: Preference.PreferenceItem.MultiSelectListPreference,
|
||||
values: Set<String>,
|
||||
onValuesChange: (Set<String>) -> Unit,
|
||||
fun <T> MultiSelectListPreferenceWidget(
|
||||
values: Set<T>,
|
||||
title: String,
|
||||
subtitle: String?,
|
||||
icon: ImageVector?,
|
||||
entries: Map<out T, String>,
|
||||
onValuesChange: (Set<T>) -> Unit,
|
||||
) {
|
||||
var isDialogShown by remember { mutableStateOf(false) }
|
||||
|
||||
TextPreferenceWidget(
|
||||
title = preference.title,
|
||||
subtitle = preference.subtitleProvider(values, preference.entries),
|
||||
icon = preference.icon,
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
icon = icon,
|
||||
onPreferenceClick = { isDialogShown = true },
|
||||
)
|
||||
|
||||
if (isDialogShown) {
|
||||
val selected = remember {
|
||||
preference.entries.keys
|
||||
entries.keys
|
||||
.filter { values.contains(it) }
|
||||
.toMutableStateList()
|
||||
}
|
||||
AlertDialog(
|
||||
onDismissRequest = { isDialogShown = false },
|
||||
title = { Text(text = preference.title) },
|
||||
title = { Text(text = title) },
|
||||
text = {
|
||||
LazyColumn {
|
||||
preference.entries.forEach { current ->
|
||||
entries.forEach { current ->
|
||||
item {
|
||||
val isSelected = selected.contains(current.key)
|
||||
LabeledCheckbox(
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -26,6 +27,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
@@ -55,6 +57,7 @@ fun ReaderAppBars(
|
||||
onShare: (() -> Unit)?,
|
||||
|
||||
chapterNavigatorType: ChapterNavigatorType,
|
||||
verticalNavigatorHeight: Float,
|
||||
onNextChapter: () -> Unit,
|
||||
enabledNext: Boolean,
|
||||
onPreviousChapter: () -> Unit,
|
||||
@@ -111,16 +114,22 @@ fun ReaderAppBars(
|
||||
) {
|
||||
Row {
|
||||
Spacer(modifier = Modifier.width(MaterialTheme.padding.small))
|
||||
ChapterNavigator(
|
||||
type = chapterNavigatorType,
|
||||
onNextChapter = onNextChapter,
|
||||
enabledNext = enabledNext,
|
||||
onPreviousChapter = onPreviousChapter,
|
||||
enabledPrevious = enabledPrevious,
|
||||
currentPage = currentPage,
|
||||
totalPages = totalPages,
|
||||
onPageIndexChange = onPageIndexChange,
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
contentAlignment = Alignment.BottomCenter,
|
||||
) {
|
||||
ChapterNavigator(
|
||||
modifier = Modifier.fillMaxHeight(verticalNavigatorHeight),
|
||||
type = chapterNavigatorType,
|
||||
onNextChapter = onNextChapter,
|
||||
enabledNext = enabledNext,
|
||||
onPreviousChapter = onPreviousChapter,
|
||||
enabledPrevious = enabledPrevious,
|
||||
currentPage = currentPage,
|
||||
totalPages = totalPages,
|
||||
onPageIndexChange = onPageIndexChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
@@ -70,6 +70,7 @@ fun ChapterNavigator(
|
||||
currentPage: Int,
|
||||
totalPages: Int,
|
||||
onPageIndexChange: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
|
||||
@@ -104,7 +105,7 @@ fun ChapterNavigator(
|
||||
)
|
||||
|
||||
if (type.isHorizontal()) {
|
||||
ChapterNavigator(
|
||||
HorizontalChapterNavigator(
|
||||
isRtl = type == ChapterNavigatorType.HORIZONTAL_RTL,
|
||||
state = state,
|
||||
onNextChapter = onNextChapter,
|
||||
@@ -117,6 +118,7 @@ fun ChapterNavigator(
|
||||
mainAxisPadding = mainAxisPadding,
|
||||
backgroundColor = backgroundColor,
|
||||
buttonColor = buttonColor,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
VerticalChapterNavigator(
|
||||
@@ -131,12 +133,13 @@ fun ChapterNavigator(
|
||||
mainAxisPadding = mainAxisPadding,
|
||||
backgroundColor = backgroundColor,
|
||||
buttonColor = buttonColor,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChapterNavigator(
|
||||
fun HorizontalChapterNavigator(
|
||||
isRtl: Boolean,
|
||||
state: SliderState,
|
||||
onNextChapter: () -> Unit,
|
||||
@@ -149,13 +152,14 @@ fun ChapterNavigator(
|
||||
mainAxisPadding: Dp,
|
||||
backgroundColor: Color,
|
||||
buttonColor: IconButtonColors,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val layoutDirection = if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
|
||||
|
||||
// We explicitly handle direction based on the reader viewer rather than the system direction
|
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = mainAxisPadding),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -233,9 +237,10 @@ fun VerticalChapterNavigator(
|
||||
mainAxisPadding: Dp,
|
||||
backgroundColor: Color,
|
||||
buttonColor: IconButtonColors,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
modifier = modifier
|
||||
.fillMaxHeight()
|
||||
.padding(vertical = mainAxisPadding),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.CheckboxItem
|
||||
@@ -61,17 +62,41 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
|
||||
pref = screenModel.preferences.showPageNumber,
|
||||
)
|
||||
|
||||
val verticalNavigatorForLongStrip by screenModel.preferences.verticalNavigatorForLongStrip.collectAsState()
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_webtoon_vertical_navigator),
|
||||
pref = screenModel.preferences.verticalNavigatorForLongStrip,
|
||||
)
|
||||
val verticalNavigatorModes by screenModel.preferences.verticalNavigator.collectAsState()
|
||||
|
||||
SettingsChipRow(MR.strings.pref_vertical_navigator) {
|
||||
ReadingMode.entries.filter { it != ReadingMode.DEFAULT }.forEach { mode ->
|
||||
FilterChip(
|
||||
selected = verticalNavigatorModes.contains(mode),
|
||||
onClick = {
|
||||
val newModes = if (verticalNavigatorModes.contains(mode)) {
|
||||
verticalNavigatorModes - mode
|
||||
} else {
|
||||
verticalNavigatorModes + mode
|
||||
}
|
||||
screenModel.preferences.verticalNavigator.set(newModes)
|
||||
},
|
||||
label = { Text(stringResource(mode.stringRes)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (verticalNavigatorModes.isNotEmpty()) {
|
||||
val verticalNavigatorHeightPref = screenModel.preferences.verticalNavigatorHeight
|
||||
val verticalNavigatorHeight by verticalNavigatorHeightPref.collectAsState()
|
||||
|
||||
if (verticalNavigatorForLongStrip) {
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_webtoon_vertical_navigator_on_left),
|
||||
pref = screenModel.preferences.verticalNavigatorOnLeft,
|
||||
)
|
||||
|
||||
SliderItem(
|
||||
label = stringResource(MR.strings.pref_vertical_navigator_height),
|
||||
value = verticalNavigatorHeight,
|
||||
valueRange = 65..100,
|
||||
steps = 6,
|
||||
onChange = { verticalNavigatorHeightPref.set(it) },
|
||||
)
|
||||
}
|
||||
|
||||
CheckboxItem(
|
||||
|
||||
@@ -461,8 +461,12 @@ class ReaderActivity : BaseActivity() {
|
||||
val isPagerType = ReadingMode.isPagerType(viewModel.getMangaReadingMode())
|
||||
val cropEnabled = if (isPagerType) cropBorderPaged else cropBorderWebtoon
|
||||
|
||||
val verticalNavigatorForLongStrip by readerPreferences.verticalNavigatorForLongStrip.collectAsState()
|
||||
val verticalNavigatorModes by readerPreferences.verticalNavigator.collectAsState()
|
||||
val verticalNavigator = verticalNavigatorModes.contains(
|
||||
ReadingMode.fromPreference(viewModel.getMangaReadingMode()),
|
||||
)
|
||||
val verticalNavigatorOnLeft by readerPreferences.verticalNavigatorOnLeft.collectAsState()
|
||||
val verticalNavigatorHeight by readerPreferences.verticalNavigatorHeight.collectAsState()
|
||||
|
||||
ReaderAppBars(
|
||||
visible = state.menuVisible,
|
||||
@@ -477,7 +481,7 @@ class ReaderActivity : BaseActivity() {
|
||||
onOpenInBrowser = ::openChapterInBrowser.takeIf { isHttpSource },
|
||||
onShare = ::shareChapter.takeIf { isHttpSource },
|
||||
|
||||
chapterNavigatorType = if (isPagerType || !verticalNavigatorForLongStrip) {
|
||||
chapterNavigatorType = if (!verticalNavigator) {
|
||||
if (state.viewer is R2LPagerViewer) {
|
||||
ChapterNavigatorType.HORIZONTAL_RTL
|
||||
} else {
|
||||
@@ -490,6 +494,7 @@ class ReaderActivity : BaseActivity() {
|
||||
ChapterNavigatorType.VERTICAL_RIGHT
|
||||
}
|
||||
},
|
||||
verticalNavigatorHeight = verticalNavigatorHeight / 100f,
|
||||
onNextChapter = ::loadNextChapter,
|
||||
enabledNext = state.viewerChapters?.nextChapter != null,
|
||||
onPreviousChapter = ::loadPreviousChapter,
|
||||
|
||||
@@ -6,6 +6,7 @@ import dev.icerock.moko.resources.StringResource
|
||||
import tachiyomi.core.common.preference.Preference
|
||||
import tachiyomi.core.common.preference.PreferenceStore
|
||||
import tachiyomi.core.common.preference.getEnum
|
||||
import tachiyomi.core.common.preference.getEnumSet
|
||||
import tachiyomi.i18n.MR
|
||||
|
||||
class ReaderPreferences(
|
||||
@@ -28,16 +29,21 @@ class ReaderPreferences(
|
||||
|
||||
val showPageNumber: Preference<Boolean> = preferenceStore.getBoolean("pref_show_page_number_key", true)
|
||||
|
||||
val verticalNavigatorForLongStrip: Preference<Boolean> = preferenceStore.getBoolean(
|
||||
"pref_webtoon_vertical_navigator",
|
||||
true,
|
||||
val verticalNavigator: Preference<Set<ReadingMode>> = preferenceStore.getEnumSet(
|
||||
"pref_vertical_navigator",
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
val verticalNavigatorOnLeft: Preference<Boolean> = preferenceStore.getBoolean(
|
||||
"pref_webtoon_vertical_navigator_on_left",
|
||||
"pref_vertical_navigator_on_left",
|
||||
false,
|
||||
)
|
||||
|
||||
val verticalNavigatorHeight: Preference<Int> = preferenceStore.getInt(
|
||||
"pref_vertical_navigator_height",
|
||||
65,
|
||||
)
|
||||
|
||||
val showReadingMode: Preference<Boolean> = preferenceStore.getBoolean("pref_show_reading_mode", true)
|
||||
|
||||
val fullscreen: Preference<Boolean> = preferenceStore.getBoolean("fullscreen", true)
|
||||
|
||||
Reference in New Issue
Block a user