use gtk::{gdk, glib, glib::clone, prelude::*, subclass::prelude::*};
use ruma::{
api::client::media::get_content_thumbnail::v3::Method, events::room::avatar::ImageInfo,
OwnedMxcUri,
};
use tracing::error;
use crate::{
session::model::Session,
spawn,
utils::media::image::{
load_image, ImageDimensions, ImageSource, ThumbnailDownloader, ThumbnailSettings,
},
};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, glib::Enum)]
#[repr(u32)]
#[enum_type(name = "AvatarUriSource")]
pub enum AvatarUriSource {
#[default]
User = 0,
Room = 1,
}
mod imp {
use std::{
cell::{Cell, OnceCell, RefCell},
marker::PhantomData,
};
use super::*;
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::AvatarImage)]
pub struct AvatarImage {
#[property(get)]
pub paintable: RefCell<Option<gdk::Paintable>>,
#[property(get, set = Self::set_needed_size, explicit_notify, minimum = 0)]
pub needed_size: Cell<u32>,
pub(super) uri: RefCell<Option<OwnedMxcUri>>,
#[property(get = Self::uri_string)]
uri_string: PhantomData<Option<String>>,
pub(super) info: RefCell<Option<Box<ImageInfo>>>,
#[property(get, construct_only, builder(AvatarUriSource::default()))]
pub uri_source: Cell<AvatarUriSource>,
#[property(get, construct_only)]
pub session: OnceCell<Session>,
}
#[glib::object_subclass]
impl ObjectSubclass for AvatarImage {
const NAME: &'static str = "AvatarImage";
type Type = super::AvatarImage;
}
#[glib::derived_properties]
impl ObjectImpl for AvatarImage {}
impl AvatarImage {
fn set_needed_size(&self, size: u32) {
if self.needed_size.get() >= size {
return;
}
let obj = self.obj();
self.needed_size.set(size);
obj.load();
obj.notify_needed_size();
}
pub(super) fn uri(&self) -> Option<OwnedMxcUri> {
self.uri.borrow().clone()
}
pub(super) fn set_uri(&self, uri: Option<OwnedMxcUri>) -> bool {
if *self.uri.borrow() == uri {
return false;
}
self.uri.replace(uri);
self.obj().notify_uri_string();
true
}
fn uri_string(&self) -> Option<String> {
self.uri.borrow().as_ref().map(ToString::to_string)
}
pub(super) fn info(&self) -> Option<Box<ImageInfo>> {
self.info.borrow().clone()
}
pub(super) fn set_info(&self, info: Option<Box<ImageInfo>>) {
self.info.replace(info);
}
pub(super) fn set_paintable(&self, paintable: Option<gdk::Paintable>) {
self.paintable.replace(paintable);
self.obj().notify_paintable();
}
}
}
glib::wrapper! {
pub struct AvatarImage(ObjectSubclass<imp::AvatarImage>);
}
impl AvatarImage {
pub fn new(
session: &Session,
uri_source: AvatarUriSource,
uri: Option<OwnedMxcUri>,
info: Option<Box<ImageInfo>>,
) -> Self {
let obj = glib::Object::builder::<Self>()
.property("session", session)
.property("uri-source", uri_source)
.build();
obj.set_uri_and_info(uri, info);
obj
}
pub fn set_uri_and_info(&self, uri: Option<OwnedMxcUri>, info: Option<Box<ImageInfo>>) {
let imp = self.imp();
let changed = imp.set_uri(uri);
imp.set_info(info);
if changed {
self.load();
}
}
pub fn uri(&self) -> Option<OwnedMxcUri> {
self.imp().uri()
}
pub fn info(&self) -> Option<Box<ImageInfo>> {
self.imp().info()
}
fn load(&self) {
if self.needed_size() == 0 {
self.imp().set_paintable(None);
return;
}
let Some(uri) = self.uri() else {
self.imp().set_paintable(None);
return;
};
spawn!(
glib::Priority::LOW,
clone!(
#[weak(rename_to = obj)]
self,
async move {
obj.load_inner(uri).await;
}
)
);
}
async fn load_inner(&self, uri: OwnedMxcUri) {
let client = self.session().client();
let info = self.info();
let needed_size = self.needed_size();
let dimensions = ImageDimensions {
width: needed_size,
height: needed_size,
};
let downloader = ThumbnailDownloader {
main: ImageSource {
source: (&uri).into(),
info: info.as_deref().map(Into::into),
},
alt: None,
};
let settings = ThumbnailSettings {
dimensions,
method: Method::Crop,
animated: true,
prefer_thumbnail: true,
};
match downloader.download_to_file(&client, settings).await {
Ok(file) => {
let paintable = load_image(file, Some(dimensions)).await.ok();
self.imp().set_paintable(paintable);
}
Err(error) => error!("Could not fetch avatar: {error}"),
};
}
}