1 // show_menu.rs
2 //
3 // Copyright 2017 Jordan Petridis <jpetridis@gnome.org>
4 //
5 // This program is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 //
18 // SPDX-License-Identifier: GPL-3.0-or-later
19 
20 use gio::prelude::ActionMapExt;
21 use glib::clone;
22 use gtk::prelude::*;
23 
24 use anyhow::Result;
25 use glib::Sender;
26 
27 use podcasts_data::dbqueries;
28 use podcasts_data::utils::delete_show;
29 use podcasts_data::Show;
30 
31 use crate::app::Action;
32 use crate::utils;
33 use crate::widgets::appnotif::InAppNotification;
34 
35 use std::sync::Arc;
36 
37 use crate::i18n::{i18n, i18n_f};
38 
39 #[derive(Debug, Clone)]
40 pub(crate) struct ShowMenu {
41     pub(crate) menu: gio::MenuModel,
42     website: gio::SimpleAction,
43     played: gio::SimpleAction,
44     unsub: gio::SimpleAction,
45     group: gio::SimpleActionGroup,
46 }
47 
48 impl Default for ShowMenu {
default() -> Self49     fn default() -> Self {
50         let builder = gtk::Builder::from_resource("/org/gnome/Podcasts/gtk/show_menu.ui");
51         let menu = builder.object("show_menu").unwrap();
52         let website = gio::SimpleAction::new("open-website", None);
53         let played = gio::SimpleAction::new("mark-played", None);
54         let unsub = gio::SimpleAction::new("unsubscribe", None);
55         let group = gio::SimpleActionGroup::new();
56 
57         group.add_action(&website);
58         group.add_action(&played);
59         group.add_action(&unsub);
60 
61         ShowMenu {
62             menu,
63             website,
64             played,
65             unsub,
66             group,
67         }
68     }
69 }
70 
71 impl ShowMenu {
new(pd: &Arc<Show>, episodes: &gtk::ListBox, sender: &Sender<Action>) -> Self72     pub(crate) fn new(pd: &Arc<Show>, episodes: &gtk::ListBox, sender: &Sender<Action>) -> Self {
73         let s = Self::default();
74         s.init(pd, episodes, sender);
75         s
76     }
77 
init(&self, pd: &Arc<Show>, episodes: &gtk::ListBox, sender: &Sender<Action>)78     fn init(&self, pd: &Arc<Show>, episodes: &gtk::ListBox, sender: &Sender<Action>) {
79         self.connect_website(pd);
80         self.connect_played(pd, episodes, sender);
81         self.connect_unsub(pd, sender);
82 
83         let app = gio::Application::default()
84             .expect("Could not get default application")
85             .downcast::<gtk::Application>()
86             .unwrap();
87         let win = app.active_window().expect("No active window");
88         win.insert_action_group("show", Some(&self.group));
89     }
90 
connect_website(&self, pd: &Arc<Show>)91     fn connect_website(&self, pd: &Arc<Show>) {
92         // TODO: tooltips for actions?
93         self.website
94             .connect_activate(clone!(@strong pd => move |_, _| {
95                 let link = pd.link();
96                 info!("Opening link: {}", link);
97                 let res = open::that(link);
98                 debug_assert!(res.is_ok());
99             }));
100     }
101 
connect_played(&self, pd: &Arc<Show>, episodes: &gtk::ListBox, sender: &Sender<Action>)102     fn connect_played(&self, pd: &Arc<Show>, episodes: &gtk::ListBox, sender: &Sender<Action>) {
103         self.played.connect_activate(
104             clone!(@strong pd, @strong sender, @weak episodes => move |_, _| {
105                 let res = dim_titles(&episodes);
106                 debug_assert!(res.is_some());
107 
108                 send!(sender, Action::MarkAllPlayerNotification(pd.clone()));
109             }),
110         );
111     }
112 
connect_unsub(&self, pd: &Arc<Show>, sender: &Sender<Action>)113     fn connect_unsub(&self, pd: &Arc<Show>, sender: &Sender<Action>) {
114         self.unsub
115             .connect_activate(clone!(@strong pd, @strong sender => move |unsub, _| {
116                 unsub.set_enabled(false);
117 
118                 send!(sender, Action::RemoveShow(pd.clone()));
119 
120                 send!(sender, Action::HeaderBarNormal);
121                 send!(sender, Action::ShowShowsAnimated);
122                 // Queue a refresh after the switch to avoid blocking the db.
123                 send!(sender, Action::RefreshShowsView);
124                 send!(sender, Action::RefreshEpisodesView);
125 
126                 unsub.set_enabled(true);
127             }));
128     }
129 }
130 
131 // Ideally if we had a custom widget this would have been as simple as:
132 // `for row in listbox { ep = row.get_episode(); ep.dim_title(); }`
133 // But now I can't think of a better way to do it than hardcoding the title
134 // position relative to the EpisodeWidget container gtk::Box.
dim_titles(episodes: &gtk::ListBox) -> Option<()>135 fn dim_titles(episodes: &gtk::ListBox) -> Option<()> {
136     let children = episodes.children();
137 
138     for row in children {
139         let row = row.downcast::<gtk::ListBoxRow>().ok()?;
140         let container = row.children().remove(0).downcast::<gtk::Box>().ok()?;
141         let first_children_box = container.children().remove(0).downcast::<gtk::Box>().ok()?;
142         let second_children_box = first_children_box
143             .children()
144             .remove(0)
145             .downcast::<gtk::Box>()
146             .ok()?;
147         let third_children_box = second_children_box
148             .children()
149             .remove(0)
150             .downcast::<gtk::Box>()
151             .ok()?;
152         let title = third_children_box
153             .children()
154             .remove(0)
155             .downcast::<gtk::Label>()
156             .ok()?;
157 
158         title.style_context().add_class("dim-label");
159 
160         let checkmark = third_children_box
161             .children()
162             .remove(1)
163             .downcast::<gtk::Image>()
164             .ok()?;
165         checkmark.show();
166     }
167     Some(())
168 }
169 
mark_all_watched(pd: &Show, sender: &Sender<Action>) -> Result<()>170 fn mark_all_watched(pd: &Show, sender: &Sender<Action>) -> Result<()> {
171     // TODO: If this fails for whatever reason, it should be impossible, show an error
172     dbqueries::update_none_to_played_now(pd)?;
173     // Not all widgets might have been loaded when the mark_all is hit
174     // So we will need to refresh again after it's done.
175     send!(sender, Action::RefreshWidgetIfSame(pd.id()));
176     send!(sender, Action::RefreshEpisodesView);
177     Ok(())
178 }
179 
mark_all_notif(pd: Arc<Show>, sender: &Sender<Action>) -> InAppNotification180 pub(crate) fn mark_all_notif(pd: Arc<Show>, sender: &Sender<Action>) -> InAppNotification {
181     let id = pd.id();
182     let sender_ = sender.clone();
183     let callback = move |revealer: gtk::Revealer| {
184         let res = mark_all_watched(&pd, &sender_);
185         debug_assert!(res.is_ok());
186 
187         revealer.set_reveal_child(false);
188         glib::Continue(false)
189     };
190 
191     let undo_callback = clone!(@strong sender => move || {
192         send!(sender, Action::RefreshWidgetIfSame(id));
193     });
194     let text = i18n("Marked all episodes as listened");
195     InAppNotification::new(&text, 6000, callback, Some(undo_callback))
196 }
197 
remove_show_notif(pd: Arc<Show>, sender: Sender<Action>) -> InAppNotification198 pub(crate) fn remove_show_notif(pd: Arc<Show>, sender: Sender<Action>) -> InAppNotification {
199     let text = i18n_f("Unsubscribed from {}", &[pd.title()]);
200 
201     let res = utils::ignore_show(pd.id());
202     debug_assert!(res.is_ok());
203 
204     let sender_ = sender.clone();
205     let pd_ = pd.clone();
206     let callback = move |revealer: gtk::Revealer| {
207         let res = utils::unignore_show(pd_.id());
208         debug_assert!(res.is_ok());
209 
210         // Spawn a thread so it won't block the ui.
211         rayon::spawn(clone!(@strong pd_, @strong sender_ => move || {
212             delete_show(&pd_)
213                 .map_err(|err| error!("Error: {}", err))
214                 .map_err(|_| error!("Failed to delete {}", pd_.title()))
215                 .ok();
216 
217             send!(sender_, Action::RefreshEpisodesView);
218         }));
219 
220         revealer.set_reveal_child(false);
221         glib::Continue(false)
222     };
223 
224     let undo_callback = move || {
225         let res = utils::unignore_show(pd.id());
226         debug_assert!(res.is_ok());
227         send!(sender, Action::RefreshShowsView);
228         send!(sender, Action::RefreshEpisodesView);
229     };
230 
231     InAppNotification::new(&text, 6000, callback, Some(undo_callback))
232 }
233