refactor: bookInfo viewmodel

This commit is contained in:
Acclorite 2025-08-10 22:21:52 +03:00
parent 28cbf1188d
commit 6f53c70b5b
No known key found for this signature in database
GPG key ID: 6E54C611F6EE8593
23 changed files with 276 additions and 224 deletions

View file

@ -27,7 +27,7 @@ class CanResetCoverImageUseCase @Inject constructor(
// Getting default cover image // Getting default cover image
val defaultCoverImage = bookRepository.getDefaultCover(book).getOrThrow() val defaultCoverImage = bookRepository.getDefaultCover(book).getOrThrow()
if (defaultCoverImage == null) { if (defaultCoverImage == null) {
return@mapCatching true return@mapCatching false
} }
// Return true if current cover is null (and default is not) // Return true if current cover is null (and default is not)

View file

@ -24,6 +24,8 @@ class UpdateCoverImageUseCase @Inject constructor(
logI("Updating cover image of [$bookId].") logI("Updating cover image of [$bookId].")
bookRepository.getBook(bookId).mapCatching { book -> bookRepository.getBook(bookId).mapCatching { book ->
if (book.coverImage == coverImage) return
// Deleting old cover // Deleting old cover
book.coverImage?.let { coverImageHandler.deleteCover(it) }?.onFailure { book.coverImage?.let { coverImageHandler.deleteCover(it) }?.onFailure {
logW("Could not delete old cover image with error: ${it.message}") logW("Could not delete old cover image with error: ${it.message}")

View file

@ -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()
}

View file

@ -6,31 +6,24 @@
package ua.acclorite.book_story.presentation.book_info package ua.acclorite.book_story.presentation.book_info
import android.content.Context import android.graphics.Bitmap
import android.net.Uri
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.core.ui.UIText import ua.acclorite.book_story.core.ui.UIText
import ua.acclorite.book_story.domain.model.library.Category import ua.acclorite.book_story.domain.model.library.Category
@Immutable @Immutable
sealed class BookInfoEvent { sealed class BookInfoEvent {
data object OnShowDetailsBottomSheet : BookInfoEvent() data object OnShowDetailsBottomSheet : BookInfoEvent()
data object OnShowChangeCoverBottomSheet : BookInfoEvent() data object OnShowChangeCoverBottomSheet : BookInfoEvent()
data class OnChangeCover( data class OnChangeCover(
val uri: Uri, val image: Bitmap
val context: Context
) : BookInfoEvent() ) : BookInfoEvent()
data class OnResetCover( data object OnResetCover : BookInfoEvent()
val context: Context
) : BookInfoEvent()
data class OnDeleteCover( data object OnDeleteCover : BookInfoEvent()
val context: Context
) : BookInfoEvent()
data object OnCheckCoverReset : BookInfoEvent() data object OnCheckCoverReset : BookInfoEvent()
@ -39,44 +32,42 @@ sealed class BookInfoEvent {
data object OnShowTitleDialog : BookInfoEvent() data object OnShowTitleDialog : BookInfoEvent()
data class OnActionTitleDialog( data class OnActionTitleDialog(
val title: String, val title: String
val context: Context
) : BookInfoEvent() ) : BookInfoEvent()
data object OnShowAuthorDialog : BookInfoEvent() data object OnShowAuthorDialog : BookInfoEvent()
data class OnActionAuthorDialog( data class OnActionAuthorDialog(
val author: UIText, val author: UIText
val context: Context
) : BookInfoEvent() ) : BookInfoEvent()
data object OnShowDescriptionDialog : BookInfoEvent() data object OnShowDescriptionDialog : BookInfoEvent()
data class OnActionDescriptionDialog( data class OnActionDescriptionDialog(
val description: String?, val description: String?
val context: Context
) : BookInfoEvent() ) : BookInfoEvent()
data object OnShowPathDialog : BookInfoEvent() data object OnShowPathDialog : BookInfoEvent()
data class OnActionPathDialog( data class OnActionPathDialog(
val path: String, val path: String
val context: Context
) : BookInfoEvent() ) : BookInfoEvent()
data object OnShowDeleteDialog : BookInfoEvent() data object OnShowDeleteDialog : BookInfoEvent()
data class OnActionDeleteDialog( data object OnActionDeleteDialog : BookInfoEvent()
val context: Context,
val navigateBack: () -> Unit
) : BookInfoEvent()
data object OnShowMoveDialog : BookInfoEvent() data object OnShowMoveDialog : BookInfoEvent()
data class OnActionMoveDialog( data class OnActionMoveDialog(
val selectedCategories: List<Category>, val selectedCategories: List<Category>
val context: Context
) : BookInfoEvent() ) : BookInfoEvent()
data object OnDismissDialog : BookInfoEvent() data object OnDismissDialog : BookInfoEvent()
data object OnNavigateBack : BookInfoEvent()
data object OnNavigateToLibrarySettings : BookInfoEvent()
data object OnNavigateToReader : BookInfoEvent()
} }

View file

@ -6,21 +6,20 @@
package ua.acclorite.book_story.presentation.book_info package ua.acclorite.book_story.presentation.book_info
import android.graphics.BitmapFactory
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job 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.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext 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.CanResetCoverImageUseCase
import ua.acclorite.book_story.domain.use_case.book.DeleteBookUseCase import ua.acclorite.book_story.domain.use_case.book.DeleteBookUseCase
import ua.acclorite.book_story.domain.use_case.book.GetBookUseCase 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.browse.BrowseScreen
import ua.acclorite.book_story.presentation.history.HistoryScreen import ua.acclorite.book_story.presentation.history.HistoryScreen
import ua.acclorite.book_story.presentation.library.LibraryScreen import ua.acclorite.book_story.presentation.library.LibraryScreen
import ua.acclorite.book_story.ui.common.helpers.showToast
import javax.inject.Inject import javax.inject.Inject
import kotlin.coroutines.coroutineContext
@HiltViewModel @HiltViewModel
class BookInfoModel @Inject constructor( class BookInfoModel @Inject constructor(
@ -50,11 +49,13 @@ class BookInfoModel @Inject constructor(
private val _state = MutableStateFlow(BookInfoState()) private val _state = MutableStateFlow(BookInfoState())
val state = _state.asStateFlow() val state = _state.asStateFlow()
private var eventJob = SupervisorJob() private val _effects = MutableSharedFlow<BookInfoEffect>()
private var resetJob: Job? = null val effects = _effects.asSharedFlow()
private val eventStack = mutableListOf<Job>()
fun onEvent(event: BookInfoEvent) { fun onEvent(event: BookInfoEvent) {
viewModelScope.launch(eventJob + Dispatchers.Main) { viewModelScope.launch {
when (event) { when (event) {
is BookInfoEvent.OnShowDetailsBottomSheet -> { is BookInfoEvent.OnShowDetailsBottomSheet -> {
_state.update { _state.update {
@ -73,20 +74,15 @@ class BookInfoModel @Inject constructor(
} }
is BookInfoEvent.OnChangeCover -> { is BookInfoEvent.OnChangeCover -> {
launch { withContext(Dispatchers.Default) {
val image = event.context.contentResolver?.openInputStream(event.uri)?.use { updateCoverImageUseCase(_state.value.book.id, event.image)
BitmapFactory.decodeStream(it) val updatedCoverImage = getBookUseCase(_state.value.book.id)?.coverImage
} ?: return@launch ?: return@withContext
updateCoverImageUseCase(_state.value.book.id, image)
val newCoverImage = getBookUseCase(_state.value.book.id)?.coverImage
?: return@launch
_state.update { _state.update {
it.copy( it.copy(
book = it.book.copy( book = it.book.copy(
coverImage = newCoverImage coverImage = updatedCoverImage
), ),
bottomSheet = null, bottomSheet = null,
canResetCover = canResetCoverImageUseCase(it.book.id) canResetCover = canResetCoverImageUseCase(it.book.id)
@ -96,33 +92,22 @@ class BookInfoModel @Inject constructor(
LibraryScreen.refreshListChannel.trySend(0) LibraryScreen.refreshListChannel.trySend(0)
HistoryScreen.refreshListChannel.trySend(0) HistoryScreen.refreshListChannel.trySend(0)
withContext(Dispatchers.Main) { _effects.emit(BookInfoEffect.OnChangedCover)
event.context.getString(R.string.cover_image_changed)
.showToast(context = event.context)
}
} }
} }
is BookInfoEvent.OnResetCover -> { is BookInfoEvent.OnResetCover -> {
launch { withContext(Dispatchers.Default) {
val result = resetCoverImageUseCase(_state.value.book.id) val result = resetCoverImageUseCase(_state.value.book.id)
if (!result) { if (!result) {
withContext(Dispatchers.Main) { _effects.emit(BookInfoEffect.OnErrorResetCover)
event.context.getString(R.string.error_could_not_reset_cover) return@withContext
.showToast(context = event.context)
}
return@launch
} }
val book = getBookUseCase(_state.value.book.id) val book = getBookUseCase(_state.value.book.id)
if (book == null) { if (book == null) {
withContext(Dispatchers.Main) { _effects.emit(BookInfoEffect.OnErrorResetCover)
event.context.getString(R.string.error_something_went_wrong) return@withContext
.showToast(context = event.context)
}
return@launch
} }
_state.update { _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) LibraryScreen.refreshListChannel.trySend(0)
HistoryScreen.refreshListChannel.trySend(0) HistoryScreen.refreshListChannel.trySend(0)
_effects.emit(BookInfoEffect.OnResetCover)
} }
} }
is BookInfoEvent.OnDeleteCover -> { is BookInfoEvent.OnDeleteCover -> {
launch { withContext(Dispatchers.Default) {
if (_state.value.book.coverImage == null) {
return@launch
}
updateCoverImageUseCase(_state.value.book.id, null) updateCoverImageUseCase(_state.value.book.id, null)
_state.update { _state.update {
it.copy( it.copy(
@ -163,22 +141,18 @@ class BookInfoModel @Inject constructor(
LibraryScreen.refreshListChannel.trySend(0) LibraryScreen.refreshListChannel.trySend(0)
HistoryScreen.refreshListChannel.trySend(0) HistoryScreen.refreshListChannel.trySend(0)
withContext(Dispatchers.Main) { _effects.emit(BookInfoEffect.OnDeletedCover)
event.context.getString(R.string.cover_image_deleted)
.showToast(context = event.context)
}
} }
} }
is BookInfoEvent.OnCheckCoverReset -> { is BookInfoEvent.OnCheckCoverReset -> {
launch(Dispatchers.IO) { withContext(Dispatchers.Default) {
if (_state.value.book.id == -1) return@launch if (_state.value.book.id == -1) return@withContext
canResetCoverImageUseCase(_state.value.book.id).apply { val canResetCover = canResetCoverImageUseCase(_state.value.book.id)
_state.update { _state.update {
it.copy( it.copy(
canResetCover = this canResetCover = canResetCover
) )
}
} }
} }
} }
@ -200,7 +174,7 @@ class BookInfoModel @Inject constructor(
} }
is BookInfoEvent.OnActionTitleDialog -> { is BookInfoEvent.OnActionTitleDialog -> {
launch { withContext(Dispatchers.Default) {
_state.update { _state.update {
it.copy( it.copy(
book = it.book.copy( book = it.book.copy(
@ -213,10 +187,7 @@ class BookInfoModel @Inject constructor(
LibraryScreen.refreshListChannel.trySend(0) LibraryScreen.refreshListChannel.trySend(0)
HistoryScreen.refreshListChannel.trySend(0) HistoryScreen.refreshListChannel.trySend(0)
withContext(Dispatchers.Main) { _effects.emit(BookInfoEffect.OnTitleChanged)
event.context.getString(R.string.title_changed)
.showToast(context = event.context)
}
} }
} }
@ -229,7 +200,7 @@ class BookInfoModel @Inject constructor(
} }
is BookInfoEvent.OnActionAuthorDialog -> { is BookInfoEvent.OnActionAuthorDialog -> {
launch { withContext(Dispatchers.Default) {
_state.update { _state.update {
it.copy( it.copy(
book = it.book.copy( book = it.book.copy(
@ -242,10 +213,7 @@ class BookInfoModel @Inject constructor(
LibraryScreen.refreshListChannel.trySend(0) LibraryScreen.refreshListChannel.trySend(0)
HistoryScreen.refreshListChannel.trySend(0) HistoryScreen.refreshListChannel.trySend(0)
withContext(Dispatchers.Main) { _effects.emit(BookInfoEffect.OnAuthorChanged)
event.context.getString(R.string.author_changed)
.showToast(context = event.context)
}
} }
} }
@ -258,7 +226,7 @@ class BookInfoModel @Inject constructor(
} }
is BookInfoEvent.OnActionDescriptionDialog -> { is BookInfoEvent.OnActionDescriptionDialog -> {
launch { withContext(Dispatchers.Default) {
_state.update { _state.update {
it.copy( it.copy(
book = it.book.copy( book = it.book.copy(
@ -271,10 +239,7 @@ class BookInfoModel @Inject constructor(
LibraryScreen.refreshListChannel.trySend(0) LibraryScreen.refreshListChannel.trySend(0)
HistoryScreen.refreshListChannel.trySend(0) HistoryScreen.refreshListChannel.trySend(0)
withContext(Dispatchers.Main) { _effects.emit(BookInfoEffect.OnDescriptionChanged)
event.context.getString(R.string.description_changed)
.showToast(context = event.context)
}
} }
} }
@ -287,7 +252,7 @@ class BookInfoModel @Inject constructor(
} }
is BookInfoEvent.OnActionPathDialog -> { is BookInfoEvent.OnActionPathDialog -> {
launch { withContext(Dispatchers.Default) {
_state.update { _state.update {
it.copy( it.copy(
book = it.book.copy( book = it.book.copy(
@ -300,10 +265,7 @@ class BookInfoModel @Inject constructor(
LibraryScreen.refreshListChannel.trySend(0) LibraryScreen.refreshListChannel.trySend(0)
HistoryScreen.refreshListChannel.trySend(0) HistoryScreen.refreshListChannel.trySend(0)
withContext(Dispatchers.Main) { _effects.emit(BookInfoEffect.OnPathChanged)
event.context.getString(R.string.path_changed)
.showToast(context = event.context)
}
val file = getFileFromBookUseCase(_state.value.book.id) val file = getFileFromBookUseCase(_state.value.book.id)
_state.update { _state.update {
@ -323,26 +285,21 @@ class BookInfoModel @Inject constructor(
} }
is BookInfoEvent.OnActionDeleteDialog -> { is BookInfoEvent.OnActionDeleteDialog -> {
launch { withContext(Dispatchers.Default) {
_state.update { _state.update {
it.copy( it.copy(
dialog = null, dialog = null,
bottomSheet = null bottomSheet = null
) )
} }
deleteBookUseCase(_state.value.book) deleteBookUseCase(_state.value.book)
LibraryScreen.refreshListChannel.trySend(0) LibraryScreen.refreshListChannel.trySend(0)
HistoryScreen.refreshListChannel.trySend(0) HistoryScreen.refreshListChannel.trySend(0)
BrowseScreen.refreshListChannel.trySend(Unit) BrowseScreen.refreshListChannel.trySend(Unit)
withContext(Dispatchers.Main) { _effects.emit(BookInfoEffect.OnBookDeleted)
event.context.getString(R.string.book_deleted) _effects.emit(BookInfoEffect.OnNavigateBack)
.showToast(context = event.context)
}
event.navigateBack()
} }
} }
@ -355,7 +312,7 @@ class BookInfoModel @Inject constructor(
} }
is BookInfoEvent.OnActionMoveDialog -> { is BookInfoEvent.OnActionMoveDialog -> {
launch { withContext(Dispatchers.Default) {
_state.update { _state.update {
it.copy( it.copy(
book = it.book.copy( book = it.book.copy(
@ -370,10 +327,7 @@ class BookInfoModel @Inject constructor(
LibraryScreen.refreshListChannel.trySend(0) LibraryScreen.refreshListChannel.trySend(0)
HistoryScreen.refreshListChannel.trySend(0) HistoryScreen.refreshListChannel.trySend(0)
withContext(Dispatchers.Main) { _effects.emit(BookInfoEffect.OnBookMoved)
event.context.getString(R.string.book_moved)
.showToast(context = event.context)
}
} }
} }
@ -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( fun init(
bookId: Int, bookId: Int,
changePath: Boolean, changePath: Boolean
navigateBack: () -> Unit
) { ) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.Default) {
val book = getBookUseCase(bookId) val book = getBookUseCase(bookId)
if (book == null) { if (book == null) {
navigateBack() _effects.emit(BookInfoEffect.OnNavigateBack)
return@launch return@launch
} }
eventJob.cancel() clear()
resetJob?.cancel()
eventJob.join()
resetJob?.join()
eventJob = SupervisorJob()
_state.update { _state.update {
BookInfoState( BookInfoState(
@ -413,9 +374,7 @@ class BookInfoModel @Inject constructor(
) )
} }
if (changePath) { if (changePath) onEvent(BookInfoEvent.OnShowPathDialog)
onEvent(BookInfoEvent.OnShowPathDialog)
}
onEvent(BookInfoEvent.OnCheckCoverReset) onEvent(BookInfoEvent.OnCheckCoverReset)
val file = getFileFromBookUseCase(bookId) val file = getFileFromBookUseCase(bookId)
@ -427,19 +386,26 @@ class BookInfoModel @Inject constructor(
} }
} }
fun resetScreen() { fun clearAsync() {
resetJob = viewModelScope.launch(Dispatchers.Main) { viewModelScope.launch {
eventJob.cancel() eventStack.forEach { job ->
eventJob = SupervisorJob() job.cancel()
}
yield()
_state.update { BookInfoState() } _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) { private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
mutex.withLock { mutex.withLock {
yield() coroutineContext.ensureActive()
this.value = function(this.value) this.value = function(this.value)
} }
} }

View file

@ -19,13 +19,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.getOrElse import kotlinx.coroutines.channels.getOrElse
import kotlinx.parcelize.Parcelize 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.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.presentation.settings.SettingsModel
import ua.acclorite.book_story.ui.book_info.BookInfoContent 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 @Parcelize
data class BookInfoScreen(val bookId: Int) : Screen, Parcelable { data class BookInfoScreen(val bookId: Int) : Screen, Parcelable {
@ -46,7 +43,6 @@ data class BookInfoScreen(val bookId: Int) : Screen, Parcelable {
@Composable @Composable
override fun Content() { override fun Content() {
val navigator = LocalNavigator.current
val screenModel = hiltViewModel<BookInfoModel>() val screenModel = hiltViewModel<BookInfoModel>()
val settingsModel = hiltViewModel<SettingsModel>() val settingsModel = hiltViewModel<SettingsModel>()
@ -57,19 +53,21 @@ data class BookInfoScreen(val bookId: Int) : Screen, Parcelable {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
screenModel.init( screenModel.init(
bookId = bookId, bookId = bookId,
changePath = changePathChannel.tryReceive().getOrElse { false }, changePath = changePathChannel.tryReceive().getOrElse { false }
navigateBack = {
navigator.pop()
}
) )
} }
DisposableEffect(Unit) { DisposableEffect(Unit) {
onDispose { onDispose {
screenModel.resetScreen() screenModel.clearAsync()
} }
} }
BookInfoEffects(
effects = screenModel.effects,
book = state.value.book
)
Box(Modifier.fillMaxSize()) Box(Modifier.fillMaxSize())
if (state.value.book.id == bookId) { if (state.value.book.id == bookId) {
@ -101,18 +99,9 @@ data class BookInfoScreen(val bookId: Int) : Screen, Parcelable {
actionMoveDialog = screenModel::onEvent, actionMoveDialog = screenModel::onEvent,
showDeleteDialog = screenModel::onEvent, showDeleteDialog = screenModel::onEvent,
actionDeleteDialog = screenModel::onEvent, actionDeleteDialog = screenModel::onEvent,
navigateToReader = { navigateToReader = screenModel::onEvent,
if (state.value.book.id != -1) { navigateToLibrarySettings = screenModel::onEvent,
HistoryScreen.insertHistoryChannel.trySend(state.value.book.id) navigateBack = screenModel::onEvent
navigator.push(ReaderScreen(state.value.book.id))
}
},
navigateToLibrarySettings = {
navigator.push(LibrarySettingsScreen)
},
navigateBack = {
navigator.pop()
}
) )
} }
} }

View file

@ -25,7 +25,7 @@ fun AboutEffects(effects: SharedFlow<AboutEffect>) {
val navigator = LocalNavigator.current val navigator = LocalNavigator.current
val activity = LocalActivity.current val activity = LocalActivity.current
LaunchedEffect(Unit) { LaunchedEffect(effects, activity, navigator) {
effects.collect { effect -> effects.collect { effect ->
when (effect) { when (effect) {
is AboutEffect.OnNavigateToBrowserPage -> { is AboutEffect.OnNavigateToBrowserPage -> {

View file

@ -7,7 +7,6 @@
package ua.acclorite.book_story.ui.book_info package ua.acclorite.book_story.ui.book_info
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.core.ui.UIText import ua.acclorite.book_story.core.ui.UIText
import ua.acclorite.book_story.domain.model.library.Book import ua.acclorite.book_story.domain.model.library.Book
@ -20,7 +19,6 @@ fun BookInfoAuthorDialog(
actionAuthorDialog: (BookInfoEvent.OnActionAuthorDialog) -> Unit, actionAuthorDialog: (BookInfoEvent.OnActionAuthorDialog) -> Unit,
dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit
) { ) {
val context = LocalContext.current
DialogWithTextField( DialogWithTextField(
initialValue = book.author.getAsString() ?: "", initialValue = book.author.getAsString() ?: "",
lengthLimit = 100, lengthLimit = 100,
@ -31,8 +29,7 @@ fun BookInfoAuthorDialog(
actionAuthorDialog( actionAuthorDialog(
BookInfoEvent.OnActionAuthorDialog( BookInfoEvent.OnActionAuthorDialog(
author = if (it.isBlank()) UIText.StringResource(R.string.unknown_author) author = if (it.isBlank()) UIText.StringResource(R.string.unknown_author)
else UIText.StringValue(it.trim().replace("\n", "")), else UIText.StringValue(it.trim().replace("\n", ""))
context = context
) )
) )
} }

View file

@ -8,12 +8,13 @@ package ua.acclorite.book_story.ui.book_info
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import ua.acclorite.book_story.presentation.book_info.BookInfoEvent
@Composable @Composable
fun BookInfoBackHandler( fun BookInfoBackHandler(
navigateBack: () -> Unit navigateBack: (BookInfoEvent.OnNavigateBack) -> Unit
) { ) {
BackHandler { BackHandler {
navigateBack() navigateBack(BookInfoEvent.OnNavigateBack)
} }
} }

View file

@ -6,6 +6,7 @@
package ua.acclorite.book_story.ui.book_info package ua.acclorite.book_story.ui.book_info
import android.graphics.BitmapFactory
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@ -43,10 +44,13 @@ fun BookInfoChangeCoverBottomSheet(
contract = ActivityResultContracts.PickVisualMedia(), contract = ActivityResultContracts.PickVisualMedia(),
onResult = { uri -> onResult = { uri ->
if (uri != null) { if (uri != null) {
val image = context.contentResolver?.openInputStream(uri)?.use {
BitmapFactory.decodeStream(it)
} ?: return@rememberLauncherForActivityResult
changeCover( changeCover(
BookInfoEvent.OnChangeCover( BookInfoEvent.OnChangeCover(
uri = uri, image = image
context = context
) )
) )
} }
@ -72,11 +76,7 @@ fun BookInfoChangeCoverBottomSheet(
text = stringResource(id = R.string.reset_cover), text = stringResource(id = R.string.reset_cover),
description = stringResource(id = R.string.reset_cover_desc) description = stringResource(id = R.string.reset_cover_desc)
) { ) {
resetCover( resetCover(BookInfoEvent.OnResetCover)
BookInfoEvent.OnResetCover(
context = context
)
)
} }
} }
} }
@ -100,11 +100,7 @@ fun BookInfoChangeCoverBottomSheet(
text = stringResource(id = R.string.delete_cover), text = stringResource(id = R.string.delete_cover),
description = stringResource(id = R.string.delete_cover_desc) description = stringResource(id = R.string.delete_cover_desc)
) { ) {
deleteCover( deleteCover(BookInfoEvent.OnDeleteCover)
BookInfoEvent.OnDeleteCover(
context = context
)
)
} }
} }
} }

View file

@ -44,9 +44,9 @@ fun BookInfoContent(
checkCoverReset: (BookInfoEvent.OnCheckCoverReset) -> Unit, checkCoverReset: (BookInfoEvent.OnCheckCoverReset) -> Unit,
dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit, dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit,
dismissBottomSheet: (BookInfoEvent.OnDismissBottomSheet) -> Unit, dismissBottomSheet: (BookInfoEvent.OnDismissBottomSheet) -> Unit,
navigateToReader: () -> Unit, navigateToReader: (BookInfoEvent.OnNavigateToReader) -> Unit,
navigateToLibrarySettings: () -> Unit, navigateToLibrarySettings: (BookInfoEvent.OnNavigateToLibrarySettings) -> Unit,
navigateBack: () -> Unit navigateBack: (BookInfoEvent.OnNavigateBack) -> Unit
) { ) {
BookInfoDialog( BookInfoDialog(
dialog = dialog, dialog = dialog,
@ -59,7 +59,6 @@ fun BookInfoContent(
actionDeleteDialog = actionDeleteDialog, actionDeleteDialog = actionDeleteDialog,
actionMoveDialog = actionMoveDialog, actionMoveDialog = actionMoveDialog,
dismissDialog = dismissDialog, dismissDialog = dismissDialog,
navigateBack = navigateBack,
navigateToLibrarySettings = navigateToLibrarySettings navigateToLibrarySettings = navigateToLibrarySettings
) )

View file

@ -9,7 +9,6 @@ package ua.acclorite.book_story.ui.book_info
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.DeleteOutline import androidx.compose.material.icons.outlined.DeleteOutline
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.book_info.BookInfoEvent import ua.acclorite.book_story.presentation.book_info.BookInfoEvent
@ -18,11 +17,8 @@ import ua.acclorite.book_story.ui.common.components.dialog.Dialog
@Composable @Composable
fun BookInfoDeleteDialog( fun BookInfoDeleteDialog(
actionDeleteDialog: (BookInfoEvent.OnActionDeleteDialog) -> Unit, actionDeleteDialog: (BookInfoEvent.OnActionDeleteDialog) -> Unit,
dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit, dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit
navigateBack: () -> Unit
) { ) {
val context = LocalContext.current
Dialog( Dialog(
title = stringResource(id = R.string.delete_book), title = stringResource(id = R.string.delete_book),
icon = Icons.Outlined.DeleteOutline, icon = Icons.Outlined.DeleteOutline,
@ -33,12 +29,7 @@ fun BookInfoDeleteDialog(
withContent = false, withContent = false,
actionEnabled = true, actionEnabled = true,
onAction = { onAction = {
actionDeleteDialog( actionDeleteDialog(BookInfoEvent.OnActionDeleteDialog)
BookInfoEvent.OnActionDeleteDialog(
context = context,
navigateBack = navigateBack
)
)
} }
) )
} }

View file

@ -7,7 +7,6 @@
package ua.acclorite.book_story.ui.book_info package ua.acclorite.book_story.ui.book_info
import androidx.compose.runtime.Composable 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.domain.model.library.Book
import ua.acclorite.book_story.presentation.book_info.BookInfoEvent import ua.acclorite.book_story.presentation.book_info.BookInfoEvent
import ua.acclorite.book_story.ui.common.components.dialog.DialogWithTextField import ua.acclorite.book_story.ui.common.components.dialog.DialogWithTextField
@ -18,7 +17,6 @@ fun BookInfoDescriptionDialog(
actionDescriptionDialog: (BookInfoEvent.OnActionDescriptionDialog) -> Unit, actionDescriptionDialog: (BookInfoEvent.OnActionDescriptionDialog) -> Unit,
dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit
) { ) {
val context = LocalContext.current
DialogWithTextField( DialogWithTextField(
initialValue = book.description ?: "", initialValue = book.description ?: "",
lengthLimit = 5000, lengthLimit = 5000,
@ -29,8 +27,7 @@ fun BookInfoDescriptionDialog(
actionDescriptionDialog( actionDescriptionDialog(
BookInfoEvent.OnActionDescriptionDialog( BookInfoEvent.OnActionDescriptionDialog(
description = if (it.isBlank()) null description = if (it.isBlank()) null
else it.trim().replace("\n", ""), else it.trim().replace("\n", "")
context = context
) )
) )
} }

View file

@ -25,15 +25,13 @@ fun BookInfoDialog(
actionDeleteDialog: (BookInfoEvent.OnActionDeleteDialog) -> Unit, actionDeleteDialog: (BookInfoEvent.OnActionDeleteDialog) -> Unit,
actionMoveDialog: (BookInfoEvent.OnActionMoveDialog) -> Unit, actionMoveDialog: (BookInfoEvent.OnActionMoveDialog) -> Unit,
dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit, dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit,
navigateBack: () -> Unit, navigateToLibrarySettings: (BookInfoEvent.OnNavigateToLibrarySettings) -> Unit
navigateToLibrarySettings: () -> Unit
) { ) {
when (dialog) { when (dialog) {
BookInfoScreen.DELETE_DIALOG -> { BookInfoScreen.DELETE_DIALOG -> {
BookInfoDeleteDialog( BookInfoDeleteDialog(
actionDeleteDialog = actionDeleteDialog, actionDeleteDialog = actionDeleteDialog,
dismissDialog = dismissDialog, dismissDialog = dismissDialog
navigateBack = navigateBack
) )
} }

View file

@ -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))
}
}
}
}
}
}

View file

@ -33,7 +33,7 @@ fun BookInfoLayout(
showDescriptionDialog: (BookInfoEvent.OnShowDescriptionDialog) -> Unit, showDescriptionDialog: (BookInfoEvent.OnShowDescriptionDialog) -> Unit,
showMoveDialog: (BookInfoEvent.OnShowMoveDialog) -> Unit, showMoveDialog: (BookInfoEvent.OnShowMoveDialog) -> Unit,
showDeleteDialog: (BookInfoEvent.OnShowDeleteDialog) -> Unit, showDeleteDialog: (BookInfoEvent.OnShowDeleteDialog) -> Unit,
navigateToReader: () -> Unit navigateToReader: (BookInfoEvent.OnNavigateToReader) -> Unit
) { ) {
LazyColumnWithScrollbar( LazyColumnWithScrollbar(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),

View file

@ -17,12 +17,13 @@ import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.core.helpers.calculateProgress import ua.acclorite.book_story.core.helpers.calculateProgress
import ua.acclorite.book_story.domain.model.library.Book 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 import ua.acclorite.book_story.ui.common.components.common.StyledText
@Composable @Composable
fun BookInfoLayoutButton( fun BookInfoLayoutButton(
book: Book, book: Book,
navigateToReader: () -> Unit navigateToReader: (BookInfoEvent.OnNavigateToReader) -> Unit
) { ) {
Button( Button(
modifier = Modifier modifier = Modifier
@ -30,9 +31,7 @@ fun BookInfoLayoutButton(
.padding(horizontal = 18.dp), .padding(horizontal = 18.dp),
shape = CircleShape, shape = CircleShape,
onClick = { onClick = {
if (book.id != -1) { navigateToReader(BookInfoEvent.OnNavigateToReader)
navigateToReader()
}
} }
) { ) {
StyledText( StyledText(

View file

@ -14,7 +14,6 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
@ -31,9 +30,8 @@ fun BookInfoMoveDialog(
categories: List<Category>, categories: List<Category>,
actionMoveDialog: (BookInfoEvent.OnActionMoveDialog) -> Unit, actionMoveDialog: (BookInfoEvent.OnActionMoveDialog) -> Unit,
dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit, dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit,
navigateToLibrarySettings: () -> Unit navigateToLibrarySettings: (BookInfoEvent.OnNavigateToLibrarySettings) -> Unit
) { ) {
val context = LocalContext.current
val selectedCategories = remember { val selectedCategories = remember {
mutableStateListOf<Category>().apply { mutableStateListOf<Category>().apply {
clear() clear()
@ -53,14 +51,13 @@ fun BookInfoMoveDialog(
onAction = { onAction = {
actionMoveDialog( actionMoveDialog(
BookInfoEvent.OnActionMoveDialog( BookInfoEvent.OnActionMoveDialog(
selectedCategories = selectedCategories, selectedCategories = selectedCategories
context = context
) )
) )
}, },
secondaryAction = stringResource(id = R.string.edit), secondaryAction = stringResource(id = R.string.edit),
onSecondaryAction = { onSecondaryAction = {
navigateToLibrarySettings() navigateToLibrarySettings(BookInfoEvent.OnNavigateToLibrarySettings)
dismissDialog(BookInfoEvent.OnDismissDialog) dismissDialog(BookInfoEvent.OnDismissDialog)
}, },
withContent = true, withContent = true,

View file

@ -7,7 +7,6 @@
package ua.acclorite.book_story.ui.book_info package ua.acclorite.book_story.ui.book_info
import androidx.compose.runtime.Composable 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.domain.model.library.Book
import ua.acclorite.book_story.presentation.book_info.BookInfoEvent import ua.acclorite.book_story.presentation.book_info.BookInfoEvent
import ua.acclorite.book_story.ui.common.components.dialog.DialogWithTextField import ua.acclorite.book_story.ui.common.components.dialog.DialogWithTextField
@ -18,7 +17,6 @@ fun BookInfoPathDialog(
actionPathDialog: (BookInfoEvent.OnActionPathDialog) -> Unit, actionPathDialog: (BookInfoEvent.OnActionPathDialog) -> Unit,
dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit
) { ) {
val context = LocalContext.current
DialogWithTextField( DialogWithTextField(
initialValue = book.filePath.trim(), initialValue = book.filePath.trim(),
lengthLimit = 10000, lengthLimit = 10000,
@ -29,8 +27,7 @@ fun BookInfoPathDialog(
if (it.isBlank()) return@DialogWithTextField if (it.isBlank()) return@DialogWithTextField
actionPathDialog( actionPathDialog(
BookInfoEvent.OnActionPathDialog( BookInfoEvent.OnActionPathDialog(
path = it.trim().replace("\n", ""), path = it.trim().replace("\n", "")
context = context
) )
) )
} }

View file

@ -30,8 +30,8 @@ fun BookInfoScaffold(
showDescriptionDialog: (BookInfoEvent.OnShowDescriptionDialog) -> Unit, showDescriptionDialog: (BookInfoEvent.OnShowDescriptionDialog) -> Unit,
showMoveDialog: (BookInfoEvent.OnShowMoveDialog) -> Unit, showMoveDialog: (BookInfoEvent.OnShowMoveDialog) -> Unit,
showDeleteDialog: (BookInfoEvent.OnShowDeleteDialog) -> Unit, showDeleteDialog: (BookInfoEvent.OnShowDeleteDialog) -> Unit,
navigateToReader: () -> Unit, navigateToReader: (BookInfoEvent.OnNavigateToReader) -> Unit,
navigateBack: () -> Unit navigateBack: (BookInfoEvent.OnNavigateBack) -> Unit
) { ) {
Scaffold( Scaffold(
Modifier Modifier

View file

@ -7,7 +7,6 @@
package ua.acclorite.book_story.ui.book_info package ua.acclorite.book_story.ui.book_info
import androidx.compose.runtime.Composable 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.domain.model.library.Book
import ua.acclorite.book_story.presentation.book_info.BookInfoEvent import ua.acclorite.book_story.presentation.book_info.BookInfoEvent
import ua.acclorite.book_story.ui.common.components.dialog.DialogWithTextField import ua.acclorite.book_story.ui.common.components.dialog.DialogWithTextField
@ -18,7 +17,6 @@ fun BookInfoTitleDialog(
actionTitleDialog: (BookInfoEvent.OnActionTitleDialog) -> Unit, actionTitleDialog: (BookInfoEvent.OnActionTitleDialog) -> Unit,
dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit
) { ) {
val context = LocalContext.current
DialogWithTextField( DialogWithTextField(
initialValue = book.title, initialValue = book.title,
lengthLimit = 100, lengthLimit = 100,
@ -29,8 +27,7 @@ fun BookInfoTitleDialog(
if (it.isBlank()) return@DialogWithTextField if (it.isBlank()) return@DialogWithTextField
actionTitleDialog( actionTitleDialog(
BookInfoEvent.OnActionTitleDialog( BookInfoEvent.OnActionTitleDialog(
title = it.trim().replace("\n", ""), title = it.trim().replace("\n", "")
context = context
) )
) )
} }

View file

@ -30,7 +30,7 @@ fun BookInfoTopBar(
book: Book, book: Book,
listState: LazyListState, listState: LazyListState,
showDetailsBottomSheet: (BookInfoEvent.OnShowDetailsBottomSheet) -> Unit, showDetailsBottomSheet: (BookInfoEvent.OnShowDetailsBottomSheet) -> Unit,
navigateBack: () -> Unit navigateBack: (BookInfoEvent.OnNavigateBack) -> Unit
) { ) {
val firstVisibleItemIndex = remember { val firstVisibleItemIndex = remember {
derivedStateOf { derivedStateOf {
@ -49,7 +49,7 @@ fun BookInfoTopBar(
contentID = 0, contentID = 0,
contentNavigationIcon = { contentNavigationIcon = {
NavigatorBackIconButton { NavigatorBackIconButton {
navigateBack() navigateBack(BookInfoEvent.OnNavigateBack)
} }
}, },
contentTitle = { contentTitle = {

View file

@ -33,7 +33,7 @@ fun HistoryEffects(
val navigator = LocalNavigator.current val navigator = LocalNavigator.current
val context = LocalContext.current val context = LocalContext.current
LaunchedEffect(Unit) { LaunchedEffect(effects, context, navigator, focusRequester, snackbarState) {
effects.collect { effect -> effects.collect { effect ->
when (effect) { when (effect) {
is HistoryEffect.OnRequestFocus -> { is HistoryEffect.OnRequestFocus -> {