Move spacers, client status and chat entries into core
The Android client needs them too: which state a client's icon shows, how spacer channels are named, and the HTML a chat line is written as. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.ts3client.ui;
|
package com.ts3client.net;
|
||||||
|
|
||||||
import com.ts3client.net.ChannelNode;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -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 <div>} 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
|
||||||
|
? "<a class=\"" + BBCode.IDENTITY_LINK_CLASS + "\" href=\""
|
||||||
|
+ BBCode.escape(TsLink.clientHref(fromId, fromUniqueId == null ? "" : fromUniqueId, from))
|
||||||
|
+ "\">" + name + "</a>"
|
||||||
|
: 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 <div>},
|
||||||
|
* 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)<img\\b[^>]*>", "")
|
||||||
|
.replaceFirst("(?i)^\\s*<p\\b", "<div")
|
||||||
|
.replaceFirst("(?i)</p>\\s*$", "</div>");
|
||||||
|
return normalized.startsWith("<div") ? normalized : div(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String stamp() {
|
||||||
|
return "[" + TIME.format(LocalTime.now()) + "] ";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String span(String cssClass, String html) {
|
||||||
|
return "<span class=\"" + cssClass + "\">" + html + "</span>";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String div(String html) {
|
||||||
|
return "<div>" + html + "</div>";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package com.ts3client.ui;
|
|||||||
import com.formdev.flatlaf.util.ScaledEmptyBorder;
|
import com.formdev.flatlaf.util.ScaledEmptyBorder;
|
||||||
import com.formdev.flatlaf.util.UIScale;
|
import com.formdev.flatlaf.util.UIScale;
|
||||||
import com.ts3client.text.BBCode;
|
import com.ts3client.text.BBCode;
|
||||||
|
import com.ts3client.text.ChatHtml;
|
||||||
import com.ts3client.text.HtmlText;
|
import com.ts3client.text.HtmlText;
|
||||||
import com.ts3client.text.TextQuery;
|
import com.ts3client.text.TextQuery;
|
||||||
import com.ts3client.text.TsLink;
|
import com.ts3client.text.TsLink;
|
||||||
@@ -33,9 +34,7 @@ import java.awt.event.MouseAdapter;
|
|||||||
import java.awt.event.MouseEvent;
|
import java.awt.event.MouseEvent;
|
||||||
import java.awt.event.MouseWheelEvent;
|
import java.awt.event.MouseWheelEvent;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -66,7 +65,6 @@ public final class ChatPanel extends JPanel {
|
|||||||
|
|
||||||
private final JTabbedPane tabs = new JTabbedPane(JTabbedPane.BOTTOM);
|
private final JTabbedPane tabs = new JTabbedPane(JTabbedPane.BOTTOM);
|
||||||
private final JTextField input = new JTextField();
|
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. */
|
/** Tab strip height, so a tab with no icon still lines up with the others. */
|
||||||
private static final int HEADER_HEIGHT = 20;
|
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. */
|
/** System notices always land in the server tab. */
|
||||||
public void appendSystem(String text) {
|
public void appendSystem(String text) {
|
||||||
edt(() -> serverTab.appendLine("<span class=\"muted\">" + stamp()
|
edt(() -> serverTab.append(ChatHtml.system(text)));
|
||||||
+ BBCode.escape(text) + "</span>"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -270,12 +267,11 @@ public final class ChatPanel extends JPanel {
|
|||||||
* clickable here — everything else in them is literal text.
|
* clickable here — everything else in them is literal text.
|
||||||
*/
|
*/
|
||||||
public void appendServerLog(String text) {
|
public void appendServerLog(String text) {
|
||||||
edt(() -> serverTab.appendLine("<span class=\"event\">" + stamp()
|
edt(() -> serverTab.append(ChatHtml.serverLog(text)));
|
||||||
+ BBCode.linksToHtml(text) + "</span>"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void appendServerLogHtml(String html) {
|
public void appendServerLogHtml(String html) {
|
||||||
edt(() -> serverTab.appendRawHtml(html));
|
edt(() -> serverTab.append(ChatHtml.logEntry(html)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void appendServerMessage(int fromId, String from, String text) {
|
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));
|
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) {
|
private Tab privateTab(int clientId, String nickname) {
|
||||||
return privateTab(clientId, "", 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) {
|
void appendMessage(int fromId, String fromUniqueId, String from, String text) {
|
||||||
String name = BBCode.escape(from);
|
append(ChatHtml.message(fromId,
|
||||||
String sender = fromId > 0
|
fromUniqueId == null || fromUniqueId.isBlank() ? clientUniqueId : fromUniqueId, from, text));
|
||||||
? "<a class=\"" + BBCode.IDENTITY_LINK_CLASS + "\" href=\""
|
|
||||||
+ BBCode.escape(TsLink.clientHref(fromId,
|
|
||||||
fromUniqueId == null || fromUniqueId.isBlank() ? clientUniqueId : fromUniqueId,
|
|
||||||
from)) + "\">" + name + "</a>"
|
|
||||||
: "<span class=\"name\">" + name + "</span>";
|
|
||||||
appendLine("<span class=\"muted\">" + stamp() + "</span>"
|
|
||||||
+ sender + ": " + BBCode.toHtml(text));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setHistory(List<String> htmlEntries) {
|
void setHistory(List<String> htmlEntries) {
|
||||||
entries.clear();
|
entries.clear();
|
||||||
if (htmlEntries != null) {
|
if (htmlEntries != null) {
|
||||||
for (String entry : htmlEntries) entries.add(normalizeTs3LogEntry(entry));
|
for (String entry : htmlEntries) entries.add(ChatHtml.logEntry(entry));
|
||||||
}
|
}
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
@@ -563,12 +548,7 @@ public final class ChatPanel extends JPanel {
|
|||||||
log.setCaretPosition(noteKey != null ? 0 : log.getDocument().getLength());
|
log.setCaretPosition(noteKey != null ? 0 : log.getDocument().getLength());
|
||||||
}
|
}
|
||||||
|
|
||||||
void appendLine(String html) {
|
void append(String entry) {
|
||||||
appendRawHtml("<div>" + html + "</div>");
|
|
||||||
}
|
|
||||||
|
|
||||||
void appendRawHtml(String html) {
|
|
||||||
String entry = normalizeTs3LogEntry(html);
|
|
||||||
entries.add(entry);
|
entries.add(entry);
|
||||||
if (shows(entry)) {
|
if (shows(entry)) {
|
||||||
try {
|
try {
|
||||||
@@ -581,14 +561,5 @@ public final class ChatPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
if (tabs.getSelectedComponent() != scroll) setUnread(true);
|
if (tabs.getSelectedComponent() != scroll) setUnread(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String normalizeTs3LogEntry(String html) {
|
|
||||||
if (html == null || html.isBlank()) return "";
|
|
||||||
String normalized = html
|
|
||||||
.replaceAll("(?i)<img\\b[^>]*>", "")
|
|
||||||
.replaceFirst("(?i)^\\s*<p\\b", "<div")
|
|
||||||
.replaceFirst("(?i)</p>\\s*$", "</div>");
|
|
||||||
return normalized.startsWith("<div") ? normalized : "<div>" + normalized + "</div>";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.ts3client.ui;
|
|||||||
|
|
||||||
import com.formdev.flatlaf.util.UIScale;
|
import com.formdev.flatlaf.util.UIScale;
|
||||||
import com.ts3client.net.ChannelNode;
|
import com.ts3client.net.ChannelNode;
|
||||||
|
import com.ts3client.net.Spacers;
|
||||||
import com.ts3client.net.ClientEntry;
|
import com.ts3client.net.ClientEntry;
|
||||||
import com.ts3client.net.ServerModel;
|
import com.ts3client.net.ServerModel;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.ts3client.ui;
|
|||||||
|
|
||||||
import com.ts3client.contacts.ContactType;
|
import com.ts3client.contacts.ContactType;
|
||||||
import com.ts3client.net.ChannelNode;
|
import com.ts3client.net.ChannelNode;
|
||||||
|
import com.ts3client.net.Spacers;
|
||||||
import com.ts3client.net.ChannelSettings;
|
import com.ts3client.net.ChannelSettings;
|
||||||
import com.ts3client.net.ClientEntry;
|
import com.ts3client.net.ClientEntry;
|
||||||
import com.ts3client.net.Group;
|
import com.ts3client.net.Group;
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import com.ts3client.config.Settings;
|
|||||||
import com.ts3client.contacts.Contact;
|
import com.ts3client.contacts.Contact;
|
||||||
import com.ts3client.contacts.ContactStore;
|
import com.ts3client.contacts.ContactStore;
|
||||||
import com.ts3client.net.ChannelNode;
|
import com.ts3client.net.ChannelNode;
|
||||||
|
import com.ts3client.net.Spacers;
|
||||||
import com.ts3client.net.ClientEntry;
|
import com.ts3client.net.ClientEntry;
|
||||||
|
import com.ts3client.net.ClientStatus;
|
||||||
|
|
||||||
import javax.swing.ImageIcon;
|
import javax.swing.ImageIcon;
|
||||||
import javax.swing.JTree;
|
import javax.swing.JTree;
|
||||||
@@ -99,18 +101,18 @@ final class ServerTreeCellRenderer extends DefaultTreeCellRenderer {
|
|||||||
return Icons.channel(c.subscribed);
|
return Icons.channel(c.subscribed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The client's state, in the order the official client gives them priority. */
|
|
||||||
private ImageIcon iconFor(ClientEntry cl) {
|
private ImageIcon iconFor(ClientEntry cl) {
|
||||||
if (cl.isQuery()) return Icons.clientQuery();
|
return switch (ClientStatus.of(cl)) {
|
||||||
if (!cl.outputHardware) return Icons.speakerDisabled();
|
case QUERY -> Icons.clientQuery();
|
||||||
if (cl.outputMuted) return Icons.speakerMuted();
|
case SPEAKERS_DISABLED -> Icons.speakerDisabled();
|
||||||
if (!cl.inputHardware) return Icons.micDisabled();
|
case SPEAKERS_MUTED -> Icons.speakerMuted();
|
||||||
if (cl.inputMuted) return Icons.micMuted();
|
case MIC_DISABLED -> Icons.micDisabled();
|
||||||
if (cl.away) return Icons.clientAway();
|
case MIC_MUTED -> Icons.micMuted();
|
||||||
if (cl.channelCommander) {
|
case AWAY -> Icons.clientAway();
|
||||||
return cl.talking ? Icons.clientCommanderTalking() : Icons.clientCommander();
|
case COMMANDER_TALKING -> Icons.clientCommanderTalking();
|
||||||
}
|
case COMMANDER -> Icons.clientCommander();
|
||||||
if (cl.talking) return Icons.clientTalking();
|
case TALKING -> Icons.clientTalking();
|
||||||
return Icons.clientIdle();
|
case IDLE -> Icons.clientIdle();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.ts3client.ui;
|
package com.ts3client.ui;
|
||||||
|
|
||||||
import com.ts3client.net.ChannelNode;
|
import com.ts3client.net.ChannelNode;
|
||||||
|
import com.ts3client.net.Spacers;
|
||||||
import com.ts3client.net.ClientEntry;
|
import com.ts3client.net.ClientEntry;
|
||||||
import com.ts3client.net.ServerModel;
|
import com.ts3client.net.ServerModel;
|
||||||
import com.ts3client.text.TsLink;
|
import com.ts3client.text.TsLink;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.ts3client.config.Settings;
|
|||||||
import com.ts3client.contacts.ContactStore;
|
import com.ts3client.contacts.ContactStore;
|
||||||
import com.ts3client.contacts.ContactType;
|
import com.ts3client.contacts.ContactType;
|
||||||
import com.ts3client.net.ChannelNode;
|
import com.ts3client.net.ChannelNode;
|
||||||
|
import com.ts3client.net.Spacers;
|
||||||
import com.ts3client.net.ChannelSettings;
|
import com.ts3client.net.ChannelSettings;
|
||||||
import com.ts3client.net.ClientEntry;
|
import com.ts3client.net.ClientEntry;
|
||||||
import com.ts3client.net.Group;
|
import com.ts3client.net.Group;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.ts3client.ui;
|
|||||||
import com.ts3client.contacts.Contact;
|
import com.ts3client.contacts.Contact;
|
||||||
import com.ts3client.contacts.ContactStore;
|
import com.ts3client.contacts.ContactStore;
|
||||||
import com.ts3client.net.ChannelNode;
|
import com.ts3client.net.ChannelNode;
|
||||||
|
import com.ts3client.net.Spacers;
|
||||||
import com.ts3client.net.ClientEntry;
|
import com.ts3client.net.ClientEntry;
|
||||||
import com.ts3client.text.TextQuery;
|
import com.ts3client.text.TextQuery;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user