Browse Source

media-viewer: Split media content display logic into MediaContentViewer

Part-of: <https://gitlab.gnome.org/GNOME/fractal/-/merge_requests/1085>
merge-requests/1327/merge
Kévin Commaille 4 years ago committed by Marge Bot
parent
commit
c216e78edf
  1. 1
      data/resources/resources.gresource.xml
  2. 4
      data/resources/style.css
  3. 44
      data/resources/ui/components-media-content-viewer.ui
  4. 3
      data/resources/ui/media-viewer.ui
  5. 2
      po/POTFILES.in
  6. 74
      src/components/audio_player.rs
  7. 278
      src/components/media_content_viewer.rs
  8. 2
      src/components/mod.rs
  9. 44
      src/session/media_viewer.rs

1
data/resources/resources.gresource.xml

@ -38,6 +38,7 @@
<file compressed="true" preprocess="xml-stripblanks" alias="components-editable-avatar.ui">ui/components-editable-avatar.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="components-entry-row.ui">ui/components-entry-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="components-loading-listbox-row.ui">ui/components-loading-listbox-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="components-media-content-viewer.ui">ui/components-media-content-viewer.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="components-password-entry-row.ui">ui/components-password-entry-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="components-reaction-chooser.ui">ui/components-reaction-chooser.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="components-video-player.ui">ui/components-video-player.ui</file>

4
data/resources/style.css

@ -173,6 +173,10 @@ row .heading {
font-weight: 600;
}
media-content-viewer controls {
min-width: 300px;
}
/* Login */

44
data/resources/ui/components-media-content-viewer.ui

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="ComponentsMediaContentViewer" parent="AdwBin">
<property name="child">
<object class="GtkStack" id="stack">
<child>
<object class="GtkStackPage">
<property name="name">loading</property>
<property name="child">
<object class="GtkSpinner">
<property name="spinning">true</property>
<property name="valign">center</property>
<property name="halign">center</property>
<property name="vexpand">True</property>
<style>
<class name="session-loading-spinner"/>
</style>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">viewer</property>
<property name="child">
<object class="AdwBin" id="viewer">
<property name="halign">center</property>
<property name="valign">center</property>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">fallback</property>
<property name="child">
<object class="AdwStatusPage" id="fallback"/>
</property>
</object>
</child>
</object>
</property>
</template>
</interface>

3
data/resources/ui/media-viewer.ui

@ -48,7 +48,8 @@
</object>
</child>
<child>
<object class="AdwBin" id="media">
<object class="ComponentsMediaContentViewer" id="media">
<property name="autoplay">true</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="vexpand">true</property>

2
po/POTFILES.in

@ -42,6 +42,7 @@ data/resources/ui/qr-code-scanner.ui
# Rust files
src/application.rs
src/components/editable_avatar.rs
src/components/media_content_viewer.rs
src/error_page.rs
src/login/mod.rs
src/secret.rs
@ -65,7 +66,6 @@ src/session/content/room_history/state_row/mod.rs
src/session/content/room_history/verification_info_bar.rs
src/session/content/verification/identity_verification_widget.rs
src/session/content/verification/session_verification.rs
src/session/media_viewer.rs
src/session/mod.rs
src/session/room/event_actions.rs
src/session/room/member_role.rs

74
src/components/audio_player.rs

