podcasts/podcasts-gtk/src/widgets/show_menu.rs
2020-02-17 13:11:44 +02:00

217 lines
7.5 KiB
Rust

// show_menu.rs
//
// Copyright 2017 Jordan Petridis <jpetridis@gnome.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-or-later
use glib;
use glib::clone;
use gtk;
use gtk::prelude::*;
use crossbeam_channel::Sender;
use failure::Error;
use open;
use rayon;
use podcasts_data::dbqueries;
use podcasts_data::utils::delete_show;
use podcasts_data::Show;
use crate::app::Action;
use crate::utils;
use crate::widgets::appnotif::InAppNotification;
use std::sync::Arc;
use crate::i18n::{i18n, i18n_f};
#[derive(Debug, Clone)]
pub(crate) struct ShowMenu {
pub(crate) 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/Podcasts/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(crate) fn new(pd: &Arc<Show>, episodes: &gtk::ListBox, sender: &Sender<Action>) -> Self {
let s = Self::default();
s.init(pd, episodes, sender);
s
}
fn init(&self, pd: &Arc<Show>, episodes: &gtk::ListBox, sender: &Sender<Action>) {
self.connect_website(pd);
self.connect_played(pd, episodes, sender);
self.connect_unsub(pd, sender)
}
fn connect_website(&self, pd: &Arc<Show>) {
self.website.set_tooltip_text(Some(pd.link()));
self.website.connect_clicked(clone!(@strong pd => move |_| {
let link = pd.link();
info!("Opening link: {}", link);
let res = open::that(link);
debug_assert!(res.is_ok());
}));
}
fn connect_played(&self, pd: &Arc<Show>, episodes: &gtk::ListBox, sender: &Sender<Action>) {
self.played.connect_clicked(clone!(@strong pd, @strong sender, @weak episodes => move |_| {
let res = dim_titles(&episodes);
debug_assert!(res.is_some());
sender.send(Action::MarkAllPlayerNotification(pd.clone())).expect("Action channel blew up somehow")
}));
}
fn connect_unsub(&self, pd: &Arc<Show>, sender: &Sender<Action>) {
self.unsub
.connect_clicked(clone!(@strong pd, @strong 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())).expect("Action channel blew up somehow");
sender.send(Action::HeaderBarNormal).expect("Action channel blew up somehow");
sender.send(Action::ShowShowsAnimated).expect("Action channel blew up somehow");
// Queue a refresh after the switch to avoid blocking the db.
sender.send(Action::RefreshShowsView).expect("Action channel blew up somehow");
sender.send(Action::RefreshEpisodesView).expect("Action channel blew up somehow");
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: &gtk::ListBox) -> Option<()> {
let children = episodes.get_children();
for row in children {
let row = row.downcast::<gtk::ListBoxRow>().ok()?;
let container = row.get_children().remove(0).downcast::<gtk::Box>().ok()?;
let foo = container
.get_children()
.remove(0)
.downcast::<gtk::Box>()
.ok()?;
let bar = foo.get_children().remove(0).downcast::<gtk::Box>().ok()?;
let baz = bar.get_children().remove(0).downcast::<gtk::Box>().ok()?;
let title = baz.get_children().remove(0).downcast::<gtk::Label>().ok()?;
title.get_style_context().add_class("dim-label");
let checkmark = baz.get_children().remove(1).downcast::<gtk::Image>().ok()?;
checkmark.show();
}
Some(())
}
fn mark_all_watched(pd: &Show, sender: &Sender<Action>) -> Result<(), Error> {
// TODO: If this fails for whatever reason, it should be impossible, show an error
dbqueries::update_none_to_played_now(pd)?;
// Not all widgets might 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()))
.expect("Action channel blew up somehow");
sender
.send(Action::RefreshEpisodesView)
.expect("Action channel blew up somehow");
Ok(())
}
pub(crate) fn mark_all_notif(pd: Arc<Show>, sender: &Sender<Action>) -> InAppNotification {
let id = pd.id();
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!(@strong sender => move || {
sender.send(Action::RefreshWidgetIfSame(id)).expect("Action channel blew up somehow")
});
let text = i18n("Marked all episodes as listened");
InAppNotification::new(&text, 6000, callback, Some(undo_callback))
}
pub(crate) fn remove_show_notif(pd: Arc<Show>, sender: Sender<Action>) -> InAppNotification {
let text = i18n_f("Unsubscribed from {}", &[pd.title()]);
let res = utils::ignore_show(pd.id());
debug_assert!(res.is_ok());
let sender_ = sender.clone();
let pd_ = pd.clone();
let callback = move |revealer: gtk::Revealer| {
let res = utils::unignore_show(pd_.id());
debug_assert!(res.is_ok());
// Spawn a thread so it won't block the ui.
rayon::spawn(clone!(@strong pd_, @strong sender_ => move || {
delete_show(&pd_)
.map_err(|err| error!("Error: {}", err))
.map_err(|_| error!("Failed to delete {}", pd_.title()))
.ok();
sender_.send(Action::RefreshEpisodesView).expect("Action channel blew up somehow");
}));
revealer.set_reveal_child(false);
glib::Continue(false)
};
let undo_callback = move || {
let res = utils::unignore_show(pd.id());
debug_assert!(res.is_ok());
sender
.send(Action::RefreshShowsView)
.expect("Action channel blew up somehow");
sender
.send(Action::RefreshEpisodesView)
.expect("Action channel blew up somehow");
};
InAppNotification::new(&text, 6000, callback, Some(undo_callback))
}