mirror of
https://github.com/gkd-kit/gkd.git
synced 2024-11-16 11:42:22 +08:00
feat: 订阅页面APP搜索 (#39)
This commit is contained in:
parent
af113619fe
commit
dcfe1ed6af
|
@ -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 {
|
||||
|
|
|
@ -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())
|
||||
|
||||
}
|
118
app/src/main/java/li/songe/gkd/ui/component/AppBarTextField.kt
Normal file
118
app/src/main/java/li/songe/gkd/ui/component/AppBarTextField.kt
Normal 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),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -112,7 +112,6 @@ fun SubsItemCard(
|
|||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "more",
|
||||
modifier = Modifier.size(30.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user