🚀 Progress Count option

* Option to change Progress Count method (Percentage/Quantity)
* Improved progress calculation
This commit is contained in:
Acclorite 2025-01-21 19:49:32 +02:00
parent fe269fd8a0
commit ad41bc9a1a
11 changed files with 145 additions and 19 deletions

View file

@ -0,0 +1,13 @@
package ua.acclorite.book_story.domain.reader
import androidx.compose.runtime.Immutable
@Immutable
enum class ReaderProgressCount {
PERCENTAGE,
QUANTITY
}
fun String.toProgressCount(): ReaderProgressCount {
return ReaderProgressCount.valueOf(this)
}

View file

@ -58,6 +58,7 @@ object DataStoreConstants {
val PROGRESS_BAR_PADDING = intPreferencesKey("progress_bar_padding") val PROGRESS_BAR_PADDING = intPreferencesKey("progress_bar_padding")
val PROGRESS_BAR_ALIGNMENT = stringPreferencesKey("progress_bar_alignment") val PROGRESS_BAR_ALIGNMENT = stringPreferencesKey("progress_bar_alignment")
val PROGRESS_BAR_FONT_SIZE = intPreferencesKey("progress_bar_font_size") val PROGRESS_BAR_FONT_SIZE = intPreferencesKey("progress_bar_font_size")
val PROGRESS_COUNT = stringPreferencesKey("progress_count")
// Browse settings // Browse settings
val BROWSE_LAYOUT = stringPreferencesKey("browse_layout") val BROWSE_LAYOUT = stringPreferencesKey("browse_layout")

View file

@ -13,6 +13,7 @@ import ua.acclorite.book_story.presentation.settings.reader.progress.components.
import ua.acclorite.book_story.presentation.settings.reader.progress.components.ProgressBarFontSizeOption import ua.acclorite.book_story.presentation.settings.reader.progress.components.ProgressBarFontSizeOption
import ua.acclorite.book_story.presentation.settings.reader.progress.components.ProgressBarOption import ua.acclorite.book_story.presentation.settings.reader.progress.components.ProgressBarOption
import ua.acclorite.book_story.presentation.settings.reader.progress.components.ProgressBarPaddingOption import ua.acclorite.book_story.presentation.settings.reader.progress.components.ProgressBarPaddingOption
import ua.acclorite.book_story.presentation.settings.reader.progress.components.ProgressCountOption
fun LazyListScope.ProgressSubcategory( fun LazyListScope.ProgressSubcategory(
titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary }, titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary },
@ -26,6 +27,10 @@ fun LazyListScope.ProgressSubcategory(
showTitle = showTitle, showTitle = showTitle,
showDivider = showDivider, showDivider = showDivider,
) { ) {
item {
ProgressCountOption()
}
item { item {
ProgressBarOption() ProgressBarOption()
} }

View file

@ -0,0 +1,41 @@
package ua.acclorite.book_story.presentation.settings.reader.progress.components
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.reader.ReaderProgressCount
import ua.acclorite.book_story.domain.ui.ButtonItem
import ua.acclorite.book_story.presentation.core.components.settings.SegmentedButtonWithTitle
import ua.acclorite.book_story.ui.main.MainEvent
import ua.acclorite.book_story.ui.main.MainModel
@Composable
fun ProgressCountOption() {
val mainModel = hiltViewModel<MainModel>()
val state = mainModel.state.collectAsStateWithLifecycle()
SegmentedButtonWithTitle(
title = stringResource(id = R.string.progress_count_option),
buttons = ReaderProgressCount.entries.map {
ButtonItem(
id = it.toString(),
title = when (it) {
ReaderProgressCount.PERCENTAGE -> stringResource(id = R.string.progress_count_percentage)
ReaderProgressCount.QUANTITY -> stringResource(id = R.string.progress_count_quantity)
},
textStyle = MaterialTheme.typography.labelLarge,
selected = it == state.value.progressCount
)
},
onClick = {
mainModel.onEvent(
MainEvent.OnChangeProgressCount(
it.id
)
)
}
)
}

View file

@ -59,4 +59,5 @@ sealed class MainEvent {
data class OnChangeProgressBarFontSize(val value: Int) : MainEvent() data class OnChangeProgressBarFontSize(val value: Int) : MainEvent()
data class OnChangeBrowsePinnedPaths(val value: String) : MainEvent() data class OnChangeBrowsePinnedPaths(val value: String) : MainEvent()
data class OnChangeFontThickness(val value: String) : MainEvent() data class OnChangeFontThickness(val value: String) : MainEvent()
data class OnChangeProgressCount(val value: String) : MainEvent()
} }

View file

@ -21,6 +21,7 @@ import ua.acclorite.book_story.domain.browse.toBrowseSortOrder
import ua.acclorite.book_story.domain.reader.toColorEffects import ua.acclorite.book_story.domain.reader.toColorEffects
import ua.acclorite.book_story.domain.reader.toFontThickness import ua.acclorite.book_story.domain.reader.toFontThickness
import ua.acclorite.book_story.domain.reader.toHorizontalGesture import ua.acclorite.book_story.domain.reader.toHorizontalGesture
import ua.acclorite.book_story.domain.reader.toProgressCount
import ua.acclorite.book_story.domain.reader.toReaderScreenOrientation import ua.acclorite.book_story.domain.reader.toReaderScreenOrientation
import ua.acclorite.book_story.domain.reader.toTextAlignment import ua.acclorite.book_story.domain.reader.toTextAlignment
import ua.acclorite.book_story.domain.use_case.data_store.ChangeLanguage import ua.acclorite.book_story.domain.use_case.data_store.ChangeLanguage
@ -494,6 +495,14 @@ class MainModel @Inject constructor(
it.copy(fontThickness = this.toFontThickness()) it.copy(fontThickness = this.toFontThickness())
} }
) )
is MainEvent.OnChangeProgressCount -> handleDatastoreUpdate(
key = DataStoreConstants.PROGRESS_COUNT,
value = event.value,
updateState = {
it.copy(progressCount = this.toProgressCount())
}
)
} }
} }

