diff --git a/app/src/main/java/com/pixelized/biblib/database/BibLibDatabase.kt b/app/src/main/java/com/pixelized/biblib/database/BibLibDatabase.kt index 898adf4..2cd3f1c 100644 --- a/app/src/main/java/com/pixelized/biblib/database/BibLibDatabase.kt +++ b/app/src/main/java/com/pixelized/biblib/database/BibLibDatabase.kt @@ -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 diff --git a/app/src/main/java/com/pixelized/biblib/database/crossref/BookAuthorCrossRef.kt b/app/src/main/java/com/pixelized/biblib/database/crossref/BookAuthorCrossRef.kt index 7e0cca8..4088fef 100644 --- a/app/src/main/java/com/pixelized/biblib/database/crossref/BookAuthorCrossRef.kt +++ b/app/src/main/java/com/pixelized/biblib/database/crossref/BookAuthorCrossRef.kt @@ -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" + } +} diff --git a/app/src/main/java/com/pixelized/biblib/database/crossref/BookGenreCrossRef.kt b/app/src/main/java/com/pixelized/biblib/database/crossref/BookGenreCrossRef.kt index 7ae574b..b257ad4 100644 --- a/app/src/main/java/com/pixelized/biblib/database/crossref/BookGenreCrossRef.kt +++ b/app/src/main/java/com/pixelized/biblib/database/crossref/BookGenreCrossRef.kt @@ -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, -) \ No newline at end of file +) { + companion object { + const val TABLE = "BookGenreCrossRef" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/database/dao/SearchDao.kt b/app/src/main/java/com/pixelized/biblib/database/dao/SearchDao.kt new file mode 100644 index 0000000..4debc85 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/database/dao/SearchDao.kt @@ -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 + + @Transaction + @RawQuery + fun getAuthors(query: SupportSQLiteQuery): List + + @Transaction + @RawQuery + fun getSeries(query: SupportSQLiteQuery): List + + @Transaction + @RawQuery + fun getGenres(query: SupportSQLiteQuery): List + + @Transaction + @RawQuery + fun getLanguages(query: SupportSQLiteQuery): List +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/module/RepositoryModule.kt b/app/src/main/java/com/pixelized/biblib/module/RepositoryModule.kt index ad2cba0..17768b5 100644 --- a/app/src/main/java/com/pixelized/biblib/module/RepositoryModule.kt +++ b/app/src/main/java/com/pixelized/biblib/module/RepositoryModule.kt @@ -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( diff --git a/app/src/main/java/com/pixelized/biblib/repository/search/ISearchRepository.kt b/app/src/main/java/com/pixelized/biblib/repository/search/ISearchRepository.kt new file mode 100644 index 0000000..2ddc6f6 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/repository/search/ISearchRepository.kt @@ -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 + + fun searchAuthor( + search: String?, + limit: Int, + offset: Int, + ): List + + fun searchGenre( + search: String?, + limit: Int, + offset: Int, + ): List + + fun searchSeries( + search: String?, + limit: Int, + offset: Int, + ): List + + fun searchLanguage( + search: String?, + limit: Int, + offset: Int, + ): List +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/repository/search/SearchRepository.kt b/app/src/main/java/com/pixelized/biblib/repository/search/SearchRepository.kt new file mode 100644 index 0000000..9da9726 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/repository/search/SearchRepository.kt @@ -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 { + // build an argument list for the SimpleSQLQuery. + val args = mutableListOf() + // 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 { + // build an argument list for the SimpleSQLQuery. + val args = mutableListOf() + // 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 { + // build an argument list for the SimpleSQLQuery. + val args = mutableListOf() + // 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 { + // build an argument list for the SimpleSQLQuery. + val args = mutableListOf() + // 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 { + // build an argument list for the SimpleSQLQuery. + val args = mutableListOf() + // 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.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, +) \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/animation/rememberSavableMutableTransitionState.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/animation/rememberSavableMutableTransitionState.kt index 979002f..917133e 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/composable/animation/rememberSavableMutableTransitionState.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/composable/animation/rememberSavableMutableTransitionState.kt @@ -13,7 +13,6 @@ inline fun rememberSavableMutableTransitionState( ): MutableTransitionState { return rememberSaveable(saver = mutableTransitionStateSaver()) { MutableTransitionState(initialState).apply { this.targetState = targetState } - } } diff --git a/app/src/main/java/com/pixelized/biblib/ui/scaffold/CategorySearchBottomSheet.kt b/app/src/main/java/com/pixelized/biblib/ui/scaffold/CategorySearchBottomSheet.kt index 0da12ae..a808fc7 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/scaffold/CategorySearchBottomSheet.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/scaffold/CategorySearchBottomSheet.kt @@ -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 { +val LocalBookSearchViewModel = staticCompositionLocalOf { error("SearchViewModel is not ready yet") } val LocalCategorySearchBottomSheetState = staticCompositionLocalOf { @@ -32,11 +28,13 @@ val LocalCategorySearchBottomSheetState = staticCompositionLocalOf 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(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, @@ -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; } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/scaffold/DetailBottomSheet.kt b/app/src/main/java/com/pixelized/biblib/ui/scaffold/DetailBottomSheet.kt index 67a9ae6..30bbedd 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/scaffold/DetailBottomSheet.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/scaffold/DetailBottomSheet.kt @@ -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 { @@ -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(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, @@ -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() } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/authentication/viewModel/AuthenticationViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/authentication/viewModel/AuthenticationViewModel.kt index 4cd952b..159767a 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/screen/authentication/viewModel/AuthenticationViewModel.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/authentication/viewModel/AuthenticationViewModel.kt @@ -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 diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/HomeScreen.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/HomeScreen.kt index 10de3da..f77e593 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/screen/home/HomeScreen.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/HomeScreen.kt @@ -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() } } diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/books/BooksPage.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/books/BooksPage.kt index 725eb63..297fd02 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/books/BooksPage.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/books/BooksPage.kt @@ -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, ) { 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) + } }, ) } diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/news/NewsPage.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/news/NewsPage.kt index 76c5e0b..fff6bf6 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/news/NewsPage.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/news/NewsPage.kt @@ -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, ) { 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) + } }, ) } diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/CategorySearchPage.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/FilterSearchPage.kt similarity index 52% rename from app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/CategorySearchPage.kt rename to app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/FilterSearchPage.kt index 733ebdc..21077c7 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/CategorySearchPage.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/FilterSearchPage.kt @@ -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, - dataFlow: () -> Flow> = { emptyFlow() }, - onSearchChange: (String) -> Unit = default(), - onData: (SearchViewModel.FilterUio?) -> Unit = default(), + search: () -> String?, + paging: () -> Flow> = { emptyFlow() }, + onSearchUpdate: (String) -> Unit = default(), + onFilter: (FilterUio) -> Unit = default(), 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" }, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/SearchPage.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/SearchPage.kt index 7b2df1c..de6d2ad 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/SearchPage.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/SearchPage.kt @@ -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> = emptyFlow(), - filters: List = SearchFilter.all, - onFilter: (filter: SearchFilter) -> Unit = default(), - onDetail: (item: MicroBookThumbnailUio) -> Unit = default() + search: Flow>, + filters: List = 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(), + onDetail = default(), + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/SearchViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/SearchViewModel.kt deleted file mode 100644 index ca26857..0000000 --- a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/SearchViewModel.kt +++ /dev/null @@ -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 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( - private val scope: CoroutineScope, - source: () -> PagingSource, - sourceFilter: (T, String) -> Boolean, - ) { - private val _filterFlow = MutableStateFlow(value = "") - val filterFlow: Flow get() = _filterFlow - - private val _confirmFlow = MutableStateFlow(value = null) - val confirmFlow: Flow get() = _confirmFlow - - val dataFlow: Flow> = 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 - } -} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/item/SearchFilter.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/item/SearchFilter.kt index 9841d78..2f0588a 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/item/SearchFilter.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/item/SearchFilter.kt @@ -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.all, - onFilter: (filter: SearchFilter) -> Unit = default(), +fun SearchFilterList( + filters: List, + onFilter: (filter: SearchFilterUio) -> Unit = default(), ) { 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 { - 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() + ) } -} \ No newline at end of file +} + +@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), + ), +) \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/item/SearchHistory.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/item/SearchHistory.kt index 42b1bcb..9725c8b 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/item/SearchHistory.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/item/SearchHistory.kt @@ -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") + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/source/AuthorSearchSource.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/source/AuthorSearchSource.kt new file mode 100644 index 0000000..2a932d6 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/source/AuthorSearchSource.kt @@ -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() { + + override fun getRefreshKey(state: PagingState): Int? { + return null + } + + override suspend fun load(params: LoadParams): LoadResult { + 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/source/BookSearchSource.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/source/BookSearchSource.kt new file mode 100644 index 0000000..50a08ad --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/source/BookSearchSource.kt @@ -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() { + + override fun getRefreshKey(state: PagingState): Int? { + return null + } + + override suspend fun load(params: LoadParams): LoadResult { + 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/source/GenreSearchSource.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/source/GenreSearchSource.kt new file mode 100644 index 0000000..580d098 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/source/GenreSearchSource.kt @@ -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() { + + override fun getRefreshKey(state: PagingState): Int? { + return null + } + + override suspend fun load(params: LoadParams): LoadResult { + 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/source/LanguageSearchSource.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/source/LanguageSearchSource.kt new file mode 100644 index 0000000..7845a7b --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/source/LanguageSearchSource.kt @@ -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() { + + override fun getRefreshKey(state: PagingState): Int? { + return null + } + + override suspend fun load(params: LoadParams): LoadResult { + 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/source/SeriesSearchSource.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/source/SeriesSearchSource.kt new file mode 100644 index 0000000..d4ec57b --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/source/SeriesSearchSource.kt @@ -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() { + + override fun getRefreshKey(state: PagingState): Int? { + return null + } + + override suspend fun load(params: LoadParams): LoadResult { + 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/viewModel/AuthorFilterViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/viewModel/AuthorFilterViewModel.kt new file mode 100644 index 0000000..339f23a --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/viewModel/AuthorFilterViewModel.kt @@ -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> + 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 { + return AuthorSearchSource( + searchRepository = searchRepository, + search = search, + limit = AUTHOR_FILTER_PAGE_SIZE + ).also { + source = it + } + } + + companion object { + private const val AUTHOR_FILTER_PAGE_SIZE = 20 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/viewModel/BookSearchViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/viewModel/BookSearchViewModel.kt new file mode 100644 index 0000000..4f7c7a4 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/viewModel/BookSearchViewModel.kt @@ -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> + + 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 { + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/viewModel/GenreFilterViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/viewModel/GenreFilterViewModel.kt new file mode 100644 index 0000000..8d6adeb --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/viewModel/GenreFilterViewModel.kt @@ -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> + 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 { + return GenreSearchSource( + searchRepository = searchRepository, + search = search, + limit = AUTHOR_FILTER_PAGE_SIZE + ).also { + source = it + } + } + + companion object { + private const val AUTHOR_FILTER_PAGE_SIZE = 20 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/viewModel/IFilterViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/viewModel/IFilterViewModel.kt new file mode 100644 index 0000000..f9ef0d6 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/viewModel/IFilterViewModel.kt @@ -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> + val search: String? + + fun updateSearch(criteria: String?) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/viewModel/LanguageFilterViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/viewModel/LanguageFilterViewModel.kt new file mode 100644 index 0000000..930dd31 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/viewModel/LanguageFilterViewModel.kt @@ -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> + 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 { + return LanguageSearchSource( + searchRepository = searchRepository, + search = search, + limit = AUTHOR_FILTER_PAGE_SIZE + ).also { + source = it + } + } + + companion object { + private const val AUTHOR_FILTER_PAGE_SIZE = 20 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/viewModel/SeriesFilterViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/viewModel/SeriesFilterViewModel.kt new file mode 100644 index 0000000..bdd6166 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/search/viewModel/SeriesFilterViewModel.kt @@ -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> + 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 { + return SeriesSearchSource( + searchRepository = searchRepository, + search = search, + limit = AUTHOR_FILTER_PAGE_SIZE + ).also { + source = it + } + } + + companion object { + private const val AUTHOR_FILTER_PAGE_SIZE = 20 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/utils/extention/LoadParamsEx.kt b/app/src/main/java/com/pixelized/biblib/utils/extention/LoadParamsEx.kt new file mode 100644 index 0000000..50bd5a3 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/utils/extention/LoadParamsEx.kt @@ -0,0 +1,6 @@ +package com.pixelized.biblib.utils.extention + +import androidx.paging.PagingSource + +val PagingSource.LoadParams.page: Int + get() = key ?: 0 \ No newline at end of file