10 Commits
v1.0.3 ... main

Author SHA1 Message Date
ua1zbe
11446a92a5 Add group monitoring mode (v1.0.5)
- Monitor button between callsign and TG selector
- On: reconnects with TG_MONITOR for all switch-list groups,
  status shows 'Мониторинг: TG...', PTT disabled
- Talker display shows callsign and its TG
- Off: back to normal talk mode
2026-08-14 15:01:46 +03:00
ua1zbe
b988e7cea0 Match callsign size to TG selector (22sp) (v1.0.4) 2026-08-14 13:50:20 +03:00
ua1zbe
f4996bef8e Enlarge TG selector to 22sp and push to right edge (v1.0.4) 2026-08-14 13:48:38 +03:00
ua1zbe
27169e9d0f Move TG selector inline with callsign, enlarge to 18sp (v1.0.4) 2026-08-14 13:46:16 +03:00
ua1zbe
a083660e03 Center app title between power and settings buttons (v1.0.4) 2026-08-14 13:43:13 +03:00
ua1zbe
3de3d7e893 Align power button to left edge (v1.0.4) 2026-08-14 13:40:10 +03:00
ua1zbe
efcec098bd Add power button next to app title (v1.0.4)
- Tap: disconnect from server, stop service and close app
2026-08-14 13:38:04 +03:00
ua1zbe
d0c055fec9 Show talk group next to nodes in list (v1.0.4)
- SvxService tracks node->TG mapping learned from talker events
- Own callsign always shown with its current TG
- Protocol carries no TG in join/leave, so others appear once they talk
2026-08-14 13:32:41 +03:00
ua1zbe
9790f9d69c Show centered node join/leave overlay for 5s (v1.0.4)
- Replaces bottom toast: dark rounded card centered on screen,
  auto-hides after 5 seconds
- Optional test trigger via node_toast_test intent extra
2026-08-14 13:28:55 +03:00
ua1zbe
ea36319fe3 Add connected nodes list dialog (v1.0.4)
- Tap callsign on main screen shows dialog with connected nodes
- SvxService tracks node list: ServerInfo + NODE_JOINED/NODE_LEFT events
- Bump to 1.0.4
2026-08-14 13:05:05 +03:00
15 changed files with 326 additions and 42 deletions

View File

