feat: 订阅页面APP搜索 (#39)

This commit is contained in:
lisonge 2023-10-24 17:25:46 +08:00
parent af113619fe
commit dcfe1ed6af
4 changed files with 198 additions and 62 deletions

View File

@ -1,7 +1,5 @@
package li.songe.gkd.ui
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -9,12 +7,14 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.FloatingActionButton
@ -25,17 +25,22 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.hilt.navigation.compose.hiltViewModel
@ -50,20 +55,19 @@ import kotlinx.serialization.encodeToString
import li.songe.gkd.data.SubsConfig
import li.songe.gkd.data.SubscriptionRaw
import li.songe.gkd.db.DbSet
import li.songe.gkd.ui.component.SimpleTopAppBar
import li.songe.gkd.ui.component.AppBarTextField
import li.songe.gkd.ui.component.SubsAppCard
import li.songe.gkd.ui.destinations.AppItemPageDestination
import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.ProfileTransitions
import li.songe.gkd.util.SafeR
import li.songe.gkd.util.Singleton
import li.songe.gkd.util.appInfoCacheFlow
import li.songe.gkd.util.formatTimeAgo
import li.songe.gkd.util.launchAsFn
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.navigate
import li.songe.gkd.util.subsIdToRawFlow
@RootNavGraph
@Destination(style = ProfileTransitions::class)
@Composable
@ -72,12 +76,12 @@ fun SubsPage(
) {
val scope = rememberCoroutineScope()
val navController = LocalNavController.current
val context = LocalContext.current
val vm = hiltViewModel<SubsVm>()
val subsItem by vm.subsItemFlow.collectAsState()
val subsIdToRaw by subsIdToRawFlow.collectAsState()
val appAndConfigs by vm.appAndConfigsFlow.collectAsState()
val appAndConfigs by vm.filterAppAndConfigsFlow.collectAsState()
val searchStr by vm.searchStrFlow.collectAsState()
val appInfoCache by appInfoCacheFlow.collectAsState()
val subsRaw = subsIdToRaw[subsItem?.id]
@ -85,9 +89,6 @@ fun SubsPage(
// 本地订阅
val editable = subsItem?.id.let { it != null && it < 0 }
var showDetailDlg by remember {
mutableStateOf(false)
}
var showAddDlg by remember {
mutableStateOf(false)
}
@ -99,24 +100,56 @@ fun SubsPage(
mutableStateOf<SubscriptionRaw.AppRaw?>(null)
}
var showSearchBar by rememberSaveable {
mutableStateOf(false)
}
val focusRequester = remember { FocusRequester() }
LaunchedEffect(key1 = showSearchBar, block = {
if (showSearchBar && searchStr.isEmpty()) {
focusRequester.requestFocus()
}
})
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
SimpleTopAppBar(onClickIcon = { navController.popBackStack() },
title = subsRaw?.name ?: subsItem?.id.toString(),
actions = {
TopAppBar(scrollBehavior = scrollBehavior, navigationIcon = {
IconButton(onClick = {
navController.popBackStack()
}) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = null,
)
}
}, title = {
if (showSearchBar) {
AppBarTextField(
value = searchStr,
onValueChange = { newValue -> vm.searchStrFlow.value = newValue.trim() },
hint = "请输入应用名称",
modifier = Modifier.focusRequester(focusRequester)
)
} else {
Text(text = subsRaw?.name ?: subsItem?.id.toString())
}
}, actions = {
if (showSearchBar) {
IconButton(onClick = {
if (subsRaw != null) {
showDetailDlg = true
}
showSearchBar = false
vm.searchStrFlow.value = ""
}) {
Icon(
painter = painterResource(SafeR.ic_info),
contentDescription = "info",
modifier = Modifier.size(30.dp)
)
Icon(Icons.Outlined.Close, contentDescription = null)
}
})
} else {
IconButton(onClick = {
showSearchBar = true
}) {
Icon(Icons.Outlined.Search, contentDescription = null)
}
}
})
},
floatingActionButton = {
if (editable) {
@ -124,7 +157,6 @@ fun SubsPage(
Icon(
imageVector = Icons.Filled.Add,
contentDescription = "add",
modifier = Modifier.size(30.dp)
)
}
}
@ -165,7 +197,11 @@ fun SubsPage(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(text = "此订阅文件暂无规则")
if (searchStr.isNotEmpty()) {
Text(text = "暂无搜索结果")
} else {
Text(text = "此订阅暂无规则")
}
}
}
}
@ -177,39 +213,6 @@ fun SubsPage(
}
val subsItemVal = subsItem
if (showDetailDlg && subsRaw != null) {
AlertDialog(onDismissRequest = { showDetailDlg = false }, title = {
Text(text = "订阅详情")
}, text = {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier
) {
Text(text = "名称: " + subsRaw.name)
Text(text = "版本: " + subsRaw.version)
if (subsRaw.author != null) {
Text(text = "作者: " + subsRaw.author)
}
val apps = subsRaw.apps
val groupsSize = apps.sumOf { it.groups.size }
if (groupsSize > 0) {
Text(text = "规则: ${apps.size}应用/${groupsSize}规则组")
}
Text(text = "更新: " + formatTimeAgo(subsItem!!.mtime))
}
}, confirmButton = {
if (subsRaw.supportUri != null) {
TextButton(onClick = {
context.startActivity(
Intent(
Intent.ACTION_VIEW, Uri.parse(subsRaw.supportUri)
)
)
}) {
Text(text = "问题反馈")
}
}
})
}
if (showAddDlg && subsRaw != null && subsItemVal != null) {
var source by remember {

View File

@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
@ -44,7 +45,9 @@ class SubsVm @Inject constructor(stateHandle: SavedStateHandle) : ViewModel() {
}
}.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
val appAndConfigsFlow = combine(appsFlow,
val searchStrFlow = MutableStateFlow("")
private val appAndConfigsFlow = combine(appsFlow,
appSubsConfigsFlow,
groupSubsConfigsFlow,
storeFlow.map(viewModelScope) { s -> s.enableGroup }) { apps, appSubsConfigs, groupSubsConfigs, enableGroup ->
@ -59,5 +62,18 @@ class SubsVm @Inject constructor(stateHandle: SavedStateHandle) : ViewModel() {
}
}.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
val filterAppAndConfigsFlow = combine(
appAndConfigsFlow, searchStrFlow, appInfoCacheFlow
) { appAndConfigs, searchStr, appInfoCache ->
if (searchStr.isBlank()) {
appAndConfigs
} else {
appAndConfigs.filter { a ->
(appInfoCache[a.t0.id]?.name ?: a.t0.name ?: a.t0.id).contains(
searchStr
)
}
}
}.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
}

View File

@ -0,0 +1,118 @@
package li.songe.gkd.ui.component
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TextFieldDefaults.indicatorLine
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
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.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
/**
* https://stackoverflow.com/questions/73664765
*/
@Composable
fun AppBarTextField(
value: String,
onValueChange: (String) -> Unit,
hint: String,
modifier: Modifier = Modifier,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
) {
val interactionSource = remember { MutableInteractionSource() }
val textStyle = LocalTextStyle.current
// make sure there is no background color in the decoration box
val colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Unspecified,
unfocusedContainerColor = Color.Unspecified,
disabledContainerColor = Color.Unspecified,
)
// If color is not provided via the text style, use content color as a default
val textColor = textStyle.color.takeOrElse {
MaterialTheme.colorScheme.onSurface
}
val mergedTextStyle = textStyle.merge(TextStyle(color = textColor, lineHeight = 50.sp))
// request focus when this composable is first initialized
// val focusRequester = FocusRequester()
// SideEffect {
// focusRequester.requestFocus()
// }
// set the correct cursor position when this composable is first initialized
var textFieldValue by remember {
mutableStateOf(TextFieldValue(value, TextRange(value.length)))
}
textFieldValue = textFieldValue.copy(text = value) // make sure to keep the value updated
CompositionLocalProvider(
LocalTextSelectionColors provides LocalTextSelectionColors.current
) {
BasicTextField(
value = textFieldValue,
onValueChange = {
textFieldValue = it
// remove newlines to avoid strange layout issues, and also because singleLine=true
onValueChange(it.text.replace("\n", ""))
},
modifier = modifier
.fillMaxWidth()
.heightIn(32.dp)
.indicatorLine(
enabled = true,
isError = false,
interactionSource = interactionSource,
colors = colors
),
// .focusRequester(focusRequester),
textStyle = mergedTextStyle,
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
interactionSource = interactionSource,
singleLine = true,
decorationBox = { innerTextField ->
// places text field with placeholder and appropriate bottom padding
TextFieldDefaults.DecorationBox(
value = value,
innerTextField = innerTextField,
enabled = true,
singleLine = true,
visualTransformation = VisualTransformation.None,
interactionSource = interactionSource,
isError = false,
placeholder = { Text(text = hint) },
colors = colors,
contentPadding = PaddingValues(bottom = 4.dp),
)
},
)
}
}

View File

@ -112,7 +112,6 @@ fun SubsItemCard(
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = "more",
modifier = Modifier.size(30.dp)
)
}
}