Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f06dbd0562 | ||
|
|
4a6f5c6268 | ||
|
|
b5dbfb1a86 | ||
|
|
d86a17f76e | ||
|
|
37c7f20256 | ||
|
|
f324407c9c | ||
|
|
c96b39d597 | ||
|
|
d4d89a56e9 | ||
|
|
f19ad133c6 | ||
|
|
00d9019f29 | ||
|
|
8951a6e237 | ||
|
|
b0ac037964 | ||
|
|
2c8ff2d2f2 | ||
|
|
03bd951848 | ||
|
|
63e2ea987e | ||
|
|
115df7f884 | ||
|
|
ed94d34589 | ||
|
|
dc5ff9d809 | ||
|
|
72a6832571 | ||
|
|
b0c692ab7d | ||
|
|
06e8bc14f4 | ||
|
|
3d160fc35c | ||
|
|
734f85a517 | ||
|
|
a56a80db88 | ||
|
|
c4ed90dd5a | ||
|
|
f173b326a4 | ||
|
|
5e302290de | ||
|
|
75af3c7f2b | ||
|
|
d7aec6fdfb | ||
|
|
9a3586fd1c | ||
|
|
063bcbd0e5 | ||
|
|
a76e69e05d | ||
|
|
e560cce879 | ||
|
|
6406c3af13 | ||
|
|
173d2d3a3a | ||
|
|
9a5cc1595d | ||
|
|
d1962ab745 | ||
|
|
af5b27d0fc | ||
|
|
a9196d27d6 | ||
|
|
2f1ea12cfa | ||
|
|
37a408e58a | ||
|
|
736a993284 | ||
|
|
09359a8df3 | ||
|
|
a68987f257 | ||
|
|
3b5831f317 | ||
|
|
5336981154 | ||
|
|
df417fa619 | ||
|
|
509bbe25d2 | ||
|
|
f49012ab51 | ||
|
|
e4fc7c336e | ||
|
|
0e4430bae4 | ||
|
|
f811a9c8f4 | ||
|
|
771999c603 | ||
|
|
18820202d7 | ||
|
|
abc8fb988f | ||
|
|
885c525d7b | ||
|
|
418a2f02b2 | ||
|
|
031078284c | ||
|
|
3c7ba8c9d9 | ||
|
|
835078a84c | ||
|
|
049418c2f5 | ||
|
|
7c03266d16 | ||
|
|
627f06ea9f | ||
|
|
9f84178182 | ||
|
|
54641fc3c5 | ||
|
|
b8995d838a | ||
|
|
a0154c5919 | ||
|
|
08365c412a | ||
|
|
2d291a08fc | ||
|
|
7a17b3df4b | ||
|
|
0589f2fe2a | ||
|
|
a9abd75b51 | ||
|
|
bc6eeec663 | ||
|
|
3132856efe | ||
|
|
4db7628eed | ||
|
|
7b71f59d3e | ||
|
|
76c177bc0f | ||
|
|
50a508b596 | ||
|
|
59c634f626 | ||
|
|
08ebb9e7d6 | ||
|
|
f5f0a5b873 | ||
|
|
1036176e51 | ||
|
|
c6a24e839a | ||
|
|
87421ce74d | ||
|
|
f94ccb9947 | ||
|
|
1f1d4af8ba | ||
|
|
633803e0ad | ||
|
|
74fb8dc75c | ||
|
|
2523a0cf90 | ||
|
|
47a58a9e65 | ||
|
|
27c4fd9b30 | ||
|
|
f3904c599b | ||
|
|
b86f288e86 | ||
|
|
67bdd3664a | ||
|
|
8d4fdb8ece | ||
|
|
0720222423 | ||
|
|
7bca841a1a | ||
|
|
572ab86bc4 | ||
|
|
29cf5940f5 | ||
|
|
4b4f5c39d4 | ||
|
|
5069430a3a | ||
|
|
28d7373779 | ||
|
|
9d5fa04d49 | ||
|
|
43bf8b3f15 | ||
|
|
ed80605755 | ||
|
|
cc84a4637d | ||
|
|
701b759ba2 | ||
|
|
9cb2782ef9 | ||
|
|
5cd0a3c451 | ||
|
|
b74dbb74bb |
+10
-5
@@ -5,9 +5,9 @@ stages:
|
||||
.cargo_test_template: &cargo_test
|
||||
stage: test
|
||||
|
||||
# variables:
|
||||
# RUSTFLAGS: "-C link-dead-code"
|
||||
# RUST_BACKTRACE: "FULL"
|
||||
variables:
|
||||
RUSTFLAGS: "--cfg rayon_unstable"
|
||||
RUST_BACKTRACE: "FULL"
|
||||
|
||||
before_script:
|
||||
- apt-get update -yqq
|
||||
@@ -53,7 +53,7 @@ flatpak:
|
||||
stage: test
|
||||
script:
|
||||
- flatpak-builder --stop-at=hammond app org.gnome.Hammond.json
|
||||
# https://gitlab.gnome.org/alatiera/Hammond/issues/55
|
||||
# https://gitlab.gnome.org/World/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
|
||||
|
||||
@@ -98,10 +98,15 @@ rustfmt:
|
||||
clippy:
|
||||
image: "rustlang/rust:nightly"
|
||||
stage: lint
|
||||
variables:
|
||||
RUSTFLAGS: "--cfg rayon_unstable"
|
||||
before_script:
|
||||
- apt-get update -yqq
|
||||
- apt-get install -yqq --no-install-recommends build-essential libgtk-3-dev meson
|
||||
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
|
||||
allow_failure: true
|
||||
|
||||
+56
-38
@@ -6,52 +6,70 @@ 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.2] - 2018-05-07
|
||||
### Added:
|
||||
- Vies now have a new fancy scrolling animation when they are refereshed.
|
||||
|
||||
### Changed:
|
||||
- Downlaoding and loading images now is done asynchronously and is not blocking programs execution.
|
||||
[#7](https://gitlab.gnome.org/World/hammond/issues/7)
|
||||
- Bold, italics links and some other `html` tags can now be rendered in the Show Description.
|
||||
[#25](https://gitlab.gnome.org/World/hammond/issues/25)
|
||||
- `Rayon` Threadpools are now used instead of unlimited one-off threads.
|
||||
- `EpisdeWidget`s are now loaded asynchronously accross views.
|
||||
- `EpisodeWidget`s no longer trigger a `View` refresh for trivial stuff 03bd95184808ccab3e0ea0e3713a52ee6b7c9ab4
|
||||
- `ShowWidget` layout was changed 9a5cc1595d982f3232ee7595b83b6512ac8f6c88
|
||||
- `ShowWidget` Description is inside a scrolled window now
|
||||
|
||||
### Fixed:
|
||||
- `EpisodeWidget` Height now is consistent accros views [#57](https://gitlab.gnome.org/World/hammond/issues/57)
|
||||
- Implemented a tail-recursion loop to follow-up when a feed redirects to another url. c6a24e839a8ba77d09673f299cfc1e64ba7078f3
|
||||
|
||||
### Removed:
|
||||
- Removed the custom configuration file and replaced instructions to just use meson. 1f1d4af8ba7db8f56435d13a1c191ecff3d4a85b
|
||||
|
||||
## [0.3.1] - 2018-03-28
|
||||
### Added:
|
||||
- Ability to mark all episodes of a Show as watched.
|
||||
[#47](https://gitlab.gnome.org/World/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/World/hammond/issues/49)
|
||||
- Hammond now remembers the window size and position. (Rowan Lewis)
|
||||
[#50](https://gitlab.gnome.org/World/hammond/issues/50)
|
||||
- Implemnted the initial work for integrating with GSettings and storing preferences. (Rowan Lewis)
|
||||
[!22](https://gitlab.gnome.org/World/hammond/merge_requests/22) [!23](https://gitlab.gnome.org/World/hammond/merge_requests/23)
|
||||
- Shows without episodes now display an empty message similar to EmptyView.
|
||||
[#44](https://gitlab.gnome.org/World/hammond/issues/44)
|
||||
|
||||
* 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)
|
||||
### Changed:
|
||||
- EpisdeWidget has been reimplemented as a compile time state machine.
|
||||
[!18](https://gitlab.gnome.org/World/hammond/merge_requests/18)
|
||||
- Content Views no longer scroll horizontally when shrunk bellow their minimum size.
|
||||
[#35](https://gitlab.gnome.org/World/hammond/issues/35)
|
||||
- Some requests now use the Tor Browser's user agent. (Rowan Lewis)
|
||||
[#53](https://gitlab.gnome.org/World/hammond/issues/53)
|
||||
|
||||
### Fixed:
|
||||
- Double border aroun the main window was fixed. (Rowan Lewis)
|
||||
[#52](https://gitlab.gnome.org/World/hammond/issues/52)
|
||||
|
||||
## [0.3.0] - 2018-02-11
|
||||
|
||||
* Tobias Bernard Redesigned the whole Gtk+ client.
|
||||
* Complete re-write of hammond-data and hammond-gtk modules.
|
||||
* Error handling for all crates was migrated from error-chain to Failure.
|
||||
* Hammond-data now uses futures to parse feeds.
|
||||
* Custom gtk-widgets are now composed structs as opposed to functions returning Gtk widgets.
|
||||
- Tobias Bernard Redesigned the whole Gtk+ client.
|
||||
- Complete re-write of hammond-data and hammond-gtk modules.
|
||||
- Error handling for all crates was migrated from error-chain to Failure.
|
||||
- Hammond-data now uses futures to parse feeds.
|
||||
- Custom gtk-widgets are now composed structs as opposed to functions returning Gtk widgets.
|
||||
|
||||
## [0.2.0] - 2017-11-28
|
||||
|
||||
* Database Schema Breaking Changes.
|
||||
* Added url sanitization. #4.
|
||||
* Reworked and refactored of the hammond-data API.
|
||||
* Added some more unit tests
|
||||
* Documented hammond-data public API.
|
||||
- Database Schema Breaking Changes.
|
||||
- Added url sanitization. #4.
|
||||
- Reworked and refactored of the hammond-data API.
|
||||
- Added some more unit tests
|
||||
- Documented hammond-data public API.
|
||||
|
||||
## [0.1.1] - 2017-11-13
|
||||
|
||||
* Added appdata.xml file
|
||||
- Added appdata.xml file
|
||||
|
||||
## [0.1.0] - 2017-11-13
|
||||
|
||||
Initial Release
|
||||
- Initial Release
|
||||
+2
-2
@@ -12,7 +12,7 @@ Please note we have a [code of conduct](https://wiki.gnome.org/Foundation/CodeOf
|
||||
## Source repository
|
||||
|
||||
Hammond's main source repository is at gitlab.gnome.org. You can view
|
||||
the web interface [here](https://gitlab.gnome.org/alatiera/hammond)
|
||||
the web interface [here](https://gitlab.gnome.org/World/hammond)
|
||||
|
||||
Development happens in the master branch.
|
||||
|
||||
@@ -51,7 +51,7 @@ In order to run the test suite use the following: `cargo test -- --test-threads=
|
||||
# Issues, issues and more issues!
|
||||
|
||||
There are many ways you can contribute to Hammond, and all of them involve creating issues
|
||||
in [Hammond issue tracker](https://gitlab.gnome.org/alatiera/Hammond/issues). This is the entry point for your contribution.
|
||||
in [Hammond issue tracker](https://gitlab.gnome.org/World/hammond/issues). This is the entry point for your contribution.
|
||||
|
||||
To create an effective and high quality ticket, try to put the following information on your
|
||||
ticket:
|
||||
|
||||
Generated
+242
-107
@@ -69,7 +69,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.5"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"backtrace-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -84,7 +84,7 @@ name = "backtrace-sys"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"cc 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cc 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
@@ -109,7 +109,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "build_const"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
@@ -160,7 +160,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.9"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
@@ -170,7 +170,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.1"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"num-integer 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -214,33 +214,33 @@ name = "crc"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"build_const 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"atty 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"criterion-plot 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"criterion-stats 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"criterion-plot 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"criterion-stats 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"handlebars 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"itertools 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"itertools-num 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_derive 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_derive 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"simplelog 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion-plot"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"byteorder 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -250,7 +250,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "criterion-stats"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"cast 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -260,6 +260,16 @@ dependencies = [
|
||||
"thread-scoped 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"crossbeam-epoch 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"parking_lot 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.2.0"
|
||||
@@ -278,6 +288,19 @@ dependencies = [
|
||||
"crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"arrayvec 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"memoffset 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.3.1"
|
||||
@@ -350,31 +373,32 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "diesel"
|
||||
version = "1.1.1"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"byteorder 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"diesel_derives 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"diesel_derives 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libsqlite3-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"r2d2 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diesel_derives"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quote 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"syn 0.12.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diesel_migrations"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"migrations_internals 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"migrations_macros 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"migrations_internals 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"migrations_macros 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -405,7 +429,7 @@ name = "error-chain"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"backtrace 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"backtrace 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -413,7 +437,7 @@ name = "failure"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"backtrace 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"backtrace 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
@@ -648,17 +672,17 @@ name = "hammond-data"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ammonia 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"chrono 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"criterion 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"criterion 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"derive_builder 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"diesel 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"diesel_migrations 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"diesel 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"diesel_migrations 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"hyper 0.11.24 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"html5ever 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"hyper 0.11.25 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"hyper-tls 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"itertools 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"native-tls 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -666,10 +690,13 @@ dependencies = [
|
||||
"pretty_assertions 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rayon 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rayon-futures 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rfc822_sanitizer 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rss 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rss 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"string_cache 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"string_cache_codegen 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-core 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"xdg 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
@@ -683,7 +710,7 @@ dependencies = [
|
||||
"failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"hammond-data 0.1.0",
|
||||
"hyper 0.11.24 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"hyper 0.11.25 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"mime_guess 1.8.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"pretty_assertions 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -695,7 +722,8 @@ dependencies = [
|
||||
name = "hammond-gtk"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"crossbeam-channel 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"gdk 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -715,8 +743,8 @@ dependencies = [
|
||||
"rayon 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"reqwest 0.8.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"send-cell 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"send-cell 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"take_mut 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
@@ -732,18 +760,20 @@ dependencies = [
|
||||
"pest_derive 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quick-error 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html2pango"
|
||||
version = "0.1.0"
|
||||
source = "git+https://gitlab.gnome.org/World/html2pango#63fb1ea644ef38bf7f268bbe8f40b19dd77d7995"
|
||||
source = "git+https://gitlab.gnome.org/World/html2pango#6bf3fe5e71459dc283148efe96a9c9f8c66d0791"
|
||||
dependencies = [
|
||||
"ammonia 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"linkify 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"maplit 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"marksman_escape 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
@@ -771,7 +801,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.11.24"
|
||||
version = "0.11.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"base64 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -786,7 +816,7 @@ dependencies = [
|
||||
"percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"relay 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-core 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-proto 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -799,9 +829,9 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"hyper 0.11.24 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"hyper 0.11.25 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"native-tls 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-core 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-tls 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -902,7 +932,15 @@ version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"vcpkg 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linkify"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -941,6 +979,11 @@ name = "maplit"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "marksman_escape"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever"
|
||||
version = "0.7.2"
|
||||
@@ -948,9 +991,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_derive 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_derive 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"string_cache 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"string_cache_codegen 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tendril 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -969,6 +1012,11 @@ dependencies = [
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.2.1"
|
||||
@@ -976,18 +1024,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "migrations_internals"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"diesel 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"diesel 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "migrations_macros"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"migrations_internals 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"migrations_internals 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
@@ -1131,18 +1179,26 @@ dependencies = [
|
||||
"foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"openssl-sys 0.9.27 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"openssl-sys 0.9.28 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.27"
|
||||
version = "0.9.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"cc 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cc 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"vcpkg 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "owning_ref"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"stable_deref_trait 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1170,6 +1226,26 @@ dependencies = [
|
||||
"pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"owning_ref 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"parking_lot_core 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"smallvec 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "1.0.1"
|
||||
@@ -1246,7 +1322,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "0.3.1"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -1272,12 +1356,20 @@ name = "quote"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"proc-macro2 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1331,6 +1423,15 @@ dependencies = [
|
||||
"rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-futures"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rayon-core 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.1.37"
|
||||
@@ -1351,14 +1452,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex-syntax 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex-syntax 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.5.3"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -1388,16 +1489,16 @@ dependencies = [
|
||||
"bytes 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"encoding_rs 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"hyper 0.11.24 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"hyper 0.11.25 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"hyper-tls 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libflate 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"mime_guess 2.0.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"native-tls 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_urlencoded 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-core 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-tls 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -1409,14 +1510,14 @@ name = "rfc822_sanitizer"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"chrono 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rss"
|
||||
version = "1.4.0"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"derive_builder 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -1483,43 +1584,43 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "send-cell"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.37"
|
||||
version = "1.0.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.37"
|
||||
version = "1.0.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"proc-macro2 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quote 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_derive_internals 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_derive_internals 0.23.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"syn 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive_internals"
|
||||
version = "0.23.0"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"proc-macro2 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"syn 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.13"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1529,7 +1630,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
@@ -1538,7 +1639,7 @@ name = "simplelog"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"chrono 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"term 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
@@ -1563,6 +1664,16 @@ name = "smallvec"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "string_cache"
|
||||
version = "0.7.1"
|
||||
@@ -1572,7 +1683,7 @@ dependencies = [
|
||||
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"precomputed-hash 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"string_cache_codegen 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"string_cache_shared 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
@@ -1608,12 +1719,22 @@ dependencies = [
|
||||
"unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "0.12.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quote 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"proc-macro2 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quote 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
@@ -1733,7 +1854,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-core"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"bytes 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -1742,11 +1863,11 @@ dependencies = [
|
||||
"log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"mio 0.6.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"scoped-tls 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"slab 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-executor 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-reactor 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-timer 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1779,7 +1900,7 @@ dependencies = [
|
||||
"slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"smallvec 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"take 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-core 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
@@ -1847,7 +1968,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"native-tls 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-core 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
@@ -1959,7 +2080,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
@@ -2029,30 +2150,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
"checksum arrayvec 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a1e964f9e24d588183fcb43503abda40d288c8657dfc27311516ce2f05675aef"
|
||||
"checksum atk-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8dc233521f7bffd3042c31082ea71bd08820abf44bac938fb36591e20f76f39"
|
||||
"checksum atty 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "af80143d6f7608d746df1520709e5d141c96f240b0e62b0aa41bdfb53374d9d4"
|
||||
"checksum backtrace 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ebbbf59b1c43eefa8c3ede390fcc36820b4999f7914104015be25025e0d62af2"
|
||||
"checksum backtrace 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ebbe525f66f42d207968308ee86bc2dd60aa5fab535b22e616323a173d097d8e"
|
||||
"checksum backtrace-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "44585761d6161b0f57afc49482ab6bd067e4edef48c12a152c237eb0203f7661"
|
||||
"checksum base64 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "229d032f1a99302697f10b27167ae6d03d49d032e6a8e2550e8d3fc13356d2b4"
|
||||
"checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5"
|
||||
"checksum bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b3c30d3802dfb7281680d6285f2ccdaa8c2d8fee41f93805dba5c4cf50dc23cf"
|
||||
"checksum build_const 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e90dc84f5e62d2ebe7676b83c22d33b6db8bd27340fb6ffbff0a364efa0cb9c9"
|
||||
"checksum build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39"
|
||||
"checksum byteorder 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "73b5bdfe7ee3ad0b99c9801d58807a9dbc9e09196365b0203853b99889ab3c87"
|
||||
"checksum bytes 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "1b7db437d718977f6dc9b2e3fd6fc343c02ac6b899b73fdd2179163447bd9ce9"
|
||||
"checksum c_vec 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "11f56b93adbca81a4ed1bb654070013b496a9b0b259a296281f4be24af84debd"
|
||||
"checksum cairo-rs 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a110f269c2fd382df5fe8bd46dfa5f1b83608aa717fecb6e7a28c08c202f0e13"
|
||||
"checksum cairo-sys-rs 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0395175ecba60accac076a02c31d143b9dcd9d5eb5316d7163a3273803b765c7"
|
||||
"checksum cast 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "926013f2860c46252efceabb19f4a6b308197505082c609025aa6706c011d427"
|
||||
"checksum cc 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)" = "2b4911e4bdcb4100c7680e7e854ff38e23f1b34d4d9e079efae3da2801341ffc"
|
||||
"checksum cc 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)" = "8b9d2900f78631a5876dc5d6c9033ede027253efcd33dd36b1309fc6cab97ee0"
|
||||
"checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de"
|
||||
"checksum chrono 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ba5f60682a4c264e7f8d77b82e7788938a76befdf949d4a98026d19099c9d873"
|
||||
"checksum chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1cce36c92cb605414e9b824f866f5babe0a0368e39ea07393b9b63cf3844c0e6"
|
||||
"checksum clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0f16b89cbb9ee36d87483dc939fe9f1e13c05898d56d7b230a0d4dff033a536"
|
||||
"checksum core-foundation 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "25bfd746d203017f7d5cbd31ee5d8e17f94b6521c7af77ece6c9e4b2d4b16c67"
|
||||
"checksum core-foundation-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "065a5d7ffdcbc8fa145d6f0746f3555025b9097a9e9cda59f7467abae670c78d"
|
||||
"checksum crc 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bd5d02c0aac6bd68393ed69e00bbc2457f3e89075c6349db7189618dc4ddc1d7"
|
||||
"checksum criterion 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4556caa5b5e69626a0b69c4892baa6ab518cf2e802384a5eeef15dd4416d4771"
|
||||
"checksum criterion-plot 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "029914bacd6bb0a521429ff8df0c6c79be1d99f5c4bb85475caabb78f5e06da6"
|
||||
"checksum criterion-stats 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a57a868c589ef2208c0f441e816810e16bfd9cf6a6ea6548f53938b8a530d362"
|
||||
"checksum criterion 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4f11151e2961d0483e5eb7a2ede5ed8071a460d04d2b7c89e8257aa5502b0e0b"
|
||||
"checksum criterion-plot 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9f7f7c88a8d341dd9fd9e31a72ca2ca24428db79afb491852873b2c784e037e6"
|
||||
"checksum criterion-stats 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "dd48feb0253b2968ff3085e7f3fba6738c9ff859f420a2fb81a48986eb66da36"
|
||||
"checksum crossbeam-channel 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9d7b07a3084d8718d95338443d5a46aab38ce16d5f991d4027a0906b369f70a3"
|
||||
"checksum crossbeam-deque 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f739f8c5363aca78cfb059edf753d8f0d36908c348f3d8d1503f03d8b75d9cf3"
|
||||
"checksum crossbeam-deque 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c1bdc73742c36f7f35ebcda81dbb33a7e0d33757d03a06d9ddca762712ec5ea2"
|
||||
"checksum crossbeam-epoch 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9898f21d6d647793e163c804944941fb19aecd1f4a1a4c254bbb0bee15ccdea5"
|
||||
"checksum crossbeam-epoch 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "927121f5407de9956180ff5e936fe3cf4324279280001cd56b669d28ee7e9150"
|
||||
"checksum crossbeam-epoch 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9b4e2817eb773f770dcb294127c011e22771899c21d18fce7dd739c0b9832e81"
|
||||
"checksum crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2760899e32a1d58d5abb31129f8fae5de75220bc2176e77ff7c627ae45c918d9"
|
||||
@@ -2060,9 +2183,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
"checksum debug_unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9a032eac705ca39214d169f83e3d3da290af06d8d1d344d1baad2fd002dca4b3"
|
||||
"checksum derive_builder 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c998e6ab02a828dd9735c18f154e14100e674ed08cb4e1938f0e4177543f439"
|
||||
"checksum derive_builder_core 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "735e24ee9e5fa8e16b86da5007856e97d592e11867e45d76e0c0d0a164a0b757"
|
||||
"checksum diesel 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "925325c57038f2f14c0413bdf6a92ca72acff644959d0a1a9ebf8d19be7e9c01"
|
||||
"checksum diesel_derives 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "28e2b2605ac6a3b9a586383f5f8b2b5f1108f07a421ade965b266289d2805e79"
|
||||
"checksum diesel_migrations 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0928a7d6f27c849954185416bd59439837de55fbc89e2985b0e46e756ae4e3da"
|
||||
"checksum diesel 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "24815a0c2094f2c8dafe74ab3b9e975892f44acbb94b4d4b4898025a7615efa4"
|
||||
"checksum diesel_derives 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6471a2b637b414d3ee1504cf230409a550381c79204282f8fe06c527e4ae56be"
|
||||
"checksum diesel_migrations 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "992026fb46e684eaede3d9d05737de34bdcdcb7f97b389b3c0fa7999389d504e"
|
||||
"checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
|
||||
"checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab"
|
||||
"checksum either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3be565ca5c557d7f59e7cfcf1844f9e3033650c929c6566f511e8005f205c1d0"
|
||||
@@ -2094,7 +2217,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
"checksum html5ever 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e579ac8647178ab915d400d7d22938bda5cd351c6c62e1c294d56884ccfc75fe"
|
||||
"checksum httparse 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c2f407128745b78abc95c0ffbe4e5d37427fdc0d45470710cfef8c44522a2e37"
|
||||
"checksum humansize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e"
|
||||
"checksum hyper 0.11.24 (registry+https://github.com/rust-lang/crates.io-index)" = "df4dd5dae401458087396b6db7fabc4d6760aa456a5fa8e92bda549f39cae661"
|
||||
"checksum hyper 0.11.25 (registry+https://github.com/rust-lang/crates.io-index)" = "549dbb86397490ce69d908425b9beebc85bbaad25157d67479d4995bb56fdf9a"
|
||||
"checksum hyper-tls 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a5aa51f6ae9842239b0fac14af5f22123b8432b4cc774a44ff059fcba0f675ca"
|
||||
"checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d"
|
||||
"checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08"
|
||||
@@ -2110,17 +2233,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
"checksum libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)" = "6fd41f331ac7c5b8ac259b8bf82c75c0fb2e469bbf37d2becbba9a6a2221965b"
|
||||
"checksum libflate 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "1a429b86418868c7ea91ee50e9170683f47fd9d94f5375438ec86ec3adb74e8e"
|
||||
"checksum libsqlite3-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0e9eb7b8e152b6a01be6a4a2917248381875758250dc3df5d46caf9250341dda"
|
||||
"checksum linkify 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9ce9439c6f4a1092dc1861272bef01034891da39f13aa1cdcf40ca3e4081de5f"
|
||||
"checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b"
|
||||
"checksum log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "89f010e843f2b1a31dbd316b3b8d443758bc634bed37aabade59c686d644e0a2"
|
||||
"checksum loggerv 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ba6b0664956d197c6e0223870c1cd1ec4117aea282b4c0bd5ab01119d31d708d"
|
||||
"checksum mac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||
"checksum maplit 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "08cbb6b4fef96b6d77bfc40ec491b1690c779e77b05cd9f07f787ed376fd4c43"
|
||||
"checksum marksman_escape 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e1a9925825c96f25fedf82d5a9290a54d1d95fe6d48d234120b27fd99affceb7"
|
||||
"checksum markup5ever 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfedc97d5a503e96816d10fedcd5b42f760b2e525ce2f7ec71f6a41780548475"
|
||||
"checksum matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "100aabe6b8ff4e4a7e32c1c13523379802df0772b82466207ac25b013f193376"
|
||||
"checksum memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "796fba70e76612589ed2ce7f45282f5af869e0fdd7cc6199fa1aa1f1d591ba9d"
|
||||
"checksum memoffset 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e163e5baece1a039e71e75b074de17a9b4114982aa109921fc20253bdf91a53c"
|
||||
"checksum memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0f9dc261e2b62d7a622bf416ea3c5245cdd5d9a7fcc428c0d06804dfce1775b3"
|
||||
"checksum migrations_internals 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bd916de6df9ac7e811e7e1ac28e0abfebe5205f3b29a7bda9ec8a41ee980a4eb"
|
||||
"checksum migrations_macros 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5a550cfd76f6cfdf15a7b541893d7c79b68277b0b309f12179211a373a56e617"
|
||||
"checksum migrations_internals 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "54b8f3128f95b1e5b5f5d5d7ce2156b306b9d11dd50e23b594f25247e30ca83a"
|
||||
"checksum migrations_macros 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a616c2538e1f09bf402efae6feb73c2711bb137c794b797f3fb8b3df84275d4b"
|
||||
"checksum mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ba626b8a6de5da682e1caa06bdb42a335aee5a84db8e5046a3e8ab17ba0a3ae0"
|
||||
"checksum mime 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e2e00e17be181010a91dbfefb01660b17311059dc8c7f48b9017677721e732bd"
|
||||
"checksum mime_guess 1.8.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b7e2b09d08313f84e0fb82d13a4d859109a17543fe9af3b6d941dc1431f7de79"
|
||||
@@ -2136,9 +2262,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
"checksum num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c51a3322e4bca9d212ad9a158a02abc6934d005490c054a2778df73a70aa0a30"
|
||||
"checksum open 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c281318d992e4432cfa799969467003d05921582a7489a8325e37f8a450d5113"
|
||||
"checksum openssl 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)" = "a3605c298474a3aa69de92d21139fb5e2a81688d308262359d85cdd0d12a7985"
|
||||
"checksum openssl-sys 0.9.27 (registry+https://github.com/rust-lang/crates.io-index)" = "d6fdc5c4a02e69ce65046f1763a0181107038e02176233acb0b3351d7cc588f9"
|
||||
"checksum openssl-sys 0.9.28 (registry+https://github.com/rust-lang/crates.io-index)" = "0bbd90640b148b46305c1691eed6039b5c8509bed16991e3562a01eeb76902a3"
|
||||
"checksum owning_ref 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "cdf84f41639e037b484f93433aa3897863b561ed65c6e59c7073d7c561710f37"
|
||||
"checksum pango 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "45374801e224373c3c0393cd48073c81093494c8735721e81d1dbaa4096b2767"
|
||||
"checksum pango-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "94039b3921a4af4058a3e4335e5d15099101f298a92f5afc40bab3a3027594a1"
|
||||
"checksum parking_lot 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "149d8f5b97f3c1133e3cfcd8886449959e856b557ff281e292b733d7c69e005e"
|
||||
"checksum parking_lot_core 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "538ef00b7317875071d5e00f603f24d16f0b474c1a5fc0ccb8b454ca72eafa79"
|
||||
"checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831"
|
||||
"checksum pest 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0fce5d8b5cc33983fc74f78ad552b5522ab41442c4ca91606e4236eb4b5ceefc"
|
||||
"checksum pest_derive 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "ab94faafeb93f4c5e3ce81ca0e5a779529a602ad5d09ae6d21996bfb8b6a52bf"
|
||||
@@ -2149,25 +2278,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
"checksum pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "3a8b4c6b8165cd1a1cd4b9b120978131389f64bdaf456435caa41e630edba903"
|
||||
"checksum precomputed-hash 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||
"checksum pretty_assertions 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3a029430f0d744bc3d15dd474d591bed2402b645d024583082b9f63bb936dac6"
|
||||
"checksum proc-macro2 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "388d7ea47318c5ccdeb9ba6312cee7d3f65dd2804be8580a170fce410d50b786"
|
||||
"checksum proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "cd07deb3c6d1d9ff827999c7f9b04cdfd66b1b17ae508e14fe47b620f2282ae0"
|
||||
"checksum proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "49b6a521dc81b643e9a51e0d1cf05df46d5a2f3c0280ea72bcb68276ba64a118"
|
||||
"checksum quick-error 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "eda5fe9b71976e62bc81b781206aaa076401769b2143379d3eb2118388babac4"
|
||||
"checksum quick-xml 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b14c27e04216596a49f2b82398a24f67ed9f131a5c0e0235496ea446bdacfb12"
|
||||
"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
|
||||
"checksum quote 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1eca14c727ad12702eb4b6bfb5a232287dcf8385cb8ca83a3eeaf6519c44c408"
|
||||
"checksum quote 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7b0ff51282f28dc1b53fd154298feaa2e77c5ea0dba68e1fd8b03b72fbe13d2a"
|
||||
"checksum r2d2 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f9078ca6a8a5568ed142083bb2f7dc9295b69d16f867ddcc9849e51b17d8db46"
|
||||
"checksum rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)" = "15a732abf9d20f0ad8eeb6f909bf6868722d9a06e1e50802b6a70351f40b4eb1"
|
||||
"checksum rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eba5f8cb59cc50ed56be8880a5c7b496bfd9bd26394e176bc67884094145c2c5"
|
||||
"checksum rayon 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "80e811e76f1dbf68abf87a759083d34600017fc4e10b6bd5ad84a700f9dba4b1"
|
||||
"checksum rayon-core 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9d24ad214285a7729b174ed6d3bcfcb80177807f959d95fafd5bfc5c4f201ac8"
|
||||
"checksum rayon-futures 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ea70dae42033c388536c7e9bdca084a14d75659c12e25fc105e7552464a87c4a"
|
||||
"checksum redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "0d92eecebad22b767915e4d529f89f28ee96dbbf5a4810d2b844373f136417fd"
|
||||
"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
|
||||
"checksum regex 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "aec3f58d903a7d2a9dc2bf0e41a746f4530e0cab6b615494e058f67a3ef947fb"
|
||||
"checksum regex-syntax 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b2550876c31dc914696a6c2e01cbce8afba79a93c8ae979d2fe051c0230b3756"
|
||||
"checksum regex-syntax 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "bd90079345f4a4c3409214734ae220fd773c6f2e8a543d07370c6c1c369cfbfb"
|
||||
"checksum relay 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1576e382688d7e9deecea24417e350d3062d97e32e45d70b1cde65994ff1489a"
|
||||
"checksum remove_dir_all 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dfc5b3ce5d5ea144bb04ebd093a9e14e9765bcfec866aecda9b6dec43b3d1e24"
|
||||
"checksum reqwest 0.8.5 (registry+https://github.com/rust-lang/crates.io-index)" = "241faa9a8ca28a03cbbb9815a5d085f271d4c0168a19181f106aa93240c22ddb"
|
||||
"checksum rfc822_sanitizer 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "680e8305c1e0cdf836dc4bec5424e045f278c975a3cac36d1ca01c4695f9d815"
|
||||
"checksum rss 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dfaaee0d75c76ae197cc2671dc8c531314006e48790c725a9bf0b227d91d19e8"
|
||||
"checksum rss 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5d76fce76b29eeec1d003ec42d8819b0d07839c65d00219941ddf815fdb94812"
|
||||
"checksum rustc-demangle 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11fb43a206a04116ffd7cfcf9bcb941f8eb6cc7ff667272246b0a1c74259a3cb"
|
||||
"checksum safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f"
|
||||
"checksum schannel 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "fbaffce35eb61c5b00846e73128b0cd62717e7c0ec46abbec132370d013975b4"
|
||||
@@ -2176,22 +2308,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
"checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27"
|
||||
"checksum security-framework 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "dfa44ee9c54ce5eecc9de7d5acbad112ee58755239381f687e564004ba4a2332"
|
||||
"checksum security-framework-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "5421621e836278a0b139268f36eee0dc7e389b784dc3f79d8f11aabadf41bead"
|
||||
"checksum send-cell 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1c620dd7e056b468b9d374a9f51cfa6bb4bf17a8ca4ee62e5efa0d99aaff2c41"
|
||||
"checksum serde 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)" = "d3bcee660dcde8f52c3765dd9ca5ee36b4bf35470a738eb0bd5a8752b0389645"
|
||||
"checksum serde_derive 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)" = "f1711ab8b208541fa8de00425f6a577d90f27bb60724d2bb5fd911314af9668f"
|
||||
"checksum serde_derive_internals 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)" = "89b340a48245bc03ddba31d0ff1709c118df90edc6adabaca4aac77aea181cce"
|
||||
"checksum serde_json 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)" = "5c508584d9913df116b91505eec55610a2f5b16e9ed793c46e4d0152872b3e74"
|
||||
"checksum send-cell 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "fecc1ff30726db72867bc644a57a8914e7cb023b396489873f78e11f9c96b280"
|
||||
"checksum serde 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)" = "53e6b6859395f46cf528414659ce43e70902b2277519707c3bd91797b3320330"
|
||||
"checksum serde_derive 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)" = "16e97f8dc5b2dabc0183e0cde24b1a53835e5bb3d2c9e0fdb077f895bba7f2a9"
|
||||
"checksum serde_derive_internals 0.23.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9d30c4596450fd7bbda79ef15559683f9a79ac0193ea819db90000d7e1cae794"
|
||||
"checksum serde_json 1.0.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7bf1cbb1387028a13739cb018ee0d9b3db534f22ca3c84a5904f7eadfde14e75"
|
||||
"checksum serde_urlencoded 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ce0fd303af908732989354c6f02e05e2e6d597152870f2c6990efb0577137480"
|
||||
"checksum simplelog 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ce595117de34b75e057b41e99079e43e9fcc4e5ec9c7ba5f2fea55321f0c624e"
|
||||
"checksum siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0df90a788073e8d0235a67e50441d47db7c8ad9debd91cbf43736a2a92d36537"
|
||||
"checksum slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "17b4fcaed89ab08ef143da37bc52adbcc04d4a69014f4c1208d6b51f0c47bc23"
|
||||
"checksum slab 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fdeff4cd9ecff59ec7e3744cbca73dfe5ac35c2aedb2cfba8a1c715a18912e9d"
|
||||
"checksum smallvec 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4c8cbcd6df1e117c2210e13ab5109635ad68a929fcbb8964dc965b76cb5ee013"
|
||||
"checksum smallvec 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "44db0ecb22921ef790d17ae13a3f6d15784183ff5f2a01aa32098c7498d2b4b9"
|
||||
"checksum stable_deref_trait 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "15132e0e364248108c5e2c02e3ab539be8d6f5d52a01ca9bbf27ed657316f02b"
|
||||
"checksum string_cache 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39cb4173bcbd1319da31faa5468a7e3870683d7a237150b0b0aaafd546f6ad12"
|
||||
"checksum string_cache_codegen 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "479cde50c3539481f33906a387f2bd17c8e87cb848c35b6021d41fb81ff9b4d7"
|
||||
"checksum string_cache_shared 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b1884d1bc09741d466d9b14e6d37ac89d6909cbcac41dd9ae982d4d063bbedfc"
|
||||
"checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550"
|
||||
"checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad"
|
||||
"checksum syn 0.12.15 (registry+https://github.com/rust-lang/crates.io-index)" = "c97c05b8ebc34ddd6b967994d5c6e9852fa92f8b82b3858c39451f97346dcce5"
|
||||
"checksum syn 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)" = "91b52877572087400e83d24b9178488541e3d535259e04ff17a63df1e5ceff59"
|
||||
"checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6"
|
||||
"checksum synstructure 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3a761d12e6d8dcb4dcf952a7a89b475e3a9d69e4a69307e01a470977642914bd"
|
||||
@@ -2206,7 +2341,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
"checksum thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "279ef31c19ededf577bfd12dfae728040a21f635b06a24cd670ff510edd38963"
|
||||
"checksum time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "a15375f1df02096fb3317256ce2cee6a1f42fc84ea5ad5fc8c421cfe40c73098"
|
||||
"checksum tokio 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "be15ef40f675c9fe66e354d74c73f3ed012ca1aa14d65846a33ee48f1ae8d922"
|
||||
"checksum tokio-core 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "799492ccba3d8ed5e41f2520a7cfd504cb65bbfe5fbbbd0012e335ae5f188051"
|
||||
"checksum tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "aeeffbbb94209023feaef3c196a41cbcdafa06b4a6f893f68779bb5e53796f71"
|
||||
"checksum tokio-executor 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8cac2a7883ff3567e9d66bb09100d09b33d90311feca0206c7ca034bc0c55113"
|
||||
"checksum tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "6af9eb326f64b2d6b68438e1953341e00ab3cf54de7e35d92bfc73af8555313a"
|
||||
"checksum tokio-proto 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8fbb47ae81353c63c487030659494b295f6cb6576242f907f203473b191b0389"
|
||||
@@ -2231,7 +2366,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
"checksum utf-8 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f1262dfab4c30d5cb7c07026be00ee343a6cf5027fdc0104a9160f354e5db75c"
|
||||
"checksum utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "662fab6525a98beff2921d7f61a39e7d59e0b425ebc7d0d9e66d316e55124122"
|
||||
"checksum uuid 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bcc7e3b898aa6f6c08e5295b6c89258d1331e9ac578cc992fb818759951bdc22"
|
||||
"checksum vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9e0a7d8bed3178a8fb112199d466eeca9ed09a14ba8ad67718179b4fd5487d0b"
|
||||
"checksum vcpkg 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7ed0f6789c8a85ca41bbc1c9d175422116a9869bd1cf31bb08e1493ecce60380"
|
||||
"checksum vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "887b5b631c2ad01628bbbaa7dd4c869f80d3186688f8d0b6f58774fbe324988c"
|
||||
"checksum version_check 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6b772017e347561807c1aa192438c5fd74242a670a6cffacc40f2defd1dc069d"
|
||||
"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
|
||||
|
||||
@@ -7,4 +7,3 @@ members = [
|
||||
|
||||
[profile.release]
|
||||
debug = false
|
||||
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
# Hammond
|
||||
|
||||
## 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
|
||||
|
||||
* TBA
|
||||
### A Podcast Client for GNOME written in Rust.
|
||||
|
||||

|
||||

|
||||
@@ -22,12 +15,13 @@ Get Builder [here](https://wiki.gnome.org/Apps/Builder/Downloads)
|
||||
## Broken Feeds
|
||||
|
||||
Found a feed that does not work in Hammond?
|
||||
Please [open an issue](https://gitlab.gnome.org/alatiera/Hammond/issues/new) and choose the `BrokenFeed` template so we will know and fix it!
|
||||
Please [open an issue](https://gitlab.gnome.org/World/hammond/issues/new) and choose the `BrokenFeed` template so we will know and fix it!
|
||||
|
||||
## Getting in Touch
|
||||
|
||||
If you have any questions regarding the use or development of Hammond,
|
||||
want to discuss design or simply hang out, please join us in [#hammond on irc.gnome.org.](irc://irc.gnome.org/#hammond)
|
||||
want to discuss design or simply hang out, please join us in `#hammond` on
|
||||
[irc.gnome.org.][irc] or [matrix][matrix].
|
||||
|
||||
Note:
|
||||
|
||||
@@ -53,22 +47,25 @@ 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 --user --repo=repo hammond org.gnome.Hammond.json --force-clean
|
||||
flatpak build-bundle repo hammond org.gnome.Hammond
|
||||
```
|
||||
|
||||
To install the resulting flatpak you can do:
|
||||
|
||||
```bash
|
||||
flatpak build-bundle repo hammond.flatpak org.gnome.Hammond
|
||||
flatpak install --user --bundle hammond.flatpak
|
||||
```
|
||||
|
||||
### Building from soure
|
||||
|
||||
```sh
|
||||
git clone https://gitlab.gnome.org/alatiera/hammond.git
|
||||
git clone https://gitlab.gnome.org/World/hammond.git
|
||||
cd hammond/
|
||||
./configure --prefix=/usr/local
|
||||
make && sudo make install
|
||||
meson --prefix=/usr build
|
||||
ninja -C build
|
||||
sudo ninja -C build install
|
||||
```
|
||||
|
||||
**Additional:**
|
||||
|
||||
You can run `sudo make uninstall` for removal
|
||||
|
||||
#### Dependencies
|
||||
|
||||
* Rust stable 1.22 or later along with cargo.
|
||||
@@ -101,11 +98,11 @@ There alot of thins yet to be done.
|
||||
|
||||
If you want to contribute, please check the [Contributions Guidelines][contribution-guidelines].
|
||||
|
||||
You can start by taking a look at [Issues](https://gitlab.gnome.org/alatiera/Hammond/issues) or by opening a [New issue](https://gitlab.gnome.org/alatiera/Hammond/issues/new?issue%5Bassignee_id%5D=&issue%5Bmilestone_id%5D=).
|
||||
You can start by taking a look at [Issues](https://gitlab.gnome.org/World/hammond/issues) or by opening a [New issue](https://gitlab.gnome.org/World/hammond/issues/new?issue%5Bassignee_id%5D=&issue%5Bmilestone_id%5D=).
|
||||
|
||||
There are also some minor tasks tagged with `TODO:` and `FIXME:` in the source code.
|
||||
|
||||
[contribution-guidelines]: https://gitlab.gnome.org/alatiera/Hammond/blob/master/CONTRIBUTING.md
|
||||
[contribution-guidelines]: https://gitlab.gnome.org/World/hammond/blob/master/CONTRIBUTING.md
|
||||
|
||||
|
||||
## Overview
|
||||
@@ -125,7 +122,7 @@ $ tree -d
|
||||
│ ├── resources # GResources folder
|
||||
│ │ └── gtk # Contains the glade.ui files.
|
||||
│ └── src
|
||||
│ ├── views # Contains the Empty, Episodes and Shows view.
|
||||
│ ├── stacks # Contains the gtk Stacks that hold all the different views.
|
||||
│ └── widgets # Contains custom widgets such as Show and Episode.
|
||||
```
|
||||
|
||||
@@ -143,4 +140,6 @@ We also copied some elements from [GNOME News](https://wiki.gnome.org/Design/App
|
||||
|
||||
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
|
||||
[vendor]: https://github.com/alexcrichton/cargo-vendor
|
||||
[irc]: irc://irc.gnome.org/#hammond
|
||||
[matrix]: https://matrix.to/#/#hammond:matrix.org
|
||||
@@ -1,186 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Adapted from:
|
||||
# https://gitlab.gnome.org/danigm/libgepub/blob/27f0d374e0c8f6fa972dbd111d4ce0c0f3096914/configure_meson
|
||||
|
||||
# configure script adapter for Meson
|
||||
# Based on build-api: https://github.com/cgwalters/build-api
|
||||
# Copyright 2010, 2011, 2013 Colin Walters <walters@verbum.org>
|
||||
# Copyright 2016, 2017 Emmanuele Bassi
|
||||
# Copyright 2017 Iñigo Martínez <inigomartinez@gmail.com>
|
||||
# Licensed under the new-BSD license (http://www.opensource.org/licenses/bsd-license.php)
|
||||
|
||||
# Build API variables:
|
||||
|
||||
# Little helper function for reading args from the commandline.
|
||||
# it automatically handles -a b and -a=b variants, and returns 1 if
|
||||
# we need to shift $3.
|
||||
read_arg() {
|
||||
# $1 = arg name
|
||||
# $2 = arg value
|
||||
# $3 = arg parameter
|
||||
local rematch='^[^=]*=(.*)$'
|
||||
if [[ $2 =~ $rematch ]]; then
|
||||
read "$1" <<< "${BASH_REMATCH[1]}"
|
||||
else
|
||||
read "$1" <<< "$3"
|
||||
# There is no way to shift our callers args, so
|
||||
# return 1 to indicate they should do it instead.
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
sanitycheck() {
|
||||
# $1 = arg name
|
||||
# $1 = arg command
|
||||
# $2 = arg alternates
|
||||
local cmd=$( which $2 2>/dev/null )
|
||||
|
||||
if [ -x "$cmd" ]; then
|
||||
read "$1" <<< "$cmd"
|
||||
return 0
|
||||
fi
|
||||
|
||||
test -z $3 || {
|
||||
for alt in $3; do
|
||||
cmd=$( which $alt 2>/dev/null )
|
||||
|
||||
if [ -x "$cmd" ]; then
|
||||
read "$1" <<< "$cmd"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
echo -e "\e[1;31mERROR\e[0m: Command '$2' not found"
|
||||
exit 1
|
||||
}
|
||||
|
||||
checkoption() {
|
||||
# $1 = arg
|
||||
option="${1#*--}"
|
||||
action="${option%%-*}"
|
||||
name="${option#*-}"
|
||||
if [ ${default_options[$name]+_} ]; then
|
||||
case "$action" in
|
||||
enable) meson_options[$name]=true;;
|
||||
disable) meson_options[$name]=false;;
|
||||
*) echo -e "\e[1;33mINFO\e[0m: Ignoring unknown action '$action'";;
|
||||
esac
|
||||
else
|
||||
echo -e "\e[1;33mINFO\e[0m: Ignoring unknown option '$option'"
|
||||
fi
|
||||
}
|
||||
|
||||
echooption() {
|
||||
# $1 = option
|
||||
if [ ${meson_options[$1]+_} ]; then
|
||||
echo ${meson_options[$1]}
|
||||
elif [ ${default_options[$1]+_} ]; then
|
||||
echo ${default_options[$1]}
|
||||
fi
|
||||
}
|
||||
|
||||
sanitycheck MESON 'meson'
|
||||
sanitycheck MESONTEST 'mesontest'
|
||||
sanitycheck NINJA 'ninja' 'ninja-build'
|
||||
|
||||
declare -A meson_options
|
||||
|
||||
while (($# > 0)); do
|
||||
case "${1%%=*}" in
|
||||
--prefix) read_arg prefix "$@" || shift;;
|
||||
--bindir) read_arg bindir "$@" || shift;;
|
||||
--sbindir) read_arg sbindir "$@" || shift;;
|
||||
--libexecdir) read_arg libexecdir "$@" || shift;;
|
||||
--datarootdir) read_arg datarootdir "$@" || shift;;
|
||||
--datadir) read_arg datadir "$@" || shift;;
|
||||
--sysconfdir) read_arg sysconfdir "$@" || shift;;
|
||||
--libdir) read_arg libdir "$@" || shift;;
|
||||
--mandir) read_arg mandir "$@" || shift;;
|
||||
--includedir) read_arg includedir "$@" || shift;;
|
||||
*) checkoption $1;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# Defaults
|
||||
test -z ${prefix} && prefix="/usr/local"
|
||||
test -z ${bindir} && bindir=${prefix}/bin
|
||||
test -z ${sbindir} && sbindir=${prefix}/sbin
|
||||
test -z ${libexecdir} && libexecdir=${prefix}/bin
|
||||
test -z ${datarootdir} && datarootdir=${prefix}/share
|
||||
test -z ${datadir} && datadir=${datarootdir}
|
||||
test -z ${sysconfdir} && sysconfdir=${prefix}/etc
|
||||
test -z ${libdir} && libdir=${prefix}/lib
|
||||
test -z ${mandir} && mandir=${prefix}/share/man
|
||||
test -z ${includedir} && includedir=${prefix}/include
|
||||
|
||||
# The source directory is the location of this file
|
||||
srcdir=$(dirname $0)
|
||||
|
||||
# The build directory is the current location
|
||||
builddir=`pwd`
|
||||
|
||||
# If we're calling this file from the source directory then
|
||||
# we automatically create a build directory and ensure that
|
||||
# both Meson and Ninja invocations are relative to that
|
||||
# location
|
||||
if [[ -f "${builddir}/meson.build" ]]; then
|
||||
mkdir -p _build
|
||||
builddir="${builddir}/_build"
|
||||
NINJA_OPT="-C ${builddir}"
|
||||
fi
|
||||
|
||||
# Wrapper Makefile for Ninja
|
||||
cat > Makefile <<END
|
||||
# Generated by configure; do not edit
|
||||
|
||||
all: rebuild
|
||||
${NINJA} ${NINJA_OPT}
|
||||
|
||||
rebuild:
|
||||
rm -f ${builddir}/hammond
|
||||
|
||||
install:
|
||||
DESTDIR="\$(DESTDIR)" ${NINJA} ${NINJA_OPT} install
|
||||
|
||||
uninstall:
|
||||
${NINJA} ${NINJA_OPT} uninstall
|
||||
|
||||
release:
|
||||
${NINJA} ${NINJA_OPT} release
|
||||
|
||||
check:
|
||||
${MESONTEST} ${NINJA_OPT}
|
||||
END
|
||||
|
||||
echo "
|
||||
|
||||
hammond
|
||||
=======
|
||||
|
||||
meson: ${MESON}
|
||||
ninja: ${NINJA}
|
||||
prefix: ${prefix}
|
||||
|
||||
Now type 'make' to build
|
||||
"
|
||||
|
||||
cmd_options=""
|
||||
for key in "${!meson_options[@]}"; do
|
||||
cmd_options="$cmd_options -Denable-$key=${meson_options[$key]}"
|
||||
done
|
||||
|
||||
exec ${MESON} \
|
||||
--prefix=${prefix} \
|
||||
--libdir=${libdir} \
|
||||
--libexecdir=${libexecdir} \
|
||||
--datadir=${datadir} \
|
||||
--sysconfdir=${sysconfdir} \
|
||||
--bindir=${bindir} \
|
||||
--includedir=${includedir} \
|
||||
--mandir=${mandir} \
|
||||
${cmd_options} \
|
||||
${builddir} \
|
||||
${srcdir}
|
||||
+13
-8
@@ -6,37 +6,42 @@ workspace = "../"
|
||||
|
||||
[dependencies]
|
||||
ammonia = "1.1.0"
|
||||
chrono = "0.4.1"
|
||||
chrono = "0.4.2"
|
||||
derive_builder = "0.5.1"
|
||||
itertools = "0.7.8"
|
||||
lazy_static = "1.0.0"
|
||||
log = "0.4.1"
|
||||
rayon = "1.0.1"
|
||||
rayon-futures = "0.1.0"
|
||||
rfc822_sanitizer = "0.3.3"
|
||||
rss = "1.4.0"
|
||||
rss = "1.5.0"
|
||||
url = "1.7.0"
|
||||
xdg = "2.1.0"
|
||||
futures = "0.1.21"
|
||||
hyper = "0.11.24"
|
||||
tokio-core = "0.1.16"
|
||||
hyper = "0.11.25"
|
||||
tokio-core = "0.1.17"
|
||||
hyper-tls = "0.1.3"
|
||||
native-tls = "0.1.5"
|
||||
num_cpus = "1.8.0"
|
||||
failure = "0.1.1"
|
||||
failure_derive = "0.1.1"
|
||||
|
||||
# newer stuff do not compile inside flatpak
|
||||
html5ever = "= 0.22.0"
|
||||
string_cache = "= 0.7.1"
|
||||
string_cache_codegen = "= 0.4.0"
|
||||
|
||||
[dependencies.diesel]
|
||||
features = ["sqlite", "r2d2"]
|
||||
version = "1.1.1"
|
||||
version = "1.2.2"
|
||||
|
||||
[dependencies.diesel_migrations]
|
||||
features = ["sqlite"]
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.4.2"
|
||||
tempdir = "0.3.7"
|
||||
criterion = "0.2.2"
|
||||
criterion = "0.2.3"
|
||||
pretty_assertions = "0.5.1"
|
||||
|
||||
[[bench]]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(unused)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate criterion;
|
||||
use criterion::Criterion;
|
||||
@@ -16,10 +18,10 @@ extern crate rss;
|
||||
// use futures::future::*;
|
||||
use tokio_core::reactor::Core;
|
||||
|
||||
use hammond_data::FeedBuilder;
|
||||
use hammond_data::Source;
|
||||
use hammond_data::database::truncate_db;
|
||||
use hammond_data::pipeline;
|
||||
use hammond_data::FeedBuilder;
|
||||
use hammond_data::Source;
|
||||
// use hammond_data::errors::*;
|
||||
|
||||
use std::io::BufReader;
|
||||
@@ -56,22 +58,6 @@ static FEEDS: &[(&[u8], &str)] = &[
|
||||
(STARS, STARS_URL),
|
||||
];
|
||||
|
||||
// This is broken and I don't know why.
|
||||
fn bench_pipeline(c: &mut Criterion) {
|
||||
truncate_db().unwrap();
|
||||
FEEDS.iter().for_each(|&(_, url)| {
|
||||
Source::from_url(url).unwrap();
|
||||
});
|
||||
|
||||
c.bench_function("pipline", move |b| {
|
||||
b.iter(|| {
|
||||
let sources = hammond_data::dbqueries::get_sources().unwrap();
|
||||
pipeline::run(sources, true).unwrap();
|
||||
})
|
||||
});
|
||||
truncate_db().unwrap();
|
||||
}
|
||||
|
||||
fn bench_index_large_feed(c: &mut Criterion) {
|
||||
truncate_db().unwrap();
|
||||
let url = "https://www.greaterthancode.com/feed/podcast";
|
||||
@@ -114,10 +100,5 @@ fn bench_index_small_feed(c: &mut Criterion) {
|
||||
truncate_db().unwrap();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_pipeline,
|
||||
bench_index_large_feed,
|
||||
bench_index_small_feed
|
||||
);
|
||||
criterion_group!(benches, bench_index_large_feed, bench_index_small_feed);
|
||||
criterion_main!(benches);
|
||||
|
||||
@@ -5,6 +5,7 @@ use diesel::prelude::*;
|
||||
|
||||
use diesel;
|
||||
use diesel::dsl::exists;
|
||||
use diesel::query_builder::AsQuery;
|
||||
use diesel::select;
|
||||
|
||||
use database::connection;
|
||||
@@ -40,7 +41,7 @@ pub fn get_podcasts_filter(filter_ids: &[i32]) -> Result<Vec<Podcast>, DataError
|
||||
|
||||
podcast
|
||||
.order(title.asc())
|
||||
.filter(id.ne_any(filter_ids))
|
||||
.filter(id.ne_all(filter_ids))
|
||||
.load::<Podcast>(&con)
|
||||
.map_err(From::from)
|
||||
}
|
||||
@@ -102,6 +103,20 @@ pub fn get_episode_from_rowid(ep_id: i32) -> Result<Episode, DataError> {
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
pub fn get_episode_widget_from_rowid(ep_id: i32) -> Result<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(rowid.eq(ep_id))
|
||||
.get_result::<EpisodeWidgetQuery>(&con)
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
pub fn get_episode_local_uri_from_id(ep_id: i32) -> Result<Option<String>, DataError> {
|
||||
use schema::episode::dsl::*;
|
||||
let db = connection();
|
||||
@@ -118,24 +133,17 @@ pub fn get_episodes_widgets_filter_limit(
|
||||
filter_ids: &[i32],
|
||||
limit: u32,
|
||||
) -> Result<Vec<EpisodeWidgetQuery>, DataError> {
|
||||
use schema::episode;
|
||||
use schema::episode::dsl::*;
|
||||
let db = connection();
|
||||
let con = db.get()?;
|
||||
let columns = (
|
||||
rowid, title, uri, local_uri, epoch, length, duration, played, podcast_id,
|
||||
);
|
||||
|
||||
episode::table
|
||||
.select((
|
||||
episode::rowid,
|
||||
episode::title,
|
||||
episode::uri,
|
||||
episode::local_uri,
|
||||
episode::epoch,
|
||||
episode::length,
|
||||
episode::duration,
|
||||
episode::played,
|
||||
episode::podcast_id,
|
||||
))
|
||||
.order(episode::epoch.desc())
|
||||
.filter(episode::podcast_id.ne_any(filter_ids))
|
||||
episode
|
||||
.select(columns)
|
||||
.order(epoch.desc())
|
||||
.filter(podcast_id.ne_all(filter_ids))
|
||||
.limit(i64::from(limit))
|
||||
.load::<EpisodeWidgetQuery>(&con)
|
||||
.map_err(From::from)
|
||||
@@ -175,14 +183,27 @@ pub fn get_pd_episodes(parent: &Podcast) -> Result<Vec<Episode>, DataError> {
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
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()?;
|
||||
let columns = (
|
||||
rowid, title, uri, local_uri, epoch, length, duration, played, podcast_id,
|
||||
);
|
||||
|
||||
episode.select((rowid, title, uri, local_uri, epoch, length, duration, played, podcast_id))
|
||||
episode
|
||||
.select(columns)
|
||||
.filter(podcast_id.eq(parent.id()))
|
||||
// .group_by(epoch)
|
||||
.order(epoch.desc())
|
||||
.load::<EpisodeWidgetQuery>(&con)
|
||||
.map_err(From::from)
|
||||
@@ -339,6 +360,34 @@ pub(crate) fn episode_exists(title_: &str, podcast_id_: i32) -> Result<bool, Dat
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
/// Check if the `episode table contains any rows
|
||||
///
|
||||
/// Return true if `episode` table is populated.
|
||||
pub fn is_episodes_populated() -> Result<bool, DataError> {
|
||||
use schema::episode::dsl::*;
|
||||
|
||||
let db = connection();
|
||||
let con = db.get()?;
|
||||
|
||||
select(exists(episode.as_query()))
|
||||
.get_result(&con)
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
/// Check if the `podcast` table contains any rows
|
||||
///
|
||||
/// Return true if `podcast table is populated.
|
||||
pub fn is_podcasts_populated(filter_ids: &[i32]) -> Result<bool, DataError> {
|
||||
use schema::podcast::dsl::*;
|
||||
|
||||
let db = connection();
|
||||
let con = db.get()?;
|
||||
|
||||
select(exists(podcast.filter(id.ne_all(filter_ids))))
|
||||
.get_result(&con)
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
pub(crate) fn index_new_episodes(eps: &[NewEpisode]) -> Result<(), DataError> {
|
||||
use schema::episode::dsl::*;
|
||||
let db = connection();
|
||||
@@ -369,7 +418,7 @@ pub fn update_none_to_played_now(parent: &Podcast) -> Result<usize, DataError> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use database::*;
|
||||
use pipeline::*;
|
||||
use pipeline;
|
||||
|
||||
#[test]
|
||||
fn test_update_none_to_played_now() {
|
||||
@@ -379,7 +428,7 @@ mod tests {
|
||||
com/InterceptedWithJeremyScahill";
|
||||
let source = Source::from_url(url).unwrap();
|
||||
let id = source.id();
|
||||
index_single_source(source, true).unwrap();
|
||||
pipeline::run(vec![source], true).unwrap();
|
||||
let pd = get_podcast_from_source_id(id).unwrap();
|
||||
|
||||
let eps_num = get_pd_unplayed_episodes(&pd).unwrap().len();
|
||||
|
||||
@@ -8,6 +8,26 @@ use url;
|
||||
|
||||
use std::io;
|
||||
|
||||
use models::Source;
|
||||
|
||||
#[fail(display = "Request to {} returned {}. Context: {}", url, status_code, context)]
|
||||
#[derive(Fail, Debug)]
|
||||
pub struct HttpStatusError {
|
||||
url: String,
|
||||
status_code: hyper::StatusCode,
|
||||
context: String,
|
||||
}
|
||||
|
||||
impl HttpStatusError {
|
||||
pub fn new(url: String, code: hyper::StatusCode, context: String) -> Self {
|
||||
HttpStatusError {
|
||||
url,
|
||||
status_code: code,
|
||||
context,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Fail, Debug)]
|
||||
pub enum DataError {
|
||||
#[fail(display = "SQL Query failed: {}", _0)]
|
||||
@@ -31,16 +51,12 @@ pub enum DataError {
|
||||
RssError(#[cause] rss::Error),
|
||||
#[fail(display = "Error: {}", _0)]
|
||||
Bail(String),
|
||||
#[fail(display = "Request to {} returned {}. Context: {}", url, status_code, context)]
|
||||
HttpStatusError {
|
||||
url: String,
|
||||
status_code: hyper::StatusCode,
|
||||
context: String,
|
||||
},
|
||||
#[fail(display = "{}", _0)]
|
||||
HttpStatusGeneral(HttpStatusError),
|
||||
#[fail(display = "FIXME: This should be better")]
|
||||
F301(Source),
|
||||
#[fail(display = "Error occured while Parsing an Episode. Reason: {}", reason)]
|
||||
ParseEpisodeError { reason: String, parent_id: i32 },
|
||||
#[fail(display = "No Futures where produced to be run.")]
|
||||
EmptyFuturesList,
|
||||
#[fail(display = "Episode was not changed and thus skipped.")]
|
||||
EpisodeNotChanged,
|
||||
}
|
||||
|
||||
+90
-94
@@ -1,16 +1,15 @@
|
||||
#![cfg_attr(feature = "cargo-clippy", allow(unit_arg))]
|
||||
//! Index Feeds.
|
||||
|
||||
use futures::future::*;
|
||||
use itertools::{Either, Itertools};
|
||||
use futures::prelude::*;
|
||||
use futures::stream;
|
||||
use rss;
|
||||
|
||||
use dbqueries;
|
||||
use errors::DataError;
|
||||
use models::{Index, IndexState, Update};
|
||||
use models::{NewEpisode, NewPodcast, Podcast};
|
||||
use pipeline::*;
|
||||
|
||||
type InsertUpdate = (Vec<NewEpisode>, Vec<Option<(NewEpisode, i32)>>);
|
||||
use models::{NewEpisode, NewEpisodeMinimal, NewPodcast, Podcast};
|
||||
|
||||
/// Wrapper struct that hold a `Source` id and the `rss::Channel`
|
||||
/// that corresponds to the `Source.uri` field.
|
||||
@@ -29,7 +28,7 @@ impl Feed {
|
||||
pub fn index(self) -> Box<Future<Item = (), Error = DataError> + Send> {
|
||||
let fut = self.parse_podcast_async()
|
||||
.and_then(|pd| pd.to_podcast())
|
||||
.and_then(move |pd| self.index_channel_items(&pd));
|
||||
.and_then(move |pd| self.index_channel_items(pd));
|
||||
|
||||
Box::new(fut)
|
||||
}
|
||||
@@ -42,85 +41,105 @@ impl Feed {
|
||||
Box::new(ok(self.parse_podcast()))
|
||||
}
|
||||
|
||||
fn index_channel_items(
|
||||
&self,
|
||||
pd: &Podcast,
|
||||
) -> Box<Future<Item = (), Error = DataError> + Send> {
|
||||
let fut = self.get_stuff(pd)
|
||||
.and_then(|(insert, update)| {
|
||||
if !insert.is_empty() {
|
||||
info!("Indexing {} episodes.", insert.len());
|
||||
if let Err(err) = dbqueries::index_new_episodes(insert.as_slice()) {
|
||||
error!("Failed batch indexng, Fallign back to individual indexing.");
|
||||
error!("{}", err);
|
||||
insert.iter().for_each(|ep| {
|
||||
if let Err(err) = ep.index() {
|
||||
error!("Failed to index episode: {:?}.", ep.title());
|
||||
error!("{}", err);
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
Ok((insert, update))
|
||||
})
|
||||
.map(|(_, update)| {
|
||||
if !update.is_empty() {
|
||||
info!("Updating {} episodes.", update.len());
|
||||
// see get_stuff for more
|
||||
update
|
||||
.into_iter()
|
||||
.filter_map(|x| x)
|
||||
.for_each(|(ref ep, rowid)| {
|
||||
if let Err(err) = ep.update(rowid) {
|
||||
error!("Failed to index episode: {:?}.", ep.title());
|
||||
error!("{}", err);
|
||||
};
|
||||
})
|
||||
}
|
||||
});
|
||||
fn index_channel_items(self, pd: Podcast) -> Box<Future<Item = (), Error = DataError> + Send> {
|
||||
let stream = stream::iter_ok::<_, DataError>(self.channel.into_items());
|
||||
|
||||
Box::new(fut)
|
||||
// Parse the episodes
|
||||
let episodes = stream.filter_map(move |item| {
|
||||
glue(&item, pd.id())
|
||||
.map_err(|err| error!("Failed to parse an episode: {}", err))
|
||||
.ok()
|
||||
});
|
||||
|
||||
// Filter errors, Index updatable episodes, return insertables.
|
||||
let insertables = filter_episodes(episodes);
|
||||
// Batch index insertable episodes.
|
||||
let idx = insertables.and_then(|eps| ok(batch_insert_episodes(&eps)));
|
||||
|
||||
Box::new(idx)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_stuff(
|
||||
&self,
|
||||
pd: &Podcast,
|
||||
) -> Box<Future<Item = InsertUpdate, Error = DataError> + Send> {
|
||||
let (insert, update): (Vec<_>, Vec<_>) = self.channel
|
||||
.items()
|
||||
.into_iter()
|
||||
.map(|item| glue_async(item, pd.id()))
|
||||
// This is sort of ugly but I think it's cheaper than pushing None
|
||||
// to updated and filtering it out later.
|
||||
// Even though we already map_filter in index_channel_items.
|
||||
// I am not sure what the optimizations are on match vs allocating None.
|
||||
.map(|fut| {
|
||||
fut.and_then(|x| match x {
|
||||
IndexState::NotChanged => Err(DataError::EpisodeNotChanged),
|
||||
_ => Ok(x),
|
||||
})
|
||||
})
|
||||
.flat_map(|fut| fut.wait())
|
||||
.partition_map(|state| match state {
|
||||
IndexState::Index(e) => Either::Left(e),
|
||||
IndexState::Update(e) => Either::Right(Some(e)),
|
||||
// This should never occur
|
||||
IndexState::NotChanged => Either::Right(None),
|
||||
});
|
||||
fn glue(item: &rss::Item, id: i32) -> Result<IndexState<NewEpisode>, DataError> {
|
||||
NewEpisodeMinimal::new(item, id).and_then(move |ep| determine_ep_state(ep, item))
|
||||
}
|
||||
|
||||
Box::new(ok((insert, update)))
|
||||
fn determine_ep_state(
|
||||
ep: NewEpisodeMinimal,
|
||||
item: &rss::Item,
|
||||
) -> Result<IndexState<NewEpisode>, DataError> {
|
||||
// Check if feed exists
|
||||
let exists = dbqueries::episode_exists(ep.title(), ep.podcast_id())?;
|
||||
|
||||
if !exists {
|
||||
Ok(IndexState::Index(ep.into_new_episode(item)))
|
||||
} else {
|
||||
let old = dbqueries::get_episode_minimal_from_pk(ep.title(), ep.podcast_id())?;
|
||||
let rowid = old.rowid();
|
||||
|
||||
if ep != old {
|
||||
Ok(IndexState::Update((ep.into_new_episode(item), rowid)))
|
||||
} else {
|
||||
Ok(IndexState::NotChanged)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn filter_episodes<'a, S>(
|
||||
stream: S,
|
||||
) -> Box<Future<Item = Vec<NewEpisode>, Error = DataError> + Send + 'a>
|
||||
where
|
||||
S: Stream<Item = IndexState<NewEpisode>, Error = DataError> + Send + 'a,
|
||||
{
|
||||
let list = stream.filter_map(|state| match state {
|
||||
IndexState::NotChanged => None,
|
||||
// Update individual rows, and filter them
|
||||
IndexState::Update((ref ep, rowid)) => {
|
||||
ep.update(rowid)
|
||||
.map_err(|err| error!("{}", err))
|
||||
.map_err(|_| error!("Failed to index episode: {:?}.", ep.title()))
|
||||
.ok();
|
||||
|
||||
None
|
||||
},
|
||||
IndexState::Index(s) => Some(s),
|
||||
})
|
||||
// only Index is left, collect them for batch index
|
||||
.collect();
|
||||
|
||||
Box::new(list)
|
||||
}
|
||||
|
||||
fn batch_insert_episodes(episodes: &[NewEpisode]) {
|
||||
if episodes.is_empty() {
|
||||
return;
|
||||
};
|
||||
|
||||
info!("Indexing {} episodes.", episodes.len());
|
||||
dbqueries::index_new_episodes(episodes)
|
||||
.map_err(|err| {
|
||||
error!("Failed batch indexng: {}", err);
|
||||
info!("Fallign back to individual indexing.");
|
||||
})
|
||||
.unwrap_or_else(|_| {
|
||||
episodes.iter().for_each(|ep| {
|
||||
ep.index()
|
||||
.map_err(|err| error!("Error: {}.", err))
|
||||
.map_err(|_| error!("Failed to index episode: {:?}.", ep.title()))
|
||||
.ok();
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rss::Channel;
|
||||
use tokio_core::reactor::Core;
|
||||
|
||||
use Source;
|
||||
use database::truncate_db;
|
||||
use dbqueries;
|
||||
use utils::get_feed;
|
||||
use Source;
|
||||
|
||||
use std::fs;
|
||||
use std::io::BufReader;
|
||||
@@ -202,31 +221,8 @@ mod tests {
|
||||
let feed = get_feed(path, 42);
|
||||
let pd = feed.parse_podcast().to_podcast().unwrap();
|
||||
|
||||
feed.index_channel_items(&pd).wait().unwrap();
|
||||
feed.index_channel_items(pd).wait().unwrap();
|
||||
assert_eq!(dbqueries::get_podcasts().unwrap().len(), 1);
|
||||
assert_eq!(dbqueries::get_episodes().unwrap().len(), 43);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_feed_get_stuff() {
|
||||
truncate_db().unwrap();
|
||||
|
||||
let path = "tests/feeds/2018-01-20-Intercepted.xml";
|
||||
let feed = get_feed(path, 42);
|
||||
let pd = feed.parse_podcast().to_podcast().unwrap();
|
||||
|
||||
let (insert, update) = feed.get_stuff(&pd).wait().unwrap();
|
||||
assert_eq!(43, insert.len());
|
||||
assert_eq!(0, update.len());
|
||||
|
||||
feed.index().wait().unwrap();
|
||||
|
||||
let path = "tests/feeds/2018-02-03-Intercepted.xml";
|
||||
let feed = get_feed(path, 42);
|
||||
let pd = feed.parse_podcast().to_podcast().unwrap();
|
||||
|
||||
let (insert, update) = feed.get_stuff(&pd).wait().unwrap();
|
||||
assert_eq!(4, insert.len());
|
||||
assert_eq!(43, update.len());
|
||||
}
|
||||
}
|
||||
|
||||
+31
-22
@@ -1,22 +1,27 @@
|
||||
#![recursion_limit = "1024"]
|
||||
#![cfg_attr(all(test, feature = "clippy"), allow(option_unwrap_used, result_unwrap_used))]
|
||||
#![cfg_attr(feature = "cargo-clippy", allow(blacklisted_name))]
|
||||
#![cfg_attr(feature = "clippy",
|
||||
warn(option_unwrap_used, result_unwrap_used, print_stdout,
|
||||
wrong_pub_self_convention, mut_mut, non_ascii_literal, similar_names,
|
||||
unicode_not_nfc, enum_glob_use, if_not_else, items_after_statements,
|
||||
used_underscore_binding))]
|
||||
#![cfg_attr(feature = "cargo-clippy", allow(option_map_unit_fn))]
|
||||
#![cfg_attr(
|
||||
feature = "clippy",
|
||||
warn(
|
||||
option_unwrap_used, result_unwrap_used, print_stdout, wrong_pub_self_convention, mut_mut,
|
||||
non_ascii_literal, similar_names, unicode_not_nfc, enum_glob_use, if_not_else,
|
||||
items_after_statements, used_underscore_binding
|
||||
)
|
||||
)]
|
||||
#![allow(unknown_lints)]
|
||||
#![deny(bad_style, const_err, dead_code, improper_ctypes, legacy_directory_ownership,
|
||||
non_shorthand_field_patterns, no_mangle_generic_items, overflowing_literals,
|
||||
path_statements, patterns_in_fns_without_body, plugin_as_library, private_in_public,
|
||||
private_no_mangle_fns, private_no_mangle_statics, safe_extern_statics,
|
||||
unconditional_recursion, unions_with_drop_fields, unused_allocation, unused_comparisons,
|
||||
unused_parens, while_true)]
|
||||
#![deny(missing_debug_implementations, missing_docs, trivial_casts, trivial_numeric_casts)]
|
||||
#![deny(unused_extern_crates, unused)]
|
||||
|
||||
// #![feature(conservative_impl_trait)]
|
||||
#![warn(
|
||||
bad_style, const_err, dead_code, improper_ctypes, legacy_directory_ownership,
|
||||
non_shorthand_field_patterns, no_mangle_generic_items, overflowing_literals, path_statements,
|
||||
patterns_in_fns_without_body, plugin_as_library, private_in_public, private_no_mangle_fns,
|
||||
private_no_mangle_statics, safe_extern_statics, unconditional_recursion,
|
||||
unions_with_drop_fields, unused_allocation, unused_comparisons, unused_parens, while_true
|
||||
)]
|
||||
#![warn(
|
||||
missing_debug_implementations, missing_docs, trivial_casts, trivial_numeric_casts,
|
||||
unused_extern_crates, unused
|
||||
)]
|
||||
#![deny(warnings)]
|
||||
|
||||
//! FIXME: Docs
|
||||
|
||||
@@ -44,31 +49,35 @@ extern crate chrono;
|
||||
extern crate futures;
|
||||
extern crate hyper;
|
||||
extern crate hyper_tls;
|
||||
extern crate itertools;
|
||||
extern crate native_tls;
|
||||
extern crate num_cpus;
|
||||
extern crate rayon;
|
||||
extern crate rayon_futures;
|
||||
extern crate rfc822_sanitizer;
|
||||
extern crate rss;
|
||||
extern crate tokio_core;
|
||||
extern crate url;
|
||||
extern crate xdg;
|
||||
|
||||
pub mod database;
|
||||
#[allow(missing_docs)]
|
||||
pub mod dbqueries;
|
||||
#[allow(missing_docs)]
|
||||
pub mod errors;
|
||||
pub mod utils;
|
||||
pub mod database;
|
||||
pub mod pipeline;
|
||||
pub(crate) mod models;
|
||||
mod feed;
|
||||
pub(crate) mod models;
|
||||
mod parser;
|
||||
pub mod pipeline;
|
||||
mod schema;
|
||||
pub mod utils;
|
||||
|
||||
pub use feed::{Feed, FeedBuilder};
|
||||
pub use models::{Episode, EpisodeWidgetQuery, Podcast, PodcastCoverQuery, Source};
|
||||
pub use models::Save;
|
||||
pub use models::{Episode, EpisodeWidgetQuery, Podcast, PodcastCoverQuery, Source};
|
||||
|
||||
// Set the user agent, See #53 for more
|
||||
// Keep this in sync with Tor-browser releases
|
||||
const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 6.1; rv:52.0) Gecko/20100101 Firefox/52.0";
|
||||
|
||||
/// [XDG Base Direcotory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) Paths.
|
||||
#[allow(missing_debug_implementations)]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use chrono::prelude::*;
|
||||
use diesel;
|
||||
use diesel::SaveChangesDsl;
|
||||
use diesel::prelude::*;
|
||||
use diesel::SaveChangesDsl;
|
||||
|
||||
use database::connection;
|
||||
use errors::DataError;
|
||||
@@ -31,10 +31,12 @@ pub struct Episode {
|
||||
podcast_id: i32,
|
||||
}
|
||||
|
||||
impl Save<Episode, DataError> for Episode {
|
||||
impl Save<Episode> for Episode {
|
||||
type Error = DataError;
|
||||
|
||||
/// Helper method to easily save/"sync" current state of self to the
|
||||
/// Database.
|
||||
fn save(&self) -> Result<Episode, DataError> {
|
||||
fn save(&self) -> Result<Episode, Self::Error> {
|
||||
let db = connection();
|
||||
let tempdb = db.get()?;
|
||||
|
||||
@@ -224,10 +226,12 @@ impl From<Episode> for EpisodeWidgetQuery {
|
||||
}
|
||||
}
|
||||
|
||||
impl Save<usize, DataError> for EpisodeWidgetQuery {
|
||||
impl Save<usize> for EpisodeWidgetQuery {
|
||||
type Error = DataError;
|
||||
|
||||
/// Helper method to easily save/"sync" current state of self to the
|
||||
/// Database.
|
||||
fn save(&self) -> Result<usize, DataError> {
|
||||
fn save(&self) -> Result<usize, Self::Error> {
|
||||
use schema::episode::dsl::*;
|
||||
|
||||
let db = connection();
|
||||
@@ -363,10 +367,12 @@ pub struct EpisodeCleanerQuery {
|
||||
played: Option<i32>,
|
||||
}
|
||||
|
||||
impl Save<usize, DataError> for EpisodeCleanerQuery {
|
||||
impl Save<usize> for EpisodeCleanerQuery {
|
||||
type Error = DataError;
|
||||
|
||||
/// Helper method to easily save/"sync" current state of self to the
|
||||
/// Database.
|
||||
fn save(&self) -> Result<usize, DataError> {
|
||||
fn save(&self) -> Result<usize, Self::Error> {
|
||||
use schema::episode::dsl::*;
|
||||
|
||||
let db = connection();
|
||||
|
||||
@@ -30,22 +30,30 @@ pub enum IndexState<T> {
|
||||
NotChanged,
|
||||
}
|
||||
|
||||
pub trait Insert<T, E> {
|
||||
fn insert(&self) -> Result<T, E>;
|
||||
pub trait Insert<T> {
|
||||
type Error;
|
||||
|
||||
fn insert(&self) -> Result<T, Self::Error>;
|
||||
}
|
||||
|
||||
pub trait Update<T, E> {
|
||||
fn update(&self, i32) -> Result<T, E>;
|
||||
pub trait Update<T> {
|
||||
type Error;
|
||||
|
||||
fn update(&self, i32) -> Result<T, Self::Error>;
|
||||
}
|
||||
|
||||
// This might need to change in the future
|
||||
pub trait Index<T, E>: Insert<T, E> + Update<T, E> {
|
||||
fn index(&self) -> Result<T, E>;
|
||||
pub trait Index<T>: Insert<T> + Update<T> {
|
||||
type Error;
|
||||
|
||||
fn index(&self) -> Result<T, <Self as Index<T>>::Error>;
|
||||
}
|
||||
|
||||
/// FIXME: DOCS
|
||||
pub trait Save<T, E> {
|
||||
pub trait Save<T> {
|
||||
/// The Error type to be returned.
|
||||
type Error;
|
||||
/// Helper method to easily save/"sync" current state of a diesel model to
|
||||
/// the Database.
|
||||
fn save(&self) -> Result<T, E>;
|
||||
fn save(&self) -> Result<T, Self::Error>;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,9 @@ impl From<NewEpisodeMinimal> for NewEpisode {
|
||||
}
|
||||
}
|
||||
|
||||
impl Insert<(), DataError> for NewEpisode {
|
||||
impl Insert<()> for NewEpisode {
|
||||
type Error = DataError;
|
||||
|
||||
fn insert(&self) -> Result<(), DataError> {
|
||||
use schema::episode::dsl::*;
|
||||
let db = connection();
|
||||
@@ -58,7 +60,9 @@ impl Insert<(), DataError> for NewEpisode {
|
||||
}
|
||||
}
|
||||
|
||||
impl Update<(), DataError> for NewEpisode {
|
||||
impl Update<()> for NewEpisode {
|
||||
type Error = DataError;
|
||||
|
||||
fn update(&self, episode_id: i32) -> Result<(), DataError> {
|
||||
use schema::episode::dsl::*;
|
||||
let db = connection();
|
||||
@@ -73,7 +77,9 @@ impl Update<(), DataError> for NewEpisode {
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<(), DataError> for NewEpisode {
|
||||
impl Index<()> for NewEpisode {
|
||||
type Error = DataError;
|
||||
|
||||
// Does not update the episode description if it's the only thing that has
|
||||
// changed.
|
||||
fn index(&self) -> Result<(), DataError> {
|
||||
@@ -197,9 +203,9 @@ impl NewEpisodeMinimal {
|
||||
let guid = item.guid().map(|s| s.value().trim().to_owned());
|
||||
|
||||
let uri = item.enclosure()
|
||||
.map(|s| url_cleaner(s.url()))
|
||||
.map(|s| url_cleaner(s.url().trim()))
|
||||
// Fallback to Rss.Item.link if enclosure is None.
|
||||
.or_else(|| item.link().map(|s| url_cleaner(s)));
|
||||
.or_else(|| item.link().map(|s| url_cleaner(s.trim())));
|
||||
|
||||
// If url is still None return an Error as this behaviour is
|
||||
// compliant with the RSS Spec.
|
||||
@@ -234,7 +240,14 @@ impl NewEpisodeMinimal {
|
||||
// TODO: TryInto is stabilizing in rustc v1.26!
|
||||
pub(crate) fn into_new_episode(self, item: &rss::Item) -> NewEpisode {
|
||||
let length = item.enclosure().and_then(|x| x.length().parse().ok());
|
||||
let description = item.description().map(|s| ammonia::clean(s));
|
||||
let description = item.description().and_then(|s| {
|
||||
let sanitized_html = ammonia::Builder::new()
|
||||
// Remove `rel` attributes from `<a>` tags
|
||||
.link_rel(None)
|
||||
.clean(s.trim())
|
||||
.to_string();
|
||||
Some(sanitized_html)
|
||||
});
|
||||
|
||||
NewEpisodeBuilder::default()
|
||||
.title(self.title)
|
||||
@@ -280,8 +293,8 @@ impl NewEpisodeMinimal {
|
||||
mod tests {
|
||||
use database::truncate_db;
|
||||
use dbqueries;
|
||||
use models::*;
|
||||
use models::new_episode::{NewEpisodeMinimal, NewEpisodeMinimalBuilder};
|
||||
use models::*;
|
||||
|
||||
use rss::Channel;
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ use diesel::prelude::*;
|
||||
use rss;
|
||||
|
||||
use errors::DataError;
|
||||
use models::{Index, Insert, Update};
|
||||
use models::Podcast;
|
||||
use models::{Index, Insert, Update};
|
||||
use schema::podcast;
|
||||
|
||||
use database::connection;
|
||||
@@ -26,8 +26,10 @@ pub(crate) struct NewPodcast {
|
||||
source_id: i32,
|
||||
}
|
||||
|
||||
impl Insert<(), DataError> for NewPodcast {
|
||||
fn insert(&self) -> Result<(), DataError> {
|
||||
impl Insert<()> for NewPodcast {
|
||||
type Error = DataError;
|
||||
|
||||
fn insert(&self) -> Result<(), Self::Error> {
|
||||
use schema::podcast::dsl::*;
|
||||
let db = connection();
|
||||
let con = db.get()?;
|
||||
@@ -40,8 +42,10 @@ impl Insert<(), DataError> for NewPodcast {
|
||||
}
|
||||
}
|
||||
|
||||
impl Update<(), DataError> for NewPodcast {
|
||||
fn update(&self, podcast_id: i32) -> Result<(), DataError> {
|
||||
impl Update<()> for NewPodcast {
|
||||
type Error = DataError;
|
||||
|
||||
fn update(&self, podcast_id: i32) -> Result<(), Self::Error> {
|
||||
use schema::podcast::dsl::*;
|
||||
let db = connection();
|
||||
let con = db.get()?;
|
||||
@@ -57,7 +61,9 @@ impl Update<(), DataError> for NewPodcast {
|
||||
|
||||
// TODO: Maybe return an Enum<Action(Resut)> Instead.
|
||||
// It would make unti testing better too.
|
||||
impl Index<(), DataError> for NewPodcast {
|
||||
impl Index<()> for NewPodcast {
|
||||
type Error = DataError;
|
||||
|
||||
fn index(&self) -> Result<(), DataError> {
|
||||
let exists = dbqueries::podcast_exists(self.source_id)?;
|
||||
|
||||
@@ -88,16 +94,20 @@ impl NewPodcast {
|
||||
/// Parses a `rss::Channel` into a `NewPodcast` Struct.
|
||||
pub(crate) fn new(chan: &rss::Channel, source_id: i32) -> NewPodcast {
|
||||
let title = chan.title().trim();
|
||||
let link = url_cleaner(chan.link().trim());
|
||||
|
||||
let description = ammonia::clean(chan.description().trim());
|
||||
let link = url_cleaner(chan.link());
|
||||
let description = ammonia::Builder::new()
|
||||
// Remove `rel` attributes from `<a>` tags
|
||||
.link_rel(None)
|
||||
.clean(chan.description().trim())
|
||||
.to_string();
|
||||
|
||||
// Try to get the itunes img first
|
||||
let itunes_img = chan.itunes_ext()
|
||||
.and_then(|s| s.image())
|
||||
.and_then(|s| s.image().map(|url| url.trim()))
|
||||
.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()));
|
||||
let image_uri = itunes_img.or_else(|| chan.image().map(|s| s.url().trim().to_owned()));
|
||||
|
||||
NewPodcastBuilder::default()
|
||||
.title(title)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(unused_mut)]
|
||||
|
||||
use diesel;
|
||||
use diesel::prelude::*;
|
||||
use url::Url;
|
||||
|
||||
@@ -25,10 +25,12 @@ pub struct Podcast {
|
||||
source_id: i32,
|
||||
}
|
||||
|
||||
impl Save<Podcast, DataError> for Podcast {
|
||||
impl Save<Podcast> for Podcast {
|
||||
type Error = DataError;
|
||||
|
||||
/// Helper method to easily save/"sync" current state of self to the
|
||||
/// Database.
|
||||
fn save(&self) -> Result<Podcast, DataError> {
|
||||
fn save(&self) -> Result<Podcast, Self::Error> {
|
||||
let db = connection();
|
||||
let tempdb = db.get()?;
|
||||
|
||||
|
||||
@@ -3,20 +3,22 @@ use diesel::SaveChangesDsl;
|
||||
use rss::Channel;
|
||||
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, UserAgent};
|
||||
use hyper::{Client, Method, Request, Response, StatusCode, Uri};
|
||||
use hyper_tls::HttpsConnector;
|
||||
|
||||
// use futures::future::ok;
|
||||
use futures::future::{loop_fn, Future, Loop};
|
||||
use futures::prelude::*;
|
||||
|
||||
use database::connection;
|
||||
use errors::DataError;
|
||||
use errors::*;
|
||||
use feed::{Feed, FeedBuilder};
|
||||
use models::{NewSource, Save};
|
||||
use schema::source;
|
||||
use USER_AGENT;
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
@@ -32,10 +34,12 @@ pub struct Source {
|
||||
http_etag: Option<String>,
|
||||
}
|
||||
|
||||
impl Save<Source, DataError> for Source {
|
||||
impl Save<Source> for Source {
|
||||
type Error = DataError;
|
||||
|
||||
/// Helper method to easily save/"sync" current state of self to the
|
||||
/// Database.
|
||||
fn save(&self) -> Result<Source, DataError> {
|
||||
fn save(&self) -> Result<Source, Self::Error> {
|
||||
let db = connection();
|
||||
let con = db.get()?;
|
||||
|
||||
@@ -102,11 +106,7 @@ impl Source {
|
||||
}
|
||||
|
||||
fn make_err(self, context: &str, code: StatusCode) -> DataError {
|
||||
DataError::HttpStatusError {
|
||||
url: self.uri,
|
||||
status_code: code,
|
||||
context: context.into(),
|
||||
}
|
||||
DataError::HttpStatusGeneral(HttpStatusError::new(self.uri, code, context.into()))
|
||||
}
|
||||
|
||||
// TODO match on more stuff
|
||||
@@ -119,7 +119,7 @@ impl Source {
|
||||
// 408: Timeout
|
||||
// 410: Feed deleted
|
||||
// TODO: Rething this api,
|
||||
fn match_status(mut self, res: Response) -> Result<(Self, Response), DataError> {
|
||||
fn match_status(mut self, res: Response) -> Result<Response, DataError> {
|
||||
self.update_etag(&res)?;
|
||||
let code = res.status();
|
||||
|
||||
@@ -128,7 +128,7 @@ impl Source {
|
||||
StatusCode::MovedPermanently => {
|
||||
error!("Feed was moved permanently.");
|
||||
self.handle_301(&res)?;
|
||||
return Err(self.make_err("301: Feed was moved permanently.", code));
|
||||
return Err(DataError::F301(self));
|
||||
}
|
||||
StatusCode::TemporaryRedirect => debug!("307: Temporary Redirect."),
|
||||
StatusCode::PermanentRedirect => warn!("308: Permanent Redirect."),
|
||||
@@ -139,7 +139,7 @@ impl Source {
|
||||
StatusCode::Gone => return Err(self.make_err("410: Feed was deleted..", code)),
|
||||
_ => info!("HTTP StatusCode: {}", code),
|
||||
};
|
||||
Ok((self, res))
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn handle_301(&mut self, res: &Response) -> Result<(), DataError> {
|
||||
@@ -151,8 +151,6 @@ impl Source {
|
||||
self.last_modified = None;
|
||||
self.save()?;
|
||||
info!("Feed url was updated succesfully.");
|
||||
// TODO: Refresh in place instead of next time, Not a priority.
|
||||
info!("New content will be fetched with the next refesh.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -177,12 +175,27 @@ impl Source {
|
||||
// Refactor into TryInto once it lands on stable.
|
||||
pub fn into_feed(
|
||||
self,
|
||||
client: &Client<HttpsConnector<HttpConnector>>,
|
||||
client: Client<HttpsConnector<HttpConnector>>,
|
||||
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))
|
||||
let response = loop_fn(self, move |source| {
|
||||
source
|
||||
.request_constructor(&client.clone(), ignore_etags)
|
||||
.then(|res| match res {
|
||||
Ok(response) => Ok(Loop::Break(response)),
|
||||
Err(err) => match err {
|
||||
DataError::F301(s) => {
|
||||
info!("Following redirect...");
|
||||
Ok(Loop::Continue(s))
|
||||
}
|
||||
e => Err(e),
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
let feed = response
|
||||
.and_then(response_to_channel)
|
||||
.and_then(move |chan| {
|
||||
FeedBuilder::default()
|
||||
.channel(chan)
|
||||
@@ -200,27 +213,23 @@ impl Source {
|
||||
self,
|
||||
client: &Client<HttpsConnector<HttpConnector>>,
|
||||
ignore_etags: bool,
|
||||
) -> Box<Future<Item = (Self, Response), Error = DataError>> {
|
||||
) -> Box<Future<Item = Response, Error = DataError>> {
|
||||
// FIXME: remove unwrap somehow
|
||||
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",
|
||||
));
|
||||
// Set the UserAgent cause ppl still seem to check it for some reason...
|
||||
req.headers_mut().set(UserAgent::new(USER_AGENT));
|
||||
|
||||
if !ignore_etags {
|
||||
if let Some(foo) = self.http_etag() {
|
||||
req.headers_mut().set(IfNoneMatch::Items(vec![
|
||||
EntityTag::new(true, foo.to_owned()),
|
||||
]));
|
||||
if let Some(etag) = self.http_etag() {
|
||||
let tag = vec![EntityTag::new(true, etag.to_owned())];
|
||||
req.headers_mut().set(IfNoneMatch::Items(tag));
|
||||
}
|
||||
|
||||
if let Some(foo) = self.last_modified() {
|
||||
if let Ok(x) = foo.parse::<HttpDate>() {
|
||||
req.headers_mut().set(IfModifiedSince(x));
|
||||
if let Some(lmod) = self.last_modified() {
|
||||
if let Ok(date) = lmod.parse::<HttpDate>() {
|
||||
req.headers_mut().set(IfModifiedSince(date));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,7 +237,6 @@ impl Source {
|
||||
let work = client
|
||||
.request(req)
|
||||
.map_err(From::from)
|
||||
// TODO: tail recursion loop that would follow redirects directly
|
||||
.and_then(move |res| self.match_status(res));
|
||||
Box::new(work)
|
||||
}
|
||||
@@ -269,7 +277,7 @@ mod tests {
|
||||
let source = Source::from_url(url).unwrap();
|
||||
let id = source.id();
|
||||
|
||||
let feed = source.into_feed(&client, 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);
|
||||
|
||||
+73
-140
@@ -2,186 +2,119 @@
|
||||
//! Docs.
|
||||
|
||||
use futures::future::*;
|
||||
// use futures::prelude::*;
|
||||
use futures::prelude::*;
|
||||
use futures::stream::*;
|
||||
|
||||
use hyper::Client;
|
||||
use hyper::client::HttpConnector;
|
||||
use hyper::Client;
|
||||
use hyper_tls::HttpsConnector;
|
||||
use tokio_core::reactor::Core;
|
||||
|
||||
use num_cpus;
|
||||
use rss;
|
||||
use rayon;
|
||||
use rayon_futures::ScopeFutureExt;
|
||||
|
||||
use Source;
|
||||
use dbqueries;
|
||||
use errors::DataError;
|
||||
use models::{IndexState, NewEpisode, NewEpisodeMinimal};
|
||||
use Source;
|
||||
|
||||
// use std::sync::{Arc, Mutex};
|
||||
|
||||
// http://gtk-rs.org/tuto/closures
|
||||
#[macro_export]
|
||||
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
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
type HttpsClient = Client<HttpsConnector<HttpConnector>>;
|
||||
|
||||
/// The pipline to be run for indexing and updating a Podcast feed that originates from
|
||||
/// `Source.uri`.
|
||||
///
|
||||
/// Messy temp diagram:
|
||||
/// Source -> GET Request -> Update Etags -> Check Status -> Parse xml/Rss ->
|
||||
/// Convert `rss::Channel` into Feed -> Index Podcast -> Index Episodes.
|
||||
pub fn pipeline<S: IntoIterator<Item = Source>>(
|
||||
/// Source -> GET Request -> Update Etags -> Check Status -> Parse `xml/Rss` ->
|
||||
/// Convert `rss::Channel` into `Feed` -> Index Podcast -> Index Episodes.
|
||||
pub fn pipeline<'a, S>(
|
||||
sources: S,
|
||||
ignore_etags: bool,
|
||||
tokio_core: &mut Core,
|
||||
client: Client<HttpsConnector<HttpConnector>>,
|
||||
) -> Result<(), DataError> {
|
||||
let list: Vec<_> = sources
|
||||
.into_iter()
|
||||
.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)))
|
||||
client: &HttpsClient,
|
||||
) -> Box<Future<Item = Vec<()>, Error = DataError> + 'a>
|
||||
where
|
||||
S: Stream<Item = Source, Error = DataError> + 'a,
|
||||
{
|
||||
let pipeline = sources
|
||||
.and_then(clone!(client => move |s| s.into_feed(client.clone(), ignore_etags)))
|
||||
.and_then(|feed| rayon::scope(|s| s.spawn_future(feed.index())))
|
||||
// the stream will stop at the first error so
|
||||
// we ensure that everything will succeded regardless.
|
||||
.map_err(|err| error!("Error: {}", err))
|
||||
.then(|_| ok::<(), DataError>(()))
|
||||
.collect();
|
||||
|
||||
if list.is_empty() {
|
||||
return Err(DataError::EmptyFuturesList);
|
||||
}
|
||||
|
||||
// Thats not really concurrent yet I think.
|
||||
tokio_core.run(collect_futures(list))?;
|
||||
|
||||
Ok(())
|
||||
Box::new(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 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, client)
|
||||
}
|
||||
|
||||
/// Docs
|
||||
pub fn index_single_source(s: Source, ignore_etags: bool) -> Result<(), DataError> {
|
||||
let mut core = Core::new()?;
|
||||
let handle = core.handle();
|
||||
|
||||
let client = Client::configure()
|
||||
.connector(HttpsConnector::new(num_cpus::get(), &handle)?)
|
||||
.build(&handle);
|
||||
|
||||
let work = s.into_feed(&client, ignore_etags)
|
||||
.and_then(move |feed| feed.index())
|
||||
.map(|_| ());
|
||||
|
||||
core.run(work)
|
||||
}
|
||||
|
||||
fn determine_ep_state(
|
||||
ep: NewEpisodeMinimal,
|
||||
item: &rss::Item,
|
||||
) -> Result<IndexState<NewEpisode>, DataError> {
|
||||
// Check if feed exists
|
||||
let exists = dbqueries::episode_exists(ep.title(), ep.podcast_id())?;
|
||||
|
||||
if !exists {
|
||||
Ok(IndexState::Index(ep.into_new_episode(item)))
|
||||
} else {
|
||||
let old = dbqueries::get_episode_minimal_from_pk(ep.title(), ep.podcast_id())?;
|
||||
let rowid = old.rowid();
|
||||
|
||||
if ep != old {
|
||||
Ok(IndexState::Update((ep.into_new_episode(item), rowid)))
|
||||
} else {
|
||||
Ok(IndexState::NotChanged)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn glue_async<'a>(
|
||||
item: &'a rss::Item,
|
||||
id: i32,
|
||||
) -> Box<Future<Item = IndexState<NewEpisode>, Error = DataError> + 'a> {
|
||||
Box::new(
|
||||
result(NewEpisodeMinimal::new(item, id)).and_then(move |ep| determine_ep_state(ep, item)),
|
||||
)
|
||||
}
|
||||
|
||||
// Weird magic from #rust irc channel
|
||||
// kudos to remexre
|
||||
/// FIXME: Docs
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(type_complexity))]
|
||||
pub fn collect_futures<F>(
|
||||
futures: Vec<F>,
|
||||
) -> Box<Future<Item = Vec<Result<F::Item, F::Error>>, Error = DataError>>
|
||||
pub fn run<S>(sources: S, ignore_etags: bool) -> Result<(), DataError>
|
||||
where
|
||||
F: 'static + Future,
|
||||
<F as Future>::Item: 'static,
|
||||
<F as Future>::Error: 'static,
|
||||
S: IntoIterator<Item = Source>,
|
||||
{
|
||||
Box::new(loop_fn((futures, vec![]), |(futures, mut done)| {
|
||||
select_all(futures).then(|r| {
|
||||
let (r, rest) = match r {
|
||||
Ok((r, _, rest)) => (Ok(r), rest),
|
||||
Err((r, _, rest)) => (Err(r), rest),
|
||||
};
|
||||
done.push(r);
|
||||
if rest.is_empty() {
|
||||
Ok(Loop::Break(done))
|
||||
} else {
|
||||
Ok(Loop::Continue((rest, done)))
|
||||
}
|
||||
})
|
||||
}))
|
||||
let mut core = Core::new()?;
|
||||
let handle = core.handle();
|
||||
let client = Client::configure()
|
||||
.connector(HttpsConnector::new(num_cpus::get(), &handle)?)
|
||||
.build(&handle);
|
||||
|
||||
let stream = iter_ok::<_, DataError>(sources);
|
||||
let p = pipeline(stream, ignore_etags, &client);
|
||||
core.run(p).map(|_| ())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use Source;
|
||||
use database::truncate_db;
|
||||
use dbqueries;
|
||||
use Source;
|
||||
|
||||
// (path, url) tuples.
|
||||
const URLS: &[(&str, &str)] = {
|
||||
&[
|
||||
(
|
||||
"tests/feeds/2018-01-20-Intercepted.xml",
|
||||
"https://web.archive.org/web/20180120083840if_/https://feeds.feedburner.\
|
||||
com/InterceptedWithJeremyScahill",
|
||||
),
|
||||
(
|
||||
"tests/feeds/2018-01-20-LinuxUnplugged.xml",
|
||||
"https://web.archive.org/web/20180120110314if_/https://feeds.feedburner.\
|
||||
com/linuxunplugged",
|
||||
),
|
||||
(
|
||||
"tests/feeds/2018-01-20-TheTipOff.xml",
|
||||
"https://web.archive.org/web/20180120110727if_/https://rss.acast.com/thetipoff",
|
||||
),
|
||||
(
|
||||
"tests/feeds/2018-01-20-StealTheStars.xml",
|
||||
"https://web.archive.org/web/20180120104957if_/https://rss.art19.\
|
||||
com/steal-the-stars",
|
||||
),
|
||||
(
|
||||
"tests/feeds/2018-01-20-GreaterThanCode.xml",
|
||||
"https://web.archive.org/web/20180120104741if_/https://www.greaterthancode.\
|
||||
com/feed/podcast",
|
||||
),
|
||||
]
|
||||
};
|
||||
const URLS: &[&str] = &[
|
||||
"https://web.archive.org/web/20180120083840if_/https://feeds.feedburner.\
|
||||
com/InterceptedWithJeremyScahill",
|
||||
"https://web.archive.org/web/20180120110314if_/https://feeds.feedburner.com/linuxunplugged",
|
||||
"https://web.archive.org/web/20180120110727if_/https://rss.acast.com/thetipoff",
|
||||
"https://web.archive.org/web/20180120104957if_/https://rss.art19.com/steal-the-stars",
|
||||
"https://web.archive.org/web/20180120104741if_/https://www.greaterthancode.\
|
||||
com/feed/podcast",
|
||||
];
|
||||
|
||||
#[test]
|
||||
/// Insert feeds and update/index them.
|
||||
fn test_pipeline() {
|
||||
truncate_db().unwrap();
|
||||
URLS.iter().for_each(|&(_, url)| {
|
||||
let bad_url = "https://gitlab.gnome.org/World/hammond.atom";
|
||||
// if a stream returns error/None it stops
|
||||
// bad we want to parse all feeds regardless if one fails
|
||||
Source::from_url(bad_url).unwrap();
|
||||
|
||||
URLS.iter().for_each(|url| {
|
||||
// Index the urls into the source table.
|
||||
Source::from_url(url).unwrap();
|
||||
});
|
||||
|
||||
let sources = dbqueries::get_sources().unwrap();
|
||||
run(sources, true).unwrap();
|
||||
|
||||
@@ -190,7 +123,7 @@ mod tests {
|
||||
run(sources, true).unwrap();
|
||||
|
||||
// Assert the index rows equal the controlled results
|
||||
assert_eq!(dbqueries::get_sources().unwrap().len(), 5);
|
||||
assert_eq!(dbqueries::get_sources().unwrap().len(), 6);
|
||||
assert_eq!(dbqueries::get_podcasts().unwrap().len(), 5);
|
||||
assert_eq!(dbqueries::get_episodes().unwrap().len(), 354);
|
||||
}
|
||||
|
||||
@@ -28,10 +28,10 @@ fn download_checker() -> Result<(), DataError> {
|
||||
})
|
||||
.for_each(|ep| {
|
||||
ep.set_local_uri(None);
|
||||
if let Err(err) = ep.save() {
|
||||
error!("Error while trying to update episode: {:#?}", ep);
|
||||
error!("{}", err);
|
||||
};
|
||||
ep.save()
|
||||
.map_err(|err| error!("{}", err))
|
||||
.map_err(|_| error!("Error while trying to update episode: {:#?}", ep))
|
||||
.ok();
|
||||
});
|
||||
|
||||
Ok(())
|
||||
@@ -48,12 +48,11 @@ fn played_cleaner(cleanup_date: DateTime<Utc>) -> Result<(), DataError> {
|
||||
.for_each(|ep| {
|
||||
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());
|
||||
error!("{}", err);
|
||||
} else {
|
||||
info!("Episode {:?} was deleted succesfully.", ep.local_uri());
|
||||
};
|
||||
delete_local_content(ep)
|
||||
.map(|_| info!("Episode {:?} was deleted succesfully.", ep.local_uri()))
|
||||
.map_err(|err| error!("Error: {}", err))
|
||||
.map_err(|_| error!("Failed to delete file: {:?}", ep.local_uri()))
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
|
||||
@@ -6,7 +6,7 @@ workspace = "../"
|
||||
|
||||
[dependencies]
|
||||
error-chain = "0.11.0"
|
||||
hyper = "0.11.24"
|
||||
hyper = "0.11.25"
|
||||
log = "0.4.1"
|
||||
mime_guess = "1.8.4"
|
||||
reqwest = "0.8.5"
|
||||
|
||||
@@ -11,8 +11,8 @@ use std::io::{BufWriter, Read, Write};
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use hammond_data::{EpisodeWidgetQuery, PodcastCoverQuery, Save};
|
||||
use hammond_data::xdg_dirs::HAMMOND_CACHE;
|
||||
use hammond_data::{EpisodeWidgetQuery, PodcastCoverQuery, Save};
|
||||
|
||||
// use failure::Error;
|
||||
use errors::DownloadError;
|
||||
@@ -234,9 +234,9 @@ pub fn cache_image(pd: &PodcastCoverQuery) -> Result<String, DownloadError> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use hammond_data::Source;
|
||||
use hammond_data::dbqueries;
|
||||
use hammond_data::pipeline;
|
||||
use hammond_data::Source;
|
||||
|
||||
use std::fs;
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#![recursion_limit = "1024"]
|
||||
#![deny(unused_extern_crates, unused)]
|
||||
#![warn(unused_extern_crates, unused)]
|
||||
#![allow(unknown_lints)]
|
||||
#![cfg_attr(feature = "cargo-clippy", allow(blacklisted_name))]
|
||||
#![cfg_attr(feature = "cargo-clippy", allow(blacklisted_name, option_map_unit_fn))]
|
||||
#![deny(warnings)]
|
||||
|
||||
extern crate failure;
|
||||
#[macro_use]
|
||||
|
||||
@@ -6,7 +6,8 @@ version = "0.1.0"
|
||||
workspace = "../"
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.1"
|
||||
chrono = "0.4.2"
|
||||
crossbeam-channel = "0.1.2"
|
||||
gdk = "0.8.0"
|
||||
gdk-pixbuf = "0.4.0"
|
||||
glib = "0.5.0"
|
||||
@@ -16,14 +17,14 @@ log = "0.4.1"
|
||||
loggerv = "0.7.1"
|
||||
open = "1.2.1"
|
||||
rayon = "1.0.1"
|
||||
send-cell = "0.1.2"
|
||||
send-cell = "0.1.3"
|
||||
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"
|
||||
serde_json = "1.0.15"
|
||||
html2pango = { git = "https://gitlab.gnome.org/World/html2pango" }
|
||||
|
||||
[dependencies.gtk]
|
||||
|
||||
@@ -56,11 +56,11 @@ Tobias Bernard
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Episode Title</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<property name="width_chars">55</property>
|
||||
<property name="single_line_mode">True</property>
|
||||
<property name="track_visited_links">False</property>
|
||||
<property name="lines">1</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="yalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -32,13 +32,14 @@ Tobias Bernard
|
||||
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
|
||||
<object class="GtkBox" id="container">
|
||||
<property name="name">container</property>
|
||||
<property name="width_request">400</property>
|
||||
<property name="height_request">600</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<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="hscrollbar_policy">never</property>
|
||||
@@ -68,8 +69,6 @@ Tobias Bernard
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="frame_parent">
|
||||
<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="margin_left">32</property>
|
||||
@@ -81,7 +80,6 @@ Tobias Bernard
|
||||
<property name="spacing">24</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="today_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="no_show_all">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
@@ -139,7 +137,6 @@ Tobias Bernard
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="yday_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="no_show_all">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
@@ -196,7 +193,6 @@ Tobias Bernard
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="week_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="no_show_all">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
@@ -253,7 +249,6 @@ Tobias Bernard
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="month_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="no_show_all">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
@@ -310,7 +305,6 @@ Tobias Bernard
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="rest_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="no_show_all">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
|
||||
@@ -31,16 +31,17 @@ Tobias Bernard
|
||||
<!-- interface-copyright 2017 - 2018 -->
|
||||
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
|
||||
<object class="GtkBox" id="container">
|
||||
<property name="width_request">400</property>
|
||||
<property name="height_request">600</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<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="vexpand">True</property>
|
||||
<property name="hscrollbar_policy">never</property>
|
||||
<child>
|
||||
<object class="GtkViewport">
|
||||
@@ -70,26 +71,26 @@ Tobias Bernard
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="width_request">700</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</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>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="spacing">12</property>
|
||||
<property name="orientation">vertical</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="pixel_size">128</property>
|
||||
<property name="pixel_size">256</property>
|
||||
<property name="icon_name">image-x-generic-symbolic</property>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -98,107 +99,107 @@ Tobias Bernard
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="hscrollbar_policy">never</property>
|
||||
<property name="min_content_height">80</property>
|
||||
<child>
|
||||
<object class="GtkViewport">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="shadow_type">none</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="description">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="label">This is embarrasing!
|
||||
Sorry, we could not find a description for this Show.</property>
|
||||
<property name="use_markup">True</property>
|
||||
<property name="justify">center</property>
|
||||
<property name="wrap">True</property>
|
||||
<property name="max_width_chars">70</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">end</property>
|
||||
<property name="orientation">vertical</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">foo</property>
|
||||
<property name="use_markup">True</property>
|
||||
<property name="wrap">True</property>
|
||||
<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="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<object class="GtkMenuButton" id="settings_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="settings_button">
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="icon_name">emblem-system-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="link_button">
|
||||
<property name="label" translatable="yes">Website</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="icon_name">emblem-system-symbolic</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="padding">5</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="unsub_button">
|
||||
<property name="label" translatable="yes">Unsubscribe</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="destructive-action"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="padding">5</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="link_button">
|
||||
<property name="label" translatable="yes">Website</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>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="padding">5</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="unsub_button">
|
||||
<property name="label" translatable="yes">Unsubscribe</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="destructive-action"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="padding">5</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</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>
|
||||
@@ -209,13 +210,17 @@ Tobias Bernard
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="episodes">
|
||||
<object class="GtkFrame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label_xalign">0</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
<object class="GtkListBox" id="episodes">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="selection_mode">none</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label_item">
|
||||
<placeholder/>
|
||||
@@ -230,7 +235,7 @@ Tobias Bernard
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
@@ -257,7 +262,7 @@ Tobias Bernard
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</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
|
||||
|
||||
@@ -39,26 +39,17 @@ Tobias Bernard
|
||||
<property name="valign">center</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkOverlay">
|
||||
<object class="GtkImage" id="pd_cover">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child type="overlay">
|
||||
<object class="GtkImage" id="pd_cover">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="pixel_size">256</property>
|
||||
<property name="icon_name">image-x-generic-symbolic</property>
|
||||
<property name="icon_size">0</property>
|
||||
</object>
|
||||
</child>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="pixel_size">256</property>
|
||||
<property name="icon_name">image-x-generic-symbolic</property>
|
||||
<property name="icon_size">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
|
||||
@@ -12,25 +12,25 @@
|
||||
desktop written in Rust.
|
||||
</p>
|
||||
</description>
|
||||
<url type="homepage">https://gitlab.gnome.org/alatiera/Hammond</url>
|
||||
<url type="homepage">https://gitlab.gnome.org/World/hammond</url>
|
||||
<screenshots>
|
||||
<screenshot>
|
||||
<image>https://gitlab.gnome.org/alatiera/hammond/raw/master/screenshots/episodes_view.png</image>
|
||||
<image>https://gitlab.gnome.org/World/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>
|
||||
<image>https://gitlab.gnome.org/World/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>
|
||||
<image>https://gitlab.gnome.org/World/hammond/raw/master/screenshots/show_widget.png</image>
|
||||
<caption>Page 3</caption>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<releases>
|
||||
<release version="0.3.1" date="2018-03-28"/>
|
||||
<release version="0.3.2" date="2018-05-07"/>
|
||||
</releases>
|
||||
<url type="homepage">https://gitlab.gnome.org/alatiera/hammond</url>
|
||||
<url type="homepage">https://gitlab.gnome.org/World/hammond</url>
|
||||
<update_contact>jpetridis@gnome.org</update_contact>
|
||||
<developer_name>Jordan Petridis and others</developer_name>
|
||||
</component>
|
||||
+51
-95
@@ -3,35 +3,27 @@
|
||||
use gio::{ApplicationExt, ApplicationExtManual, ApplicationFlags, Settings, SettingsExt};
|
||||
use glib;
|
||||
use gtk;
|
||||
use gtk::SettingsExt as GtkSettingsExt;
|
||||
use gtk::prelude::*;
|
||||
use gtk::SettingsExt as GtkSettingsExt;
|
||||
|
||||
use failure::Error;
|
||||
use rayon;
|
||||
use hammond_data::Podcast;
|
||||
|
||||
use hammond_data::{Podcast, Source};
|
||||
use hammond_data::utils::delete_show;
|
||||
|
||||
use appnotif::*;
|
||||
use headerbar::Header;
|
||||
use settings::WindowGeometry;
|
||||
use stacks::Content;
|
||||
use settings::{self, WindowGeometry};
|
||||
use stacks::{Content, PopulatedState};
|
||||
use utils;
|
||||
use widgets::mark_all_watched;
|
||||
use widgets::{mark_all_notif, remove_show_notif};
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||
use std::time::Duration;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Action {
|
||||
UpdateSources(Option<Source>),
|
||||
RefreshAllViews,
|
||||
RefreshEpisodesView,
|
||||
RefreshEpisodesViewBGR,
|
||||
RefreshShowsView,
|
||||
RefreshWidget,
|
||||
RefreshWidgetIfVis,
|
||||
ReplaceWidget(Arc<Podcast>),
|
||||
RefreshWidgetIfSame(i32),
|
||||
ShowWidgetAnimated,
|
||||
@@ -49,8 +41,8 @@ pub struct App {
|
||||
app_instance: gtk::Application,
|
||||
window: gtk::Window,
|
||||
overlay: gtk::Overlay,
|
||||
header: Arc<Header>,
|
||||
content: Arc<Content>,
|
||||
header: Rc<Header>,
|
||||
content: Rc<Content>,
|
||||
receiver: Receiver<Action>,
|
||||
sender: Sender<Action>,
|
||||
settings: Settings,
|
||||
@@ -66,6 +58,9 @@ impl App {
|
||||
glib::set_application_name("Hammond");
|
||||
glib::set_prgname(Some("Hammond"));
|
||||
|
||||
let cleanup_date = settings::get_cleanup_date(&settings);
|
||||
utils::cleanup(cleanup_date);
|
||||
|
||||
// Create the main window
|
||||
let window = gtk::Window::new(gtk::WindowType::Toplevel);
|
||||
|
||||
@@ -84,10 +79,10 @@ impl App {
|
||||
|
||||
// Create a content instance
|
||||
let content =
|
||||
Arc::new(Content::new(sender.clone()).expect("Content Initialization failed."));
|
||||
Rc::new(Content::new(sender.clone()).expect("Content Initialization failed."));
|
||||
|
||||
// Create the headerbar
|
||||
let header = Arc::new(Header::new(&content, &window, sender.clone()));
|
||||
let header = Rc::new(Header::new(&content, &window, &sender));
|
||||
|
||||
// Add the content main stack to the overlay.
|
||||
let overlay = gtk::Overlay::new();
|
||||
@@ -124,34 +119,35 @@ impl App {
|
||||
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(None, sender.clone());
|
||||
|
||||
// The ui loads async, after initialization
|
||||
// so we need to delay this a bit so it won't block
|
||||
// requests that will come from loading the gui on startup.
|
||||
gtk::timeout_add(1500, move || {
|
||||
let s: Option<Vec<_>> = None;
|
||||
utils::refresh(s, sender.clone());
|
||||
glib::Continue(false)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_auto_refresh(&self) {
|
||||
let refresh_interval = utils::get_refresh_interval(&self.settings).num_seconds() as u32;
|
||||
let refresh_interval = settings::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());
|
||||
let s: Option<Vec<_>> = None;
|
||||
utils::refresh(s, sender.clone());
|
||||
|
||||
glib::Continue(true)
|
||||
});
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn run(self) {
|
||||
WindowGeometry::from_settings(&self.settings).apply(&self.window);
|
||||
|
||||
@@ -167,86 +163,46 @@ impl App {
|
||||
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(Some(vec![s]), sender.clone());
|
||||
} else {
|
||||
utils::refresh(None, sender.clone());
|
||||
}
|
||||
}
|
||||
gtk::timeout_add(50, move || {
|
||||
match receiver.try_recv() {
|
||||
Ok(Action::RefreshAllViews) => content.update(),
|
||||
Ok(Action::RefreshShowsView) => content.update_shows_view(),
|
||||
Ok(Action::RefreshWidget) => content.update_widget(),
|
||||
Ok(Action::RefreshWidgetIfVis) => content.update_widget_if_visible(),
|
||||
Ok(Action::RefreshWidgetIfSame(id)) => content.update_widget_if_same(id),
|
||||
Ok(Action::RefreshEpisodesView) => content.update_episode_view(),
|
||||
Ok(Action::RefreshEpisodesViewBGR) => content.update_episode_view_if_baground(),
|
||||
Ok(Action::RefreshEpisodesView) => content.update_home(),
|
||||
Ok(Action::RefreshEpisodesViewBGR) => content.update_home_if_background(),
|
||||
Ok(Action::ReplaceWidget(pd)) => {
|
||||
if let Err(err) = content.get_shows().replace_widget(pd) {
|
||||
error!("Something went wrong while trying to update the ShowWidget.");
|
||||
error!("Error: {}", err);
|
||||
}
|
||||
let shows = content.get_shows();
|
||||
let mut pop = shows.borrow().populated();
|
||||
pop.borrow_mut()
|
||||
.replace_widget(pd.clone())
|
||||
.map_err(|err| error!("Failed to update ShowWidget: {}", err))
|
||||
.map_err(|_| error!("Failed ot update ShowWidget {}", pd.title()))
|
||||
.ok();
|
||||
}
|
||||
Ok(Action::ShowWidgetAnimated) => {
|
||||
let shows = content.get_shows();
|
||||
let mut pop = shows.borrow().populated();
|
||||
pop.borrow_mut().switch_visible(
|
||||
PopulatedState::Widget,
|
||||
gtk::StackTransitionType::SlideLeft,
|
||||
);
|
||||
}
|
||||
Ok(Action::ShowShowsAnimated) => {
|
||||
let shows = content.get_shows();
|
||||
let mut pop = shows.borrow().populated();
|
||||
pop.borrow_mut()
|
||||
.switch_visible(PopulatedState::View, gtk::StackTransitionType::SlideRight);
|
||||
}
|
||||
Ok(Action::ShowWidgetAnimated) => content.get_shows().switch_widget_animated(),
|
||||
Ok(Action::ShowShowsAnimated) => content.get_shows().switch_podcasts_animated(),
|
||||
Ok(Action::HeaderBarShowTile(title)) => headerbar.switch_to_back(&title),
|
||||
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());
|
||||
let notif = mark_all_notif(pd, &sender);
|
||||
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_);
|
||||
let notif = remove_show_notif(pd, sender.clone());
|
||||
notif.show(&overlay);
|
||||
}
|
||||
Err(_) => (),
|
||||
|
||||
@@ -2,11 +2,8 @@ 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 {
|
||||
@@ -35,12 +32,7 @@ impl Default for InAppNotification {
|
||||
}
|
||||
|
||||
impl InAppNotification {
|
||||
pub fn new<F, U>(
|
||||
text: String,
|
||||
mut callback: F,
|
||||
undo_callback: U,
|
||||
sender: Sender<Action>,
|
||||
) -> Self
|
||||
pub fn new<F, U>(text: &str, mut callback: F, undo_callback: U) -> Self
|
||||
where
|
||||
F: FnMut() -> glib::Continue + 'static,
|
||||
U: Fn() + 'static,
|
||||
@@ -67,10 +59,6 @@ impl InAppNotification {
|
||||
|
||||
// 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
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use glib;
|
||||
use gtk;
|
||||
use gtk::prelude::*;
|
||||
|
||||
@@ -5,14 +6,14 @@ use failure::Error;
|
||||
use failure::ResultExt;
|
||||
use url::Url;
|
||||
|
||||
use hammond_data::Source;
|
||||
use hammond_data::dbqueries;
|
||||
use hammond_data::Source;
|
||||
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use app::Action;
|
||||
use stacks::Content;
|
||||
use utils::itunes_to_rss;
|
||||
use utils::{itunes_to_rss, refresh};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Header {
|
||||
@@ -60,13 +61,13 @@ impl Default for Header {
|
||||
|
||||
// TODO: Refactor components into smaller state machines
|
||||
impl Header {
|
||||
pub fn new(content: &Content, window: >k::Window, sender: Sender<Action>) -> Header {
|
||||
pub fn new(content: &Content, window: >k::Window, sender: &Sender<Action>) -> Header {
|
||||
let h = Header::default();
|
||||
h.init(content, window, sender);
|
||||
h.init(content, window, &sender);
|
||||
h
|
||||
}
|
||||
|
||||
pub fn init(&self, content: &Content, window: >k::Window, sender: Sender<Action>) {
|
||||
pub fn init(&self, content: &Content, window: >k::Window, sender: &Sender<Action>) {
|
||||
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/headerbar.ui");
|
||||
|
||||
let add_popover: gtk::Popover = builder.get_object("add_popover").unwrap();
|
||||
@@ -76,15 +77,15 @@ impl Header {
|
||||
self.switch.set_stack(&content.get_stack());
|
||||
|
||||
new_url.connect_changed(clone!(add_button => move |url| {
|
||||
if let Err(err) = on_url_change(url, &result_label, &add_button) {
|
||||
error!("Error: {}", err);
|
||||
}
|
||||
on_url_change(url, &result_label, &add_button)
|
||||
.map_err(|err| error!("Error: {}", err))
|
||||
.ok();
|
||||
}));
|
||||
|
||||
add_button.connect_clicked(clone!(add_popover, new_url, sender => move |_| {
|
||||
if let Err(err) = on_add_bttn_clicked(&new_url, sender.clone()) {
|
||||
error!("Error: {}", err);
|
||||
}
|
||||
on_add_bttn_clicked(&new_url, sender.clone())
|
||||
.map_err(|err| error!("Error: {}", err))
|
||||
.ok();
|
||||
add_popover.hide();
|
||||
}));
|
||||
|
||||
@@ -92,9 +93,11 @@ impl Header {
|
||||
|
||||
self.update_button
|
||||
.connect_clicked(clone!(sender => move |_| {
|
||||
sender
|
||||
.send(Action::UpdateSources(None))
|
||||
.expect("Action channel blew up.");
|
||||
gtk::idle_add(clone!(sender => move || {
|
||||
let s: Option<Vec<_>> = None;
|
||||
refresh(s, sender.clone());
|
||||
glib::Continue(false)
|
||||
}));
|
||||
}));
|
||||
|
||||
self.about_button
|
||||
@@ -114,9 +117,9 @@ impl Header {
|
||||
add_toggle.show();
|
||||
back.hide();
|
||||
show_title.hide();
|
||||
if let Err(err) = sender.send(Action::ShowShowsAnimated) {
|
||||
error!("Action channel blew up: {}", err);
|
||||
}
|
||||
sender.send(Action::ShowShowsAnimated)
|
||||
.map_err(|err| error!("Action Sender: {}", err))
|
||||
.ok();
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -170,9 +173,10 @@ fn on_add_bttn_clicked(entry: >k::Entry, sender: Sender<Action>) -> Result<(),
|
||||
let source = Source::from_url(&url).context("Failed to convert url to a Source entry.")?;
|
||||
entry.set_text("");
|
||||
|
||||
sender
|
||||
.send(Action::UpdateSources(Some(source)))
|
||||
.context("App channel blew up.")?;
|
||||
gtk::idle_add(move || {
|
||||
refresh(Some(vec![source.clone()]), sender.clone());
|
||||
glib::Continue(false)
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -239,7 +243,7 @@ fn about_dialog(window: >k::Window) {
|
||||
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.1");
|
||||
dialog.set_version("0.3.2");
|
||||
dialog.set_program_name("Hammond");
|
||||
// TODO: Need a wiki page first.
|
||||
// dialog.set_website("https://wiki.gnome.org/Design/Apps/Potential/Podcasts");
|
||||
|
||||
+16
-16
@@ -1,8 +1,10 @@
|
||||
#![cfg_attr(feature = "cargo-clippy",
|
||||
allow(clone_on_ref_ptr, needless_pass_by_value, useless_format, blacklisted_name,
|
||||
match_same_arms))]
|
||||
#![cfg_attr(
|
||||
feature = "cargo-clippy",
|
||||
allow(clone_on_ref_ptr, blacklisted_name, match_same_arms, option_map_unit_fn)
|
||||
)]
|
||||
#![allow(unknown_lints)]
|
||||
#![deny(unused_extern_crates, unused)]
|
||||
#![warn(unused_extern_crates, unused)]
|
||||
#![deny(warnings)]
|
||||
|
||||
extern crate gdk;
|
||||
extern crate gdk_pixbuf;
|
||||
@@ -24,6 +26,7 @@ extern crate log;
|
||||
extern crate pretty_assertions;
|
||||
|
||||
extern crate chrono;
|
||||
extern crate crossbeam_channel;
|
||||
extern crate hammond_data;
|
||||
extern crate hammond_downloader;
|
||||
extern crate html2pango;
|
||||
@@ -61,20 +64,17 @@ macro_rules! clone {
|
||||
);
|
||||
}
|
||||
|
||||
// They do not need to be public
|
||||
// But it helps when looking at the generated docs.
|
||||
pub mod views;
|
||||
pub mod widgets;
|
||||
pub mod stacks;
|
||||
mod stacks;
|
||||
mod widgets;
|
||||
|
||||
pub mod headerbar;
|
||||
pub mod app;
|
||||
mod app;
|
||||
mod headerbar;
|
||||
|
||||
pub mod settings;
|
||||
pub mod utils;
|
||||
pub mod manager;
|
||||
pub mod static_resource;
|
||||
pub mod appnotif;
|
||||
mod appnotif;
|
||||
mod manager;
|
||||
mod settings;
|
||||
mod static_resource;
|
||||
mod utils;
|
||||
|
||||
use app::App;
|
||||
|
||||
|
||||
@@ -5,11 +5,8 @@ use rayon;
|
||||
use hammond_data::dbqueries;
|
||||
use hammond_downloader::downloader::{get_episode, DownloadProgress};
|
||||
|
||||
use app::Action;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::sync::mpsc::Sender;
|
||||
// use std::sync::atomic::AtomicUsize;
|
||||
// use std::path::PathBuf;
|
||||
|
||||
@@ -78,7 +75,7 @@ lazy_static! {
|
||||
static ref DLPOOL: rayon::ThreadPool = rayon::ThreadPoolBuilder::new().build().unwrap();
|
||||
}
|
||||
|
||||
pub fn add(id: i32, directory: String, sender: Sender<Action>) -> Result<(), Error> {
|
||||
pub fn add(id: i32, directory: String) -> Result<(), Error> {
|
||||
// Create a new `Progress` struct to keep track of dl progress.
|
||||
let prog = Arc::new(Mutex::new(Progress::default()));
|
||||
|
||||
@@ -88,14 +85,12 @@ pub fn add(id: i32, directory: String, sender: Sender<Action>) -> Result<(), Err
|
||||
};
|
||||
|
||||
DLPOOL.spawn(move || {
|
||||
if let Ok(episode) = dbqueries::get_episode_from_rowid(id) {
|
||||
let pid = episode.podcast_id();
|
||||
if let Ok(mut episode) = dbqueries::get_episode_widget_from_rowid(id) {
|
||||
let id = episode.rowid();
|
||||
|
||||
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);
|
||||
}
|
||||
get_episode(&mut episode, directory.as_str(), Some(prog))
|
||||
.map_err(|err| error!("Download Failed: {}", err))
|
||||
.ok();
|
||||
|
||||
if let Ok(mut m) = ACTIVE_DOWNLOADS.write() {
|
||||
let foo = m.remove(&id);
|
||||
@@ -105,13 +100,6 @@ pub fn add(id: i32, directory: String, sender: Sender<Action>) -> Result<(), Err
|
||||
// if let Ok(m) = ACTIVE_DOWNLOADS.read() {
|
||||
// debug!("ACTIVE DOWNLOADS: {:#?}", m);
|
||||
// }
|
||||
|
||||
sender
|
||||
.send(Action::RefreshEpisodesView)
|
||||
.expect("Action channel blew up.");
|
||||
sender
|
||||
.send(Action::RefreshWidgetIfSame(pid))
|
||||
.expect("Action channel blew up.");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -122,17 +110,16 @@ pub fn add(id: i32, directory: String, sender: Sender<Action>) -> Result<(), Err
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use hammond_data::{Episode, Source};
|
||||
use hammond_data::dbqueries;
|
||||
use hammond_data::pipeline;
|
||||
use hammond_data::utils::get_download_folder;
|
||||
use hammond_data::{Episode, Source};
|
||||
|
||||
use hammond_downloader::downloader::get_episode;
|
||||
|
||||
use std::{thread, time};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::{thread, time};
|
||||
|
||||
#[test]
|
||||
// This test inserts an rss feed to your `XDG_DATA/hammond/hammond.db` so we make it explicit
|
||||
@@ -154,11 +141,9 @@ mod tests {
|
||||
// Get an episode
|
||||
let episode: Episode = dbqueries::get_episode_from_pk(title, pd.id()).unwrap();
|
||||
|
||||
let (sender, _rx) = channel();
|
||||
|
||||
let download_fold = get_download_folder(&pd.title()).unwrap();
|
||||
let fold2 = download_fold.clone();
|
||||
add(episode.rowid(), download_fold, sender).unwrap();
|
||||
add(episode.rowid(), download_fold).unwrap();
|
||||
assert_eq!(ACTIVE_DOWNLOADS.read().unwrap().len(), 1);
|
||||
|
||||
// Give it soem time to download the file
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
use gio;
|
||||
use gio::SettingsExt;
|
||||
use gio::{Settings, SettingsExt};
|
||||
use gtk;
|
||||
use gtk::GtkWindowExt;
|
||||
|
||||
use chrono::prelude::*;
|
||||
use chrono::Duration;
|
||||
|
||||
pub struct WindowGeometry {
|
||||
left: i32,
|
||||
top: i32,
|
||||
@@ -67,6 +70,49 @@ impl WindowGeometry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_refresh_interval(settings: &Settings) -> Duration {
|
||||
let time = i64::from(settings.get_int("refresh-interval-time"));
|
||||
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 = i64::from(settings.get_int("cleanup-age-time"));
|
||||
let period = settings.get_string("cleanup-age-period").unwrap();
|
||||
let duration = time_period_to_duration(time, period.as_str());
|
||||
|
||||
Utc::now() - duration
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
#[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]
|
||||
// fn test_apply_window_geometry() {
|
||||
// gtk::init().expect("Error initializing gtk.");
|
||||
|
||||
@@ -4,91 +4,85 @@ use gtk::prelude::*;
|
||||
use failure::Error;
|
||||
|
||||
use app::Action;
|
||||
use stacks::EpisodeStack;
|
||||
use stacks::ShowStack;
|
||||
use stacks::{HomeStack, ShowStack};
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Content {
|
||||
stack: gtk::Stack,
|
||||
shows: Arc<ShowStack>,
|
||||
episodes: Arc<EpisodeStack>,
|
||||
shows: Rc<RefCell<ShowStack>>,
|
||||
home: Rc<RefCell<HomeStack>>,
|
||||
sender: Sender<Action>,
|
||||
}
|
||||
|
||||
impl Content {
|
||||
pub fn new(sender: Sender<Action>) -> Result<Content, Error> {
|
||||
let stack = gtk::Stack::new();
|
||||
let episodes = Arc::new(EpisodeStack::new(sender.clone())?);
|
||||
let shows = Arc::new(ShowStack::new(sender.clone())?);
|
||||
let home = Rc::new(RefCell::new(HomeStack::new(sender.clone())?));
|
||||
let shows = Rc::new(RefCell::new(ShowStack::new(sender.clone())?));
|
||||
|
||||
stack.add_titled(&episodes.get_stack(), "episodes", "Episodes");
|
||||
stack.add_titled(&shows.get_stack(), "shows", "Shows");
|
||||
stack.add_titled(&home.borrow().get_stack(), "home", "Recent");
|
||||
stack.add_titled(&shows.borrow().get_stack(), "shows", "Shows");
|
||||
|
||||
Ok(Content {
|
||||
stack,
|
||||
shows,
|
||||
episodes,
|
||||
home,
|
||||
sender,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update(&self) {
|
||||
self.update_episode_view();
|
||||
self.update_shows_view();
|
||||
self.update_widget()
|
||||
self.update_home();
|
||||
self.update_shows();
|
||||
}
|
||||
|
||||
// TODO: Maybe propagate the error?
|
||||
pub fn update_episode_view(&self) {
|
||||
if let Err(err) = self.episodes.update() {
|
||||
error!("Something went wrong while trying to update the episode view.");
|
||||
error!("Error: {}", err);
|
||||
pub fn update_home(&self) {
|
||||
self.home
|
||||
.borrow_mut()
|
||||
.update()
|
||||
.map_err(|err| error!("Failed to update HomeView: {}", err))
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn update_home_if_background(&self) {
|
||||
if self.stack.get_visible_child_name() != Some("home".into()) {
|
||||
self.update_home();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_episode_view_if_baground(&self) {
|
||||
if self.stack.get_visible_child_name() != Some("episodes".into()) {
|
||||
self.update_episode_view();
|
||||
}
|
||||
fn update_shows(&self) {
|
||||
self.shows
|
||||
.borrow_mut()
|
||||
.update()
|
||||
.map_err(|err| error!("Failed to update ShowsView: {}", err))
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn update_shows_view(&self) {
|
||||
if let Err(err) = self.shows.update_podcasts() {
|
||||
error!("Something went wrong while trying to update the ShowsView.");
|
||||
error!("Error: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_widget(&self) {
|
||||
if let Err(err) = self.shows.update_widget() {
|
||||
error!("Something went wrong while trying to update the Show Widget.");
|
||||
error!("Error: {}", err);
|
||||
}
|
||||
self.shows
|
||||
.borrow_mut()
|
||||
.update()
|
||||
.map_err(|err| error!("Failed to update ShowsView: {}", err))
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn update_widget_if_same(&self, pid: i32) {
|
||||
if let Err(err) = self.shows.update_widget_if_same(pid) {
|
||||
error!("Something went wrong while trying to update the Show Widget.");
|
||||
error!("Error: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_widget_if_visible(&self) {
|
||||
if self.stack.get_visible_child_name() == Some("shows".to_string())
|
||||
&& self.shows.get_stack().get_visible_child_name() == Some("widget".to_string())
|
||||
{
|
||||
self.update_widget();
|
||||
}
|
||||
let pop = self.shows.borrow().populated();
|
||||
pop.borrow_mut()
|
||||
.update_widget_if_same(pid)
|
||||
.map_err(|err| error!("Failed to update ShowsWidget: {}", err))
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn get_stack(&self) -> gtk::Stack {
|
||||
self.stack.clone()
|
||||
}
|
||||
|
||||
pub fn get_shows(&self) -> Arc<ShowStack> {
|
||||
pub fn get_shows(&self) -> Rc<RefCell<ShowStack>> {
|
||||
self.shows.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
use gtk;
|
||||
use gtk::Cast;
|
||||
use gtk::prelude::*;
|
||||
|
||||
use failure::Error;
|
||||
|
||||
use views::{EmptyView, EpisodesView};
|
||||
|
||||
use app::Action;
|
||||
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EpisodeStack {
|
||||
stack: gtk::Stack,
|
||||
sender: Sender<Action>,
|
||||
}
|
||||
|
||||
impl EpisodeStack {
|
||||
pub fn new(sender: Sender<Action>) -> Result<EpisodeStack, Error> {
|
||||
let episodes = EpisodesView::new(sender.clone())?;
|
||||
let empty = EmptyView::new();
|
||||
let stack = gtk::Stack::new();
|
||||
|
||||
stack.add_named(&episodes.container, "episodes");
|
||||
stack.add_named(&empty.container, "empty");
|
||||
|
||||
if episodes.is_empty() {
|
||||
stack.set_visible_child_name("empty");
|
||||
} else {
|
||||
stack.set_visible_child_name("episodes");
|
||||
}
|
||||
|
||||
Ok(EpisodeStack { stack, sender })
|
||||
}
|
||||
|
||||
// Look into refactoring to a state-machine.
|
||||
pub fn update(&self) -> Result<(), Error> {
|
||||
let old = self.stack
|
||||
.get_child_by_name("episodes")
|
||||
.ok_or_else(|| format_err!("Faild to get \"episodes\" child from the stack."))?
|
||||
.downcast::<gtk::Box>()
|
||||
.map_err(|_| format_err!("Failed to downcast stack child to a Box."))?;
|
||||
debug!("Name: {:?}", WidgetExt::get_name(&old));
|
||||
|
||||
let scrolled_window = old.get_children()
|
||||
.first()
|
||||
.ok_or_else(|| format_err!("Box container has no childs."))?
|
||||
.clone()
|
||||
.downcast::<gtk::ScrolledWindow>()
|
||||
.map_err(|_| format_err!("Failed to downcast stack child to a ScrolledWindow."))?;
|
||||
debug!("Name: {:?}", WidgetExt::get_name(&scrolled_window));
|
||||
|
||||
let eps = EpisodesView::new(self.sender.clone())?;
|
||||
// Copy the vertical scrollbar adjustment from the old view into the new one.
|
||||
scrolled_window
|
||||
.get_vadjustment()
|
||||
.map(|x| eps.set_vadjustment(&x));
|
||||
|
||||
self.stack.remove(&old);
|
||||
self.stack.add_named(&eps.container, "episodes");
|
||||
|
||||
if eps.is_empty() {
|
||||
self.stack.set_visible_child_name("empty");
|
||||
} else {
|
||||
self.stack.set_visible_child_name("episodes");
|
||||
}
|
||||
|
||||
old.destroy();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_stack(&self) -> gtk::Stack {
|
||||
self.stack.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
use gtk;
|
||||
use gtk::prelude::*;
|
||||
|
||||
use failure::Error;
|
||||
use hammond_data::dbqueries::is_episodes_populated;
|
||||
use hammond_data::errors::DataError;
|
||||
|
||||
use app::Action;
|
||||
use widgets::{EmptyView, HomeView};
|
||||
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum State {
|
||||
Home,
|
||||
Empty,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HomeStack {
|
||||
empty: EmptyView,
|
||||
episodes: Rc<HomeView>,
|
||||
stack: gtk::Stack,
|
||||
state: State,
|
||||
sender: Sender<Action>,
|
||||
}
|
||||
|
||||
impl HomeStack {
|
||||
pub fn new(sender: Sender<Action>) -> Result<HomeStack, Error> {
|
||||
let episodes = HomeView::new(sender.clone())?;
|
||||
let empty = EmptyView::new();
|
||||
let stack = gtk::Stack::new();
|
||||
let state = State::Empty;
|
||||
|
||||
stack.add_named(&episodes.container, "home");
|
||||
stack.add_named(&empty.container, "empty");
|
||||
|
||||
let mut home = HomeStack {
|
||||
empty,
|
||||
episodes,
|
||||
stack,
|
||||
state,
|
||||
sender,
|
||||
};
|
||||
|
||||
home.determine_state()?;
|
||||
Ok(home)
|
||||
}
|
||||
|
||||
pub fn get_stack(&self) -> gtk::Stack {
|
||||
self.stack.clone()
|
||||
}
|
||||
|
||||
pub fn update(&mut self) -> Result<(), Error> {
|
||||
// Copy the vertical scrollbar adjustment from the old view.
|
||||
self.episodes
|
||||
.save_alignment()
|
||||
.map_err(|err| error!("Failed to set episodes_view allignment: {}", err))
|
||||
.ok();
|
||||
|
||||
self.replace_view()?;
|
||||
self.determine_state().map_err(From::from)
|
||||
}
|
||||
|
||||
fn replace_view(&mut self) -> Result<(), Error> {
|
||||
// Get the container of the view
|
||||
let old = &self.episodes.container.clone();
|
||||
let eps = HomeView::new(self.sender.clone())?;
|
||||
|
||||
// Remove the old widget and add the new one
|
||||
self.stack.remove(old);
|
||||
self.stack.add_named(&eps.container, "home");
|
||||
|
||||
// replace view in the struct too
|
||||
self.episodes = eps;
|
||||
|
||||
// This might not be needed
|
||||
old.destroy();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn switch_visible(&mut self, s: State) {
|
||||
use self::State::*;
|
||||
|
||||
match s {
|
||||
Home => {
|
||||
self.stack.set_visible_child_name("home");
|
||||
self.state = Home;
|
||||
}
|
||||
Empty => {
|
||||
self.stack.set_visible_child_name("empty");
|
||||
self.state = Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn determine_state(&mut self) -> Result<(), DataError> {
|
||||
if is_episodes_populated()? {
|
||||
self.switch_visible(State::Home);
|
||||
} else {
|
||||
self.switch_visible(State::Empty);
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
mod content;
|
||||
mod episode;
|
||||
mod home;
|
||||
mod populated;
|
||||
mod show;
|
||||
|
||||
pub use self::content::Content;
|
||||
pub use self::episode::EpisodeStack;
|
||||
pub use self::show::ShowStack;
|
||||
pub use self::home::HomeStack;
|
||||
pub use self::populated::{PopulatedStack, PopulatedState};
|
||||
pub use self::show::{ShowStack, ShowState};
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
use gtk;
|
||||
use gtk::prelude::*;
|
||||
|
||||
use failure::Error;
|
||||
|
||||
use hammond_data::dbqueries;
|
||||
use hammond_data::Podcast;
|
||||
|
||||
use app::Action;
|
||||
use widgets::{ShowWidget, ShowsView};
|
||||
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum PopulatedState {
|
||||
View,
|
||||
Widget,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PopulatedStack {
|
||||
container: gtk::Box,
|
||||
populated: Rc<ShowsView>,
|
||||
show: Rc<ShowWidget>,
|
||||
stack: gtk::Stack,
|
||||
state: PopulatedState,
|
||||
sender: Sender<Action>,
|
||||
}
|
||||
|
||||
impl PopulatedStack {
|
||||
pub fn new(sender: Sender<Action>) -> Result<PopulatedStack, Error> {
|
||||
let stack = gtk::Stack::new();
|
||||
let state = PopulatedState::View;
|
||||
let populated = ShowsView::new(sender.clone())?;
|
||||
let show = Rc::new(ShowWidget::default());
|
||||
let container = gtk::Box::new(gtk::Orientation::Horizontal, 0);
|
||||
|
||||
stack.add_named(&populated.container, "shows");
|
||||
stack.add_named(&show.container, "widget");
|
||||
container.add(&stack);
|
||||
container.show_all();
|
||||
|
||||
let show = PopulatedStack {
|
||||
container,
|
||||
stack,
|
||||
populated,
|
||||
show,
|
||||
state,
|
||||
sender,
|
||||
};
|
||||
|
||||
Ok(show)
|
||||
}
|
||||
|
||||
pub fn update(&mut self) {
|
||||
self.update_widget().map_err(|err| format!("{}", err)).ok();
|
||||
self.update_shows().map_err(|err| format!("{}", err)).ok();
|
||||
}
|
||||
|
||||
pub fn update_shows(&mut self) -> Result<(), Error> {
|
||||
let old = &self.populated.container.clone();
|
||||
debug!("Name: {:?}", WidgetExt::get_name(old));
|
||||
|
||||
let pop = ShowsView::new(self.sender.clone())?;
|
||||
self.populated = pop;
|
||||
self.stack.remove(old);
|
||||
self.stack.add_named(&self.populated.container, "shows");
|
||||
|
||||
// The current visible child might change depending on
|
||||
// removal and insertion in the gtk::Stack, so we have
|
||||
// to make sure it will stay the same.
|
||||
let s = self.state;
|
||||
self.switch_visible(s, gtk::StackTransitionType::None);
|
||||
|
||||
old.destroy();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn replace_widget(&mut self, pd: Arc<Podcast>) -> Result<(), Error> {
|
||||
let old = self.show.container.clone();
|
||||
|
||||
// save the ShowWidget vertical scrollabar alignment
|
||||
self.show
|
||||
.podcast_id()
|
||||
.map(|id| self.show.save_vadjustment(id));
|
||||
|
||||
let new = ShowWidget::new(pd, self.sender.clone());
|
||||
self.show = new;
|
||||
self.stack.remove(&old);
|
||||
self.stack.add_named(&self.show.container, "widget");
|
||||
|
||||
// The current visible child might change depending on
|
||||
// removal and insertion in the gtk::Stack, so we have
|
||||
// to make sure it will stay the same.
|
||||
let s = self.state;
|
||||
self.switch_visible(s, gtk::StackTransitionType::None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_widget(&mut self) -> Result<(), Error> {
|
||||
let old = self.show.container.clone();
|
||||
let id = self.show.podcast_id();
|
||||
if id.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let pd = dbqueries::get_podcast_from_id(id.unwrap_or_default())?;
|
||||
self.replace_widget(Arc::new(pd))?;
|
||||
|
||||
// The current visible child might change depending on
|
||||
// removal and insertion in the gtk::Stack, so we have
|
||||
// to make sure it will stay the same.
|
||||
let s = self.state;
|
||||
self.switch_visible(s, gtk::StackTransitionType::None);
|
||||
|
||||
old.destroy();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Only update widget if its podcast_id is equal to pid.
|
||||
pub fn update_widget_if_same(&mut self, pid: i32) -> Result<(), Error> {
|
||||
if self.show.podcast_id() != Some(pid) {
|
||||
debug!("Different widget. Early return");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.update_widget()
|
||||
}
|
||||
|
||||
pub fn container(&self) -> gtk::Box {
|
||||
self.container.clone()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn switch_visible(&mut self, state: PopulatedState, animation: gtk::StackTransitionType) {
|
||||
use self::PopulatedState::*;
|
||||
|
||||
match state {
|
||||
View => {
|
||||
self.stack.set_visible_child_full("shows", animation);
|
||||
self.state = View;
|
||||
}
|
||||
Widget => {
|
||||
self.stack.set_visible_child_full("widget", animation);
|
||||
self.state = Widget;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
-153
@@ -1,181 +1,96 @@
|
||||
use gtk;
|
||||
use gtk::Cast;
|
||||
use gtk::prelude::*;
|
||||
|
||||
use failure::Error;
|
||||
|
||||
use hammond_data::Podcast;
|
||||
use hammond_data::dbqueries;
|
||||
|
||||
use views::{EmptyView, ShowsPopulated};
|
||||
use hammond_data::dbqueries::is_podcasts_populated;
|
||||
|
||||
use app::Action;
|
||||
use widgets::ShowWidget;
|
||||
use stacks::PopulatedStack;
|
||||
use utils::get_ignored_shows;
|
||||
use widgets::EmptyView;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ShowState {
|
||||
Populated,
|
||||
Empty,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ShowStack {
|
||||
empty: EmptyView,
|
||||
populated: Rc<RefCell<PopulatedStack>>,
|
||||
stack: gtk::Stack,
|
||||
state: ShowState,
|
||||
sender: Sender<Action>,
|
||||
}
|
||||
|
||||
impl ShowStack {
|
||||
pub fn new(sender: Sender<Action>) -> Result<ShowStack, Error> {
|
||||
pub fn new(sender: Sender<Action>) -> Result<Self, Error> {
|
||||
let populated = Rc::new(RefCell::new(PopulatedStack::new(sender.clone())?));
|
||||
let empty = EmptyView::new();
|
||||
let stack = gtk::Stack::new();
|
||||
let state = ShowState::Empty;
|
||||
|
||||
let show = ShowStack {
|
||||
stack.add_named(&populated.borrow().container(), "populated");
|
||||
stack.add_named(&empty.container, "empty");
|
||||
|
||||
let mut show = ShowStack {
|
||||
empty,
|
||||
populated,
|
||||
stack,
|
||||
sender: sender.clone(),
|
||||
state,
|
||||
sender,
|
||||
};
|
||||
|
||||
let pop = ShowsPopulated::new(sender.clone())?;
|
||||
let widget = ShowWidget::default();
|
||||
let empty = EmptyView::new();
|
||||
|
||||
show.stack.add_named(&pop.container, "podcasts");
|
||||
show.stack.add_named(&widget.container, "widget");
|
||||
show.stack.add_named(&empty.container, "empty");
|
||||
|
||||
if pop.is_empty() {
|
||||
show.stack.set_visible_child_name("empty")
|
||||
} else {
|
||||
show.stack.set_visible_child_name("podcasts")
|
||||
}
|
||||
|
||||
show.determine_state()?;
|
||||
Ok(show)
|
||||
}
|
||||
|
||||
// pub fn update(&self) {
|
||||
// self.update_widget();
|
||||
// self.update_podcasts();
|
||||
// }
|
||||
|
||||
pub fn update_podcasts(&self) -> Result<(), Error> {
|
||||
let vis = self.stack
|
||||
.get_visible_child_name()
|
||||
.ok_or_else(|| format_err!("Failed to get visible child name."))?;
|
||||
|
||||
let old = self.stack
|
||||
.get_child_by_name("podcasts")
|
||||
.ok_or_else(|| format_err!("Faild to get \"podcasts\" child from the stack."))?
|
||||
.downcast::<gtk::Box>()
|
||||
.map_err(|_| format_err!("Failed to downcast stack child to a Box."))?;
|
||||
debug!("Name: {:?}", WidgetExt::get_name(&old));
|
||||
|
||||
let scrolled_window = old.get_children()
|
||||
.first()
|
||||
.ok_or_else(|| format_err!("Box container has no childs."))?
|
||||
.clone()
|
||||
.downcast::<gtk::ScrolledWindow>()
|
||||
.map_err(|_| format_err!("Failed to downcast stack child to a ScrolledWindow."))?;
|
||||
debug!("Name: {:?}", WidgetExt::get_name(&scrolled_window));
|
||||
|
||||
let pop = ShowsPopulated::new(self.sender.clone())?;
|
||||
// Copy the vertical scrollbar adjustment from the old view into the new one.
|
||||
scrolled_window
|
||||
.get_vadjustment()
|
||||
.map(|x| pop.set_vadjustment(&x));
|
||||
|
||||
self.stack.remove(&old);
|
||||
self.stack.add_named(&pop.container, "podcasts");
|
||||
|
||||
if pop.is_empty() {
|
||||
self.stack.set_visible_child_name("empty");
|
||||
} else if vis != "empty" {
|
||||
self.stack.set_visible_child_name(&vis);
|
||||
} else {
|
||||
self.stack.set_visible_child_name("podcasts");
|
||||
}
|
||||
|
||||
old.destroy();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn replace_widget(&self, pd: Arc<Podcast>) -> Result<(), Error> {
|
||||
let old = self.stack
|
||||
.get_child_by_name("widget")
|
||||
.ok_or_else(|| format_err!("Faild to get \"widget\" child from the stack."))?
|
||||
.downcast::<gtk::Box>()
|
||||
.map_err(|_| format_err!("Failed to downcast stack child to a Box."))?;
|
||||
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.
|
||||
let oldid = WidgetExt::get_name(&old);
|
||||
let newid = WidgetExt::get_name(&new.container);
|
||||
debug!("Old widget Name: {:?}\nNew widget Name: {:?}", oldid, newid);
|
||||
|
||||
// Only copy the old scrollbar if both widget's represent the same podcast.
|
||||
if newid == oldid {
|
||||
let scrolled_window = old.get_children()
|
||||
.first()
|
||||
.ok_or_else(|| format_err!("Box container has no childs."))?
|
||||
.clone()
|
||||
.downcast::<gtk::ScrolledWindow>()
|
||||
.map_err(|_| format_err!("Failed to downcast stack child to a ScrolledWindow."))?;
|
||||
debug!("Name: {:?}", WidgetExt::get_name(&scrolled_window));
|
||||
|
||||
// Copy the vertical scrollbar adjustment from the old view into the new one.
|
||||
scrolled_window
|
||||
.get_vadjustment()
|
||||
.map(|x| new.set_vadjustment(&x));
|
||||
}
|
||||
|
||||
self.stack.remove(&old);
|
||||
self.stack.add_named(&new.container, "widget");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_widget(&self) -> Result<(), Error> {
|
||||
let vis = self.stack
|
||||
.get_visible_child_name()
|
||||
.ok_or_else(|| format_err!("Failed to get visible child name."))?;
|
||||
let old = self.stack
|
||||
.get_child_by_name("widget")
|
||||
.ok_or_else(|| format_err!("Faild to get \"widget\" child from the stack."))?;
|
||||
|
||||
let id = WidgetExt::get_name(&old);
|
||||
if id == Some("GtkBox".to_string()) || id.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let id = id.ok_or_else(|| format_err!("Failed to get widget's name."))?;
|
||||
let pd = dbqueries::get_podcast_from_id(id.parse::<i32>()?)?;
|
||||
self.replace_widget(Arc::new(pd))?;
|
||||
self.stack.set_visible_child_name(&vis);
|
||||
old.destroy();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Only update widget if it's podcast_id is equal to pid.
|
||||
pub fn update_widget_if_same(&self, pid: i32) -> Result<(), Error> {
|
||||
let old = self.stack
|
||||
.get_child_by_name("widget")
|
||||
.ok_or_else(|| format_err!("Faild to get \"widget\" child from the stack."))?;
|
||||
|
||||
let id = WidgetExt::get_name(&old);
|
||||
if id != Some(pid.to_string()) || id.is_none() {
|
||||
debug!("Different widget. Early return");
|
||||
return Ok(());
|
||||
}
|
||||
self.update_widget()
|
||||
}
|
||||
|
||||
pub fn switch_podcasts_animated(&self) {
|
||||
self.stack
|
||||
.set_visible_child_full("podcasts", gtk::StackTransitionType::SlideRight);
|
||||
}
|
||||
|
||||
pub fn switch_widget_animated(&self) {
|
||||
self.stack
|
||||
.set_visible_child_full("widget", gtk::StackTransitionType::SlideLeft)
|
||||
}
|
||||
|
||||
pub fn get_stack(&self) -> gtk::Stack {
|
||||
self.stack.clone()
|
||||
}
|
||||
|
||||
pub fn populated(&self) -> Rc<RefCell<PopulatedStack>> {
|
||||
self.populated.clone()
|
||||
}
|
||||
|
||||
pub fn update(&mut self) -> Result<(), Error> {
|
||||
self.populated.borrow_mut().update();
|
||||
self.determine_state()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn switch_visible(&mut self, s: ShowState) {
|
||||
use self::ShowState::*;
|
||||
|
||||
match s {
|
||||
Populated => {
|
||||
self.stack.set_visible_child_name("populated");
|
||||
self.state = Populated;
|
||||
}
|
||||
Empty => {
|
||||
self.stack.set_visible_child_name("empty");
|
||||
self.state = Empty;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn determine_state(&mut self) -> Result<(), Error> {
|
||||
use self::ShowState::*;
|
||||
|
||||
let ign = get_ignored_shows()?;
|
||||
debug!("IGNORED SHOWS {:?}", ign);
|
||||
if is_podcasts_populated(&ign)? {
|
||||
self.switch_visible(Populated);
|
||||
} else {
|
||||
self.switch_visible(Empty);
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+167
-107
@@ -1,11 +1,13 @@
|
||||
#![cfg_attr(feature = "cargo-clippy", allow(type_complexity))]
|
||||
|
||||
use gdk::FrameClockExt;
|
||||
use gdk_pixbuf::Pixbuf;
|
||||
use gio::{Settings, SettingsExt};
|
||||
use glib;
|
||||
use gtk;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{IsA, Widget};
|
||||
|
||||
use chrono::prelude::*;
|
||||
use failure::Error;
|
||||
use rayon;
|
||||
use regex::Regex;
|
||||
@@ -14,21 +16,128 @@ 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_data::Source;
|
||||
use hammond_downloader::downloader;
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::{Mutex, RwLock};
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::*;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Mutex, RwLock};
|
||||
|
||||
use app::Action;
|
||||
|
||||
use chrono::Duration;
|
||||
use chrono::prelude::*;
|
||||
/// Lazy evaluates and loads widgets to the parent `container` widget.
|
||||
///
|
||||
/// Accepts an `IntoIterator`, `data`, as the source from which each widget
|
||||
/// will be constructed. An `FnMut` function that returns the desired
|
||||
/// widget should be passed as the widget `constructor`. You can also specify
|
||||
/// a `callback` that will be executed when the iteration finish.
|
||||
///
|
||||
/// ```no_run
|
||||
/// # struct Message;
|
||||
/// # struct MessageWidget(gtk::Label);
|
||||
///
|
||||
/// # impl MessageWidget {
|
||||
/// # fn new(_: Message) -> Self {
|
||||
/// # MessageWidget(gtk::Label::new("A message"))
|
||||
/// # }
|
||||
/// # }
|
||||
///
|
||||
/// let messages: Vec<Message> = Vec::new();
|
||||
/// let list = gtk::ListBox::new();
|
||||
/// let constructor = |m| { MessageWidget::new(m).0};
|
||||
/// lazy_load(messages, list, constructor, || {});
|
||||
/// ```
|
||||
///
|
||||
/// If you have already constructed the widgets and only want to
|
||||
/// load them to the parent you can pass a closure that returns it's
|
||||
/// own argument to the constructor.
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use std::collections::binary_heap::BinaryHeap;
|
||||
/// let widgets: BinaryHeap<gtk::Button> = BinaryHeap::new();
|
||||
/// let list = gtk::ListBox::new();
|
||||
/// lazy_load(widgets, list, |w| w, || {});
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn lazy_load<T, C, F, W, U>(data: T, container: C, mut contructor: F, callback: U)
|
||||
where
|
||||
T: IntoIterator + 'static,
|
||||
T::Item: 'static,
|
||||
C: ContainerExt + 'static,
|
||||
F: FnMut(T::Item) -> W + 'static,
|
||||
W: IsA<Widget>,
|
||||
U: Fn() + 'static,
|
||||
{
|
||||
let func = move |x| container.add(&contructor(x));
|
||||
lazy_load_full(data, func, callback);
|
||||
}
|
||||
|
||||
/// Iterate over `data` and execute `func` using a `gtk::idle_add()`,
|
||||
/// when the iteration finishes, it executes `finish_callback`.
|
||||
///
|
||||
/// This is a more flexible version of `lazy_load` with less constrains.
|
||||
/// If you just want to lazy add `widgets` to a `container` check if
|
||||
/// `lazy_load` fits your needs first.
|
||||
#[inline]
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(redundant_closure))]
|
||||
pub fn lazy_load_full<T, F, U>(data: T, mut func: F, finish_callback: U)
|
||||
where
|
||||
T: IntoIterator + 'static,
|
||||
T::Item: 'static,
|
||||
F: FnMut(T::Item) + 'static,
|
||||
U: Fn() + 'static,
|
||||
{
|
||||
let mut data = data.into_iter();
|
||||
gtk::idle_add(move || {
|
||||
data.next()
|
||||
.map(|x| func(x))
|
||||
.map(|_| glib::Continue(true))
|
||||
.unwrap_or_else(|| {
|
||||
finish_callback();
|
||||
glib::Continue(false)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Kudos to Julian Sparber
|
||||
// https://blogs.gnome.org/jsparber/2018/04/29/animate-a-scrolledwindow/
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(float_cmp))]
|
||||
pub fn smooth_scroll_to(view: >k::ScrolledWindow, target: >k::Adjustment) {
|
||||
if let Some(adj) = view.get_vadjustment() {
|
||||
if let Some(clock) = view.get_frame_clock() {
|
||||
let duration = 200;
|
||||
let start = adj.get_value();
|
||||
let end = target.get_value();
|
||||
let start_time = clock.get_frame_time();
|
||||
let end_time = start_time + 1000 * duration;
|
||||
|
||||
view.add_tick_callback(move |_, clock| {
|
||||
let now = clock.get_frame_time();
|
||||
// FIXME: `adj.get_value != end` is a float comparison...
|
||||
if now < end_time && adj.get_value().abs() != end.abs() {
|
||||
let mut t = (now - start_time) as f64 / (end_time - start_time) as f64;
|
||||
t = ease_out_cubic(t);
|
||||
adj.set_value(start + t * (end - start));
|
||||
glib::Continue(true)
|
||||
} else {
|
||||
adj.set_value(end);
|
||||
glib::Continue(false)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// From clutter-easing.c, based on Robert Penner's
|
||||
// infamous easing equations, MIT license.
|
||||
fn ease_out_cubic(t: f64) -> f64 {
|
||||
let p = t - 1f64;
|
||||
p * p * p + 1f64
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref IGNORESHOWS: Arc<Mutex<HashSet<i32>>> = Arc::new(Mutex::new(HashSet::new()));
|
||||
@@ -56,78 +165,56 @@ pub fn get_ignored_shows() -> Result<Vec<i32>, Error> {
|
||||
}
|
||||
|
||||
pub fn cleanup(cleanup_date: DateTime<Utc>) {
|
||||
if let Err(err) = checkup(cleanup_date) {
|
||||
error!("Check up failed: {}", err);
|
||||
}
|
||||
checkup(cleanup_date)
|
||||
.map_err(|err| error!("Check up failed: {}", err))
|
||||
.ok();
|
||||
}
|
||||
|
||||
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
|
||||
pub fn refresh<S>(source: Option<S>, sender: Sender<Action>)
|
||||
where
|
||||
S: IntoIterator<Item = Source> + Send + 'static,
|
||||
{
|
||||
refresh_feed(source, sender)
|
||||
.map_err(|err| error!("Failed to update feeds: {}", err))
|
||||
.ok();
|
||||
}
|
||||
|
||||
/// 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)?;
|
||||
fn refresh_feed<S>(source: Option<S>, sender: Sender<Action>) -> Result<(), Error>
|
||||
where
|
||||
S: IntoIterator<Item = Source> + Send + 'static,
|
||||
{
|
||||
sender
|
||||
.send(Action::HeaderBarShowUpdateIndicator)
|
||||
.map_err(|err| error!("Action Sender: {}", err))
|
||||
.ok();
|
||||
|
||||
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.
|
||||
// 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.
|
||||
if sources.len() == 1 {
|
||||
let source = sources.remove(0);
|
||||
let id = source.id();
|
||||
if let Err(err) = pipeline::index_single_source(source, false) {
|
||||
error!("Error While trying to update the database.");
|
||||
error!("Error msg: {}", err);
|
||||
if let Ok(source) = dbqueries::get_source_from_id(id) {
|
||||
if let Err(err) = pipeline::index_single_source(source, false) {
|
||||
error!("Error While trying to update the database.");
|
||||
error!("Error msg: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
// This is what would normally run
|
||||
} else if let Err(err) = pipeline::run(sources, false) {
|
||||
error!("Error While trying to update the database.");
|
||||
error!("Error msg: {}", err);
|
||||
}
|
||||
if let Some(s) = source {
|
||||
// Refresh only specified feeds
|
||||
pipeline::run(s, false)
|
||||
.map_err(|err| error!("Error: {}", err))
|
||||
.map_err(|_| error!("Error While trying to update the database."))
|
||||
.ok();
|
||||
} else {
|
||||
// Refresh all the feeds
|
||||
dbqueries::get_sources()
|
||||
.map(|s| s.into_iter())
|
||||
.and_then(|s| pipeline::run(s, false))
|
||||
.map_err(|err| error!("Error: {}", err))
|
||||
.ok();
|
||||
};
|
||||
|
||||
sender
|
||||
.send(Action::HeaderBarHideUpdateIndicator)
|
||||
.expect("Action channel blew up.");
|
||||
.map_err(|err| error!("Action Sender: {}", err))
|
||||
.ok();
|
||||
sender
|
||||
.send(Action::RefreshAllViews)
|
||||
.expect("Action channel blew up.");
|
||||
.map_err(|err| error!("Action Sender: {}", err))
|
||||
.ok();
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
@@ -146,19 +233,16 @@ 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 set_image_from_path(
|
||||
image: >k::Image,
|
||||
pd: Arc<PodcastCoverQuery>,
|
||||
size: u32,
|
||||
) -> Result<(), Error> {
|
||||
#[inline]
|
||||
pub fn set_image_from_path(image: >k::Image, podcast_id: i32, 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);
|
||||
if guard.contains(&podcast_id) {
|
||||
let callback = clone!(image => move || {
|
||||
let _ = set_image_from_path(&image, podcast_id, size);
|
||||
glib::Continue(false)
|
||||
});
|
||||
gtk::timeout_add(250, callback);
|
||||
@@ -169,7 +253,7 @@ pub fn set_image_from_path(
|
||||
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)) {
|
||||
if let Some(guard) = hashmap.get(&(podcast_id, size)) {
|
||||
guard
|
||||
.lock()
|
||||
.map_err(|err| format_err!("SendCell Mutex: {}", err))
|
||||
@@ -185,16 +269,20 @@ pub fn set_image_from_path(
|
||||
}
|
||||
|
||||
let (sender, receiver) = channel();
|
||||
let pd_ = pd.clone();
|
||||
THREADPOOL.spawn(move || {
|
||||
if let Ok(mut guard) = COVER_DL_REGISTRY.write() {
|
||||
guard.insert(pd_.id());
|
||||
guard.insert(podcast_id);
|
||||
}
|
||||
|
||||
let _ = sender.send(downloader::cache_image(&pd_));
|
||||
if let Ok(pd) = dbqueries::get_podcast_cover_from_id(podcast_id) {
|
||||
sender
|
||||
.send(downloader::cache_image(&pd))
|
||||
.map_err(|err| error!("Action Sender: {}", err))
|
||||
.ok();
|
||||
}
|
||||
|
||||
if let Ok(mut guard) = COVER_DL_REGISTRY.write() {
|
||||
guard.remove(&pd_.id());
|
||||
guard.remove(&podcast_id);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -205,7 +293,7 @@ pub fn set_image_from_path(
|
||||
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())));
|
||||
hashmap.insert((podcast_id, size), Mutex::new(SendCell::new(px.clone())));
|
||||
image.set_from_pixbuf(&px);
|
||||
}
|
||||
}
|
||||
@@ -247,45 +335,17 @@ fn lookup_id(id: u32) -> Result<String, Error> {
|
||||
.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;
|
||||
|
||||
#[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]
|
||||
// Disabled till https://gitlab.gnome.org/alatiera/Hammond/issues/56
|
||||
// Disabled till https://gitlab.gnome.org/World/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
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
mod shows;
|
||||
mod episodes;
|
||||
mod empty;
|
||||
|
||||
pub use self::empty::EmptyView;
|
||||
pub use self::episodes::EpisodesView;
|
||||
pub use self::shows::ShowsPopulated;
|
||||
@@ -6,6 +6,7 @@ pub struct EmptyView {
|
||||
}
|
||||
|
||||
impl Default for EmptyView {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/empty_view.ui");
|
||||
let view: gtk::Box = builder.get_object("empty_view").unwrap();
|
||||
@@ -15,6 +16,7 @@ impl Default for EmptyView {
|
||||
}
|
||||
|
||||
impl EmptyView {
|
||||
#[inline]
|
||||
pub fn new() -> EmptyView {
|
||||
EmptyView::default()
|
||||
}
|
||||
+140
-115
@@ -7,9 +7,9 @@ use humansize::FileSize;
|
||||
use open;
|
||||
use take_mut;
|
||||
|
||||
use hammond_data::{EpisodeWidgetQuery, Podcast};
|
||||
use hammond_data::dbqueries;
|
||||
use hammond_data::utils::get_download_folder;
|
||||
use hammond_data::EpisodeWidgetQuery;
|
||||
|
||||
use app::Action;
|
||||
use manager;
|
||||
@@ -19,8 +19,8 @@ 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;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EpisodeWidget {
|
||||
@@ -28,10 +28,11 @@ pub struct EpisodeWidget {
|
||||
date: DateMachine,
|
||||
duration: DurationMachine,
|
||||
title: Rc<RefCell<TitleMachine>>,
|
||||
media: Arc<Mutex<MediaMachine>>,
|
||||
media: Rc<RefCell<MediaMachine>>,
|
||||
}
|
||||
|
||||
impl Default for EpisodeWidget {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episode_widget.ui");
|
||||
|
||||
@@ -65,7 +66,7 @@ impl Default for EpisodeWidget {
|
||||
separator2,
|
||||
prog_separator,
|
||||
);
|
||||
let media_machine = Arc::new(Mutex::new(media));
|
||||
let media_machine = Rc::new(RefCell::new(media));
|
||||
|
||||
EpisodeWidget {
|
||||
container,
|
||||
@@ -78,15 +79,15 @@ impl Default for EpisodeWidget {
|
||||
}
|
||||
|
||||
impl EpisodeWidget {
|
||||
pub fn new(episode: EpisodeWidgetQuery, sender: Sender<Action>) -> EpisodeWidget {
|
||||
#[inline]
|
||||
pub fn new(episode: EpisodeWidgetQuery, sender: &Sender<Action>) -> EpisodeWidget {
|
||||
let mut widget = EpisodeWidget::default();
|
||||
widget.init(episode, sender);
|
||||
widget
|
||||
}
|
||||
|
||||
fn init(&mut self, episode: EpisodeWidgetQuery, sender: Sender<Action>) {
|
||||
WidgetExt::set_name(&self.container, &episode.rowid().to_string());
|
||||
|
||||
#[inline]
|
||||
fn init(&mut self, episode: EpisodeWidgetQuery, sender: &Sender<Action>) {
|
||||
// Set the date label.
|
||||
self.set_date(episode.epoch());
|
||||
|
||||
@@ -97,40 +98,49 @@ impl EpisodeWidget {
|
||||
self.set_duration(episode.duration());
|
||||
|
||||
// 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);
|
||||
}
|
||||
determine_media_state(&self.media, &episode)
|
||||
.map_err(|err| error!("Error: {}", err))
|
||||
.map_err(|_| error!("Could not determine Media State"))
|
||||
.ok();
|
||||
|
||||
let episode = Arc::new(Mutex::new(episode));
|
||||
self.connect_buttons(episode, sender);
|
||||
self.connect_buttons(&episode, sender);
|
||||
}
|
||||
|
||||
fn connect_buttons(&self, episode: Arc<Mutex<EpisodeWidgetQuery>>, sender: Sender<Action>) {
|
||||
#[inline]
|
||||
fn connect_buttons(&self, episode: &Arc<Mutex<EpisodeWidgetQuery>>, sender: &Sender<Action>) {
|
||||
let title = self.title.clone();
|
||||
if let Ok(media) = self.media.lock() {
|
||||
if let Ok(media) = self.media.try_borrow_mut() {
|
||||
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.clone(), sender.clone()){
|
||||
error!("Error: {}", err);
|
||||
};
|
||||
on_play_bttn_clicked(&mut ep, &title, &sender)
|
||||
.map_err(|err| error!("Error: {}", err))
|
||||
.ok();
|
||||
}
|
||||
}));
|
||||
|
||||
media.download_connect_clicked(clone!(episode, sender => move |dl| {
|
||||
let media_machine = self.media.clone();
|
||||
media.download_connect_clicked(clone!(media_machine, episode, sender => move |dl| {
|
||||
// Make the button insensitive so it won't be pressed twice
|
||||
dl.set_sensitive(false);
|
||||
if let Ok(ep) = episode.lock() {
|
||||
if let Err(err) = on_download_clicked(&ep, sender.clone()) {
|
||||
error!("Download failed to start.");
|
||||
error!("Error: {}", err);
|
||||
} else {
|
||||
info!("Donwload started succesfully.");
|
||||
}
|
||||
on_download_clicked(&ep, &sender)
|
||||
.and_then(|_| {
|
||||
info!("Donwload started succesfully.");
|
||||
determine_media_state(&media_machine, &ep)
|
||||
})
|
||||
.map_err(|err| error!("Error: {}", err))
|
||||
.map_err(|_| error!("Could not determine Media State"))
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Restore sensitivity after operations above complete
|
||||
dl.set_sensitive(true);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Determine the title state.
|
||||
fn set_title(&mut self, episode: &EpisodeWidgetQuery) {
|
||||
let mut machine = self.title.borrow_mut();
|
||||
@@ -140,69 +150,107 @@ impl EpisodeWidget {
|
||||
});
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Set the date label depending on the current time.
|
||||
fn set_date(&mut self, epoch: i32) {
|
||||
let machine = &mut self.date;
|
||||
take_mut::take(machine, |date| date.determine_state(i64::from(epoch)));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Set the duration label.
|
||||
fn set_duration(&mut self, seconds: Option<i32>) {
|
||||
let machine = &mut self.duration;
|
||||
take_mut::take(machine, |duration| duration.determine_state(seconds));
|
||||
}
|
||||
|
||||
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>()?;
|
||||
|
||||
let active_dl = || -> Result<Option<_>, Error> {
|
||||
let m = manager::ACTIVE_DOWNLOADS
|
||||
.read()
|
||||
.map_err(|_| format_err!("Failed to get a lock on the mutex."))?;
|
||||
|
||||
Ok(m.get(&id).cloned())
|
||||
}()?;
|
||||
|
||||
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(),
|
||||
)
|
||||
});
|
||||
|
||||
// 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(), 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(), self.media.clone());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn on_download_clicked(ep: &EpisodeWidgetQuery, sender: Sender<Action>) -> Result<(), Error> {
|
||||
fn determine_media_state(
|
||||
media_machine: &Rc<RefCell<MediaMachine>>,
|
||||
episode: &EpisodeWidgetQuery,
|
||||
) -> Result<(), Error> {
|
||||
let id = episode.rowid();
|
||||
let active_dl = || -> Result<Option<_>, Error> {
|
||||
let m = manager::ACTIVE_DOWNLOADS
|
||||
.read()
|
||||
.map_err(|_| format_err!("Failed to get a lock on the mutex."))?;
|
||||
|
||||
Ok(m.get(&id).cloned())
|
||||
}()?;
|
||||
|
||||
let mut lock = media_machine.try_borrow_mut()?;
|
||||
take_mut::take(lock.deref_mut(), |media| {
|
||||
media.determine_state(
|
||||
episode.length(),
|
||||
active_dl.is_some(),
|
||||
episode.local_uri().is_some(),
|
||||
)
|
||||
});
|
||||
|
||||
// Show or hide the play/delete/download buttons upon widget initialization.
|
||||
if let Some(prog) = active_dl {
|
||||
// set a callback that will update the state when the download finishes
|
||||
let id = episode.rowid();
|
||||
let callback = clone!(media_machine => move || {
|
||||
if let Ok(guard) = manager::ACTIVE_DOWNLOADS.read() {
|
||||
if !guard.contains_key(&id) {
|
||||
if let Ok(ep) = dbqueries::get_episode_widget_from_rowid(id) {
|
||||
determine_media_state(&media_machine, &ep)
|
||||
.map_err(|err| error!("Error: {}", err))
|
||||
.map_err(|_| error!("Could not determine Media State"))
|
||||
.ok();
|
||||
|
||||
return glib::Continue(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::Continue(true)
|
||||
});
|
||||
gtk::timeout_add(250, callback);
|
||||
|
||||
lock.cancel_connect_clicked(clone!(prog, media_machine => move |_| {
|
||||
if let Ok(mut m) = prog.lock() {
|
||||
m.cancel();
|
||||
}
|
||||
|
||||
if let Ok(mut lock) = media_machine.try_borrow_mut() {
|
||||
if let Ok(episode) = dbqueries::get_episode_widget_from_rowid(id) {
|
||||
take_mut::take(lock.deref_mut(), |media| {
|
||||
media.determine_state(
|
||||
episode.length(),
|
||||
false,
|
||||
episode.local_uri().is_some(),
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
}));
|
||||
drop(lock);
|
||||
|
||||
// Setup a callback that will update the progress bar.
|
||||
update_progressbar_callback(&prog, &media_machine, 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, &media_machine);
|
||||
}
|
||||
|
||||
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())?;
|
||||
|
||||
// Start a new download.
|
||||
manager::add(ep.rowid(), download_fold, sender.clone())?;
|
||||
manager::add(ep.rowid(), download_fold)?;
|
||||
|
||||
// Update Views
|
||||
sender.send(Action::RefreshEpisodesView)?;
|
||||
sender.send(Action::RefreshWidgetIfVis)?;
|
||||
sender.send(Action::RefreshEpisodesViewBGR)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -210,8 +258,8 @@ fn on_download_clicked(ep: &EpisodeWidgetQuery, sender: Sender<Action>) -> Resul
|
||||
#[inline]
|
||||
fn on_play_bttn_clicked(
|
||||
episode: &mut EpisodeWidgetQuery,
|
||||
title: Rc<RefCell<TitleMachine>>,
|
||||
sender: Sender<Action>,
|
||||
title: &Rc<RefCell<TitleMachine>>,
|
||||
sender: &Sender<Action>,
|
||||
) -> Result<(), Error> {
|
||||
open_uri(episode.rowid())?;
|
||||
episode.set_played_now()?;
|
||||
@@ -225,6 +273,7 @@ fn on_play_bttn_clicked(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn open_uri(rowid: i32) -> Result<(), Error> {
|
||||
let uri = dbqueries::get_episode_local_uri_from_id(rowid)?
|
||||
.ok_or_else(|| format_err!("Expected Some found None."))?;
|
||||
@@ -243,24 +292,22 @@ fn open_uri(rowid: i32) -> Result<(), Error> {
|
||||
#[inline]
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(if_same_then_else))]
|
||||
fn update_progressbar_callback(
|
||||
prog: Arc<Mutex<manager::Progress>>,
|
||||
media: Arc<Mutex<MediaMachine>>,
|
||||
prog: &Arc<Mutex<manager::Progress>>,
|
||||
media: &Rc<RefCell<MediaMachine>>,
|
||||
episode_rowid: i32,
|
||||
) {
|
||||
timeout_add(
|
||||
400,
|
||||
clone!(prog, media => move || {
|
||||
progress_bar_helper(prog.clone(), media.clone(), episode_rowid)
|
||||
.unwrap_or(glib::Continue(false))
|
||||
}),
|
||||
);
|
||||
let callback = clone!(prog, media => move || {
|
||||
progress_bar_helper(&prog, &media, episode_rowid)
|
||||
.unwrap_or(glib::Continue(false))
|
||||
});
|
||||
timeout_add(300, callback);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[allow(if_same_then_else)]
|
||||
fn progress_bar_helper(
|
||||
prog: Arc<Mutex<manager::Progress>>,
|
||||
media: Arc<Mutex<MediaMachine>>,
|
||||
prog: &Arc<Mutex<manager::Progress>>,
|
||||
media: &Rc<RefCell<MediaMachine>>,
|
||||
episode_rowid: i32,
|
||||
) -> Result<glib::Continue, Error> {
|
||||
let (fraction, downloaded) = {
|
||||
@@ -277,8 +324,9 @@ fn progress_bar_helper(
|
||||
.file_size(SIZE_OPTS.clone())
|
||||
.map_err(|err| format_err!("{}", err))?;
|
||||
|
||||
let mut m = media.lock().unwrap();
|
||||
m.update_progress(&size, fraction);
|
||||
if let Ok(mut m) = media.try_borrow_mut() {
|
||||
m.update_progress(&size, fraction);
|
||||
}
|
||||
}
|
||||
|
||||
// info!("Fraction: {}", progress_bar.get_fraction());
|
||||
@@ -306,21 +354,19 @@ fn progress_bar_helper(
|
||||
// relying to the RSS feed.
|
||||
#[inline]
|
||||
fn update_total_size_callback(
|
||||
prog: Arc<Mutex<manager::Progress>>,
|
||||
media: Arc<Mutex<MediaMachine>>,
|
||||
prog: &Arc<Mutex<manager::Progress>>,
|
||||
media: &Rc<RefCell<MediaMachine>>,
|
||||
) {
|
||||
timeout_add(
|
||||
500,
|
||||
clone!(prog, media => move || {
|
||||
total_size_helper(prog.clone(), media.clone()).unwrap_or(glib::Continue(true))
|
||||
}),
|
||||
);
|
||||
let callback = clone!(prog, media => move || {
|
||||
total_size_helper(&prog, &media).unwrap_or(glib::Continue(true))
|
||||
});
|
||||
timeout_add(500, callback);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_size_helper(
|
||||
prog: Arc<Mutex<manager::Progress>>,
|
||||
media: Arc<Mutex<MediaMachine>>,
|
||||
prog: &Arc<Mutex<manager::Progress>>,
|
||||
media: &Rc<RefCell<MediaMachine>>,
|
||||
) -> Result<glib::Continue, Error> {
|
||||
// Get the total_bytes.
|
||||
let total_bytes = {
|
||||
@@ -332,7 +378,7 @@ fn total_size_helper(
|
||||
debug!("Total Size: {}", total_bytes);
|
||||
if total_bytes != 0 {
|
||||
// Update the total_size label
|
||||
if let Ok(mut m) = media.lock() {
|
||||
if let Ok(mut m) = media.try_borrow_mut() {
|
||||
take_mut::take(m.deref_mut(), |machine| {
|
||||
machine.set_size(Some(total_bytes as i32))
|
||||
});
|
||||
@@ -349,24 +395,3 @@ fn total_size_helper(
|
||||
// let mut ep = dbqueries::get_episode_from_rowid(episode_id)?.into();
|
||||
// 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)?;
|
||||
|
||||
let list = gtk::ListBox::new();
|
||||
list.set_visible(true);
|
||||
list.set_selection_mode(gtk::SelectionMode::None);
|
||||
|
||||
if episodes.is_empty() {
|
||||
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);
|
||||
}
|
||||
|
||||
episodes.into_iter().for_each(|ep| {
|
||||
let widget = EpisodeWidget::new(ep, sender.clone());
|
||||
list.add(&widget.container);
|
||||
});
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
@@ -13,9 +13,7 @@ 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;
|
||||
use std::sync::Arc;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref SIZE_OPTS: Arc<size_opts::FileSizeOpts> = {
|
||||
@@ -63,6 +61,7 @@ pub struct Title<S> {
|
||||
|
||||
impl<S> Title<S> {
|
||||
#[allow(unused_must_use)]
|
||||
#[inline]
|
||||
// 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) {
|
||||
@@ -71,6 +70,7 @@ impl<S> Title<S> {
|
||||
}
|
||||
|
||||
impl Title<Normal> {
|
||||
#[inline]
|
||||
fn new(title: gtk::Label) -> Self {
|
||||
Title {
|
||||
title,
|
||||
@@ -80,6 +80,7 @@ impl Title<Normal> {
|
||||
}
|
||||
|
||||
impl From<Title<Normal>> for Title<GreyedOut> {
|
||||
#[inline]
|
||||
fn from(f: Title<Normal>) -> Self {
|
||||
f.title
|
||||
.get_style_context()
|
||||
@@ -93,6 +94,7 @@ impl From<Title<Normal>> for Title<GreyedOut> {
|
||||
}
|
||||
|
||||
impl From<Title<GreyedOut>> for Title<Normal> {
|
||||
#[inline]
|
||||
fn from(f: Title<GreyedOut>) -> Self {
|
||||
f.title
|
||||
.get_style_context()
|
||||
@@ -112,11 +114,13 @@ pub enum TitleMachine {
|
||||
}
|
||||
|
||||
impl TitleMachine {
|
||||
#[inline]
|
||||
pub fn new(label: gtk::Label, is_played: bool) -> Self {
|
||||
let m = TitleMachine::Normal(Title::<Normal>::new(label));
|
||||
m.determine_state(is_played)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn determine_state(self, is_played: bool) -> Self {
|
||||
use self::TitleMachine::*;
|
||||
|
||||
@@ -128,6 +132,7 @@ impl TitleMachine {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_title(&mut self, s: &str) {
|
||||
use self::TitleMachine::*;
|
||||
|
||||
@@ -151,6 +156,7 @@ pub struct Date<S> {
|
||||
}
|
||||
|
||||
impl<S> Date<S> {
|
||||
#[inline]
|
||||
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());
|
||||
@@ -162,6 +168,7 @@ impl<S> Date<S> {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
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());
|
||||
@@ -175,6 +182,7 @@ impl<S> Date<S> {
|
||||
}
|
||||
|
||||
impl Date<UnInitialized> {
|
||||
#[inline]
|
||||
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());
|
||||
@@ -195,16 +203,18 @@ pub enum DateMachine {
|
||||
}
|
||||
|
||||
impl DateMachine {
|
||||
#[inline]
|
||||
pub fn new(label: gtk::Label, epoch: i64) -> Self {
|
||||
let m = DateMachine::UnInitialized(Date::<UnInitialized>::new(label, epoch));
|
||||
m.determine_state(epoch)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn determine_state(self, epoch: i64) -> Self {
|
||||
use self::DateMachine::*;
|
||||
|
||||
let ts = Utc.timestamp(epoch, 0);
|
||||
let is_old = !(NOW.year() == ts.year());
|
||||
let is_old = NOW.year() != ts.year();
|
||||
|
||||
match (self, is_old) {
|
||||
// Into Usual
|
||||
@@ -229,6 +239,7 @@ pub struct Duration<S: Visibility> {
|
||||
}
|
||||
|
||||
impl<S: Visibility> Duration<S> {
|
||||
#[inline]
|
||||
// This needs a better name.
|
||||
// TODO: make me mut
|
||||
fn set_duration(&self, minutes: i64) {
|
||||
@@ -237,6 +248,7 @@ impl<S: Visibility> Duration<S> {
|
||||
}
|
||||
|
||||
impl Duration<Hidden> {
|
||||
#[inline]
|
||||
fn new(duration: gtk::Label, separator: gtk::Label) -> Self {
|
||||
duration.hide();
|
||||
separator.hide();
|
||||
@@ -250,6 +262,7 @@ impl Duration<Hidden> {
|
||||
}
|
||||
|
||||
impl From<Duration<Hidden>> for Duration<Shown> {
|
||||
#[inline]
|
||||
fn from(f: Duration<Hidden>) -> Self {
|
||||
f.duration.show();
|
||||
f.separator.show();
|
||||
@@ -263,6 +276,7 @@ impl From<Duration<Hidden>> for Duration<Shown> {
|
||||
}
|
||||
|
||||
impl From<Duration<Shown>> for Duration<Hidden> {
|
||||
#[inline]
|
||||
fn from(f: Duration<Shown>) -> Self {
|
||||
f.duration.hide();
|
||||
f.separator.hide();
|
||||
@@ -282,11 +296,13 @@ pub enum DurationMachine {
|
||||
}
|
||||
|
||||
impl DurationMachine {
|
||||
#[inline]
|
||||
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)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn determine_state(self, seconds: Option<i32>) -> Self {
|
||||
match (self, seconds) {
|
||||
(d @ DurationMachine::Hidden(_), None) => d,
|
||||
@@ -321,6 +337,7 @@ pub struct Size<S> {
|
||||
}
|
||||
|
||||
impl<S> Size<S> {
|
||||
#[inline]
|
||||
fn set_size(self, s: &str) -> Size<Shown> {
|
||||
self.size.set_text(s);
|
||||
self.size.show();
|
||||
@@ -332,6 +349,7 @@ impl<S> Size<S> {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
// 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
|
||||
@@ -346,6 +364,7 @@ impl<S> Size<S> {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn into_hidden(self) -> Size<Hidden> {
|
||||
self.size.hide();
|
||||
self.separator.hide();
|
||||
@@ -359,6 +378,7 @@ impl<S> Size<S> {
|
||||
}
|
||||
|
||||
impl Size<UnInitialized> {
|
||||
#[inline]
|
||||
fn new(size: gtk::Label, separator: gtk::Label) -> Self {
|
||||
size.hide();
|
||||
separator.hide();
|
||||
@@ -392,6 +412,7 @@ pub struct DownloadPlay<S> {
|
||||
}
|
||||
|
||||
impl<S> DownloadPlay<S> {
|
||||
#[inline]
|
||||
// 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
|
||||
@@ -406,6 +427,7 @@ impl<S> DownloadPlay<S> {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn into_fetchable(self) -> DownloadPlay<Download> {
|
||||
self.play.hide();
|
||||
self.download.show();
|
||||
@@ -417,6 +439,7 @@ impl<S> DownloadPlay<S> {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn into_hidden(self) -> DownloadPlay<Hidden> {
|
||||
self.play.hide();
|
||||
self.download.hide();
|
||||
@@ -428,6 +451,7 @@ impl<S> DownloadPlay<S> {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn download_connect_clicked<F: Fn(>k::Button) + 'static>(
|
||||
&self,
|
||||
f: F,
|
||||
@@ -435,12 +459,14 @@ impl<S> DownloadPlay<S> {
|
||||
self.download.connect_clicked(f)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn play_connect_clicked<F: Fn(>k::Button) + 'static>(&self, f: F) -> glib::SignalHandlerId {
|
||||
self.play.connect_clicked(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl DownloadPlay<UnInitialized> {
|
||||
#[inline]
|
||||
fn new(play: gtk::Button, download: gtk::Button) -> Self {
|
||||
play.hide();
|
||||
download.hide();
|
||||
@@ -463,6 +489,7 @@ pub struct Progress<S> {
|
||||
}
|
||||
|
||||
impl<S> Progress<S> {
|
||||
#[inline]
|
||||
fn into_shown(self) -> Progress<Shown> {
|
||||
self.bar.show();
|
||||
self.cancel.show();
|
||||
@@ -478,6 +505,7 @@ impl<S> Progress<S> {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn into_hidden(self) -> Progress<Hidden> {
|
||||
self.bar.hide();
|
||||
self.cancel.hide();
|
||||
@@ -494,6 +522,7 @@ impl<S> Progress<S> {
|
||||
}
|
||||
|
||||
#[allow(unused_must_use)]
|
||||
#[inline]
|
||||
// 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) {
|
||||
@@ -501,17 +530,14 @@ impl<S> Progress<S> {
|
||||
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);
|
||||
}
|
||||
})
|
||||
#[inline]
|
||||
fn cancel_connect_clicked<F: Fn(>k::Button) + 'static>(&self, f: F) -> glib::SignalHandlerId {
|
||||
self.cancel.connect_clicked(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Progress<UnInitialized> {
|
||||
#[inline]
|
||||
fn new(
|
||||
bar: gtk::ProgressBar,
|
||||
cancel: gtk::Button,
|
||||
@@ -543,73 +569,9 @@ pub struct Media<X, Y, 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> {
|
||||
#[inline]
|
||||
fn set_size(self, s: &str) -> Media<X, Shown, Z> {
|
||||
Media {
|
||||
dl: self.dl,
|
||||
@@ -618,6 +580,7 @@ impl<X, Y, Z> Media<X, Y, Z> {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn hide_size(self) -> Media<X, Hidden, Z> {
|
||||
Media {
|
||||
dl: self.dl,
|
||||
@@ -626,6 +589,7 @@ impl<X, Y, Z> Media<X, Y, Z> {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn into_new(self, size: &str) -> New<Shown> {
|
||||
Media {
|
||||
dl: self.dl.into_fetchable(),
|
||||
@@ -634,6 +598,16 @@ impl<X, Y, Z> Media<X, Y, Z> {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn into_new_without(self) -> New<Hidden> {
|
||||
Media {
|
||||
dl: self.dl.into_fetchable(),
|
||||
size: self.size.into_hidden(),
|
||||
progress: self.progress.into_hidden(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn into_playable(self, size: &str) -> Playable<Shown> {
|
||||
Media {
|
||||
dl: self.dl.into_playable(),
|
||||
@@ -641,9 +615,19 @@ impl<X, Y, Z> Media<X, Y, Z> {
|
||||
progress: self.progress.into_hidden(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn into_playable_without(self) -> Playable<Hidden> {
|
||||
Media {
|
||||
dl: self.dl.into_playable(),
|
||||
size: self.size.into_hidden(),
|
||||
progress: self.progress.into_hidden(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<X, Z> Media<X, Shown, Z> {
|
||||
#[inline]
|
||||
fn into_progress(self) -> InProgress {
|
||||
Media {
|
||||
dl: self.dl.into_hidden(),
|
||||
@@ -654,6 +638,7 @@ impl<X, Z> Media<X, Shown, Z> {
|
||||
}
|
||||
|
||||
impl<X, Z> Media<X, Hidden, Z> {
|
||||
#[inline]
|
||||
fn into_progress(self) -> InProgress {
|
||||
Media {
|
||||
dl: self.dl.into_hidden(),
|
||||
@@ -664,6 +649,7 @@ impl<X, Z> Media<X, Hidden, Z> {
|
||||
}
|
||||
|
||||
impl<X, Z> Media<X, UnInitialized, Z> {
|
||||
#[inline]
|
||||
fn into_progress(self, size: Option<String>) -> InProgress {
|
||||
if let Some(s) = size {
|
||||
Media {
|
||||
@@ -682,6 +668,7 @@ impl<X, Z> Media<X, UnInitialized, Z> {
|
||||
}
|
||||
|
||||
impl InProgress {
|
||||
#[inline]
|
||||
#[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.
|
||||
@@ -699,6 +686,7 @@ pub enum ButtonsState {
|
||||
}
|
||||
|
||||
impl ButtonsState {
|
||||
#[inline]
|
||||
pub fn determine_state(self, size: Option<String>, is_downloaded: bool) -> Self {
|
||||
use self::ButtonsState::*;
|
||||
|
||||
@@ -719,32 +707,33 @@ impl ButtonsState {
|
||||
|
||||
// From whatever to NewWithoutSize
|
||||
(New(m), None, false) => NewWithoutSize(m.hide_size()),
|
||||
(Playable(m), None, false) => NewWithoutSize(Media::from(m).hide_size()),
|
||||
(Playable(m), None, false) => NewWithoutSize(m.into_new_without()),
|
||||
|
||||
(b @ NewWithoutSize(_), None, false) => b,
|
||||
(PlayableWithoutSize(m), None, false) => NewWithoutSize(m.into()),
|
||||
(PlayableWithoutSize(m), None, false) => NewWithoutSize(m.into_new_without()),
|
||||
|
||||
// From whatever to PlayableWithoutSize
|
||||
(New(m), None, true) => PlayableWithoutSize(Media::from(m).hide_size()),
|
||||
(New(m), None, true) => PlayableWithoutSize(m.into_playable_without()),
|
||||
(Playable(m), None, true) => PlayableWithoutSize(m.hide_size()),
|
||||
|
||||
(NewWithoutSize(val), None, true) => PlayableWithoutSize(val.into()),
|
||||
(NewWithoutSize(val), None, true) => PlayableWithoutSize(val.into_playable_without()),
|
||||
(b @ PlayableWithoutSize(_), None, true) => b,
|
||||
// _ => unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
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(),
|
||||
New(m) => m.into_progress(),
|
||||
Playable(m) => m.into_progress(),
|
||||
NewWithoutSize(m) => m.into_progress(),
|
||||
PlayableWithoutSize(m) => m.into_progress(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_size(self, size: Option<String>) -> Self {
|
||||
use self::ButtonsState::*;
|
||||
|
||||
@@ -760,6 +749,7 @@ impl ButtonsState {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn download_connect_clicked<F: Fn(>k::Button) + 'static>(
|
||||
&self,
|
||||
f: F,
|
||||
@@ -774,6 +764,7 @@ impl ButtonsState {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn play_connect_clicked<F: Fn(>k::Button) + 'static>(
|
||||
&self,
|
||||
f: F,
|
||||
@@ -788,14 +779,15 @@ impl ButtonsState {
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel_connect_clicked(&self, prog: Arc<Mutex<OtherProgress>>) -> glib::SignalHandlerId {
|
||||
#[inline]
|
||||
fn cancel_connect_clicked<F: Fn(>k::Button) + 'static>(&self, f: F) -> 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),
|
||||
New(ref val) => val.progress.cancel_connect_clicked(f),
|
||||
NewWithoutSize(ref val) => val.progress.cancel_connect_clicked(f),
|
||||
Playable(ref val) => val.progress.cancel_connect_clicked(f),
|
||||
PlayableWithoutSize(ref val) => val.progress.cancel_connect_clicked(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -809,6 +801,7 @@ pub enum MediaMachine {
|
||||
|
||||
impl MediaMachine {
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(too_many_arguments))]
|
||||
#[inline]
|
||||
pub fn new(
|
||||
play: gtk::Button,
|
||||
download: gtk::Button,
|
||||
@@ -826,6 +819,7 @@ impl MediaMachine {
|
||||
MediaMachine::UnInitialized(Media { dl, progress, size })
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn download_connect_clicked<F: Fn(>k::Button) + 'static>(
|
||||
&self,
|
||||
f: F,
|
||||
@@ -839,6 +833,7 @@ impl MediaMachine {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn play_connect_clicked<F: Fn(>k::Button) + 'static>(
|
||||
&self,
|
||||
f: F,
|
||||
@@ -852,16 +847,21 @@ impl MediaMachine {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cancel_connect_clicked(&self, prog: Arc<Mutex<OtherProgress>>) -> glib::SignalHandlerId {
|
||||
#[inline]
|
||||
pub fn cancel_connect_clicked<F: Fn(>k::Button) + 'static>(
|
||||
&self,
|
||||
f: F,
|
||||
) -> 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),
|
||||
UnInitialized(ref val) => val.progress.cancel_connect_clicked(f),
|
||||
Initialized(ref val) => val.cancel_connect_clicked(f),
|
||||
InProgress(ref val) => val.progress.cancel_connect_clicked(f),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn determine_state(self, bytes: Option<i32>, is_active: bool, is_downloaded: bool) -> Self {
|
||||
use self::ButtonsState::*;
|
||||
use self::MediaMachine::*;
|
||||
@@ -871,18 +871,36 @@ impl MediaMachine {
|
||||
|
||||
// Into New
|
||||
(UnInitialized(m), Some(s), false, false) => Initialized(New(m.into_new(&s))),
|
||||
(UnInitialized(m), None, false, false) => Initialized(NewWithoutSize(m.into())),
|
||||
(UnInitialized(m), None, false, false) => {
|
||||
Initialized(NewWithoutSize(m.into_new_without()))
|
||||
}
|
||||
|
||||
// Into Playable
|
||||
(UnInitialized(m), Some(s), true, false) => Initialized(Playable(m.into_playable(&s))),
|
||||
(UnInitialized(m), None, true, false) => Initialized(PlayableWithoutSize(m.into())),
|
||||
(UnInitialized(m), None, true, false) => {
|
||||
Initialized(PlayableWithoutSize(m.into_playable_without()))
|
||||
}
|
||||
|
||||
(Initialized(bttn), s, dl, false) => Initialized(bttn.determine_state(s, dl)),
|
||||
(Initialized(bttn), _, _, true) => InProgress(bttn.into_progress()),
|
||||
|
||||
// Into New
|
||||
(InProgress(m), Some(s), false, false) => Initialized(New(m.into_new(&s))),
|
||||
(InProgress(m), None, false, false) => {
|
||||
Initialized(NewWithoutSize(m.into_new_without()))
|
||||
}
|
||||
|
||||
// Into Playable
|
||||
(InProgress(m), Some(s), true, false) => Initialized(Playable(m.into_playable(&s))),
|
||||
(InProgress(m), None, true, false) => {
|
||||
Initialized(PlayableWithoutSize(m.into_playable_without()))
|
||||
}
|
||||
|
||||
(i @ InProgress(_), _, _, _) => i,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_size(self, bytes: Option<i32>) -> Self {
|
||||
use self::MediaMachine::*;
|
||||
let size = size_helper(bytes);
|
||||
@@ -895,6 +913,7 @@ impl MediaMachine {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn update_progress(&mut self, local_size: &str, fraction: f64) {
|
||||
use self::MediaMachine::*;
|
||||
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
use chrono::prelude::*;
|
||||
use failure::Error;
|
||||
|
||||
use gtk;
|
||||
use gtk::prelude::*;
|
||||
|
||||
use hammond_data::EpisodeWidgetQuery;
|
||||
use hammond_data::dbqueries;
|
||||
use hammond_data::EpisodeWidgetQuery;
|
||||
use send_cell::SendCell;
|
||||
|
||||
use app::Action;
|
||||
use utils::{get_ignored_shows, set_image_from_path};
|
||||
use utils::{self, lazy_load_full};
|
||||
use widgets::EpisodeWidget;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::Mutex;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref EPISODES_VIEW_VALIGNMENT: Mutex<Option<SendCell<gtk::Adjustment>>> =
|
||||
Mutex::new(None);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum ListSplit {
|
||||
@@ -23,7 +31,7 @@ enum ListSplit {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EpisodesView {
|
||||
pub struct HomeView {
|
||||
pub container: gtk::Box,
|
||||
scrolled_window: gtk::ScrolledWindow,
|
||||
frame_parent: gtk::Box,
|
||||
@@ -39,7 +47,8 @@ pub struct EpisodesView {
|
||||
rest_list: gtk::ListBox,
|
||||
}
|
||||
|
||||
impl Default for EpisodesView {
|
||||
impl Default for HomeView {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episodes_view.ui");
|
||||
let container: gtk::Box = builder.get_object("container").unwrap();
|
||||
@@ -56,7 +65,7 @@ impl Default for EpisodesView {
|
||||
let month_list: gtk::ListBox = builder.get_object("month_list").unwrap();
|
||||
let rest_list: gtk::ListBox = builder.get_object("rest_list").unwrap();
|
||||
|
||||
EpisodesView {
|
||||
HomeView {
|
||||
container,
|
||||
scrolled_window,
|
||||
frame_parent,
|
||||
@@ -75,91 +84,82 @@ impl Default for EpisodesView {
|
||||
}
|
||||
|
||||
// TODO: REFACTOR ME
|
||||
impl EpisodesView {
|
||||
pub fn new(sender: Sender<Action>) -> Result<EpisodesView, Error> {
|
||||
let view = EpisodesView::default();
|
||||
let ignore = get_ignored_shows()?;
|
||||
let episodes = dbqueries::get_episodes_widgets_filter_limit(&ignore, 50)?;
|
||||
impl HomeView {
|
||||
#[inline]
|
||||
pub fn new(sender: Sender<Action>) -> Result<Rc<HomeView>, Error> {
|
||||
use self::ListSplit::*;
|
||||
|
||||
let view = Rc::new(HomeView::default());
|
||||
let ignore = utils::get_ignored_shows()?;
|
||||
let episodes = dbqueries::get_episodes_widgets_filter_limit(&ignore, 100)?;
|
||||
let now_utc = Utc::now();
|
||||
|
||||
episodes.into_iter().for_each(|ep| {
|
||||
let view_ = view.clone();
|
||||
let func = move |ep: EpisodeWidgetQuery| {
|
||||
let epoch = ep.epoch();
|
||||
let viewep = EpisodesViewWidget::new(ep, sender.clone());
|
||||
let widget = EpisodesViewWidget::new(ep, &sender);
|
||||
|
||||
let t = split(&now_utc, i64::from(epoch));
|
||||
match t {
|
||||
ListSplit::Today => {
|
||||
view.today_list.add(&viewep.container);
|
||||
}
|
||||
ListSplit::Yday => {
|
||||
view.yday_list.add(&viewep.container);
|
||||
}
|
||||
ListSplit::Week => {
|
||||
view.week_list.add(&viewep.container);
|
||||
}
|
||||
ListSplit::Month => {
|
||||
view.month_list.add(&viewep.container);
|
||||
}
|
||||
ListSplit::Rest => {
|
||||
view.rest_list.add(&viewep.container);
|
||||
}
|
||||
match split(&now_utc, i64::from(epoch)) {
|
||||
Today => add_to_box(&widget, &view_.today_list, &view_.today_box),
|
||||
Yday => add_to_box(&widget, &view_.yday_list, &view_.yday_box),
|
||||
Week => add_to_box(&widget, &view_.week_list, &view_.week_box),
|
||||
Month => add_to_box(&widget, &view_.month_list, &view_.month_box),
|
||||
Rest => add_to_box(&widget, &view_.rest_list, &view_.rest_box),
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if view.today_list.get_children().is_empty() {
|
||||
view.today_box.hide();
|
||||
}
|
||||
|
||||
if view.yday_list.get_children().is_empty() {
|
||||
view.yday_box.hide();
|
||||
}
|
||||
|
||||
if view.week_list.get_children().is_empty() {
|
||||
view.week_box.hide();
|
||||
}
|
||||
|
||||
if view.month_list.get_children().is_empty() {
|
||||
view.month_box.hide();
|
||||
}
|
||||
|
||||
if view.rest_list.get_children().is_empty() {
|
||||
view.rest_box.hide();
|
||||
}
|
||||
let view_ = view.clone();
|
||||
let callback = move || {
|
||||
view_
|
||||
.set_vadjustment()
|
||||
.map_err(|err| format!("{}", err))
|
||||
.ok();
|
||||
};
|
||||
|
||||
lazy_load_full(episodes, func, callback);
|
||||
view.container.show_all();
|
||||
Ok(view)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
if !self.today_list.get_children().is_empty() {
|
||||
return false;
|
||||
#[inline]
|
||||
/// Set scrolled window vertical adjustment.
|
||||
fn set_vadjustment(&self) -> Result<(), Error> {
|
||||
let guard = EPISODES_VIEW_VALIGNMENT
|
||||
.lock()
|
||||
.map_err(|err| format_err!("Failed to lock widget align mutex: {}", err))?;
|
||||
|
||||
if let Some(ref sendcell) = *guard {
|
||||
// Copy the vertical scrollbar adjustment from the old view into the new one.
|
||||
sendcell
|
||||
.try_get()
|
||||
.map(|x| utils::smooth_scroll_to(&self.scrolled_window, &x));
|
||||
}
|
||||
|
||||
if !self.yday_list.get_children().is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self.week_list.get_children().is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self.month_list.get_children().is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self.rest_list.get_children().is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set scrolled window vertical adjustment.
|
||||
pub fn set_vadjustment(&self, vadjustment: >k::Adjustment) {
|
||||
self.scrolled_window.set_vadjustment(vadjustment)
|
||||
#[inline]
|
||||
/// Save the vertical scrollbar position.
|
||||
pub fn save_alignment(&self) -> Result<(), Error> {
|
||||
if let Ok(mut guard) = EPISODES_VIEW_VALIGNMENT.lock() {
|
||||
let adj = self.scrolled_window
|
||||
.get_vadjustment()
|
||||
.ok_or_else(|| format_err!("Could not get the adjustment"))?;
|
||||
*guard = Some(SendCell::new(adj));
|
||||
info!("Saved episodes_view alignment.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn add_to_box(widget: &EpisodesViewWidget, listbox: >k::ListBox, box_: >k::Box) {
|
||||
listbox.add(&widget.container);
|
||||
box_.show();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn split(now: &DateTime<Utc>, epoch: i64) -> ListSplit {
|
||||
let ep = Utc.timestamp(epoch, 0);
|
||||
|
||||
@@ -184,6 +184,7 @@ struct EpisodesViewWidget {
|
||||
}
|
||||
|
||||
impl Default for EpisodesViewWidget {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
let builder =
|
||||
gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episodes_view_widget.ui");
|
||||
@@ -201,13 +202,13 @@ impl Default for EpisodesViewWidget {
|
||||
}
|
||||
|
||||
impl EpisodesViewWidget {
|
||||
fn new(episode: EpisodeWidgetQuery, sender: Sender<Action>) -> EpisodesViewWidget {
|
||||
fn new(episode: EpisodeWidgetQuery, sender: &Sender<Action>) -> EpisodesViewWidget {
|
||||
let builder =
|
||||
gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episodes_view_widget.ui");
|
||||
let container: gtk::Box = builder.get_object("container").unwrap();
|
||||
let image: gtk::Image = builder.get_object("cover").unwrap();
|
||||
let pid = episode.podcast_id();
|
||||
let ep = EpisodeWidget::new(episode, sender.clone());
|
||||
let ep = EpisodeWidget::new(episode, sender);
|
||||
|
||||
let view = EpisodesViewWidget {
|
||||
container,
|
||||
@@ -219,16 +220,17 @@ impl EpisodesViewWidget {
|
||||
view
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn init(&self, podcast_id: i32) {
|
||||
if let Err(err) = self.set_cover(podcast_id) {
|
||||
error!("Failed to set a cover: {}", err)
|
||||
}
|
||||
self.set_cover(podcast_id)
|
||||
.map_err(|err| error!("Failed to set a cover: {}", err))
|
||||
.ok();
|
||||
|
||||
self.container.pack_start(&self.episode, true, true, 6);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_cover(&self, podcast_id: i32) -> Result<(), Error> {
|
||||
let pd = Arc::new(dbqueries::get_podcast_cover_from_id(podcast_id)?);
|
||||
set_image_from_path(&self.image, pd, 64)
|
||||
utils::set_image_from_path(&self.image, podcast_id, 64)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
mod show;
|
||||
mod empty;
|
||||
mod episode;
|
||||
mod episode_states;
|
||||
mod home_view;
|
||||
mod show;
|
||||
mod shows_view;
|
||||
|
||||
pub use self::empty::EmptyView;
|
||||
pub use self::episode::EpisodeWidget;
|
||||
pub use self::home_view::HomeView;
|
||||
pub use self::show::ShowWidget;
|
||||
pub use self::show::mark_all_watched;
|
||||
pub use self::show::{mark_all_notif, remove_show_notif};
|
||||
pub use self::shows_view::ShowsView;
|
||||
|
||||
+232
-62
@@ -1,19 +1,30 @@
|
||||
use failure::Error;
|
||||
// use glib;
|
||||
use glib;
|
||||
use gtk;
|
||||
use gtk::prelude::*;
|
||||
|
||||
use failure::Error;
|
||||
use html2pango::markup_from_raw;
|
||||
use open;
|
||||
use rayon;
|
||||
use send_cell::SendCell;
|
||||
|
||||
use hammond_data::Podcast;
|
||||
use hammond_data::dbqueries;
|
||||
use hammond_data::utils::delete_show;
|
||||
use hammond_data::Podcast;
|
||||
|
||||
use app::Action;
|
||||
use utils::set_image_from_path;
|
||||
use widgets::episode::episodes_listbox;
|
||||
use appnotif::InAppNotification;
|
||||
use utils::{self, lazy_load};
|
||||
use widgets::EpisodeWidget;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc::{SendError, Sender};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
lazy_static! {
|
||||
static ref SHOW_WIDGET_VALIGNMENT: Mutex<Option<(i32, SendCell<gtk::Adjustment>)>> =
|
||||
Mutex::new(None);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ShowWidget {
|
||||
@@ -24,15 +35,17 @@ pub struct ShowWidget {
|
||||
link: gtk::Button,
|
||||
settings: gtk::MenuButton,
|
||||
unsub: gtk::Button,
|
||||
episodes: gtk::Frame,
|
||||
episodes: gtk::ListBox,
|
||||
podcast_id: Option<i32>,
|
||||
}
|
||||
|
||||
impl Default for ShowWidget {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/show_widget.ui");
|
||||
let container: gtk::Box = builder.get_object("container").unwrap();
|
||||
let scrolled_window: gtk::ScrolledWindow = builder.get_object("scrolled_window").unwrap();
|
||||
let episodes: gtk::Frame = builder.get_object("episodes").unwrap();
|
||||
let episodes = builder.get_object("episodes").unwrap();
|
||||
|
||||
let cover: gtk::Image = builder.get_object("cover").unwrap();
|
||||
let description: gtk::Label = builder.get_object("description").unwrap();
|
||||
@@ -49,45 +62,48 @@ impl Default for ShowWidget {
|
||||
link,
|
||||
settings,
|
||||
episodes,
|
||||
podcast_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ShowWidget {
|
||||
pub fn new(pd: Arc<Podcast>, sender: Sender<Action>) -> ShowWidget {
|
||||
let pdw = ShowWidget::default();
|
||||
pdw.init(pd, sender);
|
||||
#[inline]
|
||||
pub fn new(pd: Arc<Podcast>, sender: Sender<Action>) -> Rc<ShowWidget> {
|
||||
let mut pdw = ShowWidget::default();
|
||||
pdw.init(&pd, &sender);
|
||||
let pdw = Rc::new(pdw);
|
||||
populate_listbox(&pdw, pd, sender)
|
||||
.map_err(|err| error!("Failed to populate the listbox: {}", err))
|
||||
.ok();
|
||||
|
||||
pdw
|
||||
}
|
||||
|
||||
pub fn init(&self, pd: Arc<Podcast>, sender: Sender<Action>) {
|
||||
#[inline]
|
||||
pub fn init(&mut 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());
|
||||
|
||||
self.unsub
|
||||
.connect_clicked(clone!(pd, sender => move |bttn| {
|
||||
if let Err(err) = on_unsub_button_clicked(pd.clone(), bttn, sender.clone()) {
|
||||
error!("Error: {}", err);
|
||||
}
|
||||
on_unsub_button_clicked(pd.clone(), bttn, &sender);
|
||||
}));
|
||||
|
||||
self.setup_listbox(&pd, sender.clone());
|
||||
self.set_description(pd.description());
|
||||
self.podcast_id = Some(pd.id());
|
||||
|
||||
if let Err(err) = self.set_cover(pd.clone()) {
|
||||
error!("Failed to set a cover: {}", err)
|
||||
}
|
||||
self.set_cover(&pd)
|
||||
.map_err(|err| error!("Failed to set a cover: {}", err))
|
||||
.ok();
|
||||
|
||||
let link = pd.link().to_owned();
|
||||
self.link.set_tooltip_text(Some(link.as_str()));
|
||||
self.link.connect_clicked(move |_| {
|
||||
info!("Opening link: {}", &link);
|
||||
if let Err(err) = open::that(&link) {
|
||||
error!("Failed to open link: {}", &link);
|
||||
error!("Error: {}", err);
|
||||
}
|
||||
open::that(&link)
|
||||
.map_err(|err| error!("Error: {}", err))
|
||||
.map_err(|_| error!("Failed open link: {}", &link))
|
||||
.ok();
|
||||
});
|
||||
|
||||
let show_menu: gtk::Popover = builder.get_object("show_menu").unwrap();
|
||||
@@ -98,80 +114,234 @@ impl ShowWidget {
|
||||
on_played_button_clicked(
|
||||
pd.clone(),
|
||||
&episodes,
|
||||
sender.clone()
|
||||
&sender
|
||||
)
|
||||
}));
|
||||
self.settings.set_popover(&show_menu);
|
||||
}
|
||||
|
||||
/// Populate the listbox with the shows episodes.
|
||||
fn setup_listbox(&self, pd: &Podcast, sender: Sender<Action>) {
|
||||
let listbox = episodes_listbox(pd, sender.clone());
|
||||
listbox.ok().map(|l| self.episodes.add(&l));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Set the show cover.
|
||||
fn set_cover(&self, pd: Arc<Podcast>) -> Result<(), Error> {
|
||||
set_image_from_path(&self.cover, Arc::new(pd.into()), 128)
|
||||
fn set_cover(&self, pd: &Arc<Podcast>) -> Result<(), Error> {
|
||||
utils::set_image_from_path(&self.cover, pd.id(), 256)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Set the descripton text.
|
||||
fn set_description(&self, text: &str) {
|
||||
self.description.set_markup(&markup_from_raw(text));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Save the scrollabar vajustment to the cache.
|
||||
pub fn save_vadjustment(&self, oldid: i32) -> Result<(), Error> {
|
||||
if let Ok(mut guard) = SHOW_WIDGET_VALIGNMENT.lock() {
|
||||
let adj = self.scrolled_window
|
||||
.get_vadjustment()
|
||||
.ok_or_else(|| format_err!("Could not get the adjustment"))?;
|
||||
*guard = Some((oldid, SendCell::new(adj)));
|
||||
debug!("Widget Alignment was saved with ID: {}.", oldid);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Set scrolled window vertical adjustment.
|
||||
pub fn set_vadjustment(&self, vadjustment: >k::Adjustment) {
|
||||
self.scrolled_window.set_vadjustment(vadjustment)
|
||||
fn set_vadjustment(&self, pd: &Arc<Podcast>) -> Result<(), Error> {
|
||||
let guard = SHOW_WIDGET_VALIGNMENT
|
||||
.lock()
|
||||
.map_err(|err| format_err!("Failed to lock widget align mutex: {}", err))?;
|
||||
|
||||
if let Some((oldid, ref sendcell)) = *guard {
|
||||
// Only copy the old scrollbar if both widget's represent the same podcast.
|
||||
debug!("PID: {}", pd.id());
|
||||
debug!("OLDID: {}", oldid);
|
||||
if pd.id() != oldid {
|
||||
debug!("Early return");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// Copy the vertical scrollbar adjustment from the old view into the new one.
|
||||
sendcell
|
||||
.try_get()
|
||||
.map(|x| utils::smooth_scroll_to(&self.scrolled_window, &x));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn podcast_id(&self) -> Option<i32> {
|
||||
self.podcast_id
|
||||
}
|
||||
}
|
||||
|
||||
fn on_unsub_button_clicked(
|
||||
#[inline]
|
||||
/// Populate the listbox with the shows episodes.
|
||||
fn populate_listbox(
|
||||
show: &Rc<ShowWidget>,
|
||||
pd: Arc<Podcast>,
|
||||
unsub_button: >k::Button,
|
||||
sender: Sender<Action>,
|
||||
) -> Result<(), Error> {
|
||||
// hack to get away without properly checking for none.
|
||||
// if pressed twice would panic.
|
||||
unsub_button.hide();
|
||||
sender.send(Action::RemoveShow(pd))?;
|
||||
use crossbeam_channel::bounded;
|
||||
use crossbeam_channel::TryRecvError::*;
|
||||
|
||||
sender.send(Action::HeaderBarNormal)?;
|
||||
sender.send(Action::ShowShowsAnimated)?;
|
||||
// Queue a refresh after the switch to avoid blocking the db.
|
||||
sender.send(Action::RefreshShowsView)?;
|
||||
sender.send(Action::RefreshEpisodesView)?;
|
||||
let count = dbqueries::get_pd_episodes_count(&pd)?;
|
||||
|
||||
let (sender_, receiver) = bounded(1);
|
||||
rayon::spawn(clone!(pd => move || {
|
||||
let episodes = dbqueries::get_pd_episodeswidgets(&pd).unwrap();
|
||||
// The receiver can be dropped if there's an early return
|
||||
// like on show without episodes for example.
|
||||
sender_.send(episodes).ok();
|
||||
}));
|
||||
|
||||
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();
|
||||
show.episodes.add(&container);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let show_ = show.clone();
|
||||
gtk::idle_add(move || {
|
||||
let episodes = match receiver.try_recv() {
|
||||
Ok(e) => e,
|
||||
Err(Empty) => return glib::Continue(true),
|
||||
Err(Disconnected) => return glib::Continue(false),
|
||||
};
|
||||
|
||||
let list = show_.episodes.clone();
|
||||
|
||||
let constructor = clone!(sender => move |ep| {
|
||||
EpisodeWidget::new(ep, &sender).container
|
||||
});
|
||||
|
||||
let callback = clone!(pd, show_ => move || {
|
||||
show_.set_vadjustment(&pd)
|
||||
.map_err(|err| error!("Failed to set ShowWidget Alignment: {}", err))
|
||||
.ok();
|
||||
});
|
||||
|
||||
lazy_load(episodes, list.clone(), constructor, callback);
|
||||
|
||||
glib::Continue(false)
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_played_button_clicked(pd: Arc<Podcast>, episodes: >k::Frame, sender: Sender<Action>) {
|
||||
#[inline]
|
||||
fn on_unsub_button_clicked(pd: Arc<Podcast>, unsub_button: >k::Button, sender: &Sender<Action>) {
|
||||
// hack to get away without properly checking for none.
|
||||
// if pressed twice would panic.
|
||||
unsub_button.set_sensitive(false);
|
||||
|
||||
let wrap = || -> Result<(), SendError<_>> {
|
||||
sender.send(Action::RemoveShow(pd))?;
|
||||
|
||||
sender.send(Action::HeaderBarNormal)?;
|
||||
sender.send(Action::ShowShowsAnimated)?;
|
||||
// Queue a refresh after the switch to avoid blocking the db.
|
||||
sender.send(Action::RefreshShowsView)?;
|
||||
sender.send(Action::RefreshEpisodesView)?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
wrap().map_err(|err| error!("Action Sender: {}", err)).ok();
|
||||
unsub_button.set_sensitive(true);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn on_played_button_clicked(pd: Arc<Podcast>, episodes: >k::ListBox, 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();
|
||||
sender
|
||||
.send(Action::MarkAllPlayerNotification(pd))
|
||||
.map_err(|err| error!("Action Sender: {}", err))
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn mark_all_watched(pd: &Podcast, sender: Sender<Action>) -> Result<(), Error> {
|
||||
#[inline]
|
||||
fn mark_all_watched(pd: &Podcast, sender: &Sender<Action>) -> Result<(), Error> {
|
||||
dbqueries::update_none_to_played_now(pd)?;
|
||||
sender.send(Action::RefreshWidgetIfVis)?;
|
||||
sender.send(Action::RefreshEpisodesView)?;
|
||||
Ok(())
|
||||
// Not all widgets migth have been loaded when the mark_all is hit
|
||||
// So we will need to refresh again after it's done.
|
||||
sender.send(Action::RefreshWidgetIfSame(pd.id()))?;
|
||||
sender.send(Action::RefreshEpisodesView).map_err(From::from)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn mark_all_notif(pd: Arc<Podcast>, sender: &Sender<Action>) -> InAppNotification {
|
||||
let id = pd.id();
|
||||
let callback = clone!(sender => move || {
|
||||
mark_all_watched(&pd, &sender)
|
||||
.map_err(|err| error!("Notif Callback Error: {}", err))
|
||||
.ok();
|
||||
glib::Continue(false)
|
||||
});
|
||||
|
||||
let undo_callback = clone!(sender => move || {
|
||||
sender.send(Action::RefreshWidgetIfSame(id))
|
||||
.map_err(|err| error!("Action Sender: {}", err))
|
||||
.ok();
|
||||
});
|
||||
|
||||
let text = "Marked all episodes as listened";
|
||||
InAppNotification::new(text, callback, undo_callback)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn remove_show_notif(pd: Arc<Podcast>, sender: Sender<Action>) -> InAppNotification {
|
||||
let text = format!("Unsubscribed from {}", pd.title());
|
||||
|
||||
utils::ignore_show(pd.id())
|
||||
.map_err(|err| error!("Error: {}", err))
|
||||
.map_err(|_| error!("Could not insert {} to the ignore list.", pd.title()))
|
||||
.ok();
|
||||
|
||||
let callback = clone!(pd, sender => move || {
|
||||
utils::uningore_show(pd.id())
|
||||
.map_err(|err| error!("Error: {}", err))
|
||||
.map_err(|_| error!("Could not remove {} from the ignore list.", pd.title()))
|
||||
.ok();
|
||||
|
||||
// Spawn a thread so it won't block the ui.
|
||||
rayon::spawn(clone!(pd, sender => move || {
|
||||
delete_show(&pd)
|
||||
.map_err(|err| error!("Error: {}", err))
|
||||
.map_err(|_| error!("Failed to delete {}", pd.title()))
|
||||
.ok();
|
||||
|
||||
sender.send(Action::RefreshEpisodesView).ok();
|
||||
}));
|
||||
glib::Continue(false)
|
||||
});
|
||||
|
||||
let undo_wrap = move || -> Result<(), Error> {
|
||||
utils::uningore_show(pd.id())?;
|
||||
sender.send(Action::RefreshShowsView)?;
|
||||
sender.send(Action::RefreshEpisodesView)?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let undo_callback = move || {
|
||||
undo_wrap().map_err(|err| error!("{}", err)).ok();
|
||||
};
|
||||
|
||||
InAppNotification::new(&text, callback, undo_callback)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
// 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();
|
||||
fn dim_titles(episodes: >k::ListBox) -> Option<()> {
|
||||
let children = episodes.get_children();
|
||||
|
||||
for row in children {
|
||||
let row = row.downcast::<gtk::ListBoxRow>().ok()?;
|
||||
|
||||
@@ -2,30 +2,32 @@ use failure::Error;
|
||||
use gtk;
|
||||
use gtk::prelude::*;
|
||||
|
||||
use hammond_data::{Podcast, PodcastCoverQuery};
|
||||
use hammond_data::dbqueries;
|
||||
use hammond_data::Podcast;
|
||||
|
||||
use app::Action;
|
||||
use utils::{get_ignored_shows, set_image_from_path};
|
||||
use utils::{get_ignored_shows, lazy_load, set_image_from_path};
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ShowsPopulated {
|
||||
pub struct ShowsView {
|
||||
pub container: gtk::Box,
|
||||
scrolled_window: gtk::ScrolledWindow,
|
||||
flowbox: gtk::FlowBox,
|
||||
}
|
||||
|
||||
impl Default for ShowsPopulated {
|
||||
impl Default for ShowsView {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/shows_view.ui");
|
||||
let container: gtk::Box = builder.get_object("fb_parent").unwrap();
|
||||
let scrolled_window: gtk::ScrolledWindow = builder.get_object("scrolled_window").unwrap();
|
||||
let flowbox: gtk::FlowBox = builder.get_object("flowbox").unwrap();
|
||||
|
||||
ShowsPopulated {
|
||||
ShowsView {
|
||||
container,
|
||||
scrolled_window,
|
||||
flowbox,
|
||||
@@ -33,49 +35,43 @@ impl Default for ShowsPopulated {
|
||||
}
|
||||
}
|
||||
|
||||
impl ShowsPopulated {
|
||||
pub fn new(sender: Sender<Action>) -> Result<ShowsPopulated, Error> {
|
||||
let pop = ShowsPopulated::default();
|
||||
impl ShowsView {
|
||||
#[inline]
|
||||
pub fn new(sender: Sender<Action>) -> Result<Rc<Self>, Error> {
|
||||
let pop = Rc::new(ShowsView::default());
|
||||
pop.init(sender)?;
|
||||
Ok(pop)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn init(&self, sender: Sender<Action>) -> Result<(), Error> {
|
||||
self.flowbox.connect_child_activated(move |_, child| {
|
||||
if let Err(err) = on_child_activate(child, sender.clone()) {
|
||||
error!(
|
||||
"Something went wrong during flowbox child activation: {}.",
|
||||
err
|
||||
)
|
||||
};
|
||||
on_child_activate(child, &sender)
|
||||
.map_err(|err| error!("Error along flowbox child activation: {}", err))
|
||||
.ok();
|
||||
});
|
||||
// Populate the flowbox with the Podcasts.
|
||||
self.populate_flowbox()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn populate_flowbox(&self) -> Result<(), Error> {
|
||||
let ignore = get_ignored_shows()?;
|
||||
let podcasts = dbqueries::get_podcasts_filter(&ignore)?;
|
||||
|
||||
podcasts.into_iter().for_each(|parent| {
|
||||
let flowbox_child = ShowsChild::new(parent);
|
||||
self.flowbox.add(&flowbox_child.child);
|
||||
});
|
||||
self.flowbox.show_all();
|
||||
let flowbox = self.flowbox.clone();
|
||||
let constructor = |parent| ShowsChild::new(&parent).child;
|
||||
let callback = move || flowbox.show_all();
|
||||
|
||||
let flowbox = self.flowbox.clone();
|
||||
lazy_load(podcasts, flowbox, constructor, callback);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.flowbox.get_children().is_empty()
|
||||
}
|
||||
|
||||
/// Set scrolled window vertical adjustment.
|
||||
pub fn set_vadjustment(&self, vadjustment: >k::Adjustment) {
|
||||
self.scrolled_window.set_vadjustment(vadjustment)
|
||||
}
|
||||
}
|
||||
|
||||
fn on_child_activate(child: >k::FlowBoxChild, sender: Sender<Action>) -> Result<(), Error> {
|
||||
#[inline]
|
||||
fn on_child_activate(child: >k::FlowBoxChild, sender: &Sender<Action>) -> Result<(), Error> {
|
||||
use gtk::WidgetExt;
|
||||
|
||||
// This is such an ugly hack...
|
||||
@@ -98,6 +94,7 @@ struct ShowsChild {
|
||||
}
|
||||
|
||||
impl Default for ShowsChild {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/shows_child.ui");
|
||||
|
||||
@@ -116,23 +113,25 @@ impl Default for ShowsChild {
|
||||
}
|
||||
|
||||
impl ShowsChild {
|
||||
pub fn new(pd: Podcast) -> ShowsChild {
|
||||
#[inline]
|
||||
pub fn new(pd: &Podcast) -> ShowsChild {
|
||||
let child = ShowsChild::default();
|
||||
child.init(pd);
|
||||
child
|
||||
}
|
||||
|
||||
fn init(&self, pd: Podcast) {
|
||||
#[inline]
|
||||
fn init(&self, pd: &Podcast) {
|
||||
self.container.set_tooltip_text(pd.title());
|
||||
WidgetExt::set_name(&self.child, &pd.id().to_string());
|
||||
|
||||
let pd = Arc::new(pd.into());
|
||||
if let Err(err) = self.set_cover(pd) {
|
||||
error!("Failed to set a cover: {}", err)
|
||||
}
|
||||
self.set_cover(pd.id())
|
||||
.map_err(|err| error!("Failed to set a cover: {}", err))
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn set_cover(&self, pd: Arc<PodcastCoverQuery>) -> Result<(), Error> {
|
||||
set_image_from_path(&self.cover, pd, 256)
|
||||
#[inline]
|
||||
fn set_cover(&self, podcast_id: i32) -> Result<(), Error> {
|
||||
set_image_from_path(&self.cover, podcast_id, 256)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
</p>
|
||||
</description>
|
||||
<!-- <homepage rdf:resource="https://wiki.gnome.org/Apps/Hammond" /> -->
|
||||
<bug-database rdf:resource="https://gitlab.gnome.org/alatiera/Hammond/issues" />
|
||||
<bug-database rdf:resource="https://gitlab.gnome.org/World/hammond/issues" />
|
||||
|
||||
<category rdf:resource="http://api.gnome.org/doap-extensions#apps" />
|
||||
<programming-language>Rust</programming-language>
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
project(
|
||||
'hammond', 'rust',
|
||||
version: '0.3.1',
|
||||
version: '0.3.2',
|
||||
license: 'GPLv3',
|
||||
)
|
||||
|
||||
|
||||
@@ -28,7 +28,9 @@
|
||||
"--share=network"
|
||||
],
|
||||
"env" : {
|
||||
"CARGO_HOME" : "/run/build/Hammond/cargo"
|
||||
"CARGO_HOME" : "/run/build/Hammond/cargo",
|
||||
"RUST_BACKTRACE" : "1",
|
||||
"RUSTFLAGS" : "--cfg rayon_unstable"
|
||||
}
|
||||
},
|
||||
"modules" : [
|
||||
@@ -38,7 +40,7 @@
|
||||
"sources" : [
|
||||
{
|
||||
"type" : "git",
|
||||
"url" : "https://gitlab.gnome.org/alatiera/Hammond.git",
|
||||
"url" : "https://gitlab.gnome.org/World/hammond.git",
|
||||
"branch" : "master"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -11,5 +11,3 @@ condense_wildcard_suffixes = false
|
||||
format_strings = true
|
||||
normalize_comments = true
|
||||
reorder_imports = true
|
||||
reorder_imported_names = true
|
||||
reorder_imports_in_group = true
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/bin/sh
|
||||
|
||||
export CARGO_HOME=$1/target/cargo-home
|
||||
export RUSTFLAGS="--cfg rayon_unstable"
|
||||
|
||||
cargo build --release -p hammond-gtk && cp $1/target/release/hammond-gtk $2
|
||||
@@ -13,6 +13,7 @@ cp -rf hammond-data $DIST
|
||||
cp -rf hammond-gtk $DIST
|
||||
cp -rf hammond-downloader $DIST
|
||||
cp Cargo.toml $DIST
|
||||
cp Cargo.lock $DIST
|
||||
cp configure $DIST
|
||||
cp meson.build $DIST
|
||||
cp Hammond.doap $DIST
|
||||
|
||||
Reference in New Issue
Block a user