refactor: ui, padding, text

This commit is contained in:
lisonge 2024-05-19 00:37:25 +08:00
parent 96ac199f1a
commit 30da7f6ccb
26 changed files with 582 additions and 555 deletions

View File

@ -4,6 +4,10 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.blankj.utilcode.util.LogUtils import com.blankj.utilcode.util.LogUtils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import li.songe.gkd.data.RawSubscription import li.songe.gkd.data.RawSubscription
import li.songe.gkd.data.SubsItem import li.songe.gkd.data.SubsItem
@ -12,6 +16,7 @@ import li.songe.gkd.permission.authReasonFlow
import li.songe.gkd.util.checkUpdate import li.songe.gkd.util.checkUpdate
import li.songe.gkd.util.launchTry import li.songe.gkd.util.launchTry
import li.songe.gkd.util.logZipDir import li.songe.gkd.util.logZipDir
import li.songe.gkd.util.map
import li.songe.gkd.util.newVersionApkDir import li.songe.gkd.util.newVersionApkDir
import li.songe.gkd.util.snapshotZipDir import li.songe.gkd.util.snapshotZipDir
import li.songe.gkd.util.storeFlow import li.songe.gkd.util.storeFlow
@ -60,8 +65,25 @@ class MainViewModel : ViewModel() {
} }
} }
} }
viewModelScope.launch {
storeFlow.map(viewModelScope) { s -> s.log2FileSwitch }.collect {
LogUtils.getConfig().isLog2FileSwitch = it
}
}
} }
val enableDarkThemeFlow = storeFlow.debounce(200).map { s -> s.enableDarkTheme }.stateIn(
viewModelScope,
SharingStarted.Eagerly,
storeFlow.value.enableDarkTheme
)
val enableDynamicColorFlow = storeFlow.debounce(300).map { s -> s.enableDynamicColor }.stateIn(
viewModelScope,
SharingStarted.Eagerly,
storeFlow.value.enableDynamicColor
)
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()

View File

@ -45,6 +45,23 @@ data class RawSubscription(
} }
} }
val categoryToAppMap by lazy {
val map = mutableMapOf<RawCategory, MutableList<RawApp>>()
categories.forEach { c ->
apps.forEach { a ->
if (a.groups.any { g -> g.name.startsWith(c.name) }) {
val list = map[c]
if (list == null) {
map[c] = mutableListOf(a)
} else {
list.add(a)
}
}
}
}
map
}
val groupToCategoryMap by lazy { val groupToCategoryMap by lazy {
val map = mutableMapOf<RawAppGroup, RawCategory>() val map = mutableMapOf<RawAppGroup, RawCategory>()
categoryToGroupsMap.forEach { (key, value) -> categoryToGroupsMap.forEach { (key, value) ->

View File

@ -47,7 +47,7 @@ import li.songe.gkd.shizuku.shizukuIsSafeOK
import li.songe.gkd.shizuku.useSafeGetTasksFc import li.songe.gkd.shizuku.useSafeGetTasksFc
import li.songe.gkd.shizuku.useSafeInputTapFc import li.songe.gkd.shizuku.useSafeInputTapFc
import li.songe.gkd.shizuku.useShizukuAliveState import li.songe.gkd.shizuku.useShizukuAliveState
import li.songe.gkd.ui.home.UpdateTimeOption import li.songe.gkd.util.UpdateTimeOption
import li.songe.gkd.util.VOLUME_CHANGED_ACTION import li.songe.gkd.util.VOLUME_CHANGED_ACTION
import li.songe.gkd.util.checkSubsUpdate import li.songe.gkd.util.checkSubsUpdate
import li.songe.gkd.util.launchTry import li.songe.gkd.util.launchTry

View File

@ -25,6 +25,7 @@ import androidx.compose.ui.unit.dp
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph import com.ramcosta.composedestinations.annotation.RootNavGraph
import li.songe.gkd.BuildConfig import li.songe.gkd.BuildConfig
import li.songe.gkd.ui.style.itemPadding
import li.songe.gkd.util.GIT_COMMIT_URL import li.songe.gkd.util.GIT_COMMIT_URL
import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.ProfileTransitions import li.songe.gkd.util.ProfileTransitions
@ -71,7 +72,7 @@ fun AboutPage() {
context.openUri(REPOSITORY_URL) context.openUri(REPOSITORY_URL)
} }
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp, 12.dp) .itemPadding()
) { ) {
Text( Text(
text = "开源地址", text = "开源地址",
@ -86,7 +87,7 @@ fun AboutPage() {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp, 12.dp) .itemPadding()
) { ) {
Text( Text(
text = "版本代码", text = "版本代码",
@ -101,7 +102,7 @@ fun AboutPage() {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp, 12.dp) .itemPadding()
) { ) {
Text( Text(
text = "版本名称", text = "版本名称",
@ -120,7 +121,7 @@ fun AboutPage() {
context.openUri(GIT_COMMIT_URL) context.openUri(GIT_COMMIT_URL)
} }
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp, 12.dp) .itemPadding()
) { ) {
Text( Text(
text = "代码记录", text = "代码记录",
@ -136,7 +137,7 @@ fun AboutPage() {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp, 12.dp) .itemPadding()
) { ) {
Text( Text(
text = "构建时间", text = "构建时间",
@ -151,7 +152,7 @@ fun AboutPage() {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp, 12.dp) .itemPadding()
) { ) {
Text( Text(
text = "构建类型", text = "构建类型",

View File

@ -73,6 +73,7 @@ import li.songe.gkd.ui.component.AuthCard
import li.songe.gkd.ui.component.SettingItem import li.songe.gkd.ui.component.SettingItem
import li.songe.gkd.ui.component.TextSwitch import li.songe.gkd.ui.component.TextSwitch
import li.songe.gkd.ui.destinations.SnapshotPageDestination import li.songe.gkd.ui.destinations.SnapshotPageDestination
import li.songe.gkd.ui.style.itemPadding
import li.songe.gkd.util.LocalLauncher import li.songe.gkd.util.LocalLauncher
import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.ProfileTransitions import li.songe.gkd.util.ProfileTransitions
@ -124,7 +125,7 @@ fun AdvancedPage() {
) { ) {
Text( Text(
text = "Shizuku", text = "Shizuku",
modifier = Modifier.padding(16.dp, 12.dp), modifier = Modifier.itemPadding(),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
) )
@ -150,12 +151,12 @@ fun AdvancedPage() {
Text( Text(
text = "HTTP服务", text = "HTTP服务",
modifier = Modifier.padding(16.dp, 12.dp), modifier = Modifier.itemPadding(),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
) )
Row( Row(
modifier = Modifier.padding(16.dp, 14.dp), modifier = Modifier.itemPadding(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
@ -215,7 +216,7 @@ fun AdvancedPage() {
Row( Row(
modifier = Modifier modifier = Modifier
.clickable { showPortDlg = true } .clickable { showPortDlg = true }
.padding(16.dp, 12.dp), .itemPadding(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
@ -245,7 +246,7 @@ fun AdvancedPage() {
Text( Text(
text = "快照", text = "快照",
modifier = Modifier.padding(16.dp, 12.dp), modifier = Modifier.itemPadding(),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
) )
@ -335,7 +336,7 @@ fun AdvancedPage() {
Text( Text(
text = "其它", text = "其它",
modifier = Modifier.padding(16.dp, 12.dp), modifier = Modifier.itemPadding(),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
) )

View File

@ -25,7 +25,6 @@ import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@ -58,8 +57,10 @@ import li.songe.gkd.data.stringify
import li.songe.gkd.db.DbSet import li.songe.gkd.db.DbSet
import li.songe.gkd.ui.destinations.AppItemPageDestination import li.songe.gkd.ui.destinations.AppItemPageDestination
import li.songe.gkd.ui.destinations.GlobalRulePageDestination import li.songe.gkd.ui.destinations.GlobalRulePageDestination
import li.songe.gkd.ui.style.itemPadding
import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.ProfileTransitions import li.songe.gkd.util.ProfileTransitions
import li.songe.gkd.util.RuleSortOption
import li.songe.gkd.util.appInfoCacheFlow import li.songe.gkd.util.appInfoCacheFlow
import li.songe.gkd.util.launchTry import li.songe.gkd.util.launchTry
import li.songe.gkd.util.navigate import li.songe.gkd.util.navigate
@ -80,7 +81,6 @@ fun AppConfigPage(appId: String) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
val listState = rememberLazyListState() val listState = rememberLazyListState()
var isFirstVisit by remember { mutableStateOf(true) } var isFirstVisit by remember { mutableStateOf(true) }
globalGroups.map { g -> g.group }
LaunchedEffect(globalGroups.size, appGroups.size, ruleSortType.value) { LaunchedEffect(globalGroups.size, appGroups.size, ruleSortType.value) {
if (isFirstVisit) { if (isFirstVisit) {
isFirstVisit = false isFirstVisit = false
@ -124,7 +124,7 @@ fun AppConfigPage(appId: String) {
expanded = expanded, expanded = expanded,
onDismissRequest = { expanded = false } onDismissRequest = { expanded = false }
) { ) {
RuleSortType.allSubObject.forEach { s -> RuleSortOption.allSubObject.forEach { s ->
DropdownMenuItem( DropdownMenuItem(
text = { text = {
Row( Row(
@ -250,7 +250,7 @@ private fun AppGroupCard(
Row( Row(
modifier = Modifier modifier = Modifier
.clickable(onClick = onClick) .clickable(onClick = onClick)
.padding(10.dp, 6.dp), .itemPadding(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
@ -265,7 +265,8 @@ private fun AppGroupCard(
maxLines = 1, maxLines = 1,
softWrap = false, softWrap = false,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodyLarge
) )
if (group.valid) { if (group.valid) {
if (!group.desc.isNullOrBlank()) { if (!group.desc.isNullOrBlank()) {
@ -275,14 +276,15 @@ private fun AppGroupCard(
softWrap = false, softWrap = false,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
} else { } else {
Text( Text(
text = "暂无描述", text = "暂无描述",
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = LocalContentColor.current.copy(alpha = 0.5f) color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
) )
} }
} else { } else {

View File

@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.stateIn
import li.songe.gkd.data.SubsConfig import li.songe.gkd.data.SubsConfig
import li.songe.gkd.db.DbSet import li.songe.gkd.db.DbSet
import li.songe.gkd.ui.destinations.AppConfigPageDestination import li.songe.gkd.ui.destinations.AppConfigPageDestination
import li.songe.gkd.util.RuleSortOption
import li.songe.gkd.util.collator import li.songe.gkd.util.collator
import li.songe.gkd.util.ruleSummaryFlow import li.songe.gkd.util.ruleSummaryFlow
import javax.inject.Inject import javax.inject.Inject
@ -29,7 +30,7 @@ class AppConfigVm @Inject constructor(stateHandle: SavedStateHandle) : ViewModel
SubsConfig.AppGroupType SubsConfig.AppGroupType
) )
val ruleSortTypeFlow = MutableStateFlow<RuleSortType>(RuleSortType.Default) val ruleSortTypeFlow = MutableStateFlow<RuleSortOption>(RuleSortOption.Default)
val globalGroupsFlow = combine( val globalGroupsFlow = combine(
ruleSummaryFlow.map { r -> r.globalGroups }, ruleSummaryFlow.map { r -> r.globalGroups },
@ -37,15 +38,15 @@ class AppConfigVm @Inject constructor(stateHandle: SavedStateHandle) : ViewModel
latestGlobalLogsFlow latestGlobalLogsFlow
) { list, type, logs -> ) { list, type, logs ->
when (type) { when (type) {
RuleSortType.Default -> list RuleSortOption.Default -> list
RuleSortType.ByName -> list.sortedWith { a, b -> RuleSortOption.ByName -> list.sortedWith { a, b ->
collator.compare( collator.compare(
a.group.name, a.group.name,
b.group.name b.group.name
) )
} }
RuleSortType.ByTime -> list.sortedBy { a -> RuleSortOption.ByTime -> list.sortedBy { a ->
-(logs.find { c -> c.groupKey == a.group.key && c.subsId == a.subsItem.id }?.id -(logs.find { c -> c.groupKey == a.group.key && c.subsId == a.subsItem.id }?.id
?: 0) ?: 0)
} }
@ -58,15 +59,15 @@ class AppConfigVm @Inject constructor(stateHandle: SavedStateHandle) : ViewModel
latestAppLogsFlow latestAppLogsFlow
) { list, type, logs -> ) { list, type, logs ->
when (type) { when (type) {
RuleSortType.Default -> list RuleSortOption.Default -> list
RuleSortType.ByName -> list.sortedWith { a, b -> RuleSortOption.ByName -> list.sortedWith { a, b ->
collator.compare( collator.compare(
a.group.name, a.group.name,
b.group.name b.group.name
) )
} }
RuleSortType.ByTime -> list.sortedBy { a -> RuleSortOption.ByTime -> list.sortedBy { a ->
-(logs.find { c -> c.groupKey == a.group.key && c.subsId == a.subsItem.id }?.id -(logs.find { c -> c.groupKey == a.group.key && c.subsId == a.subsItem.id }?.id
?: 0) ?: 0)
} }
@ -75,12 +76,3 @@ class AppConfigVm @Inject constructor(stateHandle: SavedStateHandle) : ViewModel
} }
sealed class RuleSortType(val value: Int, val label: String) {
data object Default : RuleSortType(0, "按订阅顺序")
data object ByTime : RuleSortType(1, "按触发时间")
data object ByName : RuleSortType(2, "按名称")
companion object {
val allSubObject by lazy { arrayOf(Default, ByTime, ByName) }
}
}

View File

@ -27,7 +27,6 @@ import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@ -68,6 +67,7 @@ import li.songe.gkd.data.stringify
import li.songe.gkd.db.DbSet import li.songe.gkd.db.DbSet
import li.songe.gkd.ui.component.getDialogResult import li.songe.gkd.ui.component.getDialogResult
import li.songe.gkd.ui.destinations.GroupItemPageDestination import li.songe.gkd.ui.destinations.GroupItemPageDestination
import li.songe.gkd.ui.style.itemPadding
import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.ProfileTransitions import li.songe.gkd.util.ProfileTransitions
import li.songe.gkd.util.appInfoCacheFlow import li.songe.gkd.util.appInfoCacheFlow
@ -166,7 +166,7 @@ fun AppItemPage(
if (group.key == focusGroupKey) MaterialTheme.colorScheme.inversePrimary else Color.Transparent if (group.key == focusGroupKey) MaterialTheme.colorScheme.inversePrimary else Color.Transparent
) )
.clickable { setShowGroupItem(group) } .clickable { setShowGroupItem(group) }
.padding(10.dp, 6.dp), .itemPadding(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
@ -181,7 +181,8 @@ fun AppItemPage(
maxLines = 1, maxLines = 1,
softWrap = false, softWrap = false,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodyLarge,
) )
if (group.valid) { if (group.valid) {
if (!group.desc.isNullOrBlank()) { if (!group.desc.isNullOrBlank()) {
@ -191,21 +192,22 @@ fun AppItemPage(
softWrap = false, softWrap = false,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
fontSize = 14.sp style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
} else { } else {
Text( Text(
text = "暂无描述", text = "暂无描述",
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
fontSize = 14.sp, style = MaterialTheme.typography.bodyMedium,
color = LocalContentColor.current.copy(alpha = 0.5f) color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
) )
} }
} else { } else {
Text( Text(
text = "非法选择器", text = "非法选择器",
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
fontSize = 14.sp, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error color = MaterialTheme.colorScheme.error
) )
} }
@ -343,13 +345,18 @@ fun AppItemPage(
} }
showGroupItem?.let { showGroupItemVal -> showGroupItem?.let { showGroupItemVal ->
AlertDialog(modifier = Modifier.defaultMinSize(300.dp), AlertDialog(
modifier = Modifier.defaultMinSize(300.dp),
onDismissRequest = { setShowGroupItem(null) }, onDismissRequest = { setShowGroupItem(null) },
title = { title = {
Text(text = showGroupItemVal.name) Text(text = "规则组详情")
}, },
text = { text = {
Text(text = showGroupItemVal.desc ?: "") Column {
Text(text = showGroupItemVal.name)
Spacer(modifier = Modifier.height(10.dp))
Text(text = showGroupItemVal.desc ?: "")
}
}, },
confirmButton = { confirmButton = {
if (showGroupItemVal.allExampleUrls.isNotEmpty()) { if (showGroupItemVal.allExampleUrls.isNotEmpty()) {

View File

@ -12,25 +12,19 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.UnfoldMore
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@ -47,8 +41,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
@ -58,18 +50,15 @@ import li.songe.gkd.data.CategoryConfig
import li.songe.gkd.data.RawSubscription import li.songe.gkd.data.RawSubscription
import li.songe.gkd.db.DbSet import li.songe.gkd.db.DbSet
import li.songe.gkd.ui.component.getDialogResult import li.songe.gkd.ui.component.getDialogResult
import li.songe.gkd.ui.style.itemPadding
import li.songe.gkd.util.EnableGroupOption
import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.ProfileTransitions import li.songe.gkd.util.ProfileTransitions
import li.songe.gkd.util.findOption
import li.songe.gkd.util.launchTry import li.songe.gkd.util.launchTry
import li.songe.gkd.util.toast import li.songe.gkd.util.toast
import li.songe.gkd.util.updateSubscription import li.songe.gkd.util.updateSubscription
val enableGroupRadioOptions = arrayOf(
"跟随订阅" to null,
"全部启用" to true,
"全部关闭" to false,
)
@RootNavGraph @RootNavGraph
@Destination(style = ProfileTransitions::class) @Destination(style = ProfileTransitions::class)
@Composable @Composable
@ -84,15 +73,13 @@ fun CategoryPage(subsItemId: Long) {
var showAddDlg by remember { var showAddDlg by remember {
mutableStateOf(false) mutableStateOf(false)
} }
var editEnableCategory by remember {
mutableStateOf<RawSubscription.RawCategory?>(null)
}
val (editNameCategory, setEditNameCategory) = remember { val (editNameCategory, setEditNameCategory) = remember {
mutableStateOf<RawSubscription.RawCategory?>(null) mutableStateOf<RawSubscription.RawCategory?>(null)
} }
val categories = subsRaw?.categories ?: emptyList() val categories = subsRaw?.categories ?: emptyList()
val categoriesGroups = subsRaw?.categoryToGroupsMap ?: emptyMap() val categoriesGroups = subsRaw?.categoryToGroupsMap ?: emptyMap()
val categoriesApps = subsRaw?.categoryToAppMap ?: emptyMap()
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = {
@ -120,35 +107,36 @@ fun CategoryPage(subsItemId: Long) {
modifier = Modifier.padding(contentPadding) modifier = Modifier.padding(contentPadding)
) { ) {
items(categories, { it.key }) { category -> items(categories, { it.key }) { category ->
var selectedExpanded by remember { mutableStateOf(false) }
Row(modifier = Modifier Row(modifier = Modifier
.clickable { .clickable { selectedExpanded = true }
editEnableCategory = category .itemPadding(),
} verticalAlignment = Alignment.CenterVertically) {
.padding(10.dp, 6.dp), verticalAlignment = Alignment.CenterVertically
) {
val size = categoriesGroups[category]?.size ?: 0 val size = categoriesGroups[category]?.size ?: 0
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
text = category.name, fontSize = 18.sp text = category.name,
style = MaterialTheme.typography.bodyLarge,
) )
if (size > 0) { if (size > 0) {
val appSize = categoriesApps[category]?.size ?: 0
Text( Text(
text = "${size}规则组", text = "${appSize}应用/${size}规则组",
fontSize = 14.sp style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
} else { } else {
Text( Text(
text = "暂无规则", text = "暂无规则",
fontSize = 14.sp, style = MaterialTheme.typography.bodyMedium,
color = LocalContentColor.current.copy(alpha = 0.5f) color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
) )
} }
} }
if (editable) { if (editable) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
Box( Box(
modifier = Modifier modifier = Modifier.wrapContentSize(Alignment.TopStart)
.wrapContentSize(Alignment.TopStart)
) { ) {
IconButton(onClick = { IconButton(onClick = {
expanded = true expanded = true
@ -158,45 +146,33 @@ fun CategoryPage(subsItemId: Long) {
contentDescription = null, contentDescription = null,
) )
} }
DropdownMenu( DropdownMenu(expanded = expanded,
expanded = expanded, onDismissRequest = { expanded = false }) {
onDismissRequest = { expanded = false } DropdownMenuItem(text = {
) { Text(text = "编辑")
DropdownMenuItem( }, onClick = {
text = { expanded = false
Text(text = "编辑") setEditNameCategory(category)
}, })
onClick = { DropdownMenuItem(text = {
expanded = false Text(text = "删除", color = MaterialTheme.colorScheme.error)
setEditNameCategory(category) }, onClick = {
} expanded = false
) vm.viewModelScope.launchTry {
DropdownMenuItem( val result = getDialogResult(
text = { "删除类别", "是否删除类别 ${category.name} ?"
Text(text = "删除", color = MaterialTheme.colorScheme.error) )
}, if (!result) return@launchTry
onClick = { subsItem?.apply {
expanded = false updateSubscription(subsRaw!!.copy(categories = subsRaw!!.categories.filter { c -> c.key != category.key }))
vm.viewModelScope.launchTry { DbSet.subsItemDao.update(copy(mtime = System.currentTimeMillis()))
val result = getDialogResult(
"删除类别",
"是否删除类别 ${category.name} ?"
)
if (!result) return@launchTry
subsItem?.apply {
updateSubscription(subsRaw!!.copy(
categories = subsRaw!!.categories.filter { c -> c.key != category.key }
))
DbSet.subsItemDao.update(copy(mtime = System.currentTimeMillis()))
}
DbSet.categoryConfigDao.deleteByCategoryKey(
subsItemId,
category.key
)
toast("删除成功")
} }
DbSet.categoryConfigDao.deleteByCategoryKey(
subsItemId, category.key
)
toast("删除成功")
} }
) })
} }
} }
@ -210,19 +186,38 @@ fun CategoryPage(subsItemId: Long) {
val enable = val enable =
if (categoryConfig != null) categoryConfig.enable else category.enable if (categoryConfig != null) categoryConfig.enable else category.enable
Text( Text(
text = enableGroupRadioOptions.find { e -> e.second == enable }?.first text = EnableGroupOption.allSubObject.findOption(enable).label,
?: "", style = MaterialTheme.typography.bodyMedium,
fontSize = 14.sp
) )
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, imageVector = Icons.Default.UnfoldMore, contentDescription = null
contentDescription = null
) )
DropdownMenu(expanded = selectedExpanded,
onDismissRequest = { selectedExpanded = false }) {
EnableGroupOption.allSubObject.forEach { option ->
DropdownMenuItem(
text = {
Text(text = option.label)
},
onClick = {
selectedExpanded = false
if (option.value != enable) {
vm.viewModelScope.launchTry(Dispatchers.IO) {
DbSet.categoryConfigDao.insert(
(categoryConfig ?: CategoryConfig(
enable = option.value,
subsItemId = subsItemId,
categoryKey = category.key
)).copy(enable = option.value)
)
}
}
},
)
}
}
} }
} }
if (categories.lastOrNull() !== category) {
HorizontalDivider()
}
} }
item { item {
Spacer(modifier = Modifier.height(40.dp)) Spacer(modifier = Modifier.height(40.dp))
@ -239,153 +234,90 @@ fun CategoryPage(subsItemId: Long) {
} }
} }
editEnableCategory?.let { category ->
val categoryConfig =
categoryConfigs.find { c -> c.categoryKey == category.key }
val enable =
if (categoryConfig != null) categoryConfig.enable else category.enable
Dialog(onDismissRequest = { editEnableCategory = null }) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
) {
enableGroupRadioOptions.forEach { option ->
val onClick: () -> Unit = {
vm.viewModelScope.launchTry(Dispatchers.IO) {
DbSet.categoryConfigDao.insert(
(categoryConfig ?: CategoryConfig(
enable = option.second,
subsItemId = subsItemId,
categoryKey = category.key
)).copy(enable = option.second)
)
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.selectable(
selected = (option.second == enable),
onClick = onClick
)
.padding(horizontal = 16.dp)
) {
RadioButton(
selected = (option.second == enable),
onClick = onClick
)
Text(
text = option.first, modifier = Modifier.padding(start = 16.dp)
)
}
}
}
}
}
val subsRawVal = subsRaw val subsRawVal = subsRaw
if (editNameCategory != null && subsRawVal != null) { if (editNameCategory != null && subsRawVal != null) {
var source by remember { var source by remember {
mutableStateOf(editNameCategory.name) mutableStateOf(editNameCategory.name)
} }
AlertDialog( AlertDialog(title = { Text(text = "编辑类别") }, text = {
title = { Text(text = "编辑类别") }, OutlinedTextField(
text = { value = source,
OutlinedTextField( onValueChange = { source = it.trim() },
value = source, modifier = Modifier.fillMaxWidth(),
onValueChange = { source = it.trim() }, placeholder = { Text(text = "请输入类别名称") },
modifier = Modifier.fillMaxWidth(), singleLine = true
placeholder = { Text(text = "请输入类别名称") }, )
singleLine = true }, onDismissRequest = { setEditNameCategory(null) }, dismissButton = {
) TextButton(onClick = { setEditNameCategory(null) }) {
}, Text(text = "取消")
onDismissRequest = { setEditNameCategory(null) },
dismissButton = {
TextButton(onClick = { setEditNameCategory(null) }) {
Text(text = "取消")
}
},
confirmButton = {
TextButton(
enabled = source.isNotBlank() && source != editNameCategory.name,
onClick = {
if (categories.any { c -> c.key != editNameCategory.key && c.name == source }) {
toast("不可添加同名类别")
return@TextButton
}
vm.viewModelScope.launchTry(Dispatchers.IO) {
subsItem?.apply {
updateSubscription(subsRawVal.copy(
categories = categories.toMutableList().apply {
val i =
categories.indexOfFirst { c -> c.key == editNameCategory.key }
if (i >= 0) {
set(i, editNameCategory.copy(name = source))
}
}
))
DbSet.subsItemDao.update(copy(mtime = System.currentTimeMillis()))
}
toast("修改成功")
setEditNameCategory(null)
}
}
) {
Text(text = "确认")
}
} }
) }, confirmButton = {
TextButton(enabled = source.isNotBlank() && source != editNameCategory.name, onClick = {
if (categories.any { c -> c.key != editNameCategory.key && c.name == source }) {
toast("不可添加同名类别")
return@TextButton
}
vm.viewModelScope.launchTry(Dispatchers.IO) {
subsItem?.apply {
updateSubscription(
subsRawVal.copy(categories = categories.toMutableList()
.apply {
val i =
categories.indexOfFirst { c -> c.key == editNameCategory.key }
if (i >= 0) {
set(i, editNameCategory.copy(name = source))
}
})
)
DbSet.subsItemDao.update(copy(mtime = System.currentTimeMillis()))
}
toast("修改成功")
setEditNameCategory(null)
}
}) {
Text(text = "确认")
}
})
} }
if (showAddDlg && subsRawVal != null) { if (showAddDlg && subsRawVal != null) {
var source by remember { var source by remember {
mutableStateOf("") mutableStateOf("")
} }
AlertDialog( AlertDialog(title = { Text(text = "添加类别") }, text = {
title = { Text(text = "添加类别") }, OutlinedTextField(
text = { value = source,
OutlinedTextField( onValueChange = { source = it.trim() },
value = source, modifier = Modifier.fillMaxWidth(),
onValueChange = { source = it.trim() }, placeholder = { Text(text = "请输入类别名称") },
modifier = Modifier.fillMaxWidth(), singleLine = true
placeholder = { Text(text = "请输入类别名称") }, )
singleLine = true }, onDismissRequest = { showAddDlg = false }, dismissButton = {
) TextButton(onClick = { showAddDlg = false }) {
}, Text(text = "取消")
onDismissRequest = { showAddDlg = false },
dismissButton = {
TextButton(onClick = { showAddDlg = false }) {
Text(text = "取消")
}
},
confirmButton = {
TextButton(enabled = source.isNotEmpty(), onClick = {
if (categories.any { c -> c.name == source }) {
toast("不可添加同名类别")
return@TextButton
}
showAddDlg = false
vm.viewModelScope.launchTry(Dispatchers.IO) {
subsItem?.apply {
updateSubscription(subsRawVal.copy(
categories = categories.toMutableList().apply {
add(RawSubscription.RawCategory(
key = (categories.maxOfOrNull { c -> c.key } ?: -1) + 1,
name = source,
enable = null
))
}
))
DbSet.subsItemDao.update(copy(mtime = System.currentTimeMillis()))
toast("添加成功")
}
}
}) {
Text(text = "确认")
}
} }
) }, confirmButton = {
TextButton(enabled = source.isNotEmpty(), onClick = {
if (categories.any { c -> c.name == source }) {
toast("不可添加同名类别")
return@TextButton
}
showAddDlg = false
vm.viewModelScope.launchTry(Dispatchers.IO) {
subsItem?.apply {
updateSubscription(
subsRawVal.copy(categories = categories.toMutableList()
.apply {
add(RawSubscription.RawCategory(key = (categories.maxOfOrNull { c -> c.key }
?: -1) + 1, name = source, enable = null))
})
)
DbSet.subsItemDao.update(copy(mtime = System.currentTimeMillis()))
toast("添加成功")
}
}
}) {
Text(text = "确认")
}
})
} }
} }

View File

@ -31,7 +31,7 @@ import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@ -70,6 +70,7 @@ import li.songe.gkd.data.stringify
import li.songe.gkd.db.DbSet import li.songe.gkd.db.DbSet
import li.songe.gkd.service.launcherAppId import li.songe.gkd.service.launcherAppId
import li.songe.gkd.ui.component.AppBarTextField import li.songe.gkd.ui.component.AppBarTextField
import li.songe.gkd.ui.style.itemPadding
import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.ProfileTransitions import li.songe.gkd.util.ProfileTransitions
import li.songe.gkd.util.SortTypeOption import li.songe.gkd.util.SortTypeOption
@ -239,7 +240,7 @@ fun GlobalRuleExcludePage(subsItemId: Long, groupKey: Int) {
Row( Row(
modifier = Modifier modifier = Modifier
.height(IntrinsicSize.Min) .height(IntrinsicSize.Min)
.padding(10.dp, 6.dp), .itemPadding(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
@ -283,7 +284,7 @@ fun GlobalRuleExcludePage(subsItemId: Long, groupKey: Int) {
softWrap = false, softWrap = false,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
style = LocalTextStyle.current.let { style = MaterialTheme.typography.bodyLarge.let {
if (appInfo.isSystem) { if (appInfo.isSystem) {
it.copy(textDecoration = TextDecoration.Underline) it.copy(textDecoration = TextDecoration.Underline)
} else { } else {
@ -296,7 +297,9 @@ fun GlobalRuleExcludePage(subsItemId: Long, groupKey: Int) {
maxLines = 1, maxLines = 1,
softWrap = false, softWrap = false,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
} }

View File

@ -28,7 +28,6 @@ import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@ -53,7 +52,6 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@ -68,6 +66,7 @@ import li.songe.gkd.db.DbSet
import li.songe.gkd.ui.component.getDialogResult import li.songe.gkd.ui.component.getDialogResult
import li.songe.gkd.ui.destinations.GlobalRuleExcludePageDestination import li.songe.gkd.ui.destinations.GlobalRuleExcludePageDestination
import li.songe.gkd.ui.destinations.GroupItemPageDestination import li.songe.gkd.ui.destinations.GroupItemPageDestination
import li.songe.gkd.ui.style.itemPadding
import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.ProfileTransitions import li.songe.gkd.util.ProfileTransitions
import li.songe.gkd.util.encodeToJson5String import li.songe.gkd.util.encodeToJson5String
@ -139,7 +138,7 @@ fun GlobalRulePage(subsItemId: Long, focusGroupKey: Int? = null) {
if (group.key == focusGroupKey) MaterialTheme.colorScheme.inversePrimary else Color.Transparent if (group.key == focusGroupKey) MaterialTheme.colorScheme.inversePrimary else Color.Transparent
) )
.clickable { setShowGroupItem(group) } .clickable { setShowGroupItem(group) }
.padding(10.dp, 6.dp), .itemPadding(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
@ -154,7 +153,8 @@ fun GlobalRulePage(subsItemId: Long, focusGroupKey: Int? = null) {
maxLines = 1, maxLines = 1,
softWrap = false, softWrap = false,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodyLarge,
) )
if (group.valid) { if (group.valid) {
if (!group.desc.isNullOrBlank()) { if (!group.desc.isNullOrBlank()) {
@ -164,21 +164,22 @@ fun GlobalRulePage(subsItemId: Long, focusGroupKey: Int? = null) {
softWrap = false, softWrap = false,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
fontSize = 14.sp style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
} else { } else {
Text( Text(
text = "暂无描述", text = "暂无描述",
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
fontSize = 14.sp, style = MaterialTheme.typography.bodyMedium,
color = LocalContentColor.current.copy(alpha = 0.5f) color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
) )
} }
} else { } else {
Text( Text(
text = "非法选择器", text = "非法选择器",
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
fontSize = 14.sp, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error color = MaterialTheme.colorScheme.error
) )
} }
@ -449,10 +450,14 @@ fun GlobalRulePage(subsItemId: Long, focusGroupKey: Int? = null) {
modifier = Modifier.defaultMinSize(300.dp), modifier = Modifier.defaultMinSize(300.dp),
onDismissRequest = { setShowGroupItem(null) }, onDismissRequest = { setShowGroupItem(null) },
title = { title = {
Text(text = showGroupItem.name) Text(text = "规则组详情")
}, },
text = { text = {
Text(text = showGroupItem.desc ?: "") Column {
Text(text = showGroupItem.name)
Spacer(modifier = Modifier.height(10.dp))
Text(text = showGroupItem.desc ?: "")
}
}, },
confirmButton = { confirmButton = {
if (showGroupItem.allExampleUrls.isNotEmpty()) { if (showGroupItem.allExampleUrls.isNotEmpty()) {

View File

@ -17,6 +17,7 @@ import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@ -34,11 +35,11 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph import com.ramcosta.composedestinations.annotation.RootNavGraph
import li.songe.gkd.ui.destinations.AppItemPageDestination import li.songe.gkd.ui.destinations.AppItemPageDestination
import li.songe.gkd.ui.destinations.GlobalRulePageDestination import li.songe.gkd.ui.destinations.GlobalRulePageDestination
import li.songe.gkd.ui.style.itemPadding
import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.ProfileTransitions import li.songe.gkd.util.ProfileTransitions
import li.songe.gkd.util.appInfoCacheFlow import li.songe.gkd.util.appInfoCacheFlow
@ -98,7 +99,7 @@ fun SlowGroupPage() {
) )
) )
} }
.padding(10.dp, 5.dp), .itemPadding(),
title = group.name, title = group.name,
desc = "${rule.rawSubs.name}/全局规则" desc = "${rule.rawSubs.name}/全局规则"
) )
@ -118,12 +119,12 @@ fun SlowGroupPage() {
) )
) )
} }
.padding(10.dp, 5.dp), .itemPadding(),
title = group.name, title = group.name,
desc = "${rule.rawSubs.name}/应用规则/${appInfoCache[rule.app.id]?.name ?: rule.app.name ?: rule.app.id}" desc = "${rule.rawSubs.name}/应用规则/${appInfoCache[rule.app.id]?.name ?: rule.app.name ?: rule.app.id}"
) )
} }
item("empty") { item {
Spacer(modifier = Modifier.height(40.dp)) Spacer(modifier = Modifier.height(40.dp))
if (ruleSummary.slowGroupCount == 0) { if (ruleSummary.slowGroupCount == 0) {
Text( Text(
@ -160,17 +161,18 @@ fun SlowGroupCard(title: String, desc: String, modifier: Modifier = Modifier) {
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
text = title, text = title,
fontSize = 18.sp, style = MaterialTheme.typography.bodyLarge,
maxLines = 1, maxLines = 1,
softWrap = false, softWrap = false,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
Text( Text(
text = desc, text = desc,
fontSize = 14.sp, style = MaterialTheme.typography.bodyMedium,
maxLines = 1, maxLines = 1,
softWrap = false, softWrap = false,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
} }
Icon( Icon(

View File

@ -259,19 +259,17 @@ fun SubsPage(
DbSet.subsConfigDao.insert(newItem) DbSet.subsConfigDao.insert(newItem)
}, },
showMenu = editable, showMenu = editable,
onDelClick = { onDelClick = vm.viewModelScope.launchAsFn {
vm.viewModelScope.launchTry { val result = getDialogResult(
val result = getDialogResult( "删除规则组",
"删除规则组", "确定删除 ${appInfoCache[appRaw.id]?.name ?: appRaw.name ?: appRaw.id} 下所有规则组?"
"确定删除 ${appInfoCache[appRaw.id]?.name ?: appRaw.name ?: appRaw.id} 下所有规则组?" )
) if (!result) return@launchAsFn
if (!result) return@launchTry if (subsRaw != null && subsItem != null) {
if (subsRaw != null && subsItem != null) { updateSubscription(subsRaw.copy(apps = subsRaw.apps.filter { a -> a.id != appRaw.id }))
updateSubscription(subsRaw.copy(apps = subsRaw.apps.filter { a -> a.id != appRaw.id })) DbSet.subsItemDao.update(subsItem.copy(mtime = System.currentTimeMillis()))
DbSet.subsItemDao.update(subsItem.copy(mtime = System.currentTimeMillis())) DbSet.subsConfigDao.delete(subsItem.id, appRaw.id)
DbSet.subsConfigDao.delete(subsItem.id, appRaw.id) toast("删除成功")
toast("删除成功")
}
} }
}) })
} }

View File

@ -3,7 +3,6 @@ package li.songe.gkd.ui.component
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
@ -12,6 +11,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import li.songe.gkd.ui.style.itemPadding
@Composable @Composable
fun AuthCard( fun AuthCard(
@ -20,7 +20,7 @@ fun AuthCard(
onAuthClick: () -> Unit, onAuthClick: () -> Unit,
) { ) {
Row( Row(
modifier = Modifier.padding(16.dp, 12.dp), modifier = Modifier.itemPadding(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {

View File

@ -0,0 +1,53 @@
package li.songe.gkd.ui.component
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.tween
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Autorenew
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import kotlin.math.sin
@Composable
fun RotatingLoadingIcon(loading: Boolean) {
val rotation = remember { Animatable(0f) }
LaunchedEffect(loading) {
if (loading) {
rotation.animateTo(
targetValue = rotation.value + 180f,
animationSpec = tween(
durationMillis = 250,
easing = { x -> sin(Math.PI / 2 * (x - 1f)).toFloat() + 1f }
)
)
rotation.animateTo(
targetValue = rotation.value + 360f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 500, easing = LinearEasing),
repeatMode = RepeatMode.Restart
)
)
} else if (rotation.value != 0f) {
rotation.animateTo(
targetValue = rotation.value + 180f,
animationSpec = tween(
durationMillis = 250,
easing = { x -> sin(Math.PI / 2 * x).toFloat() }
)
)
}
}
Icon(
imageVector = Icons.Default.Autorenew,
contentDescription = null,
modifier = Modifier.graphicsLayer(rotationZ = rotation.value)
)
}

View File

@ -4,7 +4,6 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -14,7 +13,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp import li.songe.gkd.ui.style.itemPadding
@Composable @Composable
fun SettingItem( fun SettingItem(
@ -28,7 +27,7 @@ fun SettingItem(
onClick = onClick onClick = onClick
) )
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp, 12.dp), .itemPadding(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {

View File

@ -22,8 +22,6 @@ import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -109,7 +107,7 @@ fun SubsAppCard(
softWrap = false, softWrap = false,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
style = LocalTextStyle.current.let { style = MaterialTheme.typography.bodyLarge.let {
if (appInfo?.isSystem == true) { if (appInfo?.isSystem == true) {
it.copy(textDecoration = TextDecoration.Underline) it.copy(textDecoration = TextDecoration.Underline)
} else { } else {
@ -129,13 +127,16 @@ fun SubsAppCard(
maxLines = 1, maxLines = 1,
softWrap = false, softWrap = false,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
} else { } else {
Text( Text(
text = "暂无规则", text = "暂无规则",
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
color = LocalContentColor.current.copy(alpha = 0.5f) style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
) )
} }
} }

View File

@ -0,0 +1,77 @@
package li.songe.gkd.ui.component
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.UnfoldMore
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import li.songe.gkd.ui.style.itemPadding
import li.songe.gkd.util.Option
import li.songe.gkd.util.allSubObject
@Composable
fun <T> TextMenu(
title: String,
option: Option<T>,
onOptionChange: ((Option<T>) -> Unit),
) {
var expanded by remember { mutableStateOf(false) }
Row(
modifier = Modifier
.clickable {
expanded = true
}
.fillMaxWidth()
.itemPadding(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = title,
style = MaterialTheme.typography.bodyLarge,
)
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = option.label,
style = MaterialTheme.typography.bodyMedium,
)
Icon(
imageVector = Icons.Default.UnfoldMore,
contentDescription = null
)
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
option.allSubObject.forEach { otherOption ->
DropdownMenuItem(
text = {
Text(text = otherOption.label)
},
onClick = {
expanded = false
if (otherOption != option) {
onOptionChange(otherOption)
}
},
)
}
}
}
}
}

View File

@ -3,7 +3,6 @@ package li.songe.gkd.ui.component
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
@ -12,30 +11,38 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import li.songe.gkd.ui.style.itemPadding
@Composable @Composable
fun TextSwitch( fun TextSwitch(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
name: String = "", name: String,
desc: String = "", desc: String? = null,
checked: Boolean = true, checked: Boolean = true,
enabled: Boolean = true, enabled: Boolean = true,
onCheckedChange: ((Boolean) -> Unit)? = null, onCheckedChange: ((Boolean) -> Unit)? = null,
) { ) {
Row( Row(
modifier = modifier.padding(16.dp, 12.dp), modifier = modifier.itemPadding(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Column(modifier = Modifier.weight(1f)) { if (desc != null) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = name,
style = MaterialTheme.typography.bodyLarge,
)
Text(
text = desc,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
} else {
Text( Text(
name, text = name,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
) )
Text(
desc,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
} }
Spacer(modifier = Modifier.width(10.dp)) Spacer(modifier = Modifier.width(10.dp))
Switch( Switch(

View File

@ -42,6 +42,7 @@ import li.songe.gkd.ui.component.AuthCard
import li.songe.gkd.ui.component.TextSwitch import li.songe.gkd.ui.component.TextSwitch
import li.songe.gkd.ui.destinations.ClickLogPageDestination import li.songe.gkd.ui.destinations.ClickLogPageDestination
import li.songe.gkd.ui.destinations.SlowGroupPageDestination import li.songe.gkd.ui.destinations.SlowGroupPageDestination
import li.songe.gkd.ui.style.itemPadding
import li.songe.gkd.util.HOME_PAGE_URL import li.songe.gkd.util.HOME_PAGE_URL
import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.launchAsFn import li.songe.gkd.util.launchAsFn
@ -131,7 +132,7 @@ fun useControlPage(): ScaffoldExt {
.clickable { .clickable {
context.openUri(HOME_PAGE_URL) context.openUri(HOME_PAGE_URL)
} }
.padding(16.dp, 12.dp), .itemPadding(),
) { ) {
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
@ -158,7 +159,7 @@ fun useControlPage(): ScaffoldExt {
.clickable { .clickable {
navController.navigate(ClickLogPageDestination) navController.navigate(ClickLogPageDestination)
} }
.padding(16.dp, 12.dp), .itemPadding(),
) { ) {
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
@ -185,7 +186,7 @@ fun useControlPage(): ScaffoldExt {
.clickable { .clickable {
navController.navigate(SlowGroupPageDestination) navController.navigate(SlowGroupPageDestination)
} }
.padding(16.dp, 12.dp), .itemPadding(),
) { ) {
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
@ -208,7 +209,7 @@ fun useControlPage(): ScaffoldExt {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp, 12.dp) .itemPadding()
) { ) {
Text( Text(
text = subsStatus, text = subsStatus,

View File

@ -1,10 +1,5 @@
package li.songe.gkd.ui.home package li.songe.gkd.ui.home
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -14,26 +9,21 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.filled.Autorenew
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -41,7 +31,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -56,22 +45,28 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import li.songe.gkd.MainActivity import li.songe.gkd.MainActivity
import li.songe.gkd.appScope import li.songe.gkd.appScope
import li.songe.gkd.ui.component.RotatingLoadingIcon
import li.songe.gkd.ui.component.SettingItem import li.songe.gkd.ui.component.SettingItem
import li.songe.gkd.ui.component.TextMenu
import li.songe.gkd.ui.component.TextSwitch import li.songe.gkd.ui.component.TextSwitch
import li.songe.gkd.ui.destinations.AboutPageDestination import li.songe.gkd.ui.destinations.AboutPageDestination
import li.songe.gkd.ui.destinations.AdvancedPageDestination import li.songe.gkd.ui.destinations.AdvancedPageDestination
import li.songe.gkd.ui.style.itemPadding
import li.songe.gkd.ui.theme.supportDynamicColor
import li.songe.gkd.util.DarkThemeOption
import li.songe.gkd.util.LoadStatus import li.songe.gkd.util.LoadStatus
import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.UpdateTimeOption
import li.songe.gkd.util.buildLogFile import li.songe.gkd.util.buildLogFile
import li.songe.gkd.util.checkUpdate import li.songe.gkd.util.checkUpdate
import li.songe.gkd.util.checkUpdatingFlow import li.songe.gkd.util.checkUpdatingFlow
import li.songe.gkd.util.findOption
import li.songe.gkd.util.launchAsFn import li.songe.gkd.util.launchAsFn
import li.songe.gkd.util.launchTry import li.songe.gkd.util.launchTry
import li.songe.gkd.util.navigate import li.songe.gkd.util.navigate
import li.songe.gkd.util.shareFile import li.songe.gkd.util.shareFile
import li.songe.gkd.util.storeFlow import li.songe.gkd.util.storeFlow
import li.songe.gkd.util.toast import li.songe.gkd.util.toast
import kotlin.math.sin
val settingsNav = BottomNavItem( val settingsNav = BottomNavItem(
label = "设置", icon = Icons.Outlined.Settings label = "设置", icon = Icons.Outlined.Settings
@ -85,12 +80,6 @@ fun useSettingsPage(): ScaffoldExt {
val vm = hiltViewModel<HomeVm>() val vm = hiltViewModel<HomeVm>()
val uploadStatus by vm.uploadStatusFlow.collectAsState() val uploadStatus by vm.uploadStatusFlow.collectAsState()
var showSubsIntervalDlg by remember {
mutableStateOf(false)
}
var showEnableDarkThemeDlg by remember {
mutableStateOf(false)
}
var showToastInputDlg by remember { var showToastInputDlg by remember {
mutableStateOf(false) mutableStateOf(false)
} }
@ -101,74 +90,6 @@ fun useSettingsPage(): ScaffoldExt {
val checkUpdating by checkUpdatingFlow.collectAsState() val checkUpdating by checkUpdatingFlow.collectAsState()
if (showSubsIntervalDlg) {
Dialog(onDismissRequest = { showSubsIntervalDlg = false }) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
) {
UpdateTimeOption.allSubObject.forEach { option ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.selectable(
selected = (option.value == store.updateSubsInterval),
onClick = {
storeFlow.update { it.copy(updateSubsInterval = option.value) }
})
.padding(horizontal = 16.dp)
) {
RadioButton(
selected = (option.value == store.updateSubsInterval),
onClick = {
storeFlow.update { it.copy(updateSubsInterval = option.value) }
})
Text(
text = option.label, modifier = Modifier.padding(start = 16.dp)
)
}
}
}
}
}
if (showEnableDarkThemeDlg) {
Dialog(onDismissRequest = { showEnableDarkThemeDlg = false }) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
) {
darkThemeRadioOptions.forEach { option ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.selectable(selected = (option.second == store.enableDarkTheme),
onClick = {
storeFlow.value =
store.copy(enableDarkTheme = option.second)
})
.padding(horizontal = 16.dp)
) {
RadioButton(
selected = (option.second == store.enableDarkTheme),
onClick = {
storeFlow.value = store.copy(enableDarkTheme = option.second)
})
Text(
text = option.first, modifier = Modifier.padding(start = 16.dp)
)
}
}
}
}
}
if (showToastInputDlg) { if (showToastInputDlg) {
var value by remember { var value by remember {
mutableStateOf(store.clickToast) mutableStateOf(store.clickToast)
@ -335,12 +256,13 @@ fun useSettingsPage(): ScaffoldExt {
Text( Text(
text = "常规", text = "常规",
modifier = Modifier.padding(16.dp, 12.dp), modifier = Modifier.itemPadding(),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
) )
TextSwitch(name = "触发提示", TextSwitch(
name = "触发提示",
desc = store.clickToast, desc = store.clickToast,
checked = store.toastWhenClick, checked = store.toastWhenClick,
modifier = Modifier.clickable { modifier = Modifier.clickable {
@ -350,83 +272,57 @@ fun useSettingsPage(): ScaffoldExt {
storeFlow.value = store.copy( storeFlow.value = store.copy(
toastWhenClick = it toastWhenClick = it
) )
}) }
)
TextSwitch( TextSwitch(
name = "后台隐藏", name = "后台隐藏",
desc = "在[最近任务]界面中隐藏本应用", desc = "在[最近任务]中隐藏本应用",
checked = store.excludeFromRecents, checked = store.excludeFromRecents,
onCheckedChange = { onCheckedChange = {
storeFlow.value = store.copy( storeFlow.value = store.copy(
excludeFromRecents = it excludeFromRecents = it
) )
})
Row(
modifier = Modifier
.clickable {
showEnableDarkThemeDlg = true
}
.padding(16.dp, 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.weight(1f),
text = "深色模式",
style = MaterialTheme.typography.bodyLarge,
)
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = darkThemeRadioOptions.find { it.second == store.enableDarkTheme }?.first
?: store.enableDarkTheme.toString(),
style = MaterialTheme.typography.bodyMedium,
)
Icon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = "more"
)
} }
)
TextMenu(
title = "深色模式",
option = DarkThemeOption.allSubObject.findOption(store.enableDarkTheme)
) {
storeFlow.update { s -> s.copy(enableDarkTheme = it.value) }
}
if (supportDynamicColor) {
TextSwitch(
name = "动态配色",
desc = "配色跟随系统主题",
checked = store.enableDynamicColor,
onCheckedChange = {
storeFlow.value = store.copy(
enableDynamicColor = it
)
}
)
} }
Text( Text(
text = "更新", text = "更新",
modifier = Modifier.padding(16.dp, 12.dp), modifier = Modifier.itemPadding(),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
) )
Row( TextMenu(
modifier = Modifier title = "更新订阅",
.clickable { option = UpdateTimeOption.allSubObject.findOption(store.updateSubsInterval)
showSubsIntervalDlg = true
}
.padding(16.dp, 12.dp),
verticalAlignment = Alignment.CenterVertically
) { ) {
Text( storeFlow.update { s -> s.copy(updateSubsInterval = it.value) }
modifier = Modifier.weight(1f),
text = "自动更新订阅",
style = MaterialTheme.typography.bodyLarge,
)
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = UpdateTimeOption.allSubObject.find { it.value == store.updateSubsInterval }?.label
?: store.updateSubsInterval.toString(),
style = MaterialTheme.typography.bodyMedium,
)
Icon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = "more"
)
}
} }
TextSwitch(name = "自动更新应用", TextSwitch(
desc = "打开应用时自动检测是否存在新版本", name = "自动更新",
desc = "打开应用时检测新版本",
checked = store.autoCheckAppUpdate, checked = store.autoCheckAppUpdate,
onCheckedChange = { onCheckedChange = {
storeFlow.value = store.copy( storeFlow.value = store.copy(
@ -446,7 +342,7 @@ fun useSettingsPage(): ScaffoldExt {
} }
) )
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp, 12.dp), .itemPadding(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
@ -459,13 +355,14 @@ fun useSettingsPage(): ScaffoldExt {
Text( Text(
text = "日志", text = "日志",
modifier = Modifier.padding(16.dp, 12.dp), modifier = Modifier.itemPadding(),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
) )
TextSwitch(name = "保存日志", TextSwitch(
desc = "保存最近7天日志,关闭后无法定位解决错误", name = "保存日志",
desc = "保存7天日志,帮助定位BUG",
checked = store.log2FileSwitch, checked = store.log2FileSwitch,
onCheckedChange = { onCheckedChange = {
storeFlow.value = store.copy( storeFlow.value = store.copy(
@ -485,13 +382,17 @@ fun useSettingsPage(): ScaffoldExt {
} }
) )
SettingItem(title = "分享日志", onClick = { SettingItem(
showShareLogDlg = true title = "分享日志",
}) imageVector = Icons.Default.Share,
onClick = {
showShareLogDlg = true
}
)
Text( Text(
text = "其它", text = "其它",
modifier = Modifier.padding(16.dp, 12.dp), modifier = Modifier.itemPadding(),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
) )
@ -508,57 +409,3 @@ fun useSettingsPage(): ScaffoldExt {
} }
} }
} }
sealed class UpdateTimeOption(val value: Long, val label: String) {
data object Pause : UpdateTimeOption(-1, "暂停")
data object Everyday : UpdateTimeOption(24 * 60 * 60_000, "每天")
data object Every3Days : UpdateTimeOption(24 * 60 * 60_000 * 3, "每3天")
data object Every7Days : UpdateTimeOption(24 * 60 * 60_000 * 7, "每7天")
companion object {
val allSubObject by lazy { arrayOf(Pause, Everyday, Every3Days, Every7Days) }
}
}
private val darkThemeRadioOptions = arrayOf(
"跟随系统" to null,
"启用" to true,
"关闭" to false,
)
@Composable
fun RotatingLoadingIcon(loading: Boolean) {
val rotation = remember { Animatable(0f) }
LaunchedEffect(loading) {
if (loading) {
rotation.animateTo(
targetValue = rotation.value + 180f,
animationSpec = tween(
durationMillis = 250,
easing = { x -> sin(Math.PI / 2 * (x - 1f)).toFloat() + 1f }
)
)
rotation.animateTo(
targetValue = rotation.value + 360f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 500, easing = LinearEasing),
repeatMode = RepeatMode.Restart
)
)
} else if (rotation.value != 0f) {
rotation.animateTo(
targetValue = rotation.value + 180f,
animationSpec = tween(
durationMillis = 250,
easing = { x -> sin(Math.PI / 2 * x).toFloat() }
)
)
}
}
Icon(
imageVector = Icons.Default.Autorenew,
contentDescription = null,
modifier = Modifier.graphicsLayer(rotationZ = rotation.value)
)
}

View File

@ -0,0 +1,7 @@
package li.songe.gkd.ui.style
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
fun Modifier.itemPadding() = this then padding(16.dp, 12.dp)

View File

@ -1,7 +1,6 @@
package li.songe.gkd.ui.theme package li.songe.gkd.ui.theme
import android.os.Build import android.os.Build
import androidx.activity.ComponentActivity
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
@ -12,33 +11,33 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import li.songe.gkd.util.map import li.songe.gkd.MainActivity
import li.songe.gkd.util.storeFlow
import li.songe.gkd.util.updateToastStyle import li.songe.gkd.util.updateToastStyle
val LightColorScheme = lightColorScheme() val LightColorScheme = lightColorScheme()
val DarkColorScheme = darkColorScheme() val DarkColorScheme = darkColorScheme()
val supportDynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
@Composable @Composable
fun AppTheme( fun AppTheme(
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
// https://developer.android.com/jetpack/compose/designsystems/material3?hl=zh-cn // https://developer.android.com/jetpack/compose/designsystems/material3?hl=zh-cn
val context = LocalContext.current as ComponentActivity val context = LocalContext.current as MainActivity
val scope = rememberCoroutineScope() val enableDarkTheme by context.mainVm.enableDarkThemeFlow.collectAsState()
val enableDarkTheme by storeFlow.map(scope) { s -> s.enableDarkTheme }.collectAsState() val enableDynamicColor by context.mainVm.enableDynamicColorFlow.collectAsState()
val systemInDarkTheme = isSystemInDarkTheme() val systemInDarkTheme = isSystemInDarkTheme()
val darkTheme = enableDarkTheme ?: systemInDarkTheme val darkTheme = enableDarkTheme ?: systemInDarkTheme
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val colorScheme = when { val colorScheme = when {
dynamicColor && darkTheme -> dynamicDarkColorScheme(context) supportDynamicColor && enableDynamicColor && darkTheme -> dynamicDarkColorScheme(context)
dynamicColor && !darkTheme -> dynamicLightColorScheme(context) supportDynamicColor && enableDynamicColor && !darkTheme -> dynamicLightColorScheme(context)
darkTheme -> DarkColorScheme darkTheme -> DarkColorScheme
else -> LightColorScheme else -> LightColorScheme
} }
// https://github.com/gkd-kit/gkd/pull/421
LaunchedEffect(darkTheme) { LaunchedEffect(darkTheme) {
WindowInsetsControllerCompat(context.window, context.window.decorView).apply { WindowInsetsControllerCompat(context.window, context.window.decorView).apply {
isAppearanceLightStatusBars = !darkTheme isAppearanceLightStatusBars = !darkTheme

View File

@ -0,0 +1,74 @@
package li.songe.gkd.util
sealed interface Option<T> {
val value: T
val label: String
}
fun <V, T : Option<V>> Array<T>.findOption(value: V): T {
return find { it.value == value } ?: first()
}
@Suppress("UNCHECKED_CAST")
val <T> Option<T>.allSubObject: Array<Option<T>>
get() = when (this) {
is SortTypeOption -> SortTypeOption.allSubObject
is UpdateTimeOption -> UpdateTimeOption.allSubObject
is DarkThemeOption -> DarkThemeOption.allSubObject
is EnableGroupOption -> EnableGroupOption.allSubObject
is RuleSortOption -> RuleSortOption.allSubObject
} as Array<Option<T>>
sealed class SortTypeOption(override val value: Int, override val label: String) : Option<Int> {
data object SortByName : SortTypeOption(0, "按名称")
data object SortByAppMtime : SortTypeOption(1, "按更新时间")
data object SortByTriggerTime : SortTypeOption(2, "按触发时间")
companion object {
// https://stackoverflow.com/questions/47648689
val allSubObject by lazy { arrayOf(SortByName, SortByAppMtime, SortByTriggerTime) }
}
}
sealed class UpdateTimeOption(override val value: Long, override val label: String) : Option<Long> {
data object Pause : UpdateTimeOption(-1, "暂停")
data object Everyday : UpdateTimeOption(24 * 60 * 60_000, "每天")
data object Every3Days : UpdateTimeOption(24 * 60 * 60_000 * 3, "每3天")
data object Every7Days : UpdateTimeOption(24 * 60 * 60_000 * 7, "每7天")
companion object {
val allSubObject by lazy { arrayOf(Pause, Everyday, Every3Days, Every7Days) }
}
}
sealed class DarkThemeOption(override val value: Boolean?, override val label: String) :
Option<Boolean?> {
data object FollowSystem : DarkThemeOption(null, "跟随系统")
data object AlwaysEnable : DarkThemeOption(true, "总是启用")
data object AlwaysDisable : DarkThemeOption(false, "总是关闭")
companion object {
val allSubObject by lazy { arrayOf(FollowSystem, AlwaysEnable, AlwaysDisable) }
}
}
sealed class EnableGroupOption(override val value: Boolean?, override val label: String) :
Option<Boolean?> {
data object FollowSubs : DarkThemeOption(null, "跟随订阅")
data object AllEnable : DarkThemeOption(true, "全部启用")
data object AllDisable : DarkThemeOption(false, "全部关闭")
companion object {
val allSubObject by lazy { arrayOf(FollowSubs, AllEnable, AllDisable) }
}
}
sealed class RuleSortOption(override val value: Int, override val label: String) : Option<Int> {
data object Default : RuleSortOption(0, "按订阅顺序")
data object ByTime : RuleSortOption(1, "按触发时间")
data object ByName : RuleSortOption(2, "按名称")
companion object {
val allSubObject by lazy { arrayOf(Default, ByTime, ByName) }
}
}

View File

@ -1,13 +0,0 @@
package li.songe.gkd.util
sealed class SortTypeOption(val value: Int, val label: String) {
data object SortByName : SortTypeOption(0, "按名称")
data object SortByAppMtime : SortTypeOption(1, "按更新时间")
data object SortByTriggerTime : SortTypeOption(2, "按触发时间")
companion object {
// https://stackoverflow.com/questions/47648689
val allSubObject by lazy { arrayOf(SortByName, SortByAppMtime, SortByTriggerTime) }
}
}

View File

@ -10,7 +10,6 @@ import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import li.songe.gkd.appScope import li.songe.gkd.appScope
import li.songe.gkd.ui.home.UpdateTimeOption
private inline fun <reified T> createStorageFlow( private inline fun <reified T> createStorageFlow(
key: String, key: String,
@ -57,6 +56,7 @@ data class Store(
val enableShizukuClick: Boolean = false, val enableShizukuClick: Boolean = false,
val log2FileSwitch: Boolean = true, val log2FileSwitch: Boolean = true,
val enableDarkTheme: Boolean? = null, val enableDarkTheme: Boolean? = null,
val enableDynamicColor: Boolean = true,
val enableAbFloatWindow: Boolean = true, val enableAbFloatWindow: Boolean = true,
val sortType: Int = SortTypeOption.SortByName.value, val sortType: Int = SortTypeOption.SortByName.value,
val showSystemApp: Boolean = true, val showSystemApp: Boolean = true,
@ -88,8 +88,6 @@ val clickCountFlow by lazy {
recordStoreFlow.map(appScope) { r -> r.clickCount } recordStoreFlow.map(appScope) { r -> r.clickCount }
} }
private val log2FileSwitchFlow by lazy { storeFlow.map(appScope) { s -> s.log2FileSwitch } }
fun increaseClickCount(n: Int = 1) { fun increaseClickCount(n: Int = 1) {
recordStoreFlow.update { recordStoreFlow.update {
it.copy( it.copy(
@ -101,10 +99,5 @@ fun increaseClickCount(n: Int = 1) {
fun initStore() { fun initStore() {
storeFlow.value storeFlow.value
recordStoreFlow.value recordStoreFlow.value
appScope.launchTry(Dispatchers.IO) {
log2FileSwitchFlow.collect {
LogUtils.getConfig().isLog2FileSwitch = it
}
}
} }