View file

@ -14,11 +14,13 @@ import ua.acclorite.book_story.domain.browse.toBrowseSortOrder
import ua.acclorite.book_story.domain.reader.ReaderColorEffects import ua.acclorite.book_story.domain.reader.ReaderColorEffects
import ua.acclorite.book_story.domain.reader.ReaderFontThickness import ua.acclorite.book_story.domain.reader.ReaderFontThickness
import ua.acclorite.book_story.domain.reader.ReaderHorizontalGesture import ua.acclorite.book_story.domain.reader.ReaderHorizontalGesture
import ua.acclorite.book_story.domain.reader.ReaderProgressCount
import ua.acclorite.book_story.domain.reader.ReaderScreenOrientation import ua.acclorite.book_story.domain.reader.ReaderScreenOrientation
import ua.acclorite.book_story.domain.reader.ReaderTextAlignment import ua.acclorite.book_story.domain.reader.ReaderTextAlignment
import ua.acclorite.book_story.domain.reader.toColorEffects import ua.acclorite.book_story.domain.reader.toColorEffects
import ua.acclorite.book_story.domain.reader.toFontThickness import ua.acclorite.book_story.domain.reader.toFontThickness
import ua.acclorite.book_story.domain.reader.toHorizontalGesture import ua.acclorite.book_story.domain.reader.toHorizontalGesture
import ua.acclorite.book_story.domain.reader.toProgressCount
import ua.acclorite.book_story.domain.reader.toReaderScreenOrientation import ua.acclorite.book_story.domain.reader.toReaderScreenOrientation
import ua.acclorite.book_story.domain.reader.toTextAlignment import ua.acclorite.book_story.domain.reader.toTextAlignment
import ua.acclorite.book_story.domain.util.HorizontalAlignment import ua.acclorite.book_story.domain.util.HorizontalAlignment
@ -107,6 +109,7 @@ data class MainState(
val progressBarPadding: Int = provideDefaultValue { 4 }, val progressBarPadding: Int = provideDefaultValue { 4 },
val progressBarAlignment: HorizontalAlignment = provideDefaultValue { HorizontalAlignment.CENTER }, val progressBarAlignment: HorizontalAlignment = provideDefaultValue { HorizontalAlignment.CENTER },
val progressBarFontSize: Int = provideDefaultValue { 8 }, val progressBarFontSize: Int = provideDefaultValue { 8 },
val progressCount: ReaderProgressCount = provideDefaultValue { ReaderProgressCount.PERCENTAGE },
// Browse Settings // Browse Settings
val browseLayout: BrowseLayout = provideDefaultValue { BrowseLayout.LIST }, val browseLayout: BrowseLayout = provideDefaultValue { BrowseLayout.LIST },
@ -357,6 +360,10 @@ data class MainState(
fontThickness = provideValue( fontThickness = provideValue(
FONT_THICKNESS, convert = { toFontThickness() } FONT_THICKNESS, convert = { toFontThickness() }
) { fontThickness }, ) { fontThickness },
progressCount = provideValue(
PROGRESS_COUNT, convert = { toProgressCount() }
) { progressCount },
) )
} }
} }

