1 // player.rs
2 //
3 // Copyright 2018 Jordan Petridis <jpetridis@gnome.org>
4 //
5 // This program is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 //
18 // SPDX-License-Identifier: GPL-3.0-or-later
19 
20 use gst::ClockTime;
21 
22 use gtk::prelude::*;
23 
24 use libhandy as hdy;
25 
26 use gio::File;
27 use glib::clone;
28 use glib::{SignalHandlerId, WeakRef};
29 
30 use anyhow::Result;
31 use chrono::{prelude::*, NaiveTime};
32 use fragile::Fragile;
33 use glib::Sender;
34 use url::Url;
35 
36 use podcasts_data::{dbqueries, downloader, EpisodeWidgetModel, ShowCoverModel, USER_AGENT};
37 
38 use crate::app::Action;
39 use crate::config::APP_ID;
40 use crate::utils::set_image_from_path;
41 
42 use std::cell::{RefCell, RefMut};
43 use std::convert::TryInto;
44 use std::ops::Deref;
45 use std::path::Path;
46 use std::rc::Rc;
47 use std::sync::Mutex;
48 
49 use crate::i18n::i18n;
50 
51 use mpris_player::{Metadata, MprisPlayer, OrgMprisMediaPlayer2Player, PlaybackStatus};
52 use std::sync::Arc;
53 
54 #[derive(Debug, Clone, Copy)]
55 enum SeekDirection {
56     Backwards,
57     Forward,
58 }
59 
60 trait PlayerExt {
play(&self)61     fn play(&self);
pause(&mut self)62     fn pause(&mut self);
stop(&mut self)63     fn stop(&mut self);
seek(&self, offset: ClockTime, direction: SeekDirection) -> Option<()>64     fn seek(&self, offset: ClockTime, direction: SeekDirection) -> Option<()>;
fast_forward(&self)65     fn fast_forward(&self);
rewind(&self)66     fn rewind(&self);
set_playback_rate(&self, _: f64)67     fn set_playback_rate(&self, _: f64);
68 }
69 
70 #[derive(Debug, Clone)]
71 struct PlayerInfo {
72     container: gtk::Box,
73     show: gtk::Label,
74     episode: gtk::Label,
75     cover: gtk::Image,
76     show_small: gtk::Label,
77     episode_small: gtk::Label,
78     cover_small: gtk::Image,
79     mpris: Arc<MprisPlayer>,
80     finished_restore: bool,
81     ep: Option<EpisodeWidgetModel>,
82     episode_id: RefCell<Option<i32>>,
83 }
84 
85 impl PlayerInfo {
create_bindings(&self)86     fn create_bindings(&self) {
87         self.show
88             .bind_property("label", &self.show_small, "label")
89             .flags(glib::BindingFlags::SYNC_CREATE)
90             .build();
91         self.episode
92             .bind_property("label", &self.episode_small, "label")
93             .flags(glib::BindingFlags::SYNC_CREATE)
94             .build();
95         self.cover
96             .bind_property("pixbuf", &self.cover_small, "pixbuf")
97             .flags(glib::BindingFlags::SYNC_CREATE)
98             .build();
99     }
100 
101     // FIXME: create a Diesel Model of the joined episode and podcast query instead
init(&mut self, episode: &EpisodeWidgetModel, podcast: &ShowCoverModel)102     fn init(&mut self, episode: &EpisodeWidgetModel, podcast: &ShowCoverModel) {
103         self.ep = Some(episode.clone());
104         self.episode_id.replace(Some(episode.rowid()));
105         self.set_cover_image(podcast);
106         self.set_show_title(podcast);
107         self.set_episode_title(episode);
108 
109         let mut metadata = Metadata::new();
110         metadata.artist = Some(vec![podcast.title().to_string()]);
111         metadata.title = Some(episode.title().to_string());
112 
113         // Set the cover if it is already cached.
114         if let Ok(path) = downloader::cache_image(&podcast, false) {
115             let url = Url::from_file_path(path);
116             metadata.art_url = url.map(From::from).ok();
117         } else {
118             // fallback: set the cover to the http url if it isn't cached, yet.
119             // TODO we could trigger an async download of the cover here
120             // and update the metadata when it's done.
121             metadata.art_url = podcast.image_uri().clone().map(From::from);
122         }
123 
124         self.mpris.set_metadata(metadata);
125         self.mpris.set_can_play(true);
126     }
127 
set_episode_title(&self, episode: &EpisodeWidgetModel)128     fn set_episode_title(&self, episode: &EpisodeWidgetModel) {
129         self.episode.set_text(episode.title());
130         self.episode.set_tooltip_text(Some(episode.title()));
131     }
132 
set_show_title(&self, show: &ShowCoverModel)133     fn set_show_title(&self, show: &ShowCoverModel) {
134         self.show.set_text(show.title());
135         self.show.set_tooltip_text(Some(show.title()));
136     }
137 
set_cover_image(&self, show: &ShowCoverModel)138     fn set_cover_image(&self, show: &ShowCoverModel) {
139         set_image_from_path(&self.cover, show.id(), 34)
140             .map_err(|err| error!("Player Cover: {}", err))
141             .ok();
142     }
143 }
144 
145 #[derive(Debug, Clone)]
146 struct PlayerTimes {
147     container: gtk::Box,
148     progressed: gtk::Label,
149     duration: gtk::Label,
150     separator: gtk::Label,
151     slider: gtk::Scale,
152     slider_update: Rc<SignalHandlerId>,
153     progress_bar: gtk::ProgressBar,
154 }
155 
156 #[derive(Debug, Clone, Copy)]
157 struct Duration(ClockTime);
158 
159 impl Deref for Duration {
160     type Target = ClockTime;
deref(&self) -> &Self::Target161     fn deref(&self) -> &Self::Target {
162         &self.0
163     }
164 }
165 
166 #[derive(Debug, Clone, Copy)]
167 struct Position(ClockTime);
168 
169 impl Deref for Position {
170     type Target = ClockTime;
deref(&self) -> &Self::Target171     fn deref(&self) -> &Self::Target {
172         &self.0
173     }
174 }
175 
176 impl PlayerTimes {
177     /// Update the duration `gtk::Label` and the max range of the `gtk::SclaeBar`.
on_duration_changed(&self, duration: Duration)178     pub(crate) fn on_duration_changed(&self, duration: Duration) {
179         let seconds = duration.seconds();
180 
181         self.slider.block_signal(&self.slider_update);
182         self.slider.set_range(0.0, seconds as f64);
183         self.slider.unblock_signal(&self.slider_update);
184 
185         self.duration.set_text(&format_duration(seconds as u32));
186 
187         self.update_progress_bar();
188     }
189 
190     /// Update the `gtk::Scale` bar when the pipeline position is changed.
on_position_updated(&self, position: Position)191     pub(crate) fn on_position_updated(&self, position: Position) {
192         let seconds = position.seconds();
193 
194         self.slider.block_signal(&self.slider_update);
195         self.slider.set_value(seconds as f64);
196         self.slider.unblock_signal(&self.slider_update);
197 
198         self.progressed.set_text(&format_duration(seconds as u32));
199 
200         self.update_progress_bar();
201     }
202 
update_progress_bar(&self)203     fn update_progress_bar(&self) {
204         let fraction = self.slider.value() / self.slider.adjustment().upper();
205         self.progress_bar.set_fraction(fraction);
206     }
207 }
208 
format_duration(seconds: u32) -> String209 fn format_duration(seconds: u32) -> String {
210     let time = NaiveTime::from_num_seconds_from_midnight(seconds, 0);
211 
212     if seconds >= 3600 {
213         time.format("%T").to_string()
214     } else {
215         time.format("%M∶%S").to_string()
216     }
217 }
218 
219 #[derive(Debug, Clone)]
220 struct PlayerRate {
221     action: gio::SimpleAction,
222     btn: gtk::MenuButton,
223     label: gtk::Label,
224 }
225 
226 impl PlayerRate {
new() -> Self227     fn new() -> Self {
228         let builder = gtk::Builder::from_resource("/org/gnome/Podcasts/gtk/player_rate.ui");
229 
230         // This needs to be a string to work with GMenuModel
231         let variant_type = glib::VariantTy::new("s").expect("Could not parse variant type");
232         let action =
233             gio::SimpleAction::new_stateful("set", Some(&variant_type), &"1.00".to_variant());
234         let btn: gtk::MenuButton = builder.object("rate_button").unwrap();
235         let label = builder.object("rate_label").unwrap();
236 
237         PlayerRate { action, label, btn }
238     }
239 
connect_signals(&self, widget: &Rc<RefCell<PlayerWidget>>)240     fn connect_signals(&self, widget: &Rc<RefCell<PlayerWidget>>) {
241         let group = gio::SimpleActionGroup::new();
242         self.action
243             .connect_activate(clone!(@weak widget => move |action, rate_v| {
244                 let variant = rate_v.unwrap();
245                 action.set_state(&variant);
246                 let rate = variant
247                     .get::<String>()
248                     .expect("Could not get rate from variant")
249                     .parse::<f64>()
250                     .expect("Could not parse float from variant string");
251                 widget.borrow().on_rate_changed(rate);
252             }));
253         group.add_action(&self.action);
254         widget
255             .borrow()
256             .container
257             .insert_action_group("rate", Some(&group));
258         widget
259             .borrow()
260             .dialog
261             .dialog
262             .insert_action_group("rate", Some(&group));
263     }
264 }
265 
266 #[derive(Debug, Clone)]
267 struct PlayerControls {
268     container: gtk::Box,
269     play: gtk::Button,
270     pause: gtk::Button,
271     play_small: gtk::Button,
272     pause_small: gtk::Button,
273     play_pause_small: gtk::Stack,
274     forward: gtk::Button,
275     rewind: gtk::Button,
276     last_pause: RefCell<Option<DateTime<Local>>>,
277 }
278 
279 #[derive(Debug, Clone)]
280 struct PlayerDialog {
281     dialog: gtk::Dialog,
282     close: gtk::Button,
283     headerbar: hdy::HeaderBar,
284     cover: gtk::Image,
285     play_pause: gtk::Stack,
286     play: gtk::Button,
287     pause: gtk::Button,
288     duration: gtk::Label,
289     progressed: gtk::Label,
290     slider: gtk::Scale,
291     forward: gtk::Button,
292     rewind: gtk::Button,
293     rate: PlayerRate,
294     show: gtk::Label,
295     episode: gtk::Label,
296 }
297 
298 impl PlayerDialog {
new(rate: PlayerRate) -> Self299     fn new(rate: PlayerRate) -> Self {
300         let builder = gtk::Builder::from_resource("/org/gnome/Podcasts/gtk/player_dialog.ui");
301         let dialog = builder.object("dialog").unwrap();
302 
303         let close = builder.object("close").unwrap();
304         let headerbar = builder.object("headerbar").unwrap();
305         let cover = builder.object("cover").unwrap();
306         let play_pause = builder.object("play_pause").unwrap();
307         let play = builder.object("play").unwrap();
308         let pause = builder.object("pause").unwrap();
309         let duration = builder.object("duration").unwrap();
310         let progressed = builder.object("progressed").unwrap();
311         let slider = builder.object("slider").unwrap();
312         let rewind = builder.object("rewind").unwrap();
313         let forward = builder.object("forward").unwrap();
314         let bottom: gtk::Box = builder.object("bottom").unwrap();
315         let show = builder.object("show_label").unwrap();
316         let episode = builder.object("episode_label").unwrap();
317 
318         bottom.pack_start(&rate.btn, false, true, 0);
319 
320         PlayerDialog {
321             dialog,
322             close,
323             headerbar,
324             cover,
325             play_pause,
326             play,
327             pause,
328             duration,
329             progressed,
330             slider,
331             forward,
332             rewind,
333             rate,
334             show,
335             episode,
336         }
337     }
338 
initialize_episode(&self, episode: &EpisodeWidgetModel, show: &ShowCoverModel)339     fn initialize_episode(&self, episode: &EpisodeWidgetModel, show: &ShowCoverModel) {
340         self.episode.set_text(episode.title());
341         self.show.set_text(show.title());
342 
343         set_image_from_path(&self.cover, show.id(), 256)
344             .map_err(|err| error!("Player Cover: {}", err))
345             .ok();
346     }
347 }
348 
349 #[derive(Debug, Clone)]
350 pub(crate) struct PlayerWidget {
351     pub(crate) container: gtk::Box,
352     action_bar: gtk::ActionBar,
353     evbox: gtk::EventBox,
354     player: gst_player::Player,
355     controls: PlayerControls,
356     dialog: PlayerDialog,
357     full: gtk::Box,
358     squeezer: hdy::Squeezer,
359     timer: PlayerTimes,
360     info: PlayerInfo,
361     rate: PlayerRate,
362     sender: Option<Sender<Action>>,
363 }
364 
365 impl Default for PlayerWidget {
default() -> Self366     fn default() -> Self {
367         let dispatcher = gst_player::PlayerGMainContextSignalDispatcher::new(None);
368         let player = gst_player::Player::new(
369             None,
370             // Use the gtk main thread
371             Some(&dispatcher.upcast::<gst_player::PlayerSignalDispatcher>()),
372         );
373 
374         // A few podcasts have a video track of the thumbnail, which GStreamer displays in a new
375         // window. Make sure it doesn't do that.
376         player.set_video_track_enabled(false);
377 
378         let mpris = MprisPlayer::new(
379             APP_ID.to_string(),
380             "GNOME Podcasts".to_string(),
381             format!("{}.desktop", APP_ID),
382         );
383         mpris.set_can_raise(true);
384         mpris.set_can_play(false);
385         mpris.set_can_seek(false);
386         mpris.set_can_set_fullscreen(false);
387 
388         let mut config = player.config();
389         config.set_user_agent(USER_AGENT);
390         config.set_position_update_interval(250);
391         player.set_config(config).unwrap();
392 
393         let builder = gtk::Builder::from_resource("/org/gnome/Podcasts/gtk/player_toolbar.ui");
394 
395         let buttons = builder.object("buttons").unwrap();
396         let play = builder.object("play_button").unwrap();
397         let pause = builder.object("pause_button").unwrap();
398         let play_small = builder.object("play_button_small").unwrap();
399         let pause_small = builder.object("pause_button_small").unwrap();
400         let forward: gtk::Button = builder.object("ff_button").unwrap();
401         let rewind: gtk::Button = builder.object("rewind_button").unwrap();
402         let play_pause_small = builder.object("play_pause_small").unwrap();
403 
404         let controls = PlayerControls {
405             container: buttons,
406             play,
407             pause,
408             play_small,
409             pause_small,
410             play_pause_small,
411             forward,
412             rewind,
413             last_pause: RefCell::new(None),
414         };
415 
416         let timer_container = builder.object("timer").unwrap();
417         let progressed = builder.object("progress_time_label").unwrap();
418         let duration = builder.object("total_duration_label").unwrap();
419         let separator = builder.object("separator").unwrap();
420         let slider: gtk::Scale = builder.object("seek").unwrap();
421         slider.set_range(0.0, 1.0);
422         let player_weak = player.downgrade();
423         let slider_update = Rc::new(Self::connect_update_slider(&slider, player_weak));
424         let progress_bar = builder.object("progress_bar").unwrap();
425         let timer = PlayerTimes {
426             container: timer_container,
427             progressed,
428             duration,
429             separator,
430             slider,
431             slider_update,
432             progress_bar,
433         };
434 
435         let labels = builder.object("info").unwrap();
436         let show = builder.object("show_label").unwrap();
437         let episode = builder.object("episode_label").unwrap();
438         let cover = builder.object("show_cover").unwrap();
439         let show_small = builder.object("show_label_small").unwrap();
440         let episode_small = builder.object("episode_label_small").unwrap();
441         let cover_small = builder.object("show_cover_small").unwrap();
442         let ep = None;
443         let info = PlayerInfo {
444             mpris,
445             container: labels,
446             show,
447             ep,
448             episode,
449             cover,
450             show_small,
451             episode_small,
452             cover_small,
453             finished_restore: false,
454             episode_id: RefCell::new(None),
455         };
456         info.create_bindings();
457 
458         let dialog_rate = PlayerRate::new();
459         let dialog = PlayerDialog::new(dialog_rate);
460 
461         let container = builder.object("container").unwrap();
462         let action_bar: gtk::ActionBar = builder.object("action_bar").unwrap();
463         let evbox = builder.object("evbox").unwrap();
464         let full: gtk::Box = builder.object("full").unwrap();
465         let squeezer = builder.object("squeezer").unwrap();
466 
467         let rate = PlayerRate::new();
468         full.pack_end(&rate.btn, false, true, 0);
469 
470         PlayerWidget {
471             player,
472             container,
473             action_bar,
474             evbox,
475             controls,
476             dialog,
477             full,
478             squeezer,
479             timer,
480             info,
481             rate,
482             sender: None,
483         }
484     }
485 }
486 
487 impl PlayerWidget {
on_rate_changed(&self, rate: f64)488     fn on_rate_changed(&self, rate: f64) {
489         self.set_playback_rate(rate);
490         self.rate.label.set_text(&format!("{:.2}×", rate));
491         self.dialog.rate.label.set_text(&format!("{:.2}×", rate));
492     }
493 
reveal(&self)494     fn reveal(&self) {
495         self.action_bar.show();
496     }
497 
initialize_episode(&mut self, rowid: i32) -> Result<()>498     pub(crate) fn initialize_episode(&mut self, rowid: i32) -> Result<()> {
499         let ep = dbqueries::get_episode_widget_from_rowid(rowid)?;
500         let pd = dbqueries::get_podcast_cover_from_id(ep.show_id())?;
501 
502         self.dialog.initialize_episode(&ep, &pd);
503 
504         self.info.finished_restore = false;
505         self.info.init(&ep, &pd);
506 
507         // Currently that will always be the case since the play button is
508         // only shown if the file is downloaded
509         if let Some(ref path) = ep.local_uri() {
510             if Path::new(path).exists() {
511                 // path is an absolute fs path ex. "foo/bar/baz".
512                 // Convert it so it will have a "file:///"
513                 // FIXME: convert it properly
514                 let uri = File::for_path(path).uri();
515 
516                 // If it's not the same file load the uri, otherwise just unpause
517                 if self.player.uri().map_or(true, |s| s != uri.as_str()) {
518                     self.player.set_uri(uri.as_str());
519                 } else {
520                     // just unpause, no restore required
521                     self.info.finished_restore = true;
522                 }
523                 // play the file
524                 self.play();
525 
526                 return Ok(());
527             }
528             // TODO: log an error
529         }
530 
531         // FIXME: Stream stuff
532         // unimplemented!()
533         Ok(())
534     }
535 
connect_update_slider( slider: &gtk::Scale, player: WeakRef<gst_player::Player>, ) -> SignalHandlerId536     fn connect_update_slider(
537         slider: &gtk::Scale,
538         player: WeakRef<gst_player::Player>,
539     ) -> SignalHandlerId {
540         slider.connect_value_changed(move |slider| {
541             let player = match player.upgrade() {
542                 Some(p) => p,
543                 None => return,
544             };
545 
546             let value = slider.value() as u64;
547             player.seek(ClockTime::from_seconds(value));
548         })
549     }
550 
smart_rewind(&self) -> Option<()>551     fn smart_rewind(&self) -> Option<()> {
552         lazy_static! {
553             static ref LAST_KNOWN_EPISODE: Mutex<Option<i32>> = Mutex::new(None);
554         };
555 
556         // Figure out the time delta, in seconds, between the last pause and now
557         let now = Local::now();
558         let last: &Option<DateTime<_>> = &*self.controls.last_pause.borrow();
559         let delta = (now - (*last)?).num_seconds();
560 
561         // Get interval passed in the gst stream
562         let seconds_passed = self.player.position()?.seconds();
563         // get the last known episode id
564         let mut last = LAST_KNOWN_EPISODE.lock().unwrap();
565         // get the current playing episode id
566         let current_id = *self.info.episode_id.borrow();
567         // Only rewind on pause if the stream position is passed a certain point,
568         // and the player has been paused for more than a minute,
569         // and the episode id is the same
570         if seconds_passed >= 90 && delta >= 60 && current_id == *last {
571             self.seek(ClockTime::from_seconds(5), SeekDirection::Backwards);
572         }
573 
574         // Set the last knows episode to the current one
575         *last = current_id;
576 
577         Some(())
578     }
579 
580     /// Seek to the `play_position` stored in the episode.
581     /// Returns Some(()) if the restore was successful and None otherwise.
restore_play_position(&self) -> Option<()>582     fn restore_play_position(&self) -> Option<()> {
583         let ep = self.info.ep.as_ref()?;
584         let pos = ep.play_position();
585         let s: u64 = pos.try_into().ok()?;
586         if pos != 0 {
587             self.player.seek(ClockTime::from_seconds(s));
588             Some(())
589         } else {
590             None
591         }
592     }
593 }
594 
595 impl PlayerExt for PlayerWidget {
play(&self)596     fn play(&self) {
597         self.dialog.play_pause.set_visible_child(&self.dialog.pause);
598 
599         self.reveal();
600 
601         self.controls.pause.show();
602         self.controls.play.hide();
603         self.controls
604             .play_pause_small
605             .set_visible_child(&self.controls.pause_small);
606 
607         self.smart_rewind();
608         self.player.play();
609         self.info.mpris.set_playback_status(PlaybackStatus::Playing);
610         if let Some(sender) = &self.sender {
611             send!(sender, Action::InhibitSuspend);
612         }
613     }
614 
pause(&mut self)615     fn pause(&mut self) {
616         self.dialog.play_pause.set_visible_child(&self.dialog.play);
617 
618         self.controls.pause.hide();
619         self.controls.play.show();
620         self.controls
621             .play_pause_small
622             .set_visible_child(&self.controls.play_small);
623 
624         self.player.pause();
625         self.info.mpris.set_playback_status(PlaybackStatus::Paused);
626         if let Some(sender) = &self.sender {
627             send!(sender, Action::UninhibitSuspend);
628         }
629 
630         self.controls.last_pause.replace(Some(Local::now()));
631         let pos = self.player.position();
632         self.info.ep.as_mut().map(|ep| {
633             ep.set_play_position(pos.and_then(|s| s.seconds().try_into().ok()).unwrap_or(0))
634         });
635     }
636 
stop(&mut self)637     fn stop(&mut self) {
638         self.controls.pause.hide();
639         self.controls.play.show();
640 
641         self.info.ep = None;
642         self.player.stop();
643         self.info.mpris.set_playback_status(PlaybackStatus::Paused);
644 
645         // Reset the slider bar to the start
646 
647         self.timer
648             .on_position_updated(Position(ClockTime::from_seconds(0)));
649         if let Some(sender) = &self.sender {
650             send!(sender, Action::UninhibitSuspend);
651         }
652     }
653 
654     // Adapted from https://github.com/philn/glide/blob/b52a65d99daeab0b487f79a0e1ccfad0cd433e22/src/player_context.rs#L219-L245
seek(&self, offset: ClockTime, direction: SeekDirection) -> Option<()>655     fn seek(&self, offset: ClockTime, direction: SeekDirection) -> Option<()> {
656         // How far into the podcast we are
657         let position = self.player.position()?;
658         if offset.is_zero() {
659             return Some(());
660         }
661 
662         // How much podcast we have
663         let duration = self.player.duration()?;
664         let destination = match direction {
665             // If we are more than `offset` into the podcast, jump back that far
666             SeekDirection::Backwards if position >= offset => position.checked_sub(offset),
667             // If we haven't played `offset` yet just restart the podcast
668             SeekDirection::Backwards if position < offset => Some(ClockTime::from_seconds(0)),
669             // If we have more than `offset` remaining jump forward they amount
670             SeekDirection::Forward if !duration.is_zero() && position + offset <= duration => {
671                 position.checked_add(offset)
672             }
673             // We don't have `offset` remaining just move to the end (ending playback)
674             SeekDirection::Forward if !duration.is_zero() && position + offset > duration => {
675                 Some(duration)
676             }
677             // Who knows what's going on ¯\_(ツ)_/¯
678             _ => None,
679         };
680 
681         // If we calucated a new position, jump to it
682         if let Some(destination) = destination {
683             self.player.seek(destination)
684         }
685 
686         Some(())
687     }
688 
rewind(&self)689     fn rewind(&self) {
690         let r = self.seek(ClockTime::from_seconds(10), SeekDirection::Backwards);
691         if r.is_none() {
692             warn!("Failed to rewind");
693         }
694     }
695 
fast_forward(&self)696     fn fast_forward(&self) {
697         let r = self.seek(ClockTime::from_seconds(10), SeekDirection::Forward);
698         if r.is_none() {
699             warn!("Failed to fast-forward");
700         }
701     }
702 
set_playback_rate(&self, rate: f64)703     fn set_playback_rate(&self, rate: f64) {
704         self.player.set_rate(rate);
705     }
706 }
707 
708 #[derive(Debug, Clone)]
709 pub(crate) struct PlayerWrapper(pub Rc<RefCell<PlayerWidget>>);
710 
711 impl Default for PlayerWrapper {
default() -> Self712     fn default() -> Self {
713         PlayerWrapper(Rc::new(RefCell::new(PlayerWidget::default())))
714     }
715 }
716 
717 impl Deref for PlayerWrapper {
718     type Target = Rc<RefCell<PlayerWidget>>;
deref(&self) -> &Self::Target719     fn deref(&self) -> &Self::Target {
720         &self.0
721     }
722 }
723 
724 impl PlayerWrapper {
borrow_mut(&self) -> RefMut<'_, PlayerWidget>725     pub(crate) fn borrow_mut(&self) -> RefMut<'_, PlayerWidget> {
726         self.0.borrow_mut()
727     }
new(sender: &Sender<Action>) -> Self728     pub(crate) fn new(sender: &Sender<Action>) -> Self {
729         let w = PlayerWrapper::default();
730         w.init(sender);
731         w
732     }
733 
init(&self, sender: &Sender<Action>)734     fn init(&self, sender: &Sender<Action>) {
735         self.borrow_mut().sender = Some(sender.clone());
736         self.connect_control_buttons();
737         self.connect_rate_buttons();
738         self.connect_mpris_buttons(sender);
739         self.connect_gst_signals(sender);
740         self.connect_dialog();
741     }
742 
connect_dialog(&self)743     fn connect_dialog(&self) {
744         let this = self.deref();
745         let widget = self.borrow();
746         widget
747             .squeezer
748             .connect_visible_child_notify(clone!(@weak this => move |_| {
749                 let widget = this.borrow();
750                 if let Some(child) = widget.squeezer.visible_child() {
751                     let full = child == this.borrow().full;
752                     this.borrow().timer.progress_bar.set_visible(!full);
753                     if full {
754                         this.borrow().action_bar.style_context().remove_class("player-small");
755                     } else {
756                         this.borrow().action_bar.style_context().add_class("player-small");
757                     }
758                 }
759             }));
760 
761         widget
762             .timer
763             .duration
764             .bind_property("label", &widget.dialog.duration, "label")
765             .flags(glib::BindingFlags::SYNC_CREATE)
766             .build();
767         widget
768             .timer
769             .progressed
770             .bind_property("label", &widget.dialog.progressed, "label")
771             .flags(glib::BindingFlags::SYNC_CREATE)
772             .build();
773         widget
774             .dialog
775             .slider
776             .set_adjustment(&widget.timer.slider.adjustment());
777 
778         widget.evbox.connect_button_press_event(
779             clone!(@weak this =>  @default-return Inhibit(false), move |_, event| {
780                 let widget = this.borrow();
781                 if event.button() != 1 {
782                     return Inhibit(false);
783                 }
784                 // only open the dialog when the small toolbar is visible
785                 if let Some(child) = widget.squeezer.visible_child() {
786                     if child == widget.full {
787                         return Inhibit(false);
788                     }
789                 }
790 
791                 let parent = widget.container.toplevel().and_then(|toplevel| {
792                     toplevel
793                         .downcast::<gtk::Window>()
794                         .ok()
795                 }).unwrap();
796 
797                 info!("showing dialog");
798                 widget.dialog.dialog.set_transient_for(Some(&parent));
799                 widget.dialog.dialog.show();
800 
801                 Inhibit(false)
802             }),
803         );
804 
805         widget
806             .dialog
807             .close
808             .connect_clicked(clone!(@weak this => move |_| {
809                     this.borrow().dialog.dialog.hide();
810             }));
811     }
812 
813     /// Connect the `PlayerControls` buttons to the `PlayerExt` methods.
connect_control_buttons(&self)814     fn connect_control_buttons(&self) {
815         let this = self.deref();
816         let widget = self.borrow();
817         // Connect the play button to the gst Player.
818         widget
819             .controls
820             .play
821             .connect_clicked(clone!(@weak this => move |_| {
822                      this.borrow().play();
823             }));
824 
825         // Connect the pause button to the gst Player.
826         widget
827             .controls
828             .pause
829             .connect_clicked(clone!(@weak this => move |_| {
830                 this.borrow_mut().pause();
831             }));
832 
833         // Connect the play button to the gst Player.
834         widget
835             .controls
836             .play_small
837             .connect_clicked(clone!(@weak this => move |_| {
838                  this.borrow().play();
839             }));
840 
841         // Connect the pause button to the gst Player.
842         widget
843             .controls
844             .pause_small
845             .connect_clicked(clone!(@weak this => move |_| {
846                 this.borrow_mut().pause();
847             }));
848 
849         // Connect the rewind button to the gst Player.
850         widget
851             .controls
852             .rewind
853             .connect_clicked(clone!(@weak this => move |_| {
854                 this.borrow().rewind();
855             }));
856 
857         // Connect the fast-forward button to the gst Player.
858         widget
859             .controls
860             .forward
861             .connect_clicked(clone!(@weak this => move |_| {
862                 this.borrow().fast_forward();
863             }));
864 
865         // Connect the play button to the gst Player.
866         widget
867             .dialog
868             .play
869             .connect_clicked(clone!(@weak this => move |_| {
870                      this.borrow().play();
871             }));
872 
873         // Connect the pause button to the gst Player.
874         widget
875             .dialog
876             .pause
877             .connect_clicked(clone!(@weak this => move |_| {
878                     this.borrow_mut().pause();
879             }));
880 
881         // Connect the rewind button to the gst Player.
882         widget
883             .dialog
884             .rewind
885             .connect_clicked(clone!(@weak this => move |_| {
886                     this.borrow().rewind();
887             }));
888 
889         // Connect the fast-forward button to the gst Player.
890         widget
891             .dialog
892             .forward
893             .connect_clicked(clone!(@weak this => move |_| {
894                 this.borrow().fast_forward();
895             }));
896     }
897 
connect_gst_signals(&self, sender: &Sender<Action>)898     fn connect_gst_signals(&self, sender: &Sender<Action>) {
899         let widget = self.borrow();
900         // Log gst warnings.
901         widget
902             .player
903             .connect_warning(move |_, warn| warn!("gst warning: {}", warn));
904 
905         // Log gst errors.
906         widget
907             .player
908             .connect_error(clone!(@strong sender => move |_, _error| {
909                 send!(sender, Action::ErrorNotification(format!("Player Error: {}", _error)));
910                 let s = i18n("The media player was unable to execute an action.");
911                 send!(sender, Action::ErrorNotification(s));
912             }));
913 
914         // The following callbacks require `Send` but are handled by the gtk main loop
915         let weak = Fragile::new(Rc::downgrade(self));
916 
917         widget
918             .player
919             .connect_uri_loaded(clone!(@strong weak => move |_, _| {
920                 if let Some(player_widget) = weak.get().upgrade() {
921                     player_widget.borrow().restore_play_position();
922                     player_widget.borrow_mut().info.finished_restore = true;
923                 }
924             }));
925 
926         // Update the duration label and the slider
927         widget
928             .player
929             .connect_duration_changed(clone!(@strong weak => move |_, clock| {
930                 if let Some(player_widget) = weak.get().upgrade() {
931                     if let Some(c) = clock {
932                         player_widget.borrow().timer.on_duration_changed(Duration(c));
933                     }
934                 }
935             }));
936 
937         // Update the position label and the slider
938         widget
939             .player
940             .connect_position_updated(clone!(@strong weak => move |_, clock| {
941                 if let Some(player_widget) = weak.get().upgrade() {
942                     // write to db
943                     if let Some(c) = clock {
944                         let pos = Position(c);
945                         let finished_restore = player_widget.borrow().info.finished_restore;
946                         player_widget.borrow_mut().info.ep.as_mut().map(|ep| {
947                             if finished_restore {
948                                 ep.set_play_position_if_divergent(pos.seconds() as i32)
949                             } else {
950                                 Ok(())
951                             }
952                         });
953                         player_widget.borrow().timer.on_position_updated(pos)
954                     }
955                 }
956             }));
957 
958         // Reset the slider to 0 and show a play button
959         widget
960             .player
961             .connect_end_of_stream(clone!(@strong sender, @strong weak => move |_| {
962                 if let Some(player_widget) = weak.get().upgrade() {
963                     // write postion to db
964                     player_widget.borrow_mut().info.ep.as_mut().map(|ep| {
965                         ep.set_play_position(0)?;
966                         ep.set_played_now()?;
967                         send!(sender, Action::RefreshEpisodesViewBGR);
968                         send!(sender, Action::RefreshWidgetIfSame(ep.show_id()));
969                         let ok : Result<(), podcasts_data::errors::DataError> = Ok(());
970                         ok
971                     });
972 
973                     player_widget.borrow_mut().stop()
974                 }
975             }));
976     }
977 
connect_rate_buttons(&self)978     fn connect_rate_buttons(&self) {
979         self.deref().borrow().rate.connect_signals(self.deref());
980         self.deref()
981             .borrow()
982             .dialog
983             .rate
984             .connect_signals(self.deref());
985     }
986 
connect_mpris_buttons(&self, sender: &Sender<Action>)987     fn connect_mpris_buttons(&self, sender: &Sender<Action>) {
988         let weak = Rc::downgrade(self);
989         let widget = self.borrow();
990 
991         // FIXME: Reference cycle with mpris
992         let mpris = widget.info.mpris.clone();
993         widget
994             .info
995             .mpris
996             .connect_play_pause(clone!(@strong weak => move || {
997                 let player = match weak.upgrade() {
998                     Some(s) => s,
999                     None => return
1000                 };
1001 
1002                 if let Ok(status) = mpris.get_playback_status() {
1003                     match status.as_ref() {
1004                         "Paused" => player.borrow().play(),
1005                         "Stopped" => player.borrow().play(),
1006                         _ => player.borrow_mut().pause(),
1007                     };
1008                 }
1009             }));
1010         widget
1011             .info
1012             .mpris
1013             .connect_play(clone!(@strong weak => move || {
1014                 let player = match weak.upgrade() {
1015                     Some(s) => s,
1016                     None => return
1017                 };
1018 
1019                 player.borrow().play();
1020             }));
1021 
1022         widget
1023             .info
1024             .mpris
1025             .connect_pause(clone!(@strong weak => move || {
1026                 let player = match weak.upgrade() {
1027                     Some(s) => s,
1028                     None => return
1029                 };
1030 
1031                 player.borrow_mut().pause();
1032             }));
1033 
1034         widget
1035             .info
1036             .mpris
1037             .connect_next(clone!(@strong weak => move || {
1038                 if let Some(p) = weak.upgrade() {
1039                     p.borrow().fast_forward()
1040                 }
1041             }));
1042 
1043         widget
1044             .info
1045             .mpris
1046             .connect_previous(clone!(@strong weak => move || {
1047                 if let Some(p) = weak.upgrade() {
1048                     p.borrow().rewind()
1049                 }
1050             }));
1051 
1052         widget
1053             .info
1054             .mpris
1055             .connect_raise(clone!(@strong sender => move || {
1056                 send!(sender, Action::RaiseWindow);
1057             }));
1058     }
1059 }
1060