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:
Jordan Petridis 2017-12-23 15:05:17 +00:00
commit ef705f9a2a
28 changed files with 1169 additions and 190 deletions

View File

@ -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
View File

@ -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
View File

@ -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:**

View File

@ -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;

View File

@ -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;

View File

@ -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())

View File

@ -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)]

View File

@ -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())?;
} }

View File

@ -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")]

View File

@ -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;

View 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,

View File

@ -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();

View File

@ -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!(

View File

@ -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"]

View File

@ -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>

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,7 @@
row {
border-bottom: solid 1px rgba(0,0,0, 0.1);
}
row:last-child {
border-bottom: none;
}

View File

@ -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>

View File

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

View File

@ -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: &gtk::Application) { fn build_ui(app: &gtk::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: &gtk::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: &gtk::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);
}); });

View File

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

View File

@ -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,
}
}
}

View File

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

View File

@ -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: &gtk::Button, download_bttn: &gtk::Button,
play_bttn: &gtk::Button, play_bttn: &gtk::Button,
del_bttn: &gtk::Button, del_bttn: &gtk::Button,
cancel_bttn: &gtk::Button, cancel_bttn: &gtk::Button,
progress_bar: &gtk::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);

View File

@ -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: &gtk::Button) { fn on_unsub_button_clicked(
shows: Rc<ShowStack>,
epstack: Rc<EpisodeStack>,
pd: &Podcast,
unsub_button: &gtk::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: &gt
} }
shows.switch_podcasts_animated(); shows.switch_podcasts_animated();
shows.update_podcasts(); shows.update_podcasts();
epstack.update();
} }
#[allow(dead_code)] #[allow(dead_code)]