Move clients to another channel or our own from the Android client sheet
"Move to channel…" closes the sheet and marks the client, and the next channel tapped is where they go; a bar above the tree names them and cancels. "Move to own channel" shows while they are elsewhere. Both show only with some i_client_move_power. The server's needed permissions never carry it, even for an admin, so core asks for it with permget on connect and whenever our own groups change; a refusal (guests may not ask) counts as none. Whether it beats the client's needed move power only the server knows, and it refuses when not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -142,6 +142,12 @@ class ServerController internal constructor(
|
||||
fun toggleCollapsed(channelId: Int) =
|
||||
_collapsed.update { if (channelId in it) it - channelId else it + channelId }
|
||||
|
||||
val canMoveClients: Boolean get() = session.canMoveClients()
|
||||
|
||||
fun moveClient(clientId: Int, channelId: Int) {
|
||||
if (session.isConnected) conn.moveClient(clientId, channelId, null)
|
||||
}
|
||||
|
||||
fun poke(clientId: Int, message: String) {
|
||||
if (session.isConnected) conn.poke(clientId, message)
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -114,6 +115,8 @@ fun ServerScreen(
|
||||
var clientSheet by remember { mutableStateOf<Int?>(null) }
|
||||
var channelSheet by remember { mutableStateOf<Int?>(null) }
|
||||
var serverSheet by remember { mutableStateOf(false) }
|
||||
/** The client to move, while a channel for them is being picked. */
|
||||
var moving by remember { mutableStateOf<Int?>(null) }
|
||||
val infoUpdates by server.infoUpdates.collectAsStateWithLifecycle()
|
||||
|
||||
val chatRequest by server.chatRequest.collectAsStateWithLifecycle()
|
||||
@@ -141,6 +144,7 @@ fun ServerScreen(
|
||||
}
|
||||
|
||||
BackHandler(showChat) { showChat = false }
|
||||
BackHandler(moving != null && !showChat) { moving = null }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -170,13 +174,28 @@ fun ServerScreen(
|
||||
) { padding ->
|
||||
Column(Modifier.padding(padding).consumeWindowInsets(padding).imePadding()) {
|
||||
if (!showChat) {
|
||||
moving?.let { id ->
|
||||
val client = tree.client(id)
|
||||
if (client == null) {
|
||||
LaunchedEffect(id) { moving = null }
|
||||
} else {
|
||||
MovePrompt(client.name) { moving = null }
|
||||
}
|
||||
}
|
||||
ChannelTree(
|
||||
serverName = state.title,
|
||||
tree = tree,
|
||||
collapsed = collapsed,
|
||||
currentChannel = state.channelId,
|
||||
onToggle = { server.toggleCollapsed(it.id) },
|
||||
onChannel = { if (it.spacer == null) channelSheet = it.id },
|
||||
onChannel = { channel ->
|
||||
val client = moving
|
||||
when {
|
||||
channel.spacer != null -> {}
|
||||
client != null -> { server.moveClient(client, channel.id); moving = null }
|
||||
else -> channelSheet = channel.id
|
||||
}
|
||||
},
|
||||
onClient = { clientSheet = it.id },
|
||||
onServer = { serverSheet = true },
|
||||
modifier = Modifier.weight(1f),
|
||||
@@ -221,6 +240,10 @@ fun ServerScreen(
|
||||
onAddContact = { server.addContact(id, it) },
|
||||
onEditContact = { clientSheet = null; onContacts(client.uniqueId) },
|
||||
onRemoveContact = { vm.removeContact(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 }
|
||||
} else null,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -245,6 +268,19 @@ fun ServerScreen(
|
||||
}
|
||||
}
|
||||
|
||||
/** Says a channel is being picked for [name] to move to, until it is or [onCancel]. */
|
||||
@Composable
|
||||
private fun MovePrompt(name: String, onCancel: () -> Unit) {
|
||||
Surface(color = MaterialTheme.colorScheme.secondaryContainer) {
|
||||
Row(Modifier.fillMaxWidth().padding(start = 16.dp, end = 8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
PackIcon("CHANNEL_SWITCH", null)
|
||||
Text("Move $name to…", Modifier.weight(1f).padding(horizontal = 12.dp), maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodyMedium)
|
||||
TextButton(onClick = onCancel) { Text("Cancel") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mute, deafen, away and the chat, around the push-to-talk button when that is how the
|
||||
* microphone is used. Holding the mute button switches between push to talk and voice activation.
|
||||
|
||||
@@ -37,7 +37,7 @@ import com.ts3client.contacts.ContactType
|
||||
import com.ts3client.net.Details
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/** What can be done with a client: the private chat, a poke, their volume, filing them as a contact. */
|
||||
/** What can be done with a client: the private chat, a poke, a move, their volume, filing them as a contact. */
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ClientSheet(
|
||||
@@ -54,6 +54,10 @@ fun ClientSheet(
|
||||
onAddContact: (ContactType) -> Unit,
|
||||
onEditContact: () -> Unit,
|
||||
onRemoveContact: () -> Unit,
|
||||
/** Null when the client cannot be moved. */
|
||||
onMove: (() -> Unit)?,
|
||||
/** Null when the client cannot be moved, or is in our channel already. */
|
||||
onMoveToOwnChannel: (() -> Unit)?,
|
||||
) {
|
||||
var poking by remember { mutableStateOf(false) }
|
||||
ModalBottomSheet(onDismissRequest = onDismiss) {
|
||||
@@ -73,6 +77,8 @@ fun ClientSheet(
|
||||
} else {
|
||||
Action("PLAYER_CHAT", "Send message", onMessage)
|
||||
Action("POKE", "Poke") { poking = true }
|
||||
onMove?.let { Action("CHANNEL_SWITCH", "Move to channel…", it) }
|
||||
onMoveToOwnChannel?.let { Action("MOVE_CLIENT_TO_OWN_CHANNEL", "Move to own channel", it) }
|
||||
when (client.contactType) {
|
||||
null -> {
|
||||
Action("ADD_FRIEND", "Add as friend") { onAddContact(ContactType.FRIEND) }
|
||||
|
||||
Reference in New Issue
Block a user