refactor: bookInfo viewmodel
This commit is contained in:
parent
28cbf1188d
commit
6f53c70b5b
23 changed files with 276 additions and 224 deletions
|
|
@ -27,7 +27,7 @@ class CanResetCoverImageUseCase @Inject constructor(
|
|||
// Getting default cover image
|
||||
val defaultCoverImage = bookRepository.getDefaultCover(book).getOrThrow()
|
||||
if (defaultCoverImage == null) {
|
||||
return@mapCatching true
|
||||
return@mapCatching false
|
||||
}
|
||||
|
||||
// Return true if current cover is null (and default is not)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ class UpdateCoverImageUseCase @Inject constructor(
|
|||
logI("Updating cover image of [$bookId].")
|
||||
|
||||
bookRepository.getBook(bookId).mapCatching { book ->
|
||||
if (book.coverImage == coverImage) return
|
||||
|
||||
// Deleting old cover
|
||||
book.coverImage?.let { coverImageHandler.deleteCover(it) }?.onFailure {
|
||||
logW("Could not delete old cover image with error: ${it.message}")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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.book_info
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
sealed class BookInfoEffect {
|
||||
data object OnChangedCover : BookInfoEffect()
|
||||
|
||||
data object OnErrorResetCover : BookInfoEffect()
|
||||
|
||||
data object OnResetCover : BookInfoEffect()
|
||||
|
||||
data object OnDeletedCover : BookInfoEffect()
|
||||
|
||||
data object OnTitleChanged : BookInfoEffect()
|
||||
|
||||
data object OnAuthorChanged : BookInfoEffect()
|
||||
|
||||
data object OnDescriptionChanged : BookInfoEffect()
|
||||
|
||||
data object OnPathChanged : BookInfoEffect()
|
||||
|
||||
data object OnBookDeleted : BookInfoEffect()
|
||||
|
||||
data object OnBookMoved : BookInfoEffect()
|
||||
|
||||
data object OnNavigateBack : BookInfoEffect()
|
||||
|
||||
data object OnNavigateToLibrarySettings : BookInfoEffect()
|
||||
|
||||
data object OnNavigateToReader : BookInfoEffect()
|
||||
}
|
||||
|
|
@ -6,31 +6,24 @@
|
|||
|
||||
package ua.acclorite.book_story.presentation.book_info
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.runtime.Immutable
|
||||
import ua.acclorite.book_story.core.ui.UIText
|
||||
import ua.acclorite.book_story.domain.model.library.Category
|
||||
|
||||
@Immutable
|
||||
sealed class BookInfoEvent {
|
||||
|
||||
data object OnShowDetailsBottomSheet : BookInfoEvent()
|
||||
|
||||
data object OnShowChangeCoverBottomSheet : BookInfoEvent()
|
||||
|
||||
data class OnChangeCover(
|
||||
val uri: Uri,
|
||||
val context: Context
|
||||
val image: Bitmap
|
||||
) : BookInfoEvent()
|
||||
|
||||
data class OnResetCover(
|
||||
val context: Context
|
||||
) : BookInfoEvent()
|
||||
data object OnResetCover : BookInfoEvent()
|
||||
|
||||
data class OnDeleteCover(
|
||||
val context: Context
|
||||
) : BookInfoEvent()
|
||||
data object OnDeleteCover : BookInfoEvent()
|
||||
|
||||
data object OnCheckCoverReset : BookInfoEvent()
|
||||
|
||||
|
|
@ -39,44 +32,42 @@ sealed class BookInfoEvent {
|
|||
data object OnShowTitleDialog : BookInfoEvent()
|
||||
|
||||
data class OnActionTitleDialog(
|
||||
val title: String,
|
||||
val context: Context
|
||||
val title: String
|
||||
) : BookInfoEvent()
|
||||
|
||||
data object OnShowAuthorDialog : BookInfoEvent()
|
||||
|
||||
data class OnActionAuthorDialog(
|
||||
val author: UIText,
|
||||
val context: Context
|
||||
val author: UIText
|
||||
) : BookInfoEvent()
|
||||
|
||||
data object OnShowDescriptionDialog : BookInfoEvent()
|
||||
|
||||
data class OnActionDescriptionDialog(
|
||||
val description: String?,
|
||||
val context: Context
|
||||
val description: String?
|
||||
) : BookInfoEvent()
|
||||
|
||||
data object OnShowPathDialog : BookInfoEvent()
|
||||
|
||||
data class OnActionPathDialog(
|
||||
val path: String,
|
||||
val context: Context
|
||||
val path: String
|
||||
) : BookInfoEvent()
|
||||
|
||||
data object OnShowDeleteDialog : BookInfoEvent()
|
||||
|
||||
data class OnActionDeleteDialog(
|
||||
val context: Context,
|
||||
val navigateBack: () -> Unit
|
||||
) : BookInfoEvent()
|
||||
data object OnActionDeleteDialog : BookInfoEvent()
|
||||
|
||||
data object OnShowMoveDialog : BookInfoEvent()
|
||||
|
||||
data class OnActionMoveDialog(
|
||||
val selectedCategories: List<Category>,
|
||||
val context: Context
|
||||
val selectedCategories: List<Category>
|
||||
) : BookInfoEvent()
|
||||
|
||||
data object OnDismissDialog : BookInfoEvent()
|
||||
|
||||
data object OnNavigateBack : BookInfoEvent()
|
||||
|
||||
data object OnNavigateToLibrarySettings : BookInfoEvent()
|
||||
|
||||
data object OnNavigateToReader : BookInfoEvent()
|
||||
}
|
||||
|
|
@ -6,21 +6,20 @@
|
|||
|
||||
package ua.acclorite.book_story.presentation.book_info
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
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.SupervisorJob
|
||||
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.use_case.book.CanResetCoverImageUseCase
|
||||
import ua.acclorite.book_story.domain.use_case.book.DeleteBookUseCase
|
||||
import ua.acclorite.book_story.domain.use_case.book.GetBookUseCase
|
||||
|
|
@ -31,8 +30,8 @@ import ua.acclorite.book_story.domain.use_case.book.UpdateCoverImageUseCase
|
|||
import ua.acclorite.book_story.presentation.browse.BrowseScreen
|
||||
import ua.acclorite.book_story.presentation.history.HistoryScreen
|
||||
import ua.acclorite.book_story.presentation.library.LibraryScreen
|
||||
import ua.acclorite.book_story.ui.common.helpers.showToast
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
@HiltViewModel
|
||||
class BookInfoModel @Inject constructor(
|
||||
|
|
@ -50,11 +49,13 @@ class BookInfoModel @Inject constructor(
|
|||
private val _state = MutableStateFlow(BookInfoState())
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
private var eventJob = SupervisorJob()
|
||||
private var resetJob: Job? = null
|
||||
private val _effects = MutableSharedFlow<BookInfoEffect>()
|
||||
val effects = _effects.asSharedFlow()
|
||||
|
||||
private val eventStack = mutableListOf<Job>()
|
||||
|
||||
fun onEvent(event: BookInfoEvent) {
|
||||
viewModelScope.launch(eventJob + Dispatchers.Main) {
|
||||
viewModelScope.launch {
|
||||
when (event) {
|
||||
is BookInfoEvent.OnShowDetailsBottomSheet -> {
|
||||
_state.update {
|
||||
|
|
@ -73,20 +74,15 @@ class BookInfoModel @Inject constructor(
|
|||
}
|
||||
|
||||
is BookInfoEvent.OnChangeCover -> {
|
||||
launch {
|
||||
val image = event.context.contentResolver?.openInputStream(event.uri)?.use {
|
||||
BitmapFactory.decodeStream(it)
|
||||
} ?: return@launch
|
||||
|
||||
updateCoverImageUseCase(_state.value.book.id, image)
|
||||
|
||||
val newCoverImage = getBookUseCase(_state.value.book.id)?.coverImage
|
||||
?: return@launch
|
||||
withContext(Dispatchers.Default) {
|
||||
updateCoverImageUseCase(_state.value.book.id, event.image)
|
||||
val updatedCoverImage = getBookUseCase(_state.value.book.id)?.coverImage
|
||||
?: return@withContext
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
book = it.book.copy(
|
||||
coverImage = newCoverImage
|
||||
coverImage = updatedCoverImage
|
||||
),
|
||||
bottomSheet = null,
|
||||
canResetCover = canResetCoverImageUseCase(it.book.id)
|
||||
|
|
@ -96,33 +92,22 @@ class BookInfoModel @Inject constructor(
|
|||
LibraryScreen.refreshListChannel.trySend(0)
|
||||
HistoryScreen.refreshListChannel.trySend(0)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
event.context.getString(R.string.cover_image_changed)
|
||||
.showToast(context = event.context)
|
||||
}
|
||||
_effects.emit(BookInfoEffect.OnChangedCover)
|
||||
}
|
||||
}
|
||||
|
||||
is BookInfoEvent.OnResetCover -> {
|
||||
launch {
|
||||
withContext(Dispatchers.Default) {
|
||||
val result = resetCoverImageUseCase(_state.value.book.id)
|
||||
|
||||
if (!result) {
|
||||
withContext(Dispatchers.Main) {
|
||||
event.context.getString(R.string.error_could_not_reset_cover)
|
||||
.showToast(context = event.context)
|
||||
}
|
||||
return@launch
|
||||
_effects.emit(BookInfoEffect.OnErrorResetCover)
|
||||
return@withContext
|
||||
}
|
||||
|
||||
val book = getBookUseCase(_state.value.book.id)
|
||||
|
||||
if (book == null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
event.context.getString(R.string.error_something_went_wrong)
|
||||
.showToast(context = event.context)
|
||||
}
|
||||
return@launch
|
||||
_effects.emit(BookInfoEffect.OnErrorResetCover)
|
||||
return@withContext
|
||||
}
|
||||
|
||||
_state.update {
|
||||
|
|
@ -133,22 +118,15 @@ class BookInfoModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
event.context.getString(R.string.cover_reset)
|
||||
.showToast(context = event.context)
|
||||
}
|
||||
|
||||
LibraryScreen.refreshListChannel.trySend(0)
|
||||
HistoryScreen.refreshListChannel.trySend(0)
|
||||
|
||||
_effects.emit(BookInfoEffect.OnResetCover)
|
||||
}
|
||||
}
|
||||
|
||||
is BookInfoEvent.OnDeleteCover -> {
|
||||
launch {
|
||||
if (_state.value.book.coverImage == null) {
|
||||
return@launch
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
updateCoverImageUseCase(_state.value.book.id, null)
|
||||
_state.update {
|
||||
it.copy(
|
||||
|
|
@ -163,25 +141,21 @@ class BookInfoModel @Inject constructor(
|
|||
LibraryScreen.refreshListChannel.trySend(0)
|
||||
HistoryScreen.refreshListChannel.trySend(0)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
event.context.getString(R.string.cover_image_deleted)
|
||||
.showToast(context = event.context)
|
||||
}
|
||||
_effects.emit(BookInfoEffect.OnDeletedCover)
|
||||
}
|
||||
}
|
||||
|
||||
is BookInfoEvent.OnCheckCoverReset -> {
|
||||
launch(Dispatchers.IO) {
|
||||
if (_state.value.book.id == -1) return@launch
|
||||
canResetCoverImageUseCase(_state.value.book.id).apply {
|
||||
withContext(Dispatchers.Default) {
|
||||
if (_state.value.book.id == -1) return@withContext
|
||||
val canResetCover = canResetCoverImageUseCase(_state.value.book.id)
|
||||
_state.update {
|
||||
it.copy(
|
||||
canResetCover = this
|
||||
canResetCover = canResetCover
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is BookInfoEvent.OnDismissBottomSheet -> {
|
||||
_state.update {
|
||||
|
|
@ -200,7 +174,7 @@ class BookInfoModel @Inject constructor(
|
|||
}
|
||||
|
||||
is BookInfoEvent.OnActionTitleDialog -> {
|
||||
launch {
|
||||
withContext(Dispatchers.Default) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
book = it.book.copy(
|
||||
|
|
@ -213,10 +187,7 @@ class BookInfoModel @Inject constructor(
|
|||
LibraryScreen.refreshListChannel.trySend(0)
|
||||
HistoryScreen.refreshListChannel.trySend(0)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
event.context.getString(R.string.title_changed)
|
||||
.showToast(context = event.context)
|
||||
}
|
||||
_effects.emit(BookInfoEffect.OnTitleChanged)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -229,7 +200,7 @@ class BookInfoModel @Inject constructor(
|
|||
}
|
||||
|
||||
is BookInfoEvent.OnActionAuthorDialog -> {
|
||||
launch {
|
||||
withContext(Dispatchers.Default) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
book = it.book.copy(
|
||||
|
|
@ -242,10 +213,7 @@ class BookInfoModel @Inject constructor(
|
|||
LibraryScreen.refreshListChannel.trySend(0)
|
||||
HistoryScreen.refreshListChannel.trySend(0)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
event.context.getString(R.string.author_changed)
|
||||
.showToast(context = event.context)
|
||||
}
|
||||
_effects.emit(BookInfoEffect.OnAuthorChanged)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -258,7 +226,7 @@ class BookInfoModel @Inject constructor(
|
|||
}
|
||||
|
||||
is BookInfoEvent.OnActionDescriptionDialog -> {
|
||||
launch {
|
||||
withContext(Dispatchers.Default) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
book = it.book.copy(
|
||||
|
|
@ -271,10 +239,7 @@ class BookInfoModel @Inject constructor(
|
|||
LibraryScreen.refreshListChannel.trySend(0)
|
||||
HistoryScreen.refreshListChannel.trySend(0)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
event.context.getString(R.string.description_changed)
|
||||
.showToast(context = event.context)
|
||||
}
|
||||
_effects.emit(BookInfoEffect.OnDescriptionChanged)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -287,7 +252,7 @@ class BookInfoModel @Inject constructor(
|
|||
}
|
||||
|
||||
is BookInfoEvent.OnActionPathDialog -> {
|
||||
launch {
|
||||
withContext(Dispatchers.Default) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
book = it.book.copy(
|
||||
|
|
@ -300,10 +265,7 @@ class BookInfoModel @Inject constructor(
|
|||
LibraryScreen.refreshListChannel.trySend(0)
|
||||
HistoryScreen.refreshListChannel.trySend(0)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
event.context.getString(R.string.path_changed)
|
||||
.showToast(context = event.context)
|
||||
}
|
||||
_effects.emit(BookInfoEffect.OnPathChanged)
|
||||
|
||||
val file = getFileFromBookUseCase(_state.value.book.id)
|
||||
_state.update {
|
||||
|
|
@ -323,26 +285,21 @@ class BookInfoModel @Inject constructor(
|
|||
}
|
||||
|
||||
is BookInfoEvent.OnActionDeleteDialog -> {
|
||||
launch {
|
||||
withContext(Dispatchers.Default) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
dialog = null,
|
||||
bottomSheet = null
|
||||
)
|
||||
}
|
||||
|
||||
deleteBookUseCase(_state.value.book)
|
||||
|
||||
LibraryScreen.refreshListChannel.trySend(0)
|
||||
HistoryScreen.refreshListChannel.trySend(0)
|
||||
BrowseScreen.refreshListChannel.trySend(Unit)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
event.context.getString(R.string.book_deleted)
|
||||
.showToast(context = event.context)
|
||||
}
|
||||
|
||||
event.navigateBack()
|
||||
_effects.emit(BookInfoEffect.OnBookDeleted)
|
||||
_effects.emit(BookInfoEffect.OnNavigateBack)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -355,7 +312,7 @@ class BookInfoModel @Inject constructor(
|
|||
}
|
||||
|
||||
is BookInfoEvent.OnActionMoveDialog -> {
|
||||
launch {
|
||||
withContext(Dispatchers.Default) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
book = it.book.copy(
|
||||
|
|
@ -370,10 +327,7 @@ class BookInfoModel @Inject constructor(
|
|||
LibraryScreen.refreshListChannel.trySend(0)
|
||||
HistoryScreen.refreshListChannel.trySend(0)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
event.context.getString(R.string.book_moved)
|
||||
.showToast(context = event.context)
|
||||
}
|
||||
_effects.emit(BookInfoEffect.OnBookMoved)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -384,28 +338,35 @@ class BookInfoModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
is BookInfoEvent.OnNavigateBack -> {
|
||||
_effects.emit(BookInfoEffect.OnNavigateBack)
|
||||
}
|
||||
|
||||
is BookInfoEvent.OnNavigateToLibrarySettings -> {
|
||||
_effects.emit(BookInfoEffect.OnNavigateToLibrarySettings)
|
||||
}
|
||||
|
||||
is BookInfoEvent.OnNavigateToReader -> {
|
||||
_effects.emit(BookInfoEffect.OnNavigateToReader)
|
||||
}
|
||||
}
|
||||
}.also { eventStack.add(it) }
|
||||
}
|
||||
|
||||
fun init(
|
||||
bookId: Int,
|
||||
changePath: Boolean,
|
||||
navigateBack: () -> Unit
|
||||
changePath: Boolean
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val book = getBookUseCase(bookId)
|
||||
|
||||
if (book == null) {
|
||||
navigateBack()
|
||||
_effects.emit(BookInfoEffect.OnNavigateBack)
|
||||
return@launch
|
||||
}
|
||||
|
||||
eventJob.cancel()
|
||||
resetJob?.cancel()
|
||||
eventJob.join()
|
||||
resetJob?.join()
|
||||
eventJob = SupervisorJob()
|
||||
clear()
|
||||
|
||||
_state.update {
|
||||
BookInfoState(
|
||||
|
|
@ -413,9 +374,7 @@ class BookInfoModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
if (changePath) {
|
||||
onEvent(BookInfoEvent.OnShowPathDialog)
|
||||
}
|
||||
if (changePath) onEvent(BookInfoEvent.OnShowPathDialog)
|
||||
onEvent(BookInfoEvent.OnCheckCoverReset)
|
||||
|
||||
val file = getFileFromBookUseCase(bookId)
|
||||
|
|
@ -427,19 +386,26 @@ class BookInfoModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun resetScreen() {
|
||||
resetJob = viewModelScope.launch(Dispatchers.Main) {
|
||||
eventJob.cancel()
|
||||
eventJob = SupervisorJob()
|
||||
|
||||
yield()
|
||||
fun clearAsync() {
|
||||
viewModelScope.launch {
|
||||
eventStack.forEach { job ->
|
||||
job.cancel()
|
||||
}
|
||||
_state.update { BookInfoState() }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clear() {
|
||||
eventStack.forEach { job ->
|
||||
job.cancel()
|
||||
job.join()
|
||||
}
|
||||
_state.update { BookInfoState() }
|
||||
}
|
||||
|
||||
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
|
||||
mutex.withLock {
|
||||
yield()
|
||||
coroutineContext.ensureActive()
|
||||
this.value = function(this.value)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,13 +19,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.getOrElse
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import ua.acclorite.book_story.presentation.history.HistoryScreen
|
||||
import ua.acclorite.book_story.presentation.navigator.Screen
|
||||
import ua.acclorite.book_story.presentation.reader.ReaderScreen
|
||||
import ua.acclorite.book_story.presentation.settings.LibrarySettingsScreen
|
||||
import ua.acclorite.book_story.presentation.settings.SettingsModel
|
||||
import ua.acclorite.book_story.ui.book_info.BookInfoContent
|
||||
import ua.acclorite.book_story.ui.navigator.LocalNavigator
|
||||
import ua.acclorite.book_story.ui.book_info.BookInfoEffects
|
||||
|
||||
@Parcelize
|
||||
data class BookInfoScreen(val bookId: Int) : Screen, Parcelable {
|
||||
|
|
@ -46,7 +43,6 @@ data class BookInfoScreen(val bookId: Int) : Screen, Parcelable {
|
|||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val navigator = LocalNavigator.current
|
||||
val screenModel = hiltViewModel<BookInfoModel>()
|
||||
val settingsModel = hiltViewModel<SettingsModel>()
|
||||
|
||||
|
|
@ -57,19 +53,21 @@ data class BookInfoScreen(val bookId: Int) : Screen, Parcelable {
|
|||
LaunchedEffect(Unit) {
|
||||
screenModel.init(
|
||||
bookId = bookId,
|
||||
changePath = changePathChannel.tryReceive().getOrElse { false },
|
||||
navigateBack = {
|
||||
navigator.pop()
|
||||
}
|
||||
changePath = changePathChannel.tryReceive().getOrElse { false }
|
||||
)
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
screenModel.resetScreen()
|
||||
screenModel.clearAsync()
|
||||
}
|
||||
}
|
||||
|
||||
BookInfoEffects(
|
||||
effects = screenModel.effects,
|
||||
book = state.value.book
|
||||
)
|
||||
|
||||
Box(Modifier.fillMaxSize())
|
||||
|
||||
if (state.value.book.id == bookId) {
|
||||
|
|
@ -101,18 +99,9 @@ data class BookInfoScreen(val bookId: Int) : Screen, Parcelable {
|
|||
actionMoveDialog = screenModel::onEvent,
|
||||
showDeleteDialog = screenModel::onEvent,
|
||||
actionDeleteDialog = screenModel::onEvent,
|
||||
navigateToReader = {
|
||||
if (state.value.book.id != -1) {
|
||||
HistoryScreen.insertHistoryChannel.trySend(state.value.book.id)
|
||||
navigator.push(ReaderScreen(state.value.book.id))
|
||||
}
|
||||
},
|
||||
navigateToLibrarySettings = {
|
||||
navigator.push(LibrarySettingsScreen)
|
||||
},
|
||||
navigateBack = {
|
||||
navigator.pop()
|
||||
}
|
||||
navigateToReader = screenModel::onEvent,
|
||||
navigateToLibrarySettings = screenModel::onEvent,
|
||||
navigateBack = screenModel::onEvent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ fun AboutEffects(effects: SharedFlow<AboutEffect>) {
|
|||
val navigator = LocalNavigator.current
|
||||
val activity = LocalActivity.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
LaunchedEffect(effects, activity, navigator) {
|
||||
effects.collect { effect ->
|
||||
when (effect) {
|
||||
is AboutEffect.OnNavigateToBrowserPage -> {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
package ua.acclorite.book_story.ui.book_info
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.core.ui.UIText
|
||||
import ua.acclorite.book_story.domain.model.library.Book
|
||||
|
|
@ -20,7 +19,6 @@ fun BookInfoAuthorDialog(
|
|||
actionAuthorDialog: (BookInfoEvent.OnActionAuthorDialog) -> Unit,
|
||||
dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
DialogWithTextField(
|
||||
initialValue = book.author.getAsString() ?: "",
|
||||
lengthLimit = 100,
|
||||
|
|
@ -31,8 +29,7 @@ fun BookInfoAuthorDialog(
|
|||
actionAuthorDialog(
|
||||
BookInfoEvent.OnActionAuthorDialog(
|
||||
author = if (it.isBlank()) UIText.StringResource(R.string.unknown_author)
|
||||
else UIText.StringValue(it.trim().replace("\n", "")),
|
||||
context = context
|
||||
else UIText.StringValue(it.trim().replace("\n", ""))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,13 @@ package ua.acclorite.book_story.ui.book_info
|
|||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import ua.acclorite.book_story.presentation.book_info.BookInfoEvent
|
||||
|
||||
@Composable
|
||||
fun BookInfoBackHandler(
|
||||
navigateBack: () -> Unit
|
||||
navigateBack: (BookInfoEvent.OnNavigateBack) -> Unit
|
||||
) {
|
||||
BackHandler {
|
||||
navigateBack()
|
||||
navigateBack(BookInfoEvent.OnNavigateBack)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
package ua.acclorite.book_story.ui.book_info
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
|
|
@ -43,10 +44,13 @@ fun BookInfoChangeCoverBottomSheet(
|
|||
contract = ActivityResultContracts.PickVisualMedia(),
|
||||
onResult = { uri ->
|
||||
if (uri != null) {
|
||||
val image = context.contentResolver?.openInputStream(uri)?.use {
|
||||
BitmapFactory.decodeStream(it)
|
||||
} ?: return@rememberLauncherForActivityResult
|
||||
|
||||
changeCover(
|
||||
BookInfoEvent.OnChangeCover(
|
||||
uri = uri,
|
||||
context = context
|
||||
image = image
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -72,11 +76,7 @@ fun BookInfoChangeCoverBottomSheet(
|
|||
text = stringResource(id = R.string.reset_cover),
|
||||
description = stringResource(id = R.string.reset_cover_desc)
|
||||
) {
|
||||
resetCover(
|
||||
BookInfoEvent.OnResetCover(
|
||||
context = context
|
||||
)
|
||||
)
|
||||
resetCover(BookInfoEvent.OnResetCover)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -100,11 +100,7 @@ fun BookInfoChangeCoverBottomSheet(
|
|||
text = stringResource(id = R.string.delete_cover),
|
||||
description = stringResource(id = R.string.delete_cover_desc)
|
||||
) {
|
||||
deleteCover(
|
||||
BookInfoEvent.OnDeleteCover(
|
||||
context = context
|
||||
)
|
||||
)
|
||||
deleteCover(BookInfoEvent.OnDeleteCover)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,9 +44,9 @@ fun BookInfoContent(
|
|||
checkCoverReset: (BookInfoEvent.OnCheckCoverReset) -> Unit,
|
||||
dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit,
|
||||
dismissBottomSheet: (BookInfoEvent.OnDismissBottomSheet) -> Unit,
|
||||
navigateToReader: () -> Unit,
|
||||
navigateToLibrarySettings: () -> Unit,
|
||||
navigateBack: () -> Unit
|
||||
navigateToReader: (BookInfoEvent.OnNavigateToReader) -> Unit,
|
||||
navigateToLibrarySettings: (BookInfoEvent.OnNavigateToLibrarySettings) -> Unit,
|
||||
navigateBack: (BookInfoEvent.OnNavigateBack) -> Unit
|
||||
) {
|
||||
BookInfoDialog(
|
||||
dialog = dialog,
|
||||
|
|
@ -59,7 +59,6 @@ fun BookInfoContent(
|
|||
actionDeleteDialog = actionDeleteDialog,
|
||||
actionMoveDialog = actionMoveDialog,
|
||||
dismissDialog = dismissDialog,
|
||||
navigateBack = navigateBack,
|
||||
navigateToLibrarySettings = navigateToLibrarySettings
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ package ua.acclorite.book_story.ui.book_info
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.DeleteOutline
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.presentation.book_info.BookInfoEvent
|
||||
|
|
@ -18,11 +17,8 @@ import ua.acclorite.book_story.ui.common.components.dialog.Dialog
|
|||
@Composable
|
||||
fun BookInfoDeleteDialog(
|
||||
actionDeleteDialog: (BookInfoEvent.OnActionDeleteDialog) -> Unit,
|
||||
dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit,
|
||||
navigateBack: () -> Unit
|
||||
dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Dialog(
|
||||
title = stringResource(id = R.string.delete_book),
|
||||
icon = Icons.Outlined.DeleteOutline,
|
||||
|
|
@ -33,12 +29,7 @@ fun BookInfoDeleteDialog(
|
|||
withContent = false,
|
||||
actionEnabled = true,
|
||||
onAction = {
|
||||
actionDeleteDialog(
|
||||
BookInfoEvent.OnActionDeleteDialog(
|
||||
context = context,
|
||||
navigateBack = navigateBack
|
||||
)
|
||||
)
|
||||
actionDeleteDialog(BookInfoEvent.OnActionDeleteDialog)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@
|
|||
package ua.acclorite.book_story.ui.book_info
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import ua.acclorite.book_story.domain.model.library.Book
|
||||
import ua.acclorite.book_story.presentation.book_info.BookInfoEvent
|
||||
import ua.acclorite.book_story.ui.common.components.dialog.DialogWithTextField
|
||||
|
|
@ -18,7 +17,6 @@ fun BookInfoDescriptionDialog(
|
|||
actionDescriptionDialog: (BookInfoEvent.OnActionDescriptionDialog) -> Unit,
|
||||
dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
DialogWithTextField(
|
||||
initialValue = book.description ?: "",
|
||||
lengthLimit = 5000,
|
||||
|
|
@ -29,8 +27,7 @@ fun BookInfoDescriptionDialog(
|
|||
actionDescriptionDialog(
|
||||
BookInfoEvent.OnActionDescriptionDialog(
|
||||
description = if (it.isBlank()) null
|
||||
else it.trim().replace("\n", ""),
|
||||
context = context
|
||||
else it.trim().replace("\n", "")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,15 +25,13 @@ fun BookInfoDialog(
|
|||
actionDeleteDialog: (BookInfoEvent.OnActionDeleteDialog) -> Unit,
|
||||
actionMoveDialog: (BookInfoEvent.OnActionMoveDialog) -> Unit,
|
||||
dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit,
|
||||
navigateBack: () -> Unit,
|
||||
navigateToLibrarySettings: () -> Unit
|
||||
navigateToLibrarySettings: (BookInfoEvent.OnNavigateToLibrarySettings) -> Unit
|
||||
) {
|
||||
when (dialog) {
|
||||
BookInfoScreen.DELETE_DIALOG -> {
|
||||
BookInfoDeleteDialog(
|
||||
actionDeleteDialog = actionDeleteDialog,
|
||||
dismissDialog = dismissDialog,
|
||||
navigateBack = navigateBack
|
||||
dismissDialog = dismissDialog
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* 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.book_info
|
||||
|
||||
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.domain.model.library.Book
|
||||
import ua.acclorite.book_story.presentation.book_info.BookInfoEffect
|
||||
import ua.acclorite.book_story.presentation.history.HistoryScreen
|
||||
import ua.acclorite.book_story.presentation.reader.ReaderScreen
|
||||
import ua.acclorite.book_story.presentation.settings.LibrarySettingsScreen
|
||||
import ua.acclorite.book_story.ui.common.helpers.showToast
|
||||
import ua.acclorite.book_story.ui.navigator.LocalNavigator
|
||||
|
||||
@Composable
|
||||
fun BookInfoEffects(effects: SharedFlow<BookInfoEffect>, book: Book) {
|
||||
val navigator = LocalNavigator.current
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(effects, context, navigator, book) {
|
||||
effects.collect { effect ->
|
||||
when (effect) {
|
||||
is BookInfoEffect.OnChangedCover -> {
|
||||
context.getString(R.string.cover_image_changed)
|
||||
.showToast(context = context)
|
||||
}
|
||||
|
||||
is BookInfoEffect.OnErrorResetCover -> {
|
||||
context.getString(R.string.error_could_not_reset_cover)
|
||||
.showToast(context = context)
|
||||
}
|
||||
|
||||
is BookInfoEffect.OnResetCover -> {
|
||||
context.getString(R.string.cover_reset)
|
||||
.showToast(context = context)
|
||||
}
|
||||
|
||||
is BookInfoEffect.OnDeletedCover -> {
|
||||
context.getString(R.string.cover_image_deleted)
|
||||
.showToast(context = context)
|
||||
}
|
||||
|
||||
is BookInfoEffect.OnTitleChanged -> {
|
||||
context.getString(R.string.title_changed)
|
||||
.showToast(context = context)
|
||||
}
|
||||
|
||||
is BookInfoEffect.OnAuthorChanged -> {
|
||||
context.getString(R.string.author_changed)
|
||||
.showToast(context = context)
|
||||
}
|
||||
|
||||
is BookInfoEffect.OnDescriptionChanged -> {
|
||||
context.getString(R.string.description_changed)
|
||||
.showToast(context = context)
|
||||
}
|
||||
|
||||
is BookInfoEffect.OnPathChanged -> {
|
||||
context.getString(R.string.path_changed)
|
||||
.showToast(context = context)
|
||||
}
|
||||
|
||||
is BookInfoEffect.OnBookDeleted -> {
|
||||
context.getString(R.string.book_deleted)
|
||||
.showToast(context = context)
|
||||
}
|
||||
|
||||
is BookInfoEffect.OnBookMoved -> {
|
||||
context.getString(R.string.book_moved)
|
||||
.showToast(context = context)
|
||||
}
|
||||
|
||||
is BookInfoEffect.OnNavigateBack -> {
|
||||
navigator.pop()
|
||||
}
|
||||
|
||||
is BookInfoEffect.OnNavigateToLibrarySettings -> {
|
||||
navigator.push(LibrarySettingsScreen)
|
||||
}
|
||||
|
||||
is BookInfoEffect.OnNavigateToReader -> {
|
||||
if (book.id != -1) {
|
||||
HistoryScreen.insertHistoryChannel.trySend(book.id)
|
||||
navigator.push(ReaderScreen(book.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ fun BookInfoLayout(
|
|||
showDescriptionDialog: (BookInfoEvent.OnShowDescriptionDialog) -> Unit,
|
||||
showMoveDialog: (BookInfoEvent.OnShowMoveDialog) -> Unit,
|
||||
showDeleteDialog: (BookInfoEvent.OnShowDeleteDialog) -> Unit,
|
||||
navigateToReader: () -> Unit
|
||||
navigateToReader: (BookInfoEvent.OnNavigateToReader) -> Unit
|
||||
) {
|
||||
LazyColumnWithScrollbar(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
|
|
|
|||
|
|
@ -17,12 +17,13 @@ import androidx.compose.ui.unit.dp
|
|||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.core.helpers.calculateProgress
|
||||
import ua.acclorite.book_story.domain.model.library.Book
|
||||
import ua.acclorite.book_story.presentation.book_info.BookInfoEvent
|
||||
import ua.acclorite.book_story.ui.common.components.common.StyledText
|
||||
|
||||
@Composable
|
||||
fun BookInfoLayoutButton(
|
||||
book: Book,
|
||||
navigateToReader: () -> Unit
|
||||
navigateToReader: (BookInfoEvent.OnNavigateToReader) -> Unit
|
||||
) {
|
||||
Button(
|
||||
modifier = Modifier
|
||||
|
|
@ -30,9 +31,7 @@ fun BookInfoLayoutButton(
|
|||
.padding(horizontal = 18.dp),
|
||||
shape = CircleShape,
|
||||
onClick = {
|
||||
if (book.id != -1) {
|
||||
navigateToReader()
|
||||
}
|
||||
navigateToReader(BookInfoEvent.OnNavigateToReader)
|
||||
}
|
||||
) {
|
||||
StyledText(
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import androidx.compose.runtime.derivedStateOf
|
|||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ua.acclorite.book_story.R
|
||||
|
|
@ -31,9 +30,8 @@ fun BookInfoMoveDialog(
|
|||
categories: List<Category>,
|
||||
actionMoveDialog: (BookInfoEvent.OnActionMoveDialog) -> Unit,
|
||||
dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit,
|
||||
navigateToLibrarySettings: () -> Unit
|
||||
navigateToLibrarySettings: (BookInfoEvent.OnNavigateToLibrarySettings) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val selectedCategories = remember {
|
||||
mutableStateListOf<Category>().apply {
|
||||
clear()
|
||||
|
|
@ -53,14 +51,13 @@ fun BookInfoMoveDialog(
|
|||
onAction = {
|
||||
actionMoveDialog(
|
||||
BookInfoEvent.OnActionMoveDialog(
|
||||
selectedCategories = selectedCategories,
|
||||
context = context
|
||||
selectedCategories = selectedCategories
|
||||
)
|
||||
)
|
||||
},
|
||||
secondaryAction = stringResource(id = R.string.edit),
|
||||
onSecondaryAction = {
|
||||
navigateToLibrarySettings()
|
||||
navigateToLibrarySettings(BookInfoEvent.OnNavigateToLibrarySettings)
|
||||
dismissDialog(BookInfoEvent.OnDismissDialog)
|
||||
},
|
||||
withContent = true,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
package ua.acclorite.book_story.ui.book_info
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import ua.acclorite.book_story.domain.model.library.Book
|
||||
import ua.acclorite.book_story.presentation.book_info.BookInfoEvent
|
||||
import ua.acclorite.book_story.ui.common.components.dialog.DialogWithTextField
|
||||
|
|
@ -18,7 +17,6 @@ fun BookInfoPathDialog(
|
|||
actionPathDialog: (BookInfoEvent.OnActionPathDialog) -> Unit,
|
||||
dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
DialogWithTextField(
|
||||
initialValue = book.filePath.trim(),
|
||||
lengthLimit = 10000,
|
||||
|
|
@ -29,8 +27,7 @@ fun BookInfoPathDialog(
|
|||
if (it.isBlank()) return@DialogWithTextField
|
||||
actionPathDialog(
|
||||
BookInfoEvent.OnActionPathDialog(
|
||||
path = it.trim().replace("\n", ""),
|
||||
context = context
|
||||
path = it.trim().replace("\n", "")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ fun BookInfoScaffold(
|
|||
showDescriptionDialog: (BookInfoEvent.OnShowDescriptionDialog) -> Unit,
|
||||
showMoveDialog: (BookInfoEvent.OnShowMoveDialog) -> Unit,
|
||||
showDeleteDialog: (BookInfoEvent.OnShowDeleteDialog) -> Unit,
|
||||
navigateToReader: () -> Unit,
|
||||
navigateBack: () -> Unit
|
||||
navigateToReader: (BookInfoEvent.OnNavigateToReader) -> Unit,
|
||||
navigateBack: (BookInfoEvent.OnNavigateBack) -> Unit
|
||||
) {
|
||||
Scaffold(
|
||||
Modifier
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
package ua.acclorite.book_story.ui.book_info
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import ua.acclorite.book_story.domain.model.library.Book
|
||||
import ua.acclorite.book_story.presentation.book_info.BookInfoEvent
|
||||
import ua.acclorite.book_story.ui.common.components.dialog.DialogWithTextField
|
||||
|
|
@ -18,7 +17,6 @@ fun BookInfoTitleDialog(
|
|||
actionTitleDialog: (BookInfoEvent.OnActionTitleDialog) -> Unit,
|
||||
dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
DialogWithTextField(
|
||||
initialValue = book.title,
|
||||
lengthLimit = 100,
|
||||
|
|
@ -29,8 +27,7 @@ fun BookInfoTitleDialog(
|
|||
if (it.isBlank()) return@DialogWithTextField
|
||||
actionTitleDialog(
|
||||
BookInfoEvent.OnActionTitleDialog(
|
||||
title = it.trim().replace("\n", ""),
|
||||
context = context
|
||||
title = it.trim().replace("\n", "")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ fun BookInfoTopBar(
|
|||
book: Book,
|
||||
listState: LazyListState,
|
||||
showDetailsBottomSheet: (BookInfoEvent.OnShowDetailsBottomSheet) -> Unit,
|
||||
navigateBack: () -> Unit
|
||||
navigateBack: (BookInfoEvent.OnNavigateBack) -> Unit
|
||||
) {
|
||||
val firstVisibleItemIndex = remember {
|
||||
derivedStateOf {
|
||||
|
|
@ -49,7 +49,7 @@ fun BookInfoTopBar(
|
|||
contentID = 0,
|
||||
contentNavigationIcon = {
|
||||
NavigatorBackIconButton {
|
||||
navigateBack()
|
||||
navigateBack(BookInfoEvent.OnNavigateBack)
|
||||
}
|
||||
},
|
||||
contentTitle = {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ fun HistoryEffects(
|
|||
val navigator = LocalNavigator.current
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
LaunchedEffect(effects, context, navigator, focusRequester, snackbarState) {
|
||||
effects.collect { effect ->
|
||||
when (effect) {
|
||||
is HistoryEffect.OnRequestFocus -> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue