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:
@@ -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("<span class=\"muted\">" + stamp()
|
||||
+ BBCode.escape(text) + "</span>"));
|
||||
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("<span class=\"event\">" + stamp()
|
||||
+ BBCode.linksToHtml(text) + "</span>"));
|
||||
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
|
||||
? "<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));
|
||||
append(ChatHtml.message(fromId,
|
||||
fromUniqueId == null || fromUniqueId.isBlank() ? clientUniqueId : fromUniqueId, from, text));
|
||||
}
|
||||
|
||||
void setHistory(List<String> 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("<div>" + html + "</div>");
|
||||
}
|
||||
|
||||
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)<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.ts3client.net.ChannelNode;
|
||||
import com.ts3client.net.Spacers;
|
||||
import com.ts3client.net.ClientEntry;
|
||||
import com.ts3client.net.ServerModel;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
package com.ts3client.ui;
|
||||
|
||||
import com.ts3client.net.ChannelNode;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Parses TeamSpeak 3 "spacer" channel names — cosmetic root channels used as
|
||||
* separators, e.g. {@code [spacer0]---}, {@code [*spacer1]=}, {@code [cspacer]Rules}.
|
||||
* The tag selects alignment ({@code l}/{@code c}/{@code r}) or fill ({@code *}).
|
||||
*/
|
||||
public final class Spacers {
|
||||
|
||||
/** Result of parsing a spacer name. */
|
||||
public static final class Spacer {
|
||||
public final char align; // 'l', 'c', 'r', or '*' (repeat/fill)
|
||||
public final String caption;
|
||||
|
||||
Spacer(char align, String caption) {
|
||||
this.align = align;
|
||||
this.caption = caption;
|
||||
}
|
||||
}
|
||||
|
||||
/** The tag's identifying part, which is what keeps two spacers' names apart. */
|
||||
private static final Pattern TAG = Pattern.compile("^\\[(?:\\*|[lcr])?spacer([^\\]]*)\\]");
|
||||
|
||||
private static final Pattern PATTERN =
|
||||
Pattern.compile("^\\[(\\*|[lcr])?spacer[^\\]]*\\](.*)$");
|
||||
|
||||
private Spacers() {
|
||||
}
|
||||
|
||||
/** Returns spacer info if {@code channelName} is a spacer, else {@code null}. */
|
||||
public static Spacer parse(String channelName) {
|
||||
if (channelName == null) return null;
|
||||
Matcher m = PATTERN.matcher(channelName);
|
||||
if (!m.matches()) return null;
|
||||
String tag = m.group(1);
|
||||
char align = (tag == null || tag.isEmpty()) ? 'l' : tag.charAt(0);
|
||||
return new Spacer(align, m.group(2));
|
||||
}
|
||||
|
||||
public static boolean isSpacer(String channelName) {
|
||||
return parse(channelName) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A spacer name no channel among {@code siblings} is using: {@code [spacerN]} with the
|
||||
* lowest free N. TeamSpeak needs channel names to be unique, and the number in the tag
|
||||
* is how spacers — which usually all read the same — get away with it.
|
||||
*/
|
||||
public static String freeName(List<ChannelNode> siblings) {
|
||||
Set<String> taken = new HashSet<>();
|
||||
for (ChannelNode sibling : siblings) {
|
||||
Matcher m = TAG.matcher(sibling.name == null ? "" : sibling.name);
|
||||
if (m.find()) taken.add(m.group(1));
|
||||
}
|
||||
for (int n = 0; ; n++) {
|
||||
if (taken.add(Integer.toString(n))) return "[spacer" + n + "]";
|
||||
}
|
||||
}
|
||||
|
||||
/** Builds the visible label for a spacer at roughly the given character width. */
|
||||
public static String render(Spacer s, int width) {
|
||||
String caption = s.caption == null ? "" : s.caption;
|
||||
if (s.align == '*') {
|
||||
if (caption.isEmpty()) return "";
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (sb.length() < width) sb.append(caption);
|
||||
return sb.substring(0, Math.max(caption.length(), Math.min(sb.length(), width)));
|
||||
}
|
||||
if (s.align == 'c') {
|
||||
int pad = Math.max(0, (width - caption.length()) / 2);
|
||||
return " ".repeat(pad) + caption;
|
||||
}
|
||||
if (s.align == 'r') {
|
||||
int pad = Math.max(0, width - caption.length());
|
||||
return " ".repeat(pad) + caption;
|
||||
}
|
||||
return caption;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user