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;
|
||||
}
|
||||
}
|
||||
86
ts3-client/core/src/main/java/com/ts3client/net/Spacers.java
Normal file
86
ts3-client/core/src/main/java/com/ts3client/net/Spacers.java
Normal file
@@ -0,0 +1,86 @@
|
||||
package com.ts3client.net;
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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>";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user