phase 5: improve server picker UX and status clarity
This commit is contained in:
@@ -347,19 +347,88 @@ class MainActivity : AppCompatActivity() {
|
||||
val btnEdit = view.findViewById<com.google.android.material.button.MaterialButton>(R.id.btnEdit)
|
||||
val btnRemove = view.findViewById<com.google.android.material.button.MaterialButton>(R.id.btnRemove)
|
||||
val btnDiscover = view.findViewById<com.google.android.material.button.MaterialButton>(R.id.btnDiscover)
|
||||
val txtActiveServer = view.findViewById<android.widget.TextView>(R.id.txtActiveServer)
|
||||
|
||||
fun refresh(selectionId: String? = HostStore.getActiveHostId(this)) {
|
||||
val hostStatuses = mutableMapOf<String, String>()
|
||||
val statusChecksInFlight = mutableSetOf<String>()
|
||||
lateinit var refresh: (String?) -> Unit
|
||||
|
||||
fun selectedHostIdFromList(): String? {
|
||||
val hosts = HostStore.loadHosts(this)
|
||||
val labels = hosts.map { "${it.name} • ${it.baseUrl}" }
|
||||
list.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_single_choice, labels)
|
||||
val pos = list.checkedItemPosition
|
||||
return hosts.getOrNull(pos)?.id
|
||||
}
|
||||
|
||||
fun renderActiveServer(selectionId: String?) {
|
||||
val hosts = HostStore.loadHosts(this)
|
||||
val active = hosts.firstOrNull { it.id == selectionId }
|
||||
?: hosts.firstOrNull { it.id == HostStore.getActiveHostId(this) }
|
||||
?: hosts.firstOrNull()
|
||||
txtActiveServer.text = if (active != null) {
|
||||
"${active.name}\n${active.baseUrl}"
|
||||
} else {
|
||||
getString(R.string.no_server_selected)
|
||||
}
|
||||
}
|
||||
|
||||
fun statusLabelFor(host: RelayHost): String {
|
||||
return hostStatuses[host.id] ?: getString(R.string.status_checking)
|
||||
}
|
||||
|
||||
fun scheduleHostStatusCheck(host: RelayHost) {
|
||||
if (statusChecksInFlight.contains(host.id)) return
|
||||
if (hostStatuses.containsKey(host.id)) return
|
||||
statusChecksInFlight.add(host.id)
|
||||
hostStatuses[host.id] = getString(R.string.status_checking)
|
||||
|
||||
val req = Net.get(host.baseUrl.trimEnd('/') + "/health")
|
||||
Net.client.newCall(req).enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
runOnUiThread {
|
||||
statusChecksInFlight.remove(host.id)
|
||||
hostStatuses[host.id] = getString(R.string.status_offline)
|
||||
refresh(selectedHostIdFromList() ?: HostStore.getActiveHostId(this@MainActivity))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
val ok = response.use { it.isSuccessful }
|
||||
runOnUiThread {
|
||||
statusChecksInFlight.remove(host.id)
|
||||
hostStatuses[host.id] = if (ok) {
|
||||
getString(R.string.status_online)
|
||||
} else {
|
||||
getString(R.string.status_offline)
|
||||
}
|
||||
refresh(selectedHostIdFromList() ?: HostStore.getActiveHostId(this@MainActivity))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
refresh = { selectionIdInput ->
|
||||
val selectionId = selectionIdInput ?: HostStore.getActiveHostId(this)
|
||||
val hosts = HostStore.loadHosts(this)
|
||||
val activeId = HostStore.getActiveHostId(this)
|
||||
val labels = hosts.map { host ->
|
||||
val prefix = if (host.id == activeId) {
|
||||
getString(R.string.status_active_prefix) + " • "
|
||||
} else {
|
||||
""
|
||||
}
|
||||
"${host.name} • ${prefix}${statusLabelFor(host)}\n${host.baseUrl}"
|
||||
}
|
||||
list.adapter = ArrayAdapter(this, R.layout.item_server_picker_choice, android.R.id.text1, labels)
|
||||
list.choiceMode = ListView.CHOICE_MODE_SINGLE
|
||||
val idx = hosts.indexOfFirst { it.id == selectionId }.let { if (it >= 0) it else 0 }
|
||||
if (hosts.isNotEmpty()) list.setItemChecked(idx, true)
|
||||
btnEdit.isEnabled = hosts.isNotEmpty()
|
||||
btnRemove.isEnabled = hosts.isNotEmpty()
|
||||
renderActiveServer(selectionId)
|
||||
hosts.forEach { scheduleHostStatusCheck(it) }
|
||||
}
|
||||
|
||||
refresh()
|
||||
refresh(HostStore.getActiveHostId(this))
|
||||
btnDiscover.text = getString(R.string.scan_lan)
|
||||
|
||||
val dialog = MaterialAlertDialogBuilder(this)
|
||||
@@ -411,6 +480,8 @@ class MainActivity : AppCompatActivity() {
|
||||
val host = upsertDiscoveredServer(selected)
|
||||
HostStore.setActiveHostId(this@MainActivity, host.id)
|
||||
toolbar.subtitle = host.name
|
||||
hostStatuses.remove(host.id)
|
||||
statusChecksInFlight.remove(host.id)
|
||||
refresh(host.id)
|
||||
loadServerBase(host.baseUrl, forcePickerOnFailure = false, manualRefresh = false)
|
||||
dialog.dismiss()
|
||||
@@ -431,6 +502,7 @@ class MainActivity : AppCompatActivity() {
|
||||
val hosts = HostStore.loadHosts(this)
|
||||
val chosen = hosts.getOrNull(position) ?: return@setOnItemClickListener
|
||||
toolbar.subtitle = chosen.name
|
||||
renderActiveServer(chosen.id)
|
||||
}
|
||||
fun showAddEdit(existing: RelayHost? = null) {
|
||||
val nameInput = EditText(this).apply {
|
||||
@@ -506,6 +578,8 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
HostStore.setActiveHostId(this@MainActivity, host.id)
|
||||
hostStatuses.remove(host.id)
|
||||
statusChecksInFlight.remove(host.id)
|
||||
refresh(host.id)
|
||||
dlg.dismiss()
|
||||
}
|
||||
@@ -537,7 +611,8 @@ class MainActivity : AppCompatActivity() {
|
||||
.setNegativeButton("Cancel", null)
|
||||
.setPositiveButton("Remove") { _, _ ->
|
||||
HostStore.remove(this, existing.id)
|
||||
refresh()
|
||||
hostStatuses.remove(existing.id)
|
||||
refresh(HostStore.getActiveHostId(this))
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
@@ -1,14 +1,55 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtPickerSubtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/server_picker_subtitle"
|
||||
android:textSize="13sp"
|
||||
android:textColor="@android:color/darker_gray" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardUseCompatPadding="false">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtActiveLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/active_server"
|
||||
android:textStyle="bold"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtActiveServer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/no_server_selected"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<ListView
|
||||
android:id="@+id/listServers"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="260dp"
|
||||
android:layout_height="240dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:choiceMode="singleChoice" />
|
||||
|
||||
<LinearLayout
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@android:id/text1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="56dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:checkMark="?android:attr/listChoiceIndicatorSingle"
|
||||
android:gravity="center_vertical"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textIsSelectable="false"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="3" />
|
||||
@@ -14,4 +14,11 @@
|
||||
<string name="discovered_servers">Discovered RelayTV Servers</string>
|
||||
<string name="discovery_none">No RelayTV servers found on your network.</string>
|
||||
<string name="discovery_failed">LAN discovery failed.</string>
|
||||
<string name="server_picker_subtitle">Choose a server. Health status updates automatically.</string>
|
||||
<string name="active_server">Active server</string>
|
||||
<string name="no_server_selected">No server selected</string>
|
||||
<string name="status_online">Online</string>
|
||||
<string name="status_offline">Offline</string>
|
||||
<string name="status_checking">Checking…</string>
|
||||
<string name="status_active_prefix">Active</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user