diff --git a/ts3-client/core/src/main/java/com/ts3client/net/ClientStatus.java b/ts3-client/core/src/main/java/com/ts3client/net/ClientStatus.java
new file mode 100644
index 0000000..3e27c05
--- /dev/null
+++ b/ts3-client/core/src/main/java/com/ts3client/net/ClientStatus.java
@@ -0,0 +1,29 @@
+package com.ts3client.net;
+
+/**
+ * What the channel tree shows for a client, in the order the official client gives the
+ * states priority: the first that applies wins.
+ */
+public enum ClientStatus {
+ QUERY,
+ SPEAKERS_DISABLED,
+ SPEAKERS_MUTED,
+ MIC_DISABLED,
+ MIC_MUTED,
+ AWAY,
+ COMMANDER_TALKING,
+ COMMANDER,
+ TALKING,
+ IDLE;
+
+ public static ClientStatus of(ClientEntry c) {
+ if (c.isQuery()) return QUERY;
+ if (!c.outputHardware) return SPEAKERS_DISABLED;
+ if (c.outputMuted) return SPEAKERS_MUTED;
+ if (!c.inputHardware) return MIC_DISABLED;
+ if (c.inputMuted) return MIC_MUTED;
+ if (c.away) return AWAY;
+ if (c.channelCommander) return c.talking ? COMMANDER_TALKING : COMMANDER;
+ return c.talking ? TALKING : IDLE;
+ }
+}
diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/Spacers.java b/ts3-client/core/src/main/java/com/ts3client/net/Spacers.java
similarity index 98%
rename from ts3-client/swing/src/main/java/com/ts3client/ui/Spacers.java
rename to ts3-client/core/src/main/java/com/ts3client/net/Spacers.java
index a0f702c..fd0ef43 100644
--- a/ts3-client/swing/src/main/java/com/ts3client/ui/Spacers.java
+++ b/ts3-client/core/src/main/java/com/ts3client/net/Spacers.java
@@ -1,6 +1,5 @@
-package com.ts3client.ui;
+package com.ts3client.net;
-import com.ts3client.net.ChannelNode;
import java.util.HashSet;
import java.util.List;
diff --git a/ts3-client/core/src/main/java/com/ts3client/text/ChatHtml.java b/ts3-client/core/src/main/java/com/ts3client/text/ChatHtml.java
new file mode 100644
index 0000000..d5362ed
--- /dev/null
+++ b/ts3-client/core/src/main/java/com/ts3client/text/ChatHtml.java
@@ -0,0 +1,79 @@
+package com.ts3client.text;
+
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * Builds the entries a chat shows, one {@code
} of HTML each, the form TeamSpeak's own
+ * chat logs are in — so live lines and lines loaded from a log history look alike. The
+ * {@code class} attributes name how a frontend may style them.
+ */
+public final class ChatHtml {
+
+ /** Timestamps and the chat's own notices. */
+ public static final String MUTED_CLASS = "muted";
+ /** A server event: someone joining, leaving, moving, a channel being edited, … */
+ public static final String EVENT_CLASS = "event";
+ /** A sender that cannot be linked to. */
+ public static final String NAME_CLASS = "name";
+
+ private static final DateTimeFormatter TIME = DateTimeFormatter.ofPattern("HH:mm:ss");
+
+ private ChatHtml() {
+ }
+
+ /**
+ * A chat message; the sender becomes a {@code client://} link when their id is known.
+ *
+ * @param text the message as sent, in BBCode
+ */
+ public static String message(int fromId, String fromUniqueId, String from, String text) {
+ String name = BBCode.escape(from);
+ String sender = fromId > 0
+ ? "
" + name + ""
+ : span(NAME_CLASS, name);
+ return div(span(MUTED_CLASS, stamp()) + sender + ": " + BBCode.toHtml(text));
+ }
+
+ /** One of the chat's own notices: connection progress, errors, pokes. */
+ public static String system(String text) {
+ return div(span(MUTED_CLASS, stamp() + BBCode.escape(text)));
+ }
+
+ /**
+ * A server event. It names clients and channels with TeamSpeak's own links, which stay
+ * links; everything else in it is literal text.
+ */
+ public static String serverLog(String text) {
+ return div(span(EVENT_CLASS, stamp() + BBCode.linksToHtml(text)));
+ }
+
+ /**
+ * Brings an entry of TeamSpeak's HTML log into the form of the others: one {@code
},
+ * without the images that log refers to by the native client's own paths.
+ *
+ * @return the entry, or empty for one that shows nothing
+ */
+ public static String logEntry(String html) {
+ if (html == null || html.isBlank()) return "";
+ String normalized = html
+ .replaceAll("(?i)
![]()
]*>", "")
+ .replaceFirst("(?i)^\\s*
\\s*$", "
");
+ return normalized.startsWith("
" + html + "";
+ }
+
+ private static String div(String html) {
+ return "
" + html + "
";
+ }
+}
diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ChatPanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ChatPanel.java
index c1d79f9..440fc0a 100644
--- a/ts3-client/swing/src/main/java/com/ts3client/ui/ChatPanel.java
+++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ChatPanel.java
@@ -3,6 +3,7 @@ package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.text.BBCode;
+import com.ts3client.text.ChatHtml;
import com.ts3client.text.HtmlText;
import com.ts3client.text.TextQuery;
import com.ts3client.text.TsLink;
@@ -33,9 +34,7 @@ import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.io.IOException;
-import java.text.SimpleDateFormat;
import java.util.ArrayList;
-import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -66,7 +65,6 @@ public final class ChatPanel extends JPanel {
private final JTabbedPane tabs = new JTabbedPane(JTabbedPane.BOTTOM);
private final JTextField input = new JTextField();
- private final SimpleDateFormat time = new SimpleDateFormat("HH:mm:ss");
/** Tab strip height, so a tab with no icon still lines up with the others. */
private static final int HEADER_HEIGHT = 20;
@@ -260,8 +258,7 @@ public final class ChatPanel extends JPanel {
/** System notices always land in the server tab. */
public void appendSystem(String text) {
- edt(() -> serverTab.appendLine("
" + stamp()
- + BBCode.escape(text) + ""));
+ edt(() -> serverTab.append(ChatHtml.system(text)));
}
/**
@@ -270,12 +267,11 @@ public final class ChatPanel extends JPanel {
* clickable here — everything else in them is literal text.
*/
public void appendServerLog(String text) {
- edt(() -> serverTab.appendLine("
" + stamp()
- + BBCode.linksToHtml(text) + ""));
+ edt(() -> serverTab.append(ChatHtml.serverLog(text)));
}
public void appendServerLogHtml(String html) {
- edt(() -> serverTab.appendRawHtml(html));
+ edt(() -> serverTab.append(ChatHtml.logEntry(html)));
}
public void appendServerMessage(int fromId, String from, String text) {
@@ -305,10 +301,6 @@ public final class ChatPanel extends JPanel {
edt(() -> privateTab(peerId, peerUniqueId, peerName).appendMessage(fromId, fromUniqueId, from, text));
}
- private String stamp() {
- return "[" + time.format(new Date()) + "] ";
- }
-
private Tab privateTab(int clientId, String nickname) {
return privateTab(clientId, "", nickname);
}
@@ -518,21 +510,14 @@ public final class ChatPanel extends JPanel {
}
void appendMessage(int fromId, String fromUniqueId, String from, String text) {
- String name = BBCode.escape(from);
- String sender = fromId > 0
- ? "
" + name + ""
- : "
" + name + "";
- appendLine("
" + stamp() + ""
- + sender + ": " + BBCode.toHtml(text));
+ append(ChatHtml.message(fromId,
+ fromUniqueId == null || fromUniqueId.isBlank() ? clientUniqueId : fromUniqueId, from, text));
}
void setHistory(List
htmlEntries) {
entries.clear();
if (htmlEntries != null) {
- for (String entry : htmlEntries) entries.add(normalizeTs3LogEntry(entry));
+ for (String entry : htmlEntries) entries.add(ChatHtml.logEntry(entry));
}
render();
}
@@ -563,12 +548,7 @@ public final class ChatPanel extends JPanel {
log.setCaretPosition(noteKey != null ? 0 : log.getDocument().getLength());
}
- void appendLine(String html) {
- appendRawHtml("" + html + "
");
- }
-
- void appendRawHtml(String html) {
- String entry = normalizeTs3LogEntry(html);
+ void append(String entry) {
entries.add(entry);
if (shows(entry)) {
try {
@@ -581,14 +561,5 @@ public final class ChatPanel extends JPanel {
}
if (tabs.getSelectedComponent() != scroll) setUnread(true);
}
-
- private String normalizeTs3LogEntry(String html) {
- if (html == null || html.isBlank()) return "";
- String normalized = html
- .replaceAll("(?i)
]*>", "")
- .replaceFirst("(?i)^\\s*\\s*$", "
");
- return normalized.startsWith("
" + normalized + "
";
- }
}
}
diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/DropIndicatorTree.java b/ts3-client/swing/src/main/java/com/ts3client/ui/DropIndicatorTree.java
index fd9d869..8b717d9 100644
--- a/ts3-client/swing/src/main/java/com/ts3client/ui/DropIndicatorTree.java
+++ b/ts3-client/swing/src/main/java/com/ts3client/ui/DropIndicatorTree.java
@@ -2,6 +2,7 @@ package com.ts3client.ui;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.net.ChannelNode;
+import com.ts3client.net.Spacers;
import com.ts3client.net.ClientEntry;
import com.ts3client.net.ServerModel;
diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabTreeActions.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabTreeActions.java
index 8ef519d..1de4e1e 100644
--- a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabTreeActions.java
+++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabTreeActions.java
@@ -2,6 +2,7 @@ package com.ts3client.ui;
import com.ts3client.contacts.ContactType;
import com.ts3client.net.ChannelNode;
+import com.ts3client.net.Spacers;
import com.ts3client.net.ChannelSettings;
import com.ts3client.net.ClientEntry;
import com.ts3client.net.Group;
diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreeCellRenderer.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreeCellRenderer.java
index 993a6ee..8d1f4a1 100644
--- a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreeCellRenderer.java
+++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreeCellRenderer.java
@@ -4,7 +4,9 @@ import com.ts3client.config.Settings;
import com.ts3client.contacts.Contact;
import com.ts3client.contacts.ContactStore;
import com.ts3client.net.ChannelNode;
+import com.ts3client.net.Spacers;
import com.ts3client.net.ClientEntry;
+import com.ts3client.net.ClientStatus;
import javax.swing.ImageIcon;
import javax.swing.JTree;
@@ -99,18 +101,18 @@ final class ServerTreeCellRenderer extends DefaultTreeCellRenderer {
return Icons.channel(c.subscribed);
}
- /** The client's state, in the order the official client gives them priority. */
private ImageIcon iconFor(ClientEntry cl) {
- if (cl.isQuery()) return Icons.clientQuery();
- if (!cl.outputHardware) return Icons.speakerDisabled();
- if (cl.outputMuted) return Icons.speakerMuted();
- if (!cl.inputHardware) return Icons.micDisabled();
- if (cl.inputMuted) return Icons.micMuted();
- if (cl.away) return Icons.clientAway();
- if (cl.channelCommander) {
- return cl.talking ? Icons.clientCommanderTalking() : Icons.clientCommander();
- }
- if (cl.talking) return Icons.clientTalking();
- return Icons.clientIdle();
+ return switch (ClientStatus.of(cl)) {
+ case QUERY -> Icons.clientQuery();
+ case SPEAKERS_DISABLED -> Icons.speakerDisabled();
+ case SPEAKERS_MUTED -> Icons.speakerMuted();
+ case MIC_DISABLED -> Icons.micDisabled();
+ case MIC_MUTED -> Icons.micMuted();
+ case AWAY -> Icons.clientAway();
+ case COMMANDER_TALKING -> Icons.clientCommanderTalking();
+ case COMMANDER -> Icons.clientCommander();
+ case TALKING -> Icons.clientTalking();
+ case IDLE -> Icons.clientIdle();
+ };
}
}
diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreeDragAndDrop.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreeDragAndDrop.java
index 10e7f1f..9489b0b 100644
--- a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreeDragAndDrop.java
+++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreeDragAndDrop.java
@@ -1,6 +1,7 @@
package com.ts3client.ui;
import com.ts3client.net.ChannelNode;
+import com.ts3client.net.Spacers;
import com.ts3client.net.ClientEntry;
import com.ts3client.net.ServerModel;
import com.ts3client.text.TsLink;
diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java
index 8647930..b68c2e3 100644
--- a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java
+++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java
@@ -5,6 +5,7 @@ import com.ts3client.config.Settings;
import com.ts3client.contacts.ContactStore;
import com.ts3client.contacts.ContactType;
import com.ts3client.net.ChannelNode;
+import com.ts3client.net.Spacers;
import com.ts3client.net.ChannelSettings;
import com.ts3client.net.ClientEntry;
import com.ts3client.net.Group;
diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreeSearch.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreeSearch.java
index e732092..eca15e9 100644
--- a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreeSearch.java
+++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreeSearch.java
@@ -3,6 +3,7 @@ package com.ts3client.ui;
import com.ts3client.contacts.Contact;
import com.ts3client.contacts.ContactStore;
import com.ts3client.net.ChannelNode;
+import com.ts3client.net.Spacers;
import com.ts3client.net.ClientEntry;
import com.ts3client.text.TextQuery;