mimick/library/lightbox/
video.rs1use 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}