Android: Design settings page, Contacts in Settings, server icons in the switcher

Appearance options move to their own Design page. Contacts is the first
Settings entry instead of a top-bar button. The top bar and the server
switcher show each server's own icon, else the pack's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 23:12:16 +00:00
parent 4911542d39
commit 2d83eafffd
4 changed files with 33 additions and 12 deletions

View File

@@ -73,12 +73,12 @@ fun App(vm: SessionController) {
CompositionLocalProvider(LocalIconTheme provides vm.icons, LocalWebPictures provides vm.pictures) {
val server = current
when {
settingsOpen -> SettingsScreen(vm, onBack = { settingsOpen = false })
contactsOpen -> ContactsScreen(vm, editingContact, onEdit = { uniqueId ->
if (uniqueId == null && contactDirect) contactsOpen = false
contactDirect = false
editingContact = uniqueId
}, onBack = { contactsOpen = false })
settingsOpen -> SettingsScreen(vm, onContacts = { openContacts(null) }, onBack = { settingsOpen = false })
server != null && !connectOpen -> {
serverStates.SaveableStateProvider(server.id) {
CompositionLocalProvider(LocalServerImages provides server.images) {
@@ -89,7 +89,7 @@ fun App(vm: SessionController) {
}
}
}
else -> ConnectScreen(vm, snackbar, { settingsOpen = true }, { openContacts(null) }, connectAsking,
else -> ConnectScreen(vm, snackbar, { settingsOpen = true }, connectAsking,
onBack = if (current != null) ({ connectOpen = false }) else null)
}
}

View File

@@ -64,7 +64,6 @@ fun ConnectScreen(
vm: SessionController,
snackbar: SnackbarHostState,
onSettings: () -> Unit,
onContacts: () -> Unit,
onConnect: (() -> Unit) -> Unit,
/** Back to the servers already connected; null when there are none. */
onBack: (() -> Unit)?,
@@ -89,7 +88,6 @@ fun ConnectScreen(
if (onBack != null) IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back") }
},
actions = {
IconButton(onClick = onContacts) { PackIcon("CONTACT", "Contacts") }
IconButton(onClick = onSettings) { PackIcon("SETTINGS", "Settings") }
},
)

View File

@@ -2,6 +2,7 @@ package com.ts3client.android.ui
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectTapGestures
@@ -108,8 +109,8 @@ fun ServerScreen(
server: ServerController,
snackbar: SnackbarHostState,
onSettings: () -> Unit,
/** Opens the contacts, on the given client's page when one is named. */
onContacts: (String?) -> Unit,
/** Opens the given client's contact page. */
onEditContact: (String) -> Unit,
onEnableMicrophone: () -> Unit,
onConnectOther: () -> Unit,
) {
@@ -233,7 +234,6 @@ fun ServerScreen(
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") }
},
)
@@ -333,7 +333,7 @@ fun ServerScreen(
onVolume = { server.setClientVolume(id, it) },
onAway = { server.setAway(it); clientSheet = null },
onAddContact = { server.addContact(id, it) },
onEditContact = { clientSheet = null; onContacts(client.uniqueId) },
onEditContact = { clientSheet = null; onEditContact(client.uniqueId) },
onMove = if (server.canMoveClients) ({ clientSheet = null; moving = id }) else null,
onMoveToOwnChannel = if (server.canMoveClients && tree.channel(state.channelId)?.clients?.none { it.id == id } == true) {
{ server.moveClient(id, state.channelId); clientSheet = null }
@@ -523,6 +523,16 @@ private fun PushToTalk(talking: Boolean, onPressed: (Boolean) -> Unit, modifier:
}
}
/** The server's own icon, else the pack's server icon. */
@Composable
private fun ServerLogo(server: ServerController, modifier: Modifier = Modifier) {
val tree by server.tree.collectAsStateWithLifecycle()
val version by server.images.version.collectAsStateWithLifecycle()
val icon = remember(tree.serverIconId, version) { server.images.icon(tree.serverIconId) }
if (icon != null) Image(icon, null, modifier.size(24.dp))
else PackIcon("SERVER_GREEN", null, modifier)
}
/** The server on screen, opening onto the others and connecting to another. */
@Composable
private fun ServerSwitcher(vm: SessionController, server: ServerController, state: SessionState, onConnectOther: () -> Unit) {
@@ -531,6 +541,7 @@ private fun ServerSwitcher(vm: SessionController, server: ServerController, stat
Box(Modifier.fillMaxWidth()) {
Row(Modifier.fillMaxWidth().heightIn(min = 48.dp).clickable { open = true },
verticalAlignment = Alignment.CenterVertically) {
ServerLogo(server, Modifier.padding(end = 12.dp))
Column(Modifier.weight(1f)) {
Text(state.title, maxLines = 1, overflow = TextOverflow.Ellipsis)
if (!state.connected) {
@@ -548,7 +559,7 @@ private fun ServerSwitcher(vm: SessionController, server: ServerController, stat
text = { Text(otherState.title, maxLines = 1, overflow = TextOverflow.Ellipsis) },
onClick = { open = false; vm.select(other) },
leadingIcon = {
PackIcon("SERVER_GREEN", null, Modifier.alpha(if (otherState.connected) 1f else 0.5f))
ServerLogo(other, Modifier.alpha(if (otherState.connected) 1f else 0.5f))
},
trailingIcon = if (otherState.micActive) ({ PackIcon("ACTIVATE_MICROPHONE", "Microphone") }) else null,
colors = if (other == server) MenuDefaults.itemColors(textColor = MaterialTheme.colorScheme.primary)

View File

@@ -21,17 +21,18 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
private enum class SettingsPage { SOUND, IDENTITIES, MY_TEAMSPEAK }
private enum class SettingsPage { SOUND, DESIGN, IDENTITIES, MY_TEAMSPEAK }
/**
* The settings, by category. Every change is saved and put into effect at once,
* as there is no dialog to confirm.
*/
@Composable
fun SettingsScreen(vm: SessionController, onBack: () -> Unit) {
fun SettingsScreen(vm: SessionController, onContacts: () -> Unit, onBack: () -> Unit) {
var page by rememberSaveable { mutableStateOf<SettingsPage?>(null) }
when (page) {
SettingsPage.SOUND -> return SoundSettingsScreen(vm, onBack = { page = null })
SettingsPage.DESIGN -> return DesignScreen(vm, onBack = { page = null })
SettingsPage.IDENTITIES -> return IdentitiesScreen(vm.identities, onBack = { page = null })
SettingsPage.MY_TEAMSPEAK -> return MyTeamSpeakScreen(vm, onBack = { page = null })
null -> {}
@@ -39,16 +40,27 @@ fun SettingsScreen(vm: SessionController, onBack: () -> Unit) {
SubScreen("Settings", onBack) { padding ->
Column(Modifier.padding(padding).verticalScroll(rememberScrollState())) {
LinkRow("Contacts", "Friends and blocked users", "CONTACT", onContacts)
LinkRow("Sound Settings", "Devices, transmission, voice activation, processing, volume", "PLAYBACK") {
page = SettingsPage.SOUND
}
LinkRow("Design", "Icon pack, country flags, description pictures", "GUISETUP") {
page = SettingsPage.DESIGN
}
LinkRow("Identities", "Create, import, export, security level",
"IDENTITY_MANAGER") { page = SettingsPage.IDENTITIES }
LinkRow("myTeamSpeak",
if (vm.myTeamSpeak.isSignedIn) "Signed in as ${vm.myTeamSpeak.username()}"
else "Sign in to bring over your synchronised bookmarks and identities",
"MYTS_ACCOUNT") { page = SettingsPage.MY_TEAMSPEAK }
Section("Appearance")
}
}
}
@Composable
private fun DesignScreen(vm: SessionController, onBack: () -> Unit) {
SubScreen("Design", onBack) { padding ->
Column(Modifier.padding(padding).verticalScroll(rememberScrollState())) {
IconPackPicker(vm.icons)
var flags by remember { mutableStateOf(vm.settings.countryFlags) }
Toggle("Show country flags in the channel tree", flags) {