15 changed files with 470 additions and 45 deletions
@ -0,0 +1,32 @@ |
|||||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||||
|
<interface> |
||||||
|
<template class="ComponentsVideoPlayer" parent="AdwBin"> |
||||||
|
<style> |
||||||
|
<class name="thumbnail"/> |
||||||
|
</style> |
||||||
|
<property name="overflow">hidden</property> |
||||||
|
<child> |
||||||
|
<object class="GtkOverlay"> |
||||||
|
<child> |
||||||
|
<object class="GtkPicture" id="video"/> |
||||||
|
</child> |
||||||
|
<child type="overlay"> |
||||||
|
<object class="GtkLabel" id="timestamp"> |
||||||
|
<style> |
||||||
|
<class name="osd"/> |
||||||
|
<class name="timestamp"/> |
||||||
|
</style> |
||||||
|
<property name="halign">1</property> |
||||||
|
<property name="valign">1</property> |
||||||
|
<property name="margin-start">5</property> |
||||||
|
<property name="margin-top">5</property> |
||||||
|
<property name="label">00:00</property> |
||||||
|
<layout> |
||||||
|
<property name="measure">1</property> |
||||||
|
</layout> |
||||||
|
</object> |
||||||
|
</child> |
||||||
|
</object> |
||||||
|
</child> |
||||||
|
</template> |
||||||
|
</interface> |
||||||
@ -0,0 +1,84 @@ |
|||||||
|
use adw::subclass::prelude::*; |
||||||
|
use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate}; |
||||||
|
|
||||||
|
mod imp { |
||||||
|
use super::*; |
||||||
|
use glib::subclass::InitializingObject; |
||||||
|
use std::cell::RefCell; |
||||||
|
|
||||||
|
#[derive(Debug, Default, CompositeTemplate)] |
||||||
|
#[template(resource = "/org/gnome/FractalNext/components-video-player.ui")] |
||||||
|
pub struct VideoPlayer { |
||||||
|
pub media_file: RefCell<Option<gtk::MediaFile>>, |
||||||
|
#[template_child] |
||||||
|
pub video: TemplateChild<gtk::Picture>, |
||||||
|
#[template_child] |
||||||
|
pub timestamp: TemplateChild<gtk::Label>, |
||||||
|
} |
||||||
|
|
||||||
|
#[glib::object_subclass] |
||||||
|
impl ObjectSubclass for VideoPlayer { |
||||||
|
const NAME: &'static str = "ComponentsVideoPlayer"; |
||||||
|
type Type = super::VideoPlayer; |
||||||
|
type ParentType = adw::Bin; |
||||||
|
|
||||||
|
fn class_init(klass: &mut Self::Class) { |
||||||
|
Self::bind_template(klass); |
||||||
|
} |
||||||
|
|
||||||
|
fn instance_init(obj: &InitializingObject<Self>) { |
||||||
|
obj.init_template(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl ObjectImpl for VideoPlayer {} |
||||||
|
|
||||||
|
impl WidgetImpl for VideoPlayer {} |
||||||
|
|
||||||
|
impl BinImpl for VideoPlayer {} |
||||||
|
} |
||||||
|
|
||||||
|
glib::wrapper! { |
||||||
|
/// A widget displaying a video media file.
|
||||||
|
pub struct VideoPlayer(ObjectSubclass<imp::VideoPlayer>) |
||||||
|
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible; |
||||||
|
} |
||||||
|
|
||||||
|
impl VideoPlayer { |
||||||
|
pub fn new(media_file: >k::MediaFile) -> Self { |
||||||
|
let self_: Self = glib::Object::new(&[]).expect("Failed to create VideoPlayer"); |
||||||
|
self_.build(media_file); |
||||||
|
self_ |
||||||
|
} |
||||||
|
|
||||||
|
pub fn build(&self, media_file: >k::MediaFile) { |
||||||
|
let priv_ = imp::VideoPlayer::from_instance(self); |
||||||
|
|
||||||
|
priv_.video.set_paintable(Some(media_file)); |
||||||
|
let timestamp = &*priv_.timestamp; |
||||||
|
media_file.connect_duration_notify(clone!(@weak timestamp => move |media_file| { |
||||||
|
timestamp.set_label(&duration(media_file)); |
||||||
|
})); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Get the duration of `media_file` as a `String`.
|
||||||
|
fn duration(media_file: >k::MediaFile) -> String { |
||||||
|
let mut time = media_file.duration() / 1000000; |
||||||
|
|
||||||
|
let sec = time % 60; |
||||||
|
time = time - sec; |
||||||
|
let min = (time % (60 * 60)) / 60; |
||||||
|
time = time - (min * 60); |
||||||
|
let hour = time / (60 * 60); |
||||||
|
|
||||||
|
if hour > 0 { |
||||||
|
// FIXME: Find how to localize this.
|
||||||
|
// hour:minutes:seconds
|
||||||
|
format!("{}:{:02}:{:02}", hour, min, sec) |
||||||
|
} else { |
||||||
|
// FIXME: Find how to localize this.
|
||||||
|
// minutes:seconds
|
||||||
|
format!("{:02}:{:02}", min, sec) |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,233 @@ |
|||||||
|
use adw::{prelude::BinExt, subclass::prelude::*}; |
||||||
|
use gettextrs::gettext; |
||||||
|
use gtk::{ |
||||||
|
gio, |
||||||
|
glib::{self, clone}, |
||||||
|
prelude::*, |
||||||
|
subclass::prelude::*, |
||||||
|
}; |
||||||
|
use log::warn; |
||||||
|
use matrix_sdk::ruma::events::room::message::VideoMessageEventContent; |
||||||
|
|
||||||
|
use crate::{ |
||||||
|
components::VideoPlayer, |
||||||
|
session::Session, |
||||||
|
spawn, spawn_tokio, |
||||||
|
utils::{cache_dir, uint_to_i32}, |
||||||
|
}; |
||||||
|
|
||||||
|
mod imp { |
||||||
|
use std::cell::Cell; |
||||||
|
|
||||||
|
use once_cell::sync::Lazy; |
||||||
|
|
||||||
|
use super::*; |
||||||
|
|
||||||
|
#[derive(Debug, Default)] |
||||||
|
pub struct MessageVideo { |
||||||
|
/// The intended display width of the video.
|
||||||
|
pub width: Cell<i32>, |
||||||
|
/// The intended display height of the video.
|
||||||
|
pub height: Cell<i32>, |
||||||
|
} |
||||||
|
|
||||||
|
#[glib::object_subclass] |
||||||
|
impl ObjectSubclass for MessageVideo { |
||||||
|
const NAME: &'static str = "ContentMessageVideo"; |
||||||
|
type Type = super::MessageVideo; |
||||||
|
type ParentType = adw::Bin; |
||||||
|
} |
||||||
|
|
||||||
|
impl ObjectImpl for MessageVideo { |
||||||
|
fn properties() -> &'static [glib::ParamSpec] { |
||||||
|
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| { |
||||||
|
vec![ |
||||||
|
glib::ParamSpec::new_int( |
||||||
|
"width", |
||||||
|
"Width", |
||||||
|
"The intended display width of the video", |
||||||
|
-1, |
||||||
|
i32::MAX, |
||||||
|
-1, |
||||||
|
glib::ParamFlags::WRITABLE, |
||||||
|
), |
||||||
|
glib::ParamSpec::new_int( |
||||||
|
"height", |
||||||
|
"Height", |
||||||
|
"The intended display height of the video", |
||||||
|
-1, |
||||||
|
i32::MAX, |
||||||
|
-1, |
||||||
|
glib::ParamFlags::WRITABLE, |
||||||
|
), |
||||||
|
] |
||||||
|
}); |
||||||
|
|
||||||
|
PROPERTIES.as_ref() |
||||||
|
} |
||||||
|
|
||||||
|
fn set_property( |
||||||
|
&self, |
||||||
|
_obj: &Self::Type, |
||||||
|
_id: usize, |
||||||
|
value: &glib::Value, |
||||||
|
pspec: &glib::ParamSpec, |
||||||
|
) { |
||||||
|
match pspec.name() { |
||||||
|
"width" => { |
||||||
|
self.width.set(value.get().unwrap()); |
||||||
|
} |
||||||
|
"height" => { |
||||||
|
self.height.set(value.get().unwrap()); |
||||||
|
} |
||||||
|
_ => unimplemented!(), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fn constructed(&self, obj: &Self::Type) { |
||||||
|
self.parent_constructed(obj); |
||||||
|
|
||||||
|
// We need to control the value returned by `measure`.
|
||||||
|
obj.set_layout_manager(gtk::NONE_LAYOUT_MANAGER); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl WidgetImpl for MessageVideo { |
||||||
|
fn measure( |
||||||
|
&self, |
||||||
|
obj: &Self::Type, |
||||||
|
orientation: gtk::Orientation, |
||||||
|
for_size: i32, |
||||||
|
) -> (i32, i32, i32, i32) { |
||||||
|
match obj.child() { |
||||||
|
Some(child) => { |
||||||
|
let original_width = self.width.get(); |
||||||
|
let original_height = self.height.get(); |
||||||
|
|
||||||
|
if orientation == gtk::Orientation::Vertical { |
||||||
|
// We limit the width to 320 pixels.
|
||||||
|
let width = for_size.min(320); |
||||||
|
|
||||||
|
let nat_height = if original_height > 0 && original_width > 0 { |
||||||
|
// We don't want the paintable to be upscaled.
|
||||||
|
let width = width.min(original_width); |
||||||
|
width * original_height / original_width |
||||||
|
} else { |
||||||
|
// Get the natural height of the data.
|
||||||
|
child.measure(orientation, width).1 |
||||||
|
}; |
||||||
|
|
||||||
|
// We limit the height to 240 pixels.
|
||||||
|
let height = nat_height.min(240); |
||||||
|
(0, height, -1, -1) |
||||||
|
} else { |
||||||
|
// We limit the height to 240 pixels.
|
||||||
|
let height = for_size.min(240); |
||||||
|
|
||||||
|
let nat_width = if original_height > 0 && original_width > 0 { |
||||||
|
// We don't want the paintable to be upscaled.
|
||||||
|
let height = height.min(original_height); |
||||||
|
height * original_width / original_height |
||||||
|
} else { |
||||||
|
// Get the natural height of the data.
|
||||||
|
child.measure(orientation, height).1 |
||||||
|
}; |
||||||
|
|
||||||
|
// We limit the width to 320 pixels.
|
||||||
|
let width = nat_width.min(320); |
||||||
|
(0, width, -1, -1) |
||||||
|
} |
||||||
|
} |
||||||
|
None => (0, 0, -1, -1), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fn request_mode(&self, _obj: &Self::Type) -> gtk::SizeRequestMode { |
||||||
|
gtk::SizeRequestMode::HeightForWidth |
||||||
|
} |
||||||
|
|
||||||
|
fn size_allocate(&self, obj: &Self::Type, _width: i32, height: i32, baseline: i32) { |
||||||
|
if let Some(child) = obj.child() { |
||||||
|
// We need to allocate just enough width to the child so it doesn't expand.
|
||||||
|
let original_width = self.width.get(); |
||||||
|
let original_height = self.height.get(); |
||||||
|
let width = if original_height > 0 && original_width > 0 { |
||||||
|
height * original_width / original_height |
||||||
|
} else { |
||||||
|
// Get the natural width of the video data.
|
||||||
|
child.measure(gtk::Orientation::Horizontal, height).1 |
||||||
|
}; |
||||||
|
|
||||||
|
child.allocate(width, height, baseline, None); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl BinImpl for MessageVideo {} |
||||||
|
} |
||||||
|
|
||||||
|
glib::wrapper! { |
||||||
|
/// A widget displaying an message's thumbnail.
|
||||||
|
pub struct MessageVideo(ObjectSubclass<imp::MessageVideo>) |
||||||
|
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible; |
||||||
|
} |
||||||
|
|
||||||
|
impl MessageVideo { |
||||||
|
pub fn new(video: VideoMessageEventContent, session: &Session) -> Self { |
||||||
|
let info = video.info.as_deref(); |
||||||
|
let width = uint_to_i32(info.and_then(|info| info.width)); |
||||||
|
let height = uint_to_i32(info.and_then(|info| info.height)); |
||||||
|
|
||||||
|
let self_: Self = glib::Object::new(&[("width", &width), ("height", &height)]) |
||||||
|
.expect("Failed to create MessageVideo"); |
||||||
|
self_.build(video, session); |
||||||
|
self_ |
||||||
|
} |
||||||
|
|
||||||
|
fn build(&self, video: VideoMessageEventContent, session: &Session) { |
||||||
|
let body = video.body.clone(); |
||||||
|
let client = session.client(); |
||||||
|
let handle = spawn_tokio!(async move { client.get_file(video, true,).await }); |
||||||
|
|
||||||
|
spawn!( |
||||||
|
glib::PRIORITY_LOW, |
||||||
|
clone!(@weak self as obj => async move { |
||||||
|
match handle.await.unwrap() { |
||||||
|
Ok(Some(data)) => { |
||||||
|
// The GStreamer backend of GtkVideo doesn't work with input streams so
|
||||||
|
// we need to store the file.
|
||||||
|
// See: https://gitlab.gnome.org/GNOME/gtk/-/issues/4062
|
||||||
|
let mut path = cache_dir(); |
||||||
|
path.push(body); |
||||||
|
let file = gio::File::for_path(path); |
||||||
|
file.replace_contents( |
||||||
|
&data, |
||||||
|
None, |
||||||
|
false, |
||||||
|
gio::FileCreateFlags::REPLACE_DESTINATION, |
||||||
|
gio::NONE_CANCELLABLE, |
||||||
|
) |
||||||
|
.unwrap(); |
||||||
|
let media_file = gtk::MediaFile::for_file(&file); |
||||||
|
media_file.set_muted(true); |
||||||
|
media_file.connect_prepared_notify(|media_file| media_file.play()); |
||||||
|
|
||||||
|
let video_player = VideoPlayer::new(&media_file); |
||||||
|
|
||||||
|
obj.set_child(Some(&video_player)); |
||||||
|
} |
||||||
|
Ok(None) => { |
||||||
|
warn!("Could not retrieve invalid image file"); |
||||||
|
let child = gtk::Label::new(Some(&gettext("Could not retrieve image"))); |
||||||
|
obj.set_child(Some(&child)); |
||||||
|
} |
||||||
|
Err(error) => { |
||||||
|
warn!("Could not retrieve image file: {}", error); |
||||||
|
let child = gtk::Label::new(Some(&gettext("Could not retrieve image"))); |
||||||
|
obj.set_child(Some(&child)); |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue