// player.rs // // Copyright 2018 Jordan Petridis // // 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 . // // 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, episode_id: RefCell>, } 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, 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) { 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>>, } #[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::()), ); // 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: >k::Scale, player: WeakRef, ) -> 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> = Mutex::new(None); }; // Figure out the time delta, in seconds, between the last pause and now let now = Local::now(); let last: &Option> = &*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); impl Default for PlayerWrapper { fn default() -> Self { PlayerWrapper(Rc::new(PlayerWidget::default())) } } impl Deref for PlayerWrapper { type Target = Rc; fn deref(&self) -> &Self::Target { &self.0 } } impl PlayerWrapper { pub(crate) fn new(sender: &Sender) -> Self { let w = Self::default(); w.init(sender); w } fn init(&self, sender: &Sender) { 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::() .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) { // 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) { 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"); })); } }