diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/CanResetCoverImageUseCase.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/CanResetCoverImageUseCase.kt index d2c2ac80..6853984d 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/CanResetCoverImageUseCase.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/CanResetCoverImageUseCase.kt @@ -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) diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/UpdateCoverImageUseCase.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/UpdateCoverImageUseCase.kt index 9edfcfea..f7ac1d8d 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/UpdateCoverImageUseCase.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/UpdateCoverImageUseCase.kt @@ -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}") diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoEffect.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoEffect.kt new file mode 100644 index 00000000..02098815 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoEffect.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoEvent.kt index 8ae17e60..ccd632b9 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoEvent.kt @@ -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, - val context: Context + val selectedCategories: List ) : BookInfoEvent() data object OnDismissDialog : BookInfoEvent() + + data object OnNavigateBack : BookInfoEvent() + + data object OnNavigateToLibrarySettings : BookInfoEvent() + + data object OnNavigateToReader : BookInfoEvent() } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoModel.kt index 297572fa..afd79cba 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoModel.kt @@ -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() + val effects = _effects.asSharedFlow() + + private val eventStack = mutableListOf() 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,22 +141,18 @@ 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 { - _state.update { - it.copy( - canResetCover = this - ) - } + withContext(Dispatchers.Default) { + if (_state.value.book.id == -1) return@withContext + val canResetCover = canResetCoverImageUseCase(_state.value.book.id) + _state.update { + it.copy( + canResetCover = canResetCover + ) } } } @@ -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 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/presentation/book_info/BookInfoScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoScreen.kt index 9db57325..f5218597 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoScreen.kt @@ -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() val settingsModel = hiltViewModel() @@ -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 ) } } diff --git a/app/src/main/java/ua/acclorite/book_story/ui/about/AboutEffects.kt b/app/src/main/java/ua/acclorite/book_story/ui/about/AboutEffects.kt index bda965c2..0bbc89c0 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/about/AboutEffects.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/about/AboutEffects.kt @@ -25,7 +25,7 @@ fun AboutEffects(effects: SharedFlow) { val navigator = LocalNavigator.current val activity = LocalActivity.current - LaunchedEffect(Unit) { + LaunchedEffect(effects, activity, navigator) { effects.collect { effect -> when (effect) { is AboutEffect.OnNavigateToBrowserPage -> { diff --git a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoAuthorDialog.kt b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoAuthorDialog.kt index 8b3d8e3b..38f36b14 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoAuthorDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoAuthorDialog.kt @@ -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", "")) ) ) } diff --git a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoBackHandler.kt b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoBackHandler.kt index 2f335e81..915f913b 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoBackHandler.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoBackHandler.kt @@ -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) } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoChangeCoverBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoChangeCoverBottomSheet.kt index 2b94d85d..0a704197 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoChangeCoverBottomSheet.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoChangeCoverBottomSheet.kt @@ -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) } } } diff --git a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoContent.kt b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoContent.kt index b61e1154..915a662f 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoContent.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoContent.kt @@ -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 ) diff --git a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoDeleteDialog.kt b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoDeleteDialog.kt index fb34d878..3cc859ef 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoDeleteDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoDeleteDialog.kt @@ -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) } ) } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoDescriptionDialog.kt b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoDescriptionDialog.kt index c800edfe..290b271d 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoDescriptionDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoDescriptionDialog.kt @@ -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", "") ) ) } diff --git a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoDialog.kt b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoDialog.kt index 641faf2c..98e8b8b1 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoDialog.kt @@ -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 ) } diff --git a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoEffects.kt b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoEffects.kt new file mode 100644 index 00000000..e6d2e967 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoEffects.kt @@ -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, 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)) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoLayout.kt b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoLayout.kt index 551cd5a3..66d60d1f 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoLayout.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoLayout.kt @@ -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(), diff --git a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoLayoutButton.kt b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoLayoutButton.kt index 534a4910..27609bb7 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoLayoutButton.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoLayoutButton.kt @@ -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( diff --git a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoMoveDialog.kt b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoMoveDialog.kt index 2540169f..ecb8f37a 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoMoveDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoMoveDialog.kt @@ -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, actionMoveDialog: (BookInfoEvent.OnActionMoveDialog) -> Unit, dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit, - navigateToLibrarySettings: () -> Unit + navigateToLibrarySettings: (BookInfoEvent.OnNavigateToLibrarySettings) -> Unit ) { - val context = LocalContext.current val selectedCategories = remember { mutableStateListOf().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, diff --git a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoPathDialog.kt b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoPathDialog.kt index a5c7521e..d94fe6ad 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoPathDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoPathDialog.kt @@ -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", "") ) ) } diff --git a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoScaffold.kt b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoScaffold.kt index c323d266..5171a965 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoScaffold.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoScaffold.kt @@ -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 diff --git a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoTitleDialog.kt b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoTitleDialog.kt index 8c1e20fc..5c3aae20 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoTitleDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoTitleDialog.kt @@ -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", "") ) ) } diff --git a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoTopBar.kt b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoTopBar.kt index 84054847..99f37ec2 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoTopBar.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoTopBar.kt @@ -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 = { diff --git a/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryEffects.kt b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryEffects.kt index 5966bd3f..fd3dbe31 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryEffects.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryEffects.kt @@ -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 -> {