Skip to content

Commit

Permalink
feat: 订阅页面APP搜索 (gkd-kit#39)
Browse files Browse the repository at this point in the history
  • Loading branch information
lisonge committed Oct 24, 2023
1 parent af11361 commit dcfe1ed
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 62 deletions.
123 changes: 63 additions & 60 deletions app/src/main/java/li/songe/gkd/ui/SubsPage.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
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
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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -72,22 +76,19 @@ 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]

// 本地订阅
val editable = subsItem?.id.let { it != null && it < 0 }

var showDetailDlg by remember {
mutableStateOf(false)
}
var showAddDlg by remember {
mutableStateOf(false)
}
Expand All @@ -99,32 +100,63 @@ 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) {
FloatingActionButton(onClick = { showAddDlg = true }) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = "add",
modifier = Modifier.size(30.dp)
)
}
}
Expand Down Expand Up @@ -165,7 +197,11 @@ fun SubsPage(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(text = "此订阅文件暂无规则")
if (searchStr.isNotEmpty()) {
Text(text = "暂无搜索结果")
} else {
Text(text = "此订阅暂无规则")
}
}
}
}
Expand All @@ -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 {
Expand Down
18 changes: 17 additions & 1 deletion app/src/main/java/li/songe/gkd/ui/SubsVm.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ->
Expand All @@ -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 changes: 118 additions & 0 deletions app/src/main/java/li/songe/gkd/ui/component/AppBarTextField.kt
Original file line number Diff line number Diff line change
@@ -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),
)
},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ fun SubsItemCard(
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = "more",
modifier = Modifier.size(30.dp)
)
}
}
Expand Down

0 comments on commit dcfe1ed

Please sign in to comment.