Show server and channel group icons in the tree

Download the icons a server advertises for its groups over the file
transfer channel, cache them under the config directory, and draw them as
a badge strip on each client's row, right-aligned against the tree's
visible edge like the TeamSpeak client does. Groups without a custom icon
fall back to the bundled default set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 07:06:23 +00:00
parent 0333b92ce5
commit 7c86a1f166
18 changed files with 597 additions and 38 deletions

View File

@@ -2,12 +2,15 @@ package com.ts3client.ui;
import com.ts3client.net.ChannelNode;
import com.ts3client.net.ClientEntry;
import com.ts3client.net.Group;
import com.ts3client.net.IconRepository;
import com.ts3client.net.ServerModel;
import com.ts3client.text.BBCode;
import javax.swing.BorderFactory;
import javax.swing.JEditorPane;
import javax.swing.JScrollPane;
import java.io.File;
import java.util.List;
/**
@@ -67,15 +70,27 @@ public final class InfoPanel extends JScrollPane {
setHtml(sb.toString());
}
public void showClient(ClientEntry cl, ServerModel model) {
public void showClient(ClientEntry cl, ServerModel model, IconRepository icons) {
StringBuilder sb = new StringBuilder();
sb.append(heading(esc(cl.nickname) + (cl.self ? " <span style='color:#8a8a8a'>(you)</span>" : "")));
List<String> serverGroups = model.serverGroupNames(cl.serverGroupIds);
row(sb, "Server groups", serverGroups.isEmpty() ? "" : esc(String.join(", ", serverGroups)));
List<Group> serverGroups = model.serverGroupsOf(cl.serverGroupIds);
if (serverGroups.isEmpty()) {
List<String> names = model.serverGroupNames(cl.serverGroupIds);
row(sb, "Server groups", names.isEmpty() ? "" : esc(String.join(", ", names)));
} else {
StringBuilder groups = new StringBuilder();
for (Group g : serverGroups) {
if (groups.length() > 0) groups.append(", ");
groups.append(iconTag(g, icons)).append(esc(g.name));
}
row(sb, "Server groups", groups.toString());
}
String channelGroup = model.channelGroupName(cl.channelGroupId);
row(sb, "Channel group", channelGroup != null ? esc(channelGroup) : "#" + cl.channelGroupId);
Group channelGroup = model.channelGroup(cl.channelGroupId);
row(sb, "Channel group", channelGroup != null
? iconTag(channelGroup, icons) + esc(channelGroup.name)
: "#" + cl.channelGroupId);
if (cl.talkPower != 0) row(sb, "Talk power", Integer.toString(cl.talkPower));
if (!cl.platform.isEmpty()) row(sb, "Platform", esc(cl.platform));
@@ -97,6 +112,18 @@ public final class InfoPanel extends JScrollPane {
pane.setCaretPosition(0);
}
/**
* An inline {@code <img>} for a group's icon, or nothing while the icon is missing
* or still downloading. Swing's HTML renderer only loads images by URL, so the icon
* is served from the repository's on-disk cache.
*/
private static String iconTag(Group group, IconRepository icons) {
if (group.iconId == 0) return "";
File file = icons.file(group.iconId);
if (file == null) return "";
return "<img src='" + file.toURI() + "' width='14' height='14'> ";
}
private static String heading(String text) {
return "<div style='font-weight:bold;font-size:13px;margin-bottom:4px'>" + text + "</div>";
}