Skip to main content

mimick/library/lightbox/
video.rs

1use std::rc::Rc;
2
3use gtk::prelude::*;
4
5use super::{AssetInfo, Lightbox};
6use crate::library::context_menu::show_alert_dialog;
7use crate::library::download::{open_with_chooser, spawn_video_handoff};
8use crate::library::video_player::{VideoHooks, VideoPlayer, VideoSource};
9
10impl Lightbox {
11    fn video_player(self: &Rc<Self>) -> Result<Rc<VideoPlayer>, String> {
12        if let Some(player) = self.video.borrow().as_ref() {
13            return Ok(player.clone());
14        }
15        let (volume, muted) = {
16            let config = self.ui.ctx.config.read();
17            (config.data.video_volume, config.data.video_muted)
18        };
19        let player = VideoPlayer::new(&self.video_badge_button, volume, muted, self.video_hooks())?;
20        self.picture.overlay.add_overlay(player.surface());
21        self.picture.overlay.add_overlay(player.controls());
22        let swipe = gtk::GestureSwipe::new();
23        swipe.set_touch_only(false);
24        let lb = Rc::downgrade(self);
25        swipe.connect_swipe(move |_, vx, _| {
26            if let Some(lb) = lb.upgrade() {
27                lb.handle_swipe(vx);
28            }
29        });
30        player.surface().add_controller(swipe);
31        player.bind_compact(&self.ui.split);
32        player.set_fullscreen(self.ui.window.is_fullscreen());
33        *self.video.borrow_mut() = Some(player.clone());
34        Ok(player)
35    }
36
37    fn video_hooks(self: &Rc<Self>) -> VideoHooks {
38        let start = Rc::downgrade(self);
39        let fullscreen = Rc::downgrade(self);
40        let error = Rc::downgrade(self);
41        VideoHooks {
42            start: Box::new(move || {
43                if let Some(lb) = start.upgrade() {
44                    lb.load_video();
45                }
46            }),
47            fullscreen: Box::new(move || {
48                if let Some(lb) = fullscreen.upgrade() {
49                    lb.toggle_fullscreen();
50                }
51            }),
52            error: Box::new(move |message| {
53                if let Some(lb) = error.upgrade() {
54                    show_alert_dialog(&lb.ui, "Playback Failed", &message);
55                }
56            }),
57        }
58    }
59
60    pub(super) fn prepare_video(self: &Rc<Self>, is_video: bool) {
61        let player = if is_video {
62            self.video_player()
63                .inspect_err(|err| log::debug!("Video playback unavailable: {}", err))
64                .ok()
65        } else {
66            self.video.borrow().clone()
67        };
68        if let Some(player) = player {
69            player.prepare(is_video);
70        }
71    }
72
73    pub(super) fn toggle_video(self: &Rc<Self>) {
74        match self.video_player() {
75            Ok(player) => player.toggle_play(),
76            Err(err) => show_alert_dialog(&self.ui, "Playback Unavailable", &err),
77        }
78    }
79
80    fn load_video(self: &Rc<Self>) {
81        let Some(item) = self.current_item() else {
82            return;
83        };
84        let info = AssetInfo::from_item(&item);
85        let generation = self.load_gen.get();
86        let lb = self.clone();
87        glib::MainContext::default().spawn_local(async move {
88            let source = lb.video_source(&info).await;
89            if lb.load_gen.get() != generation {
90                return;
91            }
92            let Some(player) = lb.video.borrow().clone() else {
93                return;
94            };
95            match source {
96                Ok(source) => player.load(source),
97                Err(err) => show_alert_dialog(&lb.ui, "Playback Failed", &err),
98            }
99        });
100    }
101
102    async fn video_source(&self, info: &AssetInfo) -> Result<VideoSource, String> {
103        if !info.local_path.is_empty() && std::path::Path::new(&info.local_path).exists() {
104            return Ok(VideoSource {
105                uri: gtk::gio::File::for_path(&info.local_path).uri().to_string(),
106                api_key: None,
107            });
108        }
109        if info.is_local() {
110            return Err("The video file is no longer on disk.".into());
111        }
112        let (uri, api_key) = self
113            .ui
114            .ctx
115            .api_client
116            .video_playback_source(&info.asset_id)
117            .await
118            .ok_or_else(|| "Not connected to the Immich server.".to_string())?;
119        Ok(VideoSource {
120            uri,
121            api_key: Some(api_key),
122        })
123    }
124
125    pub(super) fn handle_video_key(self: &Rc<Self>, key: gtk::gdk::Key) -> bool {
126        use gtk::gdk::Key;
127        let is_video = self
128            .current_item()
129            .is_some_and(|item| AssetInfo::from_item(&item).is_video());
130        if !is_video {
131            return false;
132        }
133        match key {
134            Key::space => self.toggle_video(),
135            _ => {
136                let Some(player) = self.video.borrow().clone() else {
137                    return false;
138                };
139                match key {
140                    Key::j | Key::J => player.skip(false),
141                    Key::l | Key::L => player.skip(true),
142                    Key::comma => player.step_frame(false),
143                    Key::period => player.step_frame(true),
144                    Key::m | Key::M => player.toggle_mute(),
145                    _ => return false,
146                }
147            }
148        }
149        true
150    }
151
152    pub(super) fn close_video(&self) {
153        let Some(player) = self.video.take() else {
154            return;
155        };
156        player.stop();
157        let mut config = self.ui.ctx.config.write();
158        if config.data.video_volume != player.volume() || config.data.video_muted != player.muted()
159        {
160            config.data.video_volume = player.volume();
161            config.data.video_muted = player.muted();
162            let _ = config.save();
163        }
164    }
165
166    pub(super) fn open_current_with(&self) {
167        let Some(item) = self.current_item() else {
168            return;
169        };
170        let info = AssetInfo::from_item(&item);
171        if let Some(player) = self.video.borrow().as_ref() {
172            player.pause();
173        }
174        if !info.local_path.is_empty() {
175            open_with_chooser(&self.ui.window, std::path::Path::new(&info.local_path));
176        } else {
177            spawn_video_handoff(self.ui.clone(), info.asset_id, info.filename);
178        }
179    }
180}