diff --git a/CLAUDE.md b/CLAUDE.md index ebc7909..e7abf27 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,13 +56,13 @@ adb install -r app/build/outputs/apk/release/app-release.apk ``` NotifyListenerService (系统通知入口) → NotificationProcessor.process(sbn) → Decision(message?, reason, code, silent) - ├─ 在线:BarkClient.push(msg) → OkHttp POST;失败入 pending → RetryWorker (15min 周期) + ├─ 在线:BarkSender.pushToFirst(enabledServers, msg) 按序依次试,首个成功止;全失败入 pending → RetryWorker (15min 周期) └─ 离线:紧要通知(验证码/来电) → SmsFallbackForwarder 走短信兜底 → LogStore.addLog(...)(silent 的静默过滤不写日志) 旁路(不经通知监听器): PhoneCallMonitor 轮询 getCallState() → 来电/未接构造 Decision 推送(绕过 MIUI 限制) - UpdateChecker 查 GitHub Releases → 新版本横幅 → 应用内下载 + 系统安装器 + UpdateChecker 查 Gitea Releases → 新版本横幅 → 应用内下载 + 系统安装器 ``` ### 包结构 `com.a2i.forwarder` @@ -75,7 +75,8 @@ NotifyListenerService (系统通知入口) | `core/CodeExtractor.kt` | 三段正则:关键词+数字、数字+关键词、纯数字 4-8 位。检测到全 `*****` 时直接返回 null。 | | `core/AdFilter.kt` | 内置营销关键词 + 用户自定义关键词(`settings.adKeywords`)。 | | `core/AppParsers.kt` | 微信/QQ/Telegram 的 `clickUrl` 映射 + 重要消息关键词(@我、验证码、通话等)。 | -| `core/BarkClient.kt` | OkHttp POST 到 `${server}/push`,超时 10/10/15s;对 connection closed / 超时 / TLS 中断等瞬时 IOException 自动重试 3 次(退避 600/1200ms),HTTP 4xx/5xx 不重试。 | +| `core/BarkClient.kt` | OkHttp POST 到 `${server}/push`,超时 10/10/15s;body 用 `msg.copy(deviceKey=server.deviceKey)`;对 connection closed / 超时 / TLS 中断等瞬时 IOException 自动重试 3 次(退避 600/1200ms),HTTP 4xx/5xx 不重试。 | +| `core/BarkSender.kt` | 多服务器容错推送:按列表顺序依次 `BarkClient.push`,首个成功即止(`pushToFirst`);NotifyListenerService / PhoneCallMonitor / RetryWorker 共用。 | | `core/RetryWorker.kt` | WorkManager 周期任务,每 15 分钟重发 `pending` 队列中的失败消息。 | | `core/LogStore.kt` | 最近 300 条日志 + 失败重发队列(≤100)。DataStore 持久化。 | | `core/IconExporter.kt` | 一键导出所有已安装 App 图标到用户选择的 SAF 目录(用于部署到 iOS Bark 图床)。 | @@ -85,7 +86,7 @@ NotifyListenerService (系统通知入口) | `core/CarrierBalanceQuery.kt` | 向运营商服务号发查询短信、解析回执余量、余额≤5 自动挂起。 | | `core/SmsFallbackForwarder.kt` | 断网时把紧要通知(验证码/来电)用短信发到 iPhone,5 分钟限频 + 余额检查。 | | `core/UpdateChecker.kt` | 查 Gitea Releases 最新版(`git.sunlunfan.com`,国内可达,24h 节流)→ 语义比较 → 下载 APK → FileProvider 调起系统安装器。 | -| `store/SettingsStore.kt` | Bark 服务器列表、全局开关、广告关键词、图标前缀等。`MutableStateFlow` + DataStore。图标前缀默认 github raw(`DEFAULT_ICON_PREFIX`);国内不稳时可手填自建 Gitea `https://git.sunlunfan.com/song/a2i/raw/branch/main/icons/`。 | +| `store/SettingsStore.kt` | Bark 服务器列表(有序,`BarkServer.enabled` 勾选参与转发)、全局开关、广告关键词、图标前缀等。`MutableStateFlow` + DataStore。`moveServer` 调序、`setServerEnabled` 启停;转发按序依次试(见 `BarkSender`)。图标前缀默认 github raw,国内不稳时可手填自建 Gitea。 | | `store/AppRulesStore.kt` | 黑/白名单模式 + 默认黑名单(系统 App + 输入法 + 三星剪贴板/键盘 + GMS + 录音机/相册/查找/密码管理 + 通话管理/AI通话);默认黑名单带版本号 `bl_version`,升级时把新增噪音包合并进老用户黑名单(只增不删)。 | | `ui/nav/AppNav.kt` | 4 个底部 Tab + 日志页。 | | `ui/theme/*` | 固定品牌配色(`Color.kt`)、自定义 `Typography`(`Type.kt`),`A2iTheme` 关闭动态取色、统一深浅色方案(`Theme.kt`)。 | @@ -111,7 +112,7 @@ NotifyListenerService (系统通知入口) ## 版本与发布 -- 当前版本:`versionCode = 9`、`versionName = "1.5.1"`(位于 `app/build.gradle.kts`)。**每次改动后同步递增 versionCode/versionName,并同步更新本节。** +- 当前版本:`versionCode = 10`、`versionName = "1.6.0"`(位于 `app/build.gradle.kts`)。**每次改动后同步递增 versionCode/versionName,并同步更新本节。** - 远端仓库:`https://github.com/lsxf/a2i.git`,默认分支 `main`。 - 提交策略:`.gitignore` 已排除 `app/build`、`.gradle`、`.kotlin`、`local.properties`、`.claude/`、`do.ps1`、`gh-token.txt`(发版用 token)、`*.token`、截图与临时文件,**不要**提交它们。 - 发版流程(github + gitea 双发;UpdateChecker 查 Gitea 源,故 Gitea 必发): diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4827e25..abe248c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,8 +13,8 @@ android { applicationId = "com.a2i.forwarder" minSdk = 34 targetSdk = 36 - versionCode = 9 - versionName = "1.5.1" + versionCode = 10 + versionName = "1.6.0" } signingConfigs { diff --git a/app/src/main/java/com/a2i/forwarder/core/BarkClient.kt b/app/src/main/java/com/a2i/forwarder/core/BarkClient.kt index 3fcac99..a94c60c 100644 --- a/app/src/main/java/com/a2i/forwarder/core/BarkClient.kt +++ b/app/src/main/java/com/a2i/forwarder/core/BarkClient.kt @@ -36,7 +36,7 @@ class BarkClient(private val server: BarkServer) { * HTTP 4xx/5xx 是服务端拒绝,重试无意义,直接抛出。 */ private suspend fun doPushWithRetry(msg: BarkMessage) { - val body = json.encodeToString(msg).toRequestBody(JSON) + val body = json.encodeToString(msg.copy(deviceKey = server.deviceKey)).toRequestBody(JSON) val url = server.server.trim().removeSuffix("/") + "/push" val req = Request.Builder().url(url).post(body).build() var lastError: IOException? = null diff --git a/app/src/main/java/com/a2i/forwarder/core/BarkSender.kt b/app/src/main/java/com/a2i/forwarder/core/BarkSender.kt new file mode 100644 index 0000000..f5e06d6 --- /dev/null +++ b/app/src/main/java/com/a2i/forwarder/core/BarkSender.kt @@ -0,0 +1,25 @@ +package com.a2i.forwarder.core + +import com.a2i.forwarder.model.BarkMessage +import com.a2i.forwarder.model.BarkServer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * 多服务器容错推送:按列表顺序依次尝试,首个成功即止。 + * 全失败 → 返回最后一个错误;空列表 → "无可用服务器"。 + */ +object BarkSender { + suspend fun pushToFirst(servers: List, msg: BarkMessage): Result = withContext(Dispatchers.IO) { + runCatching { + require(servers.any()) { "无可用服务器" } + var lastErr: Throwable? = null + for (s in servers) { + val r = BarkClient(s).push(msg) + if (r.isSuccess) return@runCatching + lastErr = r.exceptionOrNull() + } + throw lastErr ?: error("推送失败") + } + } +} diff --git a/app/src/main/java/com/a2i/forwarder/core/NotificationProcessor.kt b/app/src/main/java/com/a2i/forwarder/core/NotificationProcessor.kt index 5b15239..0be2c73 100644 --- a/app/src/main/java/com/a2i/forwarder/core/NotificationProcessor.kt +++ b/app/src/main/java/com/a2i/forwarder/core/NotificationProcessor.kt @@ -187,7 +187,7 @@ class NotificationProcessor(private val context: Context) { val level = if (important) "timeSensitive" else "active" val msg = BarkMessage( - deviceKey = settings.currentServer.value?.deviceKey.orEmpty(), + deviceKey = "", title = msgTitle.take(100), body = content.ifBlank { title }.take(500), level = level, diff --git a/app/src/main/java/com/a2i/forwarder/core/PhoneCallMonitor.kt b/app/src/main/java/com/a2i/forwarder/core/PhoneCallMonitor.kt index 941d27a..1a98e88 100644 --- a/app/src/main/java/com/a2i/forwarder/core/PhoneCallMonitor.kt +++ b/app/src/main/java/com/a2i/forwarder/core/PhoneCallMonitor.kt @@ -70,8 +70,6 @@ class PhoneCallMonitor(private val context: Context) { val app = A2iApp.instance val settings = app.settings if (!settings.forwardingEnabled.value) return - val server = settings.currentServer.value ?: return - if (server.deviceKey.isBlank()) return when { // IDLE → RINGING:来电 @@ -82,14 +80,14 @@ class PhoneCallMonitor(private val context: Context) { if (now - lastRingTime > 3000) { lastRingTime = now val msg = BarkMessage( - deviceKey = server.deviceKey, + deviceKey = "", title = "来电", body = "有电话进来", level = "timeSensitive", group = "phone_call", isArchive = "1", ) - pushToServer(server, msg, app, "来电") + pushToServers(msg, app, "来电") } } @@ -102,14 +100,14 @@ class PhoneCallMonitor(private val context: Context) { from == TelephonyManager.CALL_STATE_RINGING && to == TelephonyManager.CALL_STATE_IDLE -> { if (!offhook && ringing) { val msg = BarkMessage( - deviceKey = server.deviceKey, + deviceKey = "", title = "未接来电", body = "未接来电", level = "timeSensitive", group = "phone_call", isArchive = "1", ) - pushToServer(server, msg, app, "未接来电") + pushToServers(msg, app, "未接来电") } ringing = false offhook = false @@ -123,14 +121,11 @@ class PhoneCallMonitor(private val context: Context) { } } - private fun pushToServer( - server: com.a2i.forwarder.model.BarkServer, - msg: BarkMessage, - app: A2iApp, - logBody: String, - ) { + private fun pushToServers(msg: BarkMessage, app: A2iApp, logBody: String) { scope.launch { - val result = BarkClient(server).push(msg) + val servers = app.settings.servers.value.filter { it.enabled && it.deviceKey.isNotBlank() } + if (servers.isEmpty()) return@launch + val result = BarkSender.pushToFirst(servers, msg) val now = System.currentTimeMillis() if (result.isSuccess) { app.logStore.addLog( @@ -142,7 +137,7 @@ class PhoneCallMonitor(private val context: Context) { ForwardLog(now, "phone_call", "电话", msg.title, logBody, "failed", reason) ) app.logStore.addPending( - PendingPush(UUID.randomUUID().toString(), now, server.id, json.encodeToString(msg)) + PendingPush(UUID.randomUUID().toString(), now, servers.first().id, json.encodeToString(msg)) ) } } diff --git a/app/src/main/java/com/a2i/forwarder/core/RetryWorker.kt b/app/src/main/java/com/a2i/forwarder/core/RetryWorker.kt index d5f5cd4..9568b5a 100644 --- a/app/src/main/java/com/a2i/forwarder/core/RetryWorker.kt +++ b/app/src/main/java/com/a2i/forwarder/core/RetryWorker.kt @@ -24,13 +24,11 @@ class RetryWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, var allOk = true val json = Json { ignoreUnknownKeys = true } + val servers = settings.servers.value.filter { it.enabled && it.deviceKey.isNotBlank() } for (p in list) { - val server = settings.servers.value.firstOrNull { it.id == p.serverId } - ?: settings.currentServer.value - ?: continue - if (server.deviceKey.isBlank()) { allOk = false; continue } + if (servers.isEmpty()) { allOk = false; break } val msg = runCatching { json.decodeFromString(p.messageJson) }.getOrNull() ?: continue - val r = BarkClient(server).push(msg) + val r = BarkSender.pushToFirst(servers, msg) if (r.isSuccess) store.removePending(p.id) else allOk = false } return if (allOk) Result.success() else Result.retry() diff --git a/app/src/main/java/com/a2i/forwarder/model/Models.kt b/app/src/main/java/com/a2i/forwarder/model/Models.kt index 8f0a37a..20d5822 100644 --- a/app/src/main/java/com/a2i/forwarder/model/Models.kt +++ b/app/src/main/java/com/a2i/forwarder/model/Models.kt @@ -9,6 +9,7 @@ data class BarkServer( val name: String, val server: String, val deviceKey: String, + val enabled: Boolean = true, ) const val OFFICIAL_BARK_SERVER = "https://api.day.app" diff --git a/app/src/main/java/com/a2i/forwarder/service/NotifyListenerService.kt b/app/src/main/java/com/a2i/forwarder/service/NotifyListenerService.kt index 82633e6..07336de 100644 --- a/app/src/main/java/com/a2i/forwarder/service/NotifyListenerService.kt +++ b/app/src/main/java/com/a2i/forwarder/service/NotifyListenerService.kt @@ -3,7 +3,7 @@ package com.a2i.forwarder.service import android.service.notification.NotificationListenerService import android.service.notification.StatusBarNotification import com.a2i.forwarder.A2iApp -import com.a2i.forwarder.core.BarkClient +import com.a2i.forwarder.core.BarkSender import com.a2i.forwarder.core.ForwardLog import com.a2i.forwarder.core.NotificationProcessor import com.a2i.forwarder.core.PendingPush @@ -58,20 +58,20 @@ class NotifyListenerService : NotificationListenerService() { return } - val server = app.settings.currentServer.value - if (server == null || server.deviceKey.isBlank()) { - app.logStore.addLog(ForwardLog(now, sbn.packageName, d.appLabel, msg.title, msg.body, "failed", "未配置 Bark key")) + val servers = app.settings.servers.value.filter { it.enabled && it.deviceKey.isNotBlank() } + if (servers.isEmpty()) { + app.logStore.addLog(ForwardLog(now, sbn.packageName, d.appLabel, msg.title, msg.body, "failed", "未配置可用 Bark 服务器")) return } - val result = BarkClient(server).push(msg) + val result = BarkSender.pushToFirst(servers, msg) if (result.isSuccess) { app.logStore.addLog(ForwardLog(now, sbn.packageName, d.appLabel, msg.title, msg.body, "sent", "")) } else { val reason = result.exceptionOrNull()?.message ?: "错误" app.logStore.addLog(ForwardLog(now, sbn.packageName, d.appLabel, msg.title, msg.body, "failed", reason)) app.logStore.addPending( - PendingPush(UUID.randomUUID().toString(), now, server.id, json.encodeToString(msg)) + PendingPush(UUID.randomUUID().toString(), now, servers.first().id, json.encodeToString(msg)) ) } } diff --git a/app/src/main/java/com/a2i/forwarder/store/SettingsStore.kt b/app/src/main/java/com/a2i/forwarder/store/SettingsStore.kt index 6267fc5..64fd88a 100644 --- a/app/src/main/java/com/a2i/forwarder/store/SettingsStore.kt +++ b/app/src/main/java/com/a2i/forwarder/store/SettingsStore.kt @@ -31,7 +31,6 @@ class SettingsStore( private object K { val SERVERS = stringPreferencesKey("servers") - val CURRENT = stringPreferencesKey("current") val ICON_PREFIX = stringPreferencesKey("icon_prefix") val FWD = booleanPreferencesKey("forwarding") val CODE = booleanPreferencesKey("code") @@ -60,9 +59,6 @@ class SettingsStore( private val _servers = MutableStateFlow(defaultServers) val servers = _servers.asStateFlow() - private val _currentServerId = MutableStateFlow(ID_OFFICIAL) - val currentServer = MutableStateFlow(defaultServers.first()) - val iconPrefix = MutableStateFlow(DEFAULT_ICON_PREFIX) val forwardingEnabled = MutableStateFlow(true) val codeEnabled = MutableStateFlow(true) @@ -92,8 +88,6 @@ class SettingsStore( ?.let { runCatching { json.decodeFromString>(it) }.getOrNull() } ?: defaultServers _servers.value = list - _currentServerId.value = p[K.CURRENT] ?: ID_OFFICIAL - currentServer.value = list.firstOrNull { it.id == _currentServerId.value } ?: list.firstOrNull() iconPrefix.value = p[K.ICON_PREFIX] ?: DEFAULT_ICON_PREFIX forwardingEnabled.value = p[K.FWD] ?: true codeEnabled.value = p[K.CODE] ?: true @@ -125,15 +119,11 @@ class SettingsStore( private suspend fun saveServers(list: List) { edit { it[K.SERVERS] = json.encodeToString(list) } _servers.value = list - refreshCurrent() } suspend fun addServer(name: String, server: String, key: String): BarkServer { val s = BarkServer(name = name.trim(), server = server.trim().removeSuffix("/"), deviceKey = key.trim()) saveServers(_servers.value + s) - if (_servers.value.count { it.id == s.id } == 1 && currentServer.value?.deviceKey.isNullOrBlank()) { - setCurrent(s.id) - } return s } @@ -142,19 +132,21 @@ class SettingsStore( suspend fun deleteServer(id: String) { val list = _servers.value.filterNot { it.id == id } saveServers(list) - if (_currentServerId.value == id) { - setCurrent(if (list.isEmpty()) "" else list.first().id) - } } - suspend fun setCurrent(id: String) { - edit { it[K.CURRENT] = id } - _currentServerId.value = id - refreshCurrent() + /** 上移/下移服务器(改变转发顺序) */ + suspend fun moveServer(id: String, up: Boolean) { + val list = _servers.value.toMutableList() + val idx = list.indexOfFirst { it.id == id } + if (idx < 0) return + val target = if (up) idx - 1 else idx + 1 + if (target < 0 || target >= list.size) return + java.util.Collections.swap(list, idx, target) + saveServers(list) } - private fun refreshCurrent() { - currentServer.value = _servers.value.firstOrNull { it.id == _currentServerId.value } ?: _servers.value.firstOrNull() + suspend fun setServerEnabled(id: String, enabled: Boolean) { + saveServers(_servers.value.map { if (it.id == id) it.copy(enabled = enabled) else it }) } suspend fun setIconPrefix(v: String) { edit { it[K.ICON_PREFIX] = v }; iconPrefix.value = v } diff --git a/app/src/main/java/com/a2i/forwarder/ui/screens/HomeScreen.kt b/app/src/main/java/com/a2i/forwarder/ui/screens/HomeScreen.kt index 152025b..6d6f3d4 100644 --- a/app/src/main/java/com/a2i/forwarder/ui/screens/HomeScreen.kt +++ b/app/src/main/java/com/a2i/forwarder/ui/screens/HomeScreen.kt @@ -62,7 +62,7 @@ fun HomeScreen(onOpenLog: () -> Unit, onOpenSettings: () -> Unit) { val context = LocalContext.current val app = A2iApp.instance val forwarding by app.settings.forwardingEnabled.collectAsState() - val current by app.settings.currentServer.collectAsState() + val servers by app.settings.servers.collectAsState() val sent by app.logStore.sentCount.collectAsState() val filtered by app.logStore.filteredCount.collectAsState() val failed by app.logStore.failedCount.collectAsState() @@ -180,21 +180,14 @@ fun HomeScreen(onOpenLog: () -> Unit, onOpenSettings: () -> Unit) { ) { v -> app.appScope.launch { app.settings.setForwarding(v) } } + val enabledServers = servers.filter { it.enabled && it.deviceKey.isNotBlank() } ClickRow( - title = current?.name?.ifBlank { "未命名服务器" } ?: "未配置 Bark 服务器", - subtitle = current?.server ?: "进入设置添加官方或自建 Bark 服务", + title = if (enabledServers.isEmpty()) "未启用 Bark 服务器" else "Bark 服务器(${enabledServers.size} 个启用)", + subtitle = enabledServers.firstOrNull()?.server ?: "进入设置勾选要转发的服务器", icon = Icons.Filled.Settings, tint = MaterialTheme.colorScheme.secondary, onClick = onOpenSettings, ) - if (current?.deviceKey.isNullOrBlank()) { - Text( - "当前服务器未填写 device key", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.error, - modifier = Modifier.padding(start = 50.dp), - ) - } ClickRow( title = "查看转发日志", subtitle = "最近 300 条处理记录", diff --git a/app/src/main/java/com/a2i/forwarder/ui/screens/SettingsScreen.kt b/app/src/main/java/com/a2i/forwarder/ui/screens/SettingsScreen.kt index 627a3ef..99d0041 100644 --- a/app/src/main/java/com/a2i/forwarder/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/com/a2i/forwarder/ui/screens/SettingsScreen.kt @@ -24,12 +24,15 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.History +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.RestartAlt import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.SystemUpdateAlt import androidx.compose.material.icons.filled.Update import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon @@ -72,14 +75,11 @@ fun SettingsScreen(onOpenLog: () -> Unit) { val context = LocalContext.current val app = A2iApp.instance val servers by app.settings.servers.collectAsState() - val current by app.settings.currentServer.collectAsState() val iconPrefix by app.settings.iconPrefix.collectAsState() val prefs = remember { context.getSharedPreferences("a2i_ui", Context.MODE_PRIVATE) } var treeUri by remember { mutableStateOf(prefs.getString("icon_tree", null)?.let { runCatching { Uri.parse(it) }.getOrNull() }) } var exporting by remember { mutableStateOf(false) } var exportResult by remember { mutableStateOf(null) } - var testing by remember { mutableStateOf(false) } - var testMsg by remember { mutableStateOf(null) } var showAdd by remember { mutableStateOf(false) } var editing by remember { mutableStateOf(null) } @@ -126,18 +126,21 @@ fun SettingsScreen(onOpenLog: () -> Unit) { item { ScreenTitle("设置", "管理 Bark 服务、图标资源和维护操作") } item { - SectionCard("Bark 服务器", subtitle = "官方服务和自建服务可以并存,当前服务器用于所有推送") { + SectionCard("Bark 服务器", subtitle = "勾选的服务器按列表顺序依次尝试转发,首个成功即止") { if (servers.isEmpty()) { Text("暂无服务器,请先添加。", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) } else { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - servers.forEach { s -> + servers.forEachIndexed { index, s -> ServerRow( server = s, - selected = s.id == current?.id, - onSelect = { app.appScope.launch { app.settings.setCurrent(s.id) } }, + isFirst = index == 0, + isLast = index == servers.lastIndex, + onToggle = { v -> app.appScope.launch { app.settings.setServerEnabled(s.id, v) } }, onEdit = { editing = s }, onDelete = { app.appScope.launch { app.settings.deleteServer(s.id) } }, + onMoveUp = { app.appScope.launch { app.settings.moveServer(s.id, up = true) } }, + onMoveDown = { app.appScope.launch { app.settings.moveServer(s.id, up = false) } }, ) } } @@ -150,33 +153,6 @@ fun SettingsScreen(onOpenLog: () -> Unit) { Spacer(Modifier.width(8.dp)) Text("添加服务器") } - FilledTonalButton( - onClick = { - app.appScope.launch { - testing = true - testMsg = testPush(app) - testing = false - } - }, - modifier = Modifier.fillMaxWidth().padding(top = 8.dp), - enabled = !testing, - ) { - if (testing) { - CircularProgressIndicator(Modifier.size(18.dp), strokeWidth = 2.dp) - } else { - Icon(Icons.AutoMirrored.Filled.Send, null, modifier = Modifier.size(18.dp)) - } - Spacer(Modifier.width(8.dp)) - Text("发送测试推送") - } - testMsg?.let { - Text( - it, - style = MaterialTheme.typography.bodySmall, - color = if (it.startsWith("已发送")) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.error, - modifier = Modifier.padding(top = 8.dp), - ) - } } } @@ -367,8 +343,7 @@ fun SettingsScreen(onOpenLog: () -> Unit) { onDismiss = { showAdd = false }, onConfirm = { n, s, k -> app.appScope.launch { - val ns = app.settings.addServer(n, s, k) - app.settings.setCurrent(ns.id) + app.settings.addServer(n, s, k) } showAdd = false }, @@ -389,41 +364,66 @@ fun SettingsScreen(onOpenLog: () -> Unit) { @Composable private fun ServerRow( server: BarkServer, - selected: Boolean, - onSelect: () -> Unit, + isFirst: Boolean, + isLast: Boolean, + onToggle: (Boolean) -> Unit, onEdit: () -> Unit, onDelete: () -> Unit, + onMoveUp: () -> Unit, + onMoveDown: () -> Unit, ) { + val app = A2iApp.instance + var testing by remember { mutableStateOf(false) } + var testMsg by remember { mutableStateOf(null) } Surface( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(8.dp), - color = if (selected) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.55f) else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.36f), - border = BorderStroke(1.dp, if (selected) MaterialTheme.colorScheme.primary.copy(alpha = 0.32f) else MaterialTheme.colorScheme.outlineVariant), + color = if (server.enabled) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.40f) else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.36f), + border = BorderStroke(1.dp, if (server.enabled) MaterialTheme.colorScheme.primary.copy(alpha = 0.30f) else MaterialTheme.colorScheme.outlineVariant), ) { - Row(Modifier.fillMaxWidth().padding(8.dp), verticalAlignment = Alignment.CenterVertically) { - RadioButton(selected = selected, onClick = onSelect) - Column(Modifier.weight(1f)) { - Row(verticalAlignment = Alignment.CenterVertically) { + Column(Modifier.padding(8.dp)) { + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Checkbox(checked = server.enabled, onCheckedChange = onToggle) + Column(Modifier.weight(1f)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + server.name.ifBlank { "未命名" }, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + if (!server.enabled) StatusBadge("停用", MaterialTheme.colorScheme.onSurfaceVariant) + } Text( - server.name.ifBlank { "未命名" }, - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium, + "${server.server} · key: ${if (server.deviceKey.isBlank()) "未填" else "已填"}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f), ) - if (selected) StatusBadge("当前", MaterialTheme.colorScheme.primary) } + IconButton( + onClick = { if (!testing) app.appScope.launch { testing = true; testMsg = testPush(app, server); testing = false } }, + enabled = !testing, + ) { + if (testing) CircularProgressIndicator(Modifier.size(18.dp), strokeWidth = 2.dp) + else Icon(Icons.AutoMirrored.Filled.Send, "测试") + } + IconButton(onClick = onMoveUp, enabled = !isFirst) { Icon(Icons.Filled.KeyboardArrowUp, "上移") } + IconButton(onClick = onMoveDown, enabled = !isLast) { Icon(Icons.Filled.KeyboardArrowDown, "下移") } + IconButton(onClick = onEdit) { Icon(Icons.Filled.Edit, "编辑") } + IconButton(onClick = onDelete) { Icon(Icons.Filled.Delete, "删除") } + } + testMsg?.let { Text( - "${server.server} · key: ${if (server.deviceKey.isBlank()) "未填" else "已填"}", + it, style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + color = if (it.startsWith("已发送")) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.error, + modifier = Modifier.padding(start = 48.dp, top = 2.dp), ) } - IconButton(onClick = onEdit) { Icon(Icons.Filled.Edit, "编辑") } - IconButton(onClick = onDelete) { Icon(Icons.Filled.Delete, "删除") } } } } @@ -455,15 +455,14 @@ private fun ServerDialog( ) } -private suspend fun testPush(app: A2iApp): String { - val s = app.settings.currentServer.value ?: return "未选择服务器" - if (s.deviceKey.isBlank()) return "未填写 device key" +private suspend fun testPush(app: A2iApp, server: BarkServer): String { + if (server.deviceKey.isBlank()) return "未填写 device key" val msg = BarkMessage( - deviceKey = s.deviceKey, + deviceKey = server.deviceKey, title = "a2i 测试", body = "如果你在 iOS 看到这条推送,说明配置成功", level = "active", ) - val r = BarkClient(s).push(msg) + val r = BarkClient(server).push(msg) return if (r.isSuccess) "已发送,请查看 iOS 端" else "发送失败:${r.exceptionOrNull()?.message}" } \ No newline at end of file