use glib; use gtk; use gtk::prelude::*; use crossbeam_channel::{SendError, Sender}; use failure::Error; use html2pango::markup_from_raw; use open; use rayon; use send_cell::SendCell; use hammond_data::dbqueries; use hammond_data::utils::delete_show; use hammond_data::Podcast; use app::Action; use utils::{self, lazy_load}; use widgets::appnotif::{InAppNotification, UndoState}; use widgets::EpisodeWidget; use std::rc::Rc; use std::sync::{Arc, Mutex}; lazy_static! { static ref SHOW_WIDGET_VALIGNMENT: Mutex)>> = Mutex::new(None); } #[derive(Debug, Clone)] pub struct ShowWidget { pub container: gtk::Box, scrolled_window: gtk::ScrolledWindow, cover: gtk::Image, description: gtk::Label, link: gtk::Button, settings: gtk::MenuButton, unsub: gtk::Button, episodes: gtk::ListBox, podcast_id: Option, } impl Default for ShowWidget { fn default() -> Self { let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/show_widget.ui"); let container: gtk::Box = builder.get_object("container").unwrap(); let scrolled_window: gtk::ScrolledWindow = builder.get_object("scrolled_window").unwrap(); let episodes = builder.get_object("episodes").unwrap(); 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, podcast_id: None, } } } impl ShowWidget { pub fn new(pd: Arc, sender: Sender) -> Rc { let mut pdw = ShowWidget::default(); pdw.init(&pd, &sender); let pdw = Rc::new(pdw); populate_listbox(&pdw, pd, 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); })); self.set_description(pd.description()); self.podcast_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. fn set_cover(&self, pd: &Arc) -> Result<(), Error> { utils::set_image_from_path(&self.cover, pd.id(), 256) } /// Set the descripton text. fn set_description(&self, text: &str) { self.description.set_markup(&markup_from_raw(text)); } /// Save the scrollabar vajustment to the cache. pub fn save_vadjustment(&self, oldid: i32) -> Result<(), Error> { if let Ok(mut guard) = SHOW_WIDGET_VALIGNMENT.lock() { let adj = self .scrolled_window .get_vadjustment() .ok_or_else(|| format_err!("Could not get the adjustment"))?; *guard = Some((oldid, SendCell::new(adj))); debug!("Widget Alignment was saved with ID: {}.", oldid); } Ok(()) } /// Set scrolled window vertical adjustment. fn set_vadjustment(&self, pd: &Arc) -> Result<(), Error> { let guard = SHOW_WIDGET_VALIGNMENT .lock() .map_err(|err| format_err!("Failed to lock widget align mutex: {}", err))?; if let Some((oldid, ref sendcell)) = *guard { // Only copy the old scrollbar if both widget's represent the same podcast. debug!("PID: {}", pd.id()); debug!("OLDID: {}", oldid); if pd.id() != oldid { debug!("Early return"); return Ok(()); }; // Copy the vertical scrollbar adjustment from the old view into the new one. sendcell .try_get() .map(|x| utils::smooth_scroll_to(&self.scrolled_window, &x)); } Ok(()) } pub fn podcast_id(&self) -> Option { self.podcast_id } } /// Populate the listbox with the shows episodes. fn populate_listbox( show: &Rc, pd: Arc, sender: Sender, ) -> Result<(), Error> { use crossbeam_channel::bounded; use crossbeam_channel::TryRecvError::*; let count = dbqueries::get_pd_episodes_count(&pd)?; let (sender_, receiver) = bounded(1); rayon::spawn(clone!(pd => move || { let episodes = dbqueries::get_pd_episodeswidgets(&pd).unwrap(); // The receiver can be dropped if there's an early return // like on show without episodes for example. sender_.send(episodes).ok(); })); if count == 0 { let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/empty_show.ui"); let container: gtk::Box = builder.get_object("empty_show").unwrap(); show.episodes.add(&container); return Ok(()); } let show_ = show.clone(); gtk::idle_add(move || { let episodes = match receiver.try_recv() { Ok(e) => e, Err(Empty) => return glib::Continue(true), Err(Disconnected) => return glib::Continue(false), }; let list = show_.episodes.clone(); let constructor = clone!(sender => move |ep| { EpisodeWidget::new(&ep, &sender).container.clone() }); let callback = clone!(pd, show_ => move || { show_.set_vadjustment(&pd) .map_err(|err| error!("Failed to set ShowWidget Alignment: {}", err)) .ok(); }); lazy_load(episodes, list.clone(), constructor, callback); glib::Continue(false) }); 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); let wrap = || -> Result<(), SendError<_>> { 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)?; Ok(()) }; wrap().map_err(|err| error!("Action Sender: {}", err)).ok(); 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)) .map_err(|err| error!("Action Sender: {}", err)) .ok(); } fn mark_all_watched(pd: &Podcast, 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).map_err(From::from) } 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)) .map_err(|err| error!("Action Sender: {}", err)) .ok(); }); 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).ok(); })); glib::Continue(false) }); let undo_wrap = move || -> Result<(), Error> { utils::uningore_show(pd.id())?; sender.send(Action::RefreshShowsView)?; sender.send(Action::RefreshEpisodesView)?; Ok(()) }; let undo_callback = move || { undo_wrap().map_err(|err| error!("{}", err)).ok(); }; 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(()) }