Show client avatars and let the user set their own
Avatars are fetched from the server's file repository as /avatar_<id>, verified against client_flag_avatar and cached on disk; animated GIFs play in the info panel. Self > Set avatar... uploads or removes our own picture within the server's size limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import com.ts3client.net.ClientEntry;
|
||||
import com.ts3client.net.Group;
|
||||
import com.ts3client.net.IconRepository;
|
||||
import com.ts3client.net.ServerModel;
|
||||
import com.ts3client.net.avatar.Avatar;
|
||||
import com.ts3client.text.BBCode;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
@@ -18,6 +19,8 @@ import java.awt.BorderLayout;
|
||||
import java.awt.Cursor;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.event.ComponentAdapter;
|
||||
import java.awt.event.ComponentEvent;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.io.File;
|
||||
@@ -42,6 +45,9 @@ public final class InfoPanel extends JPanel {
|
||||
void setPanelHidden(boolean hidden);
|
||||
}
|
||||
|
||||
private static final int AVATAR_MIN_SIZE = 64;
|
||||
private static final int AVATAR_BOX_STEP = 16;
|
||||
|
||||
private final JEditorPane pane = HtmlStyles.pane("font-family:sans-serif; font-size:11px;");
|
||||
private final JScrollPane scroll = new JScrollPane(pane);
|
||||
/** Floats over the bottom-right corner of the details, out of the text's way. */
|
||||
@@ -52,7 +58,10 @@ public final class InfoPanel extends JPanel {
|
||||
private ServerModel shownModel;
|
||||
private IconRepository shownIcons;
|
||||
private Contact shownContact;
|
||||
private Avatar shownAvatar;
|
||||
private boolean inChatTab;
|
||||
/** The avatar box the current HTML was built for; a resize that changes it re-renders. */
|
||||
private int renderedAvatarBox;
|
||||
|
||||
public InfoPanel() {
|
||||
super(new BorderLayout());
|
||||
@@ -62,6 +71,12 @@ public final class InfoPanel extends JPanel {
|
||||
Links.open(e.getDescription(), this);
|
||||
});
|
||||
HtmlStyles.fillViewport(pane);
|
||||
scroll.getViewport().addComponentListener(new ComponentAdapter() {
|
||||
@Override
|
||||
public void componentResized(ComponentEvent e) {
|
||||
if (shownAvatar != null && !inChatTab && avatarBox() != renderedAvatarBox) render();
|
||||
}
|
||||
});
|
||||
|
||||
toggle.setFont(Theme.uiSmall());
|
||||
toggle.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||||
@@ -146,12 +161,15 @@ public final class InfoPanel extends JPanel {
|
||||
render();
|
||||
}
|
||||
|
||||
public void showClient(ClientEntry cl, ServerModel model, IconRepository icons, Contact contact) {
|
||||
/** @param avatar the client's avatar, or {@code null} while there is none to show */
|
||||
public void showClient(ClientEntry cl, ServerModel model, IconRepository icons, Contact contact,
|
||||
Avatar avatar) {
|
||||
shownClient = cl;
|
||||
shownChannel = null;
|
||||
shownModel = model;
|
||||
shownIcons = icons;
|
||||
shownContact = contact;
|
||||
shownAvatar = avatar;
|
||||
render();
|
||||
}
|
||||
|
||||
@@ -169,10 +187,25 @@ public final class InfoPanel extends JPanel {
|
||||
|
||||
private String body() {
|
||||
if (shownChannel != null) return channelBody(shownChannel);
|
||||
if (shownClient != null) return clientBody(shownClient, shownModel, shownIcons, shownContact);
|
||||
if (shownClient != null) {
|
||||
renderedAvatarBox = inChatTab ? 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>";
|
||||
}
|
||||
|
||||
/**
|
||||
* How large an avatar may be drawn beside the details: a share of the panel's
|
||||
* width, so a narrow panel keeps room for the text. Rounded to steps so that
|
||||
* dragging a divider does not rebuild the HTML on every pixel.
|
||||
*/
|
||||
private int avatarBox() {
|
||||
int width = scroll.getViewport().getWidth();
|
||||
if (width <= 0) return AvatarHtml.MAX_SIZE;
|
||||
int box = width * 2 / 5 / AVATAR_BOX_STEP * AVATAR_BOX_STEP;
|
||||
return Math.max(AVATAR_MIN_SIZE, Math.min(AvatarHtml.MAX_SIZE, box));
|
||||
}
|
||||
|
||||
private static String channelBody(ChannelNode ch) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(heading(esc(ch.name)));
|
||||
@@ -190,7 +223,19 @@ public final class InfoPanel extends JPanel {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String clientBody(ClientEntry cl, ServerModel model, IconRepository icons, Contact contact) {
|
||||
private static String clientBody(ClientEntry cl, ServerModel model, IconRepository icons, Contact contact,
|
||||
Avatar avatar, int avatarBox) {
|
||||
String details = clientDetails(cl, model, icons, contact);
|
||||
String picture = AvatarHtml.imgTag(avatar, avatarBox);
|
||||
if (picture.isEmpty()) return details;
|
||||
// The avatar sits top-right beside the details, like the official client's info frame.
|
||||
return "<table width='100%' cellpadding='0' cellspacing='0'><tr>"
|
||||
+ "<td valign='top'>" + details + "</td>"
|
||||
+ "<td valign='top' align='right' style='padding-left:8px'>" + picture + "</td>"
|
||||
+ "</tr></table>";
|
||||
}
|
||||
|
||||
private static String clientDetails(ClientEntry cl, ServerModel model, IconRepository icons, Contact contact) {
|
||||
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>" : "")));
|
||||
|
||||
Reference in New Issue
Block a user