Show servers' icons on bookmarks, and their host button in the toolbar's corner
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -354,6 +354,7 @@ class ServerController internal constructor(
|
||||
selfClientId = if (session.isConnected) conn.selfClientId else 0,
|
||||
channelId = if (session.isConnected) self?.channelId ?: 0 else 0,
|
||||
inputMode = app.settings.inputMode,
|
||||
hostButton = if (session.isConnected) conn.model.hostButton() else null,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -412,7 +413,15 @@ class ServerController internal constructor(
|
||||
refresh()
|
||||
}
|
||||
|
||||
override fun onModelChanged() = scheduleTree(structure = true)
|
||||
override fun onModelChanged() {
|
||||
scheduleTree(structure = true)
|
||||
// A server edit may have changed it.
|
||||
if (session.isConnected && conn.model.hostButton() != _state.value.hostButton) refresh()
|
||||
}
|
||||
|
||||
override fun onServerIconChanged(iconId: Long) =
|
||||
app.serverIconChanged(session.address(), session.port(), iconId)
|
||||
|
||||
override fun onInfoUpdated() {
|
||||
_infoUpdates.update { it + 1 }
|
||||
// Avatars announce their arrival as info updates.
|
||||
|
||||
@@ -94,7 +94,7 @@ data class ServerTree(
|
||||
countryFlags: Boolean): ServerTree =
|
||||
ServerTree(
|
||||
roots.map { channel(it, model, contacts, colorContacts) },
|
||||
model.serverProperty("virtualserver_icon_id").toLongOrNull() ?: 0,
|
||||
model.serverIconId(),
|
||||
countryFlags,
|
||||
)
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.ts3client.config.Settings
|
||||
import com.ts3client.contacts.Contact
|
||||
import com.ts3client.contacts.ContactStore
|
||||
import com.ts3client.myts.MyTeamSpeak
|
||||
import com.ts3client.net.HostButton
|
||||
import com.ts3client.myts.MyTeamSpeakLogin
|
||||
import com.ts3client.session.Sessions
|
||||
import com.ts3client.sound.SoundNotifier
|
||||
@@ -51,6 +52,7 @@ data class SessionState(
|
||||
val selfClientId: Int = 0,
|
||||
val channelId: Int = 0,
|
||||
val inputMode: Settings.InputMode = Settings.InputMode.VOICE_ACTIVATION,
|
||||
val hostButton: HostButton? = null,
|
||||
)
|
||||
|
||||
/** Something that happened once, for the UI to tell the user about. */
|
||||
@@ -270,6 +272,22 @@ class SessionController(private val context: Context) {
|
||||
saveBookmarks()
|
||||
}
|
||||
|
||||
/** Bookmarks leading to [address]:[port] take on the icon the server shows now. */
|
||||
internal fun serverIconChanged(address: String, port: Int, iconId: Long) {
|
||||
main.post {
|
||||
val all = bookmarkStore.all()
|
||||
var changed = false
|
||||
for (i in all.indices) {
|
||||
val b = all[i]
|
||||
if (!b.pointsAt(address, port) || b.iconId == iconId) continue
|
||||
// A new entry, so the list the UI holds changes along with it.
|
||||
all[i] = b.copy().also { it.iconId = iconId }
|
||||
changed = true
|
||||
}
|
||||
if (changed) saveBookmarks()
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveBookmarks() {
|
||||
bookmarkStore.save()
|
||||
_bookmarks.value = bookmarkStore.all().toList()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.ts3client.android.ui
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -9,6 +11,7 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
@@ -44,6 +47,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -51,6 +55,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.ts3client.android.IdentityInfo
|
||||
import com.ts3client.android.SessionController
|
||||
import com.ts3client.config.Bookmark
|
||||
import com.ts3client.net.IconRepository
|
||||
|
||||
/** Where to connect to: an address typed in, or a bookmark. */
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -205,6 +210,16 @@ private fun IdentityDialog(identities: List<IdentityInfo>, selected: String, onD
|
||||
)
|
||||
}
|
||||
|
||||
/** The server's icon as last seen there, else the pack's bookmark icon. */
|
||||
@Composable
|
||||
private fun BookmarkIcon(iconId: Long) {
|
||||
val cached = remember(iconId) {
|
||||
IconRepository.cached(iconId)?.let { BitmapFactory.decodeByteArray(it, 0, it.size)?.asImageBitmap() }
|
||||
}
|
||||
if (cached != null) Image(cached, null, Modifier.size(24.dp))
|
||||
else PackIcon("BOOKMARK_MANAGER", null)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BookmarkRow(
|
||||
bookmark: Bookmark,
|
||||
@@ -220,7 +235,7 @@ private fun BookmarkRow(
|
||||
supportingContent = {
|
||||
Text("${bookmark.address}:${bookmark.port}" + (identity?.let { " · ${it.name}" } ?: ""))
|
||||
},
|
||||
leadingContent = { PackIcon("BOOKMARK_MANAGER", null) },
|
||||
leadingContent = { BookmarkIcon(bookmark.iconId) },
|
||||
trailingContent = {
|
||||
Box {
|
||||
IconButton(onClick = { menu = true }) { Icon(Icons.Default.MoreVert, "Bookmark options") }
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.ts3client.android.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.ts3client.android.Picture
|
||||
import com.ts3client.android.WebPictures
|
||||
import com.ts3client.net.HostButton
|
||||
|
||||
/**
|
||||
* The server's host button in the top bar: its own picture fitted to the bar's icons, or
|
||||
* the pack's host button icon while there is none (yet).
|
||||
*/
|
||||
@Composable
|
||||
fun HostButtonAction(button: HostButton, onOpen: (String) -> Unit) {
|
||||
val label = button.tooltip.ifEmpty { button.url }.ifEmpty { null }
|
||||
val picture = hostPicture(button.gfxUrl)
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.clickable(enabled = button.url.isNotEmpty()) { onOpen(button.url) }
|
||||
.padding(horizontal = 12.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (picture == null) {
|
||||
PackIcon("HOSTER_BUTTON", label)
|
||||
} else {
|
||||
Image(
|
||||
rememberPicturePainter(picture),
|
||||
label,
|
||||
Modifier
|
||||
.widthIn(max = 24.dp * MAX_ASPECT)
|
||||
.height(24.dp)
|
||||
.aspectRatio(picture.width.toFloat() / picture.height.coerceAtLeast(1)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun hostPicture(url: String): Picture? {
|
||||
val pictures = LocalWebPictures.current
|
||||
if (pictures == null || url.isEmpty()) return null
|
||||
val version by pictures.version.collectAsStateWithLifecycle()
|
||||
val state by produceState<WebPictures.State>(WebPictures.State.Loading, url, version) {
|
||||
value = pictures.state(url)
|
||||
}
|
||||
return (state as? WebPictures.State.Shown)?.picture
|
||||
}
|
||||
|
||||
/** Keeps a banner-shaped picture from crowding out the server's name. */
|
||||
private const val MAX_ASPECT = 6
|
||||
@@ -231,6 +231,7 @@ fun ServerScreen(
|
||||
title = { ServerSwitcher(vm, server, state, onConnectOther) },
|
||||
expandedHeight = 48.dp,
|
||||
actions = {
|
||||
state.hostButton?.let { HostButtonAction(it) { url -> runCatching { uriHandler.openUri(url) } } }
|
||||
IconButton(onClick = find::show) { PackIcon("FIND", "Find") }
|
||||
IconButton(onClick = { onContacts(null) }) { PackIcon("CONTACT", "Contacts") }
|
||||
IconButton(onClick = onSettings) { PackIcon("SETTINGS", "Settings") }
|
||||
|
||||
Reference in New Issue
Block a user