From 28cbf1188ddb52609411d2cf783e452c45b42c2a Mon Sep 17 00:00:00 2001 From: Acclorite <2ma0bhpk@anonaddy.com> Date: Sat, 9 Aug 2025 10:11:47 +0300 Subject: [PATCH] refactor: history viewmodel --- .../use_case/history/GetHistoryUseCase.kt | 51 ++- .../presentation/about/AboutScreen.kt | 4 +- .../presentation/history/HistoryEffect.kt | 31 ++ .../presentation/history/HistoryEvent.kt | 29 +- .../presentation/history/HistoryModel.kt | 312 +++++++----------- .../presentation/history/HistoryScreen.kt | 26 +- .../ui/history/HistoryBackHandler.kt | 4 +- .../book_story/ui/history/HistoryContent.kt | 6 +- .../HistoryDeleteWholeHistoryDialog.kt | 8 +- .../book_story/ui/history/HistoryEffects.kt | 78 +++++ .../book_story/ui/history/HistoryLayout.kt | 25 +- .../book_story/ui/history/HistoryScaffold.kt | 5 +- .../book_story/ui/history/HistoryTopBar.kt | 2 +- 13 files changed, 320 insertions(+), 261 deletions(-) create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryEffect.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/ui/history/HistoryEffects.kt diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/GetHistoryUseCase.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/GetHistoryUseCase.kt index 52a8f344..d33c81c4 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/GetHistoryUseCase.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/GetHistoryUseCase.kt @@ -9,23 +9,66 @@ package ua.acclorite.book_story.domain.use_case.history import ua.acclorite.book_story.core.log.logE import ua.acclorite.book_story.core.log.logI import ua.acclorite.book_story.domain.model.history.History +import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.domain.repository.HistoryRepository +import ua.acclorite.book_story.presentation.history.model.GroupedHistory +import java.time.Instant +import java.time.LocalDate +import java.time.ZoneId +import java.time.format.DateTimeFormatter import javax.inject.Inject class GetHistoryUseCase @Inject constructor( + private val bookRepository: BookRepository, private val historyRepository: HistoryRepository ) { - suspend operator fun invoke(): List { + suspend operator fun invoke(query: String): List { logI("Getting all history.") - return historyRepository.getHistory().fold( + fun getDayLabel(timeMillis: Long): String { + val historyDate = Instant.ofEpochMilli(timeMillis) + .atZone(ZoneId.systemDefault()).toLocalDate() + val today = LocalDate.now() + val yesterday = today.minusDays(1) + + return when (historyDate) { + today -> "today" + yesterday -> "yesterday" + else -> historyDate.format(DateTimeFormatter.ofPattern("dd.MM.yy")) + } + } + + fun filterMaxElementsById(elements: List): List { + val groupedById = elements.groupBy { it.bookId } + val maxElementsById = groupedById.map { (_, values) -> + values.maxByOrNull { it.time } + } + return maxElementsById.filterNotNull() + } + + return runCatching { + historyRepository.getHistory().getOrThrow().sortedByDescending { it.time } + .mapNotNull { history -> + val book = bookRepository.getBook(history.bookId).getOrNull() + if ( + book == null + || !book.title.lowercase().trim().contains(query.lowercase().trim()) + ) return@mapNotNull null + + history.copy(book = book) + } + .groupBy { history -> + getDayLabel(history.time) + } + .map { (day, history) -> GroupedHistory(day, filterMaxElementsById(history)) } + }.fold( onSuccess = { - logI("Successfully got ${it.size} history entries.") + logI("Successfully got ${it.size} grouped history entries.") it }, onFailure = { - logE("Could not get history entries with error: ${it.message}") + logE("Could not get grouped history entries with error: ${it.message}") emptyList() } ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutScreen.kt index e3d4a1c1..b8e8a827 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutScreen.kt @@ -27,7 +27,9 @@ object AboutScreen : Screen, Parcelable { val (scrollBehavior, listState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior() - AboutEffects(screenModel.effects) + AboutEffects( + effects = screenModel.effects + ) AboutContent( scrollBehavior = scrollBehavior, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryEffect.kt b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryEffect.kt new file mode 100644 index 00000000..5c08db29 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryEffect.kt @@ -0,0 +1,31 @@ +/* + * 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.history + +import androidx.compose.runtime.Immutable +import ua.acclorite.book_story.domain.model.history.History + +@Immutable +sealed class HistoryEffect { + data object OnRequestFocus : HistoryEffect() + + data class OnShowSnackbar( + val history: History + ) : HistoryEffect() + + data object OnWholeHistoryDeleted : HistoryEffect() + + data object OnNavigateToLibrary : HistoryEffect() + + data class OnNavigateToBookInfo( + val bookId: Int + ) : HistoryEffect() + + data class OnNavigateToReader( + val bookId: Int + ) : HistoryEffect() +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryEvent.kt index 22d76397..194ab86b 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryEvent.kt @@ -6,10 +6,7 @@ package ua.acclorite.book_story.presentation.history -import android.content.Context -import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Immutable -import androidx.compose.ui.focus.FocusRequester import ua.acclorite.book_story.domain.model.history.History @Immutable @@ -23,9 +20,7 @@ sealed class HistoryEvent { val show: Boolean ) : HistoryEvent() - data class OnRequestFocus( - val focusRequester: FocusRequester - ) : HistoryEvent() + data object OnRequestFocus : HistoryEvent() data class OnSearchQueryChange( val query: String @@ -34,16 +29,26 @@ sealed class HistoryEvent { data object OnSearch : HistoryEvent() data class OnDeleteHistoryEntry( - val history: History, - val snackbarState: SnackbarHostState, - val context: Context + val history: History + ) : HistoryEvent() + + data class OnRestoreHistoryEntry( + val history: History ) : HistoryEvent() data object OnShowDeleteWholeHistoryDialog : HistoryEvent() - data class OnActionDeleteWholeHistoryDialog( - val context: Context - ) : HistoryEvent() + data object OnActionDeleteWholeHistoryDialog : HistoryEvent() data object OnDismissDialog : HistoryEvent() + + data object OnNavigateToLibrary : HistoryEvent() + + data class OnNavigateToBookInfo( + val bookId: Int + ) : HistoryEvent() + + data class OnNavigateToReader( + val bookId: Int + ) : HistoryEvent() } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryModel.kt index 31c1e502..dd6e73e3 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryModel.kt @@ -6,14 +6,16 @@ package ua.acclorite.book_story.presentation.history -import androidx.compose.material3.SnackbarResult 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.delay +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.flow.collectLatest import kotlinx.coroutines.flow.receiveAsFlow @@ -21,26 +23,18 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import kotlinx.coroutines.yield -import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.history.History -import ua.acclorite.book_story.domain.use_case.book.GetBookUseCase import ua.acclorite.book_story.domain.use_case.history.AddHistoryUseCase import ua.acclorite.book_story.domain.use_case.history.DeleteHistoryUseCase import ua.acclorite.book_story.domain.use_case.history.DeleteWholeHistoryUseCase import ua.acclorite.book_story.domain.use_case.history.GetHistoryUseCase -import ua.acclorite.book_story.presentation.history.model.GroupedHistory import ua.acclorite.book_story.presentation.library.LibraryScreen -import ua.acclorite.book_story.ui.common.helpers.showToast -import java.text.SimpleDateFormat -import java.util.Calendar import java.util.Date -import java.util.Locale import javax.inject.Inject +import kotlin.coroutines.coroutineContext @HiltViewModel class HistoryModel @Inject constructor( - private val getBookUseCase: GetBookUseCase, private val getHistoryUseCase: GetHistoryUseCase, private val addHistoryUseCase: AddHistoryUseCase, private val deleteHistoryUseCase: DeleteHistoryUseCase, @@ -52,21 +46,21 @@ class HistoryModel @Inject constructor( private val _state = MutableStateFlow(HistoryState()) val state = _state.asStateFlow() + private val _effects = MutableSharedFlow() + val effects = _effects.asSharedFlow() + init { - viewModelScope.launch(Dispatchers.IO) { - onEvent( - HistoryEvent.OnRefreshList( - loading = true, - hideSearch = true - ) + onEvent( + HistoryEvent.OnRefreshList( + loading = true, + hideSearch = true ) - } + ) /* Observe channel - - - - - - - - - - - */ - viewModelScope.launch(Dispatchers.IO) { - HistoryScreen.refreshListChannel.receiveAsFlow().collectLatest { - delay(it) - yield() + viewModelScope.launch { + HistoryScreen.refreshListChannel.receiveAsFlow().collectLatest { delay -> + delay(delay) onEvent( HistoryEvent.OnRefreshList( @@ -76,18 +70,17 @@ class HistoryModel @Inject constructor( ) } } - viewModelScope.launch(Dispatchers.IO) { - HistoryScreen.insertHistoryChannel.receiveAsFlow().collectLatest { + viewModelScope.launch { + HistoryScreen.insertHistoryChannel.receiveAsFlow().collectLatest { bookId -> addHistoryUseCase( History( - bookId = it, + bookId = bookId, book = null, time = Date().time ) ) delay(500) - yield() onEvent( HistoryEvent.OnRefreshList( @@ -103,36 +96,43 @@ class HistoryModel @Inject constructor( private var refreshJob: Job? = null private var searchQueryChange: Job? = null - private var deleteHistoryEntry: Job? = null fun onEvent(event: HistoryEvent) { - when (event) { - is HistoryEvent.OnRefreshList -> { - refreshJob?.cancel() - refreshJob = viewModelScope.launch(Dispatchers.IO) { - _state.update { - it.copy( - isRefreshing = true, - isLoading = event.loading, - showSearch = if (event.hideSearch) false else it.showSearch - ) - } + viewModelScope.launch { + when (event) { + is HistoryEvent.OnRefreshList -> { + refreshJob?.cancel() + refreshJob = viewModelScope.launch(Dispatchers.Default) { + _state.update { + it.copy( + isRefreshing = true, + isLoading = event.loading, + showSearch = if (event.hideSearch) false else it.showSearch + ) + } - yield() - getHistoryFromDatabase() - - delay(500) - _state.update { - it.copy( - isRefreshing = false, - isLoading = false + ensureActive() + val history = getHistoryUseCase( + if (_state.value.showSearch) _state.value.searchQuery + else "" ) + _state.update { + it.copy( + history = history, + isLoading = false + ) + } + + delay(500) // Delay for UI smoothness + _state.update { + it.copy( + isRefreshing = false + ) + } } } - } - is HistoryEvent.OnSearchVisibility -> { - viewModelScope.launch(Dispatchers.IO) { + is HistoryEvent.OnSearchVisibility -> { if (!event.show) { onEvent( HistoryEvent.OnRefreshList( @@ -140,27 +140,20 @@ class HistoryModel @Inject constructor( hideSearch = true ) ) - } else { - _state.update { - it.copy( - searchQuery = "", - hasFocused = false - ) - } } _state.update { it.copy( - showSearch = event.show + showSearch = event.show, + searchQuery = if (event.show) "" else it.searchQuery, + hasFocused = if (event.show) false else it.hasFocused ) } } - } - is HistoryEvent.OnRequestFocus -> { - viewModelScope.launch(Dispatchers.Main) { + is HistoryEvent.OnRequestFocus -> { if (!_state.value.hasFocused) { - event.focusRequester.requestFocus() + _effects.emit(HistoryEffect.OnRequestFocus) _state.update { it.copy( hasFocused = true @@ -168,26 +161,22 @@ class HistoryModel @Inject constructor( } } } - } - is HistoryEvent.OnSearchQueryChange -> { - viewModelScope.launch { + is HistoryEvent.OnSearchQueryChange -> { _state.update { it.copy( searchQuery = event.query ) } + searchQueryChange?.cancel() - searchQueryChange = launch(Dispatchers.IO) { + searchQueryChange = viewModelScope.launch(Dispatchers.IO) { delay(500) - yield() onEvent(HistoryEvent.OnSearch) } } - } - is HistoryEvent.OnSearch -> { - viewModelScope.launch(Dispatchers.IO) { + is HistoryEvent.OnSearch -> { onEvent( HistoryEvent.OnRefreshList( loading = false, @@ -195,173 +184,94 @@ class HistoryModel @Inject constructor( ) ) } - } - is HistoryEvent.OnDeleteHistoryEntry -> { - viewModelScope.launch(Dispatchers.IO) { - deleteHistoryUseCase(event.history) + is HistoryEvent.OnDeleteHistoryEntry -> { + withContext(Dispatchers.Default) { + deleteHistoryUseCase(event.history) - onEvent( - HistoryEvent.OnRefreshList( - loading = false, - hideSearch = false - ) - ) - LibraryScreen.refreshListChannel.trySend(0) - - deleteHistoryEntry?.cancel() - event.snackbarState.currentSnackbarData?.dismiss() - - deleteHistoryEntry = launch(Dispatchers.IO) { - repeat(10) { - yield() - delay(1000) - } - - yield() - event.snackbarState.currentSnackbarData?.dismiss() - } - val snackbarResult = event.snackbarState.showSnackbar( - event.context.getString(R.string.history_element_deleted), - event.context.getString(R.string.undo) - ) - - when (snackbarResult) { - SnackbarResult.Dismissed -> Unit - SnackbarResult.ActionPerformed -> { - addHistoryUseCase(event.history) - LibraryScreen.refreshListChannel.trySend(0) - - onEvent( - HistoryEvent.OnRefreshList( - loading = false, - hideSearch = false - ) + onEvent( + HistoryEvent.OnRefreshList( + loading = false, + hideSearch = false ) - } + ) + LibraryScreen.refreshListChannel.trySend(0) + + _effects.emit(HistoryEffect.OnShowSnackbar(event.history)) } } - } - is HistoryEvent.OnShowDeleteWholeHistoryDialog -> { - viewModelScope.launch { + is HistoryEvent.OnRestoreHistoryEntry -> { + withContext(Dispatchers.Default) { + addHistoryUseCase(event.history) + + onEvent( + HistoryEvent.OnRefreshList( + loading = false, + hideSearch = false + ) + ) + LibraryScreen.refreshListChannel.trySend(0) + } + } + + is HistoryEvent.OnShowDeleteWholeHistoryDialog -> { _state.update { it.copy( dialog = HistoryScreen.DELETE_WHOLE_HISTORY_DIALOG ) } } - } - is HistoryEvent.OnActionDeleteWholeHistoryDialog -> { - viewModelScope.launch { - _state.update { - it.copy( - dialog = null, - isLoading = true + is HistoryEvent.OnActionDeleteWholeHistoryDialog -> { + withContext(Dispatchers.Default) { + _state.update { + it.copy( + dialog = null, + isLoading = true + ) + } + + deleteWholeHistoryUseCase() + + onEvent( + HistoryEvent.OnRefreshList( + loading = true, + hideSearch = true + ) ) - } + LibraryScreen.refreshListChannel.trySend(0) - deleteWholeHistoryUseCase() - LibraryScreen.refreshListChannel.trySend(0) - onEvent( - HistoryEvent.OnRefreshList( - loading = true, - hideSearch = true - ) - ) - - withContext(Dispatchers.Main) { - event.context - .getString(R.string.history_deleted) - .showToast(context = event.context) + _effects.emit(HistoryEffect.OnWholeHistoryDeleted) } } - } - is HistoryEvent.OnDismissDialog -> { - viewModelScope.launch { + is HistoryEvent.OnDismissDialog -> { _state.update { it.copy( dialog = null ) } } - } - } - } - private suspend fun getHistoryFromDatabase( - query: String = if (_state.value.showSearch) _state.value.searchQuery else "" - ) { - fun isSameDay(historyTime: Calendar, nowTime: Calendar): Boolean { - return historyTime.get(Calendar.YEAR) == nowTime.get(Calendar.YEAR) && - historyTime.get(Calendar.DAY_OF_YEAR) == nowTime.get(Calendar.DAY_OF_YEAR) - } - - fun filterMaxElementsById(elements: List): List { - val groupedById = elements.groupBy { it.bookId } - val maxElementsById = groupedById.map { (_, values) -> - values.maxByOrNull { it.time } - } - return maxElementsById.filterNotNull() - } - - val history = getHistoryUseCase().sortedByDescending { - it.time - }.run { - val books = map { it.bookId }.distinct().mapNotNull { - getBookUseCase(it) - }.toMutableList() - - mapNotNull { - val book = books.find { book -> book.id == it.bookId } ?: return@mapNotNull null - if (!book.title.lowercase().trim().contains(query.lowercase().trim())) { - return@mapNotNull null + is HistoryEvent.OnNavigateToLibrary -> { + _effects.emit(HistoryEffect.OnNavigateToLibrary) } - it.copy(book = book) - } - }.ifEmpty { - _state.update { - it.copy( - history = emptyList(), - isLoading = false - ) - } - return - }.groupBy { item -> - val historyTime = Calendar.getInstance().apply { - timeInMillis = item.time - } - val nowTime = Calendar.getInstance() + is HistoryEvent.OnNavigateToBookInfo -> { + _effects.emit(HistoryEffect.OnNavigateToBookInfo(event.bookId)) + } - return@groupBy when { - isSameDay(historyTime, nowTime) -> "today" - - isSameDay( - historyTime, - nowTime.apply { add(Calendar.DAY_OF_YEAR, -1) } - ) -> "yesterday" - - else -> SimpleDateFormat( - "dd.MM.yy", - Locale.getDefault() - ).format(item.time) + is HistoryEvent.OnNavigateToReader -> { + _effects.emit(HistoryEffect.OnNavigateToReader(event.bookId)) + } } - }.map { (day, history) -> GroupedHistory(day, filterMaxElementsById(history)) } - - _state.update { - it.copy( - history = history, - isLoading = false, - ) } } 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/history/HistoryScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryScreen.kt index 1f84db1a..4ff36b2e 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryScreen.kt @@ -22,12 +22,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.channels.Channel import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize -import ua.acclorite.book_story.presentation.book_info.BookInfoScreen -import ua.acclorite.book_story.presentation.library.LibraryScreen import ua.acclorite.book_story.presentation.navigator.Screen -import ua.acclorite.book_story.presentation.reader.ReaderScreen import ua.acclorite.book_story.ui.history.HistoryContent -import ua.acclorite.book_story.ui.navigator.LocalNavigator +import ua.acclorite.book_story.ui.history.HistoryEffects @Parcelize object HistoryScreen : Screen, Parcelable { @@ -49,7 +46,6 @@ object HistoryScreen : Screen, Parcelable { @Composable override fun Content() { - val navigator = LocalNavigator.current val screenModel = hiltViewModel() val state = screenModel.state.collectAsStateWithLifecycle() @@ -79,6 +75,13 @@ object HistoryScreen : Screen, Parcelable { } ) + HistoryEffects( + effects = screenModel.effects, + focusRequester = focusRequester, + snackbarState = snackbarState, + restoreHistoryEntry = screenModel::onEvent + ) + HistoryContent( refreshState = refreshState, snackbarState = snackbarState, @@ -100,16 +103,9 @@ object HistoryScreen : Screen, Parcelable { showDeleteWholeHistoryDialog = screenModel::onEvent, actionDeleteWholeHistoryDialog = screenModel::onEvent, dismissDialog = screenModel::onEvent, - navigateToLibrary = { - navigator.push(LibraryScreen, saveInBackStack = false) - }, - navigateToBookInfo = { - navigator.push(BookInfoScreen(bookId = it)) - }, - navigateToReader = { - insertHistoryChannel.trySend(it) - navigator.push(ReaderScreen(it)) - } + navigateToLibrary = screenModel::onEvent, + navigateToBookInfo = screenModel::onEvent, + navigateToReader = screenModel::onEvent ) } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryBackHandler.kt b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryBackHandler.kt index 7d3cbac5..b1e5e8e7 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryBackHandler.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryBackHandler.kt @@ -14,7 +14,7 @@ import ua.acclorite.book_story.presentation.history.HistoryEvent fun HistoryBackHandler( showSearch: Boolean, searchVisibility: (HistoryEvent.OnSearchVisibility) -> Unit, - navigateToLibrary: () -> Unit + navigateToLibrary: (HistoryEvent.OnNavigateToLibrary) -> Unit ) { BackHandler { if (showSearch) { @@ -22,6 +22,6 @@ fun HistoryBackHandler( return@BackHandler } - navigateToLibrary() + navigateToLibrary(HistoryEvent.OnNavigateToLibrary) } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryContent.kt b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryContent.kt index b22092d4..1e791fc1 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryContent.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryContent.kt @@ -39,9 +39,9 @@ fun HistoryContent( showDeleteWholeHistoryDialog: (HistoryEvent.OnShowDeleteWholeHistoryDialog) -> Unit, actionDeleteWholeHistoryDialog: (HistoryEvent.OnActionDeleteWholeHistoryDialog) -> Unit, dismissDialog: (HistoryEvent.OnDismissDialog) -> Unit, - navigateToLibrary: () -> Unit, - navigateToBookInfo: (Int) -> Unit, - navigateToReader: (Int) -> Unit + navigateToLibrary: (HistoryEvent.OnNavigateToLibrary) -> Unit, + navigateToBookInfo: (HistoryEvent.OnNavigateToBookInfo) -> Unit, + navigateToReader: (HistoryEvent.OnNavigateToReader) -> Unit ) { HistoryDialog( dialog = dialog, diff --git a/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryDeleteWholeHistoryDialog.kt b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryDeleteWholeHistoryDialog.kt index 2d6528ca..6b2c12e8 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryDeleteWholeHistoryDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryDeleteWholeHistoryDialog.kt @@ -9,7 +9,6 @@ package ua.acclorite.book_story.ui.history 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.history.HistoryEvent @@ -20,7 +19,6 @@ fun HistoryDeleteWholeHistoryDialog( actionDeleteWholeHistoryDialog: (HistoryEvent.OnActionDeleteWholeHistoryDialog) -> Unit, dismissDialog: (HistoryEvent.OnDismissDialog) -> Unit ) { - val context = LocalContext.current Dialog( title = stringResource(id = R.string.delete_history), icon = Icons.Outlined.DeleteOutline, @@ -30,11 +28,7 @@ fun HistoryDeleteWholeHistoryDialog( dismissDialog(HistoryEvent.OnDismissDialog) }, onAction = { - actionDeleteWholeHistoryDialog( - HistoryEvent.OnActionDeleteWholeHistoryDialog( - context = context - ) - ) + actionDeleteWholeHistoryDialog(HistoryEvent.OnActionDeleteWholeHistoryDialog) }, withContent = false ) 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 new file mode 100644 index 00000000..5966bd3f --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryEffects.kt @@ -0,0 +1,78 @@ +/* + * 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.history + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.platform.LocalContext +import kotlinx.coroutines.flow.SharedFlow +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.book_info.BookInfoScreen +import ua.acclorite.book_story.presentation.history.HistoryEffect +import ua.acclorite.book_story.presentation.history.HistoryEvent +import ua.acclorite.book_story.presentation.history.HistoryScreen.insertHistoryChannel +import ua.acclorite.book_story.presentation.library.LibraryScreen +import ua.acclorite.book_story.presentation.reader.ReaderScreen +import ua.acclorite.book_story.ui.common.helpers.showToast +import ua.acclorite.book_story.ui.navigator.LocalNavigator + +@Composable +fun HistoryEffects( + effects: SharedFlow, + focusRequester: FocusRequester, + snackbarState: SnackbarHostState, + restoreHistoryEntry: (HistoryEvent.OnRestoreHistoryEntry) -> Unit +) { + val navigator = LocalNavigator.current + val context = LocalContext.current + + LaunchedEffect(Unit) { + effects.collect { effect -> + when (effect) { + is HistoryEffect.OnRequestFocus -> { + focusRequester.requestFocus() + } + + is HistoryEffect.OnShowSnackbar -> { + val snackbarResult = snackbarState.showSnackbar( + context.getString(R.string.history_element_deleted), + context.getString(R.string.undo) + ) + + if (snackbarResult == SnackbarResult.ActionPerformed) { + restoreHistoryEntry( + HistoryEvent.OnRestoreHistoryEntry( + history = effect.history + ) + ) + } + } + + is HistoryEffect.OnWholeHistoryDeleted -> { + context.getString(R.string.history_deleted) + .showToast(context = context) + } + + is HistoryEffect.OnNavigateToLibrary -> { + navigator.push(LibraryScreen, saveInBackStack = false) + } + + is HistoryEffect.OnNavigateToBookInfo -> { + navigator.push(BookInfoScreen(bookId = effect.bookId)) + } + + is HistoryEffect.OnNavigateToReader -> { + insertHistoryChannel.trySend(effect.bookId) + navigator.push(ReaderScreen(bookId = effect.bookId)) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryLayout.kt b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryLayout.kt index a5fde1ac..17de2e35 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryLayout.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryLayout.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items -import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -21,7 +20,6 @@ import ua.acclorite.book_story.presentation.history.HistoryEvent import ua.acclorite.book_story.presentation.history.model.GroupedHistory import ua.acclorite.book_story.ui.common.components.common.LazyColumnWithScrollbar import ua.acclorite.book_story.ui.common.data.ScrollbarData -import ua.acclorite.book_story.ui.common.helpers.LocalActivity import ua.acclorite.book_story.ui.settings.components.SettingsSubcategoryTitle import ua.acclorite.book_story.ui.theme.DefaultTransition @@ -29,15 +27,12 @@ import ua.acclorite.book_story.ui.theme.DefaultTransition fun HistoryLayout( listState: LazyListState, history: List, - snackbarState: SnackbarHostState, isLoading: Boolean, isRefreshing: Boolean, deleteHistoryEntry: (HistoryEvent.OnDeleteHistoryEntry) -> Unit, - navigateToBookInfo: (Int) -> Unit, - navigateToReader: (Int) -> Unit, + navigateToBookInfo: (HistoryEvent.OnNavigateToBookInfo) -> Unit, + navigateToReader: (HistoryEvent.OnNavigateToReader) -> Unit, ) { - val context = LocalActivity.current - DefaultTransition(visible = !isLoading) { LazyColumnWithScrollbar( modifier = Modifier.fillMaxSize(), @@ -75,17 +70,23 @@ fun HistoryLayout( historyEntry = historyEntry, isRefreshing = isRefreshing, onBodyClick = { - navigateToBookInfo(historyEntry.bookId) + navigateToBookInfo( + HistoryEvent.OnNavigateToBookInfo( + historyEntry.bookId + ) + ) }, onTitleClick = { - navigateToReader(historyEntry.bookId) + navigateToReader( + HistoryEvent.OnNavigateToReader( + historyEntry.bookId + ) + ) }, onDeleteClick = { deleteHistoryEntry( HistoryEvent.OnDeleteHistoryEntry( - history = historyEntry, - snackbarState = snackbarState, - context = context, + history = historyEntry ) ) } diff --git a/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryScaffold.kt b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryScaffold.kt index a4871aca..b8207933 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryScaffold.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryScaffold.kt @@ -43,8 +43,8 @@ fun HistoryScaffold( deleteHistoryEntry: (HistoryEvent.OnDeleteHistoryEntry) -> Unit, showDeleteWholeHistoryDialog: (HistoryEvent.OnShowDeleteWholeHistoryDialog) -> Unit, search: (HistoryEvent.OnSearch) -> Unit, - navigateToBookInfo: (Int) -> Unit, - navigateToReader: (Int) -> Unit + navigateToBookInfo: (HistoryEvent.OnNavigateToBookInfo) -> Unit, + navigateToReader: (HistoryEvent.OnNavigateToReader) -> Unit ) { Scaffold( Modifier @@ -79,7 +79,6 @@ fun HistoryScaffold( HistoryLayout( listState = listState, history = history, - snackbarState = snackbarState, isLoading = isLoading, isRefreshing = isRefreshing, deleteHistoryEntry = deleteHistoryEntry, diff --git a/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryTopBar.kt b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryTopBar.kt index fccfc084..ccb73f69 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryTopBar.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryTopBar.kt @@ -98,7 +98,7 @@ fun HistoryTopBar( modifier = Modifier .focusRequester(focusRequester) .onGloballyPositioned { - requestFocus(HistoryEvent.OnRequestFocus(focusRequester)) + requestFocus(HistoryEvent.OnRequestFocus) }, initialQuery = searchQuery, onQueryChange = {