@ -1,8 +1,8 @@
use adw::subclass::prelude::*;
use gtk::{glib, prelude::*, subclass::prelude::*, CompositeTemplate};
use gtk::{gio, glib, prelude::*, subclass::prelude::*, CompositeTemplate};
mod imp {
use std::cell::RefCell;
use std::cell::{Cell, RefCell};
use glib::subclass::InitializingObject;
use once_cell::sync::Lazy;
@ -14,6 +14,9 @@ mod imp {
pub struct AudioPlayer {
/// The media file to play.
pub media_file: RefCell<Option<gtk::MediaFile>>,
/// Whether to play the media automatically.
pub autoplay: Cell<bool>,
pub autoplay_handler: RefCell<Option<glib::SignalHandlerId>>,
}
#[glib::object_subclass]
@ -34,13 +37,22 @@ mod imp {
impl ObjectImpl for AudioPlayer {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecObject::new(
"media-file",
"Media File",
"The media file to play",
gtk::MediaFile::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
)]
vec![
glib::ParamSpecObject::new(
"media-file",
"Media File",
"The media file to play",
gtk::MediaFile::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
glib::ParamSpecBoolean::new(
"autoplay",
"Autoplay",
"Whether to play the media automatically",
false,
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
]
});
PROPERTIES.as_ref()
@ -57,6 +69,7 @@ mod imp {
"media-file" => {
obj.set_media_file(value.get().unwrap());
}
"autoplay" => obj.set_autoplay(value.get().unwrap()),
_ => unimplemented!(),
}
}
@ -64,6 +77,7 @@ mod imp {
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"media-file" => obj.media_file().to_value(),
"autoplay" => obj.autoplay().to_value(),
_ => unimplemented!(),
}
}
@ -97,9 +111,49 @@ impl AudioPlayer {
return;
}
self.imp().media_file.replace(media_file);
let priv_ = self.imp();
if let Some(media_file) = priv_.media_file.take() {
if let Some(handler_id) = priv_.autoplay_handler.take() {
media_file.disconnect(handler_id);
}
}
if self.autoplay() {
if let Some(media_file) = &media_file {
priv_
.autoplay_handler
.replace(Some(media_file.connect_prepared_notify(|media_file| {
if media_file.is_prepared() {
media_file.play()
}
})));
}
}
priv_.media_file.replace(media_file);
self.notify("media-file");
}
/// Set the file to play.
///
/// This is a convenience method that calls [`set_media_file()`].
pub fn set_file(&self, file: Option<&gio::File>) {
self.set_media_file(file.map(gtk::MediaFile::for_file));
}
pub fn autoplay(&self) -> bool {
self.imp().autoplay.get()
}
pub fn set_autoplay(&self, autoplay: bool) {
if self.autoplay() == autoplay {
return;
}
self.imp().autoplay.set(autoplay);
self.notify("autoplay");
}
}
impl Default for AudioPlayer {

278
src/components/media_content_viewer.rs

@ -0,0 +1,278 @@
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::{gdk, gio, glib, glib::clone, subclass::prelude::*, CompositeTemplate};
use log::warn;
use super::AudioPlayer;
use crate::spawn;
pub enum ContentType {
Image,
Audio,
Video,
Unknown,
}
impl ContentType {
pub fn icon_name(&self) -> &'static str {
match self {
ContentType::Image => "image-x-generic-symbolic",
ContentType::Audio => "audio-x-generic-symbolic",
ContentType::Video => "video-x-generic-symbolic",
ContentType::Unknown => "text-x-generic-symbolic",
}
}
}
impl Default for ContentType {
fn default() -> Self {
Self::Unknown
}
}
impl From<&str> for ContentType {
fn from(string: &str) -> Self {
match string {
"image" => Self::Image,
"audio" => Self::Audio,
"video" => Self::Video,
_ => Self::Unknown,
}
}
}
mod imp {
use std::cell::Cell;
use glib::subclass::InitializingObject;
use once_cell::sync::Lazy;
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/org/gnome/Fractal/components-media-content-viewer.ui")]
pub struct MediaContentViewer {
/// Whether to play the media content automatically.
pub autoplay: Cell<bool>,
#[template_child]
pub stack: TemplateChild<gtk::Stack>,
#[template_child]
pub viewer: TemplateChild<adw::Bin>,
#[template_child]
pub fallback: TemplateChild<adw::StatusPage>,
}
#[glib::object_subclass]
impl ObjectSubclass for MediaContentViewer {
const NAME: &'static str = "ComponentsMediaContentViewer";
type Type = super::MediaContentViewer;
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
klass.set_css_name("media-content-viewer");
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for MediaContentViewer {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecBoolean::new(
"autoplay",
"Autoplay",
"Whether to play the media content automatically",
false,
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
)]
});
PROPERTIES.as_ref()
}
fn set_property(
&self,
obj: &Self::Type,
_id: usize,
value: &glib::Value,
pspec: &glib::ParamSpec,
) {
match pspec.name() {
"autoplay" => obj.set_autoplay(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"autoplay" => obj.autoplay().to_value(),
_ => unimplemented!(),
}
}
}
impl WidgetImpl for MediaContentViewer {}
impl BinImpl for MediaContentViewer {}
}
glib::wrapper! {
/// Widget to view any media file.
pub struct MediaContentViewer(ObjectSubclass<imp::MediaContentViewer>)
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}
impl MediaContentViewer {
pub fn new(autoplay: bool) -> Self {
glib::Object::new(&[("autoplay", &autoplay)]).expect("Failed to create MediaContentViewer")
}
pub fn autoplay(&self) -> bool {
self.imp().autoplay.get()
}
fn set_autoplay(&self, autoplay: bool) {
if self.autoplay() == autoplay {
return;
}
self.imp().autoplay.set(autoplay);
self.notify("autoplay");
}
/// Show the loading screen.
pub fn show_loading(&self) {
self.imp().stack.set_visible_child_name("loading");
}
/// Show the viewer.
fn show_viewer(&self) {
self.imp().stack.set_visible_child_name("viewer");
}
/// Show the fallback message for the given content type.
pub fn show_fallback(&self, content_type: ContentType) {
let priv_ = self.imp();
let fallback = &priv_.fallback;
let title = match content_type {
ContentType::Image => gettext("Image not Viewable"),
ContentType::Audio => gettext("Audio Clip not Playable"),
ContentType::Video => gettext("Video not Playable"),
ContentType::Unknown => gettext("File not Viewable"),
};
fallback.set_title(&title);
fallback.set_icon_name(Some(content_type.icon_name()));
priv_.stack.set_visible_child_name("fallback");
}
/// View the given image as bytes.
///
/// If you have an image file, you can also use
/// [`MediaContentViewer::view_file()`].
pub fn view_image(&self, image: &gdk::Texture) {
self.show_loading();
let priv_ = self.imp();
let picture = if let Some(picture) = priv_
.viewer
.child()
.and_then(|widget| widget.downcast::<gtk::Picture>().ok())
{
picture
} else {
let picture = gtk::Picture::new();
priv_.viewer.set_child(Some(&picture));
picture
};
picture.set_paintable(Some(image));
self.show_viewer();
}
/// View the given file.
pub fn view_file(&self, file: gio::File) {
self.show_loading();
spawn!(clone!(@weak self as obj => async move {
obj.view_file_inner(file).await;
}));
}
async fn view_file_inner(&self, file: gio::File) {
let priv_ = self.imp();
let file_info = file
.query_info_future(
*gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
gio::FileQueryInfoFlags::NONE,
glib::PRIORITY_DEFAULT,
)
.await
.ok();
let content_type: ContentType = file_info
.as_ref()
.and_then(|info| info.content_type())
.and_then(|content_type| gio::content_type_get_mime_type(&content_type))
.and_then(|mime| mime.split('/').next().map(Into::into))
.unwrap_or_default();
match content_type {
ContentType::Image => match gdk::Texture::from_file(&file) {
Ok(texture) => {
self.view_image(&texture);
return;
}
Err(error) => {
warn!("Could not load GdkTexture from file: {:?}", error);
}
},
ContentType::Audio => {
let audio = if let Some(audio) = priv_
.viewer
.child()
.and_then(|widget| widget.downcast::<AudioPlayer>().ok())
{
audio
} else {
let audio = AudioPlayer::new();
audio.add_css_class("toolbar");
audio.add_css_class("osd");
audio.set_autoplay(self.autoplay());
priv_.viewer.set_child(Some(&audio));
audio
};
audio.set_file(Some(&file));
self.show_viewer();
return;
}
ContentType::Video => {
let video = if let Some(video) = priv_
.viewer
.child()
.and_then(|widget| widget.downcast::<gtk::Video>().ok())
{
video
} else {
let video = gtk::Video::new();
video.set_autoplay(self.autoplay());
priv_.viewer.set_child(Some(&video));
video
};
video.set_file(Some(&file));
self.show_viewer();
return;
}
_ => {}
}
self.show_fallback(content_type);
}
}

2
src/components/mod.rs

@ -12,6 +12,7 @@ mod entry_row;
mod in_app_notification;
mod label_with_widgets;
mod loading_listbox_row;
mod media_content_viewer;
mod password_entry_row;
mod pill;
mod reaction_chooser;
@ -36,6 +37,7 @@ pub use self::{
in_app_notification::InAppNotification,
label_with_widgets::LabelWithWidgets,
loading_listbox_row::LoadingListBoxRow,
media_content_viewer::{ContentType, MediaContentViewer},
password_entry_row::PasswordEntryRow,
pill::Pill,
reaction_chooser::ReactionChooser,

44
src/session/media_viewer.rs

@ -1,11 +1,16 @@
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::{gdk, gio, glib, glib::clone, subclass::prelude::*, CompositeTemplate};
use log::warn;
use matrix_sdk::ruma::events::{room::message::MessageType, AnyMessageLikeEventContent};
use super::room::EventActions;
use crate::{session::room::Event, spawn, utils::cache_dir, Window};
use crate::{
components::{ContentType, MediaContentViewer},
session::room::Event,
spawn,
utils::cache_dir,
Window,
};
mod imp {
use std::cell::{Cell, RefCell};
@ -26,7 +31,7 @@ mod imp {
#[template_child]
pub menu: TemplateChild<gtk::MenuButton>,
#[template_child]
pub media: TemplateChild<adw::Bin>,
pub media: TemplateChild<MediaContentViewer>,
}
#[glib::object_subclass]
@ -218,6 +223,8 @@ impl MediaViewer {
}
fn build(&self) {
self.imp().media.show_loading();
if let Some(event) = self.event() {
self.set_event_actions(Some(&event));
if let Some(AnyMessageLikeEventContent::RoomMessage(content)) = event.message_content()
@ -233,25 +240,18 @@ impl MediaViewer {
match event.get_media_content().await {
Ok((_, _, data)) => {
match gdk::Texture::from_bytes(&glib::Bytes::from(&data))
{
Ok(texture) => {
let child = gtk::Picture::for_paintable(&texture);
priv_.media.set_child(Some(&child));
}
Err(error) => {
warn!("Image file not supported: {}", error);
let child = gtk::Label::new(Some(&gettext("Image file not supported")));
priv_.media.set_child(Some(&child));
}
match gdk::Texture::from_bytes(&glib::Bytes::from(&data)) {
Ok(texture) => {
priv_.media.view_image(&texture);
return;
}
Err(error) => warn!("Could not load GdkTexture from file: {}", error),
}
}
Err(error) => {
warn!("Could not retrieve image file: {}", error);
let child = gtk::Label::new(Some(&gettext("Could not retrieve image")));
priv_.media.set_child(Some(&child));
}
Err(error) => warn!("Could not retrieve image file: {}", error),
}
priv_.media.show_fallback(ContentType::Image);
})
);
}
@ -279,14 +279,12 @@ impl MediaViewer {
gio::Cancellable::NONE,
)
.unwrap();
let child = gtk::Video::builder().file(&file).autoplay(true).build();
priv_.media.set_child(Some(&child));
priv_.media.view_file(file);
}
Err(error) => {
warn!("Could not retrieve video file: {}", error);
let child = gtk::Label::new(Some(&gettext("Could not retrieve video")));
priv_.media.set_child(Some(&child));
priv_.media.show_fallback(ContentType::Video);
}
}
})

Loading…
Cancel
Save