Sorting feature.
This commit is contained in:
parent
c3bbaa15f3
commit
bb056c55e7
15 changed files with 679 additions and 184 deletions
|
|
@ -133,6 +133,9 @@ dependencies {
|
|||
implementation "com.github.skydoves:landscapist-glide:1.5.2"
|
||||
kapt 'com.github.bumptech.glide:compiler:4.13.2' // this have to be align with landscapist-glide
|
||||
|
||||
// Reorder element in laylist
|
||||
implementation "org.burnoutcrew.composereorderable:reorderable:0.9.6"
|
||||
|
||||
// Navigation
|
||||
implementation "androidx.navigation:navigation-compose:2.5.3"
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,14 @@ import androidx.compose.runtime.Stable
|
|||
@Stable
|
||||
@Immutable
|
||||
enum class SortType {
|
||||
Book,
|
||||
New,
|
||||
Title,
|
||||
Series,
|
||||
}
|
||||
|
||||
@Stable
|
||||
@Immutable
|
||||
enum class SortValue {
|
||||
ASC,
|
||||
DESC,
|
||||
}
|
||||
|
|
@ -50,8 +50,8 @@ class BookRepository constructor(
|
|||
return bookFactory.fromDetailResponseToBook(response)
|
||||
}
|
||||
|
||||
override suspend fun update(forceRefresh:Boolean) = withContext(Dispatchers.IO) {
|
||||
if (getBookCount() <= 0 || fetchNewBooks() || forceRefresh) {
|
||||
override suspend fun update() = withContext(Dispatchers.IO) {
|
||||
if (fetchNewBooks() || getBookCount() <= 0) {
|
||||
fetchAllBooks()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import com.pixelized.biblib.model.book.*
|
|||
|
||||
interface IBookRepository {
|
||||
|
||||
suspend fun update(forceRefresh: Boolean = false)
|
||||
suspend fun update()
|
||||
|
||||
suspend fun getBookDetail(id: Int): Book
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.pixelized.biblib.repository.search
|
|||
|
||||
import com.pixelized.biblib.model.book.*
|
||||
import com.pixelized.biblib.model.search.SortType
|
||||
import com.pixelized.biblib.model.search.SortValue
|
||||
|
||||
interface ISearchRepository {
|
||||
|
||||
|
|
@ -12,7 +13,7 @@ interface ISearchRepository {
|
|||
seriesId: Int?,
|
||||
genreId: Int?,
|
||||
languageId: Int?,
|
||||
sortBy : SortType,
|
||||
sortBy: Map<SortType, SortValue?>,
|
||||
limit: Int,
|
||||
offset: Int,
|
||||
): List<Book>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.pixelized.biblib.database.data.*
|
|||
import com.pixelized.biblib.database.factory.*
|
||||
import com.pixelized.biblib.model.book.*
|
||||
import com.pixelized.biblib.model.search.SortType
|
||||
import com.pixelized.biblib.model.search.SortValue
|
||||
|
||||
class SearchRepository constructor(
|
||||
private val database: BibLibDatabase
|
||||
|
|
@ -20,12 +21,13 @@ class SearchRepository constructor(
|
|||
seriesId: Int?,
|
||||
genreId: Int?,
|
||||
languageId: Int?,
|
||||
sortBy: SortType,
|
||||
sortBy: Map<SortType, SortValue?>,
|
||||
limit: Int,
|
||||
offset: Int
|
||||
): List<Book> {
|
||||
// build an argument list for the SimpleSQLQuery.
|
||||
val args = mutableListOf<Any>()
|
||||
val sort = mutableListOf<Any>()
|
||||
// build the core of the SQL query.
|
||||
var query = "SELECT ${BookDbo.TABLE}.* FROM ${BookDbo.TABLE}"
|
||||
// add where arguments.
|
||||
|
|
@ -47,9 +49,14 @@ class SearchRepository constructor(
|
|||
query += args.where(argument = languageId) {
|
||||
BookDbo.run { "$TABLE.$LANGUAGE_ID LIKE ?" }
|
||||
}
|
||||
query += when (sortBy) {
|
||||
SortType.Book -> BookDbo.run { " ORDER BY $TABLE.$SORT" }
|
||||
SortType.Series -> BookDbo.run { " ORDER BY $TABLE.$SERIES_INDEX" }
|
||||
sortBy.entries.forEach { (type: SortType, value: SortValue?) ->
|
||||
query += sort.sort(argument = value) {
|
||||
when (type) {
|
||||
SortType.New -> BookDbo.run { "$TABLE.$NEW_ORDER" }
|
||||
SortType.Title -> BookDbo.run { "$TABLE.$SORT" }
|
||||
SortType.Series -> BookDbo.run { "$TABLE.$SERIES_INDEX" }
|
||||
}
|
||||
}
|
||||
}
|
||||
// Limit and Offset the query.
|
||||
query += " LIMIT $limit OFFSET $offset;"
|
||||
|
|
@ -135,4 +142,22 @@ private fun MutableList<Any>.where(
|
|||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<Any>.sort(
|
||||
argument: SortValue?,
|
||||
block: () -> String,
|
||||
): String {
|
||||
return if (argument != null) {
|
||||
val prefix = if (isEmpty()) "ORDER BY" else ","
|
||||
add(argument)
|
||||
" $prefix ${block()} ${argument.order()}"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
private fun SortValue.order(): String = when (this) {
|
||||
SortValue.ASC -> "ASC"
|
||||
SortValue.DESC -> "DESC"
|
||||
}
|
||||
|
|
@ -46,17 +46,17 @@ class FilterBottomSheetState(
|
|||
var type: FilterType by mutableStateOf(value = initialSheet)
|
||||
private set
|
||||
|
||||
var currentALOBottomSheetData by mutableStateOf<FilterBottomSheetData?>(value = null)
|
||||
var currentBottomSheetData by mutableStateOf<FilterBottomSheetData?>(value = null)
|
||||
private set
|
||||
|
||||
suspend fun show(type: FilterType): FilterBottomSheetResult = mutex.withLock {
|
||||
try {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
this.type = type
|
||||
currentALOBottomSheetData = FilterBottomSheetDataImpl(continuation)
|
||||
currentBottomSheetData = FilterBottomSheetDataImpl(continuation)
|
||||
}
|
||||
} finally {
|
||||
currentALOBottomSheetData = null
|
||||
currentBottomSheetData = null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -110,11 +110,11 @@ fun FilterBottomSheet(
|
|||
) {
|
||||
val keyboard = LocalSoftwareKeyboardController.current
|
||||
val focusManager: FocusManager = LocalFocusManager.current
|
||||
val currentALOBottomSheetData = sheetState.currentALOBottomSheetData
|
||||
val currentBottomSheetData = sheetState.currentBottomSheetData
|
||||
|
||||
// Check the state of the currentALOBottomSheetData and show / hide the bottomSheet accordingly.
|
||||
LaunchedEffect(currentALOBottomSheetData) {
|
||||
when (currentALOBottomSheetData) {
|
||||
LaunchedEffect(currentBottomSheetData) {
|
||||
when (currentBottomSheetData) {
|
||||
null -> {
|
||||
bottomSheetState.hide()
|
||||
focusManager.clearFocus(force = true)
|
||||
|
|
@ -127,12 +127,12 @@ fun FilterBottomSheet(
|
|||
}
|
||||
|
||||
if (bottomSheetState.currentValue == Expanded && bottomSheetState.targetValue == Hidden) {
|
||||
currentALOBottomSheetData?.dismiss()
|
||||
currentBottomSheetData?.dismiss()
|
||||
}
|
||||
|
||||
BackHandler(
|
||||
enabled = bottomSheetState.isVisible,
|
||||
onBack = { currentALOBottomSheetData?.dismiss() },
|
||||
onBack = { currentBottomSheetData?.dismiss() },
|
||||
)
|
||||
|
||||
ModalBottomSheetLayout(
|
||||
|
|
@ -155,18 +155,18 @@ fun FilterBottomSheet(
|
|||
viewModel = viewModel,
|
||||
focusRequester = focusRequester,
|
||||
onFilter = {
|
||||
currentALOBottomSheetData?.performAction(filter = it)
|
||||
currentBottomSheetData?.performAction(filter = it)
|
||||
},
|
||||
onClose = {
|
||||
currentALOBottomSheetData?.dismiss()
|
||||
currentBottomSheetData?.dismiss()
|
||||
},
|
||||
onIMEDone = {
|
||||
currentALOBottomSheetData?.dismiss()
|
||||
currentBottomSheetData?.dismiss()
|
||||
}
|
||||
)
|
||||
|
||||
LaunchedEffect(key1 = "FilterPageFocusRequest-${sheetState.type}") {
|
||||
if (currentALOBottomSheetData != null) {
|
||||
if (currentBottomSheetData != null) {
|
||||
focusRequester.requestFocus()
|
||||
keyboard?.show()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
package com.pixelized.biblib.ui.composable.scaffold
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ModalBottomSheetLayout
|
||||
import androidx.compose.material.ModalBottomSheetState
|
||||
import androidx.compose.material.ModalBottomSheetValue.Expanded
|
||||
import androidx.compose.material.ModalBottomSheetValue.Hidden
|
||||
import androidx.compose.material.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.pixelized.biblib.model.search.SortType
|
||||
import com.pixelized.biblib.model.search.SortValue
|
||||
import com.pixelized.biblib.ui.screen.home.sort.BookSortPage
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
val LocalSortBottomSheetState = staticCompositionLocalOf<SortBottomSheetState> {
|
||||
error("LocalSortBottomSheetState not yet ready")
|
||||
}
|
||||
|
||||
@Stable
|
||||
class SortBottomSheetState {
|
||||
private val mutex = Mutex()
|
||||
|
||||
var sortBy by mutableStateOf<Map<SortType, SortValue?>>(emptyMap())
|
||||
private set
|
||||
|
||||
var currentBottomSheetData by mutableStateOf<SortBottomSheetData?>(value = null)
|
||||
private set
|
||||
|
||||
suspend fun show(sortBy: Map<SortType, SortValue?>): SortBottomSheetResult = mutex.withLock {
|
||||
try {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
this.sortBy = sortBy
|
||||
currentBottomSheetData = SortBottomSheetDataImpl(continuation)
|
||||
}
|
||||
} finally {
|
||||
currentBottomSheetData = null
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
private class SortBottomSheetDataImpl(
|
||||
private val continuation: CancellableContinuation<SortBottomSheetResult>
|
||||
) : SortBottomSheetData {
|
||||
override fun performAction(sorting: Map<SortType, SortValue?>) {
|
||||
if (continuation.isActive) continuation.resume(
|
||||
SortBottomSheetResult.ActionPerformed(sortBy = sorting),
|
||||
)
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
if (continuation.isActive) continuation.resume(
|
||||
SortBottomSheetResult.Dismissed,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface SortBottomSheetData {
|
||||
fun performAction(sorting: Map<SortType, SortValue?>)
|
||||
fun dismiss()
|
||||
}
|
||||
|
||||
sealed class SortBottomSheetResult {
|
||||
data class ActionPerformed(val sortBy: Map<SortType, SortValue?>) : SortBottomSheetResult()
|
||||
object Dismissed : SortBottomSheetResult()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberSortBottomSheetState(): SortBottomSheetState = remember {
|
||||
SortBottomSheetState()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun SortBottomSheet(
|
||||
bottomSheetState: ModalBottomSheetState = rememberModalBottomSheetState(
|
||||
initialValue = Hidden,
|
||||
skipHalfExpanded = true,
|
||||
),
|
||||
sheetState: SortBottomSheetState = rememberSortBottomSheetState(),
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalSortBottomSheetState provides sheetState,
|
||||
) {
|
||||
val currentBottomSheetData = sheetState.currentBottomSheetData
|
||||
|
||||
// Check the state of the currentALOBottomSheetData and show / hide the bottomSheet accordingly.
|
||||
LaunchedEffect(currentBottomSheetData) {
|
||||
when (currentBottomSheetData) {
|
||||
null -> bottomSheetState.hide()
|
||||
else -> bottomSheetState.show()
|
||||
}
|
||||
}
|
||||
|
||||
if (bottomSheetState.currentValue == Expanded && bottomSheetState.targetValue == Hidden) {
|
||||
currentBottomSheetData?.dismiss()
|
||||
}
|
||||
|
||||
BackHandler(
|
||||
enabled = bottomSheetState.isVisible,
|
||||
onBack = { currentBottomSheetData?.dismiss() },
|
||||
)
|
||||
|
||||
ModalBottomSheetLayout(
|
||||
sheetState = bottomSheetState,
|
||||
content = content,
|
||||
scrimColor = Color.Transparent,
|
||||
sheetContent = {
|
||||
BookSortPage(
|
||||
initial = sheetState.sortBy,
|
||||
onConfirm = {
|
||||
currentBottomSheetData?.performAction(it)
|
||||
}
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import androidx.paging.PagingSource
|
|||
import androidx.paging.PagingState
|
||||
import com.pixelized.biblib.model.book.Book
|
||||
import com.pixelized.biblib.model.search.SortType
|
||||
import com.pixelized.biblib.model.search.SortValue
|
||||
import com.pixelized.biblib.repository.search.ISearchRepository
|
||||
import com.pixelized.biblib.utils.extention.page
|
||||
|
||||
|
|
@ -15,7 +16,7 @@ class BookSearchSource(
|
|||
private val authorId: Int?,
|
||||
private val seriesId: Int?,
|
||||
private val genreId: Int?,
|
||||
private val sortBy: SortType,
|
||||
private val sortBy: Map<SortType, SortValue?>,
|
||||
private val languageId: Int?,
|
||||
private val limit: Int,
|
||||
) : PagingSource<Int, Book>() {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ import com.pixelized.biblib.R
|
|||
import com.pixelized.biblib.model.book.Book
|
||||
import com.pixelized.biblib.model.search.FilterType
|
||||
import com.pixelized.biblib.model.search.SortType
|
||||
import com.pixelized.biblib.model.search.SortValue
|
||||
import com.pixelized.biblib.repository.book.BookRepository
|
||||
import com.pixelized.biblib.repository.book.IBookRepository
|
||||
import com.pixelized.biblib.repository.search.ISearchRepository
|
||||
import com.pixelized.biblib.ui.screen.home.common.item.LargeBookThumbnailUio
|
||||
import com.pixelized.biblib.ui.screen.home.common.item.MicroBookThumbnailUio
|
||||
|
|
@ -22,6 +25,7 @@ import com.pixelized.biblib.utils.extention.toSmallThumbnailUio
|
|||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.plus
|
||||
import javax.inject.Inject
|
||||
|
|
@ -30,7 +34,15 @@ import javax.inject.Inject
|
|||
class BookSearchViewModel @Inject constructor(
|
||||
application: Application,
|
||||
private val searchRepository: ISearchRepository,
|
||||
private val bookRepository: IBookRepository,
|
||||
) : AndroidViewModel(application) {
|
||||
|
||||
private val _error = MutableSharedFlow<HomeScreenErrorUio.LibraryUpdate>()
|
||||
val error: Flow<HomeScreenErrorUio.LibraryUpdate> = _error
|
||||
|
||||
private val _updating = mutableStateOf(false)
|
||||
val updating: State<Boolean> = _updating
|
||||
|
||||
private var searchSource: BookSearchSource? = null
|
||||
val microPaging: Flow<PagingData<MicroBookThumbnailUio>>
|
||||
val smallPaging: Flow<PagingData<SmallBookThumbnailUio>>
|
||||
|
|
@ -39,67 +51,22 @@ class BookSearchViewModel @Inject constructor(
|
|||
private val _search = mutableStateOf<String?>(null)
|
||||
val search: State<String?> = _search
|
||||
|
||||
private val new = mutableStateOf(
|
||||
FilterChipUio(
|
||||
id = FilterType.New,
|
||||
title = stringResource(R.string.search_filter_new),
|
||||
label = null,
|
||||
selected = true,
|
||||
closable = false,
|
||||
openable = false,
|
||||
)
|
||||
)
|
||||
var sortBy: Map<SortType, SortValue?> = mapOf(SortType.New to SortValue.ASC)
|
||||
private set
|
||||
|
||||
private val newFilter = mutableNewFilterState()
|
||||
private var filterByNew: Boolean? = true
|
||||
|
||||
private val author = mutableStateOf(
|
||||
FilterChipUio(
|
||||
id = FilterType.Author,
|
||||
title = stringResource(R.string.search_filter_author),
|
||||
label = null,
|
||||
selected = false,
|
||||
closable = false,
|
||||
openable = true,
|
||||
)
|
||||
)
|
||||
private val authorFilter = mutableAuthorFilterState()
|
||||
private var filterByAuthorId: Int? = null
|
||||
|
||||
private val series = mutableStateOf(
|
||||
FilterChipUio(
|
||||
id = FilterType.Series,
|
||||
title = stringResource(R.string.search_filter_series),
|
||||
label = null,
|
||||
selected = false,
|
||||
closable = false,
|
||||
openable = true,
|
||||
)
|
||||
)
|
||||
private val seriesFilter = mutableSeriesFilterState()
|
||||
private var filterBySeriesId: Int? = null
|
||||
|
||||
private val genre = mutableStateOf(
|
||||
FilterChipUio(
|
||||
id = FilterType.Genre,
|
||||
title = stringResource(R.string.search_filter_genre),
|
||||
label = null,
|
||||
selected = false,
|
||||
closable = false,
|
||||
openable = true,
|
||||
)
|
||||
)
|
||||
private val genreFilter = mutableGenreFilterState()
|
||||
private var filterByGenreId: Int? = null
|
||||
|
||||
private val language = mutableStateOf(
|
||||
FilterChipUio(
|
||||
id = FilterType.Language,
|
||||
title = stringResource(R.string.search_filter_language),
|
||||
label = null,
|
||||
selected = false,
|
||||
closable = false,
|
||||
openable = true,
|
||||
)
|
||||
)
|
||||
private val languageFilter = mutableLanguageFilterState()
|
||||
private var filterByLanguageId: Int? = null
|
||||
|
||||
val filters: List<State<FilterChipUio>> = listOf(new, author, series, genre, language)
|
||||
val filters: List<State<FilterChipUio>> = listOf(
|
||||
newFilter, authorFilter, seriesFilter, genreFilter, languageFilter
|
||||
)
|
||||
|
||||
init {
|
||||
val searchFlow = Pager(
|
||||
|
|
@ -121,6 +88,28 @@ class BookSearchViewModel @Inject constructor(
|
|||
.cachedIn(viewModelScope + Dispatchers.IO)
|
||||
}
|
||||
|
||||
suspend fun updateLibrary() {
|
||||
try {
|
||||
_updating.value = true
|
||||
bookRepository.update()
|
||||
searchSource?.invalidate()
|
||||
} catch (exception: Exception) {
|
||||
_error.emit(
|
||||
HomeScreenErrorUio.LibraryUpdate(
|
||||
message = R.string.error_book_update_message,
|
||||
action = R.string.error_book_update_action,
|
||||
)
|
||||
)
|
||||
} finally {
|
||||
_updating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun sort(sortBy: Map<SortType, SortValue?>) {
|
||||
this.sortBy = sortBy
|
||||
searchSource?.invalidate()
|
||||
}
|
||||
|
||||
fun filterSearch(criteria: String?) {
|
||||
_search.value = criteria
|
||||
searchSource?.invalidate()
|
||||
|
|
@ -129,7 +118,7 @@ class BookSearchViewModel @Inject constructor(
|
|||
fun filterByNew(criteria: Boolean) {
|
||||
if (filterByNew != criteria) {
|
||||
filterByNew = if (criteria) true else null
|
||||
new.value = new.value.copy(selected = criteria)
|
||||
newFilter.value = newFilter.value.copy(selected = criteria)
|
||||
searchSource?.invalidate()
|
||||
}
|
||||
}
|
||||
|
|
@ -137,10 +126,20 @@ class BookSearchViewModel @Inject constructor(
|
|||
fun filterByAuthor(label: String?, criteria: Int?) {
|
||||
if (filterByAuthorId != criteria) {
|
||||
filterByAuthorId = criteria
|
||||
author.value = if (criteria != null) {
|
||||
author.value.copy(selected = true, label = label, closable = true, openable = false)
|
||||
authorFilter.value = if (criteria != null) {
|
||||
authorFilter.value.copy(
|
||||
selected = true,
|
||||
label = label,
|
||||
closable = true,
|
||||
openable = false
|
||||
)
|
||||
} else {
|
||||
author.value.copy(selected = false, label = null, closable = false, openable = true)
|
||||
authorFilter.value.copy(
|
||||
selected = false,
|
||||
label = null,
|
||||
closable = false,
|
||||
openable = true
|
||||
)
|
||||
}
|
||||
searchSource?.invalidate()
|
||||
}
|
||||
|
|
@ -149,10 +148,20 @@ class BookSearchViewModel @Inject constructor(
|
|||
fun filterBySeries(label: String?, criteria: Int?) {
|
||||
if (filterBySeriesId != criteria) {
|
||||
filterBySeriesId = criteria
|
||||
series.value = if (criteria != null) {
|
||||
series.value.copy(selected = true, label = label, closable = true, openable = false)
|
||||
seriesFilter.value = if (criteria != null) {
|
||||
seriesFilter.value.copy(
|
||||
selected = true,
|
||||
label = label,
|
||||
closable = true,
|
||||
openable = false
|
||||
)
|
||||
} else {
|
||||
series.value.copy(selected = false, label = null, closable = false, openable = true)
|
||||
seriesFilter.value.copy(
|
||||
selected = false,
|
||||
label = null,
|
||||
closable = false,
|
||||
openable = true
|
||||
)
|
||||
}
|
||||
searchSource?.invalidate()
|
||||
}
|
||||
|
|
@ -161,10 +170,20 @@ class BookSearchViewModel @Inject constructor(
|
|||
fun filterByGenre(label: String?, criteria: Int?) {
|
||||
if (filterByGenreId != criteria) {
|
||||
filterByGenreId = criteria
|
||||
genre.value = if (criteria != null) {
|
||||
genre.value.copy(selected = true, label = label, closable = true, openable = false)
|
||||
genreFilter.value = if (criteria != null) {
|
||||
genreFilter.value.copy(
|
||||
selected = true,
|
||||
label = label,
|
||||
closable = true,
|
||||
openable = false
|
||||
)
|
||||
} else {
|
||||
genre.value.copy(selected = false, label = null, closable = false, openable = true)
|
||||
genreFilter.value.copy(
|
||||
selected = false,
|
||||
label = null,
|
||||
closable = false,
|
||||
openable = true
|
||||
)
|
||||
}
|
||||
searchSource?.invalidate()
|
||||
}
|
||||
|
|
@ -173,15 +192,15 @@ class BookSearchViewModel @Inject constructor(
|
|||
fun filterByLanguage(label: String?, criteria: Int?) {
|
||||
if (filterByLanguageId != criteria) {
|
||||
filterByLanguageId = criteria
|
||||
language.value = if (criteria != null) {
|
||||
language.value.copy(
|
||||
languageFilter.value = if (criteria != null) {
|
||||
languageFilter.value.copy(
|
||||
selected = true,
|
||||
label = label,
|
||||
closable = true,
|
||||
openable = false
|
||||
)
|
||||
} else {
|
||||
language.value.copy(
|
||||
languageFilter.value.copy(
|
||||
selected = false,
|
||||
label = null,
|
||||
closable = false,
|
||||
|
|
@ -201,13 +220,68 @@ class BookSearchViewModel @Inject constructor(
|
|||
seriesId = filterBySeriesId,
|
||||
genreId = filterByGenreId,
|
||||
languageId = filterByLanguageId,
|
||||
sortBy = SortType.Book,
|
||||
sortBy = sortBy,
|
||||
limit = SEARCH_PAGE_SIZE,
|
||||
).also {
|
||||
searchSource = it
|
||||
}
|
||||
}
|
||||
|
||||
private fun mutableNewFilterState() = mutableStateOf(
|
||||
FilterChipUio(
|
||||
id = FilterType.New,
|
||||
title = stringResource(R.string.search_filter_new),
|
||||
label = null,
|
||||
selected = true,
|
||||
closable = false,
|
||||
openable = false,
|
||||
)
|
||||
)
|
||||
|
||||
private fun mutableAuthorFilterState() = mutableStateOf(
|
||||
FilterChipUio(
|
||||
id = FilterType.Author,
|
||||
title = stringResource(R.string.search_filter_author),
|
||||
label = null,
|
||||
selected = false,
|
||||
closable = false,
|
||||
openable = true,
|
||||
)
|
||||
)
|
||||
|
||||
private fun mutableSeriesFilterState() = mutableStateOf(
|
||||
FilterChipUio(
|
||||
id = FilterType.Series,
|
||||
title = stringResource(R.string.search_filter_series),
|
||||
label = null,
|
||||
selected = false,
|
||||
closable = false,
|
||||
openable = true,
|
||||
)
|
||||
)
|
||||
|
||||
private fun mutableGenreFilterState() = mutableStateOf(
|
||||
FilterChipUio(
|
||||
id = FilterType.Genre,
|
||||
title = stringResource(R.string.search_filter_genre),
|
||||
label = null,
|
||||
selected = false,
|
||||
closable = false,
|
||||
openable = true,
|
||||
)
|
||||
)
|
||||
|
||||
private fun mutableLanguageFilterState() = mutableStateOf(
|
||||
FilterChipUio(
|
||||
id = FilterType.Language,
|
||||
title = stringResource(R.string.search_filter_language),
|
||||
label = null,
|
||||
selected = false,
|
||||
closable = false,
|
||||
openable = true,
|
||||
)
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val SEARCH_PAGE_SIZE = 20
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
package com.pixelized.biblib.ui.screen.home
|
||||
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.pixelized.biblib.R
|
||||
import com.pixelized.biblib.repository.book.IBookRepository
|
||||
import com.pixelized.biblib.ui.screen.home.HomeScreenErrorUio.LibraryUpdate
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class BookViewModel @Inject constructor(
|
||||
private val bookRepository: IBookRepository,
|
||||
) : ViewModel() {
|
||||
private val _error = MutableSharedFlow<LibraryUpdate>()
|
||||
val error: Flow<LibraryUpdate> = _error
|
||||
|
||||
private val _updating = mutableStateOf(false)
|
||||
val updating: State<Boolean> = _updating
|
||||
|
||||
suspend fun updateLibrary() {
|
||||
try {
|
||||
_updating.value = true
|
||||
bookRepository.update(forceRefresh = true)
|
||||
} catch (exception: Exception) {
|
||||
_error.emit(
|
||||
LibraryUpdate(
|
||||
message = R.string.error_book_update_message,
|
||||
action = R.string.error_book_update_action,
|
||||
)
|
||||
)
|
||||
} finally {
|
||||
_updating.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -73,9 +73,8 @@ sealed class HomeScreenErrorUio(
|
|||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun HomeScreen(
|
||||
bookViewModel: BookViewModel = hiltViewModel(),
|
||||
bookViewModel: BookSearchViewModel = hiltViewModel(),
|
||||
profileViewModel: HomeViewModel = hiltViewModel(),
|
||||
filterViewModel: BookSearchViewModel = hiltViewModel(),
|
||||
optionsViewModel: OptionsViewModel = hiltViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
|
@ -83,6 +82,7 @@ fun HomeScreen(
|
|||
val snackBarHost = LocalSnackHostState.current
|
||||
val detailState: DetailBottomSheetState = rememberDetailBottomSheetState()
|
||||
val filterState: FilterBottomSheetState = rememberFilterBottomSheetState()
|
||||
val sortingState: SortBottomSheetState = rememberSortBottomSheetState()
|
||||
val scope = rememberCoroutineScope()
|
||||
val largeGridState = rememberLazyGridState()
|
||||
val smallListState = rememberLazyListState()
|
||||
|
|
@ -91,58 +91,74 @@ fun HomeScreen(
|
|||
FilterBottomSheet(
|
||||
sheetState = filterState,
|
||||
) {
|
||||
DetailBottomSheet(
|
||||
bottomDetailState = detailState,
|
||||
SortBottomSheet(
|
||||
sheetState = sortingState,
|
||||
) {
|
||||
HomeScreenContent(
|
||||
modifier = Modifier.systemBarsPadding(),
|
||||
avatar = profileViewModel.avatar,
|
||||
search = filterViewModel.search,
|
||||
onSearch = {
|
||||
filterViewModel.filterSearch(criteria = it)
|
||||
},
|
||||
onAvatarTap = {
|
||||
navigation.navigateToProfile()
|
||||
},
|
||||
filters = filterViewModel.filters,
|
||||
onFilter = rememberOnFilter(
|
||||
filterViewModel = filterViewModel,
|
||||
filterState = filterState,
|
||||
),
|
||||
options = optionsViewModel.options,
|
||||
onUp = {
|
||||
scope.launch {
|
||||
launch { largeGridState.animateScrollToItem(index = 0) }
|
||||
launch { smallListState.animateScrollToItem(index = 0) }
|
||||
launch { microListState.animateScrollToItem(index = 0) }
|
||||
}
|
||||
},
|
||||
onSort = { },
|
||||
onMicro = optionsViewModel::onMicro,
|
||||
onSmall = optionsViewModel::onSmall,
|
||||
onLarge = optionsViewModel::onLarge,
|
||||
isLoading = bookViewModel.updating,
|
||||
onPullToRefresh = {
|
||||
scope.launch {
|
||||
bookViewModel.updateLibrary()
|
||||
}
|
||||
},
|
||||
largeGridState = largeGridState,
|
||||
largeGrid = filterViewModel.largePaging,
|
||||
smallListState = smallListState,
|
||||
smallList = filterViewModel.smallPaging,
|
||||
microListState = microListState,
|
||||
microList = filterViewModel.microPaging,
|
||||
onBook = {
|
||||
scope.launch {
|
||||
detailState.expandBookDetail(id = it)
|
||||
}
|
||||
},
|
||||
)
|
||||
DetailBottomSheet(
|
||||
bottomDetailState = detailState,
|
||||
) {
|
||||
HomeScreenContent(
|
||||
modifier = Modifier.systemBarsPadding(),
|
||||
avatar = profileViewModel.avatar,
|
||||
search = bookViewModel.search,
|
||||
onSearch = {
|
||||
bookViewModel.filterSearch(criteria = it)
|
||||
},
|
||||
onAvatarTap = {
|
||||
navigation.navigateToProfile()
|
||||
},
|
||||
filters = bookViewModel.filters,
|
||||
onFilter = rememberOnFilter(
|
||||
filterViewModel = bookViewModel,
|
||||
filterState = filterState,
|
||||
),
|
||||
options = optionsViewModel.options,
|
||||
onUp = {
|
||||
scope.launch {
|
||||
launch { largeGridState.animateScrollToItem(index = 0) }
|
||||
launch { smallListState.animateScrollToItem(index = 0) }
|
||||
launch { microListState.animateScrollToItem(index = 0) }
|
||||
}
|
||||
},
|
||||
onSort = {
|
||||
scope.launch {
|
||||
val result = sortingState.show(sortBy = bookViewModel.sortBy)
|
||||
if (result is SortBottomSheetResult.ActionPerformed) {
|
||||
bookViewModel.sort(sortBy = result.sortBy)
|
||||
}
|
||||
}
|
||||
},
|
||||
onMicro = optionsViewModel::onMicro,
|
||||
onSmall = optionsViewModel::onSmall,
|
||||
onLarge = optionsViewModel::onLarge,
|
||||
isLoading = bookViewModel.updating,
|
||||
onPullToRefresh = {
|
||||
scope.launch {
|
||||
bookViewModel.updateLibrary()
|
||||
}
|
||||
},
|
||||
largeGridState = largeGridState,
|
||||
largeGrid = bookViewModel.largePaging,
|
||||
smallListState = smallListState,
|
||||
smallList = bookViewModel.smallPaging,
|
||||
microListState = microListState,
|
||||
microList = bookViewModel.microPaging,
|
||||
onBook = {
|
||||
scope.launch {
|
||||
detailState.expandBookDetail(id = it)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = "HomeScreenErrorManagement") {
|
||||
LaunchedEffect(key1 = "HomeScreenLaunchedEffect") {
|
||||
// update
|
||||
launch {
|
||||
bookViewModel.updateLibrary()
|
||||
}
|
||||
// error management.
|
||||
launch {
|
||||
bookViewModel.error.collect {
|
||||
val result = snackBarHost.showSnackbar(
|
||||
|
|
@ -292,9 +308,11 @@ private fun HomeScreenContent(
|
|||
)
|
||||
},
|
||||
loader = {
|
||||
Box(modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.height(1.dp)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.height(1.dp)
|
||||
) {
|
||||
Divider()
|
||||
if (isLoading.value) {
|
||||
LinearProgressIndicator(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,209 @@
|
|||
package com.pixelized.biblib.ui.screen.home.sort
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowUpward
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.pixelized.biblib.R
|
||||
import com.pixelized.biblib.model.search.SortType
|
||||
import com.pixelized.biblib.model.search.SortValue
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
import com.pixelized.biblib.utils.extention.bibLib
|
||||
import org.burnoutcrew.reorderable.*
|
||||
|
||||
@Stable
|
||||
@Immutable
|
||||
data class SortUio(
|
||||
val id: SortType,
|
||||
@StringRes val label: Int,
|
||||
val ascending: Boolean? = null,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun BookSortPage(
|
||||
viewModel: BookSortViewModel = hiltViewModel(),
|
||||
initial: Map<SortType, SortValue?>,
|
||||
onConfirm: (Map<SortType, SortValue?>) -> Unit,
|
||||
) {
|
||||
LaunchedEffect(key1 = initial) {
|
||||
viewModel.update(initial)
|
||||
}
|
||||
BookSortPageContent(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.navigationBarsPadding()
|
||||
.padding(vertical = 16.dp),
|
||||
state = rememberReorderableLazyListState(onMove = viewModel::onMove),
|
||||
items = viewModel.items,
|
||||
onSort = viewModel::onSort,
|
||||
onConfirm = { onConfirm(viewModel.onConfirm()) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BookSortPageContent(
|
||||
modifier: Modifier = Modifier,
|
||||
state: ReorderableLazyListState,
|
||||
items: State<List<SortUio>>,
|
||||
onSort: (SortUio) -> Unit,
|
||||
onConfirm: () -> Unit,
|
||||
) {
|
||||
val elevation: ElevationOverlay? = LocalElevationOverlay.current
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
style = MaterialTheme.typography.h6,
|
||||
color = MaterialTheme.bibLib.colors.typography.medium,
|
||||
text = "Sort by:"
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.reorderable(state),
|
||||
state = state.listState,
|
||||
) {
|
||||
items(items = items.value, key = { it.id }) {
|
||||
ReorderableItem(state, key = it.id) { isDragging ->
|
||||
val shadow = animateDpAsState(if (isDragging) 16.dp else 0.dp)
|
||||
val background = elevation?.apply(
|
||||
color = MaterialTheme.colors.surface,
|
||||
elevation = LocalAbsoluteElevation.current + shadow.value,
|
||||
) ?: MaterialTheme.colors.surface
|
||||
|
||||
SortPageItem(
|
||||
modifier = Modifier
|
||||
.fillParentMaxWidth()
|
||||
.height(height = 52.dp)
|
||||
.shadow(elevation = shadow.value)
|
||||
.background(color = background)
|
||||
.clickable { onSort(it) }
|
||||
.padding(horizontal = 16.dp),
|
||||
state = state,
|
||||
label = stringResource(id = it.label),
|
||||
ascending = it.ascending,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
onClick = onConfirm
|
||||
) {
|
||||
Text(text = "Sort")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SortPageItem(
|
||||
modifier: Modifier = Modifier,
|
||||
state: ReorderableLazyListState,
|
||||
label: String,
|
||||
ascending: Boolean?,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Text(
|
||||
style = MaterialTheme.typography.body1,
|
||||
color = MaterialTheme.bibLib.colors.typography.medium,
|
||||
text = label
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = ascending != null,
|
||||
enter = fadeIn() + slideInVertically { it / 2 },
|
||||
exit = fadeOut() + slideOutVertically { it / 2 },
|
||||
) {
|
||||
val angle by animateFloatAsState(
|
||||
targetValue = when (ascending) {
|
||||
true -> 0f
|
||||
else -> 180f
|
||||
}
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier.rotate(degrees = angle),
|
||||
imageVector = Icons.Default.ArrowUpward,
|
||||
tint = MaterialTheme.bibLib.colors.typography.medium,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Icon(
|
||||
modifier = Modifier.detectReorderAfterLongPress(state),
|
||||
imageVector = Icons.Default.Menu,
|
||||
tint = MaterialTheme.bibLib.colors.typography.medium,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun BookSortPagePreview() {
|
||||
BibLibTheme {
|
||||
val items = remember {
|
||||
mutableStateOf(
|
||||
listOf(
|
||||
SortUio(
|
||||
id = SortType.New,
|
||||
label = R.string.search_sort_new,
|
||||
ascending = true,
|
||||
),
|
||||
SortUio(
|
||||
id = SortType.Title,
|
||||
label = R.string.search_sort_title,
|
||||
ascending = false,
|
||||
),
|
||||
SortUio(
|
||||
id = SortType.Series,
|
||||
label = R.string.search_sort_series,
|
||||
ascending = null,
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
BookSortPageContent(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
state = rememberReorderableLazyListState(onMove = { _, _ -> }),
|
||||
items = items,
|
||||
onSort = { },
|
||||
onConfirm = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package com.pixelized.biblib.ui.screen.home.sort
|
||||
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.pixelized.biblib.R
|
||||
import com.pixelized.biblib.model.search.SortType
|
||||
import com.pixelized.biblib.model.search.SortValue
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import org.burnoutcrew.reorderable.ItemPosition
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class BookSortViewModel @Inject constructor() : ViewModel() {
|
||||
val items = mutableStateOf(
|
||||
listOf(
|
||||
SortUio(
|
||||
id = SortType.New,
|
||||
label = R.string.search_sort_new,
|
||||
ascending = true,
|
||||
),
|
||||
SortUio(
|
||||
id = SortType.Title,
|
||||
label = R.string.search_sort_title,
|
||||
ascending = null,
|
||||
),
|
||||
SortUio(
|
||||
id = SortType.Series,
|
||||
label = R.string.search_sort_series,
|
||||
ascending = null,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
fun update(sortBy: Map<SortType, SortValue?>) {
|
||||
items.value = items.value.map { item ->
|
||||
item.copy(ascending = sortBy[item.id]?.let { it == SortValue.ASC })
|
||||
}
|
||||
}
|
||||
|
||||
fun onSort(item: SortUio) {
|
||||
val index = items.value.indexOf(item)
|
||||
items.value = items.value.toMutableList().also {
|
||||
it[index] = item.copy(
|
||||
ascending = when (item.ascending) {
|
||||
true -> false
|
||||
false -> null
|
||||
null -> true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onMove(from: ItemPosition, to: ItemPosition) {
|
||||
items.value = items.value.toMutableList().apply {
|
||||
add(to.index, removeAt(from.index))
|
||||
}
|
||||
}
|
||||
|
||||
fun onConfirm(): Map<SortType, SortValue?> {
|
||||
val data: List<Pair<SortType, SortValue?>> = items.value.map { item ->
|
||||
item.id to item.ascending?.let { if (it) SortValue.ASC else SortValue.DESC }
|
||||
}
|
||||
return mapOf(*data.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
|
@ -85,6 +85,11 @@
|
|||
<string name="search_filter_language">Languages</string>
|
||||
|
||||
<string name="search_sort_by">Sort by: %1$s</string>
|
||||
<string name="search_sort_new">Last addition</string>
|
||||
<string name="search_sort_title">Book title</string>
|
||||
<string name="search_sort_series">Series order</string>
|
||||
<string name="search_sort_asc_action">Ascending</string>
|
||||
<string name="search_sort_dsc_action">Descending</string>
|
||||
|
||||
<string name="profile_title">Welcome</string>
|
||||
<string name="profile_emails">Linked emails:</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue