Add an away button with global actions and message presets

The toolbar gains an away toggle with a drop-down: it toggles away on the
current server with no message, while the menu carries the global actions
("Set Globally Away", "Set Globally Away Status"), the saved presets and
their editor. Presets live in ~/.ts3jclient/away.properties and are
managed in a list with add/remove and double-click renaming.

Clients in the channel tree now show their away message in brackets after
the nickname instead of their primary server group.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 13:40:19 +00:00
parent 25a78f52fe
commit 245ae5ecc5
9 changed files with 491 additions and 18 deletions

View File

@@ -0,0 +1,89 @@
package com.ts3client.config;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
* Persistent list of away-message presets, stored alongside the settings file.
* Frontend-agnostic: no UI dependencies.
*/
public final class AwayMessages {
private static final File DIR = new File(System.getProperty("user.home"), ".ts3jclient");
private static final File FILE = new File(DIR, "away.properties");
private final List<String> entries = new ArrayList<>();
public List<String> all() {
return entries;
}
public void add(String message) {
entries.add(message == null ? "" : message);
}
public void set(int index, String message) {
if (index >= 0 && index < entries.size()) entries.set(index, message == null ? "" : message);
}
public void remove(int index) {
if (index >= 0 && index < entries.size()) entries.remove(index);
}
public static AwayMessages load() {
AwayMessages m = new AwayMessages();
if (!FILE.isFile()) return m.withDefaults();
Properties p = new Properties();
try (FileInputStream in = new FileInputStream(FILE)) {
p.load(in);
} catch (Exception e) {
return m.withDefaults();
}
int count = parseInt(p.getProperty("count"), 0);
for (int i = 0; i < count; i++) {
String text = p.getProperty("message." + i);
if (text != null && !text.isBlank()) m.entries.add(text);
}
return m;
}
public void save() {
Properties p = new Properties();
p.setProperty("count", Integer.toString(entries.size()));
for (int i = 0; i < entries.size(); i++) {
p.setProperty("message." + i, entries.get(i));
}
try {
if (!DIR.isDirectory()) {
//noinspection ResultOfMethodCallIgnored
DIR.mkdirs();
}
try (FileOutputStream out = new FileOutputStream(FILE)) {
p.store(out, "TS3J client away messages");
}
} catch (Exception ignored) {
}
}
/** First run: seed the presets a fresh client offers. */
private AwayMessages withDefaults() {
entries.add("Away from keyboard");
entries.add("Be right back");
entries.add("Lunch");
entries.add("Busy");
return this;
}
private static int parseInt(String v, int def) {
if (v == null) return def;
try {
return Integer.parseInt(v.trim());
} catch (NumberFormatException e) {
return def;
}
}
}

View File

@@ -22,6 +22,8 @@ public final class ClientEntry {
public boolean inputMuted; // microphone muted (client_input_muted)
public boolean outputMuted; // speakers muted / deafened (client_output_muted)
public boolean away;
/** The message published with the away state, empty when there is none. */
public String awayMessage = "";
public boolean channelCommander;
public boolean self;

View File

@@ -361,6 +361,7 @@ public final class TeamspeakConnection implements TS3Listener {
e.inputMuted = cl.isInputMuted();
e.outputMuted = cl.isOutputMuted();
e.away = cl.isAway();
e.awayMessage = orEmpty(cl.get("client_away_message"));
e.uniqueId = cl.getUniqueIdentifier();
e.serverGroupIds = cl.getServerGroups();
e.channelGroupId = cl.getChannelGroupId();
@@ -581,7 +582,10 @@ public final class TeamspeakConnection implements TS3Listener {
public void setAway(boolean away, String message) {
sound(away ? SoundEvent.STATUS_SET_AWAY : SoundEvent.STATUS_SET_PRESENT);
updateSelf(self -> self.away = away);
updateSelf(self -> {
self.away = away;
self.awayMessage = away && message != null ? message : "";
});
selfUpdate(cmd -> {
cmd.add(new CommandSingleParameter("client_away", away ? "1" : "0"));
cmd.add(new CommandSingleParameter("client_away_message", away && message != null ? message : ""));
@@ -648,6 +652,7 @@ public final class TeamspeakConnection implements TS3Listener {
c.inputMuted = e.isClientInputMuted();
c.outputMuted = e.isClientOutputMuted();
c.away = e.isClientAway();
c.awayMessage = orEmpty(e.get("client_away_message"));
c.uniqueId = orEmpty(e.getUniqueClientIdentifier());
c.serverGroupIds = parseIntList(e.getClientServerGroups());
c.channelGroupId = e.getClientChannelGroupId();
@@ -814,7 +819,14 @@ public final class TeamspeakConnection implements TS3Listener {
if (renamed) c.nickname = e.get("client_nickname");
if (has(e, "client_input_muted")) c.inputMuted = e.getBoolean("client_input_muted");
if (has(e, "client_output_muted")) c.outputMuted = e.getBoolean("client_output_muted");
if (has(e, "client_away")) c.away = e.getBoolean("client_away");
if (has(e, "client_away")) {
c.away = e.getBoolean("client_away");
// Both fields travel together, so an absent message here means "no message"
// — which `has` cannot tell from "not reported".
c.awayMessage = c.away ? orEmpty(e.get("client_away_message")) : "";
} else if (has(e, "client_away_message")) {
c.awayMessage = e.get("client_away_message");
}
if (has(e, "client_talk_power")) c.talkPower = e.getInt("client_talk_power");
if (has(e, "client_is_channel_commander"))
c.channelCommander = e.getBoolean("client_is_channel_commander");