Compare commits
12
Commits
ultra-function
...
v4.5.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9d87be289 | ||
|
|
012ea1eeb6 | ||
|
|
920af7bde2 | ||
|
|
88a8e795d1 | ||
|
|
44ae5cef5d | ||
|
|
518d9a4d22 | ||
|
|
72f507163a | ||
|
|
f7a3dbaa2d | ||
|
|
c525f3a11f | ||
|
|
2777607ab5 | ||
|
|
a222cbb839 | ||
|
|
418a05e343 |
No files matched your search
@@ -18,6 +18,12 @@ android {
|
|||||||
abiFilters += listOf("armeabi-v7a")
|
abiFilters += listOf("armeabi-v7a")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
androidResources {
|
||||||
|
// 显式保留全部语言(防 shrinkResources 丢弃 in/id 印尼语配置); AGP 9 用 localeFilters
|
||||||
|
localeFilters += listOf(
|
||||||
|
"en", "zh", "tr", "in", "id", "es", "ru", "si", "uk"
|
||||||
|
)
|
||||||
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
if (keystoreProperties["storeFile"] != null) {
|
if (keystoreProperties["storeFile"] != null) {
|
||||||
create("release") {
|
create("release") {
|
||||||
|
|||||||
@@ -14,7 +14,10 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
|
||||||
<application
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<application
|
||||||
android:name=".MainApplication"
|
android:name=".MainApplication"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
@@ -45,5 +48,9 @@
|
|||||||
android:name="android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED"
|
android:name="android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED"
|
||||||
android:value="com.rtbishop.look4sat.bg7nta" />
|
android:value="com.rtbishop.look4sat.bg7nta" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="com.rtbishop.look4sat.app.AprsForegroundService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
package com.rtbishop.look4sat.app
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.widget.Toast
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
import com.rtbishop.look4sat.MainApplication
|
||||||
|
import com.rtbishop.look4sat.core.presentation.R
|
||||||
|
import com.rtbishop.look4sat.core.data.aprs.AprsConfig
|
||||||
|
import com.rtbishop.look4sat.core.data.aprs.AprsStore
|
||||||
|
import com.rtbishop.look4sat.core.data.aprs.AprsReporter
|
||||||
|
import com.rtbishop.look4sat.core.data.aprs.AprsState
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APRS 前台服务:系统通知保活,切换页面不断线。
|
||||||
|
* START_STICKY:被系统杀掉后自动重启(APRSdroid 同款策略)。
|
||||||
|
*/
|
||||||
|
class AprsForegroundService : Service() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ACTION_START = AprsStore.ACTION_START
|
||||||
|
const val ACTION_STOP = AprsStore.ACTION_STOP
|
||||||
|
const val ACTION_REPORT_NOW = AprsStore.ACTION_REPORT_NOW
|
||||||
|
const val CHANNEL_ID = "aprs_service"
|
||||||
|
const val NOTIF_ID = 101
|
||||||
|
}
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
private var reporter: AprsReporter? = null
|
||||||
|
private var lastState: AprsState = AprsState.Idle
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
createChannel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
when (intent?.action) {
|
||||||
|
ACTION_STOP -> stopReporting()
|
||||||
|
ACTION_REPORT_NOW -> {
|
||||||
|
if (reporter == null) {
|
||||||
|
// 服务没在跑:先启动(未配置会 Toast 提示)
|
||||||
|
startReporting()
|
||||||
|
}
|
||||||
|
reporter?.reportNow()
|
||||||
|
}
|
||||||
|
else -> startReporting()
|
||||||
|
}
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startReporting() {
|
||||||
|
val cfg = AprsStore.loadConfig(this)
|
||||||
|
if (!cfg.enabled || cfg.callsign.isBlank()) {
|
||||||
|
runCatching {
|
||||||
|
Toast.makeText(this, getString(R.string.aprs_toast_not_configured), Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
stopSelf()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startForegroundWithNotification(cfg)
|
||||||
|
val rep = AprsReporter(
|
||||||
|
configProvider = { AprsStore.loadConfig(this) },
|
||||||
|
positionProvider = { stationPosition() },
|
||||||
|
onState = { lastState = it },
|
||||||
|
onReport = { report ->
|
||||||
|
AprsStore.saveLastReport(this, report.ok, report.detail)
|
||||||
|
updateNotification(cfg)
|
||||||
|
// 上报结果必达:成功 Toast 短显,失败 Toast 长显+原因
|
||||||
|
val msg = if (report.ok) {
|
||||||
|
getString(R.string.aprs_toast_ok)
|
||||||
|
} else {
|
||||||
|
getString(R.string.aprs_toast_fail, report.detail)
|
||||||
|
}
|
||||||
|
runCatching {
|
||||||
|
Toast.makeText(this, msg,
|
||||||
|
if (report.ok) Toast.LENGTH_SHORT else Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
reporter = rep
|
||||||
|
rep.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopReporting() {
|
||||||
|
reporter?.stop()
|
||||||
|
reporter = null
|
||||||
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startForegroundWithNotification(cfg: AprsConfig) {
|
||||||
|
try {
|
||||||
|
val notif = buildNotification(cfg)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
startForeground(NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||||
|
} else {
|
||||||
|
startForeground(NOTIF_ID, notif)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// 厂商 ROM / 旧系统兼容兜底:启动前台失败只停服务,不崩进程
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildNotification(cfg: AprsConfig): Notification {
|
||||||
|
val pi = PendingIntent.getActivity(
|
||||||
|
this, 0, packageManager.getLaunchIntentForPackage(packageName),
|
||||||
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
val stopPi = PendingIntent.getService(
|
||||||
|
this, 1, Intent(this, AprsForegroundService::class.java).setAction(ACTION_STOP),
|
||||||
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
val stateText = when (lastState) {
|
||||||
|
AprsState.Connected -> getString(R.string.aprs_notif_connected)
|
||||||
|
AprsState.Error -> getString(R.string.aprs_notif_error)
|
||||||
|
else -> getString(R.string.aprs_notif_running)
|
||||||
|
}
|
||||||
|
return Notification.Builder(this, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_radios)
|
||||||
|
.setContentTitle(getString(R.string.aprs_notif_title, cfg.callsign))
|
||||||
|
.setContentText(stateText)
|
||||||
|
.setContentIntent(pi)
|
||||||
|
.setOngoing(true)
|
||||||
|
.addAction(0, getString(R.string.aprs_notif_stop), stopPi)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateNotification(cfg: AprsConfig) {
|
||||||
|
val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
nm.notify(NOTIF_ID, buildNotification(cfg))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 上报位置:优先用设置里的站位(用户拍板);站位无效时实时 GPS 兜底 */
|
||||||
|
private fun stationPosition(): Pair<Double, Double>? {
|
||||||
|
// ① 站位(设置页设置的位置)
|
||||||
|
val station = runCatching {
|
||||||
|
val container = (application as MainApplication).getMainContainer()
|
||||||
|
container.settingsRepo.stationPosition.value
|
||||||
|
}.getOrNull()
|
||||||
|
if (station != null && (station.latitude != 0.0 || station.longitude != 0.0)) {
|
||||||
|
return Pair(station.latitude, station.longitude)
|
||||||
|
}
|
||||||
|
// ② 兜底:实时 GPS 最后位置
|
||||||
|
return runCatching {
|
||||||
|
val lm = getSystemService(Context.LOCATION_SERVICE) as android.location.LocationManager
|
||||||
|
val providers = listOf(
|
||||||
|
android.location.LocationManager.GPS_PROVIDER,
|
||||||
|
android.location.LocationManager.NETWORK_PROVIDER
|
||||||
|
)
|
||||||
|
for (p in providers) {
|
||||||
|
val loc = lm.getLastKnownLocation(p) ?: continue
|
||||||
|
if (loc.latitude != 0.0 || loc.longitude != 0.0) {
|
||||||
|
return Pair(loc.latitude, loc.longitude)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
null
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createChannel() {
|
||||||
|
val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
val channel = NotificationChannel(
|
||||||
|
CHANNEL_ID, getString(R.string.aprs_notif_channel),
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
)
|
||||||
|
nm.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
reporter?.stop()
|
||||||
|
reporter = null
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,10 +30,29 @@ class MainApplication : Application(), IContainerProvider {
|
|||||||
|
|
||||||
private lateinit var container: IMainContainer
|
private lateinit var container: IMainContainer
|
||||||
|
|
||||||
|
/** 全局崩溃捕获:堆栈写入 files/crash_log.txt,重启后可查看(用户要求错误报告) */
|
||||||
|
private fun installCrashHandler() {
|
||||||
|
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
||||||
|
runCatching {
|
||||||
|
val log = StringBuilder()
|
||||||
|
log.append("=== Crash ${System.currentTimeMillis()} ===\n")
|
||||||
|
log.append("Thread: ").append(thread.name).append("\n")
|
||||||
|
val sw = java.io.StringWriter()
|
||||||
|
throwable.printStackTrace(java.io.PrintWriter(sw))
|
||||||
|
log.append(sw.toString()).append("\n")
|
||||||
|
val file = java.io.File(filesDir, "crash_log.txt")
|
||||||
|
file.appendText(log.toString())
|
||||||
|
}
|
||||||
|
defaultHandler?.uncaughtException(thread, throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getMainContainer(): IMainContainer = container
|
override fun getMainContainer(): IMainContainer = container
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
installCrashHandler()
|
||||||
container = MainContainer(this)
|
container = MainContainer(this)
|
||||||
// trigger automatic update every 48 hours
|
// trigger automatic update every 48 hours
|
||||||
container.appScope.launch { checkAutoUpdate() }
|
container.appScope.launch { checkAutoUpdate() }
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package com.rtbishop.look4sat.core.data.aprs
|
||||||
|
|
||||||
|
import com.rtbishop.look4sat.core.domain.aprs.AprsPacket
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.io.OutputStreamWriter
|
||||||
|
import java.io.PrintWriter
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Socket
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APRS-IS TCP 客户端(从 APRSdroid TcpUploader.scala 逆向移植)。
|
||||||
|
* 纯文本协议:登录一行 + 每行一个包;断线 30 秒重连。
|
||||||
|
*/
|
||||||
|
class AprsIsClient(
|
||||||
|
private val host: String,
|
||||||
|
private val port: Int,
|
||||||
|
private val callsign: String,
|
||||||
|
private val ssid: String,
|
||||||
|
private val passcode: Int,
|
||||||
|
private val version: String,
|
||||||
|
private val filter: String = "",
|
||||||
|
private val timeoutSec: Int = 120
|
||||||
|
) {
|
||||||
|
private var socket: Socket? = null
|
||||||
|
private var writer: PrintWriter? = null
|
||||||
|
private var reader: BufferedReader? = null
|
||||||
|
private val lock = Any()
|
||||||
|
|
||||||
|
val isConnected: Boolean
|
||||||
|
get() = synchronized(lock) { socket?.isConnected == true && !socket!!.isClosed }
|
||||||
|
|
||||||
|
/** 建立连接 + 登录(同步阻塞,调用方放后台线程) */
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun connect() {
|
||||||
|
disconnect()
|
||||||
|
val s = Socket()
|
||||||
|
s.connect(InetSocketAddress(host, port), 30_000)
|
||||||
|
s.soTimeout = timeoutSec * 1000
|
||||||
|
s.tcpNoDelay = true
|
||||||
|
synchronized(lock) {
|
||||||
|
socket = s
|
||||||
|
writer = PrintWriter(OutputStreamWriter(s.getOutputStream(), Charsets.ISO_8859_1), true)
|
||||||
|
reader = BufferedReader(InputStreamReader(s.getInputStream(), Charsets.ISO_8859_1), 256)
|
||||||
|
}
|
||||||
|
// 登录行
|
||||||
|
val login = AprsPacket.formatLogin(callsign, ssid, passcode, version) + filter
|
||||||
|
writer?.println(login)
|
||||||
|
// 读登录响应(aprsc 返回 # logresp ... verified/unverified)
|
||||||
|
runCatching {
|
||||||
|
s.soTimeout = 8000
|
||||||
|
val resp = reader?.readLine()
|
||||||
|
if (resp != null && (resp.contains("Invalid", ignoreCase = true) ||
|
||||||
|
resp.contains("unverified", ignoreCase = true))) {
|
||||||
|
throw IllegalArgumentException(resp.trim())
|
||||||
|
}
|
||||||
|
// 恢复超时
|
||||||
|
s.soTimeout = timeoutSec * 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送一个 APRS 包(一行)并尝试读服务器确认。
|
||||||
|
* 返回 null=发送失败;Pair(ok, detail)=发送结果(服务器错误原文在 detail)
|
||||||
|
*/
|
||||||
|
fun sendPacket(packetLine: String): Pair<Boolean, String>? {
|
||||||
|
synchronized(lock) {
|
||||||
|
val w = writer ?: return null
|
||||||
|
w.println(packetLine)
|
||||||
|
if (w.checkError()) return Pair(false, "write failed")
|
||||||
|
}
|
||||||
|
// 尝试读服务器响应(短超时 3s;APRS-IS 对格式错误会回错误行)
|
||||||
|
return runCatching {
|
||||||
|
val s = socket ?: return@runCatching Pair(true, "OK")
|
||||||
|
val oldTimeout = s.soTimeout
|
||||||
|
s.soTimeout = 3000
|
||||||
|
try {
|
||||||
|
val resp = reader?.readLine()
|
||||||
|
if (resp != null && (resp.contains("Invalid", ignoreCase = true) ||
|
||||||
|
resp.contains("error", ignoreCase = true))) {
|
||||||
|
Pair(false, resp.trim())
|
||||||
|
} else {
|
||||||
|
Pair(true, if (resp.isNullOrBlank()) "OK" else resp.trim())
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
s.soTimeout = oldTimeout
|
||||||
|
}
|
||||||
|
}.getOrElse { Pair(true, "OK") }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 读一行(服务器响应,超时抛异常) */
|
||||||
|
fun readLine(): String? {
|
||||||
|
return reader?.readLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disconnect() {
|
||||||
|
synchronized(lock) {
|
||||||
|
runCatching { writer?.close() }
|
||||||
|
runCatching { reader?.close() }
|
||||||
|
runCatching { socket?.close() }
|
||||||
|
writer = null
|
||||||
|
reader = null
|
||||||
|
socket = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package com.rtbishop.look4sat.core.data.aprs
|
||||||
|
|
||||||
|
import com.rtbishop.look4sat.core.domain.aprs.AprsPacket
|
||||||
|
import com.rtbishop.look4sat.core.domain.aprs.AprsPosition
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/** APRS 连接状态 */
|
||||||
|
enum class AprsState { Idle, Connecting, Connected, Disconnected, Error }
|
||||||
|
|
||||||
|
/** APRS 配置(SharedPreferences 持久化,填入即保存) */
|
||||||
|
data class AprsConfig(
|
||||||
|
val enabled: Boolean = false,
|
||||||
|
val server: String = "euro.aprs2.net",
|
||||||
|
val port: Int = 14580,
|
||||||
|
val callsign: String = "",
|
||||||
|
val ssid: String = "",
|
||||||
|
val passcode: String = "",
|
||||||
|
val intervalMin: Int = 5,
|
||||||
|
val statusText: String = "Look4Sat APRS",
|
||||||
|
val symbolTable: String = "/",
|
||||||
|
val symbolCode: String = ">",
|
||||||
|
val includeCourseSpeed: Boolean = true,
|
||||||
|
val includeAltitude: Boolean = true
|
||||||
|
)
|
||||||
|
|
||||||
|
/** APRS 上报结果 */
|
||||||
|
data class AprsReport(
|
||||||
|
val timestamp: Long,
|
||||||
|
val packet: String,
|
||||||
|
val ok: Boolean,
|
||||||
|
val detail: String
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 上报调度器(周期上报 + 手动触发),连接管理放在前台服务 */
|
||||||
|
class AprsReporter(
|
||||||
|
private val configProvider: () -> AprsConfig,
|
||||||
|
private val positionProvider: () -> Pair<Double, Double>? = { null },
|
||||||
|
private val onState: (AprsState) -> Unit = {},
|
||||||
|
private val onReport: (AprsReport) -> Unit = {}
|
||||||
|
) {
|
||||||
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
private var client: AprsIsClient? = null
|
||||||
|
private var job: Job? = null
|
||||||
|
private var manualJob: Job? = null
|
||||||
|
|
||||||
|
val isRunning: Boolean get() = job?.isActive == true
|
||||||
|
|
||||||
|
/** 启动周期上报(前台服务调用) */
|
||||||
|
fun start() {
|
||||||
|
stop()
|
||||||
|
val cfg = configProvider()
|
||||||
|
if (!cfg.enabled || cfg.callsign.isBlank()) {
|
||||||
|
onState(AprsState.Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
job = scope.launch {
|
||||||
|
while (isActive) {
|
||||||
|
reportOnce()
|
||||||
|
delay(cfg.intervalMin.coerceAtLeast(1) * 60_000L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
job?.cancel()
|
||||||
|
job = null
|
||||||
|
runCatching { client?.disconnect() }
|
||||||
|
client = null
|
||||||
|
onState(AprsState.Idle)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 手动触发一次(立即上报,不等待周期) */
|
||||||
|
fun reportNow() {
|
||||||
|
manualJob?.cancel()
|
||||||
|
manualJob = scope.launch { reportOnce() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun reportOnce() {
|
||||||
|
val cfg = configProvider()
|
||||||
|
if (!cfg.enabled || cfg.callsign.isBlank()) return
|
||||||
|
onState(AprsState.Connecting)
|
||||||
|
try {
|
||||||
|
val c = client ?: AprsIsClient(
|
||||||
|
host = cfg.server,
|
||||||
|
port = cfg.port,
|
||||||
|
callsign = cfg.callsign,
|
||||||
|
ssid = cfg.ssid,
|
||||||
|
passcode = cfg.passcode.toIntOrNull()?.takeIf { it >= 0 } ?: AprsPacket.passcode(cfg.callsign),
|
||||||
|
version = "Look4Sat 4.5.4"
|
||||||
|
).also { client = it }
|
||||||
|
if (!c.isConnected) c.connect()
|
||||||
|
onState(AprsState.Connected)
|
||||||
|
|
||||||
|
val pos = positionProvider()
|
||||||
|
val packetLine = buildPositionPacket(cfg, pos?.first, pos?.second)
|
||||||
|
val result = c.sendPacket(packetLine)
|
||||||
|
val ok = result?.first == true
|
||||||
|
val detail = result?.second ?: "no connection"
|
||||||
|
onReport(AprsReport(System.currentTimeMillis(), packetLine, ok, detail))
|
||||||
|
if (ok) onState(AprsState.Connected) else onState(AprsState.Error)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
runCatching { client?.disconnect() }
|
||||||
|
client = null
|
||||||
|
onState(AprsState.Error)
|
||||||
|
onReport(AprsReport(System.currentTimeMillis(), "", false, e.message ?: "error"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 构造位置包:BG7NTA-5>APRS:=DDMM.MMN/DDDMM.MME<状态文本 */
|
||||||
|
private fun buildPositionPacket(cfg: AprsConfig, lat: Double? = null, lon: Double? = null): String {
|
||||||
|
val source = AprsPacket.formatCallSsid(cfg.callsign, cfg.ssid)
|
||||||
|
val pos = AprsPosition(
|
||||||
|
latitude = lat ?: 0.0,
|
||||||
|
longitude = lon ?: 0.0,
|
||||||
|
symbolTable = cfg.symbolTable.firstOrNull() ?: '/',
|
||||||
|
symbolCode = cfg.symbolCode.firstOrNull() ?: '>'
|
||||||
|
)
|
||||||
|
return "$source>APRS:=${pos.toUncompressedString()}${cfg.statusText}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package com.rtbishop.look4sat.core.data.aprs
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APRS 配置存取 + 服务 action 常量(被 feature/settings 和 app 服务共用,
|
||||||
|
* 避免 feature 反向依赖 app)。
|
||||||
|
*/
|
||||||
|
object AprsStore {
|
||||||
|
|
||||||
|
const val ACTION_START = "com.rtbishop.look4sat.aprs.START"
|
||||||
|
const val ACTION_STOP = "com.rtbishop.look4sat.aprs.STOP"
|
||||||
|
const val ACTION_REPORT_NOW = "com.rtbishop.look4sat.aprs.REPORT_NOW"
|
||||||
|
const val SERVICE_CLASS = "com.rtbishop.look4sat.app.AprsForegroundService"
|
||||||
|
|
||||||
|
private const val PREFS = "aprs_config"
|
||||||
|
private const val KEY_ENABLED = "enabled"
|
||||||
|
private const val KEY_SERVER = "server"
|
||||||
|
private const val KEY_PORT = "port"
|
||||||
|
private const val KEY_CALLSIGN = "callsign"
|
||||||
|
private const val KEY_SSID = "ssid"
|
||||||
|
private const val KEY_PASSCODE = "passcode"
|
||||||
|
private const val KEY_INTERVAL = "interval"
|
||||||
|
private const val KEY_STATUS = "status"
|
||||||
|
private const val KEY_SYMBOL_TABLE = "symbol_table"
|
||||||
|
private const val KEY_SYMBOL_CODE = "symbol_code"
|
||||||
|
private const val KEY_LAST_TIME = "last_report_time"
|
||||||
|
private const val KEY_LAST_OK = "last_report_ok"
|
||||||
|
private const val KEY_LAST_DETAIL = "last_report_detail"
|
||||||
|
|
||||||
|
/** 读取配置(填入即保存,无需每次打开重填) */
|
||||||
|
fun loadConfig(context: Context): AprsConfig {
|
||||||
|
val p = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
return AprsConfig(
|
||||||
|
enabled = p.getBoolean(KEY_ENABLED, false),
|
||||||
|
server = p.getString(KEY_SERVER, "euro.aprs2.net") ?: "euro.aprs2.net",
|
||||||
|
port = p.getInt(KEY_PORT, 14580),
|
||||||
|
callsign = p.getString(KEY_CALLSIGN, "") ?: "",
|
||||||
|
ssid = p.getString(KEY_SSID, "") ?: "",
|
||||||
|
passcode = p.getString(KEY_PASSCODE, "") ?: "",
|
||||||
|
intervalMin = p.getInt(KEY_INTERVAL, 5),
|
||||||
|
statusText = p.getString(KEY_STATUS, "Look4Sat APRS") ?: "Look4Sat APRS",
|
||||||
|
symbolTable = p.getString(KEY_SYMBOL_TABLE, "/") ?: "/",
|
||||||
|
symbolCode = p.getString(KEY_SYMBOL_CODE, ">") ?: ">"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 上次上报结果(供设置卡片显示) */
|
||||||
|
data class LastReport(val time: Long = 0L, val ok: Boolean = false, val detail: String = "")
|
||||||
|
|
||||||
|
fun loadLastReport(context: Context): LastReport {
|
||||||
|
val p = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
return LastReport(
|
||||||
|
time = p.getLong(KEY_LAST_TIME, 0L),
|
||||||
|
ok = p.getBoolean(KEY_LAST_OK, false),
|
||||||
|
detail = p.getString(KEY_LAST_DETAIL, "") ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveLastReport(context: Context, ok: Boolean, detail: String) {
|
||||||
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
|
||||||
|
.putLong(KEY_LAST_TIME, System.currentTimeMillis())
|
||||||
|
.putBoolean(KEY_LAST_OK, ok)
|
||||||
|
.putString(KEY_LAST_DETAIL, detail)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存配置 */
|
||||||
|
fun saveConfig(context: Context, cfg: AprsConfig) {
|
||||||
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
|
||||||
|
.putBoolean(KEY_ENABLED, cfg.enabled)
|
||||||
|
.putString(KEY_SERVER, cfg.server)
|
||||||
|
.putInt(KEY_PORT, cfg.port)
|
||||||
|
.putString(KEY_CALLSIGN, cfg.callsign)
|
||||||
|
.putString(KEY_SSID, cfg.ssid)
|
||||||
|
.putString(KEY_PASSCODE, cfg.passcode)
|
||||||
|
.putInt(KEY_INTERVAL, cfg.intervalMin)
|
||||||
|
.putString(KEY_STATUS, cfg.statusText)
|
||||||
|
.putString(KEY_SYMBOL_TABLE, cfg.symbolTable)
|
||||||
|
.putString(KEY_SYMBOL_CODE, cfg.symbolCode)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package com.rtbishop.look4sat.core.domain.aprs
|
||||||
|
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.round
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APRS-IS 协议核心(纯 Kotlin,无 Android 依赖)。
|
||||||
|
* 从 APRSdroid 1.6.3d 逆向移植:AprsPacket$.scala + ab0oo Position.java。
|
||||||
|
*/
|
||||||
|
object AprsPacket {
|
||||||
|
|
||||||
|
/** APRS-IS passcode 算法(标准):0x73E2 初值,呼号大写+\0,每对字符 XOR */
|
||||||
|
fun passcode(callsign: String): Int {
|
||||||
|
val s = callsign.split("-")[0].uppercase() + "\u0000"
|
||||||
|
var hash = 29666 // 0x73E2
|
||||||
|
var i = 0
|
||||||
|
while (i <= s.length - 2) {
|
||||||
|
hash = hash xor (s[i].code * 256 + s[i + 1].code)
|
||||||
|
i += 2
|
||||||
|
}
|
||||||
|
return hash and 0x7FFF
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 登录行:user CALL-SSID pass XXXX vers XXXX */
|
||||||
|
fun formatLogin(callsign: String, ssid: String, passcode: Int, version: String): String {
|
||||||
|
val callSsid = formatCallSsid(callsign, ssid)
|
||||||
|
return "user $callSsid pass $passcode vers $version"
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 呼号-SSID 拼接(BG7NTA + 5 → BG7NTA-5) */
|
||||||
|
fun formatCallSsid(callsign: String, ssid: String): String {
|
||||||
|
if (ssid.isNullOrEmpty()) return callsign
|
||||||
|
return "$callsign-$ssid"
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 可选距离过滤: filter r/lat/lon/dist */
|
||||||
|
fun formatRangeFilter(latitude: Double, longitude: Double, distKm: Int): String {
|
||||||
|
return String.format("r/%.3f/%.3f/%d", latitude, longitude, distKm)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 高度扩展 /A=00000(英尺) */
|
||||||
|
fun formatAltitude(altitudeMeters: Double?): String {
|
||||||
|
if (altitudeMeters == null) return ""
|
||||||
|
return String.format("/A=%06d", (altitudeMeters * 3.2808399).toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 航速航向扩展(节/度) */
|
||||||
|
fun formatCourseSpeed(speedMps: Double?, bearing: Float?): String {
|
||||||
|
if (speedMps == null || bearing == null) return ""
|
||||||
|
val knots = (speedMps * 1.94384449).toInt()
|
||||||
|
return String.format("/%03d/%03d", bearing.toInt(), knots)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APRS 位置编码(ab0oo Position.java 逆向移植)。
|
||||||
|
* 非压缩:DDMM.MMN/DDDMM.MME;压缩:base91。
|
||||||
|
*/
|
||||||
|
class AprsPosition(
|
||||||
|
val latitude: Double,
|
||||||
|
val longitude: Double,
|
||||||
|
val symbolTable: Char,
|
||||||
|
val symbolCode: Char,
|
||||||
|
val positionAmbiguity: Int = 0
|
||||||
|
) {
|
||||||
|
|
||||||
|
/** 非压缩格式(APRS-IS 默认上报格式) */
|
||||||
|
fun toUncompressedString(): String {
|
||||||
|
val lat = getDMS(latitude, true)
|
||||||
|
val lon = getDMS(longitude, false)
|
||||||
|
return "$lat$symbolTable$lon$symbolCode"
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 压缩格式(base91,标准 APRS 算法) */
|
||||||
|
fun toCompressedString(): String {
|
||||||
|
val jRound = round((90.0 - latitude) * 380926.0).toLong()
|
||||||
|
val j = jRound / 753571 + 33
|
||||||
|
val j2 = jRound % 753571
|
||||||
|
val j3 = j2 / 8281 + 33
|
||||||
|
val j4 = j2 % 8281
|
||||||
|
val i = (j4 % 91).toInt() + 33
|
||||||
|
val jRound2 = round((longitude + 180.0) * 190463.0).toLong()
|
||||||
|
val j5 = jRound2 / 753571 + 33
|
||||||
|
val j6 = jRound2 % 753571
|
||||||
|
val j7 = 33 + j6 / 8281
|
||||||
|
val j8 = j6 % 8281
|
||||||
|
return "" + symbolTable + j.toInt().toChar() + j3.toInt().toChar() +
|
||||||
|
((j4 / 91).toInt() + 33).toChar() + i.toChar() +
|
||||||
|
j5.toInt().toChar() + j7.toInt().toChar() +
|
||||||
|
((j8 / 91).toInt() + 33).toChar() +
|
||||||
|
((j8 % 91).toInt() + 33).toChar() + symbolCode
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单轴 DMS 编码(百分位) */
|
||||||
|
private fun getDMS(value: Double, isLat: Boolean): String {
|
||||||
|
var iRound = round(value * 6000.0).toInt()
|
||||||
|
if (iRound < 0) iRound = -iRound
|
||||||
|
val degrees = iRound / 6000
|
||||||
|
val minutes = (iRound / 100) % 60
|
||||||
|
val hundredths = iRound % 100
|
||||||
|
val frac = when (positionAmbiguity) {
|
||||||
|
1 -> " . "
|
||||||
|
2 -> String.format("%d . ", minutes / 10)
|
||||||
|
3 -> String.format("%02d. ", minutes)
|
||||||
|
4 -> String.format("%02d.%d ", minutes, hundredths / 10)
|
||||||
|
else -> String.format("%02d.%02d", minutes, hundredths)
|
||||||
|
}
|
||||||
|
return if (isLat) {
|
||||||
|
val ns = if (value >= 0) 'N' else 'S'
|
||||||
|
String.format("%02d%s%c", degrees, frac, ns)
|
||||||
|
} else {
|
||||||
|
val ew = if (value >= 0) 'E' else 'W'
|
||||||
|
String.format("%03d%s%c", degrees, frac, ew)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,36 @@
|
|||||||
<string name="nav_cw">Dekoder CW</string>
|
<string name="nav_cw">Dekoder CW</string>
|
||||||
<string name="nav_map">Peta</string>
|
<string name="nav_map">Peta</string>
|
||||||
<string name="nav_prefs">Pengaturan</string>
|
<string name="nav_prefs">Pengaturan</string>
|
||||||
|
<string name="prefs_aprs_title">Pelaporan APRS</string>
|
||||||
|
<string name="prefs_aprs_enable">Aktifkan pelaporan APRS</string>
|
||||||
|
<string name="prefs_aprs_summary">Server: %1$s:%2$d Panggilan: %3$s</string>
|
||||||
|
<string name="prefs_aprs_report_now">Lapor Sekarang</string>
|
||||||
|
<string name="prefs_aprs_settings_title">Pengaturan APRS</string>
|
||||||
|
<string name="prefs_aprs_server">Server</string>
|
||||||
|
<string name="prefs_aprs_port">Port</string>
|
||||||
|
<string name="prefs_aprs_callsign">Tanda panggilan</string>
|
||||||
|
<string name="prefs_aprs_ssid">SSID</string>
|
||||||
|
<string name="prefs_aprs_passcode">Kode sandi</string>
|
||||||
|
<string name="prefs_aprs_passcode_hint">Kosong = otomatis dari panggilan</string>
|
||||||
|
<string name="prefs_aprs_interval">Interval (menit)</string>
|
||||||
|
<string name="prefs_aprs_status">Teks status</string>
|
||||||
|
<string name="prefs_aprs_symbol_table">Tabel simbol</string>
|
||||||
|
<string name="prefs_aprs_symbol_code">Kode simbol</string>
|
||||||
|
<string name="prefs_aprs_save">Simpan</string>
|
||||||
|
<string name="prefs_aprs_cancel">Batal</string>
|
||||||
|
<string name="aprs_notif_channel">Layanan APRS</string>
|
||||||
|
<string name="aprs_notif_title">APRS: %1$s</string>
|
||||||
|
<string name="aprs_notif_running">Pelaporan berjalan</string>
|
||||||
|
<string name="aprs_notif_connected">Terhubung ke APRS-IS</string>
|
||||||
|
<string name="aprs_notif_error">Kesalahan koneksi</string>
|
||||||
|
<string name="aprs_notif_stop">Berhenti</string>
|
||||||
|
<string name="prefs_aprs_calc_passcode">Hitung kode sandi</string>
|
||||||
|
<string name="aprs_toast_ok">APRS: laporan terkirim</string>
|
||||||
|
<string name="aprs_toast_fail">APRS: laporan gagal - %1$s</string>
|
||||||
|
<string name="aprs_toast_not_configured">APRS belum dikonfigurasi - isi panggilan dulu</string>
|
||||||
|
<string name="prefs_aprs_last_report">Laporan terakhir: %1$s %2$s</string>
|
||||||
|
<string name="prefs_aprs_last_ok">OK</string>
|
||||||
|
<string name="prefs_aprs_last_fail">gagal - %1$s</string>
|
||||||
<string name="nav_more">Lainnya</string>
|
<string name="nav_more">Lainnya</string>
|
||||||
<string name="nav_log">Log</string>
|
<string name="nav_log">Log</string>
|
||||||
<string name="nav_amsat">AMSAT</string>
|
<string name="nav_amsat">AMSAT</string>
|
||||||
@@ -66,7 +96,8 @@
|
|||||||
* GPS kini hanya menampilkan sukses saat posisi benar-benar didapat (timeout 15 dtk, cek izin)
|
* GPS kini hanya menampilkan sukses saat posisi benar-benar didapat (timeout 15 dtk, cek izin)
|
||||||
* Pesan kegagalan jelas saat database satelit gagal diunduh
|
* Pesan kegagalan jelas saat database satelit gagal diunduh
|
||||||
* Frekuensi log mengikuti panel Doppler secara real-time; visual hapus geser dipulihkan
|
* Frekuensi log mengikuti panel Doppler secara real-time; visual hapus geser dipulihkan
|
||||||
* Perbaikan bahasa Indonesia (values-id)</string>
|
* Perbaikan bahasa Indonesia (values-id)
|
||||||
|
* BARU: Pelaporan APRS (jaringan APRS-IS, layanan latar depan dengan notifikasi, interval dapat diatur)</string>
|
||||||
<string name="radar_back">Kembali</string>
|
<string name="radar_back">Kembali</string>
|
||||||
<string name="radar_notify">Beri tahu</string>
|
<string name="radar_notify">Beri tahu</string>
|
||||||
<string name="radar_az_text">Azimuth</string>
|
<string name="radar_az_text">Azimuth</string>
|
||||||
|
|||||||
@@ -1,252 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="app_name" translatable="false">Look4Sat Pro</string>
|
|
||||||
<string name="btn_accept">Terima</string>
|
|
||||||
<string name="btn_cancel">Batal</string>
|
|
||||||
<string name="empty_list_message">Tidak ada data untuk ditampilkan</string>
|
|
||||||
<string name="nav_sat">Satelit</string>
|
|
||||||
<string name="nav_pass">Lintasan</string>
|
|
||||||
<string name="nav_radar">Radar</string>
|
|
||||||
<string name="nav_mutual">Pasangan</string>
|
|
||||||
<string name="nav_roaming">Jelajah</string>
|
|
||||||
<string name="nav_cw">Dekoder CW</string>
|
|
||||||
<string name="nav_map">Peta</string>
|
|
||||||
<string name="nav_prefs">Pengaturan</string>
|
|
||||||
<string name="nav_more">Lainnya</string>
|
|
||||||
<string name="nav_log">Log</string>
|
|
||||||
<string name="nav_amsat">AMSAT</string>
|
|
||||||
<string name="amsat_title">Status Satelit AMSAT</string>
|
|
||||||
<string name="amsat_updated">Diperbarui:</string>
|
|
||||||
<string name="amsat_active">Aktif</string>
|
|
||||||
<string name="amsat_tlm">TLM/beacon</string>
|
|
||||||
<string name="amsat_not_heard">Tidak terdengar</string>
|
|
||||||
<string name="amsat_conflict">Konflik</string>
|
|
||||||
<string name="amsat_name">Name</string>
|
|
||||||
<string name="amsat_load_failed">Gagal memuat - periksa jaringan dan coba lagi</string>
|
|
||||||
<string name="amsat_no_reports">Tidak ada laporan untuk periode ini</string>
|
|
||||||
<string name="amsat_close">Tutup</string>
|
|
||||||
<string name="sat_type_hint">Jenis: %s</string>
|
|
||||||
<string name="sat_type_title">Pilih jenis satelit</string>
|
|
||||||
<string name="sat_search_hint">Id - Nama</string>
|
|
||||||
<string name="sat_search_clear">Bersihkan</string>
|
|
||||||
<string name="sat_clear_all">Bersihkan semua</string>
|
|
||||||
<string name="sat_select_all">Pilih semua</string>
|
|
||||||
<string name="sat_empty_list_message">Pastikan kata kunci pencarian Anda benar dan DB sudah diperbarui</string>
|
|
||||||
<string name="sat_warning_title">Peringatan\!</string>
|
|
||||||
<string name="sat_warning_message">
|
|
||||||
Ada lebih dari 9000 satelit yang terdaftar di aplikasi ini.
|
|
||||||
Tidak masuk akal untuk melacak semuanya sekaligus.
|
|
||||||
\n\nSelalu coba persempit daftar hanya ke satelit yang
|
|
||||||
Anda minati melalui pencarian dan pemilih jenis.</string>
|
|
||||||
<string name="pass_filter_title">Filter lintasan</string>
|
|
||||||
<string name="pass_filter_elev">Elevasi minimal</string>
|
|
||||||
<string name="pass_filter_hours">Waktu ke depan</string>
|
|
||||||
<string name="pass_filter_aos_time">Jendela AOS</string>
|
|
||||||
<string name="pass_filter_invert_time">Balik jendela AOS</string>
|
|
||||||
<string name="pass_filter_deep_space">DeepSpace (periode >225 mnt)</string>
|
|
||||||
<string name="pass_time_placeholder" translatable="false"> -- : -- : -- </string>
|
|
||||||
<string name="pass_modes_title">Pilih jenis modulasi</string>
|
|
||||||
<string name="pass_satId" translatable="false">%05d</string>
|
|
||||||
<string name="pass_elevation">Elevasi: %.1f°</string>
|
|
||||||
<string name="pass_deep_space" translatable="false">DeepSpace</string>
|
|
||||||
<string name="pass_altitude">Ketinggian: %d km</string>
|
|
||||||
<string name="pass_aosLos" translatable="false">%03d° - %03d°</string>
|
|
||||||
<string name="pass_empty_list_message">
|
|
||||||
Pastikan filter Anda benar dan Anda sudah memilih satelit</string>
|
|
||||||
<string name="pass_error_message">Tidak ada lintasan satelit mendatang untuk ditampilkan</string>
|
|
||||||
<string name="pass_welcome_title">Selamat datang di Look4Sat\!</string>
|
|
||||||
<string name="pass_welcome_message">
|
|
||||||
Pastikan Anda mengatur posisi melalui GPS, Lat/Lon atau QTH di Pengaturan.
|
|
||||||
\n\nPerbarui database setidaknya seminggu sekali untuk prediksi yang akurat.</string>
|
|
||||||
<string name="pass_whatsnew_title" translatable="false">Apa yang baru di Look4Sat</string>
|
|
||||||
<string name="pass_whatsnew_message" translatable="false">
|
|
||||||
* BARU: Halaman status satelit AMSAT - tabel status langsung (warna resmi), jumlah laporan, laporan detail per satelit/hari
|
|
||||||
* Log dikelompokkan per lintasan satelit (sesi = satelit + waktu AOS), garis pemisah antar sesi
|
|
||||||
* GPS kini hanya menampilkan sukses saat posisi benar-benar didapat (timeout 15 dtk, cek izin)
|
|
||||||
* Pesan kegagalan jelas saat database satelit gagal diunduh
|
|
||||||
* Frekuensi log mengikuti panel Doppler secara real-time; visual hapus geser dipulihkan
|
|
||||||
* Perbaikan bahasa Indonesia (values-id)</string>
|
|
||||||
<string name="radar_back">Kembali</string>
|
|
||||||
<string name="radar_notify">Beri tahu</string>
|
|
||||||
<string name="radar_az_text">Azimuth</string>
|
|
||||||
<string name="radar_az_value" translatable="false">%.1f°</string>
|
|
||||||
<string name="radar_el_text">Elevasi</string>
|
|
||||||
<string name="radar_el_value" translatable="false">%.1f°</string>
|
|
||||||
<string name="radar_alt_text">Ketinggian</string>
|
|
||||||
<string name="radar_alt_value" translatable="false">%.0f km</string>
|
|
||||||
<string name="radar_dist_text">Jarak</string>
|
|
||||||
<string name="radar_dist_value" translatable="false">%.0f km</string>
|
|
||||||
<string name="radar_eclipsed">Gerhana</string>
|
|
||||||
<string name="radar_downlink">Downlink</string>
|
|
||||||
<string name="radar_uplink">Uplink</string>
|
|
||||||
<string name="radar_link_low" translatable="false">%.4f</string>
|
|
||||||
<string name="radar_link_lowHigh" translatable="false">%.4f - %.4f</string>
|
|
||||||
<string name="radar_no_link" translatable="false">- - . - -</string>
|
|
||||||
<string name="radar_no_data">Satelit ini tidak memiliki transceiver</string>
|
|
||||||
<string name="radar_add">Tambah</string>
|
|
||||||
<string name="radar_data">Transceiver satelit:</string>
|
|
||||||
<string name="radar_inverted">Terbalik: %s</string>
|
|
||||||
<string name="radar_mode">Mode: %s</string>
|
|
||||||
<string name="radar_string_no">Tidak</string>
|
|
||||||
<string name="radar_string_yes">Ya</string>
|
|
||||||
<string name="radar_visible">Terlihat</string>
|
|
||||||
<string name="radar_doppler_calc">Kalkulator Frekuensi Doppler</string>
|
|
||||||
<string name="radar_doppler_tx_hint">Masukkan frekuensi TX (MHz)</string>
|
|
||||||
<string name="radar_doppler_rx_hint">Masukkan frekuensi RX (MHz)</string>
|
|
||||||
<string name="radar_doppler_offset_hint">Offset (kHz)</string>
|
|
||||||
<string name="radar_doppler_info">Untuk transponder linier, ketik satu frekuensi untuk melihat frekuensi lainnya</string>
|
|
||||||
<string name="radar_cw_decoder">Dekoder CW</string>
|
|
||||||
<string name="radar_cw_start">Mulai</string>
|
|
||||||
<string name="radar_cw_stop">Berhenti</string>
|
|
||||||
<string name="radar_cw_reset">Bersihkan</string>
|
|
||||||
<string name="map_prev">Sebelumnya</string>
|
|
||||||
<string name="map_next">Berikutnya</string>
|
|
||||||
<string name="map_copyright" translatable="false">© Kontributor OpenStreetMap</string>
|
|
||||||
<string name="map_azimuth">Azimuth: %.1f°</string>
|
|
||||||
<string name="map_elevation">Elevasi: %.1f°</string>
|
|
||||||
<string name="map_altitude">Ketinggian: %.0f km</string>
|
|
||||||
<string name="map_distance">Jarak: %.0f km</string>
|
|
||||||
<string name="map_latitude">Lintang: %.1f°</string>
|
|
||||||
<string name="map_longitude">Bujur: %.1f°</string>
|
|
||||||
<string name="map_qth" translatable="false">QTH: %s</string>
|
|
||||||
<string name="map_phase">Fase: %.1f°</string>
|
|
||||||
<string name="map_eclipsed">Gerhana</string>
|
|
||||||
<string name="map_period">Periode: %.0f mnt</string>
|
|
||||||
<string name="map_velocity">Kecepatan: %.2f km/d</string>
|
|
||||||
<string name="map_visibility">Visibilitas: %s</string>
|
|
||||||
<string name="map_visible">Terlihat</string>
|
|
||||||
<string name="prefs_app_title" translatable="false">Look4Sat Pro v%s</string>
|
|
||||||
<string name="prefs_app_url" translatable="false">https://play.google.com/store/apps/details?id=com.rtbishop.look4sat</string>
|
|
||||||
<string name="prefs_donate_title">Donasi</string>
|
|
||||||
<string name="prefs_donate_url" translatable="false">https://ko-fi.com/rt_bishop</string>
|
|
||||||
<string name="prefs_fdroid_title" translatable="false">F-Droid</string>
|
|
||||||
<string name="prefs_fdroid_url" translatable="false">https://f-droid.org/en/packages/com.rtbishop.look4sat/</string>
|
|
||||||
<string name="prefs_github_title" translatable="false">GitHub</string>
|
|
||||||
<string name="prefs_github_url" translatable="false">https://github.com/rt-bishop/Look4Sat/</string>
|
|
||||||
<string name="prefs_license_title" translatable="false">GPLv3</string>
|
|
||||||
<string name="prefs_license_url" translatable="false">https://www.gnu.org/licenses/gpl-3.0.html</string>
|
|
||||||
<string name="prefs_privacy_title">Kebijakan Privasi</string>
|
|
||||||
<string name="prefs_privacy_url" translatable="false">https://sites.google.com/view/look4sat-privacy-policy/home</string>
|
|
||||||
<string name="prefs_updated_title">Diperbarui: %s</string>
|
|
||||||
<string name="prefs_updated_time">d MMM yyyy - HH:mm:ss</string>
|
|
||||||
<string name="prefs_loc_title">Posisi stasiun</string>
|
|
||||||
<string name="prefs_loc_gps_title">GPS</string>
|
|
||||||
<string name="prefs_loc_gps_error">Tidak dapat mengambil lokasi - periksa izin dan sinyal GPS/jaringan</string>
|
|
||||||
<string name="prefs_loc_input_title">Input</string>
|
|
||||||
<string name="prefs_loc_input_error">Lokasi yang dimasukkan tidak valid</string>
|
|
||||||
<string name="prefs_loc_qth_title">QTH</string>
|
|
||||||
<string name="prefs_loc_qth_error">Lokator QTH tidak valid</string>
|
|
||||||
<string name="prefs_loc_success">Posisi berhasil diperbarui</string>
|
|
||||||
<string name="prefs_station_title">Pengaturan posisi stasiun</string>
|
|
||||||
<string name="prefs_station_lat_text">Atur garis lintang stasiun Anda</string>
|
|
||||||
<string name="prefs_station_lon_text">Atur garis bujur stasiun Anda</string>
|
|
||||||
<string name="prefs_locator_title">Pengaturan lokator QTH</string>
|
|
||||||
<string name="prefs_locator_text">Atur posisi stasiun menggunakan lokator</string>
|
|
||||||
<string name="prefs_data_title">Data satelit</string>
|
|
||||||
<string name="prefs_data_entries">Satelit: %s</string>
|
|
||||||
<string name="prefs_data_radios">Transceiver: %s</string>
|
|
||||||
<string name="prefs_data_update">Perbarui</string>
|
|
||||||
<string name="prefs_data_import">URL Kustom</string>
|
|
||||||
<string name="prefs_data_clear">Bersihkan</string>
|
|
||||||
<string name="prefs_data_clear_success">Data berhasil dibersihkan</string>
|
|
||||||
<string name="prefs_data_update_success">Pembaruan berhasil diselesaikan</string>
|
|
||||||
<string name="prefs_data_update_failed">Pembaruan gagal - periksa jaringan Anda</string>
|
|
||||||
<string name="prefs_data_import_satellites_error">Tidak ada satelit yang diimpor. Pilih file TLE/3LE (.txt) atau OMM (.csv) yang valid.</string>
|
|
||||||
<string name="prefs_data_import_transceivers_error">Tidak ada transceiver yang diimpor. Pilih file SatNOGS (.json) yang valid.</string>
|
|
||||||
<string name="prefs_data_sources_title">Sumber data kustom</string>
|
|
||||||
<string name="prefs_data_sources_tle_switch">URL TLE Kustom</string>
|
|
||||||
<string name="prefs_data_sources_transceivers_switch">URL transceiver kustom</string>
|
|
||||||
<string name="prefs_data_sources_url_title" translatable="false">URL (HTTPS)</string>
|
|
||||||
<string name="prefs_data_reset_url">Setel ulang ke URL bawaan</string>
|
|
||||||
<string name="prefs_data_output_title">Keluaran data</string>
|
|
||||||
<string name="prefs_net_output">Jaringan</string>
|
|
||||||
<string name="prefs_bt_output">Bluetooth</string>
|
|
||||||
<string name="prefs_cat_output">CAT</string>
|
|
||||||
<string name="nav_radiocontrol">Kontrol Radio</string>
|
|
||||||
<string name="rc_settings_title">Kontrol Radio CAT</string>
|
|
||||||
<string name="rc_radio_model">Model Radio</string>
|
|
||||||
<string name="rc_tx_device_hint">Alamat BT Radio TX</string>
|
|
||||||
<string name="rc_rx_device_hint">Alamat BT Radio RX</string>
|
|
||||||
<string name="rc_tx_name_hint">Nama Radio TX</string>
|
|
||||||
<string name="rc_rx_name_hint">Nama Radio RX</string>
|
|
||||||
<string name="rc_enable_switch">Aktifkan Kontrol CAT</string>
|
|
||||||
<string name="prefs_net_title">Keluaran data jaringan</string>
|
|
||||||
<string name="prefs_net_rotator_switch">Aktifkan keluaran rotator</string>
|
|
||||||
<string name="prefs_net_rotator_address_hint">IP:Port</string>
|
|
||||||
<string name="prefs_net_rotator_format_hint">Format</string>
|
|
||||||
<string name="prefs_net_frequency_switch">Aktifkan keluaran frekuensi</string>
|
|
||||||
<string name="prefs_net_frequency_address_hint">IP:Port</string>
|
|
||||||
<string name="prefs_net_frequency_format_hint">Format</string>
|
|
||||||
<string name="prefs_bt_title">Keluaran data Bluetooth</string>
|
|
||||||
<string name="prefs_bt_rotator_switch">Aktifkan keluaran rotator</string>
|
|
||||||
<string name="prefs_bt_rotator_device_hint">Id perangkat</string>
|
|
||||||
<string name="prefs_bt_rotator_output_hint">Format data</string>
|
|
||||||
<string name="prefs_bt_frequency_switch">Aktifkan keluaran frekuensi</string>
|
|
||||||
<string name="prefs_bt_frequency_device_hint">Id perangkat</string>
|
|
||||||
<string name="prefs_bt_frequency_output_hint">Format data</string>
|
|
||||||
<string name="prefs_bt_perm_error">Periksa izin bluetooth Anda</string>
|
|
||||||
<string name="prefs_net_perm_error">Periksa izin jaringan Anda</string>
|
|
||||||
<string name="tracking_status">Melacak: %s</string>
|
|
||||||
<string name="loc_lat_text">Lintang: %s°</string>
|
|
||||||
<string name="loc_lon_text">Bujur: %s°</string>
|
|
||||||
<string name="loc_qth_text">QTH: %s</string>
|
|
||||||
<string name="connect_radio">Hubungkan</string>
|
|
||||||
<string name="disconnect_radio">Putuskan</string>
|
|
||||||
<string name="track_radio">Lacak</string>
|
|
||||||
<string name="stop_radio">Berhenti</string>
|
|
||||||
<string name="prefs_wavelog_title">Server log WaveLog</string>
|
|
||||||
<string name="prefs_wavelog_url_hint">Alamat server</string>
|
|
||||||
<string name="prefs_wavelog_key_hint">Kunci API</string>
|
|
||||||
<string name="prefs_wavelog_station_hint">ID stasiun</string>
|
|
||||||
<string name="prefs_wavelog_auto">Unggah otomatis</string>
|
|
||||||
<string name="prefs_wavelog_test">Uji koneksi</string>
|
|
||||||
<string name="prefs_wavelog_upload">Unggah sekarang</string>
|
|
||||||
<string name="wavelog_title">Log</string>
|
|
||||||
<string name="wavelog_select_transponder">Pilih transponder</string>
|
|
||||||
<string name="wavelog_call_hint">Tanda panggil</string>
|
|
||||||
<string name="wavelog_mode_hint">Mode</string>
|
|
||||||
<string name="wavelog_saved">Tersimpan ke log lokal</string>
|
|
||||||
<string name="wavelog_need_config">Konfigurasikan WaveLog di Pengaturan terlebih dahulu</string>
|
|
||||||
<string name="wavelog_empty">Belum ada entri log</string>
|
|
||||||
<string name="wavelog_grid_mismatch">Grid QTH (%1$s) tidak cocok dengan grid stasiun (%2$s). Tetap unggah?</string>
|
|
||||||
<string name="wavelog_ignore">Abaikan dan unggah</string>
|
|
||||||
<string name="wavelog_cancel">Batal</string>
|
|
||||||
<string name="wavelog_copy">Salin</string>
|
|
||||||
<string name="wavelog_delete_undo">Urungkan</string>
|
|
||||||
<string name="wavelog_col_time">Waktu</string>
|
|
||||||
<string name="wavelog_col_freq">Frek.</string>
|
|
||||||
<string name="wavelog_col_sat">Satelit</string>
|
|
||||||
<string name="wavelog_col_call">Panggilan</string>
|
|
||||||
<string name="wavelog_col_uploaded">Terkirim</string>
|
|
||||||
<string name="wavelog_ungrouped">Tidak dikelompokkan</string>
|
|
||||||
<string name="prefs_other_title">Pengaturan lainnya</string>
|
|
||||||
<string name="prefs_ui_title">Pengaturan UI</string>
|
|
||||||
<string name="prefs_ui_order_title">Urutan halaman</string>
|
|
||||||
<string name="prefs_ui_main_title">Menu utama (bilah bawah)</string>
|
|
||||||
<string name="prefs_ui_sub_title">Menu lainnya</string>
|
|
||||||
<string name="prefs_ui_move_out">Pindah ke menu lainnya</string>
|
|
||||||
<string name="prefs_ui_move_back">Kembalikan ke menu utama</string>
|
|
||||||
<string name="prefs_ui_order_locked">Tetap (tidak dapat dipindahkan)</string>
|
|
||||||
<string name="prefs_ui_order_reset">Setel ulang urutan</string>
|
|
||||||
<string name="prefs_other_switch_utc">Tampilkan waktu lintasan dalam UTC</string>
|
|
||||||
<string name="prefs_other_switch_update">Aktifkan pembaruan data otomatis</string>
|
|
||||||
<string name="prefs_other_switch_sweep">Aktifkan animasi sapuan radar</string>
|
|
||||||
<string name="prefs_other_switch_sensors">Gunakan sensor untuk memutar tampilan radar</string>
|
|
||||||
<string name="prefs_other_switch_night_mode">Aktifkan filter mode malam merah</string>
|
|
||||||
<string name="prefs_highlight_title">Sorotan elevasi</string>
|
|
||||||
<string name="prefs_highlight_low">Rendah < %1$d°</string>
|
|
||||||
<string name="prefs_highlight_mid">Sedang %1$d–%2$d°</string>
|
|
||||||
<string name="prefs_highlight_high">Tinggi > %1$d°</string>
|
|
||||||
<string name="prefs_outro_title">BG7NTA & penulis asli mengucapkan terima kasih</string>
|
|
||||||
<string name="prefs_outro_thanks" translatable="false">
|
|
||||||
* Pengguna dan kontributor Look4Sat!
|
|
||||||
\n* David A. B. Johnson (predict4java)
|
|
||||||
\n* Dave Moten (predict4java)
|
|
||||||
\n* Alexandru Csete (Gpredict)
|
|
||||||
\n* Dr T.S. Kelso (Celestrak)
|
|
||||||
\n* Libre Space Foundation (SatNOGS)
|
|
||||||
\n* xdsopl dan kontributor Robot36!
|
|
||||||
\n* BA7OPF (fitur pencocokan lintasan)
|
|
||||||
\n* BG7NTA</string>
|
|
||||||
<string name="prefs_outro_license">Aplikasi ini hadir tanpa jaminan</string>
|
|
||||||
</resources>
|
|
||||||
@@ -12,6 +12,36 @@
|
|||||||
<string name="nav_cw">Dekoder CW</string>
|
<string name="nav_cw">Dekoder CW</string>
|
||||||
<string name="nav_map">Peta</string>
|
<string name="nav_map">Peta</string>
|
||||||
<string name="nav_prefs">Pengaturan</string>
|
<string name="nav_prefs">Pengaturan</string>
|
||||||
|
<string name="prefs_aprs_title">Pelaporan APRS</string>
|
||||||
|
<string name="prefs_aprs_enable">Aktifkan pelaporan APRS</string>
|
||||||
|
<string name="prefs_aprs_summary">Server: %1$s:%2$d Panggilan: %3$s</string>
|
||||||
|
<string name="prefs_aprs_report_now">Lapor Sekarang</string>
|
||||||
|
<string name="prefs_aprs_settings_title">Pengaturan APRS</string>
|
||||||
|
<string name="prefs_aprs_server">Server</string>
|
||||||
|
<string name="prefs_aprs_port">Port</string>
|
||||||
|
<string name="prefs_aprs_callsign">Tanda panggilan</string>
|
||||||
|
<string name="prefs_aprs_ssid">SSID</string>
|
||||||
|
<string name="prefs_aprs_passcode">Kode sandi</string>
|
||||||
|
<string name="prefs_aprs_passcode_hint">Kosong = otomatis dari panggilan</string>
|
||||||
|
<string name="prefs_aprs_interval">Interval (menit)</string>
|
||||||
|
<string name="prefs_aprs_status">Teks status</string>
|
||||||
|
<string name="prefs_aprs_symbol_table">Tabel simbol</string>
|
||||||
|
<string name="prefs_aprs_symbol_code">Kode simbol</string>
|
||||||
|
<string name="prefs_aprs_save">Simpan</string>
|
||||||
|
<string name="prefs_aprs_cancel">Batal</string>
|
||||||
|
<string name="aprs_notif_channel">Layanan APRS</string>
|
||||||
|
<string name="aprs_notif_title">APRS: %1$s</string>
|
||||||
|
<string name="aprs_notif_running">Pelaporan berjalan</string>
|
||||||
|
<string name="aprs_notif_connected">Terhubung ke APRS-IS</string>
|
||||||
|
<string name="aprs_notif_error">Kesalahan koneksi</string>
|
||||||
|
<string name="aprs_notif_stop">Berhenti</string>
|
||||||
|
<string name="prefs_aprs_calc_passcode">Hitung kode sandi</string>
|
||||||
|
<string name="aprs_toast_ok">APRS: laporan terkirim</string>
|
||||||
|
<string name="aprs_toast_fail">APRS: laporan gagal - %1$s</string>
|
||||||
|
<string name="aprs_toast_not_configured">APRS belum dikonfigurasi - isi panggilan dulu</string>
|
||||||
|
<string name="prefs_aprs_last_report">Laporan terakhir: %1$s %2$s</string>
|
||||||
|
<string name="prefs_aprs_last_ok">OK</string>
|
||||||
|
<string name="prefs_aprs_last_fail">gagal - %1$s</string>
|
||||||
<string name="nav_more">Lainnya</string>
|
<string name="nav_more">Lainnya</string>
|
||||||
<string name="nav_log">Log</string>
|
<string name="nav_log">Log</string>
|
||||||
<string name="nav_amsat">AMSAT</string>
|
<string name="nav_amsat">AMSAT</string>
|
||||||
@@ -65,7 +95,8 @@
|
|||||||
* GPS kini hanya menampilkan sukses saat posisi benar-benar didapat (timeout 15 dtk, cek izin)
|
* GPS kini hanya menampilkan sukses saat posisi benar-benar didapat (timeout 15 dtk, cek izin)
|
||||||
* Pesan kegagalan jelas saat database satelit gagal diunduh
|
* Pesan kegagalan jelas saat database satelit gagal diunduh
|
||||||
* Frekuensi log mengikuti panel Doppler secara real-time; visual hapus geser dipulihkan
|
* Frekuensi log mengikuti panel Doppler secara real-time; visual hapus geser dipulihkan
|
||||||
* Perbaikan bahasa Indonesia (values-id)</string>
|
* Perbaikan bahasa Indonesia (values-id)
|
||||||
|
* BARU: Pelaporan APRS (jaringan APRS-IS, layanan latar depan dengan notifikasi, interval dapat diatur)</string>
|
||||||
<string name="radar_back">Kembali</string>
|
<string name="radar_back">Kembali</string>
|
||||||
<string name="radar_notify">Beri tahu</string>
|
<string name="radar_notify">Beri tahu</string>
|
||||||
<string name="radar_az_text">Azimuth</string>
|
<string name="radar_az_text">Azimuth</string>
|
||||||
|
|||||||
@@ -13,6 +13,36 @@
|
|||||||
<string name="nav_cw">CW Dekoder</string>
|
<string name="nav_cw">CW Dekoder</string>
|
||||||
<string name="nav_map">Harita</string>
|
<string name="nav_map">Harita</string>
|
||||||
<string name="nav_prefs">Ayarlar</string>
|
<string name="nav_prefs">Ayarlar</string>
|
||||||
|
<string name="prefs_aprs_title">APRS Raporlama</string>
|
||||||
|
<string name="prefs_aprs_enable">APRS raporlamayı etkinleştir</string>
|
||||||
|
<string name="prefs_aprs_summary">Sunucu: %1$s:%2$d Çağrı: %3$s</string>
|
||||||
|
<string name="prefs_aprs_report_now">Şimdi Raporla</string>
|
||||||
|
<string name="prefs_aprs_settings_title">APRS Ayarları</string>
|
||||||
|
<string name="prefs_aprs_server">Sunucu</string>
|
||||||
|
<string name="prefs_aprs_port">Port</string>
|
||||||
|
<string name="prefs_aprs_callsign">Çağrı işareti</string>
|
||||||
|
<string name="prefs_aprs_ssid">SSID</string>
|
||||||
|
<string name="prefs_aprs_passcode">Parola</string>
|
||||||
|
<string name="prefs_aprs_passcode_hint">Parola boş = çağrıdan otomatik hesaplanır</string>
|
||||||
|
<string name="prefs_aprs_interval">Aralık (dakika)</string>
|
||||||
|
<string name="prefs_aprs_status">Durum metni</string>
|
||||||
|
<string name="prefs_aprs_symbol_table">Sembol tablosu</string>
|
||||||
|
<string name="prefs_aprs_symbol_code">Sembol kodu</string>
|
||||||
|
<string name="prefs_aprs_save">Kaydet</string>
|
||||||
|
<string name="prefs_aprs_cancel">İptal</string>
|
||||||
|
<string name="aprs_notif_channel">APRS hizmeti</string>
|
||||||
|
<string name="aprs_notif_title">APRS: %1$s</string>
|
||||||
|
<string name="aprs_notif_running">Raporlama çalışıyor</string>
|
||||||
|
<string name="aprs_notif_connected">APRS-IS bağlı</string>
|
||||||
|
<string name="aprs_notif_error">Bağlantı hatası</string>
|
||||||
|
<string name="aprs_notif_stop">Durdur</string>
|
||||||
|
<string name="prefs_aprs_calc_passcode">Parolayı hesapla</string>
|
||||||
|
<string name="aprs_toast_ok">APRS: rapor gönderildi</string>
|
||||||
|
<string name="aprs_toast_fail">APRS: rapor başarısız - %1$s</string>
|
||||||
|
<string name="aprs_toast_not_configured">APRS yapılandırılmadı - önce çağrı girin</string>
|
||||||
|
<string name="prefs_aprs_last_report">Son rapor: %1$s %2$s</string>
|
||||||
|
<string name="prefs_aprs_last_ok">OK</string>
|
||||||
|
<string name="prefs_aprs_last_fail">başarısız - %1$s</string>
|
||||||
<string name="nav_more">Daha Fazla</string>
|
<string name="nav_more">Daha Fazla</string>
|
||||||
<string name="nav_log">Günlük</string>
|
<string name="nav_log">Günlük</string>
|
||||||
<string name="nav_amsat">AMSAT</string>
|
<string name="nav_amsat">AMSAT</string>
|
||||||
@@ -70,7 +100,8 @@
|
|||||||
* GPS artık yalnızca gerçek konum alındığında başarı gösterir (15 sn zaman aşımı, izin kontrolü)
|
* GPS artık yalnızca gerçek konum alındığında başarı gösterir (15 sn zaman aşımı, izin kontrolü)
|
||||||
* Uydu veritabanı indirilemediğinde net hata mesajı gösterilir
|
* Uydu veritabanı indirilemediğinde net hata mesajı gösterilir
|
||||||
* Günlük frekansları Doppler panelini gerçek zamanlı takip eder; kaydırarak silme görselleri düzeltildi
|
* Günlük frekansları Doppler panelini gerçek zamanlı takip eder; kaydırarak silme görselleri düzeltildi
|
||||||
* Endonezya dili düzeltmesi (values-id)</string>
|
* Endonezya dili düzeltmesi (values-id)
|
||||||
|
* YENİ: APRS raporlama (APRS-IS ağı, bildirimli ön plan hizmeti, ayarlanabilir aralık)</string>
|
||||||
|
|
||||||
<!-- Radar screen -->
|
<!-- Radar screen -->
|
||||||
<string name="radar_back">Geri</string>
|
<string name="radar_back">Geri</string>
|
||||||
|
|||||||
@@ -14,6 +14,36 @@
|
|||||||
<string name="nav_cw">CW解码</string>
|
<string name="nav_cw">CW解码</string>
|
||||||
<string name="nav_map">地图</string>
|
<string name="nav_map">地图</string>
|
||||||
<string name="nav_prefs">设置</string>
|
<string name="nav_prefs">设置</string>
|
||||||
|
<string name="prefs_aprs_title">APRS 上报</string>
|
||||||
|
<string name="prefs_aprs_enable">启用 APRS 上报</string>
|
||||||
|
<string name="prefs_aprs_summary">服务器: %1$s:%2$d 呼号: %3$s</string>
|
||||||
|
<string name="prefs_aprs_report_now">立即上报</string>
|
||||||
|
<string name="prefs_aprs_settings_title">APRS 设置</string>
|
||||||
|
<string name="prefs_aprs_server">服务器</string>
|
||||||
|
<string name="prefs_aprs_port">端口</string>
|
||||||
|
<string name="prefs_aprs_callsign">呼号</string>
|
||||||
|
<string name="prefs_aprs_ssid">SSID</string>
|
||||||
|
<string name="prefs_aprs_passcode">验证码</string>
|
||||||
|
<string name="prefs_aprs_passcode_hint">验证码留空 = 按呼号自动计算</string>
|
||||||
|
<string name="prefs_aprs_interval">周期(分钟)</string>
|
||||||
|
<string name="prefs_aprs_status">状态文本</string>
|
||||||
|
<string name="prefs_aprs_symbol_table">符号表</string>
|
||||||
|
<string name="prefs_aprs_symbol_code">符号码</string>
|
||||||
|
<string name="prefs_aprs_save">保存</string>
|
||||||
|
<string name="prefs_aprs_cancel">取消</string>
|
||||||
|
<string name="aprs_notif_channel">APRS 服务</string>
|
||||||
|
<string name="aprs_notif_title">APRS: %1$s</string>
|
||||||
|
<string name="aprs_notif_running">上报运行中</string>
|
||||||
|
<string name="aprs_notif_connected">已连接 APRS-IS</string>
|
||||||
|
<string name="aprs_notif_error">连接错误</string>
|
||||||
|
<string name="aprs_notif_stop">停止</string>
|
||||||
|
<string name="prefs_aprs_calc_passcode">计算验证码</string>
|
||||||
|
<string name="aprs_toast_ok">APRS: 上报成功</string>
|
||||||
|
<string name="aprs_toast_fail">APRS: 上报失败 - %1$s</string>
|
||||||
|
<string name="aprs_toast_not_configured">APRS 未配置,请先填呼号</string>
|
||||||
|
<string name="prefs_aprs_last_report">上次上报: %1$s %2$s</string>
|
||||||
|
<string name="prefs_aprs_last_ok">成功</string>
|
||||||
|
<string name="prefs_aprs_last_fail">失败 - %1$s</string>
|
||||||
<string name="nav_more">更多</string>
|
<string name="nav_more">更多</string>
|
||||||
<string name="nav_log">日志</string>
|
<string name="nav_log">日志</string>
|
||||||
<string name="nav_amsat">AMSAT</string>
|
<string name="nav_amsat">AMSAT</string>
|
||||||
@@ -60,7 +90,8 @@
|
|||||||
* GPS 真正定位成功才会提示成功(15 秒超时, 权限检查)
|
* GPS 真正定位成功才会提示成功(15 秒超时, 权限检查)
|
||||||
* 卫星数据库下载失败时有明确失败提示
|
* 卫星数据库下载失败时有明确失败提示
|
||||||
* 日志频率实时跟随多普勒面板; 左滑删除视觉恢复
|
* 日志频率实时跟随多普勒面板; 左滑删除视觉恢复
|
||||||
* 印尼语识别修复(values-id)</string>
|
* 印尼语识别修复(values-id)
|
||||||
|
* 新增: APRS 网络上报(APRS-IS,前台服务+通知保活,周期可调)</string>
|
||||||
|
|
||||||
<!-- Radar screen -->
|
<!-- Radar screen -->
|
||||||
<string name="radar_back">后退</string>
|
<string name="radar_back">后退</string>
|
||||||
@@ -115,7 +146,7 @@
|
|||||||
<string name="prefs_updated_title">更新:%s</string>
|
<string name="prefs_updated_title">更新:%s</string>
|
||||||
<string name="prefs_updated_time">yyyy MMM d - HH:mm:ss</string>
|
<string name="prefs_updated_time">yyyy MMM d - HH:mm:ss</string>
|
||||||
|
|
||||||
<string name="prefs_loc_title">站位</string>
|
<string name="prefs_loc_title">站点位置</string>
|
||||||
<string name="prefs_loc_gps_title">GPS 定位</string>
|
<string name="prefs_loc_gps_title">GPS 定位</string>
|
||||||
<string name="prefs_loc_gps_error">无法获取位置,请检查定位权限和GPS/网络信号</string>
|
<string name="prefs_loc_gps_error">无法获取位置,请检查定位权限和GPS/网络信号</string>
|
||||||
<string name="prefs_loc_input_title">输入位置</string>
|
<string name="prefs_loc_input_title">输入位置</string>
|
||||||
@@ -123,11 +154,11 @@
|
|||||||
<string name="prefs_loc_qth_title">QTH 网格</string>
|
<string name="prefs_loc_qth_title">QTH 网格</string>
|
||||||
<string name="prefs_loc_qth_error">无效的 QTH 网格</string>
|
<string name="prefs_loc_qth_error">无效的 QTH 网格</string>
|
||||||
<string name="prefs_loc_success">位置更新成功</string>
|
<string name="prefs_loc_success">位置更新成功</string>
|
||||||
<string name="prefs_station_title">站位设置</string>
|
<string name="prefs_station_title">站点位置设置</string>
|
||||||
<string name="prefs_station_lat_text">输入站位的纬度</string>
|
<string name="prefs_station_lat_text">输入站点的纬度</string>
|
||||||
<string name="prefs_station_lon_text">输入站位的经度</string>
|
<string name="prefs_station_lon_text">输入站点的经度</string>
|
||||||
<string name="prefs_locator_title">QTH 网格</string>
|
<string name="prefs_locator_title">QTH 网格</string>
|
||||||
<string name="prefs_locator_text">使用梅登黑德网格设置站位</string>
|
<string name="prefs_locator_text">使用梅登黑德网格设置站点位置</string>
|
||||||
|
|
||||||
<string name="prefs_data_title">卫星数据更新</string>
|
<string name="prefs_data_title">卫星数据更新</string>
|
||||||
<string name="prefs_data_entries">卫星:%s</string>
|
<string name="prefs_data_entries">卫星:%s</string>
|
||||||
|
|||||||
@@ -15,6 +15,36 @@
|
|||||||
<string name="nav_cw">CW Decoder</string>
|
<string name="nav_cw">CW Decoder</string>
|
||||||
<string name="nav_map">Map</string>
|
<string name="nav_map">Map</string>
|
||||||
<string name="nav_prefs">Settings</string>
|
<string name="nav_prefs">Settings</string>
|
||||||
|
<string name="prefs_aprs_title">APRS Reporting</string>
|
||||||
|
<string name="prefs_aprs_enable">Enable APRS reporting</string>
|
||||||
|
<string name="prefs_aprs_summary">Server: %1$s:%2$d Callsign: %3$s</string>
|
||||||
|
<string name="prefs_aprs_report_now">Report Now</string>
|
||||||
|
<string name="prefs_aprs_settings_title">APRS Settings</string>
|
||||||
|
<string name="prefs_aprs_server">Server</string>
|
||||||
|
<string name="prefs_aprs_port">Port</string>
|
||||||
|
<string name="prefs_aprs_callsign">Callsign</string>
|
||||||
|
<string name="prefs_aprs_ssid">SSID</string>
|
||||||
|
<string name="prefs_aprs_passcode">Passcode</string>
|
||||||
|
<string name="prefs_aprs_passcode_hint">Passcode empty = auto-computed from callsign</string>
|
||||||
|
<string name="prefs_aprs_interval">Interval (min)</string>
|
||||||
|
<string name="prefs_aprs_status">Status text</string>
|
||||||
|
<string name="prefs_aprs_symbol_table">Symbol table</string>
|
||||||
|
<string name="prefs_aprs_symbol_code">Symbol code</string>
|
||||||
|
<string name="prefs_aprs_save">Save</string>
|
||||||
|
<string name="prefs_aprs_cancel">Cancel</string>
|
||||||
|
<string name="aprs_notif_channel">APRS service</string>
|
||||||
|
<string name="aprs_notif_title">APRS: %1$s</string>
|
||||||
|
<string name="aprs_notif_running">Reporting running</string>
|
||||||
|
<string name="aprs_notif_connected">Connected to APRS-IS</string>
|
||||||
|
<string name="aprs_notif_error">Connection error</string>
|
||||||
|
<string name="aprs_notif_stop">Stop</string>
|
||||||
|
<string name="prefs_aprs_calc_passcode">Compute passcode</string>
|
||||||
|
<string name="aprs_toast_ok">APRS: report sent OK</string>
|
||||||
|
<string name="aprs_toast_fail">APRS: report failed - %1$s</string>
|
||||||
|
<string name="aprs_toast_not_configured">APRS not configured - set callsign first</string>
|
||||||
|
<string name="prefs_aprs_last_report">Last report: %1$s %2$s</string>
|
||||||
|
<string name="prefs_aprs_last_ok">OK</string>
|
||||||
|
<string name="prefs_aprs_last_fail">failed - %1$s</string>
|
||||||
<string name="nav_more">More</string>
|
<string name="nav_more">More</string>
|
||||||
<string name="nav_log">Log</string>
|
<string name="nav_log">Log</string>
|
||||||
<string name="nav_amsat">AMSAT</string>
|
<string name="nav_amsat">AMSAT</string>
|
||||||
@@ -72,7 +102,8 @@
|
|||||||
* GPS now only reports success when a real fix is acquired (15s timeout, permission check)
|
* GPS now only reports success when a real fix is acquired (15s timeout, permission check)
|
||||||
* Data update shows a clear failure message when the satellite database cannot be downloaded
|
* Data update shows a clear failure message when the satellite database cannot be downloaded
|
||||||
* Log frequencies follow the Doppler panel in real time; swipe-to-delete visuals restored
|
* Log frequencies follow the Doppler panel in real time; swipe-to-delete visuals restored
|
||||||
* Indonesian locale fix (values-id)</string>
|
* Indonesian locale fix (values-id)
|
||||||
|
* NEW: APRS reporting (APRS-IS network, foreground service with notification, configurable interval)</string>
|
||||||
|
|
||||||
<!-- Radar screen -->
|
<!-- Radar screen -->
|
||||||
<string name="radar_back">Back</string>
|
<string name="radar_back">Back</string>
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="purple_500">#ff6200ee</color>
|
|
||||||
<integer name="default_font_size">18</integer>
|
|
||||||
<string name="idle">Diam</string>
|
|
||||||
<string name="waterfall_display">Tampilan waterfall</string>
|
|
||||||
<string name="frequency_scale">Skala frekuensi</string>
|
|
||||||
<string name="decoded_text">Teks terdekode</string>
|
|
||||||
<string name="clear">Bersihkan</string>
|
|
||||||
<string name="pause">Jeda Dekode</string>
|
|
||||||
<string name="save_text">Simpan Teks</string>
|
|
||||||
<string name="record_signals">Rekam Sinyal</string>
|
|
||||||
<string name="settings">Pengaturan</string>
|
|
||||||
<string name="help">Bantuan Daring</string>
|
|
||||||
<string name="rate">Nilai Aplikasi Ini</string>
|
|
||||||
<string name="premium">Dapatkan Versi Premium</string>
|
|
||||||
<string name="tap_back_again_to_close">Ketuk Kembali lagi untuk menutup aplikasi</string>
|
|
||||||
<string name="message_type">Tipe Pesan</string>
|
|
||||||
<string name="cw_mic_permission">Izin mikrofon diperlukan untuk dekode CW</string>
|
|
||||||
<string name="cw_grant_permission">Beri izin</string>
|
|
||||||
<string name="cw_open_settings">Buka pengaturan aplikasi</string>
|
|
||||||
</resources>
|
|
||||||
@@ -5,3 +5,7 @@ plugins {
|
|||||||
android {
|
android {
|
||||||
namespace = "com.rtbishop.look4sat.feature.settings"
|
namespace = "com.rtbishop.look4sat.feature.settings"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":core:data"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
package com.rtbishop.look4sat.feature.settings
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.ElevatedCard
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.rtbishop.look4sat.core.data.aprs.AprsConfig
|
||||||
|
import com.rtbishop.look4sat.core.domain.aprs.AprsPacket
|
||||||
|
import com.rtbishop.look4sat.core.data.aprs.AprsStore
|
||||||
|
import com.rtbishop.look4sat.core.presentation.CardButton
|
||||||
|
import com.rtbishop.look4sat.core.presentation.R
|
||||||
|
|
||||||
|
/** APRS 卡片:卫星数据容器与数据输出容器之间;设置键弹窗收纳,不挤 */
|
||||||
|
@Composable
|
||||||
|
fun AprsCard() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var config by remember { mutableStateOf(AprsStore.loadConfig(context)) }
|
||||||
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
|
var lastReport by remember { mutableStateOf(AprsStore.loadLastReport(context)) }
|
||||||
|
var reportPing by remember { mutableStateOf(0) }
|
||||||
|
val notifPermissionLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { }
|
||||||
|
fun requestNotifPermissionIfNeeded() {
|
||||||
|
if (Build.VERSION.SDK_INT >= 33 &&
|
||||||
|
context.checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) !=
|
||||||
|
PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
notifPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
// 标题行:标题 + 设置键(弹窗,省空间)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.prefs_aprs_title),
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
IconButton(onClick = { showDialog = true }) {
|
||||||
|
Text(
|
||||||
|
text = "\u2699", // ⚙
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
fontSize = 18.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 启用开关
|
||||||
|
AprsSwitchRow(R.string.prefs_aprs_enable, config.enabled) { enabled ->
|
||||||
|
config = config.copy(enabled = enabled)
|
||||||
|
AprsStore.saveConfig(context, config)
|
||||||
|
if (enabled) requestNotifPermissionIfNeeded()
|
||||||
|
val intent = Intent()
|
||||||
|
.setClassName(context.packageName, AprsStore.SERVICE_CLASS)
|
||||||
|
.setAction(if (enabled) AprsStore.ACTION_START else AprsStore.ACTION_STOP)
|
||||||
|
if (enabled) context.startForegroundService(intent) else context.startService(intent)
|
||||||
|
}
|
||||||
|
// 简洁信息行(不挤)
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
id = R.string.prefs_aprs_summary,
|
||||||
|
config.server, config.port,
|
||||||
|
if (config.callsign.isBlank()) "-" else config.callsign
|
||||||
|
),
|
||||||
|
fontSize = 13.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
// 上次上报状态行
|
||||||
|
val lr = lastReport
|
||||||
|
if (lr.time > 0L) {
|
||||||
|
val timeStr = java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault())
|
||||||
|
.format(java.util.Date(lr.time))
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
id = R.string.prefs_aprs_last_report,
|
||||||
|
timeStr,
|
||||||
|
if (lr.ok) stringResource(R.string.prefs_aprs_last_ok)
|
||||||
|
else stringResource(R.string.prefs_aprs_last_fail, lr.detail)
|
||||||
|
),
|
||||||
|
fontSize = 13.sp,
|
||||||
|
color = if (lr.ok) MaterialTheme.colorScheme.primary
|
||||||
|
else MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// 手动上报按钮
|
||||||
|
CardButton(
|
||||||
|
onClick = {
|
||||||
|
val intent = Intent()
|
||||||
|
.setClassName(context.packageName, AprsStore.SERVICE_CLASS)
|
||||||
|
.setAction(AprsStore.ACTION_REPORT_NOW)
|
||||||
|
context.startForegroundService(intent)
|
||||||
|
reportPing++
|
||||||
|
},
|
||||||
|
text = stringResource(id = R.string.prefs_aprs_report_now),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 上报后延迟刷新状态行(等结果落盘)
|
||||||
|
androidx.compose.runtime.LaunchedEffect(reportPing) {
|
||||||
|
if (reportPing > 0) {
|
||||||
|
kotlinx.coroutines.delay(6000)
|
||||||
|
lastReport = AprsStore.loadLastReport(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (showDialog) {
|
||||||
|
AprsSettingsDialog(
|
||||||
|
config = config,
|
||||||
|
onDismiss = { showDialog = false },
|
||||||
|
onSave = { newConfig ->
|
||||||
|
config = newConfig
|
||||||
|
AprsStore.saveConfig(context, newConfig)
|
||||||
|
// 已启用时应用新配置(重启服务)
|
||||||
|
if (newConfig.enabled) {
|
||||||
|
context.startService(
|
||||||
|
Intent().setClassName(context.packageName, AprsStore.SERVICE_CLASS)
|
||||||
|
.setAction(AprsStore.ACTION_STOP)
|
||||||
|
)
|
||||||
|
context.startForegroundService(
|
||||||
|
Intent().setClassName(context.packageName, AprsStore.SERVICE_CLASS)
|
||||||
|
.setAction(AprsStore.ACTION_START)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
showDialog = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** APRS 设置弹窗(空间充裕,配置项齐全) */
|
||||||
|
@Composable
|
||||||
|
private fun AprsSettingsDialog(
|
||||||
|
config: AprsConfig,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onSave: (AprsConfig) -> Unit
|
||||||
|
) {
|
||||||
|
var server by remember { mutableStateOf(config.server) }
|
||||||
|
var port by remember { mutableStateOf(config.port.toString()) }
|
||||||
|
var callsign by remember { mutableStateOf(config.callsign) }
|
||||||
|
var ssid by remember { mutableStateOf(config.ssid) }
|
||||||
|
var passcode by remember { mutableStateOf(config.passcode) }
|
||||||
|
var interval by remember { mutableStateOf(config.intervalMin.toString()) }
|
||||||
|
var status by remember { mutableStateOf(config.statusText) }
|
||||||
|
var symbolTable by remember { mutableStateOf(config.symbolTable) }
|
||||||
|
var symbolCode by remember { mutableStateOf(config.symbolCode) }
|
||||||
|
val textStyle = LocalTextStyle.current.copy(fontSize = 14.sp)
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text(stringResource(id = R.string.prefs_aprs_settings_title)) },
|
||||||
|
text = {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = server,
|
||||||
|
onValueChange = { server = it },
|
||||||
|
label = { Text(stringResource(id = R.string.prefs_aprs_server)) },
|
||||||
|
singleLine = true,
|
||||||
|
textStyle = textStyle
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = port,
|
||||||
|
onValueChange = { port = it },
|
||||||
|
label = { Text(stringResource(id = R.string.prefs_aprs_port)) },
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
textStyle = textStyle
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = callsign,
|
||||||
|
onValueChange = { callsign = it },
|
||||||
|
label = { Text(stringResource(id = R.string.prefs_aprs_callsign)) },
|
||||||
|
singleLine = true,
|
||||||
|
textStyle = textStyle
|
||||||
|
)
|
||||||
|
// 计算验证码按钮(用户要求:让用户算出结果)
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
val call = callsign.trim().uppercase()
|
||||||
|
if (call.isNotBlank()) {
|
||||||
|
passcode = AprsPacket.passcode(call).toString()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = callsign.trim().isNotBlank()
|
||||||
|
) { Text(stringResource(id = R.string.prefs_aprs_calc_passcode)) }
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = ssid,
|
||||||
|
onValueChange = { ssid = it },
|
||||||
|
label = { Text(stringResource(id = R.string.prefs_aprs_ssid)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
textStyle = textStyle
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = passcode,
|
||||||
|
onValueChange = { passcode = it },
|
||||||
|
label = { Text(stringResource(id = R.string.prefs_aprs_passcode)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
textStyle = textStyle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = interval,
|
||||||
|
onValueChange = { interval = it },
|
||||||
|
label = { Text(stringResource(id = R.string.prefs_aprs_interval)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
textStyle = textStyle
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = status,
|
||||||
|
onValueChange = { status = it },
|
||||||
|
label = { Text(stringResource(id = R.string.prefs_aprs_status)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
textStyle = textStyle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = symbolTable,
|
||||||
|
onValueChange = { symbolTable = it },
|
||||||
|
label = { Text(stringResource(id = R.string.prefs_aprs_symbol_table)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
textStyle = textStyle
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = symbolCode,
|
||||||
|
onValueChange = { symbolCode = it },
|
||||||
|
label = { Text(stringResource(id = R.string.prefs_aprs_symbol_code)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
textStyle = textStyle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.prefs_aprs_passcode_hint),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
onSave(
|
||||||
|
config.copy(
|
||||||
|
server = server.ifBlank { "euro.aprs2.net" },
|
||||||
|
port = port.toIntOrNull() ?: 14580,
|
||||||
|
callsign = callsign.trim().uppercase(),
|
||||||
|
ssid = ssid.trim(),
|
||||||
|
passcode = passcode.trim(),
|
||||||
|
intervalMin = interval.toIntOrNull() ?: 5,
|
||||||
|
statusText = status,
|
||||||
|
symbolTable = symbolTable.ifBlank { "/" },
|
||||||
|
symbolCode = symbolCode.ifBlank { ">" }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}) { Text(stringResource(id = R.string.prefs_aprs_save)) }
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) { Text(stringResource(id = R.string.prefs_aprs_cancel)) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 本文件内开关行(与 SettingsScreen 的私有 SwitchRow 一致) */
|
||||||
|
@Composable
|
||||||
|
private fun AprsSwitchRow(labelResId: Int, checked: Boolean, onCheckedChange: ((Boolean) -> Unit)?) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(id = labelResId))
|
||||||
|
Switch(checked = checked, onCheckedChange = onCheckedChange)
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
@@ -381,6 +381,9 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
|
|||||||
showDataSourcesDialog = { dialogs.dataSources = true }
|
showDataSourcesDialog = { dialogs.dataSources = true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||||
|
AprsCard()
|
||||||
|
}
|
||||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||||
OutputCard(
|
OutputCard(
|
||||||
onNetworkClick = permissions.launchNetwork,
|
onNetworkClick = permissions.launchNetwork,
|
||||||
|
|||||||
+5
-5
@@ -209,7 +209,7 @@ private fun HeaderRow(statuses: List<SatStatus>) {
|
|||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
modifier = Modifier.weight(1.4f).padding(start = 4.dp)
|
modifier = Modifier.weight(2f).padding(start = 4.dp)
|
||||||
)
|
)
|
||||||
dates.forEach { date ->
|
dates.forEach { date ->
|
||||||
Text(
|
Text(
|
||||||
@@ -218,7 +218,7 @@ private fun HeaderRow(statuses: List<SatStatus>) {
|
|||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(0.8f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,13 +238,13 @@ private fun StatusRow(status: SatStatus, onClickDay: (SatDay) -> Unit) {
|
|||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.weight(1.4f).padding(start = 4.dp)
|
modifier = Modifier.weight(2f).padding(start = 4.dp)
|
||||||
)
|
)
|
||||||
status.days.forEach { day ->
|
status.days.forEach { day ->
|
||||||
val slot = day.slots.firstOrNull { it.statusColor != NoReportGray.value.toInt().toLong() } ?: day.slots.first()
|
val slot = day.slots.firstOrNull { it.statusColor != NoReportGray.value.toInt().toLong() } ?: day.slots.first()
|
||||||
DayCell(
|
DayCell(
|
||||||
slot = slot,
|
slot = slot,
|
||||||
modifier = Modifier.weight(1f).padding(horizontal = 1.dp),
|
modifier = Modifier.weight(0.8f).padding(horizontal = 1.dp),
|
||||||
onClick = { onClickDay(day) }
|
onClick = { onClickDay(day) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -257,7 +257,7 @@ private fun DayCell(slot: SatSlot, modifier: Modifier, onClick: () -> Unit) {
|
|||||||
val color = Color(slot.statusColor)
|
val color = Color(slot.statusColor)
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.height(26.dp)
|
.height(24.dp)
|
||||||
.clip(RoundedCornerShape(4.dp))
|
.clip(RoundedCornerShape(4.dp))
|
||||||
.background(color)
|
.background(color)
|
||||||
.clickable(onClick = onClick),
|
.clickable(onClick = onClick),
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
[versions]
|
[versions]
|
||||||
#noinspection UnusedVersionCatalogEntry
|
#noinspection UnusedVersionCatalogEntry
|
||||||
appVersionCode = "453"
|
appVersionCode = "454"
|
||||||
#noinspection UnusedVersionCatalogEntry
|
#noinspection UnusedVersionCatalogEntry
|
||||||
appVersionName = "4.5.3"
|
appVersionName = "4.5.4"
|
||||||
#noinspection UnusedVersionCatalogEntry
|
#noinspection UnusedVersionCatalogEntry
|
||||||
compileSdk = "37"
|
compileSdk = "37"
|
||||||
#noinspection UnusedVersionCatalogEntry
|
#noinspection UnusedVersionCatalogEntry
|
||||||
|
|||||||
Reference in new issue
Block a user