[fractal] media-viewer: Split media content display logic into MediaContentViewer
- From: Marge Bot <marge-bot src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal] media-viewer: Split media content display logic into MediaContentViewer
- Date: Wed, 27 Apr 2022 13:17:43 +0000 (UTC)
commit c216e78edfc63debb6b1462b26bf78643450087e
Author: Kévin Commaille <zecakeh tedomum fr>
Date: Sun Apr 24 15:56:31 2022 +0200
media-viewer: Split media content display logic into MediaContentViewer
Part-of: <https://gitlab.gnome.org/GNOME/fractal/-/merge_requests/1085>
data/resources/resources.gresource.xml | 1 +
data/resources/style.css | 4 +
.../ui/components-media-content-viewer.ui | 44 ++++
data/resources/ui/media-viewer.ui | 3 +-
po/POTFILES.in | 2 +-
src/components/audio_player.rs | 74 +++++-
src/components/media_content_viewer.rs | 278 +++++++++++++++++++++
src/components/mod.rs | 2 +
src/session/media_viewer.rs | 44 ++--
9 files changed, 417 insertions(+), 35 deletions(-)
---
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 0ea971393..9b335e605 100644
--- a/data/resources/resources.gresource.xml
+++ b/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>
diff --git a/data/resources/style.css b/data/resources/style.css
index 9e8c0c908..5ff488677 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -173,6 +173,10 @@ row .heading {
font-weight: 600;
}
+media-content-viewer controls {
+ min-width: 300px;
+}
+
/* Login */
diff --git a/data/resources/ui/components-media-content-viewer.ui
b/data/resources/ui/components-media-content-viewer.ui
new file mode 100644
index 000000000..d32b1efe0
--- /dev/null
+++ b/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>
diff --git a/data/resources/ui/media-viewer.ui b/data/resources/ui/media-viewer.ui
index 08116b38c..a9b8a0354 100644
--- a/data/resources/ui/media-viewer.ui
+++ b/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>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 8f7a71ec4..c9386489b 100644
--- a/po/POTFILES.in
+++ b/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
diff --git a/src/components/audio_player.rs b/src/components/audio_player.rs
index 362978367..d6ff4dde0 100644
--- a/src/components/audio_player.rs
+++ b/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 {
diff --git a/src/components/media_content_viewer.rs b/src/components/media_content_viewer.rs
new file mode 100644
index 000000000..8f06009c0
--- /dev/null
+++ b/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);
+ }
+}
diff --git a/src/components/mod.rs b/src/components/mod.rs
index 06958227e..41bf54e25 100644
--- a/src/components/mod.rs
+++ b/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,
diff --git a/src/session/media_viewer.rs b/src/session/media_viewer.rs
index 75ed9bdc1..e1ca83906 100644
--- a/src/session/media_viewer.rs
+++ b/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);
}
}
})
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]