First implementation of a complete Search
This commit is contained in:
parent
5cc5890926
commit
182c2bda48
25 changed files with 313 additions and 119 deletions
|
|
@ -2,16 +2,16 @@
|
|||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "eb9a9ac9ba58cccd34d5fa89bcd4151c",
|
||||
"identityHash": "3f612998c4903a247d1c955da20b272d",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "AUTHOR",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`AUTHOR_ID` TEXT NOT NULL, `AUTHOR_NAME` TEXT NOT NULL, `AUTHOR_SORT` TEXT NOT NULL, PRIMARY KEY(`AUTHOR_ID`))",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`AUTHOR_ID` INTEGER NOT NULL, `AUTHOR_NAME` TEXT NOT NULL, `AUTHOR_SORT` TEXT NOT NULL, PRIMARY KEY(`AUTHOR_ID`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "AUTHOR_ID",
|
||||
"affinity": "TEXT",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
},
|
||||
{
|
||||
"tableName": "BOOK",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`BOOK_ID` INTEGER NOT NULL, `BOOK_TITLE` TEXT NOT NULL, `BOOK_SORT` TEXT NOT NULL, `BOOK_HAVE_COVER` INTEGER NOT NULL, `BOOK_RELEASE_DATE` INTEGER NOT NULL, `BOOK_LANGUAGE_ID` TEXT, `BOOK_RATING` INTEGER, `BOOK_SERIES_ID` INTEGER, `BOOK_SYNOPSIS` TEXT, `BOOK_ISNEW` INTEGER NOT NULL, `BOOK_NEW_ORDER` INTEGER, PRIMARY KEY(`BOOK_ID`))",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`BOOK_ID` INTEGER NOT NULL, `BOOK_TITLE` TEXT NOT NULL, `BOOK_SORT` TEXT NOT NULL, `BOOK_HAVE_COVER` INTEGER NOT NULL, `BOOK_RELEASE_DATE` INTEGER NOT NULL, `BOOK_LANGUAGE_ID` INTEGER, `BOOK_RATING` INTEGER, `BOOK_SERIES_ID` INTEGER, `BOOK_SYNOPSIS` TEXT, `BOOK_ISNEW` INTEGER NOT NULL, `BOOK_NEW_ORDER` INTEGER, PRIMARY KEY(`BOOK_ID`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
|
|
@ -73,7 +73,7 @@
|
|||
{
|
||||
"fieldPath": "language",
|
||||
"columnName": "BOOK_LANGUAGE_ID",
|
||||
"affinity": "TEXT",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
|
|
@ -144,12 +144,12 @@
|
|||
},
|
||||
{
|
||||
"tableName": "LANGUAGE",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`LANGUAGE_ID` TEXT NOT NULL, `LANGUAGE_NAME` TEXT NOT NULL, PRIMARY KEY(`LANGUAGE_ID`))",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`LANGUAGE_ID` INTEGER NOT NULL, `LANGUAGE_NAME` TEXT NOT NULL, PRIMARY KEY(`LANGUAGE_ID`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "LANGUAGE_ID",
|
||||
"affinity": "TEXT",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
|
|
@ -208,7 +208,7 @@
|
|||
},
|
||||
{
|
||||
"tableName": "BookAuthorCrossRef",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`BOOK_ID` INTEGER NOT NULL, `AUTHOR_ID` TEXT NOT NULL, PRIMARY KEY(`BOOK_ID`, `AUTHOR_ID`))",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`BOOK_ID` INTEGER NOT NULL, `AUTHOR_ID` INTEGER NOT NULL, PRIMARY KEY(`BOOK_ID`, `AUTHOR_ID`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "bookId",
|
||||
|
|
@ -219,7 +219,7 @@
|
|||
{
|
||||
"fieldPath": "authorId",
|
||||
"columnName": "AUTHOR_ID",
|
||||
"affinity": "TEXT",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
|
|
@ -284,7 +284,7 @@
|
|||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'eb9a9ac9ba58cccd34d5fa89bcd4151c')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3f612998c4903a247d1c955da20b272d')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -10,5 +10,5 @@ data class BookAuthorCrossRef(
|
|||
@ColumnInfo(name = BookDbo.ID)
|
||||
val bookId: Int,
|
||||
@ColumnInfo(name = AuthorDbo.ID, index = true)
|
||||
val authorId: String
|
||||
val authorId: Int,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import androidx.room.PrimaryKey
|
|||
data class AuthorDbo(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = ID)
|
||||
val id: String,
|
||||
val id: Int,
|
||||
@ColumnInfo(name = NAME)
|
||||
val name: String,
|
||||
@ColumnInfo(name = SORT)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ data class BookDbo(
|
|||
@ColumnInfo(name = RELEASE_DATE)
|
||||
val releaseDate: Date,
|
||||
@ColumnInfo(name = LANGUAGE_ID)
|
||||
val language: String? = null, // one-to-many
|
||||
val language: Int? = null, // one-to-many
|
||||
@ColumnInfo(name = RATING)
|
||||
val rating: Int? = null,
|
||||
// details
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import androidx.room.PrimaryKey
|
|||
data class LanguageDbo(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = ID)
|
||||
val id: String,
|
||||
val id: Int,
|
||||
@ColumnInfo(name = NAME)
|
||||
val code: String,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.pixelized.biblib.model.book
|
||||
|
||||
data class Author(
|
||||
val id: String,
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val sort: String,
|
||||
)
|
||||
|
|
@ -2,8 +2,8 @@ package com.pixelized.biblib.model.book
|
|||
|
||||
import java.util.*
|
||||
|
||||
data class Language (
|
||||
val id : String,
|
||||
data class Language(
|
||||
val id: Int,
|
||||
val code: String,
|
||||
) {
|
||||
val displayLanguage: String by lazy { Locale(code).displayLanguage }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.pixelized.biblib.model.book
|
||||
|
||||
data class Series(
|
||||
val id: Int?,
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val sort: String,
|
||||
val index: Int?,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class BookFactory {
|
|||
val id: Int? = response.book_id
|
||||
val title: String? = response.book_title
|
||||
val sort: String? = response.book_sort
|
||||
val authorId: List<String>? = response.author_id
|
||||
val authorId: List<Int>? = response.author_id?.mapNotNull { it.toIntOrNull() }
|
||||
val authorName: List<String>? = response.author_name
|
||||
val authorSort: List<String>? = response.author_sort
|
||||
val authorIdSize = authorId?.size ?: 0
|
||||
|
|
@ -54,7 +54,7 @@ class BookFactory {
|
|||
}
|
||||
val cover: Boolean? = response.book_has_cover?.toBoolean()
|
||||
val releaseDate = response.book_date?.let { parser.parse(it) }
|
||||
val languageId = response.lang_id
|
||||
val languageId = response.lang_id?.toIntOrNull()
|
||||
val languageCode = response.lang_code
|
||||
val language = if (languageId != null && languageCode != null) {
|
||||
Language(languageId, languageCode)
|
||||
|
|
@ -91,7 +91,7 @@ class BookFactory {
|
|||
val id: Int? = response.book_id
|
||||
val title: String? = response.book_title
|
||||
val sort: String? = response.book_sort
|
||||
val authorId: List<String>? = response.author_id
|
||||
val authorId: List<Int>? = response.author_id?.mapNotNull { it.toIntOrNull() }
|
||||
val authorName: List<String>? = response.author_name
|
||||
val authorSort: List<String>? = response.author_sort
|
||||
val authorIdSize = authorId?.size
|
||||
|
|
@ -111,7 +111,7 @@ class BookFactory {
|
|||
}
|
||||
val cover: Boolean? = response.book_has_cover?.toBoolean()
|
||||
val releaseDate = response.book_date?.let { parser.parse(it) }
|
||||
val languageId = response.lang_id
|
||||
val languageId = response.lang_id?.toIntOrNull()
|
||||
val languageCode = response.lang_code
|
||||
val language = if (languageId != null && languageCode != null) {
|
||||
Language(languageId, languageCode)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class SeriesFactory {
|
|||
response.series?.forEachIndexed { index, data ->
|
||||
// build the Series items
|
||||
val series = Series(
|
||||
id = data.series_id,
|
||||
id = data.series_id ?: throw error("id"),
|
||||
name = data.series_name ?: throw error("name"),
|
||||
sort = data.series_sort ?: throw error("sort"),
|
||||
index = index,
|
||||
|
|
|
|||
|
|
@ -15,11 +15,11 @@ interface IBookRepository {
|
|||
|
||||
fun getAuthorsSource() : DataSource.Factory<Int, Author>
|
||||
|
||||
fun getSeriesSource() : DataSource.Factory<Int, Series>
|
||||
|
||||
fun getGenresSource() : DataSource.Factory<Int, Genre>
|
||||
fun getSeriesSource(): DataSource.Factory<Int, Series>
|
||||
|
||||
fun getLanguagesSource() : DataSource.Factory<Int, Language>
|
||||
fun getGenresSource(): DataSource.Factory<Int, Genre>
|
||||
|
||||
fun getLanguagesSource(): DataSource.Factory<Int, Language>
|
||||
|
||||
suspend fun update(data: List<Book>)
|
||||
}
|
||||
|
|
@ -22,7 +22,6 @@ import androidx.compose.ui.focus.focusRequester
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.pixelized.biblib.R
|
||||
import com.pixelized.biblib.ui.scaffold.SearchScaffoldState
|
||||
import com.pixelized.biblib.ui.scaffold.SearchScaffoldState.ContentState
|
||||
|
|
@ -38,10 +37,12 @@ fun Search(
|
|||
modifier: Modifier = Modifier,
|
||||
state: SearchScaffoldState = rememberSearchScaffoldState(),
|
||||
focusRequester: FocusRequester = remember { FocusRequester() },
|
||||
avatar: String?,
|
||||
onClose: () -> Unit = default(),
|
||||
onAvatar: () -> Unit = default(),
|
||||
onSearch: () -> Unit = default(),
|
||||
avatar: String? = null,
|
||||
searchValue: String = "",
|
||||
onSearchValueChange: (String) -> Unit = default<String>(),
|
||||
onCloseTap: () -> Unit = default(),
|
||||
onAvatarTap: () -> Unit = default(),
|
||||
onSearchTap: () -> Unit = default(),
|
||||
) {
|
||||
val horizontalPadding by animateDpAsState(
|
||||
targetValue = when (state.isCollapsed()) {
|
||||
|
|
@ -49,17 +50,16 @@ fun Search(
|
|||
else -> MaterialTheme.bibLib.dimen.dp4
|
||||
}
|
||||
)
|
||||
var search by remember { mutableStateOf("") }
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onSearch)
|
||||
.clickable(onClick = onSearchTap)
|
||||
.then(modifier),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(start = horizontalPadding),
|
||||
onClick = if (state.content != ContentState.INITIAL) onClose else onSearch,
|
||||
onClick = if (state.content != ContentState.INITIAL) onCloseTap else onSearchTap,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (state.content) {
|
||||
|
|
@ -84,7 +84,7 @@ fun Search(
|
|||
}
|
||||
)
|
||||
},
|
||||
value = if (state.content == ContentState.SEARCH) search else "",
|
||||
value = if (state.content == ContentState.SEARCH) searchValue else "",
|
||||
singleLine = true,
|
||||
enabled = state.content == ContentState.SEARCH,
|
||||
readOnly = state.content != ContentState.SEARCH,
|
||||
|
|
@ -93,12 +93,12 @@ fun Search(
|
|||
unfocusedBorderColor = Color.Transparent,
|
||||
disabledBorderColor = Color.Transparent,
|
||||
),
|
||||
onValueChange = { search = it }
|
||||
onValueChange = onSearchValueChange
|
||||
)
|
||||
|
||||
IconButton(
|
||||
modifier = Modifier.padding(end = horizontalPadding),
|
||||
onClick = onAvatar,
|
||||
onClick = onAvatarTap,
|
||||
) {
|
||||
val imageModifier = Modifier
|
||||
.clip(CircleShape)
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ 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.detail.BookDetailViewModel
|
||||
import com.pixelized.biblib.ui.screen.detail.DetailScreen
|
||||
import com.pixelized.biblib.ui.screen.home.common.uio.BookUio
|
||||
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.BookUio
|
||||
import com.pixelized.biblib.utils.extention.showToast
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
|
|||
|
|
@ -50,10 +50,10 @@ fun SearchBottomSheet(
|
|||
filter = state.filter,
|
||||
onClose = {
|
||||
when(state.filter) {
|
||||
is SearchFilter.Author -> searchViewModel.authors.confirm("")
|
||||
is SearchFilter.Series -> searchViewModel.series.confirm("")
|
||||
is SearchFilter.Genre -> searchViewModel.genre.confirm("")
|
||||
is SearchFilter.Language -> searchViewModel.language.confirm("")
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusManager
|
||||
|
|
@ -22,6 +23,7 @@ import com.google.accompanist.pager.ExperimentalPagerApi
|
|||
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.composable.Search
|
||||
import com.pixelized.biblib.ui.scaffold.*
|
||||
import com.pixelized.biblib.ui.scaffold.SearchScaffoldState.ContentState
|
||||
|
|
@ -38,7 +40,9 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class, ExperimentalAnimationApi::class,
|
||||
@OptIn(
|
||||
ExperimentalComposeUiApi::class,
|
||||
ExperimentalAnimationApi::class,
|
||||
ExperimentalMaterialApi::class
|
||||
)
|
||||
@Composable
|
||||
|
|
@ -58,16 +62,22 @@ fun HomeScreen(
|
|||
modifier = Modifier.statusBarsPadding(),
|
||||
state = state,
|
||||
topBar = {
|
||||
val viewModel = LocalSearchViewModel.current
|
||||
val search by viewModel.filterFlow.collectAsState(initial = "")
|
||||
Search(
|
||||
state = state,
|
||||
avatar = accountViewModel.avatar,
|
||||
focusRequester = focusRequester,
|
||||
onClose = {
|
||||
onCloseTap = {
|
||||
focusManager.clearFocus(force = true)
|
||||
keyboard?.hide()
|
||||
state.collapse()
|
||||
},
|
||||
onSearch = {
|
||||
searchValue = search,
|
||||
onSearchValueChange = {
|
||||
viewModel.filter(criteria = it)
|
||||
},
|
||||
onSearchTap = {
|
||||
if (state.content != ContentState.SEARCH || state.isCollapsed()) {
|
||||
state.expand(ContentState.SEARCH)
|
||||
scope.launch {
|
||||
|
|
@ -80,7 +90,7 @@ fun HomeScreen(
|
|||
state.collapse()
|
||||
}
|
||||
},
|
||||
onAvatar = {
|
||||
onAvatarTap = {
|
||||
if (state.content != ContentState.PROFILE || state.isCollapsed()) {
|
||||
focusManager.clearFocus(force = true)
|
||||
keyboard?.hide()
|
||||
|
|
@ -131,7 +141,9 @@ fun HomeScreen(
|
|||
|
||||
|
||||
val bottomSearchState = LocalSearchBottomSheetState.current
|
||||
BackHandler(state.isCollapsed().not() && bottomSearchState.bottomSheetState.isVisible.not()) {
|
||||
BackHandler(
|
||||
state.isCollapsed().not() && bottomSearchState.bottomSheetState.isVisible.not()
|
||||
) {
|
||||
state.collapse()
|
||||
}
|
||||
}
|
||||
|
|
@ -173,14 +185,28 @@ fun HomeScreenContent(
|
|||
when (pages[it]) {
|
||||
is Page.News -> NewsPage()
|
||||
is Page.Books -> BooksPage()
|
||||
is Page.Author -> Box(modifier = Modifier)
|
||||
is Page.Series -> Box(modifier = Modifier)
|
||||
is Page.Tag -> Box(modifier = Modifier)
|
||||
is Page.Author -> NotYetImplemented()
|
||||
is Page.Series -> NotYetImplemented()
|
||||
is Page.Tag -> NotYetImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotYetImplemented() {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
style = MaterialTheme.typography.body1,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
text = stringResource(id = R.string.not_implemented_yet)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
private fun HomeScreenContentPreview() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.biblib.ui.screen.detail
|
||||
package com.pixelized.biblib.ui.screen.home.detail
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
|
@ -6,7 +6,6 @@ import com.pixelized.biblib.model.book.Book
|
|||
import com.pixelized.biblib.network.client.IBibLibClient
|
||||
import com.pixelized.biblib.network.factory.BookFactory
|
||||
import com.pixelized.biblib.ui.composable.StateUio
|
||||
import com.pixelized.biblib.ui.screen.home.common.uio.BookUio
|
||||
import com.pixelized.biblib.utils.extention.capitalize
|
||||
import com.pixelized.biblib.utils.extention.shortDate
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.biblib.ui.screen.home.common.uio
|
||||
package com.pixelized.biblib.ui.screen.home.detail
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.biblib.ui.screen.detail
|
||||
package com.pixelized.biblib.ui.screen.home.detail
|
||||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||
|
|
@ -37,13 +37,13 @@ import com.pixelized.biblib.ui.composable.animation.AnimatedOffset
|
|||
import com.pixelized.biblib.ui.composable.isSuccessful
|
||||
import com.pixelized.biblib.ui.scaffold.DetailBottomSheetState
|
||||
import com.pixelized.biblib.ui.scaffold.LocalDetailBottomSheetState
|
||||
import com.pixelized.biblib.ui.screen.home.common.uio.BookUio
|
||||
import com.pixelized.biblib.ui.screen.home.page.profile.ProfileViewModel
|
||||
import com.pixelized.biblib.ui.screen.home.page.profile.UserUio
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
import com.pixelized.biblib.utils.extention.bibLib
|
||||
import com.pixelized.biblib.utils.extention.default
|
||||
import com.skydoves.landscapist.CircularReveal
|
||||
import com.skydoves.landscapist.ShimmerParams
|
||||
import com.skydoves.landscapist.glide.GlideImage
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
|
@ -129,6 +129,13 @@ private fun DetailScreenContent(
|
|||
.fillMaxWidth()
|
||||
.padding(vertical = MaterialTheme.bibLib.dimen.dp16)
|
||||
.height(MaterialTheme.bibLib.dimen.detail.cover),
|
||||
loading = {
|
||||
Box(modifier = Modifier.matchParentSize()) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
},
|
||||
previewPlaceholder = R.drawable.ic_launcher_foreground,
|
||||
circularReveal = CircularReveal(duration = 1000),
|
||||
contentScale = ContentScale.FillHeight,
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
package com.pixelized.biblib.ui.screen.home.page.books
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Card
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.pixelized.biblib.ui.screen.home.page.news
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
package com.pixelized.biblib.ui.screen.home.page.search
|
||||
|
||||
data class BookSearchUio(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val author: List<String>,
|
||||
val language: String? = null,
|
||||
val genre: List<String>? = null,
|
||||
val series: String? = null,
|
||||
)
|
||||
|
|
@ -88,9 +88,9 @@ fun CategorySearchPageContent(
|
|||
focusRequester: FocusRequester = FocusRequester(),
|
||||
filter: SearchFilter?,
|
||||
searchFlow: () -> Flow<String>,
|
||||
dataFlow: () -> Flow<PagingData<String>> = { emptyFlow() },
|
||||
dataFlow: () -> Flow<PagingData<SearchViewModel.FilterUio>> = { emptyFlow() },
|
||||
onSearchChange: (String) -> Unit = default<String>(),
|
||||
onData: (String) -> Unit = default<String>(),
|
||||
onData: (SearchViewModel.FilterUio?) -> Unit = default<SearchViewModel.FilterUio?>(),
|
||||
onClose: () -> Unit = default(),
|
||||
) {
|
||||
val data = dataFlow().collectAsLazyPagingItems()
|
||||
|
|
@ -142,15 +142,15 @@ fun CategorySearchPageContent(
|
|||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
items(items = data, key = { it }) {
|
||||
items(items = data, key = { it.id }) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clickable { onData(it ?: "") }
|
||||
.clickable { onData(it) }
|
||||
.fillMaxWidth()
|
||||
.padding(all = MaterialTheme.bibLib.dimen.dp16),
|
||||
style = MaterialTheme.typography.body1,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
text = it ?: ""
|
||||
text = it?.label ?: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,29 +2,40 @@ package com.pixelized.biblib.ui.screen.home.page.search
|
|||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.clickable
|
||||
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.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
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.material.icons.filled.NavigateNext
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.items
|
||||
import com.pixelized.biblib.ui.scaffold.LocalDetailBottomSheetState
|
||||
import com.pixelized.biblib.ui.scaffold.LocalSearchBottomSheetState
|
||||
import com.pixelized.biblib.ui.scaffold.LocalSearchViewModel
|
||||
import com.pixelized.biblib.ui.scaffold.SearchFilter
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun SearchPage(
|
||||
|
|
@ -35,6 +46,7 @@ fun SearchPage(
|
|||
|
||||
SearchPageContent(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
search = searchViewModel.search,
|
||||
filters = filters,
|
||||
onFilter = {
|
||||
bottomSearchState.expandSearch(it)
|
||||
|
|
@ -45,9 +57,20 @@ fun SearchPage(
|
|||
@Composable
|
||||
private fun SearchPageContent(
|
||||
modifier: Modifier = Modifier,
|
||||
search: Flow<PagingData<BookSearchUio>> = emptyFlow(),
|
||||
filters: List<SearchFilter> = SearchFilter.all,
|
||||
onFilter: (filter: SearchFilter) -> Unit = default<SearchFilter>(),
|
||||
) {
|
||||
val items = search.collectAsLazyPagingItems()
|
||||
val detail = LocalDetailBottomSheetState.current
|
||||
|
||||
SearchLoader(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(MaterialTheme.bibLib.dimen.dp2),
|
||||
isLoading = { items.isLoading },
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.bibLib.dimen.dp8),
|
||||
|
|
@ -59,6 +82,60 @@ private fun SearchPageContent(
|
|||
onFilter = onFilter,
|
||||
)
|
||||
}
|
||||
items(items = items, key = { it.id }) {
|
||||
if (it != null) {
|
||||
SearchItem(
|
||||
item = it,
|
||||
onClick = {
|
||||
detail.expandBookDetail(it.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchLoader(
|
||||
modifier: Modifier = Modifier,
|
||||
isLoading: () -> Boolean,
|
||||
) {
|
||||
val alpha by animateFloatAsState(
|
||||
animationSpec = spring(stiffness = Spring.StiffnessLow),
|
||||
targetValue = if (isLoading()) 1f else 0f
|
||||
)
|
||||
LinearProgressIndicator(
|
||||
modifier = modifier.alpha(alpha),
|
||||
color = MaterialTheme.colors.primary,
|
||||
backgroundColor = Color.Transparent,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchItem(
|
||||
modifier: Modifier = Modifier,
|
||||
item: BookSearchUio,
|
||||
onClick: () -> Unit = default()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onClick)
|
||||
.fillMaxWidth()
|
||||
.padding(all = MaterialTheme.bibLib.dimen.dp16)
|
||||
.then(modifier)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.body1,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
text = item.title
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.NavigateNext,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,13 +151,13 @@ private fun SearchFilter(
|
|||
.then(modifier)
|
||||
) {
|
||||
filters.forEachIndexed { index, filter ->
|
||||
val modifier = if (index != filters.lastIndex) {
|
||||
val chipModifier = if (index != filters.lastIndex) {
|
||||
Modifier.padding(end = MaterialTheme.bibLib.dimen.dp8)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
SearchChipFilter(
|
||||
modifier = modifier,
|
||||
modifier = chipModifier,
|
||||
selected = filter.isSelected,
|
||||
label = stringResource(id = filter.label),
|
||||
value = filter.value,
|
||||
|
|
@ -124,25 +201,46 @@ private fun SearchChipFilter(
|
|||
private fun rememberSearchFilter(
|
||||
searchViewModel: SearchViewModel = LocalSearchViewModel.current
|
||||
): List<SearchFilter> {
|
||||
val authors by searchViewModel.authors.search
|
||||
val genre by searchViewModel.genre.search
|
||||
val language by searchViewModel.language.search
|
||||
val series by searchViewModel.series.search
|
||||
|
||||
val filters by remember {
|
||||
derivedStateOf {
|
||||
listOf(
|
||||
SearchFilter.Author(value = authors),
|
||||
SearchFilter.Series(value = series),
|
||||
SearchFilter.Genre(value = genre),
|
||||
SearchFilter.Language(value = language)
|
||||
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
|
||||
}
|
||||
|
||||
private val LazyPagingItems<*>.isLoading: Boolean
|
||||
get() {
|
||||
val isLoading by derivedStateOf {
|
||||
loadState.refresh is LoadState.Loading || loadState.append is LoadState.Loading
|
||||
}
|
||||
return isLoading
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
|
||||
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
|
||||
private fun SearchItemPreview() {
|
||||
BibLibTheme {
|
||||
SearchItem(
|
||||
item = BookSearchUio(
|
||||
id = 0,
|
||||
title = "Fondation",
|
||||
author = listOf("Issac Asimov"),
|
||||
language = "Fr",
|
||||
genre = listOf("SF, Classic"),
|
||||
series = "Fondation",
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
|
||||
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
package com.pixelized.biblib.ui.screen.home.page.search
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
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.book.IBookRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -11,6 +14,7 @@ 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
|
||||
|
||||
|
|
@ -19,70 +23,122 @@ class SearchViewModel @Inject constructor(
|
|||
bookRepository: IBookRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
var search: String by mutableStateOf("")
|
||||
private set
|
||||
|
||||
val authors = FilterManager(
|
||||
val authors = CategoryFilterManager(
|
||||
scope = viewModelScope,
|
||||
source = bookRepository.getAuthorsSource()
|
||||
.map { it.name }
|
||||
.asPagingSourceFactory(Dispatchers.IO)
|
||||
.map { FilterUio(it.id, it.name) }
|
||||
.asPagingSourceFactory(Dispatchers.IO),
|
||||
sourceFilter = { data, filter -> data.label.contains(filter, ignoreCase = true) }
|
||||
)
|
||||
|
||||
val series = FilterManager(
|
||||
val series = CategoryFilterManager(
|
||||
scope = viewModelScope,
|
||||
source = bookRepository.getSeriesSource()
|
||||
.map { it.name }
|
||||
.asPagingSourceFactory(Dispatchers.IO)
|
||||
.map { FilterUio(it.id, it.name) }
|
||||
.asPagingSourceFactory(Dispatchers.IO),
|
||||
sourceFilter = { data, filter -> data.label.contains(filter, ignoreCase = true) }
|
||||
)
|
||||
|
||||
val genre = FilterManager(
|
||||
val genre = CategoryFilterManager(
|
||||
scope = viewModelScope,
|
||||
source = bookRepository.getGenresSource()
|
||||
.map { it.name }
|
||||
.asPagingSourceFactory(Dispatchers.IO)
|
||||
.map { FilterUio(it.id, it.name) }
|
||||
.asPagingSourceFactory(Dispatchers.IO),
|
||||
sourceFilter = { data, filter -> data.label.contains(filter, ignoreCase = true) }
|
||||
)
|
||||
|
||||
val language = FilterManager(
|
||||
val language = CategoryFilterManager(
|
||||
scope = viewModelScope,
|
||||
source = bookRepository.getLanguagesSource()
|
||||
.map { it.displayLanguage }
|
||||
.asPagingSourceFactory(Dispatchers.IO)
|
||||
.map { FilterUio(it.id, it.displayLanguage) }
|
||||
.asPagingSourceFactory(Dispatchers.IO),
|
||||
sourceFilter = { data, filter -> data.label.contains(filter, ignoreCase = true) }
|
||||
)
|
||||
|
||||
fun search(criteria: String) {
|
||||
private val _filterFlow = MutableStateFlow(value = "")
|
||||
val filterFlow: Flow<String> get() = _filterFlow
|
||||
|
||||
fun filter(criteria: String) {
|
||||
viewModelScope.launch { _filterFlow.emit(criteria) }
|
||||
}
|
||||
|
||||
class FilterManager(
|
||||
private val scope: CoroutineScope,
|
||||
source: () -> PagingSource<Int, String>,
|
||||
) {
|
||||
private val _searchFlow = MutableStateFlow("")
|
||||
val searchFlow: Flow<String> get() = _searchFlow
|
||||
fun clear() {
|
||||
viewModelScope.launch { _filterFlow.emit("") }
|
||||
}
|
||||
|
||||
private val _filterFlow = MutableStateFlow("")
|
||||
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.isEmpty() || it.author.any { author -> author.id == filter.id } }
|
||||
}
|
||||
.combine(series.confirmFlow) { paging, filter ->
|
||||
paging.filter { filter == null || it.series == null || it.series.id == filter.id }
|
||||
}
|
||||
.combine(genre.confirmFlow) { paging, filter ->
|
||||
paging.filter { filter == null || it.genre.isNullOrEmpty() || it.genre.any { author -> author.id == filter.id } }
|
||||
}
|
||||
.combine(language.confirmFlow) { paging, filter ->
|
||||
paging.filter { filter == null || it.language == null || it.language.id == filter.id }
|
||||
}
|
||||
.map { paging -> paging.map { it.toBookSearchUio() } }
|
||||
|
||||
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
|
||||
|
||||
val dataFlow: Flow<PagingData<String>> = Pager(
|
||||
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 { it.contains(filter, ignoreCase = true) }
|
||||
paging.filter { sourceFilter(it, filter) }
|
||||
}
|
||||
|
||||
val filter: State<String>
|
||||
@Composable get() = _filterFlow.collectAsState(initial = "")
|
||||
|
||||
val search: State<String>
|
||||
@Composable get() = _searchFlow.collectAsState(initial = "")
|
||||
var search: T? by mutableStateOf(null)
|
||||
private set
|
||||
|
||||
fun filter(criteria: String) {
|
||||
scope.launch { _filterFlow.emit(criteria) }
|
||||
}
|
||||
|
||||
fun confirm(criteria: String) {
|
||||
scope.launch { _searchFlow.emit(criteria) }
|
||||
fun clear() {
|
||||
search = null
|
||||
scope.launch { _confirmFlow.emit(null) }
|
||||
}
|
||||
|
||||
fun confirm(criteria: T?) {
|
||||
search = criteria
|
||||
scope.launch { _confirmFlow.emit(criteria) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun Book.toBookSearchUio() = BookSearchUio(
|
||||
id = id,
|
||||
title = title,
|
||||
author = author.map { it.name },
|
||||
language = language?.displayLanguage,
|
||||
genre = genre?.map { it.name },
|
||||
series = series?.name,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val SEARCH_PAGE_SIZE = 15
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import androidx.compose.ui.unit.dp
|
|||
@Immutable
|
||||
data class BibLibDimen(
|
||||
val default: Dp = 0.dp,
|
||||
val dp2: Dp = 2.dp,
|
||||
val dp4: Dp = 4.dp,
|
||||
val dp8: Dp = 8.dp,
|
||||
val dp16: Dp = 16.dp,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue