Show servers' icons on bookmarks, and their host button in the toolbar's corner
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,8 @@ public final class Bookmark {
|
||||
public String channelPassword = "";
|
||||
/** Connect to this server automatically when the client starts. */
|
||||
public boolean connectOnStartup = false;
|
||||
/** The server's icon as last seen there, 0 for none or not yet known. */
|
||||
public long iconId = 0;
|
||||
|
||||
public Bookmark() {
|
||||
}
|
||||
@@ -37,9 +39,15 @@ public final class Bookmark {
|
||||
b.channel = channel;
|
||||
b.channelPassword = channelPassword;
|
||||
b.connectOnStartup = connectOnStartup;
|
||||
b.iconId = iconId;
|
||||
return b;
|
||||
}
|
||||
|
||||
/** Whether this bookmark leads to the server at {@code address}:{@code port}. */
|
||||
public boolean pointsAt(String address, int port) {
|
||||
return this.port == port && this.address != null && this.address.trim().equalsIgnoreCase(address.trim());
|
||||
}
|
||||
|
||||
/** Display name, falling back to "address:port" when no label is set. */
|
||||
public String displayName() {
|
||||
if (label != null && !label.isBlank()) return label;
|
||||
|
||||
@@ -30,6 +30,23 @@ public final class Bookmarks {
|
||||
if (index >= 0 && index < entries.size()) entries.remove(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes on the icon a server shows now for every bookmark leading to it.
|
||||
*
|
||||
* @return whether any bookmark changed, and so needs saving and redrawing
|
||||
*/
|
||||
public boolean rememberServerIcon(String address, int port, long iconId) {
|
||||
if (address == null || address.isBlank()) return false;
|
||||
boolean changed = false;
|
||||
for (Bookmark b : entries) {
|
||||
if (b.pointsAt(address, port) && b.iconId != iconId) {
|
||||
b.iconId = iconId;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
/** Bookmarks flagged to connect at startup, each in its own tab. */
|
||||
public List<Bookmark> startupBookmarks() {
|
||||
List<Bookmark> out = new ArrayList<>();
|
||||
@@ -61,6 +78,7 @@ public final class Bookmarks {
|
||||
bm.channel = p.getProperty(prefix + "channel", "");
|
||||
bm.channelPassword = p.getProperty(prefix + "channelPassword", "");
|
||||
bm.connectOnStartup = Boolean.parseBoolean(p.getProperty(prefix + "connectOnStartup", "false"));
|
||||
bm.iconId = parseLong(p.getProperty(prefix + "iconId"));
|
||||
if (bm.address != null && !bm.address.isBlank()) b.entries.add(bm);
|
||||
}
|
||||
return b;
|
||||
@@ -81,6 +99,7 @@ public final class Bookmarks {
|
||||
p.setProperty(prefix + "channel", nullToEmpty(bm.channel));
|
||||
p.setProperty(prefix + "channelPassword", nullToEmpty(bm.channelPassword));
|
||||
p.setProperty(prefix + "connectOnStartup", Boolean.toString(bm.connectOnStartup));
|
||||
p.setProperty(prefix + "iconId", Long.toString(bm.iconId));
|
||||
}
|
||||
try {
|
||||
AppDirs.createProfile();
|
||||
@@ -98,6 +117,15 @@ public final class Bookmarks {
|
||||
}
|
||||
}
|
||||
|
||||
private static long parseLong(String v) {
|
||||
if (v == null) return 0;
|
||||
try {
|
||||
return Long.parseLong(v.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static String nullToEmpty(String s) {
|
||||
return s == null ? "" : s;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.ts3client.net;
|
||||
|
||||
/**
|
||||
* The button a server puts in the toolbar's top right corner, linking to its host's site.
|
||||
*
|
||||
* @param url what a click opens; empty when the button only shows
|
||||
* @param gfxUrl the picture to show; empty for TeamSpeak's own host button icon
|
||||
* @param tooltip shown on hover; may be empty
|
||||
*/
|
||||
public record HostButton(String url, String gfxUrl, String tooltip) {
|
||||
|
||||
/** @return the button, or {@code null} when the server sets neither a link nor a picture */
|
||||
public static HostButton of(String url, String gfxUrl, String tooltip) {
|
||||
url = url == null ? "" : url.trim();
|
||||
gfxUrl = gfxUrl == null ? "" : gfxUrl.trim();
|
||||
if (url.isEmpty() && gfxUrl.isEmpty()) return null;
|
||||
return new HostButton(url, gfxUrl, tooltip == null ? "" : tooltip.trim());
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,6 @@ public final class IconRepository {
|
||||
|
||||
private final Fetcher fetcher;
|
||||
private final Runnable onIconLoaded;
|
||||
private final File cacheDir = new File(AppDirs.profile(), "icons");
|
||||
private final Map<Long, byte[]> icons = new ConcurrentHashMap<>();
|
||||
private final Set<Long> pending = ConcurrentHashMap.newKeySet();
|
||||
private final Set<Long> failed = ConcurrentHashMap.newKeySet();
|
||||
@@ -101,11 +100,34 @@ public final class IconRepository {
|
||||
long id = normalize(iconId);
|
||||
byte[] data = get(id);
|
||||
if (data == null) return null;
|
||||
File file = new File(cacheDir, "icon_" + id + ".png");
|
||||
File file = new File(cacheDir(), "icon_" + id + ".png");
|
||||
if (!file.isFile()) writeDiskCache(id, data);
|
||||
return file.isFile() ? file : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* An icon that needs no server: one shipped with the client or downloaded earlier,
|
||||
* e.g. for a bookmark while it is not connected.
|
||||
*
|
||||
* @return the icon's PNG bytes, or {@code null} if it has never been fetched
|
||||
*/
|
||||
public static byte[] cached(long iconId) {
|
||||
long id = normalize(iconId);
|
||||
if (id == 0) return null;
|
||||
byte[] bundled = readBundled(id);
|
||||
return bundled != null || id <= MAX_BUNDLED_ID ? bundled : readDiskCache(id);
|
||||
}
|
||||
|
||||
/** An icon id as a property carries it, 0 for none or garbage. */
|
||||
public static long parseId(String value) {
|
||||
if (value == null || value.isBlank()) return 0;
|
||||
try {
|
||||
return normalize(Long.parseLong(value.trim()));
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** Forgets download failures so icons can be retried on the next reconnect. */
|
||||
public void retryFailed() {
|
||||
failed.clear();
|
||||
@@ -152,8 +174,12 @@ public final class IconRepository {
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] readDiskCache(long id) {
|
||||
File file = new File(cacheDir, "icon_" + id + ".png");
|
||||
private static File cacheDir() {
|
||||
return new File(AppDirs.profile(), "icons");
|
||||
}
|
||||
|
||||
private static byte[] readDiskCache(long id) {
|
||||
File file = new File(cacheDir(), "icon_" + id + ".png");
|
||||
if (!file.isFile()) return null;
|
||||
try {
|
||||
return Files.readAllBytes(file.toPath());
|
||||
@@ -165,8 +191,8 @@ public final class IconRepository {
|
||||
private void writeDiskCache(long id, byte[] data) {
|
||||
try {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
cacheDir.mkdirs();
|
||||
Files.write(new File(cacheDir, "icon_" + id + ".png").toPath(), data);
|
||||
cacheDir().mkdirs();
|
||||
Files.write(new File(cacheDir(), "icon_" + id + ".png").toPath(), data);
|
||||
} catch (IOException ignored) {
|
||||
// the in-memory cache still serves this session
|
||||
}
|
||||
|
||||
@@ -297,6 +297,18 @@ public final class ServerModel {
|
||||
return serverProperties.getOrDefault(key, "");
|
||||
}
|
||||
|
||||
/** The server's own icon, 0 when it has none. */
|
||||
public synchronized long serverIconId() {
|
||||
return IconRepository.parseId(serverProperty("virtualserver_icon_id"));
|
||||
}
|
||||
|
||||
/** The button the server puts in the toolbar's corner, or {@code null} when it has none. */
|
||||
public synchronized HostButton hostButton() {
|
||||
return HostButton.of(serverProperty("virtualserver_hostbutton_url"),
|
||||
serverProperty("virtualserver_hostbutton_gfx_url"),
|
||||
serverProperty("virtualserver_hostbutton_tooltip"));
|
||||
}
|
||||
|
||||
public synchronized int defaultChannelGroupId() {
|
||||
return defaultChannelGroupId;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,13 @@ public final class ServerSession {
|
||||
* @param peerName the recipient's display name for a private message, else empty
|
||||
*/
|
||||
void onOwnChat(ChatScope scope, int peerClientId, String peerName, String text);
|
||||
|
||||
/**
|
||||
* The server's icon is known or changed (0: it has none), and is on its way to the
|
||||
* icon cache, so bookmarks leading here can show it while not connected.
|
||||
*/
|
||||
default void onServerIconChanged(long iconId) {
|
||||
}
|
||||
}
|
||||
|
||||
private final Settings settings;
|
||||
@@ -60,6 +67,8 @@ public final class ServerSession {
|
||||
private volatile Target lastTarget;
|
||||
/** The channel we were in when the connection last went, to return to on a reconnect. */
|
||||
private volatile String lastChannelPath = "";
|
||||
/** The server icon last reported through {@link Listener#onServerIconChanged}; -1 for none yet. */
|
||||
private volatile long serverIconId = -1;
|
||||
|
||||
private record Target(String address, int port, String nickname, String password, String channelPassword) {
|
||||
}
|
||||
@@ -437,6 +446,15 @@ public final class ServerSession {
|
||||
setStatus(message);
|
||||
}
|
||||
|
||||
private void checkServerIcon() {
|
||||
if (!conn.isConnected()) return;
|
||||
long id = conn.getModel().serverIconId();
|
||||
if (id == serverIconId) return;
|
||||
serverIconId = id;
|
||||
conn.getIcons().get(id);
|
||||
listener.onServerIconChanged(id);
|
||||
}
|
||||
|
||||
/** Applies what a connection event means for the session, then passes it on. */
|
||||
private final class Events implements ConnectionListener {
|
||||
|
||||
@@ -452,11 +470,13 @@ public final class ServerSession {
|
||||
listener.onConnected();
|
||||
listener.onSystemMessage("Connected.");
|
||||
listener.onSessionChanged();
|
||||
checkServerIcon();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnected(String reason) {
|
||||
connecting = false;
|
||||
serverIconId = -1;
|
||||
conn.getModel().clear();
|
||||
listener.onDisconnected(reason);
|
||||
listener.onSystemMessage("Disconnected" + (reason == null || reason.isEmpty() ? "." : ": " + reason));
|
||||
@@ -473,6 +493,7 @@ public final class ServerSession {
|
||||
title = name;
|
||||
listener.onSessionChanged();
|
||||
}
|
||||
checkServerIcon();
|
||||
listener.onModelChanged();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user