Report a chosen client version and operating system

Other clients see the version and platform from clientinit in the info
panel, and until now that was whatever ts3j sends. The new Client Version
tab picks one instead.

Servers refuse a version they do not know with "client is modified", so
free text is not enough: the list holds genuine version/platform/signature
triples from released clients, newest first per platform. The fields stay
editable for a version whose signature is known from elsewhere, and a
refused connection now says where to change it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 20:06:09 +00:00
parent 97148cfe9a
commit ef95b6305d
5 changed files with 334 additions and 1 deletions

View File

@@ -0,0 +1,190 @@
package com.ts3client.ui;
import com.ts3client.config.ClientVersion;
import com.ts3client.config.Settings;
import javax.swing.BorderFactory;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JTextField;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
/**
* Options page for the version and operating system reported to the server, which
* is what other clients see next to the nickname ("3.6.2 on Windows").
*
* <p>Servers verify TeamSpeak's signature over the version and platform and refuse
* the connection ("client is modified") when it does not match, so the page is
* built around the signed versions in {@link ClientVersion#PRESETS}. The fields
* behind them can still be edited by hand for a version whose signature is known
* from somewhere else. Changes take effect on the next connect.
*/
final class ClientVersionPanel extends JPanel {
/** Combo entry that leaves the three fields to the user. */
private static final String CUSTOM = "Custom…";
private final Settings settings;
private final JCheckBox enabled = new JCheckBox("Report a custom version and operating system");
private final JComboBox<Object> presetCombo = new JComboBox<>();
private final JTextField versionField = new JTextField(24);
private final JComboBox<String> platformCombo = new JComboBox<>();
private final JTextField signField = new JTextField(24);
/** Set while a preset is being copied into the fields, so that does not count as an edit. */
private boolean filling;
ClientVersionPanel(Settings settings) {
super(new BorderLayout(0, 8));
this.settings = settings;
setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
add(buildForm(), BorderLayout.NORTH);
load();
enabled.addActionListener(e -> updateEnabled());
updateEnabled();
}
/** Copies the edited version into the settings; the caller saves them. */
void apply() {
settings.customVersion = enabled.isSelected();
settings.clientVersion = versionField.getText().trim();
settings.clientPlatform = String.valueOf(platformCombo.getEditor().getItem()).trim();
settings.clientVersionSign = signField.getText().trim();
}
private JComponent buildForm() {
JPanel p = new JPanel(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(2, 2, 2, 2);
c.anchor = GridBagConstraints.WEST;
c.fill = GridBagConstraints.HORIZONTAL;
presetCombo.addItem(CUSTOM);
for (ClientVersion preset : ClientVersion.PRESETS) presetCombo.addItem(preset);
presetCombo.setRenderer(new DefaultListCellRenderer() {
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index,
boolean selected, boolean focus) {
super.getListCellRendererComponent(list, value, index, selected, focus);
setText(value instanceof ClientVersion v ? v.label() : String.valueOf(value));
return this;
}
});
presetCombo.setToolTipText("Versions with a signature TeamSpeak issued for them");
presetCombo.addActionListener(e -> onPresetSelected());
platformCombo.setEditable(true);
for (String platform : ClientVersion.PLATFORMS) platformCombo.addItem(platform);
versionField.setToolTipText("Version string, e.g. 3.6.2 [Build: 1695203293]");
platformCombo.setToolTipText("Operating system shown to other clients");
signField.setToolTipText("TeamSpeak's signature for this version and platform");
versionField.getDocument().addDocumentListener(new EditListener());
signField.getDocument().addDocumentListener(new EditListener());
platformCombo.addActionListener(e -> onFieldEdited());
int row = 0;
c.gridx = 0;
c.gridy = row++;
c.gridwidth = 2;
p.add(enabled, c);
c.gridwidth = 1;
addRow(p, c, row++, new JLabel("Version:"), presetCombo);
addRow(p, c, row++, new JLabel("Version string:"), versionField);
addRow(p, c, row++, new JLabel("Operating system:"), platformCombo);
addRow(p, c, row++, new JLabel("Signature:"), signField);
c.gridx = 0;
c.gridy = row;
c.gridwidth = 2;
p.add(new JLabel("<html><body style='width:320px'>The three fields belong together: the "
+ "signature is TeamSpeak's, over that version and operating system. A server "
+ "answers \"client is modified\" and refuses the connection when it does not "
+ "accept the version — made up, or older than the server's minimum — so prefer "
+ "a recent one from the list. Takes effect the next time you connect."
+ "</body></html>"), c);
return p;
}
private void load() {
enabled.setSelected(settings.customVersion);
ClientVersion preset = ClientVersion.match(
settings.clientVersion, settings.clientPlatform, settings.clientVersionSign);
fill(preset != null ? preset
: new ClientVersion("", settings.clientVersion, settings.clientPlatform, settings.clientVersionSign));
presetCombo.setSelectedItem(preset != null ? preset : CUSTOM);
}
private void onPresetSelected() {
if (presetCombo.getSelectedItem() instanceof ClientVersion preset) fill(preset);
}
/** A hand-edited field no longer describes the selected preset. */
private void onFieldEdited() {
if (filling) return;
ClientVersion preset = ClientVersion.match(versionField.getText().trim(),
String.valueOf(platformCombo.getEditor().getItem()).trim(), signField.getText().trim());
presetCombo.setSelectedItem(preset != null ? preset : CUSTOM);
}
private void fill(ClientVersion version) {
filling = true;
try {
versionField.setText(version.version());
platformCombo.setSelectedItem(version.platform());
platformCombo.getEditor().setItem(version.platform());
signField.setText(version.sign());
} finally {
filling = false;
}
}
private void updateEnabled() {
boolean on = enabled.isSelected();
presetCombo.setEnabled(on);
versionField.setEnabled(on);
platformCombo.setEnabled(on);
signField.setEnabled(on);
}
/** Notices any typing in the version or signature field. */
private final class EditListener implements javax.swing.event.DocumentListener {
@Override
public void insertUpdate(javax.swing.event.DocumentEvent e) {
onFieldEdited();
}
@Override
public void removeUpdate(javax.swing.event.DocumentEvent e) {
onFieldEdited();
}
@Override
public void changedUpdate(javax.swing.event.DocumentEvent e) {
onFieldEdited();
}
}
private static void addRow(JPanel p, GridBagConstraints c, int row, JLabel label, Component field) {
c.gridx = 0;
c.gridy = row;
c.weightx = 0;
p.add(label, c);
c.gridx = 1;
c.weightx = 1;
p.add(field, c);
}
}

View File

@@ -57,6 +57,7 @@ public final class SettingsDialog extends JDialog {
private NotificationsPanel notificationsPanel;
private IconPackPanel iconPackPanel;
private ClientVersionPanel clientVersionPanel;
private JComboBox<AudioDevices.Device> inputCombo;
private JComboBox<AudioDevices.Device> outputCombo;
@@ -107,6 +108,8 @@ public final class SettingsDialog extends JDialog {
tabs.addTab("Notifications", notificationsPanel);
iconPackPanel = new IconPackPanel(settings);
tabs.addTab("Design", iconPackPanel);
clientVersionPanel = new ClientVersionPanel(settings);
tabs.addTab("Client Version", scrollable(clientVersionPanel));
JPanel buttons = new JPanel(new BorderLayout());
JPanel right = new JPanel();
@@ -551,6 +554,7 @@ public final class SettingsDialog extends JDialog {
settings.music = musicCheck.isSelected();
notificationsPanel.apply();
iconPackPanel.apply();
clientVersionPanel.apply();
settings.save();
if (liveMic != null) {