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:
2026-09-25 14:09:43 +00:00
parent c5a49fc7b3
commit d206b12a30
10 changed files with 136 additions and 51 deletions

View File

@@ -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;
}
}

View 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;
}
}

View File

@@ -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>";
}
}