[fractal] media-viewer: Split media content display logic into MediaContentViewer



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]