🚀 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_ALIGNMENT = stringPreferencesKey("progress_bar_alignment")
val PROGRESS_BAR_FONT_SIZE = intPreferencesKey("progress_bar_font_size")
val PROGRESS_COUNT = stringPreferencesKey("progress_count")
// Browse settings
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.ProgressBarOption
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(
titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary },
@ -26,6 +27,10 @@ fun LazyListScope.ProgressSubcategory(
showTitle = showTitle,
showDivider = showDivider,
) {
item {
ProgressCountOption()
}
item {
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 OnChangeBrowsePinnedPaths(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.toFontThickness
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.toTextAlignment
import ua.acclorite.book_story.domain.use_case.data_store.ChangeLanguage
@ -494,6 +495,14 @@ class MainModel @Inject constructor(
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.ReaderFontThickness
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.ReaderTextAlignment
import ua.acclorite.book_story.domain.reader.toColorEffects
import ua.acclorite.book_story.domain.reader.toFontThickness
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.toTextAlignment
import ua.acclorite.book_story.domain.util.HorizontalAlignment
@ -107,6 +109,7 @@ data class MainState(
val progressBarPadding: Int = provideDefaultValue { 4 },
val progressBarAlignment: HorizontalAlignment = provideDefaultValue { HorizontalAlignment.CENTER },
val progressBarFontSize: Int = provideDefaultValue { 8 },
val progressCount: ReaderProgressCount = provideDefaultValue { ReaderProgressCount.PERCENTAGE },
// Browse Settings
val browseLayout: BrowseLayout = provideDefaultValue { BrowseLayout.LIST },
@ -357,6 +360,10 @@ data class MainState(
fontThickness = provideValue(
FONT_THICKNESS, convert = { toFontThickness() }
) { fontThickness },
progressCount = provideValue(
PROGRESS_COUNT, convert = { toProgressCount() }
) { progressCount },
)
}
}

View file

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

View file

@ -40,6 +40,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.parcelize.Parcelize
import ua.acclorite.book_story.domain.navigator.Screen
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.presentation.core.constants.Constants
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.main.MainModel
import ua.acclorite.book_story.ui.settings.SettingsModel
import kotlin.math.roundToInt
@Parcelize
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
}
val bookProgress = remember(state.value.book.progress) {
derivedStateOf {
"${state.value.book.progress.calculateProgress(2)}%"
val bookProgress = remember(
state.value.book.progress,
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(
state.value.text,
state.value.book.progress,
state.value.currentChapter,
state.value.currentChapterProgress
state.value.currentChapterProgress,
mainState.value.progressCount
) {
derivedStateOf {
if (state.value.currentChapter == null) return@derivedStateOf ""
" (${state.value.currentChapterProgress.calculateProgress(2)}%)"
if (state.value.currentChapter == null) return@remember ""
when (mainState.value.progressCount) {
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) {
derivedStateOf {
"${bookProgress.value}${chapterProgress.value}"
}
val progress = remember(bookProgress, chapterProgress) {
"${bookProgress}${chapterProgress}"
}
LaunchedEffect(Unit) {
@ -397,7 +421,7 @@ data class ReaderScreen(val bookId: Int) : Screen, Parcelable {
horizontalGestureSensitivity = horizontalGestureSensitivity,
highlightedReading = mainState.value.highlightedReading,
highlightedReadingThickness = highlightedReadingThickness,
progress = progress.value,
progress = progress,
progressBar = mainState.value.progressBar,
progressBarPadding = progressBarPadding,
progressBarAlignment = mainState.value.progressBarAlignment,

View file

@ -208,6 +208,7 @@
<string name="progress_bar_padding_option">Поля</string>
<string name="progress_bar_alignment_option">Розташування</string>
<string name="progress_bar_font_size_option">Розмір шрифта</string>
<string name="progress_count_option">Підрахунок прогресу</string>
<!-- Browse -->
<string name="browse_layout_option">Режим відображення</string>
@ -286,6 +287,10 @@
<string name="font_thickness_normal">Звичайна</string>
<string name="font_thickness_medium">Середня</string>
<!-- Progress Count properties -->
<string name="progress_count_percentage">Відсотки</string>
<string name="progress_count_quantity">Кількість</string>
<!-- Themes -->
<string name="dynamic_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_alignment_option">Alignment</string>
<string name="progress_bar_font_size_option">Font size</string>
<string name="progress_count_option">Progress count</string>
<!-- Browse -->
<string name="browse_layout_option">Display mode</string>
@ -343,6 +344,10 @@
<string name="font_thickness_normal">Normal</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 -->
<string name="dynamic_theme">Mercury</string>
<string name="blue_theme">Neptune</string>