mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-06-23 21:56:05 +02:00
![arkon](/assets/img/avatar_default.png)
Also renaming the helper composables so it's a bit easier to find/replace everything in forks.
315 lines
13 KiB
Kotlin
315 lines
13 KiB
Kotlin
package eu.kanade.presentation.more.settings.screen
|
|
|
|
import androidx.compose.animation.Crossfade
|
|
import androidx.compose.foundation.clickable
|
|
import androidx.compose.foundation.layout.Column
|
|
import androidx.compose.foundation.layout.PaddingValues
|
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
import androidx.compose.foundation.layout.padding
|
|
import androidx.compose.foundation.layout.paddingFromBaseline
|
|
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.text.BasicTextField
|
|
import androidx.compose.foundation.text.KeyboardActions
|
|
import androidx.compose.foundation.text.KeyboardOptions
|
|
import androidx.compose.material.icons.Icons
|
|
import androidx.compose.material.icons.outlined.Close
|
|
import androidx.compose.material3.HorizontalDivider
|
|
import androidx.compose.material3.Icon
|
|
import androidx.compose.material3.IconButton
|
|
import androidx.compose.material3.MaterialTheme
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.material3.TopAppBar
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.DisposableEffect
|
|
import androidx.compose.runtime.LaunchedEffect
|
|
import androidx.compose.runtime.NonRestartableComposable
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.mutableStateOf
|
|
import androidx.compose.runtime.produceState
|
|
import androidx.compose.runtime.remember
|
|
import androidx.compose.runtime.saveable.rememberSaveable
|
|
import androidx.compose.runtime.setValue
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.focus.FocusRequester
|
|
import androidx.compose.ui.focus.focusRequester
|
|
import androidx.compose.ui.graphics.SolidColor
|
|
import androidx.compose.ui.platform.LocalFocusManager
|
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
|
import androidx.compose.ui.text.font.FontWeight
|
|
import androidx.compose.ui.text.input.ImeAction
|
|
import androidx.compose.ui.text.input.TextFieldValue
|
|
import androidx.compose.ui.text.style.TextOverflow
|
|
import androidx.compose.ui.unit.LayoutDirection
|
|
import androidx.compose.ui.unit.dp
|
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
|
import eu.kanade.presentation.components.UpIcon
|
|
import eu.kanade.presentation.more.settings.Preference
|
|
import eu.kanade.presentation.util.Screen
|
|
import tachiyomi.i18n.MR
|
|
import tachiyomi.presentation.core.components.material.Scaffold
|
|
import tachiyomi.presentation.core.i18n.stringResource
|
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
|
import tachiyomi.presentation.core.util.runOnEnterKeyPressed
|
|
import cafe.adriel.voyager.core.screen.Screen as VoyagerScreen
|
|
|
|
class SettingsSearchScreen : Screen() {
|
|
|
|
@Composable
|
|
override fun Content() {
|
|
val navigator = LocalNavigator.currentOrThrow
|
|
val softKeyboardController = LocalSoftwareKeyboardController.current
|
|
val focusManager = LocalFocusManager.current
|
|
val focusRequester = remember { FocusRequester() }
|
|
val listState = rememberLazyListState()
|
|
|
|
// Hide keyboard on change screen
|
|
DisposableEffect(Unit) {
|
|
onDispose {
|
|
softKeyboardController?.hide()
|
|
}
|
|
}
|
|
|
|
// Hide keyboard on outside text field is touched
|
|
LaunchedEffect(listState.isScrollInProgress) {
|
|
if (listState.isScrollInProgress) {
|
|
focusManager.clearFocus()
|
|
}
|
|
}
|
|
|
|
// Request text field focus on launch
|
|
LaunchedEffect(focusRequester) {
|
|
focusRequester.requestFocus()
|
|
}
|
|
|
|
var textFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
|
|
Scaffold(
|
|
topBar = {
|
|
Column {
|
|
TopAppBar(
|
|
navigationIcon = {
|
|
val canPop = remember { navigator.canPop }
|
|
if (canPop) {
|
|
IconButton(onClick = navigator::pop) {
|
|
UpIcon()
|
|
}
|
|
}
|
|
},
|
|
title = {
|
|
BasicTextField(
|
|
value = textFieldValue,
|
|
onValueChange = { textFieldValue = it },
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.focusRequester(focusRequester)
|
|
.runOnEnterKeyPressed(action = focusManager::clearFocus),
|
|
textStyle = MaterialTheme.typography.bodyLarge
|
|
.copy(color = MaterialTheme.colorScheme.onSurface),
|
|
singleLine = true,
|
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
|
keyboardActions = KeyboardActions(onSearch = { focusManager.clearFocus() }),
|
|
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
|
decorationBox = {
|
|
if (textFieldValue.text.isEmpty()) {
|
|
Text(
|
|
text = stringResource(MR.strings.action_search_settings),
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
style = MaterialTheme.typography.bodyLarge,
|
|
)
|
|
}
|
|
it()
|
|
},
|
|
)
|
|
},
|
|
actions = {
|
|
if (textFieldValue.text.isNotEmpty()) {
|
|
IconButton(onClick = { textFieldValue = TextFieldValue() }) {
|
|
Icon(
|
|
imageVector = Icons.Outlined.Close,
|
|
contentDescription = null,
|
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
)
|
|
}
|
|
}
|
|
},
|
|
)
|
|
HorizontalDivider()
|
|
}
|
|
},
|
|
) { contentPadding ->
|
|
SearchResult(
|
|
searchKey = textFieldValue.text,
|
|
listState = listState,
|
|
contentPadding = contentPadding,
|
|
) { result ->
|
|
SearchableSettings.highlightKey = result.highlightKey
|
|
navigator.replace(result.route)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun SearchResult(
|
|
searchKey: String,
|
|
modifier: Modifier = Modifier,
|
|
listState: LazyListState = rememberLazyListState(),
|
|
contentPadding: PaddingValues = PaddingValues(),
|
|
onItemClick: (SearchResultItem) -> Unit,
|
|
) {
|
|
if (searchKey.isEmpty()) return
|
|
|
|
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
|
|
|
|
val index = getIndex()
|
|
val result by produceState<List<SearchResultItem>?>(initialValue = null, searchKey) {
|
|
value = index.asSequence()
|
|
.flatMap { settingsData ->
|
|
settingsData.contents.asSequence()
|
|
// Only search from enabled prefs and one with valid title
|
|
.filter { it.enabled && it.title.isNotBlank() }
|
|
// Flatten items contained inside *enabled* PreferenceGroup
|
|
.flatMap { p ->
|
|
when (p) {
|
|
is Preference.PreferenceGroup -> {
|
|
if (p.enabled) {
|
|
p.preferenceItems.asSequence()
|
|
.filter { it.enabled && it.title.isNotBlank() }
|
|
.map { p.title to it }
|
|
} else {
|
|
emptySequence()
|
|
}
|
|
}
|
|
is Preference.PreferenceItem<*> -> sequenceOf(null to p)
|
|
}
|
|
}
|
|
// Don't show info preference
|
|
.filterNot { it.second is Preference.PreferenceItem.InfoPreference }
|
|
// Filter by search query
|
|
.filter { (_, p) ->
|
|
val inTitle = p.title.contains(searchKey, true)
|
|
val inSummary = p.subtitle?.contains(searchKey, true) ?: false
|
|
inTitle || inSummary
|
|
}
|
|
// Map result data
|
|
.map { (categoryTitle, p) ->
|
|
SearchResultItem(
|
|
route = settingsData.route,
|
|
title = p.title,
|
|
breadcrumbs = getLocalizedBreadcrumb(
|
|
path = settingsData.title,
|
|
node = categoryTitle,
|
|
isLtr = isLtr,
|
|
),
|
|
highlightKey = p.title,
|
|
)
|
|
}
|
|
}
|
|
.take(10) // Just take top 10 result for quicker result
|
|
.toList()
|
|
}
|
|
|
|
Crossfade(
|
|
targetState = result,
|
|
label = "results",
|
|
) {
|
|
when {
|
|
it == null -> {}
|
|
it.isEmpty() -> {
|
|
EmptyScreen(stringResource(MR.strings.no_results_found))
|
|
}
|
|
else -> {
|
|
LazyColumn(
|
|
modifier = modifier.fillMaxSize(),
|
|
state = listState,
|
|
contentPadding = contentPadding,
|
|
horizontalAlignment = Alignment.CenterHorizontally,
|
|
) {
|
|
items(
|
|
items = it,
|
|
key = { i -> i.hashCode() },
|
|
) { item ->
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.clickable { onItemClick(item) }
|
|
.padding(horizontal = 24.dp, vertical = 14.dp),
|
|
) {
|
|
Text(
|
|
text = item.title,
|
|
overflow = TextOverflow.Ellipsis,
|
|
maxLines = 1,
|
|
fontWeight = FontWeight.Normal,
|
|
style = MaterialTheme.typography.titleMedium,
|
|
)
|
|
Text(
|
|
text = item.breadcrumbs,
|
|
modifier = Modifier.paddingFromBaseline(top = 16.dp),
|
|
maxLines = 1,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
style = MaterialTheme.typography.bodySmall,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
@NonRestartableComposable
|
|
private fun getIndex() = settingScreens
|
|
.map { screen ->
|
|
SettingsData(
|
|
title = stringResource(screen.getTitleRes()),
|
|
route = screen,
|
|
contents = screen.getPreferences(),
|
|
)
|
|
}
|
|
|
|
private fun getLocalizedBreadcrumb(path: String, node: String?, isLtr: Boolean): String {
|
|
return if (node == null) {
|
|
path
|
|
} else {
|
|
if (isLtr) {
|
|
// This locale reads left to right.
|
|
"$path > $node"
|
|
} else {
|
|
// This locale reads right to left.
|
|
"$node < $path"
|
|
}
|
|
}
|
|
}
|
|
|
|
private val settingScreens = listOf(
|
|
SettingsAppearanceScreen,
|
|
SettingsLibraryScreen,
|
|
SettingsReaderScreen,
|
|
SettingsDownloadScreen,
|
|
SettingsTrackingScreen,
|
|
SettingsBrowseScreen,
|
|
SettingsDataScreen,
|
|
SettingsSecurityScreen,
|
|
SettingsAdvancedScreen,
|
|
)
|
|
|
|
private data class SettingsData(
|
|
val title: String,
|
|
val route: VoyagerScreen,
|
|
val contents: List<Preference>,
|
|
)
|
|
|
|
private data class SearchResultItem(
|
|
val route: VoyagerScreen,
|
|
val title: String,
|
|
val breadcrumbs: String,
|
|
val highlightKey: String,
|
|
)
|