Skip to content

Commit

Permalink
display all search items and query search
Browse files Browse the repository at this point in the history
  • Loading branch information
DatL4g committed Nov 15, 2023
1 parent d885f2a commit 2293ecd
Show file tree
Hide file tree
Showing 12 changed files with 320 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.datlag.burningseries.ui.custom

import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
Expand All @@ -12,7 +13,23 @@ actual fun VerticalScrollbar(adapter: ScrollbarAdapter, modifier: Modifier) {
@Composable
actual fun rememberScrollbarAdapter(
scrollState: LazyGridState,
): ScrollbarAdapter = remember { object : ScrollbarAdapter {
): ScrollbarAdapter = remember { DEFAULT_SCROLLBAR_ADAPTER }

@Composable
actual fun rememberScrollbarAdapter(
scrollState: LazyListState
): ScrollbarAdapter = remember {
DEFAULT_SCROLLBAR_ADAPTER
}

actual interface ScrollbarAdapter {
actual val scrollOffset: Double
actual val contentSize: Double
actual val viewportSize: Double
actual suspend fun scrollTo(scrollOffset: Double)
}

private val DEFAULT_SCROLLBAR_ADAPTER = object : ScrollbarAdapter {
override val scrollOffset: Double
get() = 0.0
override val contentSize: Double
Expand All @@ -21,11 +38,4 @@ actual fun rememberScrollbarAdapter(
get() = 0.0

override suspend fun scrollTo(scrollOffset: Double) { }
} }

actual interface ScrollbarAdapter {
actual val scrollOffset: Double
actual val contentSize: Double
actual val viewportSize: Double
actual suspend fun scrollTo(scrollOffset: Double)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dev.datlag.burningseries.common
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.waitForUpOrCancellation
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyGridItemScope
import androidx.compose.foundation.lazy.grid.LazyGridScope
Expand All @@ -15,6 +16,7 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.DpSize
import dev.datlag.burningseries.ui.theme.shape.DiagonalShape
import kotlin.math.max

inline fun Modifier.ifTrue(predicate: Boolean, builder: Modifier.() -> Modifier) = then(if (predicate) builder() else Modifier)
inline fun Modifier.ifFalse(predicate: Boolean, builder: Modifier.() -> Modifier) = then(if (!predicate) builder() else Modifier)
Expand Down Expand Up @@ -74,4 +76,26 @@ fun DpSize.toSize(): Size {
width = this.width.value,
height = this.height.value
)
}

@Composable
fun LazyListState.OnBottomReached(enabled: Boolean = true, buffer: Int = 0, block: () -> Unit) {
if (enabled) {
val maxBuffer = max(0, buffer)

val shouldCallBlock = remember {
derivedStateOf {
val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull() ?: return@derivedStateOf true
lastVisibleItem.index == layoutInfo.totalItemsCount - 1 - maxBuffer
}
}

LaunchedEffect(shouldCallBlock) {
snapshotFlow { shouldCallBlock.value }.collect {
if (it) {
block()
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.datlag.burningseries.ui.custom

import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
Expand All @@ -13,6 +14,11 @@ expect fun rememberScrollbarAdapter(
scrollState: LazyGridState,
): ScrollbarAdapter

@Composable
expect fun rememberScrollbarAdapter(
scrollState: LazyListState,
): ScrollbarAdapter

expect interface ScrollbarAdapter {
val scrollOffset: Double
val contentSize: Double
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
package dev.datlag.burningseries.ui.screen.initial.search

import dev.datlag.burningseries.model.Genre
import dev.datlag.burningseries.model.state.SearchState
import dev.datlag.burningseries.ui.navigation.Component
import kotlinx.coroutines.flow.StateFlow

interface SearchComponent : Component {

val searchState: StateFlow<SearchState>
val genres: StateFlow<List<Genre>>
val canLoadMoreGenres: StateFlow<Boolean>

val searchItems: StateFlow<List<Genre.Item>>

fun retryLoadingSearch(): Any?
fun loadMoreGenres(): Any?
fun searchQuery(text: String)
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,44 @@
package dev.datlag.burningseries.ui.screen.initial.search

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import dev.datlag.burningseries.common.OnBottomReached
import dev.datlag.burningseries.common.lifecycle.collectAsStateWithLifecycle
import dev.datlag.burningseries.common.onClick
import dev.datlag.burningseries.model.state.SearchState
import dev.datlag.burningseries.shared.SharedRes
import dev.datlag.burningseries.ui.custom.VerticalScrollbar
import dev.datlag.burningseries.ui.custom.rememberScrollbarAdapter
import dev.datlag.burningseries.ui.custom.state.ErrorState
import dev.datlag.burningseries.ui.custom.state.LoadingState
import dev.icerock.moko.resources.compose.stringResource

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SearchScreen(component: SearchComponent) {
val state by component.searchState.collectAsStateWithLifecycle()

when (val current = state) {
when (state) {
is SearchState.Loading -> {
LoadingState(SharedRes.strings.loading_search)
}
Expand All @@ -31,22 +48,116 @@ fun SearchScreen(component: SearchComponent) {
}
}
is SearchState.Success -> {
val selectedGenre = current.genres.first()

LazyColumn(
modifier = Modifier.fillMaxWidth()
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
stickyHeader {
Text(
text = selectedGenre.title,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
val genres by component.genres.collectAsStateWithLifecycle()
val canLoadMore by component.canLoadMoreGenres.collectAsStateWithLifecycle()
val listState = rememberLazyListState()

LazyColumn(
modifier = Modifier.weight(1F),
state = listState
) {
stickyHeader {
SearchBar(component)
}
genres.forEach { genre ->
item {
Text(
modifier = Modifier.fillMaxWidth(),
text = genre.title,
style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center
)
}
items(genre.items) {
Text(
modifier = Modifier.fillMaxWidth().onClick {

}.padding(8.dp),
text = it.title,
softWrap = true,
overflow = TextOverflow.Ellipsis
)
}
}
if (canLoadMore) {
item(key = canLoadMore) {
Row(
modifier = Modifier.fillMaxWidth().padding(16.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
CircularProgressIndicator()
}
}
}
}
items(selectedGenre.items) {
Text(text = it.title)

VerticalScrollbar(rememberScrollbarAdapter(listState))

listState.OnBottomReached(canLoadMore) {
component.loadMoreGenres()
}
}
}
}
}

@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
@Composable
private fun SearchBar(component: SearchComponent) {
val items by component.searchItems.collectAsStateWithLifecycle()
var queryComp by remember { mutableStateOf(String()) }

DockedSearchBar(
query = queryComp,
onQueryChange = {
queryComp = it
},
onSearch = {
queryComp = it
},
modifier = Modifier.fillMaxWidth().padding(8.dp).animateContentSize(),
active = queryComp.isNotBlank() && items.isNotEmpty(),
onActiveChange = {},
leadingIcon = {
Icon(
imageVector = Icons.Default.Search,
contentDescription = stringResource(SharedRes.strings.search)
)
},
trailingIcon = {
AnimatedVisibility(visible = queryComp.isNotBlank()) {
IconButton(
onClick = {
queryComp = String()
}
) {
Icon(
imageVector = Icons.Default.Clear,
contentDescription = stringResource(SharedRes.strings.clear)
)
}
}
}
) {
items.forEach { item ->
Text(
modifier = Modifier.fillMaxWidth().onClick {

}.padding(8.dp),
text = item.title,
softWrap = true,
overflow = TextOverflow.Ellipsis
)
}
}

LaunchedEffect(queryComp) {
component.searchQuery(queryComp)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import com.arkivanov.decompose.ComponentContext
import dev.datlag.burningseries.common.ioDispatcher
import dev.datlag.burningseries.common.ioScope
import dev.datlag.burningseries.common.launchIO
import dev.datlag.burningseries.common.runBlockingIO
import dev.datlag.burningseries.model.Genre
import dev.datlag.burningseries.model.algorithm.JaroWinkler
import dev.datlag.burningseries.model.common.safeSubList
import dev.datlag.burningseries.model.state.SearchAction
import dev.datlag.burningseries.model.state.SearchState
import dev.datlag.burningseries.network.state.SearchStateMachine
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.*
import org.kodein.di.DI
import org.kodein.di.instance

Expand All @@ -23,6 +24,38 @@ class SearchScreenComponent(
private val searchStateMachine: SearchStateMachine by di.instance()
override val searchState: StateFlow<SearchState> = searchStateMachine.state.flowOn(ioDispatcher()).stateIn(ioScope(), SharingStarted.Lazily, SearchState.Loading)

private val allGenres = searchState.mapNotNull { it as SearchState.Success }.map { it.genres }.stateIn(ioScope(), SharingStarted.Lazily, emptyList())
private val allItems = allGenres.map { it.flatMap { g -> g.items } }
private val maxGenres = allGenres.map { it.size }
private val loadedGenres = MutableStateFlow(1)

override val genres: StateFlow<List<Genre>> = combine(allGenres, loadedGenres) { t1, t2 ->
t1.safeSubList(0, t2)
}.stateIn(ioScope(), SharingStarted.Lazily, emptyList())

override val canLoadMoreGenres: StateFlow<Boolean> = combine(loadedGenres, maxGenres) { t1, t2 ->
t1 < t2
}.stateIn(ioScope(), SharingStarted.Lazily, allGenres.value.size > loadedGenres.value)


private val searchQuery: MutableStateFlow<String> = MutableStateFlow(String())
override val searchItems: StateFlow<List<Genre.Item>> = combine(allItems, searchQuery) { t1, t2 ->
if (t2.isBlank()) {
emptyList()
} else {
t1.map {
when {
it.title.equals(t2, true) -> it to 1.0
it.title.startsWith(t2, true) -> it to 0.95
it.title.contains(t2, true) -> it to 0.9
else -> it to JaroWinkler.distance(it.title, t2)
}
}.filter {
it.second > 0.85
}.sortedByDescending { it.second }.map { it.first }.safeSubList(0, 10)
}
}.stateIn(ioScope(), SharingStarted.Lazily, emptyList())

@Composable
override fun render() {
SearchScreen(this)
Expand All @@ -31,4 +64,12 @@ class SearchScreenComponent(
override fun retryLoadingSearch(): Any? = ioScope().launchIO {
searchStateMachine.dispatch(SearchAction.Retry)
}

override fun loadMoreGenres(): Any? = ioScope().launchIO {
loadedGenres.update { it + 1 }
}

override fun searchQuery(text: String) {
searchQuery.value = text.trim()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -210,11 +210,16 @@ private fun CompactScreen(component: SeriesComponent) {
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp)
) {
item {
item(key = current.series.description) {
DescriptionText(current.series.description)
}

item {
item(key = listOf(
current.series.currentSeason,
current.series.currentLanguage,
current.series.seasons,
current.series.languages
)) {
SeasonAndLanguageButtons(
modifier = Modifier.padding(vertical = 16.dp),
selectedSeason = current.series.currentSeason,
Expand Down
Loading

0 comments on commit 2293ecd

Please sign in to comment.