podcasts/podcasts-gtk/src/widgets/player.rs

967 lines
31 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// player.rs
//
// Copyright 2018 Jordan Petridis <jpetridis@gnome.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-or-later
use gst::ClockTime;
use gtk;
use gtk::prelude::*;
use libhandy as hdy;
use libhandy::prelude::*;
use gio::{File, FileExt};
use glib::clone;
use glib::{SignalHandlerId, WeakRef};
use chrono::{prelude::*, NaiveTime};
use crossbeam_channel::Sender;
use failure::Error;
use fragile::Fragile;
use podcasts_data::{dbqueries, USER_AGENT};
use podcasts_data::{EpisodeWidgetModel, ShowCoverModel};
use crate::app::Action;
use crate::config::APP_ID;
use crate::utils::set_image_from_path;
use std::cell::RefCell;
use std::ops::Deref;
use std::path::Path;
use std::rc::Rc;
use std::sync::Mutex;
use crate::i18n::i18n;
use mpris_player::{Metadata, MprisPlayer, OrgMprisMediaPlayer2Player, PlaybackStatus};
use std::sync::Arc;
#[derive(Debug, Clone, Copy)]
enum SeekDirection {
Backwards,
Forward,
}
trait PlayerExt {
fn play(&self);
fn pause(&self);
fn stop(&self);
fn seek(&self, offset: ClockTime, direction: SeekDirection);
fn fast_forward(&self);
fn rewind(&self);
fn set_playback_rate(&self, _: f64);
}
#[derive(Debug, Clone)]
struct PlayerInfo {
container: gtk::Box,
show: gtk::Label,
episode: gtk::Label,
cover: gtk::Image,
show_small: gtk::Label,
episode_small: gtk::Label,
cover_small: gtk::Image,
mpris: Arc<MprisPlayer>,
episode_id: RefCell<Option<i32>>,
}
impl PlayerInfo {
fn create_bindings(&self) {
self.show
.bind_property("label", &self.show_small, "label")
.flags(glib::BindingFlags::SYNC_CREATE)
.build();
self.episode
.bind_property("label", &self.episode_small, "label")
.flags(glib::BindingFlags::SYNC_CREATE)
.build();
self.cover
.bind_property("pixbuf", &self.cover_small, "pixbuf")
.flags(glib::BindingFlags::SYNC_CREATE)
.build();
}
// FIXME: create a Diesel Model of the joined episode and podcast query instead
fn init(&self, episode: &EpisodeWidgetModel, podcast: &ShowCoverModel) {
self.episode_id.replace(Some(episode.rowid()));
self.set_cover_image(podcast);
self.set_show_title(podcast);
self.set_episode_title(episode);
let mut metadata = Metadata::new();
metadata.artist = Some(vec![podcast.title().to_string()]);
metadata.title = Some(episode.title().to_string());
// FIXME: .image_uri() returns an http url, we should instead
// pass it the local path to the downloaded cover image.
metadata.art_url = podcast.image_uri().clone().map(From::from);
self.mpris.set_metadata(metadata);
self.mpris.set_can_play(true);
}
fn set_episode_title(&self, episode: &EpisodeWidgetModel) {
self.episode.set_text(episode.title());
self.episode.set_tooltip_text(Some(episode.title()));
}
fn set_show_title(&self, show: &ShowCoverModel) {
self.show.set_text(show.title());
self.show.set_tooltip_text(Some(show.title()));
}
fn set_cover_image(&self, show: &ShowCoverModel) {
set_image_from_path(&self.cover, show.id(), 34)
.map_err(|err| error!("Player Cover: {}", err))
.ok();
}
}
#[derive(Debug, Clone)]
struct PlayerTimes {
container: gtk::Box,
progressed: gtk::Label,
duration: gtk::Label,
separator: gtk::Label,
slider: gtk::Scale,
slider_update: Rc<SignalHandlerId>,
progress_bar: gtk::ProgressBar,
}
#[derive(Debug, Clone, Copy)]
struct Duration(ClockTime);
impl Deref for Duration {
type Target = ClockTime;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone, Copy)]
struct Position(ClockTime);
impl Deref for Position {
type Target = ClockTime;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl PlayerTimes {
/// Update the duration `gtk::Label` and the max range of the `gtk::SclaeBar`.
pub(crate) fn on_duration_changed(&self, duration: Duration) {
let seconds = duration.seconds().map(|v| v as f64).unwrap_or(0.0);
self.slider.block_signal(&self.slider_update);
self.slider.set_range(0.0, seconds);
self.slider.unblock_signal(&self.slider_update);
self.duration.set_text(&format_duration(seconds as u32));
self.update_progress_bar();
}
/// Update the `gtk::Scale` bar when the pipeline position is changed.
pub(crate) fn on_position_updated(&self, position: Position) {
let seconds = position.seconds().map(|v| v as f64).unwrap_or(0.0);
self.slider.block_signal(&self.slider_update);
self.slider.set_value(seconds);
self.slider.unblock_signal(&self.slider_update);
self.progressed.set_text(&format_duration(seconds as u32));
self.update_progress_bar();
}
fn update_progress_bar(&self) {
let fraction = self.slider.get_value() / self.slider.get_adjustment().get_upper();
self.progress_bar.set_fraction(fraction);
}
}
fn format_duration(seconds: u32) -> String {
let time = NaiveTime::from_num_seconds_from_midnight(seconds, 0);
if seconds >= 3600 {
time.format("%T").to_string()
} else {
time.format("%M%S").to_string()
}
}
#[derive(Debug, Clone)]
struct PlayerRate {
radio300: gtk::RadioButton,
radio275: gtk::RadioButton,
radio250: gtk::RadioButton,
radio225: gtk::RadioButton,
radio200: gtk::RadioButton,
radio175: gtk::RadioButton,
radio150: gtk::RadioButton,
radio125: gtk::RadioButton,
radio_normal: gtk::RadioButton,
popover: gtk::Popover,
btn: gtk::MenuButton,
label: gtk::Label,
}
impl PlayerRate {
fn new() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/Podcasts/gtk/player_rate.ui");
let radio300: gtk::RadioButton = builder.get_object("rate_3_00").unwrap();
let radio275: gtk::RadioButton = builder.get_object("rate_2_75").unwrap();
let radio250: gtk::RadioButton = builder.get_object("rate_2_50").unwrap();
let radio225: gtk::RadioButton = builder.get_object("rate_2_25").unwrap();
let radio200: gtk::RadioButton = builder.get_object("rate_2_00").unwrap();
let radio175: gtk::RadioButton = builder.get_object("rate_1_75").unwrap();
let radio150: gtk::RadioButton = builder.get_object("rate_1_50").unwrap();
let radio125: gtk::RadioButton = builder.get_object("rate_1_25").unwrap();
let radio_normal: gtk::RadioButton = builder.get_object("normal_rate").unwrap();
let popover = builder.get_object("rate_popover").unwrap();
let btn = builder.get_object("rate_button").unwrap();
let label = builder.get_object("rate_label").unwrap();
PlayerRate {
radio300,
radio275,
radio250,
radio225,
radio200,
radio175,
radio150,
radio125,
radio_normal,
popover,
label,
btn,
}
}
fn set_rate(&self, rate: f64) {
self.label.set_text(&format!("{:.2}×", rate));
}
fn connect_signals(&self, widget: &Rc<PlayerWidget>) {
self.radio_normal
.connect_toggled(clone!(@weak widget => move |_| {
widget.on_rate_changed(1.00);
}));
self.radio125
.connect_toggled(clone!(@weak widget => move |_| {
widget.on_rate_changed(1.25);
}));
self.radio150
.connect_toggled(clone!(@weak widget => move |_| {
widget.on_rate_changed(1.50);
}));
self.radio175
.connect_toggled(clone!(@weak widget => move |_| {
widget.on_rate_changed(1.75);
}));
self.radio200
.connect_toggled(clone!(@weak widget => move |_| {
widget.on_rate_changed(2.00);
}));
self.radio225
.connect_toggled(clone!(@weak widget => move |_| {
widget.on_rate_changed(2.25);
}));
self.radio250
.connect_toggled(clone!(@weak widget => move |_| {
widget.on_rate_changed(2.50);
}));
self.radio275
.connect_toggled(clone!(@weak widget => move |_| {
widget.on_rate_changed(2.75);
}));
self.radio300
.connect_toggled(clone!(@weak widget => move |_| {
widget.on_rate_changed(3.00);
}));
}
}
#[derive(Debug, Clone)]
struct PlayerControls {
container: gtk::Box,
play: gtk::Button,
pause: gtk::Button,
play_small: gtk::Button,
pause_small: gtk::Button,
play_pause_small: gtk::Stack,
forward: gtk::Button,
rewind: gtk::Button,
last_pause: RefCell<Option<DateTime<Local>>>,
}
#[derive(Debug, Clone)]
struct PlayerDialog {
dialog: hdy::Dialog,
close: gtk::Button,
headerbar: hdy::HeaderBar,
cover: gtk::Image,
play_pause: gtk::Stack,
play: gtk::Button,
pause: gtk::Button,
duration: gtk::Label,
progressed: gtk::Label,
slider: gtk::Scale,
forward: gtk::Button,
rewind: gtk::Button,
rate: PlayerRate,
show: gtk::Label,
episode: gtk::Label,
}
impl PlayerDialog {
fn new(rate: PlayerRate) -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/Podcasts/gtk/player_dialog.ui");
let dialog = builder.get_object("dialog").unwrap();
let close = builder.get_object("close").unwrap();
let headerbar = builder.get_object("headerbar").unwrap();
let cover = builder.get_object("cover").unwrap();
let play_pause = builder.get_object("play_pause").unwrap();
let play = builder.get_object("play").unwrap();
let pause = builder.get_object("pause").unwrap();
let duration = builder.get_object("duration").unwrap();
let progressed = builder.get_object("progressed").unwrap();
let slider = builder.get_object("slider").unwrap();
let rewind = builder.get_object("rewind").unwrap();
let forward = builder.get_object("forward").unwrap();
let bottom: gtk::Box = builder.get_object("bottom").unwrap();
let show = builder.get_object("show_label").unwrap();
let episode = builder.get_object("episode_label").unwrap();
bottom.pack_start(&rate.btn, false, true, 0);
PlayerDialog {
dialog,
close,
headerbar,
cover,
play_pause,
play,
pause,
duration,
progressed,
slider,
forward,
rewind,
rate,
show,
episode,
}
}
fn initialize_episode(&self, episode: &EpisodeWidgetModel, show: &ShowCoverModel) {
self.episode.set_text(episode.title());
self.show.set_text(show.title());
set_image_from_path(&self.cover, show.id(), 256)
.map_err(|err| error!("Player Cover: {}", err))
.ok();
}
}
#[derive(Debug, Clone)]
pub(crate) struct PlayerWidget {
pub(crate) container: gtk::Box,
action_bar: gtk::ActionBar,
evbox: gtk::EventBox,
player: gst_player::Player,
controls: PlayerControls,
dialog: PlayerDialog,
full: gtk::Box,
squeezer: hdy::Squeezer,
timer: PlayerTimes,
info: PlayerInfo,
rate: PlayerRate,
}
impl Default for PlayerWidget {
fn default() -> Self {
let dispatcher = gst_player::PlayerGMainContextSignalDispatcher::new(None);
let player = gst_player::Player::new(
None,
// Use the gtk main thread
Some(&dispatcher.upcast::<gst_player::PlayerSignalDispatcher>()),
);
// A few podcasts have a video track of the thumbnail, which GStreamer displays in a new
// window. Make sure it doesn't do that.
player.set_video_track_enabled(false);
let mpris = MprisPlayer::new(
APP_ID.to_string(),
"GNOME Podcasts".to_string(),
format!("{}.desktop", APP_ID).to_string(),
);
mpris.set_can_raise(true);
mpris.set_can_play(false);
mpris.set_can_seek(false);
mpris.set_can_set_fullscreen(false);
let mut config = player.get_config();
config.set_user_agent(USER_AGENT);
config.set_position_update_interval(250);
player.set_config(config).unwrap();
let builder = gtk::Builder::new_from_resource("/org/gnome/Podcasts/gtk/player_toolbar.ui");
let buttons = builder.get_object("buttons").unwrap();
let play = builder.get_object("play_button").unwrap();
let pause = builder.get_object("pause_button").unwrap();
let play_small = builder.get_object("play_button_small").unwrap();
let pause_small = builder.get_object("pause_button_small").unwrap();
let forward: gtk::Button = builder.get_object("ff_button").unwrap();
let rewind: gtk::Button = builder.get_object("rewind_button").unwrap();
let play_pause_small = builder.get_object("play_pause_small").unwrap();
let controls = PlayerControls {
container: buttons,
play,
pause,
play_small,
pause_small,
play_pause_small,
forward,
rewind,
last_pause: RefCell::new(None),
};
let timer_container = builder.get_object("timer").unwrap();
let progressed = builder.get_object("progress_time_label").unwrap();
let duration = builder.get_object("total_duration_label").unwrap();
let separator = builder.get_object("separator").unwrap();
let slider: gtk::Scale = builder.get_object("seek").unwrap();
slider.set_range(0.0, 1.0);
let player_weak = player.downgrade();
let slider_update = Rc::new(Self::connect_update_slider(&slider, player_weak));
let progress_bar = builder.get_object("progress_bar").unwrap();
let timer = PlayerTimes {
container: timer_container,
progressed,
duration,
separator,
slider,
slider_update,
progress_bar,
};
let labels = builder.get_object("info").unwrap();
let show = builder.get_object("show_label").unwrap();
let episode = builder.get_object("episode_label").unwrap();
let cover = builder.get_object("show_cover").unwrap();
let show_small = builder.get_object("show_label_small").unwrap();
let episode_small = builder.get_object("episode_label_small").unwrap();
let cover_small = builder.get_object("show_cover_small").unwrap();
let info = PlayerInfo {
mpris,
container: labels,
show,
episode,
cover,
show_small,
episode_small,
cover_small,
episode_id: RefCell::new(None),
};
info.create_bindings();
let dialog_rate = PlayerRate::new();
let dialog = PlayerDialog::new(dialog_rate);
let container = builder.get_object("container").unwrap();
let action_bar: gtk::ActionBar = builder.get_object("action_bar").unwrap();
let evbox = builder.get_object("evbox").unwrap();
let full: gtk::Box = builder.get_object("full").unwrap();
let squeezer = builder.get_object("squeezer").unwrap();
let rate = PlayerRate::new();
full.pack_end(&rate.btn, false, true, 0);
PlayerWidget {
player,
container,
action_bar,
evbox,
controls,
dialog,
full,
squeezer,
timer,
info,
rate,
}
}
}
impl PlayerWidget {
fn on_rate_changed(&self, rate: f64) {
self.set_playback_rate(rate);
self.rate.set_rate(rate);
self.dialog.rate.set_rate(rate);
}
fn reveal(&self) {
self.action_bar.show();
}
pub(crate) fn initialize_episode(&self, rowid: i32) -> Result<(), Error> {
let ep = dbqueries::get_episode_widget_from_rowid(rowid)?;
let pd = dbqueries::get_podcast_cover_from_id(ep.show_id())?;
self.dialog.initialize_episode(&ep, &pd);
self.info.init(&ep, &pd);
// Currently that will always be the case since the play button is
// only shown if the file is downloaded
if let Some(ref path) = ep.local_uri() {
if Path::new(path).exists() {
// path is an absolute fs path ex. "foo/bar/baz".
// Convert it so it will have a "file:///"
// FIXME: convert it properly
let uri = File::new_for_path(path).get_uri();
// play the file
self.player.set_uri(uri.as_str());
self.rate.set_rate(1.0);
self.dialog.rate.set_rate(1.0);
self.play();
return Ok(());
}
// TODO: log an error
}
// FIXME: Stream stuff
// unimplemented!()
Ok(())
}
fn connect_update_slider(
slider: &gtk::Scale,
player: WeakRef<gst_player::Player>,
) -> SignalHandlerId {
slider.connect_value_changed(move |slider| {
let player = match player.upgrade() {
Some(p) => p,
None => return,
};
let value = slider.get_value() as u64;
player.seek(ClockTime::from_seconds(value));
})
}
fn smart_rewind(&self) -> Option<()> {
lazy_static! {
static ref LAST_KNOWN_EPISODE: Mutex<Option<i32>> = Mutex::new(None);
};
// Figure out the time delta, in seconds, between the last pause and now
let now = Local::now();
let last: &Option<DateTime<_>> = &*self.controls.last_pause.borrow();
let last = last.clone()?;
let delta = (now - last).num_seconds();
// Get interval passed in the gst stream
let seconds_passed = self.player.get_position().seconds()?;
// get the last known episode id
let mut last = LAST_KNOWN_EPISODE.lock().unwrap();
// get the current playing episode id
let current_id = *self.info.episode_id.borrow();
// Only rewind on pause if the stream position is passed a certain point,
// and the player has been paused for more than a minute,
// and the episode id is the same
if seconds_passed >= 90 && delta >= 60 && current_id == *last {
self.seek(ClockTime::from_seconds(5), SeekDirection::Backwards);
}
// Set the last knows episode to the current one
*last = current_id;
Some(())
}
}
impl PlayerExt for PlayerWidget {
fn play(&self) {
self.dialog.play_pause.set_visible_child(&self.dialog.pause);
self.reveal();
self.controls.pause.show();
self.controls.play.hide();
self.controls
.play_pause_small
.set_visible_child(&self.controls.pause_small);
self.smart_rewind();
self.player.play();
self.info.mpris.set_playback_status(PlaybackStatus::Playing);
}
fn pause(&self) {
self.dialog.play_pause.set_visible_child(&self.dialog.play);
self.controls.pause.hide();
self.controls.play.show();
self.controls
.play_pause_small
.set_visible_child(&self.controls.play_small);
self.player.pause();
self.info.mpris.set_playback_status(PlaybackStatus::Paused);
self.controls.last_pause.replace(Some(Local::now()));
}
#[cfg_attr(rustfmt, rustfmt_skip)]
fn stop(&self) {
self.controls.pause.hide();
self.controls.play.show();
self.player.stop();
self.info.mpris.set_playback_status(PlaybackStatus::Paused);
// Reset the slider bar to the start
self.timer.on_position_updated(Position(ClockTime::from_seconds(0)));
}
// Adapted from https://github.com/philn/glide/blob/b52a65d99daeab0b487f79a0e1ccfad0cd433e22/src/player_context.rs#L219-L245
fn seek(&self, offset: ClockTime, direction: SeekDirection) {
// How far into the podcast we are
let position = self.player.get_position();
if position.is_none() || offset.is_none() {
return;
}
// How much podcast we have
let duration = self.player.get_duration();
let destination = match direction {
// If we are more than `offset` into the podcast, jump back that far
SeekDirection::Backwards if position >= offset => Some(position - offset),
// If we haven't played `offset` yet just restart the podcast
SeekDirection::Backwards if position < offset => Some(ClockTime::from_seconds(0)),
// If we have more than `offset` remaining jump forward they amount
SeekDirection::Forward if !duration.is_none() && position + offset <= duration => {
Some(position + offset)
}
// We don't have `offset` remaining just move to the end (ending playback)
SeekDirection::Forward if !duration.is_none() && position + offset > duration => {
Some(duration)
}
// Who knows what's going on ¯\_(ツ)_/¯
_ => None,
};
// If we calucated a new position, jump to it
destination.map(|d| self.player.seek(d));
}
fn rewind(&self) {
self.seek(ClockTime::from_seconds(10), SeekDirection::Backwards)
}
fn fast_forward(&self) {
self.seek(ClockTime::from_seconds(10), SeekDirection::Forward)
}
fn set_playback_rate(&self, rate: f64) {
self.player.set_rate(rate);
}
}
#[derive(Debug, Clone)]
pub(crate) struct PlayerWrapper(pub Rc<PlayerWidget>);
impl Default for PlayerWrapper {
fn default() -> Self {
PlayerWrapper(Rc::new(PlayerWidget::default()))
}
}
impl Deref for PlayerWrapper {
type Target = Rc<PlayerWidget>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl PlayerWrapper {
pub(crate) fn new(sender: &Sender<Action>) -> Self {
let w = Self::default();
w.init(sender);
w
}
fn init(&self, sender: &Sender<Action>) {
self.connect_control_buttons();
self.connect_rate_buttons();
self.connect_mpris_buttons(sender);
self.connect_gst_signals(sender);
self.connect_dialog();
}
fn connect_dialog(&self) {
let this = self.deref();
self.squeezer
.connect_property_visible_child_notify(clone!(@weak this => move |_| {
if let Some(child) = this.squeezer.get_visible_child() {
let full = child == this.full;
this.timer.progress_bar.set_visible(!full);
if full {
this.action_bar.get_style_context().remove_class("player-small");
} else {
this.action_bar.get_style_context().add_class("player-small");
}
}
}));
self.timer
.duration
.bind_property("label", &self.dialog.duration, "label")
.flags(glib::BindingFlags::SYNC_CREATE)
.build();
self.timer
.progressed
.bind_property("label", &self.dialog.progressed, "label")
.flags(glib::BindingFlags::SYNC_CREATE)
.build();
self.dialog
.slider
.set_adjustment(&self.timer.slider.get_adjustment());
self.evbox.connect_button_press_event(
clone!(@weak this => @default-return Inhibit(false), move |_, event| {
if event.get_button() != 1 {
return Inhibit(false);
}
// only open the dialog when the small toolbar is visible
if let Some(child) = this.squeezer.get_visible_child() {
if child == this.full {
return Inhibit(false);
}
}
let parent = this.container.get_toplevel().and_then(|toplevel| {
toplevel
.downcast::<gtk::Window>()
.ok()
}).unwrap();
info!("showing dialog");
this.dialog.dialog.set_transient_for(Some(&parent));
this.dialog.dialog.show();
Inhibit(false)
}),
);
self.dialog
.close
.connect_clicked(clone!(@weak this => move |_| {
this.dialog.dialog.hide();
}));
}
/// Connect the `PlayerControls` buttons to the `PlayerExt` methods.
fn connect_control_buttons(&self) {
let this = self.deref();
// Connect the play button to the gst Player.
self.controls
.play
.connect_clicked(clone!(@weak this => move |_| {
this.play();
}));
// Connect the pause button to the gst Player.
self.controls
.pause
.connect_clicked(clone!(@weak this => move |_| {
this.pause();
}));
// Connect the play button to the gst Player.
self.controls
.play_small
.connect_clicked(clone!(@weak this => move |_| {
this.play();
}));
// Connect the pause button to the gst Player.
self.controls
.pause_small
.connect_clicked(clone!(@weak this => move |_| {
this.pause();
}));
// Connect the rewind button to the gst Player.
self.controls
.rewind
.connect_clicked(clone!(@weak this => move |_| {
this.rewind();
}));
// Connect the fast-forward button to the gst Player.
self.controls
.forward
.connect_clicked(clone!(@weak this => move |_| {
this.fast_forward();
}));
// Connect the play button to the gst Player.
self.dialog
.play
.connect_clicked(clone!(@weak this => move |_| {
this.play();
}));
// Connect the pause button to the gst Player.
self.dialog
.pause
.connect_clicked(clone!(@weak this => move |_| {
this.pause();
}));
// Connect the rewind button to the gst Player.
self.dialog
.rewind
.connect_clicked(clone!(@weak this => move |_| {
this.rewind();
}));
// Connect the fast-forward button to the gst Player.
self.dialog
.forward
.connect_clicked(clone!(@weak this => move |_| {
this.fast_forward();
}));
}
#[cfg_attr(rustfmt, rustfmt_skip)]
fn connect_gst_signals(&self, sender: &Sender<Action>) {
// Log gst warnings.
self.player.connect_warning(move |_, warn| warn!("gst warning: {}", warn));
// Log gst errors.
self.player.connect_error(clone!(@strong sender => move |_, _error| {
// sender.send(Action::ErrorNotification(format!("Player Error: {}", error)));
let s = i18n("The media player was unable to execute an action.");
sender.send(Action::ErrorNotification(s)).expect("Action channel blew up somehow");
}));
// The following callbacks require `Send` but are handled by the gtk main loop
let weak = Fragile::new(Rc::downgrade(self));
// Update the duration label and the slider
self.player.connect_duration_changed(clone!(@strong weak => move |_, clock| {
weak.get()
.upgrade()
.map(|p| p.timer.on_duration_changed(Duration(clock)));
}));
// Update the position label and the slider
self.player.connect_position_updated(clone!(@strong weak => move |_, clock| {
weak.get()
.upgrade()
.map(|p| p.timer.on_position_updated(Position(clock)));
}));
// Reset the slider to 0 and show a play button
self.player.connect_end_of_stream(clone!(@strong weak => move |_| {
weak.get()
.upgrade()
.map(|p| p.stop());
}));
}
#[cfg_attr(rustfmt, rustfmt_skip)]
fn connect_rate_buttons(&self) {
self.rate.connect_signals(self);
self.dialog.rate.connect_signals(self);
}
fn connect_mpris_buttons(&self, sender: &Sender<Action>) {
let weak = Rc::downgrade(self);
// FIXME: Reference cycle with mpris
let mpris = self.info.mpris.clone();
self.info
.mpris
.connect_play_pause(clone!(@strong weak => move || {
let player = match weak.upgrade() {
Some(s) => s,
None => return
};
if let Ok(status) = mpris.get_playback_status() {
match status.as_ref() {
"Paused" => player.play(),
"Stopped" => player.play(),
_ => player.pause(),
};
}
}));
self.info
.mpris
.connect_play(clone!(@strong weak => move || {
let player = match weak.upgrade() {
Some(s) => s,
None => return
};
player.play();
}));
self.info
.mpris
.connect_pause(clone!(@strong weak => move || {
let player = match weak.upgrade() {
Some(s) => s,
None => return
};
player.pause();
}));
self.info
.mpris
.connect_next(clone!(@strong weak => move || {
weak.upgrade().map(|p| p.fast_forward());
}));
self.info
.mpris
.connect_previous(clone!(@strong weak => move || {
weak.upgrade().map(|p| p.rewind());
}));
self.info
.mpris
.connect_raise(clone!(@strong sender => move || {
sender.send(Action::RaiseWindow).expect("Action channel blew up somehow");
}));
}
}