@@ -11,8 +11,8 @@ android {
applicationId = "ru.ua1zbe.svxremote" applicationId = "ru.ua1zbe.svxremote"
minSdk = 26 minSdk = 26
targetSdk = 34 targetSdk = 34
versionCode = 5 versionCode = 6
versionName = "1.0.3" versionName = "1.0.4"
} }
buildTypes { buildTypes {

View File

@@ -9,6 +9,9 @@ import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.PixelFormat
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
@@ -16,6 +19,7 @@ import android.os.IBinder
import android.os.Looper import android.os.Looper
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import android.view.Gravity
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.widget.ImageButton import android.widget.ImageButton
@@ -55,10 +59,12 @@ class MainActivity : Activity(), SvxService.Listener {
private lateinit var micDb: TextView private lateinit var micDb: TextView
private lateinit var pttButton: PttButton private lateinit var pttButton: PttButton
private lateinit var btnSettings: ImageButton private lateinit var btnSettings: ImageButton
private lateinit var btnMonitor: ImageButton
private val mainHandler = Handler(Looper.getMainLooper()) private val mainHandler = Handler(Looper.getMainLooper())
private var connected = false private var connected = false
private var monitoring = false
private var settings: ConnectionSettings = ConnectionSettings("", 5300, "", "", 11) private var settings: ConnectionSettings = ConnectionSettings("", 5300, "", "", 11)
private var svc: SvxService? = null private var svc: SvxService? = null
@@ -99,14 +105,31 @@ class MainActivity : Activity(), SvxService.Listener {
micDb = findViewById(R.id.mic_db) micDb = findViewById(R.id.mic_db)
pttButton = findViewById(R.id.ptt_button) pttButton = findViewById(R.id.ptt_button)
btnSettings = findViewById(R.id.btn_settings) btnSettings = findViewById(R.id.btn_settings)
btnMonitor = findViewById(R.id.btn_monitor)
pttButton.enabled2 = false pttButton.enabled2 = false
btnMonitor.setOnClickListener { toggleMonitoring() }
updateMonitorButton()
val testText = intent?.getStringExtra("node_toast_test")
if (testText != null) {
mainHandler.postDelayed({ showNodeOverlay(testText) }, 1500)
}
btnSettings.setOnClickListener { btnSettings.setOnClickListener {
startActivity(Intent(this, SettingsActivity::class.java)) startActivity(Intent(this, SettingsActivity::class.java))
} }
findViewById<ImageButton>(R.id.btn_power).setOnClickListener {
svc?.disconnect()
stopService(Intent(this, SvxService::class.java))
SplashActivity.mainAlive = false
finishAffinity()
}
lblTg.setOnClickListener { showGroupsDialog() } lblTg.setOnClickListener { showGroupsDialog() }
lblCallsign.setOnClickListener { showNodesDialog() }
connectZone.setOnClickListener { connectZone.setOnClickListener {
if (connected) { if (connected) {
svc?.disconnect() svc?.disconnect()
@@ -168,6 +191,8 @@ class MainActivity : Activity(), SvxService.Listener {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
hideNodeOverlay()
mainHandler.removeCallbacksAndMessages(null)
SplashActivity.mainAlive = false SplashActivity.mainAlive = false
svc?.listener = null svc?.listener = null
unbindService(svcConn) unbindService(svcConn)
@@ -221,7 +246,11 @@ class MainActivity : Activity(), SvxService.Listener {
ReflectorClient.STATE_READY -> { ReflectorClient.STATE_READY -> {
connected = true connected = true
updatePttEnabled() updatePttEnabled()
statusChip.text = getString(R.string.status_connected) statusChip.text = if (monitoring) {
getString(R.string.status_monitoring, monitorTgLabel())
} else {
getString(R.string.status_connected)
}
statusChip.setTextColor(getColor(R.color.accent)) statusChip.setTextColor(getColor(R.color.accent))
lblTxState.text = getString(R.string.status_big_connected) lblTxState.text = getString(R.string.status_big_connected)
lblTxState.setTextColor(getColor(R.color.accent)) lblTxState.setTextColor(getColor(R.color.accent))
@@ -262,7 +291,11 @@ class MainActivity : Activity(), SvxService.Listener {
override fun onTalkerStart(tg: Long, callsign: String) { override fun onTalkerStart(tg: Long, callsign: String) {
mainHandler.post { mainHandler.post {
lblTalkerTitle.text = getString(R.string.talker_now, callsign) lblTalkerTitle.text = if (monitoring) {
getString(R.string.talker_mon, callsign, tg)
} else {
getString(R.string.talker_now, callsign)
}
lblTalker.text = callsign lblTalker.text = callsign
} }
} }
@@ -271,6 +304,53 @@ class MainActivity : Activity(), SvxService.Listener {
// последний говоривший остаётся на экране // последний говоривший остаётся на экране
} }
override fun onNodeJoined(callsign: String, tg: Long?) {
showNodeToast(R.string.toast_node_joined, callsign, tg)
}
override fun onNodeLeft(callsign: String, tg: Long?) {
showNodeToast(R.string.toast_node_left, callsign, tg)
}
private fun showNodeToast(tpl: Int, callsign: String, tg: Long?) {
if (!Prefs.nodeToastsEnabled(this)) return
if (callsign.equals(settings.callsign, ignoreCase = true)) return
val suffix = if (tg != null) getString(R.string.node_tg_suffix, tg) else ""
mainHandler.post { showNodeOverlay(getString(tpl, callsign, suffix)) }
}
private var nodeOverlay: View? = null
private fun showNodeOverlay(text: String) {
hideNodeOverlay()
val view = layoutInflater.inflate(R.layout.view_node_toast, null, false)
view.findViewById<TextView>(R.id.lbl_node_toast).text = text
try {
windowManager.addView(
view,
WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_APPLICATION,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
PixelFormat.TRANSLUCENT
).apply { gravity = Gravity.CENTER }
)
nodeOverlay = view
mainHandler.postDelayed({ hideNodeOverlay() }, 5000)
} catch (_: Exception) {
toast(text)
}
}
private fun hideNodeOverlay() {
nodeOverlay?.let { v ->
try { windowManager.removeView(v) } catch (_: Exception) { }
nodeOverlay = null
}
}
override fun onAudioPacket(packet: ByteArray, seq: Int) { override fun onAudioPacket(packet: ByteArray, seq: Int) {
} }
@@ -301,7 +381,7 @@ class MainActivity : Activity(), SvxService.Listener {
} }
private fun updatePttEnabled() { private fun updatePttEnabled() {
pttButton.enabled2 = connected pttButton.enabled2 = connected && !monitoring
} }
private fun requestPermissionsIfNeeded() { private fun requestPermissionsIfNeeded() {
@@ -326,12 +406,36 @@ class MainActivity : Activity(), SvxService.Listener {
port = settings.port, port = settings.port,
callsign = settings.callsign, callsign = settings.callsign,
authKey = settings.authKey, authKey = settings.authKey,
talkGroup = settings.talkGroup talkGroup = settings.talkGroup,
monitorTgs = if (monitoring) settings.groups else null
) )
) )
}, 300) }, 300)
} }
/** Включить/выключить режим мониторинга групп с переподключением. */
private fun toggleMonitoring() {
monitoring = !monitoring
updateMonitorButton()
updatePttEnabled()
if (connected) {
svc?.disconnect()
mainHandler.postDelayed({ connect() }, 500)
} else {
connect()
}
}
private fun updateMonitorButton() {
val active = monitoring
btnMonitor.imageTintList = ColorStateList.valueOf(
if (active) Color.parseColor("#58A6FF") else Color.parseColor("#8B949E")
)
btnMonitor.setBackgroundResource(if (active) R.drawable.bg_monitor_on else 0)
btnMonitor.contentDescription =
getString(if (active) R.string.btn_monitor_on else R.string.btn_monitor)
}
/** Переключиться на другую группу: пересохранить и переподключиться. */ /** Переключиться на другую группу: пересохранить и переподключиться. */
private fun switchGroup(tg: Long) { private fun switchGroup(tg: Long) {
settings = settings.copy(talkGroup = tg) settings = settings.copy(talkGroup = tg)
@@ -341,6 +445,28 @@ class MainActivity : Activity(), SvxService.Listener {
mainHandler.postDelayed({ connect() }, 500) mainHandler.postDelayed({ connect() }, 500)
} }
private fun monitorTgLabel(): String =
settings.groups.joinToString(", ") { "TG$it" }
private fun showNodesDialog() {
val nodes = svc?.nodeList().orEmpty()
if (nodes.isEmpty()) {
toast(getString(R.string.toast_nodes_empty))
return
}
val myCs = settings.callsign.trim()
val items = nodes
.map { (cs, tg) ->
val t = tg ?: if (cs.equals(myCs, ignoreCase = true)) settings.talkGroup else null
if (t != null) getString(R.string.node_with_tg, cs, t) else cs
}
.sorted()
AlertDialog.Builder(this)
.setTitle(getString(R.string.nodes_title, items.size))
.setItems(items.toTypedArray(), null)
.show()
}
private fun showGroupsDialog() { private fun showGroupsDialog() {
val groups = (settings.groups + settings.talkGroup).distinct() val groups = (settings.groups + settings.talkGroup).distinct()
if (groups.isEmpty()) { if (groups.isEmpty()) {

View File

@@ -3,6 +3,7 @@ package ru.ua1zbe.svxremote
import android.app.Activity import android.app.Activity
import android.os.Bundle import android.os.Bundle
import android.widget.Button import android.widget.Button
import android.widget.CheckBox
import android.widget.EditText import android.widget.EditText
import android.widget.Toast import android.widget.Toast
import ru.ua1zbe.svxremote.prefs.ConnectionSettings import ru.ua1zbe.svxremote.prefs.ConnectionSettings
@@ -27,6 +28,8 @@ class SettingsActivity : Activity() {
inputAuthKey = findViewById(R.id.input_auth_key) inputAuthKey = findViewById(R.id.input_auth_key)
inputTg = findViewById(R.id.input_tg) inputTg = findViewById(R.id.input_tg)
inputGroups = findViewById(R.id.input_groups) inputGroups = findViewById(R.id.input_groups)
val chkNodeToasts = findViewById<CheckBox>(R.id.chk_node_toasts)
chkNodeToasts.isChecked = Prefs.nodeToastsEnabled(this)
val s = Prefs.load(this) val s = Prefs.load(this)
inputHost.setText(s.host) inputHost.setText(s.host)
@@ -63,6 +66,7 @@ class SettingsActivity : Activity() {
groups = groups groups = groups
) )
) )
Prefs.setNodeToastsEnabled(this, chkNodeToasts.isChecked)
toast(getString(R.string.saved)) toast(getString(R.string.saved))
finish() finish()
} }

View File

@@ -63,6 +63,8 @@ class SplashActivity : Activity() {
Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_SINGLE_TOP Intent.FLAG_ACTIVITY_SINGLE_TOP
) )
val testText = this.intent?.getStringExtra("node_toast_test")
if (testText != null) intent.putExtra("node_toast_test", testText)
startActivity(intent) startActivity(intent)
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
finish() finish()

View File

@@ -23,6 +23,8 @@ class SvxService : Service(), ReflectorClient.Callbacks {
fun onServerInfo(info: MsgParser.ServerInfo) fun onServerInfo(info: MsgParser.ServerInfo)
fun onTalkerStart(tg: Long, callsign: String) fun onTalkerStart(tg: Long, callsign: String)
fun onTalkerStop(tg: Long, callsign: String) fun onTalkerStop(tg: Long, callsign: String)
fun onNodeJoined(callsign: String, tg: Long?)
fun onNodeLeft(callsign: String, tg: Long?)
fun onAudioPacket(packet: ByteArray, seq: Int) fun onAudioPacket(packet: ByteArray, seq: Int)
fun onAudioFlush() fun onAudioFlush()
fun onError(detail: String) fun onError(detail: String)
@@ -111,10 +113,38 @@ class SvxService : Service(), ReflectorClient.Callbacks {
} }
override fun onServerInfo(info: MsgParser.ServerInfo) { override fun onServerInfo(info: MsgParser.ServerInfo) {
synchronized(nodeSet) {
nodeSet.clear()
nodeSet.addAll(info.nodes)
}
listener?.onServerInfo(info) listener?.onServerInfo(info)
} }
private val nodeSet = LinkedHashSet<String>()
private val nodeTgs = HashMap<String, Long>()
/** Текущий список узлов и известные группы (копия для UI). */
fun nodeList(): List<Pair<String, Long?>> = synchronized(nodeSet) {
nodeSet.map { it to nodeTgs[it] }
}
override fun onNodeJoined(callsign: String, tg: Long?) {
synchronized(nodeSet) { nodeSet.add(callsign) }
listener?.onNodeJoined(callsign, tg)
}
override fun onNodeLeft(callsign: String, tg: Long?) {
synchronized(nodeSet) {
nodeSet.remove(callsign)
nodeTgs.remove(callsign)
}
listener?.onNodeLeft(callsign, tg)
}
override fun onTalkerStart(tg: Long, callsign: String) { override fun onTalkerStart(tg: Long, callsign: String) {
synchronized(nodeSet) {
if (callsign in nodeSet) nodeTgs[callsign] = tg
}
updateNotification(getString(R.string.notif_talking, callsign)) updateNotification(getString(R.string.notif_talking, callsign))
listener?.onTalkerStart(tg, callsign) listener?.onTalkerStart(tg, callsign)
} }
@@ -124,12 +154,6 @@ class SvxService : Service(), ReflectorClient.Callbacks {
listener?.onTalkerStop(tg, callsign) listener?.onTalkerStop(tg, callsign)
} }
override fun onNodeJoined(callsign: String) {
}
override fun onNodeLeft(callsign: String) {
}
override fun onAudioPacket(packet: ByteArray, seq: Int) { override fun onAudioPacket(packet: ByteArray, seq: Int) {
engine.onAudioPacket(packet) engine.onAudioPacket(packet)
} }

View File

@@ -42,4 +42,14 @@ object Prefs {
.putString("groups", s.groups.distinct().joinToString(",")) .putString("groups", s.groups.distinct().joinToString(","))
.apply() .apply()
} }
fun nodeToastsEnabled(context: Context): Boolean {
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.getBoolean("node_toasts", true)
}
fun setNodeToastsEnabled(context: Context, enabled: Boolean) {
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.edit().putBoolean("node_toasts", enabled).apply()
}
} }

