diff --git a/app/src/main/kotlin/li/songe/gkd/ui/AboutPage.kt b/app/src/main/kotlin/li/songe/gkd/ui/AboutPage.kt index 714f13ed0d..fce1922f76 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/AboutPage.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/AboutPage.kt @@ -71,6 +71,7 @@ import li.songe.gkd.util.SafeR import li.songe.gkd.util.UpdateChannelOption import li.songe.gkd.util.buildLogFile import li.songe.gkd.util.checkUpdate +import li.songe.gkd.util.copyText import li.songe.gkd.util.findOption import li.songe.gkd.util.format import li.songe.gkd.util.launchAsFn @@ -253,6 +254,7 @@ fun AboutPage() { text = REPOSITORY_URL, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.secondary, + modifier = Modifier.clickable(onClick = throttle { copyText(REPOSITORY_URL) }), ) } @@ -272,6 +274,7 @@ fun AboutPage() { text = ISSUES_URL, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.secondary, + modifier = Modifier.clickable(onClick = throttle { copyText(ISSUES_URL) }), ) } if (META.updateEnabled) { diff --git a/app/src/main/kotlin/li/songe/gkd/ui/ActionLogVm.kt b/app/src/main/kotlin/li/songe/gkd/ui/ActionLogVm.kt index 7fb8318324..ca59b1420a 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/ActionLogVm.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/ActionLogVm.kt @@ -18,7 +18,6 @@ class ActionLogVm : ViewModel() { val pagingDataFlow = Pager(PagingConfig(pageSize = 100)) { DbSet.actionLogDao.pagingSource() } .flow - .cachedIn(viewModelScope) .combine(subsIdToRawFlow) { pagingData, subsIdToRaw -> pagingData.map { c -> val group = if (c.groupType == SubsConfig.AppGroupType) { @@ -37,6 +36,7 @@ class ActionLogVm : ViewModel() { Tuple3(c, group, rule) } } + .cachedIn(viewModelScope) val actionLogCountFlow = DbSet.actionLogDao.count().stateIn(viewModelScope, SharingStarted.Eagerly, 0) diff --git a/app/src/main/kotlin/li/songe/gkd/ui/AuthA11yPage.kt b/app/src/main/kotlin/li/songe/gkd/ui/AuthA11yPage.kt index 666eb9d0c3..f0486a0363 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/AuthA11yPage.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/AuthA11yPage.kt @@ -50,6 +50,7 @@ import li.songe.gkd.shizuku.execCommandForResult import li.songe.gkd.ui.component.updateDialogOptions import li.songe.gkd.ui.style.EmptyHeight import li.songe.gkd.ui.style.itemHorizontalPadding +import li.songe.gkd.ui.style.surfaceCardColors import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.ProfileTransitions import li.songe.gkd.util.launchAsFn @@ -102,7 +103,8 @@ fun AuthA11yPage() { modifier = Modifier .padding(itemHorizontalPadding, 0.dp) .fillMaxWidth(), - onClick = { } + onClick = { }, + colors = surfaceCardColors, ) { Text( modifier = Modifier.padding(cardHorizontalPadding, 8.dp), @@ -153,7 +155,8 @@ fun AuthA11yPage() { modifier = Modifier .padding(itemHorizontalPadding, 0.dp) .fillMaxWidth(), - onClick = { } + onClick = { }, + colors = surfaceCardColors, ) { Text( modifier = Modifier.padding(cardHorizontalPadding, 8.dp), @@ -203,7 +206,8 @@ fun AuthA11yPage() { modifier = Modifier .padding(itemHorizontalPadding, 0.dp) .fillMaxWidth(), - onClick = { } + onClick = { }, + colors = surfaceCardColors, ) { Text( modifier = Modifier.padding(cardHorizontalPadding, 8.dp), diff --git a/app/src/main/kotlin/li/songe/gkd/ui/component/AuthCard.kt b/app/src/main/kotlin/li/songe/gkd/ui/component/AuthCard.kt index edadfee6b6..e7fc41df65 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/component/AuthCard.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/component/AuthCard.kt @@ -36,7 +36,7 @@ fun AuthCard( color = MaterialTheme.colorScheme.onSurfaceVariant, ) } - Spacer(modifier = Modifier.width(10.dp)) + Spacer(modifier = Modifier.width(8.dp)) OutlinedButton(onClick = throttle(fn = onAuthClick)) { Text(text = "授权") } diff --git a/app/src/main/kotlin/li/songe/gkd/ui/component/SettingItem.kt b/app/src/main/kotlin/li/songe/gkd/ui/component/SettingItem.kt index cda87ee8bc..17000b3395 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/component/SettingItem.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/component/SettingItem.kt @@ -1,7 +1,6 @@ package li.songe.gkd.ui.component import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -27,20 +26,23 @@ fun SettingItem( subtitle: String? = null, suffix: String? = null, onSuffixClick: (() -> Unit)? = null, - imageVector: ImageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - onClick: () -> Unit, + imageVector: ImageVector? = Icons.AutoMirrored.Filled.KeyboardArrowRight, + onClick: (() -> Unit)? = null, ) { Row( modifier = Modifier - .clickable( - onClick = throttle(fn = onClick) - ) + .let { + if (onClick != null) { + it.clickable(onClick = throttle(fn = onClick)) + } else { + it + } + } .fillMaxWidth() .itemPadding(), - horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - Column(modifier = Modifier.weight(1f)) { + Column(modifier = if (imageVector != null) Modifier.weight(1f) else Modifier.fillMaxWidth()) { Text( text = title, style = MaterialTheme.typography.bodyLarge, @@ -72,6 +74,8 @@ fun SettingItem( } } } - Icon(imageVector = imageVector, contentDescription = title) + if (imageVector != null) { + Icon(imageVector = imageVector, contentDescription = title) + } } } \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/ui/component/SubsItemCard.kt b/app/src/main/kotlin/li/songe/gkd/ui/component/SubsItemCard.kt index 47672d21b2..e838fadf70 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/component/SubsItemCard.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/component/SubsItemCard.kt @@ -79,7 +79,7 @@ fun SubsItemCard( containerColor = if (isSelected) { MaterialTheme.colorScheme.primaryContainer } else { - Color.Unspecified + MaterialTheme.colorScheme.surfaceContainer } ), ) { diff --git a/app/src/main/kotlin/li/songe/gkd/ui/component/TextSwitch.kt b/app/src/main/kotlin/li/songe/gkd/ui/component/TextSwitch.kt index 145b29f378..91c5f63132 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/component/TextSwitch.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/component/TextSwitch.kt @@ -63,7 +63,7 @@ fun TextSwitch( } } } - Spacer(modifier = Modifier.width(10.dp)) + Spacer(modifier = Modifier.width(8.dp)) Switch( checked = checked, enabled = enabled, diff --git a/app/src/main/kotlin/li/songe/gkd/ui/home/ControlPage.kt b/app/src/main/kotlin/li/songe/gkd/ui/home/ControlPage.kt index be1828344d..211a9e6e0b 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/home/ControlPage.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/home/ControlPage.kt @@ -1,31 +1,49 @@ package li.songe.gkd.ui.home +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.HelpOutline +import androidx.compose.material.icons.filled.History +import androidx.compose.material.icons.filled.Memory +import androidx.compose.material.icons.outlined.Eco +import androidx.compose.material.icons.outlined.Equalizer import androidx.compose.material.icons.outlined.Home +import androidx.compose.material.icons.outlined.Layers +import androidx.compose.material.icons.outlined.Notifications import androidx.compose.material.icons.outlined.RocketLaunch -import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Card import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import com.ramcosta.composedestinations.generated.destinations.ActionLogPageDestination @@ -41,12 +59,10 @@ import li.songe.gkd.permission.writeSecureSettingsState import li.songe.gkd.service.A11yService import li.songe.gkd.service.ManageService import li.songe.gkd.service.switchA11yService -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.style.EmptyHeight import li.songe.gkd.ui.style.itemHorizontalPadding -import li.songe.gkd.ui.style.itemPadding +import li.songe.gkd.ui.style.itemVerticalPadding +import li.songe.gkd.ui.style.surfaceCardColors import li.songe.gkd.util.HOME_PAGE_URL import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.SafeR @@ -82,17 +98,9 @@ fun useControlPage(): ScaffoldExt { contentDescription = null, ) } - IconButton(onClick = throttle { openUri(HOME_PAGE_URL) }) { - Icon( - imageVector = Icons.AutoMirrored.Outlined.HelpOutline, - contentDescription = null, - ) - } }) } ) { contentPadding -> - val latestRecordDesc by vm.latestRecordDescFlow.collectAsState() - val subsStatus by vm.subsStatusFlow.collectAsState() val store by storeFlow.collectAsState() val ruleSummary by ruleSummaryFlow.collectAsState() @@ -109,92 +117,226 @@ fun useControlPage(): ScaffoldExt { .padding(contentPadding) ) { if (writeSecureSettings) { - TextSwitch( + PageItemCard( + imageVector = Icons.Default.Memory, title = "服务状态", subtitle = if (a11yRunning) "无障碍服务正在运行" else "无障碍服务已关闭", - checked = a11yRunning, - onCheckedChange = { - switchA11yService() - }) + rightContent = { + Switch( + checked = a11yRunning, + onCheckedChange = throttle { + switchA11yService() + }, + ) + } + ) } if (!writeSecureSettings && !a11yRunning) { - AuthCard( + PageItemCard( + imageVector = Icons.Default.Memory, title = "无障碍授权", - desc = if (a11yBroken) "服务故障,请重新授权" else "授权使无障碍服务运行", - onAuthClick = { - navController.toDestinationsNavigator().navigate(AuthA11YPageDestination) - }) + subtitle = if (a11yBroken) "服务故障,请重新授权" else "授权使无障碍服务运行", + rightContent = { + OutlinedButton(onClick = throttle { + navController.toDestinationsNavigator() + .navigate(AuthA11YPageDestination) + }) { + Text(text = "授权") + } + } + ) } - TextSwitch( + PageItemCard( + imageVector = Icons.Outlined.Notifications, title = "常驻通知", subtitle = "显示运行状态及统计数据", - checked = manageRunning && store.enableStatusService, - onCheckedChange = vm.viewModelScope.launchAsFn { - if (it) { - requiredPermission(context, notificationState) - storeFlow.value = store.copy( - enableStatusService = true + rightContent = { + Switch( + checked = manageRunning && store.enableStatusService, + onCheckedChange = throttle(fn = vm.viewModelScope.launchAsFn { + if (it) { + requiredPermission(context, notificationState) + storeFlow.value = store.copy( + enableStatusService = true + ) + ManageService.start() + } else { + storeFlow.value = store.copy( + enableStatusService = false + ) + ManageService.stop() + } + }), + ) + } + ) + + Card( + modifier = Modifier + .padding(itemHorizontalPadding, 4.dp) + .fillMaxWidth(), + shape = RoundedCornerShape(20.dp), + colors = surfaceCardColors, + onClick = {} + ) { + IconTextCard( + imageVector = Icons.Outlined.Equalizer + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = "数据概览", + style = MaterialTheme.typography.bodyLarge, + ) + val usedSubsItemCount by vm.usedSubsItemCountFlow.collectAsState() + AnimatedVisibility(usedSubsItemCount > 0) { + Text( + text = "已开启 $usedSubsItemCount 条订阅", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + Column( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = itemVerticalPadding + 8.dp, ) - ManageService.start() - } else { - storeFlow.value = store.copy( - enableStatusService = false + ) { + val latestRecordDesc by vm.latestRecordDescFlow.collectAsState() + val subsStatus by vm.subsStatusFlow.collectAsState() + AnimatedVisibility(subsStatus.isNotEmpty()) { + Text( + text = subsStatus, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) - ManageService.stop() } - }) + AnimatedVisibility(latestRecordDesc != null) { + Text( + text = "最近点击: $latestRecordDesc", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Spacer(modifier = Modifier.height(itemVerticalPadding)) + } + } - SettingItem( + PageItemCard( title = "触发记录", subtitle = "规则误触可定位关闭", + imageVector = Icons.Default.History, onClick = { - navController.toDestinationsNavigator().navigate(ActionLogPageDestination) + navController.toDestinationsNavigator() + .navigate(ActionLogPageDestination) } ) if (store.enableActivityLog) { - SettingItem( + PageItemCard( title = "界面记录", subtitle = "记录打开的应用及界面", + imageVector = Icons.Outlined.Layers, onClick = { - navController.toDestinationsNavigator().navigate(ActivityLogPageDestination) + navController.toDestinationsNavigator() + .navigate(ActivityLogPageDestination) } ) } - if (ruleSummary.slowGroupCount > 0) { - SettingItem( + AnimatedVisibility(ruleSummary.slowGroupCount > 0) { + PageItemCard( title = "缓慢查询", subtitle = "存在 ${ruleSummary.slowGroupCount} 条记录", + imageVector = Icons.Outlined.Eco, onClick = { - navController.toDestinationsNavigator().navigate(SlowGroupPageDestination) + navController.toDestinationsNavigator() + .navigate(SlowGroupPageDestination) } ) } - HorizontalDivider(modifier = Modifier.padding(horizontal = itemHorizontalPadding)) + + PageItemCard( + title = "了解 GKD", + subtitle = "查阅规则文档和常见问题", + imageVector = Icons.AutoMirrored.Outlined.HelpOutline, + onClick = { + openUri(HOME_PAGE_URL) + } + ) + Spacer(modifier = Modifier.height(EmptyHeight)) + } + } +} + + +@Composable +private fun PageItemCard( + imageVector: ImageVector, + title: String, + subtitle: String, + onClick: () -> Unit = {}, + rightContent: @Composable (() -> Unit)? = null, +) { + Card( + modifier = Modifier + .padding(itemHorizontalPadding, 4.dp) + .fillMaxWidth(), + shape = RoundedCornerShape(20.dp), + colors = surfaceCardColors, + onClick = throttle(fn = onClick) + ) { + IconTextCard( + imageVector = imageVector, + ) { Column( - modifier = Modifier - .fillMaxWidth() - .itemPadding() + modifier = Modifier.weight(1f) ) { Text( - text = subsStatus, + text = title, style = MaterialTheme.typography.bodyLarge, ) - if (latestRecordDesc != null) { - Text( - text = "最近点击: $latestRecordDesc", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - softWrap = false, - overflow = TextOverflow.Ellipsis, - ) - } + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + if (rightContent != null) { + Spacer(Modifier.width(8.dp)) + rightContent.invoke() } - - Spacer(modifier = Modifier.height(EmptyHeight)) } } } + +@Composable +private fun IconTextCard( + imageVector: ImageVector, + content: @Composable () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(itemVerticalPadding), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = imageVector, + contentDescription = null, + modifier = Modifier + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primaryContainer) + .padding(8.dp) + .size(24.dp), + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(itemHorizontalPadding)) + content() + } +} diff --git a/app/src/main/kotlin/li/songe/gkd/ui/home/HomeVm.kt b/app/src/main/kotlin/li/songe/gkd/ui/home/HomeVm.kt index e14f8b8fc7..892504b3a3 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/home/HomeVm.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/home/HomeVm.kt @@ -19,6 +19,7 @@ import li.songe.gkd.util.orderedAppInfosFlow import li.songe.gkd.util.ruleSummaryFlow import li.songe.gkd.util.storeFlow import li.songe.gkd.util.subsIdToRawFlow +import li.songe.gkd.util.usedSubsEntriesFlow class HomeVm : ViewModel() { @@ -51,6 +52,8 @@ class HomeVm : ViewModel() { }.stateIn(appScope, SharingStarted.Eagerly, "") } + val usedSubsItemCountFlow = usedSubsEntriesFlow.map(viewModelScope) { it.size } + private val appIdToOrderFlow = DbSet.actionLogDao.queryLatestUniqueAppIds().map { appIds -> appIds.mapIndexed { index, appId -> appId to index }.toMap() } diff --git a/app/src/main/kotlin/li/songe/gkd/ui/style/Color.kt b/app/src/main/kotlin/li/songe/gkd/ui/style/Color.kt new file mode 100644 index 0000000000..15d74198b2 --- /dev/null +++ b/app/src/main/kotlin/li/songe/gkd/ui/style/Color.kt @@ -0,0 +1,12 @@ +package li.songe.gkd.ui.style + +import androidx.compose.material3.CardColors +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable + + +val surfaceCardColors: CardColors + @Composable + get() = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer) + diff --git a/app/src/main/kotlin/li/songe/gkd/util/Constants.kt b/app/src/main/kotlin/li/songe/gkd/util/Constants.kt index 5b72d75dce..179fbc4529 100644 --- a/app/src/main/kotlin/li/songe/gkd/util/Constants.kt +++ b/app/src/main/kotlin/li/songe/gkd/util/Constants.kt @@ -14,3 +14,5 @@ const val HOME_PAGE_URL = "https://gkd.li" const val LOCAL_SUBS_ID = -2L const val LOCAL_HTTP_SUBS_ID = -1L val LOCAL_SUBS_IDS = arrayOf(LOCAL_SUBS_ID, LOCAL_HTTP_SUBS_ID) + +const val EMPTY_RULE_TIP = "暂无规则" diff --git a/app/src/main/kotlin/li/songe/gkd/util/SubsState.kt b/app/src/main/kotlin/li/songe/gkd/util/SubsState.kt index 7e94fab7bf..24e8c857fa 100644 --- a/app/src/main/kotlin/li/songe/gkd/util/SubsState.kt +++ b/app/src/main/kotlin/li/songe/gkd/util/SubsState.kt @@ -141,7 +141,7 @@ data class RuleSummary( "" } } else { - "暂无规则" + EMPTY_RULE_TIP } val slowGlobalGroups = @@ -153,8 +153,9 @@ data class RuleSummary( val slowGroupCount = slowGlobalGroups.size + slowAppGroups.size } -private val usedSubsEntriesFlow by lazy { +val usedSubsEntriesFlow by lazy { subsEntriesFlow.map { it.filter { s -> s.subsItem.enable && s.subscription != null } } + .stateIn(appScope, SharingStarted.Eagerly, emptyList()) } val ruleSummaryFlow by lazy {