feat: unify email/SMS settings with Bark style (no total toggle)

- Remove emailForwardingEnabled / smsFallbackEnabled total toggles
- Email/SMS SectionCards now use headerAction + CompactStopOnFirstSwitch
  (matching Bark and ntfy)
- Sender logic: email/sms participation controlled solely by per-item
  enabled checkboxes (empty list = channel inactive)
- version 1.8.4 -> 1.8.5 (code 21 -> 22)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-10 09:07:30 +08:00
parent a48ef78844
commit af27febf1f
4 changed files with 63 additions and 55 deletions
+2 -2
View File
@@ -13,8 +13,8 @@ android {
applicationId = "com.a2i.forwarder" applicationId = "com.a2i.forwarder"
minSdk = 34 minSdk = 34
targetSdk = 36 targetSdk = 36
versionCode = 21 versionCode = 22
versionName = "1.8.4" versionName = "1.8.5"
} }
signingConfigs { signingConfigs {
@@ -120,7 +120,6 @@ object EmailSender {
/** 如果电邮转发已启用且收件人非空,异步发邮件(fire-and-forget,不阻塞主流程)。 */ /** 如果电邮转发已启用且收件人非空,异步发邮件(fire-and-forget,不阻塞主流程)。 */
fun trySend(app: A2iApp, title: String, body: String, scope: CoroutineScope) { fun trySend(app: A2iApp, title: String, body: String, scope: CoroutineScope) {
val s = app.settings val s = app.settings
if (!s.emailForwardingEnabled.value) return
val configs = s.emailServers.value.filter { it.enabled && it.to.isNotBlank() && it.host.isNotBlank() } val configs = s.emailServers.value.filter { it.enabled && it.to.isNotBlank() && it.host.isNotBlank() }
if (configs.isEmpty()) return if (configs.isEmpty()) return
scope.launch { scope.launch {
@@ -35,7 +35,6 @@ class SmsFallbackForwarder(private val context: Context) {
/** 尝试用短信转发(遍历所有启用的目标号,按 stopOnFirst 决定发通一条止还是全发)。 */ /** 尝试用短信转发(遍历所有启用的目标号,按 stopOnFirst 决定发通一条止还是全发)。 */
fun forward(d: Decision) { fun forward(d: Decision) {
scope.launch { scope.launch {
if (!settings.smsFallbackEnabled.value) return@launch
if (settings.smsSuspended.value) { if (settings.smsSuspended.value) {
log(d, "failed", "短信已挂起(余额不足)") log(d, "failed", "短信已挂起(余额不足)")
return@launch return@launch
@@ -226,68 +226,78 @@ fun SettingsScreen(onOpenLog: () -> Unit) {
// ===== 电邮 ===== // ===== 电邮 =====
item { item {
SectionCard("电邮转发", subtitle = "SMTPS 直发邮件") { SectionCard(
SwitchRow(title = "启用电邮转发", subtitle = if (emailEnabled) "已开启" else "已关闭", checked = emailEnabled) { v -> app.appScope.launch { app.settings.setEmailForwarding(v) } } "电邮转发",
if (emailEnabled) { subtitle = "勾选的邮箱参与转发;开关切换容错/广播",
SwitchRow(title = "发通一条就停", subtitle = if (emailStopFirst) "容错:首个成功止" else "广播:每个都发", checked = emailStopFirst) { v -> app.appScope.launch { app.settings.setEmailStopOnFirst(v) } } headerAction = {
if (emailServers.isNotEmpty()) { CompactStopOnFirstSwitch(
Spacer(Modifier.size(8.dp)) checked = emailStopFirst,
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { onChange = { v -> app.appScope.launch { app.settings.setEmailStopOnFirst(v) } },
emailServers.forEachIndexed { i, s -> )
ConfigRow( },
title = s.name.ifBlank { s.to }, ) {
subtitle = "${s.host}:${s.port} · ${s.user}${s.to}", if (emailServers.isNotEmpty()) {
enabled = s.enabled, isFirst = i == 0, isLast = i == emailServers.lastIndex, Spacer(Modifier.size(8.dp))
onToggle = { v -> app.appScope.launch { app.settings.setEmailEnabled(s.id, v) } }, Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
onEdit = { editingEmail = s }, onDelete = { app.appScope.launch { app.settings.deleteEmail(s.id) } }, emailServers.forEachIndexed { i, s ->
onMoveUp = { app.appScope.launch { app.settings.moveEmail(s.id, true) } }, ConfigRow(
onMoveDown = { app.appScope.launch { app.settings.moveEmail(s.id, false) } }, title = s.name.ifBlank { s.to },
onTest = { app.appScope.launch { EmailSender.send(s.host, s.port, s.user, s.password, s.from, s.to, "a2i测试", "电邮配置测试") } }, subtitle = "${s.host}:${s.port} · ${s.user}${s.to}",
) enabled = s.enabled, isFirst = i == 0, isLast = i == emailServers.lastIndex,
} onToggle = { v -> app.appScope.launch { app.settings.setEmailEnabled(s.id, v) } },
onEdit = { editingEmail = s }, onDelete = { app.appScope.launch { app.settings.deleteEmail(s.id) } },
onMoveUp = { app.appScope.launch { app.settings.moveEmail(s.id, true) } },
onMoveDown = { app.appScope.launch { app.settings.moveEmail(s.id, false) } },
onTest = { app.appScope.launch { EmailSender.send(s.host, s.port, s.user, s.password, s.from, s.to, "a2i测试", "电邮配置测试") } },
)
} }
} }
AddButton { showAddEmail = true }
} }
AddButton { showAddEmail = true }
} }
} }
// ===== 短信兜底 ===== // ===== 短信兜底 =====
item { item {
SectionCard("断网短信兜底", subtitle = "无网络时把验证码/来电用短信发出去") { SectionCard(
SwitchRow(title = "启用短信兜底", subtitle = if (smsFallback) "已开启" else "已关闭", checked = smsFallback) { v -> app.appScope.launch { app.settings.setSmsFallback(v) } } "断网短信兜底",
if (smsFallback) { subtitle = "勾选的目标号参与转发;开关切换容错/广播",
SwitchRow(title = "发通一条就停", subtitle = if (smsStopFirst) "容错:首个成功止" else "广播:每个都发", checked = smsStopFirst) { v -> app.appScope.launch { app.settings.setSmsStopOnFirst(v) } } headerAction = {
if (!hasSmsPerm) { CompactStopOnFirstSwitch(
Spacer(Modifier.size(4.dp)) checked = smsStopFirst,
OutlinedButton(onClick = { smsPermLauncher.launch(android.Manifest.permission.SEND_SMS) }, modifier = Modifier.fillMaxWidth()) { Text("授予短信权限") } onChange = { v -> app.appScope.launch { app.settings.setSmsStopOnFirst(v) } },
} )
if (smsTargets.isNotEmpty()) { },
Spacer(Modifier.size(8.dp)) ) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { if (!hasSmsPerm) {
smsTargets.forEachIndexed { i, s -> Spacer(Modifier.size(4.dp))
ConfigRow( OutlinedButton(onClick = { smsPermLauncher.launch(android.Manifest.permission.SEND_SMS) }, modifier = Modifier.fillMaxWidth()) { Text("授予短信权限") }
title = s.name.ifBlank { s.number }, }
subtitle = s.number, if (smsTargets.isNotEmpty()) {
enabled = s.enabled, isFirst = i == 0, isLast = i == smsTargets.lastIndex, Spacer(Modifier.size(8.dp))
onToggle = { v -> app.appScope.launch { app.settings.setSmsTargetEnabled(s.id, v) } }, Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
onEdit = { editingSms = s }, onDelete = { app.appScope.launch { app.settings.deleteSmsTarget(s.id) } }, smsTargets.forEachIndexed { i, s ->
onMoveUp = { app.appScope.launch { app.settings.moveSmsTarget(s.id, true) } }, ConfigRow(
onMoveDown = { app.appScope.launch { app.settings.moveSmsTarget(s.id, false) } }, title = s.name.ifBlank { s.number },
onTest = null, subtitle = s.number,
) enabled = s.enabled, isFirst = i == 0, isLast = i == smsTargets.lastIndex,
} onToggle = { v -> app.appScope.launch { app.settings.setSmsTargetEnabled(s.id, v) } },
onEdit = { editingSms = s }, onDelete = { app.appScope.launch { app.settings.deleteSmsTarget(s.id) } },
onMoveUp = { app.appScope.launch { app.settings.moveSmsTarget(s.id, true) } },
onMoveDown = { app.appScope.launch { app.settings.moveSmsTarget(s.id, false) } },
onTest = null,
)
} }
} }
AddButton { showAddSms = true }
Spacer(Modifier.size(8.dp))
val balanceText = when {
smsSuspended -> "已挂起(余额不足)"
smsBalance < 0 -> "余额未知"
else -> "剩余约 $smsBalance"
}
Text("运营商:${detectedCarrier.displayName} · 余额:$balanceText · 手动额度:$smsManualQuota", style = MaterialTheme.typography.bodySmall, color = if (smsSuspended) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant)
} }
AddButton { showAddSms = true }
Spacer(Modifier.size(8.dp))
val balanceText = when {
smsSuspended -> "已挂起(余额不足)"
smsBalance < 0 -> "余额未知"
else -> "剩余约 $smsBalance"
}
Text("运营商:${detectedCarrier.displayName} · 余额:$balanceText · 手动额度:$smsManualQuota", style = MaterialTheme.typography.bodySmall, color = if (smsSuspended) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant)
} }
} }