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

View file

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

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

View file

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

View file

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

View file

@ -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 -> {

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

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,
showMoveDialog: (BookInfoEvent.OnShowMoveDialog) -> Unit,
showDeleteDialog: (BookInfoEvent.OnShowDeleteDialog) -> Unit,
navigateToReader: () -> Unit
navigateToReader: (BookInfoEvent.OnNavigateToReader) -> Unit
) {
LazyColumnWithScrollbar(
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.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(

View file

@ -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,

View file

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

View file

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

View file

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

View file

@ -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 = {

View file

@ -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 -> {