diff --git a/app/src/main/java/ua/acclorite/book_story/core/Typealias.kt b/app/src/main/java/ua/acclorite/book_story/core/Typealias.kt index 08978e52..32f11642 100644 --- a/app/src/main/java/ua/acclorite/book_story/core/Typealias.kt +++ b/app/src/main/java/ua/acclorite/book_story/core/Typealias.kt @@ -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 \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/main/MainActivity.kt b/app/src/main/java/ua/acclorite/book_story/presentation/main/MainActivity.kt index 69b73bc8..dee42e95 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/main/MainActivity.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/main/MainActivity.kt @@ -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() val browseModel = hiltViewModel() + SettingsEffects( + effects = settingsModel.effects + ) + ProvideSettings(settings) { val tabs = persistentListOf( NavigatorItem( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderScreen.kt index 653f31bf..71d5fb77 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderScreen.kt @@ -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, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsEffect.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsEffect.kt new file mode 100644 index 00000000..2a5c24a9 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsEffect.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsEvent.kt index ac1ed9f4..e476d81f 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsEvent.kt @@ -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() } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsModel.kt index de486166..4b7a5608 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsModel.kt @@ -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() + 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.getColorPresetById(id: Int): ColorPreset? { - return firstOrNull { + return find { it.id == id } } - private fun List?.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.getSelectedColorPreset(): ColorPreset { + if (size == 1) return first() + find { it.isSelected }?.also { return it } + return ColorPreset.default } private fun List.updateColorPreset(colorPreset: ColorPreset): List { - 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 MutableStateFlow.update(function: (T) -> T) { mutex.withLock { - yield() + coroutineContext.ensureActive() this.value = function(this.value) } } diff --git a/app/src/main/java/ua/acclorite/book_story/ui/common/components/top_bar/TopAppBar.kt b/app/src/main/java/ua/acclorite/book_story/ui/common/components/top_bar/TopAppBar.kt index 493b0608..df6c53a4 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/common/components/top_bar/TopAppBar.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/common/components/top_bar/TopAppBar.kt @@ -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, customContent: @Composable ColumnScope.() -> Unit = {} ) { diff --git a/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderColorPresetChange.kt b/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderColorPresetChange.kt index d3ed74ef..b592bdb3 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderColorPresetChange.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderColorPresetChange.kt @@ -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 ) ) } diff --git a/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderContent.kt b/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderContent.kt index 6229e72e..c150af12 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderContent.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderContent.kt @@ -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, diff --git a/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderScaffold.kt b/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderScaffold.kt index 3e5522ae..0a3feb1e 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderScaffold.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderScaffold.kt @@ -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, diff --git a/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderTopBar.kt b/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderTopBar.kt index 0c4065e0..4604ef4f 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderTopBar.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderTopBar.kt @@ -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( diff --git a/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsEffects.kt b/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsEffects.kt new file mode 100644 index 00000000..52235d74 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsEffects.kt @@ -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) { + 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) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/settings/browse/scan/components/BrowseScanOption.kt b/app/src/main/java/ua/acclorite/book_story/ui/settings/browse/scan/components/BrowseScanOption.kt index b6cc3ade..c1bb9217 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/settings/browse/scan/components/BrowseScanOption.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/settings/browse/scan/components/BrowseScanOption.kt @@ -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() ) )