refactor: history viewmodel
This commit is contained in:
parent
eb87ef8b45
commit
28cbf1188d
13 changed files with 320 additions and 261 deletions
|
|
@ -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<History> {
|
||||
suspend operator fun invoke(query: String): List<GroupedHistory> {
|
||||
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 = {
|
||||
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()
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ object AboutScreen : Screen, Parcelable {
|
|||
|
||||
val (scrollBehavior, listState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior()
|
||||
|
||||
AboutEffects(screenModel.effects)
|
||||
AboutEffects(
|
||||
effects = screenModel.effects
|
||||
)
|
||||
|
||||
AboutContent(
|
||||
scrollBehavior = scrollBehavior,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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<HistoryEffect>()
|
||||
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<History>): List<History> {
|
||||
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 <T> MutableStateFlow<T>.update(function: (T) -> T) {
|
||||
mutex.withLock {
|
||||
yield()
|
||||
coroutineContext.ensureActive()
|
||||
this.value = function(this.value)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HistoryModel>()
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<GroupedHistory>,
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ fun HistoryTopBar(
|
|||
modifier = Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.onGloballyPositioned {
|
||||
requestFocus(HistoryEvent.OnRequestFocus(focusRequester))
|
||||
requestFocus(HistoryEvent.OnRequestFocus)
|
||||
},
|
||||
initialQuery = searchQuery,
|
||||
onQueryChange = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue