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: >k::ListBox, sender: &Sender<Action>) -> Self72 pub(crate) fn new(pd: &Arc<Show>, episodes: >k::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: >k::ListBox, sender: &Sender<Action>)78 fn init(&self, pd: &Arc<Show>, episodes: >k::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: >k::ListBox, sender: &Sender<Action>)102 fn connect_played(&self, pd: &Arc<Show>, episodes: >k::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: >k::ListBox) -> Option<()>135 fn dim_titles(episodes: >k::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