refactor: history viewmodel

This commit is contained in:
Acclorite 2025-08-09 10:11:47 +03:00
parent eb87ef8b45
commit 28cbf1188d
No known key found for this signature in database
GPG key ID: 6E54C611F6EE8593
13 changed files with 320 additions and 261 deletions

View file

@ -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.logE
import ua.acclorite.book_story.core.log.logI import ua.acclorite.book_story.core.log.logI
import ua.acclorite.book_story.domain.model.history.History 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.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 import javax.inject.Inject
class GetHistoryUseCase @Inject constructor( class GetHistoryUseCase @Inject constructor(
private val bookRepository: BookRepository,
private val historyRepository: HistoryRepository private val historyRepository: HistoryRepository
) { ) {
suspend operator fun invoke(): List<History> { suspend operator fun invoke(query: String): List<GroupedHistory> {
logI("Getting all history.") 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<History>): List<History> {
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 = { onSuccess = {
logI("Successfully got ${it.size} history entries.") logI("Successfully got ${it.size} grouped history entries.")
it it
}, },
onFailure = { onFailure = {
logE("Could not get history entries with error: ${it.message}") logE("Could not get grouped history entries with error: ${it.message}")
emptyList() emptyList()
} }
) )

View file

@ -27,7 +27,9 @@ object AboutScreen : Screen, Parcelable {
val (scrollBehavior, listState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior() val (scrollBehavior, listState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior()
AboutEffects(screenModel.effects) AboutEffects(
effects = screenModel.effects
)
AboutContent( AboutContent(
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,

View file

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

View file

@ -6,10 +6,7 @@
package ua.acclorite.book_story.presentation.history package ua.acclorite.book_story.presentation.history
import android.content.Context
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.ui.focus.FocusRequester
import ua.acclorite.book_story.domain.model.history.History import ua.acclorite.book_story.domain.model.history.History
@Immutable @Immutable
@ -23,9 +20,7 @@ sealed class HistoryEvent {
val show: Boolean val show: Boolean
) : HistoryEvent() ) : HistoryEvent()
data class OnRequestFocus( data object OnRequestFocus : HistoryEvent()
val focusRequester: FocusRequester
) : HistoryEvent()
data class OnSearchQueryChange( data class OnSearchQueryChange(
val query: String val query: String
@ -34,16 +29,26 @@ sealed class HistoryEvent {
data object OnSearch : HistoryEvent() data object OnSearch : HistoryEvent()
data class OnDeleteHistoryEntry( data class OnDeleteHistoryEntry(
val history: History, val history: History
val snackbarState: SnackbarHostState, ) : HistoryEvent()
val context: Context
data class OnRestoreHistoryEntry(
val history: History
) : HistoryEvent() ) : HistoryEvent()
data object OnShowDeleteWholeHistoryDialog : HistoryEvent() data object OnShowDeleteWholeHistoryDialog : HistoryEvent()
data class OnActionDeleteWholeHistoryDialog( data object OnActionDeleteWholeHistoryDialog : HistoryEvent()
val context: Context
) : HistoryEvent()
data object OnDismissDialog : HistoryEvent() data object OnDismissDialog : HistoryEvent()
data object OnNavigateToLibrary : HistoryEvent()
data class OnNavigateToBookInfo(
val bookId: Int
) : HistoryEvent()
data class OnNavigateToReader(
val bookId: Int
) : HistoryEvent()
} }

View file

@ -6,14 +6,16 @@
package ua.acclorite.book_story.presentation.history package ua.acclorite.book_story.presentation.history
import androidx.compose.material3.SnackbarResult
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.delay import kotlinx.coroutines.delay
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.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
@ -21,26 +23,18 @@ 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.model.history.History 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.AddHistoryUseCase
import ua.acclorite.book_story.domain.use_case.history.DeleteHistoryUseCase 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.DeleteWholeHistoryUseCase
import ua.acclorite.book_story.domain.use_case.history.GetHistoryUseCase 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.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.Date
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import kotlin.coroutines.coroutineContext
@HiltViewModel @HiltViewModel
class HistoryModel @Inject constructor( class HistoryModel @Inject constructor(
private val getBookUseCase: GetBookUseCase,
private val getHistoryUseCase: GetHistoryUseCase, private val getHistoryUseCase: GetHistoryUseCase,
private val addHistoryUseCase: AddHistoryUseCase, private val addHistoryUseCase: AddHistoryUseCase,
private val deleteHistoryUseCase: DeleteHistoryUseCase, private val deleteHistoryUseCase: DeleteHistoryUseCase,
@ -52,21 +46,21 @@ class HistoryModel @Inject constructor(
private val _state = MutableStateFlow(HistoryState()) private val _state = MutableStateFlow(HistoryState())
val state = _state.asStateFlow() val state = _state.asStateFlow()
private val _effects = MutableSharedFlow<HistoryEffect>()
val effects = _effects.asSharedFlow()
init { init {
viewModelScope.launch(Dispatchers.IO) {
onEvent( onEvent(
HistoryEvent.OnRefreshList( HistoryEvent.OnRefreshList(
loading = true, loading = true,
hideSearch = true hideSearch = true
) )
) )
}
/* Observe channel - - - - - - - - - - - */ /* Observe channel - - - - - - - - - - - */
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch {
HistoryScreen.refreshListChannel.receiveAsFlow().collectLatest { HistoryScreen.refreshListChannel.receiveAsFlow().collectLatest { delay ->
delay(it) delay(delay)
yield()
onEvent( onEvent(
HistoryEvent.OnRefreshList( HistoryEvent.OnRefreshList(
@ -76,18 +70,17 @@ class HistoryModel @Inject constructor(
) )
} }
} }
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch {
HistoryScreen.insertHistoryChannel.receiveAsFlow().collectLatest { HistoryScreen.insertHistoryChannel.receiveAsFlow().collectLatest { bookId ->
addHistoryUseCase( addHistoryUseCase(
History( History(
bookId = it, bookId = bookId,
book = null, book = null,
time = Date().time time = Date().time
) )
) )
delay(500) delay(500)
yield()
onEvent( onEvent(
HistoryEvent.OnRefreshList( HistoryEvent.OnRefreshList(
@ -103,13 +96,13 @@ class HistoryModel @Inject constructor(
private var refreshJob: Job? = null private var refreshJob: Job? = null
private var searchQueryChange: Job? = null private var searchQueryChange: Job? = null
private var deleteHistoryEntry: Job? = null
fun onEvent(event: HistoryEvent) { fun onEvent(event: HistoryEvent) {
viewModelScope.launch {
when (event) { when (event) {
is HistoryEvent.OnRefreshList -> { is HistoryEvent.OnRefreshList -> {
refreshJob?.cancel() refreshJob?.cancel()
refreshJob = viewModelScope.launch(Dispatchers.IO) { refreshJob = viewModelScope.launch(Dispatchers.Default) {
_state.update { _state.update {
it.copy( it.copy(
isRefreshing = true, isRefreshing = true,
@ -118,21 +111,28 @@ class HistoryModel @Inject constructor(
) )
} }
yield() ensureActive()
getHistoryFromDatabase() val history = getHistoryUseCase(
if (_state.value.showSearch) _state.value.searchQuery
delay(500) else ""
)
_state.update { _state.update {
it.copy( it.copy(
isRefreshing = false, history = history,
isLoading = false isLoading = false
) )
} }
delay(500) // Delay for UI smoothness
_state.update {
it.copy(
isRefreshing = false
)
}
} }
} }
is HistoryEvent.OnSearchVisibility -> { is HistoryEvent.OnSearchVisibility -> {
viewModelScope.launch(Dispatchers.IO) {
if (!event.show) { if (!event.show) {
onEvent( onEvent(
HistoryEvent.OnRefreshList( HistoryEvent.OnRefreshList(
@ -140,27 +140,20 @@ class HistoryModel @Inject constructor(
hideSearch = true hideSearch = true
) )
) )
} else {
_state.update {
it.copy(
searchQuery = "",
hasFocused = false
)
}
} }
_state.update { _state.update {
it.copy( 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 -> { is HistoryEvent.OnRequestFocus -> {
viewModelScope.launch(Dispatchers.Main) {
if (!_state.value.hasFocused) { if (!_state.value.hasFocused) {
event.focusRequester.requestFocus() _effects.emit(HistoryEffect.OnRequestFocus)
_state.update { _state.update {
it.copy( it.copy(
hasFocused = true hasFocused = true
@ -168,26 +161,22 @@ class HistoryModel @Inject constructor(
} }
} }
} }
}
is HistoryEvent.OnSearchQueryChange -> { is HistoryEvent.OnSearchQueryChange -> {
viewModelScope.launch {
_state.update { _state.update {
it.copy( it.copy(
searchQuery = event.query searchQuery = event.query
) )
} }
searchQueryChange?.cancel() searchQueryChange?.cancel()
searchQueryChange = launch(Dispatchers.IO) { searchQueryChange = viewModelScope.launch(Dispatchers.IO) {
delay(500) delay(500)
yield()
onEvent(HistoryEvent.OnSearch) onEvent(HistoryEvent.OnSearch)
} }
} }
}
is HistoryEvent.OnSearch -> { is HistoryEvent.OnSearch -> {
viewModelScope.launch(Dispatchers.IO) {
onEvent( onEvent(
HistoryEvent.OnRefreshList( HistoryEvent.OnRefreshList(
loading = false, loading = false,
@ -195,10 +184,9 @@ class HistoryModel @Inject constructor(
) )
) )
} }
}
is HistoryEvent.OnDeleteHistoryEntry -> { is HistoryEvent.OnDeleteHistoryEntry -> {
viewModelScope.launch(Dispatchers.IO) { withContext(Dispatchers.Default) {
deleteHistoryUseCase(event.history) deleteHistoryUseCase(event.history)
onEvent( onEvent(
@ -209,28 +197,13 @@ class HistoryModel @Inject constructor(
) )
LibraryScreen.refreshListChannel.trySend(0) LibraryScreen.refreshListChannel.trySend(0)
deleteHistoryEntry?.cancel() _effects.emit(HistoryEffect.OnShowSnackbar(event.history))
event.snackbarState.currentSnackbarData?.dismiss() }
deleteHistoryEntry = launch(Dispatchers.IO) {
repeat(10) {
yield()
delay(1000)
} }
yield() is HistoryEvent.OnRestoreHistoryEntry -> {
event.snackbarState.currentSnackbarData?.dismiss() withContext(Dispatchers.Default) {
}
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) addHistoryUseCase(event.history)
LibraryScreen.refreshListChannel.trySend(0)
onEvent( onEvent(
HistoryEvent.OnRefreshList( HistoryEvent.OnRefreshList(
@ -238,23 +211,20 @@ class HistoryModel @Inject constructor(
hideSearch = false hideSearch = false
) )
) )
} LibraryScreen.refreshListChannel.trySend(0)
}
} }
} }
is HistoryEvent.OnShowDeleteWholeHistoryDialog -> { is HistoryEvent.OnShowDeleteWholeHistoryDialog -> {
viewModelScope.launch {
_state.update { _state.update {
it.copy( it.copy(
dialog = HistoryScreen.DELETE_WHOLE_HISTORY_DIALOG dialog = HistoryScreen.DELETE_WHOLE_HISTORY_DIALOG
) )
} }
} }
}
is HistoryEvent.OnActionDeleteWholeHistoryDialog -> { is HistoryEvent.OnActionDeleteWholeHistoryDialog -> {
viewModelScope.launch { withContext(Dispatchers.Default) {
_state.update { _state.update {
it.copy( it.copy(
dialog = null, dialog = null,
@ -263,105 +233,45 @@ class HistoryModel @Inject constructor(
} }
deleteWholeHistoryUseCase() deleteWholeHistoryUseCase()
LibraryScreen.refreshListChannel.trySend(0)
onEvent( onEvent(
HistoryEvent.OnRefreshList( HistoryEvent.OnRefreshList(
loading = true, loading = true,
hideSearch = true hideSearch = true
) )
) )
LibraryScreen.refreshListChannel.trySend(0)
withContext(Dispatchers.Main) { _effects.emit(HistoryEffect.OnWholeHistoryDeleted)
event.context
.getString(R.string.history_deleted)
.showToast(context = event.context)
}
} }
} }
is HistoryEvent.OnDismissDialog -> { is HistoryEvent.OnDismissDialog -> {
viewModelScope.launch {
_state.update { _state.update {
it.copy( it.copy(
dialog = null dialog = null
) )
} }
} }
}
} is HistoryEvent.OnNavigateToLibrary -> {
_effects.emit(HistoryEffect.OnNavigateToLibrary)
} }
private suspend fun getHistoryFromDatabase( is HistoryEvent.OnNavigateToBookInfo -> {
query: String = if (_state.value.showSearch) _state.value.searchQuery else "" _effects.emit(HistoryEffect.OnNavigateToBookInfo(event.bookId))
) {
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<History>): List<History> { is HistoryEvent.OnNavigateToReader -> {
val groupedById = elements.groupBy { it.bookId } _effects.emit(HistoryEffect.OnNavigateToReader(event.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
}
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()
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)
}
}.map { (day, history) -> GroupedHistory(day, filterMaxElementsById(history)) }
_state.update {
it.copy(
history = history,
isLoading = false,
)
} }
} }
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

@ -22,12 +22,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize 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.navigator.Screen
import ua.acclorite.book_story.presentation.reader.ReaderScreen
import ua.acclorite.book_story.ui.history.HistoryContent 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 @Parcelize
object HistoryScreen : Screen, Parcelable { object HistoryScreen : Screen, Parcelable {
@ -49,7 +46,6 @@ object HistoryScreen : Screen, Parcelable {
@Composable @Composable
override fun Content() { override fun Content() {
val navigator = LocalNavigator.current
val screenModel = hiltViewModel<HistoryModel>() val screenModel = hiltViewModel<HistoryModel>()
val state = screenModel.state.collectAsStateWithLifecycle() 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( HistoryContent(
refreshState = refreshState, refreshState = refreshState,
snackbarState = snackbarState, snackbarState = snackbarState,
@ -100,16 +103,9 @@ object HistoryScreen : Screen, Parcelable {
showDeleteWholeHistoryDialog = screenModel::onEvent, showDeleteWholeHistoryDialog = screenModel::onEvent,
actionDeleteWholeHistoryDialog = screenModel::onEvent, actionDeleteWholeHistoryDialog = screenModel::onEvent,
dismissDialog = screenModel::onEvent, dismissDialog = screenModel::onEvent,
navigateToLibrary = { navigateToLibrary = screenModel::onEvent,
navigator.push(LibraryScreen, saveInBackStack = false) navigateToBookInfo = screenModel::onEvent,
}, navigateToReader = screenModel::onEvent
navigateToBookInfo = {
navigator.push(BookInfoScreen(bookId = it))
},
navigateToReader = {
insertHistoryChannel.trySend(it)
navigator.push(ReaderScreen(it))
}
) )
} }
} }

View file

@ -14,7 +14,7 @@ import ua.acclorite.book_story.presentation.history.HistoryEvent
fun HistoryBackHandler( fun HistoryBackHandler(
showSearch: Boolean, showSearch: Boolean,
searchVisibility: (HistoryEvent.OnSearchVisibility) -> Unit, searchVisibility: (HistoryEvent.OnSearchVisibility) -> Unit,
navigateToLibrary: () -> Unit navigateToLibrary: (HistoryEvent.OnNavigateToLibrary) -> Unit
) { ) {
BackHandler { BackHandler {
if (showSearch) { if (showSearch) {
@ -22,6 +22,6 @@ fun HistoryBackHandler(
return@BackHandler return@BackHandler
} }
navigateToLibrary() navigateToLibrary(HistoryEvent.OnNavigateToLibrary)
} }
} }

View file

@ -39,9 +39,9 @@ fun HistoryContent(
showDeleteWholeHistoryDialog: (HistoryEvent.OnShowDeleteWholeHistoryDialog) -> Unit, showDeleteWholeHistoryDialog: (HistoryEvent.OnShowDeleteWholeHistoryDialog) -> Unit,
actionDeleteWholeHistoryDialog: (HistoryEvent.OnActionDeleteWholeHistoryDialog) -> Unit, actionDeleteWholeHistoryDialog: (HistoryEvent.OnActionDeleteWholeHistoryDialog) -> Unit,
dismissDialog: (HistoryEvent.OnDismissDialog) -> Unit, dismissDialog: (HistoryEvent.OnDismissDialog) -> Unit,
navigateToLibrary: () -> Unit, navigateToLibrary: (HistoryEvent.OnNavigateToLibrary) -> Unit,
navigateToBookInfo: (Int) -> Unit, navigateToBookInfo: (HistoryEvent.OnNavigateToBookInfo) -> Unit,
navigateToReader: (Int) -> Unit navigateToReader: (HistoryEvent.OnNavigateToReader) -> Unit
) { ) {
HistoryDialog( HistoryDialog(
dialog = dialog, dialog = dialog,

View file

@ -9,7 +9,6 @@ package ua.acclorite.book_story.ui.history
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.history.HistoryEvent import ua.acclorite.book_story.presentation.history.HistoryEvent
@ -20,7 +19,6 @@ fun HistoryDeleteWholeHistoryDialog(
actionDeleteWholeHistoryDialog: (HistoryEvent.OnActionDeleteWholeHistoryDialog) -> Unit, actionDeleteWholeHistoryDialog: (HistoryEvent.OnActionDeleteWholeHistoryDialog) -> Unit,
dismissDialog: (HistoryEvent.OnDismissDialog) -> Unit dismissDialog: (HistoryEvent.OnDismissDialog) -> Unit
) { ) {
val context = LocalContext.current
Dialog( Dialog(
title = stringResource(id = R.string.delete_history), title = stringResource(id = R.string.delete_history),
icon = Icons.Outlined.DeleteOutline, icon = Icons.Outlined.DeleteOutline,
@ -30,11 +28,7 @@ fun HistoryDeleteWholeHistoryDialog(
dismissDialog(HistoryEvent.OnDismissDialog) dismissDialog(HistoryEvent.OnDismissDialog)
}, },
onAction = { onAction = {
actionDeleteWholeHistoryDialog( actionDeleteWholeHistoryDialog(HistoryEvent.OnActionDeleteWholeHistoryDialog)
HistoryEvent.OnActionDeleteWholeHistoryDialog(
context = context
)
)
}, },
withContent = false withContent = false
) )

View file

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

View file

@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource 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.presentation.history.model.GroupedHistory
import ua.acclorite.book_story.ui.common.components.common.LazyColumnWithScrollbar 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.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.settings.components.SettingsSubcategoryTitle
import ua.acclorite.book_story.ui.theme.DefaultTransition import ua.acclorite.book_story.ui.theme.DefaultTransition
@ -29,15 +27,12 @@ import ua.acclorite.book_story.ui.theme.DefaultTransition
fun HistoryLayout( fun HistoryLayout(
listState: LazyListState, listState: LazyListState,
history: List<GroupedHistory>, history: List<GroupedHistory>,
snackbarState: SnackbarHostState,
isLoading: Boolean, isLoading: Boolean,
isRefreshing: Boolean, isRefreshing: Boolean,
deleteHistoryEntry: (HistoryEvent.OnDeleteHistoryEntry) -> Unit, deleteHistoryEntry: (HistoryEvent.OnDeleteHistoryEntry) -> Unit,
navigateToBookInfo: (Int) -> Unit, navigateToBookInfo: (HistoryEvent.OnNavigateToBookInfo) -> Unit,
navigateToReader: (Int) -> Unit, navigateToReader: (HistoryEvent.OnNavigateToReader) -> Unit,
) { ) {
val context = LocalActivity.current
DefaultTransition(visible = !isLoading) { DefaultTransition(visible = !isLoading) {
LazyColumnWithScrollbar( LazyColumnWithScrollbar(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@ -75,17 +70,23 @@ fun HistoryLayout(
historyEntry = historyEntry, historyEntry = historyEntry,
isRefreshing = isRefreshing, isRefreshing = isRefreshing,
onBodyClick = { onBodyClick = {
navigateToBookInfo(historyEntry.bookId) navigateToBookInfo(
HistoryEvent.OnNavigateToBookInfo(
historyEntry.bookId
)
)
}, },
onTitleClick = { onTitleClick = {
navigateToReader(historyEntry.bookId) navigateToReader(
HistoryEvent.OnNavigateToReader(
historyEntry.bookId
)
)
}, },
onDeleteClick = { onDeleteClick = {
deleteHistoryEntry( deleteHistoryEntry(
HistoryEvent.OnDeleteHistoryEntry( HistoryEvent.OnDeleteHistoryEntry(
history = historyEntry, history = historyEntry
snackbarState = snackbarState,
context = context,
) )
) )
} }

View file

@ -43,8 +43,8 @@ fun HistoryScaffold(
deleteHistoryEntry: (HistoryEvent.OnDeleteHistoryEntry) -> Unit, deleteHistoryEntry: (HistoryEvent.OnDeleteHistoryEntry) -> Unit,
showDeleteWholeHistoryDialog: (HistoryEvent.OnShowDeleteWholeHistoryDialog) -> Unit, showDeleteWholeHistoryDialog: (HistoryEvent.OnShowDeleteWholeHistoryDialog) -> Unit,
search: (HistoryEvent.OnSearch) -> Unit, search: (HistoryEvent.OnSearch) -> Unit,
navigateToBookInfo: (Int) -> Unit, navigateToBookInfo: (HistoryEvent.OnNavigateToBookInfo) -> Unit,
navigateToReader: (Int) -> Unit navigateToReader: (HistoryEvent.OnNavigateToReader) -> Unit
) { ) {
Scaffold( Scaffold(
Modifier Modifier
@ -79,7 +79,6 @@ fun HistoryScaffold(
HistoryLayout( HistoryLayout(
listState = listState, listState = listState,
history = history, history = history,
snackbarState = snackbarState,
isLoading = isLoading, isLoading = isLoading,
isRefreshing = isRefreshing, isRefreshing = isRefreshing,
deleteHistoryEntry = deleteHistoryEntry, deleteHistoryEntry = deleteHistoryEntry,

View file

@ -98,7 +98,7 @@ fun HistoryTopBar(
modifier = Modifier modifier = Modifier
.focusRequester(focusRequester) .focusRequester(focusRequester)
.onGloballyPositioned { .onGloballyPositioned {
requestFocus(HistoryEvent.OnRequestFocus(focusRequester)) requestFocus(HistoryEvent.OnRequestFocus)
}, },
initialQuery = searchQuery, initialQuery = searchQuery,
onQueryChange = { onQueryChange = {