From 20342575601a73b5eb6f8257fb11124d2b7aec9e Mon Sep 17 00:00:00 2001 From: Song <168455@qq.com> Date: Fri, 10 Jul 2026 08:35:00 +0800 Subject: [PATCH] feat: add Bark setup help to settings --- .../java/com/a2i/forwarder/ui/Components.kt | 52 +++++-- .../forwarder/ui/screens/SettingsScreen.kt | 141 +++++++++++++++++- 2 files changed, 174 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/a2i/forwarder/ui/Components.kt b/app/src/main/java/com/a2i/forwarder/ui/Components.kt index 04c116f..941267f 100644 --- a/app/src/main/java/com/a2i/forwarder/ui/Components.kt +++ b/app/src/main/java/com/a2i/forwarder/ui/Components.kt @@ -68,6 +68,7 @@ fun SectionCard( title: String? = null, modifier: Modifier = Modifier, subtitle: String? = null, + headerAction: (@Composable () -> Unit)? = null, content: @Composable ColumnScope.() -> Unit, ) { Card( @@ -79,19 +80,46 @@ fun SectionCard( ) { Column(Modifier.padding(16.dp)) { if (title != null) { - Text( - title, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - modifier = Modifier.padding(bottom = if (subtitle == null) 10.dp else 2.dp), - ) - if (subtitle != null) { + if (headerAction == null) { Text( - subtitle, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(bottom = 10.dp), + title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(bottom = if (subtitle == null) 10.dp else 2.dp), ) + if (subtitle != null) { + Text( + subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(bottom = 10.dp), + ) + } + } else { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 10.dp), + verticalAlignment = Alignment.Top, + ) { + Column(Modifier.weight(1f)) { + Text( + title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(bottom = if (subtitle == null) 0.dp else 2.dp), + ) + if (subtitle != null) { + Text( + subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + Spacer(Modifier.width(12.dp)) + headerAction() + } } } content() @@ -244,4 +272,4 @@ fun drawableToImageBitmap(d: Drawable): ImageBitmap { d.setBounds(0, 0, canvas.width, canvas.height) d.draw(canvas) return bmp.asImageBitmap() -} \ No newline at end of file +} 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 4bfd825..aa7b7aa 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 @@ -6,12 +6,15 @@ import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -23,6 +26,7 @@ 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.Info import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.RestartAlt @@ -39,6 +43,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Surface +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -51,6 +56,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -74,6 +80,11 @@ import com.a2i.forwarder.ui.SwitchRow import com.a2i.forwarder.ui.appVersionName import kotlinx.coroutines.launch +private const val BARK_APP_STORE_URL = "https://apps.apple.com/app/bark-custom-notifications/id1403753865" +private const val BARK_DOCS_URL = "https://bark.day.app/#/" +private const val BARK_GITHUB_URL = "https://github.com/Finb/Bark" +private const val BARK_OFFICIAL_SERVER = "https://api.day.app" + @Composable fun SettingsScreen(onOpenLog: () -> Unit) { val context = LocalContext.current @@ -118,6 +129,7 @@ fun SettingsScreen(onOpenLog: () -> Unit) { // 弹窗状态 var showAddBark by remember { mutableStateOf(false) } + var showBarkHelp by remember { mutableStateOf(false) } var editingBark by remember { mutableStateOf(null) } var showAddNtfy by remember { mutableStateOf(false) } var editingNtfy by remember { mutableStateOf(null) } @@ -134,13 +146,21 @@ fun SettingsScreen(onOpenLog: () -> Unit) { // ===== Bark 服务器 ===== item { - SectionCard("Bark 服务器", subtitle = "勾选的服务器参与转发;开关切换容错/广播") { - SwitchRow( - title = "发通一条就停", - subtitle = if (barkStopFirst) "容错模式:首个成功即止" else "广播模式:每个都发", - checked = barkStopFirst, - icon = Icons.Filled.Settings, - ) { v -> app.appScope.launch { app.settings.setBarkStopOnFirst(v) } } + SectionCard( + "Bark 服务器", + subtitle = "勾选的服务器参与转发;开关切换容错/广播", + headerAction = { + Row(verticalAlignment = Alignment.CenterVertically) { + IconButton(onClick = { showBarkHelp = true }) { + Icon(Icons.Filled.Info, "Bark 配置帮助") + } + CompactStopOnFirstSwitch( + checked = barkStopFirst, + onChange = { v -> app.appScope.launch { app.settings.setBarkStopOnFirst(v) } }, + ) + } + }, + ) { if (servers.isNotEmpty()) { Spacer(Modifier.size(8.dp)) Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { @@ -307,6 +327,7 @@ fun SettingsScreen(onOpenLog: () -> Unit) { } // 弹窗 + if (showBarkHelp) BarkHelpDialog(onDismiss = { showBarkHelp = false }) if (showAddBark) ServerDialog(onDismiss = { showAddBark = false }, onConfirm = { n, s, k -> app.appScope.launch { app.settings.addServer(n, s, k) }; showAddBark = false }) editingBark?.let { srv -> ServerDialog(initial = srv, onDismiss = { editingBark = null }, onConfirm = { n, s, k -> app.appScope.launch { app.settings.updateServer(srv.copy(name = n, server = s, deviceKey = k)) }; editingBark = null }) } if (showAddNtfy) NtfyConfigDialog(onDismiss = { showAddNtfy = false }, onConfirm = { n, sv, t, tk -> app.appScope.launch { app.settings.addNtfy(NtfyServer(name = n, server = sv, topic = t, token = tk)) }; showAddNtfy = false }) @@ -326,6 +347,112 @@ private fun AddButton(onClick: () -> Unit) { } } +@Composable +private fun CompactStopOnFirstSwitch( + checked: Boolean, + onChange: (Boolean) -> Unit, +) { + Row( + modifier = Modifier.padding(start = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + "发通一条就停", + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + ) + Spacer(Modifier.width(6.dp)) + Switch(checked = checked, onCheckedChange = onChange) + } +} + +@Composable +private fun BarkHelpDialog(onDismiss: () -> Unit) { + val uriHandler = LocalUriHandler.current + + AlertDialog( + onDismissRequest = onDismiss, + icon = { Icon(Icons.Filled.Info, null) }, + title = { Text("Bark 配置帮助") }, + text = { + Column( + modifier = Modifier + .heightIn(max = 440.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Text( + "Bark 用来把 a2i 处理后的 Android 通知推送到 iPhone / iPad。第一次使用按下面顺序配置即可。", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + BarkHelpStep("1", "在 iPhone / iPad 上安装 Bark,首次打开时允许通知权限。") + BarkHelpStep("2", "打开 Bark,复制首页显示的推送地址,通常类似 $BARK_OFFICIAL_SERVER/你的Key。") + BarkHelpStep("3", "回到 a2i,点击本区域底部的「添加」。服务器地址填 $BARK_OFFICIAL_SERVER,自建服务则填你的自建地址。") + BarkHelpStep("4", "Device Key 填推送地址最后一段 Key,不要把完整 URL 都填进去。") + BarkHelpStep("5", "保存后点服务器行里的发送按钮测试;iPhone 收到「a2i 测试」就说明 Bark 通道已通。") + BarkHelpStep("6", "最后回首页开启「通知转发」,并按提示授予系统通知监听权限。") + + Text( + "常用链接", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(top = 4.dp), + ) + BarkHelpLink("Bark App Store", BARK_APP_STORE_URL) { uriHandler.openUri(it) } + BarkHelpLink("Bark 官方文档", BARK_DOCS_URL) { uriHandler.openUri(it) } + BarkHelpLink("Bark GitHub", BARK_GITHUB_URL) { uriHandler.openUri(it) } + + Text( + "提示:如果使用自建 Bark Server,a2i 里的服务器地址要换成自建地址,但 Device Key 仍然来自 Bark App。", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 2.dp), + ) + } + }, + confirmButton = { TextButton(onClick = onDismiss) { Text("知道了") } }, + ) +} + +@Composable +private fun BarkHelpStep( + index: String, + text: String, +) { + Row(verticalAlignment = Alignment.Top) { + Text( + "$index.", + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.width(24.dp), + ) + Text( + text, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.weight(1f), + ) + } +} + +@Composable +private fun BarkHelpLink( + title: String, + url: String, + onOpen: (String) -> Unit, +) { + OutlinedButton( + onClick = { onOpen(url) }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(title, maxLines = 1, overflow = TextOverflow.Ellipsis) + } +} + /** 通用配置行(ntfy/电邮/短信 共用,仿 ServerRow 简化版)。 */ @Composable private fun ConfigRow(