diff --git a/podcasts-gtk/resources/gtk/headerbar.ui b/podcasts-gtk/resources/gtk/headerbar.ui index 2879d0b..a3643a4 100644 --- a/podcasts-gtk/resources/gtk/headerbar.ui +++ b/podcasts-gtk/resources/gtk/headerbar.ui @@ -271,41 +271,5 @@ Tobias Bernard 2 - - - False - True - - - False - center - center - - - False - True - 6 - 0 - - - - - False - center - center - Fetching new episodes - end - - - False - True - 1 - - - - - 3 - - diff --git a/podcasts-gtk/resources/gtk/inapp_notif.ui b/podcasts-gtk/resources/gtk/inapp_notif.ui index 0b76e4f..46859e8 100644 --- a/podcasts-gtk/resources/gtk/inapp_notif.ui +++ b/podcasts-gtk/resources/gtk/inapp_notif.ui @@ -53,20 +53,12 @@ Tobias Bernard 3 6 - - 150 - True + False - center - center - 12 - 12 - An in-app action notification - start False - False + True 0 @@ -97,10 +89,27 @@ Tobias Bernard 1 + + + 150 + True + False + center + center + 12 + 12 + An in-app action notification + start + + + False + False + 2 + + Undo - True True True center @@ -113,7 +122,7 @@ Tobias Bernard False False end - 2 + 3 diff --git a/podcasts-gtk/src/app.rs b/podcasts-gtk/src/app.rs index 7892043..d27ca80 100644 --- a/podcasts-gtk/src/app.rs +++ b/podcasts-gtk/src/app.rs @@ -17,10 +17,11 @@ use settings::{self, WindowGeometry}; use stacks::{Content, PopulatedState}; use utils; use widgets::about_dialog; -use widgets::appnotif::{InAppNotification, UndoState}; +use widgets::appnotif::{InAppNotification, SpinnerState, State}; use widgets::player; use widgets::show_menu::{mark_all_notif, remove_show_notif, ShowMenu}; +use std::cell::RefCell; use std::env; use std::rc::Rc; use std::sync::Arc; @@ -57,9 +58,8 @@ pub(crate) enum Action { ShowShowsAnimated, HeaderBarShowTile(String), HeaderBarNormal, - HeaderBarShowUpdateIndicator, - HeaderBarHideUpdateIndicator, MarkAllPlayerNotification(Arc), + ShowUpdateNotif(Receiver), RemoveShow(Arc), ErrorNotification(String), InitEpisode(i32), @@ -75,6 +75,7 @@ pub(crate) struct App { content: Rc, headerbar: Rc
, player: Rc, + updater: RefCell>, sender: Sender, receiver: Receiver, } @@ -130,6 +131,8 @@ impl App { // Add the player to the main Box wrap.add(&player.action_bar); + let updater = RefCell::new(None); + window.add(&wrap); let app = App { @@ -140,6 +143,7 @@ impl App { headerbar: header, content, player, + updater, sender, receiver, }; @@ -293,8 +297,6 @@ impl App { } Action::HeaderBarShowTile(title) => self.headerbar.switch_to_back(&title), Action::HeaderBarNormal => self.headerbar.switch_to_normal(), - Action::HeaderBarShowUpdateIndicator => self.headerbar.show_update_notification(), - Action::HeaderBarHideUpdateIndicator => self.headerbar.hide_update_notification(), Action::MarkAllPlayerNotification(pd) => { let notif = mark_all_notif(pd, &self.sender); notif.show(&self.overlay); @@ -305,10 +307,38 @@ impl App { } Action::ErrorNotification(err) => { error!("An error notification was triggered: {}", err); - let callback = || glib::Continue(false); - let notif = InAppNotification::new(&err, callback, || {}, UndoState::Hidden); + let callback = |revealer: gtk::Revealer| { + revealer.set_reveal_child(false); + glib::Continue(false) + }; + let undo_cb: Option = None; + let notif = InAppNotification::new(&err, 6000, callback, undo_cb); notif.show(&self.overlay); } + Action::ShowUpdateNotif(receiver) => { + let sender = self.sender.clone(); + let callback = move |revealer: gtk::Revealer| { + if let Some(_) = receiver.try_recv() { + revealer.set_reveal_child(false); + sender.send(Action::RefreshAllViews); + return glib::Continue(false); + } + + glib::Continue(true) + }; + let txt = i18n("Fetching new episodes"); + let undo_cb: Option = None; + let updater = InAppNotification::new(&txt, 250, callback, undo_cb); + updater.set_close_state(State::Hidden); + updater.set_spinner_state(SpinnerState::Active); + + let old = self.updater.replace(Some(updater)); + old.map(|i| i.destroy()); + self.updater + .borrow() + .as_ref() + .map(|i| i.show(&self.overlay)); + } Action::InitEpisode(rowid) => { let res = self.player.initialize_episode(rowid); debug_assert!(res.is_ok()); diff --git a/podcasts-gtk/src/headerbar.rs b/podcasts-gtk/src/headerbar.rs index a082daa..766df24 100644 --- a/podcasts-gtk/src/headerbar.rs +++ b/podcasts-gtk/src/headerbar.rs @@ -26,34 +26,10 @@ pub(crate) struct Header { back: gtk::Button, show_title: gtk::Label, hamburger: gtk::MenuButton, - updater: UpdateIndicator, add: AddPopover, dots: gtk::MenuButton, } -#[derive(Debug, Clone)] -struct UpdateIndicator { - container: gtk::Box, - text: gtk::Label, - spinner: gtk::Spinner, -} - -impl UpdateIndicator { - fn show(&self) { - self.spinner.start(); - self.spinner.show(); - self.container.show(); - self.text.show(); - } - - fn hide(&self) { - self.spinner.stop(); - self.spinner.hide(); - self.container.hide(); - self.text.hide(); - } -} - #[derive(Debug, Clone)] struct AddPopover { container: gtk::Popover, @@ -158,16 +134,6 @@ impl Default for Header { // The 3 dots secondary menu let dots = builder.get_object("secondary_menu").unwrap(); - let update_box = builder.get_object("update_notification").unwrap(); - let update_label = builder.get_object("update_label").unwrap(); - let update_spinner = builder.get_object("update_spinner").unwrap(); - - let updater = UpdateIndicator { - container: update_box, - text: update_label, - spinner: update_spinner, - }; - let add_toggle = builder.get_object("add_toggle").unwrap(); let add_popover = builder.get_object("add_popover").unwrap(); let new_url = builder.get_object("new_url").unwrap(); @@ -187,7 +153,6 @@ impl Default for Header { back, show_title, hamburger, - updater, add, dots, } @@ -248,14 +213,6 @@ impl Header { self.show_title.set_text(title) } - pub(crate) fn show_update_notification(&self) { - self.updater.show(); - } - - pub(crate) fn hide_update_notification(&self) { - self.updater.hide(); - } - pub(crate) fn open_menu(&self) { self.hamburger.clicked(); } diff --git a/podcasts-gtk/src/utils.rs b/podcasts-gtk/src/utils.rs index 95cf8dd..57f65ac 100644 --- a/podcasts-gtk/src/utils.rs +++ b/podcasts-gtk/src/utils.rs @@ -8,7 +8,7 @@ use gtk::prelude::*; use gtk::{IsA, Widget}; use chrono::prelude::*; -use crossbeam_channel::{unbounded, Sender}; +use crossbeam_channel::{bounded, unbounded, Sender}; use failure::Error; use fragile::Fragile; use rayon; @@ -200,7 +200,8 @@ fn refresh_feed(source: Option, sender: Sender) -> Result<(), Erro where S: IntoIterator + Send + 'static, { - sender.send(Action::HeaderBarShowUpdateIndicator); + let (up_sender, up_receiver) = bounded(1); + sender.send(Action::ShowUpdateNotif(up_receiver)); rayon::spawn(move || { if let Some(s) = source { @@ -218,8 +219,7 @@ where .ok(); }; - sender.send(Action::HeaderBarHideUpdateIndicator); - sender.send(Action::RefreshAllViews); + up_sender.send(true); }); Ok(()) } diff --git a/podcasts-gtk/src/widgets/appnotif.rs b/podcasts-gtk/src/widgets/appnotif.rs index 8936726..9bb29fa 100644 --- a/podcasts-gtk/src/widgets/appnotif.rs +++ b/podcasts-gtk/src/widgets/appnotif.rs @@ -6,17 +6,26 @@ use std::cell::RefCell; use std::rc::Rc; #[derive(Debug, Clone, Copy)] -pub(crate) enum UndoState { +#[allow(dead_code)] +pub(crate) enum State { Shown, Hidden, } +#[derive(Debug, Clone, Copy)] +#[allow(dead_code)] +pub(crate) enum SpinnerState { + Active, + Stopped, +} + #[derive(Debug, Clone)] pub(crate) struct InAppNotification { revealer: gtk::Revealer, text: gtk::Label, undo: gtk::Button, close: gtk::Button, + spinner: gtk::Spinner, } impl Default for InAppNotification { @@ -27,37 +36,54 @@ impl Default for InAppNotification { let text: gtk::Label = builder.get_object("text").unwrap(); let undo: gtk::Button = builder.get_object("undo").unwrap(); let close: gtk::Button = builder.get_object("close").unwrap(); + let spinner = builder.get_object("spinner").unwrap(); InAppNotification { revealer, text, undo, close, + spinner, } } } +/// Timer should be in milliseconds impl InAppNotification { pub(crate) fn new( text: &str, + timer: u32, mut callback: F, - undo_callback: U, - show_undo: UndoState, + undo_callback: Option, ) -> Self where - F: FnMut() -> glib::Continue + 'static, + F: FnMut(gtk::Revealer) -> glib::Continue + 'static, U: Fn() + 'static, { let notif = InAppNotification::default(); notif.text.set_text(&text); - let revealer = notif.revealer.clone(); - let id = timeout_add_seconds(6, move || { - revealer.set_reveal_child(false); - callback() + let revealer_weak = notif.revealer.downgrade(); + let mut time = 0; + let id = timeout_add(250, move || { + if time < timer { + time += 250; + return glib::Continue(true); + }; + + let revealer = match revealer_weak.upgrade() { + Some(r) => r, + None => return glib::Continue(false), + }; + + callback(revealer) }); let id = Rc::new(RefCell::new(Some(id))); + if undo_callback.is_some() { + notif.set_undo_state(State::Shown) + }; + // Cancel the callback let revealer = notif.revealer.clone(); notif.undo.connect_clicked(move |_| { @@ -66,23 +92,25 @@ impl InAppNotification { glib::source::source_remove(id); } - undo_callback(); + if let Some(ref f) = undo_callback { + f(); + } // Hide the notification revealer.set_reveal_child(false); }); // Hide the revealer when the close button is clicked - let revealer = notif.revealer.clone(); + let revealer_weak = notif.revealer.downgrade(); notif.close.connect_clicked(move |_| { + let revealer = match revealer_weak.upgrade() { + Some(r) => r, + None => return, + }; + revealer.set_reveal_child(false); }); - match show_undo { - UndoState::Shown => (), - UndoState::Hidden => notif.undo.hide(), - } - notif } @@ -96,4 +124,35 @@ impl InAppNotification { // so there will be a nice animation. self.revealer.set_reveal_child(true); } + + pub(crate) fn set_undo_state(&self, state: State) { + match state { + State::Shown => self.undo.show(), + State::Hidden => self.undo.hide(), + } + } + + pub(crate) fn set_close_state(&self, state: State) { + match state { + State::Shown => self.close.show(), + State::Hidden => self.close.hide(), + } + } + + pub(crate) fn set_spinner_state(&self, state: SpinnerState) { + match state { + SpinnerState::Active => { + self.spinner.start(); + self.spinner.show(); + } + SpinnerState::Stopped => { + self.spinner.stop(); + self.spinner.hide(); + } + } + } + + pub(crate) fn destroy(self) { + self.revealer.destroy(); + } } diff --git a/podcasts-gtk/src/widgets/show_menu.rs b/podcasts-gtk/src/widgets/show_menu.rs index b67df6e..b97b1be 100644 --- a/podcasts-gtk/src/widgets/show_menu.rs +++ b/podcasts-gtk/src/widgets/show_menu.rs @@ -13,7 +13,7 @@ use podcasts_data::Show; use app::Action; use utils; -use widgets::appnotif::{InAppNotification, UndoState}; +use widgets::appnotif::InAppNotification; use std::sync::Arc; @@ -137,15 +137,18 @@ fn mark_all_watched(pd: &Show, sender: &Sender) -> Result<(), Error> { pub(crate) fn mark_all_notif(pd: Arc, sender: &Sender) -> InAppNotification { let id = pd.id(); - let callback = clone!(sender => move || { - let res = mark_all_watched(&pd, &sender); + let sender_ = sender.clone(); + let callback = move |revealer: gtk::Revealer| { + let res = mark_all_watched(&pd, &sender_); debug_assert!(res.is_ok()); + + revealer.set_reveal_child(false); glib::Continue(false) - }); + }; let undo_callback = clone!(sender => move || sender.send(Action::RefreshWidgetIfSame(id))); let text = i18n("Marked all episodes as listened"); - InAppNotification::new(&text, callback, undo_callback, UndoState::Shown) + InAppNotification::new(&text, 6000, callback, Some(undo_callback)) } pub(crate) fn remove_show_notif(pd: Arc, sender: Sender) -> InAppNotification { @@ -154,21 +157,25 @@ pub(crate) fn remove_show_notif(pd: Arc, sender: Sender) -> InAppN let res = utils::ignore_show(pd.id()); debug_assert!(res.is_ok()); - let callback = clone!(pd, sender => move || { - let res = utils::uningore_show(pd.id()); + let sender_ = sender.clone(); + let pd_ = pd.clone(); + let callback = move |revealer: gtk::Revealer| { + let res = utils::uningore_show(pd_.id()); debug_assert!(res.is_ok()); // Spawn a thread so it won't block the ui. - rayon::spawn(clone!(pd, sender => move || { - delete_show(&pd) + rayon::spawn(clone!(pd_, sender_ => move || { + delete_show(&pd_) .map_err(|err| error!("Error: {}", err)) - .map_err(|_| error!("Failed to delete {}", pd.title())) + .map_err(|_| error!("Failed to delete {}", pd_.title())) .ok(); - sender.send(Action::RefreshEpisodesView); + sender_.send(Action::RefreshEpisodesView); })); + + revealer.set_reveal_child(false); glib::Continue(false) - }); + }; let undo_callback = move || { let res = utils::uningore_show(pd.id()); @@ -177,5 +184,5 @@ pub(crate) fn remove_show_notif(pd: Arc, sender: Sender) -> InAppN sender.send(Action::RefreshEpisodesView); }; - InAppNotification::new(&text, callback, undo_callback, UndoState::Shown) + InAppNotification::new(&text, 6000, callback, Some(undo_callback)) }