View File

@@ -23,8 +23,8 @@ class ReflectorClient(
fun onServerInfo(info: MsgParser.ServerInfo) fun onServerInfo(info: MsgParser.ServerInfo)
fun onTalkerStart(tg: Long, callsign: String) fun onTalkerStart(tg: Long, callsign: String)
fun onTalkerStop(tg: Long, callsign: String) fun onTalkerStop(tg: Long, callsign: String)
fun onNodeJoined(callsign: String) fun onNodeJoined(callsign: String, tg: Long?)
fun onNodeLeft(callsign: String) fun onNodeLeft(callsign: String, tg: Long?)
/** Входящий аудиопакет Opus. */ /** Входящий аудиопакет Opus. */
fun onAudioPacket(packet: ByteArray, seq: Int) fun onAudioPacket(packet: ByteArray, seq: Int)
fun onAudioFlush() fun onAudioFlush()
@@ -58,7 +58,9 @@ class ReflectorClient(
val port: Int, val port: Int,
val callsign: String, val callsign: String,
val authKey: String, val authKey: String,
val talkGroup: Long val talkGroup: Long,
/** Список групп для мониторинга; null = обычный режим (своя группа). */
val monitorTgs: List<Long>? = null
) )
@Volatile @Volatile
@@ -361,19 +363,25 @@ class ReflectorClient(
// (иначе сервер не шлёт аудио группы и не показывает // (иначе сервер не шлёт аудио группы и не показывает
// подписку в статусе: monitoredTGs остаётся пустым). // подписку в статусе: monitoredTGs остаётся пустым).
sendTcp(socket, MsgBuilder.nodeInfo(CLIENT_SW_INFO, CLIENT_SW_VERSION)) sendTcp(socket, MsgBuilder.nodeInfo(CLIENT_SW_INFO, CLIENT_SW_VERSION))
sendTcp(socket, MsgBuilder.selectTg(s.talkGroup)) val monitor = s.monitorTgs
sendTcp(socket, MsgBuilder.tgMonitor(listOf(s.talkGroup))) if (monitor.isNullOrEmpty()) {
sendTcp(socket, MsgBuilder.selectTg(s.talkGroup))
sendTcp(socket, MsgBuilder.tgMonitor(listOf(s.talkGroup)))
} else {
sendTcp(socket, MsgBuilder.selectTg(0))
sendTcp(socket, MsgBuilder.tgMonitor(monitor))
}
openUdp(s) openUdp(s)
} }
ReflectorProtocol.MSG_NODE_JOINED -> { ReflectorProtocol.MSG_NODE_JOINED -> {
val cs = parseSimpleString(msg.fields) val (cs, tg) = parseNodeEvent(msg.fields)
callbacks.onNodeJoined(cs) callbacks.onNodeJoined(cs, tg)
} }
ReflectorProtocol.MSG_NODE_LEFT -> { ReflectorProtocol.MSG_NODE_LEFT -> {
val cs = parseSimpleString(msg.fields) val (cs, tg) = parseNodeEvent(msg.fields)
callbacks.onNodeLeft(cs) callbacks.onNodeLeft(cs, tg)
} }
ReflectorProtocol.MSG_TALKER_START -> { ReflectorProtocol.MSG_TALKER_START -> {
@@ -404,6 +412,17 @@ class ReflectorClient(
return true return true
} }
private fun parseNodeEvent(fields: ByteArray): Pair<String, Long?> {
return try {
val r = BinReader(fields)
val cs = r.str().ifEmpty { "?" }
val tg = try { r.str().toLongOrNull() } catch (_: Exception) { null }
cs to tg
} catch (_: Exception) {
"?" to null
}
}
private fun parseSimpleString(fields: ByteArray): String { private fun parseSimpleString(fields: ByteArray): String {
return try { return try {
BinReader(fields).str().ifEmpty { "?" } BinReader(fields).str().ifEmpty { "?" }

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#3358A6FF" />
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#E61B222A" />
<corners android:radius="24dp" />
<stroke android:width="1dp" android:color="#40FFFFFF" />
</shape>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#8B949E"
android:pathData="M12,1c-4.97,0 -9,4.03 -9,9v7c0,1.66 1.34,3 3,3h3v-8L5,12v-2c0,-3.87 3.13,-7 7,-7s7,3.13 7,7v2h-4v8h3c1.66,0 3,-1.34 3,-3v-7c0,-4.97 -4.03,-9 -9,-9z" />
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#8B949E"
android:pathData="M13,3h-2v10h2L13,3zM17.83,5.17l-1.42,1.42C17.99,7.86 19,9.81 19,12c0,3.87 -3.13,7 -7,7s-7,-3.13 -7,-7c0,-2.19 1.01,-4.14 2.58,-5.41L6.17,5.17C4.23,6.82 3,9.26 3,12c0,4.97 4.03,9 9,9s9,-4.03 9,-9C21,9.26 19.77,6.82 17.83,5.17z" />
</vector>

View File

@@ -14,6 +14,15 @@
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center_vertical"> android:gravity="center_vertical">
<ImageButton
android:id="@+id/btn_power"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_power"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/btn_power"
android:scaleType="centerInside" />
<Space <Space
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="1dp" android:layout_height="1dp"
@@ -56,17 +65,44 @@
android:layout_weight="1" android:layout_weight="1"
android:orientation="vertical"> android:orientation="vertical">
<TextView <LinearLayout
android:id="@+id/lbl_callsign" android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="monospace" android:orientation="horizontal"
android:textColor="@color/text_primary" android:gravity="center_vertical">
android:textSize="28sp"
android:textStyle="bold" <TextView
android:singleLine="true" android:id="@+id/lbl_callsign"
android:ellipsize="end" android:layout_width="0dp"
android:text="@string/settings_not_configured" /> android:layout_weight="1"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:textColor="@color/text_primary"
android:textSize="22sp"
android:textStyle="bold"
android:singleLine="true"
android:ellipsize="end"
android:text="@string/settings_not_configured" />
<ImageButton
android:id="@+id/btn_monitor"
android:layout_width="44dp"
android:layout_height="44dp"
android:src="@drawable/ic_headset"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/btn_monitor"
android:scaleType="centerInside" />
<TextView
android:id="@+id/lbl_tg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:textColor="@color/warn"
android:textSize="22sp"
android:textStyle="bold"
android:fontFamily="monospace" />
</LinearLayout>
<TextView <TextView
android:id="@+id/lbl_subtitle" android:id="@+id/lbl_subtitle"
@@ -78,16 +114,6 @@
android:singleLine="true" android:singleLine="true"
android:ellipsize="end" /> android:ellipsize="end" />
</LinearLayout> </LinearLayout>
<TextView
android:id="@+id/lbl_tg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:textColor="@color/warn"
android:textSize="16sp"
android:textStyle="bold"
android:fontFamily="monospace" />
</LinearLayout> </LinearLayout>
<!-- Статус соединения --> <!-- Статус соединения -->

View File

@@ -139,6 +139,15 @@
android:padding="12dp" android:padding="12dp"
android:hint="9, 11, 12, 13" /> android:hint="9, 11, 12, 13" />
<CheckBox
android:id="@+id/chk_node_toasts"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:text="@string/pref_node_toasts"
android:textColor="@color/text_primary"
android:textSize="15sp" />
<Button <Button
android:id="@+id/btn_save" android:id="@+id/btn_save"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/node_toast_root"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_node_toast"
android:paddingLeft="24dp"
android:paddingRight="24dp"
android:paddingTop="14dp"
android:paddingBottom="14dp">
<TextView
android:id="@+id/lbl_node_toast"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="#F0F0F0"
android:textSize="16sp"
android:textStyle="bold" />
</FrameLayout>

View File

@@ -12,13 +12,25 @@
<string name="pref_groups">Группы для переключения (через запятую)</string> <string name="pref_groups">Группы для переключения (через запятую)</string>
<string name="select_tg">Выбор группы</string> <string name="select_tg">Выбор группы</string>
<string name="toast_groups_empty">Добавьте группы в настройках</string> <string name="toast_groups_empty">Добавьте группы в настройках</string>
<string name="nodes_title">Узлы в сети (%1$d)</string>
<string name="toast_nodes_empty">Нет подключенных узлов</string>
<string name="pref_node_toasts">Всплывающее уведомление о подключении/отключении узлов</string>
<string name="toast_node_joined">Подключился: %1$s%2$s</string>
<string name="toast_node_left">Отключился: %1$s%2$s</string>
<string name="node_tg_suffix"> (группа %1$d)</string>
<string name="node_with_tg">%1$s — TG%2$d</string>
<string name="btn_save">Сохранить</string> <string name="btn_save">Сохранить</string>
<string name="app_version">SVXhomyak — v1.0.3</string> <string name="app_version">SVXhomyak — v1.0.4</string>
<string name="saved">Сохранено</string> <string name="saved">Сохранено</string>
<string name="pref_callsign_hint">например UA1ZBE</string> <string name="pref_callsign_hint">например UA1ZBE</string>
<string name="status_disconnected">Отключено</string> <string name="status_disconnected">Отключено</string>
<string name="app_title">SVXHomyak</string> <string name="app_title">SVXHomyak</string>
<string name="btn_power">Выключить: отключиться и закрыть</string>
<string name="btn_monitor">Мониторинг групп (нажмите, чтобы включить)</string>
<string name="btn_monitor_on">Мониторинг групп включен (нажмите, чтобы выключить)</string>
<string name="status_monitoring">Мониторинг: %1$s</string>
<string name="talker_mon">Говорит: %1$s · TG%2$d</string>
<string name="status_big_disconnected">DISCONNECTED</string> <string name="status_big_disconnected">DISCONNECTED</string>
<string name="status_big_connected">CONNECTED</string> <string name="status_big_connected">CONNECTED</string>
<string name="status_big_talking">ПЕРЕДАЧА</string> <string name="status_big_talking">ПЕРЕДАЧА</string>