2025-06-12 20:35:56 -07:00
|
|
|
using Gee;
|
2025-06-06 14:33:40 -07:00
|
|
|
using Gtk;
|
|
|
|
|
|
2025-06-12 20:35:56 -07:00
|
|
|
private class SizeCache
|
|
|
|
|
{
|
|
|
|
|
private static SizeCache instance = null;
|
|
|
|
|
private HashMap<string, Graphene.Size?> size_cache = new HashMap<string, Graphene.Size?>();
|
|
|
|
|
|
|
|
|
|
public static SizeCache get_instance() {
|
|
|
|
|
if (instance == null) {
|
|
|
|
|
instance = new SizeCache();
|
|
|
|
|
}
|
|
|
|
|
return instance;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 23:28:21 -08:00
|
|
|
public Graphene.Size? get_size(string attachment_guid) {
|
|
|
|
|
return size_cache.get(attachment_guid);
|
2025-06-12 20:35:56 -07:00
|
|
|
}
|
|
|
|
|
|
2026-02-21 23:28:21 -08:00
|
|
|
public void set_size(string attachment_guid, Graphene.Size size) {
|
|
|
|
|
size_cache.set(attachment_guid, size);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private class TextureCache
|
|
|
|
|
{
|
|
|
|
|
private static TextureCache instance = null;
|
|
|
|
|
private HashMap<string, Gdk.Texture> texture_cache = new HashMap<string, Gdk.Texture>();
|
|
|
|
|
|
|
|
|
|
public static TextureCache get_instance() {
|
|
|
|
|
if (instance == null) {
|
|
|
|
|
instance = new TextureCache();
|
|
|
|
|
}
|
|
|
|
|
return instance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Gdk.Texture? get_texture(string attachment_guid) {
|
|
|
|
|
return texture_cache.get(attachment_guid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void set_texture(string attachment_guid, Gdk.Texture texture) {
|
|
|
|
|
texture_cache.set(attachment_guid, texture);
|
2025-06-12 20:35:56 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 14:33:40 -07:00
|
|
|
private class ImageBubbleLayout : BubbleLayout
|
|
|
|
|
{
|
2026-02-21 23:28:21 -08:00
|
|
|
public string attachment_guid;
|
2025-06-06 14:33:40 -07:00
|
|
|
public bool is_downloaded;
|
|
|
|
|
|
|
|
|
|
private Graphene.Size image_size;
|
|
|
|
|
private Gdk.Texture? cached_texture = null;
|
2026-02-21 23:28:21 -08:00
|
|
|
private bool preview_download_queued = false;
|
2025-06-06 14:33:40 -07:00
|
|
|
|
2026-02-21 23:28:21 -08:00
|
|
|
public ImageBubbleLayout(string attachment_guid, bool from_me, Widget parent, float max_width, Graphene.Size? image_size = null) {
|
2025-06-06 14:33:40 -07:00
|
|
|
base(parent, max_width);
|
|
|
|
|
|
|
|
|
|
this.from_me = from_me;
|
2026-02-21 23:28:21 -08:00
|
|
|
this.attachment_guid = attachment_guid;
|
2025-06-06 14:33:40 -07:00
|
|
|
this.is_downloaded = false;
|
2026-02-21 23:28:21 -08:00
|
|
|
this.cached_texture = TextureCache.get_instance().get_texture(attachment_guid);
|
2025-06-06 14:33:40 -07:00
|
|
|
|
|
|
|
|
// Calculate image dimensions for layout
|
|
|
|
|
calculate_image_dimensions(image_size);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void calculate_image_dimensions(Graphene.Size? image_size) {
|
|
|
|
|
if (image_size != null) {
|
|
|
|
|
this.image_size = image_size;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 23:28:21 -08:00
|
|
|
var cached_size = SizeCache.get_instance().get_size(attachment_guid);
|
2025-06-12 20:35:56 -07:00
|
|
|
if (cached_size != null) {
|
|
|
|
|
this.image_size = cached_size;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 23:28:21 -08:00
|
|
|
this.image_size = Graphene.Size() { width = 200.0f, height = 150.0f };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void queue_preview_download_if_needed() {
|
|
|
|
|
if (is_downloaded || preview_download_queued || attachment_guid == "") {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 14:33:40 -07:00
|
|
|
try {
|
2026-02-21 23:28:21 -08:00
|
|
|
Repository.get_instance().download_attachment(attachment_guid, true);
|
|
|
|
|
preview_download_queued = true;
|
|
|
|
|
} catch (GLib.Error e) {
|
|
|
|
|
warning("Failed to queue preview download for %s: %s", attachment_guid, e.message);
|
2025-06-06 14:33:40 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void load_image_if_needed() {
|
|
|
|
|
if (cached_texture != null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!is_downloaded) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2026-02-21 23:28:21 -08:00
|
|
|
int fd = Repository.get_instance().open_attachment_fd(attachment_guid, true);
|
|
|
|
|
var stream = new UnixInputStream(fd, true);
|
|
|
|
|
var pixbuf = new Gdk.Pixbuf.from_stream(stream, null);
|
|
|
|
|
cached_texture = Gdk.Texture.for_pixbuf(pixbuf);
|
|
|
|
|
|
|
|
|
|
if (cached_texture != null) {
|
|
|
|
|
TextureCache.get_instance().set_texture(attachment_guid, cached_texture);
|
|
|
|
|
this.image_size = Graphene.Size() {
|
|
|
|
|
width = (float)cached_texture.get_width(),
|
|
|
|
|
height = (float)cached_texture.get_height()
|
|
|
|
|
};
|
|
|
|
|
SizeCache.get_instance().set_size(attachment_guid, this.image_size);
|
|
|
|
|
parent.queue_allocate();
|
|
|
|
|
}
|
2025-06-06 14:33:40 -07:00
|
|
|
} catch (Error e) {
|
2026-02-21 23:28:21 -08:00
|
|
|
warning("Failed to load preview image for %s: %s", attachment_guid, e.message);
|
2025-06-06 14:33:40 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-12 20:35:56 -07:00
|
|
|
private float intrinsic_height {
|
|
|
|
|
get {
|
|
|
|
|
var scale_factor = float.min(max_width / image_size.width, 1.0f);
|
|
|
|
|
return image_size.height * scale_factor;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private float intrinsic_width {
|
|
|
|
|
get {
|
|
|
|
|
var scale_factor = float.min(max_width / image_size.width, 1.0f);
|
|
|
|
|
return image_size.width * scale_factor;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 14:33:40 -07:00
|
|
|
public override float get_height() {
|
2025-06-12 20:35:56 -07:00
|
|
|
return float.max(intrinsic_height, 100.0f);
|
2025-06-06 14:33:40 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override float get_width() {
|
2025-06-12 20:35:56 -07:00
|
|
|
return float.max(intrinsic_width, 200.0f);
|
2025-06-06 14:33:40 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void draw_content(Snapshot snapshot) {
|
2026-02-21 23:28:21 -08:00
|
|
|
queue_preview_download_if_needed();
|
2025-06-06 14:33:40 -07:00
|
|
|
load_image_if_needed();
|
|
|
|
|
|
|
|
|
|
snapshot.save();
|
|
|
|
|
|
|
|
|
|
var image_rect = Graphene.Rect () {
|
|
|
|
|
origin = Graphene.Point() { x = 0, y = 0 },
|
2025-06-12 20:35:56 -07:00
|
|
|
size = Graphene.Size() { width = intrinsic_width, height = intrinsic_height }
|
2025-06-06 14:33:40 -07:00
|
|
|
};
|
|
|
|
|
|
2025-06-12 20:35:56 -07:00
|
|
|
// Center image in the bubble (if it's smaller than the bubble)
|
|
|
|
|
snapshot.translate(Graphene.Point() {
|
|
|
|
|
x = (get_width() - intrinsic_width) / 2,
|
|
|
|
|
y = (get_height() - intrinsic_height) / 2
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-06 14:33:40 -07:00
|
|
|
if (cached_texture != null) {
|
|
|
|
|
snapshot.append_texture(cached_texture, image_rect);
|
|
|
|
|
} else {
|
|
|
|
|
snapshot.append_color(Gdk.RGBA() { red = 0.6f, green = 0.6f, blue = 0.6f, alpha = 0.5f }, image_rect);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
snapshot.restore();
|
|
|
|
|
}
|
2025-06-18 01:49:33 -07:00
|
|
|
|
|
|
|
|
public override void copy(Gdk.Clipboard clipboard) {
|
|
|
|
|
clipboard.set_texture(cached_texture);
|
|
|
|
|
}
|
2026-02-21 23:28:21 -08:00
|
|
|
}
|