Refactor search.
Introduce custom DataSource.
This commit is contained in:
parent
978826e6a6
commit
d3d7f89e22
31 changed files with 1101 additions and 385 deletions
|
|
@ -30,6 +30,7 @@ abstract class BibLibDatabase : RoomDatabase() {
|
|||
abstract fun languageDao(): LanguageDao
|
||||
abstract fun seriesDao(): SeriesDao
|
||||
abstract fun crossRefDao(): CrossRefDao
|
||||
abstract fun searchDao(): SearchDao
|
||||
|
||||
companion object {
|
||||
const val VERSION = 1
|
||||
|
|
|
|||
|
|
@ -5,10 +5,14 @@ import androidx.room.Entity
|
|||
import com.pixelized.biblib.database.data.AuthorDbo
|
||||
import com.pixelized.biblib.database.data.BookDbo
|
||||
|
||||
@Entity(primaryKeys = [BookDbo.ID, AuthorDbo.ID])
|
||||
@Entity(tableName = BookAuthorCrossRef.TABLE, primaryKeys = [BookDbo.ID, AuthorDbo.ID])
|
||||
data class BookAuthorCrossRef(
|
||||
@ColumnInfo(name = BookDbo.ID)
|
||||
val bookId: Int,
|
||||
@ColumnInfo(name = AuthorDbo.ID, index = true)
|
||||
val authorId: Int,
|
||||
)
|
||||
) {
|
||||
companion object {
|
||||
const val TABLE = "BookAuthorCrossRef"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,14 @@ import androidx.room.Entity
|
|||
import com.pixelized.biblib.database.data.BookDbo
|
||||
import com.pixelized.biblib.database.data.GenreDbo
|
||||
|
||||
@Entity(primaryKeys = [BookDbo.ID, GenreDbo.ID])
|
||||
@Entity(tableName = BookGenreCrossRef.TABLE, primaryKeys = [BookDbo.ID, GenreDbo.ID])
|
||||
data class BookGenreCrossRef(
|
||||
@ColumnInfo(name = BookDbo.ID)
|
||||
val bookId: Int,
|
||||
@ColumnInfo(name = GenreDbo.ID, index = true)
|
||||
val genreId: Int,
|
||||
)
|
||||
) {
|
||||
companion object {
|
||||
const val TABLE = "BookGenreCrossRef"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.pixelized.biblib.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.RawQuery
|
||||
import androidx.room.Transaction
|
||||
import androidx.sqlite.db.SupportSQLiteQuery
|
||||
import com.pixelized.biblib.database.data.AuthorDbo
|
||||
import com.pixelized.biblib.database.data.GenreDbo
|
||||
import com.pixelized.biblib.database.data.LanguageDbo
|
||||
import com.pixelized.biblib.database.data.SeriesDbo
|
||||
import com.pixelized.biblib.database.relation.BookRelation
|
||||
|
||||
@Dao
|
||||
interface SearchDao {
|
||||
@Transaction
|
||||
@RawQuery
|
||||
fun getBooks(query: SupportSQLiteQuery): List<BookRelation>
|
||||
|
||||
@Transaction
|
||||
@RawQuery
|
||||
fun getAuthors(query: SupportSQLiteQuery): List<AuthorDbo>
|
||||
|
||||
@Transaction
|
||||
@RawQuery
|
||||
fun getSeries(query: SupportSQLiteQuery): List<SeriesDbo>
|
||||
|
||||
@Transaction
|
||||
@RawQuery
|
||||
fun getGenres(query: SupportSQLiteQuery): List<GenreDbo>
|
||||
|
||||
@Transaction
|
||||
@RawQuery
|
||||
fun getLanguages(query: SupportSQLiteQuery): List<LanguageDbo>
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.pixelized.biblib.module
|
|||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.room.PrimaryKey
|
||||
import com.google.gson.Gson
|
||||
import com.pixelized.biblib.database.BibLibDatabase
|
||||
import com.pixelized.biblib.network.client.IBibLibClient
|
||||
|
|
@ -13,6 +14,8 @@ import com.pixelized.biblib.repository.credential.CredentialRepository
|
|||
import com.pixelized.biblib.repository.credential.ICredentialRepository
|
||||
import com.pixelized.biblib.repository.googleSignIn.GoogleSingInRepository
|
||||
import com.pixelized.biblib.repository.googleSignIn.IGoogleSingInRepository
|
||||
import com.pixelized.biblib.repository.search.ISearchRepository
|
||||
import com.pixelized.biblib.repository.search.SearchRepository
|
||||
import com.pixelized.biblib.repository.user.IUserRepository
|
||||
import com.pixelized.biblib.repository.user.UserRepository
|
||||
import dagger.Module
|
||||
|
|
@ -68,6 +71,16 @@ class RepositoryModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSearchRepository(
|
||||
database: BibLibDatabase,
|
||||
): ISearchRepository {
|
||||
return SearchRepository(
|
||||
database = database,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideUserRepository(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
package com.pixelized.biblib.repository.search
|
||||
|
||||
import com.pixelized.biblib.model.book.*
|
||||
|
||||
interface ISearchRepository {
|
||||
|
||||
fun searchBooks(
|
||||
search: String?,
|
||||
authorId: Int?,
|
||||
seriesId: Int?,
|
||||
genreId: Int?,
|
||||
languageId: Int?,
|
||||
limit: Int,
|
||||
offset: Int,
|
||||
): List<Book>
|
||||
|
||||
fun searchAuthor(
|
||||
search: String?,
|
||||
limit: Int,
|
||||
offset: Int,
|
||||
): List<Author>
|
||||
|
||||
fun searchGenre(
|
||||
search: String?,
|
||||
limit: Int,
|
||||
offset: Int,
|
||||
): List<Genre>
|
||||
|
||||
fun searchSeries(
|
||||
search: String?,
|
||||
limit: Int,
|
||||
offset: Int,
|
||||
): List<Series>
|
||||
|
||||
fun searchLanguage(
|
||||
search: String?,
|
||||
limit: Int,
|
||||
offset: Int,
|
||||
): List<Language>
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
package com.pixelized.biblib.repository.search
|
||||
|
||||
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||
import com.pixelized.biblib.database.BibLibDatabase
|
||||
import com.pixelized.biblib.database.crossref.BookAuthorCrossRef
|
||||
import com.pixelized.biblib.database.crossref.BookGenreCrossRef
|
||||
import com.pixelized.biblib.database.data.*
|
||||
import com.pixelized.biblib.database.relation.BookRelation
|
||||
import com.pixelized.biblib.model.book.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class SearchRepository @Inject constructor(
|
||||
private val database: BibLibDatabase
|
||||
) : ISearchRepository {
|
||||
|
||||
override fun searchBooks(
|
||||
search: String?,
|
||||
authorId: Int?,
|
||||
seriesId: Int?,
|
||||
genreId: Int?,
|
||||
languageId: Int?,
|
||||
limit: Int,
|
||||
offset: Int
|
||||
): List<Book> {
|
||||
// build an argument list for the SimpleSQLQuery.
|
||||
val args = mutableListOf<Any>()
|
||||
// build the core of the SQL query.
|
||||
var query = "SELECT ${BookDbo.TABLE}.* FROM ${BookDbo.TABLE}"
|
||||
// add where arguments.
|
||||
query += args.where(argument = search) {
|
||||
BookDbo.run { "$TABLE.$TITLE LIKE '%'||?||'%'" }
|
||||
}
|
||||
query += args.where(argument = authorId) {
|
||||
"${BookDbo.ID} IN (SELECT ${BookDbo.ID} FROM ${BookAuthorCrossRef.TABLE} WHERE ${AuthorDbo.ID} LIKE ?)"
|
||||
}
|
||||
query += args.where(argument = genreId) {
|
||||
"${BookDbo.ID} IN (SELECT ${BookDbo.ID} FROM ${BookGenreCrossRef.TABLE} WHERE ${GenreDbo.ID} LIKE ?)"
|
||||
}
|
||||
query += args.where(argument = seriesId) {
|
||||
BookDbo.run { "$TABLE.$SERIES_ID LIKE ?" }
|
||||
}
|
||||
query += args.where(argument = languageId) {
|
||||
BookDbo.run { "$TABLE.$LANGUAGE_ID LIKE ?" }
|
||||
}
|
||||
// Limit and Offset the query.
|
||||
query += " LIMIT $limit OFFSET $offset;"
|
||||
// compute the query
|
||||
val liteQuery = SimpleSQLiteQuery(query, args.toTypedArray())
|
||||
val result = database.searchDao().getBooks(liteQuery)
|
||||
return result.map { it.toBook() }
|
||||
}
|
||||
|
||||
override fun searchAuthor(search: String?, limit: Int, offset: Int): List<Author> {
|
||||
// build an argument list for the SimpleSQLQuery.
|
||||
val args = mutableListOf<Any>()
|
||||
// build the SQL query.
|
||||
val query = AuthorDbo.run {
|
||||
"SELECT $TABLE.* FROM $TABLE" +
|
||||
args.where(argument = search) { "$TABLE.$NAME LIKE '%'||?||'%'" } +
|
||||
" LIMIT $limit OFFSET $offset;"
|
||||
}
|
||||
// compute the query
|
||||
val liteQuery = SimpleSQLiteQuery(query, args.toTypedArray())
|
||||
val result = database.searchDao().getAuthors(liteQuery)
|
||||
return result.map { it.toAuthor() }
|
||||
}
|
||||
|
||||
override fun searchGenre(search: String?, limit: Int, offset: Int): List<Genre> {
|
||||
// build an argument list for the SimpleSQLQuery.
|
||||
val args = mutableListOf<Any>()
|
||||
// build the SQL query.
|
||||
val query = GenreDbo.run {
|
||||
"SELECT $TABLE.* FROM $TABLE" +
|
||||
args.where(argument = search) { "$TABLE.$NAME LIKE '%'||?||'%'" } +
|
||||
" LIMIT $limit OFFSET $offset;"
|
||||
}
|
||||
// compute the query
|
||||
val liteQuery = SimpleSQLiteQuery(query, args.toTypedArray())
|
||||
val result = database.searchDao().getGenres(liteQuery)
|
||||
return result.map { it.toGenre() }
|
||||
}
|
||||
|
||||
override fun searchSeries(search: String?, limit: Int, offset: Int): List<Series> {
|
||||
// build an argument list for the SimpleSQLQuery.
|
||||
val args = mutableListOf<Any>()
|
||||
// build the SQL query.
|
||||
val query = SeriesDbo.run {
|
||||
"SELECT $TABLE.* FROM $TABLE" +
|
||||
args.where(argument = search) { "$TABLE.$NAME LIKE '%'||?||'%'" } +
|
||||
" LIMIT $limit OFFSET $offset;"
|
||||
}
|
||||
// compute the query
|
||||
val liteQuery = SimpleSQLiteQuery(query, args.toTypedArray())
|
||||
val result = database.searchDao().getSeries(liteQuery)
|
||||
return result.map { it.toSeries() }
|
||||
}
|
||||
|
||||
override fun searchLanguage(search: String?, limit: Int, offset: Int): List<Language> {
|
||||
// build an argument list for the SimpleSQLQuery.
|
||||
val args = mutableListOf<Any>()
|
||||
// build the SQL query.
|
||||
val query = LanguageDbo.run {
|
||||
"SELECT $TABLE.* FROM $TABLE" +
|
||||
args.where(argument = search) { "$TABLE.$NAME LIKE '%'||?||'%'" } +
|
||||
" LIMIT $limit OFFSET $offset;"
|
||||
}
|
||||
// compute the query
|
||||
val liteQuery = SimpleSQLiteQuery(query, args.toTypedArray())
|
||||
val result = database.searchDao().getLanguages(liteQuery)
|
||||
return result.map { it.toLanguage() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<Any>.where(
|
||||
argument: Any?,
|
||||
block: () -> String,
|
||||
): String {
|
||||
return if (argument != null) {
|
||||
val prefix = if (isEmpty()) "WHERE" else "AND"
|
||||
add(argument)
|
||||
" $prefix ${block()}"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Duplicate
|
||||
|
||||
private fun BookRelation.toBook(): Book = Book(
|
||||
id = book.id,
|
||||
title = book.title,
|
||||
sort = book.sort,
|
||||
author = authors.map { it.toAuthor() },
|
||||
haveCover = book.haveCover,
|
||||
releaseDate = book.releaseDate,
|
||||
language = language?.toLanguage(),
|
||||
rating = book.rating,
|
||||
genre = genres?.map { it.toGenre() },
|
||||
series = series?.toSeries(),
|
||||
synopsis = book.synopsis,
|
||||
isNew = book.isNew,
|
||||
)
|
||||
|
||||
private fun AuthorDbo.toAuthor() = Author(
|
||||
id = id,
|
||||
name = name,
|
||||
sort = sort,
|
||||
)
|
||||
|
||||
private fun LanguageDbo.toLanguage() = Language(
|
||||
id = id,
|
||||
code = code,
|
||||
)
|
||||
|
||||
private fun GenreDbo.toGenre() = Genre(
|
||||
id = id,
|
||||
name = name,
|
||||
)
|
||||
|
||||
private fun SeriesDbo.toSeries() = Series(
|
||||
id = id,
|
||||
name = name,
|
||||
sort = sort,
|
||||
index = index,
|
||||
)
|
||||
|
|
@ -13,7 +13,6 @@ inline fun <reified T> rememberSavableMutableTransitionState(
|
|||
): MutableTransitionState<T> {
|
||||
return rememberSaveable(saver = mutableTransitionStateSaver()) {
|
||||
MutableTransitionState(initialState).apply { this.targetState = targetState }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.pixelized.biblib.ui.scaffold
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ModalBottomSheetLayout
|
||||
|
|
@ -13,15 +12,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.pixelized.biblib.R
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.CategorySearchPage
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.SearchViewModel
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.FilterSearchPage
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.viewModel.BookSearchViewModel
|
||||
import com.pixelized.biblib.ui.theme.color.ShadowPalette
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.Serializable
|
||||
|
||||
val LocalSearchViewModel = staticCompositionLocalOf<SearchViewModel> {
|
||||
val LocalBookSearchViewModel = staticCompositionLocalOf<BookSearchViewModel> {
|
||||
error("SearchViewModel is not ready yet")
|
||||
}
|
||||
val LocalCategorySearchBottomSheetState = staticCompositionLocalOf<SearchBottomSheetState> {
|
||||
|
|
@ -32,11 +28,13 @@ val LocalCategorySearchBottomSheetState = staticCompositionLocalOf<SearchBottomS
|
|||
@Composable
|
||||
fun CategorySearchBottomSheet(
|
||||
state: SearchBottomSheetState = rememberSearchBottomSheetState(),
|
||||
searchViewModel: SearchViewModel = hiltViewModel(),
|
||||
bookSearchViewModel: BookSearchViewModel = hiltViewModel(),
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalSearchViewModel provides searchViewModel,
|
||||
LocalBookSearchViewModel provides bookSearchViewModel,
|
||||
LocalCategorySearchBottomSheetState provides state,
|
||||
) {
|
||||
ModalBottomSheetLayout(
|
||||
|
|
@ -44,27 +42,17 @@ fun CategorySearchBottomSheet(
|
|||
scrimColor = ShadowPalette.scrim,
|
||||
sheetState = state.bottomSheetState,
|
||||
sheetContent = {
|
||||
CategorySearchPage(
|
||||
searchViewModel = searchViewModel,
|
||||
FilterSearchPage(
|
||||
focusRequester = state.focusRequester,
|
||||
filter = state.filter,
|
||||
onClose = {
|
||||
when(state.filter) {
|
||||
is SearchFilter.Author -> searchViewModel.authors.clear()
|
||||
is SearchFilter.Series -> searchViewModel.series.clear()
|
||||
is SearchFilter.Genre -> searchViewModel.genre.clear()
|
||||
is SearchFilter.Language -> searchViewModel.language.clear()
|
||||
null -> Unit
|
||||
}
|
||||
state.collapse()
|
||||
}
|
||||
)
|
||||
},
|
||||
content = content,
|
||||
)
|
||||
|
||||
BackHandler(state.bottomSheetState.isVisible) {
|
||||
state.collapse()
|
||||
scope.launch {
|
||||
state.collapse()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -72,31 +60,27 @@ fun CategorySearchBottomSheet(
|
|||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun rememberSearchBottomSheetState(
|
||||
scope: CoroutineScope = rememberCoroutineScope(),
|
||||
focusRequester: FocusRequester = remember { FocusRequester() },
|
||||
bottomSheetState: ModalBottomSheetState = rememberModalBottomSheetState(
|
||||
initialValue = Hidden,
|
||||
skipHalfExpanded = true,
|
||||
),
|
||||
): SearchBottomSheetState {
|
||||
val filter = rememberSaveable(scope, bottomSheetState) {
|
||||
val filter = rememberSaveable(bottomSheetState) {
|
||||
mutableStateOf<SearchFilter?>(null)
|
||||
}
|
||||
val focusRequester = remember {
|
||||
FocusRequester()
|
||||
return remember(bottomSheetState) {
|
||||
SearchBottomSheetState(
|
||||
bottomSheetState = bottomSheetState,
|
||||
focusRequester = focusRequester,
|
||||
filter = filter,
|
||||
)
|
||||
}
|
||||
val controller = SearchBottomSheetState(
|
||||
scope = scope,
|
||||
bottomSheetState = bottomSheetState,
|
||||
focusRequester = focusRequester,
|
||||
filter = filter,
|
||||
)
|
||||
return remember(scope, bottomSheetState) { controller }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Stable
|
||||
class SearchBottomSheetState constructor(
|
||||
private val scope: CoroutineScope,
|
||||
val bottomSheetState: ModalBottomSheetState,
|
||||
val focusRequester: FocusRequester,
|
||||
filter: MutableState<SearchFilter?>,
|
||||
|
|
@ -104,56 +88,22 @@ class SearchBottomSheetState constructor(
|
|||
var filter: SearchFilter? by filter
|
||||
private set
|
||||
|
||||
fun expandSearch(filter: SearchFilter?) {
|
||||
suspend fun expandSearch(filter: SearchFilter?) {
|
||||
this.filter = filter
|
||||
scope.launch {
|
||||
bottomSheetState.show()
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
bottomSheetState.show()
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
fun collapse() {
|
||||
scope.launch {
|
||||
bottomSheetState.hide()
|
||||
}
|
||||
suspend fun collapse() {
|
||||
bottomSheetState.hide()
|
||||
}
|
||||
}
|
||||
|
||||
sealed class SearchFilter(
|
||||
@StringRes val label: Int,
|
||||
val value: String?,
|
||||
) : Serializable {
|
||||
val isSelected: Boolean get() = value != null
|
||||
|
||||
class Author(
|
||||
value: String? = null,
|
||||
) : SearchFilter(
|
||||
label = R.string.search_filter_author,
|
||||
value = value,
|
||||
)
|
||||
|
||||
class Series(
|
||||
value: String? = null,
|
||||
): SearchFilter(
|
||||
label = R.string.search_filter_serie,
|
||||
value = value,
|
||||
)
|
||||
|
||||
class Genre(
|
||||
value: String? = null,
|
||||
) : SearchFilter(
|
||||
label = R.string.search_filter_genre,
|
||||
value = value,
|
||||
)
|
||||
|
||||
class Language(
|
||||
value: String? = null,
|
||||
) : SearchFilter(
|
||||
label = R.string.search_filter_language,
|
||||
value = value,
|
||||
)
|
||||
|
||||
companion object {
|
||||
val all = listOf(Author(), Genre(), Language())
|
||||
}
|
||||
@Stable
|
||||
@Immutable
|
||||
enum class SearchFilter {
|
||||
Author,
|
||||
Series,
|
||||
Genre,
|
||||
Language;
|
||||
}
|
||||
|
|
@ -13,12 +13,11 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.pixelized.biblib.R
|
||||
import com.pixelized.biblib.ui.composable.StateUio
|
||||
import com.pixelized.biblib.ui.screen.home.detail.BookDetailUio
|
||||
import com.pixelized.biblib.ui.screen.home.detail.BookDetailViewModel
|
||||
import com.pixelized.biblib.ui.screen.home.detail.DetailScreen
|
||||
import com.pixelized.biblib.ui.screen.home.detail.BookDetailUio
|
||||
import com.pixelized.biblib.ui.theme.color.ShadowPalette
|
||||
import com.pixelized.biblib.utils.extention.showToast
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
val LocalDetailBottomSheetState = staticCompositionLocalOf<DetailBottomSheetState> {
|
||||
|
|
@ -31,6 +30,8 @@ fun DetailBottomSheet(
|
|||
bottomDetailState: DetailBottomSheetState = rememberDetailBottomSheetState(),
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalDetailBottomSheetState provides bottomDetailState
|
||||
) {
|
||||
|
|
@ -47,7 +48,9 @@ fun DetailBottomSheet(
|
|||
)
|
||||
|
||||
BackHandler(bottomDetailState.bottomSheetState.isVisible) {
|
||||
bottomDetailState.collapse()
|
||||
scope.launch {
|
||||
bottomDetailState.collapse()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -56,31 +59,28 @@ fun DetailBottomSheet(
|
|||
@Composable
|
||||
fun rememberDetailBottomSheetState(
|
||||
viewModel: BookDetailViewModel = hiltViewModel(),
|
||||
scope: CoroutineScope = rememberCoroutineScope(),
|
||||
bottomSheetState: ModalBottomSheetState = rememberModalBottomSheetState(
|
||||
initialValue = Hidden,
|
||||
skipHalfExpanded = true,
|
||||
),
|
||||
): DetailBottomSheetState {
|
||||
val context: Context = LocalContext.current
|
||||
val detail = rememberSaveable(scope, viewModel, bottomSheetState) {
|
||||
val detail = rememberSaveable(viewModel, bottomSheetState) {
|
||||
mutableStateOf<BookDetailUio?>(null)
|
||||
}
|
||||
val controller = DetailBottomSheetState(
|
||||
context = context,
|
||||
viewModel = viewModel,
|
||||
scope = scope,
|
||||
bottomSheetState = bottomSheetState,
|
||||
bookDetail = detail,
|
||||
)
|
||||
return remember(scope, viewModel, bottomSheetState) { controller }
|
||||
return remember(viewModel, bottomSheetState) { controller }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Stable
|
||||
class DetailBottomSheetState constructor(
|
||||
private val context: Context,
|
||||
private val scope: CoroutineScope,
|
||||
val viewModel: BookDetailViewModel,
|
||||
val bottomSheetState: ModalBottomSheetState,
|
||||
bookDetail: MutableState<BookDetailUio?>,
|
||||
|
|
@ -88,25 +88,21 @@ class DetailBottomSheetState constructor(
|
|||
var bookDetail: BookDetailUio? by bookDetail
|
||||
private set
|
||||
|
||||
fun expandBookDetail(id: Int) {
|
||||
scope.launch {
|
||||
when (val book = viewModel.getDetail(id)) {
|
||||
is StateUio.Failure -> {
|
||||
val mes = book.exception.message ?: context.getString(R.string.error_generic)
|
||||
context.showToast(message = mes)
|
||||
}
|
||||
is StateUio.Success -> {
|
||||
bookDetail = book.value
|
||||
bottomSheetState.show()
|
||||
}
|
||||
else -> Unit
|
||||
suspend fun expandBookDetail(id: Int) {
|
||||
when (val book = viewModel.getDetail(id)) {
|
||||
is StateUio.Failure -> {
|
||||
val mes = book.exception.message ?: context.getString(R.string.error_generic)
|
||||
context.showToast(message = mes)
|
||||
}
|
||||
is StateUio.Success -> {
|
||||
bookDetail = book.value
|
||||
bottomSheetState.show()
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
fun collapse() {
|
||||
scope.launch {
|
||||
bottomSheetState.hide()
|
||||
}
|
||||
suspend fun collapse() {
|
||||
bottomSheetState.hide()
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,6 @@ import com.pixelized.biblib.repository.book.updateBooks
|
|||
import com.pixelized.biblib.repository.credential.ICredentialRepository
|
||||
import com.pixelized.biblib.repository.googleSignIn.IGoogleSingInRepository
|
||||
import com.pixelized.biblib.ui.composable.StateUio
|
||||
import com.pixelized.biblib.utils.exception.BookFetchException
|
||||
import com.pixelized.biblib.utils.exception.MissingGoogleTokenException
|
||||
import com.pixelized.biblib.utils.exception.MissingTokenException
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import com.google.accompanist.pager.HorizontalPager
|
|||
import com.google.accompanist.pager.PagerState
|
||||
import com.google.accompanist.pager.rememberPagerState
|
||||
import com.pixelized.biblib.R
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.Search
|
||||
import com.pixelized.biblib.ui.scaffold.*
|
||||
import com.pixelized.biblib.ui.scaffold.SearchScaffoldState.ContentState
|
||||
import com.pixelized.biblib.ui.screen.home.common.connectivity.ConnectivityHeader
|
||||
|
|
@ -33,6 +32,7 @@ import com.pixelized.biblib.ui.screen.home.page.Page
|
|||
import com.pixelized.biblib.ui.screen.home.page.books.BooksPage
|
||||
import com.pixelized.biblib.ui.screen.home.page.news.NewsPage
|
||||
import com.pixelized.biblib.ui.screen.home.page.profile.ProfilePage
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.Search
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.SearchPage
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
import com.pixelized.biblib.utils.extention.bibLib
|
||||
|
|
@ -62,8 +62,7 @@ fun HomeScreen(
|
|||
modifier = Modifier.statusBarsPadding(),
|
||||
state = searchScaffoldState,
|
||||
topBar = {
|
||||
val viewModel = LocalSearchViewModel.current
|
||||
val search by viewModel.filterFlow.collectAsState(initial = "")
|
||||
val viewModel = LocalBookSearchViewModel.current
|
||||
Search(
|
||||
state = searchScaffoldState,
|
||||
avatar = accountViewModel.avatar,
|
||||
|
|
@ -73,9 +72,9 @@ fun HomeScreen(
|
|||
keyboard?.hide()
|
||||
searchScaffoldState.collapse()
|
||||
},
|
||||
searchValue = search,
|
||||
searchValue = viewModel.search ?: "",
|
||||
onSearchValueChange = {
|
||||
viewModel.filter(criteria = it)
|
||||
viewModel.filterSearch(criteria = it)
|
||||
},
|
||||
onSearchTap = {
|
||||
if (searchScaffoldState.content != ContentState.SEARCH || searchScaffoldState.isCollapsed()) {
|
||||
|
|
@ -145,8 +144,12 @@ fun HomeScreen(
|
|||
enabled = searchScaffoldState.isExpended || bottomSearchState.bottomSheetState.isVisible || bottomDetailState.bottomSheetState.isVisible
|
||||
) {
|
||||
when {
|
||||
bottomSearchState.bottomSheetState.isVisible -> bottomSearchState.collapse()
|
||||
bottomDetailState.bottomSheetState.isVisible -> bottomDetailState.collapse()
|
||||
bottomSearchState.bottomSheetState.isVisible -> scope.launch {
|
||||
bottomSearchState.collapse()
|
||||
}
|
||||
bottomDetailState.bottomSheetState.isVisible -> scope.launch {
|
||||
bottomDetailState.collapse()
|
||||
}
|
||||
searchScaffoldState.isExpended -> searchScaffoldState.collapse()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
|
|
@ -19,6 +20,7 @@ import com.pixelized.biblib.ui.screen.home.common.item.SmallBookThumbnailUio
|
|||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
import com.pixelized.biblib.utils.extention.bibLib
|
||||
import com.pixelized.biblib.utils.extention.navigationBarsHeight
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
@Composable
|
||||
|
|
@ -37,6 +39,7 @@ private fun BooksPageContent(
|
|||
books: LazyPagingItems<SmallBookThumbnailUio>,
|
||||
) {
|
||||
val bottomDetailState = LocalDetailBottomSheetState.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(
|
||||
|
|
@ -53,7 +56,9 @@ private fun BooksPageContent(
|
|||
modifier = Modifier.padding(horizontal = MaterialTheme.bibLib.dimen.thumbnail.padding),
|
||||
thumbnail = thumbnail,
|
||||
onClick = {
|
||||
bottomDetailState.expandBookDetail(id = it.id)
|
||||
scope.launch {
|
||||
bottomDetailState.expandBookDetail(id = it.id)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import androidx.compose.foundation.lazy.grid.GridCells
|
|||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
|
|
@ -20,6 +21,7 @@ import com.pixelized.biblib.ui.screen.home.common.preview.largeBookThumbnailPrev
|
|||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
import com.pixelized.biblib.utils.extention.bibLib
|
||||
import com.pixelized.biblib.utils.extention.navigationBarsHeight
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun NewsPage(
|
||||
|
|
@ -38,6 +40,7 @@ private fun NewsPageContent(
|
|||
books: LazyPagingItems<LargeBookThumbnailUio>,
|
||||
) {
|
||||
val detailBottomSheetState: DetailBottomSheetState = LocalDetailBottomSheetState.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
|
|
@ -56,7 +59,9 @@ private fun NewsPageContent(
|
|||
LargeBookThumbnail(
|
||||
thumbnail = books[index],
|
||||
onClick = {
|
||||
detailBottomSheetState.expandBookDetail(id = it.id)
|
||||
scope.launch {
|
||||
detailBottomSheetState.expandBookDetail(id = it.id)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
|
|
@ -21,64 +20,79 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||
import androidx.paging.PagingData
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.items
|
||||
import com.pixelized.biblib.R
|
||||
import com.pixelized.biblib.ui.scaffold.LocalBookSearchViewModel
|
||||
import com.pixelized.biblib.ui.scaffold.LocalCategorySearchBottomSheetState
|
||||
import com.pixelized.biblib.ui.scaffold.SearchFilter
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.viewModel.*
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
import com.pixelized.biblib.utils.extention.bibLib
|
||||
import com.pixelized.biblib.utils.extention.default
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Stable
|
||||
@Immutable
|
||||
data class FilterUio(
|
||||
val id: Int,
|
||||
val filter: String,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun CategorySearchPage(
|
||||
searchViewModel: SearchViewModel = hiltViewModel(),
|
||||
fun FilterSearchPage(
|
||||
authorFilterViewModel: AuthorFilterViewModel = hiltViewModel(),
|
||||
genreViewModel: GenreFilterViewModel = hiltViewModel(),
|
||||
languageViewModel: LanguageFilterViewModel = hiltViewModel(),
|
||||
seriesFilterViewModel: SeriesFilterViewModel = hiltViewModel(),
|
||||
focusRequester: FocusRequester = FocusRequester(),
|
||||
filter: SearchFilter?,
|
||||
onClose: () -> Unit = default(),
|
||||
) {
|
||||
val bookSearchViewModel = LocalBookSearchViewModel.current
|
||||
val bottomSearchState = LocalCategorySearchBottomSheetState.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val filter = bottomSearchState.filter
|
||||
|
||||
val viewModel: IFilterViewModel? = remember(filter) {
|
||||
when (filter) {
|
||||
SearchFilter.Author -> authorFilterViewModel
|
||||
SearchFilter.Series -> seriesFilterViewModel
|
||||
SearchFilter.Genre -> genreViewModel
|
||||
SearchFilter.Language -> languageViewModel
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
CategorySearchPageContent(
|
||||
focusRequester = focusRequester,
|
||||
filter = filter,
|
||||
onClose = onClose,
|
||||
searchFlow = {
|
||||
when (bottomSearchState.filter) {
|
||||
is SearchFilter.Author -> searchViewModel.authors.filterFlow
|
||||
is SearchFilter.Series -> searchViewModel.series.filterFlow
|
||||
is SearchFilter.Genre -> searchViewModel.genre.filterFlow
|
||||
is SearchFilter.Language -> searchViewModel.language.filterFlow
|
||||
null -> emptyFlow()
|
||||
search = { viewModel?.search },
|
||||
paging = { viewModel?.paging ?: emptyFlow() },
|
||||
onSearchUpdate = {
|
||||
viewModel?.updateSearch(criteria = it)
|
||||
},
|
||||
onClose = {
|
||||
when (filter) {
|
||||
SearchFilter.Author -> bookSearchViewModel.filterAuthor(null)
|
||||
SearchFilter.Series -> bookSearchViewModel.filterSeries(null)
|
||||
SearchFilter.Genre -> bookSearchViewModel.filterGenre(null)
|
||||
SearchFilter.Language -> bookSearchViewModel.filterLanguage(null)
|
||||
else -> Unit
|
||||
}
|
||||
scope.launch {
|
||||
bottomSearchState.collapse()
|
||||
}
|
||||
},
|
||||
dataFlow = {
|
||||
when (bottomSearchState.filter) {
|
||||
is SearchFilter.Author -> searchViewModel.authors.dataFlow
|
||||
is SearchFilter.Series -> searchViewModel.series.dataFlow
|
||||
is SearchFilter.Genre -> searchViewModel.genre.dataFlow
|
||||
is SearchFilter.Language -> searchViewModel.language.dataFlow
|
||||
null -> emptyFlow()
|
||||
onFilter = {
|
||||
when (filter) {
|
||||
SearchFilter.Author -> bookSearchViewModel.filterAuthor(it)
|
||||
SearchFilter.Series -> bookSearchViewModel.filterSeries(it)
|
||||
SearchFilter.Genre -> bookSearchViewModel.filterGenre(it)
|
||||
SearchFilter.Language -> bookSearchViewModel.filterLanguage(it)
|
||||
else -> Unit
|
||||
}
|
||||
},
|
||||
onSearchChange = {
|
||||
when (bottomSearchState.filter) {
|
||||
is SearchFilter.Author -> searchViewModel.authors.filter(it)
|
||||
is SearchFilter.Series -> searchViewModel.series.filter(it)
|
||||
is SearchFilter.Genre -> searchViewModel.genre.filter(it)
|
||||
is SearchFilter.Language -> searchViewModel.language.filter(it)
|
||||
null -> Unit
|
||||
scope.launch {
|
||||
bottomSearchState.collapse()
|
||||
}
|
||||
},
|
||||
onData = {
|
||||
when (bottomSearchState.filter) {
|
||||
is SearchFilter.Author -> searchViewModel.authors.confirm(it)
|
||||
is SearchFilter.Series -> searchViewModel.series.confirm(it)
|
||||
is SearchFilter.Genre -> searchViewModel.genre.confirm(it)
|
||||
is SearchFilter.Language -> searchViewModel.language.confirm(it)
|
||||
null -> Unit
|
||||
}
|
||||
bottomSearchState.collapse()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -87,13 +101,13 @@ fun CategorySearchPage(
|
|||
fun CategorySearchPageContent(
|
||||
focusRequester: FocusRequester = FocusRequester(),
|
||||
filter: SearchFilter?,
|
||||
searchFlow: () -> Flow<String>,
|
||||
dataFlow: () -> Flow<PagingData<SearchViewModel.FilterUio>> = { emptyFlow() },
|
||||
onSearchChange: (String) -> Unit = default<String>(),
|
||||
onData: (SearchViewModel.FilterUio?) -> Unit = default<SearchViewModel.FilterUio?>(),
|
||||
search: () -> String?,
|
||||
paging: () -> Flow<PagingData<FilterUio>> = { emptyFlow() },
|
||||
onSearchUpdate: (String) -> Unit = default<String>(),
|
||||
onFilter: (FilterUio) -> Unit = default<FilterUio>(),
|
||||
onClose: () -> Unit = default(),
|
||||
) {
|
||||
val data = dataFlow().collectAsLazyPagingItems()
|
||||
val data = paging().collectAsLazyPagingItems()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.imePadding()
|
||||
|
|
@ -103,13 +117,17 @@ fun CategorySearchPageContent(
|
|||
backgroundColor = MaterialTheme.colors.surface,
|
||||
elevation = 0.dp,
|
||||
title = {
|
||||
filter?.let {
|
||||
Text(
|
||||
style = MaterialTheme.typography.h6,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
text = stringResource(id = it.label),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
style = MaterialTheme.typography.h6,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
text = when (filter) {
|
||||
SearchFilter.Author -> stringResource(id = R.string.search_filter_author)
|
||||
SearchFilter.Genre -> stringResource(id = R.string.search_filter_genre)
|
||||
SearchFilter.Series -> stringResource(id = R.string.search_filter_serie)
|
||||
SearchFilter.Language -> stringResource(id = R.string.search_filter_language)
|
||||
else -> ""
|
||||
},
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onClose) {
|
||||
|
|
@ -130,14 +148,14 @@ fun CategorySearchPageContent(
|
|||
text = "Rechercher"
|
||||
)
|
||||
},
|
||||
value = searchFlow().collectAsState(initial = "").value,
|
||||
value = search() ?: "",
|
||||
singleLine = true,
|
||||
colors = TextFieldDefaults.outlinedTextFieldColors(
|
||||
focusedBorderColor = Color.Transparent,
|
||||
unfocusedBorderColor = Color.Transparent,
|
||||
disabledBorderColor = Color.Transparent,
|
||||
),
|
||||
onValueChange = onSearchChange
|
||||
onValueChange = onSearchUpdate
|
||||
)
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
|
|
@ -145,12 +163,12 @@ fun CategorySearchPageContent(
|
|||
items(items = data, key = { it.id }) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clickable { onData(it) }
|
||||
.clickable { it?.let { onFilter(it) } }
|
||||
.fillMaxWidth()
|
||||
.padding(all = MaterialTheme.bibLib.dimen.dp16),
|
||||
style = MaterialTheme.typography.body1,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
text = it?.label ?: ""
|
||||
text = it?.filter ?: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -163,8 +181,8 @@ fun CategorySearchPageContent(
|
|||
private fun CategorySearchPageContentPreview() {
|
||||
BibLibTheme {
|
||||
CategorySearchPageContent(
|
||||
filter = SearchFilter.Author(),
|
||||
searchFlow = { flow { "Asimov" } },
|
||||
filter = SearchFilter.Author,
|
||||
search = { "Asimov" },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,24 +10,29 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.items
|
||||
import com.pixelized.biblib.R
|
||||
import com.pixelized.biblib.ui.scaffold.LocalCategorySearchBottomSheetState
|
||||
import com.pixelized.biblib.ui.scaffold.LocalDetailBottomSheetState
|
||||
import com.pixelized.biblib.ui.scaffold.LocalSearchViewModel
|
||||
import com.pixelized.biblib.ui.scaffold.LocalBookSearchViewModel
|
||||
import com.pixelized.biblib.ui.scaffold.SearchFilter
|
||||
import com.pixelized.biblib.ui.screen.home.common.item.MicroBookThumbnail
|
||||
import com.pixelized.biblib.ui.screen.home.common.item.MicroBookThumbnailUio
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.item.SearchFilter
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.item.rememberSearchFilter
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.item.SearchFilterList
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.item.SearchFilterUio
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.item.searchFilterPreviewItems
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.viewModel.BookSearchViewModel
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
import com.pixelized.biblib.utils.extention.bibLib
|
||||
import com.pixelized.biblib.utils.extention.default
|
||||
|
|
@ -35,29 +40,34 @@ import com.pixelized.biblib.utils.extention.isLoading
|
|||
import com.pixelized.biblib.utils.extention.navigationBarsHeight
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun SearchPage(
|
||||
searchViewModel: SearchViewModel = LocalSearchViewModel.current
|
||||
bookSearchViewModel: BookSearchViewModel = LocalBookSearchViewModel.current,
|
||||
) {
|
||||
val keyboard = LocalSoftwareKeyboardController.current
|
||||
val detail = LocalDetailBottomSheetState.current
|
||||
val search = LocalCategorySearchBottomSheetState.current
|
||||
val focus = LocalFocusManager.current
|
||||
val filters = rememberSearchFilter()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
SearchPageContent(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
search = searchViewModel.search,
|
||||
filters = filters,
|
||||
search = bookSearchViewModel.paging,
|
||||
filters = filters(bookSearchViewModel),
|
||||
onFilter = {
|
||||
search.expandSearch(it)
|
||||
scope.launch {
|
||||
search.expandSearch(it.id)
|
||||
}
|
||||
},
|
||||
onDetail = {
|
||||
focus.clearFocus(force = true)
|
||||
keyboard?.hide()
|
||||
detail.expandBookDetail(id = it.id)
|
||||
scope.launch {
|
||||
detail.expandBookDetail(id = it.id)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -65,10 +75,10 @@ fun SearchPage(
|
|||
@Composable
|
||||
private fun SearchPageContent(
|
||||
modifier: Modifier = Modifier,
|
||||
search: Flow<PagingData<MicroBookThumbnailUio>> = emptyFlow(),
|
||||
filters: List<SearchFilter> = SearchFilter.all,
|
||||
onFilter: (filter: SearchFilter) -> Unit = default<SearchFilter>(),
|
||||
onDetail: (item: MicroBookThumbnailUio) -> Unit = default<MicroBookThumbnailUio>()
|
||||
search: Flow<PagingData<MicroBookThumbnailUio>>,
|
||||
filters: List<SearchFilterUio> = emptyList(),
|
||||
onFilter: (filter: SearchFilterUio) -> Unit,
|
||||
onDetail: (item: MicroBookThumbnailUio) -> Unit,
|
||||
) {
|
||||
val items = search.collectAsLazyPagingItems()
|
||||
|
||||
|
|
@ -81,9 +91,8 @@ private fun SearchPageContent(
|
|||
space = MaterialTheme.bibLib.dimen.thumbnail.arrangement
|
||||
),
|
||||
) {
|
||||
item(key = "Search Filter") {
|
||||
SearchFilter(
|
||||
modifier = Modifier.padding(horizontal = MaterialTheme.bibLib.dimen.dp16),
|
||||
item(key = "Search Filters") {
|
||||
SearchFilterList(
|
||||
filters = filters,
|
||||
onFilter = onFilter,
|
||||
)
|
||||
|
|
@ -121,11 +130,43 @@ private fun SearchLoader(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun filters(
|
||||
bookSearchViewModel: BookSearchViewModel,
|
||||
) = listOf(
|
||||
SearchFilterUio(
|
||||
id = SearchFilter.Author,
|
||||
label = stringResource(id = R.string.search_filter_author),
|
||||
value = bookSearchViewModel.author?.filter,
|
||||
),
|
||||
SearchFilterUio(
|
||||
id = SearchFilter.Genre,
|
||||
label = stringResource(id = R.string.search_filter_genre),
|
||||
value = bookSearchViewModel.genre?.filter,
|
||||
),
|
||||
SearchFilterUio(
|
||||
id = SearchFilter.Series,
|
||||
label = stringResource(id = R.string.search_filter_serie),
|
||||
value = bookSearchViewModel.series?.filter,
|
||||
),
|
||||
SearchFilterUio(
|
||||
id = SearchFilter.Language,
|
||||
label = stringResource(id = R.string.search_filter_language),
|
||||
value = bookSearchViewModel.language?.filter,
|
||||
),
|
||||
)
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
|
||||
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
|
||||
private fun SearchPageContentPreview() {
|
||||
BibLibTheme {
|
||||
SearchPageContent()
|
||||
SearchPageContent(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
search = emptyFlow(),
|
||||
filters = searchFilterPreviewItems(),
|
||||
onFilter = default<SearchFilterUio>(),
|
||||
onDetail = default<MicroBookThumbnailUio>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
package com.pixelized.biblib.ui.screen.home.page.search
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.*
|
||||
import com.pixelized.biblib.repository.book.IBookRepository
|
||||
import com.pixelized.biblib.utils.extention.toMicroThumbnailUio
|
||||
import com.pixelized.biblib.utils.extention.toSmallThumbnailUio
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SearchViewModel @Inject constructor(
|
||||
bookRepository: IBookRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
val authors = CategoryFilterManager(
|
||||
scope = viewModelScope,
|
||||
source = bookRepository.getAuthorsSource()
|
||||
.map { FilterUio(it.id, it.name) }
|
||||
.asPagingSourceFactory(Dispatchers.IO),
|
||||
sourceFilter = { data, filter -> data.label.contains(filter, ignoreCase = true) }
|
||||
)
|
||||
|
||||
val series = CategoryFilterManager(
|
||||
scope = viewModelScope,
|
||||
source = bookRepository.getSeriesSource()
|
||||
.map { FilterUio(it.id, it.name) }
|
||||
.asPagingSourceFactory(Dispatchers.IO),
|
||||
sourceFilter = { data, filter -> data.label.contains(filter, ignoreCase = true) }
|
||||
)
|
||||
|
||||
val genre = CategoryFilterManager(
|
||||
scope = viewModelScope,
|
||||
source = bookRepository.getGenresSource()
|
||||
.map { FilterUio(it.id, it.name) }
|
||||
.asPagingSourceFactory(Dispatchers.IO),
|
||||
sourceFilter = { data, filter -> data.label.contains(filter, ignoreCase = true) }
|
||||
)
|
||||
|
||||
val language = CategoryFilterManager(
|
||||
scope = viewModelScope,
|
||||
source = bookRepository.getLanguagesSource()
|
||||
.map { FilterUio(it.id, it.displayLanguage) }
|
||||
.asPagingSourceFactory(Dispatchers.IO),
|
||||
sourceFilter = { data, filter -> data.label.contains(filter, ignoreCase = true) }
|
||||
)
|
||||
|
||||
private val _filterFlow = MutableStateFlow(value = "")
|
||||
val filterFlow: Flow<String> get() = _filterFlow
|
||||
|
||||
fun filter(criteria: String) {
|
||||
viewModelScope.launch { _filterFlow.emit(criteria) }
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
viewModelScope.launch { _filterFlow.emit("") }
|
||||
}
|
||||
|
||||
val search = Pager(
|
||||
config = PagingConfig(pageSize = SEARCH_PAGE_SIZE),
|
||||
pagingSourceFactory = bookRepository.getBooksSource()
|
||||
.asPagingSourceFactory(Dispatchers.IO)
|
||||
).flow.cachedIn(viewModelScope)
|
||||
.combine(filterFlow) { paging, filter ->
|
||||
paging.filter { it.title.contains(filter, ignoreCase = true) }
|
||||
}
|
||||
.combine(authors.confirmFlow) { paging, filter ->
|
||||
paging.filter { filter == null || it.author.any { author -> author.id == filter.id } }
|
||||
}
|
||||
.combine(series.confirmFlow) { paging, filter ->
|
||||
paging.filter { filter == null || it.series?.id == filter.id }
|
||||
}
|
||||
.combine(genre.confirmFlow) { paging, filter ->
|
||||
paging.filter { filter == null || it.genre?.any { author -> author.id == filter.id } ?: false}
|
||||
}
|
||||
.combine(language.confirmFlow) { paging, filter ->
|
||||
paging.filter { filter == null || it.language?.id == filter.id }
|
||||
}
|
||||
.map { paging -> paging.map { it.toMicroThumbnailUio() } }
|
||||
|
||||
data class FilterUio(
|
||||
val id: Int,
|
||||
val label: String,
|
||||
)
|
||||
|
||||
class CategoryFilterManager<T : Any>(
|
||||
private val scope: CoroutineScope,
|
||||
source: () -> PagingSource<Int, T>,
|
||||
sourceFilter: (T, String) -> Boolean,
|
||||
) {
|
||||
private val _filterFlow = MutableStateFlow(value = "")
|
||||
val filterFlow: Flow<String> get() = _filterFlow
|
||||
|
||||
private val _confirmFlow = MutableStateFlow<T?>(value = null)
|
||||
val confirmFlow: Flow<T?> get() = _confirmFlow
|
||||
|
||||
val dataFlow: Flow<PagingData<T>> = Pager(
|
||||
config = PagingConfig(pageSize = 30),
|
||||
pagingSourceFactory = source,
|
||||
).flow.cachedIn(scope).combine(this._filterFlow) { paging, filter ->
|
||||
paging.filter { sourceFilter(it, filter) }
|
||||
}
|
||||
|
||||
var search: T? by mutableStateOf(null)
|
||||
private set
|
||||
|
||||
fun filter(criteria: String) {
|
||||
scope.launch { _filterFlow.emit(criteria) }
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
search = null
|
||||
scope.launch { _confirmFlow.emit(null) }
|
||||
}
|
||||
|
||||
fun confirm(criteria: T?) {
|
||||
search = criteria
|
||||
scope.launch { _confirmFlow.emit(criteria) }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SEARCH_PAGE_SIZE = 10
|
||||
}
|
||||
}
|
||||
|
|
@ -2,51 +2,49 @@ package com.pixelized.biblib.ui.screen.home.page.search.item
|
|||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.pixelized.biblib.ui.scaffold.LocalSearchViewModel
|
||||
import com.pixelized.biblib.R
|
||||
import com.pixelized.biblib.ui.scaffold.SearchFilter
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.SearchViewModel
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
import com.pixelized.biblib.utils.extention.bibLib
|
||||
import com.pixelized.biblib.utils.extention.default
|
||||
|
||||
@Stable
|
||||
@Immutable
|
||||
data class SearchFilterUio(
|
||||
val id: SearchFilter,
|
||||
val label: String,
|
||||
val value: String? = null,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun SearchFilter(
|
||||
modifier: Modifier = Modifier,
|
||||
filters: List<SearchFilter> = SearchFilter.all,
|
||||
onFilter: (filter: SearchFilter) -> Unit = default<SearchFilter>(),
|
||||
fun SearchFilterList(
|
||||
filters: List<SearchFilterUio>,
|
||||
onFilter: (filter: SearchFilterUio) -> Unit = default<SearchFilterUio>(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalScroll(rememberScrollState())
|
||||
.then(modifier),
|
||||
.padding(horizontal = MaterialTheme.bibLib.dimen.dp16),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.bibLib.dimen.dp8),
|
||||
) {
|
||||
filters.forEachIndexed { index, filter ->
|
||||
val chipModifier = if (index != filters.lastIndex) {
|
||||
Modifier.padding(end = MaterialTheme.bibLib.dimen.dp8)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
SearchChipFilter(
|
||||
modifier = chipModifier,
|
||||
selected = filter.isSelected,
|
||||
label = stringResource(id = filter.label),
|
||||
value = filter.value,
|
||||
filters.forEach { filter ->
|
||||
SearchFilter(
|
||||
uio = filter,
|
||||
onClick = { onFilter(filter) }
|
||||
)
|
||||
}
|
||||
|
|
@ -55,25 +53,23 @@ fun SearchFilter(
|
|||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun SearchChipFilter(
|
||||
private fun SearchFilter(
|
||||
modifier: Modifier = Modifier,
|
||||
label: String,
|
||||
selected: Boolean = false,
|
||||
value: String? = null,
|
||||
uio: SearchFilterUio,
|
||||
onClick: () -> Unit = default(),
|
||||
) {
|
||||
FilterChip(
|
||||
modifier = modifier,
|
||||
selected = selected,
|
||||
selected = uio.value != null,
|
||||
onClick = onClick,
|
||||
) {
|
||||
Text(
|
||||
color = MaterialTheme.bibLib.colors.typography.medium,
|
||||
style = MaterialTheme.typography.caption,
|
||||
text = label
|
||||
text = uio.label,
|
||||
)
|
||||
|
||||
value?.let {
|
||||
uio.value?.let {
|
||||
Text(
|
||||
color = MaterialTheme.bibLib.colors.typography.medium,
|
||||
style = MaterialTheme.typography.caption,
|
||||
|
|
@ -82,7 +78,7 @@ private fun SearchChipFilter(
|
|||
Text(
|
||||
color = MaterialTheme.bibLib.colors.typography.medium,
|
||||
style = MaterialTheme.typography.caption,
|
||||
text = value
|
||||
text = it
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -94,28 +90,34 @@ private fun SearchChipFilter(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberSearchFilter(
|
||||
searchViewModel: SearchViewModel = LocalSearchViewModel.current
|
||||
): List<SearchFilter> {
|
||||
val filters by remember {
|
||||
derivedStateOf {
|
||||
listOf(
|
||||
SearchFilter.Author(value = searchViewModel.authors.search?.label),
|
||||
SearchFilter.Series(value = searchViewModel.series.search?.label),
|
||||
SearchFilter.Genre(value = searchViewModel.genre.search?.label),
|
||||
SearchFilter.Language(value = searchViewModel.language.search?.label)
|
||||
)
|
||||
}
|
||||
}
|
||||
return filters
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun SearchFilterPreview() {
|
||||
BibLibTheme {
|
||||
SearchFilter()
|
||||
SearchFilterList(
|
||||
filters = searchFilterPreviewItems()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun searchFilterPreviewItems() = listOf(
|
||||
SearchFilterUio(
|
||||
id = SearchFilter.Author,
|
||||
label = stringResource(id = R.string.search_filter_author),
|
||||
value = "Asimov",
|
||||
),
|
||||
SearchFilterUio(
|
||||
id = SearchFilter.Genre,
|
||||
label = stringResource(id = R.string.search_filter_genre),
|
||||
),
|
||||
SearchFilterUio(
|
||||
id = SearchFilter.Series,
|
||||
label = stringResource(id = R.string.search_filter_serie),
|
||||
),
|
||||
SearchFilterUio(
|
||||
id = SearchFilter.Language,
|
||||
label = stringResource(id = R.string.search_filter_language),
|
||||
),
|
||||
)
|
||||
|
|
@ -2,6 +2,7 @@ package com.pixelized.biblib.ui.screen.home.page.search.item
|
|||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
|
|
@ -9,28 +10,40 @@ import androidx.compose.material.Text
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.History
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
import com.pixelized.biblib.utils.extention.bibLib
|
||||
|
||||
@Stable
|
||||
@Immutable
|
||||
data class SearchHistoryUio(
|
||||
val label: String,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun SearchHistory() {
|
||||
fun SearchHistory(
|
||||
uio: SearchHistoryUio,
|
||||
) {
|
||||
SearchHistoryContent(
|
||||
label = ""
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = uio.label
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchHistoryContent(
|
||||
modifier: Modifier = Modifier,
|
||||
label : String,
|
||||
label: String,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.padding(all = MaterialTheme.bibLib.dimen.dp16)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.padding(end = MaterialTheme.bibLib.dimen.dp8),
|
||||
tint = MaterialTheme.colors.onSurface,
|
||||
imageVector = Icons.Default.History,
|
||||
contentDescription = null,
|
||||
)
|
||||
|
|
@ -47,6 +60,8 @@ private fun SearchHistoryContent(
|
|||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun SearchHistoryContentPreview() {
|
||||
BibLibTheme {
|
||||
SearchHistoryContentPreview()
|
||||
SearchHistory(
|
||||
uio = SearchHistoryUio("Asimov")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.pixelized.biblib.ui.screen.home.page.search.source
|
||||
|
||||
import android.util.Log
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.pixelized.biblib.model.book.Author
|
||||
import com.pixelized.biblib.repository.search.ISearchRepository
|
||||
import com.pixelized.biblib.utils.extention.page
|
||||
|
||||
class AuthorSearchSource(
|
||||
private val searchRepository: ISearchRepository,
|
||||
private val search: String?,
|
||||
private val limit: Int,
|
||||
) : PagingSource<Int, Author>() {
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, Author>): Int? {
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Author> {
|
||||
return try {
|
||||
val index = params.page
|
||||
val page = searchRepository.searchAuthor(
|
||||
search = search,
|
||||
limit = limit,
|
||||
offset = index * limit
|
||||
)
|
||||
LoadResult.Page(
|
||||
data = page,
|
||||
prevKey = if (index == 0) null else index - 1,
|
||||
nextKey = if (page.count() < limit) null else index + 1,
|
||||
)
|
||||
} catch (exception: Exception) {
|
||||
Log.e(this::class.java.simpleName, exception.message, exception)
|
||||
LoadResult.Error(throwable = exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.pixelized.biblib.ui.screen.home.page.search.source
|
||||
|
||||
import android.util.Log
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.pixelized.biblib.model.book.Book
|
||||
import com.pixelized.biblib.repository.search.ISearchRepository
|
||||
import com.pixelized.biblib.utils.extention.page
|
||||
|
||||
class BookSearchSource(
|
||||
private val searchRepository: ISearchRepository,
|
||||
private val search: String?,
|
||||
private val authorId: Int?,
|
||||
private val seriesId: Int?,
|
||||
private val genreId: Int?,
|
||||
private val languageId: Int?,
|
||||
private val limit: Int,
|
||||
) : PagingSource<Int, Book>() {
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, Book>): Int? {
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Book> {
|
||||
return try {
|
||||
val index = params.page
|
||||
val page = searchRepository.searchBooks(
|
||||
search = search,
|
||||
authorId = authorId,
|
||||
seriesId = seriesId,
|
||||
genreId = genreId,
|
||||
languageId = languageId,
|
||||
limit = limit,
|
||||
offset = index * limit
|
||||
)
|
||||
LoadResult.Page(
|
||||
data = page,
|
||||
prevKey = if (index == 0) null else index - 1,
|
||||
nextKey = if (page.count() < limit) null else index + 1,
|
||||
)
|
||||
} catch (exception: Exception) {
|
||||
Log.e(this::class.java.simpleName, exception.message, exception)
|
||||
LoadResult.Error(throwable = exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.pixelized.biblib.ui.screen.home.page.search.source
|
||||
|
||||
import android.util.Log
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.pixelized.biblib.model.book.Author
|
||||
import com.pixelized.biblib.model.book.Genre
|
||||
import com.pixelized.biblib.repository.search.ISearchRepository
|
||||
import com.pixelized.biblib.utils.extention.page
|
||||
|
||||
class GenreSearchSource(
|
||||
private val searchRepository: ISearchRepository,
|
||||
private val search: String?,
|
||||
private val limit: Int,
|
||||
) : PagingSource<Int, Genre>() {
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, Genre>): Int? {
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Genre> {
|
||||
return try {
|
||||
val index = params.page
|
||||
val page = searchRepository.searchGenre(
|
||||
search = search,
|
||||
limit = limit,
|
||||
offset = index * limit
|
||||
)
|
||||
LoadResult.Page(
|
||||
data = page,
|
||||
prevKey = if (index == 0) null else index - 1,
|
||||
nextKey = if (page.count() < limit) null else index + 1,
|
||||
)
|
||||
} catch (exception: Exception) {
|
||||
Log.e(this::class.java.simpleName, exception.message, exception)
|
||||
LoadResult.Error(throwable = exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.pixelized.biblib.ui.screen.home.page.search.source
|
||||
|
||||
import android.util.Log
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.pixelized.biblib.model.book.Language
|
||||
import com.pixelized.biblib.model.book.Series
|
||||
import com.pixelized.biblib.repository.search.ISearchRepository
|
||||
import com.pixelized.biblib.utils.extention.page
|
||||
|
||||
class LanguageSearchSource(
|
||||
private val searchRepository: ISearchRepository,
|
||||
private val search: String?,
|
||||
private val limit: Int,
|
||||
) : PagingSource<Int, Language>() {
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, Language>): Int? {
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Language> {
|
||||
return try {
|
||||
val index = params.page
|
||||
val page = searchRepository.searchLanguage(
|
||||
search = search,
|
||||
limit = limit,
|
||||
offset = index * limit
|
||||
)
|
||||
LoadResult.Page(
|
||||
data = page,
|
||||
prevKey = if (index == 0) null else index - 1,
|
||||
nextKey = if (page.count() < limit) null else index + 1,
|
||||
)
|
||||
} catch (exception: Exception) {
|
||||
Log.e(this::class.java.simpleName, exception.message, exception)
|
||||
LoadResult.Error(throwable = exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.pixelized.biblib.ui.screen.home.page.search.source
|
||||
|
||||
import android.util.Log
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.pixelized.biblib.model.book.Author
|
||||
import com.pixelized.biblib.model.book.Genre
|
||||
import com.pixelized.biblib.model.book.Series
|
||||
import com.pixelized.biblib.repository.search.ISearchRepository
|
||||
import com.pixelized.biblib.utils.extention.page
|
||||
|
||||
class SeriesSearchSource(
|
||||
private val searchRepository: ISearchRepository,
|
||||
private val search: String?,
|
||||
private val limit: Int,
|
||||
) : PagingSource<Int, Series>() {
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, Series>): Int? {
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Series> {
|
||||
return try {
|
||||
val index = params.page
|
||||
val page = searchRepository.searchSeries(
|
||||
search = search,
|
||||
limit = limit,
|
||||
offset = index * limit
|
||||
)
|
||||
LoadResult.Page(
|
||||
data = page,
|
||||
prevKey = if (index == 0) null else index - 1,
|
||||
nextKey = if (page.count() < limit) null else index + 1,
|
||||
)
|
||||
} catch (exception: Exception) {
|
||||
Log.e(this::class.java.simpleName, exception.message, exception)
|
||||
LoadResult.Error(throwable = exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.pixelized.biblib.ui.screen.home.page.search.viewModel
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.*
|
||||
import com.pixelized.biblib.model.book.Author
|
||||
import com.pixelized.biblib.repository.search.ISearchRepository
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.FilterUio
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.source.AuthorSearchSource
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.plus
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AuthorFilterViewModel @Inject constructor(
|
||||
private val searchRepository: ISearchRepository,
|
||||
) : ViewModel(), IFilterViewModel {
|
||||
|
||||
private var source: AuthorSearchSource? = null
|
||||
override val paging: Flow<PagingData<FilterUio>>
|
||||
override var search: String? by mutableStateOf(null)
|
||||
private set
|
||||
|
||||
init {
|
||||
val authorFlow = Pager(
|
||||
config = PagingConfig(
|
||||
pageSize = AUTHOR_FILTER_PAGE_SIZE,
|
||||
enablePlaceholders = true,
|
||||
),
|
||||
pagingSourceFactory = ::buildSource,
|
||||
).flow
|
||||
paging = authorFlow
|
||||
.map { it.map { data -> FilterUio(id = data.id, filter = data.name) } }
|
||||
.cachedIn(viewModelScope + Dispatchers.IO)
|
||||
}
|
||||
|
||||
override fun updateSearch(criteria: String?) {
|
||||
this.search = criteria
|
||||
source?.invalidate()
|
||||
}
|
||||
|
||||
private fun buildSource(): PagingSource<Int, Author> {
|
||||
return AuthorSearchSource(
|
||||
searchRepository = searchRepository,
|
||||
search = search,
|
||||
limit = AUTHOR_FILTER_PAGE_SIZE
|
||||
).also {
|
||||
source = it
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val AUTHOR_FILTER_PAGE_SIZE = 20
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package com.pixelized.biblib.ui.screen.home.page.search.viewModel
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.*
|
||||
import com.pixelized.biblib.model.book.Book
|
||||
import com.pixelized.biblib.repository.search.ISearchRepository
|
||||
import com.pixelized.biblib.ui.screen.home.common.item.MicroBookThumbnailUio
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.FilterUio
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.source.BookSearchSource
|
||||
import com.pixelized.biblib.utils.extention.toMicroThumbnailUio
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.plus
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class BookSearchViewModel @Inject constructor(
|
||||
private val searchRepository: ISearchRepository,
|
||||
) : ViewModel() {
|
||||
private var searchSource: BookSearchSource? = null
|
||||
val paging: Flow<PagingData<MicroBookThumbnailUio>>
|
||||
|
||||
var search: String? by mutableStateOf(null)
|
||||
private set
|
||||
var author: FilterUio? by mutableStateOf(null)
|
||||
private set
|
||||
var series: FilterUio? by mutableStateOf(null)
|
||||
private set
|
||||
var genre: FilterUio? by mutableStateOf(null)
|
||||
private set
|
||||
var language: FilterUio? by mutableStateOf(null)
|
||||
private set
|
||||
|
||||
init {
|
||||
val searchFlow = Pager(
|
||||
config = PagingConfig(
|
||||
pageSize = SEARCH_PAGE_SIZE,
|
||||
enablePlaceholders = true,
|
||||
),
|
||||
pagingSourceFactory = ::buildBookSource,
|
||||
).flow
|
||||
// keep transaction updated with the pager.
|
||||
paging = searchFlow
|
||||
.map { pagingData -> pagingData.map { it.toMicroThumbnailUio() } }
|
||||
.cachedIn(viewModelScope + Dispatchers.IO)
|
||||
}
|
||||
|
||||
fun filterSearch(criteria: String) {
|
||||
this.search = criteria
|
||||
searchSource?.invalidate()
|
||||
}
|
||||
|
||||
fun filterAuthor(criteria: FilterUio?) {
|
||||
author = criteria
|
||||
searchSource?.invalidate()
|
||||
}
|
||||
|
||||
fun filterSeries(criteria: FilterUio?) {
|
||||
series = criteria
|
||||
searchSource?.invalidate()
|
||||
}
|
||||
fun filterGenre(criteria: FilterUio?) {
|
||||
genre = criteria
|
||||
searchSource?.invalidate()
|
||||
}
|
||||
fun filterLanguage(criteria: FilterUio?) {
|
||||
language = criteria
|
||||
searchSource?.invalidate()
|
||||
}
|
||||
|
||||
private fun buildBookSource(): PagingSource<Int, Book> {
|
||||
return BookSearchSource(
|
||||
searchRepository = searchRepository,
|
||||
search = search,
|
||||
authorId = author?.id,
|
||||
seriesId = series?.id,
|
||||
genreId = genre?.id,
|
||||
languageId = language?.id,
|
||||
limit = SEARCH_PAGE_SIZE,
|
||||
).also {
|
||||
searchSource = it
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SEARCH_PAGE_SIZE = 20
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.pixelized.biblib.ui.screen.home.page.search.viewModel
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.*
|
||||
import com.pixelized.biblib.model.book.Genre
|
||||
import com.pixelized.biblib.repository.search.ISearchRepository
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.FilterUio
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.source.GenreSearchSource
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.plus
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class GenreFilterViewModel @Inject constructor(
|
||||
private val searchRepository: ISearchRepository,
|
||||
) : ViewModel(), IFilterViewModel {
|
||||
|
||||
private var source: GenreSearchSource? = null
|
||||
override val paging: Flow<PagingData<FilterUio>>
|
||||
override var search: String? by mutableStateOf(null)
|
||||
private set
|
||||
|
||||
init {
|
||||
val authorFlow = Pager(
|
||||
config = PagingConfig(
|
||||
pageSize = AUTHOR_FILTER_PAGE_SIZE,
|
||||
enablePlaceholders = true,
|
||||
),
|
||||
pagingSourceFactory = ::buildSource,
|
||||
).flow
|
||||
paging = authorFlow
|
||||
.map { it.map { data -> FilterUio(id = data.id, filter = data.name) } }
|
||||
.cachedIn(viewModelScope + Dispatchers.IO)
|
||||
}
|
||||
|
||||
override fun updateSearch(criteria: String?) {
|
||||
this.search = criteria
|
||||
source?.invalidate()
|
||||
}
|
||||
|
||||
private fun buildSource(): PagingSource<Int, Genre> {
|
||||
return GenreSearchSource(
|
||||
searchRepository = searchRepository,
|
||||
search = search,
|
||||
limit = AUTHOR_FILTER_PAGE_SIZE
|
||||
).also {
|
||||
source = it
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val AUTHOR_FILTER_PAGE_SIZE = 20
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.pixelized.biblib.ui.screen.home.page.search.viewModel
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.FilterUio
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface IFilterViewModel {
|
||||
val paging: Flow<PagingData<FilterUio>>
|
||||
val search: String?
|
||||
|
||||
fun updateSearch(criteria: String?)
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.pixelized.biblib.ui.screen.home.page.search.viewModel
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.*
|
||||
import com.pixelized.biblib.model.book.Language
|
||||
import com.pixelized.biblib.repository.search.ISearchRepository
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.FilterUio
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.source.LanguageSearchSource
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.plus
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LanguageFilterViewModel @Inject constructor(
|
||||
private val searchRepository: ISearchRepository,
|
||||
) : ViewModel(), IFilterViewModel {
|
||||
|
||||
private var source: LanguageSearchSource? = null
|
||||
override val paging: Flow<PagingData<FilterUio>>
|
||||
override var search: String? by mutableStateOf(null)
|
||||
private set
|
||||
|
||||
init {
|
||||
val authorFlow = Pager(
|
||||
config = PagingConfig(
|
||||
pageSize = AUTHOR_FILTER_PAGE_SIZE,
|
||||
enablePlaceholders = true,
|
||||
),
|
||||
pagingSourceFactory = ::buildSource,
|
||||
).flow
|
||||
paging = authorFlow
|
||||
.map { it.map { data -> FilterUio(id = data.id, filter = data.displayLanguage) } }
|
||||
.cachedIn(viewModelScope + Dispatchers.IO)
|
||||
}
|
||||
|
||||
override fun updateSearch(criteria: String?) {
|
||||
this.search = criteria
|
||||
source?.invalidate()
|
||||
}
|
||||
|
||||
private fun buildSource(): PagingSource<Int, Language> {
|
||||
return LanguageSearchSource(
|
||||
searchRepository = searchRepository,
|
||||
search = search,
|
||||
limit = AUTHOR_FILTER_PAGE_SIZE
|
||||
).also {
|
||||
source = it
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val AUTHOR_FILTER_PAGE_SIZE = 20
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.pixelized.biblib.ui.screen.home.page.search.viewModel
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.*
|
||||
import com.pixelized.biblib.model.book.Series
|
||||
import com.pixelized.biblib.repository.search.ISearchRepository
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.FilterUio
|
||||
import com.pixelized.biblib.ui.screen.home.page.search.source.SeriesSearchSource
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.plus
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SeriesFilterViewModel @Inject constructor(
|
||||
private val searchRepository: ISearchRepository,
|
||||
) : ViewModel(), IFilterViewModel {
|
||||
|
||||
private var source: SeriesSearchSource? = null
|
||||
override val paging: Flow<PagingData<FilterUio>>
|
||||
override var search: String? by mutableStateOf(null)
|
||||
private set
|
||||
|
||||
init {
|
||||
val authorFlow = Pager(
|
||||
config = PagingConfig(
|
||||
pageSize = AUTHOR_FILTER_PAGE_SIZE,
|
||||
enablePlaceholders = true,
|
||||
),
|
||||
pagingSourceFactory = ::buildSource,
|
||||
).flow
|
||||
paging = authorFlow
|
||||
.map { it.map { data -> FilterUio(id = data.id, filter = data.name) } }
|
||||
.cachedIn(viewModelScope + Dispatchers.IO)
|
||||
}
|
||||
|
||||
override fun updateSearch(criteria: String?) {
|
||||
this.search = criteria
|
||||
source?.invalidate()
|
||||
}
|
||||
|
||||
private fun buildSource(): PagingSource<Int, Series> {
|
||||
return SeriesSearchSource(
|
||||
searchRepository = searchRepository,
|
||||
search = search,
|
||||
limit = AUTHOR_FILTER_PAGE_SIZE
|
||||
).also {
|
||||
source = it
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val AUTHOR_FILTER_PAGE_SIZE = 20
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.pixelized.biblib.utils.extention
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
|
||||
val PagingSource.LoadParams<Int>.page: Int
|
||||
get() = key ?: 0
|
||||
Loading…
Add table
Add a link
Reference in a new issue