View file

@ -179,7 +179,6 @@ class ReaderModel @Inject constructor(
launch { launch {
_state.value.apply { _state.value.apply {
val chapterIndex = text.indexOf(event.chapter).takeIf { it != -1 } val chapterIndex = text.indexOf(event.chapter).takeIf { it != -1 }
if (chapterIndex == null) { if (chapterIndex == null) {
return@launch return@launch
} }
@ -203,8 +202,9 @@ class ReaderModel @Inject constructor(
delay(300) delay(300)
yield() yield()
val scrollTo = (_state.value.text.size * event.progress).roundToInt() val scrollTo = (_state.value.text.lastIndex * event.progress).roundToInt()
_state.value.listState.requestScrollToItem(scrollTo) _state.value.listState.requestScrollToItem(scrollTo)
updateChapter(scrollTo)
} }
} }
@ -489,7 +489,7 @@ class ReaderModel @Inject constructor(
fun updateProgress(listState: LazyListState) { fun updateProgress(listState: LazyListState) {
viewModelScope.launch(Dispatchers.Main) { viewModelScope.launch(Dispatchers.Main) {
snapshotFlow { snapshotFlow {
listState.run { firstVisibleItemIndex to firstVisibleItemScrollOffset } listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset
}.distinctUntilChanged().debounce(300).collectLatest { (index, offset) -> }.distinctUntilChanged().debounce(300).collectLatest { (index, offset) ->
val progress = calculateProgress(index) val progress = calculateProgress(index)
if (progress == _state.value.book.progress) return@collectLatest if (progress == _state.value.book.progress) return@collectLatest
@ -519,6 +519,21 @@ class ReaderModel @Inject constructor(
} }
} }
fun findChapterIndexAndLength(index: Int): Pair<Int, Int> {
return findCurrentChapter(index)?.let { chapter ->
_state.value.text.run {
val startIndex = indexOf(chapter).coerceIn(0, lastIndex)
val endIndex = (indexOfFirst {
it is Chapter && indexOf(it) > startIndex
}.takeIf { it != -1 }) ?: (lastIndex + 1)
val currentIndexInChapter = (index - startIndex).coerceAtLeast(1)
val chapterLength = endIndex - (startIndex + 1)
currentIndexInChapter to chapterLength
}
} ?: (-1 to -1)
}
private fun updateChapter(index: Int) { private fun updateChapter(index: Int) {
viewModelScope.launch { viewModelScope.launch {
val (currentChapter, currentChapterProgress) = calculateCurrentChapter(index) val (currentChapter, currentChapterProgress) = calculateCurrentChapter(index)
@ -540,13 +555,13 @@ class ReaderModel @Inject constructor(
val currentChapter = findCurrentChapter(index) val currentChapter = findCurrentChapter(index)
val currentChapterProgress = currentChapter?.let { chapter -> val currentChapterProgress = currentChapter?.let { chapter ->
_state.value.text.run { _state.value.text.run {
val startIndex = (indexOf(chapter) + 1).coerceAtMost(count()) val startIndex = indexOf(chapter).coerceIn(0, lastIndex)
val endIndex = (indexOfFirst { val endIndex = (indexOfFirst {
it is Chapter && indexOf(it) > startIndex it is Chapter && indexOf(it) > startIndex
}.takeIf { it != -1 }?.minus(1)) ?: lastIndex }.takeIf { it != -1 }) ?: (lastIndex + 1)
val currentIndexInChapter = index - startIndex val currentIndexInChapter = (index - startIndex).coerceAtLeast(1)
val chapterLength = endIndex - startIndex val chapterLength = endIndex - (startIndex + 1)
(currentIndexInChapter / chapterLength.toFloat()) (currentIndexInChapter / chapterLength.toFloat())
} }
}.coerceAndPreventNaN() }.coerceAndPreventNaN()

View file

@ -40,6 +40,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import ua.acclorite.book_story.domain.navigator.Screen import ua.acclorite.book_story.domain.navigator.Screen
import ua.acclorite.book_story.domain.reader.ReaderColorEffects import ua.acclorite.book_story.domain.reader.ReaderColorEffects
import ua.acclorite.book_story.domain.reader.ReaderProgressCount
import ua.acclorite.book_story.domain.reader.ReaderTextAlignment import ua.acclorite.book_story.domain.reader.ReaderTextAlignment
import ua.acclorite.book_story.presentation.core.constants.Constants import ua.acclorite.book_story.presentation.core.constants.Constants
import ua.acclorite.book_story.presentation.core.constants.provideFonts import ua.acclorite.book_story.presentation.core.constants.provideFonts
@ -51,6 +52,7 @@ import ua.acclorite.book_story.presentation.reader.ReaderContent
import ua.acclorite.book_story.ui.book_info.BookInfoScreen import ua.acclorite.book_story.ui.book_info.BookInfoScreen
import ua.acclorite.book_story.ui.main.MainModel import ua.acclorite.book_story.ui.main.MainModel
import ua.acclorite.book_story.ui.settings.SettingsModel import ua.acclorite.book_story.ui.settings.SettingsModel
import kotlin.math.roundToInt
@Parcelize @Parcelize
data class ReaderScreen(val bookId: Int) : Screen, Parcelable { data class ReaderScreen(val bookId: Int) : Screen, Parcelable {
@ -289,24 +291,46 @@ data class ReaderScreen(val bookId: Int) : Screen, Parcelable {
(mainState.value.bottomBarPadding * 4f).dp (mainState.value.bottomBarPadding * 4f).dp
} }
val bookProgress = remember(state.value.book.progress) { val bookProgress = remember(
derivedStateOf { state.value.book.progress,
"${state.value.book.progress.calculateProgress(2)}%" state.value.text,
mainState.value.progressCount
) {
when (mainState.value.progressCount) {
ReaderProgressCount.PERCENTAGE -> {
"${state.value.book.progress.calculateProgress(2)}%"
}
ReaderProgressCount.QUANTITY -> {
val index =
(state.value.book.progress * state.value.text.lastIndex + 1).roundToInt()
"$index / ${state.value.text.size}"
}
} }
} }
val chapterProgress = remember( val chapterProgress = remember(
state.value.text,
state.value.book.progress,
state.value.currentChapter, state.value.currentChapter,
state.value.currentChapterProgress state.value.currentChapterProgress,
mainState.value.progressCount
) { ) {
derivedStateOf { if (state.value.currentChapter == null) return@remember ""
if (state.value.currentChapter == null) return@derivedStateOf "" when (mainState.value.progressCount) {
" (${state.value.currentChapterProgress.calculateProgress(2)}%)" ReaderProgressCount.PERCENTAGE -> {
" (${state.value.currentChapterProgress.calculateProgress(2)}%)"
}
ReaderProgressCount.QUANTITY -> {
val (index, length) = screenModel.findChapterIndexAndLength(
(state.value.book.progress * state.value.text.lastIndex).roundToInt()
).apply { if (first == -1 && second == -1) return@remember "" }
" (${index} / ${length})"
}
} }
} }
val progress = remember(bookProgress.value, chapterProgress.value) { val progress = remember(bookProgress, chapterProgress) {
derivedStateOf { "${bookProgress}${chapterProgress}"
"${bookProgress.value}${chapterProgress.value}"
}
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@ -397,7 +421,7 @@ data class ReaderScreen(val bookId: Int) : Screen, Parcelable {
horizontalGestureSensitivity = horizontalGestureSensitivity, horizontalGestureSensitivity = horizontalGestureSensitivity,
highlightedReading = mainState.value.highlightedReading, highlightedReading = mainState.value.highlightedReading,
highlightedReadingThickness = highlightedReadingThickness, highlightedReadingThickness = highlightedReadingThickness,
progress = progress.value, progress = progress,
progressBar = mainState.value.progressBar, progressBar = mainState.value.progressBar,
progressBarPadding = progressBarPadding, progressBarPadding = progressBarPadding,
progressBarAlignment = mainState.value.progressBarAlignment, progressBarAlignment = mainState.value.progressBarAlignment,

View file

@ -208,6 +208,7 @@
<string name="progress_bar_padding_option">Поля</string> <string name="progress_bar_padding_option">Поля</string>
<string name="progress_bar_alignment_option">Розташування</string> <string name="progress_bar_alignment_option">Розташування</string>
<string name="progress_bar_font_size_option">Розмір шрифта</string> <string name="progress_bar_font_size_option">Розмір шрифта</string>
<string name="progress_count_option">Підрахунок прогресу</string>
<!-- Browse --> <!-- Browse -->
<string name="browse_layout_option">Режим відображення</string> <string name="browse_layout_option">Режим відображення</string>
@ -286,6 +287,10 @@
<string name="font_thickness_normal">Звичайна</string> <string name="font_thickness_normal">Звичайна</string>
<string name="font_thickness_medium">Середня</string> <string name="font_thickness_medium">Середня</string>
<!-- Progress Count properties -->
<string name="progress_count_percentage">Відсотки</string>
<string name="progress_count_quantity">Кількість</string>
<!-- Themes --> <!-- Themes -->
<string name="dynamic_theme">Меркурій</string> <string name="dynamic_theme">Меркурій</string>
<string name="blue_theme">Нептун</string> <string name="blue_theme">Нептун</string>

View file

@ -265,6 +265,7 @@
<string name="progress_bar_padding_option">Margins</string> <string name="progress_bar_padding_option">Margins</string>
<string name="progress_bar_alignment_option">Alignment</string> <string name="progress_bar_alignment_option">Alignment</string>
<string name="progress_bar_font_size_option">Font size</string> <string name="progress_bar_font_size_option">Font size</string>
<string name="progress_count_option">Progress count</string>
<!-- Browse --> <!-- Browse -->
<string name="browse_layout_option">Display mode</string> <string name="browse_layout_option">Display mode</string>
@ -343,6 +344,10 @@
<string name="font_thickness_normal">Normal</string> <string name="font_thickness_normal">Normal</string>
<string name="font_thickness_medium">Medium</string> <string name="font_thickness_medium">Medium</string>
<!-- Progress Count properties -->
<string name="progress_count_percentage">Percentage</string>
<string name="progress_count_quantity">Quantity</string>
<!-- Themes --> <!-- Themes -->
<string name="dynamic_theme">Mercury</string> <string name="dynamic_theme">Mercury</string>
<string name="blue_theme">Neptune</string> <string name="blue_theme">Neptune</string>