Import bookmarks and identities from the TeamSpeak 3 client

The official client keeps what a myTeamSpeak account synchronises as protobuf
Item_Data blobs in the ProtobufItems table of settings.db. Decode them, adopt
them on first run when we have no identity yet, and offer a re-import from
Manage Bookmarks and Identities. The settings.db reader moves from contacts to
a new teamspeak package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 19:02:52 +00:00
parent bab89f9b50
commit dbb425403a
20 changed files with 805 additions and 76 deletions

View File

@@ -3,6 +3,9 @@ package com.ts3client.ui;
import com.ts3client.config.Bookmark;
import com.ts3client.config.Bookmarks;
import com.ts3client.config.IdentityStore;
import com.ts3client.config.Settings;
import com.ts3client.teamspeak.TeamSpeakImporter;
import com.ts3client.teamspeak.TeamSpeakSettingsDb;
import javax.swing.BorderFactory;
import javax.swing.Box;
@@ -26,6 +29,7 @@ import java.awt.Frame;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.io.File;
import java.util.function.Consumer;
/**
@@ -36,6 +40,8 @@ import java.util.function.Consumer;
public final class BookmarksDialog extends JDialog {
private final Bookmarks bookmarks;
private final IdentityStore identities;
private final Settings settings;
private final Consumer<Bookmark> onConnect;
private final Runnable onChanged;
private final DefaultListModel<Bookmark> listModel = new DefaultListModel<>();
@@ -56,10 +62,12 @@ public final class BookmarksDialog extends JDialog {
/** Bookmark the form currently shows, so edits can be flushed back into it. */
private Bookmark editing;
public BookmarksDialog(Frame owner, Bookmarks bookmarks, IdentityStore identities,
public BookmarksDialog(Frame owner, Bookmarks bookmarks, IdentityStore identities, Settings settings,
Consumer<Bookmark> onConnect, Runnable onChanged) {
super(owner, "Manage Bookmarks", true);
this.bookmarks = bookmarks;
this.identities = identities;
this.settings = settings;
this.onConnect = onConnect;
this.onChanged = onChanged;
this.identity = new IdentityChooser(identities, true, "");
@@ -151,17 +159,23 @@ public final class BookmarksDialog extends JDialog {
buttons.setBorder(BorderFactory.createEmptyBorder(0, 8, 8, 8));
addButton(buttons, "Add", this::addBookmark);
addButton(buttons, "Remove", this::removeSelected);
if (TeamSpeakSettingsDb.locate() != null) {
addButton(buttons, "Import from TeamSpeak 3…", this::importFromTeamSpeak)
.setToolTipText("Add the bookmarks of the TeamSpeak 3 client installed on this computer, "
+ "along with the identities they connect with");
}
buttons.add(Box.createHorizontalGlue());
addButton(buttons, "Connect", this::connectSelected);
addButton(buttons, "Close", this::closeDialog);
return buttons;
}
private void addButton(JPanel panel, String text, Runnable action) {
private JButton addButton(JPanel panel, String text, Runnable action) {
JButton b = new JButton(text);
b.addActionListener(e -> action.run());
panel.add(b);
panel.add(Box.createHorizontalStrut(4));
return b;
}
// ---- Form <-> model ----
@@ -228,6 +242,26 @@ public final class BookmarksDialog extends JDialog {
persist();
}
private void importFromTeamSpeak() {
File db = TeamSpeakSettingsDb.locate();
if (db == null) return;
commit();
try {
TeamSpeakImporter.Result result = new TeamSpeakImporter(identities, bookmarks, settings).importBookmarks(db);
identity.reload();
reload();
if (result.bookmarksAdded() > 0) list.setSelectedIndex(listModel.size() - result.bookmarksAdded());
persist();
JOptionPane.showMessageDialog(this, result.bookmarks() + " bookmark(s) read from " + db
+ "\n" + result.bookmarksAdded() + " new, " + result.bookmarksKnown() + " already present; "
+ result.identitiesAdded() + " identit" + (result.identitiesAdded() == 1 ? "y" : "ies") + " imported.",
"Import from TeamSpeak 3", JOptionPane.INFORMATION_MESSAGE);
} catch (Exception e) {
JOptionPane.showMessageDialog(this, "Could not read " + db + ":\n" + e.getMessage(),
"Import from TeamSpeak 3", JOptionPane.ERROR_MESSAGE);
}
}
private void connectSelected() {
commit();
Bookmark b = list.getSelectedValue();

View File

@@ -5,7 +5,7 @@ import com.ts3client.contacts.Contact;
import com.ts3client.contacts.ContactStore;
import com.ts3client.contacts.ContactType;
import com.ts3client.contacts.NicknameDisplay;
import com.ts3client.contacts.TeamSpeakSettingsDb;
import com.ts3client.teamspeak.TeamSpeakSettingsDb;
import com.ts3client.text.TsLink;
import javax.swing.BorderFactory;

View File

@@ -4,6 +4,8 @@ import com.ts3client.config.Bookmarks;
import com.ts3client.config.IdentityEntry;
import com.ts3client.config.IdentityStore;
import com.ts3client.config.Settings;
import com.ts3client.teamspeak.TeamSpeakImporter;
import com.ts3client.teamspeak.TeamSpeakSettingsDb;
import javax.swing.BorderFactory;
import javax.swing.Box;
@@ -79,6 +81,9 @@ public final class IdentitiesDialog extends JDialog {
buttons.setBorder(BorderFactory.createEmptyBorder(8, 0, 8, 8));
addButton(buttons, "New…", this::newIdentity);
addButton(buttons, "Import…", this::importIdentity);
if (TeamSpeakSettingsDb.locate() != null) {
addButton(buttons, "Import from TeamSpeak 3…", this::importFromTeamSpeak);
}
addButton(buttons, "Export…", this::exportSelected);
addButton(buttons, "Rename…", this::renameSelected);
addButton(buttons, "Set as default", this::setSelectedDefault);
@@ -209,6 +214,21 @@ public final class IdentitiesDialog extends JDialog {
}
}
private void importFromTeamSpeak() {
File db = TeamSpeakSettingsDb.locate();
if (db == null) return;
try {
TeamSpeakImporter.Result result = new TeamSpeakImporter(identities, bookmarks, settings).importIdentities(db);
reload();
JOptionPane.showMessageDialog(this, result.identities() + " identit" + (result.identities() == 1 ? "y" : "ies")
+ " read from " + db + "\n" + result.identitiesAdded() + " new, "
+ result.identitiesKnown() + " already present.",
"Import from TeamSpeak 3", JOptionPane.INFORMATION_MESSAGE);
} catch (Exception e) {
error("Could not read " + db, e);
}
}
private void exportSelected() {
IdentityEntry e = list.getSelectedValue();
if (e == null) return;

View File

@@ -23,9 +23,7 @@ final class IdentityChooser extends JComboBox<Object> {
IdentityChooser(IdentityStore identities, boolean allowDefault, String selectedId) {
this.identities = identities;
this.allowDefault = allowDefault;
if (allowDefault) addItem(USE_DEFAULT);
for (IdentityEntry e : identities.all()) addItem(e);
reload();
setSelectedIdentityId(selectedId);
setRenderer(new DefaultListCellRenderer() {
@@ -40,6 +38,15 @@ final class IdentityChooser extends JComboBox<Object> {
});
}
/** Re-reads the store's identities, keeping the selection where it still exists. */
void reload() {
String selected = getSelectedIdentityId();
removeAllItems();
if (allowDefault) addItem(USE_DEFAULT);
for (IdentityEntry e : identities.all()) addItem(e);
setSelectedIdentityId(selected);
}
/** Selected identity id, or an empty string when the default is to be used. */
String getSelectedIdentityId() {
Object v = getSelectedItem();

View File

@@ -8,6 +8,7 @@ import com.ts3client.config.Bookmark;
import com.ts3client.config.Bookmarks;
import com.ts3client.config.IdentityStore;
import com.ts3client.config.Settings;
import com.ts3client.teamspeak.TeamSpeakImporter;
import com.ts3client.contacts.ContactStore;
import com.ts3client.net.ChannelNode;
import com.ts3client.sound.SoundNotifier;
@@ -85,6 +86,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
super("TS3J — TeamSpeak 3 Java Client");
this.settings = settings;
this.identities = IdentityStore.load(settings);
new TeamSpeakImporter(identities, bookmarks, settings).seedIfFresh();
this.sounds = new SoundNotifier(settings);
this.soundPlayer = audio.createSoundPlayer(settings);
this.sounds.setPlayer(soundPlayer);
@@ -235,7 +237,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
@Override
public void onManageBookmarks() {
new BookmarksDialog(MainFrame.this, bookmarks, identities,
new BookmarksDialog(MainFrame.this, bookmarks, identities, settings,
MainFrame.this::connectToBookmark, menuBar::rebuildBookmarks).setVisible(true);
}
});