feat: add Bark setup help to settings

This commit is contained in:
2026-07-10 08:35:00 +08:00
parent 9a6c21b78d
commit 2034257560
2 changed files with 174 additions and 19 deletions
@@ -68,6 +68,7 @@ fun SectionCard(
title: String? = null, title: String? = null,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
subtitle: String? = null, subtitle: String? = null,
headerAction: (@Composable () -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit, content: @Composable ColumnScope.() -> Unit,
) { ) {
Card( Card(
@@ -79,6 +80,7 @@ fun SectionCard(
) { ) {
Column(Modifier.padding(16.dp)) { Column(Modifier.padding(16.dp)) {
if (title != null) { if (title != null) {
if (headerAction == null) {
Text( Text(
title, title,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
@@ -93,6 +95,32 @@ fun SectionCard(
modifier = Modifier.padding(bottom = 10.dp), 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() content()
} }
@@ -6,12 +6,15 @@ import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.BorderStroke 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width 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.Delete
import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.History 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.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material.icons.filled.RestartAlt 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.OutlinedButton
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -51,6 +56,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -74,6 +80,11 @@ import com.a2i.forwarder.ui.SwitchRow
import com.a2i.forwarder.ui.appVersionName import com.a2i.forwarder.ui.appVersionName
import kotlinx.coroutines.launch 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 @Composable
fun SettingsScreen(onOpenLog: () -> Unit) { fun SettingsScreen(onOpenLog: () -> Unit) {
val context = LocalContext.current val context = LocalContext.current
@@ -118,6 +129,7 @@ fun SettingsScreen(onOpenLog: () -> Unit) {
// 弹窗状态 // 弹窗状态
var showAddBark by remember { mutableStateOf(false) } var showAddBark by remember { mutableStateOf(false) }
var showBarkHelp by remember { mutableStateOf(false) }
var editingBark by remember { mutableStateOf<BarkServer?>(null) } var editingBark by remember { mutableStateOf<BarkServer?>(null) }
var showAddNtfy by remember { mutableStateOf(false) } var showAddNtfy by remember { mutableStateOf(false) }
var editingNtfy by remember { mutableStateOf<NtfyServer?>(null) } var editingNtfy by remember { mutableStateOf<NtfyServer?>(null) }
@@ -134,13 +146,21 @@ fun SettingsScreen(onOpenLog: () -> Unit) {
// ===== Bark 服务器 ===== // ===== Bark 服务器 =====
item { item {
SectionCard("Bark 服务器", subtitle = "勾选的服务器参与转发;开关切换容错/广播") { SectionCard(
SwitchRow( "Bark 服务器",
title = "发通一条就停", subtitle = "勾选的服务器参与转发;开关切换容错/广播",
subtitle = if (barkStopFirst) "容错模式:首个成功即止" else "广播模式:每个都发", headerAction = {
Row(verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = { showBarkHelp = true }) {
Icon(Icons.Filled.Info, "Bark 配置帮助")
}
CompactStopOnFirstSwitch(
checked = barkStopFirst, checked = barkStopFirst,
icon = Icons.Filled.Settings, onChange = { v -> app.appScope.launch { app.settings.setBarkStopOnFirst(v) } },
) { v -> app.appScope.launch { app.settings.setBarkStopOnFirst(v) } } )
}
},
) {
if (servers.isNotEmpty()) { if (servers.isNotEmpty()) {
Spacer(Modifier.size(8.dp)) Spacer(Modifier.size(8.dp))
Column(verticalArrangement = Arrangement.spacedBy(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 }) 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 }) } 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 }) 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 简化版)。 */ /** 通用配置行(ntfy/电邮/短信 共用,仿 ServerRow 简化版)。 */
@Composable @Composable
private fun ConfigRow( private fun ConfigRow(