Merge branch 'master' into state-machines-experiements

This commit is contained in:
Jordan Petridis 2018-03-12 22:10:14 +02:00
commit 1bdd2f2f5b
No known key found for this signature in database
GPG Key ID: CEABAD9F5683B9A6
21 changed files with 474 additions and 85 deletions

View File

@ -39,13 +39,10 @@ stable:test:
# Configure and run rustfmt on nightly # Configure and run rustfmt on nightly
# Exits and builds fails if on bad format # Exits and builds fails if on bad format
rustfmt: rustfmt:
image: "rustlang/rust:nightly" image: "registry.gitlab.com/alatiera/rustfmt-oci-image/rustfmt:nightly"
stage: lint stage: lint
variables:
CFG_RELEASE_CHANNEL: "nightly"
script: script:
- rustc --version && cargo --version - rustc --version && cargo --version
- cargo install rustfmt-nightly --force
- cargo fmt --all -- --write-mode=diff - cargo fmt --all -- --write-mode=diff
# Configure and run clippy on nightly # Configure and run clippy on nightly

View File

@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## [Unreleased] ## [Unreleased]
* Ability to mark all episodes of a Show as watched. [#47](https://gitlab.gnome.org/alatiera/Hammond/issues/47)
* Now you are able to subscribe to itunes™ podcasts by using the itunes link of the show.[#49](https://gitlab.gnome.org/alatiera/Hammond/issues/49)
## [0.3.0] - 2018-02-11 ## [0.3.0] - 2018-02-11
* Tobias Bernard Redesigned the whole Gtk+ client. * Tobias Bernard Redesigned the whole Gtk+ client.

3
Cargo.lock generated
View File

@ -707,7 +707,10 @@ dependencies = [
"loggerv 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "loggerv 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"open 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "open 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"rayon 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "rayon 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest 0.8.5 (registry+https://github.com/rust-lang/crates.io-index)",
"send-cell 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "send-cell 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
"take_mut 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "take_mut 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
] ]

View File

@ -16,7 +16,7 @@ type Pool = r2d2::Pool<ConnectionManager<SqliteConnection>>;
embed_migrations!("migrations/"); embed_migrations!("migrations/");
lazy_static!{ lazy_static! {
static ref POOL: Pool = init_pool(DB_PATH.to_str().unwrap()); static ref POOL: Pool = init_pool(DB_PATH.to_str().unwrap());
} }
@ -30,10 +30,7 @@ extern crate tempdir;
#[cfg(test)] #[cfg(test)]
lazy_static! { lazy_static! {
static ref TEMPDIR: tempdir::TempDir = { static ref TEMPDIR: tempdir::TempDir = { tempdir::TempDir::new("hammond_unit_test").unwrap() };
tempdir::TempDir::new("hammond_unit_test").unwrap()
};
static ref DB_PATH: PathBuf = TEMPDIR.path().join("hammond.db"); static ref DB_PATH: PathBuf = TEMPDIR.path().join("hammond.db");
} }

View File

@ -348,3 +348,29 @@ pub fn update_none_to_played_now(parent: &Podcast) -> Result<usize, DataError> {
.map_err(From::from) .map_err(From::from)
}) })
} }
#[cfg(test)]
mod tests {
use super::*;
use database::*;
use pipeline::*;
#[test]
fn test_update_none_to_played_now() {
truncate_db().unwrap();
let url = "https://web.archive.org/web/20180120083840if_/https://feeds.feedburner.\
com/InterceptedWithJeremyScahill";
let source = Source::from_url(url).unwrap();
let id = source.id();
index_single_source(source, true).unwrap();
let pd = get_podcast_from_source_id(id).unwrap();
let eps_num = get_pd_unplayed_episodes(&pd).unwrap().len();
assert_ne!(eps_num, 0);
update_none_to_played_now(&pd).unwrap();
let eps_num2 = get_pd_unplayed_episodes(&pd).unwrap().len();
assert_eq!(eps_num2, 0);
}
}

View File

