Show and control channel subscriptions

Track the subscription state from the notifychannelsubscribed events, draw
subscribed and unsubscribed channels apart with the pack's icons (and the
built-in glyph as a hollow cone), and offer subscribe/unsubscribe entries —
channel and family — in the channel context menu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 13:57:25 +00:00
parent 245ae5ecc5
commit 46206e3047
6 changed files with 183 additions and 17 deletions

View File

@@ -17,6 +17,8 @@ public final class ChannelNode {
public int maxClients = -1;
/** Id of the channel's custom icon in the server's file repository, or 0 for none. */
public long iconId;
/** Whether the server sends us this channel's client list; set from the subscription events. */
public boolean subscribed;
/** Populated when the tree is rebuilt. */
public final List<ChannelNode> children = new ArrayList<>();
@@ -26,4 +28,35 @@ public final class ChannelNode {
this.id = id;
this.name = name;
}
/** @return whether this channel or any channel below it is subscribed */
public boolean anySubscribed() {
if (subscribed) return true;
for (ChannelNode child : children) {
if (child.anySubscribed()) return true;
}
return false;
}
/** @return whether this channel and every channel below it is subscribed */
public boolean allSubscribed() {
if (!subscribed) return false;
for (ChannelNode child : children) {
if (!child.allSubscribed()) return false;
}
return true;
}
/** @return this channel's id, followed by those of the channels below it when {@code family} */
public List<Integer> familyIds(boolean family) {
List<Integer> ids = new ArrayList<>();
collectIds(family, ids);
return ids;
}
private void collectIds(boolean family, List<Integer> into) {
into.add(id);
if (!family) return;
for (ChannelNode child : children) child.collectIds(true, into);
}
}

View File

@@ -2,6 +2,7 @@ package com.ts3client.net;
import com.github.manevolent.ts3j.api.Channel;
import com.github.manevolent.ts3j.api.Client;
import com.github.manevolent.ts3j.command.MultiCommand;
import com.github.manevolent.ts3j.command.SingleCommand;
import com.github.manevolent.ts3j.command.parameter.CommandSingleParameter;
import com.github.manevolent.ts3j.event.*;
@@ -24,6 +25,9 @@ import com.ts3client.sound.SoundNotifier;
import java.io.File;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
@@ -534,6 +538,28 @@ public final class TeamspeakConnection implements TS3Listener {
}, "ts3j-move-channel").start();
}
/**
* Subscribes to (or unsubscribes from) a set of channels in one command. The model
* is left alone: the server answers with the subscription events that update it.
*/
public void setChannelsSubscribed(Collection<Integer> channelIds, boolean subscribed) {
if (channelIds.isEmpty()) return;
List<Integer> ids = List.copyOf(channelIds);
String name = subscribed ? "channelsubscribe" : "channelunsubscribe";
new Thread(() -> {
try {
List<SingleCommand> parts = new ArrayList<>(ids.size());
for (int id : ids) {
parts.add(new SingleCommand(name, ProtocolRole.CLIENT,
new CommandSingleParameter("cid", Integer.toString(id))));
}
client.executeCommand(new MultiCommand(name, ProtocolRole.CLIENT, parts)).complete();
} catch (Exception e) {
error((subscribed ? "Could not subscribe: " : "Could not unsubscribe: ") + rootMessage(e));
}
}, "ts3j-channel-subscribe").start();
}
public void sendChannelMessage(String text) {
new Thread(() -> {
try {
@@ -926,6 +952,23 @@ public final class TeamspeakConnection implements TS3Listener {
}
}
@Override
public void onChannelSubscribed(ChannelSubscribedEvent e) {
setSubscribed(safeInt(e, "cid"), true);
}
@Override
public void onChannelUnsubscribed(ChannelUnsubscribedEvent e) {
setSubscribed(safeInt(e, "cid"), false);
}
private void setSubscribed(int cid, boolean subscribed) {
ChannelNode ch = model.getChannel(cid);
if (ch == null || ch.subscribed == subscribed) return;
ch.subscribed = subscribed;
ui.onModelChanged();
}
@Override
public void onServerEdit(ServerEditedEvent e) {
if (has(e, "virtualserver_name")) model.setServerName(e.get("virtualserver_name"));