fractal/components/media/
content_viewer.rsuse adw::{prelude::*, subclass::prelude::*};
use geo_uri::GeoUri;
use gettextrs::gettext;
use gtk::{gdk, gio, glib, CompositeTemplate};
use super::{AnimatedImagePaintable, AudioPlayer, LocationViewer};
use crate::{
components::ContextMenuBin,
prelude::*,
utils::{media::image::IMAGE_QUEUE, CountedRef, File},
};
#[derive(Debug, Default, Clone, Copy)]
pub enum ContentType {
Image,
Audio,
Video,
#[default]
Other,
}
impl ContentType {
pub(crate) fn icon_name(self) -> &'static str {
match self {
ContentType::Image => "image-symbolic",
ContentType::Audio => "audio-symbolic",
ContentType::Video => "video-symbolic",
ContentType::Other => "document-symbolic",
}
}
}
impl From<&str> for ContentType {
fn from(string: &str) -> Self {
match string {
"image" => Self::Image,
"audio" => Self::Audio,
"video" => Self::Video,
_ => Self::Other,
}
}
}
mod imp {
use std::cell::{Cell, RefCell};
use glib::subclass::InitializingObject;
use super::*;
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
#[template(resource = "/org/gnome/Fractal/ui/components/media/content_viewer.ui")]
#[properties(wrapper_type = super::MediaContentViewer)]
pub struct MediaContentViewer {
#[template_child]
stack: TemplateChild<gtk::Stack>,
#[template_child]
viewer: TemplateChild<adw::Bin>,
#[template_child]
fallback: TemplateChild<adw::StatusPage>,
#[property(get, construct_only)]
autoplay: Cell<bool>,
file: RefCell<Option<File>>,
paintable_animation_ref: RefCell<Option<CountedRef>>,
}
#[glib::object_subclass]
impl ObjectSubclass for MediaContentViewer {
const NAME: &'static str = "MediaContentViewer";
type Type = super::MediaContentViewer;
type ParentType = ContextMenuBin;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
Self::bind_template_callbacks(klass);
klass.set_css_name("media-content-viewer");
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for MediaContentViewer {}
impl WidgetImpl for MediaContentViewer {}
impl ContextMenuBinImpl for MediaContentViewer {}
#[gtk::template_callbacks]
impl MediaContentViewer {
pub(super) fn set_visible_child(&self, name: &str) {
self.stack.set_visible_child_name(name);
}
pub(super) fn media_child<T: IsA<gtk::Widget>>(&self) -> Option<T> {
self.viewer.child().and_downcast()
}
pub(super) fn show_fallback(&self, content_type: ContentType) {
self.file.take();
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::Other => gettext("File not Viewable"),
};
self.fallback.set_title(&title);
self.fallback.set_icon_name(Some(content_type.icon_name()));
self.set_visible_child("fallback");
}
pub(super) fn view_image(&self, image: &gdk::Paintable) {
self.set_visible_child("loading");
self.file.take();
let picture = if let Some(picture) = self.media_child::<gtk::Picture>() {
picture
} else {
let picture = gtk::Picture::builder()
.valign(gtk::Align::Center)
.halign(gtk::Align::Center)
.build();
self.viewer.set_child(Some(&picture));
picture
};
picture.set_paintable(Some(image));
self.update_animated_paintable_state();
self.set_visible_child("viewer");
}
pub(super) async fn view_file(&self, file: File, content_type: Option<ContentType>) {
self.set_visible_child("loading");
self.file.replace(Some(file.clone()));
let content_type = if let Some(content_type) = content_type {
content_type
} else {
let file_info = file
.as_gfile()
.query_info_future(
gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
gio::FileQueryInfoFlags::NONE,
glib::Priority::DEFAULT,
)
.await
.ok();
file_info
.as_ref()
.and_then(gio::FileInfo::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 => {
let handle = IMAGE_QUEUE.add_file_request(file, None).await;
if let Ok(image) = handle.await {
self.view_image(&gdk::Paintable::from(image));
return;
}
}
ContentType::Audio => {
let audio = if let Some(audio) = self.media_child::<AudioPlayer>() {
audio
} else {
let audio = AudioPlayer::new();
audio.add_css_class("toolbar");
audio.add_css_class("osd");
audio.set_autoplay(self.autoplay.get());
audio.set_valign(gtk::Align::Center);
audio.set_halign(gtk::Align::Center);
self.viewer.set_child(Some(&audio));
audio
};
audio.set_file(Some(&file.as_gfile()));
self.update_animated_paintable_state();
self.set_visible_child("viewer");
return;
}
ContentType::Video => {
let video = if let Some(video) = self.media_child::<gtk::Video>() {
video
} else {
let video = gtk::Video::builder()
.autoplay(self.autoplay.get())
.valign(gtk::Align::Center)
.halign(gtk::Align::Center)
.build();
self.viewer.set_child(Some(&video));
video
};
video.set_file(Some(&file.as_gfile()));
self.update_animated_paintable_state();
self.set_visible_child("viewer");
return;
}
ContentType::Other => {}
}
self.show_fallback(content_type);
}
pub(super) fn view_location(&self, geo_uri: &GeoUri) {
let location =
if let Some(location) = self.viewer.child().and_downcast::<LocationViewer>() {
location
} else {
let location = LocationViewer::new();
self.viewer.set_child(Some(&location));
location
};
location.set_location(geo_uri);
self.update_animated_paintable_state();
self.set_visible_child("viewer");
}
#[template_callback]
fn update_animated_paintable_state(&self) {
self.paintable_animation_ref.take();
let Some(paintable) = self
.viewer
.child()
.and_downcast::<gtk::Picture>()
.and_then(|p| p.paintable())
.and_downcast::<AnimatedImagePaintable>()
else {
return;
};
if self.viewer.is_mapped() {
self.paintable_animation_ref
.replace(Some(paintable.animation_ref()));
}
}
pub(super) fn stop_playback(&self) {
if let Some(stream) = self
.media_child::<gtk::Video>()
.and_then(|v| v.media_stream())
{
if stream.is_playing() {
stream.pause();
stream.seek(0);
}
}
}
}
}
glib::wrapper! {
pub struct MediaContentViewer(ObjectSubclass<imp::MediaContentViewer>)
@extends gtk::Widget, ContextMenuBin, @implements gtk::Accessible;
}
impl MediaContentViewer {
pub fn new(autoplay: bool) -> Self {
glib::Object::builder()
.property("autoplay", autoplay)
.build()
}
pub(crate) fn stop_playback(&self) {
self.imp().stop_playback();
}
pub(crate) fn show_loading(&self) {
self.imp().set_visible_child("loading");
}
pub(crate) fn show_fallback(&self, content_type: ContentType) {
self.imp().show_fallback(content_type);
}
pub(crate) fn view_image(&self, image: &impl IsA<gdk::Paintable>) {
self.imp().view_image(image.upcast_ref());
}
pub(crate) async fn view_file(&self, file: File, content_type: Option<ContentType>) {
self.imp().view_file(file, content_type).await;
}
pub(crate) fn view_location(&self, geo_uri: &GeoUri) {
self.imp().view_location(geo_uri);
}
pub(crate) fn texture(&self) -> Option<gdk::Texture> {
let paintable = self
.imp()
.media_child::<gtk::Picture>()
.and_then(|p| p.paintable())?;
if let Some(paintable) = paintable.downcast_ref::<AnimatedImagePaintable>() {
paintable.current_texture()
} else {
paintable.downcast::<gdk::Texture>().ok()
}
}
}