diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index ca22965..b4d1da0 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -11,8 +11,8 @@ android {
applicationId = "ru.ua1zbe.svxremote"
minSdk = 26
targetSdk = 34
- versionCode = 2
- versionName = "1.0.0"
+ versionCode = 4
+ versionName = "1.0.2"
}
buildTypes {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c8da151..817c3de 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,6 +6,7 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt b/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt
index 469dfac..cf70053 100644
--- a/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt
+++ b/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt
@@ -2,27 +2,32 @@ package ru.ua1zbe.svxremote
import android.Manifest
import android.app.Activity
+import android.app.AlertDialog
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
+import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.IBinder
import android.os.Looper
+import android.provider.Settings
import android.util.Log
+import android.view.View
import android.view.WindowManager
-import android.widget.Button
import android.widget.ImageButton
+import android.widget.LinearLayout
+import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
+import java.io.File
import ru.ua1zbe.svxremote.prefs.ConnectionSettings
import ru.ua1zbe.svxremote.prefs.Prefs
import ru.ua1zbe.svxremote.proto.MsgParser
import ru.ua1zbe.svxremote.proto.ReflectorClient
-import ru.ua1zbe.svxremote.ui.LevelMeter
import ru.ua1zbe.svxremote.ui.PttButton
private const val REQ_MIC = 1001
@@ -36,13 +41,19 @@ class MainActivity : Activity(), SvxService.Listener {
private lateinit var statusChip: TextView
private lateinit var lblSubtitle: TextView
+ private lateinit var lblCallsign: TextView
+ private lateinit var lblTg: TextView
private lateinit var lblTalkerTitle: TextView
private lateinit var lblTalker: TextView
private lateinit var lblPttHint: TextView
private lateinit var lblTxState: TextView
- private lateinit var btnConnect: Button
+ private lateinit var lblConnectHint: TextView
+ private lateinit var connectZone: View
+ private lateinit var rxBarFill: View
+ private lateinit var rxDb: TextView
+ private lateinit var micBarFill: View
+ private lateinit var micDb: TextView
private lateinit var pttButton: PttButton
- private lateinit var levelMeter: LevelMeter
private lateinit var btnSettings: ImageButton
private val mainHandler = Handler(Looper.getMainLooper())
@@ -62,10 +73,14 @@ class MainActivity : Activity(), SvxService.Listener {
}
}
+ private var pendingUpdate: UpdateChecker.UpdateInfo? = null
+ private var updateDialog: AlertDialog? = null
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+ checkForUpdates()
statusChip = findViewById(R.id.status_chip)
lblSubtitle = findViewById(R.id.lbl_subtitle)
@@ -73,16 +88,23 @@ class MainActivity : Activity(), SvxService.Listener {
lblTalker = findViewById(R.id.lbl_talker)
lblPttHint = findViewById(R.id.lbl_ptt_hint)
lblTxState = findViewById(R.id.lbl_tx_state)
- btnConnect = findViewById(R.id.btn_connect)
+ lblConnectHint = findViewById(R.id.lbl_connect_hint)
+ connectZone = findViewById(R.id.connect_zone)
+ lblCallsign = findViewById(R.id.lbl_callsign)
+ lblTg = findViewById(R.id.lbl_tg)
+ rxBarFill = findViewById(R.id.rx_bar_fill)
+ rxDb = findViewById(R.id.rx_db)
+ micBarFill = findViewById(R.id.mic_bar_fill)
+ micDb = findViewById(R.id.mic_db)
pttButton = findViewById(R.id.ptt_button)
- levelMeter = findViewById(R.id.level_meter)
btnSettings = findViewById(R.id.btn_settings)
+ pttButton.enabled2 = false
btnSettings.setOnClickListener {
startActivity(Intent(this, SettingsActivity::class.java))
}
- btnConnect.setOnClickListener {
+ connectZone.setOnClickListener {
val s = svc
if (connected && s != null) {
s.disconnect()
@@ -127,15 +149,17 @@ class MainActivity : Activity(), SvxService.Listener {
toast(getString(R.string.perm_mic))
return
}
- lblTxState.visibility = TextView.VISIBLE
lblPttHint.text = getString(R.string.ptt_talking)
+ lblTxState.text = getString(R.string.status_big_talking)
svc?.startTx()
}
override fun onPttUp() {
svc?.stopTx()
- lblTxState.visibility = TextView.GONE
- lblPttHint.text = getString(R.string.ptt_hint)
+ lblPttHint.text = getString(R.string.hold_to_talk)
+ lblTxState.text =
+ if (connected) getString(R.string.status_big_connected)
+ else getString(R.string.status_big_disconnected)
}
})
@@ -161,6 +185,12 @@ class MainActivity : Activity(), SvxService.Listener {
} else {
getString(R.string.server_subtitle, host, settings.port, settings.talkGroup)
}
+ lblCallsign.text = if (settings.callsign.isBlank()) {
+ getString(R.string.settings_not_configured)
+ } else {
+ settings.callsign
+ }
+ lblTg.text = getString(R.string.tg_badge, settings.talkGroup)
}
// ---------- Callbacks от SvxService (фоновые потоки!) ----------
@@ -173,7 +203,11 @@ class MainActivity : Activity(), SvxService.Listener {
connected = false
updatePttEnabled()
statusChip.text = getString(R.string.status_disconnected)
- btnConnect.text = getString(R.string.btn_connect)
+ statusChip.setTextColor(getColor(R.color.danger))
+ lblTxState.text = getString(R.string.status_big_disconnected)
+ lblTxState.setTextColor(getColor(R.color.danger))
+ lblConnectHint.text = getString(R.string.hint_connect)
+ lblPttHint.text = getString(R.string.hold_to_talk)
}
ReflectorClient.STATE_CONNECTING,
@@ -182,7 +216,9 @@ class MainActivity : Activity(), SvxService.Listener {
ReflectorClient.STATE_WAIT_AUTH_OK,
ReflectorClient.STATE_WAIT_SERVER_INFO -> {
statusChip.text = getString(R.string.status_connecting)
- btnConnect.text = getString(R.string.btn_disconnect)
+ statusChip.setTextColor(getColor(R.color.warn))
+ lblTxState.text = getString(R.string.status_connecting).uppercase()
+ lblTxState.setTextColor(getColor(R.color.warn))
connected = false
updatePttEnabled()
}
@@ -191,14 +227,20 @@ class MainActivity : Activity(), SvxService.Listener {
connected = true
updatePttEnabled()
statusChip.text = getString(R.string.status_connected)
- btnConnect.text = getString(R.string.btn_disconnect)
+ statusChip.setTextColor(getColor(R.color.accent))
+ lblTxState.text = getString(R.string.status_big_connected)
+ lblTxState.setTextColor(getColor(R.color.accent))
+ lblConnectHint.text = getString(R.string.hint_disconnect)
}
ReflectorClient.STATE_AUTH_FAILED -> {
connected = false
updatePttEnabled()
statusChip.text = getString(R.string.status_auth_failed)
- btnConnect.text = getString(R.string.btn_connect)
+ statusChip.setTextColor(getColor(R.color.danger))
+ lblTxState.text = getString(R.string.status_big_disconnected)
+ lblTxState.setTextColor(getColor(R.color.danger))
+ lblConnectHint.text = getString(R.string.hint_connect)
toast(getString(R.string.status_error, detail ?: ""))
}
@@ -206,7 +248,10 @@ class MainActivity : Activity(), SvxService.Listener {
connected = false
updatePttEnabled()
statusChip.text = getString(R.string.status_error, detail ?: "")
- btnConnect.text = getString(R.string.btn_connect)
+ statusChip.setTextColor(getColor(R.color.danger))
+ lblTxState.text = getString(R.string.status_big_disconnected)
+ lblTxState.setTextColor(getColor(R.color.danger))
+ lblConnectHint.text = getString(R.string.hint_connect)
}
}
}
@@ -239,7 +284,18 @@ class MainActivity : Activity(), SvxService.Listener {
override fun onLevel(source: Int, level: Float) {
mainHandler.post {
- levelMeter.setLevel(source, level)
+ val db = if (level <= 0.001f) {
+ getString(R.string.meter_idle)
+ } else {
+ getString(R.string.meter_db, (level * 60).toInt().coerceIn(1, 60))
+ }
+ if (source == 0) { // SRC_MIC
+ micBarFill.scaleX = level.coerceIn(0f, 1f)
+ micDb.text = db
+ } else { // SRC_SPEAKER
+ rxBarFill.scaleX = level.coerceIn(0f, 1f)
+ rxDb.text = db
+ }
}
}
@@ -253,6 +309,119 @@ class MainActivity : Activity(), SvxService.Listener {
pttButton.enabled2 = connected
}
+ private fun checkForUpdates() {
+ Thread {
+ val version = try {
+ packageManager.getPackageInfo(packageName, 0).versionName
+ } catch (_: Exception) {
+ return@Thread
+ }
+ val info = try {
+ UpdateChecker.check(version)
+ } catch (e: Exception) {
+ Log.w(TAG, "update check failed: ${e.message}")
+ null
+ }
+ if (info != null) {
+ mainHandler.post { showUpdateDialog(info) }
+ } else {
+ Log.d(TAG, "update: no newer version")
+ }
+ }.start()
+ }
+
+ private fun showUpdateDialog(info: UpdateChecker.UpdateInfo) {
+ pendingUpdate = info
+ updateDialog = AlertDialog.Builder(this)
+ .setTitle(R.string.update_available_title)
+ .setMessage(getString(R.string.update_available_msg, info.version))
+ .setPositiveButton(R.string.update_btn) { _, _ -> startUpdate() }
+ .setNegativeButton(R.string.update_later) { _, _ ->
+ pendingUpdate = null
+ updateDialog = null
+ }
+ .setOnCancelListener {
+ pendingUpdate = null
+ updateDialog = null
+ }
+ .show()
+ }
+
+ private fun canInstall(): Boolean =
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.O || packageManager.canRequestPackageInstalls()
+
+ private fun startUpdate() {
+ val info = pendingUpdate ?: return
+ if (!canInstall()) {
+ startActivity(
+ Intent(
+ Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
+ Uri.parse("package:$packageName")
+ )
+ )
+ return
+ }
+ val progress = ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal)
+ progress.max = 100
+ val label = TextView(this)
+ label.setText(R.string.update_downloading)
+ label.setPadding(dp(20), dp(16), dp(20), 0)
+ val content = LinearLayout(this).apply {
+ orientation = LinearLayout.VERTICAL
+ addView(label)
+ addView(
+ progress,
+ LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT
+ ).apply { topMargin = dp(12) }
+ )
+ }
+ updateDialog = AlertDialog.Builder(this)
+ .setTitle(R.string.update_available_title)
+ .setView(content)
+ .setCancelable(false)
+ .show()
+
+ val file = File(cacheDir, "updates/svxhomyak-${info.version}.apk")
+ file.parentFile?.mkdirs()
+ Thread {
+ try {
+ UpdateChecker.download(info.apkUrl, file) { p ->
+ mainHandler.post { progress.progress = (p * 100).toInt() }
+ }
+ mainHandler.post {
+ updateDialog?.dismiss()
+ updateDialog = null
+ pendingUpdate = null
+ installApk(file)
+ }
+ } catch (e: Exception) {
+ mainHandler.post {
+ updateDialog?.dismiss()
+ updateDialog = null
+ pendingUpdate = null
+ toast(getString(R.string.update_failed, e.message))
+ }
+ }
+ }.start()
+ }
+
+ private fun installApk(file: File) {
+ val uri = Uri.Builder()
+ .scheme("content")
+ .authority("$packageName.fileprovider")
+ .path("updates/${file.name}")
+ .build()
+ val intent = Intent(Intent.ACTION_VIEW).apply {
+ setDataAndType(uri, "application/vnd.android.package-archive")
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
+ startActivity(intent)
+ }
+
+ private fun dp(v: Int): Int = (v * resources.displayMetrics.density).toInt()
+
private fun toast(msg: String) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}
diff --git a/app/src/main/java/ru/ua1zbe/svxremote/UpdateChecker.kt b/app/src/main/java/ru/ua1zbe/svxremote/UpdateChecker.kt
new file mode 100644
index 0000000..fa0f8a3
--- /dev/null
+++ b/app/src/main/java/ru/ua1zbe/svxremote/UpdateChecker.kt
@@ -0,0 +1,77 @@
+package ru.ua1zbe.svxremote
+
+import org.json.JSONArray
+import org.json.JSONObject
+import java.io.File
+import java.net.HttpURLConnection
+import java.net.URL
+
+/** Проверка и загрузка обновлений через API Gitea. */
+object UpdateChecker {
+
+ data class UpdateInfo(val version: String, val apkUrl: String)
+
+ private const val API_URL =
+ "https://git2.ua1zbe.ru/api/v1/repos/ua1zbe/svxhomyak/releases/latest"
+
+ /** Вернуть информацию об обновлении, если доступна более новая версия. */
+ fun check(currentVersion: String): UpdateInfo? {
+ val conn = URL(API_URL).openConnection() as HttpURLConnection
+ conn.connectTimeout = 10_000
+ conn.readTimeout = 10_000
+ return try {
+ if (conn.responseCode != 200) return null
+ val json = JSONObject(conn.inputStream.bufferedReader().readText())
+ val version = json.optString("tag_name", "").removePrefix("v")
+ if (!isNewer(version, currentVersion)) return null
+ val assets = json.optJSONArray("assets") ?: JSONArray()
+ var url: String? = null
+ for (i in 0 until assets.length()) {
+ val a = assets.getJSONObject(i)
+ if (a.optString("name").endsWith(".apk")) {
+ url = a.optString("browser_download_url")
+ break
+ }
+ }
+ url?.let { UpdateInfo(version, it) }
+ } finally {
+ conn.disconnect()
+ }
+ }
+
+ /** Скачать APK в файл, сообщая прогресс 0..1. */
+ fun download(url: String, target: File, onProgress: (Float) -> Unit) {
+ val conn = URL(url).openConnection() as HttpURLConnection
+ conn.connectTimeout = 15_000
+ conn.readTimeout = 15_000
+ try {
+ val total = conn.contentLengthLong
+ var done = 0L
+ conn.inputStream.use { input ->
+ target.outputStream().use { out ->
+ val buf = ByteArray(64 * 1024)
+ while (true) {
+ val n = input.read(buf)
+ if (n < 0) break
+ out.write(buf, 0, n)
+ done += n
+ if (total > 0) onProgress(done.toFloat() / total)
+ }
+ }
+ }
+ } finally {
+ conn.disconnect()
+ }
+ }
+
+ private fun isNewer(remote: String, current: String): Boolean {
+ val r = remote.split('.').mapNotNull { it.toIntOrNull() }
+ val c = current.split('.').mapNotNull { it.toIntOrNull() }
+ for (i in 0 until maxOf(r.size, c.size)) {
+ val a = r.getOrElse(i) { 0 }
+ val b = c.getOrElse(i) { 0 }
+ if (a != b) return a > b
+ }
+ return false
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/ua1zbe/svxremote/UpdateFileProvider.kt b/app/src/main/java/ru/ua1zbe/svxremote/UpdateFileProvider.kt
new file mode 100644
index 0000000..0a17316
--- /dev/null
+++ b/app/src/main/java/ru/ua1zbe/svxremote/UpdateFileProvider.kt
@@ -0,0 +1,34 @@
+package ru.ua1zbe.svxremote
+
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.database.Cursor
+import android.net.Uri
+import android.os.ParcelFileDescriptor
+import java.io.File
+
+/** Минимальный FileProvider для установки APK (без androidx). */
+class UpdateFileProvider : ContentProvider() {
+
+ override fun onCreate(): Boolean = true
+
+ override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor {
+ val name = uri.lastPathSegment
+ ?: throw IllegalArgumentException("bad uri")
+ if (name != name.substringAfterLast('/')) {
+ throw IllegalArgumentException("bad uri")
+ }
+ val path = File(context?.cacheDir, "updates/$name")
+ return ParcelFileDescriptor.open(path, ParcelFileDescriptor.MODE_READ_ONLY)
+ }
+
+ override fun getType(uri: Uri): String = "application/vnd.android.package-archive"
+
+ override fun insert(uri: Uri, values: ContentValues?): Uri? = null
+ override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0
+ override fun query(
+ uri: Uri, projection: Array?, selection: String?,
+ selectionArgs: Array?, sortOrder: String?
+ ): Cursor? = null
+ override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int = 0
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/ua1zbe/svxremote/ui/PttButton.kt b/app/src/main/java/ru/ua1zbe/svxremote/ui/PttButton.kt
index e3be73a..5ef3226 100644
--- a/app/src/main/java/ru/ua1zbe/svxremote/ui/PttButton.kt
+++ b/app/src/main/java/ru/ua1zbe/svxremote/ui/PttButton.kt
@@ -84,28 +84,46 @@ class PttButton @JvmOverloads constructor(
canvas.drawCircle(centerX, centerY, r, paint)
canvas.drawCircle(centerX, centerY, r, ringPaint)
- // Иконка микрофона
+ // Иконка микрофона (как icons8: капсула + чашка + ножка + основание)
val micPaint = Paint(Paint.ANTI_ALIAS_FLAG)
micPaint.color = Color.WHITE
micPaint.style = Paint.Style.FILL
- val s = r * 0.55f
- val path = Path()
- // Простой микрофон
+
val x = centerX
val y = centerY
- path.moveTo(x - s * 0.30f, y - s * 1.05f)
- path.lineTo(x + s * 0.30f, y - s * 1.05f)
- path.lineTo(x + s * 0.30f, y - s * 0.20f)
- path.cubicTo(x + s * 0.30f, y + s * 0.55f, x - s * 0.30f, y + s * 0.55f, x - s * 0.30f, y - s * 0.20f)
- path.close()
- canvas.drawPath(path, micPaint)
- canvas.drawRect(x - s * 0.06f, y - s * 1.15f, x + s * 0.06f, y - s * 0.28f, micPaint)
- // ножка и дуга
- canvas.drawRect(x - s * 0.05f, y + s * 0.28f, x + s * 0.05f, y + s * 0.55f, micPaint)
- canvas.drawArc(
- x - s * 0.55f, y + s * 0.30f,
- x + s * 0.55f, y + s * 1.20f,
- 0f, 180f, false, micPaint
+ val s = r * 0.5f
+
+ // корпус-капсула
+ val halfW = s * 0.34f
+ canvas.drawRoundRect(
+ x - halfW, y - s * 0.85f, x + halfW, y + s * 0.10f,
+ halfW, halfW, micPaint
+ )
+ // чашка-держатель (U-образная, заполненная)
+ val cup = Path()
+ cup.fillType = Path.FillType.EVEN_ODD
+ cup.moveTo(x - s * 0.52f, y + s * 0.06f)
+ cup.lineTo(x - s * 0.52f, y + s * 0.48f)
+ cup.quadTo(x - s * 0.52f, y + s * 0.70f, x, y + s * 0.70f)
+ cup.quadTo(x + s * 0.52f, y + s * 0.70f, x + s * 0.52f, y + s * 0.48f)
+ cup.lineTo(x + s * 0.52f, y + s * 0.06f)
+ cup.close()
+ cup.moveTo(x - s * 0.38f, y + s * 0.06f)
+ cup.lineTo(x - s * 0.38f, y + s * 0.44f)
+ cup.quadTo(x - s * 0.38f, y + s * 0.56f, x, y + s * 0.56f)
+ cup.quadTo(x + s * 0.38f, y + s * 0.56f, x + s * 0.38f, y + s * 0.44f)
+ cup.lineTo(x + s * 0.38f, y + s * 0.06f)
+ cup.close()
+ canvas.drawPath(cup, micPaint)
+ // ножка
+ canvas.drawRoundRect(
+ x - s * 0.08f, y + s * 0.46f, x + s * 0.08f, y + s * 0.72f,
+ s * 0.08f, s * 0.08f, micPaint
+ )
+ // основание
+ canvas.drawRoundRect(
+ x - s * 0.30f, y + s * 0.66f, x + s * 0.30f, y + s * 0.90f,
+ s * 0.12f, s * 0.12f, micPaint
)
}
diff --git a/app/src/main/res/drawable/bg_meter.xml b/app/src/main/res/drawable/bg_meter.xml
new file mode 100644
index 0000000..52ebba1
--- /dev/null
+++ b/app/src/main/res/drawable/bg_meter.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_meter_fill_mic.xml b/app/src/main/res/drawable/bg_meter_fill_mic.xml
new file mode 100644
index 0000000..ecdd77d
--- /dev/null
+++ b/app/src/main/res/drawable/bg_meter_fill_mic.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_meter_fill_rx.xml b/app/src/main/res/drawable/bg_meter_fill_rx.xml
new file mode 100644
index 0000000..992dc31
--- /dev/null
+++ b/app/src/main/res/drawable/bg_meter_fill_rx.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 1377df4..9fc228e 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -4,24 +4,28 @@
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/bg"
+ android:gravity="center_horizontal"
android:padding="16dp">
+
+
+
+ android:textSize="20sp"
+ android:textStyle="bold" />
-
+
+ android:layout_marginTop="36dp"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:textSize="13sp"
+ android:text="@string/talker_last_none" />
+ android:ellipsize="end" />
-
-
+
+ android:layout_width="130dp"
+ android:layout_height="130dp" />
-
-
+ android:text="@string/hold_to_talk" />
-
+ android:layout_height="0dp"
+ android:layout_weight="1" />
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index edb4f54..0e87652 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -10,11 +10,24 @@
Пароль (AUTH_KEY)
Группа вещания (TG)
Сохранить
- SVXhomyak — v1.0.0
+ SVXhomyak — v1.0.2
Сохранено
например UA1ZBE
Отключено
+ SVXHomyak
+ DISCONNECTED
+ CONNECTED
+ ПЕРЕДАЧА
+ Нажмите, чтобы подключиться
+ Нажмите, чтобы отключиться
+ HOLD TO TALK
+ RX
+ MIC
+ ---
+ -%1$d dB
+ TG%1$d
+ Последний: —
Подключение…
Проверка ключа…
На связи
@@ -45,4 +58,11 @@
Настройки не заданы
Нет связи
+
+ Доступно обновление
+ Версия %1$s уже вышла. Обновить приложение?
+ Обновить
+ Позже
+ Загрузка обновления…
+ Не удалось загрузить обновление: %1$s
\ No newline at end of file