Skip to content

Commit

Permalink
Implement Search box for SourcesScreen
Browse files Browse the repository at this point in the history
  • Loading branch information
cuong-tran committed Jan 29, 2024
1 parent 82a527b commit 7a28182
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 10 deletions.
159 changes: 150 additions & 9 deletions app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.PushPin
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.outlined.PushPin
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.AlertDialog
Expand All @@ -19,11 +25,24 @@ import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
Expand All @@ -46,10 +65,12 @@ import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.theme.header
import tachiyomi.presentation.core.util.clearFocusOnSoftKeyboardHide
import tachiyomi.presentation.core.util.plus
import tachiyomi.presentation.core.util.runOnEnterKeyPressed
import tachiyomi.presentation.core.util.secondaryItemAlpha
import tachiyomi.source.local.LocalSource
import tachiyomi.source.local.isLocal

Expand All @@ -61,20 +82,29 @@ fun SourcesScreen(
onClickPin: (Source) -> Unit,
onLongClickItem: (Source) -> Unit,
modifier: Modifier = Modifier,
// KMK -->
onChangeSearchQuery: (String?) -> Unit,
// KMK <--
) {
when {
state.isLoading -> LoadingScreen(modifier.padding(contentPadding))
state.isEmpty -> EmptyScreen(
MR.strings.source_empty_screen,
modifier = modifier.padding(contentPadding),
)
else -> {
// KMK -->
// Disable this since a query with empty result will cause empty screen and hide search box
// state.isEmpty -> EmptyScreen(
// MR.strings.source_empty_screen,
// modifier = modifier.padding(contentPadding),
// )
// KMK <--
else -> /* KMK --> */ Column /* KMK <-- */ {
// KMK -->
SearchBox(
searchQuery = state.searchQuery,
onChangeSearchQuery = onChangeSearchQuery,
placeholderText = stringResource(MR.strings.action_source_search),
)

FastScrollLazyColumn(
/*
ScrollbarLazyColumn(
// KMK <--
*/
contentPadding = contentPadding + topSmallPaddingValues,
) {
items(
Expand Down Expand Up @@ -120,6 +150,117 @@ fun SourcesScreen(
}
}

// KMK -->
@Composable
fun SearchBox(
searchQuery: String?,
onChangeSearchQuery: (String?) -> Unit,
modifier: Modifier = Modifier,
placeholderText: String? = null,
) {
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current

val searchAndClearFocus: () -> Unit = f@{
if (searchQuery.isNullOrBlank()) return@f
focusManager.clearFocus()
keyboardController?.hide()
}
val onClickClearSearch: () -> Unit = { onChangeSearchQuery(null) }
val onClickCloseSearch: () -> Unit = {
onClickClearSearch()
focusManager.clearFocus()
keyboardController?.hide()
}

var isFocused by remember { mutableStateOf(false) }

TextField(
value = searchQuery ?: "",
onValueChange = onChangeSearchQuery,
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 12.dp)
.focusRequester(focusRequester)
.onFocusChanged { isFocused = it.isFocused }
.runOnEnterKeyPressed(action = searchAndClearFocus)
.clearFocusOnSoftKeyboardHide(),
enabled = true,
textStyle = MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onBackground,
),
placeholder = {
Text(
modifier = Modifier.secondaryItemAlpha(),
text = (placeholderText ?: stringResource(MR.strings.action_search_hint)),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
leadingIcon = { SearchBoxLeadingIcon(
isFocused,
modifier = Modifier,
onClickCloseSearch,
) },
trailingIcon = { SearchBoxTrailingIcon(
searchQuery.isNullOrEmpty(),
modifier = Modifier,
onClickClearSearch,
) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(onSearch = { searchAndClearFocus() }),
singleLine = true,
shape = RoundedCornerShape(28.dp),
colors = TextFieldDefaults.colors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
errorIndicatorColor = Color.Transparent,
cursorColor = MaterialTheme.colorScheme.onBackground,
),
)
}

@Composable
fun SearchBoxLeadingIcon(
isFocused: Boolean,
@Suppress("UNUSED_PARAMETER") modifier: Modifier = Modifier,
onClickCloseSearch: () -> Unit = {},
) {
if (isFocused)
IconButton(
onClick = onClickCloseSearch,
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Close",
)
}
else
Icon(
imageVector = Icons.Filled.Search,
contentDescription = "Search",
)
}

@Composable
fun SearchBoxTrailingIcon(
isEmpty: Boolean,
@Suppress("UNUSED_PARAMETER") modifier: Modifier = Modifier,
onClickClearSearch: () -> Unit = {},
) {
if (!isEmpty)
IconButton(
onClick = onClickClearSearch,
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = "Clear",
)
}
}
// KMK <--

@Composable
private fun SourceHeader(
language: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,20 @@ import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.source.service.SourcePreferences.DataSaver
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.browse.SourceUiModel
import eu.kanade.presentation.components.SEARCH_DEBOUNCE_MILLIS
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
Expand Down Expand Up @@ -60,6 +64,9 @@ class SourcesScreenModel(
init {
// SY -->
combine(
// KMK -->
state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS),
// KMK <--
getEnabledSources.subscribe(),
getSourceCategories.subscribe(),
getShowLatest.subscribe(smartSearchConfig != null),
Expand All @@ -85,7 +92,21 @@ class SourcesScreenModel(
// SY <--
}

private fun collectLatestSources(sources: List<Source>, categories: List<String>, showLatest: Boolean, showPin: Boolean) {
private fun collectLatestSources(/* KMK --> */ searchQuery: String?, @Suppress("LocalVariableName") _sources /* KMK <-- */: List<Source>, categories: List<String>, showLatest: Boolean, showPin: Boolean) {
// KMK -->
val queryFilter: (String?) -> ((Source) -> Boolean) = { query ->
filter@{ source ->
if (query.isNullOrBlank()) return@filter true
query.split(",").any {
val input = it.trim()
if (input.isEmpty()) return@any false
source.name.contains(input, ignoreCase = true) ||
source.id == input.toLongOrNull()
}
}
}
val sources = _sources.filter(queryFilter(searchQuery))
// KMK <--
mutableState.update { state ->
val map = TreeMap<String, MutableList<Source>> { d1, d2 ->
// Sources without a lang defined will be placed at the end
Expand Down Expand Up @@ -159,6 +180,14 @@ class SourcesScreenModel(
}
// SY <--

// KMK -->
fun search(query: String?) {
mutableState.update {
it.copy(searchQuery = query)
}
}
// KMK <--

fun showSourceDialog(source: Source) {
mutableState.update { it.copy(dialog = Dialog.SourceLongClick(source)) }
}
Expand Down Expand Up @@ -187,6 +216,9 @@ class SourcesScreenModel(
val showLatest: Boolean = false,
val dataSaverEnabled: Boolean = false,
// SY <--
// KMK -->
val searchQuery: String? = null,
// KMK <--
) {
val isEmpty = items.isEmpty()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ fun Screen.sourcesTab(
},
onClickPin = screenModel::togglePin,
onLongClickItem = screenModel::showSourceDialog,
// KMK -->
onChangeSearchQuery = screenModel::search,
// KMK <--
)

when (val dialog = state.dialog) {
Expand Down
1 change: 1 addition & 0 deletions i18n/src/commonMain/resources/MR/base/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
<string name="action_search_hint">Search…</string>
<string name="action_search_settings">Search settings</string>
<string name="action_global_search">Global search</string>
<string name="action_source_search">Search for source</string>
<string name="action_select_all">Select all</string>
<string name="action_select_inverse">Select inverse</string>
<string name="action_mark_as_read">Mark as read</string>
Expand Down

0 comments on commit 7a28182

Please sign in to comment.