Tabbed servers: multiple simultaneous connections
Each connection now lives in its own ServerTab with its own tree, info and chat views plus its own mic-mute, deafen, away and commander state. MainFrame keeps only the shell (menus, toolbar, status bar) and shows one tab at a time; the tab bar below the toolbar appears once a second server is open. Incoming voice from every connection mixes into the same output — each connection already renders one line per speaker. Capture is exclusive: a connection only opens the microphone while it holds it, and a toolbar button moves it to the visible tab. The mic follows a disconnect to another connected tab, and push-to-talk drives whoever holds it. Startup bookmarks are no longer mutually exclusive: every flagged bookmark opens its own tab. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,12 +30,13 @@ public final class Bookmarks {
|
||||
if (index >= 0 && index < entries.size()) entries.remove(index);
|
||||
}
|
||||
|
||||
/** First bookmark flagged to connect at startup, or {@code null}. */
|
||||
public Bookmark startupBookmark() {
|
||||
/** Bookmarks flagged to connect at startup, each in its own tab. */
|
||||
public List<Bookmark> startupBookmarks() {
|
||||
List<Bookmark> out = new ArrayList<>();
|
||||
for (Bookmark b : entries) {
|
||||
if (b.connectOnStartup) return b;
|
||||
if (b.connectOnStartup) out.add(b);
|
||||
}
|
||||
return null;
|
||||
return out;
|
||||
}
|
||||
|
||||
public static Bookmarks load() {
|
||||
|
||||
@@ -48,6 +48,14 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
private volatile int selfClientId = -1;
|
||||
private volatile long connectedAtMs;
|
||||
private volatile String serverHost;
|
||||
private volatile int serverPort;
|
||||
|
||||
/**
|
||||
* Whether this connection owns the capture device. Only one connection may
|
||||
* capture at a time, so the frontend hands the microphone to a single
|
||||
* connection and the others keep their (idle) input pipeline.
|
||||
*/
|
||||
private volatile boolean microphoneActive;
|
||||
|
||||
/** Outstanding {@code getconnectioninfo} requests awaiting a server report, keyed by client id. */
|
||||
private final Map<Integer, PendingConnInfo> pendingConnInfo = new ConcurrentHashMap<>();
|
||||
@@ -83,6 +91,41 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
return identity;
|
||||
}
|
||||
|
||||
/** Address this connection was last asked to connect to. */
|
||||
public String getServerHost() {
|
||||
return serverHost == null ? "" : serverHost;
|
||||
}
|
||||
|
||||
public int getServerPort() {
|
||||
return serverPort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives this connection the capture device, or takes it away. Capturing only
|
||||
* starts once connected; calling this before or during a connect is fine, the
|
||||
* requested state is applied as soon as the microphone exists.
|
||||
*/
|
||||
public synchronized void setMicrophoneActive(boolean active) {
|
||||
microphoneActive = active;
|
||||
applyMicrophoneState();
|
||||
}
|
||||
|
||||
public boolean isMicrophoneActive() {
|
||||
return microphoneActive;
|
||||
}
|
||||
|
||||
/** Starts or stops capture to match {@link #microphoneActive}. */
|
||||
private synchronized void applyMicrophoneState() {
|
||||
VoiceInput mic = microphone;
|
||||
if (mic == null || !connected) return;
|
||||
try {
|
||||
if (microphoneActive) mic.start();
|
||||
else mic.stop();
|
||||
} catch (Exception e) {
|
||||
ui.onError("Microphone unavailable: " + rootMessage(e));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- connection lifecycle ----
|
||||
|
||||
/**
|
||||
@@ -112,6 +155,8 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
String channel, String channelPassword) {
|
||||
try {
|
||||
identity = withIdentity;
|
||||
serverHost = address;
|
||||
serverPort = port;
|
||||
|
||||
model.clear();
|
||||
playback = audio.createOutput(settings);
|
||||
@@ -154,7 +199,6 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
|
||||
// Protocol connection established; anything past this point is best-effort.
|
||||
selfClientId = client.getClientId();
|
||||
serverHost = address;
|
||||
fileTransfers = new FileTransferManager(client, () -> serverHost);
|
||||
client.setMicrophone(microphone);
|
||||
connected = true;
|
||||
@@ -167,16 +211,16 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
joinDefaultChannelIfNeeded(channel, channelPassword);
|
||||
ui.onStatus("Connected to " + model.getServerName());
|
||||
|
||||
try {
|
||||
microphone.start();
|
||||
} catch (Exception micError) {
|
||||
ui.onError("Microphone unavailable: " + rootMessage(micError));
|
||||
}
|
||||
// Only captures if this connection holds the microphone.
|
||||
applyMicrophoneState();
|
||||
} catch (Exception e) {
|
||||
connected = false;
|
||||
ui.onError("Connection failed: " + rootMessage(e));
|
||||
ui.onStatus("Disconnected");
|
||||
safeCleanup();
|
||||
// Report the failed attempt as a disconnect too, so the UI leaves the
|
||||
// "connecting" state and the tab becomes reusable.
|
||||
ui.onDisconnected("connection failed");
|
||||
ui.onStatus("Disconnected");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user