diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ddc1e76..32c78c8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,7 +25,7 @@ variables: before_script: - apt-get update -yqq - - apt-get install -yqq --no-install-recommends build-essential libgtk-3-dev meson + - apt-get install -yqq --no-install-recommends build-essential libgtk-3-dev meson libgstreamer1.0-dev - mkdir -p .cargo_cache # Only stuff inside the repo directory can be cached @@ -41,12 +41,12 @@ variables: - cargo test -- --test-threads=1 --ignored <<: *cargo_cache -rust:stable: +.rust:stable: # https://hub.docker.com/_/rust/ image: "rust" <<: *cargo_test -rust:nightly: +.rust:nightly: # https://hub.docker.com/r/rustlang/rust/ image: "rustlang/rust:nightly" <<: *cargo_test @@ -129,7 +129,7 @@ rustfmt: # Configure and run clippy on nightly # Only fails on errors atm. -clippy: +.clippy: image: "registry.gitlab.gnome.org/alatiera/hammond-container-images/clippy:nightly" stage: lint variables: diff --git a/CHANGELOG.md b/CHANGELOG.md index 26e8735..37e5404 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] ### Added: -- Keyboard Shortcuts and a Shortcuts dialog were implemented. (ZanderBrown) [!33](https://gitlab.gnome.org/World/hammond/merge_requests/33) +- Keyboard Shortcuts and a Shortcuts dialog were implemented. (ZanderBrown) +[!33](https://gitlab.gnome.org/World/hammond/merge_requests/33) ### Changed: -- The `FileChooser` of the OPML import was changed to use the `FileChooserNative` widget/API. (ZanderBrown) [!33](https://gitlab.gnome.org/World/hammond/merge_requests/33) -- The `EpisdeWidget` was refactored. [!38](https://gitlab.gnome.org/World/hammond/merge_requests/38) +- The `FileChooser` of the OPML import was changed to use the `FileChooserNative` widget/API. (ZanderBrown) +[!33](https://gitlab.gnome.org/World/hammond/merge_requests/33) +- The `EpisdeWidget` was refactored. +[!38](https://gitlab.gnome.org/World/hammond/merge_requests/38) - `EpisdeWidget`'s progressbar was changed to be non-blocking and should feel way more responsive now. 9b0ac5b83dadecdff51cd398293afdf0d5276012 +- An embeded audio player was implemented! +[!40](https://gitlab.gnome.org/World/hammond/merge_requests/40) ### Fixed: - Fixed a bug whre the about dialog would be unclosable. (ZanderBrown) [!37](https://gitlab.gnome.org/World/hammond/merge_requests/37) diff --git a/Cargo.lock b/Cargo.lock index c1c50ad..663be4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -701,6 +701,122 @@ dependencies = [ "pkg-config 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "gstreamer" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "glib 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gobject-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gstreamer-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", + "muldiv 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "num-rational 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "gstreamer-base" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "glib 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gobject-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gstreamer 0.11.2 (registry+https://github.com/rust-lang/crates.io-index)", + "gstreamer-base-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gstreamer-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "gstreamer-base-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gobject-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gstreamer-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "gstreamer-player" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "glib 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gobject-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gstreamer 0.11.2 (registry+https://github.com/rust-lang/crates.io-index)", + "gstreamer-player-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gstreamer-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gstreamer-video 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "gstreamer-player-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gobject-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gstreamer-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gstreamer-video-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "gstreamer-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gobject-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "gstreamer-video" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "glib 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gobject-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gstreamer 0.11.2 (registry+https://github.com/rust-lang/crates.io-index)", + "gstreamer-base 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gstreamer-base-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gstreamer-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gstreamer-video-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "gstreamer-video-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gobject-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gstreamer-base-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gstreamer-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "gtk" version = "0.4.1" @@ -805,6 +921,8 @@ dependencies = [ "gdk-pixbuf 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "gio 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "glib 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gstreamer 0.11.2 (registry+https://github.com/rust-lang/crates.io-index)", + "gstreamer-player 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "gtk 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "hammond-data 0.1.0", "hammond-downloader 0.1.0", @@ -1185,6 +1303,11 @@ dependencies = [ "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "muldiv" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "native-tls" version = "0.1.5" @@ -1230,6 +1353,15 @@ dependencies = [ "num-traits 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "num-rational" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "num-traits" version = "0.1.43" @@ -2462,6 +2594,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "615bef979b5838526aee99241afc80cfb2e34a8735d4bcb8ec6072598c18a408" "checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" "checksum gobject-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "70409d6405db8b1591602fcd0cbe8af52cd9976dd39194442b4c149ba343f86d" +"checksum gstreamer 0.11.2 (registry+https://github.com/rust-lang/crates.io-index)" = "90d7cde9eae8f6bf4d41c254915976b236143935ec2c0b027636274e998e54b4" +"checksum gstreamer-base 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05ec7a84b4160b61c72ea27ccf3f46eb9c8f996c5991746623e69e3e532e3cb5" +"checksum gstreamer-base-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "501a7add44f256aab6cb5b65ef121c449197cf55087d6a7586846c8d1e42e88b" +"checksum gstreamer-player 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d831e2c4aa1296a8d0f3b4caa0b5ae8d6f5c0eed67ed20236f8647010005c63e" +"checksum gstreamer-player-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3b9476078cc76164446e88b2c4331e81e24a07f7b7c3a8b4bf8975a47998ebd4" +"checksum gstreamer-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b2f51e25a6f97dd4bfd640cba96f192f8759b8766afd66d6d9ea0f82ca14a37" +"checksum gstreamer-video 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "76c6c8971688f530ae93e96ea29fe6051658bb4d00b4b40d30575ca1d8a25a18" +"checksum gstreamer-video-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ed798787e78a0f1c8be06bd3adcab03f962f049a820743aae9f690f56a0d538" "checksum gtk 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d695d6be4110618a97c19cd068e8a00e53e33b87e3c65cdc5397667498b1bc24" "checksum gtk-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d9554cf5b3a85a13fb39258c65b04b262989c1d7a758f8f555b77a478621a91" "checksum handlebars 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e7bdb08e879b8c78ee90f5022d121897c31ea022cb0cc6d13f2158c7a9fbabb1" @@ -2503,11 +2643,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum mime_guess 2.0.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)" = "130ea3c9c1b65dba905ab5a4d9ac59234a9585c24d135f264e187fe7336febbd" "checksum mio 0.6.14 (registry+https://github.com/rust-lang/crates.io-index)" = "6d771e3ef92d58a8da8df7d6976bfca9371ed1de6619d9d5a5ce5b1f29b85bfe" "checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" +"checksum muldiv 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1cbef5aa2e8cd82a18cc20e26434cc9843e1ef46e55bfabe5bddb022236c5b3e" "checksum native-tls 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f74dbadc8b43df7864539cedb7bc91345e532fdd913cfdc23ad94f4d2d40fbc0" "checksum net2 0.2.32 (registry+https://github.com/rust-lang/crates.io-index)" = "9044faf1413a1057267be51b5afba8eb1090bd2231c693664aa1db716fe1eae0" "checksum new_debug_unreachable 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0cdc457076c78ab54d5e0d6fa7c47981757f1e34dc39ff92787f217dede586c4" "checksum nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "9a2228dca57108069a5262f2ed8bd2e82496d2e074a06d1ccc7ce1687b6ae0a2" "checksum num-integer 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)" = "6ac0ea58d64a89d9d6b7688031b3be9358d6c919badcf7fbb0527ccfd891ee45" +"checksum num-rational 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e" "checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" "checksum num-traits 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "775393e285254d2f5004596d69bb8bc1149754570dcc08cf30cabeba67955e28" "checksum num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c51a3322e4bca9d212ad9a158a02abc6934d005490c054a2778df73a70aa0a30" diff --git a/hammond-data/src/lib.rs b/hammond-data/src/lib.rs index 7143c84..d1a617c 100644 --- a/hammond-data/src/lib.rs +++ b/hammond-data/src/lib.rs @@ -85,7 +85,9 @@ 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"; +/// The user-agent to be used for all the requests. +/// It originates from the Tor-browser UA. +pub 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)] diff --git a/hammond-gtk/Cargo.toml b/hammond-gtk/Cargo.toml index 76169ce..7619360 100644 --- a/hammond-gtk/Cargo.toml +++ b/hammond-gtk/Cargo.toml @@ -11,6 +11,8 @@ crossbeam-channel = "0.1.2" gdk = "0.8.0" gdk-pixbuf = "0.4.0" glib = "0.5.0" +gstreamer = "0.11.2" +gstreamer-player = "0.11.0" humansize = "1.1.0" lazy_static = "1.0.0" log = "0.4.1" diff --git a/hammond-gtk/resources/gtk/player_toolbar.ui b/hammond-gtk/resources/gtk/player_toolbar.ui new file mode 100644 index 0000000..653f976 --- /dev/null +++ b/hammond-gtk/resources/gtk/player_toolbar.ui @@ -0,0 +1,372 @@ + + + + + + True + False + False + 1 + media-seek-forward-symbolic + 1 + + + True + False + 1 + media-playback-pause-symbolic + 1 + + + True + False + 1 + media-playback-start-symbolic + 1 + + + True + False + 1 + media-seek-backward-symbolic + 1 + + + False + True + center + + + True + False + center + + + 42 + True + True + True + Previous + previous_image + True + + + False + False + 0 + + + + + 60 + True + True + Play + play_image + True + + + False + False + 1 + + + + + 60 + True + True + True + Play + pause_image + True + + + False + False + 2 + + + + + 42 + True + True + True + Next + ff_image + True + + + False + False + 3 + + + + + + 0 + + + + + True + False + 6 + + + True + False + center + 34 + image-x-generic-symbolic + + + False + True + 0 + + + + + True + False + True + center + center + vertical + + + True + False + start + Show Title + True + end + 20 + + + + False + True + 0 + + + + + True + False + start + Episode Title + True + end + 20 + + + + False + True + 1 + + + + + False + True + 1 + + + + + 1 + + + + + True + True + True + 1 + False + + + 2 + + + + + True + False + start + center + 6 + + + True + False + start + center + 0:00 + + + False + False + 0 + + + + + True + False + start + center + / + + + False + False + 1 + + + + + True + False + start + center + 0:00 + + + False + False + 2 + + + + + 3 + + + + + True + True + True + center + center + 6 + up + rate_popover + + + True + False + 6 + + + True + False + 1.00x + + + False + True + 0 + + + + + True + False + go-down-symbolic + 1 + + + False + True + 1 + + + + + + + 4 + + + + + False + rate_button + + + True + False + 6 + 6 + 6 + 6 + vertical + 3 + + + 1.50x + True + True + False + center + center + True + normal_rate + + + True + True + 0 + + + + + 1.25x + True + True + False + center + center + True + normal_rate + + + True + True + 1 + + + + + 1.00x + True + True + False + center + center + True + True + + + True + True + 2 + + + + + + diff --git a/hammond-gtk/resources/gtk/style.css b/hammond-gtk/resources/gtk/style.css index b97847e..3af698c 100644 --- a/hammond-gtk/resources/gtk/style.css +++ b/hammond-gtk/resources/gtk/style.css @@ -9,3 +9,12 @@ row:last-child { list, border { border-radius: 4px; } + +.player-episode-label { + font-weight: bold; + font-size: smaller; +} + +.player-show-label { + font-size: smaller; +} \ No newline at end of file diff --git a/hammond-gtk/resources/resources.xml b/hammond-gtk/resources/resources.xml index 67b98ae..4925609 100644 --- a/hammond-gtk/resources/resources.xml +++ b/hammond-gtk/resources/resources.xml @@ -13,6 +13,7 @@ gtk/inapp_notif.ui gtk/menus.ui gtk/help-overlay.ui + gtk/player_toolbar.ui gtk/style.css diff --git a/hammond-gtk/src/app.rs b/hammond-gtk/src/app.rs index 3d9f4fb..8d89c47 100644 --- a/hammond-gtk/src/app.rs +++ b/hammond-gtk/src/app.rs @@ -17,6 +17,7 @@ use settings::{self, WindowGeometry}; use stacks::{Content, PopulatedState}; use utils; use widgets::appnotif::{InAppNotification, UndoState}; +use widgets::player; use widgets::{about_dialog, mark_all_notif, remove_show_notif}; use std::rc::Rc; @@ -53,6 +54,7 @@ pub enum Action { MarkAllPlayerNotification(Arc), RemoveShow(Arc), ErrorNotification(String), + InitEpisode(i32), } #[derive(Debug)] @@ -67,10 +69,6 @@ impl App { let application = gtk::Application::new("org.gnome.Hammond", ApplicationFlags::empty()) .expect("Application Initialization failed..."); - // Weird magic I copy-pasted that sets the Application Name in the Shell. - glib::set_application_name("Hammond"); - glib::set_prgname(Some("Hammond")); - let cleanup_date = settings::get_cleanup_date(&settings); utils::cleanup(cleanup_date); @@ -101,6 +99,7 @@ impl App { Inhibit(false) })); + // Create a content instance let content = Rc::new(Content::new(sender.clone()).expect( @@ -117,8 +116,15 @@ impl App { let overlay = gtk::Overlay::new(); overlay.add(&content.get_stack()); - // Add the overlay to the main window - window.add(&overlay); + let wrap = gtk::Box::new(gtk::Orientation::Vertical, 0); + // Add the overlay to the main Box + wrap.add(&overlay); + + let player = player::PlayerWidget::new(&sender); + // Add the player to the main Box + wrap.add(&player.action_bar); + + window.add(&wrap); WindowGeometry::from_settings(&settings).apply(&window); @@ -127,8 +133,8 @@ impl App { window.show_all(); window.activate(); - gtk::timeout_add(50, clone!(sender, receiver => move || { - // Uses receiver, content, header, sender, overlay + gtk::timeout_add(25, clone!(sender, receiver => move || { + // Uses receiver, content, header, sender, overlay, playback match receiver.try_recv() { Ok(Action::RefreshAllViews) => content.update(), Ok(Action::RefreshShowsView) => content.update_shows_view(), @@ -183,7 +189,8 @@ impl App { let notif = InAppNotification::new(&err, callback, || {}, UndoState::Hidden); notif.show(&overlay); - } + }, + Ok(Action::InitEpisode(rowid)) => player.initialize_episode(rowid).unwrap(), Err(_) => (), } @@ -288,6 +295,11 @@ impl App { } pub fn run(self) { + // Weird magic I copy-pasted that sets the Application Name in the Shell. + glib::set_application_name("Hammond"); + glib::set_prgname(Some("Hammond")); + // We need out own org.gnome.Hammon icon + gtk::Window::set_default_icon_name("multimedia-player"); ApplicationExtManual::run(&self.app_instance, &[]); } } diff --git a/hammond-gtk/src/main.rs b/hammond-gtk/src/main.rs index a102303..9322f42 100644 --- a/hammond-gtk/src/main.rs +++ b/hammond-gtk/src/main.rs @@ -10,6 +10,8 @@ extern crate gdk; extern crate gdk_pixbuf; extern crate gio; extern crate glib; +extern crate gstreamer as gst; +extern crate gstreamer_player as gst_player; extern crate gtk; #[macro_use] @@ -80,6 +82,7 @@ fn main() { // TODO: make the the logger a cli -vv option loggerv::init_with_level(Level::Info).expect("Error initializing loggerv."); gtk::init().expect("Error initializing gtk."); + gst::init().expect("Error initializing gstreamer"); static_resource::init().expect("Something went wrong with the resource file initialization."); // Add custom style diff --git a/hammond-gtk/src/widgets/episode.rs b/hammond-gtk/src/widgets/episode.rs index b7cfde3..aab0310 100644 --- a/hammond-gtk/src/widgets/episode.rs +++ b/hammond-gtk/src/widgets/episode.rs @@ -7,6 +7,7 @@ use chrono::prelude::*; use crossbeam_channel::Sender; use failure::Error; use humansize::{file_size_opts as size_opts, FileSize}; +#[allow(unused_imports)] use open; use hammond_data::dbqueries; @@ -16,7 +17,6 @@ use hammond_data::EpisodeWidgetQuery; use app::Action; use manager; -use std::path::Path; use std::rc::Rc; use std::sync::{Arc, Mutex, TryLockError}; @@ -446,29 +446,19 @@ fn on_play_bttn_clicked( episode: &mut EpisodeWidgetQuery, sender: &Sender, ) -> Result<(), Error> { - open_uri(episode.rowid())?; + // Mark played episode.set_played_now()?; - + // Grey out the title widget.info.set_title(&episode); + + // Play the episode + sender.send(Action::InitEpisode(episode.rowid()))?; + // Refresh background views to match the normal/greyout title state sender .send(Action::RefreshEpisodesViewBGR) .map_err(From::from) } -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."))?; - - if Path::new(&uri).exists() { - info!("Opening {}", uri); - open::that(&uri)?; - } else { - bail!("File \"{}\" does not exist.", uri); - } - - Ok(()) -} - // Setup a callback that will update the progress bar. #[inline] #[cfg_attr(feature = "cargo-clippy", allow(if_same_then_else))] diff --git a/hammond-gtk/src/widgets/mod.rs b/hammond-gtk/src/widgets/mod.rs index df7361d..f852617 100644 --- a/hammond-gtk/src/widgets/mod.rs +++ b/hammond-gtk/src/widgets/mod.rs @@ -3,6 +3,7 @@ pub mod appnotif; mod empty; mod episode; mod home_view; +pub mod player; mod show; mod shows_view; diff --git a/hammond-gtk/src/widgets/player.rs b/hammond-gtk/src/widgets/player.rs new file mode 100644 index 0000000..4ec2362 --- /dev/null +++ b/hammond-gtk/src/widgets/player.rs @@ -0,0 +1,430 @@ +use gst::prelude::*; +use gst::ClockTime; +use gst_player; + +use gtk; +use gtk::prelude::*; + +use gio::{File, FileExt}; +use glib::SignalHandlerId; + +use chrono::NaiveTime; +use crossbeam_channel::Sender; +use failure::Error; +use send_cell::SendCell; + +use hammond_data::{dbqueries, USER_AGENT}; +use hammond_data::{EpisodeWidgetQuery, PodcastCoverQuery}; + +use app::Action; +use utils::set_image_from_path; + +use std::ops::Deref; +use std::path::Path; +use std::rc::Rc; + +#[derive(Debug, Clone, Copy)] +enum SeekDirection { + Backwards, + Forward, +} + +trait PlayerExt { + fn play(&self); + fn pause(&self); + fn stop(&self); + fn seek(&self, offset: ClockTime, direction: SeekDirection); + fn fast_forward(&self); + fn rewind(&self); + fn set_playback_rate(&self, f64); +} + +#[derive(Debug, Clone)] +struct PlayerInfo { + container: gtk::Box, + show: gtk::Label, + episode: gtk::Label, + cover: gtk::Image, +} + +impl PlayerInfo { + // FIXME: create a Diesel Model of the joined episode and podcast query instead + fn init(&self, episode: &EpisodeWidgetQuery, podcast: &PodcastCoverQuery) { + self.set_cover_image(podcast); + self.set_show_title(podcast); + self.set_episode_title(episode); + } + + fn set_episode_title(&self, episode: &EpisodeWidgetQuery) { + self.episode.set_text(episode.title()); + self.episode.set_tooltip_text(episode.title()); + } + + fn set_show_title(&self, show: &PodcastCoverQuery) { + self.show.set_text(show.title()); + self.show.set_tooltip_text(show.title()); + } + + fn set_cover_image(&self, show: &PodcastCoverQuery) { + set_image_from_path(&self.cover, show.id(), 34) + .map_err(|err| error!("Player Cover: {}", err)) + .ok(); + } +} + +#[derive(Debug, Clone)] +struct PlayerTimes { + container: gtk::Box, + progressed: gtk::Label, + duration: gtk::Label, + separator: gtk::Label, + slider: gtk::Scale, + slider_update: Rc, +} + +#[derive(Debug, Clone, Copy)] +struct Duration(ClockTime); + +impl Deref for Duration { + type Target = ClockTime; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, Clone, Copy)] +struct Position(ClockTime); + +impl Deref for Position { + type Target = ClockTime; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl PlayerTimes { + /// Update the duration `gtk::Label` and the max range of the `gtk::SclaeBar`. + pub fn on_duration_changed(&self, duration: Duration) { + let seconds = duration.seconds().map(|v| v as f64).unwrap_or(0.0); + + self.slider.block_signal(&self.slider_update); + self.slider.set_range(0.0, seconds); + self.slider.unblock_signal(&self.slider_update); + + self.duration.set_text(&format_duration(seconds as u32)); + } + + /// Update the `gtk::SclaeBar` when the pipeline position is changed. + pub fn on_position_updated(&self, position: Position) { + let seconds = position.seconds().map(|v| v as f64).unwrap_or(0.0); + + self.slider.block_signal(&self.slider_update); + self.slider.set_value(seconds); + self.slider.unblock_signal(&self.slider_update); + + self.progressed.set_text(&format_duration(seconds as u32)); + } +} + +fn format_duration(seconds: u32) -> String { + let time = NaiveTime::from_num_seconds_from_midnight(seconds, 0); + + if seconds >= 3600 { + time.format("%T").to_string() + } else { + time.format("%M:%S").to_string() + } +} + +#[derive(Debug, Clone)] +struct PlayerRate { + radio150: gtk::RadioButton, + radio125: gtk::RadioButton, + radio_normal: gtk::RadioButton, + popover: gtk::Popover, + btn: gtk::MenuButton, + label: gtk::Label, +} + +#[derive(Debug, Clone)] +struct PlayerControls { + container: gtk::Box, + play: gtk::Button, + pause: gtk::Button, + forward: gtk::Button, + rewind: gtk::Button, +} + +#[derive(Debug, Clone)] +pub struct PlayerWidget { + pub action_bar: gtk::ActionBar, + player: gst_player::Player, + controls: PlayerControls, + timer: PlayerTimes, + info: PlayerInfo, + rate: PlayerRate, +} + +impl Default for PlayerWidget { + fn default() -> Self { + let dispatcher = gst_player::PlayerGMainContextSignalDispatcher::new(None); + let player = gst_player::Player::new( + None, + // Use the gtk main thread + Some(&dispatcher.upcast::()), + ); + + let mut config = player.get_config(); + config.set_user_agent(USER_AGENT); + config.set_position_update_interval(250); + player.set_config(config).unwrap(); + + let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/player_toolbar.ui"); + let action_bar = builder.get_object("action_bar").unwrap(); + + let buttons = builder.get_object("buttons").unwrap(); + let play = builder.get_object("play_button").unwrap(); + let pause = builder.get_object("pause_button").unwrap(); + let forward = builder.get_object("ff_button").unwrap(); + let rewind = builder.get_object("rewind_button").unwrap(); + + let controls = PlayerControls { + container: buttons, + play, + pause, + forward, + rewind, + }; + + let timer_container = builder.get_object("timer").unwrap(); + let progressed = builder.get_object("progress_time_label").unwrap(); + let duration = builder.get_object("total_duration_label").unwrap(); + let separator = builder.get_object("separator").unwrap(); + let slider: gtk::Scale = builder.get_object("seek").unwrap(); + slider.set_range(0.0, 1.0); + let slider_update = Rc::new(Self::connect_update_slider(&slider, &player)); + let timer = PlayerTimes { + container: timer_container, + progressed, + duration, + separator, + slider, + slider_update, + }; + + let labels = builder.get_object("info").unwrap(); + let show = builder.get_object("show_label").unwrap(); + let episode = builder.get_object("episode_label").unwrap(); + let cover = builder.get_object("show_cover").unwrap(); + let info = PlayerInfo { + container: labels, + show, + episode, + cover, + }; + + let radio150 = builder.get_object("rate_1_50").unwrap(); + let radio125 = builder.get_object("rate_1_25").unwrap(); + let radio_normal = builder.get_object("normal_rate").unwrap(); + let popover = builder.get_object("rate_popover").unwrap(); + let btn = builder.get_object("rate_button").unwrap(); + let label = builder.get_object("rate_label").unwrap(); + let rate = PlayerRate { + radio150, + radio125, + radio_normal, + popover, + label, + btn, + }; + + PlayerWidget { + player, + action_bar, + controls, + timer, + info, + rate, + } + } +} + +impl PlayerWidget { + pub fn new(sender: &Sender) -> Rc { + let w = Rc::new(Self::default()); + Self::init(&w, sender); + w + } + + fn init(s: &Rc, sender: &Sender) { + Self::connect_control_buttons(s); + Self::connect_rate_buttons(s); + Self::connect_gst_signals(s, sender); + } + + #[cfg_attr(rustfmt, rustfmt_skip)] + /// Connect the `PlayerControls` buttons to the `PlayerExt` methods. + fn connect_control_buttons(s: &Rc) { + // Connect the play button to the gst Player. + s.controls.play.connect_clicked(clone!(s => move |_| s.play())); + + // Connect the pause button to the gst Player. + s.controls.pause.connect_clicked(clone!(s => move |_| s.pause())); + + // Connect the rewind button to the gst Player. + s.controls.rewind.connect_clicked(clone!(s => move |_| s.rewind())); + + // Connect the fast-forward button to the gst Player. + s.controls.forward.connect_clicked(clone!(s => move |_| s.fast_forward())); + } + + #[cfg_attr(rustfmt, rustfmt_skip)] + fn connect_gst_signals(s: &Rc, sender: &Sender) { + // Log gst warnings. + s.player.connect_warning(move |_, warn| warn!("gst warning: {}", warn)); + + // Log gst errors. + s.player.connect_error(clone!(sender => move |_, error| { + // FIXME: should never occur and should not be user facing. + sender.send(Action::ErrorNotification(format!("Player Error: {}", error))) + .map_err(|err| error!("Error: {}", err)) + .ok(); + + })); + + // The followign callbacks require `Send` but are handled by the gtk main loop + let s2 = SendCell::new(s.clone()); + + // Update the duration label and the slider + s.player.connect_duration_changed(clone!(s2 => move |_, clock| { + s2.borrow().timer.on_duration_changed(Duration(clock)); + })); + + // Update the position label and the slider + s.player.connect_position_updated(clone!(s2 => move |_, clock| { + s2.borrow().timer.on_position_updated(Position(clock)); + })); + + // Reset the slider to 0 and show a play button + s.player.connect_end_of_stream(clone!(s2 => move |_| s2.borrow().stop())); + } + + #[cfg_attr(rustfmt, rustfmt_skip)] + fn connect_rate_buttons(s: &Rc) { + s.rate.radio_normal.connect_toggled(clone!(s => move |_| s.on_rate_changed(1.00))); + s.rate.radio125.connect_toggled(clone!(s => move |_| s.on_rate_changed(1.25))); + s.rate.radio150.connect_toggled(clone!(s => move |_| s.on_rate_changed(1.50))); + } + + fn on_rate_changed(&self, rate: f64) { + self.set_playback_rate(rate); + self.rate.label.set_text(&format!("{:.2}x", rate)); + } + + fn reveal(&self) { + self.action_bar.show(); + } + + pub fn initialize_episode(&self, rowid: i32) -> Result<(), Error> { + let ep = dbqueries::get_episode_widget_from_rowid(rowid)?; + let pd = dbqueries::get_podcast_cover_from_id(ep.podcast_id())?; + + self.info.init(&ep, &pd); + // Currently that will always be the case since the play button is + // only shown if the file is downloaded + if let Some(ref path) = ep.local_uri() { + if Path::new(path).exists() { + // path is an absolute fs path ex. "foo/bar/baz". + // Convert it so it will have a "file:///" + // FIXME: convert it properly + if let Some(uri) = File::new_for_path(path).get_uri() { + // play the file + self.player.set_uri(&uri); + self.play(); + return Ok(()); + } + } + // TODO: log an error + } + + // FIXME: Stream stuff + // unimplemented!() + Ok(()) + } + + fn connect_update_slider(slider: >k::Scale, player: &gst_player::Player) -> SignalHandlerId { + slider.connect_value_changed(clone!(player => move |slider| { + let value = slider.get_value() as u64; + player.seek(ClockTime::from_seconds(value as u64)); + })) + } +} + +impl PlayerExt for PlayerWidget { + fn play(&self) { + self.reveal(); + + self.controls.pause.show(); + self.controls.play.hide(); + + self.player.play(); + } + + fn pause(&self) { + self.controls.pause.hide(); + self.controls.play.show(); + + self.player.pause(); + + // Only rewind on pause if the stream position is passed a certain point. + if let Some(sec) = self.player.get_position().seconds() { + if sec >= 90 { + self.seek(ClockTime::from_seconds(5), SeekDirection::Backwards); + } + } + } + + #[cfg_attr(rustfmt, rustfmt_skip)] + fn stop(&self) { + self.controls.pause.hide(); + self.controls.play.show(); + + self.player.stop(); + + // Reset the slider bar to the start + self.timer.on_position_updated(Position(ClockTime::from_seconds(0))); + } + + // Adapted from https://github.com/philn/glide/blob/b52a65d99daeab0b487f79a0e1ccfad0cd433e22/src/player_context.rs#L219-L245 + fn seek(&self, offset: ClockTime, direction: SeekDirection) { + let position = self.player.get_position(); + if position.is_none() || offset.is_none() { + return; + } + + let duration = self.player.get_duration(); + let destination = match direction { + SeekDirection::Backwards if position >= offset => Some(position - offset), + SeekDirection::Forward if !duration.is_none() && position + offset <= duration => { + Some(position + offset) + } + _ => None, + }; + + destination.map(|d| self.player.seek(d)); + } + + // FIXME: make the interval a GSetting + fn rewind(&self) { + self.seek(ClockTime::from_seconds(10), SeekDirection::Backwards) + } + + // FIXME: make the interval a GSetting + fn fast_forward(&self) { + self.seek(ClockTime::from_seconds(10), SeekDirection::Forward) + } + + fn set_playback_rate(&self, rate: f64) { + self.player.set_rate(rate); + } +} diff --git a/org.gnome.Hammond.json b/org.gnome.Hammond.json index 8e27d4f..6b305d8 100644 --- a/org.gnome.Hammond.json +++ b/org.gnome.Hammond.json @@ -20,6 +20,7 @@ "--share=ipc", "--socket=x11", "--socket=wayland", + "--socket=pulseaudio", "--talk-name=org.freedesktop.Desktop" ], "build-options" : {