Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85983ec490 | ||
|
|
7abf6bcade | ||
|
|
854581f0bf | ||
|
|
c79a92f3b2 | ||
|
|
4d6c3a67b1 | ||
|
|
83abb5a825 | ||
|
|
d618771125 | ||
|
|
45c9fd308d | ||
|
|
0c00ee1320 | ||
|
|
d332636dd4 | ||
|
|
32654f6cb2 | ||
|
|
14a90e7138 | ||
|
|
dd2366a15e | ||
|
|
370ba2d461 | ||
|
|
ef655ef5e0 | ||
|
|
5cc550c830 | ||
|
|
52cbe67756 | ||
|
|
c910e0af40 | ||
|
|
7a74534285 | ||
|
|
916775462d | ||
|
|
3d98600126 | ||
|
|
7ba834ee8d | ||
|
|
1c527cba03 | ||
|
|
2d7ba7b246 | ||
|
|
4ed70a8011 | ||
|
|
7a3a148359 | ||
|
|
e07e35110d | ||
|
|
a463753c84 | ||
|
|
a946ddfab1 | ||
|
|
af1cb43bd6 | ||
|
|
935d61324f | ||
|
|
10e016f2ea | ||
|
|
154655d571 | ||
|
|
491cd8f01c | ||
|
|
2b711ff04c | ||
|
|
07eadd2364 | ||
|
|
7086afe73d | ||
|
|
f21398357b | ||
|
|
c338802329 | ||
|
|
c3076e748e | ||
|
|
1595256c86 | ||
|
|
0623592f75 | ||
|
|
f2444f151c | ||
|
|
710a3f2552 | ||
|
|
6071c664e7 | ||
|
|
e203815f4f | ||
|
|
c3658080d3 | ||
|
|
8703470010 | ||
|
|
88cc7e6fec | ||
|
|
badcbc32c6 | ||
|
|
daa8f15ce9 | ||
|
|
89ee174ded | ||
|
|
cc03c2407b | ||
|
|
89254025f3 | ||
|
|
f693c986ec | ||
|
|
3c7f3ecb56 | ||
|
|
ef3809ed25 | ||
|
|
f5ddb7107e | ||
|
|
1d32018c02 | ||
|
|
c458b27573 | ||
|
|
524e0bb0a8 | ||
|
|
d525d1fe59 | ||
|
|
bdc6264701 | ||
|
|
7e2640e2d0 | ||
|
|
822a72efde | ||
|
|
192b13e393 | ||
|
|
2497cb31d0 | ||
|
|
446a0ede54 | ||
|
|
3d39638c99 | ||
|
|
69e87d129a | ||
|
|
f7a7510322 | ||
|
|
e9f2ba47f2 | ||
|
|
b2c95e5a73 | ||
|
|
482ed7c3c6 | ||
|
|
e497f73316 | ||
|
|
28965dc2b1 | ||
|
|
5425ca35b3 | ||
|
|
9f191d0ab8 | ||
|
|
c2a3ce5096 | ||
|
|
1af06f2e0d | ||
|
|
2f2f11b7bc | ||
|
|
260e6015a1 | ||
|
|
7696014545 | ||
|
|
2457e95f0e | ||
|
|
327c907463 | ||
|
|
1b558d3b30 | ||
|
|
a0d55417cd | ||
|
|
f182d441d1 | ||
|
|
e14f215793 | ||
|
|
34536f4e21 | ||
|
|
bc2da6e59e | ||
|
|
afdedc7575 | ||
|
|
69a7a9b180 | ||
|
|
a7540583d6 | ||
|
|
a253d7ebf5 | ||
|
|
3886402f8e | ||
|
|
97e402b980 | ||
|
|
93e15af209 | ||
|
|
9f3a5a13b3 | ||
|
|
3a0fb4bdec | ||
|
|
8a460930c6 | ||
|
|
a11c4c9bd2 | ||
|
|
d4b98b5cb2 | ||
|
|
fdf3908494 | ||
|
|
cc052eb450 | ||
|
|
4535c3005d | ||
|
|
030fed6d12 | ||
|
|
fc9579cd51 | ||
|
|
74712b5410 | ||
|
|
05628a2529 | ||
|
|
1bdd2f2f5b | ||
|
|
3af6e103aa | ||
|
|
3dcc20ae86 | ||
|
|
8a18630ae5 | ||
|
|
b87c331b12 | ||
|
|
9da2414e8b | ||
|
|
285291b2ed | ||
|
|
dbbb4e589e | ||
|
|
064879c4ce | ||
|
|
8614922213 | ||
|
|
745ea0ca10 | ||
|
|
99bc80c15b | ||
|
|
3423d854e1 | ||
|
|
483fd090f1 | ||
|
|
82988b6011 | ||
|
|
d1821163c2 | ||
|
|
7de118adeb | ||
|
|
abe7215bc0 | ||
|
|
7b064e63b9 | ||
|
|
f6890c709f | ||
|
|
e4814dbfbc | ||
|
|
8261b32c99 | ||
|
|
9a73520b25 | ||
|
|
10db4f7210 | ||
|
|
94f6fdcbe7 | ||
|
|
bb467b7aba | ||
|
|
3da503cdea | ||
|
|
b062f0a19f | ||
|
|
2a6e0b0e07 | ||
|
|
1558ee2177 | ||
|
|
3d542e5554 | ||
|
|
c61938ba62 | ||
|
|
c856b88008 | ||
|
|
fce3684113 | ||
|
|
ae25dd65bf | ||
|
|
a88a1c5f1f | ||
|
|
0cd678cc1d | ||
|
|
c9bf58af66 | ||
|
|
ed87a00225 | ||
|
|
bcc3608c04 | ||
|
|
038d28779c | ||
|
|
973d47ee05 | ||
|
|
e803e11c81 | ||
|
|
f50c990d93 | ||
|
|
72eef6f104 | ||
|
|
5ccdb5d100 | ||
|
|
4b8fceaa7d | ||
|
|
a24c9b1350 | ||
|
|
8913b7aedb | ||
|
|
978edfc11f | ||
|
|
f4b41d0fd3 | ||
|
|
20162a16a8 | ||
|
|
73f7bfa64b | ||
|
|
159b0d92dd | ||
|
|
02de2059db | ||
|
|
bdf8901dd8 | ||
|
|
2fbc833ebe | ||
|
|
f7b5b35374 | ||
|
|
46bd23cf66 | ||
|
|
6d9dfe6fe1 | ||
|
|
fc48ce9c47 | ||
|
|
3a9a2f4033 | ||
|
|
f0ce0eb653 | ||
|
|
23979b8f22 | ||
|
|
e22a78fac6 | ||
|
|
7690cb1356 | ||
|
|
a96f4c57c9 |
+1
-5
@@ -3,11 +3,7 @@ target/
|
||||
Cargo.lock
|
||||
.vscode
|
||||
*.ui~
|
||||
*.gresource
|
||||
resources.gresource
|
||||
_build
|
||||
vendor/
|
||||
.flatpak-builder/
|
||||
flatpak-build/
|
||||
repo/
|
||||
Makefile
|
||||
.criterion
|
||||
|
||||
+81
-35
@@ -1,61 +1,107 @@
|
||||
stages:
|
||||
# meson uses cargo to do the build
|
||||
# so it's ok to have the tests first.
|
||||
- test
|
||||
# - build
|
||||
- lint
|
||||
|
||||
before_script:
|
||||
- apt-get update -yqq
|
||||
- apt-get install -yqq --no-install-recommends build-essential
|
||||
- apt-get install -yqq --no-install-recommends libgtk-3-dev
|
||||
# - apt-get install -yqq --no-install-recommends meson
|
||||
|
||||
.cargo_test_template: &cargo_test
|
||||
stage: test
|
||||
|
||||
# variables:
|
||||
# RUSTFLAGS: "-C link-dead-code"
|
||||
# RUST_BACKTRACE: "FULL"
|
||||
|
||||
before_script:
|
||||
- apt-get update -yqq
|
||||
- apt-get install -yqq --no-install-recommends build-essential libgtk-3-dev meson
|
||||
|
||||
- mkdir -p .cargo_cache
|
||||
# Only stuff inside the repo directory can be cached
|
||||
# Override the CARGO_HOME variable to force it location
|
||||
- export CARGO_HOME="${PWD}/.cargo_cache"
|
||||
script:
|
||||
- rustc --version && cargo --version
|
||||
- rustc -Vv && cargo -Vv
|
||||
# Force regeneration of gresources regardless of artifacts chage
|
||||
- cd hammond-gtk/resources/ && glib-compile-resources --generate resources.xml && cd ../../
|
||||
|
||||
- cargo build
|
||||
- cargo test --verbose -- --test-threads=1
|
||||
- cargo test --verbose -- --test-threads=1 --ignored
|
||||
- cargo test -- --test-threads=1
|
||||
- cargo test -- --test-threads=1 --ignored
|
||||
|
||||
variables:
|
||||
# RUSTFLAGS: "-C link-dead-code"
|
||||
RUST_BACKTRACE: "FULL"
|
||||
CARGO_HOME: $CI_PROJECT_DIR/cargo
|
||||
cache:
|
||||
# JOB_NAME - Each job will have it's own cache
|
||||
# COMMIT_REF_SLUG = Lowercase name of the branch
|
||||
# ^ Keep diffrerent caches for each branch
|
||||
key: "$CI_JOB_NAME"
|
||||
paths:
|
||||
- target/
|
||||
- .cargo_cache/
|
||||
|
||||
stable:test:
|
||||
rust:stable:
|
||||
# https://hub.docker.com/_/rust/
|
||||
image: "rust"
|
||||
<<: *cargo_test
|
||||
|
||||
# nightly:test:
|
||||
# # https://hub.docker.com/r/rustlang/rust/
|
||||
# image: "rustlang/rust:nightly"
|
||||
# <<: *cargo_test
|
||||
rust:nightly:
|
||||
# https://hub.docker.com/r/rustlang/rust/
|
||||
image: "rustlang/rust:nightly"
|
||||
<<: *cargo_test
|
||||
only:
|
||||
- schedule
|
||||
- web
|
||||
|
||||
flatpak:
|
||||
image: registry.gitlab.com/alatiera/gnome-nightly-oci/rust-bundle:latest
|
||||
stage: test
|
||||
script:
|
||||
- flatpak-builder --stop-at=hammond app org.gnome.Hammond.json
|
||||
# https://gitlab.gnome.org/alatiera/Hammond/issues/55
|
||||
# Force regeneration of gresources regardless of artifacts chage
|
||||
- flatpak-builder --run app org.gnome.Hammond.json glib-compile-resources --sourcedir=hammond-gtk/resources/ hammond-gtk/resources/resources.xml
|
||||
|
||||
# Build the flatpak repo
|
||||
- flatpak-builder --run app org.gnome.Hammond.json meson --prefix=/app --libdir=/app/lib _build
|
||||
- flatpak-builder --run app org.gnome.Hammond.json ninja -C _build install
|
||||
- flatpak-builder --finish-only app org.gnome.Hammond.json
|
||||
- flatpak build-export repo app
|
||||
|
||||
# Create a flatpak bundle
|
||||
- flatpak build-bundle repo hammond-dev.flatpak org.gnome.Hammond
|
||||
# Run the tests
|
||||
# - flatpak-builder --run app org.gnome.Hammond.json cargo test -- --test-threads=1
|
||||
# - flatpak-builder --run app org.gnome.Hammond.json cargo test -- --test-threads=1 --ignored
|
||||
|
||||
artifacts:
|
||||
paths:
|
||||
- hammond-dev.flatpak
|
||||
expire_in: 2 days
|
||||
|
||||
cache:
|
||||
# JOB_NAME - Each job will have it's own cache
|
||||
# COMMIT_REF_SLUG = Lowercase name of the branch
|
||||
# ^ Keep diffrerent caches for each branch
|
||||
key: "$CI_JOB_NAME"
|
||||
paths:
|
||||
- .flatpak-builder/cache/
|
||||
- target/
|
||||
|
||||
# Configure and run rustfmt on nightly
|
||||
# Exits and builds fails if on bad format
|
||||
rustfmt:
|
||||
image: "rustlang/rust:nightly"
|
||||
image: "registry.gitlab.com/alatiera/rustfmt-oci-image/rustfmt:nightly"
|
||||
stage: lint
|
||||
variables:
|
||||
CFG_RELEASE_CHANNEL: "nightly"
|
||||
script:
|
||||
- rustc --version && cargo --version
|
||||
- cargo install rustfmt-nightly --force
|
||||
- rustc -Vv && cargo -Vv
|
||||
- cargo fmt --version
|
||||
- cargo fmt --all -- --write-mode=diff
|
||||
|
||||
# Configure and run clippy on nightly
|
||||
# Only fails on errors atm.
|
||||
# clippy:
|
||||
# image: "rustlang/rust:nightly"
|
||||
# stage: lint
|
||||
# script:
|
||||
# - rustc --version && cargo --version
|
||||
# - 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
|
||||
clippy:
|
||||
image: "rustlang/rust:nightly"
|
||||
stage: lint
|
||||
script:
|
||||
- rustc --version && cargo --version
|
||||
- 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
|
||||
when: manual
|
||||
|
||||
@@ -6,6 +6,32 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
* Downlaoding and loading images now is done asynchronously and is not blocking programs execution.
|
||||
[#7](https://gitlab.gnome.org/alatiera/Hammond/issues/7)
|
||||
* Bold, italics links and some other `html` tags can now be rendered in the Show Description.
|
||||
[#25](https://gitlab.gnome.org/alatiera/Hammond/issues/25)
|
||||
|
||||
## [0.3.1] - 2018-03-28
|
||||
|
||||
* Ability to mark all episodes of a Show as watched.
|
||||
[#47](https://gitlab.gnome.org/alatiera/Hammond/issues/47)
|
||||
* Now you are able to subscribe to itunes™ podcasts by using the itunes link of the show.
|
||||
[#49](https://gitlab.gnome.org/alatiera/Hammond/issues/49)
|
||||
* EpisdeWidget has been reimplemented as a compile time state machine.
|
||||
[!18](https://gitlab.gnome.org/alatiera/Hammond/merge_requests/18)
|
||||
* Content Views no longer scroll horizontally when shrunk bellow their minimum size.
|
||||
[#35](https://gitlab.gnome.org/alatiera/Hammond/issues/35)
|
||||
* Double border aroun the main window was fixed. (Rowan Lewis)
|
||||
[#52](https://gitlab.gnome.org/alatiera/Hammond/issues/52)
|
||||
* Some requests now use the Tor Browser's user agent. (Rowan Lewis)
|
||||
[#53](https://gitlab.gnome.org/alatiera/Hammond/issues/53)
|
||||
* Hammond now remembers the window size and position. (Rowan Lewis)
|
||||
[#50](https://gitlab.gnome.org/alatiera/Hammond/issues/50)
|
||||
* Implemnted the initial work for integrating with GSettings and storing preferences. (Rowan Lewis)
|
||||
[!22](https://gitlab.gnome.org/alatiera/Hammond/merge_requests/22) [!23](https://gitlab.gnome.org/alatiera/Hammond/merge_requests/23)
|
||||
* Shows without episodes now display an empty message similar to EmptyView.
|
||||
[#44](https://gitlab.gnome.org/alatiera/Hammond/issues/44)
|
||||
|
||||
## [0.3.0] - 2018-02-11
|
||||
|
||||
* Tobias Bernard Redesigned the whole Gtk+ client.
|
||||
|
||||
Generated
+719
-497
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@
|
||||
## A Podcast Client for the GNOME Desktop written in Rust.
|
||||
|
||||
[](https://gitlab.gnome.org/alatiera/Hammond/commits/master)
|
||||
[](https://dependencyci.com/github/alatiera/Hammond)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -14,22 +15,10 @@
|
||||
|
||||
## Quick start
|
||||
|
||||
As of January 19 2018, Hammond can be built and run with [Gnome Builder](https://wiki.gnome.org/Apps/Builder) Nightly
|
||||
and any Stable release of Gnome Builder >= 3.28.0.
|
||||
Hammond can be built and run with [Gnome Builder](https://wiki.gnome.org/Apps/Builder) >= 3.28.
|
||||
|
||||
Get Builder [here](https://wiki.gnome.org/Apps/Builder/Downloads)
|
||||
|
||||
Manually:
|
||||
|
||||
The following steps assume you have a working installation of rustc and cargo.
|
||||
If you dont take a look at [rustup.rs](https://rustup.rs/)
|
||||
|
||||
```sh
|
||||
git clone https://gitlab.gnome.org/alatiera/hammond.git
|
||||
cd Hammond/
|
||||
cargo run -p hammond-gtk --release
|
||||
```
|
||||
|
||||
## Broken Feeds
|
||||
|
||||
Found a feed that does not work in Hammond?
|
||||
@@ -44,6 +33,8 @@ Note:
|
||||
|
||||
There isn't much documentation yet, so you will probably have question about parts of the Code.
|
||||
|
||||
## Building
|
||||
|
||||
### Flatpak
|
||||
|
||||
Flatpak is the reccomended way of building and installing Hammond.
|
||||
@@ -54,20 +45,22 @@ Download the `org.gnome.Hammond.json` flatpak manifest from this repo.
|
||||
|
||||
```bash
|
||||
# Add flathub repo
|
||||
flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
flatpak --user remote-add flathub --if-not-exists https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
# Add the gnome-nightly repo
|
||||
flatpak --user remote-add gnome-nightly https://sdk.gnome.org/gnome-nightly.flatpakrepo
|
||||
flatpak --user remote-add gnome-nightly --if-not-exists https://sdk.gnome.org/gnome-nightly.flatpakrepo
|
||||
# Install the gnome-nightly Sdk and Platform runtim
|
||||
flatpak --user install gnome-nightly org.gnome.Sdk org.gnome.Platform
|
||||
# Install the required rust-stable extension from flathub
|
||||
flatpak --user install flathub org.freedesktop.Sdk.Extension.rust-stable
|
||||
flatpak-builder --repo=repo hammond org.gnome.Hammond.json --force-clean
|
||||
flatpak-builder --user --repo=repo hammond org.gnome.Hammond.json --force-clean
|
||||
flatpak build-bundle repo hammond org.gnome.Hammond
|
||||
```
|
||||
|
||||
## Building from soure
|
||||
### Building from soure
|
||||
|
||||
```sh
|
||||
git clone https://gitlab.gnome.org/alatiera/hammond.git
|
||||
cd Hammond/
|
||||
cd hammond/
|
||||
./configure --prefix=/usr/local
|
||||
make && sudo make install
|
||||
```
|
||||
@@ -76,33 +69,31 @@ make && sudo make install
|
||||
|
||||
You can run `sudo make uninstall` for removal
|
||||
|
||||
### Dependencies
|
||||
#### Dependencies
|
||||
|
||||
* Rust stable 1.22 or later.
|
||||
* Rust stable 1.22 or later along with cargo.
|
||||
* Gtk+ 3.22 or later
|
||||
* Meson
|
||||
* A network connection
|
||||
|
||||
Offline build are possible too, but [`cargo-vendor`][vendor] would have to be setup first
|
||||
|
||||
**Debian/Ubuntu**
|
||||
|
||||
```sh
|
||||
apt-get update -yqq
|
||||
apt-get install -yqq --no-install-recommends build-essential
|
||||
apt-get install -yqq --no-install-recommends libgtk-3-dev meson
|
||||
apt-get install -yqq --no-install-recommends rustc cargo libgtk-3-dev meson
|
||||
```
|
||||
|
||||
**Fedora**
|
||||
|
||||
```sh
|
||||
dnf install -y gtk3-devel glib2-devel openssl-devel sqlite-devel meson
|
||||
dnf install -y rust cargo gtk3-devel glib2-devel openssl-devel sqlite-devel meson
|
||||
```
|
||||
|
||||
If you happen to build it on other distributions please let me know the names of the corresponding libraries. Feel free to open a PR or an Issue to note it.
|
||||
|
||||
```sh
|
||||
git clone https://gitlab.gnome.org/alatiera/Hammond.git
|
||||
cd Hammond/
|
||||
cargo build --all
|
||||
```
|
||||
If you happen to build it on other distributions please let me know the names
|
||||
of the corresponding libraries. Feel free to open a MR or an Issue to note it.
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -151,3 +142,5 @@ Hammond's design is heavily insired by [GNOME Music](https://wiki.gnome.org/Desi
|
||||
We also copied some elements from [GNOME News](https://wiki.gnome.org/Design/Apps/Potential/News).
|
||||
|
||||
And almost the entirety of the build system is copied from the [Fractal](https://gitlab.gnome.org/danigm/fractal) project.
|
||||
|
||||
[vendor]: https://github.com/alexcrichton/cargo-vendor
|
||||
+13
-15
@@ -5,25 +5,22 @@ version = "0.1.0"
|
||||
workspace = "../"
|
||||
|
||||
[dependencies]
|
||||
ammonia = "1.0.1"
|
||||
chrono = "0.4.0"
|
||||
ammonia = "1.1.0"
|
||||
chrono = "0.4.1"
|
||||
derive_builder = "0.5.1"
|
||||
dotenv = "0.10.1"
|
||||
error-chain = "0.11.0"
|
||||
itertools = "0.7.6"
|
||||
itertools = "0.7.8"
|
||||
lazy_static = "1.0.0"
|
||||
log = "0.4.1"
|
||||
rayon = "0.9.0"
|
||||
rayon = "1.0.1"
|
||||
rfc822_sanitizer = "0.3.3"
|
||||
rss = "1.2.1"
|
||||
url = "1.6.0"
|
||||
rss = "1.4.0"
|
||||
url = "1.7.0"
|
||||
xdg = "2.1.0"
|
||||
futures = "0.1.18"
|
||||
hyper = "0.11.18"
|
||||
tokio-core = "0.1.12"
|
||||
hyper-tls = "0.1.2"
|
||||
futures = "0.1.21"
|
||||
hyper = "0.11.24"
|
||||
tokio-core = "0.1.16"
|
||||
hyper-tls = "0.1.3"
|
||||
native-tls = "0.1.5"
|
||||
futures-cpupool = "0.1.8"
|
||||
num_cpus = "1.8.0"
|
||||
failure = "0.1.1"
|
||||
failure_derive = "0.1.1"
|
||||
@@ -38,8 +35,9 @@ version = "1.1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.4.2"
|
||||
tempdir = "0.3.6"
|
||||
criterion = "0.2.0"
|
||||
tempdir = "0.3.7"
|
||||
criterion = "0.2.2"
|
||||
pretty_assertions = "0.5.1"
|
||||
|
||||
[[bench]]
|
||||
name = "bench"
|
||||
|
||||
@@ -3,7 +3,6 @@ extern crate criterion;
|
||||
use criterion::Criterion;
|
||||
|
||||
// extern crate futures;
|
||||
// extern crate futures_cpupool;
|
||||
extern crate hammond_data;
|
||||
extern crate hyper;
|
||||
extern crate hyper_tls;
|
||||
@@ -15,7 +14,6 @@ extern crate rss;
|
||||
// use rayon::prelude::*;
|
||||
|
||||
// use futures::future::*;
|
||||
// use futures_cpupool::CpuPool;
|
||||
use tokio_core::reactor::Core;
|
||||
|
||||
use hammond_data::FeedBuilder;
|
||||
|
||||
@@ -16,7 +16,7 @@ type Pool = r2d2::Pool<ConnectionManager<SqliteConnection>>;
|
||||
|
||||
embed_migrations!("migrations/");
|
||||
|
||||
lazy_static!{
|
||||
lazy_static! {
|
||||
static ref POOL: Pool = init_pool(DB_PATH.to_str().unwrap());
|
||||
}
|
||||
|
||||
@@ -30,10 +30,7 @@ extern crate tempdir;
|
||||
|
||||
#[cfg(test)]
|
||||
lazy_static! {
|
||||
static ref TEMPDIR: tempdir::TempDir = {
|
||||
tempdir::TempDir::new("hammond_unit_test").unwrap()
|
||||
};
|
||||
|
||||
static ref TEMPDIR: tempdir::TempDir = { tempdir::TempDir::new("hammond_unit_test").unwrap() };
|
||||
static ref DB_PATH: PathBuf = TEMPDIR.path().join("hammond.db");
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,18 @@ pub fn get_podcasts() -> Result<Vec<Podcast>, DataError> {
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
pub fn get_podcasts_filter(filter_ids: &[i32]) -> Result<Vec<Podcast>, DataError> {
|
||||
use schema::podcast::dsl::*;
|
||||
let db = connection();
|
||||
let con = db.get()?;
|
||||
|
||||
podcast
|
||||
.order(title.asc())
|
||||
.filter(id.ne_any(filter_ids))
|
||||
.load::<Podcast>(&con)
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
pub fn get_episodes() -> Result<Vec<Episode>, DataError> {
|
||||
use schema::episode::dsl::*;
|
||||
let db = connection();
|
||||
@@ -102,7 +114,10 @@ pub fn get_episode_local_uri_from_id(ep_id: i32) -> Result<Option<String>, DataE
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
pub fn get_episodes_widgets_with_limit(limit: u32) -> Result<Vec<EpisodeWidgetQuery>, DataError> {
|
||||
pub fn get_episodes_widgets_filter_limit(
|
||||
filter_ids: &[i32],
|
||||
limit: u32,
|
||||
) -> Result<Vec<EpisodeWidgetQuery>, DataError> {
|
||||
use schema::episode;
|
||||
let db = connection();
|
||||
let con = db.get()?;
|
||||
@@ -120,6 +135,7 @@ pub fn get_episodes_widgets_with_limit(limit: u32) -> Result<Vec<EpisodeWidgetQu
|
||||
episode::podcast_id,
|
||||
))
|
||||
.order(episode::epoch.desc())
|
||||
.filter(episode::podcast_id.ne_any(filter_ids))
|
||||
.limit(i64::from(limit))
|
||||
.load::<EpisodeWidgetQuery>(&con)
|
||||
.map_err(From::from)
|
||||
@@ -159,13 +175,23 @@ pub fn get_pd_episodes(parent: &Podcast) -> Result<Vec<Episode>, DataError> {
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
pub fn get_pd_episodeswidgets(parent: &Podcast) -> Result<Vec<EpisodeWidgetQuery>, DataError> {
|
||||
pub fn get_pd_episodes_count(parent: &Podcast) -> Result<i64, DataError> {
|
||||
let db = connection();
|
||||
let con = db.get()?;
|
||||
|
||||
Episode::belonging_to(parent)
|
||||
.count()
|
||||
.get_result(&con)
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
pub fn get_pd_episodeswidgets(_parent: &Podcast) -> Result<Vec<EpisodeWidgetQuery>, DataError> {
|
||||
use schema::episode::dsl::*;
|
||||
let db = connection();
|
||||
let con = db.get()?;
|
||||
|
||||
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)
|
||||
.order(epoch.desc())
|
||||
.load::<EpisodeWidgetQuery>(&con)
|
||||
@@ -348,3 +374,29 @@ pub fn update_none_to_played_now(parent: &Podcast) -> Result<usize, DataError> {
|
||||
.map_err(From::from)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use database::*;
|
||||
use pipeline::*;
|
||||
|
||||
#[test]
|
||||
fn test_update_none_to_played_now() {
|
||||
truncate_db().unwrap();
|
||||
|
||||
let url = "https://web.archive.org/web/20180120083840if_/https://feeds.feedburner.\
|
||||
com/InterceptedWithJeremyScahill";
|
||||
let source = Source::from_url(url).unwrap();
|
||||
let id = source.id();
|
||||
index_single_source(source, true).unwrap();
|
||||
let pd = get_podcast_from_source_id(id).unwrap();
|
||||
|
||||
let eps_num = get_pd_unplayed_episodes(&pd).unwrap().len();
|
||||
assert_ne!(eps_num, 0);
|
||||
|
||||
update_none_to_played_now(&pd).unwrap();
|
||||
let eps_num2 = get_pd_unplayed_episodes(&pd).unwrap().len();
|
||||
assert_eq!(eps_num2, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,10 @@ use diesel::r2d2;
|
||||
use diesel_migrations::RunMigrationsError;
|
||||
use hyper;
|
||||
use native_tls;
|
||||
// use rss;
|
||||
use rss;
|
||||
use url;
|
||||
|
||||
use std::io;
|
||||
// use std::fmt;
|
||||
|
||||
// fadsadfs NOT SYNC
|
||||
// #[derive(Fail, Debug)]
|
||||
// #[fail(display = "RSS Error: {}", _0)]
|
||||
// struct RSSError(rss::Error);
|
||||
|
||||
#[derive(Fail, Debug)]
|
||||
pub enum DataError {
|
||||
@@ -34,8 +28,7 @@ pub enum DataError {
|
||||
#[fail(display = "IO Error: {}", _0)]
|
||||
IOError(#[cause] io::Error),
|
||||
#[fail(display = "RSS Error: {}", _0)]
|
||||
// Rss::Error is not yet Sync
|
||||
RssCrateError(String),
|
||||
RssError(#[cause] rss::Error),
|
||||
#[fail(display = "Error: {}", _0)]
|
||||
Bail(String),
|
||||
#[fail(display = "Request to {} returned {}. Context: {}", url, status_code, context)]
|
||||
@@ -100,6 +93,12 @@ impl From<io::Error> for DataError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<rss::Error> for DataError {
|
||||
fn from(err: rss::Error) -> Self {
|
||||
DataError::RssError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for DataError {
|
||||
fn from(err: String) -> Self {
|
||||
DataError::Bail(err)
|
||||
|
||||
@@ -96,7 +96,7 @@ impl Feed {
|
||||
// I am not sure what the optimizations are on match vs allocating None.
|
||||
.map(|fut| {
|
||||
fut.and_then(|x| match x {
|
||||
IndexState::NotChanged => return Err(DataError::EpisodeNotChanged),
|
||||
IndexState::NotChanged => Err(DataError::EpisodeNotChanged),
|
||||
_ => Ok(x),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
|
||||
//! FIXME: Docs
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
extern crate pretty_assertions;
|
||||
|
||||
#[macro_use]
|
||||
extern crate derive_builder;
|
||||
#[macro_use]
|
||||
@@ -38,7 +42,6 @@ extern crate log;
|
||||
extern crate ammonia;
|
||||
extern crate chrono;
|
||||
extern crate futures;
|
||||
extern crate futures_cpupool;
|
||||
extern crate hyper;
|
||||
extern crate hyper_tls;
|
||||
extern crate itertools;
|
||||
|
||||
@@ -32,7 +32,8 @@ pub struct Episode {
|
||||
}
|
||||
|
||||
impl Save<Episode, DataError> for Episode {
|
||||
/// Helper method to easily save/"sync" current state of self to the Database.
|
||||
/// Helper method to easily save/"sync" current state of self to the
|
||||
/// Database.
|
||||
fn save(&self) -> Result<Episode, DataError> {
|
||||
let db = connection();
|
||||
let tempdb = db.get()?;
|
||||
@@ -224,7 +225,8 @@ impl From<Episode> for EpisodeWidgetQuery {
|
||||
}
|
||||
|
||||
impl Save<usize, DataError> for EpisodeWidgetQuery {
|
||||
/// Helper method to easily save/"sync" current state of self to the Database.
|
||||
/// Helper method to easily save/"sync" current state of self to the
|
||||
/// Database.
|
||||
fn save(&self) -> Result<usize, DataError> {
|
||||
use schema::episode::dsl::*;
|
||||
|
||||
@@ -362,7 +364,8 @@ pub struct EpisodeCleanerQuery {
|
||||
}
|
||||
|
||||
impl Save<usize, DataError> for EpisodeCleanerQuery {
|
||||
/// Helper method to easily save/"sync" current state of self to the Database.
|
||||
/// Helper method to easily save/"sync" current state of self to the
|
||||
/// Database.
|
||||
fn save(&self) -> Result<usize, DataError> {
|
||||
use schema::episode::dsl::*;
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ pub trait Index<T, E>: Insert<T, E> + Update<T, E> {
|
||||
|
||||
/// FIXME: DOCS
|
||||
pub trait Save<T, E> {
|
||||
/// Helper method to easily save/"sync" current state of a diesel model to the Database.
|
||||
/// Helper method to easily save/"sync" current state of a diesel model to
|
||||
/// the Database.
|
||||
fn save(&self) -> Result<T, E>;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use errors::DataError;
|
||||
use models::{Episode, EpisodeMinimal, Index, Insert, Update};
|
||||
use parser;
|
||||
use schema::episode;
|
||||
use utils::{replace_extra_spaces, url_cleaner};
|
||||
use utils::url_cleaner;
|
||||
|
||||
#[derive(Insertable, AsChangeset)]
|
||||
#[table_name = "episode"]
|
||||
@@ -74,7 +74,8 @@ impl Update<(), DataError> for NewEpisode {
|
||||
}
|
||||
|
||||
impl Index<(), DataError> for NewEpisode {
|
||||
// Does not update the episode description if it's the only thing that has changed.
|
||||
// Does not update the episode description if it's the only thing that has
|
||||
// changed.
|
||||
fn index(&self) -> Result<(), DataError> {
|
||||
let exists = dbqueries::episode_exists(self.title(), self.podcast_id())?;
|
||||
|
||||
@@ -185,7 +186,7 @@ impl NewEpisodeMinimal {
|
||||
pub(crate) fn new(item: &rss::Item, parent_id: i32) -> Result<Self, DataError> {
|
||||
if item.title().is_none() {
|
||||
let err = DataError::ParseEpisodeError {
|
||||
reason: format!("No title specified for this Episode."),
|
||||
reason: "No title specified for this Episode.".into(),
|
||||
parent_id,
|
||||
};
|
||||
|
||||
@@ -195,13 +196,16 @@ impl NewEpisodeMinimal {
|
||||
let title = item.title().unwrap().trim().to_owned();
|
||||
let guid = item.guid().map(|s| s.value().trim().to_owned());
|
||||
|
||||
let uri = if let Some(url) = item.enclosure().map(|s| url_cleaner(s.url())) {
|
||||
Some(url)
|
||||
} else if item.link().is_some() {
|
||||
item.link().map(|s| url_cleaner(s))
|
||||
} else {
|
||||
let uri = item.enclosure()
|
||||
.map(|s| url_cleaner(s.url()))
|
||||
// Fallback to Rss.Item.link if enclosure is None.
|
||||
.or_else(|| item.link().map(|s| url_cleaner(s)));
|
||||
|
||||
// If url is still None return an Error as this behaviour is
|
||||
// compliant with the RSS Spec.
|
||||
if uri.is_none() {
|
||||
let err = DataError::ParseEpisodeError {
|
||||
reason: format!("No url specified for the item."),
|
||||
reason: "No url specified for the item.".into(),
|
||||
parent_id,
|
||||
};
|
||||
|
||||
@@ -211,7 +215,7 @@ impl NewEpisodeMinimal {
|
||||
// Default to rfc2822 represantation of epoch 0.
|
||||
let date = parse_rfc822(item.pub_date().unwrap_or("Thu, 1 Jan 1970 00:00:00 +0000"));
|
||||
// Should treat information from the rss feeds as invalid by default.
|
||||
// Case: Thu, 05 Aug 2016 06:00:00 -0400 <-- Actually that was friday.
|
||||
// Case: "Thu, 05 Aug 2016 06:00:00 -0400" <-- Actually that was friday.
|
||||
let epoch = date.map(|x| x.timestamp() as i32).unwrap_or(0);
|
||||
|
||||
let duration = parser::parse_itunes_duration(item.itunes_ext());
|
||||
@@ -227,18 +231,10 @@ impl NewEpisodeMinimal {
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
// TODO: TryInto is stabilizing in rustc v1.26!
|
||||
pub(crate) fn into_new_episode(self, item: &rss::Item) -> NewEpisode {
|
||||
let length = || -> Option<i32> { item.enclosure().map(|x| x.length().parse().ok())? }();
|
||||
|
||||
// Prefer itunes summary over rss.description since many feeds put html into
|
||||
// rss.description.
|
||||
let summary = item.itunes_ext().map(|s| s.summary()).and_then(|s| s);
|
||||
let description = if summary.is_some() {
|
||||
summary.map(|s| replace_extra_spaces(&ammonia::clean(s)))
|
||||
} else {
|
||||
item.description()
|
||||
.map(|s| replace_extra_spaces(&ammonia::clean(s)))
|
||||
};
|
||||
let length = item.enclosure().and_then(|x| x.length().parse().ok());
|
||||
let description = item.description().map(|s| ammonia::clean(s));
|
||||
|
||||
NewEpisodeBuilder::default()
|
||||
.title(self.title)
|
||||
@@ -310,7 +306,6 @@ mod tests {
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
static ref EXPECTED_MINIMAL_INTERCEPTED_2: NewEpisodeMinimal = {
|
||||
NewEpisodeMinimalBuilder::default()
|
||||
.title("Atlas Golfed — U.S.-Backed Think Tanks Target Latin America")
|
||||
@@ -324,13 +319,12 @@ mod tests {
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
static ref EXPECTED_INTERCEPTED_1: NewEpisode = {
|
||||
let descr = "NSA whistleblower Edward Snowden discusses the massive Equifax data breach \
|
||||
and allegations of Russian interference in the US election. Commentator \
|
||||
Shaun King explains his call for a boycott of the NFL and talks about his \
|
||||
campaign to bring violent neo-Nazis to justice. Rapper Open Mike Eagle \
|
||||
performs.";
|
||||
let descr = "NSA whistleblower Edward Snowden discusses the massive Equifax data \
|
||||
breach and allegations of Russian interference in the US election. \
|
||||
Commentator Shaun King explains his call for a boycott of the NFL and \
|
||||
talks about his campaign to bring violent neo-Nazis to justice. Rapper \
|
||||
Open Mike Eagle performs.";
|
||||
|
||||
NewEpisodeBuilder::default()
|
||||
.title("The Super Bowl of Racism")
|
||||
@@ -346,16 +340,15 @@ mod tests {
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
static ref EXPECTED_INTERCEPTED_2: NewEpisode = {
|
||||
let descr = "This week on Intercepted: Jeremy gives an update on the aftermath of \
|
||||
Blackwater’s 2007 massacre of Iraqi civilians. Intercept reporter Lee Fang \
|
||||
lays out how a network of libertarian think tanks called the Atlas Network \
|
||||
is insidiously shaping political infrastructure in Latin America. We speak \
|
||||
with attorney and former Hugo Chavez adviser Eva Golinger about the \
|
||||
Venezuela\'s political turmoil.And we hear Claudia Lizardo of the \
|
||||
Caracas-based band, La Pequeña Revancha, talk about her music and hopes for \
|
||||
Venezuela.";
|
||||
Blackwater’s 2007 massacre of Iraqi civilians. Intercept reporter Lee \
|
||||
Fang lays out how a network of libertarian think tanks called the Atlas \
|
||||
Network is insidiously shaping political infrastructure in Latin \
|
||||
America. We speak with attorney and former Hugo Chavez adviser Eva \
|
||||
Golinger about the Venezuela\'s political turmoil.And we hear Claudia \
|
||||
Lizardo of the Caracas-based band, La Pequeña Revancha, talk about her \
|
||||
music and hopes for Venezuela.";
|
||||
|
||||
NewEpisodeBuilder::default()
|
||||
.title("Atlas Golfed — U.S.-Backed Think Tanks Target Latin America")
|
||||
@@ -371,7 +364,6 @@ mod tests {
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
static ref UPDATED_DURATION_INTERCEPTED_1: NewEpisode = {
|
||||
NewEpisodeBuilder::default()
|
||||
.title("The Super Bowl of Racism")
|
||||
@@ -387,7 +379,6 @@ mod tests {
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
static ref EXPECTED_MINIMAL_LUP_1: NewEpisodeMinimal = {
|
||||
NewEpisodeMinimalBuilder::default()
|
||||
.title("Hacking Devices with Kali Linux | LUP 214")
|
||||
@@ -401,7 +392,6 @@ mod tests {
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
static ref EXPECTED_MINIMAL_LUP_2: NewEpisodeMinimal = {
|
||||
NewEpisodeMinimalBuilder::default()
|
||||
.title("Gnome Does it Again | LUP 213")
|
||||
@@ -415,12 +405,11 @@ mod tests {
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
static ref EXPECTED_LUP_1: NewEpisode = {
|
||||
let descr = "Audit your network with a couple of easy commands on Kali Linux. Chris \
|
||||
decides to blow off a little steam by attacking his IoT devices, Wes has the \
|
||||
scope on Equifax blaming open source & the Beard just saved the show. \
|
||||
It’s a really packed episode!";
|
||||
decides to blow off a little steam by attacking his IoT devices, Wes has \
|
||||
the scope on Equifax blaming open source & the Beard just saved the \
|
||||
show. It’s a really packed episode!";
|
||||
|
||||
NewEpisodeBuilder::default()
|
||||
.title("Hacking Devices with Kali Linux | LUP 214")
|
||||
@@ -436,13 +425,13 @@ mod tests {
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
static ref EXPECTED_LUP_2: NewEpisode = {
|
||||
let descr = "The Gnome project is about to solve one of our audience's biggest Wayland’s \
|
||||
concerns. But as the project takes on a new level of relevance, decisions for the \
|
||||
next version of Gnome have us worried about the future.\nPlus we chat with Wimpy \
|
||||
about the Ubuntu Rally in NYC, Microsoft’s sneaky move to turn Windows 10 into the \
|
||||
“ULTIMATE LINUX RUNTIME”, community news & more!";
|
||||
let descr =
|
||||
"<p>The Gnome project is about to solve one of our audience's biggest Wayland’s \
|
||||
concerns. But as the project takes on a new level of relevance, decisions for \
|
||||
the next version of Gnome have us worried about the future.</p>\n\n<p>Plus we \
|
||||
chat with Wimpy about the Ubuntu Rally in NYC, Microsoft’s sneaky move to turn \
|
||||
Windows 10 into the “ULTIMATE LINUX RUNTIME”, community news & more!</p>";
|
||||
|
||||
NewEpisodeBuilder::default()
|
||||
.title("Gnome Does it Again | LUP 213")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use ammonia;
|
||||
use diesel;
|
||||
use diesel::prelude::*;
|
||||
|
||||
use ammonia;
|
||||
use rss;
|
||||
|
||||
use errors::DataError;
|
||||
@@ -11,7 +10,7 @@ use schema::podcast;
|
||||
|
||||
use database::connection;
|
||||
use dbqueries;
|
||||
use utils::{replace_extra_spaces, url_cleaner};
|
||||
use utils::url_cleaner;
|
||||
|
||||
#[derive(Insertable, AsChangeset)]
|
||||
#[table_name = "podcast"]
|
||||
@@ -90,22 +89,15 @@ impl NewPodcast {
|
||||
pub(crate) fn new(chan: &rss::Channel, source_id: i32) -> NewPodcast {
|
||||
let title = chan.title().trim();
|
||||
|
||||
// Prefer itunes summary over rss.description since many feeds put html into
|
||||
// rss.description.
|
||||
let summary = chan.itunes_ext().map(|s| s.summary()).and_then(|s| s);
|
||||
let description = if let Some(sum) = summary {
|
||||
replace_extra_spaces(&ammonia::clean(sum))
|
||||
} else {
|
||||
replace_extra_spaces(&ammonia::clean(chan.description()))
|
||||
};
|
||||
|
||||
let description = ammonia::clean(chan.description().trim());
|
||||
let link = url_cleaner(chan.link());
|
||||
let x = chan.itunes_ext().map(|s| s.image());
|
||||
let image_uri = if let Some(img) = x {
|
||||
img.map(|s| s.to_owned())
|
||||
} else {
|
||||
chan.image().map(|foo| foo.url().to_owned())
|
||||
};
|
||||
|
||||
// Try to get the itunes img first
|
||||
let itunes_img = chan.itunes_ext()
|
||||
.and_then(|s| s.image())
|
||||
.map(|s| s.to_owned());
|
||||
// If itunes is None, try to get the channel.image from the rss spec
|
||||
let image_uri = itunes_img.or_else(|| chan.image().map(|s| s.url().to_owned()));
|
||||
|
||||
NewPodcastBuilder::default()
|
||||
.title(title)
|
||||
@@ -162,13 +154,14 @@ mod tests {
|
||||
use std::io::BufReader;
|
||||
|
||||
// Pre-built expected NewPodcast structs.
|
||||
lazy_static!{
|
||||
lazy_static! {
|
||||
static ref EXPECTED_INTERCEPTED: NewPodcast = {
|
||||
let descr = "The people behind The Intercept’s fearless reporting and incisive \
|
||||
commentary—Jeremy Scahill, Glenn Greenwald, Betsy Reed and others—discuss \
|
||||
the crucial issues of our time: national security, civil liberties, foreign \
|
||||
policy, and criminal justice. Plus interviews with artists, thinkers, and \
|
||||
newsmakers who challenge our preconceptions about the world we live in.";
|
||||
commentary—Jeremy Scahill, Glenn Greenwald, Betsy Reed and \
|
||||
others—discuss the crucial issues of our time: national security, civil \
|
||||
liberties, foreign policy, and criminal justice. Plus interviews with \
|
||||
artists, thinkers, and newsmakers who challenge our preconceptions about \
|
||||
the world we live in.";
|
||||
|
||||
NewPodcastBuilder::default()
|
||||
.title("Intercepted with Jeremy Scahill")
|
||||
@@ -183,11 +176,10 @@ mod tests {
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
static ref EXPECTED_LUP: NewPodcast = {
|
||||
let descr = "An open show powered by community LINUX Unplugged takes the best attributes \
|
||||
of open collaboration and focuses them into a weekly lifestyle show about \
|
||||
Linux.";
|
||||
let descr = "An open show powered by community LINUX Unplugged takes the best \
|
||||
attributes of open collaboration and focuses them into a weekly \
|
||||
lifestyle show about Linux.";
|
||||
|
||||
NewPodcastBuilder::default()
|
||||
.title("LINUX Unplugged Podcast")
|
||||
@@ -200,17 +192,16 @@ mod tests {
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
static ref EXPECTED_TIPOFF: NewPodcast = {
|
||||
let desc = "Welcome to The Tip Off- the podcast where we take you behind the scenes of \
|
||||
some of the best investigative journalism from recent years. Each episode \
|
||||
we’ll be digging into an investigative scoop- hearing from the journalists \
|
||||
behind the work as they tell us about the leads, the dead-ends and of course, \
|
||||
the tip offs. There’ll be car chases, slammed doors, terrorist cells, \
|
||||
meetings in dimly lit bars and cafes, wrangling with despotic regimes and \
|
||||
much more. So if you’re curious about the fun, complicated detective work \
|
||||
that goes into doing great investigative journalism- then this is the podcast \
|
||||
for you.";
|
||||
let desc = "<p>Welcome to The Tip Off- the podcast where we take you behind the \
|
||||
scenes of some of the best investigative journalism from recent years. \
|
||||
Each episode we’ll be digging into an investigative scoop- hearing from \
|
||||
the journalists behind the work as they tell us about the leads, the \
|
||||
dead-ends and of course, the tip offs. There’ll be car chases, slammed \
|
||||
doors, terrorist cells, meetings in dimly lit bars and cafes, wrangling \
|
||||
with despotic regimes and much more. So if you’re curious about the fun, \
|
||||
complicated detective work that goes into doing great investigative \
|
||||
journalism- then this is the podcast for you.</p>";
|
||||
|
||||
NewPodcastBuilder::default()
|
||||
.title("The Tip Off")
|
||||
@@ -223,15 +214,13 @@ mod tests {
|
||||
.source_id(42)
|
||||
.build()
|
||||
.unwrap()
|
||||
|
||||
};
|
||||
|
||||
static ref EXPECTED_STARS: NewPodcast = {
|
||||
let descr = "<p>The first audio drama from Tor Labs and Gideon Media, Steal the Stars is \
|
||||
a gripping noir science fiction thriller in 14 episodes: Forbidden love, a \
|
||||
crashed UFO, an alien body, and an impossible heist unlike any ever \
|
||||
attempted - scripted by Mac Rogers, the award-winning playwright and writer \
|
||||
of the multi-million download The Message and LifeAfter.</p>";
|
||||
let descr = "<p>The first audio drama from Tor Labs and Gideon Media, Steal the Stars \
|
||||
is a gripping noir science fiction thriller in 14 episodes: Forbidden \
|
||||
love, a crashed UFO, an alien body, and an impossible heist unlike any \
|
||||
ever attempted - scripted by Mac Rogers, the award-winning playwright \
|
||||
and writer of the multi-million download The Message and LifeAfter.</p>";
|
||||
let img = "https://dfkfj8j276wwv.cloudfront.net/images/2c/5f/a0/1a/2c5fa01a-ae78-4a8c-\
|
||||
b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e\
|
||||
923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg";
|
||||
@@ -245,11 +234,10 @@ mod tests {
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
static ref EXPECTED_CODE: NewPodcast = {
|
||||
let descr = "A podcast about humans and technology. Panelists: Coraline Ada Ehmke, David \
|
||||
Brady, Jessica Kerr, Jay Bobo, Astrid Countee and Sam Livingston-Gray. \
|
||||
Brought to you by @therubyrep.";
|
||||
let descr = "A podcast about humans and technology. Panelists: Coraline Ada Ehmke, \
|
||||
David Brady, Jessica Kerr, Jay Bobo, Astrid Countee and Sam \
|
||||
Livingston-Gray. Brought to you by @therubyrep.";
|
||||
|
||||
NewPodcastBuilder::default()
|
||||
.title("Greater Than Code")
|
||||
@@ -262,7 +250,16 @@ mod tests {
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
static ref EXPECTED_ELLINOFRENEIA: NewPodcast = {
|
||||
NewPodcastBuilder::default()
|
||||
.title("Ελληνοφρένεια")
|
||||
.link("https://ellinofreneia.sealabs.net/feed.rss")
|
||||
.description("Ανεπίσημο feed της Ελληνοφρένειας")
|
||||
.image_uri(Some("https://ellinofreneia.sealabs.net/logo.png".into()))
|
||||
.source_id(42)
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
static ref UPDATED_DESC_INTERCEPTED: NewPodcast = {
|
||||
NewPodcastBuilder::default()
|
||||
.title("Intercepted with Jeremy Scahill")
|
||||
@@ -324,6 +321,15 @@ mod tests {
|
||||
assert_eq!(*EXPECTED_CODE, pd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_podcast_ellinofreneia() {
|
||||
let file = File::open("tests/feeds/2018-03-28-Ellinofreneia.xml").unwrap();
|
||||
let channel = Channel::read_from(BufReader::new(file)).unwrap();
|
||||
|
||||
let pd = NewPodcast::new(&channel, 42);
|
||||
assert_eq!(*EXPECTED_ELLINOFRENEIA, pd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
// This maybe could be a doc test on insert.
|
||||
fn test_new_podcast_insert() {
|
||||
@@ -343,7 +349,8 @@ mod tests {
|
||||
#[test]
|
||||
// TODO: Add more test/checks
|
||||
// Currently there's a test that only checks new description or title.
|
||||
// If you have time and want to help, implement the test for the other fields too.
|
||||
// If you have time and want to help, implement the test for the other fields
|
||||
// too.
|
||||
fn test_new_podcast_update() {
|
||||
truncate_db().unwrap();
|
||||
let old = EXPECTED_INTERCEPTED.to_podcast().unwrap();
|
||||
|
||||
@@ -26,7 +26,8 @@ pub struct Podcast {
|
||||
}
|
||||
|
||||
impl Save<Podcast, DataError> for Podcast {
|
||||
/// Helper method to easily save/"sync" current state of self to the Database.
|
||||
/// Helper method to easily save/"sync" current state of self to the
|
||||
/// Database.
|
||||
fn save(&self) -> Result<Podcast, DataError> {
|
||||
let db = connection();
|
||||
let tempdb = db.get()?;
|
||||
|
||||
@@ -6,12 +6,11 @@ use url::Url;
|
||||
use hyper::{Client, Method, Request, Response, StatusCode, Uri};
|
||||
use hyper::client::HttpConnector;
|
||||
use hyper::header::{ETag, EntityTag, HttpDate, IfModifiedSince, IfNoneMatch, LastModified,
|
||||
Location};
|
||||
Location, UserAgent};
|
||||
use hyper_tls::HttpsConnector;
|
||||
|
||||
// use futures::future::ok;
|
||||
use futures::prelude::*;
|
||||
use futures_cpupool::CpuPool;
|
||||
|
||||
use database::connection;
|
||||
use errors::DataError;
|
||||
@@ -34,7 +33,8 @@ pub struct Source {
|
||||
}
|
||||
|
||||
impl Save<Source, DataError> for Source {
|
||||
/// Helper method to easily save/"sync" current state of self to the Database.
|
||||
/// Helper method to easily save/"sync" current state of self to the
|
||||
/// Database.
|
||||
fn save(&self) -> Result<Source, DataError> {
|
||||
let db = connection();
|
||||
let con = db.get()?;
|
||||
@@ -101,6 +101,14 @@ impl Source {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn make_err(self, context: &str, code: StatusCode) -> DataError {
|
||||
DataError::HttpStatusError {
|
||||
url: self.uri,
|
||||
status_code: code,
|
||||
context: context.into(),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO match on more stuff
|
||||
// 301: Moved Permanently
|
||||
// 304: Up to date Feed, checked with the Etag
|
||||
@@ -114,67 +122,21 @@ impl Source {
|
||||
fn match_status(mut self, res: Response) -> Result<(Self, Response), DataError> {
|
||||
self.update_etag(&res)?;
|
||||
let code = res.status();
|
||||
match code {
|
||||
StatusCode::NotModified => {
|
||||
let err = DataError::HttpStatusError {
|
||||
url: self.uri,
|
||||
status_code: code,
|
||||
context: format!("304: skipping.."),
|
||||
};
|
||||
|
||||
return Err(err);
|
||||
}
|
||||
match code {
|
||||
StatusCode::NotModified => return Err(self.make_err("304: skipping..", code)),
|
||||
StatusCode::MovedPermanently => {
|
||||
error!("Feed was moved permanently.");
|
||||
self.handle_301(&res)?;
|
||||
|
||||
let err = DataError::HttpStatusError {
|
||||
url: self.uri,
|
||||
status_code: code,
|
||||
context: format!("301: Feed was moved permanently."),
|
||||
};
|
||||
|
||||
return Err(err);
|
||||
return Err(self.make_err("301: Feed was moved permanently.", code));
|
||||
}
|
||||
StatusCode::TemporaryRedirect => debug!("307: Temporary Redirect."),
|
||||
StatusCode::PermanentRedirect => warn!("308: Permanent Redirect."),
|
||||
StatusCode::Unauthorized => {
|
||||
let err = DataError::HttpStatusError {
|
||||
url: self.uri,
|
||||
status_code: code,
|
||||
context: format!("401: Unauthorized."),
|
||||
};
|
||||
|
||||
return Err(err);
|
||||
}
|
||||
StatusCode::Forbidden => {
|
||||
let err = DataError::HttpStatusError {
|
||||
url: self.uri,
|
||||
status_code: code,
|
||||
context: format!("403: Forbidden."),
|
||||
};
|
||||
|
||||
return Err(err);
|
||||
}
|
||||
StatusCode::NotFound => return Err(format!("404: Not found.")).map_err(From::from),
|
||||
StatusCode::RequestTimeout => {
|
||||
let err = DataError::HttpStatusError {
|
||||
url: self.uri,
|
||||
status_code: code,
|
||||
context: format!("408: Request Timeout."),
|
||||
};
|
||||
|
||||
return Err(err);
|
||||
}
|
||||
StatusCode::Gone => {
|
||||
let err = DataError::HttpStatusError {
|
||||
url: self.uri,
|
||||
status_code: code,
|
||||
context: format!("410: Feed was deleted.."),
|
||||
};
|
||||
|
||||
return Err(err);
|
||||
}
|
||||
StatusCode::Unauthorized => return Err(self.make_err("401: Unauthorized.", code)),
|
||||
StatusCode::Forbidden => return Err(self.make_err("403: Forbidden.", code)),
|
||||
StatusCode::NotFound => return Err(self.make_err("404: Not found.", code)),
|
||||
StatusCode::RequestTimeout => return Err(self.make_err("408: Request Timeout.", code)),
|
||||
StatusCode::Gone => return Err(self.make_err("410: Feed was deleted..", code)),
|
||||
_ => info!("HTTP StatusCode: {}", code),
|
||||
};
|
||||
Ok((self, res))
|
||||
@@ -216,12 +178,11 @@ impl Source {
|
||||
pub fn into_feed(
|
||||
self,
|
||||
client: &Client<HttpsConnector<HttpConnector>>,
|
||||
pool: CpuPool,
|
||||
ignore_etags: bool,
|
||||
) -> Box<Future<Item = Feed, Error = DataError>> {
|
||||
let id = self.id();
|
||||
let feed = self.request_constructor(client, ignore_etags)
|
||||
.and_then(move |(_, res)| response_to_channel(res, pool))
|
||||
.and_then(move |(_, res)| response_to_channel(res))
|
||||
.and_then(move |chan| {
|
||||
FeedBuilder::default()
|
||||
.channel(chan)
|
||||
@@ -244,6 +205,12 @@ impl Source {
|
||||
let uri = Uri::from_str(self.uri()).unwrap();
|
||||
let mut req = Request::new(Method::Get, uri);
|
||||
|
||||
// Set the user agent as a fix for issue #53
|
||||
// TODO: keep this in sync with tor-browser releases
|
||||
req.headers_mut().set(UserAgent::new(
|
||||
"Mozilla/5.0 (Windows NT 6.1; rv:52.0) Gecko/20100101 Firefox/52.0",
|
||||
));
|
||||
|
||||
if !ignore_etags {
|
||||
if let Some(foo) = self.http_etag() {
|
||||
req.headers_mut().set(IfNoneMatch::Items(vec![
|
||||
@@ -267,21 +234,17 @@ impl Source {
|
||||
}
|
||||
}
|
||||
|
||||
fn response_to_channel(
|
||||
res: Response,
|
||||
pool: CpuPool,
|
||||
) -> Box<Future<Item = Channel, Error = DataError> + Send> {
|
||||
#[allow(needless_pass_by_value)]
|
||||
fn response_to_channel(res: Response) -> Box<Future<Item = Channel, Error = DataError> + Send> {
|
||||
let chan = res.body()
|
||||
.concat2()
|
||||
.map(|x| x.into_iter())
|
||||
.map_err(From::from)
|
||||
.map(|iter| iter.collect::<Vec<u8>>())
|
||||
.map(|utf_8_bytes| String::from_utf8_lossy(&utf_8_bytes).into_owned())
|
||||
.and_then(|buf| {
|
||||
Channel::from_str(&buf).or_else(|err| Err(DataError::RssCrateError(format!("{}", err))))
|
||||
});
|
||||
let cpu_chan = pool.spawn(chan);
|
||||
Box::new(cpu_chan)
|
||||
.and_then(|buf| Channel::from_str(&buf).map_err(From::from));
|
||||
|
||||
Box::new(chan)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -296,7 +259,6 @@ mod tests {
|
||||
fn test_into_feed() {
|
||||
truncate_db().unwrap();
|
||||
|
||||
let pool = CpuPool::new_num_cpus();
|
||||
let mut core = Core::new().unwrap();
|
||||
let client = Client::configure()
|
||||
.connector(HttpsConnector::new(4, &core.handle()).unwrap())
|
||||
@@ -307,7 +269,7 @@ mod tests {
|
||||
let source = Source::from_url(url).unwrap();
|
||||
let id = source.id();
|
||||
|
||||
let feed = source.into_feed(&client, pool.clone(), true);
|
||||
let feed = source.into_feed(&client, true);
|
||||
let feed = core.run(feed).unwrap();
|
||||
|
||||
let expected = get_feed("tests/feeds/2018-01-20-Intercepted.xml", id);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
//! Docs.
|
||||
|
||||
use futures::future::*;
|
||||
use futures_cpupool::CpuPool;
|
||||
// use futures::prelude::*;
|
||||
|
||||
use hyper::Client;
|
||||
@@ -20,23 +19,6 @@ use models::{IndexState, NewEpisode, NewEpisodeMinimal};
|
||||
|
||||
// use std::sync::{Arc, Mutex};
|
||||
|
||||
macro_rules! clone {
|
||||
(@param _) => ( _ );
|
||||
(@param $x:ident) => ( $x );
|
||||
($($n:ident),+ => move || $body:expr) => (
|
||||
{
|
||||
$( let $n = $n.clone(); )+
|
||||
move || $body
|
||||
}
|
||||
);
|
||||
($($n:ident),+ => move |$($p:tt),+| $body:expr) => (
|
||||
{
|
||||
$( let $n = $n.clone(); )+
|
||||
move |$(clone!(@param $p),)+| $body
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// The pipline to be run for indexing and updating a Podcast feed that originates from
|
||||
/// `Source.uri`.
|
||||
///
|
||||
@@ -47,13 +29,12 @@ pub fn pipeline<S: IntoIterator<Item = Source>>(
|
||||
sources: S,
|
||||
ignore_etags: bool,
|
||||
tokio_core: &mut Core,
|
||||
pool: &CpuPool,
|
||||
client: Client<HttpsConnector<HttpConnector>>,
|
||||
) -> Result<(), DataError> {
|
||||
let list: Vec<_> = sources
|
||||
.into_iter()
|
||||
.map(clone!(pool => move |s| s.into_feed(&client, pool.clone(), ignore_etags)))
|
||||
.map(|fut| fut.and_then(clone!(pool => move |feed| pool.clone().spawn(feed.index()))))
|
||||
.map(move |s| s.into_feed(&client, ignore_etags))
|
||||
.map(|fut| fut.and_then(|feed| feed.index()))
|
||||
.map(|fut| fut.map(|_| ()).map_err(|err| error!("Error: {}", err)))
|
||||
.collect();
|
||||
|
||||
@@ -67,25 +48,24 @@ pub fn pipeline<S: IntoIterator<Item = Source>>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates a tokio `reactor::Core`, a `CpuPool`, and a `hyper::Client` and runs the pipeline.
|
||||
/// Creates a tokio `reactor::Core`, and a `hyper::Client` and
|
||||
/// runs the pipeline.
|
||||
pub fn run(sources: Vec<Source>, ignore_etags: bool) -> Result<(), DataError> {
|
||||
if sources.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let pool = CpuPool::new_num_cpus();
|
||||
let mut core = Core::new()?;
|
||||
let handle = core.handle();
|
||||
let client = Client::configure()
|
||||
.connector(HttpsConnector::new(num_cpus::get(), &handle)?)
|
||||
.build(&handle);
|
||||
|
||||
pipeline(sources, ignore_etags, &mut core, &pool, client)
|
||||
pipeline(sources, ignore_etags, &mut core, client)
|
||||
}
|
||||
|
||||
/// Docs
|
||||
pub fn index_single_source(s: Source, ignore_etags: bool) -> Result<(), DataError> {
|
||||
let pool = CpuPool::new_num_cpus();
|
||||
let mut core = Core::new()?;
|
||||
let handle = core.handle();
|
||||
|
||||
@@ -93,8 +73,8 @@ pub fn index_single_source(s: Source, ignore_etags: bool) -> Result<(), DataErro
|
||||
.connector(HttpsConnector::new(num_cpus::get(), &handle)?)
|
||||
.build(&handle);
|
||||
|
||||
let work = s.into_feed(&client, pool.clone(), ignore_etags)
|
||||
.and_then(clone!(pool => move |feed| pool.clone().spawn(feed.index())))
|
||||
let work = s.into_feed(&client, ignore_etags)
|
||||
.and_then(move |feed| feed.index())
|
||||
.map(|_| ());
|
||||
|
||||
core.run(work)
|
||||
|
||||
+16
-51
@@ -3,7 +3,6 @@
|
||||
use chrono::prelude::*;
|
||||
use rayon::prelude::*;
|
||||
|
||||
use itertools::Itertools;
|
||||
use url::{Position, Url};
|
||||
|
||||
use dbqueries;
|
||||
@@ -14,7 +13,8 @@ use xdg_dirs::DL_DIR;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
/// Scan downloaded `episode` entries that might have broken `local_uri`s and set them to `None`.
|
||||
/// Scan downloaded `episode` entries that might have broken `local_uri`s and
|
||||
/// set them to `None`.
|
||||
fn download_checker() -> Result<(), DataError> {
|
||||
let mut episodes = dbqueries::get_downloaded_episodes()?;
|
||||
|
||||
@@ -38,17 +38,15 @@ fn download_checker() -> Result<(), DataError> {
|
||||
}
|
||||
|
||||
/// Delete watched `episodes` that have exceded their liftime after played.
|
||||
fn played_cleaner() -> Result<(), DataError> {
|
||||
fn played_cleaner(cleanup_date: DateTime<Utc>) -> Result<(), DataError> {
|
||||
let mut episodes = dbqueries::get_played_cleaner_episodes()?;
|
||||
let now_utc = cleanup_date.timestamp() as i32;
|
||||
|
||||
let now_utc = Utc::now().timestamp() as i32;
|
||||
episodes
|
||||
.par_iter_mut()
|
||||
.filter(|ep| ep.local_uri().is_some() && ep.played().is_some())
|
||||
.for_each(|ep| {
|
||||
// TODO: expose a config and a user set option.
|
||||
// Chnage the test too when exposed
|
||||
let limit = ep.played().unwrap() + 172_800; // add 2days in seconds
|
||||
let limit = ep.played().unwrap();
|
||||
if now_utc > limit {
|
||||
if let Err(err) = delete_local_content(ep) {
|
||||
error!("Error while trying to delete file: {:?}", ep.local_uri());
|
||||
@@ -91,10 +89,10 @@ fn delete_local_content(ep: &mut EpisodeCleanerQuery) -> Result<(), DataError> {
|
||||
///
|
||||
/// Runs a cleaner for played Episode's that are pass the lifetime limit and
|
||||
/// scheduled for removal.
|
||||
pub fn checkup() -> Result<(), DataError> {
|
||||
pub fn checkup(cleanup_date: DateTime<Utc>) -> Result<(), DataError> {
|
||||
info!("Running database checks.");
|
||||
download_checker()?;
|
||||
played_cleaner()?;
|
||||
played_cleaner(cleanup_date)?;
|
||||
info!("Checks completed.");
|
||||
Ok(())
|
||||
}
|
||||
@@ -111,22 +109,6 @@ pub fn url_cleaner(s: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper functions that strips extra spaces and newlines and ignores the tabs.
|
||||
#[allow(match_same_arms)]
|
||||
pub fn replace_extra_spaces(s: &str) -> String {
|
||||
s.trim()
|
||||
.chars()
|
||||
.filter(|ch| *ch != '\t')
|
||||
.coalesce(|current, next| match (current, next) {
|
||||
('\n', '\n') => Ok('\n'),
|
||||
('\n', ' ') => Ok('\n'),
|
||||
(' ', '\n') => Ok('\n'),
|
||||
(' ', ' ') => Ok(' '),
|
||||
(_, _) => Err((current, next)),
|
||||
})
|
||||
.collect::<String>()
|
||||
}
|
||||
|
||||
/// Returns the URI of a Podcast Downloads given it's title.
|
||||
pub fn get_download_folder(pd_title: &str) -> Result<String, DataError> {
|
||||
// It might be better to make it a hash of the title or the podcast rowid
|
||||
@@ -181,6 +163,7 @@ mod tests {
|
||||
|
||||
use self::tempdir::TempDir;
|
||||
use super::*;
|
||||
use chrono::Duration;
|
||||
|
||||
use database::truncate_db;
|
||||
use models::NewEpisodeBuilder;
|
||||
@@ -260,15 +243,14 @@ mod tests {
|
||||
fn test_played_cleaner_expired() {
|
||||
let _tmp_dir = helper_db();
|
||||
let mut episode = dbqueries::get_episode_from_pk("foo_bar", 0).unwrap();
|
||||
let now_utc = Utc::now().timestamp() as i32;
|
||||
// let limit = now_utc - 172_800;
|
||||
let epoch = now_utc - 200_000;
|
||||
let cleanup_date = Utc::now() - Duration::seconds(1000);
|
||||
let epoch = cleanup_date.timestamp() as i32 - 1;
|
||||
episode.set_played(Some(epoch));
|
||||
episode.save().unwrap();
|
||||
let valid_path = episode.local_uri().unwrap().to_owned();
|
||||
|
||||
// This should delete the file
|
||||
played_cleaner().unwrap();
|
||||
played_cleaner(cleanup_date).unwrap();
|
||||
assert_eq!(Path::new(&valid_path).exists(), false);
|
||||
}
|
||||
|
||||
@@ -276,15 +258,14 @@ mod tests {
|
||||
fn test_played_cleaner_none() {
|
||||
let _tmp_dir = helper_db();
|
||||
let mut episode = dbqueries::get_episode_from_pk("foo_bar", 0).unwrap();
|
||||
let now_utc = Utc::now().timestamp() as i32;
|
||||
// limit = 172_800;
|
||||
let epoch = now_utc - 20_000;
|
||||
let cleanup_date = Utc::now() - Duration::seconds(1000);
|
||||
let epoch = cleanup_date.timestamp() as i32 + 1;
|
||||
episode.set_played(Some(epoch));
|
||||
episode.save().unwrap();
|
||||
let valid_path = episode.local_uri().unwrap().to_owned();
|
||||
|
||||
// This should not delete the file
|
||||
played_cleaner().unwrap();
|
||||
played_cleaner(cleanup_date).unwrap();
|
||||
assert_eq!(Path::new(&valid_path).exists(), true);
|
||||
}
|
||||
|
||||
@@ -299,24 +280,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_whitespace() {
|
||||
let bad_txt = "1 2 3 4 5";
|
||||
let valid_txt = "1 2 3 4 5";
|
||||
|
||||
assert_eq!(replace_extra_spaces(&bad_txt), valid_txt);
|
||||
|
||||
let bad_txt = "1 2 3 \n 4 5\n";
|
||||
let valid_txt = "1 2 3\n4 5";
|
||||
|
||||
assert_eq!(replace_extra_spaces(&bad_txt), valid_txt);
|
||||
|
||||
let bad_txt = "1 2 3 \n\n\n \n 4 5\n";
|
||||
let valid_txt = "1 2 3\n4 5";
|
||||
|
||||
assert_eq!(replace_extra_spaces(&bad_txt), valid_txt);
|
||||
}
|
||||
|
||||
#[test]
|
||||
// This test needs access to local system so we ignore it by default.
|
||||
#[ignore]
|
||||
fn test_get_dl_folder() {
|
||||
let foo_ = format!("{}/{}", DL_DIR.to_str().unwrap(), "foo");
|
||||
assert_eq!(get_download_folder("foo").unwrap(), foo_);
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" version="2.0">
|
||||
<channel>
|
||||
<title>Ελληνοφρένεια</title>
|
||||
<link>https://ellinofreneia.sealabs.net/feed.rss</link>
|
||||
<description>Ανεπίσημο feed της Ελληνοφρένειας</description>
|
||||
<atom:link href="https://ellinofreneia.sealabs.net/feed.rss" rel="self"/>
|
||||
<copyright>All rights reversed by http://ellinofreneianet.gr/</copyright>
|
||||
<docs>http://www.rssboard.org/rss-specification</docs>
|
||||
<generator>python-feedgen</generator>
|
||||
<image>
|
||||
<url>https://ellinofreneia.sealabs.net/logo.png</url>
|
||||
<title>Ελληνοφρένεια</title>
|
||||
<link>https://ellinofreneia.sealabs.net/feed.rss</link>
|
||||
</image>
|
||||
<language>el</language>
|
||||
<lastBuildDate>Tue, 27 Mar 2018 13:00:56 +0000</lastBuildDate>
|
||||
<pubDate>Tue, 27 Mar 2018 13:00:56 +0000</pubDate>
|
||||
<itunes:explicit>no</itunes:explicit>
|
||||
<itunes:owner>
|
||||
<itunes:name>Τζένη Μπώτση</itunes:name>
|
||||
<itunes:email>tbotsi@example.com</itunes:email>
|
||||
</itunes:owner>
|
||||
<item>
|
||||
<title>Η ρ_φ Ελληνοφρένεια της 27ης Μαρτίου 2018</title>
|
||||
<guid isPermaLink="false">https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2027%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.mp3</guid>
|
||||
<enclosure url="https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2027%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.mp3" length="36087430" type="audio/mpeg"/>
|
||||
<pubDate>Tue, 27 Mar 2018 11:11:02 +0000</pubDate>
|
||||
<itunes:image href="https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2027%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.jpg"/>
|
||||
<itunes:duration>2257</itunes:duration>
|
||||
</item>
|
||||
<item>
|
||||
<title>Η ρ_φ Ελληνοφρένεια της 26ης Μαρτίου 2018</title>
|
||||
<guid isPermaLink="false">https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2026%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.mp3</guid>
|
||||
<enclosure url="https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2026%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.mp3" length="34974405" type="audio/mpeg"/>
|
||||
<pubDate>Mon, 26 Mar 2018 11:16:17 +0000</pubDate>
|
||||
<itunes:image href="https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2026%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.jpg"/>
|
||||
<itunes:duration>2188</itunes:duration>
|
||||
</item>
|
||||
<item>
|
||||
<title>Η ρ_φ Ελληνοφρένεια της 23ης Μαρτίου 2018</title>
|
||||
<guid isPermaLink="false">https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2023%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.mp3</guid>
|
||||
<enclosure url="https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2023%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.mp3" length="38491114" type="audio/mpeg"/>
|
||||
<pubDate>Fri, 23 Mar 2018 12:11:08 +0000</pubDate>
|
||||
<itunes:image href="https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2023%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.jpg"/>
|
||||
<itunes:duration>2408</itunes:duration>
|
||||
</item>
|
||||
<item>
|
||||
<title>Η ρ_φ Ελληνοφρένεια της 22ας Μαρτίου 2018</title>
|
||||
<guid isPermaLink="false">https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2022%CE%B1%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.mp3</guid>
|
||||
<enclosure url="https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2022%CE%B1%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.mp3" length="35607613" type="audio/mpeg"/>
|
||||
<pubDate>Thu, 22 Mar 2018 12:31:20 +0000</pubDate>
|
||||
<itunes:image href="https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2022%CE%B1%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.jpg"/>
|
||||
<itunes:duration>2227</itunes:duration>
|
||||
</item>
|
||||
<item>
|
||||
<title>Η ρ_φ Ελληνοφρένεια της 21ης Μαρτίου 2018</title>
|
||||
<guid isPermaLink="false">https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2021%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.mp3</guid>
|
||||
<enclosure url="https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2021%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.mp3" length="36459832" type="audio/mpeg"/>
|
||||
<pubDate>Wed, 21 Mar 2018 12:10:59 +0000</pubDate>
|
||||
<itunes:image href="https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2021%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.jpg"/>
|
||||
<itunes:duration>2281</itunes:duration>
|
||||
</item>
|
||||
<item>
|
||||
<title>Η ρ_φ Ελληνοφρένεια της 20ης Μαρτίου 2018</title>
|
||||
<guid isPermaLink="false">https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2020%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.mp3</guid>
|
||||
<enclosure url="https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2020%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.mp3" length="34507127" type="audio/mpeg"/>
|
||||
<pubDate>Tue, 20 Mar 2018 12:11:06 +0000</pubDate>
|
||||
<itunes:image href="https://ellinofreneia.sealabs.net/audio/%CE%97%20%CF%81_%CF%86%20%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%BF%CF%86%CF%81%CE%AD%CE%BD%CE%B5%CE%B9%CE%B1%20%CF%84%CE%B7%CF%82%2020%CE%B7%CF%82%20%CE%9C%CE%B1%CF%81%CF%84%CE%AF%CE%BF%CF%85%202018.jpg"/>
|
||||
<itunes:duration>2159</itunes:duration>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -38,3 +38,8 @@ Web view: https://web.archive.org/web/20180120104741/https://www.greaterthancode
|
||||
|
||||
Raw file: https://web.archive.org/web/20180120104741if_/https://www.greaterthancode.com/feed/podcast
|
||||
|
||||
#### Ellinofreneia
|
||||
|
||||
Web view: https://web.archive.org/web/20180328083913/https://ellinofreneia.sealabs.net/audio/podcast.rss
|
||||
|
||||
Raw file: https://web.archive.org/web/20180328083913if_/https://ellinofreneia.sealabs.net/audio/podcast.rss
|
||||
@@ -6,11 +6,11 @@ workspace = "../"
|
||||
|
||||
[dependencies]
|
||||
error-chain = "0.11.0"
|
||||
hyper = "0.11.18"
|
||||
hyper = "0.11.24"
|
||||
log = "0.4.1"
|
||||
mime_guess = "1.8.3"
|
||||
reqwest = "0.8.4"
|
||||
tempdir = "0.3.6"
|
||||
mime_guess = "1.8.4"
|
||||
reqwest = "0.8.5"
|
||||
tempdir = "0.3.7"
|
||||
glob = "0.2.11"
|
||||
failure = "0.1.1"
|
||||
failure_derive = "0.1.1"
|
||||
@@ -18,3 +18,5 @@ failure_derive = "0.1.1"
|
||||
[dependencies.hammond-data]
|
||||
path = "../hammond-data"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "0.5.1"
|
||||
|
||||
@@ -18,7 +18,8 @@ use hammond_data::xdg_dirs::HAMMOND_CACHE;
|
||||
use errors::DownloadError;
|
||||
|
||||
// TODO: Replace path that are of type &str with std::path.
|
||||
// TODO: Have a convention/document absolute/relative paths, if they should end with / or not.
|
||||
// TODO: Have a convention/document absolute/relative paths, if they should end
|
||||
// with / or not.
|
||||
|
||||
pub trait DownloadProgress {
|
||||
fn set_downloaded(&mut self, downloaded: u64);
|
||||
@@ -75,7 +76,8 @@ fn download_into(
|
||||
info!("Extension: {}", ext);
|
||||
|
||||
// Construct a temp file to save desired content.
|
||||
// It has to be a `new_in` instead of new cause rename can't move cross filesystems.
|
||||
// It has to be a `new_in` instead of new cause rename can't move cross
|
||||
// filesystems.
|
||||
let tempdir = TempDir::new_in(HAMMOND_CACHE.to_str().unwrap(), "temp_download")?;
|
||||
let out_file = format!("{}/temp.part", tempdir.path().to_str().unwrap(),);
|
||||
|
||||
@@ -113,7 +115,9 @@ fn get_ext(content: Option<ContentType>) -> Option<String> {
|
||||
|
||||
// TODO: Write unit-tests.
|
||||
// TODO: Refactor... Somehow.
|
||||
/// Handles the I/O of fetching a remote file and saving into a Buffer and A File.
|
||||
/// Handles the I/O of fetching a remote file and saving into a Buffer and A
|
||||
/// File.
|
||||
#[allow(needless_pass_by_value)]
|
||||
fn save_io(
|
||||
file: &str,
|
||||
resp: &mut reqwest::Response,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#![recursion_limit = "1024"]
|
||||
#![deny(unused_extern_crates, unused)]
|
||||
#![allow(unknown_lints)]
|
||||
#![cfg_attr(feature = "cargo-clippy", allow(blacklisted_name))]
|
||||
|
||||
extern crate failure;
|
||||
#[macro_use]
|
||||
@@ -7,6 +9,10 @@ extern crate failure_derive;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
extern crate pretty_assertions;
|
||||
|
||||
extern crate glob;
|
||||
extern crate hammond_data;
|
||||
extern crate hyper;
|
||||
|
||||
+21
-10
@@ -6,29 +6,40 @@ version = "0.1.0"
|
||||
workspace = "../"
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.0"
|
||||
dissolve = "0.2.2"
|
||||
gdk = "0.7.0"
|
||||
gdk-pixbuf = "0.3.0"
|
||||
gio = "0.3.0"
|
||||
glib = "0.4.1"
|
||||
chrono = "0.4.1"
|
||||
crossbeam-channel = "0.1.2"
|
||||
gdk = "0.8.0"
|
||||
gdk-pixbuf = "0.4.0"
|
||||
glib = "0.5.0"
|
||||
humansize = "1.1.0"
|
||||
lazy_static = "1.0.0"
|
||||
log = "0.4.1"
|
||||
loggerv = "0.7.0"
|
||||
loggerv = "0.7.1"
|
||||
open = "1.2.1"
|
||||
rayon = "0.9.0"
|
||||
rayon = "1.0.1"
|
||||
send-cell = "0.1.2"
|
||||
url = "1.6.0"
|
||||
url = "1.7.0"
|
||||
failure = "0.1.1"
|
||||
failure_derive = "0.1.1"
|
||||
take_mut = "0.2.2"
|
||||
regex = "0.2.10"
|
||||
reqwest = "0.8.5"
|
||||
serde_json = "1.0.13"
|
||||
html2pango = { git = "https://gitlab.gnome.org/World/html2pango" }
|
||||
|
||||
[dependencies.gtk]
|
||||
features = ["v3_22"]
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
|
||||
[dependencies.gio]
|
||||
features = ["v2_50"]
|
||||
version = "0.4.0"
|
||||
|
||||
[dependencies.hammond-data]
|
||||
path = "../hammond-data"
|
||||
|
||||
[dependencies.hammond-downloader]
|
||||
path = "../hammond-downloader"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "0.5.1"
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.0 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<object class="GtkBox" id="empty_show">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">12</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="pixel_size">128</property>
|
||||
<property name="icon_name">application-rss+xml-symbolic</property>
|
||||
<property name="use_fallback">True</property>
|
||||
<style>
|
||||
<class name="dim-label"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">This show does not have any episodes</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
<attribute name="scale" value="1.4399999999999999"/>
|
||||
</attributes>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">If you think this is an Error, Plese consider opening a bug report.</property>
|
||||
<style>
|
||||
<class name="dim-label"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.21.0
|
||||
<!-- Generated with glade 3.22.0
|
||||
|
||||
Copyright (C) 2017 - 2018
|
||||
|
||||
@@ -31,6 +31,7 @@ Tobias Bernard
|
||||
<!-- interface-copyright 2017 - 2018 -->
|
||||
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
|
||||
<object class="GtkBox" id="episode_container">
|
||||
<property name="height_request">64</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
@@ -43,14 +44,12 @@ Tobias Bernard
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">start</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="title_label">
|
||||
<property name="visible">True</property>
|
||||
@@ -58,7 +57,6 @@ Tobias Bernard
|
||||
<property name="label" translatable="yes">Episode Title</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<property name="single_line_mode">True</property>
|
||||
<property name="max_width_chars">64</property>
|
||||
<property name="track_visited_links">False</property>
|
||||
<property name="lines">1</property>
|
||||
<property name="xalign">0</property>
|
||||
@@ -66,14 +64,14 @@ Tobias Bernard
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
@@ -81,7 +79,6 @@ Tobias Bernard
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="date_label">
|
||||
@@ -96,7 +93,7 @@ Tobias Bernard
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
@@ -112,7 +109,7 @@ Tobias Bernard
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
@@ -129,7 +126,7 @@ Tobias Bernard
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
@@ -145,7 +142,7 @@ Tobias Bernard
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
@@ -162,7 +159,7 @@ Tobias Bernard
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
@@ -179,7 +176,7 @@ Tobias Bernard
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
@@ -196,21 +193,21 @@ Tobias Bernard
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">6</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="padding">6</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
@@ -242,7 +239,6 @@ Tobias Bernard
|
||||
<property name="receives_default">True</property>
|
||||
<property name="no_show_all">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Download this episode</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<child>
|
||||
@@ -285,7 +281,7 @@ Tobias Bernard
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="padding">6</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">1</property>
|
||||
@@ -295,7 +291,6 @@ Tobias Bernard
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="padding">6</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
@@ -305,13 +300,13 @@ Tobias Bernard
|
||||
<property name="no_show_all">True</property>
|
||||
<property name="margin_left">6</property>
|
||||
<property name="margin_right">6</property>
|
||||
<property name="margin_bottom">6</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="pulse_step">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="padding">6</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
|
||||
@@ -38,9 +38,10 @@ Tobias Bernard
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="scrolled_window">
|
||||
<property name="name">scrolled_window</property>
|
||||
<property name="height_request">400</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<property name="hscrollbar_policy">never</property>
|
||||
<child>
|
||||
<object class="GtkViewport">
|
||||
<property name="visible">True</property>
|
||||
@@ -49,30 +50,33 @@ Tobias Bernard
|
||||
<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="hexpand">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="frame_parent">
|
||||
<property name="width_request">720</property>
|
||||
<property name="width_request">600</property>
|
||||
<property name="height_request">-1</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="margin_left">32</property>
|
||||
<property name="margin_right">32</property>
|
||||
<property name="margin_top">32</property>
|
||||
<property name="margin_bottom">32</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">24</property>
|
||||
<child>
|
||||
@@ -80,6 +84,7 @@ Tobias Bernard
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="no_show_all">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
@@ -87,6 +92,7 @@ Tobias Bernard
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="label" translatable="yes">Today</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
@@ -95,7 +101,7 @@ Tobias Bernard
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
@@ -103,12 +109,14 @@ Tobias Bernard
|
||||
<object class="GtkFrame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="hexpand">True</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="hexpand">True</property>
|
||||
<property name="selection_mode">none</property>
|
||||
</object>
|
||||
</child>
|
||||
@@ -118,14 +126,14 @@ Tobias Bernard
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
@@ -134,6 +142,7 @@ Tobias Bernard
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="no_show_all">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
@@ -149,7 +158,7 @@ Tobias Bernard
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
@@ -157,12 +166,14 @@ Tobias Bernard
|
||||
<object class="GtkFrame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="hexpand">True</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="hexpand">True</property>
|
||||
<property name="selection_mode">none</property>
|
||||
</object>
|
||||
</child>
|
||||
@@ -172,14 +183,14 @@ Tobias Bernard
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
@@ -188,6 +199,7 @@ Tobias Bernard
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="no_show_all">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
@@ -203,7 +215,7 @@ Tobias Bernard
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
@@ -211,12 +223,14 @@ Tobias Bernard
|
||||
<object class="GtkFrame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="hexpand">True</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="hexpand">True</property>
|
||||
<property name="selection_mode">none</property>
|
||||
</object>
|
||||
</child>
|
||||
@@ -226,14 +240,14 @@ Tobias Bernard
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
@@ -242,6 +256,7 @@ Tobias Bernard
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="no_show_all">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
@@ -257,7 +272,7 @@ Tobias Bernard
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
@@ -265,12 +280,14 @@ Tobias Bernard
|
||||
<object class="GtkFrame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="hexpand">True</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="hexpand">True</property>
|
||||
<property name="selection_mode">none</property>
|
||||
</object>
|
||||
</child>
|
||||
@@ -280,14 +297,14 @@ Tobias Bernard
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
@@ -296,6 +313,7 @@ Tobias Bernard
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="no_show_all">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
@@ -303,6 +321,7 @@ Tobias Bernard
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="label" translatable="yes">Older</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
@@ -311,7 +330,7 @@ Tobias Bernard
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
@@ -319,6 +338,7 @@ Tobias Bernard
|
||||
<object class="GtkFrame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="label_xalign">0</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
@@ -326,6 +346,7 @@ Tobias Bernard
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="no_show_all">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="selection_mode">none</property>
|
||||
</object>
|
||||
</child>
|
||||
@@ -335,14 +356,14 @@ Tobias Bernard
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
@@ -357,13 +378,14 @@ Tobias Bernard
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.21.0
|
||||
<!-- Generated with glade 3.22.0
|
||||
|
||||
Copyright (C) 2017 - 2018
|
||||
|
||||
@@ -33,14 +33,15 @@ Tobias Bernard
|
||||
<object class="GtkBox" id="container">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_top">6</property>
|
||||
<property name="margin_bottom">6</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>
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.21.0 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<object class="GtkRevealer" id="revealer">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">start</property>
|
||||
<child>
|
||||
<object class="GtkFrame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="label_xalign">0</property>
|
||||
<property name="shadow_type">none</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_left">3</property>
|
||||
<property name="margin_right">3</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="text">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_left">12</property>
|
||||
<property name="margin_right">12</property>
|
||||
<property name="label" translatable="yes">An in-app action notification</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="close">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="relief">none</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">window-close-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="flat image-button"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="undo">
|
||||
<property name="label" translatable="yes">Undo</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<style>
|
||||
<class name="text-button"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label_item">
|
||||
<placeholder/>
|
||||
</child>
|
||||
<style>
|
||||
<class name="app-notification"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="top"/>
|
||||
</style>
|
||||
</object>
|
||||
</interface>
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.21.0
|
||||
<!-- Generated with glade 3.22.0
|
||||
|
||||
Copyright (C) 2017 - 2018
|
||||
|
||||
@@ -24,7 +24,7 @@ Tobias Bernard
|
||||
|
||||
-->
|
||||
<interface domain="">
|
||||
<requires lib="gtk+" version="3.12"/>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<!-- interface-license-type gplv3 -->
|
||||
<!-- interface-name Hammond -->
|
||||
<!-- interface-description A podcast client for the GNOME Desktop -->
|
||||
@@ -37,9 +37,11 @@ Tobias Bernard
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="scrolled_window">
|
||||
<property name="name">scrolled_window</property>
|
||||
<property name="width_request">700</property>
|
||||
<property name="height_request">500</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<property name="hscrollbar_policy">never</property>
|
||||
<child>
|
||||
<object class="GtkViewport">
|
||||
<property name="visible">True</property>
|
||||
@@ -48,43 +50,35 @@ Tobias Bernard
|
||||
<object class="GtkBox">
|
||||
<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="can_default">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="width_request">624</property>
|
||||
<property name="width_request">700</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="margin_left">32</property>
|
||||
<property name="margin_right">32</property>
|
||||
<property name="margin_top">32</property>
|
||||
<property name="margin_bottom">32</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">24</property>
|
||||
<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">none</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
@@ -100,7 +94,7 @@ Tobias Bernard
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
@@ -108,25 +102,24 @@ Tobias Bernard
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">end</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">12</property>
|
||||
<property name="spacing">6</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="label">foo</property>
|
||||
<property name="use_markup">True</property>
|
||||
<property name="wrap">True</property>
|
||||
<property name="max_width_chars">57</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="medium"/>
|
||||
</attributes>
|
||||
<property name="wrap_mode">word-char</property>
|
||||
<property name="max_width_chars">90</property>
|
||||
</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>
|
||||
@@ -134,7 +127,8 @@ Tobias Bernard
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="settings_button">
|
||||
<property name="visible">True</property>
|
||||
@@ -195,14 +189,14 @@ Tobias Bernard
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
@@ -211,19 +205,7 @@ Tobias Bernard
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label_item">
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
@@ -241,14 +223,14 @@ Tobias Bernard
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
@@ -256,14 +238,16 @@ Tobias Bernard
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
@@ -279,4 +263,33 @@ Tobias Bernard
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkPopover" id="show_menu">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="position">bottom</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">6</property>
|
||||
<property name="margin_right">6</property>
|
||||
<property name="margin_top">6</property>
|
||||
<property name="margin_bottom">6</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkModelButton" id="mark_all_watched">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="text" translatable="yes">Mark all episodes as listened</property>
|
||||
<property name="centered">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
||||
@@ -40,7 +40,6 @@ Tobias Bernard
|
||||
<property name="name">scrolled_window</property>
|
||||
<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>
|
||||
|
||||
@@ -2,3 +2,5 @@
|
||||
|
||||
install_data('org.gnome.Hammond.desktop', install_dir : datadir + '/applications')
|
||||
install_data('org.gnome.Hammond.appdata.xml', install_dir : datadir + '/appdata')
|
||||
install_data('org.gnome.Hammond.gschema.xml', install_dir: join_paths(datadir, 'glib-2.0', 'schemas'))
|
||||
meson.add_install_script('../../scripts/compile-gschema.py')
|
||||
@@ -5,19 +5,32 @@
|
||||
<project_license>GPL-3.0</project_license>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<developer_name>Jordan Petridis</developer_name>
|
||||
<summary>GNOME Podcast Client written in Rust</summary>
|
||||
<url type="homepage">https://gitlab.gnome.org/alatiera/Hammond</url>
|
||||
<description>
|
||||
Hammond is a Gtk+ Podcast client for the GNOME Desktop written in Rust
|
||||
<summary>Modern Podcast Client for the GNOME desktop</summary>
|
||||
<description xml:lang="en">
|
||||
<p>
|
||||
Hammond is a modern, reliable, and fast Podcast Client for the GNOME
|
||||
desktop written in Rust.
|
||||
</p>
|
||||
</description>
|
||||
<url type="homepage">https://gitlab.gnome.org/alatiera/Hammond</url>
|
||||
<screenshots>
|
||||
<screenshot>
|
||||
<image type="source">https://gitlab.gnome.org/alatiera/Hammond/raw/master/screenshots/podcasts_view.png</image>
|
||||
<image type="source">https://gitlab.gnome.org/alatiera/Hammond/raw/master/screenshots/podcast_widget.png</image>
|
||||
<image>https://gitlab.gnome.org/alatiera/hammond/raw/master/screenshots/episodes_view.png</image>
|
||||
<caption>Page 1</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://gitlab.gnome.org/alatiera/hammond/raw/master/screenshots/shows_view.png</image>
|
||||
<caption>Page 2</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://gitlab.gnome.org/alatiera/hammond/raw/master/screenshots/show_widget.png</image>
|
||||
<caption>Page 3</caption>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<releases>
|
||||
<release version="0.3.0" date="2018-02-11"/>
|
||||
<release version="0.3.1" date="2018-03-28"/>
|
||||
</releases>
|
||||
<update_contact>jordanpetridis@protonmail.com</update_contact>
|
||||
<url type="homepage">https://gitlab.gnome.org/alatiera/hammond</url>
|
||||
<update_contact>jpetridis@gnome.org</update_contact>
|
||||
<developer_name>Jordan Petridis and others</developer_name>
|
||||
</component>
|
||||
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<schemalist>
|
||||
<enum id="org.gnome.Hammond.timePeriods">
|
||||
<value nick="seconds" value="1"/>
|
||||
<value nick="minutes" value="2"/>
|
||||
<value nick="hours" value="3"/>
|
||||
<value nick="days" value="4"/>
|
||||
<value nick="weeks" value="5"/>
|
||||
<value nick="months" value="6"/>
|
||||
</enum>
|
||||
|
||||
<schema path="/org/gnome/hammond/" id="org.gnome.Hammond">
|
||||
<key name="persist-window-geometry-top" type="i">
|
||||
<default>-1</default>
|
||||
<summary>Top position of the last open main window</summary>
|
||||
</key>
|
||||
<key name="persist-window-geometry-left" type="i">
|
||||
<default>-1</default>
|
||||
<summary>Left position of the last open main window</summary>
|
||||
</key>
|
||||
<key name="persist-window-geometry-height" type="i">
|
||||
<default>640</default>
|
||||
<summary>Height of the last open main window</summary>
|
||||
</key>
|
||||
<key name="persist-window-geometry-width" type="i">
|
||||
<default>860</default>
|
||||
<summary>Width of the last open main window</summary>
|
||||
</key>
|
||||
<key name="persist-window-geometry-maximized" type="b">
|
||||
<default>false</default>
|
||||
<summary>Maximized state of the last open main window</summary>
|
||||
</key>
|
||||
|
||||
<key name="dark-theme" type="b">
|
||||
<default>false</default>
|
||||
<summary>Enable or disable dark theme</summary>
|
||||
</key>
|
||||
|
||||
<key name="refresh-interval" type="b">
|
||||
<default>true</default>
|
||||
<summary>Whether to periodically refresh content</summary>
|
||||
</key>
|
||||
<key name="refresh-interval-time" type="i">
|
||||
<range min="1" max="100"/>
|
||||
<default>1</default>
|
||||
<summary>How many periods of time to wait between automatic refreshes</summary>
|
||||
</key>
|
||||
<key name="refresh-interval-period" enum="org.gnome.Hammond.timePeriods">
|
||||
<default>'hours'</default>
|
||||
<summary>What period of time to wait between automatic refreshes</summary>
|
||||
</key>
|
||||
<key name="refresh-on-startup" type="b">
|
||||
<default>true</default>
|
||||
<summary>Whether to refresh content after startup</summary>
|
||||
</key>
|
||||
|
||||
<key name="cleanup-age-time" type="i">
|
||||
<range min="1" max="100"/>
|
||||
<default>2</default>
|
||||
<summary>How many periods of time to wait between automatic cleanups</summary>
|
||||
</key>
|
||||
<key name="cleanup-age-period" enum="org.gnome.Hammond.timePeriods">
|
||||
<default>'days'</default>
|
||||
<summary>What period of time to wait between automatic cleanups</summary>
|
||||
</key>
|
||||
</schema>
|
||||
</schemalist>
|
||||
@@ -1,14 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gresources>
|
||||
<gresource prefix="/org/gnome/hammond/">
|
||||
<file preprocess="xml-stripblanks">gtk/episode_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/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_child.ui</file>
|
||||
<file preprocess="xml-stripblanks">gtk/headerbar.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">gtk/episode_widget.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">gtk/show_widget.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">gtk/empty_view.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">gtk/empty_show.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">gtk/episodes_view.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">gtk/episodes_view_widget.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">gtk/shows_view.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">gtk/shows_child.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">gtk/headerbar.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">gtk/inapp_notif.ui</file>
|
||||
<file compressed="true">gtk/style.css</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
|
||||
+127
-28
@@ -1,14 +1,23 @@
|
||||
use gio::{ApplicationExt, ApplicationExtManual, ApplicationFlags};
|
||||
#![allow(new_without_default)]
|
||||
|
||||
use gio::{ApplicationExt, ApplicationExtManual, ApplicationFlags, Settings, SettingsExt};
|
||||
use glib;
|
||||
use gtk;
|
||||
use gtk::SettingsExt as GtkSettingsExt;
|
||||
use gtk::prelude::*;
|
||||
|
||||
use hammond_data::{Podcast, Source};
|
||||
use hammond_data::utils::checkup;
|
||||
use failure::Error;
|
||||
use rayon;
|
||||
|
||||
use hammond_data::{Podcast, Source};
|
||||
use hammond_data::utils::delete_show;
|
||||
|
||||
use appnotif::*;
|
||||
use headerbar::Header;
|
||||
use settings::WindowGeometry;
|
||||
use stacks::Content;
|
||||
use utils;
|
||||
use widgets::mark_all_watched;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||
@@ -31,20 +40,25 @@ pub enum Action {
|
||||
HeaderBarNormal,
|
||||
HeaderBarShowUpdateIndicator,
|
||||
HeaderBarHideUpdateIndicator,
|
||||
MarkAllPlayerNotification(Arc<Podcast>),
|
||||
RemoveShow(Arc<Podcast>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct App {
|
||||
app_instance: gtk::Application,
|
||||
window: gtk::Window,
|
||||
overlay: gtk::Overlay,
|
||||
header: Arc<Header>,
|
||||
content: Arc<Content>,
|
||||
receiver: Receiver<Action>,
|
||||
sender: Sender<Action>,
|
||||
settings: Settings,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> App {
|
||||
let settings = Settings::new("org.gnome.Hammond");
|
||||
let application = gtk::Application::new("org.gnome.Hammond", ApplicationFlags::empty())
|
||||
.expect("Application Initialization failed...");
|
||||
|
||||
@@ -54,10 +68,14 @@ impl App {
|
||||
|
||||
// Create the main window
|
||||
let window = gtk::Window::new(gtk::WindowType::Toplevel);
|
||||
window.set_default_size(860, 640);
|
||||
|
||||
window.set_title("Hammond");
|
||||
|
||||
let app_clone = application.clone();
|
||||
let window_clone = window.clone();
|
||||
let settings_clone = settings.clone();
|
||||
window.connect_delete_event(move |_, _| {
|
||||
WindowGeometry::from_window(&window_clone).write(&settings_clone);
|
||||
app_clone.quit();
|
||||
Inhibit(false)
|
||||
});
|
||||
@@ -71,64 +89,91 @@ impl App {
|
||||
// Create the headerbar
|
||||
let header = Arc::new(Header::new(&content, &window, sender.clone()));
|
||||
|
||||
// Add the content main stack to the window.
|
||||
window.add(&content.get_stack());
|
||||
// Add the content main stack to the overlay.
|
||||
let overlay = gtk::Overlay::new();
|
||||
overlay.add(&content.get_stack());
|
||||
|
||||
// Add the overlay to the main window
|
||||
window.add(&overlay);
|
||||
|
||||
App {
|
||||
app_instance: application,
|
||||
window,
|
||||
overlay,
|
||||
header,
|
||||
content,
|
||||
receiver,
|
||||
sender,
|
||||
settings,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup_timed_callbacks(&self) {
|
||||
let sender = self.sender.clone();
|
||||
fn setup_timed_callbacks(&self) {
|
||||
self.setup_dark_theme();
|
||||
self.setup_refresh_on_startup();
|
||||
self.setup_auto_refresh();
|
||||
}
|
||||
|
||||
fn setup_dark_theme(&self) {
|
||||
let settings = gtk::Settings::get_default().unwrap();
|
||||
let enabled = self.settings.get_boolean("dark-theme");
|
||||
|
||||
settings.set_property_gtk_application_prefer_dark_theme(enabled);
|
||||
}
|
||||
|
||||
fn setup_refresh_on_startup(&self) {
|
||||
// Update the feeds right after the Application is initialized.
|
||||
if self.settings.get_boolean("refresh-on-startup") {
|
||||
let cleanup_date = utils::get_cleanup_date(&self.settings);
|
||||
let sender = self.sender.clone();
|
||||
|
||||
info!("Refresh on startup.");
|
||||
|
||||
utils::cleanup(cleanup_date);
|
||||
|
||||
gtk::timeout_add_seconds(2, move || {
|
||||
utils::refresh_feed_wrapper(None, sender.clone());
|
||||
utils::refresh(None, sender.clone());
|
||||
|
||||
glib::Continue(false)
|
||||
});
|
||||
|
||||
let sender = self.sender.clone();
|
||||
// Auto-updater, runs every hour.
|
||||
// TODO: expose the interval in which it run to a user setting.
|
||||
gtk::timeout_add_seconds(3600, move || {
|
||||
utils::refresh_feed_wrapper(None, sender.clone());
|
||||
glib::Continue(true)
|
||||
});
|
||||
|
||||
// Run a database checkup once the application is initialized.
|
||||
gtk::timeout_add(300, || {
|
||||
if let Err(err) = checkup() {
|
||||
error!("Check up failed: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
glib::Continue(false)
|
||||
fn setup_auto_refresh(&self) {
|
||||
let refresh_interval = utils::get_refresh_interval(&self.settings).num_seconds() as u32;
|
||||
let sender = self.sender.clone();
|
||||
|
||||
info!("Auto-refresh every {:?} seconds.", refresh_interval);
|
||||
|
||||
gtk::timeout_add_seconds(refresh_interval, move || {
|
||||
utils::refresh(None, sender.clone());
|
||||
|
||||
glib::Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
pub fn run(self) {
|
||||
WindowGeometry::from_settings(&self.settings).apply(&self.window);
|
||||
|
||||
let window = self.window.clone();
|
||||
let app = self.app_instance.clone();
|
||||
self.app_instance.connect_startup(move |_| {
|
||||
build_ui(&window, &app);
|
||||
|
||||
self.app_instance.connect_startup(move |app| {
|
||||
build_ui(&window, app);
|
||||
});
|
||||
self.setup_timed_callbacks();
|
||||
|
||||
let content = self.content.clone();
|
||||
let headerbar = self.header.clone();
|
||||
let sender = self.sender.clone();
|
||||
let overlay = self.overlay.clone();
|
||||
let receiver = self.receiver;
|
||||
gtk::idle_add(move || {
|
||||
match receiver.recv_timeout(Duration::from_millis(10)) {
|
||||
Ok(Action::UpdateSources(source)) => {
|
||||
if let Some(s) = source {
|
||||
utils::refresh_feed_wrapper(Some(vec![s]), sender.clone());
|
||||
utils::refresh(Some(vec![s]), sender.clone());
|
||||
} else {
|
||||
utils::refresh_feed_wrapper(None, sender.clone());
|
||||
utils::refresh(None, sender.clone());
|
||||
}
|
||||
}
|
||||
Ok(Action::RefreshAllViews) => content.update(),
|
||||
@@ -150,6 +195,60 @@ impl App {
|
||||
Ok(Action::HeaderBarNormal) => headerbar.switch_to_normal(),
|
||||
Ok(Action::HeaderBarShowUpdateIndicator) => headerbar.show_update_notification(),
|
||||
Ok(Action::HeaderBarHideUpdateIndicator) => headerbar.hide_update_notification(),
|
||||
Ok(Action::MarkAllPlayerNotification(pd)) => {
|
||||
let callback = clone!(sender => move || {
|
||||
if let Err(err) = mark_all_watched(&pd, sender.clone()) {
|
||||
error!("Something went horribly wrong with the notif callback: {}", err);
|
||||
}
|
||||
glib::Continue(false)
|
||||
});
|
||||
|
||||
let text = "Marked all episodes as listened".into();
|
||||
let notif = InAppNotification::new(text, callback, || {}, sender.clone());
|
||||
notif.show(&overlay);
|
||||
}
|
||||
Ok(Action::RemoveShow(pd)) => {
|
||||
let text = format!("Unsubscribed from {}", pd.title());
|
||||
|
||||
if let Err(err) = utils::ignore_show(pd.id()) {
|
||||
error!("Could not insert {} to the ignore list.", pd.title());
|
||||
error!("Error: {}", err);
|
||||
}
|
||||
|
||||
let callback = clone!(pd => move || {
|
||||
if let Err(err) = utils::uningore_show(pd.id()) {
|
||||
error!("Could not remove {} from the ignore list.", pd.title());
|
||||
error!("Error: {}", err);
|
||||
}
|
||||
|
||||
// Spawn a thread so it won't block the ui.
|
||||
rayon::spawn(clone!(pd => move || {
|
||||
if let Err(err) = delete_show(&pd) {
|
||||
error!("Something went wrong trying to remove {}", pd.title());
|
||||
error!("Error: {}", err);
|
||||
}
|
||||
}));
|
||||
glib::Continue(false)
|
||||
});
|
||||
|
||||
let sender_ = sender.clone();
|
||||
let undo_wrap = move || -> Result<(), Error> {
|
||||
utils::uningore_show(pd.id())?;
|
||||
sender_.send(Action::RefreshShowsView)?;
|
||||
sender_.send(Action::RefreshEpisodesView)?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let undo_callback = move || {
|
||||
if let Err(err) = undo_wrap() {
|
||||
error!("{}", err)
|
||||
}
|
||||
};
|
||||
|
||||
let sender_ = sender.clone();
|
||||
let notif = InAppNotification::new(text, callback, undo_callback, sender_);
|
||||
notif.show(&overlay);
|
||||
}
|
||||
Err(_) => (),
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
use glib;
|
||||
use gtk;
|
||||
use gtk::prelude::*;
|
||||
|
||||
use app::Action;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InAppNotification {
|
||||
revealer: gtk::Revealer,
|
||||
text: gtk::Label,
|
||||
undo: gtk::Button,
|
||||
close: gtk::Button,
|
||||
}
|
||||
|
||||
impl Default for InAppNotification {
|
||||
fn default() -> Self {
|
||||
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/inapp_notif.ui");
|
||||
|
||||
let revealer: gtk::Revealer = builder.get_object("revealer").unwrap();
|
||||
let text: gtk::Label = builder.get_object("text").unwrap();
|
||||
let undo: gtk::Button = builder.get_object("undo").unwrap();
|
||||
let close: gtk::Button = builder.get_object("close").unwrap();
|
||||
|
||||
InAppNotification {
|
||||
revealer,
|
||||
text,
|
||||
undo,
|
||||
close,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InAppNotification {
|
||||
pub fn new<F, U>(
|
||||
text: String,
|
||||
mut callback: F,
|
||||
undo_callback: U,
|
||||
sender: Sender<Action>,
|
||||
) -> Self
|
||||
where
|
||||
F: FnMut() -> glib::Continue + 'static,
|
||||
U: Fn() + 'static,
|
||||
{
|
||||
let notif = InAppNotification::default();
|
||||
notif.text.set_text(&text);
|
||||
|
||||
let revealer = notif.revealer.clone();
|
||||
let id = timeout_add_seconds(6, move || {
|
||||
revealer.set_reveal_child(false);
|
||||
callback()
|
||||
});
|
||||
let id = Rc::new(RefCell::new(Some(id)));
|
||||
|
||||
// Cancel the callback
|
||||
let revealer = notif.revealer.clone();
|
||||
notif.undo.connect_clicked(move |_| {
|
||||
let foo = id.borrow_mut().take();
|
||||
if let Some(id) = foo {
|
||||
glib::source::source_remove(id);
|
||||
}
|
||||
|
||||
undo_callback();
|
||||
|
||||
// Hide the notification
|
||||
revealer.set_reveal_child(false);
|
||||
// Refresh the widget if visible
|
||||
if let Err(err) = sender.send(Action::RefreshWidgetIfVis) {
|
||||
error!("Action channel blew up: {}", err)
|
||||
}
|
||||
});
|
||||
|
||||
// Hide the revealer when the close button is clicked
|
||||
let revealer = notif.revealer.clone();
|
||||
notif.close.connect_clicked(move |_| {
|
||||
revealer.set_reveal_child(false);
|
||||
});
|
||||
|
||||
notif
|
||||
}
|
||||
|
||||
// This is a seperate method cause in order to get a nice animation
|
||||
// the revealer should be attached to something that displays it.
|
||||
// Previously we where doing it in the constructor, which had the result
|
||||
// of the animation being skipped cause there was no parent widget to display it.
|
||||
pub fn show(&self, overlay: >k::Overlay) {
|
||||
overlay.add_overlay(&self.revealer);
|
||||
// We need to display the notification after the widget is added to the overlay
|
||||
// so there will be a nice animation.
|
||||
self.revealer.set_reveal_child(true);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ use std::sync::mpsc::Sender;
|
||||
|
||||
use app::Action;
|
||||
use stacks::Content;
|
||||
use utils::itunes_to_rss;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Header {
|
||||
@@ -154,8 +155,18 @@ impl Header {
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: THIS ALSO SUCKS!
|
||||
fn on_add_bttn_clicked(entry: >k::Entry, sender: Sender<Action>) -> Result<(), Error> {
|
||||
let url = entry.get_text().unwrap_or_default();
|
||||
let url = if url.contains("itunes.com") || url.contains("apple.com") {
|
||||
info!("Detected itunes url.");
|
||||
let foo = itunes_to_rss(&url)?;
|
||||
info!("Resolved to {}", foo);
|
||||
foo
|
||||
} else {
|
||||
url.to_owned()
|
||||
};
|
||||
|
||||
let source = Source::from_url(&url).context("Failed to convert url to a Source entry.")?;
|
||||
entry.set_text("");
|
||||
|
||||
@@ -165,6 +176,7 @@ fn on_add_bttn_clicked(entry: >k::Entry, sender: Sender<Action>) -> Result<(),
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// FIXME: THIS SUCKS!
|
||||
fn on_url_change(
|
||||
entry: >k::Entry,
|
||||
result: >k::Label,
|
||||
@@ -214,19 +226,20 @@ fn about_dialog(window: >k::Window) {
|
||||
"James Wykeham-Martin",
|
||||
"Jordan Petridis",
|
||||
"Julian Sparber",
|
||||
"Rowan Lewis",
|
||||
];
|
||||
|
||||
let dialog = gtk::AboutDialog::new();
|
||||
// Waiting for a logo.
|
||||
// dialog.set_logo_icon_name("org.gnome.Hammond");
|
||||
dialog.set_logo_icon_name("multimedia-player");
|
||||
dialog.set_comments("A Podcast Client for the GNOME Desktop.");
|
||||
dialog.set_comments("Podcast Client for the GNOME Desktop.");
|
||||
dialog.set_copyright("© 2017, 2018 Jordan Petridis");
|
||||
dialog.set_license_type(gtk::License::Gpl30);
|
||||
dialog.set_modal(true);
|
||||
// TODO: make it show it fetches the commit hash from which it was built
|
||||
// and the version number is kept in sync automaticly
|
||||
dialog.set_version("0.3");
|
||||
dialog.set_version("0.3.1");
|
||||
dialog.set_program_name("Hammond");
|
||||
// TODO: Need a wiki page first.
|
||||
// dialog.set_website("https://wiki.gnome.org/Design/Apps/Potential/Podcasts");
|
||||
|
||||
+18
-11
@@ -1,5 +1,8 @@
|
||||
#![cfg_attr(feature = "cargo-clippy", allow(clone_on_ref_ptr, needless_pass_by_value))]
|
||||
// #![deny(unused_extern_crates, unused)]
|
||||
#![cfg_attr(feature = "cargo-clippy",
|
||||
allow(clone_on_ref_ptr, needless_pass_by_value, useless_format, blacklisted_name,
|
||||
match_same_arms))]
|
||||
#![allow(unknown_lints)]
|
||||
#![deny(unused_extern_crates, unused)]
|
||||
|
||||
extern crate gdk;
|
||||
extern crate gdk_pixbuf;
|
||||
@@ -16,18 +19,26 @@ extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
extern crate pretty_assertions;
|
||||
|
||||
extern crate chrono;
|
||||
extern crate dissolve;
|
||||
extern crate crossbeam_channel;
|
||||
extern crate hammond_data;
|
||||
extern crate hammond_downloader;
|
||||
extern crate html2pango;
|
||||
extern crate humansize;
|
||||
extern crate loggerv;
|
||||
extern crate open;
|
||||
extern crate rayon;
|
||||
extern crate regex;
|
||||
extern crate reqwest;
|
||||
extern crate send_cell;
|
||||
extern crate serde_json;
|
||||
extern crate take_mut;
|
||||
extern crate url;
|
||||
// extern crate rayon;
|
||||
|
||||
// use rayon::prelude::*;
|
||||
use log::Level;
|
||||
|
||||
use gtk::prelude::*;
|
||||
@@ -60,9 +71,11 @@ pub mod stacks;
|
||||
pub mod headerbar;
|
||||
pub mod app;
|
||||
|
||||
pub mod settings;
|
||||
pub mod utils;
|
||||
pub mod manager;
|
||||
pub mod static_resource;
|
||||
pub mod appnotif;
|
||||
|
||||
use app::App;
|
||||
|
||||
@@ -81,11 +94,5 @@ fn main() {
|
||||
600,
|
||||
);
|
||||
|
||||
// This set's the app to dark mode.
|
||||
// It wiil be in the user's preference later.
|
||||
// Uncomment it to run with the dark theme variant.
|
||||
// let settings = gtk::Settings::get_default().unwrap();
|
||||
// settings.set_property_gtk_application_prefer_dark_theme(true);
|
||||
|
||||
App::new().run();
|
||||
}
|
||||
|
||||
+18
-21
@@ -1,4 +1,5 @@
|
||||
use failure::Error;
|
||||
use rayon;
|
||||
|
||||
// use hammond_data::Episode;
|
||||
use hammond_data::dbqueries;
|
||||
@@ -11,7 +12,6 @@ use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::sync::mpsc::Sender;
|
||||
// use std::sync::atomic::AtomicUsize;
|
||||
// use std::path::PathBuf;
|
||||
use std::thread;
|
||||
|
||||
// This is messy, undocumented and hacky af.
|
||||
// I am terrible at writting downloaders and download managers.
|
||||
@@ -73,44 +73,38 @@ impl DownloadProgress for Progress {
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref ACTIVE_DOWNLOADS: Arc<RwLock<HashMap<i32, Arc<Mutex<Progress>>>>> = {
|
||||
Arc::new(RwLock::new(HashMap::new()))
|
||||
};
|
||||
pub static ref ACTIVE_DOWNLOADS: Arc<RwLock<HashMap<i32, Arc<Mutex<Progress>>>>> =
|
||||
{ Arc::new(RwLock::new(HashMap::new())) };
|
||||
static ref DLPOOL: rayon::ThreadPool = rayon::ThreadPoolBuilder::new().build().unwrap();
|
||||
}
|
||||
|
||||
pub fn add(id: i32, directory: &str, sender: Sender<Action>) -> Result<(), Error> {
|
||||
pub fn add(id: i32, directory: String, sender: Sender<Action>) -> Result<(), Error> {
|
||||
// Create a new `Progress` struct to keep track of dl progress.
|
||||
let prog = Arc::new(Mutex::new(Progress::default()));
|
||||
|
||||
{
|
||||
let mut m = ACTIVE_DOWNLOADS
|
||||
.write()
|
||||
.map_err(|_| format_err!("Failed to get a lock on the mutex."))?;
|
||||
m.insert(id, prog.clone());
|
||||
}
|
||||
match ACTIVE_DOWNLOADS.write() {
|
||||
Ok(mut guard) => guard.insert(id, prog.clone()),
|
||||
Err(err) => return Err(format_err!("ActiveDonwloads: {}.", err)),
|
||||
};
|
||||
|
||||
let dir = directory.to_owned();
|
||||
thread::spawn(move || {
|
||||
DLPOOL.spawn(move || {
|
||||
if let Ok(episode) = dbqueries::get_episode_from_rowid(id) {
|
||||
let pid = episode.podcast_id();
|
||||
let id = episode.rowid();
|
||||
|
||||
if let Err(err) = get_episode(&mut episode.into(), dir.as_str(), Some(prog)) {
|
||||
if let Err(err) = get_episode(&mut episode.into(), directory.as_str(), Some(prog)) {
|
||||
error!("Error while trying to download an episode");
|
||||
error!("Error: {}", err);
|
||||
}
|
||||
|
||||
{
|
||||
if let Ok(mut m) = ACTIVE_DOWNLOADS.write() {
|
||||
info!("Removed: {:?}", m.remove(&id));
|
||||
}
|
||||
let foo = m.remove(&id);
|
||||
debug!("Removed: {:?}", foo);
|
||||
}
|
||||
|
||||
// {
|
||||
// if let Ok(m) = ACTIVE_DOWNLOADS.read() {
|
||||
// debug!("ACTIVE DOWNLOADS: {:#?}", m);
|
||||
// }
|
||||
// }
|
||||
|
||||
sender
|
||||
.send(Action::RefreshEpisodesView)
|
||||
@@ -163,18 +157,21 @@ mod tests {
|
||||
let (sender, _rx) = channel();
|
||||
|
||||
let download_fold = get_download_folder(&pd.title()).unwrap();
|
||||
add(episode.rowid(), download_fold.as_str(), sender).unwrap();
|
||||
let fold2 = download_fold.clone();
|
||||
add(episode.rowid(), download_fold, sender).unwrap();
|
||||
assert_eq!(ACTIVE_DOWNLOADS.read().unwrap().len(), 1);
|
||||
|
||||
// Give it soem time to download the file
|
||||
thread::sleep(time::Duration::from_secs(20));
|
||||
|
||||
let final_path = format!("{}/{}.mp3", &download_fold, episode.rowid());
|
||||
let final_path = format!("{}/{}.mp3", &fold2, episode.rowid());
|
||||
assert!(Path::new(&final_path).exists());
|
||||
fs::remove_file(final_path).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
// This test needs access to local system so we ignore it by default.
|
||||
#[ignore]
|
||||
fn test_dl_steal_the_stars() {
|
||||
let url =
|
||||
"https://web.archive.org/web/20180120104957if_/https://rss.art19.com/steal-the-stars";
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
use gio;
|
||||
use gio::SettingsExt;
|
||||
use gtk;
|
||||
use gtk::GtkWindowExt;
|
||||
|
||||
pub struct WindowGeometry {
|
||||
left: i32,
|
||||
top: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
is_maximized: bool,
|
||||
}
|
||||
|
||||
impl WindowGeometry {
|
||||
pub fn from_window(window: >k::Window) -> WindowGeometry {
|
||||
let position = window.get_position();
|
||||
let size = window.get_size();
|
||||
let left = position.0;
|
||||
let top = position.1;
|
||||
let width = size.0;
|
||||
let height = size.1;
|
||||
let is_maximized = window.is_maximized();
|
||||
|
||||
WindowGeometry {
|
||||
left,
|
||||
top,
|
||||
width,
|
||||
height,
|
||||
is_maximized,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_settings(settings: &gio::Settings) -> WindowGeometry {
|
||||
let top = settings.get_int("persist-window-geometry-top");
|
||||
let left = settings.get_int("persist-window-geometry-left");
|
||||
let width = settings.get_int("persist-window-geometry-width");
|
||||
let height = settings.get_int("persist-window-geometry-height");
|
||||
let is_maximized = settings.get_boolean("persist-window-geometry-maximized");
|
||||
|
||||
WindowGeometry {
|
||||
left,
|
||||
top,
|
||||
width,
|
||||
height,
|
||||
is_maximized,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply(&self, window: >k::Window) {
|
||||
if self.width > 0 && self.height > 0 {
|
||||
window.resize(self.width, self.height);
|
||||
}
|
||||
|
||||
if self.is_maximized {
|
||||
window.maximize();
|
||||
} else if self.top > 0 && self.left > 0 {
|
||||
window.move_(self.left, self.top);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(&self, settings: &gio::Settings) {
|
||||
settings.set_int("persist-window-geometry-left", self.left);
|
||||
settings.set_int("persist-window-geometry-top", self.top);
|
||||
settings.set_int("persist-window-geometry-width", self.width);
|
||||
settings.set_int("persist-window-geometry-height", self.height);
|
||||
settings.set_boolean("persist-window-geometry-maximized", self.is_maximized);
|
||||
}
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn test_apply_window_geometry() {
|
||||
// gtk::init().expect("Error initializing gtk.");
|
||||
|
||||
// let window = gtk::Window::new(gtk::WindowType::Toplevel);
|
||||
// let _geometry = WindowGeometry {
|
||||
// left: 0,
|
||||
// top: 0,
|
||||
// width: 100,
|
||||
// height: 100,
|
||||
// is_maximized: true
|
||||
// };
|
||||
|
||||
// assert!(!window.is_maximized());
|
||||
|
||||
// window.show();
|
||||
// window.activate();
|
||||
// geometry.apply(&window);
|
||||
|
||||
// assert!(window.is_maximized());
|
||||
// }
|
||||
@@ -102,8 +102,9 @@ impl ShowStack {
|
||||
debug!("Name: {:?}", WidgetExt::get_name(&old));
|
||||
|
||||
let new = ShowWidget::new(pd, self.sender.clone());
|
||||
// Each composite ShowWidget is a gtkBox with the Podcast.id encoded in the gtk::Widget
|
||||
// name. It's a hack since we can't yet subclass GObject easily.
|
||||
// Each composite ShowWidget is a gtkBox with the Podcast.id encoded in the
|
||||
// gtk::Widget name. It's a hack since we can't yet subclass GObject
|
||||
// easily.
|
||||
let oldid = WidgetExt::get_name(&old);
|
||||
let newid = WidgetExt::get_name(&new.container);
|
||||
debug!("Old widget Name: {:?}\nNew widget Name: {:?}", oldid, newid);
|
||||
|
||||
@@ -2,14 +2,16 @@ use gio::{resources_register, Error, Resource};
|
||||
use glib::Bytes;
|
||||
|
||||
pub fn init() -> Result<(), Error> {
|
||||
// load the gresource binary at build time and include/link it into the final binary.
|
||||
// load the gresource binary at build time and include/link it into the final
|
||||
// binary.
|
||||
let res_bytes = include_bytes!("../resources/resources.gresource");
|
||||
|
||||
// Create Resource it will live as long the value lives.
|
||||
let gbytes = Bytes::from_static(res_bytes.as_ref());
|
||||
let resource = Resource::new_from_data(&gbytes)?;
|
||||
|
||||
// Register the resource so It wont be dropped and will continue to live in memory.
|
||||
// Register the resource so It wont be dropped and will continue to live in
|
||||
// memory.
|
||||
resources_register(&resource);
|
||||
|
||||
Ok(())
|
||||
|
||||
+240
-44
@@ -1,48 +1,108 @@
|
||||
#![cfg_attr(feature = "cargo-clippy", allow(type_complexity))]
|
||||
|
||||
use failure::Error;
|
||||
use gdk_pixbuf::Pixbuf;
|
||||
use gio::{Settings, SettingsExt};
|
||||
use glib;
|
||||
use gtk;
|
||||
use gtk::prelude::*;
|
||||
|
||||
use failure::Error;
|
||||
use rayon;
|
||||
use regex::Regex;
|
||||
use reqwest;
|
||||
use send_cell::SendCell;
|
||||
use serde_json::Value;
|
||||
|
||||
// use hammond_data::feed;
|
||||
use hammond_data::{PodcastCoverQuery, Source};
|
||||
use hammond_data::dbqueries;
|
||||
use hammond_data::pipeline;
|
||||
use hammond_data::utils::checkup;
|
||||
use hammond_downloader::downloader;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::{Mutex, RwLock};
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::thread;
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::*;
|
||||
|
||||
use app::Action;
|
||||
|
||||
pub fn refresh_feed_wrapper(source: Option<Vec<Source>>, sender: Sender<Action>) {
|
||||
use chrono::Duration;
|
||||
use chrono::prelude::*;
|
||||
|
||||
lazy_static! {
|
||||
static ref IGNORESHOWS: Arc<Mutex<HashSet<i32>>> = Arc::new(Mutex::new(HashSet::new()));
|
||||
}
|
||||
|
||||
pub fn ignore_show(id: i32) -> Result<bool, Error> {
|
||||
IGNORESHOWS
|
||||
.lock()
|
||||
.map(|mut guard| guard.insert(id))
|
||||
.map_err(|err| format_err!("{}", err))
|
||||
}
|
||||
|
||||
pub fn uningore_show(id: i32) -> Result<bool, Error> {
|
||||
IGNORESHOWS
|
||||
.lock()
|
||||
.map(|mut guard| guard.remove(&id))
|
||||
.map_err(|err| format_err!("{}", err))
|
||||
}
|
||||
|
||||
pub fn get_ignored_shows() -> Result<Vec<i32>, Error> {
|
||||
IGNORESHOWS
|
||||
.lock()
|
||||
.map(|guard| guard.iter().cloned().collect::<Vec<_>>())
|
||||
.map_err(|err| format_err!("{}", err))
|
||||
}
|
||||
|
||||
pub fn cleanup(cleanup_date: DateTime<Utc>) {
|
||||
if let Err(err) = checkup(cleanup_date) {
|
||||
error!("Check up failed: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh(source: Option<Vec<Source>>, sender: Sender<Action>) {
|
||||
if let Err(err) = refresh_feed(source, sender) {
|
||||
error!("An error occured while trying to update the feeds.");
|
||||
error!("Error: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_refresh_interval(settings: &Settings) -> Duration {
|
||||
let time = settings.get_int("refresh-interval-time") as i64;
|
||||
let period = settings.get_string("refresh-interval-period").unwrap();
|
||||
|
||||
time_period_to_duration(time, period.as_str())
|
||||
}
|
||||
|
||||
pub fn get_cleanup_date(settings: &Settings) -> DateTime<Utc> {
|
||||
let time = settings.get_int("cleanup-age-time") as i64;
|
||||
let period = settings.get_string("cleanup-age-period").unwrap();
|
||||
let duration = time_period_to_duration(time, period.as_str());
|
||||
|
||||
Utc::now() - duration
|
||||
}
|
||||
|
||||
/// Update the rss feed(s) originating from `source`.
|
||||
/// If `source` is None, Fetches all the `Source` entries in the database and updates them.
|
||||
/// When It's done,it queues up a `RefreshViews` action.
|
||||
fn refresh_feed(source: Option<Vec<Source>>, sender: Sender<Action>) -> Result<(), Error> {
|
||||
sender.send(Action::HeaderBarShowUpdateIndicator)?;
|
||||
|
||||
thread::spawn(move || {
|
||||
rayon::spawn(move || {
|
||||
let mut sources = source.unwrap_or_else(|| {
|
||||
dbqueries::get_sources().expect("Failed to retrieve Sources from the database.")
|
||||
});
|
||||
|
||||
// Work around to improve the feed addition experience.
|
||||
// Many times links to rss feeds are just redirects(usually to an https version).
|
||||
// Sadly I haven't figured yet a nice way to follow up links redirects without getting
|
||||
// to lifetime hell with futures and hyper.
|
||||
// Many times links to rss feeds are just redirects(usually to an https
|
||||
// version). Sadly I haven't figured yet a nice way to follow up links
|
||||
// redirects without getting to lifetime hell with futures and hyper.
|
||||
// So the requested refresh is only of 1 feed, and the feed fails to be indexed,
|
||||
// (as a 301 redict would update the source entry and exit), another refresh is run.
|
||||
// For more see hammond_data/src/models/source.rs `fn request_constructor`.
|
||||
// also ping me on irc if or open an issue if you want to tackle it.
|
||||
// (as a 301 redict would update the source entry and exit), another refresh is
|
||||
// run. For more see hammond_data/src/models/source.rs `fn
|
||||
// request_constructor`. also ping me on irc if or open an issue if you
|
||||
// want to tackle it.
|
||||
if sources.len() == 1 {
|
||||
let source = sources.remove(0);
|
||||
let id = source.id();
|
||||
@@ -56,13 +116,11 @@ fn refresh_feed(source: Option<Vec<Source>>, sender: Sender<Action>) -> Result<(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This is what would normally run
|
||||
if let Err(err) = pipeline::run(sources, false) {
|
||||
} else if let Err(err) = pipeline::run(sources, false) {
|
||||
error!("Error While trying to update the database.");
|
||||
error!("Error msg: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
sender
|
||||
.send(Action::HeaderBarHideUpdateIndicator)
|
||||
@@ -75,9 +133,10 @@ fn refresh_feed(source: Option<Vec<Source>>, sender: Sender<Action>) -> Result<(
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref CACHED_PIXBUFS: RwLock<HashMap<(i32, u32), Mutex<SendCell<Pixbuf>>>> = {
|
||||
RwLock::new(HashMap::new())
|
||||
};
|
||||
static ref CACHED_PIXBUFS: RwLock<HashMap<(i32, u32), Mutex<SendCell<Pixbuf>>>> =
|
||||
{ RwLock::new(HashMap::new()) };
|
||||
static ref COVER_DL_REGISTRY: RwLock<HashSet<i32>> = RwLock::new(HashSet::new());
|
||||
static ref THREADPOOL: rayon::ThreadPool = rayon::ThreadPoolBuilder::new().build().unwrap();
|
||||
}
|
||||
|
||||
// Since gdk_pixbuf::Pixbuf is refference counted and every episode,
|
||||
@@ -87,48 +146,185 @@ lazy_static! {
|
||||
// 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) -> Result<Pixbuf, Error> {
|
||||
{
|
||||
let hashmap = CACHED_PIXBUFS
|
||||
.read()
|
||||
.map_err(|_| format_err!("Failed to get a lock on the pixbuf cache mutex."))?;
|
||||
if let Some(px) = hashmap.get(&(pd.id(), size)) {
|
||||
let m = px.lock()
|
||||
.map_err(|_| format_err!("Failed to lock pixbuf mutex."))?;
|
||||
return Ok(m.clone().into_inner());
|
||||
pub fn set_image_from_path(
|
||||
image: >k::Image,
|
||||
pd: Arc<PodcastCoverQuery>,
|
||||
size: u32,
|
||||
) -> Result<(), Error> {
|
||||
// Check if there's an active download about this show cover.
|
||||
// If there is, a callback will be set so this function will be called again.
|
||||
// If the download succedes, there should be a quick return from the pixbuf cache_image
|
||||
// If it fails another download will be scheduled.
|
||||
if let Ok(guard) = COVER_DL_REGISTRY.read() {
|
||||
if guard.contains(&pd.id()) {
|
||||
let callback = clone!(image, pd => move || {
|
||||
let _ = set_image_from_path(&image, pd.clone(), size);
|
||||
glib::Continue(false)
|
||||
});
|
||||
gtk::timeout_add(250, callback);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let img_path = downloader::cache_image(pd)?;
|
||||
let px = Pixbuf::new_from_file_at_scale(&img_path, size as i32, size as i32, true)?;
|
||||
let mut hashmap = CACHED_PIXBUFS
|
||||
.write()
|
||||
.map_err(|_| format_err!("Failed to lock pixbuf mutex."))?;
|
||||
if let Ok(hashmap) = CACHED_PIXBUFS.read() {
|
||||
// Check if the requested (cover + size) is already in the chache
|
||||
// and if so do an early return after that.
|
||||
if let Some(guard) = hashmap.get(&(pd.id(), size)) {
|
||||
guard
|
||||
.lock()
|
||||
.map_err(|err| format_err!("SendCell Mutex: {}", err))
|
||||
.and_then(|sendcell| {
|
||||
sendcell
|
||||
.try_get()
|
||||
.map(|px| image.set_from_pixbuf(px))
|
||||
.ok_or_else(|| format_err!("Pixbuf was accessed from a different thread"))
|
||||
})?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let (sender, receiver) = channel();
|
||||
let pd_ = pd.clone();
|
||||
THREADPOOL.spawn(move || {
|
||||
if let Ok(mut guard) = COVER_DL_REGISTRY.write() {
|
||||
guard.insert(pd_.id());
|
||||
}
|
||||
|
||||
let _ = sender.send(downloader::cache_image(&pd_));
|
||||
|
||||
if let Ok(mut guard) = COVER_DL_REGISTRY.write() {
|
||||
guard.remove(&pd_.id());
|
||||
}
|
||||
});
|
||||
|
||||
let image = image.clone();
|
||||
let s = size as i32;
|
||||
gtk::timeout_add(25, move || {
|
||||
if let Ok(path) = receiver.try_recv() {
|
||||
if let Ok(path) = path {
|
||||
if let Ok(px) = Pixbuf::new_from_file_at_scale(&path, s, s, true) {
|
||||
if let Ok(mut hashmap) = CACHED_PIXBUFS.write() {
|
||||
hashmap.insert((pd.id(), size), Mutex::new(SendCell::new(px.clone())));
|
||||
Ok(px)
|
||||
image.set_from_pixbuf(&px);
|
||||
}
|
||||
}
|
||||
}
|
||||
glib::Continue(false)
|
||||
} else {
|
||||
glib::Continue(true)
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
// FIXME: the signature should be `fn foo(s: Url) -> Result<Url, Error>`
|
||||
pub fn itunes_to_rss(url: &str) -> Result<String, Error> {
|
||||
let id = itunes_id_from_url(url).ok_or_else(|| format_err!("Failed to find an Itunes ID."))?;
|
||||
lookup_id(id)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn itunes_id_from_url(url: &str) -> Option<u32> {
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(r"/id([0-9]+)").unwrap();
|
||||
}
|
||||
|
||||
// Get the itunes id from the url
|
||||
let foo = RE.captures_iter(url).nth(0)?.get(1)?.as_str();
|
||||
// Parse it to a u32, this *should* never fail
|
||||
foo.parse::<u32>().ok()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn lookup_id(id: u32) -> Result<String, Error> {
|
||||
let url = format!("https://itunes.apple.com/lookup?id={}&entity=podcast", id);
|
||||
let req: Value = reqwest::get(&url)?.json()?;
|
||||
let rssurl = || -> Option<&str> { req.get("results")?.get(0)?.get("feedUrl")?.as_str() };
|
||||
rssurl()
|
||||
.map(From::from)
|
||||
.ok_or_else(|| format_err!("Failed to get url from itunes response"))
|
||||
}
|
||||
|
||||
pub fn time_period_to_duration(time: i64, period: &str) -> Duration {
|
||||
match period {
|
||||
"weeks" => Duration::weeks(time),
|
||||
"days" => Duration::days(time),
|
||||
"hours" => Duration::hours(time),
|
||||
"minutes" => Duration::minutes(time),
|
||||
_ => Duration::seconds(time),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use hammond_data::Source;
|
||||
use hammond_data::dbqueries;
|
||||
// use hammond_data::Source;
|
||||
// use hammond_data::dbqueries;
|
||||
|
||||
#[test]
|
||||
fn test_time_period_to_duration() {
|
||||
let time = 2;
|
||||
let week = 604800 * time;
|
||||
let day = 86400 * time;
|
||||
let hour = 3600 * time;
|
||||
let minute = 60 * time;
|
||||
|
||||
assert_eq!(week, time_period_to_duration(time, "weeks").num_seconds());
|
||||
assert_eq!(day, time_period_to_duration(time, "days").num_seconds());
|
||||
assert_eq!(hour, time_period_to_duration(time, "hours").num_seconds());
|
||||
assert_eq!(
|
||||
minute,
|
||||
time_period_to_duration(time, "minutes").num_seconds()
|
||||
);
|
||||
assert_eq!(time, time_period_to_duration(time, "seconds").num_seconds());
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// This test inserts an rss feed to your `XDG_DATA/hammond/hammond.db` so we make it explicit
|
||||
// to run it.
|
||||
#[ignore]
|
||||
fn test_get_pixbuf_from_path() {
|
||||
let url = "https://web.archive.org/web/20180120110727if_/https://rss.acast.com/thetipoff";
|
||||
// #[ignore]
|
||||
// Disabled till https://gitlab.gnome.org/alatiera/Hammond/issues/56
|
||||
// fn test_set_image_from_path() {
|
||||
// let url = "https://web.archive.org/web/20180120110727if_/https://rss.acast.com/thetipoff";
|
||||
// Create and index a source
|
||||
let source = Source::from_url(url).unwrap();
|
||||
// let source = Source::from_url(url).unwrap();
|
||||
// Copy it's id
|
||||
let sid = source.id();
|
||||
pipeline::run(vec![source], true).unwrap();
|
||||
// let sid = source.id();
|
||||
// pipeline::run(vec![source], true).unwrap();
|
||||
|
||||
// Get the Podcast
|
||||
let pd = dbqueries::get_podcast_from_source_id(sid).unwrap();
|
||||
let pxbuf = get_pixbuf_from_path(&pd.into(), 256);
|
||||
assert!(pxbuf.is_ok());
|
||||
// let img = gtk::Image::new();
|
||||
// let pd = dbqueries::get_podcast_from_source_id(sid).unwrap().into();
|
||||
// let pxbuf = set_image_from_path(&img, Arc::new(pd), 256);
|
||||
// assert!(pxbuf.is_ok());
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn test_itunes_to_rss() {
|
||||
let itunes_url = "https://itunes.apple.com/podcast/id1195206601";
|
||||
let rss_url = String::from("http://feeds.feedburner.com/InterceptedWithJeremyScahill");
|
||||
assert_eq!(rss_url, itunes_to_rss(itunes_url).unwrap());
|
||||
|
||||
let itunes_url = "https://itunes.apple.com/podcast/id000000000000000";
|
||||
assert!(itunes_to_rss(itunes_url).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_itunes_id() {
|
||||
let id = 1195206601;
|
||||
let itunes_url = "https://itunes.apple.com/podcast/id1195206601";
|
||||
assert_eq!(id, itunes_id_from_url(itunes_url).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_itunes_lookup_id() {
|
||||
let id = 1195206601;
|
||||
let rss_url = "http://feeds.feedburner.com/InterceptedWithJeremyScahill";
|
||||
assert_eq!(rss_url, lookup_id(id).unwrap());
|
||||
|
||||
let id = 000000000;
|
||||
assert!(lookup_id(id).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,10 @@ use hammond_data::EpisodeWidgetQuery;
|
||||
use hammond_data::dbqueries;
|
||||
|
||||
use app::Action;
|
||||
use utils::get_pixbuf_from_path;
|
||||
use utils::{get_ignored_shows, set_image_from_path};
|
||||
use widgets::EpisodeWidget;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -77,7 +78,8 @@ impl Default for EpisodesView {
|
||||
impl EpisodesView {
|
||||
pub fn new(sender: Sender<Action>) -> Result<EpisodesView, Error> {
|
||||
let view = EpisodesView::default();
|
||||
let episodes = dbqueries::get_episodes_widgets_with_limit(50)?;
|
||||
let ignore = get_ignored_shows()?;
|
||||
let episodes = dbqueries::get_episodes_widgets_filter_limit(&ignore, 50)?;
|
||||
let now_utc = Utc::now();
|
||||
|
||||
episodes.into_iter().for_each(|ep| {
|
||||
@@ -226,9 +228,7 @@ impl EpisodesViewWidget {
|
||||
}
|
||||
|
||||
fn set_cover(&self, podcast_id: i32) -> Result<(), Error> {
|
||||
let pd = dbqueries::get_podcast_cover_from_id(podcast_id)?;
|
||||
let img = get_pixbuf_from_path(&pd, 64)?;
|
||||
self.image.set_from_pixbuf(&img);
|
||||
Ok(())
|
||||
let pd = Arc::new(dbqueries::get_podcast_cover_from_id(podcast_id)?);
|
||||
set_image_from_path(&self.image, pd, 64)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ use failure::Error;
|
||||
use gtk;
|
||||
use gtk::prelude::*;
|
||||
|
||||
use hammond_data::Podcast;
|
||||
use hammond_data::{Podcast, PodcastCoverQuery};
|
||||
use hammond_data::dbqueries;
|
||||
|
||||
use app::Action;
|
||||
use utils::get_pixbuf_from_path;
|
||||
use utils::{get_ignored_shows, set_image_from_path};
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::Sender;
|
||||
@@ -54,12 +54,10 @@ impl ShowsPopulated {
|
||||
}
|
||||
|
||||
fn populate_flowbox(&self) -> Result<(), Error> {
|
||||
let podcasts = dbqueries::get_podcasts()?;
|
||||
let ignore = get_ignored_shows()?;
|
||||
let podcasts = dbqueries::get_podcasts_filter(&ignore)?;
|
||||
|
||||
podcasts
|
||||
.into_iter()
|
||||
.map(|pd| Arc::new(pd))
|
||||
.for_each(|parent| {
|
||||
podcasts.into_iter().for_each(|parent| {
|
||||
let flowbox_child = ShowsChild::new(parent);
|
||||
self.flowbox.add(&flowbox_child.child);
|
||||
});
|
||||
@@ -118,25 +116,23 @@ impl Default for ShowsChild {
|
||||
}
|
||||
|
||||
impl ShowsChild {
|
||||
pub fn new(pd: Arc<Podcast>) -> ShowsChild {
|
||||
pub fn new(pd: Podcast) -> ShowsChild {
|
||||
let child = ShowsChild::default();
|
||||
child.init(pd);
|
||||
child
|
||||
}
|
||||
|
||||
fn init(&self, pd: Arc<Podcast>) {
|
||||
fn init(&self, pd: Podcast) {
|
||||
self.container.set_tooltip_text(pd.title());
|
||||
WidgetExt::set_name(&self.child, &pd.id().to_string());
|
||||
|
||||
if let Err(err) = self.set_cover(pd.clone()) {
|
||||
let pd = Arc::new(pd.into());
|
||||
if let Err(err) = self.set_cover(pd) {
|
||||
error!("Failed to set a cover: {}", err)
|
||||
}
|
||||
|
||||
WidgetExt::set_name(&self.child, &pd.id().to_string());
|
||||
}
|
||||
|
||||
fn set_cover(&self, pd: Arc<Podcast>) -> Result<(), Error> {
|
||||
let image = get_pixbuf_from_path(&pd.clone().into(), 256)?;
|
||||
self.cover.set_from_pixbuf(&image);
|
||||
Ok(())
|
||||
fn set_cover(&self, pd: Arc<PodcastCoverQuery>) -> Result<(), Error> {
|
||||
set_image_from_path(&self.cover, pd, 256)
|
||||
}
|
||||
}
|
||||
|
||||
+161
-181
@@ -1,13 +1,12 @@
|
||||
use glib;
|
||||
use gtk;
|
||||
|
||||
use chrono::prelude::*;
|
||||
use gtk::prelude::*;
|
||||
|
||||
use chrono::Duration;
|
||||
use failure::Error;
|
||||
use humansize::{file_size_opts as size_opts, FileSize};
|
||||
use humansize::FileSize;
|
||||
use open;
|
||||
use rayon;
|
||||
use take_mut;
|
||||
|
||||
use hammond_data::{EpisodeWidgetQuery, Podcast};
|
||||
use hammond_data::dbqueries;
|
||||
@@ -15,46 +14,22 @@ use hammond_data::utils::get_download_folder;
|
||||
|
||||
use app::Action;
|
||||
use manager;
|
||||
use widgets::episode_states::*;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::ops::DerefMut;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
lazy_static! {
|
||||
static ref SIZE_OPTS: Arc<size_opts::FileSizeOpts> = {
|
||||
// Declare a custom humansize option struct
|
||||
// See: https://docs.rs/humansize/1.0.2/humansize/file_size_opts/struct.FileSizeOpts.html
|
||||
Arc::new(size_opts::FileSizeOpts {
|
||||
divider: size_opts::Kilo::Binary,
|
||||
units: size_opts::Kilo::Decimal,
|
||||
decimal_places: 0,
|
||||
decimal_zeroes: 0,
|
||||
fixed_at: size_opts::FixedAt::No,
|
||||
long_units: false,
|
||||
space: true,
|
||||
suffix: "",
|
||||
allow_negative: false,
|
||||
})
|
||||
};
|
||||
|
||||
static ref NOW: DateTime<Utc> = Utc::now();
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug)]
|
||||
pub struct EpisodeWidget {
|
||||
pub container: gtk::Box,
|
||||
play: gtk::Button,
|
||||
download: gtk::Button,
|
||||
cancel: gtk::Button,
|
||||
title: gtk::Label,
|
||||
date: gtk::Label,
|
||||
duration: gtk::Label,
|
||||
progress: gtk::ProgressBar,
|
||||
total_size: gtk::Label,
|
||||
local_size: gtk::Label,
|
||||
separator1: gtk::Label,
|
||||
separator2: gtk::Label,
|
||||
prog_separator: gtk::Label,
|
||||
date: DateMachine,
|
||||
duration: DurationMachine,
|
||||
title: Rc<RefCell<TitleMachine>>,
|
||||
media: Arc<Mutex<MediaMachine>>,
|
||||
}
|
||||
|
||||
impl Default for EpisodeWidget {
|
||||
@@ -78,75 +53,72 @@ impl Default for EpisodeWidget {
|
||||
let separator2: gtk::Label = builder.get_object("separator2").unwrap();
|
||||
let prog_separator: gtk::Label = builder.get_object("prog_separator").unwrap();
|
||||
|
||||
EpisodeWidget {
|
||||
container,
|
||||
progress,
|
||||
download,
|
||||
let date_machine = DateMachine::new(date, 0);
|
||||
let dur_machine = DurationMachine::new(duration, separator1, None);
|
||||
let title_machine = Rc::new(RefCell::new(TitleMachine::new(title, false)));
|
||||
let media = MediaMachine::new(
|
||||
play,
|
||||
download,
|
||||
progress,
|
||||
cancel,
|
||||
title,
|
||||
duration,
|
||||
date,
|
||||
total_size,
|
||||
local_size,
|
||||
separator1,
|
||||
separator2,
|
||||
prog_separator,
|
||||
);
|
||||
let media_machine = Arc::new(Mutex::new(media));
|
||||
|
||||
EpisodeWidget {
|
||||
container,
|
||||
title: title_machine,
|
||||
duration: dur_machine,
|
||||
date: date_machine,
|
||||
media: media_machine,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EpisodeWidget {
|
||||
pub fn new(episode: EpisodeWidgetQuery, sender: Sender<Action>) -> EpisodeWidget {
|
||||
let widget = EpisodeWidget::default();
|
||||
let mut widget = EpisodeWidget::default();
|
||||
widget.init(episode, sender);
|
||||
widget
|
||||
}
|
||||
|
||||
fn init(&self, episode: EpisodeWidgetQuery, sender: Sender<Action>) {
|
||||
fn init(&mut self, episode: EpisodeWidgetQuery, sender: Sender<Action>) {
|
||||
WidgetExt::set_name(&self.container, &episode.rowid().to_string());
|
||||
|
||||
// Set the date label.
|
||||
self.set_date(episode.epoch());
|
||||
|
||||
// Set the title label state.
|
||||
self.set_title(&episode);
|
||||
|
||||
// Set the duaration label.
|
||||
self.set_duration(episode.duration());
|
||||
|
||||
// Set the date label.
|
||||
self.set_date(episode.epoch());
|
||||
|
||||
// Show or hide the play/delete/download buttons upon widget initialization.
|
||||
if let Err(err) = self.show_buttons(episode.local_uri()) {
|
||||
debug!("Failed to determine play/download button state.");
|
||||
debug!("Error: {}", err);
|
||||
}
|
||||
|
||||
// Set the size label.
|
||||
if let Err(err) = self.set_total_size(episode.length()) {
|
||||
error!("Failed to set the Size label.");
|
||||
error!("Error: {}", err);
|
||||
}
|
||||
|
||||
// Determine what the state of the progress bar should be.
|
||||
if let Err(err) = self.determine_progess_bar() {
|
||||
error!("Something went wrong determining the ProgressBar State.");
|
||||
// Determine what the state of the media widgets should be.
|
||||
if let Err(err) = self.determine_media_state(&episode) {
|
||||
error!("Something went wrong determining the Media State.");
|
||||
error!("Error: {}", err);
|
||||
}
|
||||
|
||||
let episode = Arc::new(Mutex::new(episode));
|
||||
self.connect_buttons(episode, sender);
|
||||
}
|
||||
|
||||
fn connect_buttons(&self, episode: Arc<Mutex<EpisodeWidgetQuery>>, sender: Sender<Action>) {
|
||||
let title = self.title.clone();
|
||||
self.play
|
||||
.connect_clicked(clone!(episode, sender => move |_| {
|
||||
if let Ok(media) = self.media.lock() {
|
||||
media.play_connect_clicked(clone!(episode, sender => move |_| {
|
||||
if let Ok(mut ep) = episode.lock() {
|
||||
if let Err(err) = on_play_bttn_clicked(&mut ep, &title, sender.clone()){
|
||||
if let Err(err) = on_play_bttn_clicked(&mut ep, title.clone(), sender.clone()){
|
||||
error!("Error: {}", err);
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
self.download
|
||||
.connect_clicked(clone!(episode, sender => move |dl| {
|
||||
media.download_connect_clicked(clone!(episode, sender => move |dl| {
|
||||
dl.set_sensitive(false);
|
||||
if let Ok(ep) = episode.lock() {
|
||||
if let Err(err) = on_download_clicked(&ep, sender.clone()) {
|
||||
@@ -158,71 +130,30 @@ impl EpisodeWidget {
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/// Show or hide the play/delete/download buttons upon widget initialization.
|
||||
fn show_buttons(&self, local_uri: Option<&str>) -> Result<(), Error> {
|
||||
let path = local_uri.ok_or_else(|| format_err!("Path is None"))?;
|
||||
if Path::new(path).exists() {
|
||||
self.download.hide();
|
||||
self.play.show();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Determine the title state.
|
||||
fn set_title(&self, episode: &EpisodeWidgetQuery) {
|
||||
self.title.set_text(episode.title());
|
||||
|
||||
// Grey out the title if the episode is played.
|
||||
if episode.played().is_some() {
|
||||
self.title
|
||||
.get_style_context()
|
||||
.map(|c| c.add_class("dim-label"));
|
||||
}
|
||||
fn set_title(&mut self, episode: &EpisodeWidgetQuery) {
|
||||
let mut machine = self.title.borrow_mut();
|
||||
machine.set_title(episode.title());
|
||||
take_mut::take(machine.deref_mut(), |title| {
|
||||
title.determine_state(episode.played().is_some())
|
||||
});
|
||||
}
|
||||
|
||||
/// Set the date label depending on the current time.
|
||||
fn set_date(&self, epoch: i32) {
|
||||
let date = Utc.timestamp(i64::from(epoch), 0);
|
||||
if NOW.year() == date.year() {
|
||||
self.date.set_text(date.format("%e %b").to_string().trim());
|
||||
} else {
|
||||
self.date
|
||||
.set_text(date.format("%e %b %Y").to_string().trim());
|
||||
};
|
||||
fn set_date(&mut self, epoch: i32) {
|
||||
let machine = &mut self.date;
|
||||
take_mut::take(machine, |date| date.determine_state(i64::from(epoch)));
|
||||
}
|
||||
|
||||
/// Set the duration label.
|
||||
fn set_duration(&self, seconds: Option<i32>) -> Option<()> {
|
||||
let minutes = Duration::seconds(seconds?.into()).num_minutes();
|
||||
if minutes == 0 {
|
||||
return None;
|
||||
fn set_duration(&mut self, seconds: Option<i32>) {
|
||||
let machine = &mut self.duration;
|
||||
take_mut::take(machine, |duration| duration.determine_state(seconds));
|
||||
}
|
||||
|
||||
self.duration.set_text(&format!("{} min", minutes));
|
||||
self.duration.show();
|
||||
self.separator1.show();
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// Set the Episode label dependings on its size
|
||||
fn set_total_size(&self, bytes: Option<i32>) -> Result<(), Error> {
|
||||
let size = bytes.ok_or_else(|| format_err!("Size is None."))?;
|
||||
if size == 0 {
|
||||
bail!("Size is 0.");
|
||||
}
|
||||
|
||||
let s = size.file_size(SIZE_OPTS.clone())
|
||||
.map_err(|err| format_err!("{}", err))?;
|
||||
self.total_size.set_text(&s);
|
||||
self.total_size.show();
|
||||
self.separator2.show();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// FIXME: REFACTOR ME
|
||||
// Something Something State-Machine?
|
||||
fn determine_progess_bar(&self) -> Result<(), Error> {
|
||||
fn determine_media_state(&self, episode: &EpisodeWidgetQuery) -> Result<(), Error> {
|
||||
let id = WidgetExt::get_name(&self.container)
|
||||
.ok_or_else(|| format_err!("Failed to get widget Name"))?
|
||||
.parse::<i32>()?;
|
||||
@@ -235,46 +166,40 @@ impl EpisodeWidget {
|
||||
Ok(m.get(&id).cloned())
|
||||
}()?;
|
||||
|
||||
if let Some(prog) = active_dl {
|
||||
// FIXME: Document me?
|
||||
self.download.hide();
|
||||
self.progress.show();
|
||||
self.local_size.show();
|
||||
self.total_size.show();
|
||||
self.separator2.show();
|
||||
self.prog_separator.show();
|
||||
self.cancel.show();
|
||||
let mut lock = self.media.lock().map_err(|err| format_err!("{}", err))?;
|
||||
take_mut::take(lock.deref_mut(), |media| {
|
||||
media.determine_state(
|
||||
episode.length(),
|
||||
active_dl.is_some(),
|
||||
episode.local_uri().is_some(),
|
||||
)
|
||||
});
|
||||
|
||||
let progress_bar = self.progress.clone();
|
||||
let total_size = self.total_size.clone();
|
||||
let local_size = self.local_size.clone();
|
||||
// Show or hide the play/delete/download buttons upon widget initialization.
|
||||
if let Some(prog) = active_dl {
|
||||
lock.cancel_connect_clicked(prog.clone());
|
||||
drop(lock);
|
||||
|
||||
// Setup a callback that will update the progress bar.
|
||||
update_progressbar_callback(prog.clone(), id, &progress_bar, &local_size);
|
||||
update_progressbar_callback(prog.clone(), self.media.clone(), id);
|
||||
|
||||
// Setup a callback that will update the total_size label
|
||||
// with the http ContentLength header number rather than
|
||||
// relying to the RSS feed.
|
||||
update_total_size_callback(prog.clone(), &total_size);
|
||||
|
||||
self.cancel.connect_clicked(clone!(prog => move |cancel| {
|
||||
if let Ok(mut m) = prog.lock() {
|
||||
m.cancel();
|
||||
cancel.set_sensitive(false);
|
||||
}
|
||||
}));
|
||||
update_total_size_callback(prog.clone(), self.media.clone());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn on_download_clicked(ep: &EpisodeWidgetQuery, sender: Sender<Action>) -> Result<(), Error> {
|
||||
let pd = dbqueries::get_podcast_from_id(ep.podcast_id())?;
|
||||
let download_fold = get_download_folder(&pd.title().to_owned())?;
|
||||
let download_fold = get_download_folder(&pd.title())?;
|
||||
|
||||
// Start a new download.
|
||||
manager::add(ep.rowid(), &download_fold, sender.clone())?;
|
||||
manager::add(ep.rowid(), download_fold, sender.clone())?;
|
||||
|
||||
// Update Views
|
||||
sender.send(Action::RefreshEpisodesView)?;
|
||||
@@ -283,18 +208,21 @@ fn on_download_clicked(ep: &EpisodeWidgetQuery, sender: Sender<Action>) -> Resul
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn on_play_bttn_clicked(
|
||||
episode: &mut EpisodeWidgetQuery,
|
||||
title: >k::Label,
|
||||
title: Rc<RefCell<TitleMachine>>,
|
||||
sender: Sender<Action>,
|
||||
) -> Result<(), Error> {
|
||||
open_uri(episode.rowid())?;
|
||||
episode.set_played_now()?;
|
||||
|
||||
let mut machine = title.try_borrow_mut()?;
|
||||
take_mut::take(machine.deref_mut(), |title| {
|
||||
title.determine_state(episode.played().is_some())
|
||||
});
|
||||
|
||||
if episode.set_played_now().is_ok() {
|
||||
title.get_style_context().map(|c| c.add_class("dim-label"));
|
||||
sender.send(Action::RefreshEpisodesViewBGR)?;
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -313,27 +241,28 @@ fn open_uri(rowid: i32) -> Result<(), Error> {
|
||||
}
|
||||
|
||||
// Setup a callback that will update the progress bar.
|
||||
#[inline]
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(if_same_then_else))]
|
||||
fn update_progressbar_callback(
|
||||
prog: Arc<Mutex<manager::Progress>>,
|
||||
media: Arc<Mutex<MediaMachine>>,
|
||||
episode_rowid: i32,
|
||||
progress_bar: >k::ProgressBar,
|
||||
local_size: >k::Label,
|
||||
) {
|
||||
timeout_add(
|
||||
400,
|
||||
clone!(prog, progress_bar, progress_bar, local_size=> move || {
|
||||
progress_bar_helper(prog.clone(), episode_rowid, &progress_bar, &local_size)
|
||||
clone!(prog, media => move || {
|
||||
progress_bar_helper(prog.clone(), media.clone(), episode_rowid)
|
||||
.unwrap_or(glib::Continue(false))
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[allow(if_same_then_else)]
|
||||
fn progress_bar_helper(
|
||||
prog: Arc<Mutex<manager::Progress>>,
|
||||
media: Arc<Mutex<MediaMachine>>,
|
||||
episode_rowid: i32,
|
||||
progress_bar: >k::ProgressBar,
|
||||
local_size: >k::Label,
|
||||
) -> Result<glib::Continue, Error> {
|
||||
let (fraction, downloaded) = {
|
||||
let m = prog.lock()
|
||||
@@ -341,16 +270,16 @@ fn progress_bar_helper(
|
||||
(m.get_fraction(), m.get_downloaded())
|
||||
};
|
||||
|
||||
// Update local_size label
|
||||
downloaded
|
||||
.file_size(SIZE_OPTS.clone())
|
||||
.map_err(|err| format_err!("{}", err))
|
||||
.map(|x| local_size.set_text(&x))?;
|
||||
|
||||
// I hate floating points.
|
||||
// Update the progress_bar.
|
||||
if (fraction >= 0.0) && (fraction <= 1.0) && (!fraction.is_nan()) {
|
||||
progress_bar.set_fraction(fraction);
|
||||
// Update local_size label
|
||||
let size = downloaded
|
||||
.file_size(SIZE_OPTS.clone())
|
||||
.map_err(|err| format_err!("{}", err))?;
|
||||
|
||||
let mut m = media.lock().unwrap();
|
||||
m.update_progress(&size, fraction);
|
||||
}
|
||||
|
||||
// info!("Fraction: {}", progress_bar.get_fraction());
|
||||
@@ -376,18 +305,23 @@ fn progress_bar_helper(
|
||||
// Setup a callback that will update the total_size label
|
||||
// with the http ContentLength header number rather than
|
||||
// relying to the RSS feed.
|
||||
fn update_total_size_callback(prog: Arc<Mutex<manager::Progress>>, total_size: >k::Label) {
|
||||
#[inline]
|
||||
fn update_total_size_callback(
|
||||
prog: Arc<Mutex<manager::Progress>>,
|
||||
media: Arc<Mutex<MediaMachine>>,
|
||||
) {
|
||||
timeout_add(
|
||||
500,
|
||||
clone!(prog, total_size => move || {
|
||||
total_size_helper(prog.clone(), &total_size).unwrap_or(glib::Continue(true))
|
||||
clone!(prog, media => move || {
|
||||
total_size_helper(prog.clone(), media.clone()).unwrap_or(glib::Continue(true))
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_size_helper(
|
||||
prog: Arc<Mutex<manager::Progress>>,
|
||||
total_size: >k::Label,
|
||||
media: Arc<Mutex<MediaMachine>>,
|
||||
) -> Result<glib::Continue, Error> {
|
||||
// Get the total_bytes.
|
||||
let total_bytes = {
|
||||
@@ -399,10 +333,12 @@ fn total_size_helper(
|
||||
debug!("Total Size: {}", total_bytes);
|
||||
if total_bytes != 0 {
|
||||
// Update the total_size label
|
||||
total_bytes
|
||||
.file_size(SIZE_OPTS.clone())
|
||||
.map_err(|err| format_err!("{}", err))
|
||||
.map(|x| total_size.set_text(&x))?;
|
||||
if let Ok(mut m) = media.lock() {
|
||||
take_mut::take(m.deref_mut(), |machine| {
|
||||
machine.set_size(Some(total_bytes as i32))
|
||||
});
|
||||
}
|
||||
|
||||
// Do not call again the callback
|
||||
Ok(glib::Continue(false))
|
||||
} else {
|
||||
@@ -415,19 +351,63 @@ fn total_size_helper(
|
||||
// delete_local_content(&mut ep).map_err(From::from).map(|_| ())
|
||||
// }
|
||||
|
||||
pub fn episodes_listbox(pd: &Podcast, sender: Sender<Action>) -> Result<gtk::ListBox, Error> {
|
||||
let episodes = dbqueries::get_pd_episodeswidgets(pd)?;
|
||||
pub fn episodes_listbox(pd: Arc<Podcast>, sender: Sender<Action>) -> Result<gtk::ListBox, Error> {
|
||||
use crossbeam_channel::TryRecvError::*;
|
||||
use crossbeam_channel::bounded;
|
||||
|
||||
let list = gtk::ListBox::new();
|
||||
let count = dbqueries::get_pd_episodes_count(&pd)?;
|
||||
|
||||
episodes.into_iter().for_each(|ep| {
|
||||
let widget = EpisodeWidget::new(ep, sender.clone());
|
||||
list.add(&widget.container);
|
||||
let (sender_, receiver) = bounded(1);
|
||||
rayon::spawn(move || {
|
||||
let episodes = dbqueries::get_pd_episodeswidgets(&pd).unwrap();
|
||||
sender_
|
||||
.send(episodes)
|
||||
.expect("Something terrible happened to the channnel");
|
||||
});
|
||||
|
||||
list.set_vexpand(false);
|
||||
list.set_hexpand(false);
|
||||
let list = gtk::ListBox::new();
|
||||
list.set_visible(true);
|
||||
list.set_selection_mode(gtk::SelectionMode::None);
|
||||
|
||||
if count == 0 {
|
||||
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/empty_show.ui");
|
||||
let container: gtk::Box = builder.get_object("empty_show").unwrap();
|
||||
list.add(&container);
|
||||
return Ok(list);
|
||||
}
|
||||
|
||||
gtk::idle_add(clone!(list => move || {
|
||||
let episodes = match receiver.try_recv() {
|
||||
Ok(e) => e,
|
||||
Err(Empty) => return glib::Continue(true),
|
||||
Err(Disconnected) => return glib::Continue(false),
|
||||
};
|
||||
|
||||
lazy_load(episodes, list.clone(), clone!(sender => move |ep| {
|
||||
EpisodeWidget::new(ep, sender.clone()).container
|
||||
}));
|
||||
|
||||
glib::Continue(false)
|
||||
}));
|
||||
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
use gtk::{IsA, Widget};
|
||||
|
||||
fn lazy_load<T, U, P, Z>(data: T, container: Z, mut predicate: P)
|
||||
where
|
||||
T: IntoIterator + 'static,
|
||||
T::Item: 'static,
|
||||
Z: ContainerExt + 'static,
|
||||
P: FnMut(T::Item) -> U + 'static,
|
||||
U: IsA<Widget>,
|
||||
{
|
||||
let mut data = data.into_iter();
|
||||
gtk::idle_add(move || {
|
||||
data.next()
|
||||
.map(|x| container.add(&predicate(x)))
|
||||
.map(|_| glib::Continue(true))
|
||||
.unwrap_or(glib::Continue(false))
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,917 @@
|
||||
// TODO: Things that should be done.
|
||||
//
|
||||
// * Wherever there's a function that take 2 or more arguments of the same type,
|
||||
// eg: fn new(total_size: gtk::Label, local_size: gtk::Label ..)
|
||||
// Wrap the types into Struct-tuples and imple deref so it won't be possible to pass
|
||||
// the wrong argument to the wrong position.
|
||||
|
||||
use chrono;
|
||||
use glib;
|
||||
use gtk;
|
||||
|
||||
use chrono::prelude::*;
|
||||
use gtk::prelude::*;
|
||||
use humansize::{file_size_opts as size_opts, FileSize};
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use manager::Progress as OtherProgress;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref SIZE_OPTS: Arc<size_opts::FileSizeOpts> = {
|
||||
// Declare a custom humansize option struct
|
||||
// See: https://docs.rs/humansize/1.0.2/humansize/file_size_opts/struct.FileSizeOpts.html
|
||||
Arc::new(size_opts::FileSizeOpts {
|
||||
divider: size_opts::Kilo::Binary,
|
||||
units: size_opts::Kilo::Decimal,
|
||||
decimal_places: 0,
|
||||
decimal_zeroes: 0,
|
||||
fixed_at: size_opts::FixedAt::No,
|
||||
long_units: false,
|
||||
space: true,
|
||||
suffix: "",
|
||||
allow_negative: false,
|
||||
})
|
||||
};
|
||||
|
||||
static ref NOW: DateTime<Utc> = Utc::now();
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UnInitialized;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Shown;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Hidden;
|
||||
|
||||
pub trait Visibility {}
|
||||
|
||||
impl Visibility for Shown {}
|
||||
impl Visibility for Hidden {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Normal;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GreyedOut;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Title<S> {
|
||||
title: gtk::Label,
|
||||
state: S,
|
||||
}
|
||||
|
||||
impl<S> Title<S> {
|
||||
#[allow(unused_must_use)]
|
||||
// This does not need to be &mut since gtk-rs does not model ownership
|
||||
// But I think it wouldn't hurt if we treat it as a Rust api.
|
||||
fn set_title(&mut self, s: &str) {
|
||||
self.title.set_text(s);
|
||||
}
|
||||
}
|
||||
|
||||
impl Title<Normal> {
|
||||
fn new(title: gtk::Label) -> Self {
|
||||
Title {
|
||||
title,
|
||||
state: Normal {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Title<Normal>> for Title<GreyedOut> {
|
||||
fn from(f: Title<Normal>) -> Self {
|
||||
f.title
|
||||
.get_style_context()
|
||||
.map(|c| c.add_class("dim-label"));
|
||||
|
||||
Title {
|
||||
title: f.title,
|
||||
state: GreyedOut {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Title<GreyedOut>> for Title<Normal> {
|
||||
fn from(f: Title<GreyedOut>) -> Self {
|
||||
f.title
|
||||
.get_style_context()
|
||||
.map(|c| c.remove_class("dim-label"));
|
||||
|
||||
Title {
|
||||
title: f.title,
|
||||
state: Normal {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TitleMachine {
|
||||
Normal(Title<Normal>),
|
||||
GreyedOut(Title<GreyedOut>),
|
||||
}
|
||||
|
||||
impl TitleMachine {
|
||||
pub fn new(label: gtk::Label, is_played: bool) -> Self {
|
||||
let m = TitleMachine::Normal(Title::<Normal>::new(label));
|
||||
m.determine_state(is_played)
|
||||
}
|
||||
|
||||
pub fn determine_state(self, is_played: bool) -> Self {
|
||||
use self::TitleMachine::*;
|
||||
|
||||
match (self, is_played) {
|
||||
(title @ Normal(_), false) => title,
|
||||
(title @ GreyedOut(_), true) => title,
|
||||
(Normal(val), true) => GreyedOut(val.into()),
|
||||
(GreyedOut(val), false) => Normal(val.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_title(&mut self, s: &str) {
|
||||
use self::TitleMachine::*;
|
||||
|
||||
match *self {
|
||||
Normal(ref mut val) => val.set_title(s),
|
||||
GreyedOut(ref mut val) => val.set_title(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Usual;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct YearShown;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Date<S> {
|
||||
date: gtk::Label,
|
||||
epoch: i64,
|
||||
state: S,
|
||||
}
|
||||
|
||||
impl<S> Date<S> {
|
||||
fn into_usual(self, epoch: i64) -> Date<Usual> {
|
||||
let ts = Utc.timestamp(epoch, 0);
|
||||
self.date.set_text(ts.format("%e %b").to_string().trim());
|
||||
|
||||
Date {
|
||||
date: self.date,
|
||||
epoch: self.epoch,
|
||||
state: Usual {},
|
||||
}
|
||||
}
|
||||
|
||||
fn into_year_shown(self, epoch: i64) -> Date<YearShown> {
|
||||
let ts = Utc.timestamp(epoch, 0);
|
||||
self.date.set_text(ts.format("%e %b %Y").to_string().trim());
|
||||
|
||||
Date {
|
||||
date: self.date,
|
||||
epoch: self.epoch,
|
||||
state: YearShown {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Date<UnInitialized> {
|
||||
fn new(date: gtk::Label, epoch: i64) -> Self {
|
||||
let ts = Utc.timestamp(epoch, 0);
|
||||
date.set_text(ts.format("%e %b %Y").to_string().trim());
|
||||
|
||||
Date {
|
||||
date,
|
||||
epoch,
|
||||
state: UnInitialized {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DateMachine {
|
||||
UnInitialized(Date<UnInitialized>),
|
||||
Usual(Date<Usual>),
|
||||
WithYear(Date<YearShown>),
|
||||
}
|
||||
|
||||
impl DateMachine {
|
||||
pub fn new(label: gtk::Label, epoch: i64) -> Self {
|
||||
let m = DateMachine::UnInitialized(Date::<UnInitialized>::new(label, epoch));
|
||||
m.determine_state(epoch)
|
||||
}
|
||||
|
||||
pub fn determine_state(self, epoch: i64) -> Self {
|
||||
use self::DateMachine::*;
|
||||
|
||||
let ts = Utc.timestamp(epoch, 0);
|
||||
let is_old = !(NOW.year() == ts.year());
|
||||
|
||||
match (self, is_old) {
|
||||
// Into Usual
|
||||
(Usual(val), false) => Usual(val.into_usual(epoch)),
|
||||
(WithYear(val), false) => Usual(val.into_usual(epoch)),
|
||||
(UnInitialized(val), false) => Usual(val.into_usual(epoch)),
|
||||
|
||||
// Into Year Shown
|
||||
(Usual(val), true) => WithYear(val.into_year_shown(epoch)),
|
||||
(WithYear(val), true) => WithYear(val.into_year_shown(epoch)),
|
||||
(UnInitialized(val), true) => WithYear(val.into_year_shown(epoch)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Duration<S: Visibility> {
|
||||
// TODO: make duration and separator diff types
|
||||
duration: gtk::Label,
|
||||
separator: gtk::Label,
|
||||
state: S,
|
||||
}
|
||||
|
||||
impl<S: Visibility> Duration<S> {
|
||||
// This needs a better name.
|
||||
// TODO: make me mut
|
||||
fn set_duration(&self, minutes: i64) {
|
||||
self.duration.set_text(&format!("{} min", minutes));
|
||||
}
|
||||
}
|
||||
|
||||
impl Duration<Hidden> {
|
||||
fn new(duration: gtk::Label, separator: gtk::Label) -> Self {
|
||||
duration.hide();
|
||||
separator.hide();
|
||||
|
||||
Duration {
|
||||
duration,
|
||||
separator,
|
||||
state: Hidden {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Duration<Hidden>> for Duration<Shown> {
|
||||
fn from(f: Duration<Hidden>) -> Self {
|
||||
f.duration.show();
|
||||
f.separator.show();
|
||||
|
||||
Duration {
|
||||
duration: f.duration,
|
||||
separator: f.separator,
|
||||
state: Shown {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Duration<Shown>> for Duration<Hidden> {
|
||||
fn from(f: Duration<Shown>) -> Self {
|
||||
f.duration.hide();
|
||||
f.separator.hide();
|
||||
|
||||
Duration {
|
||||
duration: f.duration,
|
||||
separator: f.separator,
|
||||
state: Hidden {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DurationMachine {
|
||||
Hidden(Duration<Hidden>),
|
||||
Shown(Duration<Shown>),
|
||||
}
|
||||
|
||||
impl DurationMachine {
|
||||
pub fn new(duration: gtk::Label, separator: gtk::Label, seconds: Option<i32>) -> Self {
|
||||
let m = DurationMachine::Hidden(Duration::<Hidden>::new(duration, separator));
|
||||
m.determine_state(seconds)
|
||||
}
|
||||
|
||||
pub fn determine_state(self, seconds: Option<i32>) -> Self {
|
||||
match (self, seconds) {
|
||||
(d @ DurationMachine::Hidden(_), None) => d,
|
||||
(DurationMachine::Shown(val), None) => DurationMachine::Hidden(val.into()),
|
||||
(DurationMachine::Hidden(val), Some(s)) => {
|
||||
let minutes = chrono::Duration::seconds(s.into()).num_minutes();
|
||||
if minutes == 0 {
|
||||
DurationMachine::Hidden(val)
|
||||
} else {
|
||||
val.set_duration(minutes);
|
||||
DurationMachine::Shown(val.into())
|
||||
}
|
||||
}
|
||||
(DurationMachine::Shown(val), Some(s)) => {
|
||||
let minutes = chrono::Duration::seconds(s.into()).num_minutes();
|
||||
if minutes == 0 {
|
||||
DurationMachine::Hidden(val.into())
|
||||
} else {
|
||||
val.set_duration(minutes);
|
||||
DurationMachine::Shown(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Size<S> {
|
||||
size: gtk::Label,
|
||||
separator: gtk::Label,
|
||||
state: S,
|
||||
}
|
||||
|
||||
impl<S> Size<S> {
|
||||
fn set_size(self, s: &str) -> Size<Shown> {
|
||||
self.size.set_text(s);
|
||||
self.size.show();
|
||||
self.separator.show();
|
||||
Size {
|
||||
size: self.size,
|
||||
separator: self.separator,
|
||||
state: Shown {},
|
||||
}
|
||||
}
|
||||
|
||||
// https://play.rust-lang.org/?gist=1acffaf62743eeb85be1ae6ecf474784&version=stable
|
||||
// It might be possible to make a generic definition with Specialization.
|
||||
// https://github.com/rust-lang/rust/issues/31844
|
||||
fn into_shown(self) -> Size<Shown> {
|
||||
self.size.show();
|
||||
self.separator.show();
|
||||
|
||||
Size {
|
||||
size: self.size,
|
||||
separator: self.separator,
|
||||
state: Shown {},
|
||||
}
|
||||
}
|
||||
|
||||
fn into_hidden(self) -> Size<Hidden> {
|
||||
self.size.hide();
|
||||
self.separator.hide();
|
||||
|
||||
Size {
|
||||
size: self.size,
|
||||
separator: self.separator,
|
||||
state: Hidden {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Size<UnInitialized> {
|
||||
fn new(size: gtk::Label, separator: gtk::Label) -> Self {
|
||||
size.hide();
|
||||
separator.hide();
|
||||
|
||||
Size {
|
||||
size,
|
||||
separator,
|
||||
state: UnInitialized {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pub trait Playable {}
|
||||
|
||||
// impl Playable for Download {}
|
||||
// impl Playable for Play {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Download;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Play;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
// FIXME: Needs better name.
|
||||
// Should each button also has it's own type and machine?
|
||||
pub struct DownloadPlay<S> {
|
||||
play: gtk::Button,
|
||||
download: gtk::Button,
|
||||
state: S,
|
||||
}
|
||||
|
||||
impl<S> DownloadPlay<S> {
|
||||
// https://play.rust-lang.org/?gist=1acffaf62743eeb85be1ae6ecf474784&version=stable
|
||||
// It might be possible to make a generic definition with Specialization.
|
||||
// https://github.com/rust-lang/rust/issues/31844
|
||||
fn into_playable(self) -> DownloadPlay<Play> {
|
||||
self.play.show();
|
||||
self.download.hide();
|
||||
|
||||
DownloadPlay {
|
||||
play: self.play,
|
||||
download: self.download,
|
||||
state: Play {},
|
||||
}
|
||||
}
|
||||
|
||||
fn into_fetchable(self) -> DownloadPlay<Download> {
|
||||
self.play.hide();
|
||||
self.download.show();
|
||||
|
||||
DownloadPlay {
|
||||
play: self.play,
|
||||
download: self.download,
|
||||
state: Download {},
|
||||
}
|
||||
}
|
||||
|
||||
fn into_hidden(self) -> DownloadPlay<Hidden> {
|
||||
self.play.hide();
|
||||
self.download.hide();
|
||||
|
||||
DownloadPlay {
|
||||
play: self.play,
|
||||
download: self.download,
|
||||
state: Hidden {},
|
||||
}
|
||||
}
|
||||
|
||||
fn download_connect_clicked<F: Fn(>k::Button) + 'static>(
|
||||
&self,
|
||||
f: F,
|
||||
) -> glib::SignalHandlerId {
|
||||
self.download.connect_clicked(f)
|
||||
}
|
||||
|
||||
fn play_connect_clicked<F: Fn(>k::Button) + 'static>(&self, f: F) -> glib::SignalHandlerId {
|
||||
self.play.connect_clicked(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl DownloadPlay<UnInitialized> {
|
||||
fn new(play: gtk::Button, download: gtk::Button) -> Self {
|
||||
play.hide();
|
||||
download.hide();
|
||||
|
||||
DownloadPlay {
|
||||
play,
|
||||
download,
|
||||
state: UnInitialized {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Progress<S> {
|
||||
bar: gtk::ProgressBar,
|
||||
cancel: gtk::Button,
|
||||
local_size: gtk::Label,
|
||||
prog_separator: gtk::Label,
|
||||
state: S,
|
||||
}
|
||||
|
||||
impl<S> Progress<S> {
|
||||
fn into_shown(self) -> Progress<Shown> {
|
||||
self.bar.show();
|
||||
self.cancel.show();
|
||||
self.local_size.show();
|
||||
self.prog_separator.show();
|
||||
|
||||
Progress {
|
||||
bar: self.bar,
|
||||
cancel: self.cancel,
|
||||
local_size: self.local_size,
|
||||
prog_separator: self.prog_separator,
|
||||
state: Shown {},
|
||||
}
|
||||
}
|
||||
|
||||
fn into_hidden(self) -> Progress<Hidden> {
|
||||
self.bar.hide();
|
||||
self.cancel.hide();
|
||||
self.local_size.hide();
|
||||
self.prog_separator.hide();
|
||||
|
||||
Progress {
|
||||
bar: self.bar,
|
||||
cancel: self.cancel,
|
||||
local_size: self.local_size,
|
||||
prog_separator: self.prog_separator,
|
||||
state: Hidden {},
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_must_use)]
|
||||
// This does not need to be &mut since gtk-rs does not model ownership
|
||||
// But I think it wouldn't hurt if we treat it as a Rust api.
|
||||
fn update_progress(&mut self, local_size: &str, fraction: f64) {
|
||||
self.local_size.set_text(local_size);
|
||||
self.bar.set_fraction(fraction);
|
||||
}
|
||||
|
||||
fn cancel_connect_clicked(&self, prog: Arc<Mutex<OtherProgress>>) -> glib::SignalHandlerId {
|
||||
self.cancel.connect_clicked(move |cancel| {
|
||||
if let Ok(mut m) = prog.lock() {
|
||||
m.cancel();
|
||||
cancel.set_sensitive(false);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Progress<UnInitialized> {
|
||||
fn new(
|
||||
bar: gtk::ProgressBar,
|
||||
cancel: gtk::Button,
|
||||
local_size: gtk::Label,
|
||||
prog_separator: gtk::Label,
|
||||
) -> Self {
|
||||
bar.hide();
|
||||
cancel.hide();
|
||||
local_size.hide();
|
||||
prog_separator.hide();
|
||||
|
||||
Progress {
|
||||
bar,
|
||||
cancel,
|
||||
local_size,
|
||||
prog_separator,
|
||||
state: UnInitialized {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Media<X, Y, Z> {
|
||||
dl: DownloadPlay<X>,
|
||||
size: Size<Y>,
|
||||
progress: Progress<Z>,
|
||||
}
|
||||
|
||||
type New<Y> = Media<Download, Y, Hidden>;
|
||||
type Playable<Y> = Media<Play, Y, Hidden>;
|
||||
type InProgress = Media<Hidden, Shown, Shown>;
|
||||
type MediaUnInitialized = Media<UnInitialized, UnInitialized, UnInitialized>;
|
||||
|
||||
impl From<New<Shown>> for InProgress {
|
||||
fn from(f: New<Shown>) -> Self {
|
||||
f.into_progress()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<New<Hidden>> for InProgress {
|
||||
fn from(f: New<Hidden>) -> Self {
|
||||
f.into_progress()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Playable<Shown>> for InProgress {
|
||||
fn from(f: Playable<Shown>) -> Self {
|
||||
f.into_progress()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Playable<Hidden>> for InProgress {
|
||||
fn from(f: Playable<Hidden>) -> Self {
|
||||
f.into_progress()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Y: Visibility> From<Playable<Y>> for New<Y> {
|
||||
fn from(f: Playable<Y>) -> Self {
|
||||
Media {
|
||||
dl: f.dl.into_fetchable(),
|
||||
size: f.size,
|
||||
progress: f.progress,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Y: Visibility> From<New<Y>> for Playable<Y> {
|
||||
fn from(f: New<Y>) -> Self {
|
||||
Media {
|
||||
dl: f.dl.into_playable(),
|
||||
size: f.size,
|
||||
progress: f.progress,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MediaUnInitialized> for New<Hidden> {
|
||||
fn from(f: MediaUnInitialized) -> Self {
|
||||
Media {
|
||||
dl: f.dl.into_fetchable(),
|
||||
size: f.size.into_hidden(),
|
||||
progress: f.progress.into_hidden(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MediaUnInitialized> for Playable<Hidden> {
|
||||
fn from(f: MediaUnInitialized) -> Self {
|
||||
Media {
|
||||
dl: f.dl.into_playable(),
|
||||
size: f.size.into_hidden(),
|
||||
progress: f.progress.into_hidden(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<X, Y, Z> Media<X, Y, Z> {
|
||||
fn set_size(self, s: &str) -> Media<X, Shown, Z> {
|
||||
Media {
|
||||
dl: self.dl,
|
||||
size: self.size.set_size(s),
|
||||
progress: self.progress,
|
||||
}
|
||||
}
|
||||
|
||||
fn hide_size(self) -> Media<X, Hidden, Z> {
|
||||
Media {
|
||||
dl: self.dl,
|
||||
size: self.size.into_hidden(),
|
||||
progress: self.progress,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new(self, size: &str) -> New<Shown> {
|
||||
Media {
|
||||
dl: self.dl.into_fetchable(),
|
||||
size: self.size.set_size(size),
|
||||
progress: self.progress.into_hidden(),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_playable(self, size: &str) -> Playable<Shown> {
|
||||
Media {
|
||||
dl: self.dl.into_playable(),
|
||||
size: self.size.set_size(size),
|
||||
progress: self.progress.into_hidden(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<X, Z> Media<X, Shown, Z> {
|
||||
fn into_progress(self) -> InProgress {
|
||||
Media {
|
||||
dl: self.dl.into_hidden(),
|
||||
size: self.size.into_shown(),
|
||||
progress: self.progress.into_shown(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<X, Z> Media<X, Hidden, Z> {
|
||||
fn into_progress(self) -> InProgress {
|
||||
Media {
|
||||
dl: self.dl.into_hidden(),
|
||||
size: self.size.set_size("Unkown"),
|
||||
progress: self.progress.into_shown(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<X, Z> Media<X, UnInitialized, Z> {
|
||||
fn into_progress(self, size: Option<String>) -> InProgress {
|
||||
if let Some(s) = size {
|
||||
Media {
|
||||
dl: self.dl.into_hidden(),
|
||||
size: self.size.set_size(&s),
|
||||
progress: self.progress.into_shown(),
|
||||
}
|
||||
} else {
|
||||
Media {
|
||||
dl: self.dl.into_hidden(),
|
||||
size: self.size.set_size("Unkown"),
|
||||
progress: self.progress.into_shown(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InProgress {
|
||||
#[allow(unused_must_use)]
|
||||
// This does not need to be &mut since gtk-rs does not model ownership
|
||||
// But I think it wouldn't hurt if we treat it as a Rust api.
|
||||
fn update_progress(&mut self, local_size: &str, fraction: f64) {
|
||||
self.progress.update_progress(local_size, fraction)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ButtonsState {
|
||||
New(Media<Download, Shown, Hidden>),
|
||||
NewWithoutSize(Media<Download, Hidden, Hidden>),
|
||||
Playable(Media<Play, Shown, Hidden>),
|
||||
PlayableWithoutSize(Media<Play, Hidden, Hidden>),
|
||||
}
|
||||
|
||||
impl ButtonsState {
|
||||
pub fn determine_state(self, size: Option<String>, is_downloaded: bool) -> Self {
|
||||
use self::ButtonsState::*;
|
||||
|
||||
match (self, size, is_downloaded) {
|
||||
// From whatever to New
|
||||
(New(m), Some(s), false) => New(m.into_new(&s)),
|
||||
(Playable(m), Some(s), false) => New(m.into_new(&s)),
|
||||
|
||||
(NewWithoutSize(m), Some(s), false) => New(m.into_new(&s)),
|
||||
(PlayableWithoutSize(m), Some(s), false) => New(m.into_new(&s)),
|
||||
|
||||
// From whatever to Playable
|
||||
(New(m), Some(s), true) => Playable(m.into_playable(&s)),
|
||||
(Playable(m), Some(s), true) => Playable(m.into_playable(&s)),
|
||||
|
||||
(NewWithoutSize(m), Some(s), true) => Playable(m.into_playable(&s)),
|
||||
(PlayableWithoutSize(m), Some(s), true) => Playable(m.into_playable(&s)),
|
||||
|
||||
// From whatever to NewWithoutSize
|
||||
(New(m), None, false) => NewWithoutSize(m.hide_size()),
|
||||
(Playable(m), None, false) => NewWithoutSize(Media::from(m).hide_size()),
|
||||
|
||||
(b @ NewWithoutSize(_), None, false) => b,
|
||||
(PlayableWithoutSize(m), None, false) => NewWithoutSize(m.into()),
|
||||
|
||||
// From whatever to PlayableWithoutSize
|
||||
(New(m), None, true) => PlayableWithoutSize(Media::from(m).hide_size()),
|
||||
(Playable(m), None, true) => PlayableWithoutSize(m.hide_size()),
|
||||
|
||||
(NewWithoutSize(val), None, true) => PlayableWithoutSize(val.into()),
|
||||
(b @ PlayableWithoutSize(_), None, true) => b,
|
||||
// _ => unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
fn into_progress(self) -> InProgress {
|
||||
use self::ButtonsState::*;
|
||||
|
||||
match self {
|
||||
New(m) => m.into(),
|
||||
Playable(m) => m.into(),
|
||||
NewWithoutSize(m) => m.into(),
|
||||
PlayableWithoutSize(m) => m.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_size(self, size: Option<String>) -> Self {
|
||||
use self::ButtonsState::*;
|
||||
|
||||
match (self, size) {
|
||||
(New(m), Some(s)) => New(m.set_size(&s)),
|
||||
(New(m), None) => NewWithoutSize(m.hide_size()),
|
||||
(Playable(m), Some(s)) => Playable(m.set_size(&s)),
|
||||
(Playable(m), None) => PlayableWithoutSize(m.hide_size()),
|
||||
(bttn @ NewWithoutSize(_), None) => bttn,
|
||||
(bttn @ PlayableWithoutSize(_), None) => bttn,
|
||||
(NewWithoutSize(m), Some(s)) => New(m.into_new(&s)),
|
||||
(PlayableWithoutSize(m), Some(s)) => Playable(m.into_playable(&s)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn download_connect_clicked<F: Fn(>k::Button) + 'static>(
|
||||
&self,
|
||||
f: F,
|
||||
) -> glib::SignalHandlerId {
|
||||
use self::ButtonsState::*;
|
||||
|
||||
match *self {
|
||||
New(ref val) => val.dl.download_connect_clicked(f),
|
||||
NewWithoutSize(ref val) => val.dl.download_connect_clicked(f),
|
||||
Playable(ref val) => val.dl.download_connect_clicked(f),
|
||||
PlayableWithoutSize(ref val) => val.dl.download_connect_clicked(f),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play_connect_clicked<F: Fn(>k::Button) + 'static>(
|
||||
&self,
|
||||
f: F,
|
||||
) -> glib::SignalHandlerId {
|
||||
use self::ButtonsState::*;
|
||||
|
||||
match *self {
|
||||
New(ref val) => val.dl.play_connect_clicked(f),
|
||||
NewWithoutSize(ref val) => val.dl.play_connect_clicked(f),
|
||||
Playable(ref val) => val.dl.play_connect_clicked(f),
|
||||
PlayableWithoutSize(ref val) => val.dl.play_connect_clicked(f),
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel_connect_clicked(&self, prog: Arc<Mutex<OtherProgress>>) -> glib::SignalHandlerId {
|
||||
use self::ButtonsState::*;
|
||||
|
||||
match *self {
|
||||
New(ref val) => val.progress.cancel_connect_clicked(prog),
|
||||
NewWithoutSize(ref val) => val.progress.cancel_connect_clicked(prog),
|
||||
Playable(ref val) => val.progress.cancel_connect_clicked(prog),
|
||||
PlayableWithoutSize(ref val) => val.progress.cancel_connect_clicked(prog),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MediaMachine {
|
||||
UnInitialized(Media<UnInitialized, UnInitialized, UnInitialized>),
|
||||
Initialized(ButtonsState),
|
||||
InProgress(Media<Hidden, Shown, Shown>),
|
||||
}
|
||||
|
||||
impl MediaMachine {
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(too_many_arguments))]
|
||||
pub fn new(
|
||||
play: gtk::Button,
|
||||
download: gtk::Button,
|
||||
bar: gtk::ProgressBar,
|
||||
cancel: gtk::Button,
|
||||
total_size: gtk::Label,
|
||||
local_size: gtk::Label,
|
||||
separator: gtk::Label,
|
||||
prog_separator: gtk::Label,
|
||||
) -> Self {
|
||||
let dl = DownloadPlay::<UnInitialized>::new(play, download);
|
||||
let progress = Progress::<UnInitialized>::new(bar, cancel, local_size, prog_separator);
|
||||
let size = Size::<UnInitialized>::new(total_size, separator);
|
||||
|
||||
MediaMachine::UnInitialized(Media { dl, progress, size })
|
||||
}
|
||||
|
||||
pub fn download_connect_clicked<F: Fn(>k::Button) + 'static>(
|
||||
&self,
|
||||
f: F,
|
||||
) -> glib::SignalHandlerId {
|
||||
use self::MediaMachine::*;
|
||||
|
||||
match *self {
|
||||
UnInitialized(ref val) => val.dl.download_connect_clicked(f),
|
||||
Initialized(ref val) => val.download_connect_clicked(f),
|
||||
InProgress(ref val) => val.dl.download_connect_clicked(f),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play_connect_clicked<F: Fn(>k::Button) + 'static>(
|
||||
&self,
|
||||
f: F,
|
||||
) -> glib::SignalHandlerId {
|
||||
use self::MediaMachine::*;
|
||||
|
||||
match *self {
|
||||
UnInitialized(ref val) => val.dl.play_connect_clicked(f),
|
||||
Initialized(ref val) => val.play_connect_clicked(f),
|
||||
InProgress(ref val) => val.dl.play_connect_clicked(f),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cancel_connect_clicked(&self, prog: Arc<Mutex<OtherProgress>>) -> glib::SignalHandlerId {
|
||||
use self::MediaMachine::*;
|
||||
|
||||
match *self {
|
||||
UnInitialized(ref val) => val.progress.cancel_connect_clicked(prog),
|
||||
Initialized(ref val) => val.cancel_connect_clicked(prog),
|
||||
InProgress(ref val) => val.progress.cancel_connect_clicked(prog),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn determine_state(self, bytes: Option<i32>, is_active: bool, is_downloaded: bool) -> Self {
|
||||
use self::ButtonsState::*;
|
||||
use self::MediaMachine::*;
|
||||
|
||||
match (self, size_helper(bytes), is_downloaded, is_active) {
|
||||
(UnInitialized(m), s, _, true) => InProgress(m.into_progress(s)),
|
||||
|
||||
// Into New
|
||||
(UnInitialized(m), Some(s), false, false) => Initialized(New(m.into_new(&s))),
|
||||
(UnInitialized(m), None, false, false) => Initialized(NewWithoutSize(m.into())),
|
||||
|
||||
// Into Playable
|
||||
(UnInitialized(m), Some(s), true, false) => Initialized(Playable(m.into_playable(&s))),
|
||||
(UnInitialized(m), None, true, false) => Initialized(PlayableWithoutSize(m.into())),
|
||||
|
||||
(Initialized(bttn), s, dl, false) => Initialized(bttn.determine_state(s, dl)),
|
||||
(Initialized(bttn), _, _, true) => InProgress(bttn.into_progress()),
|
||||
(i @ InProgress(_), _, _, _) => i,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_size(self, bytes: Option<i32>) -> Self {
|
||||
use self::MediaMachine::*;
|
||||
let size = size_helper(bytes);
|
||||
|
||||
match (self, size) {
|
||||
(Initialized(bttn), s) => Initialized(bttn.set_size(s)),
|
||||
(InProgress(val), Some(s)) => InProgress(val.set_size(&s)),
|
||||
(n @ InProgress(_), None) => n,
|
||||
(n @ UnInitialized(_), _) => n,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_progress(&mut self, local_size: &str, fraction: f64) {
|
||||
use self::MediaMachine::*;
|
||||
|
||||
match *self {
|
||||
Initialized(_) => (),
|
||||
UnInitialized(_) => (),
|
||||
InProgress(ref mut val) => val.update_progress(local_size, fraction),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_helper(bytes: Option<i32>) -> Option<String> {
|
||||
let s = bytes?;
|
||||
if s == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
s.file_size(SIZE_OPTS.clone()).ok()
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
mod show;
|
||||
mod episode;
|
||||
mod episode_states;
|
||||
|
||||
pub use self::episode::EpisodeWidget;
|
||||
pub use self::show::ShowWidget;
|
||||
pub use self::show::mark_all_watched;
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
use dissolve;
|
||||
use failure::Error;
|
||||
// use glib;
|
||||
use gtk;
|
||||
use gtk::prelude::*;
|
||||
use html2pango::markup_from_raw;
|
||||
use open;
|
||||
|
||||
use hammond_data::Podcast;
|
||||
use hammond_data::dbqueries;
|
||||
use hammond_data::utils::{delete_show, replace_extra_spaces};
|
||||
|
||||
use app::Action;
|
||||
use utils::get_pixbuf_from_path;
|
||||
use utils::set_image_from_path;
|
||||
use widgets::episode::episodes_listbox;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::thread;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ShowWidget {
|
||||
@@ -62,6 +61,8 @@ impl ShowWidget {
|
||||
}
|
||||
|
||||
pub fn init(&self, pd: Arc<Podcast>, sender: Sender<Action>) {
|
||||
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/show_widget.ui");
|
||||
|
||||
// Hacky workaround so the pd.id() can be retrieved from the `ShowStack`.
|
||||
WidgetExt::set_name(&self.container, &pd.id().to_string());
|
||||
|
||||
@@ -72,7 +73,7 @@ impl ShowWidget {
|
||||
}
|
||||
}));
|
||||
|
||||
self.setup_listbox(&pd, sender.clone());
|
||||
self.setup_listbox(pd.clone(), sender.clone());
|
||||
self.set_description(pd.description());
|
||||
|
||||
if let Err(err) = self.set_cover(pd.clone()) {
|
||||
@@ -88,26 +89,35 @@ impl ShowWidget {
|
||||
error!("Error: {}", err);
|
||||
}
|
||||
});
|
||||
|
||||
let show_menu: gtk::Popover = builder.get_object("show_menu").unwrap();
|
||||
let mark_all: gtk::ModelButton = builder.get_object("mark_all_watched").unwrap();
|
||||
|
||||
let episodes = self.episodes.clone();
|
||||
mark_all.connect_clicked(clone!(pd, sender => move |_| {
|
||||
on_played_button_clicked(
|
||||
pd.clone(),
|
||||
&episodes,
|
||||
sender.clone()
|
||||
)
|
||||
}));
|
||||
self.settings.set_popover(&show_menu);
|
||||
}
|
||||
|
||||
/// Populate the listbox with the shows episodes.
|
||||
fn setup_listbox(&self, pd: &Podcast, sender: Sender<Action>) {
|
||||
fn setup_listbox(&self, pd: Arc<Podcast>, sender: Sender<Action>) {
|
||||
let listbox = episodes_listbox(pd, sender.clone());
|
||||
listbox.ok().map(|l| self.episodes.add(&l));
|
||||
}
|
||||
|
||||
/// Set the show cover.
|
||||
fn set_cover(&self, pd: Arc<Podcast>) -> Result<(), Error> {
|
||||
let image = get_pixbuf_from_path(&pd.into(), 128)?;
|
||||
self.cover.set_from_pixbuf(&image);
|
||||
Ok(())
|
||||
set_image_from_path(&self.cover, Arc::new(pd.into()), 128)
|
||||
}
|
||||
|
||||
/// Set the descripton text.
|
||||
fn set_description(&self, text: &str) {
|
||||
// TODO: Temporary solution until we render html urls/bold/italic probably with markup.
|
||||
let desc = dissolve::strip_html_tags(text).join(" ");
|
||||
self.description.set_text(&replace_extra_spaces(&desc));
|
||||
self.description.set_markup(&markup_from_raw(text));
|
||||
}
|
||||
|
||||
/// Set scrolled window vertical adjustment.
|
||||
@@ -124,13 +134,7 @@ fn on_unsub_button_clicked(
|
||||
// hack to get away without properly checking for none.
|
||||
// if pressed twice would panic.
|
||||
unsub_button.hide();
|
||||
// Spawn a thread so it won't block the ui.
|
||||
thread::spawn(move || {
|
||||
if let Err(err) = delete_show(&pd) {
|
||||
error!("Something went wrong trying to remove {}", pd.title());
|
||||
error!("Error: {}", err);
|
||||
}
|
||||
});
|
||||
sender.send(Action::RemoveShow(pd))?;
|
||||
|
||||
sender.send(Action::HeaderBarNormal)?;
|
||||
sender.send(Action::ShowShowsAnimated)?;
|
||||
@@ -141,9 +145,47 @@ fn on_unsub_button_clicked(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn on_played_button_clicked(pd: &Podcast, sender: Sender<Action>) -> Result<(), Error> {
|
||||
fn on_played_button_clicked(pd: Arc<Podcast>, episodes: >k::Frame, sender: Sender<Action>) {
|
||||
if dim_titles(episodes).is_none() {
|
||||
error!("Something went horribly wrong when dimming the titles.");
|
||||
warn!("RUN WHILE YOU STILL CAN!");
|
||||
}
|
||||
|
||||
sender.send(Action::MarkAllPlayerNotification(pd)).unwrap();
|
||||
}
|
||||
|
||||
pub fn mark_all_watched(pd: &Podcast, sender: Sender<Action>) -> Result<(), Error> {
|
||||
dbqueries::update_none_to_played_now(pd)?;
|
||||
sender.send(Action::RefreshWidget)?;
|
||||
sender.send(Action::RefreshWidgetIfVis)?;
|
||||
sender.send(Action::RefreshEpisodesView)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Ideally if we had a custom widget this would have been as simple as:
|
||||
// `for row in listbox { ep = row.get_episode(); ep.dim_title(); }`
|
||||
// But now I can't think of a better way to do it than hardcoding the title
|
||||
// position relative to the EpisodeWidget container gtk::Box.
|
||||
fn dim_titles(episodes: >k::Frame) -> Option<()> {
|
||||
let listbox = episodes
|
||||
.get_children()
|
||||
.remove(0)
|
||||
.downcast::<gtk::ListBox>()
|
||||
.ok()?;
|
||||
let children = listbox.get_children();
|
||||
|
||||
for row in children {
|
||||
let row = row.downcast::<gtk::ListBoxRow>().ok()?;
|
||||
let container = row.get_children().remove(0).downcast::<gtk::Box>().ok()?;
|
||||
let foo = container
|
||||
.get_children()
|
||||
.remove(0)
|
||||
.downcast::<gtk::Box>()
|
||||
.ok()?;
|
||||
let bar = foo.get_children().remove(0).downcast::<gtk::Box>().ok()?;
|
||||
let baz = bar.get_children().remove(0).downcast::<gtk::Box>().ok()?;
|
||||
let title = baz.get_children().remove(0).downcast::<gtk::Label>().ok()?;
|
||||
|
||||
title.get_style_context().map(|c| c.add_class("dim-label"));
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Project xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
|
||||
xmlns:foaf="http://xmlns.com/foaf/0.1/"
|
||||
xmlns:gnome="http://api.gnome.org/doap-extensions#"
|
||||
xmlns="http://usefulinc.com/ns/doap#">
|
||||
|
||||
<name xml:lang="en">Hammond</name>
|
||||
<shortdesc xml:lang="en">
|
||||
Modern Podcast Client for the GNOME desktop
|
||||
</shortdesc>
|
||||
<description xml:lang="en">
|
||||
<p>
|
||||
Hammond is a modern, reliable, and fast Podcast Client for the GNOME
|
||||
desktop written in Rust.
|
||||
</p>
|
||||
</description>
|
||||
<!-- <homepage rdf:resource="https://wiki.gnome.org/Apps/Hammond" /> -->
|
||||
<bug-database rdf:resource="https://gitlab.gnome.org/alatiera/Hammond/issues" />
|
||||
|
||||
<category rdf:resource="http://api.gnome.org/doap-extensions#apps" />
|
||||
<programming-language>Rust</programming-language>
|
||||
|
||||
<maintainer>
|
||||
<foaf:Person>
|
||||
<foaf:name>Jordan petridis</foaf:name>
|
||||
<foaf:mbox rdf:resource="mailto:jpetridis@gnome.org" />
|
||||
<gnome:userid>jpetridis</gnome:userid>
|
||||
</foaf:Person>
|
||||
</maintainer>
|
||||
</Project>
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
project(
|
||||
'hammond', 'rust',
|
||||
version: '0.3.0',
|
||||
version: '0.3.1',
|
||||
license: 'GPLv3',
|
||||
)
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
],
|
||||
"desktop-file-name-prefix" : "(Nightly) ",
|
||||
"finish-args" : [
|
||||
"--filesystem=xdg-run/dconf",
|
||||
"--filesystem=~/.config/dconf:ro",
|
||||
"--talk-name=ca.desrt.dconf",
|
||||
"--env=DCONF_USER_CONFIG_DIR=.config/dconf",
|
||||
"--share=network",
|
||||
"--share=ipc",
|
||||
"--socket=x11",
|
||||
@@ -29,7 +33,7 @@
|
||||
},
|
||||
"modules" : [
|
||||
{
|
||||
"name" : "Hammond",
|
||||
"name" : "hammond",
|
||||
"buildsystem" : "meson",
|
||||
"sources" : [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
schemadir = os.path.join(os.environ['MESON_INSTALL_PREFIX'], 'share', 'glib-2.0', 'schemas')
|
||||
|
||||
if not os.environ.get('DESTDIR'):
|
||||
print('Compiling gsettings schemas...')
|
||||
subprocess.call(['glib-compile-schemas', schemadir])
|
||||
Reference in New Issue
Block a user