fractal/components/media/
content_viewer.rs1use adw::{prelude::*, subclass::prelude::*};
2use geo_uri::GeoUri;
3use gettextrs::gettext;
4use gtk::{gdk, gio, glib};
5
6use super::{AnimatedImagePaintable, AudioPlayer, AudioPlayerSource, LocationViewer};
7use crate::{
8 MEDIA_FILE_NOTIFIER,
9 components::ContextMenuBin,
10 prelude::*,
11 utils::{CountedRef, File, media::image::IMAGE_QUEUE},
12};
13
14#[derive(Debug, Default, Clone, Copy)]
16pub enum ContentType {
17 Image,
19 Audio,
21 Video,
23 #[default]
27 Other,
28}
29
30impl ContentType {
31 pub(crate) fn icon_name(self) -> &'static str {
33 match self {
34 ContentType::Image => "image-symbolic",
35 ContentType::Audio => "audio-symbolic",
36 ContentType::Video => "video-symbolic",
37 ContentType::Other => "document-symbolic",
38 }
39 }
40}
41
42impl From<&str> for ContentType {
43 fn from(string: &str) -> Self {
44 match string {
45 "image" => Self::Image,
46 "audio" => Self::Audio,
47 "video" => Self::Video,
48 _ => Self::Other,
49 }
50 }
51}
52
53mod imp {
54 use std::cell::{Cell, RefCell};
55
56 use glib::subclass::InitializingObject;
57
58 use super::*;
59
60 #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
61 #[template(resource = "/org/gnome/Fractal/ui/components/media/content_viewer.ui")]
62 #[properties(wrapper_type = super::MediaContentViewer)]
63 pub struct MediaContentViewer {
64 #[template_child]
65 stack: TemplateChild<gtk::Stack>,
66 #[template_child]
67 viewer: TemplateChild<adw::Bin>,
68 #[template_child]
69 fallback: TemplateChild<adw::StatusPage>,
70 #[property(get, construct_only)]
72 autoplay: Cell<bool>,
73 file: RefCell<Option<File>>,
75 paintable_animation_ref: RefCell<Option<CountedRef>>,
76 }
77
78 #[glib::object_subclass]
79 impl ObjectSubclass for MediaContentViewer {
80 const NAME: &'static str = "MediaContentViewer";
81 type Type = super::MediaContentViewer;
82 type ParentType = ContextMenuBin;
83
84 fn class_init(klass: &mut Self::Class) {
85 Self::bind_template(klass);
86 Self::bind_template_callbacks(klass);
87
88 klass.set_css_name("media-content-viewer");
89 }
90
91 fn instance_init(obj: &InitializingObject<Self>) {
92 obj.init_template();
93 }
94 }
95
96 #[glib::derived_properties]
97 impl ObjectImpl for MediaContentViewer {
98 fn dispose(&self) {
99 self.clear();
100 }
101 }
102
103 impl WidgetImpl for MediaContentViewer {}
104 impl ContextMenuBinImpl for MediaContentViewer {}
105
106 #[gtk::template_callbacks]
107 impl MediaContentViewer {
108 pub(super) fn set_visible_child(&self, name: &str) {
110 self.stack.set_visible_child_name(name);
111 }
112
113 pub(super) fn media_child<T: IsA<gtk::Widget>>(&self) -> Option<T> {
115 self.viewer.child().and_downcast()
116 }
117
118 pub(super) fn show_fallback(&self, content_type: ContentType) {
120 let title = match content_type {
121 ContentType::Image => gettext("Image not Viewable"),
122 ContentType::Audio => gettext("Audio Clip not Playable"),
123 ContentType::Video => gettext("Video not Playable"),
124 ContentType::Other => gettext("File not Viewable"),
125 };
126 self.fallback.set_title(&title);
127 self.fallback.set_icon_name(Some(content_type.icon_name()));
128
129 self.set_visible_child("fallback");
130 self.clear();
131 }
132
133 pub(super) fn view_image(&self, image: &gdk::Paintable) {
138 self.set_visible_child("loading");
139 self.clear();
140
141 let picture = if let Some(picture) = self.media_child::<gtk::Picture>() {
142 picture
143 } else {
144 let picture = gtk::Picture::builder()
145 .valign(gtk::Align::Center)
146 .halign(gtk::Align::Center)
147 .build();
148 self.viewer.set_child(Some(&picture));
149 picture
150 };
151
152 picture.set_paintable(Some(image));
153 self.update_animated_paintable_state();
154 self.set_visible_child("viewer");
155 }
156
157 pub(super) async fn view_file(&self, file: File, content_type: Option<ContentType>) {
159 self.set_visible_child("loading");
160 self.clear();
161 self.file.replace(Some(file.clone()));
162
163 let content_type = if let Some(content_type) = content_type {
164 content_type
165 } else {
166 let file_info = file
167 .as_gfile()
168 .query_info_future(
169 gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
170 gio::FileQueryInfoFlags::NONE,
171 glib::Priority::DEFAULT,
172 )
173 .await
174 .ok();
175
176 file_info
177 .as_ref()
178 .and_then(gio::FileInfo::content_type)
179 .and_then(|content_type| gio::content_type_get_mime_type(&content_type))
180 .and_then(|mime| mime.split('/').next().map(Into::into))
181 .unwrap_or_default()
182 };
183
184 match content_type {
185 ContentType::Image => {
186 let handle = IMAGE_QUEUE.add_file_request(file, None);
187 if let Ok(image) = handle.await {
188 self.view_image(&gdk::Paintable::from(image));
189
190 return;
191 }
192 }
193 ContentType::Audio => {
194 let audio = if let Some(audio) = self.media_child::<AudioPlayer>() {
195 audio
196 } else {
197 let audio = AudioPlayer::new();
198 audio.set_standalone(true);
199 audio.set_margin_start(12);
200 audio.set_margin_end(12);
201 audio.set_valign(gtk::Align::Center);
202 self.viewer.set_child(Some(&audio));
203 audio
204 };
205
206 audio.set_source(Some(AudioPlayerSource::File(file.as_gfile())));
207 self.set_visible_child("viewer");
208
209 return;
210 }
211 ContentType::Video => {
212 let video = if let Some(video) = self.media_child::<gtk::Video>() {
213 video
214 } else {
215 let video = gtk::Video::builder()
216 .autoplay(self.autoplay.get())
217 .valign(gtk::Align::Center)
218 .halign(gtk::Align::Center)
219 .build();
220 self.viewer.set_child(Some(&video));
221 video
222 };
223
224 MEDIA_FILE_NOTIFIER.notify();
228
229 video.set_file(Some(&file.as_gfile()));
230 self.set_visible_child("viewer");
231
232 return;
233 }
234 ContentType::Other => {}
236 }
237
238 self.show_fallback(content_type);
239 }
240
241 pub(super) fn view_location(&self, geo_uri: &GeoUri) {
243 let location = self.viewer.child_or_default::<LocationViewer>();
244
245 location.set_location(geo_uri);
246 self.set_visible_child("viewer");
247 self.clear();
248 }
249
250 #[template_callback]
252 fn update_animated_paintable_state(&self) {
253 self.paintable_animation_ref.take();
254
255 let Some(paintable) = self
256 .media_child::<gtk::Picture>()
257 .and_then(|p| p.paintable())
258 .and_downcast::<AnimatedImagePaintable>()
259 else {
260 return;
261 };
262
263 if self.viewer.is_mapped() {
264 self.paintable_animation_ref
265 .replace(Some(paintable.animation_ref()));
266 }
267 }
268
269 pub(super) fn clear(&self) {
271 if let Some(video) = self.media_child::<gtk::Video>() {
272 video.set_file(None::<&gio::File>);
273 }
274
275 self.paintable_animation_ref.take();
276 self.file.take();
277 }
278 }
279}
280
281glib::wrapper! {
282 pub struct MediaContentViewer(ObjectSubclass<imp::MediaContentViewer>)
284 @extends gtk::Widget, ContextMenuBin,
285 @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
286}
287
288impl MediaContentViewer {
289 pub fn new(autoplay: bool) -> Self {
290 glib::Object::builder()
291 .property("autoplay", autoplay)
292 .build()
293 }
294
295 pub(crate) fn clear(&self) {
299 self.imp().clear();
300 }
301
302 pub(crate) fn show_loading(&self) {
304 self.imp().set_visible_child("loading");
305 }
306
307 pub(crate) fn show_fallback(&self, content_type: ContentType) {
309 self.imp().show_fallback(content_type);
310 }
311
312 pub(crate) fn view_image(&self, image: &impl IsA<gdk::Paintable>) {
317 self.imp().view_image(image.upcast_ref());
318 }
319
320 pub(crate) async fn view_file(&self, file: File, content_type: Option<ContentType>) {
324 self.imp().view_file(file, content_type).await;
325 }
326
327 pub(crate) fn view_location(&self, geo_uri: &GeoUri) {
329 self.imp().view_location(geo_uri);
330 }
331
332 pub(crate) fn texture(&self) -> Option<gdk::Texture> {
334 let paintable = self
335 .imp()
336 .media_child::<gtk::Picture>()
337 .and_then(|p| p.paintable())?;
338
339 if let Some(paintable) = paintable.downcast_ref::<AnimatedImagePaintable>() {
340 paintable.current_texture()
341 } else {
342 paintable.downcast::<gdk::Texture>().ok()
343 }
344 }
345}