feat: multi-server failover forwarding + reorder/enable
Bark servers now form an ordered failover chain: forwarding tries each enabled server in list order, stopping at the first success. - model: BarkServer.enabled (defaults true; legacy JSON compatible) - core/BarkSender.pushToFirst: ordered try-until-success helper, shared by NotifyListenerService / PhoneCallMonitor / RetryWorker - BarkClient.push uses msg.copy(deviceKey=server.deviceKey) so each server pushes its own key - SettingsStore: drop currentServer/setCurrent; add moveServer + setServerEnabled; NotificationProcessor no longer sources deviceKey - UI: ServerRow RadioButton -> Checkbox + per-row test/move-up/move-down; remove top "send test push" button; HomeScreen shows enabled count - version 1.5.1 -> 1.6.0 (code 9 -> 10); CLAUDE.md synced Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 必发):
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<BarkServer>, msg: BarkMessage): Result<Unit> = 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("推送失败")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<com.a2i.forwarder.model.BarkMessage>(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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<BarkServer?>(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<List<BarkServer>>(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<BarkServer>) {
|
||||
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 }
|
||||
|
||||
@@ -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 条处理记录",
|
||||
|
||||
@@ -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<Uri?>(prefs.getString("icon_tree", null)?.let { runCatching { Uri.parse(it) }.getOrNull() }) }
|
||||
var exporting by remember { mutableStateOf(false) }
|
||||
var exportResult by remember { mutableStateOf<String?>(null) }
|
||||
var testing by remember { mutableStateOf(false) }
|
||||
var testMsg by remember { mutableStateOf<String?>(null) }
|
||||
var showAdd by remember { mutableStateOf(false) }
|
||||
var editing by remember { mutableStateOf<BarkServer?>(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,19 +364,26 @@ 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<String?>(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.padding(8.dp)) {
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(checked = server.enabled, onCheckedChange = onToggle)
|
||||
Column(Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
@@ -412,7 +394,7 @@ private fun ServerRow(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
if (selected) StatusBadge("当前", MaterialTheme.colorScheme.primary)
|
||||
if (!server.enabled) StatusBadge("停用", MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
Text(
|
||||
"${server.server} · key: ${if (server.deviceKey.isBlank()) "未填" else "已填"}",
|
||||
@@ -422,9 +404,27 @@ private fun ServerRow(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
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(
|
||||
it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (it.startsWith("已发送")) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(start = 48.dp, top = 2.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}"
|
||||
}
|
||||
Reference in New Issue
Block a user