@ -311,7 +311,6 @@ mod tests {
.build() .build()
.unwrap() .unwrap()
}; };
static ref EXPECTED_MINIMAL_INTERCEPTED_2: NewEpisodeMinimal = { static ref EXPECTED_MINIMAL_INTERCEPTED_2: NewEpisodeMinimal = {
NewEpisodeMinimalBuilder::default() NewEpisodeMinimalBuilder::default()
.title("Atlas Golfed — U.S.-Backed Think Tanks Target Latin America") .title("Atlas Golfed — U.S.-Backed Think Tanks Target Latin America")
@ -325,13 +324,12 @@ mod tests {
.build() .build()
.unwrap() .unwrap()
}; };
static ref EXPECTED_INTERCEPTED_1: NewEpisode = { static ref EXPECTED_INTERCEPTED_1: NewEpisode = {
let descr = "NSA whistleblower Edward Snowden discusses the massive Equifax data breach \ let descr = "NSA whistleblower Edward Snowden discusses the massive Equifax data \
and allegations of Russian interference in the US election. Commentator \ breach and allegations of Russian interference in the US election. \
Shaun King explains his call for a boycott of the NFL and talks about his \ Commentator Shaun King explains his call for a boycott of the NFL and \
campaign to bring violent neo-Nazis to justice. Rapper Open Mike Eagle \ talks about his campaign to bring violent neo-Nazis to justice. Rapper \
performs."; Open Mike Eagle performs.";
NewEpisodeBuilder::default() NewEpisodeBuilder::default()
.title("The Super Bowl of Racism") .title("The Super Bowl of Racism")
@ -347,16 +345,15 @@ mod tests {
.build() .build()
.unwrap() .unwrap()
}; };
static ref EXPECTED_INTERCEPTED_2: NewEpisode = { static ref EXPECTED_INTERCEPTED_2: NewEpisode = {
let descr = "This week on Intercepted: Jeremy gives an update on the aftermath of \ let descr = "This week on Intercepted: Jeremy gives an update on the aftermath of \
Blackwaters 2007 massacre of Iraqi civilians. Intercept reporter Lee Fang \ Blackwaters 2007 massacre of Iraqi civilians. Intercept reporter Lee \
lays out how a network of libertarian think tanks called the Atlas Network \ Fang lays out how a network of libertarian think tanks called the Atlas \
is insidiously shaping political infrastructure in Latin America. We speak \ Network is insidiously shaping political infrastructure in Latin \
with attorney and former Hugo Chavez adviser Eva Golinger about the \ America. We speak with attorney and former Hugo Chavez adviser Eva \
Venezuela\'s political turmoil.And we hear Claudia Lizardo of the \ Golinger about the Venezuela\'s political turmoil.And we hear Claudia \
Caracas-based band, La Pequeña Revancha, talk about her music and hopes for \ Lizardo of the Caracas-based band, La Pequeña Revancha, talk about her \
Venezuela."; music and hopes for Venezuela.";
NewEpisodeBuilder::default() NewEpisodeBuilder::default()
.title("Atlas Golfed — U.S.-Backed Think Tanks Target Latin America") .title("Atlas Golfed — U.S.-Backed Think Tanks Target Latin America")
@ -372,7 +369,6 @@ mod tests {
.build() .build()
.unwrap() .unwrap()
}; };
static ref UPDATED_DURATION_INTERCEPTED_1: NewEpisode = { static ref UPDATED_DURATION_INTERCEPTED_1: NewEpisode = {
NewEpisodeBuilder::default() NewEpisodeBuilder::default()
.title("The Super Bowl of Racism") .title("The Super Bowl of Racism")
@ -388,7 +384,6 @@ mod tests {
.build() .build()
.unwrap() .unwrap()
}; };
static ref EXPECTED_MINIMAL_LUP_1: NewEpisodeMinimal = { static ref EXPECTED_MINIMAL_LUP_1: NewEpisodeMinimal = {
NewEpisodeMinimalBuilder::default() NewEpisodeMinimalBuilder::default()
.title("Hacking Devices with Kali Linux | LUP 214") .title("Hacking Devices with Kali Linux | LUP 214")
@ -402,7 +397,6 @@ mod tests {
.build() .build()
.unwrap() .unwrap()
}; };
static ref EXPECTED_MINIMAL_LUP_2: NewEpisodeMinimal = { static ref EXPECTED_MINIMAL_LUP_2: NewEpisodeMinimal = {
NewEpisodeMinimalBuilder::default() NewEpisodeMinimalBuilder::default()
.title("Gnome Does it Again | LUP 213") .title("Gnome Does it Again | LUP 213")
@ -416,12 +410,11 @@ mod tests {
.build() .build()
.unwrap() .unwrap()
}; };
static ref EXPECTED_LUP_1: NewEpisode = { static ref EXPECTED_LUP_1: NewEpisode = {
let descr = "Audit your network with a couple of easy commands on Kali Linux. Chris \ let descr = "Audit your network with a couple of easy commands on Kali Linux. Chris \
decides to blow off a little steam by attacking his IoT devices, Wes has the \ decides to blow off a little steam by attacking his IoT devices, Wes has \
scope on Equifax blaming open source &amp; the Beard just saved the show. \ the scope on Equifax blaming open source &amp; the Beard just saved the \
Its a really packed episode!"; show. Its a really packed episode!";
NewEpisodeBuilder::default() NewEpisodeBuilder::default()
.title("Hacking Devices with Kali Linux | LUP 214") .title("Hacking Devices with Kali Linux | LUP 214")
@ -437,13 +430,13 @@ mod tests {
.build() .build()
.unwrap() .unwrap()
}; };
static ref EXPECTED_LUP_2: NewEpisode = { static ref EXPECTED_LUP_2: NewEpisode = {
let descr = "The Gnome project is about to solve one of our audience's biggest Waylands \ let descr = "The Gnome project is about to solve one of our audience's biggest \
concerns. But as the project takes on a new level of relevance, decisions for the \ Waylands concerns. But as the project takes on a new level of \
next version of Gnome have us worried about the future.\nPlus we chat with Wimpy \ relevance, decisions for the next version of Gnome have us worried about \
about the Ubuntu Rally in NYC, Microsofts sneaky move to turn Windows 10 into the \ the future.\nPlus we chat with Wimpy about the Ubuntu Rally in NYC, \
ULTIMATE LINUX RUNTIME, community news &amp; more!"; Microsofts sneaky move to turn Windows 10 into the ULTIMATE LINUX \
RUNTIME, community news &amp; more!";
NewEpisodeBuilder::default() NewEpisodeBuilder::default()
.title("Gnome Does it Again | LUP 213") .title("Gnome Does it Again | LUP 213")

View File

@ -162,13 +162,14 @@ mod tests {
use std::io::BufReader; use std::io::BufReader;
// Pre-built expected NewPodcast structs. // Pre-built expected NewPodcast structs.
lazy_static!{ lazy_static! {
static ref EXPECTED_INTERCEPTED: NewPodcast = { static ref EXPECTED_INTERCEPTED: NewPodcast = {
let descr = "The people behind The Intercepts fearless reporting and incisive \ let descr = "The people behind The Intercepts fearless reporting and incisive \
commentaryJeremy Scahill, Glenn Greenwald, Betsy Reed and othersdiscuss \ commentaryJeremy Scahill, Glenn Greenwald, Betsy Reed and \
the crucial issues of our time: national security, civil liberties, foreign \ othersdiscuss the crucial issues of our time: national security, civil \
policy, and criminal justice. Plus interviews with artists, thinkers, and \ liberties, foreign policy, and criminal justice. Plus interviews with \
newsmakers who challenge our preconceptions about the world we live in."; artists, thinkers, and newsmakers who challenge our preconceptions about \
the world we live in.";
NewPodcastBuilder::default() NewPodcastBuilder::default()
.title("Intercepted with Jeremy Scahill") .title("Intercepted with Jeremy Scahill")
@ -183,11 +184,10 @@ mod tests {
.build() .build()
.unwrap() .unwrap()
}; };
static ref EXPECTED_LUP: NewPodcast = { static ref EXPECTED_LUP: NewPodcast = {
let descr = "An open show powered by community LINUX Unplugged takes the best attributes \ let descr = "An open show powered by community LINUX Unplugged takes the best \
of open collaboration and focuses them into a weekly lifestyle show about \ attributes of open collaboration and focuses them into a weekly \
Linux."; lifestyle show about Linux.";
NewPodcastBuilder::default() NewPodcastBuilder::default()
.title("LINUX Unplugged Podcast") .title("LINUX Unplugged Podcast")
@ -200,17 +200,16 @@ mod tests {
.build() .build()
.unwrap() .unwrap()
}; };
static ref EXPECTED_TIPOFF: NewPodcast = { static ref EXPECTED_TIPOFF: NewPodcast = {
let desc = "Welcome to The Tip Off- the podcast where we take you behind the scenes of \ let desc = "Welcome to The Tip Off- the podcast where we take you behind the scenes \
some of the best investigative journalism from recent years. Each episode \ of some of the best investigative journalism from recent years. Each \
well be digging into an investigative scoop- hearing from the journalists \ episode well be digging into an investigative scoop- hearing from the \
behind the work as they tell us about the leads, the dead-ends and of course, \ journalists behind the work as they tell us about the leads, the \
the tip offs. Therell be car chases, slammed doors, terrorist cells, \ dead-ends and of course, the tip offs. Therell be car chases, slammed \
meetings in dimly lit bars and cafes, wrangling with despotic regimes and \ doors, terrorist cells, meetings in dimly lit bars and cafes, wrangling \
much more. So if youre curious about the fun, complicated detective work \ with despotic regimes and much more. So if youre curious about the fun, \
that goes into doing great investigative journalism- then this is the podcast \ complicated detective work that goes into doing great investigative \
for you."; journalism- then this is the podcast for you.";
NewPodcastBuilder::default() NewPodcastBuilder::default()
.title("The Tip Off") .title("The Tip Off")
@ -223,15 +222,13 @@ mod tests {
.source_id(42) .source_id(42)
.build() .build()
.unwrap() .unwrap()
}; };
static ref EXPECTED_STARS: NewPodcast = { static ref EXPECTED_STARS: NewPodcast = {
let descr = "<p>The first audio drama from Tor Labs and Gideon Media, Steal the Stars is \ let descr = "<p>The first audio drama from Tor Labs and Gideon Media, Steal the Stars \
a gripping noir science fiction thriller in 14 episodes: Forbidden love, a \ is a gripping noir science fiction thriller in 14 episodes: Forbidden \
crashed UFO, an alien body, and an impossible heist unlike any ever \ love, a crashed UFO, an alien body, and an impossible heist unlike any \
attempted - scripted by Mac Rogers, the award-winning playwright and writer \ ever attempted - scripted by Mac Rogers, the award-winning playwright \
of the multi-million download The Message and LifeAfter.</p>"; and writer of the multi-million download The Message and LifeAfter.</p>";
let img = "https://dfkfj8j276wwv.cloudfront.net/images/2c/5f/a0/1a/2c5fa01a-ae78-4a8c-\ let img = "https://dfkfj8j276wwv.cloudfront.net/images/2c/5f/a0/1a/2c5fa01a-ae78-4a8c-\
b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e\ b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e\
923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg"; 923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg";
@ -245,11 +242,10 @@ mod tests {
.build() .build()
.unwrap() .unwrap()
}; };
static ref EXPECTED_CODE: NewPodcast = { static ref EXPECTED_CODE: NewPodcast = {
let descr = "A podcast about humans and technology. Panelists: Coraline Ada Ehmke, David \ let descr = "A podcast about humans and technology. Panelists: Coraline Ada Ehmke, \
Brady, Jessica Kerr, Jay Bobo, Astrid Countee and Sam Livingston-Gray. \ David Brady, Jessica Kerr, Jay Bobo, Astrid Countee and Sam \
Brought to you by @therubyrep."; Livingston-Gray. Brought to you by @therubyrep.";
NewPodcastBuilder::default() NewPodcastBuilder::default()
.title("Greater Than Code") .title("Greater Than Code")
@ -262,7 +258,6 @@ mod tests {
.build() .build()
.unwrap() .unwrap()
}; };
static ref UPDATED_DESC_INTERCEPTED: NewPodcast = { static ref UPDATED_DESC_INTERCEPTED: NewPodcast = {
NewPodcastBuilder::default() NewPodcastBuilder::default()
.title("Intercepted with Jeremy Scahill") .title("Intercepted with Jeremy Scahill")

View File

@ -318,6 +318,8 @@ mod tests {
} }
#[test] #[test]
// This test needs access to local system so we ignore it by default.
#[ignore]
fn test_get_dl_folder() { fn test_get_dl_folder() {
let foo_ = format!("{}/{}", DL_DIR.to_str().unwrap(), "foo"); let foo_ = format!("{}/{}", DL_DIR.to_str().unwrap(), "foo");
assert_eq!(get_download_folder("foo").unwrap(), foo_); assert_eq!(get_download_folder("foo").unwrap(), foo_);

View File

@ -23,6 +23,9 @@ url = "1.6.0"
failure = "0.1.1" failure = "0.1.1"
failure_derive = "0.1.1" failure_derive = "0.1.1"
take_mut = "0.2.0" take_mut = "0.2.0"
regex = "0.2.6"
reqwest = "0.8.5"
serde_json = "1.0"
[dependencies.gtk] [dependencies.gtk]
features = ["v3_22"] features = ["v3_22"]

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>

View File

@ -207,7 +207,7 @@ Tobias Bernard
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">False</property> <property name="fill">False</property>
<property name="position">0</property> <property name="position">1</property>
</packing> </packing>
</child> </child>
<child> <child>
@ -227,7 +227,7 @@ Tobias Bernard
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">False</property> <property name="fill">False</property>
<property name="position">1</property> <property name="position">2</property>
</packing> </packing>
</child> </child>
</object> </object>
@ -266,4 +266,33 @@ Tobias Bernard
</packing> </packing>
</child> </child>
</object> </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> </interface>

View File

@ -9,6 +9,7 @@
<file preprocess="xml-stripblanks">gtk/shows_view.ui</file> <file preprocess="xml-stripblanks">gtk/shows_view.ui</file>
<file preprocess="xml-stripblanks">gtk/shows_child.ui</file> <file preprocess="xml-stripblanks">gtk/shows_child.ui</file>
<file preprocess="xml-stripblanks">gtk/headerbar.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> <file compressed="true">gtk/style.css</file>
</gresource> </gresource>
</gresources> </gresources>

View File

@ -8,9 +8,11 @@ use gtk::prelude::*;
use hammond_data::{Podcast, Source}; use hammond_data::{Podcast, Source};
use hammond_data::utils::checkup; use hammond_data::utils::checkup;
use appnotif::*;
use headerbar::Header; use headerbar::Header;
use stacks::Content; use stacks::Content;
use utils; use utils;
use widgets::mark_all_watched;
use std::sync::Arc; use std::sync::Arc;
use std::sync::mpsc::{channel, Receiver, Sender}; use std::sync::mpsc::{channel, Receiver, Sender};
@ -33,12 +35,14 @@ pub enum Action {
HeaderBarNormal, HeaderBarNormal,
HeaderBarShowUpdateIndicator, HeaderBarShowUpdateIndicator,
HeaderBarHideUpdateIndicator, HeaderBarHideUpdateIndicator,
MarkAllPlayerNotification(Arc<Podcast>),
} }
#[derive(Debug)] #[derive(Debug)]
pub struct App { pub struct App {
app_instance: gtk::Application, app_instance: gtk::Application,
window: gtk::Window, window: gtk::Window,
overlay: gtk::Overlay,
header: Arc<Header>, header: Arc<Header>,
content: Arc<Content>, content: Arc<Content>,
receiver: Receiver<Action>, receiver: Receiver<Action>,
@ -73,12 +77,17 @@ impl App {
// Create the headerbar // Create the headerbar
let header = Arc::new(Header::new(&content, &window, sender.clone())); let header = Arc::new(Header::new(&content, &window, sender.clone()));
// Add the content main stack to the window. // Add the content main stack to the overlay.
window.add(&content.get_stack()); let overlay = gtk::Overlay::new();
overlay.add(&content.get_stack());
// Add the overlay to the main window
window.add(&overlay);
App { App {
app_instance: application, app_instance: application,
window, window,
overlay,
header, header,
content, content,
receiver, receiver,
@ -86,7 +95,7 @@ impl App {
} }
} }
pub fn setup_timed_callbacks(&self) { fn setup_timed_callbacks(&self) {
let sender = self.sender.clone(); let sender = self.sender.clone();
// Update the feeds right after the Application is initialized. // Update the feeds right after the Application is initialized.
gtk::timeout_add_seconds(2, move || { gtk::timeout_add_seconds(2, move || {
@ -114,15 +123,15 @@ impl App {
pub fn run(self) { pub fn run(self) {
let window = self.window.clone(); let window = self.window.clone();
let app = self.app_instance.clone(); self.app_instance.connect_startup(move |app| {
self.app_instance.connect_startup(move |_| { build_ui(&window, app);
build_ui(&window, &app);
}); });
self.setup_timed_callbacks(); self.setup_timed_callbacks();
let content = self.content.clone(); let content = self.content.clone();
let headerbar = self.header.clone(); let headerbar = self.header.clone();
let sender = self.sender.clone(); let sender = self.sender.clone();
let overlay = self.overlay.clone();
let receiver = self.receiver; let receiver = self.receiver;
gtk::idle_add(move || { gtk::idle_add(move || {
match receiver.recv_timeout(Duration::from_millis(10)) { match receiver.recv_timeout(Duration::from_millis(10)) {
@ -152,6 +161,21 @@ impl App {
Ok(Action::HeaderBarNormal) => headerbar.switch_to_normal(), Ok(Action::HeaderBarNormal) => headerbar.switch_to_normal(),
Ok(Action::HeaderBarShowUpdateIndicator) => headerbar.show_update_notification(), Ok(Action::HeaderBarShowUpdateIndicator) => headerbar.show_update_notification(),
Ok(Action::HeaderBarHideUpdateIndicator) => headerbar.hide_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(_) => (), Err(_) => (),
} }

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);
}
}

View File

@ -12,6 +12,7 @@ use std::sync::mpsc::Sender;
use app::Action; use app::Action;
use stacks::Content; use stacks::Content;
use utils::itunes_to_rss;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Header { 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> { fn on_add_bttn_clicked(entry: &gtk::Entry, sender: Sender<Action>) -> Result<(), Error> {
let url = entry.get_text().unwrap_or_default(); 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.")?; let source = Source::from_url(&url).context("Failed to convert url to a Source entry.")?;
entry.set_text(""); entry.set_text("");
@ -165,6 +176,7 @@ fn on_add_bttn_clicked(entry: &gtk::Entry, sender: Sender<Action>) -> Result<(),
Ok(()) Ok(())
} }
// FIXME: THIS SUCKS!
fn on_url_change( fn on_url_change(
entry: &gtk::Entry, entry: &gtk::Entry,
result: &gtk::Label, result: &gtk::Label,

View File

@ -26,8 +26,11 @@ extern crate hammond_downloader;
extern crate humansize; extern crate humansize;
extern crate loggerv; extern crate loggerv;
extern crate open; extern crate open;
extern crate regex;
extern crate reqwest;
extern crate send_cell; extern crate send_cell;
extern crate take_mut; extern crate take_mut;
extern crate serde_json;
extern crate url; extern crate url;
// extern crate rayon; // extern crate rayon;
@ -67,6 +70,7 @@ pub mod app;
pub mod utils; pub mod utils;
pub mod manager; pub mod manager;
pub mod static_resource; pub mod static_resource;
pub mod appnotif;
use app::App; use app::App;

View File

@ -73,9 +73,8 @@ impl DownloadProgress for Progress {
} }
lazy_static! { lazy_static! {
pub static ref ACTIVE_DOWNLOADS: Arc<RwLock<HashMap<i32, Arc<Mutex<Progress>>>>> = { pub static ref ACTIVE_DOWNLOADS: Arc<RwLock<HashMap<i32, Arc<Mutex<Progress>>>>> =
Arc::new(RwLock::new(HashMap::new())) { Arc::new(RwLock::new(HashMap::new())) };
};
} }
pub fn add(id: i32, directory: &str, sender: Sender<Action>) -> Result<(), Error> { pub fn add(id: i32, directory: &str, sender: Sender<Action>) -> Result<(), Error> {
@ -175,6 +174,8 @@ mod tests {
} }
#[test] #[test]
// This test needs access to local system so we ignore it by default.
#[ignore]
fn test_dl_steal_the_stars() { fn test_dl_steal_the_stars() {
let url = let url =
"https://web.archive.org/web/20180120104957if_/https://rss.art19.com/steal-the-stars"; "https://web.archive.org/web/20180120104957if_/https://rss.art19.com/steal-the-stars";

View File

@ -2,7 +2,10 @@
use failure::Error; use failure::Error;
use gdk_pixbuf::Pixbuf; use gdk_pixbuf::Pixbuf;
use regex::Regex;
use reqwest;
use send_cell::SendCell; use send_cell::SendCell;
use serde_json::Value;
// use hammond_data::feed; // use hammond_data::feed;
use hammond_data::{PodcastCoverQuery, Source}; use hammond_data::{PodcastCoverQuery, Source};
@ -74,9 +77,8 @@ fn refresh_feed(source: Option<Vec<Source>>, sender: Sender<Action>) -> Result<(
} }
lazy_static! { lazy_static! {
static ref CACHED_PIXBUFS: RwLock<HashMap<(i32, u32), Mutex<SendCell<Pixbuf>>>> = { static ref CACHED_PIXBUFS: RwLock<HashMap<(i32, u32), Mutex<SendCell<Pixbuf>>>> =
RwLock::new(HashMap::new()) { RwLock::new(HashMap::new()) };
};
} }
// Since gdk_pixbuf::Pixbuf is refference counted and every episode, // 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) 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -130,4 +161,25 @@ mod tests {
let pxbuf = get_pixbuf_from_path(&pd.into(), 256); let pxbuf = get_pixbuf_from_path(&pd.into(), 256);
assert!(pxbuf.is_ok()); 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());
}
} }

View File

@ -4,3 +4,4 @@ mod episode_states;
pub use self::episode::EpisodeWidget; pub use self::episode::EpisodeWidget;
pub use self::show::ShowWidget; pub use self::show::ShowWidget;
pub use self::show::mark_all_watched;

View File

@ -1,5 +1,6 @@
use dissolve; use dissolve;
use failure::Error; use failure::Error;
// use glib;
use gtk; use gtk;
use gtk::prelude::*; use gtk::prelude::*;
use open; use open;
@ -62,6 +63,8 @@ impl ShowWidget {
} }
pub fn init(&self, pd: Arc<Podcast>, sender: Sender<Action>) { 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`. // Hacky workaround so the pd.id() can be retrieved from the `ShowStack`.
WidgetExt::set_name(&self.container, &pd.id().to_string()); WidgetExt::set_name(&self.container, &pd.id().to_string());
@ -88,6 +91,19 @@ impl ShowWidget {
error!("Error: {}", err); 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. /// Populate the listbox with the shows episodes.
@ -142,9 +158,47 @@ fn on_unsub_button_clicked(
Ok(()) Ok(())
} }
#[allow(dead_code)] fn on_played_button_clicked(pd: Arc<Podcast>, episodes: &gtk::Frame, sender: Sender<Action>) {
fn on_played_button_clicked(pd: &Podcast, sender: Sender<Action>) -> Result<(), Error> { 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)?; dbqueries::update_none_to_played_now(pd)?;
sender.send(Action::RefreshWidget)?; sender.send(Action::RefreshWidgetIfVis)?;
sender.send(Action::RefreshEpisodesView)?;
Ok(()) 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(())
}

View File

@ -12,6 +12,10 @@
], ],
"desktop-file-name-prefix" : "(Nightly) ", "desktop-file-name-prefix" : "(Nightly) ",
"finish-args" : [ "finish-args" : [
"--filesystem=xdg-run/dconf",
"--filesystem=~/.config/dconf:ro",
"--talk-name=ca.desrt.dconf",
"--env=DCONF_USER_CONFIG_DIR=.config/dconf",
"--share=network", "--share=network",
"--share=ipc", "--share=ipc",
"--socket=x11", "--socket=x11",