Merge branch 'master' into state-machines-experiements

This commit is contained in:
Jordan Petridis
2018-03-12 22:10:14 +02:00
21 changed files with 474 additions and 85 deletions
+3
View File
@@ -23,6 +23,9 @@ url = "1.6.0"
failure = "0.1.1"
failure_derive = "0.1.1"
take_mut = "0.2.0"
regex = "0.2.6"
reqwest = "0.8.5"
serde_json = "1.0"
[dependencies.gtk]
features = ["v3_22"]
+101
View File
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.21.0 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkRevealer" id="revealer">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">start</property>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin_left">6</property>
<property name="margin_right">6</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel" id="text">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="label" translatable="yes">An in-app action notification</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="close">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="focus_on_click">False</property>
<property name="receives_default">True</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="relief">none</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">window-close-symbolic</property>
</object>
</child>
<style>
<class name="flat image-button"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="undo">
<property name="label" translatable="yes">Undo</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="halign">center</property>
<property name="valign">center</property>
<style>
<class name="text-button"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack_type">end</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
<style>
<class name="app-notification"/>
</style>
</object>
</child>
<style>
<class name="top"/>
</style>
</object>
</interface>
+31 -2
View File
@@ -207,7 +207,7 @@ Tobias Bernard
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
<property name="position">1</property>
</packing>
</child>
<child>
@@ -227,7 +227,7 @@ Tobias Bernard
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
<property name="position">2</property>
</packing>
</child>
</object>
@@ -266,4 +266,33 @@ Tobias Bernard
</packing>
</child>
</object>
<object class="GtkPopover" id="show_menu">
<property name="can_focus">False</property>
<property name="position">bottom</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">6</property>
<property name="margin_right">6</property>
<property name="margin_top">6</property>
<property name="margin_bottom">6</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkModelButton" id="mark_all_watched">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="text" translatable="yes">Mark all episodes as listened</property>
<property name="centered">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
</child>
</object>
</interface>
+1
View File
@@ -9,6 +9,7 @@
<file preprocess="xml-stripblanks">gtk/shows_view.ui</file>
<file preprocess="xml-stripblanks">gtk/shows_child.ui</file>
<file preprocess="xml-stripblanks">gtk/headerbar.ui</file>
<file preprocess="xml-stripblanks">gtk/inapp_notif.ui</file>
<file compressed="true">gtk/style.css</file>
</gresource>
</gresources>
+30 -6
View File
@@ -8,9 +8,11 @@ use gtk::prelude::*;
use hammond_data::{Podcast, Source};
use hammond_data::utils::checkup;
use appnotif::*;
use headerbar::Header;
use stacks::Content;
use utils;
use widgets::mark_all_watched;
use std::sync::Arc;
use std::sync::mpsc::{channel, Receiver, Sender};
@@ -33,12 +35,14 @@ pub enum Action {
HeaderBarNormal,
HeaderBarShowUpdateIndicator,
HeaderBarHideUpdateIndicator,
MarkAllPlayerNotification(Arc<Podcast>),
}
#[derive(Debug)]
pub struct App {
app_instance: gtk::Application,
window: gtk::Window,
overlay: gtk::Overlay,
header: Arc<Header>,
content: Arc<Content>,
receiver: Receiver<Action>,
@@ -73,12 +77,17 @@ impl App {
// Create the headerbar
let header = Arc::new(Header::new(&content, &window, sender.clone()));
// Add the content main stack to the window.
window.add(&content.get_stack());
// Add the content main stack to the overlay.
let overlay = gtk::Overlay::new();
overlay.add(&content.get_stack());
// Add the overlay to the main window
window.add(&overlay);
App {
app_instance: application,
window,
overlay,
header,
content,
receiver,
@@ -86,7 +95,7 @@ impl App {
}
}
pub fn setup_timed_callbacks(&self) {
fn setup_timed_callbacks(&self) {
let sender = self.sender.clone();
// Update the feeds right after the Application is initialized.
gtk::timeout_add_seconds(2, move || {
@@ -114,15 +123,15 @@ impl App {
pub fn run(self) {
let window = self.window.clone();
let app = self.app_instance.clone();
self.app_instance.connect_startup(move |_| {
build_ui(&window, &app);
self.app_instance.connect_startup(move |app| {
build_ui(&window, app);
});
self.setup_timed_callbacks();
let content = self.content.clone();
let headerbar = self.header.clone();
let sender = self.sender.clone();
let overlay = self.overlay.clone();
let receiver = self.receiver;
gtk::idle_add(move || {
match receiver.recv_timeout(Duration::from_millis(10)) {
@@ -152,6 +161,21 @@ impl App {
Ok(Action::HeaderBarNormal) => headerbar.switch_to_normal(),
Ok(Action::HeaderBarShowUpdateIndicator) => headerbar.show_update_notification(),
Ok(Action::HeaderBarHideUpdateIndicator) => headerbar.hide_update_notification(),
Ok(Action::MarkAllPlayerNotification(pd)) => {
let callback = clone!(sender => move || {
if let Err(err) = mark_all_watched(&pd, sender.clone()) {
error!("Something went horribly wrong with the notif callback: {}", err);
}
glib::Continue(false)
});
let text = "Marked all episodes as listened";
let notif = InAppNotification::new(text.into(), callback, sender.clone());
overlay.add_overlay(&notif.revealer);
// We need to display the notification after the widget is added to the overlay
// so there will be a nice animation.
notif.show();
}
Err(_) => (),
}
+87
View File
@@ -0,0 +1,87 @@
use glib;
use gtk;
use gtk::prelude::*;
use app::Action;
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::mpsc::Sender;
#[derive(Debug, Clone)]
pub struct InAppNotification {
pub revealer: gtk::Revealer,
text: gtk::Label,
undo: gtk::Button,
close: gtk::Button,
}
impl Default for InAppNotification {
fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/inapp_notif.ui");
let revealer: gtk::Revealer = builder.get_object("revealer").unwrap();
let text: gtk::Label = builder.get_object("text").unwrap();
let undo: gtk::Button = builder.get_object("undo").unwrap();
let close: gtk::Button = builder.get_object("close").unwrap();
InAppNotification {
revealer,
text,
undo,
close,
}
}
}
impl InAppNotification {
pub fn new<F>(text: String, mut callback: F, sender: Sender<Action>) -> Self
where
F: FnMut() -> glib::Continue + 'static,
{
let notif = InAppNotification::default();
notif.text.set_text(&text);
let revealer = notif.revealer.clone();
let id = timeout_add_seconds(6, move || {
revealer.set_reveal_child(false);
callback()
});
let id = Rc::new(RefCell::new(Some(id)));
// Cancel the callback
let revealer = notif.revealer.clone();
notif.undo.connect_clicked(move |_| {
let foo = id.borrow_mut().take();
if let Some(id) = foo {
glib::source::source_remove(id);
}
// Hide the notification
revealer.set_reveal_child(false);
// Refresh the widget if visible
if let Err(err) = sender.send(Action::RefreshWidgetIfVis) {
error!(
"Something went horribly wrong with the Action channel: {}",
err
)
}
});
// Hide the revealer when the close button is clicked
let revealer = notif.revealer.clone();
notif.close.connect_clicked(move |_| {
revealer.set_reveal_child(false);
});
notif
}
// This is a seperate method cause in order to get a nice animation
// the revealer should be attached to something that will display it.
// Previouslyi we where doing it in the constructor, which had the result
// of the animation being skipped cause there was no parent widget to display it.
pub fn show(&self) {
self.revealer.set_reveal_child(true);
}
}
+12
View File
@@ -12,6 +12,7 @@ use std::sync::mpsc::Sender;
use app::Action;
use stacks::Content;
use utils::itunes_to_rss;
#[derive(Debug, Clone)]
pub struct Header {
@@ -154,8 +155,18 @@ impl Header {
}
}
// FIXME: THIS ALSO SUCKS!
fn on_add_bttn_clicked(entry: &gtk::Entry, sender: Sender<Action>) -> Result<(), Error> {
let url = entry.get_text().unwrap_or_default();
let url = if url.contains("itunes.com") || url.contains("apple.com") {
info!("Detected itunes url.");
let foo = itunes_to_rss(&url)?;
info!("Resolved to {}", foo);
foo
} else {
url.to_owned()
};
let source = Source::from_url(&url).context("Failed to convert url to a Source entry.")?;
entry.set_text("");
@@ -165,6 +176,7 @@ fn on_add_bttn_clicked(entry: &gtk::Entry, sender: Sender<Action>) -> Result<(),
Ok(())
}
// FIXME: THIS SUCKS!
fn on_url_change(
entry: &gtk::Entry,
result: &gtk::Label,
+4
View File
@@ -26,8 +26,11 @@ extern crate hammond_downloader;
extern crate humansize;
extern crate loggerv;
extern crate open;
extern crate regex;
extern crate reqwest;
extern crate send_cell;
extern crate take_mut;
extern crate serde_json;
extern crate url;
// extern crate rayon;
@@ -67,6 +70,7 @@ pub mod app;
pub mod utils;
pub mod manager;
pub mod static_resource;
pub mod appnotif;
use app::App;
+4 -3
View File
@@ -73,9 +73,8 @@ impl DownloadProgress for Progress {
}
lazy_static! {
pub static ref ACTIVE_DOWNLOADS: Arc<RwLock<HashMap<i32, Arc<Mutex<Progress>>>>> = {
Arc::new(RwLock::new(HashMap::new()))
};
pub static ref ACTIVE_DOWNLOADS: Arc<RwLock<HashMap<i32, Arc<Mutex<Progress>>>>> =
{ Arc::new(RwLock::new(HashMap::new())) };
}
pub fn add(id: i32, directory: &str, sender: Sender<Action>) -> Result<(), Error> {
@@ -175,6 +174,8 @@ mod tests {
}
#[test]
// This test needs access to local system so we ignore it by default.
#[ignore]
fn test_dl_steal_the_stars() {
let url =
"https://web.archive.org/web/20180120104957if_/https://rss.art19.com/steal-the-stars";
+55 -3
View File
@@ -2,7 +2,10 @@
use failure::Error;
use gdk_pixbuf::Pixbuf;
use regex::Regex;
use reqwest;
use send_cell::SendCell;
use serde_json::Value;
// use hammond_data::feed;
use hammond_data::{PodcastCoverQuery, Source};
@@ -74,9 +77,8 @@ fn refresh_feed(source: Option<Vec<Source>>, sender: Sender<Action>) -> Result<(
}
lazy_static! {
static ref CACHED_PIXBUFS: RwLock<HashMap<(i32, u32), Mutex<SendCell<Pixbuf>>>> = {
RwLock::new(HashMap::new())
};
static ref CACHED_PIXBUFS: RwLock<HashMap<(i32, u32), Mutex<SendCell<Pixbuf>>>> =
{ RwLock::new(HashMap::new()) };
}
// Since gdk_pixbuf::Pixbuf is refference counted and every episode,
@@ -107,6 +109,35 @@ pub fn get_pixbuf_from_path(pd: &PodcastCoverQuery, size: u32) -> Result<Pixbuf,
Ok(px)
}
#[inline]
// FIXME: the signature should be `fn foo(s: Url) -> Result<Url, Error>`
pub fn itunes_to_rss(url: &str) -> Result<String, Error> {
let id = itunes_id_from_url(url).ok_or_else(|| format_err!("Failed to find an Itunes ID."))?;
lookup_id(id)
}
#[inline]
fn itunes_id_from_url(url: &str) -> Option<u32> {
lazy_static! {
static ref RE: Regex = Regex::new(r"/id([0-9]+)").unwrap();
}
// Get the itunes id from the url
let foo = RE.captures_iter(url).nth(0)?.get(1)?.as_str();
// Parse it to a u32, this *should* never fail
foo.parse::<u32>().ok()
}
#[inline]
fn lookup_id(id: u32) -> Result<String, Error> {
let url = format!("https://itunes.apple.com/lookup?id={}&entity=podcast", id);
let req: Value = reqwest::get(&url)?.json()?;
// FIXME: First time using serde, this could be done better and avoid using [] for indexing.
let feedurl = req["results"][0]["feedUrl"].as_str();
let feedurl = feedurl.ok_or_else(|| format_err!("Failed to get url from itunes response"))?;
Ok(feedurl.into())
}
#[cfg(test)]
mod tests {
use super::*;
@@ -130,4 +161,25 @@ mod tests {
let pxbuf = get_pixbuf_from_path(&pd.into(), 256);
assert!(pxbuf.is_ok());
}
#[test]
fn test_itunes_to_rss() {
let itunes_url = "https://itunes.apple.com/podcast/id1195206601";
let rss_url = String::from("http://feeds.feedburner.com/InterceptedWithJeremyScahill");
assert_eq!(rss_url, itunes_to_rss(itunes_url).unwrap());
}
#[test]
fn test_itunes_id() {
let id = 1195206601;
let itunes_url = "https://itunes.apple.com/podcast/id1195206601";
assert_eq!(id, itunes_id_from_url(itunes_url).unwrap());
}
#[test]
fn test_itunes_lookup_id() {
let id = 1195206601;
let rss_url = "http://feeds.feedburner.com/InterceptedWithJeremyScahill";
assert_eq!(rss_url, lookup_id(id).unwrap());
}
}
+1
View File
@@ -4,3 +4,4 @@ mod episode_states;
pub use self::episode::EpisodeWidget;
pub use self::show::ShowWidget;
pub use self::show::mark_all_watched;
+57 -3
View File
@@ -1,5 +1,6 @@
use dissolve;
use failure::Error;
// use glib;
use gtk;
use gtk::prelude::*;
use open;
@@ -62,6 +63,8 @@ impl ShowWidget {
}
pub fn init(&self, pd: Arc<Podcast>, sender: Sender<Action>) {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/show_widget.ui");
// Hacky workaround so the pd.id() can be retrieved from the `ShowStack`.
WidgetExt::set_name(&self.container, &pd.id().to_string());
@@ -88,6 +91,19 @@ impl ShowWidget {
error!("Error: {}", err);
}
});
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.clone()
)
}));
self.settings.set_popover(&show_menu);
}
/// Populate the listbox with the shows episodes.
@@ -142,9 +158,47 @@ fn on_unsub_button_clicked(
Ok(())
}
#[allow(dead_code)]
fn on_played_button_clicked(pd: &Podcast, sender: Sender<Action>) -> Result<(), Error> {
fn on_played_button_clicked(pd: Arc<Podcast>, episodes: &gtk::Frame, sender: Sender<Action>) {
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)).unwrap();
}
pub fn mark_all_watched(pd: &Podcast, sender: Sender<Action>) -> Result<(), Error> {
dbqueries::update_none_to_played_now(pd)?;
sender.send(Action::RefreshWidget)?;
sender.send(Action::RefreshWidgetIfVis)?;
sender.send(Action::RefreshEpisodesView)?;
Ok(())
}
// 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::Frame) -> Option<()> {
let listbox = episodes
.get_children()
.remove(0)
.downcast::<gtk::ListBox>()
.ok()?;
let children = listbox.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().map(|c| c.add_class("dim-label"));
}
Some(())
}