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
|
||||
# Exits and builds fails if on bad format
|
||||
rustfmt:
|
||||
image: "rustlang/rust:nightly"
|
||||
image: "registry.gitlab.com/alatiera/rustfmt-oci-image/rustfmt:nightly"
|
||||
stage: lint
|
||||
variables:
|
||||
CFG_RELEASE_CHANNEL: "nightly"
|
||||
script:
|
||||
- rustc --version && cargo --version
|
||||
- cargo install rustfmt-nightly --force
|
||||
- cargo fmt --all -- --write-mode=diff
|
||||
|
||||
# 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]
|
||||
|
||||
* 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
|
||||
|
||||
* 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)",
|
||||
"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)",
|
||||
"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)",
|
||||
"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)",
|
||||
"url 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
@ -30,10 +30,7 @@ extern crate tempdir;
|
||||
|
||||
#[cfg(test)]
|
||||
lazy_static! {
|
||||
static ref TEMPDIR: tempdir::TempDir = {
|
||||
tempdir::TempDir::new("hammond_unit_test").unwrap()
|
||||
};
|
||||
|
||||
static ref TEMPDIR: tempdir::TempDir = { tempdir::TempDir::new("hammond_unit_test").unwrap() };
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
#[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()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
static ref EXPECTED_MINIMAL_INTERCEPTED_2: NewEpisodeMinimal = {
|
||||
NewEpisodeMinimalBuilder::default()
|
||||
.title("Atlas Golfed — U.S.-Backed Think Tanks Target Latin America")
|
||||
@ -325,13 +324,12 @@ mod tests {
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
static ref EXPECTED_INTERCEPTED_1: NewEpisode = {
|
||||
let descr = "NSA whistleblower Edward Snowden discusses the massive Equifax data breach \
|
||||
and allegations of Russian interference in the US election. Commentator \
|
||||
Shaun King explains his call for a boycott of the NFL and talks about his \
|
||||
campaign to bring violent neo-Nazis to justice. Rapper Open Mike Eagle \
|
||||
performs.";
|
||||
let descr = "NSA whistleblower Edward Snowden discusses the massive Equifax data \
|
||||
breach and allegations of Russian interference in the US election. \
|
||||
Commentator Shaun King explains his call for a boycott of the NFL and \
|
||||
talks about his campaign to bring violent neo-Nazis to justice. Rapper \
|
||||
Open Mike Eagle performs.";
|
||||
|
||||
NewEpisodeBuilder::default()
|
||||
.title("The Super Bowl of Racism")
|
||||
@ -347,16 +345,15 @@ mod tests {
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
static ref EXPECTED_INTERCEPTED_2: NewEpisode = {
|
||||
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 \
|
||||
lays out how a network of libertarian think tanks called the Atlas Network \
|
||||
is insidiously shaping political infrastructure in Latin America. We speak \
|
||||
with attorney and former Hugo Chavez adviser Eva Golinger about the \
|
||||
Venezuela\'s political turmoil.And we hear Claudia Lizardo of the \
|
||||
Caracas-based band, La Pequeña Revancha, talk about her music and hopes for \
|
||||
Venezuela.";
|
||||
Blackwater’s 2007 massacre of Iraqi civilians. Intercept reporter Lee \
|
||||
Fang lays out how a network of libertarian think tanks called the Atlas \
|
||||
Network is insidiously shaping political infrastructure in Latin \
|
||||
America. We speak with attorney and former Hugo Chavez adviser Eva \
|
||||
Golinger about the Venezuela\'s political turmoil.And we hear Claudia \
|
||||
Lizardo of the Caracas-based band, La Pequeña Revancha, talk about her \
|
||||
music and hopes for Venezuela.";
|
||||
|
||||
NewEpisodeBuilder::default()
|
||||
.title("Atlas Golfed — U.S.-Backed Think Tanks Target Latin America")
|
||||
@ -372,7 +369,6 @@ mod tests {
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
static ref UPDATED_DURATION_INTERCEPTED_1: NewEpisode = {
|
||||
NewEpisodeBuilder::default()
|
||||
.title("The Super Bowl of Racism")
|
||||
@ -388,7 +384,6 @@ mod tests {
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
static ref EXPECTED_MINIMAL_LUP_1: NewEpisodeMinimal = {
|
||||
NewEpisodeMinimalBuilder::default()
|
||||
.title("Hacking Devices with Kali Linux | LUP 214")
|
||||
@ -402,7 +397,6 @@ mod tests {
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
static ref EXPECTED_MINIMAL_LUP_2: NewEpisodeMinimal = {
|
||||
NewEpisodeMinimalBuilder::default()
|
||||
.title("Gnome Does it Again | LUP 213")
|
||||
@ -416,12 +410,11 @@ mod tests {
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
static ref EXPECTED_LUP_1: NewEpisode = {
|
||||
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 \
|
||||
scope on Equifax blaming open source & the Beard just saved the show. \
|
||||
It’s a really packed episode!";
|
||||
decides to blow off a little steam by attacking his IoT devices, Wes has \
|
||||
the scope on Equifax blaming open source & the Beard just saved the \
|
||||
show. It’s a really packed episode!";
|
||||
|
||||
NewEpisodeBuilder::default()
|
||||
.title("Hacking Devices with Kali Linux | LUP 214")
|
||||
@ -437,13 +430,13 @@ mod tests {
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
static ref EXPECTED_LUP_2: NewEpisode = {
|
||||
let descr = "The Gnome project is about to solve one of our audience's biggest Wayland’s \
|
||||
concerns. But as the project takes on a new level of relevance, decisions for the \
|
||||
next version of Gnome have us worried about the future.\nPlus we chat with Wimpy \
|
||||
about the Ubuntu Rally in NYC, Microsoft’s sneaky move to turn Windows 10 into the \
|
||||
“ULTIMATE LINUX RUNTIME”, community news & more!";
|
||||
let descr = "The Gnome project is about to solve one of our audience's biggest \
|
||||
Wayland’s concerns. But as the project takes on a new level of \
|
||||
relevance, decisions for the next version of Gnome have us worried about \
|
||||
the future.\nPlus we chat with Wimpy about the Ubuntu Rally in NYC, \
|
||||
Microsoft’s sneaky move to turn Windows 10 into the “ULTIMATE LINUX \
|
||||
RUNTIME”, community news & more!";
|
||||
|
||||
NewEpisodeBuilder::default()
|
||||
.title("Gnome Does it Again | LUP 213")
|
||||
|
||||
@ -165,10 +165,11 @@ mod tests {
|
||||
lazy_static! {
|
||||
static ref EXPECTED_INTERCEPTED: NewPodcast = {
|
||||
let descr = "The people behind The Intercept’s fearless reporting and incisive \
|
||||
commentary—Jeremy Scahill, Glenn Greenwald, Betsy Reed and others—discuss \
|
||||
the crucial issues of our time: national security, civil liberties, foreign \
|
||||
policy, and criminal justice. Plus interviews with artists, thinkers, and \
|
||||
newsmakers who challenge our preconceptions about the world we live in.";
|
||||
commentary—Jeremy Scahill, Glenn Greenwald, Betsy Reed and \
|
||||
others—discuss the crucial issues of our time: national security, civil \
|
||||
liberties, foreign policy, and criminal justice. Plus interviews with \
|
||||
artists, thinkers, and newsmakers who challenge our preconceptions about \
|
||||
the world we live in.";
|
||||
|
||||
NewPodcastBuilder::default()
|
||||
.title("Intercepted with Jeremy Scahill")
|
||||
@ -183,11 +184,10 @@ mod tests {
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
static ref EXPECTED_LUP: NewPodcast = {
|
||||
let descr = "An open show powered by community LINUX Unplugged takes the best attributes \
|
||||
of open collaboration and focuses them into a weekly lifestyle show about \
|
||||
Linux.";
|
||||
let descr = "An open show powered by community LINUX Unplugged takes the best \
|
||||
attributes of open collaboration and focuses them into a weekly \
|
||||
lifestyle show about Linux.";
|
||||
|
||||
NewPodcastBuilder::default()
|
||||
.title("LINUX Unplugged Podcast")
|
||||
@ -200,17 +200,16 @@ mod tests {
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
static ref EXPECTED_TIPOFF: NewPodcast = {
|
||||
let desc = "Welcome to The Tip Off- the podcast where we take you behind the scenes of \
|
||||
some of the best investigative journalism from recent years. Each episode \
|
||||
we’ll be digging into an investigative scoop- hearing from the journalists \
|
||||
behind the work as they tell us about the leads, the dead-ends and of course, \
|
||||
the tip offs. There’ll be car chases, slammed doors, terrorist cells, \
|
||||
meetings in dimly lit bars and cafes, wrangling with despotic regimes and \
|
||||
much more. So if you’re curious about the fun, complicated detective work \
|
||||
that goes into doing great investigative journalism- then this is the podcast \
|
||||
for you.";
|
||||
let desc = "Welcome to The Tip Off- the podcast where we take you behind the scenes \
|
||||
of some of the best investigative journalism from recent years. Each \
|
||||
episode we’ll be digging into an investigative scoop- hearing from the \
|
||||
journalists behind the work as they tell us about the leads, the \
|
||||
dead-ends and of course, the tip offs. There’ll be car chases, slammed \
|
||||
doors, terrorist cells, meetings in dimly lit bars and cafes, wrangling \
|
||||
with despotic regimes and much more. So if you’re curious about the fun, \
|
||||
complicated detective work that goes into doing great investigative \
|
||||
journalism- then this is the podcast for you.";
|
||||
|
||||
NewPodcastBuilder::default()
|
||||
.title("The Tip Off")
|
||||
@ -223,15 +222,13 @@ mod tests {
|
||||
.source_id(42)
|
||||
.build()
|
||||
.unwrap()
|
||||
|
||||
};
|
||||
|
||||
static ref EXPECTED_STARS: NewPodcast = {
|
||||
let descr = "<p>The first audio drama from Tor Labs and Gideon Media, Steal the Stars is \
|
||||
a gripping noir science fiction thriller in 14 episodes: Forbidden love, a \
|
||||
crashed UFO, an alien body, and an impossible heist unlike any ever \
|
||||
attempted - scripted by Mac Rogers, the award-winning playwright and writer \
|
||||
of the multi-million download The Message and LifeAfter.</p>";
|
||||
let descr = "<p>The first audio drama from Tor Labs and Gideon Media, Steal the Stars \
|
||||
is a gripping noir science fiction thriller in 14 episodes: Forbidden \
|
||||
love, a crashed UFO, an alien body, and an impossible heist unlike any \
|
||||
ever attempted - scripted by Mac Rogers, the award-winning playwright \
|
||||
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-\
|
||||
b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e\
|
||||
923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg";
|
||||
@ -245,11 +242,10 @@ mod tests {
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
static ref EXPECTED_CODE: NewPodcast = {
|
||||
let descr = "A podcast about humans and technology. Panelists: Coraline Ada Ehmke, David \
|
||||
Brady, Jessica Kerr, Jay Bobo, Astrid Countee and Sam Livingston-Gray. \
|
||||
Brought to you by @therubyrep.";
|
||||
let descr = "A podcast about humans and technology. Panelists: Coraline Ada Ehmke, \
|
||||
David Brady, Jessica Kerr, Jay Bobo, Astrid Countee and Sam \
|
||||
Livingston-Gray. Brought to you by @therubyrep.";
|
||||
|
||||
NewPodcastBuilder::default()
|
||||
.title("Greater Than Code")
|
||||
@ -262,7 +258,6 @@ mod tests {
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
static ref UPDATED_DESC_INTERCEPTED: NewPodcast = {
|
||||
NewPodcastBuilder::default()
|
||||
.title("Intercepted with Jeremy Scahill")
|
||||
|
||||
@ -318,6 +318,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
// This test needs access to local system so we ignore it by default.
|
||||
#[ignore]
|
||||
fn test_get_dl_folder() {
|
||||
let foo_ = format!("{}/{}", DL_DIR.to_str().unwrap(), "foo");
|
||||
assert_eq!(get_download_folder("foo").unwrap(), foo_);
|
||||
|
||||
@ -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
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>
|
||||
<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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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(¬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(_) => (),
|
||||
}
|
||||
|
||||
|
||||
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 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: >k::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: >k::Entry, sender: Sender<Action>) -> Result<(),
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// FIXME: THIS SUCKS!
|
||||
fn on_url_change(
|
||||
entry: >k::Entry,
|
||||
result: >k::Label,
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,3 +4,4 @@ mod episode_states;
|
||||
|
||||
pub use self::episode::EpisodeWidget;
|
||||
pub use self::show::ShowWidget;
|
||||
pub use self::show::mark_all_watched;
|
||||
|
||||
@ -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: >k::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: >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) ",
|
||||
"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=ipc",
|
||||
"--socket=x11",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user