Show clients' country flags beside their names, and optionally in the tree

The server places each client in a country (client_country, from its GeoIP
database); a client's details now show that country's flag left of their
name, as TeamSpeak's info frame does, on both clients. "Show country flags
in the channel tree" (off by default, as TeamSpeak's EnableCountryFlags)
also puts it last on client rows.

The flags are TeamSpeak's countries.zip, which now ships with the icon
packs; core's CountryFlags reads it, else a TeamSpeak 3 install's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 23:40:44 +00:00
parent 4ca5534ef4
commit e799218444
20 changed files with 256 additions and 19 deletions

View File

@@ -241,8 +241,8 @@ public final class InfoPanel extends JPanel {
private String body() {
showsPictures = false;
if (shownServer != null) return html(Details.server(shownModel, shownServer), "", null, null);
if (shownChannel != null) return html(Details.channel(shownChannel), "", null, "No description.");
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);
@@ -278,17 +278,19 @@ public final class InfoPanel extends JPanel {
private 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);
// The country's flag goes before the name, as in TeamSpeak's info frame.
return html(Details.client(cl, model, contact), flagTag(cl.country), you, icons, null);
}
/**
* @param titlePrefix HTML before the title
* @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 String html(Details details, String titleSuffix, IconRepository icons, String none) {
private String html(Details details, String titlePrefix, String titleSuffix, IconRepository icons, String none) {
StringBuilder sb = new StringBuilder();
sb.append(heading(esc(details.title()) + titleSuffix));
sb.append(heading(titlePrefix + 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(description(details.description())).append("</div>");
@@ -327,6 +329,13 @@ public final class InfoPanel extends JPanel {
return "<img src='" + file.toURI() + "' width='" + UIScale.scale(14) + "' height='" + UIScale.scale(14) + "'> ";
}
/** An inline {@code <img>} for a country's flag, or nothing for no country. */
private static String flagTag(String country) {
File file = IconFiles.flag(country);
if (file == null) return "";
return "<img src='" + file.toURI() + "' width='" + UIScale.scale(16) + "' height='" + UIScale.scale(11) + "'>&nbsp;";
}
private static String heading(String text) {
return "<div style='font-weight:bold;font-size:13px;margin-bottom:" + HtmlStyles.px(4) + "'>" + text + "</div>";
}