12 changed files with 287 additions and 411 deletions
@ -1,145 +0,0 @@
|
||||
use gtk::{gdk, glib, prelude::*, subclass::prelude::*}; |
||||
use tracing::warn; |
||||
|
||||
use super::AvatarImage; |
||||
use crate::{ |
||||
application::Application, |
||||
utils::notifications::{paintable_as_notification_icon, string_as_notification_icon}, |
||||
}; |
||||
|
||||
mod imp { |
||||
use std::cell::RefCell; |
||||
|
||||
use once_cell::sync::Lazy; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Debug, Default)] |
||||
pub struct AvatarData { |
||||
/// The data of the user-defined image.
|
||||
pub image: RefCell<Option<AvatarImage>>, |
||||
/// The display name used as a fallback for this avatar.
|
||||
pub display_name: RefCell<Option<String>>, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for AvatarData { |
||||
const NAME: &'static str = "AvatarData"; |
||||
type Type = super::AvatarData; |
||||
} |
||||
|
||||
impl ObjectImpl for AvatarData { |
||||
fn properties() -> &'static [glib::ParamSpec] { |
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| { |
||||
vec![ |
||||
glib::ParamSpecObject::builder::<AvatarImage>("image") |
||||
.explicit_notify() |
||||
.build(), |
||||
glib::ParamSpecString::builder("display-name") |
||||
.explicit_notify() |
||||
.build(), |
||||
] |
||||
}); |
||||
|
||||
PROPERTIES.as_ref() |
||||
} |
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { |
||||
let obj = self.obj(); |
||||
|
||||
match pspec.name() { |
||||
"image" => obj.set_image(value.get().unwrap()), |
||||
"display-name" => obj.set_display_name(value.get().unwrap()), |
||||
_ => unimplemented!(), |
||||
} |
||||
} |
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { |
||||
let obj = self.obj(); |
||||
|
||||
match pspec.name() { |
||||
"image" => obj.image().to_value(), |
||||
"display-name" => obj.display_name().to_value(), |
||||
_ => unimplemented!(), |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
/// Data about a User’s or Room’s avatar.
|
||||
pub struct AvatarData(ObjectSubclass<imp::AvatarData>); |
||||
} |
||||
|
||||
impl AvatarData { |
||||
/// Construct a new empty `AvatarData`.
|
||||
pub fn new() -> Self { |
||||
glib::Object::new() |
||||
} |
||||
|
||||
/// Constructs an `AvatarData` with the given image data.
|
||||
pub fn with_image(image: AvatarImage) -> Self { |
||||
glib::Object::builder().property("image", image).build() |
||||
} |
||||
|
||||
/// The data of the user-defined image.
|
||||
pub fn image(&self) -> Option<AvatarImage> { |
||||
self.imp().image.borrow().clone() |
||||
} |
||||
|
||||
/// Set the data of the user-defined image.
|
||||
pub fn set_image(&self, image: Option<AvatarImage>) { |
||||
let imp = self.imp(); |
||||
|
||||
if imp.image.borrow().as_ref() == image.as_ref() { |
||||
return; |
||||
} |
||||
|
||||
imp.image.replace(image); |
||||
self.notify("image"); |
||||
} |
||||
|
||||
/// Set the display name used as a fallback for this avatar.
|
||||
pub fn set_display_name(&self, display_name: Option<String>) { |
||||
let imp = self.imp(); |
||||
|
||||
if imp.display_name.borrow().as_ref() == display_name.as_ref() { |
||||
return; |
||||
} |
||||
|
||||
imp.display_name.replace(display_name); |
||||
self.notify("display-name"); |
||||
} |
||||
|
||||
/// The display name used as a fallback for this avatar.
|
||||
pub fn display_name(&self) -> Option<String> { |
||||
self.imp().display_name.borrow().clone() |
||||
} |
||||
|
||||
/// Get this avatar as a notification icon.
|
||||
///
|
||||
/// Returns `None` if an error occurred while generating the icon.
|
||||
pub fn as_notification_icon(&self) -> Option<gdk::Texture> { |
||||
let window = Application::default().active_window()?.upcast(); |
||||
|
||||
let icon = if let Some(paintable) = self.image().and_then(|i| i.paintable()) { |
||||
paintable_as_notification_icon(paintable.upcast_ref(), &window) |
||||
} else { |
||||
string_as_notification_icon(&self.display_name().unwrap_or_default(), &window) |
||||
}; |
||||
|
||||
match icon { |
||||
Ok(icon) => Some(icon), |
||||
Err(error) => { |
||||
warn!("Failed to generate icon for notification: {error}"); |
||||
None |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl Default for AvatarData { |
||||
fn default() -> Self { |
||||
Self::new() |
||||
} |
||||
} |
||||
@ -1,237 +0,0 @@
|
||||
use gtk::{gdk, glib, glib::clone, prelude::*, subclass::prelude::*}; |
||||
use matrix_sdk::{ |
||||
media::{MediaFormat, MediaRequest, MediaThumbnailSize}, |
||||
ruma::{ |
||||
api::client::media::get_content_thumbnail::v3::Method, events::room::MediaSource, MxcUri, |
||||
OwnedMxcUri, |
||||
}, |
||||
}; |
||||
use tracing::error; |
||||
|
||||
use crate::{components::ImagePaintable, session::model::Session, spawn, spawn_tokio}; |
||||
|
||||
/// The source of an avatar's URI.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, glib::Enum)] |
||||
#[repr(u32)] |
||||
#[enum_type(name = "AvatarUriSource")] |
||||
pub enum AvatarUriSource { |
||||
/// The URI comes from a Matrix user.
|
||||
#[default] |
||||
User = 0, |
||||
/// The URI comes from a Matrix room.
|
||||
Room = 1, |
||||
} |
||||
|
||||
mod imp { |
||||
use std::cell::{Cell, RefCell}; |
||||
|
||||
use once_cell::sync::{Lazy, OnceCell}; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Debug, Default)] |
||||
pub struct AvatarImage { |
||||
pub paintable: RefCell<Option<gdk::Paintable>>, |
||||
pub needed_size: Cell<u32>, |
||||
pub uri: RefCell<Option<OwnedMxcUri>>, |
||||
/// The source of the avatar's URI.
|
||||
pub uri_source: Cell<AvatarUriSource>, |
||||
pub session: OnceCell<Session>, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for AvatarImage { |
||||
const NAME: &'static str = "AvatarImage"; |
||||
type Type = super::AvatarImage; |
||||
} |
||||
|
||||
impl ObjectImpl for AvatarImage { |
||||
fn properties() -> &'static [glib::ParamSpec] { |
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| { |
||||
vec![ |
||||
glib::ParamSpecObject::builder::<gdk::Paintable>("paintable") |
||||
.read_only() |
||||
.build(), |
||||
glib::ParamSpecUInt::builder("needed-size") |
||||
.minimum(0) |
||||
.explicit_notify() |
||||
.build(), |
||||
glib::ParamSpecString::builder("uri") |
||||
.explicit_notify() |
||||
.build(), |
||||
glib::ParamSpecEnum::builder::<AvatarUriSource>("uri-source") |
||||
.construct_only() |
||||
.build(), |
||||
glib::ParamSpecObject::builder::<Session>("session") |
||||
.construct_only() |
||||
.build(), |
||||
] |
||||
}); |
||||
|
||||
PROPERTIES.as_ref() |
||||
} |
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { |
||||
let obj = self.obj(); |
||||
|
||||
match pspec.name() { |
||||
"needed-size" => obj.set_needed_size(value.get().unwrap()), |
||||
"uri" => obj.set_uri(value.get::<&str>().ok().map(Into::into)), |
||||
"uri-source" => obj.set_uri_source(value.get().unwrap()), |
||||
"session" => { |
||||
if let Some(session) = value.get().unwrap() { |
||||
if self.session.set(session).is_err() { |
||||
error!("Trying to set a session while it is already set"); |
||||
} |
||||
} |
||||
} |
||||
_ => unimplemented!(), |
||||
} |
||||
} |
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { |
||||
let obj = self.obj(); |
||||
|
||||
match pspec.name() { |
||||
"paintable" => obj.paintable().to_value(), |
||||
"needed-size" => obj.needed_size().to_value(), |
||||
"uri" => obj.uri().map_or_else( |
||||
|| { |
||||
let none: Option<&str> = None; |
||||
none.to_value() |
||||
}, |
||||
|url| url.as_str().to_value(), |
||||
), |
||||
"uri-source" => obj.uri_source().to_value(), |
||||
"session" => obj.session().to_value(), |
||||
_ => unimplemented!(), |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
/// The image data for an avatar.
|
||||
pub struct AvatarImage(ObjectSubclass<imp::AvatarImage>); |
||||
} |
||||
|
||||
impl AvatarImage { |
||||
/// Construct a new `AvatarImage` with the given session and Matrix URI.
|
||||
pub fn new(session: &Session, uri: Option<&MxcUri>, uri_source: AvatarUriSource) -> Self { |
||||
glib::Object::builder() |
||||
.property("session", session) |
||||
.property("uri", uri.map(|uri| uri.to_string())) |
||||
.property("uri-source", uri_source) |
||||
.build() |
||||
} |
||||
|
||||
/// The current session.
|
||||
fn session(&self) -> &Session { |
||||
self.imp().session.get().unwrap() |
||||
} |
||||
|
||||
/// The image content as a paintable, if any.
|
||||
pub fn paintable(&self) -> Option<gdk::Paintable> { |
||||
self.imp().paintable.borrow().clone() |
||||
} |
||||
|
||||
/// Set the content of the image.
|
||||
fn set_image_data(&self, data: Option<Vec<u8>>) { |
||||
let paintable = data |
||||
.and_then(|data| ImagePaintable::from_bytes(&glib::Bytes::from(&data), None).ok()) |
||||
.map(|texture| texture.upcast()); |
||||
self.imp().paintable.replace(paintable); |
||||
self.notify("paintable"); |
||||
} |
||||
|
||||
fn load(&self) { |
||||
// Don't do anything here if we don't need the avatar.
|
||||
if self.needed_size() == 0 { |
||||
return; |
||||
} |
||||
|
||||
let Some(uri) = self.uri() else { |
||||
return; |
||||
}; |
||||
|
||||
let client = self.session().client(); |
||||
let needed_size = self.needed_size(); |
||||
let request = MediaRequest { |
||||
source: MediaSource::Plain(uri), |
||||
format: MediaFormat::Thumbnail(MediaThumbnailSize { |
||||
width: needed_size.into(), |
||||
height: needed_size.into(), |
||||
method: Method::Scale, |
||||
}), |
||||
}; |
||||
let handle = |
||||
spawn_tokio!(async move { client.media().get_media_content(&request, true).await }); |
||||
|
||||
spawn!( |
||||
glib::Priority::LOW, |
||||
clone!(@weak self as obj => async move { |
||||
match handle.await.unwrap() { |
||||
Ok(data) => obj.set_image_data(Some(data)), |
||||
Err(error) => error!("Could not fetch avatar: {error}"), |
||||
}; |
||||
}) |
||||
); |
||||
} |
||||
|
||||
/// Set the needed size of the user-defined image.
|
||||
///
|
||||
/// Only the biggest size will be stored.
|
||||
pub fn set_needed_size(&self, size: u32) { |
||||
let imp = self.imp(); |
||||
|
||||
if imp.needed_size.get() < size { |
||||
imp.needed_size.set(size); |
||||
|
||||
self.load(); |
||||
} |
||||
|
||||
self.notify("needed-size"); |
||||
} |
||||
|
||||
/// Get the biggest needed size of the user-defined image.
|
||||
///
|
||||
/// If this is `0`, no image will be loaded.
|
||||
pub fn needed_size(&self) -> u32 { |
||||
self.imp().needed_size.get() |
||||
} |
||||
|
||||
/// Set the Matrix URI of the `AvatarImage`.
|
||||
pub fn set_uri(&self, uri: Option<OwnedMxcUri>) { |
||||
let imp = self.imp(); |
||||
|
||||
if imp.uri.borrow().as_ref() == uri.as_ref() { |
||||
return; |
||||
} |
||||
|
||||
let has_uri = uri.is_some(); |
||||
imp.uri.replace(uri); |
||||
|
||||
if has_uri { |
||||
self.load(); |
||||
} else { |
||||
self.set_image_data(None); |
||||
} |
||||
|
||||
self.notify("uri"); |
||||
} |
||||
|
||||
/// The Matrix URI of the `AvatarImage`.
|
||||
pub fn uri(&self) -> Option<OwnedMxcUri> { |
||||
self.imp().uri.borrow().to_owned() |
||||
} |
||||
|
||||
/// The source of the avatar's URI.
|
||||
pub fn uri_source(&self) -> AvatarUriSource { |
||||
self.imp().uri_source.get() |
||||
} |
||||
|
||||
/// Set the source of the avatar's URI.
|
||||
fn set_uri_source(&self, uri_source: AvatarUriSource) { |
||||
self.imp().uri_source.set(uri_source); |
||||
} |
||||
} |
||||
@ -1,7 +0,0 @@
|
||||
mod data; |
||||
mod image; |
||||
|
||||
pub use self::{ |
||||
data::AvatarData, |
||||
image::{AvatarImage, AvatarUriSource}, |
||||
}; |
||||
@ -0,0 +1,161 @@
|
||||
use gtk::{gdk, glib, glib::clone, prelude::*, subclass::prelude::*}; |
||||
use matrix_sdk::{ |
||||
media::{MediaFormat, MediaRequest, MediaThumbnailSize}, |
||||
ruma::{ |
||||
api::client::media::get_content_thumbnail::v3::Method, events::room::MediaSource, MxcUri, |
||||
OwnedMxcUri, |
||||
}, |
||||
}; |
||||
use tracing::error; |
||||
|
||||
use crate::{components::ImagePaintable, session::model::Session, spawn, spawn_tokio}; |
||||
|
||||
/// The source of an avatar's URI.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, glib::Enum)] |
||||
#[repr(u32)] |
||||
#[enum_type(name = "AvatarUriSource")] |
||||
pub enum AvatarUriSource { |
||||
/// The URI comes from a Matrix user.
|
||||
#[default] |
||||
User = 0, |
||||
/// The URI comes from a Matrix room.
|
||||
Room = 1, |
||||
} |
||||
|
||||
mod imp { |
||||
use std::cell::{Cell, OnceCell, RefCell}; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Debug, Default, glib::Properties)] |
||||
#[properties(wrapper_type = super::AvatarImage)] |
||||
pub struct AvatarImage { |
||||
/// The image content as a paintable, if any.
|
||||
#[property(get)] |
||||
pub paintable: RefCell<Option<gdk::Paintable>>, |
||||
/// The biggest needed size of the user-defined image.
|
||||
///
|
||||
/// If this is `0`, no image will be loaded.
|
||||
#[property(get, set = Self::set_needed_size, explicit_notify, minimum = 0)] |
||||
pub needed_size: Cell<u32>, |
||||
/// The Matrix URI of the `AvatarImage`.
|
||||
#[property(get = Self::uri, set = Self::set_uri, explicit_notify, nullable, type = Option<String>)] |
||||
pub uri: RefCell<Option<OwnedMxcUri>>, |
||||
/// The source of the avatar's URI.
|
||||
#[property(get, construct_only, builder(AvatarUriSource::default()))] |
||||
pub uri_source: Cell<AvatarUriSource>, |
||||
/// The current session.
|
||||
#[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 { |
||||
/// Set the needed size of the user-defined image.
|
||||
///
|
||||
/// Only the biggest size will be stored.
|
||||
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(); |
||||
} |
||||
|
||||
/// The Matrix URI of the `AvatarImage`.
|
||||
fn uri(&self) -> Option<String> { |
||||
self.uri.borrow().as_ref().map(ToString::to_string) |
||||
} |
||||
|
||||
/// Set the Matrix URI of the `AvatarImage`.
|
||||
fn set_uri(&self, uri: Option<String>) { |
||||
let uri = uri.map(OwnedMxcUri::from); |
||||
|
||||
if self.uri.borrow().as_ref() == uri.as_ref() { |
||||
return; |
||||
} |
||||
let obj = self.obj(); |
||||
|
||||
let has_uri = uri.is_some(); |
||||
self.uri.replace(uri); |
||||
|
||||
if has_uri { |
||||
obj.load(); |
||||
} else { |
||||
obj.set_image_data(None); |
||||
} |
||||
|
||||
obj.notify_uri(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
/// The image data for an avatar.
|
||||
pub struct AvatarImage(ObjectSubclass<imp::AvatarImage>); |
||||
} |
||||
|
||||
impl AvatarImage { |
||||
/// Construct a new `AvatarImage` with the given session and Matrix URI.
|
||||
pub fn new(session: &Session, uri: Option<&MxcUri>, uri_source: AvatarUriSource) -> Self { |
||||
glib::Object::builder() |
||||
.property("session", session) |
||||
.property("uri", uri.map(|uri| uri.to_string())) |
||||
.property("uri-source", uri_source) |
||||
.build() |
||||
} |
||||
|
||||
/// Set the content of the image.
|
||||
fn set_image_data(&self, data: Option<Vec<u8>>) { |
||||
let paintable = data |
||||
.and_then(|data| ImagePaintable::from_bytes(&glib::Bytes::from(&data), None).ok()) |
||||
.map(|texture| texture.upcast()); |
||||
self.imp().paintable.replace(paintable); |
||||
self.notify("paintable"); |
||||
} |
||||
|
||||
fn load(&self) { |
||||
// Don't do anything here if we don't need the avatar.
|
||||
if self.needed_size() == 0 { |
||||
return; |
||||
} |
||||
|
||||
let Some(uri) = self.imp().uri.borrow().clone() else { |
||||
return; |
||||
}; |
||||
|
||||
let client = self.session().client(); |
||||
let needed_size = self.needed_size(); |
||||
let request = MediaRequest { |
||||
source: MediaSource::Plain(uri), |
||||
format: MediaFormat::Thumbnail(MediaThumbnailSize { |
||||
width: needed_size.into(), |
||||
height: needed_size.into(), |
||||
method: Method::Scale, |
||||
}), |
||||
}; |
||||
let handle = |
||||
spawn_tokio!(async move { client.media().get_media_content(&request, true).await }); |
||||
|
||||
spawn!( |
||||
glib::Priority::LOW, |
||||
clone!(@weak self as obj => async move { |
||||
match handle.await.unwrap() { |
||||
Ok(data) => obj.set_image_data(Some(data)), |
||||
Err(error) => error!("Could not fetch avatar: {error}"), |
||||
}; |
||||
}) |
||||
); |
||||
} |
||||
} |
||||
@ -0,0 +1,102 @@
|
||||
use gtk::{gdk, glib, prelude::*, subclass::prelude::*}; |
||||
use tracing::warn; |
||||
|
||||
mod avatar_image; |
||||
|
||||
pub use self::avatar_image::{AvatarImage, AvatarUriSource}; |
||||
use crate::{ |
||||
application::Application, |
||||
utils::notifications::{paintable_as_notification_icon, string_as_notification_icon}, |
||||
}; |
||||
|
||||
mod imp { |
||||
use std::cell::RefCell; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Debug, Default, glib::Properties)] |
||||
#[properties(wrapper_type = super::AvatarData)] |
||||
pub struct AvatarData { |
||||
/// The data of the user-defined image.
|
||||
#[property(get, set = Self::set_image, explicit_notify, nullable)] |
||||
pub image: RefCell<Option<AvatarImage>>, |
||||
/// The display name used as a fallback for this avatar.
|
||||
#[property(get, set = Self::set_display_name, explicit_notify, nullable)] |
||||
pub display_name: RefCell<Option<String>>, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for AvatarData { |
||||
const NAME: &'static str = "AvatarData"; |
||||
type Type = super::AvatarData; |
||||
} |
||||
|
||||
#[glib::derived_properties] |
||||
impl ObjectImpl for AvatarData {} |
||||
|
||||
impl AvatarData { |
||||
/// Set the data of the user-defined image.
|
||||
fn set_image(&self, image: Option<AvatarImage>) { |
||||
if self.image.borrow().as_ref() == image.as_ref() { |
||||
return; |
||||
} |
||||
|
||||
self.image.replace(image); |
||||
self.obj().notify_image(); |
||||
} |
||||
|
||||
/// Set the display name used as a fallback for this avatar.
|
||||
fn set_display_name(&self, display_name: Option<String>) { |
||||
if self.display_name.borrow().as_ref() == display_name.as_ref() { |
||||
return; |
||||
} |
||||
|
||||
self.display_name.replace(display_name); |
||||
self.obj().notify_display_name(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
/// Data about a User’s or Room’s avatar.
|
||||
pub struct AvatarData(ObjectSubclass<imp::AvatarData>); |
||||
} |
||||
|
||||
impl AvatarData { |
||||
/// Construct a new empty `AvatarData`.
|
||||
pub fn new() -> Self { |
||||
glib::Object::new() |
||||
} |
||||
|
||||
/// Constructs an `AvatarData` with the given image data.
|
||||
pub fn with_image(image: AvatarImage) -> Self { |
||||
glib::Object::builder().property("image", image).build() |
||||
} |
||||
|
||||
/// Get this avatar as a notification icon.
|
||||
///
|
||||
/// Returns `None` if an error occurred while generating the icon.
|
||||
pub fn as_notification_icon(&self) -> Option<gdk::Texture> { |
||||
let window = Application::default().active_window()?.upcast(); |
||||
|
||||
let icon = if let Some(paintable) = self.image().and_then(|i| i.paintable()) { |
||||
paintable_as_notification_icon(paintable.upcast_ref(), &window) |
||||
} else { |
||||
string_as_notification_icon(&self.display_name().unwrap_or_default(), &window) |
||||
}; |
||||
|
||||
match icon { |
||||
Ok(icon) => Some(icon), |
||||
Err(error) => { |
||||
warn!("Failed to generate icon for notification: {error}"); |
||||
None |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl Default for AvatarData { |
||||
fn default() -> Self { |
||||
Self::new() |
||||
} |
||||
} |
||||
Loading…
Reference in new issue