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,326 @@
package com.ts3client.ui;
import com.ts3client.net.ConnectionStats;
import com.ts3client.net.TeamspeakConnection;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.SwingUtilities;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.text.DecimalFormat;
/**
* Live "Client Connection Info" window, reachable from a client's right-click
* menu. Mirrors the TeamSpeak 3 dialog: a summary header (address, version,
* platform, idle/connected time, ping, filetransfer) above a
* <em>Total / Speech / Keep Alive / Control</em> tab strip, each tab showing
* that category's packet loss, packet and byte totals and live bandwidth.
*
* <p>Polls the {@link TeamspeakConnection} once a second while open; for the
* local client the figures update live from local counters, for a remote client
* they refresh from the server's connection report.
*/
public final class ConnectionInfoDialog extends JDialog {
private static final int REFRESH_MS = 1000;
private final TeamspeakConnection conn;
private final int clientId;
private final javax.swing.Timer timer;
private final java.util.concurrent.atomic.AtomicBoolean inFlight =
new java.util.concurrent.atomic.AtomicBoolean();
private final JLabel addressValue = value();
private final JLabel versionValue = value();
private final JLabel platformValue = value();
private final JLabel idleValue = value();
private final JLabel connectedValue = value();
private final JLabel pingValue = value();
private final JLabel filetransferValue = value();
private final KindTab totalTab = new KindTab();
private final KindTab speechTab = new KindTab();
private final KindTab keepAliveTab = new KindTab();
private final KindTab controlTab = new KindTab();
public ConnectionInfoDialog(Frame owner, TeamspeakConnection conn, int clientId, String nickname) {
super(owner, "Connection Info — " + nickname, false);
this.conn = conn;
this.clientId = clientId;
JPanel content = new JPanel(new BorderLayout(0, 10));
content.setBackground(Theme.WINDOW_BG);
content.setBorder(BorderFactory.createEmptyBorder(12, 14, 12, 14));
content.add(buildSummary(), BorderLayout.NORTH);
content.add(buildTabs(), BorderLayout.CENTER);
content.add(buildButtons(), BorderLayout.SOUTH);
setContentPane(content);
pack();
setLocationRelativeTo(owner);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosed(WindowEvent e) {
timer.stop();
}
});
timer = new javax.swing.Timer(REFRESH_MS, e -> refresh());
timer.setInitialDelay(0);
timer.start();
}
// ---- construction ----
private JPanel buildSummary() {
JPanel grid = new JPanel(new GridBagLayout());
grid.setBackground(Theme.WINDOW_BG);
int row = 0;
addRow(grid, row++, "Address", addressValue);
addRow(grid, row++, "Client version", versionValue);
addRow(grid, row++, "Platform", platformValue);
addRow(grid, row++, "Ping", pingValue);
addRow(grid, row++, "Idle time", idleValue);
addRow(grid, row++, "Connected", connectedValue);
addRow(grid, row, "Filetransfer (↑/↓)", filetransferValue);
return grid;
}
private JTabbedPane buildTabs() {
JTabbedPane tabs = new JTabbedPane();
tabs.setFont(Theme.UI_FONT);
tabs.setBackground(Theme.WINDOW_BG);
tabs.addTab("Total", totalTab);
tabs.addTab("Speech", speechTab);
tabs.addTab("Keep Alive", keepAliveTab);
tabs.addTab("Control", controlTab);
return tabs;
}
private JPanel buildButtons() {
JPanel bar = new JPanel();
bar.setLayout(new BoxLayout(bar, BoxLayout.X_AXIS));
bar.setBackground(Theme.WINDOW_BG);
bar.add(Box.createHorizontalGlue());
JButton close = new JButton("Close");
close.addActionListener(e -> dispose());
bar.add(close);
return bar;
}
private static JLabel value() {
JLabel l = new JLabel("");
l.setFont(Theme.UI_FONT);
l.setForeground(Theme.TREE_TEXT);
return l;
}
private static void addRow(JPanel grid, int row, String label, JLabel valueLabel) {
GridBagConstraints lc = new GridBagConstraints();
lc.gridx = 0;
lc.gridy = row;
lc.anchor = GridBagConstraints.WEST;
lc.insets = new Insets(2, 0, 2, 16);
JLabel key = new JLabel(label);
key.setFont(Theme.UI_FONT);
key.setForeground(new Color(0x5A6B7B));
grid.add(key, lc);
GridBagConstraints vc = new GridBagConstraints();
vc.gridx = 1;
vc.gridy = row;
vc.weightx = 1;
vc.anchor = GridBagConstraints.WEST;
vc.fill = GridBagConstraints.HORIZONTAL;
vc.insets = new Insets(2, 0, 2, 0);
grid.add(valueLabel, vc);
}
// ---- live update ----
private void refresh() {
if (!conn.isConnected()) {
timer.stop();
return;
}
if (!inFlight.compareAndSet(false, true)) return; // a previous poll is still running
conn.requestConnectionInfo(clientId, stats -> SwingUtilities.invokeLater(() -> {
inFlight.set(false);
apply(stats);
}));
}
private void apply(ConnectionStats s) {
if (!isDisplayable()) return;
addressValue.setText(s.self ? "This computer" : orDash(s.ip));
versionValue.setText(orDash(s.version));
platformValue.setText(orDash(s.platform));
pingValue.setText(formatPing(s.pingMs, s.pingDeviationMs));
idleValue.setText(formatDuration(s.idleTimeMs));
connectedValue.setText(formatDuration(s.connectedTimeMs));
filetransferValue.setText(formatRate(s.filetransferBandwidthSent)
+ " / " + formatRate(s.filetransferBandwidthReceived));
totalTab.update(totalSample(s));
speechTab.update(kindSample(s, ConnectionStats.Kind.SPEECH));
keepAliveTab.update(kindSample(s, ConnectionStats.Kind.KEEPALIVE));
controlTab.update(kindSample(s, ConnectionStats.Kind.CONTROL));
growToFit();
}
/**
* The window is first packed around placeholder text; once real (longer)
* values arrive it may need more room. Grow to fit, but never shrink, so the
* window doesn't jitter as counters tick up each second.
*/
private void growToFit() {
Dimension pref = getContentPane().getPreferredSize();
Dimension have = getContentPane().getSize();
if (pref.width > have.width || pref.height > have.height) {
pack();
}
}
private static Sample totalSample(ConnectionStats s) {
Sample sample = new Sample();
sample.packetLoss = s.packetLoss;
sample.packetsSent = s.packetsSentTotal;
sample.packetsReceived = s.packetsReceivedTotal;
sample.bytesSent = s.bytesSentTotal;
sample.bytesReceived = s.bytesReceivedTotal;
sample.bwSentSecond = s.bandwidthSentLastSecond;
sample.bwReceivedSecond = s.bandwidthReceivedLastSecond;
sample.bwSentMinute = s.bandwidthSentLastMinute;
sample.bwReceivedMinute = s.bandwidthReceivedLastMinute;
return sample;
}
private static Sample kindSample(ConnectionStats s, ConnectionStats.Kind kind) {
Sample sample = new Sample();
ConnectionStats.KindStats k = s.kind(kind);
if (k == null) return sample; // all fields stay unknown (-1)
sample.packetLoss = k.packetLoss;
sample.packetsSent = k.packetsSent;
sample.packetsReceived = k.packetsReceived;
sample.bytesSent = k.bytesSent;
sample.bytesReceived = k.bytesReceived;
sample.bwSentSecond = k.bandwidthSentLastSecond;
sample.bwReceivedSecond = k.bandwidthReceivedLastSecond;
sample.bwSentMinute = k.bandwidthSentLastMinute;
sample.bwReceivedMinute = k.bandwidthReceivedLastMinute;
return sample;
}
// ---- one category tab ----
/** Numeric snapshot fed to a {@link KindTab}; -1 means "unknown". */
private static final class Sample {
double packetLoss = -1;
long packetsSent = -1;
long packetsReceived = -1;
long bytesSent = -1;
long bytesReceived = -1;
long bwSentSecond = -1;
long bwReceivedSecond = -1;
long bwSentMinute = -1;
long bwReceivedMinute = -1;
}
private static final class KindTab extends JPanel {
private final JLabel packetLoss = value();
private final JLabel packets = value();
private final JLabel bytes = value();
private final JLabel bandwidthSecond = value();
private final JLabel bandwidthMinute = value();
KindTab() {
super(new GridBagLayout());
setBackground(Theme.WINDOW_BG);
setBorder(BorderFactory.createEmptyBorder(10, 12, 10, 12));
int row = 0;
addRow(this, row++, "Packet loss", packetLoss);
addRow(this, row++, "Packets (↑/↓)", packets);
addRow(this, row++, "Transferred (↑/↓)", bytes);
addRow(this, row++, "Bandwidth · last second (↑/↓)", bandwidthSecond);
addRow(this, row, "Bandwidth · last minute (↑/↓)", bandwidthMinute);
}
void update(Sample s) {
packetLoss.setText(formatLoss(s.packetLoss));
packets.setText(formatCount(s.packetsSent) + " / " + formatCount(s.packetsReceived));
bytes.setText(formatBytes(s.bytesSent) + " / " + formatBytes(s.bytesReceived));
bandwidthSecond.setText(formatRate(s.bwSentSecond) + " / " + formatRate(s.bwReceivedSecond));
bandwidthMinute.setText(formatRate(s.bwSentMinute) + " / " + formatRate(s.bwReceivedMinute));
}
}
// ---- formatting ----
private static final DecimalFormat GROUPED = new DecimalFormat("#,##0");
private static String orDash(String s) {
return (s == null || s.isEmpty()) ? "" : s;
}
private static String formatPing(double ms, double deviationMs) {
if (ms < 0) return "";
String base = Math.round(ms) + " ms";
if (deviationMs > 0) base += "" + Math.round(deviationMs) + " ms)";
return base;
}
private static String formatLoss(double fraction) {
if (fraction < 0) return "";
return new DecimalFormat("0.00").format(fraction * 100.0) + " %";
}
private static String formatCount(long n) {
return n < 0 ? "" : GROUPED.format(n);
}
private static String formatBytes(long bytes) {
if (bytes < 0) return "";
if (bytes < 1024) return bytes + " B";
double kib = bytes / 1024.0;
if (kib < 1024) return new DecimalFormat("0.0").format(kib) + " KiB";
double mib = kib / 1024.0;
if (mib < 1024) return new DecimalFormat("0.00").format(mib) + " MiB";
return new DecimalFormat("0.00").format(mib / 1024.0) + " GiB";
}
private static String formatRate(long bytesPerSecond) {
return bytesPerSecond < 0 ? "" : formatBytes(bytesPerSecond) + "/s";
}
private static String formatDuration(long ms) {
if (ms < 0) return "";
long totalSeconds = ms / 1000;
long days = totalSeconds / 86400;
long hours = (totalSeconds % 86400) / 3600;
long minutes = (totalSeconds % 3600) / 60;
long seconds = totalSeconds % 60;
StringBuilder sb = new StringBuilder();
if (days > 0) sb.append(days).append("d ");
if (days > 0 || hours > 0) sb.append(hours).append("h ");
if (days > 0 || hours > 0 || minutes > 0) sb.append(minutes).append("m ");
sb.append(seconds).append("s");
return sb.toString();
}
}