Merge branch 'master' into state-machines-experiements
This commit is contained in:
commit
1bdd2f2f5b
@ -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
|
||||||
|
|||||||
@ -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
3
Cargo.lock
generated
@ -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)",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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 \
|
||||||
Blackwater’s 2007 massacre of Iraqi civilians. Intercept reporter Lee Fang \
|
Blackwater’s 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 & the Beard just saved the show. \
|
the scope on Equifax blaming open source & the Beard just saved the \
|
||||||
It’s a really packed episode!";
|
show. It’s 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 Wayland’s \
|
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 \
|
Wayland’s 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, Microsoft’s 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 & more!";
|
Microsoft’s sneaky move to turn Windows 10 into the “ULTIMATE LINUX \
|
||||||
|
RUNTIME”, community news & more!";
|
||||||
|
|
||||||
NewEpisodeBuilder::default()
|
NewEpisodeBuilder::default()
|
||||||
.title("Gnome Does it Again | LUP 213")
|
.title("Gnome Does it Again | LUP 213")
|
||||||
|
|||||||
@ -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 Intercept’s fearless reporting and incisive \
|
let descr = "The people behind The Intercept’s fearless reporting and incisive \
|
||||||
commentary—Jeremy Scahill, Glenn Greenwald, Betsy Reed and others—discuss \
|
commentary—Jeremy Scahill, Glenn Greenwald, Betsy Reed and \
|
||||||
the crucial issues of our time: national security, civil liberties, foreign \
|
others—discuss 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 \
|
||||||
we’ll be digging into an investigative scoop- hearing from the journalists \
|
episode we’ll 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. There’ll be car chases, slammed doors, terrorist cells, \
|
dead-ends and of course, the tip offs. There’ll 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 you’re curious about the fun, complicated detective work \
|
with despotic regimes and much more. So if you’re 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")
|
||||||
|
|||||||
@ -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_);
|
||||||
|
|||||||
@ -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"]
|
||||||
|
|||||||
101
hammond-gtk/resources/gtk/inapp_notif.ui
Normal file
101
hammond-gtk/resources/gtk/inapp_notif.ui
Normal 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>
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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(¬if.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(_) => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
87
hammond-gtk/src/appnotif.rs
Normal file
87
hammond-gtk/src/appnotif.rs
Normal 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,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: >k::Entry, sender: Sender<Action>) -> Result<(), Error> {
|
fn on_add_bttn_clicked(entry: >k::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: >k::Entry, sender: Sender<Action>) -> Result<(),
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: THIS SUCKS!
|
||||||
fn on_url_change(
|
fn on_url_change(
|
||||||
entry: >k::Entry,
|
entry: >k::Entry,
|
||||||
result: >k::Label,
|
result: >k::Label,
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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: >k::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: >k::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(())
|
||||||
|
}
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user