diff --git a/hammond-gtk/resources/gtk/headerbar.ui b/hammond-gtk/resources/gtk/headerbar.ui index dbaf339..ae3f316 100644 --- a/hammond-gtk/resources/gtk/headerbar.ui +++ b/hammond-gtk/resources/gtk/headerbar.ui @@ -257,6 +257,29 @@ Tobias Bernard + + + True + False + True + center + + + True + False + view-more-symbolic + 1 + + + + + + end + 2 + + True diff --git a/hammond-gtk/resources/gtk/secondary_menu.ui b/hammond-gtk/resources/gtk/secondary_menu.ui new file mode 100644 index 0000000..393a7e6 --- /dev/null +++ b/hammond-gtk/resources/gtk/secondary_menu.ui @@ -0,0 +1,20 @@ + + + + +
+ + _Mark all episodes as played + + + + _Website + + + + _Unsubscribe + + +
+
+
\ No newline at end of file diff --git a/hammond-gtk/resources/gtk/show_menu.ui b/hammond-gtk/resources/gtk/show_menu.ui new file mode 100644 index 0000000..64c5966 --- /dev/null +++ b/hammond-gtk/resources/gtk/show_menu.ui @@ -0,0 +1,70 @@ + + + + + + False + center + center + + + True + False + center + center + 6 + 6 + 6 + 6 + vertical + 6 + + + True + True + True + Open Website + True + + + False + True + 0 + + + + + True + True + True + Mark all as played + True + + + False + True + 1 + + + + + True + True + True + Unsubscribe + True + + + False + True + 2 + + + + + main + 1 + + + + diff --git a/hammond-gtk/resources/gtk/show_widget.ui b/hammond-gtk/resources/gtk/show_widget.ui index 16b40ff..cf4e875 100644 --- a/hammond-gtk/resources/gtk/show_widget.ui +++ b/hammond-gtk/resources/gtk/show_widget.ui @@ -79,13 +79,12 @@ Tobias Bernard 32 True vertical - 24 True False vertical - 6 + 12 True @@ -133,75 +132,6 @@ Sorry, we could not find a description for this Show. 1 - - - True - False - 6 - - - True - True - True - - - True - False - center - center - emblem-system-symbolic - - - - - False - True - 0 - - - - - Website - True - True - True - center - center - - - False - True - 5 - 1 - - - - - Unsubscribe - True - True - True - center - center - - - - False - True - 5 - end - 2 - - - - - False - False - 2 - - False @@ -213,6 +143,7 @@ Sorry, we could not find a description for this Show. True False + 12 0 in diff --git a/hammond-gtk/resources/resources.xml b/hammond-gtk/resources/resources.xml index 7703ec0..57a377d 100644 --- a/hammond-gtk/resources/resources.xml +++ b/hammond-gtk/resources/resources.xml @@ -12,6 +12,7 @@ gtk/headerbar.ui gtk/inapp_notif.ui gtk/hamburger.ui + gtk/show_menu.ui gtk/help-overlay.ui gtk/player_toolbar.ui gtk/style.css diff --git a/hammond-gtk/src/app.rs b/hammond-gtk/src/app.rs index 3c51f1b..265c2ec 100644 --- a/hammond-gtk/src/app.rs +++ b/hammond-gtk/src/app.rs @@ -11,14 +11,16 @@ use gtk::SettingsExt as GtkSettingsExt; use crossbeam_channel::{unbounded, Receiver, Sender}; use hammond_data::Show; +use send_cell::SendCell; use headerbar::Header; use settings::{self, WindowGeometry}; use stacks::{Content, PopulatedState}; use utils; +use widgets::about_dialog; use widgets::appnotif::{InAppNotification, UndoState}; use widgets::player; -use widgets::{about_dialog, mark_all_notif, remove_show_notif}; +use widgets::show_menu::{mark_all_notif, remove_show_notif, ShowMenu}; use std::env; use std::rc::Rc; @@ -56,6 +58,7 @@ pub enum Action { RemoveShow(Arc), ErrorNotification(String), InitEpisode(i32), + InitShowMenu(SendCell), } #[derive(Debug, Clone)] @@ -265,6 +268,10 @@ impl App { notif.show(&self.overlay); } Action::InitEpisode(rowid) => self.player.initialize_episode(rowid).unwrap(), + Action::InitShowMenu(s) => { + let menu = s.borrow(); + self.headerbar.set_secondary_menu(&menu.container); + } } } diff --git a/hammond-gtk/src/headerbar.rs b/hammond-gtk/src/headerbar.rs index c87c251..cd1facf 100644 --- a/hammond-gtk/src/headerbar.rs +++ b/hammond-gtk/src/headerbar.rs @@ -27,6 +27,7 @@ pub struct Header { app_menu: MenuModel, updater: UpdateIndicator, add: AddPopover, + dots: gtk::MenuButton, } #[derive(Debug, Clone)] @@ -141,6 +142,7 @@ impl Default for Header { let back = builder.get_object("back").unwrap(); let show_title = builder.get_object("show_title").unwrap(); let menu_button = builder.get_object("menu_button").unwrap(); + let dots = builder.get_object("secondary_menu").unwrap(); let app_menu = menus.get_object("menu").unwrap(); let update_box = builder.get_object("update_notification").unwrap(); @@ -175,11 +177,11 @@ impl Default for Header { app_menu, updater, add, + dots, } } } -// TODO: Factor out the hamburger menu // TODO: Make a proper state machine for the headerbar states impl Header { pub fn new(content: &Content, sender: &Sender) -> Rc { @@ -209,13 +211,15 @@ impl Header { let add_toggle = &s.add.toggle; let show_title = &s.show_title; let menu = &s.menu_button; + let dots = &s.dots; s.back.connect_clicked( - clone!(switch, add_toggle, show_title, sender, menu => move |back| { + clone!(switch, add_toggle, show_title, sender, menu, dots => move |back| { switch.show(); add_toggle.show(); back.hide(); show_title.hide(); menu.show(); + dots.hide(); sender.send(Action::ShowShowsAnimated); }), ); @@ -230,6 +234,7 @@ impl Header { self.set_show_title(title); self.show_title.show(); self.menu_button.hide(); + self.dots.show(); } pub fn switch_to_normal(&self) { @@ -238,6 +243,7 @@ impl Header { self.back.hide(); self.show_title.hide(); self.menu_button.show(); + self.dots.hide(); } pub fn set_show_title(&self, title: &str) { @@ -255,4 +261,8 @@ impl Header { pub fn open_menu(&self) { self.menu_button.clicked(); } + + pub fn set_secondary_menu(&self, pop: >k::PopoverMenu) { + self.dots.set_popover(Some(pop)); + } } diff --git a/hammond-gtk/src/widgets/mod.rs b/hammond-gtk/src/widgets/mod.rs index f852617..1cc8f9c 100644 --- a/hammond-gtk/src/widgets/mod.rs +++ b/hammond-gtk/src/widgets/mod.rs @@ -5,6 +5,7 @@ mod episode; mod home_view; pub mod player; mod show; +pub mod show_menu; mod shows_view; pub use self::aboutdialog::about_dialog; @@ -12,5 +13,5 @@ pub use self::empty::EmptyView; pub use self::episode::EpisodeWidget; pub use self::home_view::HomeView; pub use self::show::ShowWidget; -pub use self::show::{mark_all_notif, remove_show_notif}; +pub use self::show_menu::ShowMenu; pub use self::shows_view::ShowsView; diff --git a/hammond-gtk/src/widgets/show.rs b/hammond-gtk/src/widgets/show.rs index 5159d2f..307167f 100644 --- a/hammond-gtk/src/widgets/show.rs +++ b/hammond-gtk/src/widgets/show.rs @@ -5,18 +5,15 @@ use gtk::prelude::*; use crossbeam_channel::Sender; use failure::Error; use html2text; -use open; use rayon; use send_cell::SendCell; use hammond_data::dbqueries; -use hammond_data::utils::delete_show; use hammond_data::Show; use app::Action; use utils::{self, lazy_load}; -use widgets::appnotif::{InAppNotification, UndoState}; -use widgets::EpisodeWidget; +use widgets::{EpisodeWidget, ShowMenu}; use std::rc::Rc; use std::sync::{Arc, Mutex}; @@ -32,9 +29,6 @@ pub struct ShowWidget { scrolled_window: gtk::ScrolledWindow, cover: gtk::Image, description: gtk::Label, - link: gtk::Button, - settings: gtk::MenuButton, - unsub: gtk::Button, episodes: gtk::ListBox, show_id: Option, } @@ -48,18 +42,12 @@ impl Default for ShowWidget { let cover: gtk::Image = builder.get_object("cover").unwrap(); let description: gtk::Label = builder.get_object("description").unwrap(); - let unsub: gtk::Button = builder.get_object("unsub_button").unwrap(); - let link: gtk::Button = builder.get_object("link_button").unwrap(); - let settings: gtk::MenuButton = builder.get_object("settings_button").unwrap(); ShowWidget { container, scrolled_window, cover, description, - unsub, - link, - settings, episodes, show_id: None, } @@ -69,52 +57,26 @@ impl Default for ShowWidget { impl ShowWidget { pub fn new(pd: Arc, sender: Sender) -> Rc { let mut pdw = ShowWidget::default(); - pdw.init(&pd, &sender); + pdw.init(&pd); + + let menu = ShowMenu::new(&pd, &pdw.episodes, &sender); + sender.send(Action::InitShowMenu(SendCell::new(menu))); + let pdw = Rc::new(pdw); - populate_listbox(&pdw, pd, sender) + populate_listbox(&pdw, pd.clone(), sender) .map_err(|err| error!("Failed to populate the listbox: {}", err)) .ok(); pdw } - pub fn init(&mut self, pd: &Arc, sender: &Sender) { - let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/show_widget.ui"); - - self.unsub - .connect_clicked(clone!(pd, sender => move |bttn| { - on_unsub_button_clicked(pd.clone(), bttn, &sender); - })); - + pub fn init(&mut self, pd: &Arc) { self.set_description(pd.description()); self.show_id = Some(pd.id()); self.set_cover(&pd) .map_err(|err| error!("Failed to set a cover: {}", err)) .ok(); - - let link = pd.link().to_owned(); - self.link.set_tooltip_text(Some(link.as_str())); - self.link.connect_clicked(move |_| { - info!("Opening link: {}", &link); - open::that(&link) - .map_err(|err| error!("Error: {}", err)) - .map_err(|_| error!("Failed open link: {}", &link)) - .ok(); - }); - - let show_menu: gtk::Popover = builder.get_object("show_menu").unwrap(); - let mark_all: gtk::ModelButton = builder.get_object("mark_all_watched").unwrap(); - - let episodes = self.episodes.clone(); - mark_all.connect_clicked(clone!(pd, sender => move |_| { - on_played_button_clicked( - pd.clone(), - &episodes, - &sender - ) - })); - self.settings.set_popover(&show_menu); } /// Set the show cover. @@ -173,6 +135,7 @@ impl ShowWidget { /// Populate the listbox with the shows episodes. fn populate_listbox( + // FIXME: Refference cycle show: &Rc, pd: Arc, sender: Sender, @@ -222,111 +185,3 @@ fn populate_listbox( Ok(()) } - -fn on_unsub_button_clicked(pd: Arc, unsub_button: >k::Button, sender: &Sender) { - // hack to get away without properly checking for none. - // if pressed twice would panic. - unsub_button.set_sensitive(false); - - sender.send(Action::RemoveShow(pd)); - - sender.send(Action::HeaderBarNormal); - sender.send(Action::ShowShowsAnimated); - // Queue a refresh after the switch to avoid blocking the db. - sender.send(Action::RefreshShowsView); - sender.send(Action::RefreshEpisodesView); - - unsub_button.set_sensitive(true); -} - -fn on_played_button_clicked(pd: Arc, episodes: >k::ListBox, sender: &Sender) { - if dim_titles(episodes).is_none() { - error!("Something went horribly wrong when dimming the titles."); - warn!("RUN WHILE YOU STILL CAN!"); - } - - sender.send(Action::MarkAllPlayerNotification(pd)) -} - -fn mark_all_watched(pd: &Show, sender: &Sender) -> Result<(), Error> { - dbqueries::update_none_to_played_now(pd)?; - // Not all widgets migth have been loaded when the mark_all is hit - // So we will need to refresh again after it's done. - sender.send(Action::RefreshWidgetIfSame(pd.id())); - sender.send(Action::RefreshEpisodesView); - Ok(()) -} - -pub fn mark_all_notif(pd: Arc, sender: &Sender) -> InAppNotification { - let id = pd.id(); - let callback = clone!(sender => move || { - mark_all_watched(&pd, &sender) - .map_err(|err| error!("Notif Callback Error: {}", err)) - .ok(); - glib::Continue(false) - }); - - let undo_callback = clone!(sender => move || sender.send(Action::RefreshWidgetIfSame(id))); - let text = "Marked all episodes as listened"; - InAppNotification::new(text, callback, undo_callback, UndoState::Shown) -} - -pub fn remove_show_notif(pd: Arc, sender: Sender) -> InAppNotification { - let text = format!("Unsubscribed from {}", pd.title()); - - utils::ignore_show(pd.id()) - .map_err(|err| error!("Error: {}", err)) - .map_err(|_| error!("Could not insert {} to the ignore list.", pd.title())) - .ok(); - - let callback = clone!(pd, sender => move || { - utils::uningore_show(pd.id()) - .map_err(|err| error!("Error: {}", err)) - .map_err(|_| error!("Could not remove {} from the ignore list.", pd.title())) - .ok(); - - // Spawn a thread so it won't block the ui. - rayon::spawn(clone!(pd, sender => move || { - delete_show(&pd) - .map_err(|err| error!("Error: {}", err)) - .map_err(|_| error!("Failed to delete {}", pd.title())) - .ok(); - - sender.send(Action::RefreshEpisodesView); - })); - glib::Continue(false) - }); - - let undo_callback = move || { - utils::uningore_show(pd.id()) - .map_err(|err| error!("{}", err)) - .ok(); - sender.send(Action::RefreshShowsView); - sender.send(Action::RefreshEpisodesView); - }; - - InAppNotification::new(&text, callback, undo_callback, UndoState::Shown) -} - -// Ideally if we had a custom widget this would have been as simple as: -// `for row in listbox { ep = row.get_episode(); ep.dim_title(); }` -// But now I can't think of a better way to do it than hardcoding the title -// position relative to the EpisodeWidget container gtk::Box. -fn dim_titles(episodes: >k::ListBox) -> Option<()> { - let children = episodes.get_children(); - - for row in children { - let row = row.downcast::().ok()?; - let container = row.get_children().remove(0).downcast::().ok()?; - let foo = container - .get_children() - .remove(0) - .downcast::() - .ok()?; - let bar = foo.get_children().remove(0).downcast::().ok()?; - let title = bar.get_children().remove(0).downcast::().ok()?; - - title.get_style_context().map(|c| c.add_class("dim-label")); - } - Some(()) -} diff --git a/hammond-gtk/src/widgets/show_menu.rs b/hammond-gtk/src/widgets/show_menu.rs new file mode 100644 index 0000000..5e2efad --- /dev/null +++ b/hammond-gtk/src/widgets/show_menu.rs @@ -0,0 +1,181 @@ +use glib; +use gtk; +use gtk::prelude::*; + +use crossbeam_channel::Sender; +use failure::Error; +use open; +use rayon; + +use hammond_data::dbqueries; +use hammond_data::utils::delete_show; +use hammond_data::Show; + +use app::Action; +use utils; +use widgets::appnotif::{InAppNotification, UndoState}; + +use std::sync::Arc; + +#[derive(Debug, Clone)] +pub struct ShowMenu { + pub container: gtk::PopoverMenu, + website: gtk::ModelButton, + played: gtk::ModelButton, + unsub: gtk::ModelButton, +} + +impl Default for ShowMenu { + fn default() -> Self { + let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/show_menu.ui"); + let container = builder.get_object("menu").unwrap(); + let website = builder.get_object("website").unwrap(); + let played = builder.get_object("played").unwrap(); + let unsub = builder.get_object("unsub").unwrap(); + + ShowMenu { + container, + website, + played, + unsub, + } + } +} + +impl ShowMenu { + pub fn new(pd: &Arc, episodes: >k::ListBox, sender: &Sender) -> Self { + let s = Self::default(); + s.init(pd, episodes, sender); + s + } + + fn init(&self, pd: &Arc, episodes: >k::ListBox, sender: &Sender) { + self.connect_website(pd); + self.connect_played(pd, episodes, sender); + self.connect_unsub(pd, sender) + } + + fn connect_website(&self, pd: &Arc) { + self.website.set_tooltip_text(Some(pd.link())); + self.website.connect_clicked(clone!(pd => move |_| { + let link = pd.link(); + info!("Opening link: {}", link); + open::that(link) + .map_err(|err| error!("Error: {}", err)) + .map_err(|_| error!("Failed open link: {}", link)) + .ok(); + })); + } + + fn connect_played(&self, pd: &Arc, episodes: >k::ListBox, sender: &Sender) { + self.played + .connect_clicked(clone!(pd, episodes, sender => move |_| { + let res = dim_titles(&episodes); + debug_assert!(res.is_some()); + + sender.send(Action::MarkAllPlayerNotification(pd.clone())) + })); + } + + fn connect_unsub(&self, pd: &Arc, sender: &Sender) { + self.unsub + .connect_clicked(clone!(pd, sender => move |unsub| { + // hack to get away without properly checking for none. + // if pressed twice would panic. + unsub.set_sensitive(false); + + sender.send(Action::RemoveShow(pd.clone())); + + sender.send(Action::HeaderBarNormal); + sender.send(Action::ShowShowsAnimated); + // Queue a refresh after the switch to avoid blocking the db. + sender.send(Action::RefreshShowsView); + sender.send(Action::RefreshEpisodesView); + + unsub.set_sensitive(true); + })); + } +} + +// Ideally if we had a custom widget this would have been as simple as: +// `for row in listbox { ep = row.get_episode(); ep.dim_title(); }` +// But now I can't think of a better way to do it than hardcoding the title +// position relative to the EpisodeWidget container gtk::Box. +fn dim_titles(episodes: >k::ListBox) -> Option<()> { + let children = episodes.get_children(); + + for row in children { + let row = row.downcast::().ok()?; + let container = row.get_children().remove(0).downcast::().ok()?; + let foo = container + .get_children() + .remove(0) + .downcast::() + .ok()?; + let bar = foo.get_children().remove(0).downcast::().ok()?; + let title = bar.get_children().remove(0).downcast::().ok()?; + + title.get_style_context().map(|c| c.add_class("dim-label")); + } + Some(()) +} + +fn mark_all_watched(pd: &Show, sender: &Sender) -> Result<(), Error> { + dbqueries::update_none_to_played_now(pd)?; + // Not all widgets migth have been loaded when the mark_all is hit + // So we will need to refresh again after it's done. + sender.send(Action::RefreshWidgetIfSame(pd.id())); + sender.send(Action::RefreshEpisodesView); + Ok(()) +} + +pub fn mark_all_notif(pd: Arc, sender: &Sender) -> InAppNotification { + let id = pd.id(); + let callback = clone!(sender => move || { + mark_all_watched(&pd, &sender) + .map_err(|err| error!("Notif Callback Error: {}", err)) + .ok(); + glib::Continue(false) + }); + + let undo_callback = clone!(sender => move || sender.send(Action::RefreshWidgetIfSame(id))); + let text = "Marked all episodes as listened"; + InAppNotification::new(text, callback, undo_callback, UndoState::Shown) +} + +pub fn remove_show_notif(pd: Arc, sender: Sender) -> InAppNotification { + let text = format!("Unsubscribed from {}", pd.title()); + + utils::ignore_show(pd.id()) + .map_err(|err| error!("Error: {}", err)) + .map_err(|_| error!("Could not insert {} to the ignore list.", pd.title())) + .ok(); + + let callback = clone!(pd, sender => move || { + utils::uningore_show(pd.id()) + .map_err(|err| error!("Error: {}", err)) + .map_err(|_| error!("Could not remove {} from the ignore list.", pd.title())) + .ok(); + + // Spawn a thread so it won't block the ui. + rayon::spawn(clone!(pd, sender => move || { + delete_show(&pd) + .map_err(|err| error!("Error: {}", err)) + .map_err(|_| error!("Failed to delete {}", pd.title())) + .ok(); + + sender.send(Action::RefreshEpisodesView); + })); + glib::Continue(false) + }); + + let undo_callback = move || { + utils::uningore_show(pd.id()) + .map_err(|err| error!("{}", err)) + .ok(); + sender.send(Action::RefreshShowsView); + sender.send(Action::RefreshEpisodesView); + }; + + InAppNotification::new(&text, callback, undo_callback, UndoState::Shown) +}