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;