diff --git a/Cargo.lock b/Cargo.lock index dd36dc98..c102175c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -211,30 +211,6 @@ dependencies = [ "syn", ] -[[package]] -name = "atk" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a83b21d2aa75e464db56225e1bda2dd5993311ba1095acaa8fa03d1ae67026ba" -dependencies = [ - "atk-sys", - "bitflags", - "glib", - "libc", -] - -[[package]] -name = "atk-sys" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "badcf670157c84bb8b1cf6b5f70b650fed78da2033c9eed84c4e49b11cbe83ea" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - [[package]] name = "atomic-waker" version = "1.0.0" @@ -885,22 +861,6 @@ dependencies = [ "slab", ] -[[package]] -name = "gdk" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "679e22651cd15888e7acd01767950edca2ee9fcd6421fbf5b3c3b420d4e88bb0" -dependencies = [ - "bitflags", - "cairo-rs", - "gdk-pixbuf", - "gdk-sys", - "gio", - "glib", - "libc", - "pango", -] - [[package]] name = "gdk-pixbuf" version = "0.14.0" @@ -927,19 +887,35 @@ dependencies = [ ] [[package]] -name = "gdk-sys" -version = "0.14.0" +name = "gdk4" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce41092cc569129a0afa34926e6dd1cf8411e25652d87febdea36859f7ff7ba" +dependencies = [ + "bitflags", + "cairo-rs", + "gdk-pixbuf", + "gdk4-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk4-sys" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e091b3d3d6696949ac3b3fb3c62090e5bfd7bd6850bef5c3c5ea701de1b1f1e" +checksum = "ce39c71861b5bcde319fd4711a74e1bd6f4f474911170d51096597fef0b56011" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", "gio-sys", "glib-sys", "gobject-sys", + "graphene-sys", "libc", "pango-sys", - "pkg-config", "system-deps", ] @@ -1034,26 +1010,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "gladis" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed050efc446f5350bbd26a3ffaf5ba1569f754a5ee5f0cb4b772e759a183327" -dependencies = [ - "gladis_proc_macro", - "gtk", -] - -[[package]] -name = "gladis_proc_macro" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53987c930e0b3a52e33148d4f5f3cddc8019573139162155b59f85aa84abd264" -dependencies = [ - "quote", - "syn", -] - [[package]] name = "glib" version = "0.14.2" @@ -1129,54 +1085,92 @@ dependencies = [ ] [[package]] -name = "gtk" +name = "graphene-rs" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1460a39f06e491e6112f27e71e51435c833ba370723224dd1743dfd1f201f19" +dependencies = [ + "glib", + "graphene-sys", + "libc", +] + +[[package]] +name = "graphene-sys" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10ae864e5eab8bc8b6b8544ed259eb02dd61b25323b20e777a77aa289c05fd0c" +checksum = "e7d23fb7a9547e5f072a7e0cd49cd648fedeb786d122b106217511980cbb8962" +dependencies = [ + "glib-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gsk4" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64932b730eaad3340378a03d633616eeed6d6705b59b81c9f579c88be8932475" dependencies = [ - "atk", "bitflags", "cairo-rs", - "field-offset", - "futures-channel", - "gdk", - "gdk-pixbuf", - "gio", + "gdk4", "glib", - "gtk-sys", - "gtk3-macros", + "graphene-rs", + "gsk4-sys", "libc", - "once_cell", "pango", - "pkg-config", ] [[package]] -name = "gtk-sys" -version = "0.14.0" +name = "gsk4-sys" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c14c8d3da0545785a7c5a120345b3abb534010fb8ae0f2ef3f47c027fba303e" +checksum = "685ffc776bedd91d68f47b41239525778b669432889721d7050d045270549b9a" dependencies = [ - "atk-sys", "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk-sys", - "gio-sys", + "gdk4-sys", "glib-sys", "gobject-sys", + "graphene-sys", "libc", "pango-sys", "system-deps", ] [[package]] -name = "gtk3-macros" -version = "0.14.0" +name = "gtk4" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c49e0311dac847a8ebc05e31f5c44c596314ee3b16c5f638ccfe24086d24bf1b" +dependencies = [ + "bitflags", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk-pixbuf", + "gdk4", + "gio", + "glib", + "graphene-rs", + "gsk4", + "gtk4-macros", + "gtk4-sys", + "libc", + "once_cell", + "pango", +] + +[[package]] +name = "gtk4-macros" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21de1da96dc117443fb03c2e270b2d34b7de98d0a79a19bbb689476173745b79" +checksum = "bbe4b77996bcf1ef20208c00043edda854ca2091b4be5e6a7c367f0f3846fa67" dependencies = [ "anyhow", "heck", + "itertools", "proc-macro-crate 1.0.0", "proc-macro-error", "proc-macro2", @@ -1184,6 +1178,25 @@ dependencies = [ "syn", ] +[[package]] +name = "gtk4-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3737e91619cf4257d8a07834f7a2c035d4daeaf9ad8e3958e56b2c411dbdca18" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "gsk4-sys", + "libc", + "pango-sys", + "system-deps", +] + [[package]] name = "hashbrown" version = "0.9.1" @@ -1528,47 +1541,44 @@ dependencies = [ ] [[package]] -name = "libc" -version = "0.2.94" +name = "libadwaita" +version = "0.1.0-alpha-2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" - -[[package]] -name = "libhandy" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bcf9c79ec810a62f442ffd568d2de233983dc91c160abee4949b67a647024ed" +checksum = "d117dc3147a6e5917a4652f638cfcb512edf5d29de2af6a8e30e0d5de4f7395b" dependencies = [ - "bitflags", - "gdk", "gdk-pixbuf", + "gdk4", "gio", "glib", - "gtk", - "lazy_static", + "gtk4", + "libadwaita-sys", "libc", - "libhandy-sys", "pango", ] [[package]] -name = "libhandy-sys" -version = "0.8.0" +name = "libadwaita-sys" +version = "0.1.0-alpha-2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1938b93a8f29417992c452b7f43e7eff8a9f8d25b7f0bc923ae9d75b50a9cde3" +checksum = "64f1631562bdc3061b757290f17ea23237d59354ff6907069f40e1366fdb1426" dependencies = [ "gdk-pixbuf-sys", - "gdk-sys", + "gdk4-sys", "gio-sys", "glib-sys", "gobject-sys", - "gtk-sys", + "gtk4-sys", "libc", "pango-sys", - "pkg-config", "system-deps", ] +[[package]] +name = "libc" +version = "0.2.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" + [[package]] name = "libloading" version = "0.5.2" @@ -2969,16 +2979,15 @@ dependencies = [ "async-std", "form_urlencoded", "futures", - "gdk", "gdk-pixbuf", + "gdk4", "gettext-rs", "gio", - "gladis", "glib", - "gtk", + "gtk4", "isahc", "lazy_static", - "libhandy", + "libadwaita", "librespot", "protobuf", "rand 0.8.4", diff --git a/Cargo.toml b/Cargo.toml index 44e3116d..aab3cfcd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,16 +4,13 @@ version = "0.1.16" edition = "2018" license = "MIT" -[features] -warn-cache = [] - [dependencies.gtk] -version = "^0.14.0" -features = ["v3_24"] +version = "^0.2.0" +package = "gtk4" [dependencies.gdk] -version = "^0.14.0" -features = ["v3_24"] +version = "^0.2.0" +package = "gdk4" [dependencies.gio] version = "^0.14.0" @@ -61,8 +58,7 @@ features = ["gettext-system"] [dependencies] secret-service = "^2.0.1" gdk-pixbuf = "^0.14.0" -libhandy = "^0.8.0" -gladis = "^1.0.0" +libadwaita = "0.1.0-alpha-2" ref_filter_map = "^1.0.1" regex = "^1.4.6" async-std = "^1.9.0" diff --git a/README.md b/README.md index 36841e7b..04ef5052 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,6 @@ Based on [librespot](https://github.com/librespot-org/librespot/). ![Spot screenshot](./data/appstream/2.png) -[Older demo gif](./demo.gif) - ## Installing | Package | Maintainer | Repo | @@ -17,11 +15,6 @@ Based on [librespot](https://github.com/librespot-org/librespot/). | AUR version | dpeukert | https://gitlab.com/dpeukert/pkgbuilds/tree/main/spot-client | -## GTK4 - -**The GTK4 port is almost ready :) all future development and contributions should ideally target the [gtk4/main](https://github.com/xou816/spot/tree/gtk4/main) branch!** - - ## Usage notes ### Credentials @@ -77,7 +70,7 @@ See [this comment](https://github.com/xou816/spot/issues/209#issuecomment-860180 - playlist management (creation and edition) - liked tracks - GNOME search provider? -- improved search? (track results, ) +- improved search? (track results) - recommendations? ## Contributing @@ -105,17 +98,15 @@ If you can't build Spot locally, you may run the `spot-snapshots` action against ### With GNOME Builder and flatpak -Pre-requisite: install the `org.freedesktop.Sdk.Extension.rust-stable` SDK extension with flatpak. Builder might do this for you automatically, but it will install an older version; make sure the version installed matches the version of the Freedesktop SDK GNOME uses (at the time of writing: 20.08). +Pre-requisite: install the `org.freedesktop.Sdk.Extension.rust-stable` SDK extension with flatpak. Builder might do this for you automatically, but it will install an older version; make sure the version installed matches the version of the Freedesktop SDK GNOME uses. Open the project in GNOME Builder and make the `dev.alextren.Spot.development.json` configuration active. Then build :) ### Manually -Requires Rust (stable), GTK3, and a couple other things. Also requires libhandy1: it is not packaged on all distros at the moment, you might have to build it yourself! - -**Build** dependencies on Ubuntu 20.04 for instance: ```build-essential pkg-config meson libssl-dev libglib2.0-dev-bin libgtk-3-dev libasound2-dev libpulse-dev```. +Requires Rust (stable), **GTK4**, and a couple other things. Also requires **libadwaita**: it is not packaged on all distros at the moment, you might have to build it yourself! -Then, with meson: +With meson: ``` meson target -Dbuildtype=debug -Doffline=false --prefix="$HOME/.local" diff --git a/build-aux/clippy.sh b/build-aux/clippy.sh index 70f0ab65..f60004bf 100644 --- a/build-aux/clippy.sh +++ b/build-aux/clippy.sh @@ -8,4 +8,4 @@ if [[ $OFFLINE = "true" ]]; then export CARGO_HOME="$SRC"/cargo fi -cargo clippy --manifest-path "$SRC"/Cargo.toml -- -D warnings -A clippy::module_inception +cargo clippy --manifest-path "$SRC"/Cargo.toml -- -D warnings -A clippy::module_inception -A clippy::new_without_default diff --git a/cargo-sources.json b/cargo-sources.json index 220df21b..ed0280bc 100644 --- a/cargo-sources.json +++ b/cargo-sources.json @@ -233,32 +233,6 @@ "dest": "cargo/vendor/async-trait-0.1.42", "dest-filename": ".cargo-checksum.json" }, - { - "type": "file", - "url": "https://static.crates.io/crates/atk/atk-0.14.0.crate", - "sha256": "a83b21d2aa75e464db56225e1bda2dd5993311ba1095acaa8fa03d1ae67026ba", - "dest": "cargo/vendor", - "dest-filename": "atk-0.14.0.crate" - }, - { - "type": "file", - "url": "data:%7B%22package%22%3A%20%22a83b21d2aa75e464db56225e1bda2dd5993311ba1095acaa8fa03d1ae67026ba%22%2C%20%22files%22%3A%20%7B%7D%7D", - "dest": "cargo/vendor/atk-0.14.0", - "dest-filename": ".cargo-checksum.json" - }, - { - "type": "file", - "url": "https://static.crates.io/crates/atk-sys/atk-sys-0.14.0.crate", - "sha256": "badcf670157c84bb8b1cf6b5f70b650fed78da2033c9eed84c4e49b11cbe83ea", - "dest": "cargo/vendor", - "dest-filename": "atk-sys-0.14.0.crate" - }, - { - "type": "file", - "url": "data:%7B%22package%22%3A%20%22badcf670157c84bb8b1cf6b5f70b650fed78da2033c9eed84c4e49b11cbe83ea%22%2C%20%22files%22%3A%20%7B%7D%7D", - "dest": "cargo/vendor/atk-sys-0.14.0", - "dest-filename": ".cargo-checksum.json" - }, { "type": "file", "url": "https://static.crates.io/crates/atomic-waker/atomic-waker-1.0.0.crate", @@ -1130,19 +1104,6 @@ "dest": "cargo/vendor/futures-util-0.3.16", "dest-filename": ".cargo-checksum.json" }, - { - "type": "file", - "url": "https://static.crates.io/crates/gdk/gdk-0.14.0.crate", - "sha256": "679e22651cd15888e7acd01767950edca2ee9fcd6421fbf5b3c3b420d4e88bb0", - "dest": "cargo/vendor", - "dest-filename": "gdk-0.14.0.crate" - }, - { - "type": "file", - "url": "data:%7B%22package%22%3A%20%22679e22651cd15888e7acd01767950edca2ee9fcd6421fbf5b3c3b420d4e88bb0%22%2C%20%22files%22%3A%20%7B%7D%7D", - "dest": "cargo/vendor/gdk-0.14.0", - "dest-filename": ".cargo-checksum.json" - }, { "type": "file", "url": "https://static.crates.io/crates/gdk-pixbuf/gdk-pixbuf-0.14.0.crate", @@ -1171,15 +1132,28 @@ }, { "type": "file", - "url": "https://static.crates.io/crates/gdk-sys/gdk-sys-0.14.0.crate", - "sha256": "0e091b3d3d6696949ac3b3fb3c62090e5bfd7bd6850bef5c3c5ea701de1b1f1e", + "url": "https://static.crates.io/crates/gdk4/gdk4-0.2.0.crate", + "sha256": "8ce41092cc569129a0afa34926e6dd1cf8411e25652d87febdea36859f7ff7ba", "dest": "cargo/vendor", - "dest-filename": "gdk-sys-0.14.0.crate" + "dest-filename": "gdk4-0.2.0.crate" }, { "type": "file", - "url": "data:%7B%22package%22%3A%20%220e091b3d3d6696949ac3b3fb3c62090e5bfd7bd6850bef5c3c5ea701de1b1f1e%22%2C%20%22files%22%3A%20%7B%7D%7D", - "dest": "cargo/vendor/gdk-sys-0.14.0", + "url": "data:%7B%22package%22%3A%20%228ce41092cc569129a0afa34926e6dd1cf8411e25652d87febdea36859f7ff7ba%22%2C%20%22files%22%3A%20%7B%7D%7D", + "dest": "cargo/vendor/gdk4-0.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "file", + "url": "https://static.crates.io/crates/gdk4-sys/gdk4-sys-0.2.0.crate", + "sha256": "ce39c71861b5bcde319fd4711a74e1bd6f4f474911170d51096597fef0b56011", + "dest": "cargo/vendor", + "dest-filename": "gdk4-sys-0.2.0.crate" + }, + { + "type": "file", + "url": "data:%7B%22package%22%3A%20%22ce39c71861b5bcde319fd4711a74e1bd6f4f474911170d51096597fef0b56011%22%2C%20%22files%22%3A%20%7B%7D%7D", + "dest": "cargo/vendor/gdk4-sys-0.2.0", "dest-filename": ".cargo-checksum.json" }, { @@ -1286,32 +1260,6 @@ "dest": "cargo/vendor/gio-sys-0.14.0", "dest-filename": ".cargo-checksum.json" }, - { - "type": "file", - "url": "https://static.crates.io/crates/gladis/gladis-1.0.0.crate", - "sha256": "6ed050efc446f5350bbd26a3ffaf5ba1569f754a5ee5f0cb4b772e759a183327", - "dest": "cargo/vendor", - "dest-filename": "gladis-1.0.0.crate" - }, - { - "type": "file", - "url": "data:%7B%22package%22%3A%20%226ed050efc446f5350bbd26a3ffaf5ba1569f754a5ee5f0cb4b772e759a183327%22%2C%20%22files%22%3A%20%7B%7D%7D", - "dest": "cargo/vendor/gladis-1.0.0", - "dest-filename": ".cargo-checksum.json" - }, - { - "type": "file", - "url": "https://static.crates.io/crates/gladis_proc_macro/gladis_proc_macro-1.0.0.crate", - "sha256": "53987c930e0b3a52e33148d4f5f3cddc8019573139162155b59f85aa84abd264", - "dest": "cargo/vendor", - "dest-filename": "gladis_proc_macro-1.0.0.crate" - }, - { - "type": "file", - "url": "data:%7B%22package%22%3A%20%2253987c930e0b3a52e33148d4f5f3cddc8019573139162155b59f85aa84abd264%22%2C%20%22files%22%3A%20%7B%7D%7D", - "dest": "cargo/vendor/gladis_proc_macro-1.0.0", - "dest-filename": ".cargo-checksum.json" - }, { "type": "file", "url": "https://static.crates.io/crates/glib/glib-0.14.2.crate", @@ -1392,41 +1340,93 @@ }, { "type": "file", - "url": "https://static.crates.io/crates/gtk/gtk-0.14.0.crate", - "sha256": "10ae864e5eab8bc8b6b8544ed259eb02dd61b25323b20e777a77aa289c05fd0c", + "url": "https://static.crates.io/crates/graphene-rs/graphene-rs-0.14.0.crate", + "sha256": "f1460a39f06e491e6112f27e71e51435c833ba370723224dd1743dfd1f201f19", + "dest": "cargo/vendor", + "dest-filename": "graphene-rs-0.14.0.crate" + }, + { + "type": "file", + "url": "data:%7B%22package%22%3A%20%22f1460a39f06e491e6112f27e71e51435c833ba370723224dd1743dfd1f201f19%22%2C%20%22files%22%3A%20%7B%7D%7D", + "dest": "cargo/vendor/graphene-rs-0.14.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "file", + "url": "https://static.crates.io/crates/graphene-sys/graphene-sys-0.14.0.crate", + "sha256": "e7d23fb7a9547e5f072a7e0cd49cd648fedeb786d122b106217511980cbb8962", "dest": "cargo/vendor", - "dest-filename": "gtk-0.14.0.crate" + "dest-filename": "graphene-sys-0.14.0.crate" }, { "type": "file", - "url": "data:%7B%22package%22%3A%20%2210ae864e5eab8bc8b6b8544ed259eb02dd61b25323b20e777a77aa289c05fd0c%22%2C%20%22files%22%3A%20%7B%7D%7D", - "dest": "cargo/vendor/gtk-0.14.0", + "url": "data:%7B%22package%22%3A%20%22e7d23fb7a9547e5f072a7e0cd49cd648fedeb786d122b106217511980cbb8962%22%2C%20%22files%22%3A%20%7B%7D%7D", + "dest": "cargo/vendor/graphene-sys-0.14.0", "dest-filename": ".cargo-checksum.json" }, { "type": "file", - "url": "https://static.crates.io/crates/gtk-sys/gtk-sys-0.14.0.crate", - "sha256": "8c14c8d3da0545785a7c5a120345b3abb534010fb8ae0f2ef3f47c027fba303e", + "url": "https://static.crates.io/crates/gsk4/gsk4-0.2.0.crate", + "sha256": "64932b730eaad3340378a03d633616eeed6d6705b59b81c9f579c88be8932475", "dest": "cargo/vendor", - "dest-filename": "gtk-sys-0.14.0.crate" + "dest-filename": "gsk4-0.2.0.crate" }, { "type": "file", - "url": "data:%7B%22package%22%3A%20%228c14c8d3da0545785a7c5a120345b3abb534010fb8ae0f2ef3f47c027fba303e%22%2C%20%22files%22%3A%20%7B%7D%7D", - "dest": "cargo/vendor/gtk-sys-0.14.0", + "url": "data:%7B%22package%22%3A%20%2264932b730eaad3340378a03d633616eeed6d6705b59b81c9f579c88be8932475%22%2C%20%22files%22%3A%20%7B%7D%7D", + "dest": "cargo/vendor/gsk4-0.2.0", "dest-filename": ".cargo-checksum.json" }, { "type": "file", - "url": "https://static.crates.io/crates/gtk3-macros/gtk3-macros-0.14.0.crate", - "sha256": "21de1da96dc117443fb03c2e270b2d34b7de98d0a79a19bbb689476173745b79", + "url": "https://static.crates.io/crates/gsk4-sys/gsk4-sys-0.2.0.crate", + "sha256": "685ffc776bedd91d68f47b41239525778b669432889721d7050d045270549b9a", "dest": "cargo/vendor", - "dest-filename": "gtk3-macros-0.14.0.crate" + "dest-filename": "gsk4-sys-0.2.0.crate" }, { "type": "file", - "url": "data:%7B%22package%22%3A%20%2221de1da96dc117443fb03c2e270b2d34b7de98d0a79a19bbb689476173745b79%22%2C%20%22files%22%3A%20%7B%7D%7D", - "dest": "cargo/vendor/gtk3-macros-0.14.0", + "url": "data:%7B%22package%22%3A%20%22685ffc776bedd91d68f47b41239525778b669432889721d7050d045270549b9a%22%2C%20%22files%22%3A%20%7B%7D%7D", + "dest": "cargo/vendor/gsk4-sys-0.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "file", + "url": "https://static.crates.io/crates/gtk4/gtk4-0.2.0.crate", + "sha256": "c49e0311dac847a8ebc05e31f5c44c596314ee3b16c5f638ccfe24086d24bf1b", + "dest": "cargo/vendor", + "dest-filename": "gtk4-0.2.0.crate" + }, + { + "type": "file", + "url": "data:%7B%22package%22%3A%20%22c49e0311dac847a8ebc05e31f5c44c596314ee3b16c5f638ccfe24086d24bf1b%22%2C%20%22files%22%3A%20%7B%7D%7D", + "dest": "cargo/vendor/gtk4-0.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "file", + "url": "https://static.crates.io/crates/gtk4-macros/gtk4-macros-0.2.0.crate", + "sha256": "bbe4b77996bcf1ef20208c00043edda854ca2091b4be5e6a7c367f0f3846fa67", + "dest": "cargo/vendor", + "dest-filename": "gtk4-macros-0.2.0.crate" + }, + { + "type": "file", + "url": "data:%7B%22package%22%3A%20%22bbe4b77996bcf1ef20208c00043edda854ca2091b4be5e6a7c367f0f3846fa67%22%2C%20%22files%22%3A%20%7B%7D%7D", + "dest": "cargo/vendor/gtk4-macros-0.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "file", + "url": "https://static.crates.io/crates/gtk4-sys/gtk4-sys-0.2.0.crate", + "sha256": "3737e91619cf4257d8a07834f7a2c035d4daeaf9ad8e3958e56b2c411dbdca18", + "dest": "cargo/vendor", + "dest-filename": "gtk4-sys-0.2.0.crate" + }, + { + "type": "file", + "url": "data:%7B%22package%22%3A%20%223737e91619cf4257d8a07834f7a2c035d4daeaf9ad8e3958e56b2c411dbdca18%22%2C%20%22files%22%3A%20%7B%7D%7D", + "dest": "cargo/vendor/gtk4-sys-0.2.0", "dest-filename": ".cargo-checksum.json" }, { @@ -1873,41 +1873,41 @@ }, { "type": "file", - "url": "https://static.crates.io/crates/libc/libc-0.2.94.crate", - "sha256": "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e", + "url": "https://static.crates.io/crates/libadwaita/libadwaita-0.1.0-alpha-2.crate", + "sha256": "d117dc3147a6e5917a4652f638cfcb512edf5d29de2af6a8e30e0d5de4f7395b", "dest": "cargo/vendor", - "dest-filename": "libc-0.2.94.crate" + "dest-filename": "libadwaita-0.1.0-alpha-2.crate" }, { "type": "file", - "url": "data:%7B%22package%22%3A%20%2218794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e%22%2C%20%22files%22%3A%20%7B%7D%7D", - "dest": "cargo/vendor/libc-0.2.94", + "url": "data:%7B%22package%22%3A%20%22d117dc3147a6e5917a4652f638cfcb512edf5d29de2af6a8e30e0d5de4f7395b%22%2C%20%22files%22%3A%20%7B%7D%7D", + "dest": "cargo/vendor/libadwaita-0.1.0-alpha-2", "dest-filename": ".cargo-checksum.json" }, { "type": "file", - "url": "https://static.crates.io/crates/libhandy/libhandy-0.8.0.crate", - "sha256": "5bcf9c79ec810a62f442ffd568d2de233983dc91c160abee4949b67a647024ed", + "url": "https://static.crates.io/crates/libadwaita-sys/libadwaita-sys-0.1.0-alpha-2.crate", + "sha256": "64f1631562bdc3061b757290f17ea23237d59354ff6907069f40e1366fdb1426", "dest": "cargo/vendor", - "dest-filename": "libhandy-0.8.0.crate" + "dest-filename": "libadwaita-sys-0.1.0-alpha-2.crate" }, { "type": "file", - "url": "data:%7B%22package%22%3A%20%225bcf9c79ec810a62f442ffd568d2de233983dc91c160abee4949b67a647024ed%22%2C%20%22files%22%3A%20%7B%7D%7D", - "dest": "cargo/vendor/libhandy-0.8.0", + "url": "data:%7B%22package%22%3A%20%2264f1631562bdc3061b757290f17ea23237d59354ff6907069f40e1366fdb1426%22%2C%20%22files%22%3A%20%7B%7D%7D", + "dest": "cargo/vendor/libadwaita-sys-0.1.0-alpha-2", "dest-filename": ".cargo-checksum.json" }, { "type": "file", - "url": "https://static.crates.io/crates/libhandy-sys/libhandy-sys-0.8.0.crate", - "sha256": "1938b93a8f29417992c452b7f43e7eff8a9f8d25b7f0bc923ae9d75b50a9cde3", + "url": "https://static.crates.io/crates/libc/libc-0.2.94.crate", + "sha256": "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e", "dest": "cargo/vendor", - "dest-filename": "libhandy-sys-0.8.0.crate" + "dest-filename": "libc-0.2.94.crate" }, { "type": "file", - "url": "data:%7B%22package%22%3A%20%221938b93a8f29417992c452b7f43e7eff8a9f8d25b7f0bc923ae9d75b50a9cde3%22%2C%20%22files%22%3A%20%7B%7D%7D", - "dest": "cargo/vendor/libhandy-sys-0.8.0", + "url": "data:%7B%22package%22%3A%20%2218794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e%22%2C%20%22files%22%3A%20%7B%7D%7D", + "dest": "cargo/vendor/libc-0.2.94", "dest-filename": ".cargo-checksum.json" }, { diff --git a/data/appstream/1.png b/data/appstream/1.png index d3170cd4..5ca810e9 100644 Binary files a/data/appstream/1.png and b/data/appstream/1.png differ diff --git a/data/appstream/2.png b/data/appstream/2.png index 146e867c..fa4d58d1 100644 Binary files a/data/appstream/2.png and b/data/appstream/2.png differ diff --git a/data/appstream/3.png b/data/appstream/3.png index 5d4c32f0..1f9e8384 100644 Binary files a/data/appstream/3.png and b/data/appstream/3.png differ diff --git a/data/dev.alextren.Spot.gschema.xml b/data/dev.alextren.Spot.gschema.xml index 3051e3c8..12b74fce 100644 --- a/data/dev.alextren.Spot.gschema.xml +++ b/data/dev.alextren.Spot.gschema.xml @@ -10,10 +10,6 @@ - - false - Has the old cache been cleared? - true Prefer dark theme diff --git a/dev.alextren.Spot.develoment.json b/dev.alextren.Spot.develoment.json index df430901..35003b42 100644 --- a/dev.alextren.Spot.develoment.json +++ b/dev.alextren.Spot.develoment.json @@ -36,14 +36,52 @@ "*.a" ], "modules": [ + { + "name": "libadwaita", + "buildsystem": "meson", + "config-opts": [ + "-Dexamples=false", + "-Dtests=false" + ], + "sources": [ + { + "type": "git", + "url": "https://gitlab.gnome.org/GNOME/libadwaita.git", + "branch": "main" + } + ], + "modules": [ + { + "name": "libsass", + "buildsystem": "meson", + "sources": [ + { + "type": "git", + "url": "https://github.com/lazka/libsass.git", + "branch": "meson" + } + ] + }, + { + "name": "sassc", + "buildsystem": "meson", + "sources": [ + { + "type": "git", + "url": "https://github.com/lazka/sassc.git", + "branch": "meson" + } + ] + } + ] + }, { "name": "spot", "builddir": true, "buildsystem": "meson", "config-opts": [ "-Doffline=false", - "-Dbuildtype=debug", - "-Dfeatures=" + "-Dbuildtype=debug" ], "sources": [ { diff --git a/dev.alextren.Spot.snapshots.json b/dev.alextren.Spot.snapshots.json index c00c734c..86d95740 100644 --- a/dev.alextren.Spot.snapshots.json +++ b/dev.alextren.Spot.snapshots.json @@ -39,14 +39,52 @@ "*.a" ], "modules": [ + { + "name": "libadwaita", + "buildsystem": "meson", + "config-opts": [ + "-Dexamples=false", + "-Dtests=false" + ], + "sources": [ + { + "type": "git", + "url": "https://gitlab.gnome.org/GNOME/libadwaita.git", + "branch": "main" + } + ], + "modules": [ + { + "name": "libsass", + "buildsystem": "meson", + "sources": [ + { + "type": "git", + "url": "https://github.com/lazka/libsass.git", + "branch": "meson" + } + ] + }, + { + "name": "sassc", + "buildsystem": "meson", + "sources": [ + { + "type": "git", + "url": "https://github.com/lazka/sassc.git", + "branch": "meson" + } + ] + } + ] + }, { "name": "spot", "builddir": true, "buildsystem": "meson", "config-opts": [ "-Doffline=true", - "-Dbuildtype=debug", - "-Dfeatures=" + "-Dbuildtype=debug" ], "sources": [ { diff --git a/meson_options.txt b/meson_options.txt index 84a1bdc9..a2d65a62 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,2 +1,2 @@ option('offline', type: 'boolean', value: true) -option('features', type: 'string', value: 'warn-cache') \ No newline at end of file +option('features', type: 'string', value: '') \ No newline at end of file diff --git a/po/POTFILES b/po/POTFILES index b14b1335..c0108ddf 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -1,97 +1,31 @@ -src/settings.rs -src/api/cache.rs -src/api/api_models.rs -src/api/client.rs -src/api/cached_client.rs -src/api/mod.rs -src/app/dispatch.rs -src/app/components/details/details.rs -src/app/components/details/details_model.rs -src/app/components/details/mod.rs -src/app/components/artist/mod.rs -src/app/components/utils.rs -src/app/components/playback/playback_info.rs -src/app/components/playback/playback_control.rs -src/app/components/playback/mod.rs -src/app/components/playlist_details/playlist_details.rs -src/app/components/playlist_details/playlist_details_model.rs -src/app/components/playlist_details/mod.rs -src/app/components/album/album.rs -src/app/components/album/mod.rs -src/app/components/artist_details/artist_details.rs -src/app/components/artist_details/artist_details_model.rs -src/app/components/artist_details/mod.rs -src/app/components/login/login.rs -src/app/components/login/mod.rs +# grep gettext src/**/*.rs | cut -d: -f1 | uniq +src/app/components/details/release_details.rs +src/app/components/labels.rs src/app/components/login/login_model.rs -src/app/components/window/mod.rs -src/app/components/saved_playlists/saved_playlists_model.rs -src/app/components/saved_playlists/mod.rs -src/app/components/saved_playlists/saved_playlists.rs -src/app/components/library/library_model.rs -src/app/components/library/library.rs -src/app/components/library/mod.rs -src/app/components/player_notifier.rs -src/app/components/now_playing/now_playing.rs -src/app/components/now_playing/mod.rs -src/app/components/now_playing/now_playing_model.rs -src/app/components/user_menu/user_menu_model.rs -src/app/components/user_menu/mod.rs -src/app/components/user_menu/user_menu.rs -src/app/components/playlist/song_actions.rs -src/app/components/playlist/song.rs -src/app/components/playlist/playlist.rs -src/app/components/playlist/mod.rs -src/app/components/navigation/navigation.rs -src/app/components/navigation/factory.rs +src/app/components/mod.rs src/app/components/navigation/home.rs -src/app/components/navigation/mod.rs -src/app/components/navigation/navigation_model.rs -src/app/components/notification/mod.rs -src/app/components/search/search_bar_model.rs -src/app/components/search/search_model.rs -src/app/components/search/search_bar.rs -src/app/components/search/mod.rs +src/app/components/playback/playback_info.rs src/app/components/search/search.rs -src/app/components/labels.rs -src/app/components/mod.rs -src/app/components/selection/mod.rs src/app/components/selection/selection_heading.rs -src/app/components/selection/selection_tools.rs -src/app/components/selection/selection_widgets.rs -src/app/loader.rs -src/app/list_store.rs -src/app/credentials.rs -src/app/models.rs -src/app/state/app_model.rs -src/app/state/selection_state.rs -src/app/state/browser_state.rs +src/app/components/user_menu/user_menu.rs src/app/state/login_state.rs -src/app/state/screen_states.rs -src/app/state/playback_state.rs -src/app/state/app_state.rs -src/app/state/mod.rs -src/app/gtypes/album_model.rs -src/app/gtypes/song_model.rs -src/app/gtypes/artist_model.rs -src/app/gtypes/mod.rs -src/app/mod.rs -src/config.rs -src/dbus/mpris.rs -src/dbus/mod.rs -src/dbus/types.rs -src/player/player.rs -src/player/mod.rs src/main.rs -src/app/components/details/details.ui -src/app/components/artist/artist.ui -src/app/components/playlist_details/playlist_details.ui + +# find src -name "*.ui*" -print src/app/components/album/album.ui +src/app/components/artist/artist.ui src/app/components/artist_details/artist_details.ui -src/app/components/login/login.ui -src/app/components/saved_playlists/saved_playlists.ui +src/app/components/details/details.ui +src/app/components/details/release_details.ui src/app/components/library/library.ui +src/app/components/login/login.ui src/app/components/now_playing/now_playing.ui +src/app/components/playback/playback_controls.ui +src/app/components/playback/playback_info.ui +src/app/components/playback/playback_widget.ui +src/app/components/playlist_details/playlist_details.ui src/app/components/playlist/song.ui +src/app/components/saved_playlists/saved_playlists.ui src/app/components/search/search.ui -src/window.ui.in \ No newline at end of file +src/app/components/user_details/user_details.ui +src/window.ui.in diff --git a/po/ca.po b/po/ca.po index 97314e53..d5168b55 100644 --- a/po/ca.po +++ b/po/ca.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: spot\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-08-15 03:30-0400\n" +"POT-Creation-Date: 2021-08-19 10:05+0200\n" "PO-Revision-Date: 2021-03-17 22:33+0100\n" "Last-Translator: Ícar N. S. \n" "Language-Team: \n" @@ -18,83 +18,37 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 2.4.1\n" -#. translators: This is a menu entry. -#: src/app/components/details/details.rs:147 -#: src/app/components/user_menu/user_menu.rs:60 -msgid "About" -msgstr "Quant a" - -#: src/app/components/details/details.rs:152 +#. translators: This is part of a larger label that reads " by " +#: src/app/components/details/release_details.rs:101 msgid "by" msgstr "" -#: src/app/components/details/details.rs:158 +#. translators: This refers to a music label +#: src/app/components/details/release_details.rs:108 msgid "Label:" msgstr "" -#: src/app/components/details/details.rs:162 +#. translators: This refers to a release date +#: src/app/components/details/release_details.rs:115 #, fuzzy msgid "Released:" msgstr "Llançaments" -#: src/app/components/details/details.rs:168 +#. translators: This refers to a number of tracks +#: src/app/components/details/release_details.rs:122 #, fuzzy msgid "Tracks:" msgstr "Millors pistes" -#: src/app/components/details/details.rs:174 +#. translators: This refers to the duration of eg. an album +#: src/app/components/details/release_details.rs:129 msgid "Duration:" msgstr "" -#: src/app/components/details/details.rs:180 +#. translators: Self explanatory +#: src/app/components/details/release_details.rs:136 msgid "Copyright:" -msgid_plural "Copyrights:" -msgstr[0] "" -msgstr[1] "" - -#. translators: Short text displayed instead of a song title when nothing plays -#. Short text displayed instead of a song title when nothing plays -#: src/app/components/playback/playback_info.rs:90 src/window.ui.in:488 -msgid "No song playing" -msgstr "Cap cançó en reproducció" - -#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). -#: src/app/components/login/login_model.rs:52 -msgid "Could not save password. Make sure the session keyring is unlocked." msgstr "" -"No s'ha pogut desar la sessió. Assegura't que l'anell de claus de la sessió " -"està desbloquejat." - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:62 -msgid "Quit" -msgstr "Surt" - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:67 -msgid "Log out" -msgstr "Tanca la sessió" - -#. translators: This is a sidebar entry to browse to saved albums. -#: src/app/components/navigation/home.rs:32 -msgid "Library" -msgstr "Biblioteca" - -#. translators: This is a sidebar entry to browse to saved playlists. -#: src/app/components/navigation/home.rs:37 -msgid "Playlists" -msgstr "Llistes de reproducció" - -#. This is the visible name for the play queue. It appears in the sidebar as well. -#: src/app/components/navigation/home.rs:42 -#: src/app/components/now_playing/now_playing.ui:21 -msgid "Now playing" -msgstr "S'està reproduïnt" - -#. translators: This text is part of a larger text that says "Search results for ". -#: src/app/components/search/search.rs:123 -msgid "Search results for" -msgstr "Cerca resultats per" #. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. #: src/app/components/labels.rs:5 @@ -126,13 +80,47 @@ msgstr "Suprimeix de la cua" msgid "Add to {}" msgstr "" +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:52 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "" +"No s'ha pogut desar la sessió. Assegura't que l'anell de claus de la sessió " +"està desbloquejat." + #. translators: This notification is the default message for unhandled errors. Logs refer to console output. -#: src/app/components/mod.rs:106 +#: src/app/components/mod.rs:105 msgid "An error occured. Check logs for details!" msgstr "Ha ocorregut un error. Comprova els registres per més detalls!" +#: src/app/components/navigation/home.rs:35 +msgid "Library" +msgstr "Biblioteca" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/home.rs:41 +msgid "Playlists" +msgstr "Llistes de reproducció" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/navigation/home.rs:46 +#: src/app/components/now_playing/now_playing.ui:14 +msgid "Now playing" +msgstr "S'està reproduïnt" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.ui:32 +msgid "No song playing" +msgstr "Cap cançó en reproducció" + +#. translators: This text is part of a larger text that says "Search results for ". +#: src/app/components/search/search.rs:102 +msgid "Search results for" +msgstr "Cerca resultats per" + #. This text appears when entering selection mode. It should be as short as possible. -#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:26 +#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:51 msgid "No song selected" msgstr "Cap cançó seleccionada" @@ -143,52 +131,67 @@ msgid_plural "songs selected" msgstr[0] "cançó seleccionada" msgstr[1] "cançons seleccionades" +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:56 +msgid "About" +msgstr "Quant a" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:58 +msgid "Quit" +msgstr "Surt" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:63 +msgid "Log out" +msgstr "Tanca la sessió" + #: src/app/state/login_state.rs:117 msgid "Connection restored" msgstr "Connexió restablerta" #. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. -#: src/app/components/artist_details/artist_details.ui:57 +#: src/app/components/artist_details/artist_details.ui:42 msgid "Top tracks" msgstr "Millors pistes" #. Title of the sections that contains all releases from an artist (both singles and albums). -#: src/app/components/artist_details/artist_details.ui:112 +#: src/app/components/artist_details/artist_details.ui:73 msgid "Releases" msgstr "Llançaments" #. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). -#: src/app/components/login/login.ui:69 +#: src/app/components/login/login.ui:45 msgid "Login to Spotify Premium" msgstr "Inicia la sessió a Spotify Premium" #. Placeholder for the username field -#: src/app/components/login/login.ui:97 +#: src/app/components/login/login.ui:64 msgid "Username" msgstr "Nom d'usuari" #. Placeholder for the password field -#: src/app/components/login/login.ui:112 +#: src/app/components/login/login.ui:72 msgid "Password" msgstr "Contrasenya" #. This error is shown when authentication fails. -#: src/app/components/login/login.ui:156 +#: src/app/components/login/login.ui:95 msgid "Authentication failed!" msgstr "L'autenticació ha fallat!" #. Log in button label -#: src/app/components/login/login.ui:181 +#: src/app/components/login/login.ui:110 msgid "Log in" msgstr "Inicia sessió" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:95 +#: src/app/components/search/search.ui:68 msgid "Albums" msgstr "Àlbums" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:144 +#: src/app/components/search/search.ui:101 msgid "Artists" msgstr "Artistes" diff --git a/po/cs.po b/po/cs.po index eaea5cbd..1502f2da 100644 --- a/po/cs.po +++ b/po/cs.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: spot\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-08-15 03:30-0400\n" +"POT-Creation-Date: 2021-08-19 10:05+0200\n" "PO-Revision-Date: 2021-03-17 17:03+0100\n" "Last-Translator: Ondřej Sluka \n" "Language-Team: none\n" @@ -17,83 +17,37 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" -#. translators: This is a menu entry. -#: src/app/components/details/details.rs:147 -#: src/app/components/user_menu/user_menu.rs:60 -msgid "About" -msgstr "O aplikaci" - -#: src/app/components/details/details.rs:152 +#. translators: This is part of a larger label that reads " by " +#: src/app/components/details/release_details.rs:101 msgid "by" msgstr "" -#: src/app/components/details/details.rs:158 +#. translators: This refers to a music label +#: src/app/components/details/release_details.rs:108 msgid "Label:" msgstr "" -#: src/app/components/details/details.rs:162 +#. translators: This refers to a release date +#: src/app/components/details/release_details.rs:115 #, fuzzy msgid "Released:" msgstr "Diskografie" -#: src/app/components/details/details.rs:168 +#. translators: This refers to a number of tracks +#: src/app/components/details/release_details.rs:122 #, fuzzy msgid "Tracks:" msgstr "Populární" -#: src/app/components/details/details.rs:174 +#. translators: This refers to the duration of eg. an album +#: src/app/components/details/release_details.rs:129 msgid "Duration:" msgstr "" -#: src/app/components/details/details.rs:180 +#. translators: Self explanatory +#: src/app/components/details/release_details.rs:136 msgid "Copyright:" -msgid_plural "Copyrights:" -msgstr[0] "" -msgstr[1] "" -msgstr[2] "" - -#. translators: Short text displayed instead of a song title when nothing plays -#. Short text displayed instead of a song title when nothing plays -#: src/app/components/playback/playback_info.rs:90 src/window.ui.in:488 -msgid "No song playing" -msgstr "Nic nehraje" - -#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). -#: src/app/components/login/login_model.rs:52 -msgid "Could not save password. Make sure the session keyring is unlocked." msgstr "" -"Heslo nelze uložit. Zkontrolujte, že je klíčenka (session keyring) odemčená." - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:62 -msgid "Quit" -msgstr "Konec" - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:67 -msgid "Log out" -msgstr "Odhlásit se" - -#. translators: This is a sidebar entry to browse to saved albums. -#: src/app/components/navigation/home.rs:32 -msgid "Library" -msgstr "Knihovna" - -#. translators: This is a sidebar entry to browse to saved playlists. -#: src/app/components/navigation/home.rs:37 -msgid "Playlists" -msgstr "Playlisty" - -#. This is the visible name for the play queue. It appears in the sidebar as well. -#: src/app/components/navigation/home.rs:42 -#: src/app/components/now_playing/now_playing.ui:21 -msgid "Now playing" -msgstr "Právě hraje" - -#. translators: This text is part of a larger text that says "Search results for ". -#: src/app/components/search/search.rs:123 -msgid "Search results for" -msgstr "Výsledky vyhledávání pro" #. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. #: src/app/components/labels.rs:5 @@ -125,13 +79,46 @@ msgstr "Odebrat z fronty" msgid "Add to {}" msgstr "" +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:52 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "" +"Heslo nelze uložit. Zkontrolujte, že je klíčenka (session keyring) odemčená." + #. translators: This notification is the default message for unhandled errors. Logs refer to console output. -#: src/app/components/mod.rs:106 +#: src/app/components/mod.rs:105 msgid "An error occured. Check logs for details!" msgstr "Došlo k chybě. Podrobnosti najdete v protokolu." +#: src/app/components/navigation/home.rs:35 +msgid "Library" +msgstr "Knihovna" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/home.rs:41 +msgid "Playlists" +msgstr "Playlisty" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/navigation/home.rs:46 +#: src/app/components/now_playing/now_playing.ui:14 +msgid "Now playing" +msgstr "Právě hraje" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.ui:32 +msgid "No song playing" +msgstr "Nic nehraje" + +#. translators: This text is part of a larger text that says "Search results for ". +#: src/app/components/search/search.rs:102 +msgid "Search results for" +msgstr "Výsledky vyhledávání pro" + #. This text appears when entering selection mode. It should be as short as possible. -#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:26 +#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:51 msgid "No song selected" msgstr "Nebyla vybrána žádná skladba" @@ -143,52 +130,67 @@ msgstr[0] "skladba vybrána" msgstr[1] "skladby vybrány" msgstr[2] "skladeb vybráno" +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:56 +msgid "About" +msgstr "O aplikaci" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:58 +msgid "Quit" +msgstr "Konec" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:63 +msgid "Log out" +msgstr "Odhlásit se" + #: src/app/state/login_state.rs:117 msgid "Connection restored" msgstr "Připojení obnoveno" #. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. -#: src/app/components/artist_details/artist_details.ui:57 +#: src/app/components/artist_details/artist_details.ui:42 msgid "Top tracks" msgstr "Populární" #. Title of the sections that contains all releases from an artist (both singles and albums). -#: src/app/components/artist_details/artist_details.ui:112 +#: src/app/components/artist_details/artist_details.ui:73 msgid "Releases" msgstr "Diskografie" #. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). -#: src/app/components/login/login.ui:69 +#: src/app/components/login/login.ui:45 msgid "Login to Spotify Premium" msgstr "Přihlásit se ke Spotify Premium" #. Placeholder for the username field -#: src/app/components/login/login.ui:97 +#: src/app/components/login/login.ui:64 msgid "Username" msgstr "Uživatelské jméno" #. Placeholder for the password field -#: src/app/components/login/login.ui:112 +#: src/app/components/login/login.ui:72 msgid "Password" msgstr "Heslo" #. This error is shown when authentication fails. -#: src/app/components/login/login.ui:156 +#: src/app/components/login/login.ui:95 msgid "Authentication failed!" msgstr "Chyba autentizace!" #. Log in button label -#: src/app/components/login/login.ui:181 +#: src/app/components/login/login.ui:110 msgid "Log in" msgstr "Přihlásit se" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:95 +#: src/app/components/search/search.ui:68 msgid "Albums" msgstr "Alba" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:144 +#: src/app/components/search/search.ui:101 msgid "Artists" msgstr "Umělci" diff --git a/po/de.po b/po/de.po index 2b46bbb7..551a1323 100644 --- a/po/de.po +++ b/po/de.po @@ -12,7 +12,7 @@ msgid "" msgstr "" "Project-Id-Version: spot\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-08-15 03:30-0400\n" +"POT-Creation-Date: 2021-08-19 10:05+0200\n" "PO-Revision-Date: 2021-05-09 13:58+0200\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -23,83 +23,37 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 2.4.3\n" -#. translators: This is a menu entry. -#: src/app/components/details/details.rs:147 -#: src/app/components/user_menu/user_menu.rs:60 -msgid "About" -msgstr "Info" - -#: src/app/components/details/details.rs:152 +#. translators: This is part of a larger label that reads " by " +#: src/app/components/details/release_details.rs:101 msgid "by" msgstr "" -#: src/app/components/details/details.rs:158 +#. translators: This refers to a music label +#: src/app/components/details/release_details.rs:108 msgid "Label:" msgstr "" -#: src/app/components/details/details.rs:162 +#. translators: This refers to a release date +#: src/app/components/details/release_details.rs:115 #, fuzzy msgid "Released:" msgstr "Diskografie" -#: src/app/components/details/details.rs:168 +#. translators: This refers to a number of tracks +#: src/app/components/details/release_details.rs:122 #, fuzzy msgid "Tracks:" msgstr "Beliebte Titel" -#: src/app/components/details/details.rs:174 +#. translators: This refers to the duration of eg. an album +#: src/app/components/details/release_details.rs:129 msgid "Duration:" msgstr "" -#: src/app/components/details/details.rs:180 +#. translators: Self explanatory +#: src/app/components/details/release_details.rs:136 msgid "Copyright:" -msgid_plural "Copyrights:" -msgstr[0] "" -msgstr[1] "" - -#. translators: Short text displayed instead of a song title when nothing plays -#. Short text displayed instead of a song title when nothing plays -#: src/app/components/playback/playback_info.rs:90 src/window.ui.in:488 -msgid "No song playing" -msgstr "Es wird kein Titel gespielt" - -#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). -#: src/app/components/login/login_model.rs:52 -msgid "Could not save password. Make sure the session keyring is unlocked." msgstr "" -"Das Passwort konnte nicht gespeichert werden. Stellen Sie sicher, dass der " -"Sitzungs-Schlüsselbund entsperrt ist." - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:62 -msgid "Quit" -msgstr "Beenden" - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:67 -msgid "Log out" -msgstr "Abmelden" - -#. translators: This is a sidebar entry to browse to saved albums. -#: src/app/components/navigation/home.rs:32 -msgid "Library" -msgstr "Bibliothek" - -#. translators: This is a sidebar entry to browse to saved playlists. -#: src/app/components/navigation/home.rs:37 -msgid "Playlists" -msgstr "Playlists" - -#. This is the visible name for the play queue. It appears in the sidebar as well. -#: src/app/components/navigation/home.rs:42 -#: src/app/components/now_playing/now_playing.ui:21 -msgid "Now playing" -msgstr "Es läuft" - -#. translators: This text is part of a larger text that says "Search results for ". -#: src/app/components/search/search.rs:123 -msgid "Search results for" -msgstr "Suchergebnisse für" #. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. #: src/app/components/labels.rs:5 @@ -131,14 +85,48 @@ msgstr "Von Warteschlange entfernen" msgid "Add to {}" msgstr "" +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:52 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "" +"Das Passwort konnte nicht gespeichert werden. Stellen Sie sicher, dass der " +"Sitzungs-Schlüsselbund entsperrt ist." + #. translators: This notification is the default message for unhandled errors. Logs refer to console output. -#: src/app/components/mod.rs:106 +#: src/app/components/mod.rs:105 msgid "An error occured. Check logs for details!" msgstr "" "Es ist ein Fehler aufgetreten. Überprüfen Sie die Protokolle für Details!" +#: src/app/components/navigation/home.rs:35 +msgid "Library" +msgstr "Bibliothek" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/home.rs:41 +msgid "Playlists" +msgstr "Playlists" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/navigation/home.rs:46 +#: src/app/components/now_playing/now_playing.ui:14 +msgid "Now playing" +msgstr "Es läuft" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.ui:32 +msgid "No song playing" +msgstr "Es wird kein Titel gespielt" + +#. translators: This text is part of a larger text that says "Search results for ". +#: src/app/components/search/search.rs:102 +msgid "Search results for" +msgstr "Suchergebnisse für" + #. This text appears when entering selection mode. It should be as short as possible. -#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:26 +#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:51 msgid "No song selected" msgstr "Kein Titel ausgewählt" @@ -149,52 +137,67 @@ msgid_plural "songs selected" msgstr[0] "Titel ausgewählt" msgstr[1] "Titel ausgewählt" +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:56 +msgid "About" +msgstr "Info" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:58 +msgid "Quit" +msgstr "Beenden" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:63 +msgid "Log out" +msgstr "Abmelden" + #: src/app/state/login_state.rs:117 msgid "Connection restored" msgstr "Verbindung wiederhergestellt" #. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. -#: src/app/components/artist_details/artist_details.ui:57 +#: src/app/components/artist_details/artist_details.ui:42 msgid "Top tracks" msgstr "Beliebte Titel" #. Title of the sections that contains all releases from an artist (both singles and albums). -#: src/app/components/artist_details/artist_details.ui:112 +#: src/app/components/artist_details/artist_details.ui:73 msgid "Releases" msgstr "Diskografie" #. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). -#: src/app/components/login/login.ui:69 +#: src/app/components/login/login.ui:45 msgid "Login to Spotify Premium" msgstr "Bei Spotify Premium anmelden" #. Placeholder for the username field -#: src/app/components/login/login.ui:97 +#: src/app/components/login/login.ui:64 msgid "Username" msgstr "Benutzername" #. Placeholder for the password field -#: src/app/components/login/login.ui:112 +#: src/app/components/login/login.ui:72 msgid "Password" msgstr "Passwort" #. This error is shown when authentication fails. -#: src/app/components/login/login.ui:156 +#: src/app/components/login/login.ui:95 msgid "Authentication failed!" msgstr "Anmeldung fehlgeschlagen!" #. Log in button label -#: src/app/components/login/login.ui:181 +#: src/app/components/login/login.ui:110 msgid "Log in" msgstr "Anmelden" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:95 +#: src/app/components/search/search.ui:68 msgid "Albums" msgstr "Alben" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:144 +#: src/app/components/search/search.ui:101 msgid "Artists" msgstr "Künstler" diff --git a/po/en.po b/po/en.po index 57249a98..a6ce2c7e 100644 --- a/po/en.po +++ b/po/en.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: spot\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-08-15 03:30-0400\n" +"POT-Creation-Date: 2021-08-19 10:05+0200\n" "PO-Revision-Date: 2021-03-16 19:21+0100\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -17,81 +17,37 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#. translators: This is a menu entry. -#: src/app/components/details/details.rs:147 -#: src/app/components/user_menu/user_menu.rs:60 -msgid "About" -msgstr "About" - -#: src/app/components/details/details.rs:152 +#. translators: This is part of a larger label that reads " by " +#: src/app/components/details/release_details.rs:101 msgid "by" msgstr "" -#: src/app/components/details/details.rs:158 +#. translators: This refers to a music label +#: src/app/components/details/release_details.rs:108 msgid "Label:" msgstr "" -#: src/app/components/details/details.rs:162 +#. translators: This refers to a release date +#: src/app/components/details/release_details.rs:115 #, fuzzy msgid "Released:" msgstr "Releases" -#: src/app/components/details/details.rs:168 +#. translators: This refers to a number of tracks +#: src/app/components/details/release_details.rs:122 #, fuzzy msgid "Tracks:" msgstr "Top tracks" -#: src/app/components/details/details.rs:174 +#. translators: This refers to the duration of eg. an album +#: src/app/components/details/release_details.rs:129 msgid "Duration:" msgstr "" -#: src/app/components/details/details.rs:180 +#. translators: Self explanatory +#: src/app/components/details/release_details.rs:136 msgid "Copyright:" -msgid_plural "Copyrights:" -msgstr[0] "" -msgstr[1] "" - -#. translators: Short text displayed instead of a song title when nothing plays -#. Short text displayed instead of a song title when nothing plays -#: src/app/components/playback/playback_info.rs:90 src/window.ui.in:488 -msgid "No song playing" -msgstr "No song playing" - -#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). -#: src/app/components/login/login_model.rs:52 -msgid "Could not save password. Make sure the session keyring is unlocked." -msgstr "Could not save password. Make sure the session keyring is unlocked." - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:62 -msgid "Quit" -msgstr "Quit" - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:67 -msgid "Log out" -msgstr "Log out" - -#. translators: This is a sidebar entry to browse to saved albums. -#: src/app/components/navigation/home.rs:32 -msgid "Library" -msgstr "Library" - -#. translators: This is a sidebar entry to browse to saved playlists. -#: src/app/components/navigation/home.rs:37 -msgid "Playlists" -msgstr "Playlists" - -#. This is the visible name for the play queue. It appears in the sidebar as well. -#: src/app/components/navigation/home.rs:42 -#: src/app/components/now_playing/now_playing.ui:21 -msgid "Now playing" -msgstr "Now playing" - -#. translators: This text is part of a larger text that says "Search results for ". -#: src/app/components/search/search.rs:123 -msgid "Search results for" -msgstr "Search results for" +msgstr "" #. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. #: src/app/components/labels.rs:5 @@ -123,13 +79,45 @@ msgstr "Remove from queue" msgid "Add to {}" msgstr "Add to {}" +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:52 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "Could not save password. Make sure the session keyring is unlocked." + #. translators: This notification is the default message for unhandled errors. Logs refer to console output. -#: src/app/components/mod.rs:106 +#: src/app/components/mod.rs:105 msgid "An error occured. Check logs for details!" msgstr "An error occured. Check logs for details!" +#: src/app/components/navigation/home.rs:35 +msgid "Library" +msgstr "Library" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/home.rs:41 +msgid "Playlists" +msgstr "Playlists" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/navigation/home.rs:46 +#: src/app/components/now_playing/now_playing.ui:14 +msgid "Now playing" +msgstr "Now playing" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.ui:32 +msgid "No song playing" +msgstr "No song playing" + +#. translators: This text is part of a larger text that says "Search results for ". +#: src/app/components/search/search.rs:102 +msgid "Search results for" +msgstr "Search results for" + #. This text appears when entering selection mode. It should be as short as possible. -#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:26 +#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:51 msgid "No song selected" msgstr "No song selected" @@ -140,52 +128,67 @@ msgid_plural "songs selected" msgstr[0] "song selected" msgstr[1] "songs selected" +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:56 +msgid "About" +msgstr "About" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:58 +msgid "Quit" +msgstr "Quit" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:63 +msgid "Log out" +msgstr "Log out" + #: src/app/state/login_state.rs:117 msgid "Connection restored" msgstr "Connection restored" #. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. -#: src/app/components/artist_details/artist_details.ui:57 +#: src/app/components/artist_details/artist_details.ui:42 msgid "Top tracks" msgstr "Top tracks" #. Title of the sections that contains all releases from an artist (both singles and albums). -#: src/app/components/artist_details/artist_details.ui:112 +#: src/app/components/artist_details/artist_details.ui:73 msgid "Releases" msgstr "Releases" #. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). -#: src/app/components/login/login.ui:69 +#: src/app/components/login/login.ui:45 msgid "Login to Spotify Premium" msgstr "Login to Spotify Premium" #. Placeholder for the username field -#: src/app/components/login/login.ui:97 +#: src/app/components/login/login.ui:64 msgid "Username" msgstr "Username" #. Placeholder for the password field -#: src/app/components/login/login.ui:112 +#: src/app/components/login/login.ui:72 msgid "Password" msgstr "Password" #. This error is shown when authentication fails. -#: src/app/components/login/login.ui:156 +#: src/app/components/login/login.ui:95 msgid "Authentication failed!" msgstr "Authentication failed!" #. Log in button label -#: src/app/components/login/login.ui:181 +#: src/app/components/login/login.ui:110 msgid "Log in" msgstr "Log in" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:95 +#: src/app/components/search/search.ui:68 msgid "Albums" msgstr "Albums" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:144 +#: src/app/components/search/search.ui:101 msgid "Artists" msgstr "Artists" diff --git a/po/es.po b/po/es.po index 26c35b12..ee2c0604 100644 --- a/po/es.po +++ b/po/es.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: spot\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-08-15 03:30-0400\n" +"POT-Creation-Date: 2021-08-19 10:05+0200\n" "PO-Revision-Date: 2021-03-21 09:47+0100\n" "Last-Translator: \n" "Language-Team: \n" @@ -18,83 +18,37 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 2.4.1\n" -#. translators: This is a menu entry. -#: src/app/components/details/details.rs:147 -#: src/app/components/user_menu/user_menu.rs:60 -msgid "About" -msgstr "Acerca de" - -#: src/app/components/details/details.rs:152 +#. translators: This is part of a larger label that reads " by " +#: src/app/components/details/release_details.rs:101 msgid "by" msgstr "" -#: src/app/components/details/details.rs:158 +#. translators: This refers to a music label +#: src/app/components/details/release_details.rs:108 msgid "Label:" msgstr "" -#: src/app/components/details/details.rs:162 +#. translators: This refers to a release date +#: src/app/components/details/release_details.rs:115 #, fuzzy msgid "Released:" msgstr "Lanzamientos" -#: src/app/components/details/details.rs:168 +#. translators: This refers to a number of tracks +#: src/app/components/details/release_details.rs:122 #, fuzzy msgid "Tracks:" msgstr "Mejores canciones" -#: src/app/components/details/details.rs:174 +#. translators: This refers to the duration of eg. an album +#: src/app/components/details/release_details.rs:129 msgid "Duration:" msgstr "" -#: src/app/components/details/details.rs:180 +#. translators: Self explanatory +#: src/app/components/details/release_details.rs:136 msgid "Copyright:" -msgid_plural "Copyrights:" -msgstr[0] "" -msgstr[1] "" - -#. translators: Short text displayed instead of a song title when nothing plays -#. Short text displayed instead of a song title when nothing plays -#: src/app/components/playback/playback_info.rs:90 src/window.ui.in:488 -msgid "No song playing" -msgstr "Ninguna canción en reproducción" - -#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). -#: src/app/components/login/login_model.rs:52 -msgid "Could not save password. Make sure the session keyring is unlocked." msgstr "" -"No se pudo guardar la contraseña. Asegúrate de que el anillo de claves esta " -"desbloqueado." - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:62 -msgid "Quit" -msgstr "Salir" - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:67 -msgid "Log out" -msgstr "Cerrar sesión" - -#. translators: This is a sidebar entry to browse to saved albums. -#: src/app/components/navigation/home.rs:32 -msgid "Library" -msgstr "Biblioteca" - -#. translators: This is a sidebar entry to browse to saved playlists. -#: src/app/components/navigation/home.rs:37 -msgid "Playlists" -msgstr "Lista de reproducción" - -#. This is the visible name for the play queue. It appears in the sidebar as well. -#: src/app/components/navigation/home.rs:42 -#: src/app/components/now_playing/now_playing.ui:21 -msgid "Now playing" -msgstr "Reproduciendo ahora" - -#. translators: This text is part of a larger text that says "Search results for ". -#: src/app/components/search/search.rs:123 -msgid "Search results for" -msgstr "Buscar resultados para" #. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. #: src/app/components/labels.rs:5 @@ -126,13 +80,47 @@ msgstr "Borrar de la lista" msgid "Add to {}" msgstr "" +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:52 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "" +"No se pudo guardar la contraseña. Asegúrate de que el anillo de claves esta " +"desbloqueado." + #. translators: This notification is the default message for unhandled errors. Logs refer to console output. -#: src/app/components/mod.rs:106 +#: src/app/components/mod.rs:105 msgid "An error occured. Check logs for details!" msgstr "Ha ocurrido un error. Revisa los logs para más detalles!" +#: src/app/components/navigation/home.rs:35 +msgid "Library" +msgstr "Biblioteca" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/home.rs:41 +msgid "Playlists" +msgstr "Lista de reproducción" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/navigation/home.rs:46 +#: src/app/components/now_playing/now_playing.ui:14 +msgid "Now playing" +msgstr "Reproduciendo ahora" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.ui:32 +msgid "No song playing" +msgstr "Ninguna canción en reproducción" + +#. translators: This text is part of a larger text that says "Search results for ". +#: src/app/components/search/search.rs:102 +msgid "Search results for" +msgstr "Buscar resultados para" + #. This text appears when entering selection mode. It should be as short as possible. -#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:26 +#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:51 msgid "No song selected" msgstr "Ninguna canción seleccionada" @@ -143,52 +131,67 @@ msgid_plural "songs selected" msgstr[0] "canción seleccionada" msgstr[1] "canciones seleccionadas" +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:56 +msgid "About" +msgstr "Acerca de" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:58 +msgid "Quit" +msgstr "Salir" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:63 +msgid "Log out" +msgstr "Cerrar sesión" + #: src/app/state/login_state.rs:117 msgid "Connection restored" msgstr "Conexión restaurada" #. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. -#: src/app/components/artist_details/artist_details.ui:57 +#: src/app/components/artist_details/artist_details.ui:42 msgid "Top tracks" msgstr "Mejores canciones" #. Title of the sections that contains all releases from an artist (both singles and albums). -#: src/app/components/artist_details/artist_details.ui:112 +#: src/app/components/artist_details/artist_details.ui:73 msgid "Releases" msgstr "Lanzamientos" #. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). -#: src/app/components/login/login.ui:69 +#: src/app/components/login/login.ui:45 msgid "Login to Spotify Premium" msgstr "Iniciar sesión en Spotify Premium" #. Placeholder for the username field -#: src/app/components/login/login.ui:97 +#: src/app/components/login/login.ui:64 msgid "Username" msgstr "Usuario" #. Placeholder for the password field -#: src/app/components/login/login.ui:112 +#: src/app/components/login/login.ui:72 msgid "Password" msgstr "Contraseña" #. This error is shown when authentication fails. -#: src/app/components/login/login.ui:156 +#: src/app/components/login/login.ui:95 msgid "Authentication failed!" msgstr "Autenticación fallida!" #. Log in button label -#: src/app/components/login/login.ui:181 +#: src/app/components/login/login.ui:110 msgid "Log in" msgstr "Iniciar sesión" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:95 +#: src/app/components/search/search.ui:68 msgid "Albums" msgstr "Álbumes" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:144 +#: src/app/components/search/search.ui:101 msgid "Artists" msgstr "Artistas" diff --git a/po/fr.po b/po/fr.po index f3359e84..92716ae0 100644 --- a/po/fr.po +++ b/po/fr.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: spot\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-08-15 03:30-0400\n" +"POT-Creation-Date: 2021-08-19 10:05+0200\n" "PO-Revision-Date: 2021-03-15 19:18+0100\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -17,83 +17,37 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#. translators: This is a menu entry. -#: src/app/components/details/details.rs:147 -#: src/app/components/user_menu/user_menu.rs:60 -msgid "About" -msgstr "À propos" - -#: src/app/components/details/details.rs:152 +#. translators: This is part of a larger label that reads " by " +#: src/app/components/details/release_details.rs:101 msgid "by" msgstr "" -#: src/app/components/details/details.rs:158 +#. translators: This refers to a music label +#: src/app/components/details/release_details.rs:108 msgid "Label:" msgstr "" -#: src/app/components/details/details.rs:162 +#. translators: This refers to a release date +#: src/app/components/details/release_details.rs:115 #, fuzzy msgid "Released:" msgstr "Discographie" -#: src/app/components/details/details.rs:168 +#. translators: This refers to a number of tracks +#: src/app/components/details/release_details.rs:122 #, fuzzy msgid "Tracks:" msgstr "Morceaux populaires" -#: src/app/components/details/details.rs:174 +#. translators: This refers to the duration of eg. an album +#: src/app/components/details/release_details.rs:129 msgid "Duration:" msgstr "" -#: src/app/components/details/details.rs:180 +#. translators: Self explanatory +#: src/app/components/details/release_details.rs:136 msgid "Copyright:" -msgid_plural "Copyrights:" -msgstr[0] "" -msgstr[1] "" - -#. translators: Short text displayed instead of a song title when nothing plays -#. Short text displayed instead of a song title when nothing plays -#: src/app/components/playback/playback_info.rs:90 src/window.ui.in:488 -msgid "No song playing" -msgstr "Aucune lecture en cours" - -#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). -#: src/app/components/login/login_model.rs:52 -msgid "Could not save password. Make sure the session keyring is unlocked." msgstr "" -"Le mot de passe n'a pu être enregistré, assurez-vous que le Trousseau de " -"session est déverouillé." - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:62 -msgid "Quit" -msgstr "Quitter" - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:67 -msgid "Log out" -msgstr "Déconnexion" - -#. translators: This is a sidebar entry to browse to saved albums. -#: src/app/components/navigation/home.rs:32 -msgid "Library" -msgstr "Bibliothèque" - -#. translators: This is a sidebar entry to browse to saved playlists. -#: src/app/components/navigation/home.rs:37 -msgid "Playlists" -msgstr "Listes de lecture" - -#. This is the visible name for the play queue. It appears in the sidebar as well. -#: src/app/components/navigation/home.rs:42 -#: src/app/components/now_playing/now_playing.ui:21 -msgid "Now playing" -msgstr "En cours de lecture" - -#. translators: This text is part of a larger text that says "Search results for ". -#: src/app/components/search/search.rs:123 -msgid "Search results for" -msgstr "Résultats pour" #. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. #: src/app/components/labels.rs:5 @@ -125,15 +79,49 @@ msgstr "Retirer de la file d'attente" msgid "Add to {}" msgstr "Ajouter à {}" +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:52 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "" +"Le mot de passe n'a pu être enregistré, assurez-vous que le Trousseau de " +"session est déverouillé." + #. translators: This notification is the default message for unhandled errors. Logs refer to console output. -#: src/app/components/mod.rs:106 +#: src/app/components/mod.rs:105 msgid "An error occured. Check logs for details!" msgstr "" "Une erreur est survenue. Consultez les journaux de débogage pour plus " "d'information." +#: src/app/components/navigation/home.rs:35 +msgid "Library" +msgstr "Bibliothèque" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/home.rs:41 +msgid "Playlists" +msgstr "Listes de lecture" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/navigation/home.rs:46 +#: src/app/components/now_playing/now_playing.ui:14 +msgid "Now playing" +msgstr "En cours de lecture" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.ui:32 +msgid "No song playing" +msgstr "Aucune lecture en cours" + +#. translators: This text is part of a larger text that says "Search results for ". +#: src/app/components/search/search.rs:102 +msgid "Search results for" +msgstr "Résultats pour" + #. This text appears when entering selection mode. It should be as short as possible. -#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:26 +#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:51 msgid "No song selected" msgstr "Aucun morceau sélectionné" @@ -144,52 +132,67 @@ msgid_plural "songs selected" msgstr[0] "morceau sélectionné" msgstr[1] "morceaux sélectionnés" +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:56 +msgid "About" +msgstr "À propos" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:58 +msgid "Quit" +msgstr "Quitter" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:63 +msgid "Log out" +msgstr "Déconnexion" + #: src/app/state/login_state.rs:117 msgid "Connection restored" msgstr "Connexion rétablie" #. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. -#: src/app/components/artist_details/artist_details.ui:57 +#: src/app/components/artist_details/artist_details.ui:42 msgid "Top tracks" msgstr "Morceaux populaires" #. Title of the sections that contains all releases from an artist (both singles and albums). -#: src/app/components/artist_details/artist_details.ui:112 +#: src/app/components/artist_details/artist_details.ui:73 msgid "Releases" msgstr "Discographie" #. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). -#: src/app/components/login/login.ui:69 +#: src/app/components/login/login.ui:45 msgid "Login to Spotify Premium" msgstr "Connexion à Spotify Premium" #. Placeholder for the username field -#: src/app/components/login/login.ui:97 +#: src/app/components/login/login.ui:64 msgid "Username" msgstr "Nom d'utilisateur" #. Placeholder for the password field -#: src/app/components/login/login.ui:112 +#: src/app/components/login/login.ui:72 msgid "Password" msgstr "Mot de passe" #. This error is shown when authentication fails. -#: src/app/components/login/login.ui:156 +#: src/app/components/login/login.ui:95 msgid "Authentication failed!" msgstr "L'authentification a échoué !" #. Log in button label -#: src/app/components/login/login.ui:181 +#: src/app/components/login/login.ui:110 msgid "Log in" msgstr "Connexion" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:95 +#: src/app/components/search/search.ui:68 msgid "Albums" msgstr "Albums" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:144 +#: src/app/components/search/search.ui:101 msgid "Artists" msgstr "Artistes" diff --git a/po/id.po b/po/id.po index 4fab115a..3ae8a25e 100644 --- a/po/id.po +++ b/po/id.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: spot\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-08-15 03:30-0400\n" +"POT-Creation-Date: 2021-08-19 10:05+0200\n" "PO-Revision-Date: 2021-08-18 08:20+0700\n" "Last-Translator: Kukuh Syafaat \n" "Language-Team: \n" @@ -18,79 +18,35 @@ msgstr "" "Plural-Forms: nplurals=2; plural= n!=1;\n" "X-Generator: Poedit 3.0\n" -#. translators: This is a menu entry. -#: src/app/components/details/details.rs:147 -#: src/app/components/user_menu/user_menu.rs:60 -msgid "About" -msgstr "Tentang" - -#: src/app/components/details/details.rs:152 +#. translators: This is part of a larger label that reads " by " +#: src/app/components/details/release_details.rs:101 msgid "by" msgstr "oleh" -#: src/app/components/details/details.rs:158 +#. translators: This refers to a music label +#: src/app/components/details/release_details.rs:108 msgid "Label:" msgstr "Label:" -#: src/app/components/details/details.rs:162 +#. translators: This refers to a release date +#: src/app/components/details/release_details.rs:115 msgid "Released:" msgstr "Dirilis:" -#: src/app/components/details/details.rs:168 +#. translators: This refers to a number of tracks +#: src/app/components/details/release_details.rs:122 msgid "Tracks:" msgstr "Trek:" -#: src/app/components/details/details.rs:174 +#. translators: This refers to the duration of eg. an album +#: src/app/components/details/release_details.rs:129 msgid "Duration:" msgstr "Durasi:" -#: src/app/components/details/details.rs:180 +#. translators: Self explanatory +#: src/app/components/details/release_details.rs:136 msgid "Copyright:" -msgid_plural "Copyrights:" -msgstr[0] "Hak Cipta:" -msgstr[1] "Hak Cipta:" - -#. translators: Short text displayed instead of a song title when nothing plays -#. Short text displayed instead of a song title when nothing plays -#: src/app/components/playback/playback_info.rs:90 src/window.ui.in:488 -msgid "No song playing" -msgstr "Tidak ada lagu yang diputar" - -#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). -#: src/app/components/login/login_model.rs:52 -msgid "Could not save password. Make sure the session keyring is unlocked." -msgstr "Tak bisa menyimpan sandi. Pastikan bahwa kunci sesi tidak terkunci." - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:62 -msgid "Quit" -msgstr "Keluar" - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:67 -msgid "Log out" -msgstr "Log keluar" - -#. translators: This is a sidebar entry to browse to saved albums. -#: src/app/components/navigation/home.rs:32 -msgid "Library" -msgstr "Pustaka" - -#. translators: This is a sidebar entry to browse to saved playlists. -#: src/app/components/navigation/home.rs:37 -msgid "Playlists" -msgstr "Daftar putar" - -#. This is the visible name for the play queue. It appears in the sidebar as well. -#: src/app/components/navigation/home.rs:42 -#: src/app/components/now_playing/now_playing.ui:21 -msgid "Now playing" -msgstr "Sedang memutar" - -#. translators: This text is part of a larger text that says "Search results for ". -#: src/app/components/search/search.rs:123 -msgid "Search results for" -msgstr "Hasil pencarian untuk" +msgstr "Hak Cipta:" #. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. #: src/app/components/labels.rs:5 @@ -122,13 +78,45 @@ msgstr "Hapus dari antrian" msgid "Add to {}" msgstr "Tambah ke {}" +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:52 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "Tak bisa menyimpan sandi. Pastikan bahwa kunci sesi tidak terkunci." + #. translators: This notification is the default message for unhandled errors. Logs refer to console output. -#: src/app/components/mod.rs:106 +#: src/app/components/mod.rs:105 msgid "An error occured. Check logs for details!" msgstr "Timbul galat. Periksa log untuk detailnya!" +#: src/app/components/navigation/home.rs:35 +msgid "Library" +msgstr "Pustaka" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/home.rs:41 +msgid "Playlists" +msgstr "Daftar putar" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/navigation/home.rs:46 +#: src/app/components/now_playing/now_playing.ui:14 +msgid "Now playing" +msgstr "Sedang memutar" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.ui:32 +msgid "No song playing" +msgstr "Tidak ada lagu yang diputar" + +#. translators: This text is part of a larger text that says "Search results for ". +#: src/app/components/search/search.rs:102 +msgid "Search results for" +msgstr "Hasil pencarian untuk" + #. This text appears when entering selection mode. It should be as short as possible. -#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:26 +#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:51 msgid "No song selected" msgstr "Tidak ada lagu yang dipilih" @@ -139,51 +127,66 @@ msgid_plural "songs selected" msgstr[0] "lagu dipilih" msgstr[1] "lagu dipilih" +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:56 +msgid "About" +msgstr "Tentang" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:58 +msgid "Quit" +msgstr "Keluar" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:63 +msgid "Log out" +msgstr "Log keluar" + #: src/app/state/login_state.rs:117 msgid "Connection restored" msgstr "Sambungan dipulihkan" #. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. -#: src/app/components/artist_details/artist_details.ui:57 +#: src/app/components/artist_details/artist_details.ui:42 msgid "Top tracks" msgstr "Trek teratas" #. Title of the sections that contains all releases from an artist (both singles and albums). -#: src/app/components/artist_details/artist_details.ui:112 +#: src/app/components/artist_details/artist_details.ui:73 msgid "Releases" msgstr "Rilis" #. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). -#: src/app/components/login/login.ui:69 +#: src/app/components/login/login.ui:45 msgid "Login to Spotify Premium" msgstr "Masuk ke Spotify Premium" #. Placeholder for the username field -#: src/app/components/login/login.ui:97 +#: src/app/components/login/login.ui:64 msgid "Username" msgstr "Nama Pengguna" #. Placeholder for the password field -#: src/app/components/login/login.ui:112 +#: src/app/components/login/login.ui:72 msgid "Password" msgstr "Kata Sandi" #. This error is shown when authentication fails. -#: src/app/components/login/login.ui:156 +#: src/app/components/login/login.ui:95 msgid "Authentication failed!" msgstr "Autentikasi gagal!" #. Log in button label -#: src/app/components/login/login.ui:181 +#: src/app/components/login/login.ui:110 msgid "Log in" msgstr "Log masuk" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:95 +#: src/app/components/search/search.ui:68 msgid "Albums" msgstr "Album" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:144 +#: src/app/components/search/search.ui:101 msgid "Artists" msgstr "Artis" diff --git a/po/nl.po b/po/nl.po index d712f3ed..647307cd 100644 --- a/po/nl.po +++ b/po/nl.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: spot\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-08-15 03:30-0400\n" +"POT-Creation-Date: 2021-08-19 10:05+0200\n" "PO-Revision-Date: 2021-03-16 19:41+0100\n" "Last-Translator: Heimen Stoffels \n" "Language-Team: \n" @@ -18,83 +18,37 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 2.4.2\n" -#. translators: This is a menu entry. -#: src/app/components/details/details.rs:147 -#: src/app/components/user_menu/user_menu.rs:60 -msgid "About" -msgstr "Over" - -#: src/app/components/details/details.rs:152 +#. translators: This is part of a larger label that reads " by " +#: src/app/components/details/release_details.rs:101 msgid "by" msgstr "" -#: src/app/components/details/details.rs:158 +#. translators: This refers to a music label +#: src/app/components/details/release_details.rs:108 msgid "Label:" msgstr "" -#: src/app/components/details/details.rs:162 +#. translators: This refers to a release date +#: src/app/components/details/release_details.rs:115 #, fuzzy msgid "Released:" msgstr "Uitgaven" -#: src/app/components/details/details.rs:168 +#. translators: This refers to a number of tracks +#: src/app/components/details/release_details.rs:122 #, fuzzy msgid "Tracks:" msgstr "Topnummers" -#: src/app/components/details/details.rs:174 +#. translators: This refers to the duration of eg. an album +#: src/app/components/details/release_details.rs:129 msgid "Duration:" msgstr "" -#: src/app/components/details/details.rs:180 +#. translators: Self explanatory +#: src/app/components/details/release_details.rs:136 msgid "Copyright:" -msgid_plural "Copyrights:" -msgstr[0] "" -msgstr[1] "" - -#. translators: Short text displayed instead of a song title when nothing plays -#. Short text displayed instead of a song title when nothing plays -#: src/app/components/playback/playback_info.rs:90 src/window.ui.in:488 -msgid "No song playing" -msgstr "Er wordt niks afgespeeld" - -#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). -#: src/app/components/login/login_model.rs:52 -msgid "Could not save password. Make sure the session keyring is unlocked." msgstr "" -"Het wachtwoord kan niet worden opgeslagen - zorg dat de sessiesleutelhanger " -"ontgrendeld is." - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:62 -msgid "Quit" -msgstr "Afsluiten" - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:67 -msgid "Log out" -msgstr "Uitloggen" - -#. translators: This is a sidebar entry to browse to saved albums. -#: src/app/components/navigation/home.rs:32 -msgid "Library" -msgstr "Verzameling" - -#. translators: This is a sidebar entry to browse to saved playlists. -#: src/app/components/navigation/home.rs:37 -msgid "Playlists" -msgstr "Afspeellijsten" - -#. This is the visible name for the play queue. It appears in the sidebar as well. -#: src/app/components/navigation/home.rs:42 -#: src/app/components/now_playing/now_playing.ui:21 -msgid "Now playing" -msgstr "Je luistert naar" - -#. translators: This text is part of a larger text that says "Search results for ". -#: src/app/components/search/search.rs:123 -msgid "Search results for" -msgstr "Zoekresultaten voor" #. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. #: src/app/components/labels.rs:5 @@ -126,13 +80,47 @@ msgstr "Verwijderen uit wachtrij" msgid "Add to {}" msgstr "" +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:52 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "" +"Het wachtwoord kan niet worden opgeslagen - zorg dat de sessiesleutelhanger " +"ontgrendeld is." + #. translators: This notification is the default message for unhandled errors. Logs refer to console output. -#: src/app/components/mod.rs:106 +#: src/app/components/mod.rs:105 msgid "An error occured. Check logs for details!" msgstr "Er is een fout opgetreden - bekijk het logboek!" +#: src/app/components/navigation/home.rs:35 +msgid "Library" +msgstr "Verzameling" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/home.rs:41 +msgid "Playlists" +msgstr "Afspeellijsten" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/navigation/home.rs:46 +#: src/app/components/now_playing/now_playing.ui:14 +msgid "Now playing" +msgstr "Je luistert naar" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.ui:32 +msgid "No song playing" +msgstr "Er wordt niks afgespeeld" + +#. translators: This text is part of a larger text that says "Search results for ". +#: src/app/components/search/search.rs:102 +msgid "Search results for" +msgstr "Zoekresultaten voor" + #. This text appears when entering selection mode. It should be as short as possible. -#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:26 +#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:51 msgid "No song selected" msgstr "Geen nummer geselecteerd" @@ -143,52 +131,67 @@ msgid_plural "songs selected" msgstr[0] "nummer geselecteerd" msgstr[1] "nummers geselecteerd" +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:56 +msgid "About" +msgstr "Over" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:58 +msgid "Quit" +msgstr "Afsluiten" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:63 +msgid "Log out" +msgstr "Uitloggen" + #: src/app/state/login_state.rs:117 msgid "Connection restored" msgstr "De verbinding is hersteld" #. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. -#: src/app/components/artist_details/artist_details.ui:57 +#: src/app/components/artist_details/artist_details.ui:42 msgid "Top tracks" msgstr "Topnummers" #. Title of the sections that contains all releases from an artist (both singles and albums). -#: src/app/components/artist_details/artist_details.ui:112 +#: src/app/components/artist_details/artist_details.ui:73 msgid "Releases" msgstr "Uitgaven" #. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). -#: src/app/components/login/login.ui:69 +#: src/app/components/login/login.ui:45 msgid "Login to Spotify Premium" msgstr "Inloggen op Spotify Premium" #. Placeholder for the username field -#: src/app/components/login/login.ui:97 +#: src/app/components/login/login.ui:64 msgid "Username" msgstr "Gebruikersnaam" #. Placeholder for the password field -#: src/app/components/login/login.ui:112 +#: src/app/components/login/login.ui:72 msgid "Password" msgstr "Wachtwoord" #. This error is shown when authentication fails. -#: src/app/components/login/login.ui:156 +#: src/app/components/login/login.ui:95 msgid "Authentication failed!" msgstr "Het inloggen is mislukt." #. Log in button label -#: src/app/components/login/login.ui:181 +#: src/app/components/login/login.ui:110 msgid "Log in" msgstr "Inloggen" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:95 +#: src/app/components/search/search.ui:68 msgid "Albums" msgstr "Albums" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:144 +#: src/app/components/search/search.ui:101 msgid "Artists" msgstr "Artiesten" diff --git a/po/pl.po b/po/pl.po index fc9c1cab..ae5ca262 100644 --- a/po/pl.po +++ b/po/pl.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: spot\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-08-15 03:30-0400\n" +"POT-Creation-Date: 2021-08-19 10:05+0200\n" "PO-Revision-Date: 2021-03-19 14:31+0100\n" "Last-Translator: Jonasz Potoniec \n" "Language-Team: \n" @@ -19,84 +19,37 @@ msgstr "" "|| n%100>14) ? 1 : 2);\n" "X-Generator: Poedit 2.4.2\n" -#. translators: This is a menu entry. -#: src/app/components/details/details.rs:147 -#: src/app/components/user_menu/user_menu.rs:60 -msgid "About" -msgstr "O programie" - -#: src/app/components/details/details.rs:152 +#. translators: This is part of a larger label that reads " by " +#: src/app/components/details/release_details.rs:101 msgid "by" msgstr "" -#: src/app/components/details/details.rs:158 +#. translators: This refers to a music label +#: src/app/components/details/release_details.rs:108 msgid "Label:" msgstr "" -#: src/app/components/details/details.rs:162 +#. translators: This refers to a release date +#: src/app/components/details/release_details.rs:115 #, fuzzy msgid "Released:" msgstr "Premiery" -#: src/app/components/details/details.rs:168 +#. translators: This refers to a number of tracks +#: src/app/components/details/release_details.rs:122 #, fuzzy msgid "Tracks:" msgstr "Najlepsze piosenki" -#: src/app/components/details/details.rs:174 +#. translators: This refers to the duration of eg. an album +#: src/app/components/details/release_details.rs:129 msgid "Duration:" msgstr "" -#: src/app/components/details/details.rs:180 +#. translators: Self explanatory +#: src/app/components/details/release_details.rs:136 msgid "Copyright:" -msgid_plural "Copyrights:" -msgstr[0] "" -msgstr[1] "" -msgstr[2] "" - -#. translators: Short text displayed instead of a song title when nothing plays -#. Short text displayed instead of a song title when nothing plays -#: src/app/components/playback/playback_info.rs:90 src/window.ui.in:488 -msgid "No song playing" -msgstr "Aktualnie nie gra żadna piosenka" - -#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). -#: src/app/components/login/login_model.rs:52 -msgid "Could not save password. Make sure the session keyring is unlocked." msgstr "" -"Nie można zapisać hasła. Upewnij się że narzędzie zarządzające kluczami jest " -"odblokowane." - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:62 -msgid "Quit" -msgstr "Wyjdź" - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:67 -msgid "Log out" -msgstr "Wyloguj się" - -#. translators: This is a sidebar entry to browse to saved albums. -#: src/app/components/navigation/home.rs:32 -msgid "Library" -msgstr "Biblioteka" - -#. translators: This is a sidebar entry to browse to saved playlists. -#: src/app/components/navigation/home.rs:37 -msgid "Playlists" -msgstr "Playlisty" - -#. This is the visible name for the play queue. It appears in the sidebar as well. -#: src/app/components/navigation/home.rs:42 -#: src/app/components/now_playing/now_playing.ui:21 -msgid "Now playing" -msgstr "Aktualnie odtwarzane" - -#. translators: This text is part of a larger text that says "Search results for ". -#: src/app/components/search/search.rs:123 -msgid "Search results for" -msgstr "Wyniki wyszukiwania dla" #. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. #: src/app/components/labels.rs:5 @@ -128,13 +81,47 @@ msgstr "Usuń z kolejki" msgid "Add to {}" msgstr "" +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:52 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "" +"Nie można zapisać hasła. Upewnij się że narzędzie zarządzające kluczami jest " +"odblokowane." + #. translators: This notification is the default message for unhandled errors. Logs refer to console output. -#: src/app/components/mod.rs:106 +#: src/app/components/mod.rs:105 msgid "An error occured. Check logs for details!" msgstr "Wystąpił błąd. Sprawdź logi po więcej informacji!" +#: src/app/components/navigation/home.rs:35 +msgid "Library" +msgstr "Biblioteka" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/home.rs:41 +msgid "Playlists" +msgstr "Playlisty" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/navigation/home.rs:46 +#: src/app/components/now_playing/now_playing.ui:14 +msgid "Now playing" +msgstr "Aktualnie odtwarzane" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.ui:32 +msgid "No song playing" +msgstr "Aktualnie nie gra żadna piosenka" + +#. translators: This text is part of a larger text that says "Search results for ". +#: src/app/components/search/search.rs:102 +msgid "Search results for" +msgstr "Wyniki wyszukiwania dla" + #. This text appears when entering selection mode. It should be as short as possible. -#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:26 +#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:51 msgid "No song selected" msgstr "Nie wybrano piosenki" @@ -146,52 +133,67 @@ msgstr[0] "wybrana piosenka" msgstr[1] "wybrane piosenki" msgstr[2] "wybranych piosenek" +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:56 +msgid "About" +msgstr "O programie" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:58 +msgid "Quit" +msgstr "Wyjdź" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:63 +msgid "Log out" +msgstr "Wyloguj się" + #: src/app/state/login_state.rs:117 msgid "Connection restored" msgstr "Odzyskano połączenie" #. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. -#: src/app/components/artist_details/artist_details.ui:57 +#: src/app/components/artist_details/artist_details.ui:42 msgid "Top tracks" msgstr "Najlepsze piosenki" #. Title of the sections that contains all releases from an artist (both singles and albums). -#: src/app/components/artist_details/artist_details.ui:112 +#: src/app/components/artist_details/artist_details.ui:73 msgid "Releases" msgstr "Premiery" #. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). -#: src/app/components/login/login.ui:69 +#: src/app/components/login/login.ui:45 msgid "Login to Spotify Premium" msgstr "Zaloguj się do Spotify Premium" #. Placeholder for the username field -#: src/app/components/login/login.ui:97 +#: src/app/components/login/login.ui:64 msgid "Username" msgstr "Nazwa użytkownika" #. Placeholder for the password field -#: src/app/components/login/login.ui:112 +#: src/app/components/login/login.ui:72 msgid "Password" msgstr "Hasło" #. This error is shown when authentication fails. -#: src/app/components/login/login.ui:156 +#: src/app/components/login/login.ui:95 msgid "Authentication failed!" msgstr "Błąd autoryzacji!" #. Log in button label -#: src/app/components/login/login.ui:181 +#: src/app/components/login/login.ui:110 msgid "Log in" msgstr "Zaloguj" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:95 +#: src/app/components/search/search.ui:68 msgid "Albums" msgstr "Albumy" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:144 +#: src/app/components/search/search.ui:101 msgid "Artists" msgstr "Artyści" diff --git a/po/pt.po b/po/pt.po index 7edb1264..d9e7e31c 100644 --- a/po/pt.po +++ b/po/pt.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: spot\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-08-15 03:30-0400\n" +"POT-Creation-Date: 2021-08-19 10:05+0200\n" "PO-Revision-Date: 2021-03-24 17:40+0000\n" "Last-Translator: Hugo Gonçalves \n" "Language-Team: none\n" @@ -17,83 +17,37 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 2.3\n" -#. translators: This is a menu entry. -#: src/app/components/details/details.rs:147 -#: src/app/components/user_menu/user_menu.rs:60 -msgid "About" -msgstr "Sobre" - -#: src/app/components/details/details.rs:152 +#. translators: This is part of a larger label that reads " by " +#: src/app/components/details/release_details.rs:101 msgid "by" msgstr "" -#: src/app/components/details/details.rs:158 +#. translators: This refers to a music label +#: src/app/components/details/release_details.rs:108 msgid "Label:" msgstr "" -#: src/app/components/details/details.rs:162 +#. translators: This refers to a release date +#: src/app/components/details/release_details.rs:115 #, fuzzy msgid "Released:" msgstr "Lançamentos" -#: src/app/components/details/details.rs:168 +#. translators: This refers to a number of tracks +#: src/app/components/details/release_details.rs:122 #, fuzzy msgid "Tracks:" msgstr "Melhores faixas" -#: src/app/components/details/details.rs:174 +#. translators: This refers to the duration of eg. an album +#: src/app/components/details/release_details.rs:129 msgid "Duration:" msgstr "" -#: src/app/components/details/details.rs:180 +#. translators: Self explanatory +#: src/app/components/details/release_details.rs:136 msgid "Copyright:" -msgid_plural "Copyrights:" -msgstr[0] "" -msgstr[1] "" - -#. translators: Short text displayed instead of a song title when nothing plays -#. Short text displayed instead of a song title when nothing plays -#: src/app/components/playback/playback_info.rs:90 src/window.ui.in:488 -msgid "No song playing" -msgstr "Nenhuma canção a ser reproduzida" - -#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). -#: src/app/components/login/login_model.rs:52 -msgid "Could not save password. Make sure the session keyring is unlocked." msgstr "" -"Não foi possível guardar a palavra-passe. Garanta que o chaveiro da sessão " -"está desbloqueado." - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:62 -msgid "Quit" -msgstr "Sair" - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:67 -msgid "Log out" -msgstr "Terminar sessão" - -#. translators: This is a sidebar entry to browse to saved albums. -#: src/app/components/navigation/home.rs:32 -msgid "Library" -msgstr "Biblioteca" - -#. translators: This is a sidebar entry to browse to saved playlists. -#: src/app/components/navigation/home.rs:37 -msgid "Playlists" -msgstr "Listas de reprodução" - -#. This is the visible name for the play queue. It appears in the sidebar as well. -#: src/app/components/navigation/home.rs:42 -#: src/app/components/now_playing/now_playing.ui:21 -msgid "Now playing" -msgstr "A reproduzir" - -#. translators: This text is part of a larger text that says "Search results for ". -#: src/app/components/search/search.rs:123 -msgid "Search results for" -msgstr "Pesquisar resultados para" #. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. #: src/app/components/labels.rs:5 @@ -125,13 +79,47 @@ msgstr "Remover da fila" msgid "Add to {}" msgstr "" +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:52 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "" +"Não foi possível guardar a palavra-passe. Garanta que o chaveiro da sessão " +"está desbloqueado." + #. translators: This notification is the default message for unhandled errors. Logs refer to console output. -#: src/app/components/mod.rs:106 +#: src/app/components/mod.rs:105 msgid "An error occured. Check logs for details!" msgstr "Ocorreu um erro! Verifique os logs para mais detalhes!" +#: src/app/components/navigation/home.rs:35 +msgid "Library" +msgstr "Biblioteca" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/home.rs:41 +msgid "Playlists" +msgstr "Listas de reprodução" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/navigation/home.rs:46 +#: src/app/components/now_playing/now_playing.ui:14 +msgid "Now playing" +msgstr "A reproduzir" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.ui:32 +msgid "No song playing" +msgstr "Nenhuma canção a ser reproduzida" + +#. translators: This text is part of a larger text that says "Search results for ". +#: src/app/components/search/search.rs:102 +msgid "Search results for" +msgstr "Pesquisar resultados para" + #. This text appears when entering selection mode. It should be as short as possible. -#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:26 +#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:51 msgid "No song selected" msgstr "Nenhuma canção selecionada" @@ -142,52 +130,67 @@ msgid_plural "songs selected" msgstr[0] "canção selecionada" msgstr[1] "canções selecionadas" +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:56 +msgid "About" +msgstr "Sobre" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:58 +msgid "Quit" +msgstr "Sair" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:63 +msgid "Log out" +msgstr "Terminar sessão" + #: src/app/state/login_state.rs:117 msgid "Connection restored" msgstr "Conexão restaurada" #. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. -#: src/app/components/artist_details/artist_details.ui:57 +#: src/app/components/artist_details/artist_details.ui:42 msgid "Top tracks" msgstr "Melhores faixas" #. Title of the sections that contains all releases from an artist (both singles and albums). -#: src/app/components/artist_details/artist_details.ui:112 +#: src/app/components/artist_details/artist_details.ui:73 msgid "Releases" msgstr "Lançamentos" #. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). -#: src/app/components/login/login.ui:69 +#: src/app/components/login/login.ui:45 msgid "Login to Spotify Premium" msgstr "Autenticar-se no Spotify Premium" #. Placeholder for the username field -#: src/app/components/login/login.ui:97 +#: src/app/components/login/login.ui:64 msgid "Username" msgstr "Utilizador" #. Placeholder for the password field -#: src/app/components/login/login.ui:112 +#: src/app/components/login/login.ui:72 msgid "Password" msgstr "Palavra-passe" #. This error is shown when authentication fails. -#: src/app/components/login/login.ui:156 +#: src/app/components/login/login.ui:95 msgid "Authentication failed!" msgstr "Autenticação falhou!" #. Log in button label -#: src/app/components/login/login.ui:181 +#: src/app/components/login/login.ui:110 msgid "Log in" msgstr "Autenticar" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:95 +#: src/app/components/search/search.ui:68 msgid "Albums" msgstr "Álbums" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:144 +#: src/app/components/search/search.ui:101 msgid "Artists" msgstr "Artistas" diff --git a/po/pt_BR.po b/po/pt_BR.po index 9237fbab..59bf6e28 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: spot\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-08-15 03:30-0400\n" +"POT-Creation-Date: 2021-08-19 10:05+0200\n" "PO-Revision-Date: 2021-06-28 15:11-0300\n" "Last-Translator: Lucas Araujo \n" "Language-Team: \n" @@ -18,83 +18,37 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "X-Generator: Poedit 2.3\n" -#. translators: This is a menu entry. -#: src/app/components/details/details.rs:147 -#: src/app/components/user_menu/user_menu.rs:60 -msgid "About" -msgstr "Sobre" - -#: src/app/components/details/details.rs:152 +#. translators: This is part of a larger label that reads " by " +#: src/app/components/details/release_details.rs:101 msgid "by" msgstr "" -#: src/app/components/details/details.rs:158 +#. translators: This refers to a music label +#: src/app/components/details/release_details.rs:108 msgid "Label:" msgstr "" -#: src/app/components/details/details.rs:162 +#. translators: This refers to a release date +#: src/app/components/details/release_details.rs:115 #, fuzzy msgid "Released:" msgstr "Lançamentos" -#: src/app/components/details/details.rs:168 +#. translators: This refers to a number of tracks +#: src/app/components/details/release_details.rs:122 #, fuzzy msgid "Tracks:" msgstr "Melhores faixas" -#: src/app/components/details/details.rs:174 +#. translators: This refers to the duration of eg. an album +#: src/app/components/details/release_details.rs:129 msgid "Duration:" msgstr "" -#: src/app/components/details/details.rs:180 +#. translators: Self explanatory +#: src/app/components/details/release_details.rs:136 msgid "Copyright:" -msgid_plural "Copyrights:" -msgstr[0] "" -msgstr[1] "" - -#. translators: Short text displayed instead of a song title when nothing plays -#. Short text displayed instead of a song title when nothing plays -#: src/app/components/playback/playback_info.rs:90 src/window.ui.in:488 -msgid "No song playing" -msgstr "Nenhum música sendo executada" - -#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). -#: src/app/components/login/login_model.rs:52 -msgid "Could not save password. Make sure the session keyring is unlocked." msgstr "" -"Não foi possível salvar a senha. Verifique se o seu chaveiro de sessão está " -"desbloqueado." - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:62 -msgid "Quit" -msgstr "Fechar" - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:67 -msgid "Log out" -msgstr "Sair" - -#. translators: This is a sidebar entry to browse to saved albums. -#: src/app/components/navigation/home.rs:32 -msgid "Library" -msgstr "Biblioteca" - -#. translators: This is a sidebar entry to browse to saved playlists. -#: src/app/components/navigation/home.rs:37 -msgid "Playlists" -msgstr "Listas de reprodução" - -#. This is the visible name for the play queue. It appears in the sidebar as well. -#: src/app/components/navigation/home.rs:42 -#: src/app/components/now_playing/now_playing.ui:21 -msgid "Now playing" -msgstr "Tocando agora" - -#. translators: This text is part of a larger text that says "Search results for ". -#: src/app/components/search/search.rs:123 -msgid "Search results for" -msgstr "Procurar por" #. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. #: src/app/components/labels.rs:5 @@ -126,13 +80,47 @@ msgstr "Remover da fila" msgid "Add to {}" msgstr "Adicionar a {}" +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:52 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "" +"Não foi possível salvar a senha. Verifique se o seu chaveiro de sessão está " +"desbloqueado." + #. translators: This notification is the default message for unhandled errors. Logs refer to console output. -#: src/app/components/mod.rs:106 +#: src/app/components/mod.rs:105 msgid "An error occured. Check logs for details!" msgstr "Ocorreu um erro. Verifique os logs para mais detalhes!" +#: src/app/components/navigation/home.rs:35 +msgid "Library" +msgstr "Biblioteca" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/home.rs:41 +msgid "Playlists" +msgstr "Listas de reprodução" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/navigation/home.rs:46 +#: src/app/components/now_playing/now_playing.ui:14 +msgid "Now playing" +msgstr "Tocando agora" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.ui:32 +msgid "No song playing" +msgstr "Nenhum música sendo executada" + +#. translators: This text is part of a larger text that says "Search results for ". +#: src/app/components/search/search.rs:102 +msgid "Search results for" +msgstr "Procurar por" + #. This text appears when entering selection mode. It should be as short as possible. -#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:26 +#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:51 msgid "No song selected" msgstr "Nenhuma música selecionada" @@ -143,51 +131,66 @@ msgid_plural "songs selected" msgstr[0] "música selecionada" msgstr[1] "músicas selecionadas" +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:56 +msgid "About" +msgstr "Sobre" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:58 +msgid "Quit" +msgstr "Fechar" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:63 +msgid "Log out" +msgstr "Sair" + #: src/app/state/login_state.rs:117 msgid "Connection restored" msgstr "Conexão reestabelecida" #. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. -#: src/app/components/artist_details/artist_details.ui:57 +#: src/app/components/artist_details/artist_details.ui:42 msgid "Top tracks" msgstr "Melhores faixas" #. Title of the sections that contains all releases from an artist (both singles and albums). -#: src/app/components/artist_details/artist_details.ui:112 +#: src/app/components/artist_details/artist_details.ui:73 msgid "Releases" msgstr "Lançamentos" #. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). -#: src/app/components/login/login.ui:69 +#: src/app/components/login/login.ui:45 msgid "Login to Spotify Premium" msgstr "Entrar no Spotify Premium" #. Placeholder for the username field -#: src/app/components/login/login.ui:97 +#: src/app/components/login/login.ui:64 msgid "Username" msgstr "Nome de usuário" #. Placeholder for the password field -#: src/app/components/login/login.ui:112 +#: src/app/components/login/login.ui:72 msgid "Password" msgstr "Senha" #. This error is shown when authentication fails. -#: src/app/components/login/login.ui:156 +#: src/app/components/login/login.ui:95 msgid "Authentication failed!" msgstr "A autenticação falhou!" #. Log in button label -#: src/app/components/login/login.ui:181 +#: src/app/components/login/login.ui:110 msgid "Log in" msgstr "Entrar" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:95 +#: src/app/components/search/search.ui:68 msgid "Albums" msgstr "Álbuns" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:144 +#: src/app/components/search/search.ui:101 msgid "Artists" msgstr "Artistas" diff --git a/po/ru.po b/po/ru.po index 3bc55c62..223333a1 100644 --- a/po/ru.po +++ b/po/ru.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: spot\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-08-15 03:30-0400\n" +"POT-Creation-Date: 2021-08-19 10:05+0200\n" "PO-Revision-Date: 2021-05-07 13:40+0300\n" "Last-Translator: Automatically generated\n" "Language-Team: Paul Bragin \n" @@ -19,83 +19,37 @@ msgstr "" "%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n" "X-Generator: Poedit 2.4.3\n" -#. translators: This is a menu entry. -#: src/app/components/details/details.rs:147 -#: src/app/components/user_menu/user_menu.rs:60 -msgid "About" -msgstr "Об" - -#: src/app/components/details/details.rs:152 +#. translators: This is part of a larger label that reads " by " +#: src/app/components/details/release_details.rs:101 msgid "by" msgstr "" -#: src/app/components/details/details.rs:158 +#. translators: This refers to a music label +#: src/app/components/details/release_details.rs:108 msgid "Label:" msgstr "" -#: src/app/components/details/details.rs:162 +#. translators: This refers to a release date +#: src/app/components/details/release_details.rs:115 #, fuzzy msgid "Released:" msgstr "Релизы" -#: src/app/components/details/details.rs:168 +#. translators: This refers to a number of tracks +#: src/app/components/details/release_details.rs:122 #, fuzzy msgid "Tracks:" msgstr "Топ треков" -#: src/app/components/details/details.rs:174 +#. translators: This refers to the duration of eg. an album +#: src/app/components/details/release_details.rs:129 msgid "Duration:" msgstr "" -#: src/app/components/details/details.rs:180 +#. translators: Self explanatory +#: src/app/components/details/release_details.rs:136 msgid "Copyright:" -msgid_plural "Copyrights:" -msgstr[0] "" -msgstr[1] "" -msgstr[2] "" - -#. translators: Short text displayed instead of a song title when nothing plays -#. Short text displayed instead of a song title when nothing plays -#: src/app/components/playback/playback_info.rs:90 src/window.ui.in:488 -msgid "No song playing" -msgstr "Песня сейчас не играет" - -#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). -#: src/app/components/login/login_model.rs:52 -msgid "Could not save password. Make sure the session keyring is unlocked." msgstr "" -"Не удалось сохранить пароль. Проверьте, GNOME Keyring/KDE wallet открыта." - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:62 -msgid "Quit" -msgstr "Выйти из программы" - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:67 -msgid "Log out" -msgstr "Выйти из аккаунта" - -#. translators: This is a sidebar entry to browse to saved albums. -#: src/app/components/navigation/home.rs:32 -msgid "Library" -msgstr "Библиотека" - -#. translators: This is a sidebar entry to browse to saved playlists. -#: src/app/components/navigation/home.rs:37 -msgid "Playlists" -msgstr "Плейлисты" - -#. This is the visible name for the play queue. It appears in the sidebar as well. -#: src/app/components/navigation/home.rs:42 -#: src/app/components/now_playing/now_playing.ui:21 -msgid "Now playing" -msgstr "Сейчас играет" - -#. translators: This text is part of a larger text that says "Search results for ". -#: src/app/components/search/search.rs:123 -msgid "Search results for" -msgstr "Результаты поиска для" #. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. #: src/app/components/labels.rs:5 @@ -127,13 +81,46 @@ msgstr "Удалить из очереди" msgid "Add to {}" msgstr "" +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:52 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "" +"Не удалось сохранить пароль. Проверьте, GNOME Keyring/KDE wallet открыта." + #. translators: This notification is the default message for unhandled errors. Logs refer to console output. -#: src/app/components/mod.rs:106 +#: src/app/components/mod.rs:105 msgid "An error occured. Check logs for details!" msgstr "Ошибка появилась. Посмотрите в логи за подробностями!" +#: src/app/components/navigation/home.rs:35 +msgid "Library" +msgstr "Библиотека" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/home.rs:41 +msgid "Playlists" +msgstr "Плейлисты" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/navigation/home.rs:46 +#: src/app/components/now_playing/now_playing.ui:14 +msgid "Now playing" +msgstr "Сейчас играет" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.ui:32 +msgid "No song playing" +msgstr "Песня сейчас не играет" + +#. translators: This text is part of a larger text that says "Search results for ". +#: src/app/components/search/search.rs:102 +msgid "Search results for" +msgstr "Результаты поиска для" + #. This text appears when entering selection mode. It should be as short as possible. -#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:26 +#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:51 msgid "No song selected" msgstr "Песня не выбрана" @@ -145,52 +132,67 @@ msgstr[0] "песня выбрана" msgstr[1] "песни выбраны" msgstr[2] "песни были выбраны" +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:56 +msgid "About" +msgstr "Об" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:58 +msgid "Quit" +msgstr "Выйти из программы" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:63 +msgid "Log out" +msgstr "Выйти из аккаунта" + #: src/app/state/login_state.rs:117 msgid "Connection restored" msgstr "Подключение восстановлено" #. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. -#: src/app/components/artist_details/artist_details.ui:57 +#: src/app/components/artist_details/artist_details.ui:42 msgid "Top tracks" msgstr "Топ треков" #. Title of the sections that contains all releases from an artist (both singles and albums). -#: src/app/components/artist_details/artist_details.ui:112 +#: src/app/components/artist_details/artist_details.ui:73 msgid "Releases" msgstr "Релизы" #. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). -#: src/app/components/login/login.ui:69 +#: src/app/components/login/login.ui:45 msgid "Login to Spotify Premium" msgstr "Войти в Spotify Premium" #. Placeholder for the username field -#: src/app/components/login/login.ui:97 +#: src/app/components/login/login.ui:64 msgid "Username" msgstr "Имя аккаунта" #. Placeholder for the password field -#: src/app/components/login/login.ui:112 +#: src/app/components/login/login.ui:72 msgid "Password" msgstr "Пароль" #. This error is shown when authentication fails. -#: src/app/components/login/login.ui:156 +#: src/app/components/login/login.ui:95 msgid "Authentication failed!" msgstr "Вход не удался!" #. Log in button label -#: src/app/components/login/login.ui:181 +#: src/app/components/login/login.ui:110 msgid "Log in" msgstr "Войти" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:95 +#: src/app/components/search/search.ui:68 msgid "Albums" msgstr "Альбомы" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:144 +#: src/app/components/search/search.ui:101 msgid "Artists" msgstr "Артисты" diff --git a/po/spot.pot b/po/spot.pot index 7b92c578..6e4b946d 100644 --- a/po/spot.pot +++ b/po/spot.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: spot\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-08-15 03:30-0400\n" +"POT-Creation-Date: 2021-08-19 10:05+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,78 +18,34 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" -#. translators: This is a menu entry. -#: src/app/components/details/details.rs:147 -#: src/app/components/user_menu/user_menu.rs:60 -msgid "About" -msgstr "" - -#: src/app/components/details/details.rs:152 +#. translators: This is part of a larger label that reads " by " +#: src/app/components/details/release_details.rs:101 msgid "by" msgstr "" -#: src/app/components/details/details.rs:158 +#. translators: This refers to a music label +#: src/app/components/details/release_details.rs:108 msgid "Label:" msgstr "" -#: src/app/components/details/details.rs:162 +#. translators: This refers to a release date +#: src/app/components/details/release_details.rs:115 msgid "Released:" msgstr "" -#: src/app/components/details/details.rs:168 +#. translators: This refers to a number of tracks +#: src/app/components/details/release_details.rs:122 msgid "Tracks:" msgstr "" -#: src/app/components/details/details.rs:174 +#. translators: This refers to the duration of eg. an album +#: src/app/components/details/release_details.rs:129 msgid "Duration:" msgstr "" -#: src/app/components/details/details.rs:180 +#. translators: Self explanatory +#: src/app/components/details/release_details.rs:136 msgid "Copyright:" -msgid_plural "Copyrights:" -msgstr[0] "" -msgstr[1] "" - -#. translators: Short text displayed instead of a song title when nothing plays -#. Short text displayed instead of a song title when nothing plays -#: src/app/components/playback/playback_info.rs:90 src/window.ui.in:488 -msgid "No song playing" -msgstr "" - -#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). -#: src/app/components/login/login_model.rs:52 -msgid "Could not save password. Make sure the session keyring is unlocked." -msgstr "" - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:62 -msgid "Quit" -msgstr "" - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:67 -msgid "Log out" -msgstr "" - -#. translators: This is a sidebar entry to browse to saved albums. -#: src/app/components/navigation/home.rs:32 -msgid "Library" -msgstr "" - -#. translators: This is a sidebar entry to browse to saved playlists. -#: src/app/components/navigation/home.rs:37 -msgid "Playlists" -msgstr "" - -#. This is the visible name for the play queue. It appears in the sidebar as well. -#: src/app/components/navigation/home.rs:42 -#: src/app/components/now_playing/now_playing.ui:21 -msgid "Now playing" -msgstr "" - -#. translators: This text is part of a larger text that says "Search results for ". -#: src/app/components/search/search.rs:123 -msgid "Search results for" msgstr "" #. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. @@ -122,13 +78,45 @@ msgstr "" msgid "Add to {}" msgstr "" +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:52 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "" + #. translators: This notification is the default message for unhandled errors. Logs refer to console output. -#: src/app/components/mod.rs:106 +#: src/app/components/mod.rs:105 msgid "An error occured. Check logs for details!" msgstr "" +#: src/app/components/navigation/home.rs:35 +msgid "Library" +msgstr "" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/home.rs:41 +msgid "Playlists" +msgstr "" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/navigation/home.rs:46 +#: src/app/components/now_playing/now_playing.ui:14 +msgid "Now playing" +msgstr "" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.ui:32 +msgid "No song playing" +msgstr "" + +#. translators: This text is part of a larger text that says "Search results for ". +#: src/app/components/search/search.rs:102 +msgid "Search results for" +msgstr "" + #. This text appears when entering selection mode. It should be as short as possible. -#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:26 +#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:51 msgid "No song selected" msgstr "" @@ -139,51 +127,66 @@ msgid_plural "songs selected" msgstr[0] "" msgstr[1] "" +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:56 +msgid "About" +msgstr "" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:58 +msgid "Quit" +msgstr "" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:63 +msgid "Log out" +msgstr "" + #: src/app/state/login_state.rs:117 msgid "Connection restored" msgstr "" #. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. -#: src/app/components/artist_details/artist_details.ui:57 +#: src/app/components/artist_details/artist_details.ui:42 msgid "Top tracks" msgstr "" #. Title of the sections that contains all releases from an artist (both singles and albums). -#: src/app/components/artist_details/artist_details.ui:112 +#: src/app/components/artist_details/artist_details.ui:73 msgid "Releases" msgstr "" #. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). -#: src/app/components/login/login.ui:69 +#: src/app/components/login/login.ui:45 msgid "Login to Spotify Premium" msgstr "" #. Placeholder for the username field -#: src/app/components/login/login.ui:97 +#: src/app/components/login/login.ui:64 msgid "Username" msgstr "" #. Placeholder for the password field -#: src/app/components/login/login.ui:112 +#: src/app/components/login/login.ui:72 msgid "Password" msgstr "" #. This error is shown when authentication fails. -#: src/app/components/login/login.ui:156 +#: src/app/components/login/login.ui:95 msgid "Authentication failed!" msgstr "" #. Log in button label -#: src/app/components/login/login.ui:181 +#: src/app/components/login/login.ui:110 msgid "Log in" msgstr "" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:95 +#: src/app/components/search/search.ui:68 msgid "Albums" msgstr "" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:144 +#: src/app/components/search/search.ui:101 msgid "Artists" msgstr "" diff --git a/po/tr.po b/po/tr.po index 32b906d9..c99737c5 100644 --- a/po/tr.po +++ b/po/tr.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: spot\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-08-15 03:30-0400\n" +"POT-Creation-Date: 2021-08-19 10:05+0200\n" "PO-Revision-Date: 2021-06-14 00:00+0200\n" "Last-Translator: Automatically generated\n" "Language-Team: Yusuf Çınar Özmen \n" @@ -15,83 +15,37 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#. translators: This is a menu entry. -#: src/app/components/details/details.rs:147 -#: src/app/components/user_menu/user_menu.rs:60 -msgid "About" -msgstr "Hakkında" - -#: src/app/components/details/details.rs:152 +#. translators: This is part of a larger label that reads " by " +#: src/app/components/details/release_details.rs:101 msgid "by" msgstr "" -#: src/app/components/details/details.rs:158 +#. translators: This refers to a music label +#: src/app/components/details/release_details.rs:108 msgid "Label:" msgstr "" -#: src/app/components/details/details.rs:162 +#. translators: This refers to a release date +#: src/app/components/details/release_details.rs:115 #, fuzzy msgid "Released:" msgstr "Parçalar" -#: src/app/components/details/details.rs:168 +#. translators: This refers to a number of tracks +#: src/app/components/details/release_details.rs:122 #, fuzzy msgid "Tracks:" msgstr "En çok dinlenenler" -#: src/app/components/details/details.rs:174 +#. translators: This refers to the duration of eg. an album +#: src/app/components/details/release_details.rs:129 msgid "Duration:" msgstr "" -#: src/app/components/details/details.rs:180 +#. translators: Self explanatory +#: src/app/components/details/release_details.rs:136 msgid "Copyright:" -msgid_plural "Copyrights:" -msgstr[0] "" -msgstr[1] "" - -#. translators: Short text displayed instead of a song title when nothing plays -#. Short text displayed instead of a song title when nothing plays -#: src/app/components/playback/playback_info.rs:90 src/window.ui.in:488 -msgid "No song playing" -msgstr "Herhangi bir şarkı oynatılmıyor" - -#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). -#: src/app/components/login/login_model.rs:52 -msgid "Could not save password. Make sure the session keyring is unlocked." msgstr "" -"Şifreniz kaydedilemedi. Oturum anahtarlığınızın kilidinin açık olduğundan " -"emin olun." - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:62 -msgid "Quit" -msgstr "Çıkış" - -#. translators: This is a menu entry. -#: src/app/components/user_menu/user_menu.rs:67 -msgid "Log out" -msgstr "Oturumdan çık" - -#. translators: This is a sidebar entry to browse to saved albums. -#: src/app/components/navigation/home.rs:32 -msgid "Library" -msgstr "Kütüphane" - -#. translators: This is a sidebar entry to browse to saved playlists. -#: src/app/components/navigation/home.rs:37 -msgid "Playlists" -msgstr "Oynatma listesi" - -#. This is the visible name for the play queue. It appears in the sidebar as well. -#: src/app/components/navigation/home.rs:42 -#: src/app/components/now_playing/now_playing.ui:21 -msgid "Now playing" -msgstr "Şu an oynatılıyor" - -#. translators: This text is part of a larger text that says "Search results for ". -#: src/app/components/search/search.rs:123 -msgid "Search results for" -msgstr "Arama sonuçları" #. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. #: src/app/components/labels.rs:5 @@ -123,13 +77,47 @@ msgstr "Sıradan çıkar" msgid "Add to {}" msgstr "{} listesine ekle" +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:52 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "" +"Şifreniz kaydedilemedi. Oturum anahtarlığınızın kilidinin açık olduğundan " +"emin olun." + #. translators: This notification is the default message for unhandled errors. Logs refer to console output. -#: src/app/components/mod.rs:106 +#: src/app/components/mod.rs:105 msgid "An error occured. Check logs for details!" msgstr "Bir hata meydana geldi. Günlükleri kontrol edin." +#: src/app/components/navigation/home.rs:35 +msgid "Library" +msgstr "Kütüphane" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/home.rs:41 +msgid "Playlists" +msgstr "Oynatma listesi" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/navigation/home.rs:46 +#: src/app/components/now_playing/now_playing.ui:14 +msgid "Now playing" +msgstr "Şu an oynatılıyor" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.ui:32 +msgid "No song playing" +msgstr "Herhangi bir şarkı oynatılmıyor" + +#. translators: This text is part of a larger text that says "Search results for ". +#: src/app/components/search/search.rs:102 +msgid "Search results for" +msgstr "Arama sonuçları" + #. This text appears when entering selection mode. It should be as short as possible. -#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:26 +#: src/app/components/selection/selection_heading.rs:73 src/window.ui.in:51 msgid "No song selected" msgstr "Herhangi bir şarkı seçilmedi" @@ -140,51 +128,66 @@ msgid_plural "songs selected" msgstr[0] "seçildi" msgstr[1] "seçildi" +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:56 +msgid "About" +msgstr "Hakkında" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:58 +msgid "Quit" +msgstr "Çıkış" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:63 +msgid "Log out" +msgstr "Oturumdan çık" + #: src/app/state/login_state.rs:117 msgid "Connection restored" msgstr "Bağlantı geri yüklendi." #. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. -#: src/app/components/artist_details/artist_details.ui:57 +#: src/app/components/artist_details/artist_details.ui:42 msgid "Top tracks" msgstr "En çok dinlenenler" #. Title of the sections that contains all releases from an artist (both singles and albums). -#: src/app/components/artist_details/artist_details.ui:112 +#: src/app/components/artist_details/artist_details.ui:73 msgid "Releases" msgstr "Parçalar" #. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). -#: src/app/components/login/login.ui:69 +#: src/app/components/login/login.ui:45 msgid "Login to Spotify Premium" msgstr "Spotify Premium'a giriş yap" #. Placeholder for the username field -#: src/app/components/login/login.ui:97 +#: src/app/components/login/login.ui:64 msgid "Username" msgstr "Kullanıcı adı" #. Placeholder for the password field -#: src/app/components/login/login.ui:112 +#: src/app/components/login/login.ui:72 msgid "Password" msgstr "Şifre" #. This error is shown when authentication fails. -#: src/app/components/login/login.ui:156 +#: src/app/components/login/login.ui:95 msgid "Authentication failed!" msgstr "Kimlik doğrulama başarısız." #. Log in button label -#: src/app/components/login/login.ui:181 +#: src/app/components/login/login.ui:110 msgid "Log in" msgstr "Giriş yap" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:95 +#: src/app/components/search/search.ui:68 msgid "Albums" msgstr "Albümler" #. This is the title of a section of the search results -#: src/app/components/search/search.ui:144 +#: src/app/components/search/search.ui:101 msgid "Artists" msgstr "Sanatçılar" diff --git a/src/api/mod.rs b/src/api/mod.rs index cfc79c5f..e089f611 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -7,17 +7,6 @@ pub mod cache; pub use cached_client::{CachedSpotifyClient, SpotifyApiClient, SpotifyResult}; pub use client::SpotifyApiError; -pub async fn _clear_old_cache() -> Option<()> { - let cache = cache::CacheManager::new(&[]).unwrap(); - let img_cache = regex::Regex::new(r"^[0-9]{15,30}\.jpg$").unwrap(); - cache - .clear_cache_pattern("net", &*cached_client::ALL_CACHE) - .await - .ok()?; - cache.clear_cache_pattern("img", &img_cache).await.ok()?; - Some(()) -} - pub async fn clear_user_cache() -> Option<()> { cache::CacheManager::new(&[])? .clear_cache_pattern("spot/net", &*cached_client::USER_CACHE) diff --git a/src/app.css b/src/app.css index 1ed54344..2519e7cf 100644 --- a/src/app.css +++ b/src/app.css @@ -7,24 +7,10 @@ opacity: 1; } -.seek-bar { - padding: 0; - padding-bottom: 2px; - min-height: 0; +headerbar.selection-mode { + background-color: @theme_selected_bg_color; } -.seek-bar trough, .seek-bar highlight { - border-radius: 0; - border-left: none; - border-right: none; - min-height: 0; -} - -.seek-bar--active trough, .seek-bar--active highlight { - min-height: 5px; -} - -.seek-bar highlight { - border-left: none; - border-right: none; +button.tool, menubutton.tool { + -gtk-icon-size: 25px; } \ No newline at end of file diff --git a/src/app/components/album/album.css b/src/app/components/album/album.css index ba657e88..261d10fe 100644 --- a/src/app/components/album/album.css +++ b/src/app/components/album/album.css @@ -1,4 +1,27 @@ -.album button.album__cover { +.album { + opacity: 0; + transition: opacity 300ms ease; +} + +.album--loaded { + opacity: 1; +} + +/* large style */ + +leaflet.unfolded .album .album__cover { + min-width: 200px; + min-height: 200px; +} + +/* small style */ + +leaflet.folded .album .album__cover { + min-width: 100px; + min-height: 100px; +} + +.album .album__cover { padding: 0; border-radius: 0; } @@ -11,3 +34,12 @@ .album label.album__artist { font-size: 14px; } + +.album__cover image { + border-radius: 4px; + transition: all 100ms ease; +} + +.album__cover:hover image { + filter: brightness(1.15); +} \ No newline at end of file diff --git a/src/app/components/album/album.rs b/src/app/components/album/album.rs index e875c07e..b8925c8f 100644 --- a/src/app/components/album/album.rs +++ b/src/app/components/album/album.rs @@ -1,78 +1,113 @@ -use crate::app::components::{screen_add_css_provider, Component}; +use crate::app::components::screen_add_css_provider; use crate::app::dispatch::Worker; use crate::app::loader::ImageLoader; use crate::app::models::AlbumModel; -use gladis::Gladis; + use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; + +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/album.ui")] + pub struct AlbumWidget { + #[template_child] + pub album_label: TemplateChild, + + #[template_child] + pub artist_label: TemplateChild, + + #[template_child] + pub cover_btn: TemplateChild, + + #[template_child] + pub cover_image: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for AlbumWidget { + const NAME: &'static str = "AlbumWidget"; + type Type = super::AlbumWidget; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } -#[derive(Gladis, Clone)] -struct AlbumWidget { - root: gtk::Widget, - revealer: gtk::Revealer, - album_label: gtk::Label, - artist_label: gtk::Label, - cover_btn: gtk::Button, - cover_image: gtk::Image, + impl ObjectImpl for AlbumWidget {} + impl WidgetImpl for AlbumWidget {} + impl BoxImpl for AlbumWidget {} +} + +glib::wrapper! { + pub struct AlbumWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; } impl AlbumWidget { pub fn new() -> Self { screen_add_css_provider(resource!("/components/album.css")); - Self::from_resource(resource!("/components/album.ui")).unwrap() + glib::Object::new(&[]).expect("Failed to create an instance of AlbumWidget") } -} -pub struct Album { - widget: AlbumWidget, - model: AlbumModel, -} + pub fn for_model(album_model: &AlbumModel, worker: Worker) -> Self { + let _self = Self::new(); + _self.bind(album_model, worker); + _self + } + + fn set_loaded(&self) { + let context = self.style_context(); + context.add_class("album--loaded"); + } -impl Album { - pub fn new(album_model: &AlbumModel, worker: Worker) -> Self { - let widget = AlbumWidget::new(); + fn set_image(&self, pixbuf: Option<&gdk_pixbuf::Pixbuf>) { + imp::AlbumWidget::from_instance(self) + .cover_image + .set_from_pixbuf(pixbuf); + } + + fn bind(&self, album_model: &AlbumModel, worker: Worker) { + let widget = imp::AlbumWidget::from_instance(self); + widget.cover_image.set_overflow(gtk::Overflow::Hidden); - let image = widget.cover_image.downgrade(); - let revealer = widget.revealer.downgrade(); if let Some(url) = album_model.cover_url() { + let _self = self.downgrade(); worker.send_local_task(async move { - if let (Some(image), Some(revealer)) = (image.upgrade(), revealer.upgrade()) { + if let Some(_self) = _self.upgrade() { let loader = ImageLoader::new(); let result = loader.load_remote(&url, "jpg", 200, 200).await; - image.set_from_pixbuf(result.as_ref()); - revealer.set_reveal_child(true); + _self.set_image(result.as_ref()); + _self.set_loaded(); } }); } else { - widget.revealer.set_reveal_child(true); + self.set_loaded(); } album_model - .bind_property("album", &widget.album_label, "label") + .bind_property("album", &*widget.album_label, "label") .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) .build(); album_model - .bind_property("artist", &widget.artist_label, "label") + .bind_property("artist", &*widget.artist_label, "label") .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) .build(); - - Self { - widget, - model: album_model.clone(), - } } - pub fn connect_album_pressed(&self, f: F) { - self.widget + pub fn connect_album_pressed(&self, f: F) { + imp::AlbumWidget::from_instance(self) .cover_btn - .connect_clicked(clone!(@weak self.model as model => move |_| { - f(&model); + .connect_clicked(clone!(@weak self as _self => move |_| { + f(&_self); })); } } - -impl Component for Album { - fn get_root_widget(&self) -> >k::Widget { - &self.widget.root - } -} diff --git a/src/app/components/album/album.ui b/src/app/components/album/album.ui index 674b7c2b..46e1fdf4 100644 --- a/src/app/components/album/album.ui +++ b/src/app/components/album/album.ui @@ -1,11 +1,8 @@ - - - - - True - False + + + diff --git a/src/app/components/album/mod.rs b/src/app/components/album/mod.rs index e853e857..fc74bceb 100644 --- a/src/app/components/album/mod.rs +++ b/src/app/components/album/mod.rs @@ -1,2 +1,2 @@ mod album; -pub use album::Album; +pub use album::AlbumWidget; diff --git a/src/app/components/artist/artist.ui b/src/app/components/artist/artist.ui index 23c05e63..e6de92f9 100644 --- a/src/app/components/artist/artist.ui +++ b/src/app/components/artist/artist.ui @@ -1,29 +1,23 @@ - - - - - True - False + + + diff --git a/src/app/components/artist/mod.rs b/src/app/components/artist/mod.rs index dc68dcf9..5c460b7e 100644 --- a/src/app/components/artist/mod.rs +++ b/src/app/components/artist/mod.rs @@ -1,67 +1,89 @@ -use gladis::Gladis; use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; -use crate::app::components::Component; use crate::app::loader::ImageLoader; use crate::app::models::ArtistModel; use crate::app::Worker; -#[derive(Gladis, Clone)] -struct ArtistWidget { - root: gtk::Widget, - artist: gtk::Label, - avatar_btn: gtk::Button, - avatar: libhandy::Avatar, +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/artist.ui")] + pub struct ArtistWidget { + #[template_child] + pub artist: TemplateChild, + + #[template_child] + pub avatar_btn: TemplateChild, + + #[template_child] + pub avatar: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for ArtistWidget { + const NAME: &'static str = "ArtistWidget"; + type Type = super::ArtistWidget; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for ArtistWidget {} + impl WidgetImpl for ArtistWidget {} + impl BoxImpl for ArtistWidget {} +} + +glib::wrapper! { + pub struct ArtistWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; } impl ArtistWidget { pub fn new() -> Self { - Self::from_resource(resource!("/components/artist.ui")).unwrap() + glib::Object::new(&[]).expect("Failed to create an instance of ArtistWidget") } -} -pub struct Artist { - widget: ArtistWidget, - model: ArtistModel, -} + pub fn for_model(model: &ArtistModel, worker: Worker) -> Self { + let _self = Self::new(); + _self.bind(model, worker); + _self + } + + pub fn connect_artist_pressed(&self, f: F) { + imp::ArtistWidget::from_instance(self) + .avatar_btn + .connect_clicked(clone!(@weak self as _self => move |_| { + f(&_self); + })); + } -impl Artist { - pub fn new(artist_model: &ArtistModel, worker: Worker) -> Self { - let widget = ArtistWidget::new(); + fn bind(&self, model: &ArtistModel, worker: Worker) { + let widget = imp::ArtistWidget::from_instance(self); - if let Some(url) = artist_model.image_url() { + if let Some(url) = model.image_url() { let avatar = widget.avatar.downgrade(); worker.send_local_task(async move { if let Some(avatar) = avatar.upgrade() { let loader = ImageLoader::new(); let pixbuf = loader.load_remote(&url, "jpg", 200, 200).await; - avatar.set_image_load_func(Some(Box::new(move |_| pixbuf.clone()))); + let texture = pixbuf.as_ref().map(|p| gdk::Texture::for_pixbuf(p)); + avatar.set_custom_image(texture.as_ref()); } }); } - artist_model - .bind_property("artist", &widget.artist, "label") + model + .bind_property("artist", &*widget.artist, "label") .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) .build(); - - Self { - widget, - model: artist_model.clone(), - } - } - - pub fn connect_artist_pressed(&self, f: F) { - self.widget - .avatar_btn - .connect_clicked(clone!(@weak self.model as model => move |_| { - f(&model); - })); - } -} - -impl Component for Artist { - fn get_root_widget(&self) -> >k::Widget { - &self.widget.root } } diff --git a/src/app/components/artist_details/artist_details.css b/src/app/components/artist_details/artist_details.css index 3a7e1eb5..161cce3b 100644 --- a/src/app/components/artist_details/artist_details.css +++ b/src/app/components/artist_details/artist_details.css @@ -1,10 +1,10 @@ -list.artist__top-tracks { +listview.artist__top-tracks { padding: 8px; border-radius: 8px; margin: 8px; } -list.artist__top-tracks row { +listview.artist__top-tracks row { border-radius: 4px; } diff --git a/src/app/components/artist_details/artist_details.rs b/src/app/components/artist_details/artist_details.rs index 41c409d8..d11ff741 100644 --- a/src/app/components/artist_details/artist_details.rs +++ b/src/app/components/artist_details/artist_details.rs @@ -1,25 +1,116 @@ -use gladis::Gladis; use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; use std::rc::Rc; -use crate::app::components::{screen_add_css_provider, Album, Component, EventListener, Playlist}; -use crate::app::models::*; +use crate::app::components::{ + screen_add_css_provider, AlbumWidget, Component, EventListener, Playlist, +}; +use crate::app::{models::*, ListStore}; use crate::app::{AppEvent, BrowserEvent, Worker}; use super::ArtistDetailsModel; -#[derive(Clone, Gladis)] -struct ArtistDetailsWidget { - pub root: gtk::ScrolledWindow, - pub artist_name: gtk::Label, - pub top_tracks: gtk::ListBox, - pub artist_releases: gtk::FlowBox, +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/artist_details.ui")] + pub struct ArtistDetailsWidget { + #[template_child] + pub scrolled_window: TemplateChild, + + #[template_child] + pub artist_name: TemplateChild, + + #[template_child] + pub top_tracks: TemplateChild, + + #[template_child] + pub artist_releases: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for ArtistDetailsWidget { + const NAME: &'static str = "ArtistDetailsWidget"; + type Type = super::ArtistDetailsWidget; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for ArtistDetailsWidget {} + impl WidgetImpl for ArtistDetailsWidget {} + impl BoxImpl for ArtistDetailsWidget {} +} + +glib::wrapper! { + pub struct ArtistDetailsWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; } impl ArtistDetailsWidget { fn new() -> Self { screen_add_css_provider(resource!("/components/artist_details.css")); - Self::from_resource(resource!("/components/artist_details.ui")).unwrap() + glib::Object::new(&[]).expect("Failed to create an instance of ArtistDetailsWidget") + } + + fn widget(&self) -> &imp::ArtistDetailsWidget { + imp::ArtistDetailsWidget::from_instance(self) + } + + fn top_tracks_widget(&self) -> >k::ListView { + self.widget().top_tracks.as_ref() + } + + fn set_artist_name(&self, name: &str) { + let context = self.style_context(); + context.add_class("artist__loaded"); + self.widget().artist_name.set_text(name); + } + + fn connect_bottom_edge(&self, f: F) + where + F: Fn() + 'static, + { + self.widget() + .scrolled_window + .connect_edge_reached(move |_, pos| { + if let gtk::PositionType::Bottom = pos { + f() + } + }); + } + + fn bind_artist_releases( + &self, + worker: Worker, + store: &ListStore, + on_album_pressed: F, + ) where + F: Fn(&String) + Clone + 'static, + { + self.widget() + .artist_releases + .bind_model(Some(store.unsafe_store()), move |item| { + let item = item.downcast_ref::().unwrap(); + let child = gtk::FlowBoxChild::new(); + let album = AlbumWidget::for_model(item, worker.clone()); + let f = on_album_pressed.clone(); + album.connect_album_pressed(clone!(@weak item => move |_| { + if let Some(id) = item.uri().as_ref() { + f(id); + } + })); + child.set_child(Some(&album)); + child.upcast::() + }); } } @@ -35,35 +126,24 @@ impl ArtistDetails { let widget = ArtistDetailsWidget::new(); - let weak_model = Rc::downgrade(&model); - widget.root.connect_edge_reached(move |_, pos| { - if let (gtk::PositionType::Bottom, Some(model)) = (pos, weak_model.upgrade()) { - let _ = model.load_more(); - } - }); + widget.connect_bottom_edge(clone!(@weak model => move || { + model.load_more(); + })); if let Some(store) = model.get_list_store() { - let model_clone = Rc::clone(&model); - - widget - .artist_releases - .bind_model(Some(store.unsafe_store()), move |item| { - let item = item.downcast_ref::().unwrap(); - let child = gtk::FlowBoxChild::new(); - let album = Album::new(item, worker.clone()); - let weak = Rc::downgrade(&model_clone); - album.connect_album_pressed(move |a| { - if let (Some(id), Some(m)) = (a.uri().as_ref(), weak.upgrade()) { - m.open_album(id); - } - }); - child.add(album.get_root_widget()); - child.show_all(); - child.upcast::() - }); + widget.bind_artist_releases( + worker, + &*store, + clone!(@weak model => move |id| { + model.open_album(id); + }), + ); } - let playlist = Box::new(Playlist::new(widget.top_tracks.clone(), Rc::clone(&model))); + let playlist = Box::new(Playlist::new( + widget.top_tracks_widget().clone(), + Rc::clone(&model), + )); Self { model, @@ -74,16 +154,14 @@ impl ArtistDetails { fn update_details(&mut self) { if let Some(name) = self.model.get_artist_name() { - let context = self.widget.root.style_context(); - context.add_class("artist__loaded"); - self.widget.artist_name.set_text(&name); + self.widget.set_artist_name(&name); } } } impl Component for ArtistDetails { fn get_root_widget(&self) -> >k::Widget { - self.widget.root.upcast_ref() + self.widget.upcast_ref() } fn get_children(&mut self) -> Option<&mut Vec>> { diff --git a/src/app/components/artist_details/artist_details.ui b/src/app/components/artist_details/artist_details.ui index 678259bb..fa082a6a 100644 --- a/src/app/components/artist_details/artist_details.ui +++ b/src/app/components/artist_details/artist_details.ui @@ -1,20 +1,14 @@ - - - - - True - True - never + + diff --git a/src/app/components/artist_details/artist_details_model.rs b/src/app/components/artist_details/artist_details_model.rs index 0401fca8..55383b70 100644 --- a/src/app/components/artist_details/artist_details_model.rs +++ b/src/app/components/artist_details/artist_details_model.rs @@ -139,7 +139,11 @@ impl PlaylistModel for ArtistDetailsModel { menu.append(Some(&*labels::VIEW_ALBUM), Some("song.view_album")); for artist in song.artists.iter().filter(|a| self.id != a.id) { menu.append( - Some(&format!("{} {}", *labels::MORE_FROM, artist.name)), + Some(&format!( + "{} {}", + *labels::MORE_FROM, + glib::markup_escape_text(&artist.name) + )), Some(&format!("song.view_artist_{}", artist.id)), ); } diff --git a/src/app/components/details/details.css b/src/app/components/details/details.css index 3300ae28..b2308c90 100644 --- a/src/app/components/details/details.css +++ b/src/app/components/details/details.css @@ -30,12 +30,12 @@ label.details__album-label { opacity: 1; } -list.details__songs { +listview.details__songs { padding: 8px; border-radius: 8px; } -list.details__songs row { +listview.details__songs row { border-radius: 4px; } diff --git a/src/app/components/details/details.rs b/src/app/components/details/details.rs index 0c30c001..f05b8356 100644 --- a/src/app/components/details/details.rs +++ b/src/app/components/details/details.rs @@ -1,8 +1,9 @@ -use gettextrs::{gettext, ngettext}; -use gladis::Gladis; use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; use std::rc::Rc; +use super::release_details::ReleaseDetailsWindow; use super::DetailsModel; use crate::app::components::{screen_add_css_provider, Component, EventListener, Playlist}; @@ -10,43 +11,154 @@ use crate::app::dispatch::Worker; use crate::app::loader::ImageLoader; use crate::app::{AppEvent, BrowserEvent}; -#[derive(Gladis, Clone)] -struct DetailsWidget { - pub root: gtk::Widget, - pub album_label: gtk::Label, - pub album_tracks: gtk::ListBox, - pub album_art: gtk::Image, - pub like_button: gtk::Button, - pub artist_button: gtk::LinkButton, - pub artist_button_label: gtk::Label, - pub album_info: gtk::Button, - info_window: libhandy::Window, - info_close: gtk::Button, - info_art: gtk::Image, - info_album_artist: gtk::Label, - info_label: gtk::Label, - info_release: gtk::Label, - info_tracks: gtk::Label, - info_duration: gtk::Label, - info_copyright: gtk::Label, +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/details.ui")] + pub struct AlbumDetailsWidget { + #[template_child] + pub scrolled_window: TemplateChild, + + #[template_child] + pub header_revealer: TemplateChild, + + #[template_child] + pub album_label: TemplateChild, + + #[template_child] + pub album_tracks: TemplateChild, + + #[template_child] + pub album_art: TemplateChild, + + #[template_child] + pub like_button: TemplateChild, + + #[template_child] + pub info_button: TemplateChild, + + #[template_child] + pub artist_button: TemplateChild, + + #[template_child] + pub artist_button_label: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for AlbumDetailsWidget { + const NAME: &'static str = "AlbumDetailsWidget"; + type Type = super::AlbumDetailsWidget; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for AlbumDetailsWidget {} + impl WidgetImpl for AlbumDetailsWidget {} + impl BoxImpl for AlbumDetailsWidget {} } -impl DetailsWidget { +glib::wrapper! { + pub struct AlbumDetailsWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; +} + +impl AlbumDetailsWidget { fn new() -> Self { screen_add_css_provider(resource!("/components/details.css")); - Self::from_resource(resource!("/components/details.ui")).unwrap() + glib::Object::new(&[]).expect("Failed to create an instance of AlbumDetailsWidget") + } + + fn widget(&self) -> &imp::AlbumDetailsWidget { + imp::AlbumDetailsWidget::from_instance(self) + } + + fn set_header_visible(&self, visible: bool) -> bool { + let widget = self.widget(); + let is_up_to_date = widget.header_revealer.reveals_child() == visible; + if !is_up_to_date { + widget.header_revealer.set_reveal_child(visible); + } + is_up_to_date + } + + fn connect_header(&self) { + self.set_header_visible(true); + + let scroll_controller = + gtk::EventControllerScroll::new(gtk::EventControllerScrollFlags::VERTICAL); + scroll_controller.connect_scroll( + clone!(@weak self as _self => @default-return gtk::Inhibit(false), move |_, _, dy| { + gtk::Inhibit(!_self.set_header_visible(dy < 0f64)) + }), + ); + + let widget = self.widget(); + widget.scrolled_window.add_controller(&scroll_controller); + } + + fn connect_liked(&self, f: F) + where + F: Fn() + 'static, + { + self.widget().like_button.connect_clicked(move |_| f()); + } + + fn connect_info(&self, f: F) + where + F: Fn() + 'static, + { + self.widget().info_button.connect_clicked(move |_| f()); + } + + fn connect_artist_clicked(&self, f: F) + where + F: Fn() + 'static, + { + self.widget().artist_button.connect_activate_link(move |_| { + f(); + glib::signal::Inhibit(true) + }); + } + + fn album_tracks_widget(&self) -> >k::ListView { + self.widget().album_tracks.as_ref() + } + + fn set_liked(&self, is_liked: bool) { + self.widget() + .like_button + .set_label(if is_liked { "♥" } else { "♡" }); } fn set_loaded(&self) { - let context = self.root.style_context(); + let context = self.style_context(); context.add_class("details--loaded"); } + + pub fn set_artwork(&self, art: &gdk_pixbuf::Pixbuf) { + self.widget().album_art.set_from_pixbuf(Some(art)); + } + + fn set_album_and_artist(&self, album: &str, artist: &str) { + let widget = self.widget(); + widget.album_label.set_label(album); + widget.artist_button_label.set_label(artist); + } } pub struct Details { model: Rc, worker: Worker, - widget: DetailsWidget, + widget: AlbumDetailsWidget, + modal: ReleaseDetailsWindow, children: Vec>, } @@ -55,140 +167,96 @@ impl Details { if model.get_album_info().is_none() { model.load_album_info(); } - let widget = DetailsWidget::new(); - let playlist = Box::new(Playlist::new(widget.album_tracks.clone(), model.clone())); - - widget - .like_button - .connect_clicked(clone!(@weak model => move |_| { - model.toggle_save_album(); - })); - let info_window = widget.info_window.clone(); + let widget = AlbumDetailsWidget::new(); + let playlist = Box::new(Playlist::new( + widget.album_tracks_widget().clone(), + model.clone(), + )); - info_window.connect_delete_event(|info_window, _| info_window.hide_on_delete()); + let modal = ReleaseDetailsWindow::new(); - info_window.connect_key_press_event(|info_window, event| { - if let gdk::keys::constants::Escape = event.keyval() { - info_window.hide() - } - glib::signal::Inhibit(false) - }); + widget.connect_liked(clone!(@weak model => move || model.toggle_save_album())); - widget - .info_close - .connect_clicked(clone!(@weak info_window => move |_| info_window.hide())); + widget.connect_header(); - widget - .album_info - .connect_clicked(clone!(@weak info_window => move |_| info_window.show())); + widget.connect_info(clone!(@weak modal, @weak widget => move || { + let modal = modal.upcast_ref::(); + modal.set_modal(true); + modal.set_transient_for( + widget + .root() + .and_then(|r| r.downcast::().ok()) + .as_ref(), + ); + modal.show(); + })); Self { model, worker, widget, + modal, children: vec![playlist], } } fn update_liked(&self) { - if let Some(album) = self.model.get_album_description() { - let is_liked = album.is_liked; - self.widget - .like_button - .set_label(if is_liked { "♥" } else { "♡" }); + if let Some(info) = self.model.get_album_info() { + let is_liked = info.description.is_liked; + self.widget.set_liked(is_liked); } } fn update_details(&mut self) { - if let Some(info) = self.model.get_album_description() { - let album = &info.title[..]; - let artist = &info.artists_name(); - - self.widget.album_label.set_label(album); - self.widget.artist_button_label.set_label(artist); - let weak_model = Rc::downgrade(&self.model); - self.widget.artist_button.connect_activate_link(move |_| { - if let Some(model) = weak_model.upgrade() { + if let Some(album) = self.model.get_album_info() { + let details = &album.release_details; + let album = &album.description; + + self.widget.set_liked(album.is_liked); + self.widget + .set_album_and_artist(&album.title[..], &album.artists_name()); + self.widget + .connect_artist_clicked(clone!(@weak self.model as model => move || { model.view_artist(); - } - glib::signal::Inhibit(true) - }); + })); - let widget = self.widget.clone(); - if let Some(art) = info.art.clone() { - self.worker.send_local_task(async move { - let pixbuf = ImageLoader::new() - .load_remote(&art[..], "jpg", 100, 100) - .await; - widget.album_art.set_from_pixbuf(pixbuf.as_ref()); - widget.set_loaded(); - }); - } else { - widget.set_loaded(); - } - } - } + self.modal.set_details( + &album.title, + &album.artists_name(), + &details.label, + &details.release_date, + album.songs.len(), + &album.formatted_time(), + &details.copyrights(), + ); - fn update_dialog(&mut self) { - if let Some(album) = self.model.get_album_info() { - let widget = self.widget.clone(); - let info = &album.release_details; - let album = &album.description; if let Some(art) = album.art.clone() { + let widget = self.widget.downgrade(); + let modal = self.modal.downgrade(); + self.worker.send_local_task(async move { let pixbuf = ImageLoader::new() .load_remote(&art[..], "jpg", 200, 200) .await; - widget.info_art.set_from_pixbuf(pixbuf.as_ref()); + if let (Some(widget), Some(modal), Some(ref pixbuf)) = + (widget.upgrade(), modal.upgrade(), pixbuf) + { + widget.set_artwork(pixbuf); + widget.set_loaded(); + modal.set_artwork(pixbuf); + } }); + } else { + self.widget.set_loaded(); } - - self.widget - .info_window - .set_title(&format!("{} {}", gettext("About"), album.title)); - - self.widget.info_album_artist.set_text(&format!( - "{} {} {}", - album.title, - gettext("by"), - album.artists_name() - )); - - self.widget - .info_label - .set_text(&format!("{} {}", gettext("Label:"), info.label)); - - self.widget.info_release.set_text(&format!( - "{} {}", - gettext("Released:"), - info.release_date - )); - - self.widget.info_tracks.set_text(&format!( - "{} {}", - gettext("Tracks:"), - album.songs.len() - )); - - self.widget.info_duration.set_text(&format!( - "{} {}", - gettext("Duration:"), - album.formatted_time() - )); - - self.widget.info_copyright.set_text(&format!( - "{} {}", - ngettext("Copyright:", "Copyrights:", info.copyrights.len() as u32), - info.copyrights() - )); } } } impl Component for Details { fn get_root_widget(&self) -> >k::Widget { - &self.widget.root + self.widget.upcast_ref() } fn get_children(&mut self) -> Option<&mut Vec>> { @@ -203,8 +271,6 @@ impl EventListener for Details { if id == &self.model.id => { self.update_details(); - self.update_liked(); - self.update_dialog(); } AppEvent::BrowserEvent(BrowserEvent::AlbumSaved(id)) | AppEvent::BrowserEvent(BrowserEvent::AlbumUnsaved(id)) diff --git a/src/app/components/details/details.ui b/src/app/components/details/details.ui index 11b5e439..798a4359 100644 --- a/src/app/components/details/details.ui +++ b/src/app/components/details/details.ui @@ -1,302 +1,59 @@ - - - - - - False - True - 250 - dialog + + + diff --git a/src/app/components/details/details_model.rs b/src/app/components/details/details_model.rs index fb9251ba..ed66198c 100644 --- a/src/app/components/details/details_model.rs +++ b/src/app/components/details/details_model.rs @@ -193,7 +193,11 @@ impl PlaylistModel for DetailsModel { let menu = gio::Menu::new(); for artist in song.artists.iter() { menu.append( - Some(&format!("{} {}", *labels::MORE_FROM, artist.name)), + Some(&format!( + "{} {}", + *labels::MORE_FROM, + glib::markup_escape_text(&artist.name) + )), Some(&format!("song.view_artist_{}", artist.id)), ); } diff --git a/src/app/components/details/mod.rs b/src/app/components/details/mod.rs index 32e87dc2..2d367f88 100644 --- a/src/app/components/details/mod.rs +++ b/src/app/components/details/mod.rs @@ -1,5 +1,7 @@ mod details; mod details_model; -pub use details::*; -pub use details_model::*; +pub use details::Details; +pub use details_model::DetailsModel; + +mod release_details; diff --git a/src/app/components/details/release_details.rs b/src/app/components/details/release_details.rs new file mode 100644 index 00000000..a3d8db30 --- /dev/null +++ b/src/app/components/details/release_details.rs @@ -0,0 +1,140 @@ +use gettextrs::gettext; +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; +use libadwaita::subclass::prelude::*; + +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/release_details.ui")] + pub struct ReleaseDetailsWindow { + #[template_child] + pub close: TemplateChild, + + #[template_child] + pub art: TemplateChild, + + #[template_child] + pub album_artist: TemplateChild, + + #[template_child] + pub label: TemplateChild, + + #[template_child] + pub release: TemplateChild, + + #[template_child] + pub tracks: TemplateChild, + + #[template_child] + pub duration: TemplateChild, + + #[template_child] + pub copyright: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for ReleaseDetailsWindow { + const NAME: &'static str = "ReleaseDetailsWindow"; + type Type = super::ReleaseDetailsWindow; + type ParentType = libadwaita::Window; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for ReleaseDetailsWindow { + fn constructed(&self, obj: &Self::Type) { + self.parent_constructed(obj); + self.close + .connect_clicked(clone!(@weak obj => move |_| obj.hide())); + } + } + + impl WidgetImpl for ReleaseDetailsWindow {} + impl AdwWindowImpl for ReleaseDetailsWindow {} + impl WindowImpl for ReleaseDetailsWindow {} +} + +glib::wrapper! { + pub struct ReleaseDetailsWindow(ObjectSubclass) @extends gtk::Widget, libadwaita::Window; +} + +impl ReleaseDetailsWindow { + pub fn new() -> Self { + glib::Object::new(&[]).expect("Failed to create an instance of ReleaseDetailsWindow") + } + + fn widget(&self) -> &imp::ReleaseDetailsWindow { + imp::ReleaseDetailsWindow::from_instance(self) + } + + pub fn set_artwork(&self, art: &gdk_pixbuf::Pixbuf) { + self.widget().art.set_from_pixbuf(Some(art)); + } + + #[allow(clippy::too_many_arguments)] + pub fn set_details( + &self, + album: &str, + artist: &str, + label: &str, + release_date: &str, + track_count: usize, + duration: &str, + copyright: &str, + ) { + let widget = self.widget(); + + widget.album_artist.set_text(&format!( + "{} {} {}", + album, + // translators: This is part of a larger label that reads " by " + gettext("by"), + artist + )); + + widget.label.set_text(&format!( + "{} {}", + // translators: This refers to a music label + gettext("Label:"), + label + )); + + widget.release.set_text(&format!( + "{} {}", + // translators: This refers to a release date + gettext("Released:"), + release_date + )); + + widget.tracks.set_text(&format!( + "{} {}", + // translators: This refers to a number of tracks + gettext("Tracks:"), + track_count + )); + + widget.duration.set_text(&format!( + "{} {}", + // translators: This refers to the duration of eg. an album + gettext("Duration:"), + duration + )); + + widget.copyright.set_text(&format!( + "{} {}", + // translators: Self explanatory + gettext("Copyright:"), + copyright + )); + } +} diff --git a/src/app/components/details/release_details.ui b/src/app/components/details/release_details.ui new file mode 100644 index 00000000..c12ebca0 --- /dev/null +++ b/src/app/components/details/release_details.ui @@ -0,0 +1,136 @@ + + + + + + diff --git a/src/app/components/library/library.rs b/src/app/components/library/library.rs index 66746f87..109dd774 100644 --- a/src/app/components/library/library.rs +++ b/src/app/components/library/library.rs @@ -1,28 +1,91 @@ -use gladis::Gladis; use gtk::prelude::*; - -use std::rc::{Rc, Weak}; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; +use std::rc::Rc; use super::LibraryModel; -use crate::app::components::{Album, Component, EventListener}; +use crate::app::components::utils::wrap_flowbox_item; +use crate::app::components::{AlbumWidget, Component, EventListener}; use crate::app::dispatch::Worker; use crate::app::models::AlbumModel; use crate::app::state::LoginEvent; -use crate::app::AppEvent; +use crate::app::{AppEvent, ListStore}; + +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/library.ui")] + pub struct LibraryWidget { + #[template_child] + pub scrolled_window: TemplateChild, + + #[template_child] + pub flowbox: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for LibraryWidget { + const NAME: &'static str = "LibraryWidget"; + type Type = super::LibraryWidget; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for LibraryWidget {} + impl WidgetImpl for LibraryWidget {} + impl BoxImpl for LibraryWidget {} +} -#[derive(Clone, Gladis)] -struct LibraryWidget { - pub scrolled_window: gtk::ScrolledWindow, - pub flowbox: gtk::FlowBox, +glib::wrapper! { + pub struct LibraryWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; } impl LibraryWidget { - fn new() -> Self { - Self::from_resource(resource!("/components/library.ui")).unwrap() + pub fn new() -> Self { + glib::Object::new(&[]).expect("Failed to create an instance of LibraryWidget") } - fn root(&self) -> >k::Widget { - self.scrolled_window.upcast_ref() + fn connect_bottom_edge(&self, f: F) + where + F: Fn() + 'static, + { + imp::LibraryWidget::from_instance(self) + .scrolled_window + .connect_edge_reached(move |_, pos| { + if let gtk::PositionType::Bottom = pos { + f() + } + }); + } + + fn bind_albums(&self, worker: Worker, store: &ListStore, on_album_pressed: F) + where + F: Fn(&String) + Clone + 'static, + { + imp::LibraryWidget::from_instance(self).flowbox.bind_model( + Some(store.unsafe_store()), + move |item| { + wrap_flowbox_item(item, |album_model| { + let f = on_album_pressed.clone(); + let album = AlbumWidget::for_model(album_model, worker.clone()); + album.connect_album_pressed(clone!(@weak album_model => move |_| { + if let Some(id) = album_model.uri().as_ref() { + f(id); + } + })); + album + }) + }, + ); } } @@ -35,15 +98,10 @@ pub struct Library { impl Library { pub fn new(worker: Worker, model: LibraryModel) -> Self { let model = Rc::new(model); - let widget = LibraryWidget::new(); - - let weak_model = Rc::downgrade(&model); - widget.scrolled_window.connect_edge_reached(move |_, pos| { - if let (gtk::PositionType::Bottom, Some(model)) = (pos, weak_model.upgrade()) { - let _ = model.load_more_albums(); - } - }); + widget.connect_bottom_edge(clone!(@weak model => move || { + model.load_more_albums(); + })); Self { widget, @@ -52,16 +110,14 @@ impl Library { } } - fn bind_flowbox(&self, store: &gio::ListStore) { - let weak_model = Rc::downgrade(&self.model); - let worker_clone = self.worker.clone(); - - self.widget.flowbox.bind_model(Some(store), move |item| { - let item = item.downcast_ref::().unwrap(); - let child = create_album_for(item, worker_clone.clone(), weak_model.clone()); - child.show_all(); - child.upcast::() - }); + fn bind_flowbox(&self) { + self.widget.bind_albums( + self.worker.clone(), + &*self.model.get_list_store().unwrap(), + clone!(@weak self.model as model => move |id| { + model.open_album(id.clone()); + }), + ); } } @@ -70,7 +126,7 @@ impl EventListener for Library { match event { AppEvent::Started => { let _ = self.model.refresh_saved_albums(); - self.bind_flowbox(self.model.get_list_store().unwrap().unsafe_store()) + self.bind_flowbox(); } AppEvent::LoginEvent(LoginEvent::LoginCompleted(_)) => { let _ = self.model.refresh_saved_albums(); @@ -82,25 +138,6 @@ impl EventListener for Library { impl Component for Library { fn get_root_widget(&self) -> >k::Widget { - self.widget.root() + self.widget.as_ref() } } - -fn create_album_for( - album_model: &AlbumModel, - worker: Worker, - model: Weak, -) -> gtk::FlowBoxChild { - let child = gtk::FlowBoxChild::new(); - - let album = Album::new(album_model, worker); - child.add(album.get_root_widget()); - - album.connect_album_pressed(move |a| { - if let (Some(model), Some(id)) = (model.upgrade(), a.uri()) { - model.open_album(id); - } - }); - - child -} diff --git a/src/app/components/library/library.ui b/src/app/components/library/library.ui index aaa166e7..48f5f256 100644 --- a/src/app/components/library/library.ui +++ b/src/app/components/library/library.ui @@ -1,30 +1,25 @@ - - - - True - True - always - 250 + + diff --git a/src/app/components/login/login.rs b/src/app/components/login/login.rs index b8e50215..7e8f3892 100644 --- a/src/app/components/login/login.rs +++ b/src/app/components/login/login.rs @@ -1,6 +1,6 @@ -use gdk::{keys::constants::Return, EventKey}; -use gladis::Gladis; use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; use std::rc::Rc; use crate::app::components::EventListener; @@ -9,128 +9,179 @@ use crate::app::state::{LoginCompletedEvent, LoginEvent}; use crate::app::AppEvent; use super::LoginModel; +mod imp { -#[derive(Clone, Gladis)] -struct LoginWidget { - pub window: libhandy::Window, - username: gtk::Entry, - password: gtk::Entry, - close_button: gtk::Button, - login_button: gtk::Button, - error_container: gtk::Revealer, + use libadwaita::subclass::prelude::AdwWindowImpl; + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/login.ui")] + pub struct LoginWindow { + #[template_child] + pub username: TemplateChild, + + #[template_child] + pub password: TemplateChild, + + #[template_child] + pub close_button: TemplateChild, + + #[template_child] + pub login_button: TemplateChild, + + #[template_child] + pub error_container: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for LoginWindow { + const NAME: &'static str = "LoginWindow"; + type Type = super::LoginWindow; + type ParentType = libadwaita::Window; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for LoginWindow {} + impl WidgetImpl for LoginWindow {} + impl AdwWindowImpl for LoginWindow {} + impl WindowImpl for LoginWindow {} } -impl LoginWidget { - fn new() -> Self { - Self::from_resource(resource!("/components/login.ui")).unwrap() +glib::wrapper! { + pub struct LoginWindow(ObjectSubclass) @extends gtk::Widget, libadwaita::Window; +} + +impl LoginWindow { + pub fn new() -> Self { + glib::Object::new(&[]).expect("Failed to create an instance of LoginWindow") + } + + fn connect_close(&self, on_close: F) + where + F: Fn() + 'static, + { + let widget = imp::LoginWindow::from_instance(self); + widget.close_button.connect_clicked(move |_| { + on_close(); + }); + } + + fn connect_submit(&self, on_submit: SubmitFn) + where + SubmitFn: Fn(&str, &str) + Clone + 'static, + { + let widget = imp::LoginWindow::from_instance(self); + + let on_submit_clone = on_submit.clone(); + let controller = gtk::EventControllerKey::new(); + controller.set_propagation_phase(gtk::PropagationPhase::Capture); + controller.connect_key_pressed( + clone!(@weak self as _self => @default-return gtk::Inhibit(false), move |_, key, _, _| { + if key == gdk::keys::constants::Return { + _self.submit(&on_submit_clone); + gtk::Inhibit(true) + } else { + gtk::Inhibit(false) + } + }), + ); + self.add_controller(&controller); + + widget + .login_button + .connect_clicked(clone!(@weak self as _self => move |_| { + _self.submit(&on_submit); + })); + } + + fn show_error(&self, shown: bool) { + let widget = imp::LoginWindow::from_instance(self); + widget.error_container.set_reveal_child(shown); + } + + fn submit(&self, on_submit: &SubmitFn) + where + SubmitFn: Fn(&str, &str), + { + let widget = imp::LoginWindow::from_instance(self); + + self.show_error(false); + + let username_text = widget.username.text(); + let password_text = widget.password.text(); + + if username_text.is_empty() { + widget.username.grab_focus(); + } else if password_text.is_empty() { + widget.password.grab_focus(); + } else { + on_submit(username_text.as_str(), password_text.as_str()); + } } } pub struct Login { parent: gtk::Window, - window: libhandy::Window, - error_container: gtk::Revealer, + login_window: LoginWindow, model: Rc, } impl Login { pub fn new(parent: gtk::Window, model: LoginModel) -> Self { let model = Rc::new(model); - let LoginWidget { - window, - username, - password, - close_button, - login_button, - error_container, - } = LoginWidget::new(); - - login_button.connect_clicked( - clone!(@weak username, @weak password, @weak error_container, @weak model => move |_| { - Self::submit_login_form(username, password, error_container, model); - }), - ); - username.connect_key_press_event( - clone!(@weak username, @weak password, @weak error_container, @weak model => @default-return Inhibit(false), move |_, event | { - Self::handle_keypress(username, password, error_container, model, event) - }), - ); + let login_window = LoginWindow::new(); - password.connect_key_press_event( - clone!(@weak username, @weak password, @weak error_container, @weak model => @default-return Inhibit(false), move |_, event | { - Self::handle_keypress(username, password, error_container, model, event) - }), - ); - - close_button.connect_clicked(clone!(@weak parent => move |_| { + login_window.connect_close(clone!(@weak parent => move || { if let Some(app) = parent.application().as_ref() { app.quit(); } })); + login_window.connect_submit(clone!(@weak model => move |username, password| { + model.login(username.to_string(), password.to_string()); + })); + Self { parent, - window, - error_container, + login_window, model, } } + fn window(&self) -> &libadwaita::Window { + self.login_window.upcast_ref::() + } + fn show_self_if_needed(&self) { if self.model.try_autologin() { - self.window.close(); + self.window().close(); } else { self.show_self(); } } fn show_self(&self) { - self.window.set_transient_for(Some(&self.parent)); - self.window.set_modal(true); - self.window.show_all(); + self.window().set_transient_for(Some(&self.parent)); + self.window().set_modal(true); + self.window().show(); } fn hide_and_save_creds(&self, credentials: Credentials) { - self.window.hide(); + self.window().hide(); self.model.save_for_autologin(credentials); } - fn handle_keypress( - username: gtk::Entry, - password: gtk::Entry, - error_container: gtk::Revealer, - model: Rc, - event: &EventKey, - ) -> Inhibit { - if event.keyval() == Return { - Login::submit_login_form(username, password, error_container, model); - Inhibit(true) - } else { - Inhibit(false) - } - } - - fn submit_login_form( - username: gtk::Entry, - password: gtk::Entry, - error_container: gtk::Revealer, - model: Rc, - ) { - error_container.set_reveal_child(false); - let username_text = username.text().as_str().to_string(); - let password_text = password.text().as_str().to_string(); - if username_text.is_empty() { - username.grab_focus(); - } else if password_text.is_empty() { - password.grab_focus(); - } else { - model.login(username_text, password_text); - } - } - fn reveal_error(&self) { - self.error_container.set_reveal_child(true); + self.login_window.show_error(true); } } diff --git a/src/app/components/login/login.ui b/src/app/components/login/login.ui index c8e2edc8..a13b46ce 100644 --- a/src/app/components/login/login.ui +++ b/src/app/components/login/login.ui @@ -1,20 +1,14 @@ - - - - - False + + + + \ No newline at end of file diff --git a/src/app/components/mod.rs b/src/app/components/mod.rs index 415b0d40..b114fb37 100644 --- a/src/app/components/mod.rs +++ b/src/app/components/mod.rs @@ -6,7 +6,6 @@ macro_rules! resource { } use gettextrs::*; -use gtk::prelude::*; use std::cell::RefCell; use std::collections::HashSet; use std::future::Future; @@ -124,8 +123,8 @@ pub fn screen_add_css_provider(resource: &'static str) { let provider = gtk::CssProvider::new(); provider.load_from_resource(resource); - gtk::StyleContext::add_provider_for_screen( - &gdk::Screen::default().unwrap(), + gtk::StyleContext::add_provider_for_display( + &gdk::Display::default().unwrap(), &provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, ); diff --git a/src/app/components/navigation/home.rs b/src/app/components/navigation/home.rs index cd3bfba9..0a714017 100644 --- a/src/app/components/navigation/home.rs +++ b/src/app/components/navigation/home.rs @@ -4,11 +4,11 @@ use gtk::prelude::*; use crate::app::components::{Component, EventListener, ScreenFactory}; use crate::app::AppEvent; -fn find_listbox_descendant(w: >k::Widget) -> Option { - match w.clone().downcast::() { - Ok(listbox) => Some(listbox), - Err(widget) => { - let next = widget.downcast::().ok()?.child()?; +fn find_listbox_descendant(widget: >k::Widget) -> Option { + match widget.downcast_ref::() { + Some(listbox) => Some(listbox.clone()), + None => { + let next = widget.first_child()?; find_listbox_descendant(&next) } } @@ -29,16 +29,20 @@ impl HomePane { let stack = gtk::Stack::new(); stack.set_transition_type(gtk::StackTransitionType::Crossfade); // translators: This is a sidebar entry to browse to saved albums. - stack.add_titled(library.get_root_widget(), "library", &gettext("Library")); + stack.add_titled( + library.get_root_widget(), + Some("library"), + &gettext("Library"), + ); stack.add_titled( saved_playlists.get_root_widget(), - "saved_playlists", + Some("saved_playlists"), // translators: This is a sidebar entry to browse to saved playlists. &gettext("Playlists"), ); stack.add_titled( now_playing.get_root_widget(), - "now_playing", + Some("now_playing"), &gettext("Now playing"), ); diff --git a/src/app/components/navigation/navigation.rs b/src/app/components/navigation/navigation.rs index 77bb8420..6a5d9109 100644 --- a/src/app/components/navigation/navigation.rs +++ b/src/app/components/navigation/navigation.rs @@ -1,6 +1,5 @@ use gtk::prelude::*; -use libhandy::traits::LeafletExt; -use libhandy::NavigationDirection; +use libadwaita::NavigationDirection; use std::rc::Rc; use crate::app::components::{EventListener, ListenerComponent}; @@ -11,7 +10,7 @@ use super::{factory::ScreenFactory, home::HomePane, NavigationModel}; pub struct Navigation { model: Rc, - leaflet: libhandy::Leaflet, + leaflet: libadwaita::Leaflet, navigation_stack: gtk::Stack, home_stack_sidebar: gtk::StackSidebar, back_button: gtk::Button, @@ -22,7 +21,7 @@ pub struct Navigation { impl Navigation { pub fn new( model: NavigationModel, - leaflet: libhandy::Leaflet, + leaflet: libadwaita::Leaflet, back_button: gtk::Button, navigation_stack: gtk::Stack, home_stack_sidebar: gtk::StackSidebar, @@ -55,7 +54,7 @@ impl Navigation { fn update_back_button( back_button: >k::Button, - leaflet: &libhandy::Leaflet, + leaflet: &libadwaita::Leaflet, model: &Rc, ) { let is_main = leaflet @@ -67,7 +66,7 @@ impl Navigation { fn connect_back_button( back_button: >k::Button, - leaflet: &libhandy::Leaflet, + leaflet: &libadwaita::Leaflet, model: &Rc, ) { back_button.connect_clicked(clone!(@weak leaflet, @weak model => move |_| { @@ -116,10 +115,9 @@ impl Navigation { }; let widget = component.get_root_widget(); - widget.show_all(); self.navigation_stack - .add_named(widget, name.identifier().as_ref()); + .add_named(widget, Some(name.identifier().as_ref())); self.children.push(component); self.navigation_stack .set_visible_child_name(name.identifier().as_ref()); diff --git a/src/app/components/now_playing/now_playing.rs b/src/app/components/now_playing/now_playing.rs index af2c34b4..f86f8548 100644 --- a/src/app/components/now_playing/now_playing.rs +++ b/src/app/components/now_playing/now_playing.rs @@ -1,5 +1,6 @@ -use gladis::Gladis; use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; use std::rc::Rc; use crate::app::components::{screen_add_css_provider, Component, EventListener, Playlist}; @@ -7,16 +8,67 @@ use crate::app::{state::PlaybackEvent, AppEvent}; use super::NowPlayingModel; -#[derive(Clone, Gladis)] -struct NowPlayingWidget { - root: gtk::Widget, - listbox: gtk::ListBox, +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/now_playing.ui")] + pub struct NowPlayingWidget { + #[template_child] + pub song_list: TemplateChild, + + #[template_child] + pub scrolled_window: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for NowPlayingWidget { + const NAME: &'static str = "NowPlayingWidget"; + type Type = super::NowPlayingWidget; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for NowPlayingWidget {} + impl WidgetImpl for NowPlayingWidget {} + impl BoxImpl for NowPlayingWidget {} +} + +glib::wrapper! { + pub struct NowPlayingWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; } impl NowPlayingWidget { fn new() -> Self { screen_add_css_provider(resource!("/components/now_playing.css")); - Self::from_resource(resource!("/components/now_playing.ui")).unwrap() + glib::Object::new(&[]).expect("Failed to create an instance of NowPlayingWidget") + } + + fn connect_bottom_edge(&self, f: F) + where + F: Fn() + 'static, + { + imp::NowPlayingWidget::from_instance(self) + .scrolled_window + .connect_edge_reached(move |_, pos| { + if let gtk::PositionType::Bottom = pos { + f() + } + }); + } + + fn song_list_widget(&self) -> >k::ListView { + imp::NowPlayingWidget::from_instance(self) + .song_list + .as_ref() } } @@ -29,7 +81,12 @@ pub struct NowPlaying { impl NowPlaying { pub fn new(model: Rc) -> Self { let widget = NowPlayingWidget::new(); - let playlist = Playlist::new(widget.listbox.clone(), model.clone()); + + widget.connect_bottom_edge(clone!(@weak model => move || { + model.load_more(); + })); + + let playlist = Playlist::new(widget.song_list_widget().clone(), model.clone()); Self { widget, @@ -41,7 +98,7 @@ impl NowPlaying { impl Component for NowPlaying { fn get_root_widget(&self) -> >k::Widget { - &self.widget.root + self.widget.upcast_ref() } fn get_children(&mut self) -> Option<&mut Vec>> { diff --git a/src/app/components/now_playing/now_playing.ui b/src/app/components/now_playing/now_playing.ui index b2a62724..e339db46 100644 --- a/src/app/components/now_playing/now_playing.ui +++ b/src/app/components/now_playing/now_playing.ui @@ -1,78 +1,40 @@ - - - - True - False + + diff --git a/src/app/components/now_playing/now_playing_model.rs b/src/app/components/now_playing/now_playing_model.rs index 71c32b27..0f80905c 100644 --- a/src/app/components/now_playing/now_playing_model.rs +++ b/src/app/components/now_playing/now_playing_model.rs @@ -8,6 +8,7 @@ use std::sync::Arc; use crate::app::components::{labels, PlaylistModel, SelectionTool, SelectionToolsModel}; use crate::app::models::SongDescription; use crate::app::models::SongModel; +use crate::app::state::PlaylistChange; use crate::app::state::{ PlaybackAction, PlaybackEvent, PlaybackState, PlaylistSource, SelectionAction, SelectionContext, SelectionState, @@ -38,10 +39,15 @@ impl NowPlayingModel { pub fn load_more_if_needed(&self) -> Option<()> { let queue = self.queue(); - if !queue.exhausted() { - return None; + if queue.exhausted() { + self.load_more() + } else { + None } + } + pub fn load_more(&self) -> Option<()> { + let queue = self.queue(); let api = self.app_model.get_spotify(); let batch = queue.next_batch()?; let batch_size = batch.batch_size; @@ -73,16 +79,15 @@ impl PlaylistModel for NowPlayingModel { fn diff_for_event(&self, event: &AppEvent) -> Option> { let queue = self.queue(); - let offset = queue.current_offset().unwrap_or(0); - let songs = queue - .songs() - .enumerate() - .map(|(i, s)| s.to_song_model(offset + i)); + let songs = queue.songs().enumerate().map(|(i, s)| s.to_song_model(i)); match event { - AppEvent::PlaybackEvent(PlaybackEvent::PlaylistChanged) => { - Some(ListDiff::Set(songs.collect())) - } + AppEvent::PlaybackEvent(PlaybackEvent::PlaylistChanged(change)) => match change { + PlaylistChange::Reset => Some(ListDiff::Set(songs.collect())), + PlaylistChange::AppendedAt(i) => Some(ListDiff::Append(songs.skip(*i).collect())), + PlaylistChange::MovedDown(i) => Some(ListDiff::MoveDown(*i)), + PlaylistChange::MovedUp(i) => Some(ListDiff::MoveUp(*i)), + }, _ => None, } } @@ -114,7 +119,11 @@ impl PlaylistModel for NowPlayingModel { menu.append(Some(&*labels::VIEW_ALBUM), Some("song.view_album")); for artist in song.artists.iter() { menu.append( - Some(&format!("{} {}", *labels::MORE_FROM, artist.name)), + Some(&format!( + "{} {}", + *labels::MORE_FROM, + glib::markup_escape_text(&artist.name) + )), Some(&format!("song.view_artist_{}", artist.id)), ); } diff --git a/src/app/components/playback/component.rs b/src/app/components/playback/component.rs new file mode 100644 index 00000000..73e16269 --- /dev/null +++ b/src/app/components/playback/component.rs @@ -0,0 +1,159 @@ +use std::ops::Deref; +use std::rc::Rc; + +use crate::app::components::EventListener; +use crate::app::models::SongDescription; +use crate::app::state::{PlaybackAction, PlaybackEvent, RepeatMode, ScreenName}; +use crate::app::{ + ActionDispatcher, AppAction, AppEvent, AppModel, AppState, BrowserAction, Worker, +}; + +use super::playback_widget::PlaybackWidget; + +pub struct PlaybackModel { + app_model: Rc, + dispatcher: Box, +} + +impl PlaybackModel { + pub fn new(app_model: Rc, dispatcher: Box) -> Self { + Self { + app_model, + dispatcher, + } + } + + fn state(&self) -> impl Deref + '_ { + self.app_model.get_state() + } + + fn go_home(&self) { + self.dispatcher.dispatch(AppAction::ViewNowPlaying); + self.dispatcher + .dispatch(BrowserAction::NavigationPopTo(ScreenName::Home).into()); + } + + fn is_playing(&self) -> bool { + self.state().playback.is_playing() + } + + fn is_shuffled(&self) -> bool { + self.state().playback.is_shuffled() + } + + fn current_song(&self) -> Option + '_> { + self.app_model.map_state_opt(|s| s.playback.current_song()) + } + + fn play_next_song(&self) { + self.dispatcher.dispatch(PlaybackAction::Next.into()); + } + + fn play_prev_song(&self) { + self.dispatcher.dispatch(PlaybackAction::Previous.into()); + } + + fn toggle_playback(&self) { + self.dispatcher.dispatch(PlaybackAction::TogglePlay.into()); + } + + fn toggle_shuffle(&self) { + self.dispatcher + .dispatch(PlaybackAction::ToggleShuffle.into()); + } + + fn toggle_repeat(&self) { + self.dispatcher + .dispatch(PlaybackAction::ToggleRepeat.into()); + } + + fn seek_to(&self, position: u32) { + self.dispatcher + .dispatch(PlaybackAction::Seek(position).into()); + } +} + +pub struct PlaybackControl { + model: Rc, + widget: PlaybackWidget, + worker: Worker, +} + +impl PlaybackControl { + pub fn new(model: PlaybackModel, widget: PlaybackWidget, worker: Worker) -> Self { + let model = Rc::new(model); + + widget.connect_play_pause(clone!(@weak model => move || model.toggle_playback() )); + widget.connect_next(clone!(@weak model => move || model.play_next_song())); + widget.connect_prev(clone!(@weak model => move || model.play_prev_song())); + widget.connect_shuffle(clone!(@weak model => move || model.toggle_shuffle())); + widget.connect_repeat(clone!(@weak model => move || model.toggle_repeat())); + widget.connect_seek(clone!(@weak model => move |position| model.seek_to(position))); + widget.connect_now_playing_clicked(clone!(@weak model => move || model.go_home())); + + Self { + model, + widget, + worker, + } + } + + fn update_repeat(&self, mode: &RepeatMode) { + self.widget.set_repeat_mode(*mode); + } + + fn update_shuffled(&self) { + self.widget.set_shuffled(self.model.is_shuffled()); + } + + fn update_playing(&self) { + let is_playing = self.model.is_playing(); + self.widget.set_playing(is_playing); + } + + fn update_current_info(&self) { + if let Some(song) = self.model.current_song() { + self.widget + .set_title_and_artist(&song.title, &song.artists_name()); + self.widget.set_song_duration(Some(song.duration as f64)); + if let Some(url) = song.art.clone() { + self.widget.set_artwork_from_url(url, &self.worker); + } + } else { + self.widget.reset_info(); + } + } + + fn sync_seek(&self, pos: u32) { + self.widget.set_seek_position(pos as f64); + } +} + +impl EventListener for PlaybackControl { + fn on_event(&mut self, event: &AppEvent) { + match event { + AppEvent::PlaybackEvent(PlaybackEvent::PlaybackPaused) + | AppEvent::PlaybackEvent(PlaybackEvent::PlaybackResumed) => { + self.update_playing(); + } + AppEvent::PlaybackEvent(PlaybackEvent::RepeatModeChanged(mode)) => { + self.update_repeat(mode); + } + AppEvent::PlaybackEvent(PlaybackEvent::ShuffleChanged) => { + self.update_shuffled(); + } + AppEvent::PlaybackEvent(PlaybackEvent::TrackChanged(_)) => { + self.update_current_info(); + } + AppEvent::PlaybackEvent(PlaybackEvent::PlaybackStopped) => { + self.update_playing(); + self.update_current_info(); + } + AppEvent::PlaybackEvent(PlaybackEvent::SeekSynced(pos)) + | AppEvent::PlaybackEvent(PlaybackEvent::TrackSeeked(pos)) => { + self.sync_seek(*pos); + } + _ => {} + } + } +} diff --git a/src/app/components/playback/mod.rs b/src/app/components/playback/mod.rs index 3bf32aca..2800d3f7 100644 --- a/src/app/components/playback/mod.rs +++ b/src/app/components/playback/mod.rs @@ -1,5 +1,11 @@ -mod playback_control; -pub use playback_control::*; - +mod component; +mod playback_controls; mod playback_info; -pub use playback_info::*; +mod playback_widget; +pub use component::*; + +use glib::prelude::*; + +pub fn expose_widgets() { + playback_widget::PlaybackWidget::static_type(); +} diff --git a/src/app/components/playback/playback.css b/src/app/components/playback/playback.css new file mode 100644 index 00000000..affd77f8 --- /dev/null +++ b/src/app/components/playback/playback.css @@ -0,0 +1,32 @@ +.seek-bar { + padding: 0; + padding-bottom: 2px; + min-height: 1px; +} + +.seek-bar trough, .seek-bar highlight { + border-radius: 0; + border-left: none; + border-right: none; + min-height: 1px; + transition: min-height 100ms ease; +} + +.seek-bar--active trough, .seek-bar--active highlight { + min-height: 5px; +} + +.seek-bar--active:hover trough, .seek-bar--active:hover highlight { + min-height: 10px; +} + +.seek-bar highlight { + border-left: none; + border-right: none; +} + +.playback-button { + -gtk-icon-size: 28px; + min-width: 40px; + min-height: 40px; +} \ No newline at end of file diff --git a/src/app/components/playback/playback_control.rs b/src/app/components/playback/playback_control.rs deleted file mode 100644 index a2ce2f65..00000000 --- a/src/app/components/playback/playback_control.rs +++ /dev/null @@ -1,269 +0,0 @@ -use glib::signal; -use gtk::prelude::*; -use std::ops::Deref; -use std::rc::Rc; - -use crate::app::components::utils::format_duration; -use crate::app::components::{ - utils::{Clock, Debouncer}, - EventListener, -}; -use crate::app::state::{PlaybackAction, PlaybackEvent, RepeatMode}; -use crate::app::{ActionDispatcher, AppEvent, AppModel, AppState}; - -pub struct PlaybackControlModel { - app_model: Rc, - dispatcher: Box, -} - -impl PlaybackControlModel { - pub fn new(app_model: Rc, dispatcher: Box) -> Self { - Self { - app_model, - dispatcher, - } - } - - fn state(&self) -> impl Deref + '_ { - self.app_model.get_state() - } - - pub fn is_playing(&self) -> bool { - self.state().playback.is_playing() - } - - pub fn current_song_duration(&self) -> Option { - self.state() - .playback - .current_song() - .map(|s| s.duration as f64) - } - - pub fn play_next_song(&self) { - self.dispatcher.dispatch(PlaybackAction::Next.into()); - } - - pub fn play_prev_song(&self) { - self.dispatcher.dispatch(PlaybackAction::Previous.into()); - } - - pub fn toggle_playback(&self) { - self.dispatcher.dispatch(PlaybackAction::TogglePlay.into()); - } - - pub fn toggle_shuffle(&self) { - self.dispatcher - .dispatch(PlaybackAction::ToggleShuffle.into()); - } - - pub fn toggle_repeat(&self) { - self.dispatcher - .dispatch(PlaybackAction::ToggleRepeat.into()); - } - - pub fn seek_to(&self, position: u32) { - self.dispatcher - .dispatch(PlaybackAction::Seek(position).into()); - } -} - -pub struct PlaybackControlWidget { - play_button: gtk::Button, - seek_bar: gtk::Scale, - track_position: gtk::Label, - track_duration: gtk::Label, - next: gtk::Button, - prev: gtk::Button, - shuffle_button: gtk::Button, - repeat_button: gtk::Button, -} - -impl PlaybackControlWidget { - #[allow(clippy::too_many_arguments)] - pub fn new( - play_button: gtk::Button, - seek_bar: gtk::Scale, - track_position: gtk::Label, - track_duration: gtk::Label, - next: gtk::Button, - prev: gtk::Button, - shuffle_button: gtk::Button, - repeat_button: gtk::Button, - ) -> Self { - Self { - play_button, - seek_bar, - track_position, - track_duration, - next, - prev, - shuffle_button, - repeat_button, - } - } -} - -pub struct PlaybackControl { - model: Rc, - widget: PlaybackControlWidget, - _debouncer: Debouncer, - clock: Clock, -} - -const STEP: f64 = 5000.0; -impl PlaybackControl { - pub fn new(model: PlaybackControlModel, widget: PlaybackControlWidget) -> Self { - let model = Rc::new(model); - let debouncer = Debouncer::new(); - let debouncer_clone = debouncer.clone(); - let track_position = &widget.track_position; - widget.seek_bar.set_increments(STEP, STEP); - widget.seek_bar.connect_change_value( - clone!(@weak model, @weak track_position => @default-return signal::Inhibit(false), move |_, _, requested| { - track_position.set_text(&format_duration(requested)); - debouncer_clone.debounce(200, move || { - model.seek_to(requested as u32); - }); - signal::Inhibit(false) - }), - ); - - widget - .play_button - .connect_clicked(clone!(@weak model => move |_| { - model.toggle_playback(); - })); - - widget.next.connect_clicked(clone!(@weak model => move |_| { - model.play_next_song(); - })); - - widget.prev.connect_clicked(clone!(@weak model => move |_| { - model.play_prev_song(); - })); - - widget - .shuffle_button - .connect_clicked(clone!(@weak model => move |_| { - model.toggle_shuffle(); - })); - - widget - .repeat_button - .connect_clicked(clone!(@weak model => move |_| { - model.toggle_repeat(); - })); - - Self { - model, - widget, - _debouncer: debouncer, - clock: Clock::new(), - } - } - - fn set_playing(&self, is_playing: bool) { - let playback_image = if is_playing { - "media-playback-pause-symbolic" - } else { - "media-playback-start-symbolic" - }; - - self.widget - .play_button - .child() - .and_then(|child| child.downcast::().ok()) - .map(|image| { - image.set_from_icon_name(Some(playback_image), image.icon_size()); - }) - .expect("error updating icon"); - } - - fn update_repeat(&self, mode: &RepeatMode) { - let playback_image = match mode { - RepeatMode::Song => "media-playlist-repeat-song-symbolic.symbolic", - RepeatMode::Playlist => "media-playlist-repeat-symbolic", - RepeatMode::None => "media-playlist-consecutive-symbolic.symbolic", - }; - - self.widget - .repeat_button - .child() - .and_then(|child| child.downcast::().ok()) - .map(|image| { - image.set_from_icon_name(Some(playback_image), image.icon_size()); - }) - .expect("error updating icon"); - } - - fn update_playing(&self) { - let is_playing = self.model.is_playing(); - self.set_playing(is_playing); - - if is_playing { - let seek_bar = &self.widget.seek_bar; - let track_position = &self.widget.track_position; - self.clock - .start(clone!(@weak seek_bar, @weak track_position => move || { - let value = seek_bar.value() + 1000.0; - seek_bar.set_value(value); - track_position.set_text(&format_duration(value)); - })); - } else { - self.clock.stop(); - } - } - - fn update_current_info(&self) { - let class = "seek-bar--active"; - let style_context = self.widget.seek_bar.style_context(); - if let Some(duration) = self.model.current_song_duration() { - style_context.add_class(class); - self.widget.seek_bar.set_range(0.0, duration); - self.widget.seek_bar.set_value(0.0); - self.widget.track_position.set_text("0:00"); - self.widget - .track_duration - .set_text(&format!(" / {}", format_duration(duration))); - self.widget.track_position.show(); - self.widget.track_duration.show(); - } else { - style_context.remove_class(class); - self.widget.seek_bar.set_range(0.0, 0.0); - self.widget.track_position.hide(); - self.widget.track_duration.hide(); - } - } - - fn sync_seek(&self, pos: u32) { - let pos = pos as f64; - self.widget.seek_bar.set_value(pos); - self.widget.track_position.set_text(&format_duration(pos)); - } -} - -impl EventListener for PlaybackControl { - fn on_event(&mut self, event: &AppEvent) { - match event { - AppEvent::PlaybackEvent(PlaybackEvent::PlaybackPaused) - | AppEvent::PlaybackEvent(PlaybackEvent::PlaybackResumed) => { - self.update_playing(); - } - AppEvent::PlaybackEvent(PlaybackEvent::TrackChanged(_)) => { - self.update_current_info(); - } - AppEvent::PlaybackEvent(PlaybackEvent::RepeatModeChanged(mode)) => { - self.update_repeat(mode); - } - AppEvent::PlaybackEvent(PlaybackEvent::PlaybackStopped) => { - self.update_playing(); - self.update_current_info(); - } - AppEvent::PlaybackEvent(PlaybackEvent::SeekSynced(pos)) - | AppEvent::PlaybackEvent(PlaybackEvent::TrackSeeked(pos)) => { - self.sync_seek(*pos); - } - _ => {} - } - } -} diff --git a/src/app/components/playback/playback_controls.rs b/src/app/components/playback/playback_controls.rs new file mode 100644 index 00000000..47f4213e --- /dev/null +++ b/src/app/components/playback/playback_controls.rs @@ -0,0 +1,129 @@ +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::{glib, CompositeTemplate}; + +use crate::app::state::RepeatMode; + +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/playback_controls.ui")] + pub struct PlaybackControlsWidget { + #[template_child] + pub play_pause: TemplateChild, + + #[template_child] + pub next: TemplateChild, + + #[template_child] + pub prev: TemplateChild, + + #[template_child] + pub shuffle: TemplateChild, + + #[template_child] + pub repeat: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for PlaybackControlsWidget { + const NAME: &'static str = "PlaybackControlsWidget"; + type Type = super::PlaybackControlsWidget; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for PlaybackControlsWidget {} + impl WidgetImpl for PlaybackControlsWidget {} + impl BoxImpl for PlaybackControlsWidget {} +} + +glib::wrapper! { + pub struct PlaybackControlsWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; +} + +impl PlaybackControlsWidget { + pub fn set_playing(&self, is_playing: bool) { + let playback_icon = if is_playing { + "media-playback-pause-symbolic" + } else { + "media-playback-start-symbolic" + }; + + imp::PlaybackControlsWidget::from_instance(self) + .play_pause + .set_icon_name(playback_icon); + } + + pub fn set_shuffled(&self, shuffled: bool) { + imp::PlaybackControlsWidget::from_instance(self) + .shuffle + .set_active(shuffled); + } + + pub fn set_repeat_mode(&self, mode: RepeatMode) { + let repeat_mode_icon = match mode { + RepeatMode::Song => "media-playlist-repeat-song-symbolic", + RepeatMode::Playlist => "media-playlist-repeat-symbolic", + RepeatMode::None => "media-playlist-consecutive-symbolic", + }; + + imp::PlaybackControlsWidget::from_instance(self) + .repeat + .set_icon_name(repeat_mode_icon); + } + + pub fn connect_play_pause(&self, f: F) + where + F: Fn() + 'static, + { + imp::PlaybackControlsWidget::from_instance(self) + .play_pause + .connect_clicked(move |_| f()); + } + + pub fn connect_prev(&self, f: F) + where + F: Fn() + 'static, + { + imp::PlaybackControlsWidget::from_instance(self) + .prev + .connect_clicked(move |_| f()); + } + + pub fn connect_next(&self, f: F) + where + F: Fn() + 'static, + { + imp::PlaybackControlsWidget::from_instance(self) + .next + .connect_clicked(move |_| f()); + } + + pub fn connect_shuffle(&self, f: F) + where + F: Fn() + 'static, + { + imp::PlaybackControlsWidget::from_instance(self) + .shuffle + .connect_clicked(move |_| f()); + } + + pub fn connect_repeat(&self, f: F) + where + F: Fn() + 'static, + { + imp::PlaybackControlsWidget::from_instance(self) + .repeat + .connect_clicked(move |_| f()); + } +} diff --git a/src/app/components/playback/playback_controls.ui b/src/app/components/playback/playback_controls.ui new file mode 100644 index 00000000..7695944b --- /dev/null +++ b/src/app/components/playback/playback_controls.ui @@ -0,0 +1,59 @@ + + + + + + \ No newline at end of file diff --git a/src/app/components/playback/playback_info.rs b/src/app/components/playback/playback_info.rs index 54c613fc..a6d4dff6 100644 --- a/src/app/components/playback/playback_info.rs +++ b/src/app/components/playback/playback_info.rs @@ -1,107 +1,78 @@ -use gettextrs::*; +use gettextrs::gettext; use gtk::prelude::*; -use std::ops::Deref; -use std::rc::Rc; +use gtk::subclass::prelude::*; +use gtk::{glib, CompositeTemplate}; -use crate::app::components::EventListener; -use crate::app::dispatch::Worker; -use crate::app::loader::ImageLoader; -use crate::app::models::*; -use crate::app::state::{BrowserAction, PlaybackEvent, ScreenName}; -use crate::app::{ActionDispatcher, AppAction, AppEvent, AppModel}; +mod imp { -pub struct PlaybackInfoModel { - app_model: Rc, - dispatcher: Box, -} + use super::*; -impl PlaybackInfoModel { - pub fn new(app_model: Rc, dispatcher: Box) -> Self { - Self { - app_model, - dispatcher, - } - } + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/playback_info.ui")] + pub struct PlaybackInfoWidget { + #[template_child] + pub playing_image: TemplateChild, - fn current_song(&self) -> Option + '_> { - self.app_model.map_state_opt(|s| s.playback.current_song()) + #[template_child] + pub current_song_info: TemplateChild, } - fn go_home(&self) { - self.dispatcher.dispatch(AppAction::ViewNowPlaying); - self.dispatcher - .dispatch(BrowserAction::NavigationPopTo(ScreenName::Home).into()); + #[glib::object_subclass] + impl ObjectSubclass for PlaybackInfoWidget { + const NAME: &'static str = "PlaybackInfoWidget"; + type Type = super::PlaybackInfoWidget; + type ParentType = gtk::Button; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } } + + impl ObjectImpl for PlaybackInfoWidget {} + impl WidgetImpl for PlaybackInfoWidget {} + impl ButtonImpl for PlaybackInfoWidget {} } -pub struct PlaybackInfo { - model: Rc, - worker: Worker, - current_song_image: gtk::Image, - current_song_image_small: gtk::Image, - current_song_info: gtk::Label, +glib::wrapper! { + pub struct PlaybackInfoWidget(ObjectSubclass) @extends gtk::Widget, gtk::Button; } -impl PlaybackInfo { - pub fn new( - model: PlaybackInfoModel, - worker: Worker, - now_playing: gtk::Button, - now_playing_small: gtk::Button, - current_song_image: gtk::Image, - current_song_image_small: gtk::Image, - current_song_info: gtk::Label, - ) -> Self { - let model = Rc::new(model); - now_playing.connect_clicked(clone!(@weak model => move |_| model.go_home())); - now_playing_small.connect_clicked(clone!(@weak model => move |_| model.go_home())); - Self { - model, - worker, - current_song_image, - current_song_image_small, - current_song_info, - } +impl PlaybackInfoWidget { + pub fn set_title_and_artist(&self, title: &str, artist: &str) { + let widget = imp::PlaybackInfoWidget::from_instance(self); + let title = glib::markup_escape_text(title); + let artist = glib::markup_escape_text(artist); + let label = format!("{}\n{}", title.as_str(), artist.as_str()); + widget.current_song_info.set_label(&label[..]); } - fn update_current_info(&self) { - if let Some(song) = self.model.current_song() { - let title = glib::markup_escape_text(&song.title); - let artist = glib::markup_escape_text(&song.artists_name()); - let label = format!("{}\n{}", title.as_str(), artist.as_str()); - self.current_song_info.set_label(&label[..]); - - let image1 = self.current_song_image.downgrade(); - let image2 = self.current_song_image_small.downgrade(); + pub fn reset_info(&self) { + let widget = imp::PlaybackInfoWidget::from_instance(self); + widget + .current_song_info + // translators: Short text displayed instead of a song title when nothing plays + .set_label(&gettext("No song playing")); + widget + .playing_image + .set_from_icon_name(Some("emblem-music-symbolic")); + widget + .playing_image + .set_from_icon_name(Some("emblem-music-symbolic")); + } - if let Some(url) = song.art.clone() { - self.worker.send_local_task(async move { - let loader = ImageLoader::new(); - let result = loader.load_remote(&url, "jpg", 48, 48).await; - if let (Some(image1), Some(image2)) = (image1.upgrade(), image2.upgrade()) { - image1.set_from_pixbuf(result.as_ref()); - image2.set_from_pixbuf(result.as_ref()); - } - }); - } - } else { - self.current_song_info - // translators: Short text displayed instead of a song title when nothing plays - .set_label(&gettext("No song playing")); - self.current_song_image - .set_from_icon_name(Some("emblem-music-symbolic"), gtk::IconSize::Button); - self.current_song_image_small - .set_from_icon_name(Some("emblem-music-symbolic"), gtk::IconSize::Button); - } + pub fn set_info_visible(&self, visible: bool) { + imp::PlaybackInfoWidget::from_instance(self) + .current_song_info + .set_visible(visible); } -} -impl EventListener for PlaybackInfo { - fn on_event(&mut self, event: &AppEvent) { - if let AppEvent::PlaybackEvent(PlaybackEvent::TrackChanged(_)) - | AppEvent::PlaybackEvent(PlaybackEvent::PlaybackStopped) = event - { - self.update_current_info(); - } + pub fn set_artwork(&self, art: &gdk_pixbuf::Pixbuf) { + imp::PlaybackInfoWidget::from_instance(self) + .playing_image + .set_from_pixbuf(Some(art)); } } diff --git a/src/app/components/playback/playback_info.ui b/src/app/components/playback/playback_info.ui new file mode 100644 index 00000000..92d28ebc --- /dev/null +++ b/src/app/components/playback/playback_info.ui @@ -0,0 +1,41 @@ + + + + + + \ No newline at end of file diff --git a/src/app/components/playback/playback_widget.rs b/src/app/components/playback/playback_widget.rs new file mode 100644 index 00000000..ede4b525 --- /dev/null +++ b/src/app/components/playback/playback_widget.rs @@ -0,0 +1,238 @@ +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::{glib, CompositeTemplate}; + +use crate::app::components::screen_add_css_provider; +use crate::app::components::utils::{format_duration, Clock, Debouncer}; +use crate::app::loader::ImageLoader; +use crate::app::state::RepeatMode; +use crate::app::Worker; + +use super::playback_controls::PlaybackControlsWidget; +use super::playback_info::PlaybackInfoWidget; + +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/playback_widget.ui")] + pub struct PlaybackWidget { + #[template_child] + pub controls: TemplateChild, + + #[template_child] + pub controls_mobile: TemplateChild, + + #[template_child] + pub now_playing: TemplateChild, + + #[template_child] + pub now_playing_mobile: TemplateChild, + + #[template_child] + pub seek_bar: TemplateChild, + + #[template_child] + pub track_position: TemplateChild, + + #[template_child] + pub track_duration: TemplateChild, + + pub clock: Clock, + } + + #[glib::object_subclass] + impl ObjectSubclass for PlaybackWidget { + const NAME: &'static str = "PlaybackWidget"; + type Type = super::PlaybackWidget; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for PlaybackWidget { + fn constructed(&self, obj: &Self::Type) { + self.parent_constructed(obj); + self.now_playing_mobile.set_info_visible(false); + self.now_playing.set_info_visible(true); + screen_add_css_provider(resource!("/components/playback.css")); + } + } + + impl WidgetImpl for PlaybackWidget {} + impl BoxImpl for PlaybackWidget {} +} + +glib::wrapper! { + pub struct PlaybackWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; +} + +impl PlaybackWidget { + pub fn set_title_and_artist(&self, title: &str, artist: &str) { + let widget = imp::PlaybackWidget::from_instance(self); + widget.now_playing.set_title_and_artist(title, artist); + } + + pub fn reset_info(&self) { + let widget = imp::PlaybackWidget::from_instance(self); + widget.now_playing.reset_info(); + widget.now_playing_mobile.reset_info(); + self.set_song_duration(None); + } + + fn set_artwork(&self, image: &gdk_pixbuf::Pixbuf) { + let widget = imp::PlaybackWidget::from_instance(self); + widget.now_playing.set_artwork(image); + widget.now_playing_mobile.set_artwork(image); + } + + pub fn set_artwork_from_url(&self, url: String, worker: &Worker) { + let weak_self = self.downgrade(); + worker.send_local_task(async move { + let loader = ImageLoader::new(); + let result = loader.load_remote(&url, "jpg", 48, 48).await; + if let (Some(ref _self), Some(ref result)) = (weak_self.upgrade(), result) { + _self.set_artwork(result); + } + }); + } + + pub fn set_song_duration(&self, duration: Option) { + let widget = imp::PlaybackWidget::from_instance(self); + let class = "seek-bar--active"; + let style_context = widget.seek_bar.style_context(); + if let Some(duration) = duration { + style_context.add_class(class); + widget.seek_bar.set_range(0.0, duration); + widget.seek_bar.set_value(0.0); + widget.track_position.set_text("0:00"); + widget + .track_duration + .set_text(&format!(" / {}", format_duration(duration))); + widget.track_position.show(); + widget.track_duration.show(); + } else { + style_context.remove_class(class); + widget.seek_bar.set_range(0.0, 0.0); + widget.track_position.hide(); + widget.track_duration.hide(); + } + } + + pub fn set_seek_position(&self, pos: f64) { + let widget = imp::PlaybackWidget::from_instance(self); + widget.seek_bar.set_value(pos); + widget.track_position.set_text(&format_duration(pos)); + } + + pub fn increment_seek_position(&self) { + let value = imp::PlaybackWidget::from_instance(self).seek_bar.value() + 1_000.0; + self.set_seek_position(value); + } + + pub fn connect_now_playing_clicked(&self, f: F) + where + F: Fn() + Clone + 'static, + { + let widget = imp::PlaybackWidget::from_instance(self); + let f_clone = f.clone(); + widget.now_playing.connect_clicked(move |_| f_clone()); + widget.now_playing_mobile.connect_clicked(move |_| f()); + } + + pub fn connect_seek(&self, seek: Seek) + where + Seek: Fn(u32) + Clone + 'static, + { + let debouncer = Debouncer::new(); + let widget = imp::PlaybackWidget::from_instance(self); + widget.seek_bar.set_increments(5_000.0, 10_000.0); + widget.seek_bar.connect_change_value( + clone!(@weak self as _self => @default-return glib::signal::Inhibit(false), move |_, _, requested| { + imp::PlaybackWidget::from_instance(&_self) + .track_position + .set_text(&format_duration(requested)); + let seek = seek.clone(); + debouncer.debounce(200, move || seek(requested as u32)); + glib::signal::Inhibit(false) + }), + ); + } + + pub fn set_playing(&self, is_playing: bool) { + let widget = imp::PlaybackWidget::from_instance(self); + widget.controls.set_playing(is_playing); + widget.controls_mobile.set_playing(is_playing); + if is_playing { + widget + .clock + .start(clone!(@weak self as _self => move || _self.increment_seek_position())); + } else { + widget.clock.stop(); + } + } + + pub fn set_repeat_mode(&self, mode: RepeatMode) { + let widget = imp::PlaybackWidget::from_instance(self); + widget.controls.set_repeat_mode(mode); + widget.controls_mobile.set_repeat_mode(mode); + } + + pub fn set_shuffled(&self, shuffled: bool) { + let widget = imp::PlaybackWidget::from_instance(self); + widget.controls.set_shuffled(shuffled); + widget.controls_mobile.set_shuffled(shuffled); + } + + pub fn connect_play_pause(&self, f: F) + where + F: Fn() + Clone + 'static, + { + let widget = imp::PlaybackWidget::from_instance(self); + widget.controls.connect_play_pause(f.clone()); + widget.controls_mobile.connect_play_pause(f); + } + + pub fn connect_prev(&self, f: F) + where + F: Fn() + Clone + 'static, + { + let widget = imp::PlaybackWidget::from_instance(self); + widget.controls.connect_prev(f.clone()); + widget.controls_mobile.connect_prev(f); + } + + pub fn connect_next(&self, f: F) + where + F: Fn() + Clone + 'static, + { + let widget = imp::PlaybackWidget::from_instance(self); + widget.controls.connect_next(f.clone()); + widget.controls_mobile.connect_next(f); + } + + pub fn connect_shuffle(&self, f: F) + where + F: Fn() + Clone + 'static, + { + let widget = imp::PlaybackWidget::from_instance(self); + widget.controls.connect_shuffle(f.clone()); + widget.controls_mobile.connect_shuffle(f); + } + + pub fn connect_repeat(&self, f: F) + where + F: Fn() + Clone + 'static, + { + let widget = imp::PlaybackWidget::from_instance(self); + widget.controls.connect_repeat(f.clone()); + widget.controls_mobile.connect_repeat(f); + } +} diff --git a/src/app/components/playback/playback_widget.ui b/src/app/components/playback/playback_widget.ui new file mode 100644 index 00000000..c02501dd --- /dev/null +++ b/src/app/components/playback/playback_widget.ui @@ -0,0 +1,108 @@ + + + + + + \ No newline at end of file diff --git a/src/app/components/playlist/playlist.rs b/src/app/components/playlist/playlist.rs index 857223d5..cdaccc08 100644 --- a/src/app/components/playlist/playlist.rs +++ b/src/app/components/playlist/playlist.rs @@ -3,8 +3,8 @@ use gtk::prelude::*; use std::ops::Deref; use std::rc::Rc; -use crate::app::components::utils::{in_viewport, vscroll_to, AnimatorDefault}; -use crate::app::components::{Component, EventListener, Song}; +use crate::app::components::utils::AnimatorDefault; +use crate::app::components::{Component, EventListener, SongWidget}; use crate::app::models::SongModel; use crate::app::{ state::{PlaybackEvent, SelectionEvent, SelectionState}, @@ -51,36 +51,55 @@ pub trait PlaylistModel { } pub struct Playlist { - listbox: gtk::ListBox, + animator: AnimatorDefault, + listview: gtk::ListView, _press_gesture: gtk::GestureLongPress, list_model: ListStore, model: Rc, - animator: AnimatorDefault, } impl Playlist where Model: PlaylistModel + 'static, { - pub fn new(listbox: gtk::ListBox, model: Rc) -> Self { + pub fn new(listview: gtk::ListView, model: Rc) -> Self { let list_model = ListStore::new(); + let selection_model = gtk::NoSelection::new(Some(list_model.unsafe_store())); + let factory = gtk::SignalListItemFactory::new(); - Self::set_selection_active(&listbox, model.is_selection_enabled()); - listbox.style_context().add_class("playlist"); - listbox.set_activate_on_single_click(true); + listview.set_factory(Some(&factory)); + listview.style_context().add_class("playlist"); + listview.set_single_click_activate(true); + listview.set_model(Some(&selection_model)); + Self::set_selection_active(&listview, model.is_selection_enabled()); - let press_gesture = gtk::GestureLongPress::new(&listbox); - listbox.add_events(gdk::EventMask::TOUCH_MASK); - press_gesture.set_touch_only(false); - press_gesture.set_propagation_phase(gtk::PropagationPhase::Capture); - press_gesture.connect_pressed(clone!(@weak model => move |_, _, _| { - model.enable_selection(); + factory.connect_setup(|_, item| { + item.set_child(Some(&SongWidget::new())); + }); + + factory.connect_bind(clone!(@weak model => move |_, item| { + let song_model = item.item().unwrap().downcast::().unwrap(); + song_model.set_state(Self::get_item_state(&*model, &song_model)); + + let widget = item.child().unwrap().downcast::().unwrap(); + widget.bind(&song_model); + + let id = &song_model.get_id(); + widget.set_actions(model.actions_for(id).as_ref()); + widget.set_menu(model.menu_for(id).as_ref()); })); - let list_model_clone = list_model.clone(); - listbox.connect_row_activated(clone!(@weak model => move |_, row| { - let index = row.index() as u32; - let song: SongModel = list_model_clone.get(index); + factory.connect_unbind(|_, item| { + let song_model = item.item().unwrap().downcast::().unwrap(); + song_model.unbind_all(); + + let widget = item.child().unwrap().downcast::().unwrap(); + widget.set_actions(None); + widget.set_menu(None); + }); + + listview.connect_activate(clone!(@weak list_model, @weak model => move |_, position| { + let song = list_model.get(position); let selection_enabled = model.is_selection_enabled(); if selection_enabled { Self::select_song(&*model, &song); @@ -89,36 +108,20 @@ where } })); - listbox.bind_model( - Some(list_model.unsafe_store()), - clone!(@weak model, @weak listbox => @default-panic, move |item| { - let item = item.downcast_ref::().unwrap(); - let id = &item.get_id(); - - let row = gtk::ListBoxRow::new(); - let event_box = gtk::EventBox::new(); - row.add(&event_box); - - let song = Song::new(item.clone()); - event_box.add(song.get_root_widget()); - - song.set_menu(model.menu_for(id).as_ref()); - song.set_actions(model.actions_for(id).as_ref()); - - Self::set_row_state(&listbox, item, &row, Self::get_row_state(item, &*model, None)); - Self::connect_events(item, &row, model); - - row.show_all(); - row.upcast::() - }), - ); + let press_gesture = gtk::GestureLongPress::new(); + listview.add_controller(&press_gesture); + press_gesture.set_touch_only(false); + press_gesture.set_propagation_phase(gtk::PropagationPhase::Capture); + press_gesture.connect_pressed(clone!(@weak model => move |_, _, _| { + model.enable_selection(); + })); Self { - listbox, + animator: AnimatorDefault::ease_in_out_animator(), + listview, _press_gesture: press_gesture, list_model, model, - animator: AnimatorDefault::ease_in_out_animator(), } } @@ -132,29 +135,9 @@ where } } - fn connect_events(item: &SongModel, row: >k::ListBoxRow, model: Rc) { - row.connect_button_release_event( - clone!(@weak model, @strong item => @default-return Inhibit(false), move |_, event| { - if event.button() == 3 && model.enable_selection() { - Self::select_song(&*model, &item); - Inhibit(true) - } else { - Inhibit(false) - } - }), - ); - } - - fn get_row_state( - item: &SongModel, - model: &Model, - current_song_id: Option<&String>, - ) -> RowState { + fn get_item_state(model: &Model, item: &SongModel) -> RowState { let id = &item.get_id(); - let is_playing = current_song_id - .map(|s| s.eq(id)) - .or_else(|| Some(model.current_song_id()?.eq(id))) - .unwrap_or(false); + let is_playing = model.current_song_id().map(|s| s.eq(id)).unwrap_or(false); let is_selected = model .selection() .map(|s| s.is_song_selected(id)) @@ -165,56 +148,59 @@ where } } - fn set_row_state( - listbox: >k::ListBox, - item: &SongModel, - row: >k::ListBoxRow, - state: RowState, - ) { - item.set_playing(state.is_playing); - item.set_selected(state.is_selected); - if state.is_selected { - row.set_selectable(true); - listbox.select_row(Some(row)); - } else { - row.set_selectable(false); + fn autoscroll_to_playing(&self, index: usize) { + let len = self.list_model.len() as f64; + let adj = self + .listview + .parent() + .and_then(|p| p.downcast::().ok()) + .and_then(|w| w.vadjustment()); + if let Some(adj) = adj { + let v = adj.value(); + let pos = (index as f64) * adj.upper() / len; + if pos < v || pos > v + 0.9 * adj.page_size() { + self.animator.animate( + 20, + clone!(@weak adj => @default-return false, move |p| { + let v = adj.value(); + adj.set_value(v + p * (pos - v)); + true + }), + ); + } } } - fn rows_and_songs(&self) -> impl Iterator + '_ { - let listbox = &self.listbox; - self.list_model - .iter() - .enumerate() - .filter_map(move |(i, song)| listbox.row_at_index(i as i32).map(|r| (r, song))) - } - - fn update_list(&self, scroll: bool) { - let autoscroll = scroll && self.model.autoscroll_to_playing(); - let current_song_id = self.model.current_song_id(); - for (row, model_song) in self.rows_and_songs() { - let state = Self::get_row_state(&model_song, &*self.model, current_song_id.as_ref()); - Self::set_row_state(&self.listbox, &model_song, &row, state); - - if state.is_playing && autoscroll && !in_viewport(row.upcast_ref()).unwrap_or(true) { - self.animator - .animate(20, move |p| vscroll_to(row.upcast_ref(), p).is_some()); + fn update_list(&self) { + for (i, model_song) in self.list_model.iter().enumerate() { + let state = Self::get_item_state(&*self.model, &model_song); + model_song.set_state(state); + if state.is_playing + && self.model.autoscroll_to_playing() + && !self.model.is_selection_enabled() + { + self.autoscroll_to_playing(i); } } } - fn set_selection_active(listbox: >k::ListBox, active: bool) { - let context = listbox.style_context(); + fn set_selection_active(listview: >k::ListView, active: bool) { + let context = listview.style_context(); if active { context.add_class("playlist--selectable"); - listbox.set_selection_mode(gtk::SelectionMode::Multiple); } else { context.remove_class("playlist--selectable"); - listbox.set_selection_mode(gtk::SelectionMode::None); } } } +impl SongModel { + fn set_state(&self, state: RowState) { + self.set_playing(state.is_playing); + self.set_selected(state.is_selected); + } +} + impl EventListener for Playlist where Model: PlaylistModel + 'static, @@ -225,14 +211,14 @@ where } else { match event { AppEvent::SelectionEvent(SelectionEvent::SelectionChanged) => { - self.update_list(false); + self.update_list(); } AppEvent::PlaybackEvent(PlaybackEvent::TrackChanged(_)) => { - self.update_list(true); + self.update_list(); } AppEvent::SelectionEvent(SelectionEvent::SelectionModeChanged(_)) => { - Self::set_selection_active(&self.listbox, self.model.is_selection_enabled()); - self.update_list(false); + Self::set_selection_active(&self.listview, self.model.is_selection_enabled()); + self.update_list(); } _ => {} } @@ -242,6 +228,6 @@ where impl Component for Playlist { fn get_root_widget(&self) -> >k::Widget { - self.listbox.upcast_ref() + self.listview.upcast_ref() } } diff --git a/src/app/components/playlist/song.css b/src/app/components/playlist/song.css index c92727a0..9a8a11e6 100644 --- a/src/app/components/playlist/song.css +++ b/src/app/components/playlist/song.css @@ -19,12 +19,13 @@ opacity: 1; } -.playlist--selectable .song__index { +.playlist--selectable .song__index, .playlist--selectable .song__icon { opacity: 0; } -.playlist--selectable .song__checkbox { +.playlist--selectable .song__checkbox, .playlist--selectable .song__checkbox check { opacity: 1; + filter: none; } .song__title { @@ -32,6 +33,7 @@ } row { + transition: background-color 150ms ease; margin: 1px 0; } @@ -49,7 +51,7 @@ row { } .song__menu { - opacity: 0 + opacity: 0; } .song__menu--enabled { @@ -58,4 +60,18 @@ row { row:hover .song__menu--enabled, .song__menu--enabled:checked { opacity: 1; +} + +.song check { + filter: opacity(1); +} + +/* copied from adwaita */ + +.playlist row:hover { + background-color: alpha(currentColor, 0.07); +} + +.playlist row:active { + background-color: alpha(currentColor, 0.16); } \ No newline at end of file diff --git a/src/app/components/playlist/song.rs b/src/app/components/playlist/song.rs index c8666926..fafbf9d4 100644 --- a/src/app/components/playlist/song.rs +++ b/src/app/components/playlist/song.rs @@ -1,97 +1,135 @@ -use crate::app::components::{screen_add_css_provider, Component}; +use crate::app::components::screen_add_css_provider; use crate::app::models::SongModel; + use gio::MenuModel; -use gladis::Gladis; use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; -#[derive(Gladis, Clone)] -struct SongWidget { - root: gtk::Widget, - song_index: gtk::Label, - song_icon: gtk::Image, - song_checkbox: gtk::CheckButton, - song_title: gtk::Label, - song_artist: gtk::Label, - song_length: gtk::Label, - menu_btn: gtk::MenuButton, -} +mod imp { + use super::*; -impl SongWidget { - pub fn new() -> Self { - screen_add_css_provider(resource!("/components/song.css")); - Self::from_resource(resource!("/components/song.ui")).unwrap() + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/song.ui")] + pub struct SongWidget { + #[template_child] + pub song_index: TemplateChild, + + #[template_child] + pub song_icon: TemplateChild, + + #[template_child] + pub song_checkbox: TemplateChild, + + #[template_child] + pub song_title: TemplateChild, + + #[template_child] + pub song_artist: TemplateChild, + + #[template_child] + pub song_length: TemplateChild, + + #[template_child] + pub menu_btn: TemplateChild, } - fn set_playing(widget: >k::Widget, is_playing: bool) { - let song_class = "song--playing"; - let context = widget.style_context(); - if is_playing { - context.add_class(song_class); - } else { - context.remove_class(song_class); + #[glib::object_subclass] + impl ObjectSubclass for SongWidget { + const NAME: &'static str = "SongWidget"; + type Type = super::SongWidget; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); } } -} -pub struct Song { - widget: SongWidget, -} + impl ObjectImpl for SongWidget { + fn constructed(&self, obj: &Self::Type) { + self.parent_constructed(obj); + self.song_checkbox.set_sensitive(false); + } + } -impl Song { - pub fn new(model: SongModel) -> Self { - let widget = SongWidget::new(); - - model - .bind_property("index", &widget.song_index, "label") - .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) - .build(); - - model - .bind_property("title", &widget.song_title, "label") - .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) - .build(); - - model - .bind_property("artist", &widget.song_artist, "label") - .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) - .build(); - model - .bind_property("duration", &widget.song_length, "label") - .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) - .build(); - - SongWidget::set_playing(&widget.root, model.get_playing()); - - model.connect_playing_local(clone!(@weak widget.root as root => move |song| { - SongWidget::set_playing(&root, song.get_playing()); - })); + impl WidgetImpl for SongWidget {} + impl BoxImpl for SongWidget {} +} - model.connect_selected_local( - clone!(@weak widget.song_checkbox as checkbox => move |song| { - checkbox.set_active(song.get_selected()); - }), - ); +glib::wrapper! { + pub struct SongWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; +} - widget.song_checkbox.set_sensitive(false); +impl SongWidget { + pub fn new() -> Self { + screen_add_css_provider(resource!("/components/song.css")); + glib::Object::new(&[]).expect("Failed to create an instance of SongWidget") + } - Self { widget } + pub fn for_model(model: SongModel) -> Self { + let _self = Self::new(); + _self.bind(&model); + _self } pub fn set_actions(&self, actions: Option<&gio::ActionGroup>) { - self.get_root_widget().insert_action_group("song", actions); + self.insert_action_group("song", actions); } pub fn set_menu(&self, menu: Option<&MenuModel>) { if menu.is_some() { - let menu_btn = &self.widget.menu_btn; - menu_btn.set_menu_model(menu); - menu_btn.style_context().add_class("song__menu--enabled"); + let widget = imp::SongWidget::from_instance(self); + widget.menu_btn.set_menu_model(menu); + widget + .menu_btn + .style_context() + .add_class("song__menu--enabled"); } } -} -impl Component for Song { - fn get_root_widget(&self) -> >k::Widget { - &self.widget.root + fn set_playing(&self, is_playing: bool) { + let song_class = "song--playing"; + let context = self.style_context(); + if is_playing { + context.add_class(song_class); + } else { + context.remove_class(song_class); + } + } + + fn set_selected(&self, is_selected: bool) { + imp::SongWidget::from_instance(self) + .song_checkbox + .set_active(is_selected); + let song_class = "song-selected"; + let context = self.style_context(); + if is_selected { + context.add_class(song_class); + } else { + context.remove_class(song_class); + } + } + + pub fn bind(&self, model: &SongModel) { + let widget = imp::SongWidget::from_instance(self); + + model.bind_index(&*widget.song_index, "label"); + model.bind_title(&*widget.song_title, "label"); + model.bind_artist(&*widget.song_artist, "label"); + model.bind_duration(&*widget.song_length, "label"); + + self.set_playing(model.get_playing()); + model.connect_playing_local(clone!(@weak self as _self => move |song| { + _self.set_playing(song.get_playing()); + })); + + self.set_selected(model.get_selected()); + model.connect_selected_local(clone!(@weak self as _self => move |song| { + _self.set_selected(song.get_selected()); + })); } } diff --git a/src/app/components/playlist/song.ui b/src/app/components/playlist/song.ui index a63a00cb..97f8bb6d 100644 --- a/src/app/components/playlist/song.ui +++ b/src/app/components/playlist/song.ui @@ -1,12 +1,8 @@ - - - - + + diff --git a/src/app/components/playlist/song_actions.rs b/src/app/components/playlist/song_actions.rs index 935dab2a..c5a45ee6 100644 --- a/src/app/components/playlist/song_actions.rs +++ b/src/app/components/playlist/song_actions.rs @@ -1,6 +1,5 @@ -use gdk::SELECTION_CLIPBOARD; +use gdk::prelude::*; use gio::SimpleAction; -use gtk::Clipboard; use crate::app::models::SongDescription; use crate::app::state::{AppAction, PlaybackAction}; @@ -37,8 +36,11 @@ impl SongDescription { let track_id = self.id.clone(); let copy_link = SimpleAction::new(name.unwrap_or("copy_link"), None); copy_link.connect_activate(move |_, _| { - let clipboard = Clipboard::get(&SELECTION_CLIPBOARD); - clipboard.set_text(&format!("https://open.spotify.com/track/{}", &track_id)); + let link = format!("https://open.spotify.com/track/{}", &track_id); + let clipboard = gdk::Display::default().unwrap().clipboard(); + clipboard + .set_content(Some(&gdk::ContentProvider::for_value(&link.to_value()))) + .expect("Failed to set clipboard content"); }); copy_link } diff --git a/src/app/components/playlist_details/playlist_details.css b/src/app/components/playlist_details/playlist_details.css index 001a7f76..9919ab2e 100644 --- a/src/app/components/playlist_details/playlist_details.css +++ b/src/app/components/playlist_details/playlist_details.css @@ -25,11 +25,11 @@ opacity: 1; } -list.playlist_details__songs { +listview.playlist_details__songs { padding: 8px; border-radius: 8px; } -list.playlist_details__songs row { +listview.playlist_details__songs row { border-radius: 4px; -} +} \ No newline at end of file diff --git a/src/app/components/playlist_details/playlist_details.rs b/src/app/components/playlist_details/playlist_details.rs index aeead598..c3b70796 100644 --- a/src/app/components/playlist_details/playlist_details.rs +++ b/src/app/components/playlist_details/playlist_details.rs @@ -1,5 +1,6 @@ -use gladis::Gladis; use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; use std::rc::Rc; use super::PlaylistDetailsModel; @@ -9,26 +10,152 @@ use crate::app::dispatch::Worker; use crate::app::loader::ImageLoader; use crate::app::{AppEvent, BrowserEvent}; -#[derive(Gladis, Clone)] -struct PlaylistDetailsWidget { - pub root: gtk::ScrolledWindow, - pub name_label: gtk::Label, - pub owner_button: gtk::LinkButton, - pub owner_button_label: gtk::Label, - pub tracks: gtk::ListBox, - pub art: gtk::Image, +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/playlist_details.ui")] + pub struct PlaylistDetailsWidget { + #[template_child] + pub scrolled_window: TemplateChild, + + #[template_child] + pub header_revealer: TemplateChild, + + #[template_child] + pub name_label: TemplateChild, + + #[template_child] + pub owner_button: TemplateChild, + + #[template_child] + pub owner_button_label: TemplateChild, + + #[template_child] + pub tracks: TemplateChild, + + #[template_child] + pub art: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for PlaylistDetailsWidget { + const NAME: &'static str = "PlaylistDetailsWidget"; + type Type = super::PlaylistDetailsWidget; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for PlaylistDetailsWidget {} + impl WidgetImpl for PlaylistDetailsWidget {} + impl BoxImpl for PlaylistDetailsWidget {} +} + +glib::wrapper! { + pub struct PlaylistDetailsWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; } impl PlaylistDetailsWidget { fn new() -> Self { screen_add_css_provider(resource!("/components/playlist_details.css")); - Self::from_resource(resource!("/components/playlist_details.ui")).unwrap() + glib::Object::new(&[]).expect("Failed to create an instance of PlaylistDetailsWidget") + } + + fn widget(&self) -> &imp::PlaylistDetailsWidget { + imp::PlaylistDetailsWidget::from_instance(self) + } + + fn playlist_tracks_widget(&self) -> >k::ListView { + self.widget().tracks.as_ref() + } + + fn connect_owner_clicked(&self, f: F) + where + F: Fn() + 'static, + { + self.widget().owner_button.connect_activate_link(move |_| { + f(); + glib::signal::Inhibit(true) + }); + } + + fn connect_bottom_edge(&self, f: F) + where + F: Fn() + 'static, + { + self.widget() + .scrolled_window + .connect_edge_reached(move |_, pos| { + if let gtk::PositionType::Bottom = pos { + f() + } + }); + } + + fn set_header_visible(&self, visible: bool) -> bool { + let widget = self.widget(); + let is_up_to_date = widget.header_revealer.reveals_child() == visible; + if !is_up_to_date { + widget.header_revealer.set_reveal_child(visible); + } + is_up_to_date + } + + fn connect_header(&self) { + self.set_header_visible(true); + + let scroll_controller = + gtk::EventControllerScroll::new(gtk::EventControllerScrollFlags::VERTICAL); + scroll_controller.connect_scroll( + clone!(@weak self as _self => @default-return gtk::Inhibit(false), move |_, _, dy| { + gtk::Inhibit(!_self.set_header_visible(dy < 0f64)) + }), + ); + + let widget = self.widget(); + widget.scrolled_window.add_controller(&scroll_controller); } fn set_loaded(&self) { - let context = self.root.style_context(); + let context = self.style_context(); context.add_class("playlist_details--loaded"); } + + fn set_name_and_owner( + &self, + name: &str, + owner: &str, + art_url: Option<&String>, + worker: &Worker, + ) { + let widget = self.widget(); + + widget.name_label.set_label(name); + widget.owner_button_label.set_label(owner); + + let weak_self = self.downgrade(); + if let Some(art_url) = art_url.cloned() { + worker.send_local_task(async move { + if let Some(_self) = weak_self.upgrade() { + let pixbuf = ImageLoader::new() + .load_remote(&art_url[..], "jpg", 100, 100) + .await; + _self.widget().art.set_from_pixbuf(pixbuf.as_ref()); + _self.set_loaded(); + } + }); + } else { + self.set_loaded(); + } + } } pub struct PlaylistDetails { @@ -44,22 +171,20 @@ impl PlaylistDetails { model.load_playlist_info(); } let widget = PlaylistDetailsWidget::new(); - let playlist = Box::new(Playlist::new(widget.tracks.clone(), model.clone())); + let playlist = Box::new(Playlist::new( + widget.playlist_tracks_widget().clone(), + model.clone(), + )); - widget - .root - .connect_edge_reached(clone!(@weak model => move |_, pos| { - if let gtk::PositionType::Bottom = pos { - model.load_more_tracks(); - } - })); + widget.connect_header(); - widget.owner_button.connect_activate_link( - clone!(@weak model => @default-return glib::signal::Inhibit(false), move |_| { - model.view_owner(); - glib::signal::Inhibit(true) - }), - ); + widget.connect_bottom_edge(clone!(@weak model => move || { + model.load_more_tracks(); + })); + + widget.connect_owner_clicked(clone!(@weak model => move || { + model.view_owner(); + })); Self { model, @@ -73,29 +198,17 @@ impl PlaylistDetails { if let Some(info) = self.model.get_playlist_info() { let title = &info.title[..]; let owner = &info.owner.display_name[..]; + let art_url = info.art.as_ref(); - self.widget.name_label.set_label(title); - self.widget.owner_button_label.set_label(owner); - - let widget = self.widget.clone(); - if let Some(art) = info.art.clone() { - self.worker.send_local_task(async move { - let pixbuf = ImageLoader::new() - .load_remote(&art[..], "jpg", 100, 100) - .await; - widget.art.set_from_pixbuf(pixbuf.as_ref()); - widget.set_loaded(); - }); - } else { - widget.set_loaded(); - } + self.widget + .set_name_and_owner(title, owner, art_url, &self.worker); } } } impl Component for PlaylistDetails { fn get_root_widget(&self) -> >k::Widget { - self.widget.root.upcast_ref() + self.widget.upcast_ref() } fn get_children(&mut self) -> Option<&mut Vec>> { diff --git a/src/app/components/playlist_details/playlist_details.ui b/src/app/components/playlist_details/playlist_details.ui index b91085be..4caef178 100644 --- a/src/app/components/playlist_details/playlist_details.ui +++ b/src/app/components/playlist_details/playlist_details.ui @@ -1,100 +1,70 @@ - - - - - - True - True - never - True + + + diff --git a/src/app/components/playlist_details/playlist_details_model.rs b/src/app/components/playlist_details/playlist_details_model.rs index 56fb9a00..9cfc6901 100644 --- a/src/app/components/playlist_details/playlist_details_model.rs +++ b/src/app/components/playlist_details/playlist_details_model.rs @@ -188,7 +188,11 @@ impl PlaylistModel for PlaylistDetailsModel { menu.append(Some(&*labels::VIEW_ALBUM), Some("song.view_album")); for artist in song.artists.iter() { menu.append( - Some(&format!("{} {}", *labels::MORE_FROM, artist.name)), + Some(&format!( + "{} {}", + *labels::MORE_FROM, + glib::markup_escape_text(&artist.name) + )), Some(&format!("song.view_artist_{}", artist.id)), ); } @@ -284,7 +288,6 @@ impl PlaylistDetailsModel { let id = playlist.to_string(); let uris: Vec = selection .peek_selection() - .iter() .map(|s| &s.uri) .cloned() .collect(); diff --git a/src/app/components/saved_playlists/saved_playlists.rs b/src/app/components/saved_playlists/saved_playlists.rs index 5db511e2..34b89c02 100644 --- a/src/app/components/saved_playlists/saved_playlists.rs +++ b/src/app/components/saved_playlists/saved_playlists.rs @@ -1,28 +1,92 @@ -use gladis::Gladis; use gtk::prelude::*; - -use std::rc::{Rc, Weak}; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; +use std::rc::Rc; use super::SavedPlaylistsModel; -use crate::app::components::{Album, Component, EventListener}; +use crate::app::components::{AlbumWidget, Component, EventListener}; use crate::app::dispatch::Worker; use crate::app::models::AlbumModel; use crate::app::state::LoginEvent; -use crate::app::AppEvent; +use crate::app::{AppEvent, ListStore}; + +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/saved_playlists.ui")] + pub struct SavedPlaylistsWidget { + #[template_child] + pub scrolled_window: TemplateChild, + + #[template_child] + pub flowbox: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for SavedPlaylistsWidget { + const NAME: &'static str = "SavedPlaylistsWidget"; + type Type = super::SavedPlaylistsWidget; + type ParentType = gtk::Box; -#[derive(Clone, Gladis)] -struct SavedPlaylistsWidget { - pub scrolled_window: gtk::ScrolledWindow, - pub flowbox: gtk::FlowBox, + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for SavedPlaylistsWidget {} + impl WidgetImpl for SavedPlaylistsWidget {} + impl BoxImpl for SavedPlaylistsWidget {} +} + +glib::wrapper! { + pub struct SavedPlaylistsWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; } impl SavedPlaylistsWidget { - fn new() -> Self { - Self::from_resource(resource!("/components/saved_playlists.ui")).unwrap() + pub fn new() -> Self { + glib::Object::new(&[]).expect("Failed to create an instance of SavedPlaylistsWidget") + } + + fn connect_bottom_edge(&self, f: F) + where + F: Fn() + 'static, + { + imp::SavedPlaylistsWidget::from_instance(self) + .scrolled_window + .connect_edge_reached(move |_, pos| { + if let gtk::PositionType::Bottom = pos { + f() + } + }); } - fn root(&self) -> >k::Widget { - self.scrolled_window.upcast_ref() + fn bind_albums(&self, worker: Worker, store: &ListStore, on_album_pressed: F) + where + F: Fn(&String) + Clone + 'static, + { + imp::SavedPlaylistsWidget::from_instance(self) + .flowbox + .bind_model(Some(store.unsafe_store()), move |item| { + let album_model = item.downcast_ref::().unwrap(); + let child = gtk::FlowBoxChild::new(); + let album = AlbumWidget::for_model(album_model, worker.clone()); + + let f = on_album_pressed.clone(); + album.connect_album_pressed(clone!(@weak album_model => move |_| { + if let Some(id) = album_model.uri().as_ref() { + f(id); + } + })); + + child.set_child(Some(&album)); + child.upcast::() + }); } } @@ -38,12 +102,9 @@ impl SavedPlaylists { let widget = SavedPlaylistsWidget::new(); - let weak_model = Rc::downgrade(&model); - widget.scrolled_window.connect_edge_reached(move |_, pos| { - if let (gtk::PositionType::Bottom, Some(model)) = (pos, weak_model.upgrade()) { - let _ = model.load_more_playlists(); - } - }); + widget.connect_bottom_edge(clone!(@weak model => move || { + model.load_more_playlists(); + })); Self { widget, @@ -52,16 +113,14 @@ impl SavedPlaylists { } } - fn bind_flowbox(&self, store: &gio::ListStore) { - let weak_model = Rc::downgrade(&self.model); - let worker_clone = self.worker.clone(); - - self.widget.flowbox.bind_model(Some(store), move |item| { - let item = item.downcast_ref::().unwrap(); - let child = create_album_for(item, worker_clone.clone(), weak_model.clone()); - child.show_all(); - child.upcast::() - }); + fn bind_flowbox(&self) { + self.widget.bind_albums( + self.worker.clone(), + &*self.model.get_list_store().unwrap(), + clone!(@weak self.model as model => move |id| { + model.open_playlist(id.clone()); + }), + ); } } @@ -70,7 +129,7 @@ impl EventListener for SavedPlaylists { match event { AppEvent::Started => { let _ = self.model.refresh_saved_playlists(); - self.bind_flowbox(self.model.get_list_store().unwrap().unsafe_store()) + self.bind_flowbox(); } AppEvent::LoginEvent(LoginEvent::LoginCompleted(_)) => { let _ = self.model.refresh_saved_playlists(); @@ -82,25 +141,6 @@ impl EventListener for SavedPlaylists { impl Component for SavedPlaylists { fn get_root_widget(&self) -> >k::Widget { - self.widget.root() + self.widget.as_ref() } } - -fn create_album_for( - album_model: &AlbumModel, - worker: Worker, - model: Weak, -) -> gtk::FlowBoxChild { - let child = gtk::FlowBoxChild::new(); - - let album = Album::new(album_model, worker); - child.add(album.get_root_widget()); - - album.connect_album_pressed(move |a| { - if let (Some(model), Some(id)) = (model.upgrade(), a.uri()) { - model.open_playlist(id); - } - }); - - child -} diff --git a/src/app/components/saved_playlists/saved_playlists.ui b/src/app/components/saved_playlists/saved_playlists.ui index aaa166e7..6d56ad4f 100644 --- a/src/app/components/saved_playlists/saved_playlists.ui +++ b/src/app/components/saved_playlists/saved_playlists.ui @@ -1,30 +1,25 @@ - - - - True - True - always - 250 + + diff --git a/src/app/components/search/search.rs b/src/app/components/search/search.rs index 07748276..81dd0913 100644 --- a/src/app/components/search/search.rs +++ b/src/app/components/search/search.rs @@ -1,28 +1,108 @@ use gettextrs::*; -use gio::prelude::*; -use gladis::Gladis; use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; use std::rc::Rc; use crate::app::components::utils::{wrap_flowbox_item, Debouncer}; -use crate::app::components::{Album, Artist, Component, EventListener}; +use crate::app::components::{AlbumWidget, ArtistWidget, Component, EventListener}; use crate::app::dispatch::Worker; use crate::app::models::{AlbumModel, ArtistModel}; use crate::app::state::{AppEvent, BrowserEvent}; use super::SearchResultsModel; +mod imp { -#[derive(Gladis, Clone)] -struct SearchResultsWidget { - search_root: gtk::Widget, - results_label: gtk::Label, - albums_results: gtk::FlowBox, - artist_results: gtk::FlowBox, + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/search.ui")] + pub struct SearchResultsWidget { + #[template_child] + pub results_label: TemplateChild, + + #[template_child] + pub albums_results: TemplateChild, + + #[template_child] + pub artist_results: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for SearchResultsWidget { + const NAME: &'static str = "SearchResultsWidget"; + type Type = super::SearchResultsWidget; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for SearchResultsWidget {} + impl WidgetImpl for SearchResultsWidget {} + impl BoxImpl for SearchResultsWidget {} +} + +glib::wrapper! { + pub struct SearchResultsWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; } impl SearchResultsWidget { - fn new() -> Self { - Self::from_resource(resource!("/components/search.ui")).unwrap() + pub fn new() -> Self { + glib::Object::new(&[]).expect("Failed to create an instance of SearchResultsWidget") + } + + fn bind_albums_results(&self, worker: Worker, store: &gio::ListStore, on_album_pressed: F) + where + F: Fn(&String) + Clone + 'static, + { + imp::SearchResultsWidget::from_instance(self) + .albums_results + .bind_model(Some(store), move |item| { + wrap_flowbox_item(item, |album_model| { + let f = on_album_pressed.clone(); + let album = AlbumWidget::for_model(album_model, worker.clone()); + album.connect_album_pressed(clone!(@weak album_model => move |_| { + if let Some(id) = album_model.uri().as_ref() { + f(id); + } + })); + album + }) + }); + } + + fn bind_artists_results(&self, worker: Worker, store: &gio::ListStore, on_artist_pressed: F) + where + F: Fn(&String) + Clone + 'static, + { + imp::SearchResultsWidget::from_instance(self) + .artist_results + .bind_model(Some(store), move |item| { + wrap_flowbox_item(item, |artist_model| { + let f = on_artist_pressed.clone(); + let artist = ArtistWidget::for_model(artist_model, worker.clone()); + artist.connect_artist_pressed(clone!(@weak artist_model => move |_| { + if let Some(id) = artist_model.id().as_ref() { + f(id); + } + })); + artist + }) + }); + } + + fn set_search_query(&self, query: &str) { + // translators: This text is part of a larger text that says "Search results for ". + let formatted = format!("{} « {} »", gettext("Search results for"), query); + imp::SearchResultsWidget::from_instance(self) + .results_label + .set_label(&formatted[..]); } } @@ -42,38 +122,21 @@ impl SearchResults { let album_results_model = gio::ListStore::new(AlbumModel::static_type()); let artist_results_model = gio::ListStore::new(ArtistModel::static_type()); - let model_clone = Rc::downgrade(&model); - let worker_clone = worker.clone(); - widget - .albums_results - .bind_model(Some(&album_results_model), move |item| { - wrap_flowbox_item(item, |item: &AlbumModel| { - let album = Album::new(item, worker_clone.clone()); - let weak = model_clone.clone(); - album.connect_album_pressed(move |a| { - if let (Some(id), Some(m)) = (a.uri().as_ref(), weak.upgrade()) { - m.open_album(id); - } - }); - album.get_root_widget().clone() - }) - }); + widget.bind_albums_results( + worker.clone(), + &album_results_model, + clone!(@weak model => move |uri| { + model.open_album(uri); + }), + ); - let model_clone = Rc::downgrade(&model); - widget - .artist_results - .bind_model(Some(&artist_results_model), move |item| { - wrap_flowbox_item(item, |item: &ArtistModel| { - let artist = Artist::new(item, worker.clone()); - let weak = model_clone.clone(); - artist.connect_artist_pressed(move |a| { - if let (Some(id), Some(m)) = (a.id().as_ref(), weak.upgrade()) { - m.open_artist(id); - } - }); - artist.get_root_widget().clone() - }) - }); + widget.bind_artists_results( + worker, + &artist_results_model, + clone!(@weak model => move |id| { + model.open_artist(id); + }), + ); Self { widget, @@ -109,26 +172,20 @@ impl SearchResults { } fn update_search_query(&self) { - { - let model = Rc::downgrade(&self.model); - self.debouncer.debounce(600, move || { - if let Some(model) = model.upgrade() { - model.fetch_results(); - } - }); - } + self.debouncer.debounce( + 600, + clone!(@weak self.model as model => move || model.fetch_results()), + ); if let Some(query) = self.model.get_query() { - // translators: This text is part of a larger text that says "Search results for ". - let formatted = format!("{} « {} »", gettext("Search results for"), *query); - self.widget.results_label.set_label(&formatted[..]); + self.widget.set_search_query(&*query); } } } impl Component for SearchResults { fn get_root_widget(&self) -> >k::Widget { - &self.widget.search_root + self.widget.as_ref() } } diff --git a/src/app/components/search/search.ui b/src/app/components/search/search.ui index 72af156b..0fc2a1f4 100644 --- a/src/app/components/search/search.ui +++ b/src/app/components/search/search.ui @@ -1,39 +1,31 @@ - - - - True - True - never + + diff --git a/src/app/components/search/search_bar.rs b/src/app/components/search/search_bar.rs index 32ac8d6a..aab426db 100644 --- a/src/app/components/search/search_bar.rs +++ b/src/app/components/search/search_bar.rs @@ -1,5 +1,4 @@ use gtk::prelude::*; -use libhandy::traits::SearchBarExt; use std::rc::Rc; use super::SearchBarModel; @@ -11,7 +10,7 @@ impl SearchBar { pub fn new( model: SearchBarModel, search_button: gtk::ToggleButton, - search_bar: libhandy::SearchBar, + search_bar: gtk::SearchBar, search_entry: gtk::SearchEntry, ) -> Self { let model = Rc::new(model); @@ -26,13 +25,14 @@ impl SearchBar { }); } - search_entry.connect_focus_in_event(move |s, _| { - let query = s.text().as_str().to_string(); + let search_entry_controller = gtk::EventControllerFocus::new(); + search_entry_controller.connect_enter(clone!(@weak search_entry => move |_| { + let query = search_entry.text().as_str().to_string(); if !query.is_empty() { model.search(query); } - Inhibit(false) - }); + })); + search_entry.add_controller(&search_entry_controller); search_button.connect_clicked(clone!(@weak search_bar => move |b| { search_bar.set_search_mode(b.is_active()); diff --git a/src/app/components/selection/selection_heading.rs b/src/app/components/selection/selection_heading.rs index 9001b10c..b4694dbf 100644 --- a/src/app/components/selection/selection_heading.rs +++ b/src/app/components/selection/selection_heading.rs @@ -37,7 +37,7 @@ impl SelectionHeadingModel { pub struct SelectionHeading { model: Rc, - headerbar: libhandy::HeaderBar, + headerbar: libadwaita::HeaderBar, selection_toggle: gtk::ToggleButton, selection_label: gtk::Label, } @@ -45,7 +45,7 @@ pub struct SelectionHeading { impl SelectionHeading { pub fn new( model: SelectionHeadingModel, - headerbar: libhandy::HeaderBar, + headerbar: libadwaita::HeaderBar, selection_toggle: gtk::ToggleButton, selection_label: gtk::Label, ) -> Self { diff --git a/src/app/components/selection/selection_tools.rs b/src/app/components/selection/selection_tools.rs index 7aab1184..6e5f0134 100644 --- a/src/app/components/selection/selection_tools.rs +++ b/src/app/components/selection/selection_tools.rs @@ -85,7 +85,6 @@ pub trait SelectionToolsModel { let id = playlist.to_string(); let uris: Vec = selection .peek_selection() - .iter() .map(|s| &s.uri) .cloned() .collect(); diff --git a/src/app/components/selection/selection_widgets.rs b/src/app/components/selection/selection_widgets.rs index ec23432c..1be67d2a 100644 --- a/src/app/components/selection/selection_widgets.rs +++ b/src/app/components/selection/selection_widgets.rs @@ -29,15 +29,13 @@ struct AddSelectionButton { impl AddSelectionButton { fn new(tools: Vec, model: &Rc) -> Self { - let image = gtk::ImageBuilder::new() - .icon_name("list-add-symbolic") - .icon_size(gtk::IconSize::LargeToolbar) - .build(); let button = gtk::MenuButtonBuilder::new() .visible(true) - .image(&image) + .icon_name("list-add-symbolic") .build(); - button.style_context().add_class("osd"); + let ctx = button.style_context(); + ctx.add_class("osd"); + ctx.add_class("tool"); let action_group = SimpleActionGroup::new(); button.insert_action_group("add_to", Some(&action_group)); @@ -80,10 +78,11 @@ impl AddSelectionButton { } } - let popover = gtk::Popover::from_model(Some(&button), &menu); + let popover = gtk::PopoverMenu::from_model(Some(&menu)); popover.style_context().add_class("osd"); + popover.set_position(gtk::PositionType::Top); button.set_popover(Some(&popover)); - MenuButtonExt::set_direction(&button, gtk::ArrowType::Up); + gtk::MenuButton::set_direction(&button, gtk::ArrowType::Up); Self { button } } @@ -102,15 +101,13 @@ struct SelectionButton { impl SelectionButton { fn new(tool: SimpleSelectionTool, model: &Rc) -> Self { - let image = gtk::ImageBuilder::new() - .icon_name(tool.icon_name()) - .icon_size(gtk::IconSize::LargeToolbar) - .build(); let button = gtk::ButtonBuilder::new() .visible(true) - .image(&image) + .icon_name(tool.icon_name()) .build(); - button.style_context().add_class("osd"); + let ctx = button.style_context(); + ctx.add_class("osd"); + ctx.add_class("tool"); button.connect_clicked(clone!(@weak model => move |_| { let selection = model.enabled_selection(); if let Some(selection) = selection { @@ -165,7 +162,9 @@ where fn update_visible_tools(&mut self) { self.selection_widgets = vec![]; - self.button_box.foreach(|w| self.button_box.remove(w)); + while let Some(w) = self.button_box.first_child() { + self.button_box.remove(&w); + } if self.model.enabled_selection().is_some() { self.selection_widgets = Self::make_buttons(&self.button_box, &self.model); @@ -184,7 +183,7 @@ where SelectionTool::Simple(tool) => Some({ let button = SelectionButton::new(*tool, model); button.update_for_state(&*selection); - button_box.add(&button.button); + button_box.append(&button.button); Box::new(button) as Box }), _ => None, @@ -201,7 +200,7 @@ where if !add_tools.is_empty() { let button = AddSelectionButton::new(add_tools, model); button.update_for_state(&*selection); - button_box.add(&button.button); + button_box.append(&button.button); other_tools.push(Box::new(button)); } @@ -215,16 +214,22 @@ where let button_box = gtk::BoxBuilder::new() .halign(gtk::Align::Center) .valign(gtk::Align::End) - .margin(20) + .margin_bottom(20) + .margin_top(20) + .margin_start(20) + .margin_end(20) .build(); button_box.style_context().add_class("linked"); button_box } fn make_widgets(main_child: >k::Widget) -> (gtk::Widget, gtk::Box) { - let root = gtk::OverlayBuilder::new().expand(true).build(); + let root = gtk::OverlayBuilder::new() + .hexpand(true) + .vexpand(true) + .build(); let button_box = Self::make_button_box(); - root.add(main_child); + root.set_child(Some(main_child)); root.add_overlay(&button_box); (root.upcast(), button_box) } diff --git a/src/app/components/user_details/user_details.rs b/src/app/components/user_details/user_details.rs index 6065a2d1..20f338b5 100644 --- a/src/app/components/user_details/user_details.rs +++ b/src/app/components/user_details/user_details.rs @@ -1,26 +1,103 @@ -use gladis::Gladis; use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; use std::rc::Rc; -use crate::app::components::{ - screen_add_css_provider, utils::wrap_flowbox_item, Album, Component, EventListener, -}; -use crate::app::models::*; +use crate::app::components::utils::wrap_flowbox_item; +use crate::app::components::{screen_add_css_provider, AlbumWidget, Component, EventListener}; +use crate::app::{models::*, ListStore}; use crate::app::{AppEvent, BrowserEvent, Worker}; use super::UserDetailsModel; -#[derive(Clone, Gladis)] -struct UserDetailsWidget { - pub root: gtk::ScrolledWindow, - pub user_name: gtk::Label, - pub user_playlists: gtk::FlowBox, +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/user_details.ui")] + pub struct UserDetailsWidget { + #[template_child] + pub scrolled_window: TemplateChild, + + #[template_child] + pub user_name: TemplateChild, + + #[template_child] + pub user_playlists: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for UserDetailsWidget { + const NAME: &'static str = "UserDetailsWidget"; + type Type = super::UserDetailsWidget; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for UserDetailsWidget {} + impl WidgetImpl for UserDetailsWidget {} + impl BoxImpl for UserDetailsWidget {} +} + +glib::wrapper! { + pub struct UserDetailsWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; } impl UserDetailsWidget { fn new() -> Self { screen_add_css_provider(resource!("/components/user_details.css")); - Self::from_resource(resource!("/components/user_details.ui")).unwrap() + glib::Object::new(&[]).expect("Failed to create an instance of UserDetailsWidget") + } + + fn widget(&self) -> &imp::UserDetailsWidget { + imp::UserDetailsWidget::from_instance(self) + } + + fn set_user_name(&self, name: &str) { + let context = self.style_context(); + context.add_class("user__loaded"); + self.widget().user_name.set_text(name); + } + + fn connect_bottom_edge(&self, f: F) + where + F: Fn() + 'static, + { + self.widget() + .scrolled_window + .connect_edge_reached(move |_, pos| { + if let gtk::PositionType::Bottom = pos { + f() + } + }); + } + + fn bind_user_playlists(&self, worker: Worker, store: &ListStore, on_pressed: F) + where + F: Fn(&String) + Clone + 'static, + { + self.widget() + .user_playlists + .bind_model(Some(store.unsafe_store()), move |item| { + wrap_flowbox_item(item, |item: &AlbumModel| { + let f = on_pressed.clone(); + let album = AlbumWidget::for_model(item, worker.clone()); + album.connect_album_pressed(clone!(@weak item => move |_| { + if let Some(id) = item.uri().as_ref() { + f(id); + } + })); + album + }) + }); } } @@ -36,27 +113,16 @@ impl UserDetails { let widget = UserDetailsWidget::new(); let model = Rc::new(model); - widget - .root - .connect_edge_reached(clone!(@weak model => move |_, pos| { - if pos == gtk::PositionType::Bottom { - let _ = model.load_more(); - } - })); + widget.connect_bottom_edge(clone!(@weak model => move || { + model.load_more(); + })); if let Some(store) = model.get_list_store() { - widget.user_playlists.bind_model( - Some(store.unsafe_store()), - clone!(@weak model => @default-panic, move |item| { - wrap_flowbox_item(item, |item: &AlbumModel| { - let album = Album::new(item, worker.clone()); - album.connect_album_pressed(clone!(@weak model => move |a| { - if let Some(id) = a.uri().as_ref() { - model.open_playlist(id); - } - })); - album.get_root_widget().clone() - }) + widget.bind_user_playlists( + worker, + &*store, + clone!(@weak model => move |uri| { + model.open_playlist(uri); }), ); } @@ -66,16 +132,14 @@ impl UserDetails { fn update_details(&self) { if let Some(name) = self.model.get_user_name() { - let context = self.widget.root.style_context(); - context.add_class("user__loaded"); - self.widget.user_name.set_text(&name); + self.widget.set_user_name(&name); } } } impl Component for UserDetails { fn get_root_widget(&self) -> >k::Widget { - self.widget.root.upcast_ref() + self.widget.as_ref() } } diff --git a/src/app/components/user_details/user_details.ui b/src/app/components/user_details/user_details.ui index abe0e994..e0125738 100644 --- a/src/app/components/user_details/user_details.ui +++ b/src/app/components/user_details/user_details.ui @@ -1,20 +1,14 @@ - - - - - True - True - never + + diff --git a/src/app/components/user_menu/user_menu.rs b/src/app/components/user_menu/user_menu.rs index 161202a3..4e02a6c3 100644 --- a/src/app/components/user_menu/user_menu.rs +++ b/src/app/components/user_menu/user_menu.rs @@ -20,17 +20,13 @@ impl UserMenu { ) -> Self { let model = Rc::new(model); - about.connect_delete_event( - clone!(@weak about => @default-return gtk::Inhibit(false), move |_, _| { + about.connect_close_request( + clone!(@weak about => @default-return gtk::Inhibit(false), move |_| { about.hide(); gtk::Inhibit(true) }), ); - about.connect_response(clone!(@weak about => move |_, _| { - about.hide(); - })); - let action_group = SimpleActionGroup::new(); action_group.add_action(&{ @@ -44,7 +40,7 @@ impl UserMenu { action_group.add_action(&{ let about_action = SimpleAction::new("about", None); about_action.connect_activate(clone!(@weak about => move |_, _| { - about.show_all(); + about.present(); })); about_action }); diff --git a/src/app/components/utils.rs b/src/app/components/utils.rs index bd03a300..84b2fe4d 100644 --- a/src/app/components/utils.rs +++ b/src/app/components/utils.rs @@ -3,16 +3,31 @@ use std::cell::Cell; use std::rc::Rc; use std::time::Duration; +#[derive(Clone)] pub struct Clock { interval_ms: u32, - source: Cell>, + source: Rc>>, +} + +impl Default for Clock { + fn default() -> Self { + Self::new(1000) + } +} + +impl std::fmt::Debug for Clock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Clock") + .field("interval_ms", &self.interval_ms) + .finish() + } } impl Clock { - pub fn new() -> Self { + pub fn new(interval_ms: u32) -> Self { Self { - interval_ms: 1000, - source: Cell::new(None), + interval_ms, + source: Rc::new(Cell::new(None)), } } @@ -120,8 +135,7 @@ pub fn wrap_flowbox_item< let item = item.downcast_ref::().unwrap(); let widget = f(item); let child = gtk::FlowBoxChild::new(); - child.add(&widget); - child.show_all(); + child.set_child(Some(&widget)); child.upcast::() } @@ -136,29 +150,3 @@ pub fn format_duration(duration: f64) -> String { format!("{}:{:02}", minutes, seconds) } } - -fn parent_scrolled_window(widget: >k::Widget) -> Option { - let parent = widget.parent()?; - match parent.downcast_ref::() { - Some(scrolled_window) => Some(scrolled_window.clone()), - None => parent_scrolled_window(&parent), - } -} - -pub fn in_viewport(widget: >k::Widget) -> Option { - let window = parent_scrolled_window(widget)?; - let adjustment = window.vadjustment(); - let (_, y) = widget.translate_coordinates(&window, 0, 0)?; - let y = y as f64; - Some(y > 0.0 && y < 0.9 * adjustment.page_size()) -} - -pub fn vscroll_to(widget: >k::Widget, progress: f64) -> Option { - let window = parent_scrolled_window(widget)?; - let adjustment = window.vadjustment(); - let (_, y) = widget.translate_coordinates(&window, 0, 0)?; - let y = y as f64; - let target = adjustment.value() + y * progress; - adjustment.set_value(target); - Some(target) -} diff --git a/src/app/components/window/mod.rs b/src/app/components/window/mod.rs index dadb43be..fb185cd2 100644 --- a/src/app/components/window/mod.rs +++ b/src/app/components/window/mod.rs @@ -1,12 +1,9 @@ -use gio::prelude::SettingsExt; use gtk::prelude::*; -use libhandy::traits::SearchBarExt; use std::cell::RefCell; use std::rc::Rc; -use crate::api::_clear_old_cache; use crate::app::components::EventListener; -use crate::app::{AppEvent, AppModel, Worker}; +use crate::app::{AppEvent, AppModel}; use crate::settings::WindowGeometry; thread_local! { @@ -15,101 +12,56 @@ thread_local! { }); } -const MESSAGE: &str = "The old application cache must be cleared. -Please check ~/.cache/img and ~/.cache/net for any files you might own before proceeding. -Do you wish to clear the cache now?"; - -// see https://github.com/xou816/spot/issues/107 -fn _clear_old_cache_warn(window: >k::Window, worker: Worker) { - let settings = gio::Settings::new("dev.alextren.Spot"); - if settings.boolean("old-cache-cleared") { - return; - } - let do_clear = move || { - let _ = settings.set_boolean("old-cache-cleared", true); - worker.send_task(Box::pin(async { - _clear_old_cache().await; - })); - }; - - if cfg!(feature = "warn-cache") { - let dialog = gtk::MessageDialog::new( - Some(window), - gtk::DialogFlags::MODAL, - gtk::MessageType::Question, - gtk::ButtonsType::YesNo, - MESSAGE, - ); - dialog.show_all(); - dialog.connect_response(move |dialog, response| { - if response == gtk::ResponseType::Yes { - do_clear(); - } - dialog.close(); - }); - } else { - do_clear(); - } -} - pub struct MainWindow { initial_window_geometry: WindowGeometry, - window: libhandy::ApplicationWindow, - worker: Worker, + window: libadwaita::ApplicationWindow, } impl MainWindow { pub fn new( initial_window_geometry: WindowGeometry, app_model: Rc, - window: libhandy::ApplicationWindow, - search_bar: libhandy::SearchBar, - worker: Worker, + window: libadwaita::ApplicationWindow, + search_bar: gtk::SearchBar, ) -> Self { - window.connect_delete_event( - clone!(@weak app_model => @default-return Inhibit(false), move |window, _| { + window.connect_close_request( + clone!(@weak app_model => @default-return gtk::Inhibit(false), move |window| { let state = app_model.get_state(); if state.playback.is_playing() { window.hide(); - Inhibit(true) + gtk::Inhibit(true) } else { - Inhibit(false) + gtk::Inhibit(false) } }), ); - window.connect_key_press_event(move |window, event| { - let search_triggered = search_bar.handle_event(&mut event.clone()); - if !search_triggered { - Inhibit(window.propagate_key_event(event)) + let window_controller = gtk::EventControllerKey::new(); + window.add_controller(&window_controller); + window_controller.set_propagation_phase(gtk::PropagationPhase::Bubble); + window_controller.connect_key_pressed(clone!(@weak search_bar, @weak window => @default-return gtk::Inhibit(false), move |controller, _, _, _| { + let search_triggered = controller.forward(&search_bar) || search_bar.is_search_mode(); + if search_triggered { + search_bar.set_search_mode(true); + gtk::Inhibit(true) + } else if let Some(child) = window.first_child().as_ref() { + gtk::Inhibit(controller.forward(child)) } else { - Inhibit(true) + gtk::Inhibit(false) } - }); + })); - window.connect_size_allocate(|window, _| { - let (width, height) = window.size(); - let is_maximized = window.is_maximized(); - WINDOW_GEOMETRY.with(|g| { - let mut g = g.borrow_mut(); - g.is_maximized = is_maximized; - if !is_maximized { - g.width = width; - g.height = height; - } - }); - }); + window.connect_default_height_notify(Self::save_window_geometry); + window.connect_default_width_notify(Self::save_window_geometry); + window.connect_maximized_notify(Self::save_window_geometry); - window.connect_destroy(|_| { - WINDOW_GEOMETRY.with(|g| { - g.borrow().save(); - }); + window.connect_unrealize(|_| { + WINDOW_GEOMETRY.with(|g| g.borrow().save()); }); Self { initial_window_geometry, window, - worker, } } @@ -122,12 +74,24 @@ impl MainWindow { self.window.maximize(); } self.window.present(); - _clear_old_cache_warn(self.window.upcast_ref(), self.worker.clone()); } fn raise(&self) { self.window.present(); } + + fn save_window_geometry(window: &W) { + let (width, height) = window.default_size(); + let is_maximized = window.is_maximized(); + WINDOW_GEOMETRY.with(|g| { + let mut g = g.borrow_mut(); + g.is_maximized = is_maximized; + if !is_maximized { + g.width = width; + g.height = height; + } + }); + } } impl EventListener for MainWindow { diff --git a/src/app/gtypes/song_model.rs b/src/app/gtypes/song_model.rs index 910d504e..5d69fe27 100644 --- a/src/app/gtypes/song_model.rs +++ b/src/app/gtypes/song_model.rs @@ -1,7 +1,7 @@ #![allow(clippy::all)] use gio::prelude::*; -use glib::subclass::prelude::*; +use glib::{subclass::prelude::*, SignalHandlerId}; glib::wrapper! { pub struct SongModel(ObjectSubclass); @@ -47,24 +47,64 @@ impl SongModel { .to_string() } + pub fn bind_index(&self, o: &O, property: &str) { + imp::SongModel::from_instance(self).push_binding( + self.bind_property("index", o, property) + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(), + ); + } + + pub fn bind_artist(&self, o: &O, property: &str) { + imp::SongModel::from_instance(self).push_binding( + self.bind_property("artist", o, property) + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(), + ); + } + + pub fn bind_title(&self, o: &O, property: &str) { + imp::SongModel::from_instance(self).push_binding( + self.bind_property("title", o, property) + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(), + ); + } + + pub fn bind_duration(&self, o: &O, property: &str) { + imp::SongModel::from_instance(self).push_binding( + self.bind_property("duration", o, property) + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(), + ); + } + pub fn connect_playing_local(&self, handler: F) { - self.connect_local("notify::playing", true, move |values| { - if let Ok(_self) = values[0].get::() { - handler(&_self); - } - None - }) - .expect("connecting to prop 'playing' failed"); + let signal_id = self + .connect_local("notify::playing", true, move |values| { + if let Ok(_self) = values[0].get::() { + handler(&_self); + } + None + }) + .expect("connecting to prop 'playing' failed"); + imp::SongModel::from_instance(self).push_signal(signal_id); } pub fn connect_selected_local(&self, handler: F) { - self.connect_local("notify::selected", true, move |values| { - if let Ok(_self) = values[0].get::() { - handler(&_self); - } - None - }) - .expect("connecting to prop 'selected' failed"); + let signal_id = self + .connect_local("notify::selected", true, move |values| { + if let Ok(_self) = values[0].get::() { + handler(&_self); + } + None + }) + .expect("connecting to prop 'selected' failed"); + imp::SongModel::from_instance(self).push_signal(signal_id); + } + + pub fn unbind_all(&self) { + imp::SongModel::from_instance(self).unbind_all(self); } } @@ -73,6 +113,12 @@ mod imp { use super::*; use std::cell::RefCell; + #[derive(Default)] + struct BindingsInner { + pub signals: Vec, + pub bindings: Vec, + } + // This is the struct containing all state carried with // the new type. Generally this has to make use of // interior mutability. @@ -84,6 +130,25 @@ mod imp { duration: RefCell>, playing: RefCell, selected: RefCell, + bindings: RefCell, + } + + impl SongModel { + pub fn push_signal(&self, id: SignalHandlerId) { + self.bindings.borrow_mut().signals.push(id); + } + + pub fn push_binding(&self, binding: Option) { + if let Some(binding) = binding { + self.bindings.borrow_mut().bindings.push(binding); + } + } + + pub fn unbind_all(&self, o: &O) { + let mut bindings = self.bindings.borrow_mut(); + bindings.signals.drain(..).for_each(|s| o.disconnect(s)); + bindings.bindings.drain(..).for_each(|b| b.unbind()); + } } // ObjectSubclass is the trait that defines the new type and @@ -113,6 +178,7 @@ mod imp { duration: RefCell::new(None), playing: RefCell::new(false), selected: RefCell::new(false), + bindings: RefCell::new(Default::default()), } } } diff --git a/src/app/list_store.rs b/src/app/list_store.rs index fac9a978..5ebd10d3 100644 --- a/src/app/list_store.rs +++ b/src/app/list_store.rs @@ -10,6 +10,8 @@ where { Set(Vec), Append(Vec), + MoveUp(usize), + MoveDown(usize), } pub struct ListStore { @@ -37,6 +39,8 @@ where match diff { ListDiff::Set(elements) => self.replace_all(elements.into_iter()), ListDiff::Append(elements) => self.extend(elements.into_iter()), + ListDiff::MoveDown(i) => self.move_down_unchecked(i as u32), + ListDiff::MoveUp(i) => self.move_up_unchecked(i as u32), } } @@ -54,6 +58,21 @@ where self.store.splice(0, self.store.n_items(), &upcast_vec[..]); } + pub fn move_up_unchecked(&mut self, index: u32) { + self.swap(index - 1, index).unwrap(); + } + + pub fn move_down_unchecked(&mut self, index: u32) { + self.swap(index, index + 1).unwrap(); + } + + fn swap(&mut self, ia: u32, ib: u32) -> Option<()> { + let a = self.store.item(ia)?; + let b = self.store.item(ib)?; + self.store.splice(ia, 2, &[b, a]); + Some(()) + } + pub fn insert(&mut self, position: u32, element: GType) { self.store.insert(position, &element); } diff --git a/src/app/mod.rs b/src/app/mod.rs index 175e413b..5881fab8 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,7 +1,6 @@ use crate::api::CachedSpotifyClient; use crate::settings::SpotSettings; use futures::channel::mpsc::UnboundedSender; -use gtk::prelude::*; use std::rc::Rc; use std::sync::Arc; @@ -66,10 +65,9 @@ impl App { let dispatcher = Box::new(ActionDispatcherImpl::new(sender.clone(), worker.clone())); let mut components: Vec> = vec![ - App::make_window(&self.settings, builder, Rc::clone(model), worker.clone()), + App::make_window(&self.settings, builder, Rc::clone(model)), App::make_selection_editor(builder, Rc::clone(model), dispatcher.box_clone()), - App::make_playback_control(builder, Rc::clone(model), dispatcher.box_clone()), - App::make_playback_info( + App::make_playback( builder, Rc::clone(model), dispatcher.box_clone(), @@ -111,16 +109,14 @@ impl App { settings: &SpotSettings, builder: >k::Builder, app_model: Rc, - worker: Worker, ) -> Box { - let window: libhandy::ApplicationWindow = builder.object("window").unwrap(); - let search_bar: libhandy::SearchBar = builder.object("search_bar").unwrap(); + let window: libadwaita::ApplicationWindow = builder.object("window").unwrap(); + let search_bar: gtk::SearchBar = builder.object("search_bar").unwrap(); Box::new(MainWindow::new( settings.window.clone(), app_model, window, search_bar, - worker, )) } @@ -129,7 +125,7 @@ impl App { app_model: Rc, dispatcher: Box, ) -> Box { - let headerbar: libhandy::HeaderBar = builder.object("header_bar").unwrap(); + let headerbar: libadwaita::HeaderBar = builder.object("header_bar").unwrap(); let selection_toggle: gtk::ToggleButton = builder.object("selection_toggle").unwrap(); let selection_label: gtk::Label = builder.object("selection_label").unwrap(); let model = SelectionHeadingModel::new(app_model, dispatcher); @@ -148,7 +144,7 @@ impl App { worker: Worker, ) -> Box { let back_btn: gtk::Button = builder.object("nav_back").unwrap(); - let leaflet: libhandy::Leaflet = builder.object("leaflet").unwrap(); + let leaflet: libadwaita::Leaflet = builder.object("leaflet").unwrap(); let navigation_stack: gtk::Stack = builder.object("navigation_stack").unwrap(); let home_stack_sidebar: gtk::StackSidebar = builder.object("home_stack_sidebar").unwrap(); @@ -171,66 +167,27 @@ impl App { Box::new(Login::new(parent, model)) } - fn make_playback_info( + fn make_playback( builder: >k::Builder, app_model: Rc, dispatcher: Box, worker: Worker, - ) -> Box { - let now_playing: gtk::Button = builder.object("now_playing").unwrap(); - let now_playing_small: gtk::Button = builder.object("now_playing_small").unwrap(); - let image: gtk::Image = builder.object("playing_image").unwrap(); - let image_small: gtk::Image = builder.object("playing_image_small").unwrap(); - let current_song_info: gtk::Label = builder.object("current_song_info").unwrap(); - - let model = PlaybackInfoModel::new(app_model, dispatcher); - Box::new(PlaybackInfo::new( + ) -> Box { + let model = PlaybackModel::new(app_model, dispatcher); + Box::new(PlaybackControl::new( model, + builder.object("playback").unwrap(), worker, - now_playing, - now_playing_small, - image, - image_small, - current_song_info, )) } - fn make_playback_control( - builder: >k::Builder, - app_model: Rc, - dispatcher: Box, - ) -> Box { - let play_button: gtk::Button = builder.object("play_pause").unwrap(); - let next: gtk::Button = builder.object("next").unwrap(); - let prev: gtk::Button = builder.object("prev").unwrap(); - let shuffle_button: gtk::Button = builder.object("shuffle").unwrap(); - let repeat_button: gtk::Button = builder.object("repeat").unwrap(); - let seek_bar: gtk::Scale = builder.object("seek_bar").unwrap(); - let track_position: gtk::Label = builder.object("track_position").unwrap(); - let track_duration: gtk::Label = builder.object("track_duration").unwrap(); - - let widget = PlaybackControlWidget::new( - play_button, - seek_bar, - track_position, - track_duration, - next, - prev, - shuffle_button, - repeat_button, - ); - - let model = PlaybackControlModel::new(app_model, dispatcher); - Box::new(PlaybackControl::new(model, widget)) - } - fn make_search_bar( builder: >k::Builder, dispatcher: Box, ) -> Box { let search_button: gtk::ToggleButton = builder.object("search_button").unwrap(); let search_entry: gtk::SearchEntry = builder.object("search_entry").unwrap(); - let search_bar: libhandy::SearchBar = builder.object("search_bar").unwrap(); + let search_bar: gtk::SearchBar = builder.object("search_bar").unwrap(); let model = SearchBarModel(dispatcher); Box::new(SearchBar::new( model, diff --git a/src/app/state/app_state.rs b/src/app/state/app_state.rs index 8b76af97..7777a38d 100644 --- a/src/app/state/app_state.rs +++ b/src/app/state/app_state.rs @@ -3,7 +3,7 @@ use crate::app::state::{ login_state::{LoginAction, LoginEvent, LoginState}, playback_state::{PlaybackAction, PlaybackEvent, PlaybackState}, selection_state::{SelectionAction, SelectionContext, SelectionEvent, SelectionState}, - ScreenName, UpdatableState, + PlaylistChange, ScreenName, UpdatableState, }; #[derive(Clone, Debug)] @@ -94,12 +94,13 @@ impl AppState { AppAction::ViewNowPlaying => vec![AppEvent::NowPlayingShown], AppAction::Raise => vec![AppEvent::Raised], AppAction::QueueSelection => { + let append_at = self.playback.len(); for track in self.selection.take_selection() { self.playback.queue(track); } vec![ SelectionEvent::SelectionModeChanged(false).into(), - PlaybackEvent::PlaylistChanged.into(), + PlaybackEvent::PlaylistChanged(PlaylistChange::AppendedAt(append_at)).into(), ] } AppAction::DequeueSelection => { @@ -108,24 +109,32 @@ impl AppState { } vec![ SelectionEvent::SelectionModeChanged(false).into(), - PlaybackEvent::PlaylistChanged.into(), + PlaybackEvent::PlaylistChanged(PlaylistChange::Reset).into(), ] } AppAction::MoveDownSelection => { - if let Some(song) = self.selection.peek_selection().first() { - if self.playback.move_down(&song.id) { - return vec![PlaybackEvent::PlaylistChanged.into()]; - } - } - vec![] + let mut selection = self.selection.peek_selection(); + let playback = &mut self.playback; + selection + .next() + .and_then(|song| playback.move_down(&song.id)) + .map(|index| { + vec![ + PlaybackEvent::PlaylistChanged(PlaylistChange::MovedDown(index)).into(), + ] + }) + .unwrap_or_else(Vec::new) } AppAction::MoveUpSelection => { - if let Some(song) = self.selection.peek_selection().first() { - if self.playback.move_up(&song.id) { - return vec![PlaybackEvent::PlaylistChanged.into()]; - } - } - vec![] + let mut selection = self.selection.peek_selection(); + let playback = &mut self.playback; + selection + .next() + .and_then(|song| playback.move_up(&song.id)) + .map(|index| { + vec![PlaybackEvent::PlaylistChanged(PlaylistChange::MovedUp(index)).into()] + }) + .unwrap_or_else(Vec::new) } AppAction::ChangeSelectionMode(active) => { let context = if active { diff --git a/src/app/state/playback_state.rs b/src/app/state/playback_state.rs index 9b634a70..39a748a2 100644 --- a/src/app/state/playback_state.rs +++ b/src/app/state/playback_state.rs @@ -1,5 +1,5 @@ use rand::{rngs::SmallRng, seq::SliceRandom, RngCore, SeedableRng}; -use std::collections::{HashMap, VecDeque}; +use std::collections::HashMap; use crate::app::models::{Batch, SongBatch, SongDescription}; use crate::app::state::{AppAction, AppEvent, UpdatableState}; @@ -25,62 +25,12 @@ impl Eq for PlaylistSource {} const RANGE_SIZE: usize = 25; pub const QUEUE_DEFAULT_SIZE: usize = 100; -#[derive(Debug, Copy, Clone)] -struct Position { - pub index: usize, - pub start: usize, - pub count: usize, -} - -impl Default for Position { - fn default() -> Self { - Position { - index: 0, - start: 0, - count: 2 * RANGE_SIZE, - } - } -} - -impl Position { - fn has_range_moved(old: Option, new: Option) -> bool { - old.and_then(|old| new.map(|new| old.start != new.start)) - .unwrap_or(true) - } - - fn update_into(self, pos: usize, max: usize) -> Self { - let cutoff = RANGE_SIZE.saturating_sub(max - pos); - let start = pos.saturating_sub(RANGE_SIZE + cutoff); - let count = usize::min(2 * RANGE_SIZE, max - start); - Self { - index: usize::min(pos, max - 1), - start, - count, - } - } - - fn update(&mut self, pos: usize, max: usize) { - let s = *self; - *self = s.update_into(pos, max); - } - - fn update_count(&mut self, max: usize) { - let s = *self; - *self = s.update_into(self.index, max); - } - - fn decrement(&mut self) { - let s = *self; - *self = s.update_into(self.index.saturating_sub(1), self.count.saturating_sub(1)); - } -} - pub struct PlaybackState { rng: SmallRng, indexed_songs: HashMap, - running_order: VecDeque, - running_order_shuffled: Option>, - position: Option, + running_order: Vec, + running_order_shuffled: Option>, + position: Option, pub source: Option, current_batch: Option, repeat: RepeatMode, @@ -88,10 +38,6 @@ pub struct PlaybackState { } impl PlaybackState { - pub fn current_offset(&self) -> Option { - self.position.map(|p| p.start) - } - pub fn is_playing(&self) -> bool { self.is_playing && self.position.is_some() } @@ -100,6 +46,10 @@ impl PlaybackState { self.running_order_shuffled.is_some() } + pub fn repeat_mode(&self) -> RepeatMode { + self.repeat + } + pub fn next_batch(&self) -> Option { self.current_batch?.next() } @@ -108,30 +58,31 @@ impl PlaybackState { self.indexed_songs.get(id) } - fn running_order(&self) -> &VecDeque { + fn running_order(&self) -> &Vec { self.running_order_shuffled .as_ref() .unwrap_or(&self.running_order) } - fn running_order_mut(&mut self) -> &mut VecDeque { + fn running_order_mut(&mut self) -> &mut Vec { self.running_order_shuffled .as_mut() .unwrap_or(&mut self.running_order) } + pub fn len(&self) -> usize { + self.running_order().len() + } + pub fn songs(&self) -> impl Iterator + '_ { let indexed = &self.indexed_songs; - let Position { start, count, .. } = self.position.unwrap_or_default(); self.running_order() .iter() - .skip(start) - .take(count) .filter_map(move |id| indexed.get(id)) } pub fn current_song_id(&self) -> Option<&String> { - self.position.map(|pos| &self.running_order()[pos.index]) + self.position.map(|pos| &self.running_order()[pos]) } pub fn current_song(&self) -> Option<&SongDescription> { @@ -169,12 +120,10 @@ impl PlaybackState { .filter(|&id| Some(id) != current_song) .cloned() .collect(); - let mut final_list: VecDeque = current_song.cloned().into_iter().collect(); + let mut final_list: Vec = current_song.cloned().into_iter().collect(); to_shuffle.shuffle(&mut self.rng); - final_list.append(&mut to_shuffle.into()); - if let Some(p) = self.position.as_mut() { - p.update(0, final_list.len()) - } + final_list.append(&mut to_shuffle); + self.position = self.position.map(|_| 0); self.running_order_shuffled = Some(final_list); } @@ -199,18 +148,13 @@ impl PlaybackState { return; } - self.running_order.push_back(track.id.clone()); + self.running_order.push(track.id.clone()); if let Some(shuffled) = self.running_order_shuffled.as_mut() { let next = (self.rng.next_u32() as usize) % (shuffled.len() - 1); shuffled.insert(next + 1, track.id.clone()); } self.indexed_songs.insert(track.id.clone(), track); - - let max = self.running_order().len(); - if let Some(position) = self.position.as_mut() { - position.update_count(max); - } } pub fn dequeue(&mut self, id: &str) { @@ -219,22 +163,15 @@ impl PlaybackState { } if let Some(position) = self.running_order().iter().position(|t| t == id) { - let new_max = { + let new_len = { let running_order = self.running_order_mut(); running_order.remove(position); running_order.len() }; - let current_comes_after = self.position.map(|p| p.index >= position).unwrap_or(false); - // fix the position of the current song - if current_comes_after { - if new_max > 0 { - if let Some(position) = self.position.as_mut() { - position.decrement(); - } - } else { - self.position = None; - } - } + self.position = + self.position + .filter(|_| new_len > 0) + .map(|p| if p >= position { p - 1 } else { p }); } // if the playlist is shuffled, we also need to remove the track from the unshuffled list @@ -246,59 +183,50 @@ impl PlaybackState { } fn swap(&mut self, index: usize, other_index: usize) { - let max = self.running_order().len(); let running_order = self.running_order_mut(); running_order.swap(index, other_index); - if let Some(position) = self.position.as_mut() { - if index == position.index { - position.update(other_index, max); - } else if other_index == position.index { - position.update(index, max); - } - } + self.position = self.position.map(|position| match position { + i if i == index => other_index, + i if i == other_index => index, + _ => position, + }); } - pub fn move_down(&mut self, id: &str) -> bool { + pub fn move_down(&mut self, id: &str) -> Option { let running_order = self.running_order(); let len = running_order.len(); - let index = running_order + running_order .iter() .position(|s| s == id) - .filter(|&index| index + 1 < len); - if let Some(index) = index { - self.swap(index, index + 1); - true - } else { - false - } + .filter(|&index| index + 1 < len) + .map(|index| { + self.swap(index, index + 1); + index + }) } - pub fn move_up(&mut self, id: &str) -> bool { - let index = self - .running_order() + pub fn move_up(&mut self, id: &str) -> Option { + self.running_order() .iter() .position(|s| s == id) - .filter(|&index| index > 0); - if let Some(index) = index { - self.swap(index - 1, index); - true - } else { - false - } + .filter(|&index| index > 0) + .map(|index| { + self.swap(index - 1, index); + index + }) } fn play(&mut self, id: &str) -> bool { if self.current_song_id().map(|cur| cur == id).unwrap_or(false) { return false; } - let max = self.running_order().len(); if let Some(mut index) = self.running_order().iter().position(|s| s == id) { if self.is_shuffled() && self.position.is_none() { // Hacky fix for now if we reach this state self.running_order_mut().swap(index, 0); index = 0; } - self.position = Some(self.position.unwrap_or_default().update_into(index, max)); + self.position.replace(index); self.is_playing = true; true } else { @@ -312,10 +240,8 @@ impl PlaybackState { } fn play_index(&mut self, index: usize) -> String { - // Assumes index is in running order - let len = self.running_order().len(); self.is_playing = true; - self.position = Some(self.position.unwrap_or_default().update_into(index, len)); + self.position.replace(index); self.running_order()[index].clone() } @@ -326,9 +252,9 @@ impl PlaybackState { fn next_index(&self) -> Option { let len = self.running_order().len(); self.position.and_then(|p| match self.repeat { - RepeatMode::Song => Some(p.index), - RepeatMode::Playlist => Some((p.index + 1) % len), - RepeatMode::None => Some(p.index + 1).filter(|&i| i < len), + RepeatMode::Song => Some(p), + RepeatMode::Playlist => Some((p + 1) % len), + RepeatMode::None => Some(p + 1).filter(|&i| i < len), }) } @@ -339,9 +265,9 @@ impl PlaybackState { fn prev_index(&self) -> Option { let len = self.running_order().len(); self.position.and_then(|p| match self.repeat { - RepeatMode::Song => Some(p.index), - RepeatMode::Playlist => Some((if p.index == 0 { len } else { p.index }) - 1), - RepeatMode::None => Some(p.index).filter(|&i| i > 0).map(|i| i - 1), + RepeatMode::Song => Some(p), + RepeatMode::Playlist => Some((if p == 0 { len } else { p }) - 1), + RepeatMode::None => Some(p).filter(|&i| i > 0).map(|i| i - 1), }) } @@ -368,7 +294,7 @@ impl PlaybackState { pub fn exhausted(&self) -> bool { self.position - .map(|pos| pos.index + RANGE_SIZE >= self.running_order().len() - 1) + .map(|pos| pos + RANGE_SIZE >= self.running_order().len() - 1) .unwrap_or(false) } } @@ -378,7 +304,7 @@ impl Default for PlaybackState { Self { rng: SmallRng::from_entropy(), indexed_songs: HashMap::new(), - running_order: VecDeque::with_capacity(QUEUE_DEFAULT_SIZE), + running_order: Vec::with_capacity(QUEUE_DEFAULT_SIZE), running_order_shuffled: None, position: None, source: None, @@ -395,6 +321,7 @@ pub enum PlaybackAction { Play, Pause, Stop, + SetRepeatMode(RepeatMode), ToggleRepeat, ToggleShuffle, Seek(u32), @@ -415,6 +342,14 @@ impl From for AppAction { } } +#[derive(Clone, Debug)] +pub enum PlaylistChange { + Reset, + AppendedAt(usize), + MovedUp(usize), + MovedDown(usize), +} + #[derive(Clone, Debug)] pub enum PlaybackEvent { PlaybackPaused, @@ -423,7 +358,8 @@ pub enum PlaybackEvent { TrackSeeked(u32), SeekSynced(u32), TrackChanged(String), - PlaylistChanged, + ShuffleChanged, + PlaylistChanged(PlaylistChange), PlaybackStopped, } @@ -483,18 +419,22 @@ impl UpdatableState for PlaybackState { }; vec![PlaybackEvent::RepeatModeChanged(self.repeat)] } + PlaybackAction::SetRepeatMode(mode) => { + self.repeat = mode; + vec![PlaybackEvent::RepeatModeChanged(self.repeat)] + } PlaybackAction::ToggleShuffle => { self.toggle_shuffle(); - vec![PlaybackEvent::PlaylistChanged] + vec![ + PlaybackEvent::PlaylistChanged(PlaylistChange::Reset), + PlaybackEvent::ShuffleChanged, + ] } PlaybackAction::Next => { - let old_position = self.position; if let Some(id) = self.play_next() { make_events(vec![ Some(PlaybackEvent::TrackChanged(id)), Some(PlaybackEvent::PlaybackResumed), - Some(PlaybackEvent::PlaylistChanged) - .filter(|_| Position::has_range_moved(old_position, self.position)), ]) } else { self.stop(); @@ -506,26 +446,21 @@ impl UpdatableState for PlaybackState { vec![PlaybackEvent::PlaybackStopped] } PlaybackAction::Previous => { - let old_position = self.position; if let Some(id) = self.play_prev() { make_events(vec![ Some(PlaybackEvent::TrackChanged(id)), Some(PlaybackEvent::PlaybackResumed), - Some(PlaybackEvent::PlaylistChanged) - .filter(|_| Position::has_range_moved(old_position, self.position)), ]) } else { vec![] } } PlaybackAction::Load(id) => { - let old_position = self.position; if self.play(&id) { make_events(vec![ Some(PlaybackEvent::TrackChanged(id)), Some(PlaybackEvent::PlaybackResumed), - Some(PlaybackEvent::PlaylistChanged) - .filter(|_| Position::has_range_moved(old_position, self.position)), + Some(PlaybackEvent::PlaylistChanged(PlaylistChange::Reset)), ]) } else { vec![] @@ -533,29 +468,35 @@ impl UpdatableState for PlaybackState { } PlaybackAction::LoadSongs(source, tracks) => { self.set_playlist(source, None, tracks); - vec![PlaybackEvent::PlaylistChanged] + vec![PlaybackEvent::PlaylistChanged(PlaylistChange::Reset)] } PlaybackAction::LoadPagedSongs(source, SongBatch { songs, batch }) => { self.set_playlist(source, Some(batch), songs); - vec![PlaybackEvent::PlaylistChanged] + vec![PlaybackEvent::PlaylistChanged(PlaylistChange::Reset)] } PlaybackAction::Queue(tracks) => { + let append_at = self.running_order().len(); self.current_batch = None; for track in tracks { self.queue(track); } - vec![PlaybackEvent::PlaylistChanged] + vec![PlaybackEvent::PlaylistChanged(PlaylistChange::AppendedAt( + append_at, + ))] } PlaybackAction::QueuePaged(SongBatch { batch, songs }) => { + let append_at = self.running_order().len(); self.current_batch = Some(batch); for song in songs { self.queue(song); } - vec![PlaybackEvent::PlaylistChanged] + vec![PlaybackEvent::PlaylistChanged(PlaylistChange::AppendedAt( + append_at, + ))] } PlaybackAction::Dequeue(id) => { self.dequeue(&id); - vec![PlaybackEvent::PlaylistChanged] + vec![PlaybackEvent::PlaylistChanged(PlaylistChange::Reset)] } PlaybackAction::Seek(pos) => vec![PlaybackEvent::TrackSeeked(pos)], PlaybackAction::SyncSeek(pos) => vec![PlaybackEvent::SeekSynced(pos)], @@ -586,7 +527,7 @@ mod tests { impl PlaybackState { fn current_position(&self) -> Option { - Some(self.position?.index) + self.position } fn song_ids(&self) -> Vec<&str> { diff --git a/src/app/state/selection_state.rs b/src/app/state/selection_state.rs index ef3e7151..7404a11a 100644 --- a/src/app/state/selection_state.rs +++ b/src/app/state/selection_state.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use crate::app::models::SongDescription; use crate::app::state::{AppAction, AppEvent, UpdatableState}; @@ -34,14 +36,16 @@ pub enum SelectionContext { } pub struct SelectionState { - selected_songs: Option>, + selected_songs: HashMap, + selection_active: bool, pub context: SelectionContext, } impl Default for SelectionState { fn default() -> Self { Self { - selected_songs: None, + selected_songs: Default::default(), + selection_active: false, context: SelectionContext::Global, } } @@ -49,39 +53,29 @@ impl Default for SelectionState { impl SelectionState { fn select(&mut self, song: SongDescription) -> bool { - if let Some(selected_songs) = self.selected_songs.as_mut() { - let selected = selected_songs.iter().any(|t| t.id == song.id); - if !selected { - selected_songs.push(song); - } - !selected - } else { - false + let selected = self.selected_songs.contains_key(&song.id); + if !selected { + self.selected_songs.insert(song.id.clone(), song); } + !selected } fn deselect(&mut self, id: &str) -> bool { - if let Some(selected_songs) = self.selected_songs.as_mut() { - let selected = selected_songs.iter().any(|t| t.id == id); - if selected { - selected_songs.retain(|t| t.id != id); - } - selected - } else { - false - } + self.selected_songs.remove(id).is_some() } pub fn set_mode(&mut self, context: Option) -> Option { - let currently_active = self.selected_songs.is_some(); + let currently_active = self.selection_active; match (currently_active, context) { (false, Some(context)) => { - self.selected_songs = Some(vec![]); + self.selected_songs = Default::default(); + self.selection_active = true; self.context = context; Some(true) } (true, None) => { - self.selected_songs = None; + self.selected_songs = Default::default(); + self.selection_active = false; Some(false) } _ => None, @@ -89,14 +83,11 @@ impl SelectionState { } pub fn is_selection_enabled(&self) -> bool { - self.selected_songs.is_some() + self.selection_active } pub fn is_song_selected(&self, id: &str) -> bool { - self.selected_songs - .as_ref() - .map(|s| s.iter().any(|t| t.id == id)) - .unwrap_or(false) + self.selected_songs.contains_key(id) } pub fn all_selected<'a>(&self, mut ids: impl Iterator) -> bool { @@ -104,15 +95,15 @@ impl SelectionState { } pub fn count(&self) -> usize { - self.selected_songs.as_ref().map(|s| s.len()).unwrap_or(0) + self.selected_songs.len() } pub fn take_selection(&mut self) -> Vec { - self.selected_songs.take().unwrap_or_else(Vec::new) + std::mem::take(self).selected_songs.into_values().collect() } - pub fn peek_selection(&self) -> &[SongDescription] { - self.selected_songs.as_ref().map(|s| &s[..]).unwrap_or(&[]) + pub fn peek_selection(&self) -> impl Iterator { + self.selected_songs.values() } } @@ -143,7 +134,7 @@ impl UpdatableState for SelectionState { } } SelectionAction::Clear => { - self.selected_songs = None; + self.take_selection(); vec![SelectionEvent::SelectionModeChanged(false)] } } diff --git a/src/dbus/mod.rs b/src/dbus/mod.rs index d22bbc12..34679e3a 100644 --- a/src/dbus/mod.rs +++ b/src/dbus/mod.rs @@ -4,7 +4,7 @@ use std::thread; use zbus::fdo; use crate::app::components::EventListener; -use crate::app::state::PlaybackEvent; +use crate::app::state::{PlaybackEvent, RepeatMode}; use crate::app::{models::SongDescription, AppAction, AppEvent, AppModel}; mod mpris; @@ -73,6 +73,20 @@ impl AppPlaybackStateListener { state.playback.next_song().is_some(), ) } + + fn is_shuffled(&self) -> bool { + let state = self.app_model.get_state(); + state.playback.is_shuffled() + } + + fn loop_status(&self) -> LoopStatus { + let state = self.app_model.get_state(); + match state.playback.repeat_mode() { + RepeatMode::None => LoopStatus::None, + RepeatMode::Song => LoopStatus::Track, + RepeatMode::Playlist => LoopStatus::Playlist, + } + } } impl EventListener for AppPlaybackStateListener { @@ -102,8 +116,7 @@ impl EventListener for AppPlaybackStateListener { }) .unwrap(); } - AppEvent::PlaybackEvent(PlaybackEvent::TrackChanged(_)) - | AppEvent::PlaybackEvent(PlaybackEvent::RepeatModeChanged(_)) => { + AppEvent::PlaybackEvent(PlaybackEvent::TrackChanged(_)) => { self.with_player(|player| { let meta = self.make_track_meta(); let (has_prev, has_next) = self.has_prev_next(); @@ -115,6 +128,26 @@ impl EventListener for AppPlaybackStateListener { }) .unwrap(); } + AppEvent::PlaybackEvent(PlaybackEvent::RepeatModeChanged(_)) => { + self.with_player(|player| { + let (has_prev, has_next) = self.has_prev_next(); + player.state.set_loop_status(self.loop_status()); + player.loop_status_changed()?; + player.state.set_has_prev(has_prev); + player.state.set_has_next(has_next); + player.notify_metadata_and_prev_next()?; + Ok(()) + }) + .unwrap(); + } + AppEvent::PlaybackEvent(PlaybackEvent::ShuffleChanged) => { + self.with_player(|player| { + player.state.set_shuffled(self.is_shuffled()); + player.shuffle_changed()?; + Ok(()) + }) + .unwrap(); + } AppEvent::PlaybackEvent(PlaybackEvent::TrackSeeked(pos)) | AppEvent::PlaybackEvent(PlaybackEvent::SeekSynced(pos)) => { self.with_player(|player| { diff --git a/src/dbus/mpris.rs b/src/dbus/mpris.rs index a6118818..18bf7c94 100644 --- a/src/dbus/mpris.rs +++ b/src/dbus/mpris.rs @@ -9,6 +9,7 @@ use zbus::ObjectServer; use zvariant::ObjectPath; use super::types::*; +use crate::app::state::RepeatMode; use crate::app::{state::PlaybackAction, AppAction}; #[derive(Clone)] @@ -269,6 +270,23 @@ impl SpotMprisPlayer { self.state.status() } + #[dbus_interface(property)] + pub fn loop_status(&self) -> LoopStatus { + self.state.loop_status() + } + + #[dbus_interface(property)] + pub fn set_loop_status(&self, value: LoopStatus) -> Result<()> { + let mode = match value { + LoopStatus::None => RepeatMode::None, + LoopStatus::Track => RepeatMode::Song, + LoopStatus::Playlist => RepeatMode::Playlist, + }; + self.sender + .unbounded_send(PlaybackAction::SetRepeatMode(mode).into()) + .map_err(|_| zbus::fdo::Error::Failed("Could not send action".to_string())) + } + #[dbus_interface(property)] pub fn position(&self) -> i64 { self.state.position() as i64 @@ -284,11 +302,15 @@ impl SpotMprisPlayer { #[dbus_interface(property)] pub fn shuffle(&self) -> bool { - false + self.state.is_shuffled() } #[dbus_interface(property)] - pub fn set_shuffle(&self, value: bool) {} + pub fn set_shuffle(&self, value: bool) -> Result<()> { + self.sender + .unbounded_send(PlaybackAction::ToggleShuffle.into()) + .map_err(|_| zbus::fdo::Error::Failed("Could not send action".to_string())) + } #[dbus_interface(property)] pub fn volume(&self) -> f64 { diff --git a/src/dbus/types.rs b/src/dbus/types.rs index f1880023..d49d734a 100644 --- a/src/dbus/types.rs +++ b/src/dbus/types.rs @@ -1,4 +1,4 @@ -use std::convert::Into; +use std::convert::{Into, TryFrom}; use std::sync::{Arc, Mutex}; use std::time::Instant; use zvariant::Type; @@ -8,6 +8,43 @@ fn boxed_value<'a, V: Into>>(v: V) -> Value<'a> { Value::new(v.into()) } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LoopStatus { + None, + Track, + Playlist, +} + +impl Type for LoopStatus { + fn signature() -> Signature<'static> { + Str::signature() + } +} + +impl TryFrom<&Value<'_>> for LoopStatus { + type Error = zvariant::Error; + + fn try_from(value: &Value<'_>) -> Result { + let s = String::try_from(value)?; + let s = s.as_str(); + Ok(match s { + "Track" => LoopStatus::Track, + "Playlist" => LoopStatus::Playlist, + _ => LoopStatus::None, + }) + } +} + +impl From for Value<'_> { + fn from(status: LoopStatus) -> Self { + match status { + LoopStatus::None => "None".into(), + LoopStatus::Track => "Track".into(), + LoopStatus::Playlist => "Playlist".into(), + } + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum PlaybackStatus { Playing, @@ -109,6 +146,8 @@ impl From for Value<'_> { struct MprisState { status: PlaybackStatus, + loop_status: LoopStatus, + shuffled: bool, position: PositionMicros, metadata: Option, has_prev: bool, @@ -122,6 +161,8 @@ impl SharedMprisState { pub fn new() -> Self { Self(Arc::new(Mutex::new(MprisState { status: PlaybackStatus::Stopped, + loop_status: LoopStatus::None, + shuffled: false, position: PositionMicros::new(1.0), metadata: None, has_prev: false, @@ -136,6 +177,17 @@ impl SharedMprisState { .unwrap_or(PlaybackStatus::Stopped) } + pub fn loop_status(&self) -> LoopStatus { + self.0 + .lock() + .map(|s| s.loop_status) + .unwrap_or(LoopStatus::None) + } + + pub fn is_shuffled(&self) -> bool { + self.0.lock().ok().map(|s| s.shuffled).unwrap_or(false) + } + pub fn current_track(&self) -> Option { self.0.lock().ok().and_then(|s| s.metadata.clone()) } @@ -183,6 +235,18 @@ impl SharedMprisState { } } + pub fn set_loop_status(&self, loop_status: LoopStatus) { + if let Ok(mut state) = self.0.lock() { + (*state).loop_status = loop_status; + } + } + + pub fn set_shuffled(&self, shuffled: bool) { + if let Ok(mut state) = self.0.lock() { + (*state).shuffled = shuffled; + } + } + pub fn set_playing(&self, status: PlaybackStatus) { if let Ok(mut state) = self.0.lock() { (*state).status = status; diff --git a/src/main.rs b/src/main.rs index d3e5ad46..49a29aa9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,6 @@ use gettextrs::*; use gio::prelude::*; use gio::SimpleAction; use gtk::prelude::*; -use gtk::traits::SettingsExt; mod api; mod app; @@ -18,6 +17,7 @@ mod player; mod settings; pub use config::VERSION; +use crate::app::components::expose_widgets; use crate::app::dispatch::{spawn_task_handler, DispatchLoop}; use crate::app::{state::PlaybackAction, App, AppAction, BrowserAction}; @@ -30,8 +30,9 @@ fn main() { let settings = settings::SpotSettings::new_from_gsettings().unwrap_or_default(); startup(&settings); let gtk_app = gtk::Application::new(Some(config::APPID), Default::default()); + expose_widgets(); let builder = gtk::Builder::from_resource("/dev/alextren/Spot/window.ui"); - let window: libhandy::ApplicationWindow = builder.object("window").unwrap(); + let window: libadwaita::ApplicationWindow = builder.object("window").unwrap(); if cfg!(debug_assertions) { window.style_context().add_class("devel"); } @@ -70,7 +71,7 @@ fn main() { fn startup(settings: &settings::SpotSettings) { gtk::init().unwrap_or_else(|_| panic!("Failed to initialize GTK")); - libhandy::init(); + libadwaita::init(); let res = gio::Resource::load(config::PKGDATADIR.to_owned() + "/spot.gresource") .expect("Could not load resources"); @@ -83,8 +84,8 @@ fn startup(settings: &settings::SpotSettings) { let provider = gtk::CssProvider::new(); provider.load_from_resource("/dev/alextren/Spot/app.css"); - gtk::StyleContext::add_provider_for_screen( - &gdk::Screen::default().expect("Error initializing gtk css provider."), + gtk::StyleContext::add_provider_for_display( + &gdk::Display::default().unwrap(), &provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, ); @@ -96,35 +97,30 @@ fn register_actions(app: >k::Application, sender: UnboundedSender) app.quit(); })); app.add_action(&quit); - app.set_accels_for_action("app.quit", &["Q"]); app.add_action(&make_action( "toggle_playback", PlaybackAction::TogglePlay.into(), sender.clone(), )); - app.set_accels_for_action("app.toggle_playback", &["space"]); app.add_action(&make_action( "player_prev", PlaybackAction::Previous.into(), sender.clone(), )); - app.set_accels_for_action("app.player_prev", &["P"]); app.add_action(&make_action( "player_next", PlaybackAction::Next.into(), sender.clone(), )); - app.set_accels_for_action("app.player_next", &["N"]); app.add_action(&make_action( "nav_pop", AppAction::BrowserAction(BrowserAction::NavigationPop), sender, )); - app.set_accels_for_action("app.nav_pop", &["Left"]); } fn make_action( diff --git a/src/meson.build b/src/meson.build index 829db1b6..0f4ae16b 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,7 +1,7 @@ gnome = import('gnome') -dependency('libhandy-1') -dependency('gtk+-3.0') +dependency('libadwaita-1') +dependency('gtk4') dependency('glib-2.0') dependency('openssl') dependency('alsa') @@ -63,94 +63,98 @@ run_command( # find . -name "*.rs" -printf "'%p',\n" sources = files( -'./api/cache.rs', -'./api/api_models.rs', -'./api/client.rs', './api/cached_client.rs', './api/mod.rs', -'./app/dispatch.rs', -'./app/components/details/details.rs', -'./app/components/details/details_model.rs', -'./app/components/details/mod.rs', -'./app/components/artist/mod.rs', -'./app/components/utils.rs', -'./app/components/playback/playback_info.rs', -'./app/components/playback/playback_control.rs', -'./app/components/playback/mod.rs', -'./app/components/playlist_details/playlist_details.rs', -'./app/components/playlist_details/playlist_details_model.rs', -'./app/components/playlist_details/mod.rs', -'./app/components/album/album.rs', -'./app/components/album/mod.rs', -'./app/components/artist_details/artist_details.rs', -'./app/components/artist_details/artist_details_model.rs', -'./app/components/artist_details/mod.rs', -'./app/components/login/login.rs', -'./app/components/login/mod.rs', -'./app/components/login/login_model.rs', -'./app/components/window/mod.rs', +'./api/client.rs', +'./api/api_models.rs', +'./api/cache.rs', +'./dbus/types.rs', +'./dbus/mpris.rs', +'./dbus/mod.rs', +'./config.rs', +'./app/state/pagination.rs', +'./app/state/app_state.rs', +'./app/state/login_state.rs', +'./app/state/browser_state.rs', +'./app/state/playback_state.rs', +'./app/state/mod.rs', +'./app/state/screen_states.rs', +'./app/state/selection_state.rs', +'./app/state/app_model.rs', +'./app/components/saved_playlists/saved_playlists.rs', './app/components/saved_playlists/saved_playlists_model.rs', './app/components/saved_playlists/mod.rs', -'./app/components/saved_playlists/saved_playlists.rs', -'./app/components/library/library_model.rs', -'./app/components/library/library.rs', -'./app/components/library/mod.rs', -'./app/components/player_notifier.rs', -'./app/components/now_playing/now_playing.rs', -'./app/components/now_playing/mod.rs', -'./app/components/now_playing/now_playing_model.rs', -'./app/components/user_menu/user_menu_model.rs', +'./app/components/notification/mod.rs', './app/components/user_menu/mod.rs', './app/components/user_menu/user_menu.rs', -'./app/components/playlist/song.rs', -'./app/components/playlist/song_actions.rs', -'./app/components/playlist/playlist.rs', -'./app/components/playlist/mod.rs', -'./app/components/navigation/navigation.rs', -'./app/components/navigation/factory.rs', -'./app/components/navigation/home.rs', -'./app/components/navigation/mod.rs', -'./app/components/navigation/navigation_model.rs', -'./app/components/notification/mod.rs', +'./app/components/user_menu/user_menu_model.rs', +'./app/components/artist_details/mod.rs', +'./app/components/artist_details/artist_details_model.rs', +'./app/components/artist_details/artist_details.rs', +'./app/components/search/search_bar.rs', './app/components/search/search_bar_model.rs', './app/components/search/search_model.rs', -'./app/components/search/search_bar.rs', './app/components/search/mod.rs', './app/components/search/search.rs', -'./app/components/user_details/mod.rs', +'./app/components/artist/mod.rs', +'./app/components/player_notifier.rs', +'./app/components/window/mod.rs', +'./app/components/navigation/navigation.rs', +'./app/components/navigation/home.rs', +'./app/components/navigation/navigation_model.rs', +'./app/components/navigation/mod.rs', +'./app/components/navigation/factory.rs', './app/components/user_details/user_details_model.rs', +'./app/components/user_details/mod.rs', './app/components/user_details/user_details.rs', -'./app/components/labels.rs', -'./app/components/mod.rs', -'./app/components/selection/mod.rs', -'./app/components/selection/selection_heading.rs', './app/components/selection/selection_tools.rs', +'./app/components/selection/selection_heading.rs', './app/components/selection/selection_widgets.rs', +'./app/components/selection/mod.rs', +'./app/components/mod.rs', +'./app/components/details/mod.rs', +'./app/components/details/details.rs', +'./app/components/details/details_model.rs', +'./app/components/details/release_details.rs', +'./app/components/utils.rs', +'./app/components/now_playing/now_playing_model.rs', +'./app/components/now_playing/mod.rs', +'./app/components/now_playing/now_playing.rs', +'./app/components/login/login.rs', +'./app/components/login/mod.rs', +'./app/components/login/login_model.rs', +'./app/components/playlist_details/playlist_details_model.rs', +'./app/components/playlist_details/mod.rs', +'./app/components/playlist_details/playlist_details.rs', +'./app/components/album/album.rs', +'./app/components/album/mod.rs', +'./app/components/labels.rs', +'./app/components/playlist/song_actions.rs', +'./app/components/playlist/playlist.rs', +'./app/components/playlist/mod.rs', +'./app/components/playlist/song.rs', +'./app/components/playback/playback_controls.rs', +'./app/components/playback/playback_widget.rs', +'./app/components/playback/component.rs', +'./app/components/playback/mod.rs', +'./app/components/playback/playback_info.rs', +'./app/components/library/library.rs', +'./app/components/library/mod.rs', +'./app/components/library/library_model.rs', './app/loader.rs', -'./app/list_store.rs', -'./app/credentials.rs', -'./app/models.rs', -'./app/state/app_model.rs', -'./app/state/selection_state.rs', -'./app/state/browser_state.rs', -'./app/state/login_state.rs', -'./app/state/screen_states.rs', -'./app/state/pagination.rs', -'./app/state/playback_state.rs', -'./app/state/app_state.rs', -'./app/state/mod.rs', -'./app/gtypes/album_model.rs', -'./app/gtypes/song_model.rs', './app/gtypes/artist_model.rs', +'./app/gtypes/song_model.rs', +'./app/gtypes/album_model.rs', './app/gtypes/mod.rs', +'./app/dispatch.rs', +'./app/list_store.rs', './app/mod.rs', -'./dbus/mpris.rs', -'./dbus/mod.rs', -'./dbus/types.rs', -'./player/player.rs', -'./player/mod.rs', +'./app/credentials.rs', +'./app/models.rs', './main.rs', './settings.rs', +'./player/player.rs', +'./player/mod.rs', ) cargo_script = find_program(meson.source_root() / 'build-aux/cargo.sh') @@ -195,4 +199,4 @@ test('Clippy', get_option('offline') ? 'true' : 'false' ], timeout: 180 -) \ No newline at end of file +) diff --git a/src/player/player.rs b/src/player/player.rs index 22c28c95..60296c05 100644 --- a/src/player/player.rs +++ b/src/player/player.rs @@ -226,7 +226,10 @@ async fn get_access_token_and_expiry_time( ) -> Result<(String, SystemTime), SpotifyError> { let token = keymaster::get_token(session, CLIENT_ID, SCOPES) .await - .map_err(|_| SpotifyError::TokenFailed)?; + .map_err(|e| { + dbg!(e); + SpotifyError::TokenFailed + })?; let expiry_time = SystemTime::now() + Duration::from_secs(token.expires_in.into()); Ok((token.access_token, expiry_time)) } @@ -234,7 +237,10 @@ async fn get_access_token_and_expiry_time( async fn create_session(credentials: Credentials) -> Result { let session_config = SessionConfig::default(); let result = Session::connect(session_config, credentials, None).await; - result.map_err(|_| SpotifyError::LoginFailed) + result.map_err(|e| { + dbg!(e); + SpotifyError::LoginFailed + }) } async fn player_setup_delegate( diff --git a/src/spot.gresource.xml b/src/spot.gresource.xml index 6b39db82..41c648ac 100644 --- a/src/spot.gresource.xml +++ b/src/spot.gresource.xml @@ -15,6 +15,7 @@ app/components/details/details.css app/components/details/details.ui + app/components/details/release_details.ui app/components/playlist_details/playlist_details.css app/components/playlist_details/playlist_details.ui @@ -34,5 +35,10 @@ app/components/user_details/user_details.css app/components/user_details/user_details.ui + + app/components/playback/playback.css + app/components/playback/playback_controls.ui + app/components/playback/playback_info.ui + app/components/playback/playback_widget.ui diff --git a/src/window.ui.in b/src/window.ui.in index 336786d0..6bbc5f09 100644 --- a/src/window.ui.in +++ b/src/window.ui.in @@ -1,547 +1,164 @@ - - - - - - 500 - False + + + 1080 720 - True - False vertical - - True - False - 8 - True - - - False - No song selected - end + + local + + + space + action(app.toggle_playback) - - True - True - True - start - center - - - True - False - go-previous-symbolic - - + + <Ctrl>Q + action(app.quit) - - True - True - True - - - True - False - open-menu-symbolic - - + + <Alt>P + action(app.player_prev) - - end - 1 - - - True - True - True - - - True - False - object-select-symbolic - - + + <Alt>N + action(app.player_next) - - end - 2 - - - True - True - True - - - True - False - system-search-symbolic - - + + <Alt>Left + action(app.nav_pop) - - 3 - - - False - True - 0 - - - True - False - True - - - True - True - edit-find-symbolic - False - False + + + + 0 + No song selected + end - - - False - True - 1 - - - - - True - False - True - main - - 200 - True - False - True + + 1 + start + center + go-previous-symbolic + 0 - - True - False + + 1 + system-search-symbolic - - False - - - - True - False - True - True - - - True - False - slide-left-right - - - - - - -1 - - - - - False - center - start - 10 - - - True - False - Information - True - - - False - True - 0 - - - - - True - True - True - - - True - False - window-close-symbolic - - - - - False - True - end - 1 - - - - - - -1 - - + + + 1 + open-menu-symbolic + + + + + 1 + object-select-symbolic - - main - - - True - True - 2 - - - True - True - True - False - 0 - 0 - False - left - + + 1 + + + 1 + + - - False - True - 3 - - - True - False - 4 - 4 - 8 - 8 - True - - - True - False - center - 0 - True - - - True - False - False - True - center - center - none - - - True - False - media-playlist-shuffle-symbolic - - - - - False - True - 0 - - - - - True - True - True - center - center - none - - - True - False - media-skip-backward-symbolic - - - - - False - True - 1 - - - - - True - True - True - center - center - - - True - False - center - center - 6 - 6 - 6 - 6 - 6 - 6 - media-playback-start-symbolic - 3 - - - - - - False - True - 2 - - - - - True - True - True - center - center - none - - - True - False - media-skip-forward-symbolic - - - - - False - True - 3 - - - - - True - False - False - True - center - center - none - - - True - False - media-playlist-consecutive-symbolic.symbolic - - + + 1 + main + 1 + + + 0 + + + 200 + 1 - - False - True - 4 - - + - - True - True - 2 - - - True - False - True - - - True - False - 4 - 4 - 4 - 4 - - - False - False - 0:00 - - - False - True - 0 - - + + main + + + 1 + 1 - - False - False - / 0:00 + + slide-left-right - - False - True - 1 - - - - - - True - False - - - - - False - True - end - 0 - - - - - True - False - True - - - True - True - True - start - center - none - - - True - False + + center + start + 10 - - 40 - 40 - True - False - emblem-music-symbolic + + Information + 1 - - False - True - 0 - - - 100 - True - False - start - 12 - 12 - No song playing - True - middle - 1 + + 1 + window-close-symbolic - - False - True - 1 - + - - - - True - True - True - start - center - none - - - 40 - 40 - True - False - emblem-music-symbolic - - - - + - - False - True - 1 - - - False - True - 4 - + + + + 1 + - False - True - center-on-parent - True - dialog + 1 + 1 window Spot @VERSION@ @@ -550,32 +167,5 @@ @TRANSLATORS@ dev.alextren.Spot mit-x11 - - - False - vertical - 2 - - - False - end - - - - - - - - - False - False - 0 - - - - - - -