Initial commit: TS3J TeamSpeak 3 Java client

Swing desktop client (core/desktop/swing Maven modules) built on the
ts3j protocol library, included as a submodule. Native Opus voice with
voice-activation detection, push-to-talk, and audio pre-processing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 22:01:17 +00:00
commit 0f76258a05
48 changed files with 6347 additions and 0 deletions

View File

@@ -0,0 +1,100 @@
package com.ts3client.ui;
import com.ts3client.net.ChannelNode;
import com.ts3client.net.ClientEntry;
import com.ts3client.net.ServerModel;
import javax.swing.BorderFactory;
import javax.swing.JEditorPane;
import javax.swing.JScrollPane;
import java.util.List;
/**
* Read-only detail view for the currently selected channel or client, mirroring
* the TeamSpeak 3 info box: channel topic/description, or a client's server and
* channel groups, platform and version.
*/
public final class InfoPanel extends JScrollPane {
private final JEditorPane pane = new JEditorPane();
public InfoPanel() {
pane.setContentType("text/html");
pane.setEditable(false);
pane.setBackground(Theme.CHAT_BG);
pane.setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8));
setViewportView(pane);
setBorder(BorderFactory.createLineBorder(new java.awt.Color(0xD0D0D0)));
clear();
}
public void clear() {
setHtml("<i style='color:#8a8a8a'>Select a channel or client to see details.</i>");
}
public void showChannel(ChannelNode ch) {
StringBuilder sb = new StringBuilder();
sb.append(heading(esc(ch.name)));
row(sb, "Type", ch.permanent ? "Permanent" : "Temporary");
if (ch.maxClients >= 0) row(sb, "Max clients", Integer.toString(ch.maxClients));
if (ch.hasPassword) row(sb, "Password", "protected");
if (!ch.topic.isEmpty()) row(sb, "Topic", esc(ch.topic));
sb.append("<hr>");
if (ch.description != null && !ch.description.isEmpty()) {
sb.append("<div>").append(multiline(ch.description)).append("</div>");
} else if (ch.descriptionLoaded) {
sb.append("<i style='color:#8a8a8a'>No description.</i>");
} else {
sb.append("<i style='color:#8a8a8a'>Loading description…</i>");
}
setHtml(sb.toString());
}
public void showClient(ClientEntry cl, ServerModel model) {
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)));
String channelGroup = model.channelGroupName(cl.channelGroupId);
row(sb, "Channel group", channelGroup != null ? esc(channelGroup) : "#" + cl.channelGroupId);
if (cl.talkPower != 0) row(sb, "Talk power", Integer.toString(cl.talkPower));
if (!cl.platform.isEmpty()) row(sb, "Platform", esc(cl.platform));
if (!cl.version.isEmpty()) row(sb, "Version", esc(cl.version));
if (cl.away) row(sb, "Status", "Away");
if (cl.inputMuted) row(sb, "Microphone", "muted");
if (cl.outputMuted) row(sb, "Speakers", "muted");
if (!cl.uniqueId.isEmpty()) row(sb, "Unique ID", esc(cl.uniqueId));
if (cl.description != null && !cl.description.isEmpty()) {
sb.append("<hr><div>").append(multiline(cl.description)).append("</div>");
}
setHtml(sb.toString());
}
private void setHtml(String body) {
pane.setText("<html><body style='font-family:sans-serif;font-size:11px;color:#202020'>"
+ body + "</body></html>");
pane.setCaretPosition(0);
}
private static String heading(String text) {
return "<div style='font-weight:bold;font-size:13px;margin-bottom:4px'>" + text + "</div>";
}
private static void row(StringBuilder sb, String label, String value) {
sb.append("<div style='margin:1px 0'><span style='color:#5a6b7b'>")
.append(label).append(":</span> ").append(value).append("</div>");
}
private static String multiline(String text) {
return esc(text).replace("\n", "<br>");
}
private static String esc(String s) {
if (s == null) return "";
return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
}
}