Show server, channel and client details on Android, and the server's on the desktop
Core's Details says what the info box tells, as TeamSpeak's info frame does: labelled facts, then a BBCode description. The desktop info panel now lays it out instead of building its own, and selecting the server row shows the server: address, version, clients, channels, creation date and its welcome and host messages, from the virtualserver_* properties kept since initserver. On Android the channel and client sheets show the same details, asking the server for a missing channel description or a client's variables; tapping the server row opens a server sheet. Descriptions render as TeamSpeak's do: BBCode now knows [hr], [left]/[right]/[justify], numbered and lettered lists and tables, which Android's HTML parser approximates — rules as dividers, table rows as lines, list items numbered, font sizes as big/small. Links are blue whatever the wallpaper. Push-to-talk in the bottom bar follows whichever server holds the microphone, and a server without it keeps its button to take it in a fixed slot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import com.formdev.flatlaf.util.UIScale;
|
||||
import com.ts3client.contacts.Contact;
|
||||
import com.ts3client.net.ChannelNode;
|
||||
import com.ts3client.net.ClientEntry;
|
||||
import com.ts3client.net.Details;
|
||||
import com.ts3client.net.Group;
|
||||
import com.ts3client.net.IconRepository;
|
||||
import com.ts3client.net.ServerModel;
|
||||
@@ -29,9 +30,8 @@ import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Read-only detail view for the currently selected channel or client, mirroring
|
||||
* the TeamSpeak 3 info box: channel topic/description, or a client's server and
|
||||
* channel groups, platform and version.
|
||||
* Read-only detail view for the server, channel or client selected, mirroring the
|
||||
* TeamSpeak 3 info box; what it says comes from {@link Details}.
|
||||
*/
|
||||
public final class InfoPanel extends JPanel {
|
||||
|
||||
@@ -57,6 +57,8 @@ public final class InfoPanel extends JPanel {
|
||||
private DescriptionHandler descriptionHandler;
|
||||
private ChannelNode shownChannel;
|
||||
private ClientEntry shownClient;
|
||||
/** The server's address while the server itself is shown. */
|
||||
private String shownServer;
|
||||
private ServerModel shownModel;
|
||||
private IconRepository shownIcons;
|
||||
private Contact shownContact;
|
||||
@@ -143,6 +145,7 @@ public final class InfoPanel extends JPanel {
|
||||
public void clear() {
|
||||
shownChannel = null;
|
||||
shownClient = null;
|
||||
shownServer = null;
|
||||
inChatTab = false;
|
||||
render();
|
||||
}
|
||||
@@ -157,9 +160,18 @@ public final class InfoPanel extends JPanel {
|
||||
render();
|
||||
}
|
||||
|
||||
public void showServer(ServerModel model, String address) {
|
||||
shownServer = address;
|
||||
shownModel = model;
|
||||
shownChannel = null;
|
||||
shownClient = null;
|
||||
render();
|
||||
}
|
||||
|
||||
public void showChannel(ChannelNode ch) {
|
||||
shownChannel = ch;
|
||||
shownClient = null;
|
||||
shownServer = null;
|
||||
render();
|
||||
}
|
||||
|
||||
@@ -168,6 +180,7 @@ public final class InfoPanel extends JPanel {
|
||||
Avatar avatar) {
|
||||
shownClient = cl;
|
||||
shownChannel = null;
|
||||
shownServer = null;
|
||||
shownModel = model;
|
||||
shownIcons = icons;
|
||||
shownContact = contact;
|
||||
@@ -176,7 +189,7 @@ public final class InfoPanel extends JPanel {
|
||||
}
|
||||
|
||||
private void render() {
|
||||
boolean selected = shownChannel != null || shownClient != null;
|
||||
boolean selected = shownChannel != null || shownClient != null || shownServer != null;
|
||||
boolean hide = inChatTab && selected;
|
||||
if (descriptionHandler != null) descriptionHandler.setPanelHidden(hide);
|
||||
toggle.setVisible(selected && !hide);
|
||||
@@ -188,12 +201,13 @@ public final class InfoPanel extends JPanel {
|
||||
}
|
||||
|
||||
private String body() {
|
||||
if (shownChannel != null) return channelBody(shownChannel);
|
||||
if (shownServer != null) return html(Details.server(shownModel, shownServer), "", null, null);
|
||||
if (shownChannel != null) return html(Details.channel(shownChannel), "", null, "No description.");
|
||||
if (shownClient != null) {
|
||||
renderedAvatarBox = inChatTab ? UIScale.scale(AvatarHtml.MAX_SIZE) : avatarBox();
|
||||
return clientBody(shownClient, shownModel, shownIcons, shownContact, shownAvatar, renderedAvatarBox);
|
||||
}
|
||||
return "<i class='muted'>Select a channel or client to see details.</i>";
|
||||
return "<i class='muted'>Select the server, a channel or a client to see details.</i>";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,23 +224,6 @@ public final class InfoPanel extends JPanel {
|
||||
return Math.max(UIScale.scale(AVATAR_MIN_SIZE), Math.min(max, box));
|
||||
}
|
||||
|
||||
private static String channelBody(ChannelNode ch) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(heading(esc(ch.name)));
|
||||
if (ch.maxClients >= 0) row(sb, "Max clients", Integer.toString(ch.maxClients));
|
||||
if (ch.hasPassword) row(sb, "Password", "protected");
|
||||
if (!ch.topic.isEmpty()) row(sb, "Topic", esc(ch.topic));
|
||||
sb.append("<hr>");
|
||||
if (ch.description != null && !ch.description.isEmpty()) {
|
||||
sb.append("<div>").append(multiline(ch.description)).append("</div>");
|
||||
} else if (ch.descriptionLoaded) {
|
||||
sb.append("<i class='muted'>No description.</i>");
|
||||
} else {
|
||||
sb.append("<i class='muted'>Loading description…</i>");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String clientBody(ClientEntry cl, ServerModel model, IconRepository icons, Contact contact,
|
||||
Avatar avatar, int avatarBox) {
|
||||
String details = clientDetails(cl, model, icons, contact);
|
||||
@@ -240,49 +237,35 @@ public final class InfoPanel extends JPanel {
|
||||
}
|
||||
|
||||
private static String clientDetails(ClientEntry cl, ServerModel model, IconRepository icons, Contact contact) {
|
||||
String you = cl.self ? " <span class='muted'>(you)</span>" : "";
|
||||
return html(Details.client(cl, model, contact), you, icons, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param titleSuffix HTML after the title
|
||||
* @param icons for the icons of the groups a field names; null for none
|
||||
* @param none what to say when there is no description; null to say nothing
|
||||
*/
|
||||
private static String html(Details details, String titleSuffix, IconRepository icons, String none) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String name = contact == null ? cl.nickname : contact.displayName(cl.nickname);
|
||||
sb.append(heading(esc(name) + (cl.self ? " <span class='muted'>(you)</span>" : "")));
|
||||
if (contact != null) {
|
||||
String type = contact.type.label();
|
||||
if (!contact.phoneticNickname.isEmpty()) type += " (" + esc(contact.phoneticNickname) + ")";
|
||||
row(sb, "Contact", type);
|
||||
sb.append(heading(esc(details.title()) + titleSuffix));
|
||||
for (Details.Field f : details.fields()) row(sb, esc(f.label()), value(f, icons));
|
||||
if (!details.description().isEmpty()) {
|
||||
sb.append("<hr><div>").append(multiline(details.description())).append("</div>");
|
||||
} else if (details.pending()) {
|
||||
sb.append("<hr><i class='muted'>Loading description…</i>");
|
||||
} else if (none != null) {
|
||||
sb.append("<hr><i class='muted'>").append(none).append("</i>");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
List<Group> serverGroups = model.serverGroupsOf(cl.serverGroupIds);
|
||||
if (serverGroups.isEmpty()) {
|
||||
List<String> names = model.serverGroupNames(cl.serverGroupIds);
|
||||
row(sb, "Server groups", names.isEmpty() ? "—" : esc(String.join(", ", names)));
|
||||
} else {
|
||||
StringBuilder groups = new StringBuilder();
|
||||
for (Group g : serverGroups) {
|
||||
if (groups.length() > 0) groups.append(", ");
|
||||
groups.append(iconTag(g, icons)).append(esc(g.name));
|
||||
}
|
||||
row(sb, "Server groups", groups.toString());
|
||||
}
|
||||
|
||||
Group channelGroup = model.channelGroup(cl.channelGroupId);
|
||||
row(sb, "Channel group", channelGroup != null
|
||||
? iconTag(channelGroup, icons) + esc(channelGroup.name)
|
||||
: "#" + cl.channelGroupId);
|
||||
|
||||
if (cl.talkPower != 0) row(sb, "Talk power", Integer.toString(cl.talkPower));
|
||||
if (!cl.version.isEmpty()) {
|
||||
row(sb, "Version", esc(cl.version)
|
||||
+ (cl.platform.isEmpty() ? "" : " on " + esc(cl.platform)));
|
||||
} else if (!cl.platform.isEmpty()) {
|
||||
row(sb, "Platform", esc(cl.platform));
|
||||
}
|
||||
if (cl.away) row(sb, "Status", "Away");
|
||||
if (!cl.inputHardware) row(sb, "Microphone", "disabled");
|
||||
else if (cl.inputMuted) row(sb, "Microphone", "muted");
|
||||
if (!cl.outputHardware) row(sb, "Speakers", "disabled");
|
||||
else if (cl.outputMuted) row(sb, "Speakers", "muted");
|
||||
if (!cl.uniqueId.isEmpty()) row(sb, "Unique ID", esc(cl.uniqueId));
|
||||
|
||||
if (cl.description != null && !cl.description.isEmpty()) {
|
||||
sb.append("<hr><div>").append(multiline(cl.description)).append("</div>");
|
||||
private static String value(Details.Field field, IconRepository icons) {
|
||||
if (field.groups().isEmpty() || icons == null) return esc(field.value());
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Group g : field.groups()) {
|
||||
if (sb.length() > 0) sb.append(", ");
|
||||
sb.append(iconTag(g, icons)).append(esc(g.name));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user