refactor: browse viewmodel
This commit is contained in:
parent
6f53c70b5b
commit
d50b1a5f9a
14 changed files with 398 additions and 414 deletions
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* 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.domain.use_case.file_system
|
||||||
|
|
||||||
|
import ua.acclorite.book_story.core.helpers.compareByWithOrder
|
||||||
|
import ua.acclorite.book_story.core.log.logE
|
||||||
|
import ua.acclorite.book_story.core.log.logI
|
||||||
|
import ua.acclorite.book_story.data.settings.SettingsManager
|
||||||
|
import ua.acclorite.book_story.domain.model.file.File
|
||||||
|
import ua.acclorite.book_story.domain.repository.FileSystemRepository
|
||||||
|
import ua.acclorite.book_story.presentation.browse.model.BrowseSortOrder
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class GetFilesUseCase @Inject constructor(
|
||||||
|
private val fileSystemRepository: FileSystemRepository,
|
||||||
|
private val settings: SettingsManager
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend operator fun invoke(query: String = ""): List<File> {
|
||||||
|
logI("Searching for files with query: \"$query\".")
|
||||||
|
|
||||||
|
fun List<File>.filterFiles(): List<File> {
|
||||||
|
return if (settings.browseIncludedFilterItems.lastValue.isEmpty()) this
|
||||||
|
else filter { file ->
|
||||||
|
settings.browseIncludedFilterItems.lastValue.any { extension ->
|
||||||
|
file.path.endsWith(
|
||||||
|
extension, ignoreCase = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<File>.sortFiles(): List<File> {
|
||||||
|
return sortedWith(
|
||||||
|
compareByWithOrder(settings.browseSortOrderDescending.lastValue) { file ->
|
||||||
|
when (settings.browseSortOrder.lastValue) {
|
||||||
|
BrowseSortOrder.NAME -> file.name.trim()
|
||||||
|
|
||||||
|
BrowseSortOrder.FILE_FORMAT -> file.path
|
||||||
|
.substringAfterLast(".")
|
||||||
|
.lowercase()
|
||||||
|
.trimEnd()
|
||||||
|
|
||||||
|
BrowseSortOrder.FILE_SIZE -> file.size
|
||||||
|
|
||||||
|
else -> file.lastModified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return runCatching {
|
||||||
|
fileSystemRepository.searchFiles(query)
|
||||||
|
.getOrThrow()
|
||||||
|
.filterFiles()
|
||||||
|
.sortFiles()
|
||||||
|
}.fold(
|
||||||
|
onSuccess = {
|
||||||
|
logI("Successfully found [${it.size}] files.")
|
||||||
|
it
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
logE("Could not find files with error: ${it.message}")
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.domain.use_case.file_system
|
|
||||||
|
|
||||||
import ua.acclorite.book_story.core.log.logE
|
|
||||||
import ua.acclorite.book_story.core.log.logI
|
|
||||||
import ua.acclorite.book_story.domain.model.file.File
|
|
||||||
import ua.acclorite.book_story.domain.repository.FileSystemRepository
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class SearchFilesUseCase @Inject constructor(
|
|
||||||
private val fileSystemRepository: FileSystemRepository
|
|
||||||
) {
|
|
||||||
|
|
||||||
suspend operator fun invoke(query: String = ""): List<File> {
|
|
||||||
logI("Searching for files with query: \"$query\".")
|
|
||||||
|
|
||||||
return fileSystemRepository.searchFiles(query).fold(
|
|
||||||
onSuccess = {
|
|
||||||
logI("Successfully found [${it.size}] files.")
|
|
||||||
it
|
|
||||||
},
|
|
||||||
onFailure = {
|
|
||||||
logE("Could not find files with error: ${it.message}")
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* 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.browse
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
sealed class BrowseEffect {
|
||||||
|
data object OnRequestFocus : BrowseEffect()
|
||||||
|
|
||||||
|
data object OnBooksAdded : BrowseEffect()
|
||||||
|
|
||||||
|
data object OnNavigateToLibrary : BrowseEffect()
|
||||||
|
|
||||||
|
data class OnUpdatePinnedPaths(
|
||||||
|
val path: String
|
||||||
|
) : BrowseEffect()
|
||||||
|
|
||||||
|
data object OnNavigateToBrowseSettings : BrowseEffect()
|
||||||
|
}
|
||||||
|
|
@ -6,9 +6,7 @@
|
||||||
|
|
||||||
package ua.acclorite.book_story.presentation.browse
|
package ua.acclorite.book_story.presentation.browse
|
||||||
|
|
||||||
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.presentation.browse.model.SelectableFile
|
import ua.acclorite.book_story.presentation.browse.model.SelectableFile
|
||||||
import ua.acclorite.book_story.presentation.library.model.SelectableNullableBook
|
import ua.acclorite.book_story.presentation.library.model.SelectableNullableBook
|
||||||
|
|
||||||
|
|
@ -23,9 +21,7 @@ sealed class BrowseEvent {
|
||||||
val show: Boolean
|
val show: Boolean
|
||||||
) : BrowseEvent()
|
) : BrowseEvent()
|
||||||
|
|
||||||
data class OnRequestFocus(
|
data object OnRequestFocus : BrowseEvent()
|
||||||
val focusRequester: FocusRequester
|
|
||||||
) : BrowseEvent()
|
|
||||||
|
|
||||||
data class OnSearchQueryChange(
|
data class OnSearchQueryChange(
|
||||||
val query: String
|
val query: String
|
||||||
|
|
@ -36,15 +32,9 @@ sealed class BrowseEvent {
|
||||||
data object OnClearSelectedFiles : BrowseEvent()
|
data object OnClearSelectedFiles : BrowseEvent()
|
||||||
|
|
||||||
data class OnSelectFiles(
|
data class OnSelectFiles(
|
||||||
val includedFileFormats: List<String>,
|
|
||||||
val files: List<SelectableFile>
|
val files: List<SelectableFile>
|
||||||
) : BrowseEvent()
|
) : BrowseEvent()
|
||||||
|
|
||||||
data class OnSelectFile(
|
|
||||||
val includedFileFormats: List<String>,
|
|
||||||
val file: SelectableFile
|
|
||||||
) : BrowseEvent()
|
|
||||||
|
|
||||||
data object OnShowFilterBottomSheet : BrowseEvent()
|
data object OnShowFilterBottomSheet : BrowseEvent()
|
||||||
|
|
||||||
data object OnDismissBottomSheet : BrowseEvent()
|
data object OnDismissBottomSheet : BrowseEvent()
|
||||||
|
|
@ -57,10 +47,15 @@ sealed class BrowseEvent {
|
||||||
val book: SelectableNullableBook
|
val book: SelectableNullableBook
|
||||||
) : BrowseEvent()
|
) : BrowseEvent()
|
||||||
|
|
||||||
data class OnActionAddDialog(
|
data object OnActionAddDialog : BrowseEvent()
|
||||||
val context: Context,
|
|
||||||
val navigateToLibrary: () -> Unit
|
|
||||||
) : BrowseEvent()
|
|
||||||
|
|
||||||
data object OnDismissDialog : BrowseEvent()
|
data object OnDismissDialog : BrowseEvent()
|
||||||
|
|
||||||
|
data class OnUpdatePinnedPaths(
|
||||||
|
val path: String
|
||||||
|
) : BrowseEvent()
|
||||||
|
|
||||||
|
data object OnNavigateToLibrary : BrowseEvent()
|
||||||
|
|
||||||
|
data object OnNavigateToBrowseSettings : BrowseEvent()
|
||||||
}
|
}
|
||||||
|
|
@ -13,7 +13,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
|
||||||
|
|
@ -21,25 +24,21 @@ 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.core.helpers.compareByWithOrder
|
|
||||||
import ua.acclorite.book_story.data.model.common.NullableBook
|
import ua.acclorite.book_story.data.model.common.NullableBook
|
||||||
import ua.acclorite.book_story.domain.model.file.File
|
import ua.acclorite.book_story.domain.model.file.File
|
||||||
import ua.acclorite.book_story.domain.use_case.book.AddBookUseCase
|
import ua.acclorite.book_story.domain.use_case.book.AddBookUseCase
|
||||||
import ua.acclorite.book_story.domain.use_case.file_system.GetBookFromFileUseCase
|
import ua.acclorite.book_story.domain.use_case.file_system.GetBookFromFileUseCase
|
||||||
import ua.acclorite.book_story.domain.use_case.file_system.SearchFilesUseCase
|
import ua.acclorite.book_story.domain.use_case.file_system.GetFilesUseCase
|
||||||
import ua.acclorite.book_story.presentation.browse.model.BrowseSortOrder
|
|
||||||
import ua.acclorite.book_story.presentation.browse.model.SelectableFile
|
import ua.acclorite.book_story.presentation.browse.model.SelectableFile
|
||||||
import ua.acclorite.book_story.presentation.library.LibraryScreen
|
import ua.acclorite.book_story.presentation.library.LibraryScreen
|
||||||
import ua.acclorite.book_story.presentation.library.model.SelectableNullableBook
|
import ua.acclorite.book_story.presentation.library.model.SelectableNullableBook
|
||||||
import ua.acclorite.book_story.ui.common.helpers.showToast
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class BrowseModel @Inject constructor(
|
class BrowseModel @Inject constructor(
|
||||||
private val addBookUseCase: AddBookUseCase,
|
private val addBookUseCase: AddBookUseCase,
|
||||||
private val searchFilesUseCase: SearchFilesUseCase,
|
private val getFilesUseCase: GetFilesUseCase,
|
||||||
private val getBookFromFileUseCase: GetBookFromFileUseCase
|
private val getBookFromFileUseCase: GetBookFromFileUseCase
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
|
@ -48,18 +47,19 @@ class BrowseModel @Inject constructor(
|
||||||
private val _state = MutableStateFlow(BrowseState())
|
private val _state = MutableStateFlow(BrowseState())
|
||||||
val state = _state.asStateFlow()
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
private val _effects = MutableSharedFlow<BrowseEffect>()
|
||||||
|
val effects = _effects.asSharedFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
onEvent(
|
||||||
onEvent(
|
BrowseEvent.OnRefreshList(
|
||||||
BrowseEvent.OnRefreshList(
|
loading = true,
|
||||||
loading = true,
|
hideSearch = true
|
||||||
hideSearch = true
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
|
|
||||||
/* Observe channel - - - - - - - - - - - */
|
/* Observe channel - - - - - - - - - - - */
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch {
|
||||||
BrowseScreen.refreshListChannel.receiveAsFlow().collectLatest {
|
BrowseScreen.refreshListChannel.receiveAsFlow().collectLatest {
|
||||||
onEvent(
|
onEvent(
|
||||||
BrowseEvent.OnRefreshList(
|
BrowseEvent.OnRefreshList(
|
||||||
|
|
@ -73,37 +73,57 @@ class BrowseModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private var refreshJob: Job? = null
|
private var refreshJob: Job? = null
|
||||||
private var changeSearchQueryJob: Job? = null
|
private var searchQueryChangeJob: Job? = null
|
||||||
private var getAddDialogBooksJob: Job? = null
|
private var getAddDialogBooksJob: Job? = null
|
||||||
|
|
||||||
fun onEvent(event: BrowseEvent) {
|
fun onEvent(event: BrowseEvent) {
|
||||||
when (event) {
|
viewModelScope.launch {
|
||||||
is BrowseEvent.OnRefreshList -> {
|
when (event) {
|
||||||
refreshJob?.cancel()
|
is BrowseEvent.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()
|
||||||
getFilesFromDownloads()
|
val selectableFiles = getFilesUseCase(
|
||||||
|
if (_state.value.showSearch) _state.value.searchQuery
|
||||||
|
else ""
|
||||||
|
).map { file ->
|
||||||
|
SelectableFile(
|
||||||
|
name = file.name,
|
||||||
|
uri = file.uri.toUri(),
|
||||||
|
path = file.path,
|
||||||
|
size = file.size,
|
||||||
|
lastModified = file.lastModified,
|
||||||
|
isDirectory = file.isDirectory,
|
||||||
|
selected = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
files = selectableFiles,
|
||||||
|
hasSelectedItems = false,
|
||||||
|
isLoading = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
delay(500)
|
|
||||||
_state.update {
|
delay(500) // Delay for UI smoothness
|
||||||
it.copy(
|
_state.update {
|
||||||
isRefreshing = false,
|
it.copy(
|
||||||
isLoading = false
|
isRefreshing = false
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is BrowseEvent.OnSearchVisibility -> {
|
is BrowseEvent.OnSearchVisibility -> {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
if (!event.show) {
|
if (!event.show) {
|
||||||
onEvent(
|
onEvent(
|
||||||
BrowseEvent.OnRefreshList(
|
BrowseEvent.OnRefreshList(
|
||||||
|
|
@ -111,27 +131,20 @@ class BrowseModel @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 BrowseEvent.OnRequestFocus -> {
|
is BrowseEvent.OnRequestFocus -> {
|
||||||
viewModelScope.launch(Dispatchers.Main) {
|
|
||||||
if (!_state.value.hasFocused) {
|
if (!_state.value.hasFocused) {
|
||||||
event.focusRequester.requestFocus()
|
_effects.emit(BrowseEffect.OnRequestFocus)
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
hasFocused = true
|
hasFocused = true
|
||||||
|
|
@ -139,26 +152,22 @@ class BrowseModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is BrowseEvent.OnSearchQueryChange -> {
|
is BrowseEvent.OnSearchQueryChange -> {
|
||||||
viewModelScope.launch {
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
searchQuery = event.query
|
searchQuery = event.query
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
changeSearchQueryJob?.cancel()
|
|
||||||
changeSearchQueryJob = launch(Dispatchers.IO) {
|
searchQueryChangeJob?.cancel()
|
||||||
|
searchQueryChangeJob = viewModelScope.launch(Dispatchers.IO) {
|
||||||
delay(500)
|
delay(500)
|
||||||
yield()
|
|
||||||
onEvent(BrowseEvent.OnSearch)
|
onEvent(BrowseEvent.OnSearch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is BrowseEvent.OnSearch -> {
|
is BrowseEvent.OnSearch -> {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
onEvent(
|
onEvent(
|
||||||
BrowseEvent.OnRefreshList(
|
BrowseEvent.OnRefreshList(
|
||||||
loading = false,
|
loading = false,
|
||||||
|
|
@ -166,106 +175,62 @@ class BrowseModel @Inject constructor(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is BrowseEvent.OnClearSelectedFiles -> {
|
is BrowseEvent.OnClearSelectedFiles -> {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
withContext(Dispatchers.Default) {
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
files = it.files.map { file -> file.copy(selected = false) },
|
files = it.files.map { file -> file.copy(selected = false) },
|
||||||
hasSelectedItems = false
|
hasSelectedItems = false
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is BrowseEvent.OnSelectFiles -> {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
val editedList = _state.value.files.map { file ->
|
|
||||||
if (
|
|
||||||
event.files.any {
|
|
||||||
file.path.startsWith(it.path)
|
|
||||||
} && event.includedFileFormats.run {
|
|
||||||
if (isEmpty()) return@run true
|
|
||||||
any {
|
|
||||||
file.path.endsWith(it, ignoreCase = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
file.copy(selected = true)
|
|
||||||
} else {
|
|
||||||
file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
files = editedList,
|
|
||||||
selectedItemsCount = editedList.filter { file ->
|
|
||||||
file.selected
|
|
||||||
}.size.run {
|
|
||||||
if (this == 0) return@run it.selectedItemsCount
|
|
||||||
this
|
|
||||||
},
|
|
||||||
hasSelectedItems = editedList.any { file ->
|
|
||||||
file.selected
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is BrowseEvent.OnSelectFile -> {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
val editedList = _state.value.files.map { file ->
|
|
||||||
if (event.file.path == file.path) {
|
|
||||||
file.copy(
|
|
||||||
selected = !file.selected
|
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
file
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_state.update {
|
is BrowseEvent.OnSelectFiles -> {
|
||||||
it.copy(
|
withContext(Dispatchers.Default) {
|
||||||
files = editedList,
|
val editedList = _state.value.files.map { file ->
|
||||||
selectedItemsCount = editedList.filter { file ->
|
if (event.files.any { file.path.startsWith(it.path, true) }) {
|
||||||
file.selected
|
file.copy(
|
||||||
}.size.run {
|
selected = if (event.files.size > 1) true else !file.selected
|
||||||
if (this == 0) return@run it.selectedItemsCount
|
)
|
||||||
this
|
} else file
|
||||||
},
|
}
|
||||||
hasSelectedItems = editedList.any { file ->
|
|
||||||
file.selected
|
_state.update {
|
||||||
}
|
it.copy(
|
||||||
)
|
files = editedList,
|
||||||
|
selectedItemsCount = editedList.filter { file ->
|
||||||
|
file.selected
|
||||||
|
}.size.let { selectedItems ->
|
||||||
|
if (selectedItems == 0) return@let it.selectedItemsCount
|
||||||
|
selectedItems
|
||||||
|
},
|
||||||
|
hasSelectedItems = editedList.any { file ->
|
||||||
|
file.selected
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is BrowseEvent.OnShowFilterBottomSheet -> {
|
is BrowseEvent.OnShowFilterBottomSheet -> {
|
||||||
viewModelScope.launch {
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
bottomSheet = BrowseScreen.FILTER_BOTTOM_SHEET
|
bottomSheet = BrowseScreen.FILTER_BOTTOM_SHEET
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is BrowseEvent.OnDismissBottomSheet -> {
|
is BrowseEvent.OnDismissBottomSheet -> {
|
||||||
viewModelScope.launch {
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
bottomSheet = null
|
bottomSheet = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is BrowseEvent.OnShowAddDialog -> {
|
is BrowseEvent.OnShowAddDialog -> {
|
||||||
viewModelScope.launch {
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
dialog = BrowseScreen.ADD_DIALOG,
|
dialog = BrowseScreen.ADD_DIALOG,
|
||||||
|
|
@ -273,62 +238,51 @@ class BrowseModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
getAddDialogBooksJob = launch(Dispatchers.IO) {
|
getAddDialogBooksJob = viewModelScope.launch(Dispatchers.IO) {
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
loadingAddDialog = true
|
loadingAddDialog = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
yield()
|
val books = _state.value.files.mapNotNull { file ->
|
||||||
|
ensureActive()
|
||||||
|
if (!file.selected) return@mapNotNull null
|
||||||
|
SelectableNullableBook(
|
||||||
|
data = getBookFromFileUseCase(
|
||||||
|
File(
|
||||||
|
name = file.name,
|
||||||
|
uri = file.uri.toString(),
|
||||||
|
path = file.path,
|
||||||
|
size = file.size,
|
||||||
|
lastModified = file.lastModified,
|
||||||
|
isDirectory = file.isDirectory
|
||||||
|
)
|
||||||
|
),
|
||||||
|
selected = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val books = mutableListOf<NullableBook>()
|
if (books.isEmpty()) {
|
||||||
_state.value.files
|
_state.update {
|
||||||
.filter { it.selected }
|
it.copy(
|
||||||
.ifEmpty {
|
loadingAddDialog = false,
|
||||||
_state.update {
|
dialog = null
|
||||||
it.copy(
|
|
||||||
loadingAddDialog = false,
|
|
||||||
dialog = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
.forEach {
|
|
||||||
yield()
|
|
||||||
books.add(
|
|
||||||
getBookFromFileUseCase(
|
|
||||||
File(
|
|
||||||
name = it.name,
|
|
||||||
uri = it.uri.toString(),
|
|
||||||
path = it.path,
|
|
||||||
size = it.size,
|
|
||||||
lastModified = it.lastModified,
|
|
||||||
isDirectory = it.isDirectory
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
return@launch
|
||||||
yield()
|
}
|
||||||
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
selectedBooksAddDialog = books.map { book ->
|
selectedBooksAddDialog = books,
|
||||||
SelectableNullableBook(
|
|
||||||
data = book,
|
|
||||||
selected = true
|
|
||||||
)
|
|
||||||
},
|
|
||||||
loadingAddDialog = false
|
loadingAddDialog = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is BrowseEvent.OnDismissAddDialog -> {
|
is BrowseEvent.OnDismissAddDialog -> {
|
||||||
viewModelScope.launch {
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
dialog = null
|
dialog = null
|
||||||
|
|
@ -336,160 +290,94 @@ class BrowseModel @Inject constructor(
|
||||||
}
|
}
|
||||||
getAddDialogBooksJob?.cancel()
|
getAddDialogBooksJob?.cancel()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is BrowseEvent.OnActionAddDialog -> {
|
is BrowseEvent.OnActionAddDialog -> {
|
||||||
viewModelScope.launch {
|
withContext(Dispatchers.Default) {
|
||||||
val booksToInsert = _state.value.selectedBooksAddDialog.mapNotNull {
|
_state.value.selectedBooksAddDialog.mapNotNull {
|
||||||
if (it.data is NullableBook.NotNull && it.selected) {
|
if (it.data is NullableBook.NotNull && it.selected) return@mapNotNull it.data.bookWithCover
|
||||||
return@mapNotNull it.data
|
return@mapNotNull null
|
||||||
}
|
}.ifEmpty { return@withContext }.forEach { bookWithCover ->
|
||||||
return@mapNotNull null
|
|
||||||
}.ifEmpty { return@launch }
|
|
||||||
|
|
||||||
for (book in booksToInsert) {
|
|
||||||
book.bookWithCover?.let {
|
|
||||||
addBookUseCase(
|
addBookUseCase(
|
||||||
it.book,
|
bookWithCover.book,
|
||||||
it.coverImage
|
bookWithCover.coverImage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LibraryScreen.refreshListChannel.trySend(0)
|
||||||
|
LibraryScreen.scrollToPageCompositionChannel.trySend(0)
|
||||||
|
|
||||||
|
_effects.emit(BrowseEffect.OnNavigateToLibrary)
|
||||||
|
_effects.emit(BrowseEffect.OnBooksAdded)
|
||||||
|
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
dialog = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onEvent(
|
||||||
|
BrowseEvent.OnRefreshList(
|
||||||
|
loading = false,
|
||||||
|
hideSearch = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
onEvent(BrowseEvent.OnClearSelectedFiles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is BrowseEvent.OnSelectAddDialog -> {
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
val index = _state.value.selectedBooksAddDialog.indexOf(event.book)
|
||||||
|
if (index == -1) return@withContext
|
||||||
|
|
||||||
|
val editedList = _state.value.selectedBooksAddDialog.toMutableList()
|
||||||
|
editedList[index] = editedList[index].let {
|
||||||
|
it.copy(
|
||||||
|
selected = !it.selected
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!editedList
|
||||||
|
.filter { it.data is NullableBook.NotNull }
|
||||||
|
.any { it.selected }
|
||||||
|
) {
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
selectedBooksAddDialog = editedList
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LibraryScreen.refreshListChannel.trySend(0)
|
|
||||||
LibraryScreen.scrollToPageCompositionChannel.trySend(0)
|
|
||||||
|
|
||||||
event.navigateToLibrary()
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
event.context
|
|
||||||
.getString(R.string.books_added)
|
|
||||||
.showToast(context = event.context)
|
|
||||||
}
|
|
||||||
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
dialog = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onEvent(
|
|
||||||
BrowseEvent.OnRefreshList(
|
|
||||||
loading = false,
|
|
||||||
hideSearch = false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
onEvent(BrowseEvent.OnClearSelectedFiles)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is BrowseEvent.OnSelectAddDialog -> {
|
is BrowseEvent.OnDismissDialog -> {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
val index = _state.value.selectedBooksAddDialog.indexOf(event.book).apply {
|
|
||||||
if (this == -1) {
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val editedList = _state.value.selectedBooksAddDialog.toMutableList()
|
|
||||||
editedList[index] = editedList[index].run { copy(selected = !selected) }
|
|
||||||
|
|
||||||
if (!editedList.filter { it.data is NullableBook.NotNull }
|
|
||||||
.any { it.selected }) {
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
selectedBooksAddDialog = editedList
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is BrowseEvent.OnDismissDialog -> {
|
|
||||||
viewModelScope.launch {
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
dialog = null
|
dialog = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getFilesFromDownloads(
|
is BrowseEvent.OnUpdatePinnedPaths -> {
|
||||||
query: String = if (_state.value.showSearch) _state.value.searchQuery else ""
|
_effects.emit(BrowseEffect.OnUpdatePinnedPaths(event.path))
|
||||||
) {
|
}
|
||||||
searchFilesUseCase(query).apply {
|
|
||||||
yield()
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
files = map {
|
|
||||||
SelectableFile(
|
|
||||||
name = it.name,
|
|
||||||
uri = it.uri.toUri(),
|
|
||||||
path = it.path,
|
|
||||||
size = it.size,
|
|
||||||
lastModified = it.lastModified,
|
|
||||||
isDirectory = it.isDirectory,
|
|
||||||
selected = false
|
|
||||||
)
|
|
||||||
},
|
|
||||||
hasSelectedItems = false,
|
|
||||||
isLoading = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun filterList(
|
is BrowseEvent.OnNavigateToLibrary -> {
|
||||||
files: List<SelectableFile>,
|
_effects.emit(BrowseEffect.OnNavigateToLibrary)
|
||||||
sortOrderDescending: Boolean,
|
}
|
||||||
includedFilterItems: List<String>,
|
|
||||||
sortOrder: BrowseSortOrder
|
|
||||||
): List<SelectableFile> {
|
|
||||||
fun List<SelectableFile>.filterFiles(): List<SelectableFile> {
|
|
||||||
if (includedFilterItems.isEmpty()) {
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
return filter { file ->
|
is BrowseEvent.OnNavigateToBrowseSettings -> {
|
||||||
includedFilterItems.any {
|
_effects.emit(BrowseEffect.OnNavigateToBrowseSettings)
|
||||||
file.path.endsWith(
|
|
||||||
it, ignoreCase = true
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return files
|
|
||||||
.filterFiles()
|
|
||||||
.sortedWith(
|
|
||||||
compareByWithOrder(sortOrderDescending) {
|
|
||||||
when (sortOrder) {
|
|
||||||
BrowseSortOrder.NAME -> {
|
|
||||||
it.name.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
BrowseSortOrder.FILE_FORMAT -> {
|
|
||||||
it.path.substringAfterLast(".").lowercase().trimEnd()
|
|
||||||
}
|
|
||||||
|
|
||||||
BrowseSortOrder.FILE_SIZE -> {
|
|
||||||
it.size
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
it.lastModified
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,14 +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.core.helpers.toggle
|
|
||||||
import ua.acclorite.book_story.presentation.browse.model.BrowseLayout
|
import ua.acclorite.book_story.presentation.browse.model.BrowseLayout
|
||||||
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.settings.BrowseSettingsScreen
|
|
||||||
import ua.acclorite.book_story.ui.browse.BrowseContent
|
import ua.acclorite.book_story.ui.browse.BrowseContent
|
||||||
|
import ua.acclorite.book_story.ui.browse.BrowseEffects
|
||||||
import ua.acclorite.book_story.ui.common.helpers.LocalSettings
|
import ua.acclorite.book_story.ui.common.helpers.LocalSettings
|
||||||
import ua.acclorite.book_story.ui.navigator.LocalNavigator
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object BrowseScreen : Screen, Parcelable {
|
object BrowseScreen : Screen, Parcelable {
|
||||||
|
|
@ -62,7 +59,6 @@ object BrowseScreen : Screen, Parcelable {
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val navigator = LocalNavigator.current
|
|
||||||
val screenModel = hiltViewModel<BrowseModel>()
|
val screenModel = hiltViewModel<BrowseModel>()
|
||||||
val settings = LocalSettings.current
|
val settings = LocalSettings.current
|
||||||
|
|
||||||
|
|
@ -84,17 +80,16 @@ object BrowseScreen : Screen, Parcelable {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
val files = remember(
|
LaunchedEffect(
|
||||||
state.value.files,
|
|
||||||
settings.browseIncludedFilterItems.value,
|
settings.browseIncludedFilterItems.value,
|
||||||
settings.browseSortOrderDescending.value,
|
settings.browseSortOrderDescending.value,
|
||||||
settings.browseSortOrder.value
|
settings.browseSortOrder.value
|
||||||
) {
|
) {
|
||||||
screenModel.filterList(
|
screenModel.onEvent(
|
||||||
files = state.value.files,
|
BrowseEvent.OnRefreshList(
|
||||||
sortOrderDescending = settings.browseSortOrderDescending.lastValue,
|
loading = false,
|
||||||
includedFilterItems = settings.browseIncludedFilterItems.lastValue,
|
hideSearch = false
|
||||||
sortOrder = settings.browseSortOrder.lastValue
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,8 +123,13 @@ object BrowseScreen : Screen, Parcelable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BrowseEffects(
|
||||||
|
effects = screenModel.effects,
|
||||||
|
focusRequester = focusRequester
|
||||||
|
)
|
||||||
|
|
||||||
BrowseContent(
|
BrowseContent(
|
||||||
files = files,
|
files = state.value.files,
|
||||||
selectedBooksAddDialog = state.value.selectedBooksAddDialog,
|
selectedBooksAddDialog = state.value.selectedBooksAddDialog,
|
||||||
refreshState = refreshState,
|
refreshState = refreshState,
|
||||||
loadingAddDialog = state.value.loadingAddDialog,
|
loadingAddDialog = state.value.loadingAddDialog,
|
||||||
|
|
@ -149,7 +149,7 @@ object BrowseScreen : Screen, Parcelable {
|
||||||
isRefreshing = state.value.isRefreshing,
|
isRefreshing = state.value.isRefreshing,
|
||||||
isLoading = state.value.isLoading,
|
isLoading = state.value.isLoading,
|
||||||
dialogHidden = state.value.dialog == null,
|
dialogHidden = state.value.dialog == null,
|
||||||
filesEmpty = files.isEmpty(),
|
filesEmpty = state.value.files.isEmpty(),
|
||||||
showSearch = state.value.showSearch,
|
showSearch = state.value.showSearch,
|
||||||
searchQuery = state.value.searchQuery,
|
searchQuery = state.value.searchQuery,
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
|
|
@ -159,24 +159,15 @@ object BrowseScreen : Screen, Parcelable {
|
||||||
requestFocus = screenModel::onEvent,
|
requestFocus = screenModel::onEvent,
|
||||||
clearSelectedFiles = screenModel::onEvent,
|
clearSelectedFiles = screenModel::onEvent,
|
||||||
selectFiles = screenModel::onEvent,
|
selectFiles = screenModel::onEvent,
|
||||||
selectFile = screenModel::onEvent,
|
|
||||||
showFilterBottomSheet = screenModel::onEvent,
|
showFilterBottomSheet = screenModel::onEvent,
|
||||||
dismissBottomSheet = screenModel::onEvent,
|
dismissBottomSheet = screenModel::onEvent,
|
||||||
showAddDialog = screenModel::onEvent,
|
showAddDialog = screenModel::onEvent,
|
||||||
dismissAddDialog = screenModel::onEvent,
|
dismissAddDialog = screenModel::onEvent,
|
||||||
selectAddDialog = screenModel::onEvent,
|
selectAddDialog = screenModel::onEvent,
|
||||||
actionAddDialog = screenModel::onEvent,
|
actionAddDialog = screenModel::onEvent,
|
||||||
updatePinnedPaths = {
|
updatePinnedPaths = screenModel::onEvent,
|
||||||
settings.browsePinnedPaths.update(
|
navigateToLibrary = screenModel::onEvent,
|
||||||
settings.browsePinnedPaths.lastValue.toggle(it)
|
navigateToBrowseSettings = screenModel::onEvent
|
||||||
)
|
|
||||||
},
|
|
||||||
navigateToLibrary = {
|
|
||||||
navigator.push(LibraryScreen, saveInBackStack = false)
|
|
||||||
},
|
|
||||||
navigateToBrowseSettings = {
|
|
||||||
navigator.push(BrowseSettingsScreen)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -34,8 +34,7 @@ fun BrowseAddDialog(
|
||||||
selectedBooksAddDialog: List<SelectableNullableBook>,
|
selectedBooksAddDialog: List<SelectableNullableBook>,
|
||||||
dismissAddDialog: (BrowseEvent.OnDismissAddDialog) -> Unit,
|
dismissAddDialog: (BrowseEvent.OnDismissAddDialog) -> Unit,
|
||||||
actionAddDialog: (BrowseEvent.OnActionAddDialog) -> Unit,
|
actionAddDialog: (BrowseEvent.OnActionAddDialog) -> Unit,
|
||||||
selectAddDialog: (BrowseEvent.OnSelectAddDialog) -> Unit,
|
selectAddDialog: (BrowseEvent.OnSelectAddDialog) -> Unit
|
||||||
navigateToLibrary: () -> Unit
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
Dialog(
|
Dialog(
|
||||||
|
|
@ -45,12 +44,7 @@ fun BrowseAddDialog(
|
||||||
actionEnabled = !loadingAddDialog && selectedBooksAddDialog.any { it.data is NullableBook.NotNull },
|
actionEnabled = !loadingAddDialog && selectedBooksAddDialog.any { it.data is NullableBook.NotNull },
|
||||||
onDismiss = { dismissAddDialog(BrowseEvent.OnDismissAddDialog) },
|
onDismiss = { dismissAddDialog(BrowseEvent.OnDismissAddDialog) },
|
||||||
onAction = {
|
onAction = {
|
||||||
actionAddDialog(
|
actionAddDialog(BrowseEvent.OnActionAddDialog)
|
||||||
BrowseEvent.OnActionAddDialog(
|
|
||||||
context = context,
|
|
||||||
navigateToLibrary = navigateToLibrary
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
withContent = true,
|
withContent = true,
|
||||||
items = {
|
items = {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ fun BrowseBackHandler(
|
||||||
showSearch: Boolean,
|
showSearch: Boolean,
|
||||||
searchVisibility: (BrowseEvent.OnSearchVisibility) -> Unit,
|
searchVisibility: (BrowseEvent.OnSearchVisibility) -> Unit,
|
||||||
clearSelectedFiles: (BrowseEvent.OnClearSelectedFiles) -> Unit,
|
clearSelectedFiles: (BrowseEvent.OnClearSelectedFiles) -> Unit,
|
||||||
navigateToLibrary: () -> Unit
|
navigateToLibrary: (BrowseEvent.OnNavigateToLibrary) -> Unit
|
||||||
) {
|
) {
|
||||||
BackHandler {
|
BackHandler {
|
||||||
if (hasSelectedItems) {
|
if (hasSelectedItems) {
|
||||||
|
|
@ -29,6 +29,6 @@ fun BrowseBackHandler(
|
||||||
return@BackHandler
|
return@BackHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
navigateToLibrary()
|
navigateToLibrary(BrowseEvent.OnNavigateToLibrary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -52,16 +52,15 @@ fun BrowseContent(
|
||||||
requestFocus: (BrowseEvent.OnRequestFocus) -> Unit,
|
requestFocus: (BrowseEvent.OnRequestFocus) -> Unit,
|
||||||
clearSelectedFiles: (BrowseEvent.OnClearSelectedFiles) -> Unit,
|
clearSelectedFiles: (BrowseEvent.OnClearSelectedFiles) -> Unit,
|
||||||
selectFiles: (BrowseEvent.OnSelectFiles) -> Unit,
|
selectFiles: (BrowseEvent.OnSelectFiles) -> Unit,
|
||||||
selectFile: (BrowseEvent.OnSelectFile) -> Unit,
|
|
||||||
dismissBottomSheet: (BrowseEvent.OnDismissBottomSheet) -> Unit,
|
dismissBottomSheet: (BrowseEvent.OnDismissBottomSheet) -> Unit,
|
||||||
showFilterBottomSheet: (BrowseEvent.OnShowFilterBottomSheet) -> Unit,
|
showFilterBottomSheet: (BrowseEvent.OnShowFilterBottomSheet) -> Unit,
|
||||||
showAddDialog: (BrowseEvent.OnShowAddDialog) -> Unit,
|
showAddDialog: (BrowseEvent.OnShowAddDialog) -> Unit,
|
||||||
dismissAddDialog: (BrowseEvent.OnDismissAddDialog) -> Unit,
|
dismissAddDialog: (BrowseEvent.OnDismissAddDialog) -> Unit,
|
||||||
actionAddDialog: (BrowseEvent.OnActionAddDialog) -> Unit,
|
actionAddDialog: (BrowseEvent.OnActionAddDialog) -> Unit,
|
||||||
selectAddDialog: (BrowseEvent.OnSelectAddDialog) -> Unit,
|
selectAddDialog: (BrowseEvent.OnSelectAddDialog) -> Unit,
|
||||||
updatePinnedPaths: (String) -> Unit,
|
updatePinnedPaths: (BrowseEvent.OnUpdatePinnedPaths) -> Unit,
|
||||||
navigateToLibrary: () -> Unit,
|
navigateToLibrary: (BrowseEvent.OnNavigateToLibrary) -> Unit,
|
||||||
navigateToBrowseSettings: () -> Unit,
|
navigateToBrowseSettings: (BrowseEvent.OnNavigateToBrowseSettings) -> Unit
|
||||||
) {
|
) {
|
||||||
BrowseDialog(
|
BrowseDialog(
|
||||||
dialog = dialog,
|
dialog = dialog,
|
||||||
|
|
@ -69,8 +68,7 @@ fun BrowseContent(
|
||||||
actionAddDialog = actionAddDialog,
|
actionAddDialog = actionAddDialog,
|
||||||
dismissAddDialog = dismissAddDialog,
|
dismissAddDialog = dismissAddDialog,
|
||||||
selectedBooksAddDialog = selectedBooksAddDialog,
|
selectedBooksAddDialog = selectedBooksAddDialog,
|
||||||
selectAddDialog = selectAddDialog,
|
selectAddDialog = selectAddDialog
|
||||||
navigateToLibrary = navigateToLibrary
|
|
||||||
)
|
)
|
||||||
|
|
||||||
BrowseBottomSheet(
|
BrowseBottomSheet(
|
||||||
|
|
@ -105,7 +103,6 @@ fun BrowseContent(
|
||||||
selectFiles = selectFiles,
|
selectFiles = selectFiles,
|
||||||
isLoading = isLoading,
|
isLoading = isLoading,
|
||||||
filesEmpty = filesEmpty,
|
filesEmpty = filesEmpty,
|
||||||
selectFile = selectFile,
|
|
||||||
showFilterBottomSheet = showFilterBottomSheet,
|
showFilterBottomSheet = showFilterBottomSheet,
|
||||||
showAddDialog = showAddDialog,
|
showAddDialog = showAddDialog,
|
||||||
updatePinnedPaths = updatePinnedPaths,
|
updatePinnedPaths = updatePinnedPaths,
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,7 @@ fun BrowseDialog(
|
||||||
selectedBooksAddDialog: List<SelectableNullableBook>,
|
selectedBooksAddDialog: List<SelectableNullableBook>,
|
||||||
dismissAddDialog: (BrowseEvent.OnDismissAddDialog) -> Unit,
|
dismissAddDialog: (BrowseEvent.OnDismissAddDialog) -> Unit,
|
||||||
actionAddDialog: (BrowseEvent.OnActionAddDialog) -> Unit,
|
actionAddDialog: (BrowseEvent.OnActionAddDialog) -> Unit,
|
||||||
selectAddDialog: (BrowseEvent.OnSelectAddDialog) -> Unit,
|
selectAddDialog: (BrowseEvent.OnSelectAddDialog) -> Unit
|
||||||
navigateToLibrary: () -> Unit
|
|
||||||
) {
|
) {
|
||||||
when (dialog) {
|
when (dialog) {
|
||||||
BrowseScreen.ADD_DIALOG -> {
|
BrowseScreen.ADD_DIALOG -> {
|
||||||
|
|
@ -29,8 +28,7 @@ fun BrowseDialog(
|
||||||
selectedBooksAddDialog = selectedBooksAddDialog,
|
selectedBooksAddDialog = selectedBooksAddDialog,
|
||||||
dismissAddDialog = dismissAddDialog,
|
dismissAddDialog = dismissAddDialog,
|
||||||
actionAddDialog = actionAddDialog,
|
actionAddDialog = actionAddDialog,
|
||||||
selectAddDialog = selectAddDialog,
|
selectAddDialog = selectAddDialog
|
||||||
navigateToLibrary = navigateToLibrary
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* 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.browse
|
||||||
|
|
||||||
|
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.core.helpers.toggle
|
||||||
|
import ua.acclorite.book_story.presentation.browse.BrowseEffect
|
||||||
|
import ua.acclorite.book_story.presentation.library.LibraryScreen
|
||||||
|
import ua.acclorite.book_story.presentation.settings.BrowseSettingsScreen
|
||||||
|
import ua.acclorite.book_story.ui.common.helpers.LocalSettings
|
||||||
|
import ua.acclorite.book_story.ui.common.helpers.showToast
|
||||||
|
import ua.acclorite.book_story.ui.navigator.LocalNavigator
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BrowseEffects(effects: SharedFlow<BrowseEffect>, focusRequester: FocusRequester) {
|
||||||
|
val navigator = LocalNavigator.current
|
||||||
|
val settings = LocalSettings.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
LaunchedEffect(effects, focusRequester, context, navigator, settings.browsePinnedPaths.value) {
|
||||||
|
effects.collect { effect ->
|
||||||
|
when (effect) {
|
||||||
|
is BrowseEffect.OnRequestFocus -> {
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
is BrowseEffect.OnBooksAdded -> {
|
||||||
|
context.getString(R.string.books_added)
|
||||||
|
.showToast(context = context)
|
||||||
|
}
|
||||||
|
|
||||||
|
is BrowseEffect.OnNavigateToLibrary -> {
|
||||||
|
navigator.push(LibraryScreen, saveInBackStack = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
is BrowseEffect.OnUpdatePinnedPaths -> {
|
||||||
|
settings.browsePinnedPaths.update(
|
||||||
|
settings.browsePinnedPaths.lastValue.toggle(effect.path)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is BrowseEffect.OnNavigateToBrowseSettings -> {
|
||||||
|
navigator.push(BrowseSettingsScreen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,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.browse.BrowseEvent
|
||||||
import ua.acclorite.book_story.ui.common.components.common.AnimatedVisibility
|
import ua.acclorite.book_story.ui.common.components.common.AnimatedVisibility
|
||||||
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
|
||||||
|
|
@ -23,7 +24,7 @@ fun BoxScope.BrowseEmptyPlaceholder(
|
||||||
dialogHidden: Boolean,
|
dialogHidden: Boolean,
|
||||||
isLoading: Boolean,
|
isLoading: Boolean,
|
||||||
isRefreshing: Boolean,
|
isRefreshing: Boolean,
|
||||||
navigateToBrowseSettings: () -> Unit
|
navigateToBrowseSettings: (BrowseEvent.OnNavigateToBrowseSettings) -> Unit
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = !isLoading
|
visible = !isLoading
|
||||||
|
|
@ -39,7 +40,7 @@ fun BoxScope.BrowseEmptyPlaceholder(
|
||||||
icon = painterResource(id = R.drawable.empty_browse),
|
icon = painterResource(id = R.drawable.empty_browse),
|
||||||
actionTitle = stringResource(id = R.string.set_up_scanning),
|
actionTitle = stringResource(id = R.string.set_up_scanning),
|
||||||
action = {
|
action = {
|
||||||
navigateToBrowseSettings()
|
navigateToBrowseSettings(BrowseEvent.OnNavigateToBrowseSettings)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,11 +53,10 @@ fun BrowseScaffold(
|
||||||
requestFocus: (BrowseEvent.OnRequestFocus) -> Unit,
|
requestFocus: (BrowseEvent.OnRequestFocus) -> Unit,
|
||||||
clearSelectedFiles: (BrowseEvent.OnClearSelectedFiles) -> Unit,
|
clearSelectedFiles: (BrowseEvent.OnClearSelectedFiles) -> Unit,
|
||||||
selectFiles: (BrowseEvent.OnSelectFiles) -> Unit,
|
selectFiles: (BrowseEvent.OnSelectFiles) -> Unit,
|
||||||
selectFile: (BrowseEvent.OnSelectFile) -> Unit,
|
|
||||||
showFilterBottomSheet: (BrowseEvent.OnShowFilterBottomSheet) -> Unit,
|
showFilterBottomSheet: (BrowseEvent.OnShowFilterBottomSheet) -> Unit,
|
||||||
showAddDialog: (BrowseEvent.OnShowAddDialog) -> Unit,
|
showAddDialog: (BrowseEvent.OnShowAddDialog) -> Unit,
|
||||||
updatePinnedPaths: (String) -> Unit,
|
updatePinnedPaths: (BrowseEvent.OnUpdatePinnedPaths) -> Unit,
|
||||||
navigateToBrowseSettings: () -> Unit,
|
navigateToBrowseSettings: (BrowseEvent.OnNavigateToBrowseSettings) -> Unit
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -106,7 +105,11 @@ fun BrowseScaffold(
|
||||||
header = header,
|
header = header,
|
||||||
pinned = pinned,
|
pinned = pinned,
|
||||||
pin = {
|
pin = {
|
||||||
updatePinnedPaths(header)
|
updatePinnedPaths(
|
||||||
|
BrowseEvent.OnUpdatePinnedPaths(
|
||||||
|
path = header
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -118,16 +121,14 @@ fun BrowseScaffold(
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
selectFiles(
|
selectFiles(
|
||||||
BrowseEvent.OnSelectFiles(
|
BrowseEvent.OnSelectFiles(
|
||||||
includedFileFormats = includedFilterItems,
|
|
||||||
files = groupFiles
|
files = groupFiles
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
selectFile(
|
selectFiles(
|
||||||
BrowseEvent.OnSelectFile(
|
BrowseEvent.OnSelectFiles(
|
||||||
includedFileFormats = includedFilterItems,
|
files = listOf(file)
|
||||||
file = file
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ fun BrowseTopBar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.focusRequester(focusRequester)
|
.focusRequester(focusRequester)
|
||||||
.onGloballyPositioned {
|
.onGloballyPositioned {
|
||||||
requestFocus(BrowseEvent.OnRequestFocus(focusRequester))
|
requestFocus(BrowseEvent.OnRequestFocus)
|
||||||
},
|
},
|
||||||
initialQuery = searchQuery,
|
initialQuery = searchQuery,
|
||||||
onQueryChange = {
|
onQueryChange = {
|
||||||
|
|
@ -172,7 +172,6 @@ fun BrowseTopBar(
|
||||||
) {
|
) {
|
||||||
selectFiles(
|
selectFiles(
|
||||||
BrowseEvent.OnSelectFiles(
|
BrowseEvent.OnSelectFiles(
|
||||||
includedFileFormats = includedFilterItems,
|
|
||||||
files = files
|
files = files
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue