



























@@ -493,6 +493,8 @@ private fun playVoiceSetupTone() {
493493Handler(Looper.getMainLooper()).postDelayed({ tone.release() }, 300L)
494494}
495495496+private const val NOTIFICATION_PICKER_RESULT_LIMIT = 40
497+496498@Composable
497499private fun NotificationSettingsScreen(
498500viewModel: MainViewModel,
@@ -507,6 +509,19 @@ private fun NotificationSettingsScreen(
507509val quietEnd by viewModel.notificationForwardingQuietEnd.collectAsState()
508510val maxEventsPerMinute by viewModel.notificationForwardingMaxEventsPerMinute.collectAsState()
509511val modeLabel = if (mode == NotificationPackageFilterMode.Blocklist) "Blocklist" else "Allowlist"
512+val installedApps = remember(context, packages) { queryInstalledApps(context, packages) }
513+var notificationPickerExpanded by remember { mutableStateOf(false) }
514+var notificationAppSearch by remember { mutableStateOf("") }
515+var notificationShowSystemApps by remember { mutableStateOf(false) }
516+val filteredApps =
517+ remember(installedApps, packages, notificationAppSearch, notificationShowSystemApps) {
518+ filterNotificationAppsForPicker(
519+ apps = installedApps,
520+ selectedPackages = packages,
521+ query = notificationAppSearch,
522+ showSystemApps = notificationShowSystemApps,
523+ )
524+ }
510525var listenerEnabled by remember { mutableStateOf(DeviceNotificationListenerService.isAccessEnabled(context)) }
511526val notificationPermissionLauncher =
512527 rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
@@ -567,6 +582,124 @@ private fun NotificationSettingsScreen(
567582 )
568583 }
569584 }
585+NotificationPackagePickerPanel(
586+ mode = mode,
587+ selectedPackages = packages,
588+ apps = filteredApps,
589+ search = notificationAppSearch,
590+ showSystemApps = notificationShowSystemApps,
591+ expanded = notificationPickerExpanded,
592+ onSearchChange = { notificationAppSearch = it },
593+ onShowSystemAppsChange = { notificationShowSystemApps = it },
594+ onExpandedChange = { notificationPickerExpanded = it },
595+ onPackageSelectionChange = { packageName, selected ->
596+val next = packages.toMutableSet()
597+if (selected) {
598+ next.add(packageName)
599+ } else {
600+ next.remove(packageName)
601+ }
602+ viewModel.setNotificationForwardingPackagesCsv(next.sorted().joinToString(","))
603+ },
604+ )
605+ }
606+}
607+608+@Composable
609+private fun NotificationPackagePickerPanel(
610+mode: NotificationPackageFilterMode,
611+selectedPackages: Set<String>,
612+apps: List<InstalledApp>,
613+search: String,
614+showSystemApps: Boolean,
615+expanded: Boolean,
616+onSearchChange: (String) -> Unit,
617+onShowSystemAppsChange: (Boolean) -> Unit,
618+onExpandedChange: (Boolean) -> Unit,
619+onPackageSelectionChange: (String, Boolean) -> Unit,
620+) {
621+val visibleApps = apps.take(NOTIFICATION_PICKER_RESULT_LIMIT)
622+ClawPanel {
623+Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
624+Text(text = "App Filter", style = ClawTheme.type.section, color = ClawTheme.colors.text)
625+Text(
626+ text = notificationPackageSelectionSummary(mode = mode, selectedCount = selectedPackages.size),
627+ style = ClawTheme.type.body,
628+ color = ClawTheme.colors.textMuted,
629+ )
630+ClawSecondaryButton(
631+ text = if (expanded) "Close App Picker" else "Open App Picker",
632+ onClick = { onExpandedChange(!expanded) },
633+ modifier = Modifier.fillMaxWidth(),
634+ )
635+if (expanded) {
636+ClawTextField(value = search, onValueChange = onSearchChange, placeholder = "Search apps")
637+SettingsToggleListRow(
638+SettingsToggleRow(
639+ title = "Show System Apps",
640+ subtitle = "Include Android and background packages.",
641+ icon = Icons.Default.Storage,
642+ checked = showSystemApps,
643+ onCheckedChange = onShowSystemAppsChange,
644+ ),
645+ )
646+if (visibleApps.isEmpty()) {
647+Text(text = "No matching apps.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
648+ } else {
649+ClawSeparatedColumn(items = visibleApps) { app ->
650+NotificationPackageAppRow(
651+ app = app,
652+ selected = selectedPackages.contains(app.packageName),
653+ onSelectedChange = { selected -> onPackageSelectionChange(app.packageName, selected) },
654+ )
655+ }
656+if (apps.size > visibleApps.size) {
657+Text(
658+ text = "Showing ${visibleApps.size} of ${apps.size}. Refine search for more.",
659+ style = ClawTheme.type.caption,
660+ color = ClawTheme.colors.textMuted,
661+ )
662+ }
663+ }
664+ }
665+ }
666+ }
667+}
668+669+@Composable
670+private fun NotificationPackageAppRow(
671+app: InstalledApp,
672+selected: Boolean,
673+onSelectedChange: (Boolean) -> Unit,
674+) {
675+Row(
676+ modifier =
677+Modifier
678+ .fillMaxWidth()
679+ .heightIn(min = 58.dp)
680+ .clickable { onSelectedChange(!selected) }
681+ .padding(vertical = 7.dp),
682+ verticalAlignment = Alignment.CenterVertically,
683+ horizontalArrangement = Arrangement.spacedBy(9.dp),
684+ ) {
685+ClawTextBadge(text = notificationAppBadge(app.label))
686+Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(1.dp)) {
687+Text(
688+ text = app.label,
689+ style = ClawTheme.type.body,
690+ color = ClawTheme.colors.text,
691+ maxLines = 1,
692+ overflow = TextOverflow.Ellipsis,
693+ )
694+Text(
695+ text = app.packageName,
696+ style = ClawTheme.type.caption,
697+ color = ClawTheme.colors.textMuted,
698+ maxLines = 1,
699+ overflow = TextOverflow.Ellipsis,
700+ )
701+ }
702+Switch(checked = selected, onCheckedChange = onSelectedChange)
570703 }
571704}
572705@@ -1112,6 +1245,55 @@ private fun cronJobStatus(job: GatewayCronJobSummary): ClawStatus {
11121245 }
11131246}
111412471248+internal fun filterNotificationAppsForPicker(
1249+apps: List<InstalledApp>,
1250+selectedPackages: Set<String>,
1251+query: String,
1252+showSystemApps: Boolean,
1253+): List<InstalledApp> {
1254+val normalizedQuery = query.trim().lowercase()
1255+return apps.filter { app ->
1256+val selected = app.packageName in selectedPackages
1257+val visibleByType = showSystemApps || !app.isSystemApp || selected
1258+val visibleBySearch =
1259+ normalizedQuery.isEmpty() ||
1260+ app.label.lowercase().contains(normalizedQuery) ||
1261+ app.packageName.lowercase().contains(normalizedQuery)
1262+ visibleByType && visibleBySearch
1263+ }
1264+}
1265+1266+private fun notificationPackageSelectionSummary(
1267+mode: NotificationPackageFilterMode,
1268+selectedCount: Int,
1269+): String =
1270+when (mode) {
1271+NotificationPackageFilterMode.Allowlist ->
1272+if (selectedCount == 0) {
1273+"No apps selected. Nothing forwards until you add apps."
1274+ } else {
1275+"$selectedCount ${if (selectedCount == 1) "app" else "apps"} allowed to forward."
1276+ }
1277+NotificationPackageFilterMode.Blocklist ->
1278+if (selectedCount == 0) {
1279+"No apps blocked. Apps can forward unless you add blocks."
1280+ } else {
1281+"$selectedCount ${if (selectedCount == 1) "app" else "apps"} blocked from forwarding."
1282+ }
1283+ }
1284+1285+private fun notificationAppBadge(label: String): String {
1286+val initials =
1287+ label
1288+ .split(' ', '-', '_', '.')
1289+ .asSequence()
1290+ .filter { it.isNotBlank() }
1291+ .take(2)
1292+ .mapNotNull { it.firstOrNull()?.uppercaseChar()?.toString() }
1293+ .joinToString("")
1294+return initials.ifBlank { "A" }
1295+}
1296+11151297/**
11161298 * Converts cron wake times into short relative labels for scheduled-work rows.
11171299 */
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。