refactor: settings viewmodel

This commit is contained in:
Acclorite 2025-08-24 15:25:46 +03:00
parent 2ab62e629f
commit b0c567bdc9
No known key found for this signature in database
GPG key ID: 6E54C611F6EE8593
13 changed files with 359 additions and 444 deletions

View file

@ -10,7 +10,6 @@ import android.graphics.Bitmap
typealias CoverImage = Bitmap
typealias Selected = Boolean
typealias ID = Int
typealias BottomSheet = String
typealias Dialog = String
typealias Drawer = String

View file

@ -40,6 +40,7 @@ import ua.acclorite.book_story.ui.common.helpers.ProvideSettings
import ua.acclorite.book_story.ui.main.MainActivityKeyboardManager
import ua.acclorite.book_story.ui.navigator.Navigator
import ua.acclorite.book_story.ui.navigator.NavigatorTabs
import ua.acclorite.book_story.ui.settings.SettingsEffects
import ua.acclorite.book_story.ui.theme.BookStoryTheme
import ua.acclorite.book_story.ui.theme.Transitions
import java.lang.reflect.Field
@ -56,7 +57,7 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen().setKeepOnScreenCondition {
!settings.initialized.value || !settingsModel.isReady.value
!settings.initialized.value || !settingsModel.initialized.value
}
super.onCreate(savedInstanceState)
@ -80,6 +81,10 @@ class MainActivity : AppCompatActivity() {
val historyModel = hiltViewModel<HistoryModel>()
val browseModel = hiltViewModel<BrowseModel>()
SettingsEffects(
effects = settingsModel.effects
)
ProvideSettings(settings) {
val tabs = persistentListOf(
NavigatorItem(

View file

@ -438,8 +438,7 @@ data class ReaderScreen(val bookId: Int) : Screen, Parcelable {
letterSpacing = letterSpacing,
paragraphIndentation = paragraphIndentation,
doubleClickTranslation = settings.doubleClickTranslation.value,
selectPreviousPreset = settingsModel::onEvent,
selectNextPreset = settingsModel::onEvent,
switchColorPreset = settingsModel::onEvent,
leave = screenModel::onEvent,
restoreCheckpoint = screenModel::onEvent,
scroll = screenModel::onEvent,

View file

@ -0,0 +1,24 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.presentation.settings
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.model.reader.ColorPreset
@Immutable
sealed class SettingsEffect {
data class OnScroll(
val listState: LazyListState,
val index: Int,
val offset: Int
) : SettingsEffect()
data class OnSwitchedColorPreset(
val newColorPreset: ColorPreset
) : SettingsEffect()
}

View file

@ -6,11 +6,8 @@
package ua.acclorite.book_story.presentation.settings
import android.content.Context
import android.net.Uri
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import ua.acclorite.book_story.core.ID
import ua.acclorite.book_story.core.language.Language
import ua.acclorite.book_story.domain.model.library.Category
import ua.acclorite.book_story.presentation.library.model.LibrarySortOrder
@ -22,11 +19,11 @@ sealed class SettingsEvent {
) : SettingsEvent()
data class OnGrantPersistableUriPermission(
val uri: Uri
val uri: String
) : SettingsEvent()
data class OnReleasePersistableUriPermission(
val uri: Uri
val uri: String
) : SettingsEvent()
data class OnCreateCategory(
@ -53,28 +50,24 @@ sealed class SettingsEvent {
) : SettingsEvent()
data class OnSelectColorPreset(
val id: ID
val id: Int,
) : SettingsEvent()
data class OnSelectPreviousPreset(
val context: Context
) : SettingsEvent()
data class OnSelectNextPreset(
val context: Context
data class OnSwitchColorPreset(
val previous: Boolean
) : SettingsEvent()
data class OnDeleteColorPreset(
val id: ID
val id: Int
) : SettingsEvent()
data class OnUpdateColorPresetTitle(
val id: ID,
val id: Int,
val title: String
) : SettingsEvent()
data class OnShuffleColorPreset(
val id: ID
val id: Int
) : SettingsEvent()
data class OnAddColorPreset(
@ -82,20 +75,16 @@ sealed class SettingsEvent {
val fontColor: Color
) : SettingsEvent()
data class OnUpdateColorPresetColor(
val id: Int,
val backgroundColor: Color?,
val fontColor: Color?
) : SettingsEvent()
data class OnReorderColorPresets(
val from: Int,
val to: Int
) : SettingsEvent()
data class OnUpdateColorPresetColor(
val id: ID,
val backgroundColor: Color?,
val fontColor: Color?
) : SettingsEvent()
data object OnConfirmReorderColorPresets : SettingsEvent()
data class OnScrollToColorPreset(
val index: Int
) : SettingsEvent()
}

View file

@ -6,20 +6,20 @@
package ua.acclorite.book_story.presentation.settings
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.library.CategorySort
import ua.acclorite.book_story.domain.model.reader.ColorPreset
import ua.acclorite.book_story.domain.use_case.category.AddCategoryUseCase
@ -37,8 +37,8 @@ import ua.acclorite.book_story.domain.use_case.color_preset.UpdateColorPresetUse
import ua.acclorite.book_story.domain.use_case.permission.GrantPersistableUriPermissionUseCase
import ua.acclorite.book_story.domain.use_case.permission.ReleasePersistableUriPermissionUseCase
import ua.acclorite.book_story.domain.use_case.settings.UpdateLanguageUseCase
import ua.acclorite.book_story.ui.common.helpers.showToast
import javax.inject.Inject
import kotlin.coroutines.coroutineContext
import kotlin.random.Random
@HiltViewModel
@ -65,75 +65,64 @@ class SettingsModel @Inject constructor(
private val _state = MutableStateFlow(SettingsState())
val state = _state.asStateFlow()
private val _isReady = MutableStateFlow(false)
val isReady = _isReady.asStateFlow()
private val _effects = MutableSharedFlow<SettingsEffect>()
val effects = _effects.asSharedFlow()
private var selectColorPresetJob: Job? = null
private var addColorPresetJob: Job? = null
private var deleteColorPresetJob: Job? = null
private var updateColorColorPresetJob: Job? = null
private var updateTitleColorPresetJob: Job? = null
private var shuffleColorPresetJob: Job? = null
private val _initialized = MutableStateFlow(false)
val initialized = _initialized.asStateFlow()
private var colorPresetJob: Job? = null
init {
viewModelScope.launch(Dispatchers.IO) {
viewModelScope.launch {
var colorPresets = getColorPresetsUseCase()
if (colorPresets.isEmpty()) {
updateColorPresetUseCase(ColorPreset.default)
getColorPresetsUseCase().first().select()
getColorPresetsUseCase().first().selectColorPreset()
colorPresets = getColorPresetsUseCase()
}
val scrollIndex = colorPresets.indexOfFirst {
it.isSelected
}
val scrollIndex = colorPresets.indexOfFirst { it.isSelected }
if (scrollIndex != -1) {
launch(Dispatchers.Main) {
try {
_state.value.colorPresetListState.requestScrollToItem(index = scrollIndex)
} catch (e: Exception) {
e.printStackTrace()
}
}
_effects.emit(
SettingsEffect.OnScroll(
listState = _state.value.colorPresetListState,
index = scrollIndex,
offset = 0
)
)
}
_state.update {
it.copy(
selectedColorPreset = colorPresets.selected(),
selectedColorPreset = colorPresets.getSelectedColorPreset(),
colorPresets = colorPresets,
categories = getCategoriesUseCase(), // Getting categories
categories = getCategoriesUseCase(),
categoriesSort = getCategorySortingUseCase()
)
}
Log.i("SETTINGS", "SettingsModel is ready.")
_isReady.update { true }
_initialized.update { true }
}
}
fun onEvent(event: SettingsEvent) {
when (event) {
is SettingsEvent.OnUpdateLanguage -> updateLanguageUseCase(event.language)
is SettingsEvent.OnGrantPersistableUriPermission -> {
viewModelScope.launch {
grantPersistableUriPermissionUseCase(
event.uri.toString()
)
viewModelScope.launch {
when (event) {
is SettingsEvent.OnUpdateLanguage -> {
updateLanguageUseCase(event.language)
}
}
is SettingsEvent.OnReleasePersistableUriPermission -> {
viewModelScope.launch {
releasePersistableUriPermissionUseCase(
event.uri.toString()
)
is SettingsEvent.OnGrantPersistableUriPermission -> {
grantPersistableUriPermissionUseCase(event.uri)
}
}
is SettingsEvent.OnCreateCategory -> {
viewModelScope.launch {
is SettingsEvent.OnReleasePersistableUriPermission -> {
releasePersistableUriPermissionUseCase(event.uri)
}
is SettingsEvent.OnCreateCategory -> {
addCategoryUseCase(event.title)
_state.update {
it.copy(
@ -141,10 +130,8 @@ class SettingsModel @Inject constructor(
)
}
}
}
is SettingsEvent.OnUpdateCategoryTitle -> {
viewModelScope.launch {
is SettingsEvent.OnUpdateCategoryTitle -> {
updateCategoryUseCase(
categoryId = event.id,
newTitle = event.title
@ -155,10 +142,8 @@ class SettingsModel @Inject constructor(
)
}
}
}
is SettingsEvent.OnUpdateCategoryOrder -> {
viewModelScope.launch {
is SettingsEvent.OnUpdateCategoryOrder -> {
updateCategoriesOrderUseCase(
categories = event.categories
)
@ -168,10 +153,8 @@ class SettingsModel @Inject constructor(
)
}
}
}
is SettingsEvent.OnRemoveCategory -> {
viewModelScope.launch {
is SettingsEvent.OnRemoveCategory -> {
deleteCategoryUseCase(
category = event.category
)
@ -181,10 +164,8 @@ class SettingsModel @Inject constructor(
)
}
}
}
is SettingsEvent.OnUpdateCategorySort -> {
viewModelScope.launch {
is SettingsEvent.OnUpdateCategorySort -> {
updateCategorySortingUseCase(
categorySort = CategorySort(
categoryId = event.categoryId,
@ -198,359 +179,268 @@ class SettingsModel @Inject constructor(
)
}
}
}
is SettingsEvent.OnSelectColorPreset -> {
viewModelScope.launch {
cancelColorPresetJobs()
selectColorPresetJob = launch {
val colorPreset = _state.value.colorPresets.getColorPresetById(event.id)
?: return@launch
is SettingsEvent.OnSelectColorPreset -> {
withContext(Dispatchers.IO) {
colorPresetJob?.join()
colorPresetJob = viewModelScope.launch(Dispatchers.Default) {
val colorPreset = _state.value.colorPresets.getColorPresetById(event.id)
?: return@launch
yield()
ensureActive()
colorPreset.select(animate = true)
val colorPresets = _state.value.colorPresets.map {
it.copy(isSelected = colorPreset.id == it.id)
}
_state.update {
it.copy(
selectedColorPreset = colorPresets.selected(),
colorPresets = colorPresets
)
colorPreset.selectColorPreset(animate = true)
val colorPresets = _state.value.colorPresets.map {
it.copy(isSelected = colorPreset.id == it.id)
}
_state.update {
it.copy(
selectedColorPreset = colorPresets.getSelectedColorPreset(),
colorPresets = colorPresets
)
}
}
}
}
}
is SettingsEvent.OnSelectPreviousPreset -> {
viewModelScope.launch {
cancelColorPresetJobs()
selectColorPresetJob = launch {
val colorPresets = _state.value.colorPresets
val selectedPreset = _state.value.selectedColorPreset
is SettingsEvent.OnSwitchColorPreset -> {
withContext(Dispatchers.IO) {
colorPresetJob?.join()
colorPresetJob = viewModelScope.launch(Dispatchers.Default) {
val colorPresets = _state.value.colorPresets
if (colorPresets.size == 1) return@launch
if (colorPresets.size == 1) {
return@launch
}
val selectedPreset = _state.value.selectedColorPreset
val selectedPresetIndex = colorPresets.indexOf(selectedPreset)
if (selectedPresetIndex == -1) return@launch
val selectedPresetIndex = colorPresets.indexOf(selectedPreset)
if (selectedPresetIndex == -1) {
return@launch
}
val newColorPresetIndex = if (event.previous) {
when (selectedPresetIndex) {
0 -> colorPresets.lastIndex
else -> selectedPresetIndex - 1
}
} else {
when (selectedPresetIndex) {
colorPresets.lastIndex -> 0
else -> selectedPresetIndex + 1
}
}
val newColorPreset = colorPresets.getOrNull(newColorPresetIndex)
?: return@launch
val previousColorPresetIndex = when (selectedPresetIndex) {
0 -> {
colorPresets.lastIndex
ensureActive()
newColorPreset.selectColorPreset()
val updatedColorPresets = _state.value.colorPresets.map {
it.copy(isSelected = newColorPreset.id == it.id)
}
_state.update {
it.copy(
selectedColorPreset = updatedColorPresets.getSelectedColorPreset(),
colorPresets = updatedColorPresets
)
}
else -> {
selectedPresetIndex - 1
}
_effects.emit(SettingsEffect.OnSwitchedColorPreset(newColorPreset))
}
val previousColorPreset = colorPresets.getOrNull(previousColorPresetIndex)
?: return@launch
}
}
yield()
is SettingsEvent.OnDeleteColorPreset -> {
withContext(Dispatchers.IO) {
colorPresetJob?.join()
colorPresetJob = viewModelScope.launch(Dispatchers.Default) {
if (_state.value.colorPresets.size == 1) return@launch
val colorPreset = _state.value.colorPresets.getColorPresetById(event.id)
?: return@launch
previousColorPreset.select()
val updatedColorPresets = _state.value.colorPresets.map {
it.copy(isSelected = previousColorPreset.id == it.id)
}
_state.update {
it.copy(
selectedColorPreset = updatedColorPresets.selected(),
colorPresets = updatedColorPresets
)
}
val position = _state.value.colorPresets.indexOf(colorPreset)
if (position == -1) return@launch
withContext(Dispatchers.Main) {
event.context.getString(
R.string.color_preset_selected_query,
if (previousColorPreset.name.isNullOrBlank()) {
event.context.getString(
R.string.color_preset_query,
previousColorPreset.id.toString()
)
val nextPosition =
if (position == _state.value.colorPresets.lastIndex) {
position - 1
} else {
previousColorPreset.name
}.trim()
).showToast(event.context, longToast = false)
position
}
ensureActive()
deleteColorPresetUseCase(colorPreset)
val nextColorPreset = getColorPresetsUseCase().getOrNull(nextPosition)
?: return@launch
nextColorPreset.selectColorPreset()
val colorPresets = getColorPresetsUseCase()
_state.update {
it.copy(
selectedColorPreset = colorPresets.getSelectedColorPreset(),
colorPresets = colorPresets
)
}
_effects.emit(
SettingsEffect.OnScroll(
listState = _state.value.colorPresetListState,
index = nextPosition,
offset = 0
)
)
}
}
}
}
is SettingsEvent.OnSelectNextPreset -> {
viewModelScope.launch {
cancelColorPresetJobs()
selectColorPresetJob = launch {
val colorPresets = _state.value.colorPresets
val selectedPreset = _state.value.selectedColorPreset
is SettingsEvent.OnUpdateColorPresetTitle -> {
withContext(Dispatchers.IO) {
colorPresetJob?.join()
colorPresetJob = viewModelScope.launch(Dispatchers.Default) {
val colorPreset = _state.value.colorPresets.getColorPresetById(event.id)
?: return@launch
val updatedColorPreset = colorPreset.copy(name = event.title)
if (colorPresets.size == 1) {
return@launch
}
ensureActive()
val selectedPresetIndex = colorPresets.indexOf(selectedPreset)
if (selectedPresetIndex == -1) {
return@launch
}
val nextColorPresetIndex = when (selectedPresetIndex) {
colorPresets.lastIndex -> {
0
}
else -> {
selectedPresetIndex + 1
}
}
val nextColorPreset = colorPresets.getOrNull(nextColorPresetIndex)
?: return@launch
yield()
nextColorPreset.select()
val updatedColorPresets = _state.value.colorPresets.map {
it.copy(isSelected = nextColorPreset.id == it.id)
}
_state.update {
it.copy(
selectedColorPreset = updatedColorPresets.selected(),
colorPresets = updatedColorPresets
)
}
withContext(Dispatchers.Main) {
event.context.getString(
R.string.color_preset_selected_query,
if (nextColorPreset.name.isNullOrBlank()) {
event.context.getString(
R.string.color_preset_query,
nextColorPreset.id.toString()
updateColorPresetUseCase(updatedColorPreset)
_state.update {
it.copy(
selectedColorPreset = updatedColorPreset,
colorPresets = it.colorPresets.updateColorPreset(
updatedColorPreset
)
} else {
nextColorPreset.name
}.trim()
).showToast(event.context, longToast = false)
)
}
}
}
}
}
is SettingsEvent.OnDeleteColorPreset -> {
viewModelScope.launch {
cancelColorPresetJobs()
deleteColorPresetJob = launch {
if (_state.value.colorPresets.size == 1) return@launch
val colorPreset = _state.value.colorPresets.getColorPresetById(event.id)
?: return@launch
yield()
val position = _state.value.colorPresets.indexOf(colorPreset)
if (position == -1) {
return@launch
}
val nextPosition = if (position == _state.value.colorPresets.lastIndex) {
position - 1
} else {
position
}
yield()
deleteColorPresetUseCase(colorPreset)
val nextColorPreset = getColorPresetsUseCase().getOrNull(nextPosition)
?: return@launch
nextColorPreset.select()
val colorPresets = getColorPresetsUseCase()
_state.update {
it.copy(
selectedColorPreset = colorPresets.selected(),
colorPresets = colorPresets
)
}
onEvent(
SettingsEvent.OnScrollToColorPreset(
index = nextPosition
)
)
}
}
}
is SettingsEvent.OnUpdateColorPresetTitle -> {
viewModelScope.launch {
cancelColorPresetJobs()
updateTitleColorPresetJob = launch {
val colorPreset = _state.value.colorPresets.getColorPresetById(event.id)
?: return@launch
yield()
val updatedColorPreset = colorPreset.copy(
name = event.title
)
yield()
updateColorPresetUseCase(updatedColorPreset)
_state.update {
it.copy(
selectedColorPreset = updatedColorPreset,
colorPresets = it.colorPresets.updateColorPreset(
updatedColorPreset
is SettingsEvent.OnShuffleColorPreset -> {
withContext(Dispatchers.IO) {
colorPresetJob?.join()
colorPresetJob = viewModelScope.launch(Dispatchers.Default) {
val colorPreset = _state.value.colorPresets.getColorPresetById(event.id)
?: return@launch
val shuffledColorPreset = colorPreset.copy(
backgroundColor = colorPreset.backgroundColor.copy(
red = Random.nextFloat(),
green = Random.nextFloat(),
blue = Random.nextFloat()
),
fontColor = colorPreset.fontColor.copy(
red = Random.nextFloat(),
green = Random.nextFloat(),
blue = Random.nextFloat()
)
)
}
}
}
}
is SettingsEvent.OnShuffleColorPreset -> {
viewModelScope.launch {
cancelColorPresetJobs()
shuffleColorPresetJob = launch {
val colorPreset = _state.value.colorPresets.getColorPresetById(event.id)
?: return@launch
ensureActive()
yield()
val shuffledColorPreset = colorPreset.copy(
backgroundColor = colorPreset.backgroundColor.copy(
red = Random.nextFloat(),
green = Random.nextFloat(),
blue = Random.nextFloat()
),
fontColor = colorPreset.fontColor.copy(
red = Random.nextFloat(),
green = Random.nextFloat(),
blue = Random.nextFloat()
)
)
yield()
updateColorPresetUseCase(shuffledColorPreset)
_state.update {
it.copy(
selectedColorPreset = shuffledColorPreset,
colorPresets = it.colorPresets.updateColorPreset(
shuffledColorPreset
updateColorPresetUseCase(shuffledColorPreset)
_state.update {
it.copy(
selectedColorPreset = shuffledColorPreset,
colorPresets = it.colorPresets.updateColorPreset(
shuffledColorPreset
)
)
)
}
}
}
}
}
is SettingsEvent.OnAddColorPreset -> {
viewModelScope.launch {
cancelColorPresetJobs()
addColorPresetJob = launch {
yield()
val newColorPreset = ColorPreset.default.copy(
backgroundColor = event.backgroundColor,
fontColor = event.fontColor
)
updateColorPresetUseCase(newColorPreset)
getColorPresetsUseCase().last().select()
val colorPresets = getColorPresetsUseCase()
_state.update {
it.copy(
selectedColorPreset = colorPresets.selected(),
colorPresets = colorPresets
is SettingsEvent.OnAddColorPreset -> {
withContext(Dispatchers.IO) {
colorPresetJob?.join()
colorPresetJob = viewModelScope.launch(Dispatchers.Default) {
val newColorPreset = ColorPreset.default.copy(
backgroundColor = event.backgroundColor,
fontColor = event.fontColor
)
}
onEvent(SettingsEvent.OnScrollToColorPreset(colorPresets.lastIndex))
}
}
}
ensureActive()
is SettingsEvent.OnUpdateColorPresetColor -> {
viewModelScope.launch {
cancelColorPresetJobs()
updateColorColorPresetJob = launch {
val colorPreset = _state.value.colorPresets.getColorPresetById(event.id)
?: return@launch
updateColorPresetUseCase(newColorPreset)
getColorPresetsUseCase().last().selectColorPreset()
val colorPresets = getColorPresetsUseCase()
yield()
val updatedColorPreset = colorPreset.copy(
backgroundColor = event.backgroundColor
?: colorPreset.backgroundColor,
fontColor = event.fontColor
?: colorPreset.fontColor
)
yield()
updateColorPresetUseCase(updatedColorPreset)
_state.update {
it.copy(
selectedColorPreset = updatedColorPreset,
colorPresets = it.colorPresets.updateColorPreset(
updatedColorPreset
_state.update {
it.copy(
selectedColorPreset = colorPresets.getSelectedColorPreset(),
colorPresets = colorPresets
)
)
}
}
}
}
is SettingsEvent.OnReorderColorPresets -> {
viewModelScope.launch {
cancelColorPresetJobs()
launch {
val reorderedColorPresets = _state.value.colorPresets
.toMutableList()
.apply {
add(event.to, removeAt(event.from))
}
_state.update {
it.copy(
colorPresets = reorderedColorPresets
_effects.emit(
SettingsEffect.OnScroll(
listState = _state.value.colorPresetListState,
index = colorPresets.lastIndex,
offset = 0
)
)
}
}
}
}
is SettingsEvent.OnConfirmReorderColorPresets -> {
viewModelScope.launch {
cancelColorPresetJobs()
launch {
reorderColorPresetsUseCase(_state.value.colorPresets)
is SettingsEvent.OnUpdateColorPresetColor -> {
withContext(Dispatchers.IO) {
colorPresetJob?.join()
colorPresetJob = viewModelScope.launch(Dispatchers.Default) {
val colorPreset = _state.value.colorPresets.getColorPresetById(event.id)
?: return@launch
val updatedColorPreset = colorPreset.copy(
backgroundColor = event.backgroundColor
?: colorPreset.backgroundColor,
fontColor = event.fontColor
?: colorPreset.fontColor
)
ensureActive()
updateColorPresetUseCase(updatedColorPreset)
_state.update {
it.copy(
selectedColorPreset = updatedColorPreset,
colorPresets = it.colorPresets.updateColorPreset(
updatedColorPreset
)
)
}
}
}
}
}
is SettingsEvent.OnScrollToColorPreset -> {
viewModelScope.launch {
try {
_state.value.colorPresetListState.requestScrollToItem(
index = event.index
)
} catch (e: Exception) {
e.printStackTrace()
is SettingsEvent.OnReorderColorPresets -> {
withContext(Dispatchers.IO) {
colorPresetJob?.join()
withContext(Dispatchers.Default) {
val reorderedColorPresets = _state.value.colorPresets
.toMutableList()
.also { colorPresets ->
colorPresets.add(event.to, colorPresets.removeAt(event.from))
}
_state.update {
it.copy(
colorPresets = reorderedColorPresets
)
}
}
}
}
is SettingsEvent.OnConfirmReorderColorPresets -> {
withContext(Dispatchers.IO) {
colorPresetJob?.join()
withContext(Dispatchers.Default) {
reorderColorPresetsUseCase(_state.value.colorPresets)
}
}
}
}
}
}
private suspend fun ColorPreset.select(animate: Boolean = false) {
private suspend fun ColorPreset.selectColorPreset(animate: Boolean = false) {
selectColorPresetUseCase(this)
_state.update {
it.copy(
@ -560,53 +450,28 @@ class SettingsModel @Inject constructor(
}
private fun List<ColorPreset>.getColorPresetById(id: Int): ColorPreset? {
return firstOrNull {
return find {
it.id == id
}
}
private fun List<ColorPreset>?.selected(): ColorPreset {
val presets = this ?: _state.value.colorPresets
if (presets.size == 1) {
return presets.first()
}
val selectedPreset = presets.firstOrNull { it.isSelected }
if (selectedPreset == null) {
return ColorPreset.default
}
return selectedPreset
private fun List<ColorPreset>.getSelectedColorPreset(): ColorPreset {
if (size == 1) return first()
find { it.isSelected }?.also { return it }
return ColorPreset.default
}
private fun List<ColorPreset>.updateColorPreset(colorPreset: ColorPreset): List<ColorPreset> {
if (size == 1) {
return listOf(colorPreset)
if (size == 1) return listOf(colorPreset)
return map {
if (it.isSelected) colorPreset
else it
}
return this.map {
if (it.isSelected) {
colorPreset
} else {
it
}
}
}
private fun cancelColorPresetJobs() {
selectColorPresetJob?.cancel()
addColorPresetJob?.cancel()
updateTitleColorPresetJob?.cancel()
shuffleColorPresetJob?.cancel()
updateColorColorPresetJob?.cancel()
deleteColorPresetJob?.cancel()
}
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
mutex.withLock {
yield()
coroutineContext.ensureActive()
this.value = function(this.value)
}
}

View file

@ -28,11 +28,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.lerp
import ua.acclorite.book_story.core.ID
import ua.acclorite.book_story.ui.common.components.common.AnimatedVisibility
data class TopAppBarData(
val contentID: ID,
val contentID: Int,
val contentNavigationIcon: @Composable () -> Unit,
val contentTitle: @Composable () -> Unit,
val contentActions: @Composable RowScope.() -> Unit
@ -46,7 +45,7 @@ data class TopAppBarData(
* @param scrolledContainerColor Scrolled container color of the TopBar.
* @param scrollBehavior [TopAppBarScrollBehavior].
* @param isTopBarScrolled Whether isScrolled state should be forced or not.
* @param shownTopBar [ID] of the top bar to show.
* @param shownTopBar ID of the top bar to show.
* @param topBars Pass a list of all [TopAppBarData] to show.
* @param customContent Custom content below [TopAppBar].
*/
@ -59,7 +58,7 @@ fun TopAppBar(
scrollBehavior: TopAppBarScrollBehavior?,
isTopBarScrolled: Boolean?,
shownTopBar: ID,
shownTopBar: Int,
topBars: List<TopAppBarData>,
customContent: @Composable ColumnScope.() -> Unit = {}
) {

View file

@ -12,17 +12,14 @@ import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import ua.acclorite.book_story.presentation.settings.SettingsEvent
@Composable
fun Modifier.readerColorPresetChange(
colorPresetChangeEnabled: Boolean,
isLoading: Boolean,
selectPreviousPreset: (SettingsEvent.OnSelectPreviousPreset) -> Unit,
selectNextPreset: (SettingsEvent.OnSelectNextPreset) -> Unit
switchColorPreset: (SettingsEvent.OnSwitchColorPreset) -> Unit
): Modifier {
val context = LocalContext.current
val offset = remember { mutableFloatStateOf(0f) }
return this.then(
if (colorPresetChangeEnabled && !isLoading) {
@ -33,17 +30,17 @@ fun Modifier.readerColorPresetChange(
onDragEnd = {
when {
offset.floatValue > 200 -> {
selectPreviousPreset(
SettingsEvent.OnSelectPreviousPreset(
context = context
switchColorPreset(
SettingsEvent.OnSwitchColorPreset(
previous = true
)
)
}
offset.floatValue < -200 -> {
selectNextPreset(
SettingsEvent.OnSelectNextPreset(
context = context
switchColorPreset(
SettingsEvent.OnSwitchColorPreset(
previous = false
)
)
}

View file

@ -88,8 +88,7 @@ fun ReaderContent(
letterSpacing: TextUnit,
paragraphIndentation: TextUnit,
doubleClickTranslation: Boolean,
selectPreviousPreset: (SettingsEvent.OnSelectPreviousPreset) -> Unit,
selectNextPreset: (SettingsEvent.OnSelectNextPreset) -> Unit,
switchColorPreset: (SettingsEvent.OnSwitchColorPreset) -> Unit,
menuVisibility: (ReaderEvent.OnMenuVisibility) -> Unit,
leave: (ReaderEvent.OnLeave) -> Unit,
restoreCheckpoint: (ReaderEvent.OnRestoreCheckpoint) -> Unit,
@ -165,8 +164,7 @@ fun ReaderContent(
letterSpacing = letterSpacing,
paragraphIndentation = paragraphIndentation,
doubleClickTranslation = doubleClickTranslation,
selectPreviousPreset = selectPreviousPreset,
selectNextPreset = selectNextPreset,
switchColorPreset = switchColorPreset,
menuVisibility = menuVisibility,
leave = leave,
restoreCheckpoint = restoreCheckpoint,

View file

@ -92,8 +92,7 @@ fun ReaderScaffold(
letterSpacing: TextUnit,
paragraphIndentation: TextUnit,
doubleClickTranslation: Boolean,
selectPreviousPreset: (SettingsEvent.OnSelectPreviousPreset) -> Unit,
selectNextPreset: (SettingsEvent.OnSelectNextPreset) -> Unit,
switchColorPreset: (SettingsEvent.OnSwitchColorPreset) -> Unit,
menuVisibility: (ReaderEvent.OnMenuVisibility) -> Unit,
leave: (ReaderEvent.OnLeave) -> Unit,
restoreCheckpoint: (ReaderEvent.OnRestoreCheckpoint) -> Unit,
@ -127,8 +126,7 @@ fun ReaderScaffold(
isLoading = isLoading,
lockMenu = lockMenu,
leave = leave,
selectPreviousPreset = selectPreviousPreset,
selectNextPreset = selectNextPreset,
switchColorPreset = switchColorPreset,
showSettingsBottomSheet = showSettingsBottomSheet,
showChaptersDrawer = showChaptersDrawer,
navigateBack = navigateBack,

View file

@ -49,8 +49,7 @@ fun ReaderTopBar(
isLoading: Boolean,
lockMenu: Boolean,
leave: (ReaderEvent.OnLeave) -> Unit,
selectPreviousPreset: (SettingsEvent.OnSelectPreviousPreset) -> Unit,
selectNextPreset: (SettingsEvent.OnSelectNextPreset) -> Unit,
switchColorPreset: (SettingsEvent.OnSwitchColorPreset) -> Unit,
showSettingsBottomSheet: (ReaderEvent.OnShowSettingsBottomSheet) -> Unit,
showChaptersDrawer: (ReaderEvent.OnShowChaptersDrawer) -> Unit,
navigateToBookInfo: (ReaderEvent.OnNavigateToBookInfo) -> Unit,
@ -68,8 +67,7 @@ fun ReaderTopBar(
.readerColorPresetChange(
colorPresetChangeEnabled = fastColorPresetChange,
isLoading = isLoading,
selectPreviousPreset = selectPreviousPreset,
selectNextPreset = selectNextPreset
switchColorPreset = switchColorPreset
)
) {
TopAppBar(

View file

@ -0,0 +1,44 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.ui.settings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.LocalContext
import kotlinx.coroutines.flow.SharedFlow
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.settings.SettingsEffect
import ua.acclorite.book_story.ui.common.helpers.showToast
@Composable
fun SettingsEffects(effects: SharedFlow<SettingsEffect>) {
val context = LocalContext.current
LaunchedEffect(effects) {
effects.collect { effect ->
when (effect) {
is SettingsEffect.OnScroll -> {
effect.listState.requestScrollToItem(effect.index, effect.offset)
}
is SettingsEffect.OnSwitchedColorPreset -> {
context.getString(
R.string.color_preset_selected_query,
if (effect.newColorPreset.name.isNullOrBlank()) {
context.getString(
R.string.color_preset_query,
effect.newColorPreset.id.toString()
)
} else {
effect.newColorPreset.name
}.trim()
).showToast(context, longToast = false)
}
}
}
}
}

View file

@ -73,7 +73,7 @@ fun BrowseScanOption() {
if (uri == null) return@rememberLauncherForActivityResult
settingsModel.onEvent(
SettingsEvent.OnGrantPersistableUriPermission(
uri = uri
uri = uri.toString()
)
)
@ -95,7 +95,7 @@ fun BrowseScanOption() {
releasePersistableUriPermission = {
settingsModel.onEvent(
SettingsEvent.OnReleasePersistableUriPermission(
uri = permission.uri
uri = permission.uri.toString()
)
)