This commit is contained in:
tom5079
2024-03-17 15:02:35 -07:00
parent f34876ca93
commit 9e9a5998cd
9 changed files with 317 additions and 82 deletions

View File

@@ -105,8 +105,6 @@ dependencies {
annotationProcessor "androidx.room:room-compiler:$room_version"
ksp "androidx.room:room-compiler:$room_version"
implementation "androidx.paging:paging-compose:3.2.1"
implementation "io.ktor:ktor-client-core:2.3.8"
implementation "io.ktor:ktor-client-okhttp:2.3.8"

View File

@@ -18,7 +18,7 @@ data class Group(val group: String): TagLike {
}
@Serializable
data class Series(val series: String): TagLike {
data class Series(@SerialName("parody") val series: String): TagLike {
override fun toTag() = SearchQuery.Tag("series", series)
}

View File

@@ -0,0 +1,36 @@
package xyz.quaver.pupil.networking
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
class GallerySearchSource(val query: SearchQuery?) {
private var searchResult: List<Int>? = null
private var job: Job? = null
suspend fun load(range: IntRange): Result<Pair<List<GalleryInfo>, Int>> = runCatching {
val searchResult = searchResult ?: (
HitomiHttpClient
.search(query)
.getOrThrow()
.toList()
.also { searchResult = it }
)
val galleryResults = coroutineScope {
searchResult.slice(range).map { galleryID ->
async {
HitomiHttpClient.getGalleryInfo(galleryID)
}
}
}
val galleries = galleryResults.map { result ->
result.await().getOrThrow()
}
Pair(galleries, searchResult.size)
}
fun cancel() = job?.cancel()
}

View File

@@ -11,6 +11,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import xyz.quaver.pupil.hitomi.max_node_size
import java.nio.ByteBuffer
import java.nio.ByteOrder
@@ -36,7 +37,7 @@ data class Suggestion(
)
fun IntBuffer.toSet(): Set<Int> {
val result = mutableSetOf<Int>()
val result = LinkedHashSet<Int>()
while (this.hasRemaining()) {
result.add(this.get())
@@ -45,6 +46,11 @@ fun IntBuffer.toSet(): Set<Int> {
return result
}
private val json = Json {
isLenient = true
ignoreUnknownKeys = true
}
object HitomiHttpClient {
private val httpClient = HttpClient(OkHttp)
@@ -193,6 +199,15 @@ object HitomiHttpClient {
data?.let { getSuggestionsFromData(field, data) } ?: emptyList()
}
suspend fun getGalleryInfo(galleryID: Int) = runCatching {
withContext(Dispatchers.IO) {
json.decodeFromString<GalleryInfo>(
httpClient.get("https://$domain/galleries/$galleryID.js").bodyAsText()
.replace("var galleryinfo = ", "")
)
}
}
suspend fun search(query: SearchQuery?): Result<Set<Int>> = runCatching {
when (query) {
is SearchQuery.Tag -> getGalleryIDsForQuery(query).toSet()
@@ -203,7 +218,7 @@ object HitomiHttpClient {
val queriedGalleries = search(query.query).getOrThrow()
val result = mutableSetOf<Int>()
val result = LinkedHashSet<Int>()
with (allGalleries.await()) {
while (this.hasRemaining()) {
@@ -241,7 +256,7 @@ object HitomiHttpClient {
}
}
val result = mutableSetOf<Int>()
val result = LinkedHashSet<Int>()
queries.forEach {
val queryResult = it.await()

View File

@@ -30,6 +30,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.adaptive.calculateDisplayFeatures
import xyz.quaver.pupil.ui.composable.MainApp
import xyz.quaver.pupil.ui.theme.AppTheme
import xyz.quaver.pupil.ui.viewmodel.MainUIState
import xyz.quaver.pupil.ui.viewmodel.MainViewModel
class MainActivity : BaseActivity() {
@@ -54,7 +55,9 @@ class MainActivity : BaseActivity() {
displayFeatures = displayFeatures,
uiState = uiState,
navigateToDestination = viewModel::navigateToDestination,
closeDetailScreen = viewModel::closeDetailScreen
closeDetailScreen = viewModel::closeDetailScreen,
onQueryChange = viewModel::onQueryChange,
loadSearchResult = viewModel::loadSearchResult
)
}
}

View File

@@ -24,6 +24,7 @@ import androidx.compose.ui.Modifier
import androidx.window.layout.DisplayFeature
import androidx.window.layout.FoldingFeature
import kotlinx.coroutines.launch
import xyz.quaver.pupil.networking.SearchQuery
import xyz.quaver.pupil.ui.viewmodel.MainUIState
@Composable
@@ -32,7 +33,9 @@ fun MainApp(
displayFeatures: List<DisplayFeature>,
uiState: MainUIState,
navigateToDestination: (MainDestination) -> Unit,
closeDetailScreen: () -> Unit
closeDetailScreen: () -> Unit,
onQueryChange: (SearchQuery?) -> Unit,
loadSearchResult: (IntRange) -> Unit
) {
val navigationType: NavigationType
val contentType: ContentType
@@ -85,7 +88,9 @@ fun MainApp(
navigationContentPosition,
uiState,
navigateToDestination,
closeDetailScreen = closeDetailScreen
closeDetailScreen = closeDetailScreen,
onQueryChange = onQueryChange,
loadSearchResult = loadSearchResult
)
}
@@ -97,7 +102,9 @@ private fun MainNavigationWrapper(
navigationContentPosition: NavigationContentPosition,
uiState: MainUIState,
navigateToDestination: (MainDestination) -> Unit,
closeDetailScreen: () -> Unit
closeDetailScreen: () -> Unit,
onQueryChange: (SearchQuery?) -> Unit,
loadSearchResult: (IntRange) -> Unit
) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val coroutineScope = rememberCoroutineScope()
@@ -126,7 +133,9 @@ private fun MainNavigationWrapper(
uiState = uiState,
navigateToDestination = navigateToDestination,
onDrawerClicked = openDrawer,
closeDetailScreen = closeDetailScreen
closeDetailScreen = closeDetailScreen,
onQueryChange = onQueryChange,
loadSearchResult = loadSearchResult
)
}
} else {
@@ -153,7 +162,9 @@ private fun MainNavigationWrapper(
uiState = uiState,
navigateToDestination = navigateToDestination,
onDrawerClicked = openDrawer,
closeDetailScreen = closeDetailScreen
closeDetailScreen = closeDetailScreen,
onQueryChange = onQueryChange,
loadSearchResult = loadSearchResult
)
}
}
@@ -168,7 +179,9 @@ fun MainContent(
uiState: MainUIState,
navigateToDestination: (MainDestination) -> Unit,
onDrawerClicked: () -> Unit,
closeDetailScreen: () -> Unit
closeDetailScreen: () -> Unit,
onQueryChange: (SearchQuery?) -> Unit,
loadSearchResult: (IntRange) -> Unit
) {
Row(modifier = Modifier.fillMaxSize()) {
AnimatedVisibility(visible = navigationType == NavigationType.NAVIGATION_RAIL) {
@@ -197,7 +210,9 @@ fun MainContent(
contentType = contentType,
displayFeatures = displayFeatures,
uiState = uiState,
closeDetailScreen = closeDetailScreen
closeDetailScreen = closeDetailScreen,
onQueryChange = onQueryChange,
loadSearchResult = loadSearchResult
)
}
AnimatedVisibility(visible = navigationType == NavigationType.BOTTOM_NAVIGATION) {

View File

@@ -1,6 +1,7 @@
package xyz.quaver.pupil.ui.composable
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn
@@ -14,8 +15,13 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -23,9 +29,12 @@ import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -42,6 +51,7 @@ import androidx.compose.material.icons.filled.Male
import androidx.compose.material.icons.filled.Translate
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
@@ -61,10 +71,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.window.layout.DisplayFeature
@@ -73,7 +83,6 @@ import com.google.accompanist.adaptive.TwoPane
import xyz.quaver.pupil.R
import xyz.quaver.pupil.networking.GalleryInfo
import xyz.quaver.pupil.networking.SearchQuery
import xyz.quaver.pupil.networking.SearchQueryPreviewParameterProvider
import xyz.quaver.pupil.ui.theme.Blue600
import xyz.quaver.pupil.ui.theme.Pink600
import xyz.quaver.pupil.ui.theme.Yellow400
@@ -217,7 +226,7 @@ fun TagChip(
@Composable
fun QueryView(
query: SearchQuery,
query: SearchQuery?,
topLevel: Boolean = true
) {
val modifier = if (topLevel) {
@@ -227,6 +236,16 @@ fun QueryView(
}
when (query) {
null -> {
Text(
modifier = Modifier
.height(60.dp)
.wrapContentHeight()
.padding(horizontal = 16.dp),
text = stringResource(id = R.string.search_hint),
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
}
is SearchQuery.Tag -> {
TagChip(
query,
@@ -250,8 +269,8 @@ fun QueryView(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
query.queries.forEach { subquery ->
QueryView(subquery, topLevel = false)
query.queries.forEach { subQuery ->
QueryView(subQuery, topLevel = false)
}
}
}
@@ -272,17 +291,25 @@ fun QueryView(
fun SearchBar(
contentType: ContentType,
query: SearchQuery?,
onQueryChange: (SearchQuery) -> Unit,
onQueryChange: (SearchQuery?) -> Unit,
onSearch: () -> Unit,
topOffset: Int,
onTopOffsetChange: (Int) -> Unit,
content: @Composable () -> Unit
) {
var focused by remember { mutableStateOf(true) }
val scrimAlpha: Float by animateFloatAsState(if (focused && contentType == ContentType.SINGLE_PANE) 0.3f else 0f, label = "skrim alpha")
var focused by remember { mutableStateOf(false) }
val scrimAlpha: Float by animateFloatAsState(if (focused && contentType == ContentType.SINGLE_PANE) 0.3f else 0f, label = "scrim alpha")
val interactionSource = remember { MutableInteractionSource() }
val state = remember(query) { query.toEditableState() }
LaunchedEffect(focused) {
if (!focused) {
onQueryChange(state.toSearchQuery())
}
}
if (focused) {
BackHandler {
focused = false
@@ -299,13 +326,15 @@ fun SearchBar(
) {
focused = false
}
.safeDrawingPadding()
.padding(16.dp)
) {
val height: Dp by animateDpAsState(if (focused) maxHeight else 60.dp, label = "searchbar height")
content()
Card(
modifier = Modifier
.safeDrawingPadding()
.padding(16.dp)
.fillMaxWidth()
.height(height)
.clickable(
@@ -321,17 +350,7 @@ fun SearchBar(
) else CardDefaults.cardElevation()
) {
Box {
androidx.compose.animation.AnimatedVisibility(query == null && !focused, enter = fadeIn(), exit = fadeOut()) {
Text(
modifier = Modifier
.height(60.dp)
.wrapContentHeight()
.padding(horizontal = 16.dp),
text = stringResource(id = R.string.search_hint),
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
}
androidx.compose.animation.AnimatedVisibility(query != null && !focused, enter = fadeIn(), exit = fadeOut()) {
androidx.compose.animation.AnimatedVisibility(!focused, enter = fadeIn(), exit = fadeOut()) {
Row(
modifier = Modifier
.heightIn(min = 60.dp)
@@ -339,13 +358,11 @@ fun SearchBar(
verticalAlignment = Alignment.CenterVertically
) {
Box(Modifier.size(8.dp))
QueryView(query!!)
QueryView(query)
Box(Modifier.size(8.dp))
}
}
androidx.compose.animation.AnimatedVisibility(focused, enter = fadeIn(), exit = fadeOut()) {
val state = remember(query) { query.toEditableState() }
Column(
Modifier
.fillMaxSize()
@@ -373,7 +390,9 @@ fun MainScreen(
contentType: ContentType,
displayFeatures: List<DisplayFeature>,
uiState: MainUIState,
closeDetailScreen: () -> Unit
closeDetailScreen: () -> Unit,
onQueryChange: (SearchQuery?) -> Unit,
loadSearchResult: (IntRange) -> Unit
) {
LaunchedEffect(contentType) {
if (contentType == ContentType.SINGLE_PANE && !uiState.isDetailOnlyOpen) {
@@ -383,12 +402,34 @@ fun MainScreen(
val galleryLazyListState = rememberLazyListState()
val itemsPerPage by remember { mutableIntStateOf(20) }
val currentRange = remember(uiState) {
if (uiState.currentRange != IntRange.EMPTY) {
uiState.currentRange
} else {
0 ..< itemsPerPage
}
}
val search = remember(currentRange) {{ loadSearchResult(currentRange) }}
LaunchedEffect(Unit) {
search()
}
if (contentType == ContentType.DUAL_PANE) {
TwoPane(
first = {
GalleryList(
contentType = contentType,
galleryLazyListState = galleryLazyListState
galleries = uiState.galleries,
query = uiState.query,
loading = uiState.loading,
error = uiState.error,
galleryLazyListState = galleryLazyListState,
onQueryChange = onQueryChange,
search = search
)
},
second = {
@@ -400,7 +441,13 @@ fun MainScreen(
} else {
GalleryList(
contentType = contentType,
galleryLazyListState = galleryLazyListState
galleries = uiState.galleries,
query = uiState.query,
loading = uiState.loading,
error = uiState.error,
galleryLazyListState = galleryLazyListState,
onQueryChange = onQueryChange,
search = search
)
}
}
@@ -408,11 +455,13 @@ fun MainScreen(
@Composable
fun GalleryList(
contentType: ContentType,
galleries: List<GalleryInfo> = emptyList(),
galleries: List<GalleryInfo>,
query: SearchQuery?,
loading: Boolean = false,
error: Boolean = false,
openedGallery: GalleryInfo? = null,
query: SearchQuery? = SearchQueryPreviewParameterProvider().values.first(),
onQueryChange: (SearchQuery) -> Unit = {},
onSearch: () -> Unit = { },
onQueryChange: (SearchQuery?) -> Unit = {},
search: () -> Unit = {},
selectedGalleryIds: Set<Int> = emptySet(),
toggleGallerySelection: (Int) -> Unit = {},
galleryLazyListState: LazyListState,
@@ -424,10 +473,43 @@ fun GalleryList(
contentType = contentType,
query = query,
onQueryChange = onQueryChange,
onSearch = onSearch,
onSearch = search,
topOffset = topOffset,
onTopOffsetChange = { topOffset = it },
) {
AnimatedVisibility (loading) {
Box(Modifier.fillMaxSize()) {
CircularProgressIndicator(Modifier.align(Alignment.Center))
}
}
AnimatedVisibility(error) {
Box(Modifier.fillMaxSize()) {
Column(
Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text("(´∇`)", style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f))
Text("No sources found!\nLet's go download one.", textAlign = TextAlign.Center)
}
}
}
AnimatedVisibility(!loading && !error) {
LazyColumn(
contentPadding = WindowInsets.systemBars.asPaddingValues().let { systemBarPaddingValues ->
val layoutDirection = LocalLayoutDirection.current
PaddingValues(
top = systemBarPaddingValues.calculateTopPadding() + 96.dp,
bottom = systemBarPaddingValues.calculateBottomPadding(),
start = systemBarPaddingValues.calculateStartPadding(layoutDirection),
end = systemBarPaddingValues.calculateEndPadding(layoutDirection),
)
}
) {
items(galleries) {galleryInfo ->
Text(galleryInfo.title)
}
}
}
}
}

View File

@@ -1,6 +1,5 @@
package xyz.quaver.pupil.ui.composable
import android.util.Log
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring
@@ -10,7 +9,6 @@ import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
@@ -92,11 +90,23 @@ private fun SearchQuery.toEditableStateInternal(): EditableSearchQueryState = wh
fun SearchQuery?.toEditableState(): EditableSearchQueryState.Root
= EditableSearchQueryState.Root(this?.toEditableStateInternal())
private fun EditableSearchQueryState.Tag.toSearchQueryInternal(): SearchQuery.Tag? =
if (namespace.value != null || tag.value.isNotBlank()) SearchQuery.Tag(namespace.value, tag.value.lowercase().trim()) else null
private fun EditableSearchQueryState.And.toSearchQueryInternal(): SearchQuery.And? =
queries.mapNotNull { it.toSearchQueryInternal() }.let { if (it.isNotEmpty()) SearchQuery.And(it) else null }
private fun EditableSearchQueryState.Or.toSearchQueryInternal(): SearchQuery.Or? =
queries.mapNotNull { it.toSearchQueryInternal() }.let { if (it.isNotEmpty()) SearchQuery.Or(it) else null }
private fun EditableSearchQueryState.Not.toSearchQueryInternal(): SearchQuery.Not? =
query.value?.toSearchQueryInternal()?.let { SearchQuery.Not(it) }
private fun EditableSearchQueryState.toSearchQueryInternal(): SearchQuery? = when (this) {
is EditableSearchQueryState.Tag -> SearchQuery.Tag(namespace.value, tag.value)
is EditableSearchQueryState.And -> SearchQuery.And(queries.mapNotNull { it.toSearchQueryInternal() })
is EditableSearchQueryState.Or -> SearchQuery.Or(queries.mapNotNull { it.toSearchQueryInternal() })
is EditableSearchQueryState.Not -> query.value?.toSearchQueryInternal()?.let { SearchQuery.Not(it) }
is EditableSearchQueryState.Tag -> this.toSearchQueryInternal()
is EditableSearchQueryState.And -> this.toSearchQueryInternal()
is EditableSearchQueryState.Or -> this.toSearchQueryInternal()
is EditableSearchQueryState.Not -> this.toSearchQueryInternal()
}
fun EditableSearchQueryState.Root.toSearchQuery(): SearchQuery?
@@ -162,9 +172,16 @@ fun TagSuggestionList(
LaunchedEffect(namespace, tag) {
suggestionList = null
suggestionList = HitomiHttpClient.getSuggestionsForQuery(SearchQuery.Tag(namespace, tag))
.getOrDefault(emptyList())
.filterNot { it.tag == SearchQuery.Tag(namespace, tag) }
val searchQuery = state.toSearchQueryInternal()
suggestionList = if (searchQuery != null) {
HitomiHttpClient.getSuggestionsForQuery(searchQuery)
.getOrDefault(emptyList())
.filterNot { it.tag == SearchQuery.Tag(namespace, tag) }
} else {
emptyList()
}
}
val suggestionListSnapshot = suggestionList
@@ -576,18 +593,6 @@ fun QueryEditorQueryView(
EditableTagChip(
newSearchQuery,
requestScrollTo = requestScrollTo,
rightIcon = {
Icon(
modifier = Modifier
.padding(8.dp)
.size(16.dp)
.clickable {
onQueryRemove(state)
},
imageVector = Icons.Default.RemoveCircleOutline,
contentDescription = stringResource(R.string.search_remove_query_item_description)
)
}
)
NewQueryChip(state) { newQueryState ->
state.queries.add(newQueryState)
@@ -677,26 +682,60 @@ fun QueryEditor(
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
val rootQuerySnapshot = rootQuery
val requestScrollTo: (Float) -> Unit = { target ->
val topYSnapshot = topY
coroutineScope.launch {
scrollState.animateScrollBy(target - topYSnapshot - scrollOffset, spring(stiffness = Spring.StiffnessLow))
}
}
val requestScrollBy: (Float) -> Unit = { value ->
coroutineScope.launch {
scrollState.animateScrollBy(value)
}
}
if (rootQuerySnapshot != null) {
QueryEditorQueryView(
state = rootQuerySnapshot,
onQueryRemove = { rootQuery = null },
requestScrollTo = { target ->
val topYSnapshot = topY
coroutineScope.launch {
scrollState.animateScrollBy(target - topYSnapshot - scrollOffset, spring(stiffness = Spring.StiffnessLow))
}
},
requestScrollBy = { value ->
coroutineScope.launch {
scrollState.animateScrollBy(value)
}
}
requestScrollTo = requestScrollTo,
requestScrollBy = requestScrollBy
)
}
if (rootQuerySnapshot is EditableSearchQueryState.Tag?) {
val newSearchQuery = remember { EditableSearchQueryState.Tag(expanded = true) }
var newQueryNamespace by newSearchQuery.namespace
var newQueryTag by newSearchQuery.tag
var newQueryExpanded by newSearchQuery.expanded
val offset = with (LocalDensity.current) { 40.dp.toPx() }
LaunchedEffect(newQueryExpanded) {
if (!newQueryExpanded && (newQueryNamespace != null || newQueryTag.isNotBlank())) {
rootQuery = if (rootQuerySnapshot == null) {
EditableSearchQueryState.Tag(newQueryNamespace, newQueryTag)
} else {
EditableSearchQueryState.And(listOf(
rootQuerySnapshot,
EditableSearchQueryState.Tag(newQueryNamespace, newQueryTag)
))
}
newQueryNamespace = null
newQueryTag = ""
newQueryExpanded = true
requestScrollBy(offset)
}
}
EditableTagChip(
newSearchQuery,
requestScrollTo = requestScrollTo
)
NewQueryChip(rootQuerySnapshot) { newState ->
rootQuery = coalesceTags(rootQuerySnapshot, newState)
}

View File

@@ -1,9 +1,16 @@
package xyz.quaver.pupil.ui.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.observeOn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import xyz.quaver.pupil.networking.GalleryInfo
import xyz.quaver.pupil.networking.GallerySearchSource
import xyz.quaver.pupil.networking.SearchQuery
import xyz.quaver.pupil.ui.composable.MainDestination
import xyz.quaver.pupil.ui.composable.mainDestinations
@@ -11,6 +18,8 @@ import xyz.quaver.pupil.ui.composable.mainDestinations
class MainViewModel : ViewModel() {
private val _uiState = MutableStateFlow(MainUIState())
val uiState: StateFlow<MainUIState> = _uiState
private var searchSource: GallerySearchSource = GallerySearchSource(null)
private var job: Job? = null
fun navigateToDestination(destination: MainDestination) {
_uiState.value = MainUIState(
@@ -24,6 +33,40 @@ class MainViewModel : ViewModel() {
)
}
fun onQueryChange(query: SearchQuery?) {
_uiState.value = _uiState.value.copy(
query = query,
validRange = IntRange.EMPTY,
currentRange = IntRange.EMPTY
)
searchSource = GallerySearchSource(query)
}
fun loadSearchResult(range: IntRange) {
job?.cancel()
job = viewModelScope.launch {
_uiState.value = _uiState.value.copy(
loading = true,
currentRange = range
)
var error = false
val (galleries, galleryCount) = searchSource.load(range).getOrElse {
error = true
it.printStackTrace()
emptyList<GalleryInfo>() to 0
}
_uiState.value = _uiState.value.copy(
galleries = galleries,
validRange = IntRange(1, galleryCount),
error = error,
loading = false
)
}
}
fun navigateToDetail() {
}
@@ -32,7 +75,11 @@ class MainViewModel : ViewModel() {
data class MainUIState(
val currentDestination: MainDestination = mainDestinations.first(),
val query: SearchQuery? = null,
val loading: Boolean = true,
val galleries: List<GalleryInfo> = emptyList(),
val loading: Boolean = false,
val error: Boolean = false,
val validRange: IntRange = IntRange.EMPTY,
val currentRange: IntRange = IntRange.EMPTY,
val openedGallery: GalleryInfo? = null,
val isDetailOnlyOpen: Boolean = false
)