Add identity management
Manage several TeamSpeak identities like the official client instead of using one auto-generated identity file. - IdentityStore/IdentityEntry keep each identity as a TeamSpeak-format INI in ~/.ts3jclient/identities, so files interchange with the TS3 client; supports create, import, export, rename, remove and a cancellable security-level search. A legacy identity.ini is migrated in on first load. - Settings hold the default identity; bookmarks may pin their own, falling back to the default when unset. - TeamspeakConnection now connects with the identity it is handed. - Swing: Tools -> Identities manager, plus an identity drop-down in the connect and bookmark forms. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ package com.ts3client.ui;
|
||||
|
||||
import com.ts3client.config.Bookmark;
|
||||
import com.ts3client.config.Bookmarks;
|
||||
import com.ts3client.config.IdentityStore;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.Box;
|
||||
@@ -26,14 +27,17 @@ import java.util.function.Consumer;
|
||||
public final class BookmarksDialog extends JDialog {
|
||||
|
||||
private final Bookmarks bookmarks;
|
||||
private final IdentityStore identities;
|
||||
private final Consumer<Bookmark> onConnect;
|
||||
private final Runnable onChanged;
|
||||
private final DefaultListModel<Bookmark> listModel = new DefaultListModel<>();
|
||||
private final JList<Bookmark> list = new JList<>(listModel);
|
||||
|
||||
public BookmarksDialog(Frame owner, Bookmarks bookmarks, Consumer<Bookmark> onConnect, Runnable onChanged) {
|
||||
public BookmarksDialog(Frame owner, Bookmarks bookmarks, IdentityStore identities,
|
||||
Consumer<Bookmark> onConnect, Runnable onChanged) {
|
||||
super(owner, "Manage Bookmarks", true);
|
||||
this.bookmarks = bookmarks;
|
||||
this.identities = identities;
|
||||
this.onConnect = onConnect;
|
||||
this.onChanged = onChanged;
|
||||
|
||||
@@ -120,6 +124,7 @@ public final class BookmarksDialog extends JDialog {
|
||||
JTextField port = new JTextField(Integer.toString(b.port));
|
||||
JTextField nick = new JTextField(b.nickname == null ? "" : b.nickname);
|
||||
JPasswordField password = new JPasswordField(b.password == null ? "" : b.password);
|
||||
IdentityChooser identity = new IdentityChooser(identities, true, b.identityId);
|
||||
|
||||
JPanel form = new JPanel(new GridLayout(0, 1, 0, 2));
|
||||
form.add(new JLabel("Label:"));
|
||||
@@ -132,6 +137,8 @@ public final class BookmarksDialog extends JDialog {
|
||||
form.add(nick);
|
||||
form.add(new JLabel("Password (optional):"));
|
||||
form.add(password);
|
||||
form.add(new JLabel("Identity:"));
|
||||
form.add(identity);
|
||||
|
||||
int result = JOptionPane.showConfirmDialog(this, form,
|
||||
"Bookmark", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
|
||||
@@ -150,6 +157,7 @@ public final class BookmarksDialog extends JDialog {
|
||||
}
|
||||
b.nickname = nick.getText().trim();
|
||||
b.password = new String(password.getPassword());
|
||||
b.identityId = identity.getSelectedIdentityId();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.ts3client.ui;
|
||||
|
||||
import com.ts3client.config.IdentityStore;
|
||||
import com.ts3client.config.Settings;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
@@ -23,10 +24,11 @@ public final class ConnectDialog extends JDialog {
|
||||
private final JTextField portField;
|
||||
private final JTextField nickField;
|
||||
private final JPasswordField passwordField;
|
||||
private final IdentityChooser identityChooser;
|
||||
|
||||
private boolean confirmed;
|
||||
|
||||
public ConnectDialog(Frame owner, Settings settings) {
|
||||
public ConnectDialog(Frame owner, Settings settings, IdentityStore identities) {
|
||||
super(owner, "Connect to Server", true);
|
||||
|
||||
String addr = settings.lastAddress;
|
||||
@@ -44,6 +46,7 @@ public final class ConnectDialog extends JDialog {
|
||||
portField = new JTextField(Integer.toString(port), 6);
|
||||
nickField = new JTextField(settings.nickname, 18);
|
||||
passwordField = new JPasswordField(settings.serverPassword, 18);
|
||||
identityChooser = new IdentityChooser(identities, false, settings.defaultIdentityId);
|
||||
|
||||
JPanel form = new JPanel(new GridBagLayout());
|
||||
form.setBorder(BorderFactory.createEmptyBorder(12, 12, 8, 12));
|
||||
@@ -57,6 +60,7 @@ public final class ConnectDialog extends JDialog {
|
||||
add(form, c, row++, "Port:", portField);
|
||||
add(form, c, row++, "Nickname:", nickField);
|
||||
add(form, c, row++, "Password (optional):", passwordField);
|
||||
add(form, c, row++, "Identity:", identityChooser);
|
||||
|
||||
JPanel buttons = new JPanel(new BorderLayout());
|
||||
JPanel right = new JPanel();
|
||||
@@ -116,4 +120,9 @@ public final class ConnectDialog extends JDialog {
|
||||
public String getPassword() {
|
||||
return new String(passwordField.getPassword());
|
||||
}
|
||||
|
||||
/** Chosen identity id, or an empty string to fall back to the default identity. */
|
||||
public String getIdentityId() {
|
||||
return identityChooser.getSelectedIdentityId();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
package com.ts3client.ui;
|
||||
|
||||
import com.ts3client.config.Bookmarks;
|
||||
import com.ts3client.config.IdentityEntry;
|
||||
import com.ts3client.config.IdentityStore;
|
||||
import com.ts3client.config.Settings;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.Box;
|
||||
import javax.swing.BoxLayout;
|
||||
import javax.swing.DefaultListCellRenderer;
|
||||
import javax.swing.DefaultListModel;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JFileChooser;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JList;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Component;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Frame;
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.GridBagLayout;
|
||||
import java.awt.Insets;
|
||||
import java.io.File;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Identity manager, mirroring the TeamSpeak 3 client's Tools → Identities:
|
||||
* create, import, export, rename and delete identities, mark one as the default
|
||||
* and raise an identity's security level.
|
||||
*/
|
||||
public final class IdentitiesDialog extends JDialog {
|
||||
|
||||
private final IdentityStore identities;
|
||||
private final Settings settings;
|
||||
private final Bookmarks bookmarks;
|
||||
private final Runnable onChanged;
|
||||
|
||||
private final DefaultListModel<IdentityEntry> listModel = new DefaultListModel<>();
|
||||
private final JList<IdentityEntry> list = new JList<>(listModel);
|
||||
|
||||
private final JTextField nameField = new JTextField();
|
||||
private final JTextField uidField = new JTextField();
|
||||
private final JTextField levelField = new JTextField();
|
||||
|
||||
public IdentitiesDialog(Frame owner, IdentityStore identities, Settings settings,
|
||||
Bookmarks bookmarks, Runnable onChanged) {
|
||||
super(owner, "Identities", true);
|
||||
this.identities = identities;
|
||||
this.settings = settings;
|
||||
this.bookmarks = bookmarks;
|
||||
this.onChanged = onChanged;
|
||||
|
||||
list.setCellRenderer(new DefaultListCellRenderer() {
|
||||
@Override
|
||||
public Component getListCellRendererComponent(JList<?> l, Object value, int index,
|
||||
boolean isSelected, boolean cellHasFocus) {
|
||||
String text = value instanceof IdentityEntry ? label((IdentityEntry) value) : String.valueOf(value);
|
||||
return super.getListCellRendererComponent(l, text, index, isSelected, cellHasFocus);
|
||||
}
|
||||
});
|
||||
list.addListSelectionListener(e -> showDetails(list.getSelectedValue()));
|
||||
list.setVisibleRowCount(10);
|
||||
|
||||
JScrollPane scroll = new JScrollPane(list);
|
||||
scroll.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
||||
scroll.setPreferredSize(new Dimension(220, 260));
|
||||
|
||||
JPanel buttons = new JPanel();
|
||||
buttons.setLayout(new BoxLayout(buttons, BoxLayout.Y_AXIS));
|
||||
buttons.setBorder(BorderFactory.createEmptyBorder(8, 0, 8, 8));
|
||||
addButton(buttons, "New…", this::newIdentity);
|
||||
addButton(buttons, "Import…", this::importIdentity);
|
||||
addButton(buttons, "Export…", this::exportSelected);
|
||||
addButton(buttons, "Rename…", this::renameSelected);
|
||||
addButton(buttons, "Set as default", this::setSelectedDefault);
|
||||
addButton(buttons, "Improve security…", this::improveSelected);
|
||||
addButton(buttons, "Remove", this::removeSelected);
|
||||
buttons.add(Box.createVerticalGlue());
|
||||
addButton(buttons, "Close", this::dispose);
|
||||
|
||||
getContentPane().setLayout(new BorderLayout());
|
||||
getContentPane().add(scroll, BorderLayout.WEST);
|
||||
getContentPane().add(buildDetails(), BorderLayout.CENTER);
|
||||
getContentPane().add(buttons, BorderLayout.EAST);
|
||||
|
||||
reload();
|
||||
if (!listModel.isEmpty()) list.setSelectedIndex(0);
|
||||
|
||||
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||
pack();
|
||||
setMinimumSize(new Dimension(640, 320));
|
||||
setLocationRelativeTo(owner);
|
||||
}
|
||||
|
||||
private JPanel buildDetails() {
|
||||
JPanel form = new JPanel(new GridBagLayout());
|
||||
form.setBorder(BorderFactory.createEmptyBorder(12, 4, 12, 8));
|
||||
GridBagConstraints c = new GridBagConstraints();
|
||||
c.insets = new Insets(4, 4, 4, 4);
|
||||
c.anchor = GridBagConstraints.NORTHWEST;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
nameField.setEditable(false);
|
||||
uidField.setEditable(false);
|
||||
levelField.setEditable(false);
|
||||
|
||||
int row = 0;
|
||||
addRow(form, c, row++, "Name:", nameField);
|
||||
addRow(form, c, row++, "Unique ID:", uidField);
|
||||
addRow(form, c, row++, "Security level:", levelField);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = row;
|
||||
c.gridwidth = 2;
|
||||
c.weighty = 1;
|
||||
c.fill = GridBagConstraints.BOTH;
|
||||
form.add(Box.createGlue(), c);
|
||||
return form;
|
||||
}
|
||||
|
||||
private void addRow(JPanel form, GridBagConstraints c, int row, String label, Component field) {
|
||||
c.gridx = 0;
|
||||
c.gridy = row;
|
||||
c.weightx = 0;
|
||||
form.add(new JLabel(label), c);
|
||||
c.gridx = 1;
|
||||
c.weightx = 1;
|
||||
form.add(field, c);
|
||||
}
|
||||
|
||||
private void addButton(JPanel panel, String text, Runnable action) {
|
||||
JButton b = new JButton(text);
|
||||
b.setAlignmentX(LEFT_ALIGNMENT);
|
||||
b.setMaximumSize(new Dimension(Integer.MAX_VALUE, b.getPreferredSize().height));
|
||||
b.addActionListener(e -> action.run());
|
||||
panel.add(b);
|
||||
panel.add(Box.createVerticalStrut(4));
|
||||
}
|
||||
|
||||
private String label(IdentityEntry e) {
|
||||
return e.getId().equals(settings.defaultIdentityId) ? e.getName() + " (default)" : e.getName();
|
||||
}
|
||||
|
||||
private void reload() {
|
||||
IdentityEntry selected = list.getSelectedValue();
|
||||
listModel.clear();
|
||||
for (IdentityEntry e : identities.all()) listModel.addElement(e);
|
||||
if (selected != null && listModel.contains(selected)) {
|
||||
list.setSelectedValue(selected, true);
|
||||
} else if (!listModel.isEmpty()) {
|
||||
list.setSelectedIndex(0);
|
||||
} else {
|
||||
showDetails(null);
|
||||
}
|
||||
if (onChanged != null) onChanged.run();
|
||||
}
|
||||
|
||||
private void showDetails(IdentityEntry e) {
|
||||
nameField.setText(e == null ? "" : e.getName());
|
||||
uidField.setText(e == null ? "" : e.getUniqueId());
|
||||
levelField.setText(e == null ? "" : Integer.toString(e.getSecurityLevel()));
|
||||
}
|
||||
|
||||
// ---- actions ----
|
||||
|
||||
private void newIdentity() {
|
||||
String name = JOptionPane.showInputDialog(this, "Name for the new identity:", "New identity");
|
||||
if (name == null || name.trim().isEmpty()) return;
|
||||
|
||||
int level = askLevel("Security level to generate:", IdentityStore.DEFAULT_SECURITY_LEVEL);
|
||||
if (level < 0) return;
|
||||
|
||||
runWithProgress("Generating identity…", (cancelled, status) -> {
|
||||
IdentityEntry created = identities.generate(name.trim(), level);
|
||||
if (settings.defaultIdentityId == null || identities.byId(settings.defaultIdentityId) == null) {
|
||||
settings.defaultIdentityId = created.getId();
|
||||
settings.save();
|
||||
}
|
||||
return created;
|
||||
}, false);
|
||||
}
|
||||
|
||||
private void importIdentity() {
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
chooser.setDialogTitle("Import identity");
|
||||
chooser.setFileFilter(new FileNameExtensionFilter("TeamSpeak identity (*.ini)", "ini"));
|
||||
if (chooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) return;
|
||||
File file = chooser.getSelectedFile();
|
||||
try {
|
||||
IdentityEntry imported = identities.importFile(file, null);
|
||||
if (identities.byId(settings.defaultIdentityId) == null) {
|
||||
settings.defaultIdentityId = imported.getId();
|
||||
settings.save();
|
||||
}
|
||||
reload();
|
||||
list.setSelectedValue(imported, true);
|
||||
} catch (Exception e) {
|
||||
error("Could not import identity", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void exportSelected() {
|
||||
IdentityEntry e = list.getSelectedValue();
|
||||
if (e == null) return;
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
chooser.setDialogTitle("Export identity");
|
||||
chooser.setFileFilter(new FileNameExtensionFilter("TeamSpeak identity (*.ini)", "ini"));
|
||||
chooser.setSelectedFile(new File(e.getName().replaceAll("[^\\w.-]+", "_") + ".ini"));
|
||||
if (chooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) return;
|
||||
|
||||
File target = chooser.getSelectedFile();
|
||||
if (target.exists() && JOptionPane.showConfirmDialog(this,
|
||||
target.getName() + " already exists. Overwrite?", "Export identity",
|
||||
JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
identities.exportTo(e, target);
|
||||
JOptionPane.showMessageDialog(this, "Identity exported to\n" + target.getAbsolutePath(),
|
||||
"Export identity", JOptionPane.INFORMATION_MESSAGE);
|
||||
} catch (Exception ex) {
|
||||
error("Could not export identity", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void renameSelected() {
|
||||
IdentityEntry e = list.getSelectedValue();
|
||||
if (e == null) return;
|
||||
String name = JOptionPane.showInputDialog(this, "Identity name:", e.getName());
|
||||
if (name == null || name.trim().isEmpty()) return;
|
||||
try {
|
||||
identities.rename(e, name.trim());
|
||||
reload();
|
||||
} catch (Exception ex) {
|
||||
error("Could not rename identity", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void setSelectedDefault() {
|
||||
IdentityEntry e = list.getSelectedValue();
|
||||
if (e == null) return;
|
||||
settings.defaultIdentityId = e.getId();
|
||||
settings.save();
|
||||
reload();
|
||||
}
|
||||
|
||||
private void removeSelected() {
|
||||
IdentityEntry e = list.getSelectedValue();
|
||||
if (e == null) return;
|
||||
int result = JOptionPane.showConfirmDialog(this,
|
||||
"Delete identity \"" + e.getName() + "\"?\n\n"
|
||||
+ "Server groups and permissions tied to it will be lost.\n"
|
||||
+ "Export it first if you may need it again.",
|
||||
"Remove identity", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
|
||||
if (result != JOptionPane.YES_OPTION) return;
|
||||
identities.remove(e, settings, bookmarks);
|
||||
reload();
|
||||
}
|
||||
|
||||
private void improveSelected() {
|
||||
IdentityEntry e = list.getSelectedValue();
|
||||
if (e == null) return;
|
||||
int level = askLevel("Target security level (current: " + e.getSecurityLevel() + "):",
|
||||
Math.max(e.getSecurityLevel() + 1, IdentityStore.DEFAULT_SECURITY_LEVEL));
|
||||
if (level < 0) return;
|
||||
if (level <= e.getSecurityLevel()) {
|
||||
JOptionPane.showMessageDialog(this, "The identity already reaches that level.");
|
||||
return;
|
||||
}
|
||||
runWithProgress("Improving security level…",
|
||||
(cancelled, status) -> {
|
||||
identities.improveSecurity(e, level, (best, offset) -> {
|
||||
status.accept("Improving security level… reached " + best + " of " + level);
|
||||
return !cancelled.get();
|
||||
});
|
||||
return e;
|
||||
}, true);
|
||||
}
|
||||
|
||||
private int askLevel(String prompt, int suggestion) {
|
||||
String s = JOptionPane.showInputDialog(this, prompt, Integer.toString(suggestion));
|
||||
if (s == null) return -1;
|
||||
try {
|
||||
int level = Integer.parseInt(s.trim());
|
||||
if (level < 0 || level > 40) throw new NumberFormatException();
|
||||
return level;
|
||||
} catch (NumberFormatException e) {
|
||||
JOptionPane.showMessageDialog(this, "Enter a security level between 0 and 40.");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/** Work that produces the identity to select once it finishes. */
|
||||
private interface IdentityTask {
|
||||
IdentityEntry run(AtomicBoolean cancelled, Consumer<String> status) throws Exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a long identity computation off the EDT behind a modal progress dialog.
|
||||
* Both generating and improving an identity are proof-of-work searches whose
|
||||
* duration grows exponentially with the security level.
|
||||
*/
|
||||
private void runWithProgress(String message, IdentityTask task, boolean cancellable) {
|
||||
AtomicBoolean cancelled = new AtomicBoolean(false);
|
||||
|
||||
JDialog progress = new JDialog(this, "Please wait", true);
|
||||
JLabel status = new JLabel(message);
|
||||
JPanel panel = new JPanel(new BorderLayout(8, 8));
|
||||
panel.setBorder(BorderFactory.createEmptyBorder(16, 16, 12, 16));
|
||||
panel.add(status, BorderLayout.CENTER);
|
||||
if (cancellable) {
|
||||
JButton cancel = new JButton("Stop");
|
||||
cancel.setToolTipText("Keep the best level found so far");
|
||||
cancel.addActionListener(e -> cancelled.set(true));
|
||||
JPanel south = new JPanel();
|
||||
south.add(cancel);
|
||||
panel.add(south, BorderLayout.SOUTH);
|
||||
}
|
||||
progress.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
|
||||
progress.getContentPane().add(panel);
|
||||
progress.pack();
|
||||
progress.setLocationRelativeTo(this);
|
||||
|
||||
Consumer<String> report = text -> SwingUtilities.invokeLater(() -> status.setText(text));
|
||||
Thread worker = new Thread(() -> {
|
||||
IdentityEntry result = null;
|
||||
Exception failure = null;
|
||||
try {
|
||||
result = task.run(cancelled, report);
|
||||
} catch (Exception e) {
|
||||
failure = e;
|
||||
}
|
||||
IdentityEntry selected = result;
|
||||
Exception thrown = failure;
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
progress.dispose();
|
||||
if (thrown != null) {
|
||||
error("Identity operation failed", thrown);
|
||||
return;
|
||||
}
|
||||
reload();
|
||||
if (selected != null) list.setSelectedValue(selected, true);
|
||||
});
|
||||
}, "identity-work");
|
||||
|
||||
// Start only once the modal dialog is up, so a fast task can't dispose it
|
||||
// before it becomes visible (which would leave it on screen forever).
|
||||
SwingUtilities.invokeLater(worker::start);
|
||||
progress.setVisible(true);
|
||||
}
|
||||
|
||||
private void error(String what, Exception e) {
|
||||
String detail = e.getMessage() == null ? e.toString() : e.getMessage();
|
||||
JOptionPane.showMessageDialog(this, what + ":\n" + detail, "Identities", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.ts3client.ui;
|
||||
|
||||
import com.ts3client.config.IdentityEntry;
|
||||
import com.ts3client.config.IdentityStore;
|
||||
|
||||
import javax.swing.DefaultListCellRenderer;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JList;
|
||||
import java.awt.Component;
|
||||
|
||||
/**
|
||||
* Drop-down for picking the identity to connect with. Optionally offers a
|
||||
* "use the default identity" entry for places where the choice may stay unset
|
||||
* (bookmarks), so changing the default later applies retroactively.
|
||||
*/
|
||||
final class IdentityChooser extends JComboBox<Object> {
|
||||
|
||||
private static final String USE_DEFAULT = "‹Default identity›";
|
||||
|
||||
IdentityChooser(IdentityStore identities, boolean allowDefault, String selectedId) {
|
||||
if (allowDefault) addItem(USE_DEFAULT);
|
||||
for (IdentityEntry e : identities.all()) addItem(e);
|
||||
|
||||
IdentityEntry selected = identities.byId(selectedId);
|
||||
if (selected != null) {
|
||||
setSelectedItem(selected);
|
||||
} else if (allowDefault) {
|
||||
setSelectedItem(USE_DEFAULT);
|
||||
}
|
||||
|
||||
setRenderer(new DefaultListCellRenderer() {
|
||||
@Override
|
||||
public Component getListCellRendererComponent(JList<?> list, Object value, int index,
|
||||
boolean isSelected, boolean cellHasFocus) {
|
||||
String text = value instanceof IdentityEntry
|
||||
? ((IdentityEntry) value).getName()
|
||||
: String.valueOf(value);
|
||||
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Selected identity id, or an empty string when the default is to be used. */
|
||||
String getSelectedIdentityId() {
|
||||
Object v = getSelectedItem();
|
||||
return v instanceof IdentityEntry ? ((IdentityEntry) v).getId() : "";
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import com.ts3client.audio.AudioBackend;
|
||||
import com.ts3client.audio.desktop.JavaSoundAudioBackend;
|
||||
import com.ts3client.config.Bookmark;
|
||||
import com.ts3client.config.Bookmarks;
|
||||
import com.ts3client.config.IdentityEntry;
|
||||
import com.ts3client.config.IdentityStore;
|
||||
import com.ts3client.config.Settings;
|
||||
import com.ts3client.net.ChannelNode;
|
||||
import com.ts3client.net.ClientEntry;
|
||||
@@ -43,9 +45,13 @@ public final class MainFrame extends JFrame implements ConnectionListener, Serve
|
||||
|
||||
private final Settings settings;
|
||||
private final Bookmarks bookmarks = Bookmarks.load();
|
||||
private final IdentityStore identities;
|
||||
private final AudioBackend audio = new JavaSoundAudioBackend();
|
||||
private TeamspeakConnection conn;
|
||||
|
||||
/** Identity of the current/last connection, so it can be saved into a bookmark. */
|
||||
private String currentIdentityId = "";
|
||||
|
||||
private JMenu bookmarksMenu;
|
||||
private JCheckBoxMenuItem awayItem;
|
||||
private JCheckBoxMenuItem commanderItem;
|
||||
@@ -73,6 +79,7 @@ public final class MainFrame extends JFrame implements ConnectionListener, Serve
|
||||
public MainFrame(Settings settings) {
|
||||
super("TS3J — TeamSpeak 3 Java Client");
|
||||
this.settings = settings;
|
||||
this.identities = IdentityStore.load(settings);
|
||||
|
||||
setIconImage(Icons.app().getImage());
|
||||
// We tear the connection down ourselves on close, so don't let Swing kill the JVM.
|
||||
@@ -165,8 +172,12 @@ public final class MainFrame extends JFrame implements ConnectionListener, Serve
|
||||
self.add(rename);
|
||||
|
||||
JMenu tools = new JMenu("Tools");
|
||||
JMenuItem identitiesItem = new JMenuItem("Identities…");
|
||||
identitiesItem.addActionListener(e -> showIdentities());
|
||||
JMenuItem options = new JMenuItem("Options…");
|
||||
options.addActionListener(e -> showSettings());
|
||||
tools.add(identitiesItem);
|
||||
tools.addSeparator();
|
||||
tools.add(options);
|
||||
|
||||
JMenu help = new JMenu("Help");
|
||||
@@ -193,7 +204,7 @@ public final class MainFrame extends JFrame implements ConnectionListener, Serve
|
||||
JMenuItem addCurrent = new JMenuItem("Add current server…");
|
||||
addCurrent.addActionListener(e -> addCurrentServerBookmark());
|
||||
JMenuItem manage = new JMenuItem("Manage bookmarks…");
|
||||
manage.addActionListener(e -> new BookmarksDialog(this, bookmarks,
|
||||
manage.addActionListener(e -> new BookmarksDialog(this, bookmarks, identities,
|
||||
this::connectToBookmark, this::rebuildBookmarksMenu).setVisible(true));
|
||||
bookmarksMenu.add(addCurrent);
|
||||
bookmarksMenu.add(manage);
|
||||
@@ -290,13 +301,16 @@ public final class MainFrame extends JFrame implements ConnectionListener, Serve
|
||||
"Connect", JOptionPane.INFORMATION_MESSAGE);
|
||||
return;
|
||||
}
|
||||
ConnectDialog dlg = new ConnectDialog(this, settings);
|
||||
ConnectDialog dlg = new ConnectDialog(this, settings, identities);
|
||||
dlg.setVisible(true);
|
||||
if (!dlg.isConfirmed()) return;
|
||||
startConnection(dlg.getAddress(), dlg.getPort(), dlg.getNickname(), dlg.getPassword());
|
||||
startConnection(dlg.getAddress(), dlg.getPort(), dlg.getNickname(), dlg.getPassword(), dlg.getIdentityId());
|
||||
}
|
||||
|
||||
private void startConnection(String address, int port, String nickname, String password) {
|
||||
/**
|
||||
* @param identityId identity to use, or empty for the default one
|
||||
*/
|
||||
private void startConnection(String address, int port, String nickname, String password, String identityId) {
|
||||
if (conn.isConnected()) {
|
||||
JOptionPane.showMessageDialog(this, "Already connected. Disconnect first.",
|
||||
"Connect", JOptionPane.INFORMATION_MESSAGE);
|
||||
@@ -307,12 +321,28 @@ public final class MainFrame extends JFrame implements ConnectionListener, Serve
|
||||
settings.serverPassword = password;
|
||||
settings.save();
|
||||
chatPanel.appendSystem("Connecting to " + address + ":" + port + " …");
|
||||
conn.connect(address, port, nickname, password);
|
||||
|
||||
// Resolving may have to generate a first identity, so keep it off the EDT.
|
||||
onStatus("Loading identity…");
|
||||
new Thread(() -> {
|
||||
final IdentityEntry entry;
|
||||
try {
|
||||
entry = identities.resolve(settings, identityId);
|
||||
} catch (Exception e) {
|
||||
onError("Could not load identity: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
currentIdentityId = entry.getId();
|
||||
chatPanel.appendSystem("Using identity \"" + entry.getName() + "\".");
|
||||
});
|
||||
conn.connect(address, port, nickname, password, entry.getIdentity());
|
||||
}, "identity-resolve").start();
|
||||
}
|
||||
|
||||
private void connectToBookmark(Bookmark b) {
|
||||
String nick = (b.nickname != null && !b.nickname.isBlank()) ? b.nickname : settings.nickname;
|
||||
startConnection(b.address, b.port, nick, b.password);
|
||||
startConnection(b.address, b.port, nick, b.password, b.identityId);
|
||||
}
|
||||
|
||||
private void addCurrentServerBookmark() {
|
||||
@@ -328,7 +358,9 @@ public final class MainFrame extends JFrame implements ConnectionListener, Serve
|
||||
}
|
||||
String label = JOptionPane.showInputDialog(this, "Bookmark label:", addr);
|
||||
if (label == null) return;
|
||||
bookmarks.add(new Bookmark(label.trim(), addr, port, settings.nickname, settings.serverPassword));
|
||||
Bookmark bookmark = new Bookmark(label.trim(), addr, port, settings.nickname, settings.serverPassword);
|
||||
bookmark.identityId = currentIdentityId;
|
||||
bookmarks.add(bookmark);
|
||||
bookmarks.save();
|
||||
rebuildBookmarksMenu();
|
||||
}
|
||||
@@ -371,6 +403,10 @@ public final class MainFrame extends JFrame implements ConnectionListener, Serve
|
||||
}
|
||||
}
|
||||
|
||||
private void showIdentities() {
|
||||
new IdentitiesDialog(this, identities, settings, bookmarks, null).setVisible(true);
|
||||
}
|
||||
|
||||
private void showSettings() {
|
||||
SettingsDialog dlg = new SettingsDialog(this, settings,
|
||||
conn.getMicrophone(), conn.getPlayback(), () -> {
|
||||
|
||||
Reference in New Issue
Block a user