Merge branch '27-episodesview' into 'master'
Resolve "EpisodesView" Closes #27, #21, #22, #19, and #18 See merge request alatiera/Hammond!8
This commit is contained in:
commit
ef705f9a2a
@ -18,12 +18,19 @@ before_script:
|
|||||||
stage: test
|
stage: test
|
||||||
script:
|
script:
|
||||||
- rustc --version && cargo --version
|
- rustc --version && cargo --version
|
||||||
|
# Force regeneration of gresources regardless of artifacts chage
|
||||||
|
- cd hammond-gtk/resources/ && glib-compile-resources --generate resources.xml && cd ../../
|
||||||
- cargo build
|
- cargo build
|
||||||
- cargo test --verbose -- --test-threads=1
|
- cargo test --verbose -- --test-threads=1
|
||||||
|
cache:
|
||||||
|
paths:
|
||||||
|
- target/
|
||||||
|
- cargo/
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
# RUSTFLAGS: "-C link-dead-code"
|
# RUSTFLAGS: "-C link-dead-code"
|
||||||
RUST_BACKTRACE: "FULL"
|
RUST_BACKTRACE: "FULL"
|
||||||
|
CARGO_HOME: $CI_PROJECT_DIR/cargo
|
||||||
|
|
||||||
stable:test:
|
stable:test:
|
||||||
# https://hub.docker.com/_/rust/
|
# https://hub.docker.com/_/rust/
|
||||||
@ -44,7 +51,7 @@ rustfmt:
|
|||||||
CFG_RELEASE_CHANNEL: "nightly"
|
CFG_RELEASE_CHANNEL: "nightly"
|
||||||
script:
|
script:
|
||||||
- rustc --version && cargo --version
|
- rustc --version && cargo --version
|
||||||
- cargo install rustfmt-nightly
|
- 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
|
||||||
@ -54,5 +61,7 @@ clippy:
|
|||||||
stage: lint
|
stage: lint
|
||||||
script:
|
script:
|
||||||
- rustc --version && cargo --version
|
- rustc --version && cargo --version
|
||||||
- cargo install clippy
|
- cargo install clippy --force
|
||||||
|
# Force regeneration of gresources regardless of artifacts chage
|
||||||
|
- cd hammond-gtk/resources/ && glib-compile-resources --generate resources.xml && cd ../../
|
||||||
- cargo clippy --all
|
- cargo clippy --all
|
||||||
|
|||||||
15
Cargo.lock
generated
15
Cargo.lock
generated
@ -611,11 +611,14 @@ dependencies = [
|
|||||||
"gtk 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"gtk 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"hammond-data 0.1.0",
|
"hammond-data 0.1.0",
|
||||||
"hammond-downloader 0.1.0",
|
"hammond-downloader 0.1.0",
|
||||||
|
"humansize 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"loggerv 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"loggerv 0.6.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 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rayon 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"regex 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
"regex 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"send-cell 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -635,6 +638,11 @@ name = "httparse"
|
|||||||
version = "1.2.3"
|
version = "1.2.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "humansize"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "0.11.9"
|
version = "0.11.9"
|
||||||
@ -1280,6 +1288,11 @@ dependencies = [
|
|||||||
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "send-cell"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
@ -1673,6 +1686,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
"checksum gtk-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "905fcfbaaad1b44ec0b4bba9e4d527d728284c62bc2ba41fccedace2b096766f"
|
"checksum gtk-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "905fcfbaaad1b44ec0b4bba9e4d527d728284c62bc2ba41fccedace2b096766f"
|
||||||
"checksum html5ever 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ba3a1fd1857a714d410c191364c5d7bf8a6487c0ab5575146d37dd7eb17ef523"
|
"checksum html5ever 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ba3a1fd1857a714d410c191364c5d7bf8a6487c0ab5575146d37dd7eb17ef523"
|
||||||
"checksum httparse 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "af2f2dd97457e8fb1ae7c5a420db346af389926e36f43768b96f101546b04a07"
|
"checksum httparse 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "af2f2dd97457e8fb1ae7c5a420db346af389926e36f43768b96f101546b04a07"
|
||||||
|
"checksum humansize 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d99804bdb0790b0c312a5a1115f83804b821f1a96d80759fbb57ce796d1f3778"
|
||||||
"checksum hyper 0.11.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e0594792d2109069d0caffd176f674d770a84adf024c5bb48e686b1ee5ac7659"
|
"checksum hyper 0.11.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e0594792d2109069d0caffd176f674d770a84adf024c5bb48e686b1ee5ac7659"
|
||||||
"checksum hyper-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9c81fa95203e2a6087242c38691a0210f23e9f3f8f944350bd676522132e2985"
|
"checksum hyper-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9c81fa95203e2a6087242c38691a0210f23e9f3f8f944350bd676522132e2985"
|
||||||
"checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d"
|
"checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d"
|
||||||
@ -1746,6 +1760,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
"checksum secur32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3f412dfa83308d893101dd59c10d6fda8283465976c28c287c5c855bf8d216bc"
|
"checksum secur32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3f412dfa83308d893101dd59c10d6fda8283465976c28c287c5c855bf8d216bc"
|
||||||
"checksum security-framework 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "dfa44ee9c54ce5eecc9de7d5acbad112ee58755239381f687e564004ba4a2332"
|
"checksum security-framework 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "dfa44ee9c54ce5eecc9de7d5acbad112ee58755239381f687e564004ba4a2332"
|
||||||
"checksum security-framework-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "5421621e836278a0b139268f36eee0dc7e389b784dc3f79d8f11aabadf41bead"
|
"checksum security-framework-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "5421621e836278a0b139268f36eee0dc7e389b784dc3f79d8f11aabadf41bead"
|
||||||
|
"checksum send-cell 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1c620dd7e056b468b9d374a9f51cfa6bb4bf17a8ca4ee62e5efa0d99aaff2c41"
|
||||||
"checksum serde 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)" = "1c57ab4ec5fa85d08aaf8ed9245899d9bbdd66768945b21113b84d5f595cb6a1"
|
"checksum serde 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)" = "1c57ab4ec5fa85d08aaf8ed9245899d9bbdd66768945b21113b84d5f595cb6a1"
|
||||||
"checksum serde_json 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7cf5b0b5b4bd22eeecb7e01ac2e1225c7ef5e4272b79ee28a8392a8c8489c839"
|
"checksum serde_json 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7cf5b0b5b4bd22eeecb7e01ac2e1225c7ef5e4272b79ee28a8392a8c8489c839"
|
||||||
"checksum serde_urlencoded 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ce0fd303af908732989354c6f02e05e2e6d597152870f2c6990efb0577137480"
|
"checksum serde_urlencoded 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ce0fd303af908732989354c6f02e05e2e6d597152870f2c6990efb0577137480"
|
||||||
|
|||||||
19
TODO.md
19
TODO.md
@ -1,20 +1,13 @@
|
|||||||
## TODOs:
|
# TODOs
|
||||||
|
|
||||||
## Planned Features
|
## Planned Features
|
||||||
|
|
||||||
## Priorities:
|
## Priorities
|
||||||
|
|
||||||
- [ ] Unplayed Only and Downloaded only view.
|
- [ ] Unplayed Only and Downloaded only view.
|
||||||
- [ ] Auto-updater
|
|
||||||
- [ ] OPML import/export // Probably need to create a crate.
|
- [ ] OPML import/export // Probably need to create a crate.
|
||||||
|
|
||||||
**Proper Desing Mockups for the Gtk Client:**
|
## Second
|
||||||
|
|
||||||
- [ ] Re-design EpisodeWidget.
|
|
||||||
- [ ] Re-design PodcastWidget.
|
|
||||||
- [ ] Polish the flowbox_child banner.
|
|
||||||
|
|
||||||
## Second:
|
|
||||||
|
|
||||||
- [ ] Make use of file metadas, [This](https://github.com/GuillaumeGomez/audio-video-metadata) might be helpfull.
|
- [ ] Make use of file metadas, [This](https://github.com/GuillaumeGomez/audio-video-metadata) might be helpfull.
|
||||||
- [ ] Notifications
|
- [ ] Notifications
|
||||||
@ -23,8 +16,7 @@
|
|||||||
- [ ] MPRIS integration
|
- [ ] MPRIS integration
|
||||||
- [ ] Search Implementation
|
- [ ] Search Implementation
|
||||||
|
|
||||||
|
## Third
|
||||||
## Third:
|
|
||||||
|
|
||||||
- [ ] Download Queue
|
- [ ] Download Queue
|
||||||
- [ ] Ability to Stream content on demand
|
- [ ] Ability to Stream content on demand
|
||||||
@ -37,8 +29,7 @@
|
|||||||
**Would be nice:**
|
**Would be nice:**
|
||||||
|
|
||||||
- [ ] Make Podcast cover fetchng and loading not block the execution of the program at startup.
|
- [ ] Make Podcast cover fetchng and loading not block the execution of the program at startup.
|
||||||
- [ ] Lazy evaluate episode loading based on the podcast_widget's view scrolling.
|
- [ ] Lazy evaluate episode loading based on the show_widget's scrolling.
|
||||||
- [ ] Headerbar back button and stack switching
|
|
||||||
|
|
||||||
**FIXME:**
|
**FIXME:**
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
ALTER TABLE episode RENAME TO old_table;
|
||||||
|
|
||||||
|
CREATE TABLE episode (
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
uri TEXT,
|
||||||
|
local_uri TEXT,
|
||||||
|
description TEXT,
|
||||||
|
published_date TEXT,
|
||||||
|
epoch INTEGER NOT NULL DEFAULT 0,
|
||||||
|
length INTEGER,
|
||||||
|
guid TEXT,
|
||||||
|
played INTEGER,
|
||||||
|
podcast_id INTEGER NOT NULL,
|
||||||
|
favorite INTEGER DEFAULT 0,
|
||||||
|
archive INTEGER DEFAULT 0,
|
||||||
|
PRIMARY KEY (title, podcast_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO episode (title, uri, local_uri, description, published_date, epoch, length, guid, played, favorite, archive, podcast_id)
|
||||||
|
SELECT title, uri, local_uri, description, published_date, epoch, length, guid, played, favorite, archive, podcast_id
|
||||||
|
FROM old_table;
|
||||||
|
Drop table old_table;
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
ALTER TABLE episode RENAME TO old_table;
|
||||||
|
|
||||||
|
CREATE TABLE episode (
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
uri TEXT,
|
||||||
|
local_uri TEXT,
|
||||||
|
description TEXT,
|
||||||
|
published_date TEXT,
|
||||||
|
epoch INTEGER NOT NULL DEFAULT 0,
|
||||||
|
length INTEGER,
|
||||||
|
duration INTEGER,
|
||||||
|
guid TEXT,
|
||||||
|
played INTEGER,
|
||||||
|
podcast_id INTEGER NOT NULL,
|
||||||
|
favorite INTEGER DEFAULT 0,
|
||||||
|
archive INTEGER DEFAULT 0,
|
||||||
|
PRIMARY KEY (title, podcast_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO episode (title, uri, local_uri, description, published_date, epoch, length, guid, played, favorite, archive, podcast_id)
|
||||||
|
SELECT title, uri, local_uri, description, published_date, epoch, length, guid, played, favorite, archive, podcast_id
|
||||||
|
FROM old_table;
|
||||||
|
Drop table old_table;
|
||||||
@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use diesel;
|
use diesel;
|
||||||
use models::queryables::{Episode, EpisodeWidgetQuery, Podcast, Source};
|
use models::queryables::{Episode, EpisodeCleanerQuery, EpisodeWidgetQuery, Podcast,
|
||||||
|
PodcastCoverQuery, Source};
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use errors::*;
|
use errors::*;
|
||||||
|
|
||||||
@ -32,14 +33,15 @@ pub fn get_episodes() -> Result<Vec<Episode>> {
|
|||||||
Ok(episode.order(epoch.desc()).load::<Episode>(&*con)?)
|
Ok(episode.order(epoch.desc()).load::<Episode>(&*con)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_downloaded_episodes() -> Result<Vec<Episode>> {
|
pub(crate) fn get_downloaded_episodes() -> Result<Vec<EpisodeCleanerQuery>> {
|
||||||
use schema::episode::dsl::*;
|
use schema::episode::dsl::*;
|
||||||
|
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
Ok(episode
|
Ok(episode
|
||||||
|
.select((rowid, local_uri, played))
|
||||||
.filter(local_uri.is_not_null())
|
.filter(local_uri.is_not_null())
|
||||||
.load::<Episode>(&*con)?)
|
.load::<EpisodeCleanerQuery>(&*con)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_played_episodes() -> Result<Vec<Episode>> {
|
pub fn get_played_episodes() -> Result<Vec<Episode>> {
|
||||||
@ -50,6 +52,17 @@ pub fn get_played_episodes() -> Result<Vec<Episode>> {
|
|||||||
Ok(episode.filter(played.is_not_null()).load::<Episode>(&*con)?)
|
Ok(episode.filter(played.is_not_null()).load::<Episode>(&*con)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_played_cleaner_episodes() -> Result<Vec<EpisodeCleanerQuery>> {
|
||||||
|
use schema::episode::dsl::*;
|
||||||
|
|
||||||
|
let db = connection();
|
||||||
|
let con = db.get()?;
|
||||||
|
Ok(episode
|
||||||
|
.select((rowid, local_uri, played))
|
||||||
|
.filter(played.is_not_null())
|
||||||
|
.load::<EpisodeCleanerQuery>(&*con)?)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_episode_from_rowid(ep_id: i32) -> Result<Episode> {
|
pub fn get_episode_from_rowid(ep_id: i32) -> Result<Episode> {
|
||||||
use schema::episode::dsl::*;
|
use schema::episode::dsl::*;
|
||||||
|
|
||||||
@ -84,6 +97,29 @@ pub fn get_episodes_with_limit(limit: u32) -> Result<Vec<Episode>> {
|
|||||||
.load::<Episode>(&*con)?)
|
.load::<Episode>(&*con)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_episodes_widgets_with_limit(limit: u32) -> Result<Vec<EpisodeWidgetQuery>> {
|
||||||
|
use schema::episode;
|
||||||
|
|
||||||
|
let db = connection();
|
||||||
|
let con = db.get()?;
|
||||||
|
|
||||||
|
Ok(episode::table
|
||||||
|
.select((
|
||||||
|
episode::rowid,
|
||||||
|
episode::title,
|
||||||
|
episode::uri,
|
||||||
|
episode::local_uri,
|
||||||
|
episode::epoch,
|
||||||
|
episode::length,
|
||||||
|
episode::duration,
|
||||||
|
episode::played,
|
||||||
|
episode::podcast_id,
|
||||||
|
))
|
||||||
|
.order(episode::epoch.desc())
|
||||||
|
.limit(i64::from(limit))
|
||||||
|
.load::<EpisodeWidgetQuery>(&*con)?)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_podcast_from_id(pid: i32) -> Result<Podcast> {
|
pub fn get_podcast_from_id(pid: i32) -> Result<Podcast> {
|
||||||
use schema::podcast::dsl::*;
|
use schema::podcast::dsl::*;
|
||||||
|
|
||||||
@ -92,6 +128,17 @@ pub fn get_podcast_from_id(pid: i32) -> Result<Podcast> {
|
|||||||
Ok(podcast.filter(id.eq(pid)).get_result::<Podcast>(&*con)?)
|
Ok(podcast.filter(id.eq(pid)).get_result::<Podcast>(&*con)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_podcast_cover_from_id(pid: i32) -> Result<PodcastCoverQuery> {
|
||||||
|
use schema::podcast::dsl::*;
|
||||||
|
|
||||||
|
let db = connection();
|
||||||
|
let con = db.get()?;
|
||||||
|
Ok(podcast
|
||||||
|
.select((id, title, image_uri))
|
||||||
|
.filter(id.eq(pid))
|
||||||
|
.get_result::<PodcastCoverQuery>(&*con)?)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_pd_episodes(parent: &Podcast) -> Result<Vec<Episode>> {
|
pub fn get_pd_episodes(parent: &Podcast) -> Result<Vec<Episode>> {
|
||||||
use schema::episode::dsl::*;
|
use schema::episode::dsl::*;
|
||||||
|
|
||||||
@ -110,7 +157,7 @@ pub fn get_pd_episodeswidgets(parent: &Podcast) -> Result<Vec<EpisodeWidgetQuery
|
|||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
|
|
||||||
Ok(
|
Ok(
|
||||||
episode.select((rowid, title, uri, local_uri, epoch, length, played, podcast_id))
|
episode.select((rowid, title, uri, local_uri, epoch, length, duration, played, podcast_id))
|
||||||
.filter(podcast_id.eq(parent.id()))
|
.filter(podcast_id.eq(parent.id()))
|
||||||
// .group_by(epoch)
|
// .group_by(epoch)
|
||||||
.order(epoch.desc())
|
.order(epoch.desc())
|
||||||
|
|||||||
@ -61,7 +61,7 @@ pub(crate) mod models;
|
|||||||
mod parser;
|
mod parser;
|
||||||
mod schema;
|
mod schema;
|
||||||
|
|
||||||
pub use models::queryables::{Episode, EpisodeWidgetQuery, Podcast, Source};
|
pub use models::queryables::{Episode, EpisodeWidgetQuery, Podcast, PodcastCoverQuery, Source};
|
||||||
|
|
||||||
/// [XDG Base Direcotory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) Paths.
|
/// [XDG Base Direcotory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) Paths.
|
||||||
#[allow(missing_debug_implementations)]
|
#[allow(missing_debug_implementations)]
|
||||||
|
|||||||
@ -112,10 +112,6 @@ impl NewPodcast {
|
|||||||
let con = db.get()?;
|
let con = db.get()?;
|
||||||
match pd {
|
match pd {
|
||||||
Ok(foo) => {
|
Ok(foo) => {
|
||||||
if foo.source_id() != self.source_id {
|
|
||||||
error!("NSPD sid: {}, SPD sid: {}", self.source_id, foo.source_id());
|
|
||||||
};
|
|
||||||
|
|
||||||
if (foo.link() != self.link) || (foo.title() != self.title)
|
if (foo.link() != self.link) || (foo.title() != self.title)
|
||||||
|| (foo.image_uri() != self.image_uri.as_ref().map(|x| x.as_str()))
|
|| (foo.image_uri() != self.image_uri.as_ref().map(|x| x.as_str()))
|
||||||
{
|
{
|
||||||
@ -166,6 +162,7 @@ pub(crate) struct NewEpisode {
|
|||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
published_date: Option<String>,
|
published_date: Option<String>,
|
||||||
length: Option<i32>,
|
length: Option<i32>,
|
||||||
|
duration: Option<i32>,
|
||||||
guid: Option<String>,
|
guid: Option<String>,
|
||||||
epoch: i32,
|
epoch: i32,
|
||||||
podcast_id: i32,
|
podcast_id: i32,
|
||||||
@ -211,6 +208,7 @@ impl NewEpisode {
|
|||||||
|
|
||||||
if foo.title() != self.title.as_str() || foo.epoch() != self.epoch
|
if foo.title() != self.title.as_str() || foo.epoch() != self.epoch
|
||||||
|| foo.uri() != self.uri.as_ref().map(|s| s.as_str())
|
|| foo.uri() != self.uri.as_ref().map(|s| s.as_str())
|
||||||
|
|| foo.duration() != self.duration
|
||||||
{
|
{
|
||||||
self.update(con, foo.rowid())?;
|
self.update(con, foo.rowid())?;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,7 @@ pub struct Episode {
|
|||||||
published_date: Option<String>,
|
published_date: Option<String>,
|
||||||
epoch: i32,
|
epoch: i32,
|
||||||
length: Option<i32>,
|
length: Option<i32>,
|
||||||
|
duration: Option<i32>,
|
||||||
guid: Option<String>,
|
guid: Option<String>,
|
||||||
played: Option<i32>,
|
played: Option<i32>,
|
||||||
favorite: bool,
|
favorite: bool,
|
||||||
@ -125,6 +126,8 @@ impl Episode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the `length`.
|
/// Get the `length`.
|
||||||
|
///
|
||||||
|
/// The number represents the size of the file in bytes.
|
||||||
pub fn length(&self) -> Option<i32> {
|
pub fn length(&self) -> Option<i32> {
|
||||||
self.length
|
self.length
|
||||||
}
|
}
|
||||||
@ -134,6 +137,18 @@ impl Episode {
|
|||||||
self.length = value;
|
self.length = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the `duration` value.
|
||||||
|
///
|
||||||
|
/// The number represents the duration of the item/episode in seconds.
|
||||||
|
pub fn duration(&self) -> Option<i32> {
|
||||||
|
self.duration
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the `duration`.
|
||||||
|
pub fn set_duration(&mut self, value: Option<i32>) {
|
||||||
|
self.duration = value;
|
||||||
|
}
|
||||||
|
|
||||||
/// Epoch representation of the last time the episode was played.
|
/// Epoch representation of the last time the episode was played.
|
||||||
///
|
///
|
||||||
/// None/Null for unplayed.
|
/// None/Null for unplayed.
|
||||||
@ -204,6 +219,7 @@ pub struct EpisodeWidgetQuery {
|
|||||||
local_uri: Option<String>,
|
local_uri: Option<String>,
|
||||||
epoch: i32,
|
epoch: i32,
|
||||||
length: Option<i32>,
|
length: Option<i32>,
|
||||||
|
duration: Option<i32>,
|
||||||
played: Option<i32>,
|
played: Option<i32>,
|
||||||
// favorite: bool,
|
// favorite: bool,
|
||||||
// archive: bool,
|
// archive: bool,
|
||||||
@ -250,6 +266,8 @@ impl EpisodeWidgetQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the `length`.
|
/// Get the `length`.
|
||||||
|
///
|
||||||
|
/// The number represents the size of the file in bytes.
|
||||||
pub fn length(&self) -> Option<i32> {
|
pub fn length(&self) -> Option<i32> {
|
||||||
self.length
|
self.length
|
||||||
}
|
}
|
||||||
@ -259,6 +277,18 @@ impl EpisodeWidgetQuery {
|
|||||||
self.length = value;
|
self.length = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the `duration` value.
|
||||||
|
///
|
||||||
|
/// The number represents the duration of the item/episode in seconds.
|
||||||
|
pub fn duration(&self) -> Option<i32> {
|
||||||
|
self.duration
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the `duration`.
|
||||||
|
pub fn set_duration(&mut self, value: Option<i32>) {
|
||||||
|
self.duration = value;
|
||||||
|
}
|
||||||
|
|
||||||
/// Epoch representation of the last time the episode was played.
|
/// Epoch representation of the last time the episode was played.
|
||||||
///
|
///
|
||||||
/// None/Null for unplayed.
|
/// None/Null for unplayed.
|
||||||
@ -320,6 +350,72 @@ impl EpisodeWidgetQuery {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Queryable, AsChangeset, PartialEq)]
|
||||||
|
#[table_name = "episode"]
|
||||||
|
#[changeset_options(treat_none_as_null = "true")]
|
||||||
|
#[primary_key(title, podcast_id)]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
/// Diesel Model to be used internal with the `utils::checkup` function.
|
||||||
|
pub struct EpisodeCleanerQuery {
|
||||||
|
rowid: i32,
|
||||||
|
local_uri: Option<String>,
|
||||||
|
played: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Episode> for EpisodeCleanerQuery {
|
||||||
|
fn from(e: Episode) -> EpisodeCleanerQuery {
|
||||||
|
EpisodeCleanerQuery {
|
||||||
|
rowid: e.rowid(),
|
||||||
|
local_uri: e.local_uri,
|
||||||
|
played: e.played,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EpisodeCleanerQuery {
|
||||||
|
/// Get the value of the sqlite's `ROW_ID`
|
||||||
|
pub fn rowid(&self) -> i32 {
|
||||||
|
self.rowid
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the value of the `local_uri`.
|
||||||
|
///
|
||||||
|
/// Represents the local uri,usually filesystem path,
|
||||||
|
/// that the media file will be located at.
|
||||||
|
pub fn local_uri(&self) -> Option<&str> {
|
||||||
|
self.local_uri.as_ref().map(|s| s.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the `local_uri`.
|
||||||
|
pub fn set_local_uri(&mut self, value: Option<&str>) {
|
||||||
|
self.local_uri = value.map(|x| x.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Epoch representation of the last time the episode was played.
|
||||||
|
///
|
||||||
|
/// None/Null for unplayed.
|
||||||
|
pub fn played(&self) -> Option<i32> {
|
||||||
|
self.played
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the `played` value.
|
||||||
|
pub fn set_played(&mut self, value: Option<i32>) {
|
||||||
|
self.played = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper method to easily save/"sync" current state of self to the Database.
|
||||||
|
pub fn save(&self) -> Result<usize> {
|
||||||
|
use schema::episode::dsl::*;
|
||||||
|
|
||||||
|
let db = connection();
|
||||||
|
let tempdb = db.get()?;
|
||||||
|
|
||||||
|
Ok(diesel::update(episode.filter(rowid.eq(self.rowid())))
|
||||||
|
.set(self)
|
||||||
|
.execute(&*tempdb)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Queryable, Identifiable, AsChangeset, Associations, PartialEq)]
|
#[derive(Queryable, Identifiable, AsChangeset, Associations, PartialEq)]
|
||||||
#[belongs_to(Source, foreign_key = "source_id")]
|
#[belongs_to(Source, foreign_key = "source_id")]
|
||||||
#[changeset_options(treat_none_as_null = "true")]
|
#[changeset_options(treat_none_as_null = "true")]
|
||||||
@ -427,6 +523,44 @@ impl Podcast {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Queryable, Debug, Clone)]
|
||||||
|
/// Diesel Model of the podcast cover query.
|
||||||
|
/// Used for fetching information about a Podcast's cover.
|
||||||
|
pub struct PodcastCoverQuery {
|
||||||
|
id: i32,
|
||||||
|
title: String,
|
||||||
|
image_uri: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Podcast> for PodcastCoverQuery {
|
||||||
|
fn from(p: Podcast) -> PodcastCoverQuery {
|
||||||
|
PodcastCoverQuery {
|
||||||
|
id: *p.id(),
|
||||||
|
title: p.title,
|
||||||
|
image_uri: p.image_uri,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PodcastCoverQuery {
|
||||||
|
/// Get the Feed `id`.
|
||||||
|
pub fn id(&self) -> i32 {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the Feed `title`.
|
||||||
|
pub fn title(&self) -> &str {
|
||||||
|
&self.title
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the `image_uri`.
|
||||||
|
///
|
||||||
|
/// Represents the uri(url usually) that the Feed cover image is located at.
|
||||||
|
pub fn image_uri(&self) -> Option<&str> {
|
||||||
|
self.image_uri.as_ref().map(|s| s.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Queryable, Identifiable, AsChangeset, PartialEq)]
|
#[derive(Queryable, Identifiable, AsChangeset, PartialEq)]
|
||||||
#[table_name = "source"]
|
#[table_name = "source"]
|
||||||
#[changeset_options(treat_none_as_null = "true")]
|
#[changeset_options(treat_none_as_null = "true")]
|
||||||
|
|||||||
@ -17,9 +17,9 @@ pub(crate) fn new_podcast(chan: &Channel, source_id: i32) -> NewPodcast {
|
|||||||
let link = url_cleaner(chan.link());
|
let link = url_cleaner(chan.link());
|
||||||
let x = chan.itunes_ext().map(|s| s.image());
|
let x = chan.itunes_ext().map(|s| s.image());
|
||||||
let image_uri = if let Some(img) = x {
|
let image_uri = if let Some(img) = x {
|
||||||
img.map(|s| url_cleaner(s))
|
img.map(|s| s.to_owned())
|
||||||
} else {
|
} else {
|
||||||
chan.image().map(|foo| url_cleaner(foo.url()))
|
chan.image().map(|foo| foo.url().to_owned())
|
||||||
};
|
};
|
||||||
|
|
||||||
NewPodcastBuilder::default()
|
NewPodcastBuilder::default()
|
||||||
@ -43,10 +43,6 @@ pub(crate) fn new_episode(item: &Item, parent_id: i32) -> Result<NewEpisode> {
|
|||||||
.map(|s| replace_extra_spaces(&ammonia::clean(s)));
|
.map(|s| replace_extra_spaces(&ammonia::clean(s)));
|
||||||
let guid = item.guid().map(|s| s.value().trim().to_owned());
|
let guid = item.guid().map(|s| s.value().trim().to_owned());
|
||||||
|
|
||||||
// Its kinda weird this being an Option type.
|
|
||||||
// Rss 2.0 specified that it's optional.
|
|
||||||
// Though the db scema has a requirment of episode uri being Unique && Not Null.
|
|
||||||
// TODO: Restructure
|
|
||||||
let x = item.enclosure().map(|s| url_cleaner(s.url()));
|
let x = item.enclosure().map(|s| url_cleaner(s.url()));
|
||||||
// FIXME: refactor
|
// FIXME: refactor
|
||||||
let uri = if x.is_some() {
|
let uri = if x.is_some() {
|
||||||
@ -67,13 +63,15 @@ pub(crate) fn new_episode(item: &Item, parent_id: i32) -> Result<NewEpisode> {
|
|||||||
let pub_date = date.map(|x| x.to_rfc2822()).ok();
|
let pub_date = date.map(|x| x.to_rfc2822()).ok();
|
||||||
let epoch = date.map(|x| x.timestamp() as i32).unwrap_or(0);
|
let epoch = date.map(|x| x.timestamp() as i32).unwrap_or(0);
|
||||||
|
|
||||||
let length = item.enclosure().map(|x| x.length().parse().unwrap_or(0));
|
let length = || -> Option<i32> { item.enclosure().map(|x| x.length().parse().ok())? }();
|
||||||
|
let duration = parse_itunes_duration(item);
|
||||||
|
|
||||||
Ok(NewEpisodeBuilder::default()
|
Ok(NewEpisodeBuilder::default()
|
||||||
.title(title)
|
.title(title)
|
||||||
.uri(uri)
|
.uri(uri)
|
||||||
.description(description)
|
.description(description)
|
||||||
.length(length)
|
.length(length)
|
||||||
|
.duration(duration)
|
||||||
.published_date(pub_date)
|
.published_date(pub_date)
|
||||||
.epoch(epoch)
|
.epoch(epoch)
|
||||||
.guid(guid)
|
.guid(guid)
|
||||||
@ -82,6 +80,36 @@ pub(crate) fn new_episode(item: &Item, parent_id: i32) -> Result<NewEpisode> {
|
|||||||
.unwrap())
|
.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses an Item Itunes extension and returns it's duration value in seconds.
|
||||||
|
// FIXME: Rafactor
|
||||||
|
// TODO: Write tests
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
fn parse_itunes_duration(item: &Item) -> Option<i32> {
|
||||||
|
let duration = item.itunes_ext().map(|s| s.duration())??;
|
||||||
|
|
||||||
|
// FOR SOME FUCKING REASON, IN THE APPLE EXTENSION SPEC
|
||||||
|
// THE DURATION CAN BE EITHER AN INT OF SECONDS OR
|
||||||
|
// A STRING OF THE FOLLOWING FORMATS:
|
||||||
|
// HH:MM:SS, H:MM:SS, MM:SS, M:SS
|
||||||
|
// LIKE WHO THE FUCK THOUGH THAT WOULD BE A GOOD IDEA.
|
||||||
|
if let Ok(NO_FUCKING_LOGIC) = duration.parse::<i32>() {
|
||||||
|
return Some(NO_FUCKING_LOGIC);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut seconds = 0;
|
||||||
|
let fk_apple = duration.split(':').collect::<Vec<_>>();
|
||||||
|
if fk_apple.len() == 3 {
|
||||||
|
seconds += fk_apple[0].parse::<i32>().unwrap_or(0) * 3600;
|
||||||
|
seconds += fk_apple[1].parse::<i32>().unwrap_or(0) * 60;
|
||||||
|
seconds += fk_apple[2].parse::<i32>().unwrap_or(0);
|
||||||
|
} else if fk_apple.len() == 2 {
|
||||||
|
seconds += fk_apple[0].parse::<i32>().unwrap_or(0) * 60;
|
||||||
|
seconds += fk_apple[1].parse::<i32>().unwrap_or(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(seconds)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
|||||||
@ -8,6 +8,7 @@ table! {
|
|||||||
published_date -> Nullable<Text>,
|
published_date -> Nullable<Text>,
|
||||||
epoch -> Integer,
|
epoch -> Integer,
|
||||||
length -> Nullable<Integer>,
|
length -> Nullable<Integer>,
|
||||||
|
duration -> Nullable<Integer>,
|
||||||
guid -> Nullable<Text>,
|
guid -> Nullable<Text>,
|
||||||
played -> Nullable<Integer>,
|
played -> Nullable<Integer>,
|
||||||
favorite -> Bool,
|
favorite -> Bool,
|
||||||
|
|||||||
@ -8,7 +8,7 @@ use itertools::Itertools;
|
|||||||
|
|
||||||
use errors::*;
|
use errors::*;
|
||||||
use dbqueries;
|
use dbqueries;
|
||||||
use models::queryables::Episode;
|
use models::queryables::EpisodeCleanerQuery;
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@ -23,7 +23,7 @@ fn download_checker() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn checker_helper(ep: &mut Episode) {
|
fn checker_helper(ep: &mut EpisodeCleanerQuery) {
|
||||||
if !Path::new(ep.local_uri().unwrap()).exists() {
|
if !Path::new(ep.local_uri().unwrap()).exists() {
|
||||||
ep.set_local_uri(None);
|
ep.set_local_uri(None);
|
||||||
let res = ep.save();
|
let res = ep.save();
|
||||||
@ -35,7 +35,7 @@ fn checker_helper(ep: &mut Episode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn played_cleaner() -> Result<()> {
|
fn played_cleaner() -> Result<()> {
|
||||||
let episodes = dbqueries::get_played_episodes()?;
|
let episodes = dbqueries::get_played_cleaner_episodes()?;
|
||||||
|
|
||||||
let now_utc = Utc::now().timestamp() as i32;
|
let now_utc = Utc::now().timestamp() as i32;
|
||||||
episodes.into_par_iter().for_each(|mut ep| {
|
episodes.into_par_iter().for_each(|mut ep| {
|
||||||
@ -50,7 +50,7 @@ fn played_cleaner() -> Result<()> {
|
|||||||
error!("Error while trying to delete file: {:?}", ep.local_uri());
|
error!("Error while trying to delete file: {:?}", ep.local_uri());
|
||||||
error!("Error: {}", err);
|
error!("Error: {}", err);
|
||||||
} else {
|
} else {
|
||||||
info!("Episode {:?} was deleted succesfully.", ep.title());
|
info!("Episode {:?} was deleted succesfully.", ep.local_uri());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -59,7 +59,7 @@ fn played_cleaner() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check `ep.local_uri` field and delete the file it points to.
|
/// Check `ep.local_uri` field and delete the file it points to.
|
||||||
pub fn delete_local_content(ep: &mut Episode) -> Result<()> {
|
pub fn delete_local_content(ep: &mut EpisodeCleanerQuery) -> Result<()> {
|
||||||
if ep.local_uri().is_some() {
|
if ep.local_uri().is_some() {
|
||||||
let uri = ep.local_uri().unwrap().to_owned();
|
let uri = ep.local_uri().unwrap().to_owned();
|
||||||
if Path::new(&uri).exists() {
|
if Path::new(&uri).exists() {
|
||||||
@ -106,7 +106,7 @@ pub fn url_cleaner(s: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper functions that strips extra spaces and newlines and all the tabs.
|
/// Helper functions that strips extra spaces and newlines and ignores the tabs.
|
||||||
#[allow(match_same_arms)]
|
#[allow(match_same_arms)]
|
||||||
pub fn replace_extra_spaces(s: &str) -> String {
|
pub fn replace_extra_spaces(s: &str) -> String {
|
||||||
s.trim()
|
s.trim()
|
||||||
@ -176,12 +176,16 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_download_checker() {
|
fn test_download_checker() {
|
||||||
let _tmp_dir = helper_db();
|
let tmp_dir = helper_db();
|
||||||
download_checker().unwrap();
|
download_checker().unwrap();
|
||||||
let episodes = dbqueries::get_downloaded_episodes().unwrap();
|
let episodes = dbqueries::get_downloaded_episodes().unwrap();
|
||||||
|
let valid_path = tmp_dir.path().join("virtual_dl.mp3");
|
||||||
|
|
||||||
assert_eq!(episodes.len(), 1);
|
assert_eq!(episodes.len(), 1);
|
||||||
assert_eq!("foo_bar", episodes.first().unwrap().title());
|
assert_eq!(
|
||||||
|
Some(valid_path.to_str().unwrap()),
|
||||||
|
episodes.first().unwrap().local_uri()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -190,7 +194,9 @@ mod tests {
|
|||||||
let mut episode = {
|
let mut episode = {
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get().unwrap();
|
let con = db.get().unwrap();
|
||||||
dbqueries::get_episode_from_pk(&con, "bar_baz", 1).unwrap()
|
dbqueries::get_episode_from_pk(&con, "bar_baz", 1)
|
||||||
|
.unwrap()
|
||||||
|
.into()
|
||||||
};
|
};
|
||||||
|
|
||||||
checker_helper(&mut episode);
|
checker_helper(&mut episode);
|
||||||
@ -200,10 +206,12 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_download_cleaner() {
|
fn test_download_cleaner() {
|
||||||
let _tmp_dir = helper_db();
|
let _tmp_dir = helper_db();
|
||||||
let mut episode = {
|
let mut episode: EpisodeCleanerQuery = {
|
||||||
let db = connection();
|
let db = connection();
|
||||||
let con = db.get().unwrap();
|
let con = db.get().unwrap();
|
||||||
dbqueries::get_episode_from_pk(&con, "foo_bar", 0).unwrap()
|
dbqueries::get_episode_from_pk(&con, "foo_bar", 0)
|
||||||
|
.unwrap()
|
||||||
|
.into()
|
||||||
};
|
};
|
||||||
|
|
||||||
let valid_path = episode.local_uri().unwrap().to_owned();
|
let valid_path = episode.local_uri().unwrap().to_owned();
|
||||||
|
|||||||
@ -6,9 +6,10 @@ use mime_guess;
|
|||||||
use std::fs::{rename, DirBuilder, File};
|
use std::fs::{rename, DirBuilder, File};
|
||||||
use std::io::{BufWriter, Read, Write};
|
use std::io::{BufWriter, Read, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
use errors::*;
|
use errors::*;
|
||||||
use hammond_data::{EpisodeWidgetQuery, Podcast};
|
use hammond_data::{EpisodeWidgetQuery, PodcastCoverQuery};
|
||||||
use hammond_data::xdg_dirs::{DL_DIR, HAMMOND_CACHE};
|
use hammond_data::xdg_dirs::{DL_DIR, HAMMOND_CACHE};
|
||||||
|
|
||||||
// TODO: Replace path that are of type &str with std::path.
|
// TODO: Replace path that are of type &str with std::path.
|
||||||
@ -123,6 +124,13 @@ pub fn get_episode(ep: &mut EpisodeWidgetQuery, download_folder: &str) -> Result
|
|||||||
if let Ok(path) = res {
|
if let Ok(path) = res {
|
||||||
// If download succedes set episode local_uri to dlpath.
|
// If download succedes set episode local_uri to dlpath.
|
||||||
ep.set_local_uri(Some(&path));
|
ep.set_local_uri(Some(&path));
|
||||||
|
|
||||||
|
// Over-write episode lenght
|
||||||
|
let size = fs::metadata(path);
|
||||||
|
if let Ok(s) = size {
|
||||||
|
ep.set_length(Some(s.len() as i32))
|
||||||
|
};
|
||||||
|
|
||||||
ep.save()?;
|
ep.save()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
@ -131,7 +139,7 @@ pub fn get_episode(ep: &mut EpisodeWidgetQuery, download_folder: &str) -> Result
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cache_image(pd: &Podcast) -> Option<String> {
|
pub fn cache_image(pd: &PodcastCoverQuery) -> Option<String> {
|
||||||
let url = pd.image_uri()?.to_owned();
|
let url = pd.image_uri()?.to_owned();
|
||||||
if url == "" {
|
if url == "" {
|
||||||
return None;
|
return None;
|
||||||
@ -207,7 +215,7 @@ mod tests {
|
|||||||
index(vec![feed]);
|
index(vec![feed]);
|
||||||
|
|
||||||
// Get the Podcast
|
// Get the Podcast
|
||||||
let pd = dbqueries::get_podcast_from_source_id(sid).unwrap();
|
let pd = dbqueries::get_podcast_from_source_id(sid).unwrap().into();
|
||||||
|
|
||||||
let img_path = cache_image(&pd);
|
let img_path = cache_image(&pd);
|
||||||
let foo_ = format!(
|
let foo_ = format!(
|
||||||
|
|||||||
@ -12,11 +12,14 @@ gdk = "0.7.0"
|
|||||||
gdk-pixbuf = "0.3.0"
|
gdk-pixbuf = "0.3.0"
|
||||||
gio = "0.3.0"
|
gio = "0.3.0"
|
||||||
glib = "0.4.0"
|
glib = "0.4.0"
|
||||||
|
humansize = "1.0.2"
|
||||||
|
lazy_static = "1.0.0"
|
||||||
log = "0.3.8"
|
log = "0.3.8"
|
||||||
loggerv = "0.6.0"
|
loggerv = "0.6.0"
|
||||||
open = "1.2.1"
|
open = "1.2.1"
|
||||||
rayon = "0.9.0"
|
rayon = "0.9.0"
|
||||||
regex = "0.2.3"
|
regex = "0.2.3"
|
||||||
|
send-cell = "0.1.2"
|
||||||
|
|
||||||
[dependencies.diesel]
|
[dependencies.diesel]
|
||||||
features = ["sqlite"]
|
features = ["sqlite"]
|
||||||
|
|||||||
@ -10,13 +10,14 @@
|
|||||||
<object class="GtkBox">
|
<object class="GtkBox">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="spacing">5</property>
|
<property name="spacing">6</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkBox">
|
<object class="GtkBox">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
<property name="orientation">vertical</property>
|
<property name="orientation">vertical</property>
|
||||||
<property name="spacing">5</property>
|
<property name="spacing">6</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkBox">
|
<object class="GtkBox">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
@ -50,7 +51,7 @@
|
|||||||
<object class="GtkBox">
|
<object class="GtkBox">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="spacing">5</property>
|
<property name="spacing">6</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="date_label">
|
<object class="GtkLabel" id="date_label">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
@ -103,8 +104,8 @@
|
|||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="separator2">
|
<object class="GtkLabel" id="separator2">
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
|
<property name="no_show_all">True</property>
|
||||||
<property name="label" translatable="yes">·</property>
|
<property name="label" translatable="yes">·</property>
|
||||||
<property name="track_visited_links">False</property>
|
<property name="track_visited_links">False</property>
|
||||||
<style>
|
<style>
|
||||||
@ -119,8 +120,8 @@
|
|||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="size_label">
|
<object class="GtkLabel" id="size_label">
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
|
<property name="no_show_all">True</property>
|
||||||
<property name="label" translatable="yes">42 MB</property>
|
<property name="label" translatable="yes">42 MB</property>
|
||||||
<property name="single_line_mode">True</property>
|
<property name="single_line_mode">True</property>
|
||||||
<property name="track_visited_links">False</property>
|
<property name="track_visited_links">False</property>
|
||||||
@ -162,7 +163,7 @@
|
|||||||
<packing>
|
<packing>
|
||||||
<property name="expand">False</property>
|
<property name="expand">False</property>
|
||||||
<property name="fill">True</property>
|
<property name="fill">True</property>
|
||||||
<property name="padding">5</property>
|
<property name="padding">6</property>
|
||||||
<property name="position">0</property>
|
<property name="position">0</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
@ -170,12 +171,13 @@
|
|||||||
<object class="GtkBox">
|
<object class="GtkBox">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="spacing">5</property>
|
<property name="spacing">6</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkButton" id="cancel_button">
|
<object class="GtkButton" id="cancel_button">
|
||||||
<property name="label" translatable="yes">Cancel</property>
|
<property name="label" translatable="yes">Cancel</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="receives_default">True</property>
|
<property name="receives_default">True</property>
|
||||||
|
<property name="no_show_all">True</property>
|
||||||
<property name="valign">center</property>
|
<property name="valign">center</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
@ -190,6 +192,7 @@
|
|||||||
<property name="name">delete_button</property>
|
<property name="name">delete_button</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="receives_default">True</property>
|
<property name="receives_default">True</property>
|
||||||
|
<property name="no_show_all">True</property>
|
||||||
<property name="halign">end</property>
|
<property name="halign">end</property>
|
||||||
<property name="valign">center</property>
|
<property name="valign">center</property>
|
||||||
<child>
|
<child>
|
||||||
@ -212,6 +215,7 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="receives_default">True</property>
|
<property name="receives_default">True</property>
|
||||||
|
<property name="no_show_all">True</property>
|
||||||
<property name="halign">end</property>
|
<property name="halign">end</property>
|
||||||
<property name="valign">center</property>
|
<property name="valign">center</property>
|
||||||
<property name="always_show_image">True</property>
|
<property name="always_show_image">True</property>
|
||||||
@ -234,6 +238,7 @@
|
|||||||
<object class="GtkButton" id="play_button">
|
<object class="GtkButton" id="play_button">
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="receives_default">True</property>
|
<property name="receives_default">True</property>
|
||||||
|
<property name="no_show_all">True</property>
|
||||||
<property name="valign">center</property>
|
<property name="valign">center</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkImage">
|
<object class="GtkImage">
|
||||||
@ -254,27 +259,29 @@
|
|||||||
<packing>
|
<packing>
|
||||||
<property name="expand">False</property>
|
<property name="expand">False</property>
|
||||||
<property name="fill">True</property>
|
<property name="fill">True</property>
|
||||||
<property name="padding">5</property>
|
<property name="padding">6</property>
|
||||||
<property name="pack_type">end</property>
|
<property name="pack_type">end</property>
|
||||||
<property name="position">1</property>
|
<property name="position">1</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">False</property>
|
<property name="expand">True</property>
|
||||||
<property name="fill">True</property>
|
<property name="fill">False</property>
|
||||||
<property name="padding">5</property>
|
<property name="padding">6</property>
|
||||||
<property name="position">0</property>
|
<property name="position">0</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkProgressBar" id="progress_bar">
|
<object class="GtkProgressBar" id="progress_bar">
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
|
<property name="no_show_all">True</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">False</property>
|
<property name="expand">False</property>
|
||||||
<property name="fill">True</property>
|
<property name="fill">True</property>
|
||||||
<property name="padding">5</property>
|
<property name="padding">6</property>
|
||||||
|
<property name="pack_type">end</property>
|
||||||
<property name="position">1</property>
|
<property name="position">1</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
|
|||||||
353
hammond-gtk/resources/gtk/episodes_view.ui
Normal file
353
hammond-gtk/resources/gtk/episodes_view.ui
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Generated with glade 3.20.2 -->
|
||||||
|
<interface>
|
||||||
|
<requires lib="gtk+" version="3.20"/>
|
||||||
|
<object class="GtkBox" id="container">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkScrolledWindow">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="shadow_type">in</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkViewport">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="halign">center</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">True</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox" id="frame_parent">
|
||||||
|
<property name="width_request">600</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="halign">center</property>
|
||||||
|
<property name="margin_top">24</property>
|
||||||
|
<property name="margin_bottom">24</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">24</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox" id="today_box">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="no_show_all">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">6</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="label" translatable="yes">Today</property>
|
||||||
|
<attributes>
|
||||||
|
<attribute name="weight" value="bold"/>
|
||||||
|
<attribute name="scale" value="1.5"/>
|
||||||
|
</attributes>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkFrame">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="label_xalign">0</property>
|
||||||
|
<property name="shadow_type">in</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkListBox" id="today_list">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="selection_mode">none</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child type="label_item">
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox" id="yday_box">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="no_show_all">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">6</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="label" translatable="yes">Yesterday</property>
|
||||||
|
<attributes>
|
||||||
|
<attribute name="weight" value="bold"/>
|
||||||
|
<attribute name="scale" value="1.5"/>
|
||||||
|
</attributes>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkFrame">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="label_xalign">0</property>
|
||||||
|
<property name="shadow_type">in</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkListBox" id="yday_list">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="selection_mode">none</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child type="label_item">
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox" id="week_box">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="no_show_all">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">6</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="label" translatable="yes">This Week</property>
|
||||||
|
<attributes>
|
||||||
|
<attribute name="weight" value="bold"/>
|
||||||
|
<attribute name="scale" value="1.5"/>
|
||||||
|
</attributes>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkFrame">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="label_xalign">0</property>
|
||||||
|
<property name="shadow_type">in</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkListBox" id="week_list">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="selection_mode">none</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child type="label_item">
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox" id="month_box">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="no_show_all">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">6</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="label" translatable="yes">This Month</property>
|
||||||
|
<attributes>
|
||||||
|
<attribute name="weight" value="bold"/>
|
||||||
|
<attribute name="scale" value="1.5"/>
|
||||||
|
</attributes>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkFrame">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="label_xalign">0</property>
|
||||||
|
<property name="shadow_type">in</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkListBox" id="month_list">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="selection_mode">none</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child type="label_item">
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">3</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox" id="rest_box">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="no_show_all">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">6</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="label" translatable="yes">Older</property>
|
||||||
|
<attributes>
|
||||||
|
<attribute name="weight" value="bold"/>
|
||||||
|
<attribute name="scale" value="1.5"/>
|
||||||
|
</attributes>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkFrame">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="label_xalign">0</property>
|
||||||
|
<property name="shadow_type">in</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkListBox" id="rest_list">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="no_show_all">True</property>
|
||||||
|
<property name="selection_mode">none</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child type="label_item">
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">5</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">True</property>
|
||||||
|
<property name="fill">False</property>
|
||||||
|
<property name="position">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">True</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</interface>
|
||||||
29
hammond-gtk/resources/gtk/episodes_view_widget.ui
Normal file
29
hammond-gtk/resources/gtk/episodes_view_widget.ui
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Generated with glade 3.20.2 -->
|
||||||
|
<interface>
|
||||||
|
<requires lib="gtk+" version="3.20"/>
|
||||||
|
<object class="GtkBox" id="container">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="spacing">6</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImage" id="cover">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="margin_left">6</property>
|
||||||
|
<property name="margin_top">6</property>
|
||||||
|
<property name="margin_bottom">6</property>
|
||||||
|
<property name="pixel_size">64</property>
|
||||||
|
<property name="icon_name">image-x-generic-symbolic</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</interface>
|
||||||
@ -20,6 +20,8 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="halign">center</property>
|
<property name="halign">center</property>
|
||||||
|
<property name="margin_top">24</property>
|
||||||
|
<property name="margin_bottom">24</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkBox">
|
<object class="GtkBox">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
@ -42,6 +44,7 @@
|
|||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="halign">center</property>
|
<property name="halign">center</property>
|
||||||
<property name="orientation">vertical</property>
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">24</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkFrame">
|
<object class="GtkFrame">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
@ -58,7 +61,7 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="valign">center</property>
|
<property name="valign">center</property>
|
||||||
<property name="spacing">10</property>
|
<property name="spacing">12</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkImage" id="cover">
|
<object class="GtkImage" id="cover">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
@ -77,7 +80,27 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="orientation">vertical</property>
|
<property name="orientation">vertical</property>
|
||||||
<property name="spacing">10</property>
|
<property name="spacing">12</property>
|
||||||
|
<child type="center">
|
||||||
|
<object class="GtkLabel" id="description">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="valign">end</property>
|
||||||
|
<property name="label" translatable="yes">Show description</property>
|
||||||
|
<property name="wrap">True</property>
|
||||||
|
<property name="max_width_chars">57</property>
|
||||||
|
<attributes>
|
||||||
|
<attribute name="weight" value="medium"/>
|
||||||
|
</attributes>
|
||||||
|
</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>
|
<child>
|
||||||
<object class="GtkBox">
|
<object class="GtkBox">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
@ -128,6 +151,9 @@
|
|||||||
<property name="receives_default">True</property>
|
<property name="receives_default">True</property>
|
||||||
<property name="halign">center</property>
|
<property name="halign">center</property>
|
||||||
<property name="valign">center</property>
|
<property name="valign">center</property>
|
||||||
|
<style>
|
||||||
|
<class name="destructive-action"/>
|
||||||
|
</style>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">False</property>
|
<property name="expand">False</property>
|
||||||
@ -145,27 +171,6 @@
|
|||||||
<property name="position">0</property>
|
<property name="position">0</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="description">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">False</property>
|
|
||||||
<property name="halign">start</property>
|
|
||||||
<property name="valign">center</property>
|
|
||||||
<property name="label" translatable="yes">Show description</property>
|
|
||||||
<property name="wrap">True</property>
|
|
||||||
<property name="wrap_mode">word-char</property>
|
|
||||||
<property name="max_width_chars">55</property>
|
|
||||||
<attributes>
|
|
||||||
<attribute name="weight" value="medium"/>
|
|
||||||
</attributes>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">True</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
<property name="pack_type">end</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">True</property>
|
<property name="expand">True</property>
|
||||||
@ -176,7 +181,7 @@
|
|||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">False</property>
|
<property name="expand">False</property>
|
||||||
<property name="fill">True</property>
|
<property name="fill">False</property>
|
||||||
<property name="position">0</property>
|
<property name="position">0</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
@ -189,7 +194,6 @@
|
|||||||
<packing>
|
<packing>
|
||||||
<property name="expand">False</property>
|
<property name="expand">False</property>
|
||||||
<property name="fill">True</property>
|
<property name="fill">True</property>
|
||||||
<property name="padding">25</property>
|
|
||||||
<property name="position">0</property>
|
<property name="position">0</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
|
|||||||
@ -21,9 +21,11 @@
|
|||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="halign">center</property>
|
<property name="halign">center</property>
|
||||||
<property name="valign">start</property>
|
<property name="valign">start</property>
|
||||||
|
<property name="margin_top">24</property>
|
||||||
|
<property name="margin_bottom">24</property>
|
||||||
<property name="homogeneous">True</property>
|
<property name="homogeneous">True</property>
|
||||||
<property name="column_spacing">5</property>
|
<property name="column_spacing">12</property>
|
||||||
<property name="row_spacing">5</property>
|
<property name="row_spacing">12</property>
|
||||||
<property name="max_children_per_line">20</property>
|
<property name="max_children_per_line">20</property>
|
||||||
<property name="selection_mode">none</property>
|
<property name="selection_mode">none</property>
|
||||||
</object>
|
</object>
|
||||||
|
|||||||
7
hammond-gtk/resources/gtk/style.css
Normal file
7
hammond-gtk/resources/gtk/style.css
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
row {
|
||||||
|
border-bottom: solid 1px rgba(0,0,0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
@ -4,8 +4,11 @@
|
|||||||
<file preprocess="xml-stripblanks">gtk/episode_widget.ui</file>
|
<file preprocess="xml-stripblanks">gtk/episode_widget.ui</file>
|
||||||
<file preprocess="xml-stripblanks">gtk/show_widget.ui</file>
|
<file preprocess="xml-stripblanks">gtk/show_widget.ui</file>
|
||||||
<file preprocess="xml-stripblanks">gtk/empty_view.ui</file>
|
<file preprocess="xml-stripblanks">gtk/empty_view.ui</file>
|
||||||
|
<file preprocess="xml-stripblanks">gtk/episodes_view.ui</file>
|
||||||
|
<file preprocess="xml-stripblanks">gtk/episodes_view_widget.ui</file>
|
||||||
<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 compressed="true">gtk/style.css</file>
|
||||||
</gresource>
|
</gresource>
|
||||||
</gresources>
|
</gresources>
|
||||||
|
|||||||
@ -6,6 +6,7 @@ use hammond_data::dbqueries;
|
|||||||
|
|
||||||
use views::shows::ShowsPopulated;
|
use views::shows::ShowsPopulated;
|
||||||
use views::empty::EmptyView;
|
use views::empty::EmptyView;
|
||||||
|
use views::episodes::EpisodesView;
|
||||||
|
|
||||||
use widgets::show::ShowWidget;
|
use widgets::show::ShowWidget;
|
||||||
use headerbar::Header;
|
use headerbar::Header;
|
||||||
@ -22,8 +23,8 @@ pub struct Content {
|
|||||||
impl Content {
|
impl Content {
|
||||||
pub fn new(header: Rc<Header>) -> Rc<Content> {
|
pub fn new(header: Rc<Header>) -> Rc<Content> {
|
||||||
let stack = gtk::Stack::new();
|
let stack = gtk::Stack::new();
|
||||||
let shows = ShowStack::new(header);
|
|
||||||
let episodes = EpisodeStack::new();
|
let episodes = EpisodeStack::new();
|
||||||
|
let shows = ShowStack::new(header, episodes.clone());
|
||||||
|
|
||||||
stack.add_titled(&episodes.stack, "episodes", "Episodes");
|
stack.add_titled(&episodes.stack, "episodes", "Episodes");
|
||||||
stack.add_titled(&shows.stack, "shows", "Shows");
|
stack.add_titled(&shows.stack, "shows", "Shows");
|
||||||
@ -49,15 +50,17 @@ impl Content {
|
|||||||
pub struct ShowStack {
|
pub struct ShowStack {
|
||||||
pub stack: gtk::Stack,
|
pub stack: gtk::Stack,
|
||||||
header: Rc<Header>,
|
header: Rc<Header>,
|
||||||
|
epstack: Rc<EpisodeStack>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShowStack {
|
impl ShowStack {
|
||||||
fn new(header: Rc<Header>) -> Rc<ShowStack> {
|
fn new(header: Rc<Header>, epstack: Rc<EpisodeStack>) -> Rc<ShowStack> {
|
||||||
let stack = gtk::Stack::new();
|
let stack = gtk::Stack::new();
|
||||||
|
|
||||||
let show = Rc::new(ShowStack {
|
let show = Rc::new(ShowStack {
|
||||||
stack,
|
stack,
|
||||||
header: header.clone(),
|
header: header.clone(),
|
||||||
|
epstack,
|
||||||
});
|
});
|
||||||
|
|
||||||
let pop = ShowsPopulated::new(show.clone(), header);
|
let pop = ShowsPopulated::new(show.clone(), header);
|
||||||
@ -109,7 +112,12 @@ impl ShowStack {
|
|||||||
|
|
||||||
pub fn replace_widget(&self, pd: &Podcast) {
|
pub fn replace_widget(&self, pd: &Podcast) {
|
||||||
let old = self.stack.get_child_by_name("widget").unwrap();
|
let old = self.stack.get_child_by_name("widget").unwrap();
|
||||||
let new = ShowWidget::new(Rc::new(self.clone()), self.header.clone(), pd);
|
let new = ShowWidget::new(
|
||||||
|
Rc::new(self.clone()),
|
||||||
|
self.epstack.clone(),
|
||||||
|
self.header.clone(),
|
||||||
|
pd,
|
||||||
|
);
|
||||||
|
|
||||||
self.stack.remove(&old);
|
self.stack.remove(&old);
|
||||||
self.stack.add_named(&new.container, "widget");
|
self.stack.add_named(&new.container, "widget");
|
||||||
@ -144,10 +152,7 @@ impl ShowStack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct RecentEpisodes;
|
pub struct EpisodeStack {
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct EpisodeStack {
|
|
||||||
// populated: RecentEpisodes,
|
// populated: RecentEpisodes,
|
||||||
// empty: EmptyView,
|
// empty: EmptyView,
|
||||||
stack: gtk::Stack,
|
stack: gtk::Stack,
|
||||||
@ -155,14 +160,18 @@ struct EpisodeStack {
|
|||||||
|
|
||||||
impl EpisodeStack {
|
impl EpisodeStack {
|
||||||
fn new() -> Rc<EpisodeStack> {
|
fn new() -> Rc<EpisodeStack> {
|
||||||
let _pop = RecentEpisodes {};
|
let episodes = EpisodesView::new();
|
||||||
let empty = EmptyView::new();
|
let empty = EmptyView::new();
|
||||||
let stack = gtk::Stack::new();
|
let stack = gtk::Stack::new();
|
||||||
|
|
||||||
// stack.add_named(&pop.container, "populated");
|
stack.add_named(&episodes.container, "episodes");
|
||||||
stack.add_named(&empty.container, "empty");
|
stack.add_named(&empty.container, "empty");
|
||||||
// FIXME:
|
|
||||||
stack.set_visible_child_name("empty");
|
if episodes.is_empty() {
|
||||||
|
stack.set_visible_child_name("empty");
|
||||||
|
} else {
|
||||||
|
stack.set_visible_child_name("episodes");
|
||||||
|
}
|
||||||
|
|
||||||
Rc::new(EpisodeStack {
|
Rc::new(EpisodeStack {
|
||||||
// empty,
|
// empty,
|
||||||
@ -171,7 +180,19 @@ impl EpisodeStack {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&self) {
|
pub fn update(&self) {
|
||||||
// unimplemented!()
|
let old = self.stack.get_child_by_name("episodes").unwrap();
|
||||||
|
let eps = EpisodesView::new();
|
||||||
|
|
||||||
|
self.stack.remove(&old);
|
||||||
|
self.stack.add_named(&eps.container, "episodes");
|
||||||
|
|
||||||
|
if eps.is_empty() {
|
||||||
|
self.stack.set_visible_child_name("empty");
|
||||||
|
} else {
|
||||||
|
self.stack.set_visible_child_name("episodes");
|
||||||
|
}
|
||||||
|
|
||||||
|
old.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
#![cfg_attr(feature = "cargo-clippy", allow(clone_on_ref_ptr))]
|
#![cfg_attr(feature = "cargo-clippy", allow(clone_on_ref_ptr, needless_pass_by_value))]
|
||||||
|
|
||||||
extern crate gdk;
|
extern crate gdk;
|
||||||
extern crate gdk_pixbuf;
|
extern crate gdk_pixbuf;
|
||||||
@ -11,11 +11,15 @@ extern crate diesel;
|
|||||||
extern crate dissolve;
|
extern crate dissolve;
|
||||||
extern crate hammond_data;
|
extern crate hammond_data;
|
||||||
extern crate hammond_downloader;
|
extern crate hammond_downloader;
|
||||||
|
extern crate humansize;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate lazy_static;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
extern crate loggerv;
|
extern crate loggerv;
|
||||||
extern crate open;
|
extern crate open;
|
||||||
extern crate regex;
|
extern crate regex;
|
||||||
|
extern crate send_cell;
|
||||||
// extern crate rayon;
|
// extern crate rayon;
|
||||||
|
|
||||||
// use rayon::prelude::*;
|
// use rayon::prelude::*;
|
||||||
@ -23,7 +27,7 @@ use log::LogLevel;
|
|||||||
use hammond_data::utils::checkup;
|
use hammond_data::utils::checkup;
|
||||||
|
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gio::{ActionMapExt, ApplicationExt, MenuExt, SimpleActionExt};
|
use gio::ApplicationExt;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
// http://gtk-rs.org/tuto/closures
|
// http://gtk-rs.org/tuto/closures
|
||||||
@ -54,15 +58,9 @@ mod utils;
|
|||||||
mod static_resource;
|
mod static_resource;
|
||||||
|
|
||||||
fn build_ui(app: >k::Application) {
|
fn build_ui(app: >k::Application) {
|
||||||
let menu = gio::Menu::new();
|
|
||||||
menu.append("Quit", "app.quit");
|
|
||||||
menu.append("Checkup", "app.check");
|
|
||||||
menu.append("Update feeds", "app.update");
|
|
||||||
app.set_app_menu(&menu);
|
|
||||||
|
|
||||||
// Get the main window
|
// Get the main window
|
||||||
let window = gtk::ApplicationWindow::new(app);
|
let window = gtk::ApplicationWindow::new(app);
|
||||||
window.set_default_size(1150, 650);
|
window.set_default_size(860, 640);
|
||||||
|
|
||||||
// Get the headerbar
|
// Get the headerbar
|
||||||
let header = Rc::new(headerbar::Header::default());
|
let header = Rc::new(headerbar::Header::default());
|
||||||
@ -76,28 +74,6 @@ fn build_ui(app: >k::Application) {
|
|||||||
Inhibit(false)
|
Inhibit(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup quit in the app menu since default is overwritten.
|
|
||||||
let quit = gio::SimpleAction::new("quit", None);
|
|
||||||
let window2 = window.clone();
|
|
||||||
quit.connect_activate(move |_, _| {
|
|
||||||
window2.destroy();
|
|
||||||
});
|
|
||||||
app.add_action(&quit);
|
|
||||||
|
|
||||||
// Setup the checkup in the app menu.
|
|
||||||
let check = gio::SimpleAction::new("check", None);
|
|
||||||
check.connect_activate(move |_, _| {
|
|
||||||
let _ = checkup();
|
|
||||||
});
|
|
||||||
app.add_action(&check);
|
|
||||||
|
|
||||||
let update = gio::SimpleAction::new("update", None);
|
|
||||||
let ct_clone = ct.clone();
|
|
||||||
update.connect_activate(move |_, _| {
|
|
||||||
utils::refresh_feed(ct_clone.clone(), None);
|
|
||||||
});
|
|
||||||
app.add_action(&update);
|
|
||||||
|
|
||||||
// Update on startup
|
// Update on startup
|
||||||
gtk::timeout_add_seconds(
|
gtk::timeout_add_seconds(
|
||||||
30,
|
30,
|
||||||
@ -106,7 +82,6 @@ fn build_ui(app: >k::Application) {
|
|||||||
glib::Continue(false)
|
glib::Continue(false)
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Auto-updater, runs every hour.
|
// Auto-updater, runs every hour.
|
||||||
// TODO: expose the interval in which it run to a user setting.
|
// TODO: expose the interval in which it run to a user setting.
|
||||||
// TODO: show notifications.
|
// TODO: show notifications.
|
||||||
@ -138,6 +113,15 @@ fn main() {
|
|||||||
let application = gtk::Application::new("org.gnome.Hammond", gio::ApplicationFlags::empty())
|
let application = gtk::Application::new("org.gnome.Hammond", gio::ApplicationFlags::empty())
|
||||||
.expect("Initialization failed...");
|
.expect("Initialization failed...");
|
||||||
|
|
||||||
|
// Add custom style
|
||||||
|
let provider = gtk::CssProvider::new();
|
||||||
|
gtk::CssProvider::load_from_resource(&provider, "/org/gnome/hammond/gtk/style.css");
|
||||||
|
gtk::StyleContext::add_provider_for_screen(
|
||||||
|
&gdk::Screen::get_default().unwrap(),
|
||||||
|
&provider,
|
||||||
|
600,
|
||||||
|
);
|
||||||
|
|
||||||
application.connect_startup(move |app| {
|
application.connect_startup(move |app| {
|
||||||
build_ui(app);
|
build_ui(app);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,14 +1,17 @@
|
|||||||
|
use send_cell::SendCell;
|
||||||
use glib;
|
use glib;
|
||||||
use gdk_pixbuf::Pixbuf;
|
use gdk_pixbuf::Pixbuf;
|
||||||
|
|
||||||
use hammond_data::feed;
|
use hammond_data::feed;
|
||||||
use hammond_data::{Podcast, Source};
|
use hammond_data::{PodcastCoverQuery, Source};
|
||||||
use hammond_downloader::downloader;
|
use hammond_downloader::downloader;
|
||||||
|
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::sync::mpsc::{channel, Receiver};
|
use std::sync::mpsc::{channel, Receiver};
|
||||||
|
use std::sync::Mutex;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use content::Content;
|
use content::Content;
|
||||||
|
|
||||||
@ -59,14 +62,36 @@ fn refresh_podcasts_view() -> glib::Continue {
|
|||||||
glib::Continue(false)
|
glib::Continue(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_pixbuf_from_path(pd: &Podcast) -> Option<Pixbuf> {
|
lazy_static! {
|
||||||
let img_path = downloader::cache_image(pd)?;
|
static ref CACHED_PIXBUFS: Mutex<HashMap<(i32, u32), Mutex<SendCell<Pixbuf>>>> = {
|
||||||
Pixbuf::new_from_file_at_scale(&img_path, 256, 256, true).ok()
|
Mutex::new(HashMap::new())
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_pixbuf_from_path_128(pd: &Podcast) -> Option<Pixbuf> {
|
// Since gdk_pixbuf::Pixbuf is refference counted and every episode,
|
||||||
|
// use the cover of the Podcast Feed/Show, We can only create a Pixbuf
|
||||||
|
// cover per show and pass around the Rc pointer.
|
||||||
|
//
|
||||||
|
// GObjects do not implement Send trait, so SendCell is a way around that.
|
||||||
|
// Also lazy_static requires Sync trait, so that's what the mutexes are.
|
||||||
|
// TODO: maybe use something that would just scale to requested size?
|
||||||
|
pub fn get_pixbuf_from_path(pd: &PodcastCoverQuery, size: u32) -> Option<Pixbuf> {
|
||||||
|
let mut hashmap = CACHED_PIXBUFS.lock().unwrap();
|
||||||
|
{
|
||||||
|
let res = hashmap.get(&(pd.id(), size));
|
||||||
|
if let Some(px) = res {
|
||||||
|
let m = px.lock().unwrap();
|
||||||
|
return Some(m.clone().into_inner());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let img_path = downloader::cache_image(pd)?;
|
let img_path = downloader::cache_image(pd)?;
|
||||||
Pixbuf::new_from_file_at_scale(&img_path, 128, 128, true).ok()
|
let px = Pixbuf::new_from_file_at_scale(&img_path, size as i32, size as i32, true).ok();
|
||||||
|
if let Some(px) = px {
|
||||||
|
hashmap.insert((pd.id(), size), Mutex::new(SendCell::new(px.clone())));
|
||||||
|
return Some(px);
|
||||||
|
}
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -92,7 +117,7 @@ mod tests {
|
|||||||
|
|
||||||
// Get the Podcast
|
// Get the Podcast
|
||||||
let pd = dbqueries::get_podcast_from_source_id(sid).unwrap();
|
let pd = dbqueries::get_podcast_from_source_id(sid).unwrap();
|
||||||
let pxbuf = get_pixbuf_from_path(&pd);
|
let pxbuf = get_pixbuf_from_path(&pd.into(), 256);
|
||||||
assert!(pxbuf.is_some());
|
assert!(pxbuf.is_some());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1 +1,213 @@
|
|||||||
|
use gtk;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use chrono::prelude::*;
|
||||||
|
|
||||||
|
use hammond_data::dbqueries;
|
||||||
|
use hammond_data::EpisodeWidgetQuery;
|
||||||
|
|
||||||
|
use widgets::episode::EpisodeWidget;
|
||||||
|
use utils::get_pixbuf_from_path;
|
||||||
|
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum ListSplit {
|
||||||
|
Today,
|
||||||
|
Yday,
|
||||||
|
Week,
|
||||||
|
Month,
|
||||||
|
Rest,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct EpisodesView {
|
||||||
|
pub container: gtk::Box,
|
||||||
|
frame_parent: gtk::Box,
|
||||||
|
today_box: gtk::Box,
|
||||||
|
yday_box: gtk::Box,
|
||||||
|
week_box: gtk::Box,
|
||||||
|
month_box: gtk::Box,
|
||||||
|
rest_box: gtk::Box,
|
||||||
|
today_list: gtk::ListBox,
|
||||||
|
yday_list: gtk::ListBox,
|
||||||
|
week_list: gtk::ListBox,
|
||||||
|
month_list: gtk::ListBox,
|
||||||
|
rest_list: gtk::ListBox,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EpisodesView {
|
||||||
|
fn default() -> Self {
|
||||||
|
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episodes_view.ui");
|
||||||
|
let container: gtk::Box = builder.get_object("container").unwrap();
|
||||||
|
let frame_parent: gtk::Box = builder.get_object("frame_parent").unwrap();
|
||||||
|
let today_box: gtk::Box = builder.get_object("today_box").unwrap();
|
||||||
|
let yday_box: gtk::Box = builder.get_object("yday_box").unwrap();
|
||||||
|
let week_box: gtk::Box = builder.get_object("week_box").unwrap();
|
||||||
|
let month_box: gtk::Box = builder.get_object("month_box").unwrap();
|
||||||
|
let rest_box: gtk::Box = builder.get_object("rest_box").unwrap();
|
||||||
|
let today_list: gtk::ListBox = builder.get_object("today_list").unwrap();
|
||||||
|
let yday_list: gtk::ListBox = builder.get_object("yday_list").unwrap();
|
||||||
|
let week_list: gtk::ListBox = builder.get_object("week_list").unwrap();
|
||||||
|
let month_list: gtk::ListBox = builder.get_object("month_list").unwrap();
|
||||||
|
let rest_list: gtk::ListBox = builder.get_object("rest_list").unwrap();
|
||||||
|
|
||||||
|
EpisodesView {
|
||||||
|
container,
|
||||||
|
frame_parent,
|
||||||
|
today_box,
|
||||||
|
yday_box,
|
||||||
|
week_box,
|
||||||
|
month_box,
|
||||||
|
rest_box,
|
||||||
|
today_list,
|
||||||
|
yday_list,
|
||||||
|
week_list,
|
||||||
|
month_list,
|
||||||
|
rest_list,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EpisodesView {
|
||||||
|
pub fn new() -> Rc<EpisodesView> {
|
||||||
|
let view = EpisodesView::default();
|
||||||
|
let episodes = dbqueries::get_episodes_widgets_with_limit(100).unwrap();
|
||||||
|
let now_utc = Utc::now();
|
||||||
|
|
||||||
|
episodes.into_iter().for_each(|mut ep| {
|
||||||
|
let viewep = EpisodesViewWidget::new(&mut ep);
|
||||||
|
|
||||||
|
let t = split(&now_utc, i64::from(ep.epoch()));
|
||||||
|
match t {
|
||||||
|
ListSplit::Today => {
|
||||||
|
view.today_list.add(&viewep.container);
|
||||||
|
}
|
||||||
|
ListSplit::Yday => {
|
||||||
|
view.yday_list.add(&viewep.container);
|
||||||
|
}
|
||||||
|
ListSplit::Week => {
|
||||||
|
view.week_list.add(&viewep.container);
|
||||||
|
}
|
||||||
|
ListSplit::Month => {
|
||||||
|
view.month_list.add(&viewep.container);
|
||||||
|
}
|
||||||
|
ListSplit::Rest => {
|
||||||
|
view.rest_list.add(&viewep.container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if view.today_list.get_children().is_empty() {
|
||||||
|
view.today_box.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
if view.yday_list.get_children().is_empty() {
|
||||||
|
view.yday_box.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
if view.week_list.get_children().is_empty() {
|
||||||
|
view.week_box.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
if view.month_list.get_children().is_empty() {
|
||||||
|
view.month_box.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
if view.rest_list.get_children().is_empty() {
|
||||||
|
view.rest_box.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
view.container.show_all();
|
||||||
|
Rc::new(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
if !self.today_list.get_children().is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.yday_list.get_children().is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.week_list.get_children().is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.month_list.get_children().is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.rest_list.get_children().is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split(now: &DateTime<Utc>, epoch: i64) -> ListSplit {
|
||||||
|
let ep = Utc.timestamp(epoch, 0);
|
||||||
|
|
||||||
|
if now.ordinal() == ep.ordinal() && now.year() == ep.year() {
|
||||||
|
ListSplit::Today
|
||||||
|
} else if now.ordinal() == ep.ordinal() + 1 && now.year() == ep.year() {
|
||||||
|
ListSplit::Yday
|
||||||
|
} else if now.iso_week().week() == ep.iso_week().week() && now.year() == ep.year() {
|
||||||
|
ListSplit::Week
|
||||||
|
} else if now.month() == ep.month() && now.year() == ep.year() {
|
||||||
|
ListSplit::Month
|
||||||
|
} else {
|
||||||
|
ListSplit::Rest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct EpisodesViewWidget {
|
||||||
|
container: gtk::Box,
|
||||||
|
image: gtk::Image,
|
||||||
|
episode: gtk::Box,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EpisodesViewWidget {
|
||||||
|
fn default() -> Self {
|
||||||
|
let builder =
|
||||||
|
gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episodes_view_widget.ui");
|
||||||
|
let container: gtk::Box = builder.get_object("container").unwrap();
|
||||||
|
let image: gtk::Image = builder.get_object("cover").unwrap();
|
||||||
|
let ep = EpisodeWidget::default();
|
||||||
|
container.pack_start(&ep.container, true, true, 6);
|
||||||
|
|
||||||
|
EpisodesViewWidget {
|
||||||
|
container,
|
||||||
|
image,
|
||||||
|
episode: ep.container,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EpisodesViewWidget {
|
||||||
|
fn new(episode: &mut EpisodeWidgetQuery) -> EpisodesViewWidget {
|
||||||
|
let builder =
|
||||||
|
gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episodes_view_widget.ui");
|
||||||
|
let container: gtk::Box = builder.get_object("container").unwrap();
|
||||||
|
let image: gtk::Image = builder.get_object("cover").unwrap();
|
||||||
|
|
||||||
|
// FIXME:
|
||||||
|
if let Ok(pd) = dbqueries::get_podcast_cover_from_id(episode.podcast_id()) {
|
||||||
|
let img = get_pixbuf_from_path(&pd, 64);
|
||||||
|
if let Some(i) = img {
|
||||||
|
image.set_from_pixbuf(&i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ep = EpisodeWidget::new(episode);
|
||||||
|
container.pack_start(&ep.container, true, true, 6);
|
||||||
|
|
||||||
|
EpisodesViewWidget {
|
||||||
|
container,
|
||||||
|
image,
|
||||||
|
episode: ep.container,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -111,7 +111,7 @@ impl ShowsChild {
|
|||||||
fn init(&self, pd: &Podcast) {
|
fn init(&self, pd: &Podcast) {
|
||||||
self.container.set_tooltip_text(pd.title());
|
self.container.set_tooltip_text(pd.title());
|
||||||
|
|
||||||
let cover = get_pixbuf_from_path(pd);
|
let cover = get_pixbuf_from_path(&pd.clone().into(), 256);
|
||||||
if let Some(img) = cover {
|
if let Some(img) = cover {
|
||||||
self.cover.set_from_pixbuf(&img);
|
self.cover.set_from_pixbuf(&img);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,6 +5,7 @@ use gtk::prelude::*;
|
|||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
|
|
||||||
use open;
|
use open;
|
||||||
|
use humansize::{file_size_opts as size_opts, FileSize};
|
||||||
|
|
||||||
use hammond_data::dbqueries;
|
use hammond_data::dbqueries;
|
||||||
use hammond_data::{EpisodeWidgetQuery, Podcast};
|
use hammond_data::{EpisodeWidgetQuery, Podcast};
|
||||||
@ -32,9 +33,9 @@ type Foo = RefCell<
|
|||||||
|
|
||||||
thread_local!(static GLOBAL: Foo = RefCell::new(None));
|
thread_local!(static GLOBAL: Foo = RefCell::new(None));
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
struct EpisodeWidget {
|
pub struct EpisodeWidget {
|
||||||
container: gtk::Box,
|
pub container: gtk::Box,
|
||||||
play: gtk::Button,
|
play: gtk::Button,
|
||||||
delete: gtk::Button,
|
delete: gtk::Button,
|
||||||
download: gtk::Button,
|
download: gtk::Button,
|
||||||
@ -45,6 +46,8 @@ struct EpisodeWidget {
|
|||||||
size: gtk::Label,
|
size: gtk::Label,
|
||||||
progress: gtk::ProgressBar,
|
progress: gtk::ProgressBar,
|
||||||
progress_label: gtk::Label,
|
progress_label: gtk::Label,
|
||||||
|
separator1: gtk::Label,
|
||||||
|
separator2: gtk::Label,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for EpisodeWidget {
|
impl Default for EpisodeWidget {
|
||||||
@ -65,6 +68,9 @@ impl Default for EpisodeWidget {
|
|||||||
let size: gtk::Label = builder.get_object("size_label").unwrap();
|
let size: gtk::Label = builder.get_object("size_label").unwrap();
|
||||||
let progress_label: gtk::Label = builder.get_object("progress_label").unwrap();
|
let progress_label: gtk::Label = builder.get_object("progress_label").unwrap();
|
||||||
|
|
||||||
|
let separator1: gtk::Label = builder.get_object("separator1").unwrap();
|
||||||
|
let separator2: gtk::Label = builder.get_object("separator2").unwrap();
|
||||||
|
|
||||||
EpisodeWidget {
|
EpisodeWidget {
|
||||||
container,
|
container,
|
||||||
progress,
|
progress,
|
||||||
@ -77,21 +83,23 @@ impl Default for EpisodeWidget {
|
|||||||
size,
|
size,
|
||||||
date,
|
date,
|
||||||
progress_label,
|
progress_label,
|
||||||
|
separator1,
|
||||||
|
separator2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EpisodeWidget {
|
impl EpisodeWidget {
|
||||||
pub fn new(episode: &mut EpisodeWidgetQuery, pd: &Podcast) -> EpisodeWidget {
|
pub fn new(episode: &mut EpisodeWidgetQuery) -> EpisodeWidget {
|
||||||
let widget = EpisodeWidget::default();
|
let widget = EpisodeWidget::default();
|
||||||
widget.init(episode, pd);
|
widget.init(episode);
|
||||||
widget
|
widget
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: calculate lenght.
|
// TODO: calculate lenght.
|
||||||
// TODO: wire the progress_bar to the downloader.
|
// TODO: wire the progress_bar to the downloader.
|
||||||
// TODO: wire the cancel button.
|
// TODO: wire the cancel button.
|
||||||
fn init(&self, episode: &mut EpisodeWidgetQuery, pd: &Podcast) {
|
fn init(&self, episode: &mut EpisodeWidgetQuery) {
|
||||||
self.title.set_xalign(0.0);
|
self.title.set_xalign(0.0);
|
||||||
self.title.set_text(episode.title());
|
self.title.set_text(episode.title());
|
||||||
|
|
||||||
@ -101,21 +109,43 @@ impl EpisodeWidget {
|
|||||||
.map(|c| c.add_class("dim-label"));
|
.map(|c| c.add_class("dim-label"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let progress = self.progress.clone();
|
// Declare a custom humansize option struct
|
||||||
timeout_add(200, move || {
|
// See: https://docs.rs/humansize/1.0.2/humansize/file_size_opts/struct.FileSizeOpts.html
|
||||||
progress.pulse();
|
let custom_options = size_opts::FileSizeOpts {
|
||||||
glib::Continue(true)
|
divider: size_opts::Kilo::Binary,
|
||||||
});
|
units: size_opts::Kilo::Decimal,
|
||||||
|
decimal_places: 0,
|
||||||
if let Some(size) = episode.length() {
|
decimal_zeroes: 0,
|
||||||
let megabytes = size / 1024 / 1024; // episode.length represents bytes
|
fixed_at: size_opts::FixedAt::No,
|
||||||
self.size.set_text(&format!("{} MB", megabytes))
|
long_units: false,
|
||||||
|
space: true,
|
||||||
|
suffix: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
let date = Utc.timestamp(i64::from(episode.epoch()), 0)
|
if let Some(size) = episode.length() {
|
||||||
.format("%b %e")
|
if size != 0 {
|
||||||
.to_string();
|
let s = size.file_size(custom_options);
|
||||||
self.date.set_text(&date);
|
if let Ok(s) = s {
|
||||||
|
self.size.set_text(&s);
|
||||||
|
self.size.show();
|
||||||
|
self.separator2.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(secs) = episode.duration() {
|
||||||
|
self.duration.set_text(&format!("{} min", secs / 60));
|
||||||
|
self.duration.show();
|
||||||
|
self.separator1.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
let now = Utc::now();
|
||||||
|
let date = Utc.timestamp(i64::from(episode.epoch()), 0);
|
||||||
|
if now.year() == date.year() {
|
||||||
|
self.date.set_text(&date.format("%e %b").to_string());
|
||||||
|
} else {
|
||||||
|
self.date.set_text(&date.format("%e %b %Y").to_string());
|
||||||
|
};
|
||||||
|
|
||||||
// Show or hide the play/delete/download buttons upon widget initialization.
|
// Show or hide the play/delete/download buttons upon widget initialization.
|
||||||
let local_uri = episode.local_uri();
|
let local_uri = episode.local_uri();
|
||||||
@ -147,21 +177,19 @@ impl EpisodeWidget {
|
|||||||
download.show();
|
download.show();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let pd_title = pd.title().to_owned();
|
|
||||||
let play = &self.play;
|
let play = &self.play;
|
||||||
let delete = &self.delete;
|
let delete = &self.delete;
|
||||||
let cancel = &self.cancel;
|
let cancel = &self.cancel;
|
||||||
let progress = &self.progress;
|
let progress = self.progress.clone();
|
||||||
self.download.connect_clicked(
|
self.download.connect_clicked(
|
||||||
clone!(play, delete, episode, cancel, progress => move |dl| {
|
clone!(play, delete, episode, cancel, progress => move |dl| {
|
||||||
on_download_clicked(
|
on_download_clicked(
|
||||||
&pd_title,
|
|
||||||
&mut episode.clone(),
|
&mut episode.clone(),
|
||||||
dl,
|
dl,
|
||||||
&play,
|
&play,
|
||||||
&delete,
|
&delete,
|
||||||
&cancel,
|
&cancel,
|
||||||
&progress
|
progress.clone()
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -170,34 +198,42 @@ impl EpisodeWidget {
|
|||||||
|
|
||||||
// TODO: show notification when dl is finished.
|
// TODO: show notification when dl is finished.
|
||||||
fn on_download_clicked(
|
fn on_download_clicked(
|
||||||
pd_title: &str,
|
|
||||||
ep: &mut EpisodeWidgetQuery,
|
ep: &mut EpisodeWidgetQuery,
|
||||||
download_bttn: >k::Button,
|
download_bttn: >k::Button,
|
||||||
play_bttn: >k::Button,
|
play_bttn: >k::Button,
|
||||||
del_bttn: >k::Button,
|
del_bttn: >k::Button,
|
||||||
cancel_bttn: >k::Button,
|
cancel_bttn: >k::Button,
|
||||||
progress_bar: >k::ProgressBar,
|
progress_bar: gtk::ProgressBar,
|
||||||
) {
|
) {
|
||||||
|
let progress = progress_bar.clone();
|
||||||
|
|
||||||
|
// Start the proggress_bar pulse.
|
||||||
|
timeout_add(200, move || {
|
||||||
|
progress_bar.pulse();
|
||||||
|
glib::Continue(true)
|
||||||
|
});
|
||||||
|
|
||||||
// Create a async channel.
|
// Create a async channel.
|
||||||
let (sender, receiver) = channel();
|
let (sender, receiver) = channel();
|
||||||
|
|
||||||
// Pass the desired arguments into the Local Thread Storage.
|
// Pass the desired arguments into the Local Thread Storage.
|
||||||
GLOBAL.with(
|
GLOBAL.with(
|
||||||
clone!(download_bttn, play_bttn, del_bttn, cancel_bttn, progress_bar => move |global| {
|
clone!(download_bttn, play_bttn, del_bttn, cancel_bttn, progress => move |global| {
|
||||||
*global.borrow_mut() = Some((
|
*global.borrow_mut() = Some((
|
||||||
download_bttn,
|
download_bttn,
|
||||||
play_bttn,
|
play_bttn,
|
||||||
del_bttn,
|
del_bttn,
|
||||||
cancel_bttn,
|
cancel_bttn,
|
||||||
progress_bar,
|
progress,
|
||||||
receiver));
|
receiver));
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
let pd_title = pd_title.to_owned();
|
let pd = dbqueries::get_podcast_from_id(ep.podcast_id()).unwrap();
|
||||||
|
let pd_title = pd.title().to_owned();
|
||||||
let mut ep = ep.clone();
|
let mut ep = ep.clone();
|
||||||
cancel_bttn.show();
|
cancel_bttn.show();
|
||||||
progress_bar.show();
|
progress.show();
|
||||||
download_bttn.hide();
|
download_bttn.hide();
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let download_fold = downloader::get_download_folder(&pd_title).unwrap();
|
let download_fold = downloader::get_download_folder(&pd_title).unwrap();
|
||||||
@ -232,7 +268,9 @@ fn on_play_bttn_clicked(episode_id: i32) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_delete_bttn_clicked(episode_id: i32) {
|
fn on_delete_bttn_clicked(episode_id: i32) {
|
||||||
let mut ep = dbqueries::get_episode_from_rowid(episode_id).unwrap();
|
let mut ep = dbqueries::get_episode_from_rowid(episode_id)
|
||||||
|
.unwrap()
|
||||||
|
.into();
|
||||||
|
|
||||||
let e = delete_local_content(&mut ep);
|
let e = delete_local_content(&mut ep);
|
||||||
if let Err(err) = e {
|
if let Err(err) = e {
|
||||||
@ -270,15 +308,8 @@ pub fn episodes_listbox(pd: &Podcast) -> Result<gtk::ListBox> {
|
|||||||
let list = gtk::ListBox::new();
|
let list = gtk::ListBox::new();
|
||||||
|
|
||||||
episodes.into_iter().for_each(|mut ep| {
|
episodes.into_iter().for_each(|mut ep| {
|
||||||
let widget = EpisodeWidget::new(&mut ep, pd);
|
let widget = EpisodeWidget::new(&mut ep);
|
||||||
list.add(&widget.container);
|
list.add(&widget.container);
|
||||||
|
|
||||||
let sep = gtk::Separator::new(gtk::Orientation::Vertical);
|
|
||||||
sep.set_sensitive(false);
|
|
||||||
sep.set_can_focus(false);
|
|
||||||
|
|
||||||
list.add(&sep);
|
|
||||||
sep.show()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
list.set_vexpand(false);
|
list.set_vexpand(false);
|
||||||
|
|||||||
@ -10,8 +10,8 @@ use hammond_data::utils::replace_extra_spaces;
|
|||||||
use hammond_downloader::downloader;
|
use hammond_downloader::downloader;
|
||||||
|
|
||||||
use widgets::episode::episodes_listbox;
|
use widgets::episode::episodes_listbox;
|
||||||
use utils::get_pixbuf_from_path_128;
|
use utils::get_pixbuf_from_path;
|
||||||
use content::ShowStack;
|
use content::{EpisodeStack, ShowStack};
|
||||||
use headerbar::Header;
|
use headerbar::Header;
|
||||||
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
@ -40,10 +40,6 @@ impl Default for ShowWidget {
|
|||||||
let link: gtk::Button = builder.get_object("link_button").unwrap();
|
let link: gtk::Button = builder.get_object("link_button").unwrap();
|
||||||
let settings: gtk::MenuButton = builder.get_object("settings_button").unwrap();
|
let settings: gtk::MenuButton = builder.get_object("settings_button").unwrap();
|
||||||
|
|
||||||
unsub
|
|
||||||
.get_style_context()
|
|
||||||
.map(|c| c.add_class("destructive-action"));
|
|
||||||
|
|
||||||
ShowWidget {
|
ShowWidget {
|
||||||
container,
|
container,
|
||||||
cover,
|
cover,
|
||||||
@ -57,18 +53,30 @@ impl Default for ShowWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ShowWidget {
|
impl ShowWidget {
|
||||||
pub fn new(shows: Rc<ShowStack>, header: Rc<Header>, pd: &Podcast) -> ShowWidget {
|
pub fn new(
|
||||||
|
shows: Rc<ShowStack>,
|
||||||
|
epstack: Rc<EpisodeStack>,
|
||||||
|
header: Rc<Header>,
|
||||||
|
pd: &Podcast,
|
||||||
|
) -> ShowWidget {
|
||||||
let pdw = ShowWidget::default();
|
let pdw = ShowWidget::default();
|
||||||
pdw.init(shows, header, pd);
|
pdw.init(shows, epstack, header, pd);
|
||||||
pdw
|
pdw
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(&self, shows: Rc<ShowStack>, header: Rc<Header>, pd: &Podcast) {
|
pub fn init(
|
||||||
|
&self,
|
||||||
|
shows: Rc<ShowStack>,
|
||||||
|
epstack: Rc<EpisodeStack>,
|
||||||
|
header: Rc<Header>,
|
||||||
|
pd: &Podcast,
|
||||||
|
) {
|
||||||
WidgetExt::set_name(&self.container, &pd.id().to_string());
|
WidgetExt::set_name(&self.container, &pd.id().to_string());
|
||||||
|
|
||||||
// TODO: should spawn a thread to avoid locking the UI probably.
|
// TODO: should spawn a thread to avoid locking the UI probably.
|
||||||
self.unsub.connect_clicked(clone!(shows, pd => move |bttn| {
|
self.unsub
|
||||||
on_unsub_button_clicked(shows.clone(), &pd, bttn);
|
.connect_clicked(clone!(shows, epstack, pd => move |bttn| {
|
||||||
|
on_unsub_button_clicked(shows.clone(), epstack.clone(), &pd, bttn);
|
||||||
header.switch_to_normal();
|
header.switch_to_normal();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -81,7 +89,7 @@ impl ShowWidget {
|
|||||||
let desc = dissolve::strip_html_tags(pd.description()).join(" ");
|
let desc = dissolve::strip_html_tags(pd.description()).join(" ");
|
||||||
self.description.set_text(&replace_extra_spaces(&desc));
|
self.description.set_text(&replace_extra_spaces(&desc));
|
||||||
|
|
||||||
let img = get_pixbuf_from_path_128(pd);
|
let img = get_pixbuf_from_path(&pd.clone().into(), 128);
|
||||||
if let Some(i) = img {
|
if let Some(i) = img {
|
||||||
self.cover.set_from_pixbuf(&i);
|
self.cover.set_from_pixbuf(&i);
|
||||||
}
|
}
|
||||||
@ -98,7 +106,12 @@ impl ShowWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_unsub_button_clicked(shows: Rc<ShowStack>, pd: &Podcast, unsub_button: >k::Button) {
|
fn on_unsub_button_clicked(
|
||||||
|
shows: Rc<ShowStack>,
|
||||||
|
epstack: Rc<EpisodeStack>,
|
||||||
|
pd: &Podcast,
|
||||||
|
unsub_button: >k::Button,
|
||||||
|
) {
|
||||||
let res = dbqueries::remove_feed(pd);
|
let res = dbqueries::remove_feed(pd);
|
||||||
if res.is_ok() {
|
if res.is_ok() {
|
||||||
info!("{} was removed succesfully.", pd.title());
|
info!("{} was removed succesfully.", pd.title());
|
||||||
@ -116,6 +129,7 @@ fn on_unsub_button_clicked(shows: Rc<ShowStack>, pd: &Podcast, unsub_button: >
|
|||||||
}
|
}
|
||||||
shows.switch_podcasts_animated();
|
shows.switch_podcasts_animated();
|
||||||
shows.update_podcasts();
|
shows.update_podcasts();
|
||||||
|
epstack.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user