refactor: library viewmodel
This commit is contained in:
parent
d50b1a5f9a
commit
3817cc1980
13 changed files with 283 additions and 223 deletions
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* 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.library
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
sealed class LibraryEffect {
|
||||||
|
data object OnRequestFocus : LibraryEffect()
|
||||||
|
data object OnBooksMoved : LibraryEffect()
|
||||||
|
data object OnBooksDeleted : LibraryEffect()
|
||||||
|
data object OnNavigateToLibrarySettings : LibraryEffect()
|
||||||
|
data object OnNavigateToBrowse : LibraryEffect()
|
||||||
|
data class OnNavigateToBookInfo(val id: Int) : LibraryEffect()
|
||||||
|
data class OnNavigateToReader(val id: Int) : LibraryEffect()
|
||||||
|
}
|
||||||
|
|
@ -6,9 +6,7 @@
|
||||||
|
|
||||||
package ua.acclorite.book_story.presentation.library
|
package ua.acclorite.book_story.presentation.library
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
|
||||||
import ua.acclorite.book_story.domain.model.library.Category
|
import ua.acclorite.book_story.domain.model.library.Category
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|
@ -28,9 +26,7 @@ sealed class LibraryEvent {
|
||||||
|
|
||||||
data object OnSearch : LibraryEvent()
|
data object OnSearch : LibraryEvent()
|
||||||
|
|
||||||
data class OnRequestFocus(
|
data object OnRequestFocus : LibraryEvent()
|
||||||
val focusRequester: FocusRequester
|
|
||||||
) : LibraryEvent()
|
|
||||||
|
|
||||||
data object OnClearSelectedBooks : LibraryEvent()
|
data object OnClearSelectedBooks : LibraryEvent()
|
||||||
|
|
||||||
|
|
@ -42,19 +38,28 @@ sealed class LibraryEvent {
|
||||||
data object OnShowMoveDialog : LibraryEvent()
|
data object OnShowMoveDialog : LibraryEvent()
|
||||||
|
|
||||||
data class OnActionMoveDialog(
|
data class OnActionMoveDialog(
|
||||||
val selectedCategories: List<Category>,
|
val selectedCategories: List<Category>
|
||||||
val context: Context
|
|
||||||
) : LibraryEvent()
|
) : LibraryEvent()
|
||||||
|
|
||||||
data object OnShowDeleteDialog : LibraryEvent()
|
data object OnShowDeleteDialog : LibraryEvent()
|
||||||
|
|
||||||
data class OnActionDeleteDialog(
|
data object OnActionDeleteDialog : LibraryEvent()
|
||||||
val context: Context
|
|
||||||
) : LibraryEvent()
|
|
||||||
|
|
||||||
data object OnDismissDialog : LibraryEvent()
|
data object OnDismissDialog : LibraryEvent()
|
||||||
|
|
||||||
data object OnShowFilterBottomSheet : LibraryEvent()
|
data object OnShowFilterBottomSheet : LibraryEvent()
|
||||||
|
|
||||||
data object OnDismissBottomSheet : LibraryEvent()
|
data object OnDismissBottomSheet : LibraryEvent()
|
||||||
|
|
||||||
|
data object OnNavigateToLibrarySettings : LibraryEvent()
|
||||||
|
|
||||||
|
data object OnNavigateToBrowse : LibraryEvent()
|
||||||
|
|
||||||
|
data class OnNavigateToBookInfo(
|
||||||
|
val id: Int
|
||||||
|
) : LibraryEvent()
|
||||||
|
|
||||||
|
data class OnNavigateToReader(
|
||||||
|
val id: Int
|
||||||
|
) : LibraryEvent()
|
||||||
}
|
}
|
||||||
|
|
@ -12,7 +12,10 @@ 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
|
||||||
|
|
@ -20,17 +23,14 @@ 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.library.Book
|
|
||||||
import ua.acclorite.book_story.domain.use_case.book.DeleteBookUseCase
|
import ua.acclorite.book_story.domain.use_case.book.DeleteBookUseCase
|
||||||
import ua.acclorite.book_story.domain.use_case.book.SearchBooksUseCase
|
import ua.acclorite.book_story.domain.use_case.book.SearchBooksUseCase
|
||||||
import ua.acclorite.book_story.domain.use_case.book.UpdateBookUseCase
|
import ua.acclorite.book_story.domain.use_case.book.UpdateBookUseCase
|
||||||
import ua.acclorite.book_story.presentation.browse.BrowseScreen
|
import ua.acclorite.book_story.presentation.browse.BrowseScreen
|
||||||
import ua.acclorite.book_story.presentation.history.HistoryScreen
|
import ua.acclorite.book_story.presentation.history.HistoryScreen
|
||||||
import ua.acclorite.book_story.presentation.library.model.SelectableBook
|
import ua.acclorite.book_story.presentation.library.model.SelectableBook
|
||||||
import ua.acclorite.book_story.ui.common.helpers.showToast
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class LibraryModel @Inject constructor(
|
class LibraryModel @Inject constructor(
|
||||||
|
|
@ -44,22 +44,21 @@ class LibraryModel @Inject constructor(
|
||||||
private val _state = MutableStateFlow(LibraryState())
|
private val _state = MutableStateFlow(LibraryState())
|
||||||
val state = _state.asStateFlow()
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
private val _effects = MutableSharedFlow<LibraryEffect>()
|
||||||
|
val effects = _effects.asSharedFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
onEvent(
|
||||||
onEvent(
|
LibraryEvent.OnRefreshList(
|
||||||
LibraryEvent.OnRefreshList(
|
loading = true,
|
||||||
loading = true,
|
hideSearch = true
|
||||||
hideSearch = true
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
|
|
||||||
/* Observe channel - - - - - - - - - - - */
|
/* Observe channel - - - - - - - - - - - */
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch {
|
||||||
LibraryScreen.refreshListChannel.receiveAsFlow().collectLatest {
|
LibraryScreen.refreshListChannel.receiveAsFlow().collectLatest {
|
||||||
delay(it)
|
delay(it)
|
||||||
yield()
|
|
||||||
|
|
||||||
onEvent(
|
onEvent(
|
||||||
LibraryEvent.OnRefreshList(
|
LibraryEvent.OnRefreshList(
|
||||||
loading = false,
|
loading = false,
|
||||||
|
|
@ -75,33 +74,42 @@ class LibraryModel @Inject constructor(
|
||||||
private var searchQueryChange: Job? = null
|
private var searchQueryChange: Job? = null
|
||||||
|
|
||||||
fun onEvent(event: LibraryEvent) {
|
fun onEvent(event: LibraryEvent) {
|
||||||
when (event) {
|
viewModelScope.launch {
|
||||||
is LibraryEvent.OnRefreshList -> {
|
when (event) {
|
||||||
refreshJob?.cancel()
|
is LibraryEvent.OnRefreshList -> {
|
||||||
refreshJob = viewModelScope.launch(Dispatchers.IO) {
|
refreshJob?.cancel()
|
||||||
_state.update {
|
refreshJob = viewModelScope.launch(Dispatchers.Default) {
|
||||||
it.copy(
|
_state.update {
|
||||||
isRefreshing = true,
|
it.copy(
|
||||||
isLoading = event.loading,
|
isRefreshing = true,
|
||||||
showSearch = if (event.hideSearch) false else it.showSearch
|
isLoading = event.loading,
|
||||||
)
|
showSearch = if (event.hideSearch) false else it.showSearch
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
|
||||||
yield()
|
ensureActive()
|
||||||
getBooksFromDatabase()
|
val books = searchBooksUseCase(
|
||||||
|
if (_state.value.showSearch) _state.value.searchQuery
|
||||||
|
else ""
|
||||||
|
).map { book -> SelectableBook(book, false) }
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
books = books,
|
||||||
|
hasSelectedItems = false,
|
||||||
|
isLoading = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
delay(500)
|
delay(500) // Delay for UI smoothness
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isRefreshing = false,
|
isRefreshing = false
|
||||||
isLoading = false
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is LibraryEvent.OnSearchVisibility -> {
|
is LibraryEvent.OnSearchVisibility -> {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
if (!event.show) {
|
if (!event.show) {
|
||||||
onEvent(
|
onEvent(
|
||||||
LibraryEvent.OnRefreshList(
|
LibraryEvent.OnRefreshList(
|
||||||
|
|
@ -109,41 +117,31 @@ class LibraryModel @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 LibraryEvent.OnSearchQueryChange -> {
|
is LibraryEvent.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(LibraryEvent.OnSearch)
|
onEvent(LibraryEvent.OnSearch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is LibraryEvent.OnSearch -> {
|
is LibraryEvent.OnSearch -> {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
onEvent(
|
onEvent(
|
||||||
LibraryEvent.OnRefreshList(
|
LibraryEvent.OnRefreshList(
|
||||||
loading = false,
|
loading = false,
|
||||||
|
|
@ -151,12 +149,10 @@ class LibraryModel @Inject constructor(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is LibraryEvent.OnRequestFocus -> {
|
is LibraryEvent.OnRequestFocus -> {
|
||||||
viewModelScope.launch(Dispatchers.Main) {
|
|
||||||
if (!_state.value.hasFocused) {
|
if (!_state.value.hasFocused) {
|
||||||
event.focusRequester.requestFocus()
|
_effects.emit(LibraryEffect.OnRequestFocus)
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
hasFocused = true
|
hasFocused = true
|
||||||
|
|
@ -164,170 +160,152 @@ class LibraryModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is LibraryEvent.OnClearSelectedBooks -> {
|
is LibraryEvent.OnClearSelectedBooks -> {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
withContext(Dispatchers.Default) {
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
books = it.books.map { book -> book.copy(selected = false) },
|
books = it.books.map { book -> book.copy(selected = false) },
|
||||||
hasSelectedItems = false
|
hasSelectedItems = false
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is LibraryEvent.OnSelectBook -> {
|
is LibraryEvent.OnSelectBook -> {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
withContext(Dispatchers.Default) {
|
||||||
val editedList = _state.value.books.map {
|
val editedList = _state.value.books.map {
|
||||||
if (it.data.id == event.id) it.copy(selected = event.select ?: !it.selected)
|
if (it.data.id == event.id) it.copy(
|
||||||
else it
|
selected = event.select ?: !it.selected
|
||||||
}
|
)
|
||||||
|
else it
|
||||||
|
}
|
||||||
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
books = editedList,
|
books = editedList,
|
||||||
selectedItemsCount = editedList.filter { book -> book.selected }.size,
|
selectedItemsCount = editedList.filter { book -> book.selected }.size,
|
||||||
hasSelectedItems = editedList.any { book -> book.selected }
|
hasSelectedItems = editedList.any { book -> book.selected }
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is LibraryEvent.OnShowMoveDialog -> {
|
is LibraryEvent.OnShowMoveDialog -> {
|
||||||
viewModelScope.launch {
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
dialog = LibraryScreen.MOVE_DIALOG
|
dialog = LibraryScreen.MOVE_DIALOG
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is LibraryEvent.OnActionMoveDialog -> {
|
is LibraryEvent.OnActionMoveDialog -> {
|
||||||
viewModelScope.launch {
|
withContext(Dispatchers.Default) {
|
||||||
_state.value.books.forEach { book ->
|
_state.value.books.forEach { book ->
|
||||||
if (!book.selected) return@forEach
|
if (!book.selected) return@forEach
|
||||||
updateBookUseCase(
|
updateBookUseCase(
|
||||||
book.data.copy(
|
book.data.copy(
|
||||||
categories = event.selectedCategories.map { it.id }
|
categories = event.selectedCategories.map { it.id }
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
books = it.books.map { book ->
|
|
||||||
if (!book.selected) return@map book
|
|
||||||
book.copy(
|
|
||||||
data = book.data.copy(
|
|
||||||
categories = event.selectedCategories.map { it.id }
|
|
||||||
),
|
|
||||||
selected = false
|
|
||||||
)
|
)
|
||||||
},
|
)
|
||||||
hasSelectedItems = false,
|
}
|
||||||
dialog = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
HistoryScreen.refreshListChannel.trySend(0)
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
books = it.books.map { book ->
|
||||||
|
if (!book.selected) return@map book
|
||||||
|
book.copy(
|
||||||
|
data = book.data.copy(
|
||||||
|
categories = event.selectedCategories.map { it.id }
|
||||||
|
),
|
||||||
|
selected = false
|
||||||
|
)
|
||||||
|
},
|
||||||
|
hasSelectedItems = false,
|
||||||
|
dialog = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
HistoryScreen.refreshListChannel.trySend(0)
|
||||||
event.context
|
_effects.emit(LibraryEffect.OnBooksMoved)
|
||||||
.getString(R.string.books_moved)
|
|
||||||
.showToast(context = event.context)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is LibraryEvent.OnShowDeleteDialog -> {
|
is LibraryEvent.OnShowDeleteDialog -> {
|
||||||
viewModelScope.launch {
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
dialog = LibraryScreen.DELETE_DIALOG
|
dialog = LibraryScreen.DELETE_DIALOG
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is LibraryEvent.OnActionDeleteDialog -> {
|
is LibraryEvent.OnActionDeleteDialog -> {
|
||||||
viewModelScope.launch {
|
withContext(Dispatchers.Default) {
|
||||||
_state.value.books.forEach {
|
_state.value.books.forEach {
|
||||||
if (!it.selected) return@forEach
|
if (!it.selected) return@forEach
|
||||||
deleteBookUseCase(it.data)
|
deleteBookUseCase(it.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
books = it.books.filter { book -> !book.selected },
|
books = it.books.filter { book -> !book.selected },
|
||||||
hasSelectedItems = false,
|
hasSelectedItems = false,
|
||||||
dialog = null
|
dialog = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
HistoryScreen.refreshListChannel.trySend(0)
|
HistoryScreen.refreshListChannel.trySend(0)
|
||||||
BrowseScreen.refreshListChannel.trySend(Unit)
|
BrowseScreen.refreshListChannel.trySend(Unit)
|
||||||
|
_effects.emit(LibraryEffect.OnBooksDeleted)
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
event.context
|
|
||||||
.getString(R.string.books_deleted)
|
|
||||||
.showToast(context = event.context)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is LibraryEvent.OnDismissDialog -> {
|
is LibraryEvent.OnDismissDialog -> {
|
||||||
viewModelScope.launch {
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
dialog = null
|
dialog = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is LibraryEvent.OnShowFilterBottomSheet -> {
|
is LibraryEvent.OnShowFilterBottomSheet -> {
|
||||||
viewModelScope.launch {
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
bottomSheet = LibraryScreen.FILTER_BOTTOM_SHEET
|
bottomSheet = LibraryScreen.FILTER_BOTTOM_SHEET
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is LibraryEvent.OnDismissBottomSheet -> {
|
is LibraryEvent.OnDismissBottomSheet -> {
|
||||||
viewModelScope.launch {
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
bottomSheet = null
|
bottomSheet = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is LibraryEvent.OnNavigateToLibrarySettings -> {
|
||||||
|
_effects.emit(LibraryEffect.OnNavigateToLibrarySettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
is LibraryEvent.OnNavigateToBrowse -> {
|
||||||
|
_effects.emit(LibraryEffect.OnNavigateToBrowse)
|
||||||
|
}
|
||||||
|
|
||||||
|
is LibraryEvent.OnNavigateToBookInfo -> {
|
||||||
|
_effects.emit(LibraryEffect.OnNavigateToBookInfo(event.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
is LibraryEvent.OnNavigateToReader -> {
|
||||||
|
_effects.emit(LibraryEffect.OnNavigateToReader(event.id))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getBooksFromDatabase(
|
|
||||||
query: String = if (_state.value.showSearch) _state.value.searchQuery else ""
|
|
||||||
) {
|
|
||||||
val books = searchBooksUseCase(query)
|
|
||||||
.sortedWith(compareByDescending<Book> { it.lastOpened }.thenBy { it.title })
|
|
||||||
.map { book -> SelectableBook(book, false) }
|
|
||||||
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
books = books,
|
|
||||||
hasSelectedItems = false,
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,16 +23,11 @@ import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
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.browse.BrowseScreen
|
|
||||||
import ua.acclorite.book_story.presentation.history.HistoryScreen
|
|
||||||
import ua.acclorite.book_story.presentation.navigator.Screen
|
import ua.acclorite.book_story.presentation.navigator.Screen
|
||||||
import ua.acclorite.book_story.presentation.reader.ReaderScreen
|
|
||||||
import ua.acclorite.book_story.presentation.settings.LibrarySettingsScreen
|
|
||||||
import ua.acclorite.book_story.presentation.settings.SettingsModel
|
import ua.acclorite.book_story.presentation.settings.SettingsModel
|
||||||
import ua.acclorite.book_story.ui.common.helpers.LocalSettings
|
import ua.acclorite.book_story.ui.common.helpers.LocalSettings
|
||||||
import ua.acclorite.book_story.ui.library.LibraryContent
|
import ua.acclorite.book_story.ui.library.LibraryContent
|
||||||
import ua.acclorite.book_story.ui.navigator.LocalNavigator
|
import ua.acclorite.book_story.ui.library.LibraryEffects
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object LibraryScreen : Screen, Parcelable {
|
object LibraryScreen : Screen, Parcelable {
|
||||||
|
|
@ -58,7 +53,6 @@ object LibraryScreen : Screen, Parcelable {
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val navigator = LocalNavigator.current
|
|
||||||
val screenModel = hiltViewModel<LibraryModel>()
|
val screenModel = hiltViewModel<LibraryModel>()
|
||||||
val settingsModel = hiltViewModel<SettingsModel>()
|
val settingsModel = hiltViewModel<SettingsModel>()
|
||||||
val settings = LocalSettings.current
|
val settings = LocalSettings.current
|
||||||
|
|
@ -105,6 +99,11 @@ object LibraryScreen : Screen, Parcelable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LibraryEffects(
|
||||||
|
effects = screenModel.effects,
|
||||||
|
focusRequester = focusRequester
|
||||||
|
)
|
||||||
|
|
||||||
LibraryContent(
|
LibraryContent(
|
||||||
books = state.value.books,
|
books = state.value.books,
|
||||||
selectedItemsCount = state.value.selectedItemsCount,
|
selectedItemsCount = state.value.selectedItemsCount,
|
||||||
|
|
@ -150,19 +149,10 @@ object LibraryScreen : Screen, Parcelable {
|
||||||
showFilterBottomSheet = screenModel::onEvent,
|
showFilterBottomSheet = screenModel::onEvent,
|
||||||
dismissBottomSheet = screenModel::onEvent,
|
dismissBottomSheet = screenModel::onEvent,
|
||||||
dismissDialog = screenModel::onEvent,
|
dismissDialog = screenModel::onEvent,
|
||||||
navigateToBrowse = {
|
navigateToBrowse = screenModel::onEvent,
|
||||||
navigator.push(BrowseScreen)
|
navigateToReader = screenModel::onEvent,
|
||||||
},
|
navigateToBookInfo = screenModel::onEvent,
|
||||||
navigateToReader = {
|
navigateToLibrarySettings = screenModel::onEvent
|
||||||
HistoryScreen.insertHistoryChannel.trySend(it)
|
|
||||||
navigator.push(ReaderScreen(it))
|
|
||||||
},
|
|
||||||
navigateToBookInfo = {
|
|
||||||
navigator.push(BookInfoScreen(bookId = it))
|
|
||||||
},
|
|
||||||
navigateToLibrarySettings = {
|
|
||||||
navigator.push(LibrarySettingsScreen)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -69,10 +69,10 @@ fun LibraryContent(
|
||||||
dismissDialog: (LibraryEvent.OnDismissDialog) -> Unit,
|
dismissDialog: (LibraryEvent.OnDismissDialog) -> Unit,
|
||||||
showFilterBottomSheet: (LibraryEvent.OnShowFilterBottomSheet) -> Unit,
|
showFilterBottomSheet: (LibraryEvent.OnShowFilterBottomSheet) -> Unit,
|
||||||
dismissBottomSheet: (LibraryEvent.OnDismissBottomSheet) -> Unit,
|
dismissBottomSheet: (LibraryEvent.OnDismissBottomSheet) -> Unit,
|
||||||
navigateToBrowse: () -> Unit,
|
navigateToBrowse: (LibraryEvent.OnNavigateToBrowse) -> Unit,
|
||||||
navigateToBookInfo: (id: Int) -> Unit,
|
navigateToBookInfo: (LibraryEvent.OnNavigateToBookInfo) -> Unit,
|
||||||
navigateToReader: (id: Int) -> Unit,
|
navigateToReader: (LibraryEvent.OnNavigateToReader) -> Unit,
|
||||||
navigateToLibrarySettings: () -> Unit
|
navigateToLibrarySettings: (LibraryEvent.OnNavigateToLibrarySettings) -> Unit
|
||||||
) {
|
) {
|
||||||
LibraryDialog(
|
LibraryDialog(
|
||||||
dialog = dialog,
|
dialog = dialog,
|
||||||
|
|
@ -146,6 +146,6 @@ fun LibraryContent(
|
||||||
pagerState = pagerState,
|
pagerState = pagerState,
|
||||||
doublePressExit = doublePressExit,
|
doublePressExit = doublePressExit,
|
||||||
clearSelectedBooks = clearSelectedBooks,
|
clearSelectedBooks = clearSelectedBooks,
|
||||||
searchVisibility = searchVisibility,
|
searchVisibility = searchVisibility
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -9,7 +9,6 @@ package ua.acclorite.book_story.ui.library
|
||||||
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.library.LibraryEvent
|
import ua.acclorite.book_story.presentation.library.LibraryEvent
|
||||||
|
|
@ -21,8 +20,6 @@ fun LibraryDeleteDialog(
|
||||||
actionDeleteDialog: (LibraryEvent.OnActionDeleteDialog) -> Unit,
|
actionDeleteDialog: (LibraryEvent.OnActionDeleteDialog) -> Unit,
|
||||||
dismissDialog: (LibraryEvent.OnDismissDialog) -> Unit
|
dismissDialog: (LibraryEvent.OnDismissDialog) -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
Dialog(
|
Dialog(
|
||||||
title = stringResource(id = R.string.delete_books),
|
title = stringResource(id = R.string.delete_books),
|
||||||
icon = Icons.Outlined.DeleteOutline,
|
icon = Icons.Outlined.DeleteOutline,
|
||||||
|
|
@ -35,11 +32,7 @@ fun LibraryDeleteDialog(
|
||||||
dismissDialog(LibraryEvent.OnDismissDialog)
|
dismissDialog(LibraryEvent.OnDismissDialog)
|
||||||
},
|
},
|
||||||
onAction = {
|
onAction = {
|
||||||
actionDeleteDialog(
|
actionDeleteDialog(LibraryEvent.OnActionDeleteDialog)
|
||||||
LibraryEvent.OnActionDeleteDialog(
|
|
||||||
context = context
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
withContent = false
|
withContent = false
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ fun LibraryDialog(
|
||||||
actionMoveDialog: (LibraryEvent.OnActionMoveDialog) -> Unit,
|
actionMoveDialog: (LibraryEvent.OnActionMoveDialog) -> Unit,
|
||||||
actionDeleteDialog: (LibraryEvent.OnActionDeleteDialog) -> Unit,
|
actionDeleteDialog: (LibraryEvent.OnActionDeleteDialog) -> Unit,
|
||||||
dismissDialog: (LibraryEvent.OnDismissDialog) -> Unit,
|
dismissDialog: (LibraryEvent.OnDismissDialog) -> Unit,
|
||||||
navigateToLibrarySettings: () -> Unit
|
navigateToLibrarySettings: (LibraryEvent.OnNavigateToLibrarySettings) -> Unit
|
||||||
) {
|
) {
|
||||||
when (dialog) {
|
when (dialog) {
|
||||||
LibraryScreen.MOVE_DIALOG -> {
|
LibraryScreen.MOVE_DIALOG -> {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* 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.library
|
||||||
|
|
||||||
|
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.browse.BrowseScreen
|
||||||
|
import ua.acclorite.book_story.presentation.history.HistoryScreen
|
||||||
|
import ua.acclorite.book_story.presentation.library.LibraryEffect
|
||||||
|
import ua.acclorite.book_story.presentation.reader.ReaderScreen
|
||||||
|
import ua.acclorite.book_story.presentation.settings.LibrarySettingsScreen
|
||||||
|
import ua.acclorite.book_story.ui.common.helpers.showToast
|
||||||
|
import ua.acclorite.book_story.ui.navigator.LocalNavigator
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LibraryEffects(effects: SharedFlow<LibraryEffect>, focusRequester: FocusRequester) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val navigator = LocalNavigator.current
|
||||||
|
|
||||||
|
LaunchedEffect(effects, focusRequester, context, navigator) {
|
||||||
|
effects.collect { effect ->
|
||||||
|
when (effect) {
|
||||||
|
is LibraryEffect.OnRequestFocus -> {
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
is LibraryEffect.OnBooksMoved -> {
|
||||||
|
context.getString(R.string.books_moved)
|
||||||
|
.showToast(context = context)
|
||||||
|
}
|
||||||
|
|
||||||
|
is LibraryEffect.OnBooksDeleted -> {
|
||||||
|
context.getString(R.string.books_deleted)
|
||||||
|
.showToast(context = context)
|
||||||
|
}
|
||||||
|
|
||||||
|
is LibraryEffect.OnNavigateToLibrarySettings -> {
|
||||||
|
navigator.push(LibrarySettingsScreen)
|
||||||
|
}
|
||||||
|
|
||||||
|
is LibraryEffect.OnNavigateToBrowse -> {
|
||||||
|
navigator.push(BrowseScreen)
|
||||||
|
}
|
||||||
|
|
||||||
|
is LibraryEffect.OnNavigateToBookInfo -> {
|
||||||
|
navigator.push(BookInfoScreen(bookId = effect.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
is LibraryEffect.OnNavigateToReader -> {
|
||||||
|
HistoryScreen.insertHistoryChannel.trySend(effect.id)
|
||||||
|
navigator.push(ReaderScreen(effect.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
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.library.LibraryEvent
|
||||||
import ua.acclorite.book_story.ui.common.components.placeholder.EmptyPlaceholder
|
import ua.acclorite.book_story.ui.common.components.placeholder.EmptyPlaceholder
|
||||||
import ua.acclorite.book_story.ui.theme.Transitions
|
import ua.acclorite.book_story.ui.theme.Transitions
|
||||||
|
|
||||||
|
|
@ -24,7 +25,7 @@ fun BoxScope.LibraryEmptyPlaceholder(
|
||||||
isLoading: Boolean,
|
isLoading: Boolean,
|
||||||
isRefreshing: Boolean,
|
isRefreshing: Boolean,
|
||||||
isBooksEmpty: Boolean,
|
isBooksEmpty: Boolean,
|
||||||
navigateToBrowse: () -> Unit
|
navigateToBrowse: (LibraryEvent.OnNavigateToBrowse) -> Unit
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = !isLoading
|
visible = !isLoading
|
||||||
|
|
@ -40,7 +41,7 @@ fun BoxScope.LibraryEmptyPlaceholder(
|
||||||
modifier = Modifier.align(Alignment.Center),
|
modifier = Modifier.align(Alignment.Center),
|
||||||
actionTitle = stringResource(id = R.string.add_book)
|
actionTitle = stringResource(id = R.string.add_book)
|
||||||
) {
|
) {
|
||||||
navigateToBrowse()
|
navigateToBrowse(LibraryEvent.OnNavigateToBrowse)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,6 @@ import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
|
|
@ -31,10 +30,8 @@ fun LibraryMoveDialog(
|
||||||
categories: List<Category>,
|
categories: List<Category>,
|
||||||
actionMoveDialog: (LibraryEvent.OnActionMoveDialog) -> Unit,
|
actionMoveDialog: (LibraryEvent.OnActionMoveDialog) -> Unit,
|
||||||
dismissDialog: (LibraryEvent.OnDismissDialog) -> Unit,
|
dismissDialog: (LibraryEvent.OnDismissDialog) -> Unit,
|
||||||
navigateToLibrarySettings: () -> Unit
|
navigateToLibrarySettings: (LibraryEvent.OnNavigateToLibrarySettings) -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
val selectedBooks = remember {
|
val selectedBooks = remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
books.filter { it.selected }
|
books.filter { it.selected }
|
||||||
|
|
@ -61,14 +58,13 @@ fun LibraryMoveDialog(
|
||||||
onAction = {
|
onAction = {
|
||||||
actionMoveDialog(
|
actionMoveDialog(
|
||||||
LibraryEvent.OnActionMoveDialog(
|
LibraryEvent.OnActionMoveDialog(
|
||||||
selectedCategories = selectedCategories,
|
selectedCategories = selectedCategories
|
||||||
context = context
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
secondaryAction = stringResource(id = R.string.edit),
|
secondaryAction = stringResource(id = R.string.edit),
|
||||||
onSecondaryAction = {
|
onSecondaryAction = {
|
||||||
navigateToLibrarySettings()
|
navigateToLibrarySettings(LibraryEvent.OnNavigateToLibrarySettings)
|
||||||
dismissDialog(LibraryEvent.OnDismissDialog)
|
dismissDialog(LibraryEvent.OnDismissDialog)
|
||||||
},
|
},
|
||||||
withContent = true,
|
withContent = true,
|
||||||
|
|
|
||||||
|
|
@ -44,9 +44,9 @@ fun LibraryPager(
|
||||||
isLoading: Boolean,
|
isLoading: Boolean,
|
||||||
isRefreshing: Boolean,
|
isRefreshing: Boolean,
|
||||||
selectBook: (LibraryEvent.OnSelectBook) -> Unit,
|
selectBook: (LibraryEvent.OnSelectBook) -> Unit,
|
||||||
navigateToBrowse: () -> Unit,
|
navigateToBrowse: (LibraryEvent.OnNavigateToBrowse) -> Unit,
|
||||||
navigateToBookInfo: (id: Int) -> Unit,
|
navigateToBookInfo: (LibraryEvent.OnNavigateToBookInfo) -> Unit,
|
||||||
navigateToReader: (id: Int) -> Unit,
|
navigateToReader: (LibraryEvent.OnNavigateToReader) -> Unit,
|
||||||
) {
|
) {
|
||||||
val categorizedBooks = remember(
|
val categorizedBooks = remember(
|
||||||
books,
|
books,
|
||||||
|
|
@ -172,8 +172,20 @@ fun LibraryPager(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
navigateToBookInfo = { navigateToBookInfo(book.data.id) },
|
navigateToBookInfo = {
|
||||||
navigateToReader = { navigateToReader(book.data.id) },
|
navigateToBookInfo(
|
||||||
|
LibraryEvent.OnNavigateToBookInfo(
|
||||||
|
book.data.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigateToReader = {
|
||||||
|
navigateToReader(
|
||||||
|
LibraryEvent.OnNavigateToReader(
|
||||||
|
book.data.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,9 +63,9 @@ fun LibraryScaffold(
|
||||||
showMoveDialog: (LibraryEvent.OnShowMoveDialog) -> Unit,
|
showMoveDialog: (LibraryEvent.OnShowMoveDialog) -> Unit,
|
||||||
showDeleteDialog: (LibraryEvent.OnShowDeleteDialog) -> Unit,
|
showDeleteDialog: (LibraryEvent.OnShowDeleteDialog) -> Unit,
|
||||||
showFilterBottomSheet: (LibraryEvent.OnShowFilterBottomSheet) -> Unit,
|
showFilterBottomSheet: (LibraryEvent.OnShowFilterBottomSheet) -> Unit,
|
||||||
navigateToBrowse: () -> Unit,
|
navigateToBrowse: (LibraryEvent.OnNavigateToBrowse) -> Unit,
|
||||||
navigateToBookInfo: (id: Int) -> Unit,
|
navigateToBookInfo: (LibraryEvent.OnNavigateToBookInfo) -> Unit,
|
||||||
navigateToReader: (id: Int) -> Unit,
|
navigateToReader: (LibraryEvent.OnNavigateToReader) -> Unit,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
Modifier
|
Modifier
|
||||||
|
|
|
||||||
|
|
@ -189,7 +189,7 @@ fun LibraryTopBar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.focusRequester(focusRequester)
|
.focusRequester(focusRequester)
|
||||||
.onGloballyPositioned {
|
.onGloballyPositioned {
|
||||||
requestFocus(LibraryEvent.OnRequestFocus(focusRequester))
|
requestFocus(LibraryEvent.OnRequestFocus)
|
||||||
},
|
},
|
||||||
initialQuery = searchQuery,
|
initialQuery = searchQuery,
|
||||||
onQueryChange = {
|
onQueryChange = {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue