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 com.blankj.utilcode.util.LogUtils
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 li.songe.gkd.data.RawSubscription
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.launchTry
import li.songe.gkd.util.logZipDir
import li.songe.gkd.util.map
import li.songe.gkd.util.newVersionApkDir
import li.songe.gkd.util.snapshotZipDir
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() {
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 map = mutableMapOf<RawAppGroup, RawCategory>()
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.useSafeInputTapFc
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.checkSubsUpdate
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.RootNavGraph
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.LocalNavController
import li.songe.gkd.util.ProfileTransitions
@ -71,7 +72,7 @@ fun AboutPage() {
context.openUri(REPOSITORY_URL)
}
.fillMaxWidth()
.padding(16.dp, 12.dp)
.itemPadding()
) {
Text(
text = "开源地址",
@ -86,7 +87,7 @@ fun AboutPage() {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp, 12.dp)
.itemPadding()
) {
Text(
text = "版本代码",
@ -101,7 +102,7 @@ fun AboutPage() {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp, 12.dp)
.itemPadding()
) {
Text(
text = "版本名称",
@ -120,7 +121,7 @@ fun AboutPage() {
context.openUri(GIT_COMMIT_URL)
}
.fillMaxWidth()
.padding(16.dp, 12.dp)
.itemPadding()
) {
Text(
text = "代码记录",
@ -136,7 +137,7 @@ fun AboutPage() {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp, 12.dp)
.itemPadding()
) {
Text(
text = "构建时间",
@ -151,7 +152,7 @@ fun AboutPage() {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp, 12.dp)
.itemPadding()
) {
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.TextSwitch
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.LocalNavController
import li.songe.gkd.util.ProfileTransitions
@ -124,7 +125,7 @@ fun AdvancedPage() {
) {
Text(
text = "Shizuku",
modifier = Modifier.padding(16.dp, 12.dp),
modifier = Modifier.itemPadding(),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
)
@ -150,12 +151,12 @@ fun AdvancedPage() {
Text(
text = "HTTP服务",
modifier = Modifier.padding(16.dp, 12.dp),
modifier = Modifier.itemPadding(),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
)
Row(
modifier = Modifier.padding(16.dp, 14.dp),
modifier = Modifier.itemPadding(),
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
@ -215,7 +216,7 @@ fun AdvancedPage() {
Row(
modifier = Modifier
.clickable { showPortDlg = true }
.padding(16.dp, 12.dp),
.itemPadding(),
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
@ -245,7 +246,7 @@ fun AdvancedPage() {
Text(
text = "快照",
modifier = Modifier.padding(16.dp, 12.dp),
modifier = Modifier.itemPadding(),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
)
@ -335,7 +336,7 @@ fun AdvancedPage() {
Text(
text = "其它",
modifier = Modifier.padding(16.dp, 12.dp),
modifier = Modifier.itemPadding(),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
)

View File

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

View File

@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.stateIn
import li.songe.gkd.data.SubsConfig
import li.songe.gkd.db.DbSet
import li.songe.gkd.ui.destinations.AppConfigPageDestination
import li.songe.gkd.util.RuleSortOption
import li.songe.gkd.util.collator
import li.songe.gkd.util.ruleSummaryFlow
import javax.inject.Inject
@ -29,7 +30,7 @@ class AppConfigVm @Inject constructor(stateHandle: SavedStateHandle) : ViewModel
SubsConfig.AppGroupType
)
val ruleSortTypeFlow = MutableStateFlow<RuleSortType>(RuleSortType.Default)
val ruleSortTypeFlow = MutableStateFlow<RuleSortOption>(RuleSortOption.Default)
val globalGroupsFlow = combine(
ruleSummaryFlow.map { r -> r.globalGroups },
@ -37,15 +38,15 @@ class AppConfigVm @Inject constructor(stateHandle: SavedStateHandle) : ViewModel
latestGlobalLogsFlow
) { list, type, logs ->
when (type) {
RuleSortType.Default -> list
RuleSortType.ByName -> list.sortedWith { a, b ->
RuleSortOption.Default -> list
RuleSortOption.ByName -> list.sortedWith { a, b ->
collator.compare(
a.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
?: 0)
}
@ -58,15 +59,15 @@ class AppConfigVm @Inject constructor(stateHandle: SavedStateHandle) : ViewModel
latestAppLogsFlow
) { list, type, logs ->
when (type) {
RuleSortType.Default -> list
RuleSortType.ByName -> list.sortedWith { a, b ->
RuleSortOption.Default -> list
RuleSortOption.ByName -> list.sortedWith { a, b ->
collator.compare(
a.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
?: 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.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
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.ui.component.getDialogResult
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.ProfileTransitions
import li.songe.gkd.util.appInfoCacheFlow
@ -166,7 +166,7 @@ fun AppItemPage(
if (group.key == focusGroupKey) MaterialTheme.colorScheme.inversePrimary else Color.Transparent
)
.clickable { setShowGroupItem(group) }
.padding(10.dp, 6.dp),
.itemPadding(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
@ -181,7 +181,8 @@ fun AppItemPage(
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodyLarge,
)
if (group.valid) {
if (!group.desc.isNullOrBlank()) {
@ -191,21 +192,22 @@ fun AppItemPage(
softWrap = false,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(),
fontSize = 14.sp
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
} else {
Text(
text = "暂无描述",
modifier = Modifier.fillMaxWidth(),
fontSize = 14.sp,
color = LocalContentColor.current.copy(alpha = 0.5f)
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
)
}
} else {
Text(
text = "非法选择器",
modifier = Modifier.fillMaxWidth(),
fontSize = 14.sp,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error
)
}
@ -343,13 +345,18 @@ fun AppItemPage(
}
showGroupItem?.let { showGroupItemVal ->
AlertDialog(modifier = Modifier.defaultMinSize(300.dp),
AlertDialog(
modifier = Modifier.defaultMinSize(300.dp),
onDismissRequest = { setShowGroupItem(null) },
title = {
Text(text = showGroupItemVal.name)
Text(text = "规则组详情")
},
text = {
Text(text = showGroupItemVal.desc ?: "")
Column {
Text(text = showGroupItemVal.name)
Spacer(modifier = Modifier.height(10.dp))
Text(text = showGroupItemVal.desc ?: "")
}
},
confirmButton = {
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.lazy.LazyColumn
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.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.UnfoldMore
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
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.text.style.TextAlign
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.lifecycle.viewModelScope
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.db.DbSet
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.ProfileTransitions
import li.songe.gkd.util.findOption
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.toast
import li.songe.gkd.util.updateSubscription
val enableGroupRadioOptions = arrayOf(
"跟随订阅" to null,
"全部启用" to true,
"全部关闭" to false,
)
@RootNavGraph
@Destination(style = ProfileTransitions::class)
@Composable
@ -84,15 +73,13 @@ fun CategoryPage(subsItemId: Long) {
var showAddDlg by remember {
mutableStateOf(false)
}
var editEnableCategory by remember {
mutableStateOf<RawSubscription.RawCategory?>(null)
}
val (editNameCategory, setEditNameCategory) = remember {
mutableStateOf<RawSubscription.RawCategory?>(null)
}
val categories = subsRaw?.categories ?: emptyList()
val categoriesGroups = subsRaw?.categoryToGroupsMap ?: emptyMap()
val categoriesApps = subsRaw?.categoryToAppMap ?: emptyMap()
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = {
@ -120,35 +107,36 @@ fun CategoryPage(subsItemId: Long) {
modifier = Modifier.padding(contentPadding)
) {
items(categories, { it.key }) { category ->
var selectedExpanded by remember { mutableStateOf(false) }
Row(modifier = Modifier
.clickable {
editEnableCategory = category
}
.padding(10.dp, 6.dp), verticalAlignment = Alignment.CenterVertically
) {
.clickable { selectedExpanded = true }
.itemPadding(),
verticalAlignment = Alignment.CenterVertically) {
val size = categoriesGroups[category]?.size ?: 0
Column(modifier = Modifier.weight(1f)) {
Text(
text = category.name, fontSize = 18.sp
text = category.name,
style = MaterialTheme.typography.bodyLarge,
)
if (size > 0) {
val appSize = categoriesApps[category]?.size ?: 0
Text(
text = "${size}规则组",
fontSize = 14.sp
text = "${appSize}应用/${size}规则组",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
} else {
Text(
text = "暂无规则",
fontSize = 14.sp,
color = LocalContentColor.current.copy(alpha = 0.5f)
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
)
}
}
if (editable) {
var expanded by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.wrapContentSize(Alignment.TopStart)
modifier = Modifier.wrapContentSize(Alignment.TopStart)
) {
IconButton(onClick = {
expanded = true
@ -158,45 +146,33 @@ fun CategoryPage(subsItemId: Long) {
contentDescription = null,
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = {
Text(text = "编辑")
},
onClick = {
expanded = false
setEditNameCategory(category)
}
)
DropdownMenuItem(
text = {
Text(text = "删除", color = MaterialTheme.colorScheme.error)
},
onClick = {
expanded = false
vm.viewModelScope.launchTry {
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("删除成功")
DropdownMenu(expanded = expanded,
onDismissRequest = { expanded = false }) {
DropdownMenuItem(text = {
Text(text = "编辑")
}, onClick = {
expanded = false
setEditNameCategory(category)
})
DropdownMenuItem(text = {
Text(text = "删除", color = MaterialTheme.colorScheme.error)
}, onClick = {
expanded = false
vm.viewModelScope.launchTry {
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("删除成功")
}
)
})
}
}
@ -210,19 +186,38 @@ fun CategoryPage(subsItemId: Long) {
val enable =
if (categoryConfig != null) categoryConfig.enable else category.enable
Text(
text = enableGroupRadioOptions.find { e -> e.second == enable }?.first
?: "",
fontSize = 14.sp
text = EnableGroupOption.allSubObject.findOption(enable).label,
style = MaterialTheme.typography.bodyMedium,
)
Icon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = null
imageVector = Icons.Default.UnfoldMore, 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 {
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
if (editNameCategory != null && subsRawVal != null) {
var source by remember {
mutableStateOf(editNameCategory.name)
}
AlertDialog(
title = { Text(text = "编辑类别") },
text = {
OutlinedTextField(
value = source,
onValueChange = { source = it.trim() },
modifier = Modifier.fillMaxWidth(),
placeholder = { Text(text = "请输入类别名称") },
singleLine = true
)
},
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 = "确认")
}
AlertDialog(title = { Text(text = "编辑类别") }, text = {
OutlinedTextField(
value = source,
onValueChange = { source = it.trim() },
modifier = Modifier.fillMaxWidth(),
placeholder = { Text(text = "请输入类别名称") },
singleLine = true
)
}, 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 = "确认")
}
})
}
if (showAddDlg && subsRawVal != null) {
var source by remember {
mutableStateOf("")
}
AlertDialog(
title = { Text(text = "添加类别") },
text = {
OutlinedTextField(
value = source,
onValueChange = { source = it.trim() },
modifier = Modifier.fillMaxWidth(),
placeholder = { Text(text = "请输入类别名称") },
singleLine = true
)
},
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 = "确认")
}
AlertDialog(title = { Text(text = "添加类别") }, text = {
OutlinedTextField(
value = source,
onValueChange = { source = it.trim() },
modifier = Modifier.fillMaxWidth(),
placeholder = { Text(text = "请输入类别名称") },
singleLine = true
)
}, 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 = "确认")
}
})
}
}

View File

@ -31,7 +31,7 @@ import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
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.service.launcherAppId
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.ProfileTransitions
import li.songe.gkd.util.SortTypeOption
@ -239,7 +240,7 @@ fun GlobalRuleExcludePage(subsItemId: Long, groupKey: Int) {
Row(
modifier = Modifier
.height(IntrinsicSize.Min)
.padding(10.dp, 6.dp),
.itemPadding(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
@ -283,7 +284,7 @@ fun GlobalRuleExcludePage(subsItemId: Long, groupKey: Int) {
softWrap = false,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(),
style = LocalTextStyle.current.let {
style = MaterialTheme.typography.bodyLarge.let {
if (appInfo.isSystem) {
it.copy(textDecoration = TextDecoration.Underline)
} else {
@ -296,7 +297,9 @@ fun GlobalRuleExcludePage(subsItemId: Long, groupKey: Int) {
maxLines = 1,
softWrap = false,
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.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
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.TextOverflow
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.lifecycle.viewModelScope
@ -68,6 +66,7 @@ import li.songe.gkd.db.DbSet
import li.songe.gkd.ui.component.getDialogResult
import li.songe.gkd.ui.destinations.GlobalRuleExcludePageDestination
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.ProfileTransitions
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
)
.clickable { setShowGroupItem(group) }
.padding(10.dp, 6.dp),
.itemPadding(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
@ -154,7 +153,8 @@ fun GlobalRulePage(subsItemId: Long, focusGroupKey: Int? = null) {
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodyLarge,
)
if (group.valid) {
if (!group.desc.isNullOrBlank()) {
@ -164,21 +164,22 @@ fun GlobalRulePage(subsItemId: Long, focusGroupKey: Int? = null) {
softWrap = false,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(),
fontSize = 14.sp
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
} else {
Text(
text = "暂无描述",
modifier = Modifier.fillMaxWidth(),
fontSize = 14.sp,
color = LocalContentColor.current.copy(alpha = 0.5f)
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
)
}
} else {
Text(
text = "非法选择器",
modifier = Modifier.fillMaxWidth(),
fontSize = 14.sp,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error
)
}
@ -449,10 +450,14 @@ fun GlobalRulePage(subsItemId: Long, focusGroupKey: Int? = null) {
modifier = Modifier.defaultMinSize(300.dp),
onDismissRequest = { setShowGroupItem(null) },
title = {
Text(text = showGroupItem.name)
Text(text = "规则组详情")
},
text = {
Text(text = showGroupItem.desc ?: "")
Column {
Text(text = showGroupItem.name)
Spacer(modifier = Modifier.height(10.dp))
Text(text = showGroupItem.desc ?: "")
}
},
confirmButton = {
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.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
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.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph
import li.songe.gkd.ui.destinations.AppItemPageDestination
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.ProfileTransitions
import li.songe.gkd.util.appInfoCacheFlow
@ -98,7 +99,7 @@ fun SlowGroupPage() {
)
)
}
.padding(10.dp, 5.dp),
.itemPadding(),
title = group.name,
desc = "${rule.rawSubs.name}/全局规则"
)
@ -118,12 +119,12 @@ fun SlowGroupPage() {
)
)
}
.padding(10.dp, 5.dp),
.itemPadding(),
title = group.name,
desc = "${rule.rawSubs.name}/应用规则/${appInfoCache[rule.app.id]?.name ?: rule.app.name ?: rule.app.id}"
)
}
item("empty") {
item {
Spacer(modifier = Modifier.height(40.dp))
if (ruleSummary.slowGroupCount == 0) {
Text(
@ -160,17 +161,18 @@ fun SlowGroupCard(title: String, desc: String, modifier: Modifier = Modifier) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = title,
fontSize = 18.sp,
style = MaterialTheme.typography.bodyLarge,
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
)
Text(
text = desc,
fontSize = 14.sp,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Icon(

View File

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

View File

@ -3,7 +3,6 @@ package li.songe.gkd.ui.component
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
@ -12,6 +11,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import li.songe.gkd.ui.style.itemPadding
@Composable
fun AuthCard(
@ -20,7 +20,7 @@ fun AuthCard(
onAuthClick: () -> Unit,
) {
Row(
modifier = Modifier.padding(16.dp, 12.dp),
modifier = Modifier.itemPadding(),
verticalAlignment = Alignment.CenterVertically
) {
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.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material3.Icon
@ -14,7 +13,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import li.songe.gkd.ui.style.itemPadding
@Composable
fun SettingItem(
@ -28,7 +27,7 @@ fun SettingItem(
onClick = onClick
)
.fillMaxWidth()
.padding(16.dp, 12.dp),
.itemPadding(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {

View File

@ -22,8 +22,6 @@ import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
@ -109,7 +107,7 @@ fun SubsAppCard(
softWrap = false,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(),
style = LocalTextStyle.current.let {
style = MaterialTheme.typography.bodyLarge.let {
if (appInfo?.isSystem == true) {
it.copy(textDecoration = TextDecoration.Underline)
} else {
@ -129,13 +127,16 @@ fun SubsAppCard(
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
} else {
Text(
text = "暂无规则",
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.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
@ -12,30 +11,38 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import li.songe.gkd.ui.style.itemPadding
@Composable
fun TextSwitch(
modifier: Modifier = Modifier,
name: String = "",
desc: String = "",
name: String,
desc: String? = null,
checked: Boolean = true,
enabled: Boolean = true,
onCheckedChange: ((Boolean) -> Unit)? = null,
) {
Row(
modifier = modifier.padding(16.dp, 12.dp),
modifier = modifier.itemPadding(),
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(
name,
text = name,
style = MaterialTheme.typography.bodyLarge,
)
Text(
desc,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Spacer(modifier = Modifier.width(10.dp))
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.destinations.ClickLogPageDestination
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.LocalNavController
import li.songe.gkd.util.launchAsFn
@ -131,7 +132,7 @@ fun useControlPage(): ScaffoldExt {
.clickable {
context.openUri(HOME_PAGE_URL)
}
.padding(16.dp, 12.dp),
.itemPadding(),
) {
Column(modifier = Modifier.weight(1f)) {
Text(
@ -158,7 +159,7 @@ fun useControlPage(): ScaffoldExt {
.clickable {
navController.navigate(ClickLogPageDestination)
}
.padding(16.dp, 12.dp),
.itemPadding(),
) {
Column(modifier = Modifier.weight(1f)) {
Text(
@ -185,7 +186,7 @@ fun useControlPage(): ScaffoldExt {
.clickable {
navController.navigate(SlowGroupPageDestination)
}
.padding(16.dp, 12.dp),
.itemPadding(),
) {
Column(modifier = Modifier.weight(1f)) {
Text(
@ -208,7 +209,7 @@ fun useControlPage(): ScaffoldExt {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp, 12.dp)
.itemPadding()
) {
Text(
text = subsStatus,

View File

@ -1,10 +1,5 @@
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.layout.Arrangement
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.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Autorenew
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
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
@ -41,7 +31,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
@ -56,22 +45,28 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.update
import li.songe.gkd.MainActivity
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.TextMenu
import li.songe.gkd.ui.component.TextSwitch
import li.songe.gkd.ui.destinations.AboutPageDestination
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.LocalNavController
import li.songe.gkd.util.UpdateTimeOption
import li.songe.gkd.util.buildLogFile
import li.songe.gkd.util.checkUpdate
import li.songe.gkd.util.checkUpdatingFlow
import li.songe.gkd.util.findOption
import li.songe.gkd.util.launchAsFn
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.navigate
import li.songe.gkd.util.shareFile
import li.songe.gkd.util.storeFlow
import li.songe.gkd.util.toast
import kotlin.math.sin
val settingsNav = BottomNavItem(
label = "设置", icon = Icons.Outlined.Settings
@ -85,12 +80,6 @@ fun useSettingsPage(): ScaffoldExt {
val vm = hiltViewModel<HomeVm>()
val uploadStatus by vm.uploadStatusFlow.collectAsState()
var showSubsIntervalDlg by remember {
mutableStateOf(false)
}
var showEnableDarkThemeDlg by remember {
mutableStateOf(false)
}
var showToastInputDlg by remember {
mutableStateOf(false)
}
@ -101,74 +90,6 @@ fun useSettingsPage(): ScaffoldExt {
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) {
var value by remember {
mutableStateOf(store.clickToast)
@ -335,12 +256,13 @@ fun useSettingsPage(): ScaffoldExt {
Text(
text = "常规",
modifier = Modifier.padding(16.dp, 12.dp),
modifier = Modifier.itemPadding(),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
)
TextSwitch(name = "触发提示",
TextSwitch(
name = "触发提示",
desc = store.clickToast,
checked = store.toastWhenClick,
modifier = Modifier.clickable {
@ -350,83 +272,57 @@ fun useSettingsPage(): ScaffoldExt {
storeFlow.value = store.copy(
toastWhenClick = it
)
})
}
)
TextSwitch(
name = "后台隐藏",
desc = "在[最近任务]界面中隐藏本应用",
desc = "在[最近任务]中隐藏本应用",
checked = store.excludeFromRecents,
onCheckedChange = {
storeFlow.value = store.copy(
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 = "更新",
modifier = Modifier.padding(16.dp, 12.dp),
modifier = Modifier.itemPadding(),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
)
Row(
modifier = Modifier
.clickable {
showSubsIntervalDlg = true
}
.padding(16.dp, 12.dp),
verticalAlignment = Alignment.CenterVertically
TextMenu(
title = "更新订阅",
option = UpdateTimeOption.allSubObject.findOption(store.updateSubsInterval)
) {
Text(
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"
)
}
storeFlow.update { s -> s.copy(updateSubsInterval = it.value) }
}
TextSwitch(name = "自动更新应用",
desc = "打开应用时自动检测是否存在新版本",
TextSwitch(
name = "自动更新",
desc = "打开应用时检测新版本",
checked = store.autoCheckAppUpdate,
onCheckedChange = {
storeFlow.value = store.copy(
@ -446,7 +342,7 @@ fun useSettingsPage(): ScaffoldExt {
}
)
.fillMaxWidth()
.padding(16.dp, 12.dp),
.itemPadding(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
@ -459,13 +355,14 @@ fun useSettingsPage(): ScaffoldExt {
Text(
text = "日志",
modifier = Modifier.padding(16.dp, 12.dp),
modifier = Modifier.itemPadding(),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
)
TextSwitch(name = "保存日志",
desc = "保存最近7天日志,关闭后无法定位解决错误",
TextSwitch(
name = "保存日志",
desc = "保存7天日志,帮助定位BUG",
checked = store.log2FileSwitch,
onCheckedChange = {
storeFlow.value = store.copy(
@ -485,13 +382,17 @@ fun useSettingsPage(): ScaffoldExt {
}
)
SettingItem(title = "分享日志", onClick = {
showShareLogDlg = true
})
SettingItem(
title = "分享日志",
imageVector = Icons.Default.Share,
onClick = {
showShareLogDlg = true
}
)
Text(
text = "其它",
modifier = Modifier.padding(16.dp, 12.dp),
modifier = Modifier.itemPadding(),
style = MaterialTheme.typography.titleMedium,
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
import android.os.Build
import androidx.activity.ComponentActivity
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
@ -12,33 +11,33 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.core.view.WindowInsetsControllerCompat
import li.songe.gkd.util.map
import li.songe.gkd.util.storeFlow
import li.songe.gkd.MainActivity
import li.songe.gkd.util.updateToastStyle
val LightColorScheme = lightColorScheme()
val DarkColorScheme = darkColorScheme()
val supportDynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
@Composable
fun AppTheme(
content: @Composable () -> Unit,
) {
// https://developer.android.com/jetpack/compose/designsystems/material3?hl=zh-cn
val context = LocalContext.current as ComponentActivity
val scope = rememberCoroutineScope()
val enableDarkTheme by storeFlow.map(scope) { s -> s.enableDarkTheme }.collectAsState()
val context = LocalContext.current as MainActivity
val enableDarkTheme by context.mainVm.enableDarkThemeFlow.collectAsState()
val enableDynamicColor by context.mainVm.enableDynamicColorFlow.collectAsState()
val systemInDarkTheme = isSystemInDarkTheme()
val darkTheme = enableDarkTheme ?: systemInDarkTheme
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val colorScheme = when {
dynamicColor && darkTheme -> dynamicDarkColorScheme(context)
dynamicColor && !darkTheme -> dynamicLightColorScheme(context)
supportDynamicColor && enableDynamicColor && darkTheme -> dynamicDarkColorScheme(context)
supportDynamicColor && enableDynamicColor && !darkTheme -> dynamicLightColorScheme(context)
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
// https://github.com/gkd-kit/gkd/pull/421
LaunchedEffect(darkTheme) {
WindowInsetsControllerCompat(context.window, context.window.decorView).apply {
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.encodeToString
import li.songe.gkd.appScope
import li.songe.gkd.ui.home.UpdateTimeOption
private inline fun <reified T> createStorageFlow(
key: String,
@ -57,6 +56,7 @@ data class Store(
val enableShizukuClick: Boolean = false,
val log2FileSwitch: Boolean = true,
val enableDarkTheme: Boolean? = null,
val enableDynamicColor: Boolean = true,
val enableAbFloatWindow: Boolean = true,
val sortType: Int = SortTypeOption.SortByName.value,
val showSystemApp: Boolean = true,
@ -88,8 +88,6 @@ val clickCountFlow by lazy {
recordStoreFlow.map(appScope) { r -> r.clickCount }
}
private val log2FileSwitchFlow by lazy { storeFlow.map(appScope) { s -> s.log2FileSwitch } }
fun increaseClickCount(n: Int = 1) {
recordStoreFlow.update {
it.copy(
@ -101,10 +99,5 @@ fun increaseClickCount(n: Int = 1) {
fun initStore() {
storeFlow.value
recordStoreFlow.value
appScope.launchTry(Dispatchers.IO) {
log2FileSwitchFlow.collect {
LogUtils.getConfig().isLog2FileSwitch = it
}
}
}