|
|
|
|
@ -1,19 +1,16 @@
|
|
|
|
|
use gtk::{gdk, glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate}; |
|
|
|
|
use matrix_sdk::{ |
|
|
|
|
media::{MediaEventContent, MediaThumbnailSize}, |
|
|
|
|
ruma::{ |
|
|
|
|
api::client::media::get_content_thumbnail::v3::Method, |
|
|
|
|
events::room::message::{ImageMessageEventContent, MessageType, VideoMessageEventContent}, |
|
|
|
|
uint, |
|
|
|
|
}, |
|
|
|
|
use matrix_sdk::media::{MediaEventContent, MediaThumbnailSize}; |
|
|
|
|
use ruma::{ |
|
|
|
|
api::client::media::get_content_thumbnail::v3::Method, |
|
|
|
|
events::room::message::{ImageMessageEventContent, MessageType, VideoMessageEventContent}, |
|
|
|
|
}; |
|
|
|
|
use tracing::warn; |
|
|
|
|
|
|
|
|
|
use super::{HistoryViewerEvent, MediaHistoryViewer}; |
|
|
|
|
use crate::{ |
|
|
|
|
matrix_filename, session::model::Session, spawn, spawn_tokio, |
|
|
|
|
utils::add_activate_binding_action, |
|
|
|
|
}; |
|
|
|
|
use crate::{matrix_filename, spawn, spawn_tokio, utils::add_activate_binding_action}; |
|
|
|
|
|
|
|
|
|
/// The default size requested by a thumbnail.
|
|
|
|
|
const THUMBNAIL_SIZE: u32 = 300; |
|
|
|
|
|
|
|
|
|
mod imp { |
|
|
|
|
use std::cell::RefCell; |
|
|
|
|
@ -91,137 +88,151 @@ mod imp {
|
|
|
|
|
if *self.event.borrow() == event { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
let obj = self.obj(); |
|
|
|
|
|
|
|
|
|
if let Some(event) = &event { |
|
|
|
|
let Some(room) = event.room() else { |
|
|
|
|
return; |
|
|
|
|
}; |
|
|
|
|
let Some(session) = room.session() else { |
|
|
|
|
return; |
|
|
|
|
}; |
|
|
|
|
match event.message_content() { |
|
|
|
|
MessageType::Image(content) => { |
|
|
|
|
obj.show_image(content, &session); |
|
|
|
|
} |
|
|
|
|
MessageType::Video(content) => { |
|
|
|
|
obj.show_video(content, &session); |
|
|
|
|
} |
|
|
|
|
_ => {} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
self.event.replace(event); |
|
|
|
|
obj.notify_event(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
self.update(); |
|
|
|
|
|
|
|
|
|
glib::wrapper! { |
|
|
|
|
/// A row presenting a media (image or video) event.
|
|
|
|
|
pub struct MediaItem(ObjectSubclass<imp::MediaItem>) |
|
|
|
|
@extends gtk::Widget, @implements gtk::Accessible; |
|
|
|
|
} |
|
|
|
|
self.obj().notify_event(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[gtk::template_callbacks] |
|
|
|
|
impl MediaItem { |
|
|
|
|
fn show_image(&self, image: ImageMessageEventContent, session: &Session) { |
|
|
|
|
let imp = self.imp(); |
|
|
|
|
/// Update this item for the current state.
|
|
|
|
|
fn update(&self) { |
|
|
|
|
let Some(message_content) = self.event.borrow().as_ref().map(|e| e.message_content()) |
|
|
|
|
else { |
|
|
|
|
return; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
if let Some(icon) = imp.overlay_icon.take() { |
|
|
|
|
imp.overlay.remove_overlay(&icon); |
|
|
|
|
match message_content { |
|
|
|
|
MessageType::Image(content) => { |
|
|
|
|
self.show_image(content); |
|
|
|
|
} |
|
|
|
|
MessageType::Video(content) => { |
|
|
|
|
self.show_video(content); |
|
|
|
|
} |
|
|
|
|
_ => {} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let filename = matrix_filename!(image, Some(mime::IMAGE)); |
|
|
|
|
self.set_tooltip_text(Some(&filename)); |
|
|
|
|
/// Show the given image with this item.
|
|
|
|
|
fn show_image(&self, image: ImageMessageEventContent) { |
|
|
|
|
if let Some(icon) = self.overlay_icon.take() { |
|
|
|
|
self.overlay.remove_overlay(&icon); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
self.load_thumbnail(image, session); |
|
|
|
|
} |
|
|
|
|
let filename = matrix_filename!(image, Some(mime::IMAGE)); |
|
|
|
|
self.obj().set_tooltip_text(Some(&filename)); |
|
|
|
|
|
|
|
|
|
self.load_thumbnail(image); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fn show_video(&self, video: VideoMessageEventContent, session: &Session) { |
|
|
|
|
let imp = self.imp(); |
|
|
|
|
/// Show the given video with this item.
|
|
|
|
|
fn show_video(&self, video: VideoMessageEventContent) { |
|
|
|
|
if self.overlay_icon.borrow().is_none() { |
|
|
|
|
let icon = gtk::Image::builder() |
|
|
|
|
.icon_name("media-playback-start-symbolic") |
|
|
|
|
.css_classes(vec!["osd".to_string()]) |
|
|
|
|
.halign(gtk::Align::Center) |
|
|
|
|
.valign(gtk::Align::Center) |
|
|
|
|
.accessible_role(gtk::AccessibleRole::Presentation) |
|
|
|
|
.build(); |
|
|
|
|
|
|
|
|
|
self.overlay.add_overlay(&icon); |
|
|
|
|
self.overlay_icon.replace(Some(icon)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if imp.overlay_icon.borrow().is_none() { |
|
|
|
|
let icon = gtk::Image::builder() |
|
|
|
|
.icon_name("media-playback-start-symbolic") |
|
|
|
|
.css_classes(vec!["osd".to_string()]) |
|
|
|
|
.halign(gtk::Align::Center) |
|
|
|
|
.valign(gtk::Align::Center) |
|
|
|
|
.accessible_role(gtk::AccessibleRole::Presentation) |
|
|
|
|
.build(); |
|
|
|
|
let filename = matrix_filename!(video, Some(mime::VIDEO)); |
|
|
|
|
self.obj().set_tooltip_text(Some(&filename)); |
|
|
|
|
|
|
|
|
|
imp.overlay.add_overlay(&icon); |
|
|
|
|
imp.overlay_icon.replace(Some(icon)); |
|
|
|
|
self.load_thumbnail(video); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let filename = matrix_filename!(video, Some(mime::VIDEO)); |
|
|
|
|
self.set_tooltip_text(Some(&filename)); |
|
|
|
|
/// Load the thumbnail for the given media event content.
|
|
|
|
|
fn load_thumbnail<C>(&self, content: C) |
|
|
|
|
where |
|
|
|
|
C: MediaEventContent + Send + Sync + Clone + 'static, |
|
|
|
|
{ |
|
|
|
|
let Some(session) = self |
|
|
|
|
.event |
|
|
|
|
.borrow() |
|
|
|
|
.as_ref() |
|
|
|
|
.and_then(|e| e.room()) |
|
|
|
|
.and_then(|r| r.session()) |
|
|
|
|
else { |
|
|
|
|
return; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
self.load_thumbnail(video, session); |
|
|
|
|
} |
|
|
|
|
let media = session.client().media(); |
|
|
|
|
let handle = spawn_tokio!(async move { |
|
|
|
|
let thumbnail = if content.thumbnail_source().is_some() { |
|
|
|
|
media |
|
|
|
|
.get_thumbnail( |
|
|
|
|
&content, |
|
|
|
|
MediaThumbnailSize { |
|
|
|
|
method: Method::Scale, |
|
|
|
|
width: THUMBNAIL_SIZE.into(), |
|
|
|
|
height: THUMBNAIL_SIZE.into(), |
|
|
|
|
}, |
|
|
|
|
true, |
|
|
|
|
) |
|
|
|
|
.await |
|
|
|
|
.ok() |
|
|
|
|
.flatten() |
|
|
|
|
} else { |
|
|
|
|
None |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
fn load_thumbnail<C>(&self, content: C, session: &Session) |
|
|
|
|
where |
|
|
|
|
C: MediaEventContent + Send + Sync + Clone + 'static, |
|
|
|
|
{ |
|
|
|
|
let media = session.client().media(); |
|
|
|
|
let handle = spawn_tokio!(async move { |
|
|
|
|
let thumbnail = if content.thumbnail_source().is_some() { |
|
|
|
|
media |
|
|
|
|
.get_thumbnail( |
|
|
|
|
&content, |
|
|
|
|
MediaThumbnailSize { |
|
|
|
|
method: Method::Scale, |
|
|
|
|
width: uint!(300), |
|
|
|
|
height: uint!(300), |
|
|
|
|
}, |
|
|
|
|
true, |
|
|
|
|
) |
|
|
|
|
.await |
|
|
|
|
.ok() |
|
|
|
|
.flatten() |
|
|
|
|
} else { |
|
|
|
|
None |
|
|
|
|
}; |
|
|
|
|
if let Some(data) = thumbnail { |
|
|
|
|
Ok(Some(data)) |
|
|
|
|
} else { |
|
|
|
|
media.get_file(&content, true).await |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
if let Some(data) = thumbnail { |
|
|
|
|
Ok(Some(data)) |
|
|
|
|
} else { |
|
|
|
|
media.get_file(&content, true).await |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
spawn!( |
|
|
|
|
glib::Priority::LOW, |
|
|
|
|
clone!( |
|
|
|
|
#[weak(rename_to = obj)] |
|
|
|
|
self, |
|
|
|
|
async move { |
|
|
|
|
let imp = obj.imp(); |
|
|
|
|
|
|
|
|
|
match handle.await.unwrap() { |
|
|
|
|
Ok(Some(data)) => { |
|
|
|
|
match gdk::Texture::from_bytes(&glib::Bytes::from(&data)) { |
|
|
|
|
Ok(texture) => { |
|
|
|
|
imp.picture.set_paintable(Some(&texture)); |
|
|
|
|
} |
|
|
|
|
Err(error) => { |
|
|
|
|
warn!("Image file not supported: {}", error); |
|
|
|
|
spawn!( |
|
|
|
|
glib::Priority::LOW, |
|
|
|
|
clone!( |
|
|
|
|
#[weak(rename_to = imp)] |
|
|
|
|
self, |
|
|
|
|
async move { |
|
|
|
|
match handle.await.unwrap() { |
|
|
|
|
Ok(Some(data)) => { |
|
|
|
|
match gdk::Texture::from_bytes(&glib::Bytes::from(&data)) { |
|
|
|
|
Ok(texture) => { |
|
|
|
|
imp.picture.set_paintable(Some(&texture)); |
|
|
|
|
} |
|
|
|
|
Err(error) => { |
|
|
|
|
warn!("Image file not supported: {}", error); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
Ok(None) => { |
|
|
|
|
warn!("Could not retrieve invalid media file"); |
|
|
|
|
} |
|
|
|
|
Err(error) => { |
|
|
|
|
warn!("Could not retrieve media file: {}", error); |
|
|
|
|
Ok(None) => { |
|
|
|
|
warn!("Could not retrieve invalid media file"); |
|
|
|
|
} |
|
|
|
|
Err(error) => { |
|
|
|
|
warn!("Could not retrieve media file: {}", error); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
) |
|
|
|
|
); |
|
|
|
|
) |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
glib::wrapper! { |
|
|
|
|
/// A row presenting a media (image or video) event.
|
|
|
|
|
pub struct MediaItem(ObjectSubclass<imp::MediaItem>) |
|
|
|
|
@extends gtk::Widget, @implements gtk::Accessible; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[gtk::template_callbacks] |
|
|
|
|
impl MediaItem { |
|
|
|
|
/// Construct a new empty `MediaItem`.
|
|
|
|
|
pub fn new() -> Self { |
|
|
|
|
glib::Object::new() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// The item was activated.
|
|
|
|
|
#[template_callback] |
|
|
|
|
fn activate(&self) { |
|
|
|
|
let media_history_viewer = self |
|
|
|
|
|