Add vertical chapter navigator for paged reader with height adjustment option (#3531)
This commit is contained in:
@@ -19,6 +19,8 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
|||||||
- Support resumable image downloads if supported by source ([@xMohnad](https://github.com/xMohnad)) ([#3167](https://github.com/mihonapp/mihon/pull/3167))
|
- Support resumable image downloads if supported by source ([@xMohnad](https://github.com/xMohnad)) ([#3167](https://github.com/mihonapp/mihon/pull/3167))
|
||||||
- Display authors and description in Shikimori search results ([@MajorTanya](https://github.com/MajorTanya)) ([#3499](https://github.com/mihonapp/mihon/pull/3499))
|
- Display authors and description in Shikimori search results ([@MajorTanya](https://github.com/MajorTanya)) ([#3499](https://github.com/mihonapp/mihon/pull/3499))
|
||||||
- Invalidate download cache after backup restore ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3096](https://github.com/mihonapp/mihon/pull/3096))
|
- Invalidate download cache after backup restore ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3096](https://github.com/mihonapp/mihon/pull/3096))
|
||||||
|
- Add setting to control vertical chapter navigator height ([@AntsyLich](https://github.com/AntsyLich)) ([#3528](https://github.com/mihonapp/mihon/pull/3528))
|
||||||
|
- Add option to set which readers use vertical chapter navigator ([@AntsyLich](https://github.com/AntsyLich)) ([#3528](https://github.com/mihonapp/mihon/pull/3528))
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Don't use GMS to detect if GMS is available ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3525](https://github.com/mihonapp/mihon/pull/3525))
|
- Don't use GMS to detect if GMS is available ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3525](https://github.com/mihonapp/mihon/pull/3525))
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "app.mihon"
|
applicationId = "app.mihon"
|
||||||
|
|
||||||
versionCode = 24
|
versionCode = 25
|
||||||
versionName = "0.20.0"
|
versionName = "0.20.0"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getLatestCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getLatestCommitCount()}\"")
|
||||||
|
|||||||
@@ -103,12 +103,13 @@ sealed class Preference {
|
|||||||
* A [PreferenceItem] that displays a list of entries as a dialog.
|
* A [PreferenceItem] that displays a list of entries as a dialog.
|
||||||
* Multiple entries can be selected at the same time.
|
* Multiple entries can be selected at the same time.
|
||||||
*/
|
*/
|
||||||
data class MultiSelectListPreference(
|
@Suppress("UNCHECKED_CAST")
|
||||||
val preference: PreferenceData<Set<String>>,
|
data class MultiSelectListPreference<T>(
|
||||||
val entries: Map<String, String>,
|
val preference: PreferenceData<Set<T>>,
|
||||||
|
val entries: Map<T, String>,
|
||||||
override val title: String,
|
override val title: String,
|
||||||
override val subtitle: String? = "%s",
|
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 ->
|
{ v, e ->
|
||||||
val combined = remember(v, e) {
|
val combined = remember(v, e) {
|
||||||
v.mapNotNull { e[it] }
|
v.mapNotNull { e[it] }
|
||||||
@@ -120,8 +121,15 @@ sealed class Preference {
|
|||||||
},
|
},
|
||||||
override val icon: ImageVector? = null,
|
override val icon: ImageVector? = null,
|
||||||
override val enabled: Boolean = true,
|
override val enabled: Boolean = true,
|
||||||
override val onValueChanged: suspend (value: Set<String>) -> Boolean = { true },
|
override val onValueChanged: suspend (value: Set<T>) -> Boolean = { true },
|
||||||
) : PreferenceItem<Set<String>, Boolean>()
|
) : 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.
|
* A [PreferenceItem] that shows a EditText in the dialog.
|
||||||
|
|||||||
@@ -128,15 +128,18 @@ internal fun PreferenceItem(
|
|||||||
onValueChange = { scope.launch { item.onValueChanged(it) } },
|
onValueChange = { scope.launch { item.onValueChanged(it) } },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is Preference.PreferenceItem.MultiSelectListPreference -> {
|
is Preference.PreferenceItem.MultiSelectListPreference<*> -> {
|
||||||
val values by item.preference.collectAsState()
|
val values by item.preference.collectAsState()
|
||||||
MultiSelectListPreferenceWidget(
|
MultiSelectListPreferenceWidget(
|
||||||
preference = item,
|
|
||||||
values = values,
|
values = values,
|
||||||
|
title = item.title,
|
||||||
|
subtitle = item.internalSubtitleProvider(values, item.entries),
|
||||||
|
icon = item.icon,
|
||||||
|
entries = item.entries,
|
||||||
onValuesChange = { newValues ->
|
onValuesChange = { newValues ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
if (item.onValueChanged(newValues)) {
|
if (item.internalOnValueChanged(newValues)) {
|
||||||
item.preference.set(newValues.toMutableSet())
|
item.internalSet(newValues)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -115,7 +115,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||||||
private fun getExcludedCategoriesPreference(
|
private fun getExcludedCategoriesPreference(
|
||||||
downloadPreferences: DownloadPreferences,
|
downloadPreferences: DownloadPreferences,
|
||||||
categories: () -> List<Category>,
|
categories: () -> List<Category>,
|
||||||
): Preference.PreferenceItem.MultiSelectListPreference {
|
): Preference.PreferenceItem.MultiSelectListPreference<String> {
|
||||||
return Preference.PreferenceItem.MultiSelectListPreference(
|
return Preference.PreferenceItem.MultiSelectListPreference(
|
||||||
preference = downloadPreferences.removeExcludeCategories,
|
preference = downloadPreferences.removeExcludeCategories,
|
||||||
entries = categories()
|
entries = categories()
|
||||||
|
|||||||
+24
-10
@@ -71,7 +71,6 @@ object SettingsReaderScreen : SearchableSettings {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun getDisplayGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
private fun getDisplayGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||||
val fullscreen by readerPreferences.fullscreen.collectAsState()
|
val fullscreen by readerPreferences.fullscreen.collectAsState()
|
||||||
val verticalNavigatorForLongStrip by readerPreferences.verticalNavigatorForLongStrip.collectAsState()
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_display),
|
title = stringResource(MR.strings.pref_category_display),
|
||||||
preferenceItems = listOf(
|
preferenceItems = listOf(
|
||||||
@@ -108,15 +107,6 @@ object SettingsReaderScreen : SearchableSettings {
|
|||||||
preference = readerPreferences.showPageNumber,
|
preference = readerPreferences.showPageNumber,
|
||||||
title = stringResource(MR.strings.pref_show_page_number),
|
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 {
|
private fun getNavigationGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||||
val readWithVolumeKeysPref = readerPreferences.readWithVolumeKeys
|
val readWithVolumeKeysPref = readerPreferences.readWithVolumeKeys
|
||||||
val readWithVolumeKeys by readWithVolumeKeysPref.collectAsState()
|
val readWithVolumeKeys by readWithVolumeKeysPref.collectAsState()
|
||||||
|
|
||||||
|
val verticalNavigator by readerPreferences.verticalNavigator.collectAsState()
|
||||||
|
val verticalNavigatorHeightPref = readerPreferences.verticalNavigatorHeight
|
||||||
|
val verticalNavigatorHeight by verticalNavigatorHeightPref.collectAsState()
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_reader_navigation),
|
title = stringResource(MR.strings.pref_reader_navigation),
|
||||||
preferenceItems = listOf(
|
preferenceItems = listOf(
|
||||||
@@ -404,6 +399,25 @@ object SettingsReaderScreen : SearchableSettings {
|
|||||||
title = stringResource(MR.strings.pref_read_with_volume_keys_inverted),
|
title = stringResource(MR.strings.pref_read_with_volume_keys_inverted),
|
||||||
enabled = readWithVolumeKeys,
|
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.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.toMutableStateList
|
import androidx.compose.runtime.toMutableStateList
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import eu.kanade.presentation.more.settings.Preference
|
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MultiSelectListPreferenceWidget(
|
fun <T> MultiSelectListPreferenceWidget(
|
||||||
preference: Preference.PreferenceItem.MultiSelectListPreference,
|
values: Set<T>,
|
||||||
values: Set<String>,
|
title: String,
|
||||||
onValuesChange: (Set<String>) -> Unit,
|
subtitle: String?,
|
||||||
|
icon: ImageVector?,
|
||||||
|
entries: Map<out T, String>,
|
||||||
|
onValuesChange: (Set<T>) -> Unit,
|
||||||
) {
|
) {
|
||||||
var isDialogShown by remember { mutableStateOf(false) }
|
var isDialogShown by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
TextPreferenceWidget(
|
TextPreferenceWidget(
|
||||||
title = preference.title,
|
title = title,
|
||||||
subtitle = preference.subtitleProvider(values, preference.entries),
|
subtitle = subtitle,
|
||||||
icon = preference.icon,
|
icon = icon,
|
||||||
onPreferenceClick = { isDialogShown = true },
|
onPreferenceClick = { isDialogShown = true },
|
||||||
)
|
)
|
||||||
|
|
||||||
if (isDialogShown) {
|
if (isDialogShown) {
|
||||||
val selected = remember {
|
val selected = remember {
|
||||||
preference.entries.keys
|
entries.keys
|
||||||
.filter { values.contains(it) }
|
.filter { values.contains(it) }
|
||||||
.toMutableStateList()
|
.toMutableStateList()
|
||||||
}
|
}
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { isDialogShown = false },
|
onDismissRequest = { isDialogShown = false },
|
||||||
title = { Text(text = preference.title) },
|
title = { Text(text = title) },
|
||||||
text = {
|
text = {
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
preference.entries.forEach { current ->
|
entries.forEach { current ->
|
||||||
item {
|
item {
|
||||||
val isSelected = selected.contains(current.key)
|
val isSelected = selected.contains(current.key)
|
||||||
LabeledCheckbox(
|
LabeledCheckbox(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -26,6 +27,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
@@ -55,6 +57,7 @@ fun ReaderAppBars(
|
|||||||
onShare: (() -> Unit)?,
|
onShare: (() -> Unit)?,
|
||||||
|
|
||||||
chapterNavigatorType: ChapterNavigatorType,
|
chapterNavigatorType: ChapterNavigatorType,
|
||||||
|
verticalNavigatorHeight: Float,
|
||||||
onNextChapter: () -> Unit,
|
onNextChapter: () -> Unit,
|
||||||
enabledNext: Boolean,
|
enabledNext: Boolean,
|
||||||
onPreviousChapter: () -> Unit,
|
onPreviousChapter: () -> Unit,
|
||||||
@@ -111,7 +114,12 @@ fun ReaderAppBars(
|
|||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
Spacer(modifier = Modifier.width(MaterialTheme.padding.small))
|
Spacer(modifier = Modifier.width(MaterialTheme.padding.small))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxHeight(),
|
||||||
|
contentAlignment = Alignment.BottomCenter,
|
||||||
|
) {
|
||||||
ChapterNavigator(
|
ChapterNavigator(
|
||||||
|
modifier = Modifier.fillMaxHeight(verticalNavigatorHeight),
|
||||||
type = chapterNavigatorType,
|
type = chapterNavigatorType,
|
||||||
onNextChapter = onNextChapter,
|
onNextChapter = onNextChapter,
|
||||||
enabledNext = enabledNext,
|
enabledNext = enabledNext,
|
||||||
@@ -123,6 +131,7 @@ fun ReaderAppBars(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ fun ChapterNavigator(
|
|||||||
currentPage: Int,
|
currentPage: Int,
|
||||||
totalPages: Int,
|
totalPages: Int,
|
||||||
onPageIndexChange: (Int) -> Unit,
|
onPageIndexChange: (Int) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
|
|
||||||
@@ -104,7 +105,7 @@ fun ChapterNavigator(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (type.isHorizontal()) {
|
if (type.isHorizontal()) {
|
||||||
ChapterNavigator(
|
HorizontalChapterNavigator(
|
||||||
isRtl = type == ChapterNavigatorType.HORIZONTAL_RTL,
|
isRtl = type == ChapterNavigatorType.HORIZONTAL_RTL,
|
||||||
state = state,
|
state = state,
|
||||||
onNextChapter = onNextChapter,
|
onNextChapter = onNextChapter,
|
||||||
@@ -117,6 +118,7 @@ fun ChapterNavigator(
|
|||||||
mainAxisPadding = mainAxisPadding,
|
mainAxisPadding = mainAxisPadding,
|
||||||
backgroundColor = backgroundColor,
|
backgroundColor = backgroundColor,
|
||||||
buttonColor = buttonColor,
|
buttonColor = buttonColor,
|
||||||
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
VerticalChapterNavigator(
|
VerticalChapterNavigator(
|
||||||
@@ -131,12 +133,13 @@ fun ChapterNavigator(
|
|||||||
mainAxisPadding = mainAxisPadding,
|
mainAxisPadding = mainAxisPadding,
|
||||||
backgroundColor = backgroundColor,
|
backgroundColor = backgroundColor,
|
||||||
buttonColor = buttonColor,
|
buttonColor = buttonColor,
|
||||||
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChapterNavigator(
|
fun HorizontalChapterNavigator(
|
||||||
isRtl: Boolean,
|
isRtl: Boolean,
|
||||||
state: SliderState,
|
state: SliderState,
|
||||||
onNextChapter: () -> Unit,
|
onNextChapter: () -> Unit,
|
||||||
@@ -149,13 +152,14 @@ fun ChapterNavigator(
|
|||||||
mainAxisPadding: Dp,
|
mainAxisPadding: Dp,
|
||||||
backgroundColor: Color,
|
backgroundColor: Color,
|
||||||
buttonColor: IconButtonColors,
|
buttonColor: IconButtonColors,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val layoutDirection = if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
|
val layoutDirection = if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
|
||||||
|
|
||||||
// We explicitly handle direction based on the reader viewer rather than the system direction
|
// We explicitly handle direction based on the reader viewer rather than the system direction
|
||||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = mainAxisPadding),
|
.padding(horizontal = mainAxisPadding),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -233,9 +237,10 @@ fun VerticalChapterNavigator(
|
|||||||
mainAxisPadding: Dp,
|
mainAxisPadding: Dp,
|
||||||
backgroundColor: Color,
|
backgroundColor: Color,
|
||||||
buttonColor: IconButtonColors,
|
buttonColor: IconButtonColors,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.padding(vertical = mainAxisPadding),
|
.padding(vertical = mainAxisPadding),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||||
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
|
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.CheckboxItem
|
import tachiyomi.presentation.core.components.CheckboxItem
|
||||||
@@ -61,17 +62,41 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
|
|||||||
pref = screenModel.preferences.showPageNumber,
|
pref = screenModel.preferences.showPageNumber,
|
||||||
)
|
)
|
||||||
|
|
||||||
val verticalNavigatorForLongStrip by screenModel.preferences.verticalNavigatorForLongStrip.collectAsState()
|
val verticalNavigatorModes by screenModel.preferences.verticalNavigator.collectAsState()
|
||||||
CheckboxItem(
|
|
||||||
label = stringResource(MR.strings.pref_webtoon_vertical_navigator),
|
SettingsChipRow(MR.strings.pref_vertical_navigator) {
|
||||||
pref = screenModel.preferences.verticalNavigatorForLongStrip,
|
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(
|
CheckboxItem(
|
||||||
label = stringResource(MR.strings.pref_webtoon_vertical_navigator_on_left),
|
label = stringResource(MR.strings.pref_webtoon_vertical_navigator_on_left),
|
||||||
pref = screenModel.preferences.verticalNavigatorOnLeft,
|
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(
|
CheckboxItem(
|
||||||
|
|||||||
@@ -461,8 +461,12 @@ class ReaderActivity : BaseActivity() {
|
|||||||
val isPagerType = ReadingMode.isPagerType(viewModel.getMangaReadingMode())
|
val isPagerType = ReadingMode.isPagerType(viewModel.getMangaReadingMode())
|
||||||
val cropEnabled = if (isPagerType) cropBorderPaged else cropBorderWebtoon
|
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 verticalNavigatorOnLeft by readerPreferences.verticalNavigatorOnLeft.collectAsState()
|
||||||
|
val verticalNavigatorHeight by readerPreferences.verticalNavigatorHeight.collectAsState()
|
||||||
|
|
||||||
ReaderAppBars(
|
ReaderAppBars(
|
||||||
visible = state.menuVisible,
|
visible = state.menuVisible,
|
||||||
@@ -477,7 +481,7 @@ class ReaderActivity : BaseActivity() {
|
|||||||
onOpenInBrowser = ::openChapterInBrowser.takeIf { isHttpSource },
|
onOpenInBrowser = ::openChapterInBrowser.takeIf { isHttpSource },
|
||||||
onShare = ::shareChapter.takeIf { isHttpSource },
|
onShare = ::shareChapter.takeIf { isHttpSource },
|
||||||
|
|
||||||
chapterNavigatorType = if (isPagerType || !verticalNavigatorForLongStrip) {
|
chapterNavigatorType = if (!verticalNavigator) {
|
||||||
if (state.viewer is R2LPagerViewer) {
|
if (state.viewer is R2LPagerViewer) {
|
||||||
ChapterNavigatorType.HORIZONTAL_RTL
|
ChapterNavigatorType.HORIZONTAL_RTL
|
||||||
} else {
|
} else {
|
||||||
@@ -490,6 +494,7 @@ class ReaderActivity : BaseActivity() {
|
|||||||
ChapterNavigatorType.VERTICAL_RIGHT
|
ChapterNavigatorType.VERTICAL_RIGHT
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
verticalNavigatorHeight = verticalNavigatorHeight / 100f,
|
||||||
onNextChapter = ::loadNextChapter,
|
onNextChapter = ::loadNextChapter,
|
||||||
enabledNext = state.viewerChapters?.nextChapter != null,
|
enabledNext = state.viewerChapters?.nextChapter != null,
|
||||||
onPreviousChapter = ::loadPreviousChapter,
|
onPreviousChapter = ::loadPreviousChapter,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import dev.icerock.moko.resources.StringResource
|
|||||||
import tachiyomi.core.common.preference.Preference
|
import tachiyomi.core.common.preference.Preference
|
||||||
import tachiyomi.core.common.preference.PreferenceStore
|
import tachiyomi.core.common.preference.PreferenceStore
|
||||||
import tachiyomi.core.common.preference.getEnum
|
import tachiyomi.core.common.preference.getEnum
|
||||||
|
import tachiyomi.core.common.preference.getEnumSet
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
|
||||||
class ReaderPreferences(
|
class ReaderPreferences(
|
||||||
@@ -28,16 +29,21 @@ class ReaderPreferences(
|
|||||||
|
|
||||||
val showPageNumber: Preference<Boolean> = preferenceStore.getBoolean("pref_show_page_number_key", true)
|
val showPageNumber: Preference<Boolean> = preferenceStore.getBoolean("pref_show_page_number_key", true)
|
||||||
|
|
||||||
val verticalNavigatorForLongStrip: Preference<Boolean> = preferenceStore.getBoolean(
|
val verticalNavigator: Preference<Set<ReadingMode>> = preferenceStore.getEnumSet(
|
||||||
"pref_webtoon_vertical_navigator",
|
"pref_vertical_navigator",
|
||||||
true,
|
emptySet(),
|
||||||
)
|
)
|
||||||
|
|
||||||
val verticalNavigatorOnLeft: Preference<Boolean> = preferenceStore.getBoolean(
|
val verticalNavigatorOnLeft: Preference<Boolean> = preferenceStore.getBoolean(
|
||||||
"pref_webtoon_vertical_navigator_on_left",
|
"pref_vertical_navigator_on_left",
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val verticalNavigatorHeight: Preference<Int> = preferenceStore.getInt(
|
||||||
|
"pref_vertical_navigator_height",
|
||||||
|
65,
|
||||||
|
)
|
||||||
|
|
||||||
val showReadingMode: Preference<Boolean> = preferenceStore.getBoolean("pref_show_reading_mode", true)
|
val showReadingMode: Preference<Boolean> = preferenceStore.getBoolean("pref_show_reading_mode", true)
|
||||||
|
|
||||||
val fullscreen: Preference<Boolean> = preferenceStore.getBoolean("fullscreen", true)
|
val fullscreen: Preference<Boolean> = preferenceStore.getBoolean("fullscreen", true)
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ package mihon.core.migration
|
|||||||
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
|
|
||||||
class MigrationContext(val dryrun: Boolean) {
|
class MigrationContext(
|
||||||
|
val dryrun: Boolean,
|
||||||
|
val previousVersion: Int,
|
||||||
|
) {
|
||||||
|
|
||||||
inline fun <reified T> get(): T? {
|
inline fun <reified T> get(): T? {
|
||||||
return Injekt.getInstanceOrNull(T::class.java)
|
return Injekt.getInstanceOrNull(T::class.java)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ object Migrator {
|
|||||||
dryrun: Boolean = false,
|
dryrun: Boolean = false,
|
||||||
onMigrationComplete: () -> Unit,
|
onMigrationComplete: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val migrationContext = MigrationContext(dryrun)
|
val migrationContext = MigrationContext(dryrun, old)
|
||||||
val migrationJobFactory = MigrationJobFactory(migrationContext, scope)
|
val migrationJobFactory = MigrationJobFactory(migrationContext, scope)
|
||||||
val migrationStrategyFactory = MigrationStrategyFactory(migrationJobFactory, onMigrationComplete)
|
val migrationStrategyFactory = MigrationStrategyFactory(migrationJobFactory, onMigrationComplete)
|
||||||
val strategy = migrationStrategyFactory.create(old, new)
|
val strategy = migrationStrategyFactory.create(old, new)
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ val migrations: List<Migration>
|
|||||||
TrustExtensionRepositoryMigration(),
|
TrustExtensionRepositoryMigration(),
|
||||||
CategoryPreferencesCleanupMigration(),
|
CategoryPreferencesCleanupMigration(),
|
||||||
InstallationIdMigration(),
|
InstallationIdMigration(),
|
||||||
|
VerticalNavigatorMigration(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package mihon.core.migration.migrations
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||||
|
import mihon.core.migration.Migration
|
||||||
|
import mihon.core.migration.MigrationContext
|
||||||
|
import tachiyomi.core.common.preference.PreferenceStore
|
||||||
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
|
|
||||||
|
class VerticalNavigatorMigration : Migration {
|
||||||
|
override val version: Float = 25f
|
||||||
|
|
||||||
|
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
|
||||||
|
val preferenceStore = migrationContext.get<PreferenceStore>() ?: return@withIOContext false
|
||||||
|
val readerPreferences = migrationContext.get<ReaderPreferences>() ?: return@withIOContext false
|
||||||
|
|
||||||
|
if (migrationContext.previousVersion == 24) {
|
||||||
|
val oldVerticalNavigator = preferenceStore.getBoolean("pref_webtoon_vertical_navigator", true)
|
||||||
|
if (oldVerticalNavigator.get()) {
|
||||||
|
readerPreferences.verticalNavigator.set(setOf(ReadingMode.WEBTOON, ReadingMode.CONTINUOUS_VERTICAL))
|
||||||
|
}
|
||||||
|
if (oldVerticalNavigator.isSet()) oldVerticalNavigator.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
val oldVerticalNavigatorOnLeft = preferenceStore.getBoolean("pref_webtoon_vertical_navigator_on_left", false)
|
||||||
|
if (oldVerticalNavigatorOnLeft.isSet()) {
|
||||||
|
readerPreferences.verticalNavigatorOnLeft.set(oldVerticalNavigatorOnLeft.get())
|
||||||
|
oldVerticalNavigatorOnLeft.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
return@withIOContext true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ class MigratorTest {
|
|||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun initialize() {
|
fun initialize() {
|
||||||
migrationContext = MigrationContext(false)
|
migrationContext = MigrationContext(false, 0)
|
||||||
migrationJobFactory = spyk(MigrationJobFactory(migrationContext, CoroutineScope(Dispatchers.Main + Job())))
|
migrationJobFactory = spyk(MigrationJobFactory(migrationContext, CoroutineScope(Dispatchers.Main + Job())))
|
||||||
migrationCompletedListener = spyk<MigrationCompletedListener>(block = {})
|
migrationCompletedListener = spyk<MigrationCompletedListener>(block = {})
|
||||||
migrationStrategyFactory = spyk(MigrationStrategyFactory(migrationJobFactory, migrationCompletedListener))
|
migrationStrategyFactory = spyk(MigrationStrategyFactory(migrationJobFactory, migrationCompletedListener))
|
||||||
|
|||||||
@@ -212,4 +212,25 @@ sealed class AndroidPreference<T>(
|
|||||||
putInt(key, serializer(value))
|
putInt(key, serializer(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ObjectSetAsStringSet<T>(
|
||||||
|
preferences: SharedPreferences,
|
||||||
|
keyFlow: Flow<String?>,
|
||||||
|
key: String,
|
||||||
|
defaultValue: Set<T>,
|
||||||
|
private val serializer: (T) -> String,
|
||||||
|
private val deserializer: (String) -> T?,
|
||||||
|
) : AndroidPreference<Set<T>>(preferences, keyFlow, key, defaultValue) {
|
||||||
|
override fun read(preferences: SharedPreferences, key: String, defaultValue: Set<T>): Set<T> {
|
||||||
|
return try {
|
||||||
|
preferences.getStringSet(key, null)?.mapNotNull(deserializer)?.toSet() ?: defaultValue
|
||||||
|
} catch (_: Exception) {
|
||||||
|
defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(key: String, value: Set<T>): Editor.() -> Unit = {
|
||||||
|
putStringSet(key, value.map(serializer).toSet())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
@@ -11,6 +11,7 @@ import tachiyomi.core.common.preference.AndroidPreference.IntPrimitive
|
|||||||
import tachiyomi.core.common.preference.AndroidPreference.LongPrimitive
|
import tachiyomi.core.common.preference.AndroidPreference.LongPrimitive
|
||||||
import tachiyomi.core.common.preference.AndroidPreference.ObjectAsInt
|
import tachiyomi.core.common.preference.AndroidPreference.ObjectAsInt
|
||||||
import tachiyomi.core.common.preference.AndroidPreference.ObjectAsString
|
import tachiyomi.core.common.preference.AndroidPreference.ObjectAsString
|
||||||
|
import tachiyomi.core.common.preference.AndroidPreference.ObjectSetAsStringSet
|
||||||
import tachiyomi.core.common.preference.AndroidPreference.StringPrimitive
|
import tachiyomi.core.common.preference.AndroidPreference.StringPrimitive
|
||||||
import tachiyomi.core.common.preference.AndroidPreference.StringSetPrimitive
|
import tachiyomi.core.common.preference.AndroidPreference.StringSetPrimitive
|
||||||
|
|
||||||
@@ -77,6 +78,22 @@ class AndroidPreferenceStore(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun <T> getObjectSetFromStringSet(
|
||||||
|
key: String,
|
||||||
|
defaultValue: Set<T>,
|
||||||
|
serializer: (T) -> String,
|
||||||
|
deserializer: (String) -> T?,
|
||||||
|
): Preference<Set<T>> {
|
||||||
|
return ObjectSetAsStringSet(
|
||||||
|
preferences = sharedPreferences,
|
||||||
|
keyFlow = keyFlow,
|
||||||
|
key = key,
|
||||||
|
defaultValue = defaultValue,
|
||||||
|
serializer = serializer,
|
||||||
|
deserializer = deserializer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getAll(): Map<String, *> {
|
override fun getAll(): Map<String, *> {
|
||||||
return sharedPreferences.all ?: emptyMap<String, Any>()
|
return sharedPreferences.all ?: emptyMap<String, Any>()
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-1
@@ -48,7 +48,11 @@ class InMemoryPreferenceStore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getStringSet(key: String, defaultValue: Set<String>): Preference<Set<String>> {
|
override fun getStringSet(key: String, defaultValue: Set<String>): Preference<Set<String>> {
|
||||||
TODO("Not yet implemented")
|
val default = InMemoryPreference(key, null, defaultValue)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val data: Set<String>? = preferences[key]?.get() as? Set<String>
|
||||||
|
return if (data == null) default else InMemoryPreference(key, data, defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
@@ -75,6 +79,18 @@ class InMemoryPreferenceStore(
|
|||||||
return if (data == null) default else InMemoryPreference(key, data, defaultValue)
|
return if (data == null) default else InMemoryPreference(key, data, defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T> getObjectSetFromStringSet(
|
||||||
|
key: String,
|
||||||
|
defaultValue: Set<T>,
|
||||||
|
serializer: (T) -> String,
|
||||||
|
deserializer: (String) -> T?,
|
||||||
|
): Preference<Set<T>> {
|
||||||
|
val default = InMemoryPreference(key, null, defaultValue)
|
||||||
|
val data: Set<T>? = preferences[key]?.get() as? Set<T>
|
||||||
|
return if (data == null) default else InMemoryPreference(key, data, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getAll(): Map<String, *> {
|
override fun getAll(): Map<String, *> {
|
||||||
return preferences
|
return preferences
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,13 @@ interface PreferenceStore {
|
|||||||
deserializer: (Int) -> T,
|
deserializer: (Int) -> T,
|
||||||
): Preference<T>
|
): Preference<T>
|
||||||
|
|
||||||
|
fun <T> getObjectSetFromStringSet(
|
||||||
|
key: String,
|
||||||
|
defaultValue: Set<T>,
|
||||||
|
serializer: (T) -> String,
|
||||||
|
deserializer: (String) -> T?,
|
||||||
|
): Preference<Set<T>>
|
||||||
|
|
||||||
fun getAll(): Map<String, *>
|
fun getAll(): Map<String, *>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,3 +67,21 @@ inline fun <reified T : Enum<T>> PreferenceStore.getEnum(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun <reified T : Enum<T>> PreferenceStore.getEnumSet(
|
||||||
|
key: String,
|
||||||
|
defaultValue: Set<T>,
|
||||||
|
): Preference<Set<T>> {
|
||||||
|
return getObjectSetFromStringSet(
|
||||||
|
key = key,
|
||||||
|
defaultValue = defaultValue,
|
||||||
|
serializer = { it.name },
|
||||||
|
deserializer = {
|
||||||
|
try {
|
||||||
|
enumValueOf<T>(it)
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -400,7 +400,8 @@
|
|||||||
<string name="pref_flash_style_white_black">White and Black</string>
|
<string name="pref_flash_style_white_black">White and Black</string>
|
||||||
<string name="pref_double_tap_anim_speed">Double tap animation speed</string>
|
<string name="pref_double_tap_anim_speed">Double tap animation speed</string>
|
||||||
<string name="pref_show_page_number">Show page number</string>
|
<string name="pref_show_page_number">Show page number</string>
|
||||||
<string name="pref_webtoon_vertical_navigator">Use vertical navigator for long strip reading</string>
|
<string name="pref_vertical_navigator">Use vertical chapter navigator in</string>
|
||||||
|
<string name="pref_vertical_navigator_height">Vertical navigator height</string>
|
||||||
<string name="pref_webtoon_vertical_navigator_on_left">Place vertical navigator on the left side</string>
|
<string name="pref_webtoon_vertical_navigator_on_left">Place vertical navigator on the left side</string>
|
||||||
<string name="pref_show_reading_mode">Show reading mode</string>
|
<string name="pref_show_reading_mode">Show reading mode</string>
|
||||||
<string name="pref_show_reading_mode_summary">Briefly show current mode when reader is opened</string>
|
<string name="pref_show_reading_mode_summary">Briefly show current mode when reader is opened</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user