feat: add Meow forwarding channel
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
# A2i ·安到果(按倒过[呲牙])
|
||||
|
||||
把安卓手机上的通知(短信、验证码、电话“有电话进来这个通知,不是电话呼叫转移”、微信、QQ、Telegram 等)实时转发到 iOS,通过 [Bark](https://github.com/Finb/Bark) 推送到 iPhone / iPad。在 iOS 上点击通知,还能一键跳转到对应的 App。
|
||||
把安卓手机上的通知(短信、验证码、电话“有电话进来这个通知,不是电话呼叫转移”、微信、QQ、Telegram 等)实时转发到 iOS / Android / 鸿蒙等接收端。iOS 可通过 [Bark](https://github.com/Finb/Bark) 推送到 iPhone / iPad;Android 可用 ntfy;鸿蒙可用 Meow。在 iOS 上点击通知,还能一键跳转到对应的 App。
|
||||
|
||||
适用于把安卓备机的消息同步到 iOS 设备,不漏验证码、不漏重要聊天。
|
||||
|
||||
## 主要功能
|
||||
|
||||
- **系统级通知监听**:接入 Android 通知监听服务,覆盖所有发出通知的 App。
|
||||
- **Bark 推送**:支持官方 Bark 服务和自建 Bark Server,可保存多个服务器并随时切换。
|
||||
- **多通道推送**:支持 Bark、ntfy、Meow、电邮和短信兜底;每个通道可保存多个目标,勾选参与转发,并支持“发通一条就停”或全部广播。
|
||||
- **验证码自动提取**:自动识别通知中的验证码,收到即复制,也可点击复制。
|
||||
- **通知优化**:
|
||||
- 广告关键词过滤(内置 + 自定义)
|
||||
@@ -40,6 +40,8 @@
|
||||
- 服务器地址:`https://api.day.app`(自建则填自建地址)
|
||||
- Device Key:Bark 地址中的那段 Key
|
||||
|
||||
如果接收端是 Android 或鸿蒙手机,也可以在「设置」里配置 ntfy 或 Meow;对应区块右上角有帮助按钮,按弹窗步骤填写服务器地址、Topic / Device Key 后点测试发送。
|
||||
|
||||
### 3. 授权通知监听
|
||||
|
||||
打开 a2i,首页会提示「需要授权通知监听」。点击「前往系统授权」,在系统的通知访问设置中开启 a2i。
|
||||
@@ -107,7 +109,7 @@ a2i 默认用 Bark 官方服务器(`https://api.day.app`),免费够用。
|
||||
|
||||
- Kotlin + Jetpack Compose(Material 3)
|
||||
- AndroidX DataStore(持久化设置与日志)
|
||||
- OkHttp(Bark 推送)
|
||||
- OkHttp(Bark / ntfy / Meow / 更新检查)
|
||||
- WorkManager(失败重试)
|
||||
- Navigation Compose(单 Activity 多页面)
|
||||
- 最低 Android 14(API 34),目标 Android 16(API 36)
|
||||
|
||||
@@ -13,8 +13,8 @@ android {
|
||||
applicationId = "com.a2i.forwarder"
|
||||
minSdk = 34
|
||||
targetSdk = 36
|
||||
versionCode = 25
|
||||
versionName = "1.8.8"
|
||||
versionCode = 26
|
||||
versionName = "1.8.9"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package com.a2i.forwarder.core
|
||||
|
||||
import com.a2i.forwarder.A2iApp
|
||||
import com.a2i.forwarder.model.MeowServer
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.json.JSONObject
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Meow for HarmonyOS push channel.
|
||||
*
|
||||
* Meow's public API shape is less standardized than Bark/ntfy in this app,
|
||||
* so this sender accepts a user-provided endpoint and tries common JSON/GET forms.
|
||||
*/
|
||||
object MeowSender {
|
||||
private val http = OkHttpClient.Builder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.writeTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(15, TimeUnit.SECONDS)
|
||||
.retryOnConnectionFailure(true)
|
||||
.build()
|
||||
|
||||
suspend fun send(
|
||||
server: String,
|
||||
deviceKey: String,
|
||||
title: String,
|
||||
body: String,
|
||||
): Result<Unit> = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val endpoint = server.trim().removeSuffix("/")
|
||||
val key = deviceKey.trim()
|
||||
require(endpoint.isNotBlank()) { "Meow 服务器地址未填" }
|
||||
require(key.isNotBlank()) { "Meow Device Key 未填" }
|
||||
|
||||
val attempts = buildRequests(endpoint, key, title.take(200), body.take(4000))
|
||||
var last: Throwable? = null
|
||||
for (req in attempts) {
|
||||
val r = runCatching { doPushWithRetry(req) }
|
||||
if (r.isSuccess) return@runCatching
|
||||
last = r.exceptionOrNull()
|
||||
}
|
||||
throw last ?: error("Meow 推送失败")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun pushToEnabled(configs: List<MeowServer>, title: String, body: String, stopOnFirst: Boolean): Result<Unit> = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
require(configs.any()) { "无 Meow 配置" }
|
||||
var anyOk = false
|
||||
var lastErr: Throwable? = null
|
||||
for (c in configs) {
|
||||
val r = send(c.server, c.deviceKey, title, body)
|
||||
if (r.isSuccess) {
|
||||
anyOk = true
|
||||
if (stopOnFirst) return@runCatching
|
||||
} else {
|
||||
lastErr = r.exceptionOrNull()
|
||||
}
|
||||
}
|
||||
if (!anyOk) throw lastErr ?: error("全部 Meow 推送失败")
|
||||
}
|
||||
}
|
||||
|
||||
fun trySend(app: A2iApp, title: String, body: String, scope: CoroutineScope) {
|
||||
val s = app.settings
|
||||
val configs = s.meowServers.value.filter { it.enabled && it.server.isNotBlank() && it.deviceKey.isNotBlank() }
|
||||
if (configs.isEmpty()) return
|
||||
scope.launch {
|
||||
pushToEnabled(configs, title, body, s.meowStopOnFirst.value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildRequests(endpoint: String, key: String, title: String, body: String): List<Request> {
|
||||
val payload = JSONObject()
|
||||
.put("deviceKey", key)
|
||||
.put("device_key", key)
|
||||
.put("key", key)
|
||||
.put("title", title)
|
||||
.put("content", body)
|
||||
.put("message", body)
|
||||
.put("body", body)
|
||||
.toString()
|
||||
|
||||
val postTargets = listOf(endpoint, "$endpoint/push").distinct()
|
||||
val postRequests = postTargets.map { url ->
|
||||
Request.Builder()
|
||||
.url(url)
|
||||
.header("Authorization", "Bearer $key")
|
||||
.header("X-Device-Key", key)
|
||||
.post(payload.toRequestBody(JSON))
|
||||
.build()
|
||||
}
|
||||
|
||||
val getRequest = endpoint.toHttpUrlOrNull()?.newBuilder()
|
||||
?.addQueryParameter("deviceKey", key)
|
||||
?.addQueryParameter("key", key)
|
||||
?.addQueryParameter("title", title)
|
||||
?.addQueryParameter("content", body)
|
||||
?.addQueryParameter("message", body)
|
||||
?.build()
|
||||
?.let { url ->
|
||||
Request.Builder()
|
||||
.url(url)
|
||||
.header("Authorization", "Bearer $key")
|
||||
.header("X-Device-Key", key)
|
||||
.get()
|
||||
.build()
|
||||
}
|
||||
|
||||
return postRequests + listOfNotNull(getRequest)
|
||||
}
|
||||
|
||||
private fun doPushWithRetry(req: Request) {
|
||||
var lastError: IOException? = null
|
||||
repeat(MAX_ATTEMPTS) { attempt ->
|
||||
try {
|
||||
http.newCall(req).execute().use { res ->
|
||||
if (res.isSuccessful) return
|
||||
error("HTTP ${res.code}")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
lastError = e
|
||||
if (attempt < MAX_ATTEMPTS - 1) {
|
||||
Thread.sleep(BACKOFF_MS[attempt])
|
||||
}
|
||||
} catch (e: IllegalStateException) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
throw lastError ?: error("Meow 推送失败")
|
||||
}
|
||||
|
||||
private val JSON = "application/json; charset=utf-8".toMediaType()
|
||||
private const val MAX_ATTEMPTS = 3
|
||||
private val BACKOFF_MS = longArrayOf(600L, 1200L)
|
||||
}
|
||||
@@ -127,6 +127,7 @@ class PhoneCallMonitor(private val context: Context) {
|
||||
if (servers.isEmpty()) {
|
||||
EmailSender.trySend(app, msg.title ?: logBody, msg.body ?: "", scope)
|
||||
NtfySender.trySend(app, msg.title ?: logBody, msg.body ?: "", scope)
|
||||
MeowSender.trySend(app, msg.title ?: logBody, msg.body ?: "", scope)
|
||||
return@launch
|
||||
}
|
||||
val result = BarkSender.pushToEnabled(servers, msg, app.settings.barkStopOnFirst.value)
|
||||
@@ -146,6 +147,7 @@ class PhoneCallMonitor(private val context: Context) {
|
||||
}
|
||||
EmailSender.trySend(app, msg.title ?: logBody, msg.body ?: "", scope)
|
||||
NtfySender.trySend(app, msg.title ?: logBody, msg.body ?: "", scope)
|
||||
MeowSender.trySend(app, msg.title ?: logBody, msg.body ?: "", scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +142,9 @@ class UpdateChecker(private val context: Context) {
|
||||
// ntfy
|
||||
NtfySender.trySend(app, title, body, scope)
|
||||
|
||||
// Meow
|
||||
MeowSender.trySend(app, title, body, scope)
|
||||
|
||||
// 电邮
|
||||
EmailSender.trySend(app, title, body, scope)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,15 @@ data class NtfyServer(
|
||||
val enabled: Boolean = true,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MeowServer(
|
||||
val id: String = UUID.randomUUID().toString(),
|
||||
val name: String,
|
||||
val server: String = "",
|
||||
val deviceKey: String = "",
|
||||
val enabled: Boolean = true,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class EmailServer(
|
||||
val id: String = UUID.randomUUID().toString(),
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.a2i.forwarder.A2iApp
|
||||
import com.a2i.forwarder.core.BarkSender
|
||||
import com.a2i.forwarder.core.EmailSender
|
||||
import com.a2i.forwarder.core.ForwardLog
|
||||
import com.a2i.forwarder.core.MeowSender
|
||||
import com.a2i.forwarder.core.NotificationProcessor
|
||||
import com.a2i.forwarder.core.NtfySender
|
||||
import com.a2i.forwarder.core.PendingPush
|
||||
@@ -65,6 +66,7 @@ class NotifyListenerService : NotificationListenerService() {
|
||||
app.logStore.addLog(ForwardLog(now, sbn.packageName, d.appLabel, msg.title, msg.body, "failed", "未配置可用 Bark 服务器"))
|
||||
EmailSender.trySend(app, msg.title ?: d.appLabel, msg.body, scope)
|
||||
NtfySender.trySend(app, msg.title ?: d.appLabel, msg.body, scope)
|
||||
MeowSender.trySend(app, msg.title ?: d.appLabel, msg.body, scope)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -80,5 +82,6 @@ class NotifyListenerService : NotificationListenerService() {
|
||||
}
|
||||
EmailSender.trySend(app, msg.title ?: d.appLabel, msg.body, scope)
|
||||
NtfySender.trySend(app, msg.title ?: d.appLabel, msg.body, scope)
|
||||
MeowSender.trySend(app, msg.title ?: d.appLabel, msg.body, scope)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.a2i.forwarder.model.BarkServer
|
||||
import com.a2i.forwarder.model.EmailServer
|
||||
import com.a2i.forwarder.model.MeowServer
|
||||
import com.a2i.forwarder.model.NtfyServer
|
||||
import com.a2i.forwarder.model.OFFICIAL_BARK_SERVER
|
||||
import com.a2i.forwarder.model.SmsTarget
|
||||
@@ -44,6 +45,7 @@ class SettingsStore(
|
||||
// 各通道「发通一条止 / 全发」开关
|
||||
val BARK_STOP_FIRST = booleanPreferencesKey("bark_stop_first")
|
||||
val NTFY_STOP_FIRST = booleanPreferencesKey("ntfy_stop_first")
|
||||
val MEOW_STOP_FIRST = booleanPreferencesKey("meow_stop_first")
|
||||
val EMAIL_STOP_FIRST = booleanPreferencesKey("email_stop_first")
|
||||
val SMS_STOP_FIRST = booleanPreferencesKey("sms_stop_first")
|
||||
// 短信兜底(目标号已列表化;运营商/余额/限频保持全局)
|
||||
@@ -58,6 +60,7 @@ class SettingsStore(
|
||||
val EMAIL_ENABLED = booleanPreferencesKey("email_enabled")
|
||||
val EMAIL_SERVERS = stringPreferencesKey("email_servers")
|
||||
val NTFY_SERVERS = stringPreferencesKey("ntfy_servers")
|
||||
val MEOW_SERVERS = stringPreferencesKey("meow_servers")
|
||||
// 应用更新
|
||||
val UPDATE_CHECK_AUTO = booleanPreferencesKey("update_check_auto")
|
||||
val LAST_UPDATE_CHECK = longPreferencesKey("last_update_check")
|
||||
@@ -87,6 +90,9 @@ class SettingsStore(
|
||||
private val _ntfyServers = MutableStateFlow<List<NtfyServer>>(emptyList())
|
||||
val ntfyServers = _ntfyServers.asStateFlow()
|
||||
|
||||
private val _meowServers = MutableStateFlow<List<MeowServer>>(emptyList())
|
||||
val meowServers = _meowServers.asStateFlow()
|
||||
|
||||
private val _emailServers = MutableStateFlow<List<EmailServer>>(emptyList())
|
||||
val emailServers = _emailServers.asStateFlow()
|
||||
|
||||
@@ -104,6 +110,7 @@ class SettingsStore(
|
||||
// 各通道「发通一条止 / 全发」
|
||||
val barkStopOnFirst = MutableStateFlow(true)
|
||||
val ntfyStopOnFirst = MutableStateFlow(false)
|
||||
val meowStopOnFirst = MutableStateFlow(false)
|
||||
val emailStopOnFirst = MutableStateFlow(false)
|
||||
val smsStopOnFirst = MutableStateFlow(false)
|
||||
|
||||
@@ -142,6 +149,7 @@ class SettingsStore(
|
||||
// 各通道 stopOnFirst
|
||||
barkStopOnFirst.value = p[K.BARK_STOP_FIRST] ?: true
|
||||
ntfyStopOnFirst.value = p[K.NTFY_STOP_FIRST] ?: false
|
||||
meowStopOnFirst.value = p[K.MEOW_STOP_FIRST] ?: false
|
||||
emailStopOnFirst.value = p[K.EMAIL_STOP_FIRST] ?: false
|
||||
smsStopOnFirst.value = p[K.SMS_STOP_FIRST] ?: false
|
||||
// 短信兜底全局
|
||||
@@ -161,6 +169,9 @@ class SettingsStore(
|
||||
_ntfyServers.value = p[K.NTFY_SERVERS]
|
||||
?.let { runCatching { json.decodeFromString<List<NtfyServer>>(it) }.getOrNull() }
|
||||
?: emptyList()
|
||||
_meowServers.value = p[K.MEOW_SERVERS]
|
||||
?.let { runCatching { json.decodeFromString<List<MeowServer>>(it) }.getOrNull() }
|
||||
?: emptyList()
|
||||
_emailServers.value = p[K.EMAIL_SERVERS]
|
||||
?.let { runCatching { json.decodeFromString<List<EmailServer>>(it) }.getOrNull() }
|
||||
?: emptyList()
|
||||
@@ -261,6 +272,22 @@ class SettingsStore(
|
||||
}
|
||||
suspend fun setNtfyEnabled(id: String, enabled: Boolean) = saveNtfyServers(_ntfyServers.value.map { if (it.id == id) it.copy(enabled = enabled) else it })
|
||||
|
||||
// ---- Meow 服务器 CRUD ----
|
||||
private suspend fun saveMeowServers(list: List<MeowServer>) {
|
||||
edit { it[K.MEOW_SERVERS] = json.encodeToString(list) }
|
||||
_meowServers.value = list
|
||||
}
|
||||
suspend fun addMeow(s: MeowServer) { saveMeowServers(_meowServers.value + s) }
|
||||
suspend fun updateMeow(s: MeowServer) = saveMeowServers(_meowServers.value.map { if (it.id == s.id) s else it })
|
||||
suspend fun deleteMeow(id: String) = saveMeowServers(_meowServers.value.filterNot { it.id == id })
|
||||
suspend fun moveMeow(id: String, up: Boolean) {
|
||||
val list = _meowServers.value.toMutableList()
|
||||
val i = list.indexOfFirst { it.id == id }
|
||||
if (i in 1..list.lastIndex && up) { java.util.Collections.swap(list, i, i - 1); saveMeowServers(list) }
|
||||
else if (i in 0 until list.lastIndex && !up) { java.util.Collections.swap(list, i, i + 1); saveMeowServers(list) }
|
||||
}
|
||||
suspend fun setMeowEnabled(id: String, enabled: Boolean) = saveMeowServers(_meowServers.value.map { if (it.id == id) it.copy(enabled = enabled) else it })
|
||||
|
||||
// ---- 电邮服务器 CRUD ----
|
||||
private suspend fun saveEmailServers(list: List<EmailServer>) {
|
||||
edit { it[K.EMAIL_SERVERS] = json.encodeToString(list) }
|
||||
@@ -306,6 +333,7 @@ class SettingsStore(
|
||||
|
||||
suspend fun setBarkStopOnFirst(v: Boolean) { edit { it[K.BARK_STOP_FIRST] = v }; barkStopOnFirst.value = v }
|
||||
suspend fun setNtfyStopOnFirst(v: Boolean) { edit { it[K.NTFY_STOP_FIRST] = v }; ntfyStopOnFirst.value = v }
|
||||
suspend fun setMeowStopOnFirst(v: Boolean) { edit { it[K.MEOW_STOP_FIRST] = v }; meowStopOnFirst.value = v }
|
||||
suspend fun setEmailStopOnFirst(v: Boolean) { edit { it[K.EMAIL_STOP_FIRST] = v }; emailStopOnFirst.value = v }
|
||||
suspend fun setSmsStopOnFirst(v: Boolean) { edit { it[K.SMS_STOP_FIRST] = v }; smsStopOnFirst.value = v }
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -43,7 +44,6 @@ 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
|
||||
@@ -66,10 +66,12 @@ import com.a2i.forwarder.A2iApp
|
||||
import com.a2i.forwarder.core.BarkClient
|
||||
import com.a2i.forwarder.core.EmailSender
|
||||
import com.a2i.forwarder.core.IconExporter
|
||||
import com.a2i.forwarder.core.MeowSender
|
||||
import com.a2i.forwarder.core.NtfySender
|
||||
import com.a2i.forwarder.model.BarkMessage
|
||||
import com.a2i.forwarder.model.BarkServer
|
||||
import com.a2i.forwarder.model.EmailServer
|
||||
import com.a2i.forwarder.model.MeowServer
|
||||
import com.a2i.forwarder.model.NtfyServer
|
||||
import com.a2i.forwarder.model.OFFICIAL_BARK_SERVER
|
||||
import com.a2i.forwarder.model.SmsTarget
|
||||
@@ -99,10 +101,12 @@ fun SettingsScreen(onOpenLog: () -> Unit) {
|
||||
val app = A2iApp.instance
|
||||
val servers by app.settings.servers.collectAsState()
|
||||
val ntfyServers by app.settings.ntfyServers.collectAsState()
|
||||
val meowServers by app.settings.meowServers.collectAsState()
|
||||
val emailServers by app.settings.emailServers.collectAsState()
|
||||
val smsTargets by app.settings.smsTargets.collectAsState()
|
||||
val barkStopFirst by app.settings.barkStopOnFirst.collectAsState()
|
||||
val ntfyStopFirst by app.settings.ntfyStopOnFirst.collectAsState()
|
||||
val meowStopFirst by app.settings.meowStopOnFirst.collectAsState()
|
||||
val emailStopFirst by app.settings.emailStopOnFirst.collectAsState()
|
||||
val smsStopFirst by app.settings.smsStopOnFirst.collectAsState()
|
||||
val emailEnabled by app.settings.emailForwardingEnabled.collectAsState()
|
||||
@@ -141,6 +145,9 @@ fun SettingsScreen(onOpenLog: () -> Unit) {
|
||||
var showAddNtfy by remember { mutableStateOf(false) }
|
||||
var showNtfyHelp by remember { mutableStateOf(false) }
|
||||
var editingNtfy by remember { mutableStateOf<NtfyServer?>(null) }
|
||||
var showAddMeow by remember { mutableStateOf(false) }
|
||||
var showMeowHelp by remember { mutableStateOf(false) }
|
||||
var editingMeow by remember { mutableStateOf<MeowServer?>(null) }
|
||||
var showAddEmail by remember { mutableStateOf(false) }
|
||||
var editingEmail by remember { mutableStateOf<EmailServer?>(null) }
|
||||
var showAddSms by remember { mutableStateOf(false) }
|
||||
@@ -154,21 +161,18 @@ fun SettingsScreen(onOpenLog: () -> Unit) {
|
||||
|
||||
// ===== Bark 服务器 =====
|
||||
item {
|
||||
SectionCard(
|
||||
"Bark 服务器",
|
||||
subtitle = "勾选的服务器参与转发;开关切换容错/广播",
|
||||
headerAction = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton(onClick = { showBarkHelp = true }) {
|
||||
Icon(Icons.Filled.Info, "Bark 配置帮助", tint = Color(0xFFFF9800))
|
||||
}
|
||||
CompactStopOnFirstSwitch(
|
||||
checked = barkStopFirst,
|
||||
onChange = { v -> app.appScope.launch { app.settings.setBarkStopOnFirst(v) } },
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
SectionCard {
|
||||
ChannelSectionHeader(
|
||||
title = "Bark 服务器",
|
||||
summary = "iPhone / iPad 推送目标",
|
||||
mark = "B",
|
||||
markColor = Color(0xFFFF9800),
|
||||
platforms = listOf("iOS"),
|
||||
stopFirst = barkStopFirst,
|
||||
onStopFirstChange = { v -> app.appScope.launch { app.settings.setBarkStopOnFirst(v) } },
|
||||
onHelpClick = { showBarkHelp = true },
|
||||
helpContentDescription = "Bark 配置帮助",
|
||||
)
|
||||
if (servers.isNotEmpty()) {
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
@@ -189,21 +193,18 @@ fun SettingsScreen(onOpenLog: () -> Unit) {
|
||||
|
||||
// ===== ntfy =====
|
||||
item {
|
||||
SectionCard(
|
||||
"ntfy 转发",
|
||||
subtitle = "勾选的 topic 参与转发;开关切换容错/广播",
|
||||
headerAction = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton(onClick = { showNtfyHelp = true }) {
|
||||
Icon(Icons.Filled.Info, "ntfy 配置帮助", tint = Color(0xFFFF9800))
|
||||
}
|
||||
CompactStopOnFirstSwitch(
|
||||
checked = ntfyStopFirst,
|
||||
onChange = { v -> app.appScope.launch { app.settings.setNtfyStopOnFirst(v) } },
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
SectionCard {
|
||||
ChannelSectionHeader(
|
||||
title = "ntfy 转发",
|
||||
summary = "Android / iOS topic 推送",
|
||||
mark = "n",
|
||||
markColor = Color(0xFF43A047),
|
||||
platforms = listOf("Android", "iOS"),
|
||||
stopFirst = ntfyStopFirst,
|
||||
onStopFirstChange = { v -> app.appScope.launch { app.settings.setNtfyStopOnFirst(v) } },
|
||||
onHelpClick = { showNtfyHelp = true },
|
||||
helpContentDescription = "ntfy 配置帮助",
|
||||
)
|
||||
if (ntfyServers.isNotEmpty()) {
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
@@ -225,18 +226,53 @@ fun SettingsScreen(onOpenLog: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Meow =====
|
||||
item {
|
||||
SectionCard {
|
||||
ChannelSectionHeader(
|
||||
title = "Meow 转发",
|
||||
summary = "鸿蒙手机推送目标",
|
||||
mark = "M",
|
||||
markColor = Color(0xFF7E57C2),
|
||||
platforms = listOf("HarmonyOS"),
|
||||
stopFirst = meowStopFirst,
|
||||
onStopFirstChange = { v -> app.appScope.launch { app.settings.setMeowStopOnFirst(v) } },
|
||||
onHelpClick = { showMeowHelp = true },
|
||||
helpContentDescription = "Meow 配置帮助",
|
||||
)
|
||||
if (meowServers.isNotEmpty()) {
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
meowServers.forEachIndexed { i, s ->
|
||||
MeowServerRow(
|
||||
server = s,
|
||||
isFirst = i == 0,
|
||||
isLast = i == meowServers.lastIndex,
|
||||
onToggle = { v -> app.appScope.launch { app.settings.setMeowEnabled(s.id, v) } },
|
||||
onEdit = { editingMeow = s },
|
||||
onDelete = { app.appScope.launch { app.settings.deleteMeow(s.id) } },
|
||||
onMoveUp = { app.appScope.launch { app.settings.moveMeow(s.id, true) } },
|
||||
onMoveDown = { app.appScope.launch { app.settings.moveMeow(s.id, false) } },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
AddButton { showAddMeow = true }
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 电邮 =====
|
||||
item {
|
||||
SectionCard(
|
||||
"电邮转发",
|
||||
subtitle = "勾选的邮箱参与转发;开关切换容错/广播",
|
||||
headerAction = {
|
||||
CompactStopOnFirstSwitch(
|
||||
checked = emailStopFirst,
|
||||
onChange = { v -> app.appScope.launch { app.settings.setEmailStopOnFirst(v) } },
|
||||
)
|
||||
},
|
||||
) {
|
||||
SectionCard {
|
||||
ChannelSectionHeader(
|
||||
title = "电邮转发",
|
||||
summary = "SMTP 邮箱推送目标",
|
||||
mark = "@",
|
||||
markColor = Color(0xFF1976D2),
|
||||
platforms = listOf("Email"),
|
||||
stopFirst = emailStopFirst,
|
||||
onStopFirstChange = { v -> app.appScope.launch { app.settings.setEmailStopOnFirst(v) } },
|
||||
)
|
||||
if (emailServers.isNotEmpty()) {
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
@@ -260,16 +296,16 @@ fun SettingsScreen(onOpenLog: () -> Unit) {
|
||||
|
||||
// ===== 短信兜底 =====
|
||||
item {
|
||||
SectionCard(
|
||||
"断网短信兜底",
|
||||
subtitle = "勾选的目标号参与转发;开关切换容错/广播",
|
||||
headerAction = {
|
||||
CompactStopOnFirstSwitch(
|
||||
checked = smsStopFirst,
|
||||
onChange = { v -> app.appScope.launch { app.settings.setSmsStopOnFirst(v) } },
|
||||
)
|
||||
},
|
||||
) {
|
||||
SectionCard {
|
||||
ChannelSectionHeader(
|
||||
title = "断网短信兜底",
|
||||
summary = "无网络时发送关键通知",
|
||||
mark = "S",
|
||||
markColor = Color(0xFF00897B),
|
||||
platforms = listOf("SMS"),
|
||||
stopFirst = smsStopFirst,
|
||||
onStopFirstChange = { v -> app.appScope.launch { app.settings.setSmsStopOnFirst(v) } },
|
||||
)
|
||||
if (!hasSmsPerm) {
|
||||
Spacer(Modifier.size(4.dp))
|
||||
OutlinedButton(onClick = { smsPermLauncher.launch(android.Manifest.permission.SEND_SMS) }, modifier = Modifier.fillMaxWidth()) { Text("授予短信权限") }
|
||||
@@ -357,10 +393,13 @@ fun SettingsScreen(onOpenLog: () -> Unit) {
|
||||
// 弹窗
|
||||
if (showBarkHelp) BarkHelpDialog(onDismiss = { showBarkHelp = false })
|
||||
if (showNtfyHelp) NtfyHelpDialog(onDismiss = { showNtfyHelp = false })
|
||||
if (showMeowHelp) MeowHelpDialog(onDismiss = { showMeowHelp = 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 })
|
||||
editingNtfy?.let { srv -> NtfyConfigDialog(initial = srv, onDismiss = { editingNtfy = null }, onConfirm = { n, sv, t, tk -> app.appScope.launch { app.settings.updateNtfy(srv.copy(name = n, server = sv, topic = t, token = tk)) }; editingNtfy = null }) }
|
||||
if (showAddMeow) MeowConfigDialog(onDismiss = { showAddMeow = false }, onConfirm = { n, sv, k -> app.appScope.launch { app.settings.addMeow(MeowServer(name = n, server = sv, deviceKey = k)) }; showAddMeow = false })
|
||||
editingMeow?.let { srv -> MeowConfigDialog(initial = srv, onDismiss = { editingMeow = null }, onConfirm = { n, sv, k -> app.appScope.launch { app.settings.updateMeow(srv.copy(name = n, server = sv, deviceKey = k)) }; editingMeow = null }) }
|
||||
if (showAddEmail) EmailConfigDialog(onDismiss = { showAddEmail = false }, onConfirm = { c -> app.appScope.launch { app.settings.addEmail(c) }; showAddEmail = false })
|
||||
editingEmail?.let { srv -> EmailConfigDialog(initial = srv, onDismiss = { editingEmail = null }, onConfirm = { c -> app.appScope.launch { app.settings.updateEmail(c.copy(id = srv.id)) }; editingEmail = null }) }
|
||||
if (showAddSms) SmsConfigDialog(onDismiss = { showAddSms = false }, onConfirm = { n, num -> app.appScope.launch { app.settings.addSmsTarget(SmsTarget(name = n, number = num)) }; showAddSms = false })
|
||||
@@ -377,23 +416,174 @@ private fun AddButton(onClick: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CompactStopOnFirstSwitch(
|
||||
checked: Boolean,
|
||||
onChange: (Boolean) -> Unit,
|
||||
private fun ChannelSectionHeader(
|
||||
title: String,
|
||||
summary: String,
|
||||
mark: String,
|
||||
markColor: Color,
|
||||
platforms: List<String>,
|
||||
stopFirst: Boolean,
|
||||
onStopFirstChange: (Boolean) -> Unit,
|
||||
onHelpClick: (() -> Unit)? = null,
|
||||
helpContentDescription: String? = null,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
ChannelMark(mark = mark, tint = markColor)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Row(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
)
|
||||
if (platforms.isNotEmpty()) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
platforms.forEach { PlatformBadge(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (onHelpClick != null) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
IconButton(onClick = onHelpClick, modifier = Modifier.size(40.dp)) {
|
||||
Icon(
|
||||
Icons.Filled.Info,
|
||||
helpContentDescription,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
summary,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
DeliveryModeControl(
|
||||
stopFirst = stopFirst,
|
||||
onStopFirstChange = onStopFirstChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChannelMark(mark: String, tint: Color) {
|
||||
Surface(
|
||||
modifier = Modifier.size(36.dp),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
color = tint.copy(alpha = 0.14f),
|
||||
border = BorderStroke(1.dp, tint.copy(alpha = 0.28f)),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
mark,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = tint,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PlatformBadge(label: String) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(999.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.72f),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),
|
||||
) {
|
||||
Text(
|
||||
"发通一条就停",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
label,
|
||||
modifier = Modifier.padding(horizontal = 7.dp, vertical = 3.dp),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeliveryModeControl(
|
||||
stopFirst: Boolean,
|
||||
onStopFirstChange: (Boolean) -> Unit,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
"模式",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
)
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Switch(checked = checked, onCheckedChange = onChange)
|
||||
DeliveryModeOption(
|
||||
label = "容错",
|
||||
selected = stopFirst,
|
||||
shape = RoundedCornerShape(topStart = 8.dp, topEnd = 0.dp, bottomEnd = 0.dp, bottomStart = 8.dp),
|
||||
onClick = { onStopFirstChange(true) },
|
||||
)
|
||||
DeliveryModeOption(
|
||||
label = "广播",
|
||||
selected = !stopFirst,
|
||||
shape = RoundedCornerShape(topStart = 0.dp, topEnd = 8.dp, bottomEnd = 8.dp, bottomStart = 0.dp),
|
||||
onClick = { onStopFirstChange(false) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeliveryModeOption(
|
||||
label: String,
|
||||
selected: Boolean,
|
||||
shape: RoundedCornerShape,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.clickable(onClick = onClick),
|
||||
shape = shape,
|
||||
color = if (selected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surface,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
if (selected) MaterialTheme.colorScheme.primary.copy(alpha = 0.50f) else MaterialTheme.colorScheme.outlineVariant,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
label,
|
||||
modifier = Modifier.padding(horizontal = 9.dp, vertical = 5.dp),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Medium,
|
||||
color = if (selected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,6 +690,45 @@ private fun NtfyHelpDialog(onDismiss: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MeowHelpDialog(onDismiss: () -> Unit) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
icon = { Icon(Icons.Filled.Info, null, tint = Color(0xFFFF9800)) },
|
||||
title = { Text("Meow 配置帮助") },
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.heightIn(max = 440.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Text(
|
||||
"Meow 用来把 a2i 处理后的 Android 通知推送到鸿蒙手机。a2i 只负责按你填写的 Meow 接口地址和 Device Key 发出 HTTP 请求。",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
HelpStep("1", "在鸿蒙手机上安装并打开 Meow,先确认 Meow 自身可以正常接收推送。")
|
||||
HelpStep("2", "在 Meow 里找到推送接口、服务器地址或 API 地址,同时复制对应的 Device Key / Token。")
|
||||
HelpStep("3", "回到 a2i,点击本区域底部的「添加」。名称按你的鸿蒙设备填写,方便以后区分。")
|
||||
HelpStep("4", "服务器地址可以填 Meow 给出的完整 API 地址;如果 Meow 只给了服务根地址,a2i 也会尝试常见的 /push 路径。")
|
||||
HelpStep("5", "Device Key 填 Meow 给你的设备 Key 或 Token,不要填写鸿蒙手机锁屏密码、华为账号密码等敏感账号密码。")
|
||||
HelpStep("6", "保存后点设备行里的发送按钮测试;鸿蒙手机收到「a2i 测试」就说明 Meow 通道已通。")
|
||||
HelpStep("7", "后续只要这一行左侧保持勾选,它就会参与转发;取消勾选就是停用这个鸿蒙设备。")
|
||||
|
||||
Text(
|
||||
"提示:如果测试失败,优先检查 Meow 里的接口地址是否完整、Key 是否复制完整,以及鸿蒙手机是否允许 Meow 通知。",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 2.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = { TextButton(onClick = onDismiss) { Text("知道了") } },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HelpStep(
|
||||
index: String,
|
||||
@@ -568,6 +797,46 @@ private fun ConfigRow(
|
||||
}
|
||||
|
||||
// ===== ntfy / Bark Row + Dialog =====
|
||||
@Composable
|
||||
private fun MeowServerRow(
|
||||
server: MeowServer, 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 (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),
|
||||
) {
|
||||
Column(Modifier.padding(start = 4.dp, end = 4.dp, top = 2.dp, bottom = 6.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(checked = server.enabled, onCheckedChange = onToggle)
|
||||
Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(server.name.ifBlank { "鸿蒙手机" }, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f, fill = false))
|
||||
if (!server.enabled) { Spacer(Modifier.width(8.dp)); StatusBadge("停用", MaterialTheme.colorScheme.onSurfaceVariant) }
|
||||
}
|
||||
IconButton(onClick = { if (!testing) app.appScope.launch { testing = true; testMsg = testMeow(server); testing = false } }, enabled = !testing) {
|
||||
if (testing) CircularProgressIndicator(Modifier.size(18.dp), strokeWidth = 2.dp) else Icon(Icons.AutoMirrored.Filled.Send, "测试")
|
||||
}
|
||||
IconButton(onClick = onEdit) { Icon(Icons.Filled.Edit, "编辑") }
|
||||
IconButton(onClick = onDelete) { Icon(Icons.Filled.Delete, "删除") }
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 2.dp)) {
|
||||
Column(Modifier.weight(1f).padding(start = 36.dp)) {
|
||||
Text(server.server.ifBlank { "服务器地址未填" }, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
||||
Text("device key: " + if (server.deviceKey.isBlank()) "未填" else "已填(${server.deviceKey.length} 位)", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
IconButton(onClick = onMoveUp, enabled = !isFirst) { Icon(Icons.Filled.KeyboardArrowUp, "上移") }
|
||||
IconButton(onClick = onMoveDown, enabled = !isLast) { Icon(Icons.Filled.KeyboardArrowDown, "下移") }
|
||||
}
|
||||
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)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NtfyServerRow(
|
||||
server: NtfyServer, isFirst: Boolean, isLast: Boolean,
|
||||
@@ -725,6 +994,20 @@ private fun NtfyConfigDialog(initial: NtfyServer? = null, onDismiss: () -> Unit,
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MeowConfigDialog(initial: MeowServer? = null, onDismiss: () -> Unit, onConfirm: (String, String, String) -> Unit) {
|
||||
var name by remember { mutableStateOf(initial?.name ?: "") }
|
||||
var server by remember { mutableStateOf(initial?.server ?: "") }
|
||||
var key by remember { mutableStateOf(initial?.deviceKey ?: "") }
|
||||
ConfigDialogSkeleton(title = if (initial == null) "添加 Meow" else "编辑 Meow", onDismiss = onDismiss, onConfirm = { onConfirm(name.trim(), server.trim().removeSuffix("/"), key.trim()) }) {
|
||||
OutlinedTextField(name, { name = it }, label = { Text("名称") }, singleLine = true, modifier = Modifier.fillMaxWidth())
|
||||
Spacer(Modifier.size(8.dp))
|
||||
OutlinedTextField(server, { server = it }, label = { Text("服务器 / API 地址") }, singleLine = true, modifier = Modifier.fillMaxWidth(), supportingText = { Text("填写 Meow 提供的完整 API 地址或服务根地址") })
|
||||
Spacer(Modifier.size(8.dp))
|
||||
OutlinedTextField(key, { key = it }, label = { Text("Device Key / Token") }, singleLine = true, modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmailConfigDialog(initial: EmailServer? = null, onDismiss: () -> Unit, onConfirm: (EmailServer) -> Unit) {
|
||||
var name by remember { mutableStateOf(initial?.name ?: "") }
|
||||
@@ -787,3 +1070,10 @@ private suspend fun testNtfy(server: NtfyServer): String {
|
||||
val r = NtfySender.send(server.server, server.topic, server.token, "a2i 测试", "如果你在 ntfy 看到这条推送,说明配置成功")
|
||||
return if (r.isSuccess) "已发送,请查看 ntfy 接收端" else "发送失败:${r.exceptionOrNull()?.message}"
|
||||
}
|
||||
|
||||
private suspend fun testMeow(server: MeowServer): String {
|
||||
if (server.server.isBlank()) return "未填写服务器地址"
|
||||
if (server.deviceKey.isBlank()) return "未填写 Device Key"
|
||||
val r = MeowSender.send(server.server, server.deviceKey, "a2i 测试", "如果你在鸿蒙 Meow 看到这条推送,说明配置成功")
|
||||
return if (r.isSuccess) "已发送,请查看鸿蒙 Meow" else "发送失败:${r.exceptionOrNull()?.message}"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user