From e8c567cf051b54019ce448fc89a275de750a7a57 Mon Sep 17 00:00:00 2001 From: Alexandre Trendel Date: Sat, 29 Jan 2022 16:04:07 +0000 Subject: [PATCH] Refactor list models (#456) * refctor * more refactor * MORE * M.O.R.E * who said this was over * stuff * gtk rs update * reorg files * improve list model diffing --- .vscode/tasks.json | 2 +- Cargo.lock | 200 ++++------ Cargo.toml | 10 +- src/api/api_models.rs | 22 +- .../artist_details/artist_details.rs | 7 +- .../artist_details/artist_details_model.rs | 70 ++-- src/app/components/details/details.rs | 4 +- src/app/components/details/details_model.rs | 79 ++-- src/app/components/details/release_details.rs | 5 - src/app/components/details/release_details.ui | 10 - src/app/components/headerbar/component.rs | 2 +- src/app/components/library/library.rs | 8 +- src/app/components/login/login.rs | 4 +- src/app/components/navigation/home.rs | 16 +- src/app/components/notification/mod.rs | 2 +- src/app/components/now_playing/now_playing.rs | 7 +- .../now_playing/now_playing_model.rs | 56 +-- src/app/components/playback/component.rs | 6 +- src/app/components/playlist/playlist.rs | 152 ++++---- src/app/components/playlist/song.rs | 105 ++++-- .../playlist_details/playlist_details.rs | 1 - .../playlist_details_model.rs | 83 ++-- .../saved_playlists/saved_playlists.rs | 8 +- .../components/saved_tracks/saved_tracks.rs | 7 +- .../saved_tracks/saved_tracks_model.rs | 53 +-- src/app/components/search/search.rs | 14 +- src/app/components/search/search_model.rs | 10 +- .../sidebar_listbox/sidebar_icon_widget.rs | 9 +- .../sidebar_listbox/sidebar_item.rs | 28 +- .../components/sidebar_listbox/sidebar_row.rs | 8 +- .../components/user_details/user_details.rs | 6 +- .../user_details/user_details_model.rs | 5 +- src/app/components/utils.rs | 6 +- src/app/list_store.rs | 42 --- src/app/models/{gtypes => }/album_model.rs | 83 ++-- src/app/models/{gtypes => }/artist_model.rs | 43 +-- src/app/models/gtypes/mod.rs | 8 - src/app/models/gtypes/song_model.rs | 337 ----------------- src/app/models/main.rs | 131 ++++--- src/app/models/mod.rs | 60 ++- src/app/models/songs/mod.rs | 7 + src/app/models/songs/song_list_model.rs | 251 ++++++++++++ src/app/models/songs/song_model.rs | 300 +++++++++++++++ src/app/models/{core.rs => songs/support.rs} | 356 ++++++++++++------ src/app/state/app_state.rs | 32 +- src/app/state/browser_state.rs | 6 +- src/app/state/playback_state.rs | 311 ++++++++------- src/app/state/screen_states.rs | 79 ++-- src/dbus/listener.rs | 44 +-- src/main.rs | 3 +- src/meson.build | 21 +- 51 files changed, 1591 insertions(+), 1528 deletions(-) rename src/app/models/{gtypes => }/album_model.rs (64%) rename src/app/models/{gtypes => }/artist_model.rs (72%) delete mode 100644 src/app/models/gtypes/mod.rs delete mode 100644 src/app/models/gtypes/song_model.rs create mode 100644 src/app/models/songs/mod.rs create mode 100644 src/app/models/songs/song_list_model.rs create mode 100644 src/app/models/songs/song_model.rs rename src/app/models/{core.rs => songs/support.rs} (53%) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 93154afe..ab4bc61d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -5,7 +5,7 @@ "options": { "env": { "RUST_BACKTRACE": "1", - "RUST_LOG": "info", + "RUST_LOG": "spot=debug", //"https_proxy": "localhost:8080" } }, diff --git a/Cargo.lock b/Cargo.lock index 805cab65..de9f1965 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -351,9 +351,9 @@ checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" [[package]] name = "cairo-rs" -version = "0.14.9" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b5725979db0c586d98abad2193cdb612dd40ef95cd26bd99851bf93b3cb482" +checksum = "b869e97a87170f96762f9f178eae8c461147e722ba21dd8814105bf5716bf14a" dependencies = [ "bitflags", "cairo-sys-rs", @@ -364,13 +364,13 @@ dependencies = [ [[package]] name = "cairo-sys-rs" -version = "0.14.9" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b448b876970834fda82ba3aeaccadbd760206b75388fc5c1b02f1e343b697570" +checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" dependencies = [ "glib-sys", "libc", - "system-deps 3.2.0", + "system-deps", ] [[package]] @@ -403,15 +403,6 @@ dependencies = [ "nom", ] -[[package]] -name = "cfg-expr" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b412e83326147c2bb881f8b40edfbf9905b9b8abaebd0e47ca190ba62fda8f0e" -dependencies = [ - "smallvec", -] - [[package]] name = "cfg-expr" version = "0.9.0" @@ -720,12 +711,6 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6907e25393cdcc1f4f3f513d9aac1e840eb1cc341a0fccb01171f7d14d10b946" -[[package]] -name = "either" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" - [[package]] name = "encoding_rs" version = "0.8.30" @@ -949,10 +934,11 @@ dependencies = [ [[package]] name = "gdk-pixbuf" -version = "0.14.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534192cb8f01daeb8fab2c8d4baa8f9aae5b7a39130525779f5c2608e235b10f" +checksum = "172dfe1d9dfb62936bf7ad3ede2913a1b21b1e3db56990e46e00789201de9070" dependencies = [ + "bitflags", "gdk-pixbuf-sys", "gio", "glib", @@ -961,22 +947,22 @@ dependencies = [ [[package]] name = "gdk-pixbuf-sys" -version = "0.14.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f097c0704201fbc8f69c1762dc58c6947c8bb188b8ed0bc7e65259f1894fe590" +checksum = "413424d9818621fa3cfc8a3a915cdb89a7c3c507d56761b4ec83a9a98e587171" dependencies = [ "gio-sys", "glib-sys", "gobject-sys", "libc", - "system-deps 3.2.0", + "system-deps", ] [[package]] name = "gdk4" -version = "0.3.1" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f97a162c17214d1bf981af3f683156a0b1667dd1927057c4f0a68513251ecf0f" +checksum = "319c74160dbe3e29cc1bf36ae4a08b9072f352b751e9e3e5501b3aa3ca633f66" dependencies = [ "bitflags", "cairo-rs", @@ -990,19 +976,19 @@ dependencies = [ [[package]] name = "gdk4-sys" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9498f4e06969fb96a4e4234dfe1d308a3ac6b120b3c6d93e3ec5c77fe88bc6d5" +checksum = "48a39e34abe35ee2cf54a1e29dd983accecd113ad30bdead5050418fa92f2a1b" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", "gio-sys", "glib-sys", "gobject-sys", - "graphene-sys", "libc", "pango-sys", - "system-deps 5.0.0", + "pkg-config", + "system-deps", ] [[package]] @@ -1057,9 +1043,9 @@ dependencies = [ [[package]] name = "gio" -version = "0.14.8" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711c3632b3ebd095578a9c091418d10fed492da9443f58ebc8f45efbeb215cb0" +checksum = "4f80391bd2ac4981a3433137691211775bbb37c5347f8cfb7c0980187e0300c5" dependencies = [ "bitflags", "futures-channel", @@ -1074,22 +1060,22 @@ dependencies = [ [[package]] name = "gio-sys" -version = "0.14.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0a41df66e57fcc287c4bcf74fc26b884f31901ea9792ec75607289b456f48fa" +checksum = "04b57719ccaacf2a0d9c79f151be629f3a3ef3991658ee2af0bb66287e4ea86c" dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps 3.2.0", + "system-deps", "winapi", ] [[package]] name = "glib" -version = "0.14.8" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c515f1e62bf151ef6635f528d05b02c11506de986e43b34a5c920ef0b3796a4" +checksum = "4a703581e2538fe699c5476cf26b456d694c5272b6e999d3ab47711c5eaa2dd2" dependencies = [ "bitflags", "futures-channel", @@ -1102,16 +1088,17 @@ dependencies = [ "libc", "once_cell", "smallvec", + "thiserror", ] [[package]] name = "glib-macros" -version = "0.14.1" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aad66361f66796bfc73f530c51ef123970eb895ffba991a234fcf7bea89e518" +checksum = "e58b262ff65ef771003873cea8c10e0fe854f1c508d48d62a4111a1ff163f7d1" dependencies = [ "anyhow", - "heck", + "heck 0.4.0", "proc-macro-crate 1.1.0", "proc-macro-error", "proc-macro2", @@ -1121,12 +1108,12 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.14.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c1d60554a212445e2a858e42a0e48cece1bd57b311a19a9468f70376cf554ae" +checksum = "c668102c6e15e0a7f6b99b59f602c2e806967bb86414f617b77e19b1de5b3fac" dependencies = [ "libc", - "system-deps 3.2.0", + "system-deps", ] [[package]] @@ -1150,20 +1137,20 @@ dependencies = [ [[package]] name = "gobject-sys" -version = "0.14.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa92cae29759dae34ab5921d73fff5ad54b3d794ab842c117e36cafc7994c3f5" +checksum = "6edb1f0b3e4c08e2a0a490d1082ba9e902cdff8ff07091e85c6caec60d17e2ab" dependencies = [ "glib-sys", "libc", - "system-deps 3.2.0", + "system-deps", ] [[package]] name = "graphene-rs" -version = "0.14.8" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3380f132530ef9eb9e0a2bac180e30390aa5e49892d20294f822a974117a563" +checksum = "7c54f9fbbeefdb62c99f892dfca35f83991e2cb5b46a8dc2a715e58612f85570" dependencies = [ "glib", "graphene-sys", @@ -1172,21 +1159,21 @@ dependencies = [ [[package]] name = "graphene-sys" -version = "0.14.8" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a9ac7450b3aa80792513a3c029920a2ede419de13fb5169a4e51b07a5685332" +checksum = "03f311acb023cf7af5537f35de028e03706136eead7f25a31e8fd26f5011e0b3" dependencies = [ "glib-sys", "libc", "pkg-config", - "system-deps 3.2.0", + "system-deps", ] [[package]] name = "gsk4" -version = "0.3.1" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff59ca46c4fc5087fd7a0c3770a71ea4b6e94f8c24c12e2c2e8538f9f6fd764" +checksum = "0672c63e4101e19d5e9cb4a0aed8b3278e9573529bd0b6a86d9c748c71bd9882" dependencies = [ "bitflags", "cairo-rs", @@ -1200,9 +1187,9 @@ dependencies = [ [[package]] name = "gsk4-sys" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13aa53ce70234da02f9954339d988d5ab853d746a8f47a4ae17735ff873545b5" +checksum = "e31d21d7ce02ba261bb24c50c4ab238a10b41a2c97c32afffae29471b7cca69b" dependencies = [ "cairo-sys-rs", "gdk4-sys", @@ -1211,14 +1198,14 @@ dependencies = [ "graphene-sys", "libc", "pango-sys", - "system-deps 5.0.0", + "system-deps", ] [[package]] name = "gtk4" -version = "0.3.1" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58a04f421d1485ba4739e723199f5828bca05ab4e622ed39a96a342b6b1a6a3d" +checksum = "d6f9e36fb6db3d70edf5cea9f4a20928c1d08ed3f27697cfd2d21ca3d8ac4a2d" dependencies = [ "bitflags", "cairo-rs", @@ -1239,13 +1226,11 @@ dependencies = [ [[package]] name = "gtk4-macros" -version = "0.3.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5068d4354af02454f44687adc613100aa98ae11e273cdcac84f89dc08be2b4a1" +checksum = "573db42bb64973a4d5f718b73caa7204285a1a665308a23b11723d0ee56ec305" dependencies = [ "anyhow", - "heck", - "itertools", "proc-macro-crate 1.1.0", "proc-macro-error", "proc-macro2", @@ -1255,9 +1240,9 @@ dependencies = [ [[package]] name = "gtk4-sys" -version = "0.3.1" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e20a64c8f0ddcff8902ff04c130747f2fb7834a43530f75d03d6c71335733b49" +checksum = "c47c075e8f795c38f6e9a47b51a73eab77b325f83c0154979ed4d4245c36490d" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -1269,7 +1254,7 @@ dependencies = [ "gsk4-sys", "libc", "pango-sys", - "system-deps 5.0.0", + "system-deps", ] [[package]] @@ -1312,6 +1297,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1532,15 +1523,6 @@ dependencies = [ "waker-fn", ] -[[package]] -name = "itertools" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "0.4.8" @@ -1625,9 +1607,9 @@ dependencies = [ [[package]] name = "libadwaita" -version = "0.1.0-beta-1" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef1e2d4b5490caff8a1d44648f68721ab917f765a7fa1d0226fcdac42d86552" +checksum = "0d4b1d54d907dfa5d6663fdf4bdbe46c34747258b85c787adbf66187ccbaac81" dependencies = [ "gdk-pixbuf", "gdk4", @@ -1642,9 +1624,9 @@ dependencies = [ [[package]] name = "libadwaita-sys" -version = "0.1.0-beta-1" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a165d8c96824753ec072b70a9149790efa3d5abb07e130cda7eb04ef2006d4c" +checksum = "f18b6ac4cadd252a89f5cba0a5a4e99836131795d6fad37b859ac79e8cb7d2c8" dependencies = [ "gdk4-sys", "gio-sys", @@ -1652,7 +1634,7 @@ dependencies = [ "gobject-sys", "gtk4-sys", "libc", - "system-deps 3.2.0", + "system-deps", ] [[package]] @@ -2456,9 +2438,9 @@ dependencies = [ [[package]] name = "pango" -version = "0.14.8" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "546fd59801e5ca735af82839007edd226fe7d3bb06433ec48072be4439c28581" +checksum = "79211eff430c29cc38c69e0ab54bc78fa1568121ca9737707eee7f92a8417a94" dependencies = [ "bitflags", "glib", @@ -2469,14 +2451,14 @@ dependencies = [ [[package]] name = "pango-sys" -version = "0.14.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2367099ca5e761546ba1d501955079f097caa186bb53ce0f718dca99ac1942fe" +checksum = "7022c2fb88cd2d9d55e1a708a8c53a3ae8678234c4a54bf623400aeb7f31fac2" dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps 3.2.0", + "system-deps", ] [[package]] @@ -3102,24 +3084,6 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" -[[package]] -name = "strum" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2" - -[[package]] -name = "strum_macros" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "subtle" version = "2.4.1" @@ -3151,30 +3115,12 @@ dependencies = [ [[package]] name = "system-deps" -version = "3.2.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "480c269f870722b3b08d2f13053ce0c2ab722839f472863c3e2d61ff3a1c2fa6" +checksum = "7b1487aaddaacbc5d60a2a507ba1617c5ca66c57dd0dd07d0c5efd5b693841d4" dependencies = [ - "anyhow", - "cfg-expr 0.8.1", - "heck", - "itertools", - "pkg-config", - "strum", - "strum_macros", - "thiserror", - "toml", - "version-compare", -] - -[[package]] -name = "system-deps" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18db855554db7bd0e73e06cf7ba3df39f97812cb11d3f75e71c39bf45171797e" -dependencies = [ - "cfg-expr 0.9.0", - "heck", + "cfg-expr", + "heck 0.3.3", "pkg-config", "toml", "version-compare", @@ -3467,9 +3413,9 @@ dependencies = [ [[package]] name = "version-compare" -version = "0.0.11" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" +checksum = "fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73" [[package]] name = "version_check" diff --git a/Cargo.toml b/Cargo.toml index 1f7cf68e..157b1a90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,19 +5,19 @@ edition = "2018" license = "MIT" [dependencies.gtk] -version = "^0.3.0" +version = "^0.4.1" package = "gtk4" [dependencies.gdk] -version = "^0.3.0" +version = "^0.4.0" package = "gdk4" [dependencies.gio] -version = "^0.14.8" +version = "^0.15.2" features = ["v2_60"] [dependencies.glib] -version = "^0.14.8" +version = "^0.15.2" features = ["v2_60"] [dependencies.librespot] @@ -57,7 +57,7 @@ features = ["gettext-system"] [dependencies] secret-service = "^2.0.1" -gdk-pixbuf = "^0.14.0" +gdk-pixbuf = "^0.15.1" libadwaita = "^0.1.0-beta-1" ref_filter_map = "^1.0.1" regex = "^1.4.6" diff --git a/src/api/api_models.rs b/src/api/api_models.rs index 61525997..64bf2e5c 100644 --- a/src/api/api_models.rs +++ b/src/api/api_models.rs @@ -208,6 +208,7 @@ pub struct Album { pub struct AlbumInfo { pub label: String, pub copyrights: Vec, + pub total_tracks: u32, } #[derive(Deserialize, Debug, Clone)] @@ -435,12 +436,10 @@ impl From for AlbumDescription { name: a.name.clone(), }) .collect::>(); - let songs = SongList::new_from_initial_batch( - album - .clone() - .try_into() - .unwrap_or_else(|_| SongBatch::empty()), - ); //FIXME + let songs = album + .clone() + .try_into() + .unwrap_or_else(|_| SongBatch::empty()); let art = album.best_image_for_width(200).map(|i| i.url.clone()); Self { @@ -456,7 +455,13 @@ impl From for AlbumDescription { } impl From for AlbumReleaseDetails { - fn from(AlbumInfo { label, copyrights }: AlbumInfo) -> Self { + fn from( + AlbumInfo { + label, + copyrights, + total_tracks, + }: AlbumInfo, + ) -> Self { let copyright_text = copyrights .iter() .map(|c| format!("[{}] {}", c.type_, c.text)) @@ -466,6 +471,7 @@ impl From for AlbumReleaseDetails { Self { label, copyright_text, + total_tracks: total_tracks as usize, } } } @@ -489,7 +495,7 @@ impl From for PlaylistDescription { id, title: name, art, - songs: SongList::new_from_initial_batch(song_batch), + songs: song_batch, owner: UserRef { id: owner_id, display_name, diff --git a/src/app/components/artist_details/artist_details.rs b/src/app/components/artist_details/artist_details.rs index 0425621c..729a81f1 100644 --- a/src/app/components/artist_details/artist_details.rs +++ b/src/app/components/artist_details/artist_details.rs @@ -90,7 +90,7 @@ impl ArtistDetailsWidget { store: &ListStore, on_album_pressed: F, ) where - F: Fn(&String) + Clone + 'static, + F: Fn(String) + Clone + 'static, { self.widget() .artist_releases @@ -100,9 +100,7 @@ impl ArtistDetailsWidget { 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); - } + f(item.uri()); })); child.set_child(Some(&album)); child.upcast::() @@ -140,7 +138,6 @@ impl ArtistDetails { widget.top_tracks_widget().clone(), Rc::clone(&model), worker, - true, )); Self { diff --git a/src/app/components/artist_details/artist_details_model.rs b/src/app/components/artist_details/artist_details_model.rs index 94316d9f..f4e542bf 100644 --- a/src/app/components/artist_details/artist_details_model.rs +++ b/src/app/components/artist_details/artist_details_model.rs @@ -10,7 +10,7 @@ use crate::app::state::SelectionContext; use crate::app::state::{ BrowserAction, BrowserEvent, PlaybackAction, SelectionAction, SelectionState, }; -use crate::app::{ActionDispatcher, AppAction, AppEvent, AppModel, ListDiff, ListStore}; +use crate::app::{ActionDispatcher, AppAction, AppEvent, AppModel, ListStore}; pub struct ArtistDetailsModel { pub id: String, @@ -27,11 +27,6 @@ impl ArtistDetailsModel { } } - fn songs_ref(&self) -> Option> + '_> { - self.app_model - .map_state_opt(|s| Some(&s.browser.artist_state(&self.id)?.top_tracks)) - } - pub fn get_artist_name(&self) -> Option + '_> { self.app_model .map_state_opt(|s| s.browser.artist_state(&self.id)?.artist.as_ref()) @@ -52,9 +47,8 @@ impl ArtistDetailsModel { }); } - pub fn open_album(&self, id: &str) { - self.dispatcher - .dispatch(AppAction::ViewAlbum(id.to_string())); + pub fn open_album(&self, id: String) { + self.dispatcher.dispatch(AppAction::ViewAlbum(id)); } pub fn load_more(&self) -> Option<()> { @@ -78,39 +72,31 @@ impl ArtistDetailsModel { } impl PlaylistModel for ArtistDetailsModel { - fn current_song_id(&self) -> Option { + fn song_list_model(&self) -> SongListModel { self.app_model .get_state() - .playback - .current_song_id() - .cloned() + .browser + .artist_state(&self.id) + .expect("illegal attempt to read artist_state") + .top_tracks + .clone() } - fn play_song_at(&self, _pos: usize, id: &str) { - let tracks = self.songs_ref(); - if let Some(tracks) = tracks { - self.dispatcher - .dispatch(PlaybackAction::LoadSongs(tracks.clone()).into()); - self.dispatcher - .dispatch(PlaybackAction::Load(id.to_string()).into()); - } + fn current_song_id(&self) -> Option { + self.app_model.get_state().playback.current_song_id() } - fn diff_for_event(&self, event: &AppEvent) -> Option> { - if matches!( - event, - AppEvent::BrowserEvent(BrowserEvent::ArtistDetailsUpdated(id)) if id == &self.id - ) { - let tracks = self.songs_ref()?; - Some(ListDiff::Set(tracks.iter().map(|s| s.into()).collect())) - } else { - None - } + fn play_song_at(&self, _pos: usize, id: &str) { + let tracks: Vec = self.song_list_model().collect(); + self.dispatcher + .dispatch(PlaybackAction::LoadSongs(tracks).into()); + self.dispatcher + .dispatch(PlaybackAction::Load(id.to_string()).into()); } fn actions_for(&self, id: &str) -> Option { - let songs = self.songs_ref()?; - let song = songs.iter().find(|&song| song.id == id)?; + let song = self.song_list_model().get(id)?; + let song = song.description(); let group = SimpleActionGroup::new(); @@ -125,8 +111,8 @@ impl PlaylistModel for ArtistDetailsModel { } fn menu_for(&self, id: &str) -> Option { - let songs = self.songs_ref()?; - let song = songs.iter().find(|&song| song.id == id)?; + let song = self.song_list_model().get(id)?; + let song = song.description(); let menu = gio::Menu::new(); menu.append(Some(&*labels::VIEW_ALBUM), Some("song.view_album")); @@ -143,12 +129,10 @@ impl PlaylistModel for ArtistDetailsModel { } fn select_song(&self, id: &str) { - let song = self - .songs_ref() - .and_then(|songs| songs.iter().find(|&song| song.id == id).cloned()); + let song = self.song_list_model().get(id); if let Some(song) = song { self.dispatcher - .dispatch(SelectionAction::Select(vec![song]).into()); + .dispatch(SelectionAction::Select(vec![song.into_description()]).into()); } } @@ -185,10 +169,8 @@ impl SimpleHeaderBarModel for ArtistDetailsModel { } fn select_all(&self) { - if let Some(songs) = self.songs_ref() { - let songs: Vec = songs.iter().cloned().collect(); - self.dispatcher - .dispatch(SelectionAction::Select(songs).into()); - } + let songs: Vec = self.song_list_model().collect(); + self.dispatcher + .dispatch(SelectionAction::Select(songs).into()); } } diff --git a/src/app/components/details/details.rs b/src/app/components/details/details.rs index 9dca0b13..93b4feaf 100644 --- a/src/app/components/details/details.rs +++ b/src/app/components/details/details.rs @@ -218,7 +218,6 @@ impl Details { widget.album_tracks_widget().clone(), model.clone(), worker.clone(), - false, )); let headerbar_widget = widget.headerbar_widget(); @@ -288,8 +287,7 @@ impl Details { &album.artists_name(), &details.label, album.release_date.as_ref().unwrap(), - album.songs.len(), - &album.formatted_time(), + details.total_tracks, &details.copyright_text, ); diff --git a/src/app/components/details/details_model.rs b/src/app/components/details/details_model.rs index 29c89f8b..e3598fb3 100644 --- a/src/app/components/details/details_model.rs +++ b/src/app/components/details/details_model.rs @@ -12,10 +12,8 @@ use crate::app::components::SimpleHeaderBarModelWrapper; use crate::app::dispatch::ActionDispatcher; use crate::app::models::*; use crate::app::state::SelectionContext; -use crate::app::state::{ - BrowserAction, BrowserEvent, PlaybackAction, SelectionAction, SelectionState, -}; -use crate::app::{AppAction, AppEvent, AppModel, AppState, BatchQuery, ListDiff, SongsSource}; +use crate::app::state::{BrowserAction, PlaybackAction, SelectionAction, SelectionState}; +use crate::app::{AppAction, AppEvent, AppModel, AppState, BatchQuery, SongsSource}; pub struct DetailsModel { pub id: String, @@ -36,19 +34,6 @@ impl DetailsModel { self.app_model.get_state() } - fn songs_ref(&self) -> Option + '_> { - self.app_model.map_state_opt(|s| { - Some( - &s.browser - .details_state(&self.id)? - .content - .as_ref()? - .description - .songs, - ) - }) - } - pub fn get_album_info(&self) -> Option + '_> { self.app_model .map_state_opt(|s| s.browser.details_state(&self.id)?.content.as_ref()) @@ -108,7 +93,7 @@ impl DetailsModel { } pub fn load_more(&self) -> Option<()> { - let last_batch = self.songs_ref()?.last_batch()?; + let last_batch = self.song_list_model().last_batch()?; let query = BatchQuery { source: SongsSource::Album(self.id.clone()), batch: last_batch, @@ -140,11 +125,25 @@ impl DetailsModel { } impl PlaylistModel for DetailsModel { + fn song_list_model(&self) -> SongListModel { + self.app_model + .get_state() + .browser + .details_state(&self.id) + .expect("illegal attempt to read details_state") + .songs + .clone() + } + + fn show_song_covers(&self) -> bool { + false + } + fn select_song(&self, id: &str) { - let song = self.songs_ref().and_then(|songs| songs.get(id).cloned()); - if let Some(song) = song { + let songs = self.song_list_model(); + if let Some(song) = songs.get(id) { self.dispatcher - .dispatch(SelectionAction::Select(vec![song]).into()); + .dispatch(SelectionAction::Select(vec![song.description().clone()]).into()); } } @@ -164,12 +163,12 @@ impl PlaylistModel for DetailsModel { } fn current_song_id(&self) -> Option { - self.state().playback.current_song_id().cloned() + self.state().playback.current_song_id() } fn play_song_at(&self, pos: usize, id: &str) { let source = SongsSource::Album(self.id.clone()); - let batch = self.songs_ref().and_then(|songs| songs.song_batch_for(pos)); + let batch = self.song_list_model().song_batch_for(pos); if let Some(batch) = batch { self.dispatcher .dispatch(PlaybackAction::LoadPagedSongs(source, batch).into()); @@ -178,27 +177,9 @@ impl PlaylistModel for DetailsModel { } } - fn diff_for_event(&self, event: &AppEvent) -> Option> { - match event { - AppEvent::BrowserEvent(BrowserEvent::AlbumDetailsLoaded(id)) if id == &self.id => { - let songs = self.songs_ref()?; - Some(ListDiff::Set(songs.iter().map(|s| s.into()).collect())) - } - AppEvent::BrowserEvent(BrowserEvent::AlbumTracksAppended(id, index)) - if id == &self.id => - { - let songs = self.songs_ref()?; - Some(ListDiff::Append( - songs.iter().skip(*index).map(|s| s.into()).collect(), - )) - } - _ => None, - } - } - fn actions_for(&self, id: &str) -> Option { - let songs = self.songs_ref()?; - let song = songs.get(id)?; + let song = self.song_list_model().get(id)?; + let song = song.description(); let group = SimpleActionGroup::new(); @@ -212,8 +193,8 @@ impl PlaylistModel for DetailsModel { } fn menu_for(&self, id: &str) -> Option { - let songs = self.songs_ref()?; - let song = songs.get(id)?; + let song = self.song_list_model().get(id)?; + let song = song.description(); let menu = gio::Menu::new(); for artist in song.artists.iter() { @@ -243,10 +224,8 @@ impl SimpleHeaderBarModel for DetailsModel { } fn select_all(&self) { - if let Some(songs) = self.songs_ref() { - let songs: Vec = songs.iter().cloned().collect(); - self.dispatcher - .dispatch(SelectionAction::Select(songs).into()); - } + let songs: Vec = self.song_list_model().collect(); + self.dispatcher + .dispatch(SelectionAction::Select(songs).into()); } } diff --git a/src/app/components/details/release_details.rs b/src/app/components/details/release_details.rs index fd46d752..4226a433 100644 --- a/src/app/components/details/release_details.rs +++ b/src/app/components/details/release_details.rs @@ -24,9 +24,6 @@ mod imp { #[template_child] pub tracks: TemplateChild, - #[template_child] - pub duration: TemplateChild, - #[template_child] pub copyright: TemplateChild, } @@ -78,7 +75,6 @@ impl ReleaseDetailsWindow { label: &str, release_date: &str, track_count: usize, - duration: &str, copyright: &str, ) { let widget = self.widget(); @@ -90,7 +86,6 @@ impl ReleaseDetailsWindow { widget.label.set_text(label); widget.release.set_text(release_date); widget.tracks.set_text(&track_count.to_string()); - widget.duration.set_text(duration); widget.copyright.set_text(copyright); } } diff --git a/src/app/components/details/release_details.ui b/src/app/components/details/release_details.ui index 00745763..9f3cbee4 100644 --- a/src/app/components/details/release_details.ui +++ b/src/app/components/details/release_details.ui @@ -63,16 +63,6 @@ - - - Duration - - - Duration - - - - Copyright diff --git a/src/app/components/headerbar/component.rs b/src/app/components/headerbar/component.rs index 4f6e2912..a230d6e3 100644 --- a/src/app/components/headerbar/component.rs +++ b/src/app/components/headerbar/component.rs @@ -252,7 +252,7 @@ where let widget = HeaderBarWidget::new(); common::bind_headerbar(&widget, &model); - let root = gtk::BoxBuilder::new() + let root = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .build(); root.append(&widget); diff --git a/src/app/components/library/library.rs b/src/app/components/library/library.rs index c09a0cca..cf97f578 100644 --- a/src/app/components/library/library.rs +++ b/src/app/components/library/library.rs @@ -72,7 +72,7 @@ impl LibraryWidget { fn bind_albums(&self, worker: Worker, store: &ListStore, on_album_pressed: F) where - F: Fn(&String) + Clone + 'static, + F: Fn(String) + Clone + 'static, { imp::LibraryWidget::from_instance(self).flowbox.bind_model( Some(store.unsafe_store()), @@ -81,9 +81,7 @@ impl LibraryWidget { 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); - } + f(album_model.uri()); })); album }) @@ -122,7 +120,7 @@ impl Library { self.worker.clone(), &*self.model.get_list_store().unwrap(), clone!(@weak self.model as model => move |id| { - model.open_album(id.clone()); + model.open_album(id); }), ); } diff --git a/src/app/components/login/login.rs b/src/app/components/login/login.rs index e80fc8ba..63b894ca 100644 --- a/src/app/components/login/login.rs +++ b/src/app/components/login/login.rs @@ -88,8 +88,8 @@ impl LoginWindow { controller.set_propagation_phase(gtk::PropagationPhase::Capture); controller.connect_key_pressed( clone!(@weak self as _self => @default-return gtk::Inhibit(false), move |_, key, _, modifier| { - _self.show_caps_lock_info((modifier == gdk::ModifierType::LOCK_MASK) ^ (key == gdk::keys::constants::Caps_Lock)); - if key == gdk::keys::constants::Return { + _self.show_caps_lock_info((modifier == gdk::ModifierType::LOCK_MASK) ^ (key == gdk::Key::Caps_Lock)); + if key == gdk::Key::Return { _self.submit(&on_submit_clone); gtk::Inhibit(true) } else { diff --git a/src/app/components/navigation/home.rs b/src/app/components/navigation/home.rs index 0d1c50ec..313404e2 100644 --- a/src/app/components/navigation/home.rs +++ b/src/app/components/navigation/home.rs @@ -27,12 +27,12 @@ fn add_to_stack_and_listbox( } fn make_playlist_item(playlist_item: AlbumModel) -> SideBarItem { - let mut title = playlist_item.album_title().unwrap(); + let mut title = playlist_item.album_title(); if title.is_empty() { title = gettext("Unnamed playlist"); } - let id = playlist_item.uri().unwrap(); + let id = playlist_item.uri(); SideBarItem::new(id.as_str(), &title, "playlist2-symbolic", false) } @@ -136,14 +136,12 @@ impl HomePane { } fn update_playlists_in_sidebar(&mut self) { - let mut vec = Vec::new(); let playlists = self.saved_playlists_model.get_playlists(); - for (i, playlist_item) in playlists.iter().enumerate() { - if i == NUM_PLAYLISTS { - break; - } - vec.push(make_playlist_item(playlist_item).upcast()); - } + let vec: Vec = playlists + .iter() + .take(NUM_PLAYLISTS) + .map(make_playlist_item) + .collect(); self.list_store.splice( NUM_FIXED_ENTRIES, self.list_store.n_items() - NUM_FIXED_ENTRIES, diff --git a/src/app/components/notification/mod.rs b/src/app/components/notification/mod.rs index 5d171c1c..becb9848 100644 --- a/src/app/components/notification/mod.rs +++ b/src/app/components/notification/mod.rs @@ -11,7 +11,7 @@ impl Notification { } fn show(&self, content: &str) { - let toast = libadwaita::ToastBuilder::new() + let toast = libadwaita::Toast::builder() .title(content) .timeout(4) .build(); diff --git a/src/app/components/now_playing/now_playing.rs b/src/app/components/now_playing/now_playing.rs index 0bfb2369..17f119da 100644 --- a/src/app/components/now_playing/now_playing.rs +++ b/src/app/components/now_playing/now_playing.rs @@ -85,12 +85,7 @@ impl NowPlaying { model.load_more(); })); - let playlist = Playlist::new( - widget.song_list_widget().clone(), - model.clone(), - worker, - true, - ); + let playlist = Playlist::new(widget.song_list_widget().clone(), model.clone(), worker); Self { widget, diff --git a/src/app/components/now_playing/now_playing_model.rs b/src/app/components/now_playing/now_playing_model.rs index 89bc6e9a..d6f94871 100644 --- a/src/app/components/now_playing/now_playing_model.rs +++ b/src/app/components/now_playing/now_playing_model.rs @@ -1,20 +1,16 @@ use gettextrs::gettext; use gio::prelude::*; use gio::SimpleActionGroup; -use std::cell::Ref; use std::ops::Deref; use std::rc::Rc; use crate::app::components::SimpleHeaderBarModel; use crate::app::components::{labels, PlaylistModel}; use crate::app::models::SongDescription; -use crate::app::models::SongModel; -use crate::app::state::PlaylistChange; +use crate::app::models::SongListModel; use crate::app::state::SelectionContext; -use crate::app::state::{ - PlaybackAction, PlaybackEvent, PlaybackState, SelectionAction, SelectionState, -}; -use crate::app::{ActionDispatcher, AppAction, AppEvent, AppModel, AppState, ListDiff}; +use crate::app::state::{PlaybackAction, PlaybackState, SelectionAction, SelectionState}; +use crate::app::{ActionDispatcher, AppAction, AppEvent, AppModel}; pub struct NowPlayingModel { app_model: Rc, @@ -29,18 +25,15 @@ impl NowPlayingModel { } } - fn state(&self) -> Ref<'_, AppState> { - self.app_model.get_state() - } - - fn queue(&self) -> Ref<'_, PlaybackState> { - Ref::map(self.state(), |s| &s.playback) + fn queue(&self) -> impl Deref + '_ { + self.app_model.map_state(|s| &s.playback) } pub fn load_more(&self) -> Option<()> { let queue = self.queue(); let loader = self.app_model.get_batch_loader(); let query = queue.next_query()?; + debug!("next_query = {:?}", &query); self.dispatcher.dispatch_async(Box::pin(async move { let source = query.source.clone(); @@ -57,8 +50,12 @@ impl NowPlayingModel { } impl PlaylistModel for NowPlayingModel { + fn song_list_model(&self) -> SongListModel { + self.queue().songs().clone() + } + fn current_song_id(&self) -> Option { - self.queue().current_song_id().cloned() + self.queue().current_song_id() } fn play_song_at(&self, _pos: usize, id: &str) { @@ -66,31 +63,14 @@ impl PlaylistModel for NowPlayingModel { .dispatch(PlaybackAction::Load(id.to_string()).into()); } - fn diff_for_event(&self, event: &AppEvent) -> Option> { - let queue = self.queue(); - let songs = queue.songs().map(|s| s.into()); - - match event { - AppEvent::PlaybackEvent(PlaybackEvent::PlaylistChanged(change)) => match change { - PlaylistChange::Reset => Some(ListDiff::Set(songs.collect())), - PlaylistChange::InsertedAt(i, n) => { - Some(ListDiff::Insert(*i, songs.skip(*i).take(*n).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, - } - } - fn autoscroll_to_playing(&self) -> bool { true } fn actions_for(&self, id: &str) -> Option { let queue = self.queue(); - let song = queue.song(id)?; + let song = queue.songs().get(id)?; + let song = song.description(); let group = SimpleActionGroup::new(); for view_artist in song.make_artist_actions(self.dispatcher.box_clone(), None) { @@ -105,7 +85,8 @@ impl PlaylistModel for NowPlayingModel { fn menu_for(&self, id: &str) -> Option { let queue = self.queue(); - let song = queue.song(id)?; + let song = queue.songs().get(id)?; + let song = song.description(); let menu = gio::Menu::new(); menu.append(Some(&*labels::VIEW_ALBUM), Some("song.view_album")); @@ -124,9 +105,10 @@ impl PlaylistModel for NowPlayingModel { fn select_song(&self, id: &str) { let queue = self.queue(); - if let Some(song) = queue.song(id) { + if let Some(song) = queue.songs().get(id) { + let song = song.description().clone(); self.dispatcher - .dispatch(SelectionAction::Select(vec![song.clone()]).into()); + .dispatch(SelectionAction::Select(vec![song]).into()); } } @@ -161,7 +143,7 @@ impl SimpleHeaderBarModel for NowPlayingModel { } fn select_all(&self) { - let songs: Vec = self.queue().songs().cloned().collect(); + let songs: Vec = self.queue().songs().collect(); self.dispatcher .dispatch(SelectionAction::Select(songs).into()); } diff --git a/src/app/components/playback/component.rs b/src/app/components/playback/component.rs index 7fc33aca..7ed8ff31 100644 --- a/src/app/components/playback/component.rs +++ b/src/app/components/playback/component.rs @@ -41,8 +41,8 @@ impl PlaybackModel { self.state().playback.is_shuffled() } - fn current_song(&self) -> Option + '_> { - self.app_model.map_state_opt(|s| s.playback.current_song()) + fn current_song(&self) -> Option { + self.app_model.get_state().playback.current_song() } fn play_next_song(&self) { @@ -116,7 +116,7 @@ impl PlaybackControl { 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() { + if let Some(url) = song.art { self.widget.set_artwork_from_url(url, &self.worker); } } else { diff --git a/src/app/components/playlist/playlist.rs b/src/app/components/playlist/playlist.rs index 10a8ac06..40d99578 100644 --- a/src/app/components/playlist/playlist.rs +++ b/src/app/components/playlist/playlist.rs @@ -5,29 +5,25 @@ use std::rc::Rc; 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}, - AppEvent, ListDiff, ListStore, Worker, -}; - -#[derive(Clone, Copy, Debug)] -struct RowState { - is_selected: bool, - is_playing: bool, -} +use crate::app::models::{SongListModel, SongModel, SongState}; +use crate::app::state::{PlaybackEvent, SelectionEvent, SelectionState}; +use crate::app::{AppEvent, Worker}; pub trait PlaylistModel { + fn song_list_model(&self) -> SongListModel; + fn current_song_id(&self) -> Option; fn play_song_at(&self, pos: usize, id: &str); - fn diff_for_event(&self, event: &AppEvent) -> Option>; - fn autoscroll_to_playing(&self) -> bool { true } + fn show_song_covers(&self) -> bool { + true + } + fn actions_for(&self, _id: &str) -> Option { None } @@ -50,13 +46,34 @@ pub trait PlaylistModel { .map(|s| s.is_selection_enabled()) .unwrap_or(false) } + + fn song_state(&self, id: &str) -> SongState { + let is_playing = self.current_song_id().map(|s| s.eq(id)).unwrap_or(false); + let is_selected = self + .selection() + .map(|s| s.is_song_selected(id)) + .unwrap_or(false); + SongState { + is_selected, + is_playing, + } + } + + fn toggle_select(&self, id: &str) { + if let Some(selection) = self.selection() { + if selection.is_song_selected(id) { + self.deselect_song(id); + } else { + self.select_song(id); + } + } + } } pub struct Playlist { animator: AnimatorDefault, listview: gtk::ListView, _press_gesture: gtk::GestureLongPress, - list_model: ListStore, model: Rc, } @@ -64,14 +81,9 @@ impl Playlist where Model: PlaylistModel + 'static, { - pub fn new( - listview: gtk::ListView, - model: Rc, - worker: Worker, - show_song_covers: bool, - ) -> Self { - let list_model = ListStore::new(); - let selection_model = gtk::NoSelection::new(Some(list_model.unsafe_store())); + pub fn new(listview: gtk::ListView, model: Rc, worker: Worker) -> Self { + let list_model = model.song_list_model(); + let selection_model = gtk::NoSelection::new(Some(&list_model)); let factory = gtk::SignalListItemFactory::new(); let style_context = listview.style_context(); @@ -90,10 +102,10 @@ where 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)); + song_model.set_state(model.song_state(&song_model.get_id())); let widget = item.child().unwrap().downcast::().unwrap(); - widget.bind(&song_model, worker.clone(), show_song_covers); + widget.bind(&song_model, worker.clone(), model.show_song_covers()); let id = &song_model.get_id(); widget.set_actions(model.actions_for(id).as_ref()); @@ -106,12 +118,13 @@ where }); listview.connect_activate(clone!(@weak list_model, @weak model => move |_, position| { - let song = list_model.get(position); + let song = list_model.index_continuous(position as usize).expect("attempt to access invalid index"); + let song = song.description(); let selection_enabled = model.is_selection_enabled(); if selection_enabled { - Self::select_song(&*model, &song); + model.toggle_select(&song.id); } else { - model.play_song_at(position as usize, &song.get_id()); + model.play_song_at(position as usize, &song.id); } })); @@ -127,41 +140,17 @@ where animator: AnimatorDefault::ease_in_out_animator(), listview, _press_gesture: press_gesture, - list_model, model, } } - fn select_song(model: &Model, song: &SongModel) { - if let Some(selection) = model.selection() { - if selection.is_song_selected(&song.get_id()) { - model.deselect_song(&song.get_id()); - } else { - model.select_song(&song.get_id()); - } - } - } - - fn get_item_state(model: &Model, item: &SongModel) -> RowState { - let id = &item.get_id(); - 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)) - .unwrap_or(false); - RowState { - is_selected, - is_playing, - } - } - fn autoscroll_to_playing(&self, index: usize) { - let len = self.list_model.len() as f64; + let len = 1f64; //self.model.song_list_model().len() as f64; let adj = self .listview .parent() .and_then(|p| p.downcast::().ok()) - .and_then(|w| w.vadjustment()); + .map(|w| w.vadjustment()); if let Some(adj) = adj { let v = adj.value(); let pos = (index as f64) * adj.upper() / len; @@ -179,32 +168,39 @@ where } fn update_list(&self) { - for (i, model_song) in self.list_model.iter().enumerate() { - let state = Self::get_item_state(&*self.model, &model_song); + let autoscroll_to_playing = self.model.autoscroll_to_playing(); + let is_selection_enabled = self.model.is_selection_enabled(); + + self.model.song_list_model().for_each(|i, model_song| { + let state = self.model.song_state(&model_song.get_id()); model_song.set_state(state); - if state.is_playing - && self.model.autoscroll_to_playing() - && !self.model.is_selection_enabled() - { + if state.is_playing && autoscroll_to_playing && !is_selection_enabled { self.autoscroll_to_playing(i); } - } + }); } fn set_selection_active(listview: >k::ListView, active: bool) { + let class_name = "playlist--selectable"; let context = listview.style_context(); if active { - context.add_class("playlist--selectable"); + context.add_class(class_name); } else { - context.remove_class("playlist--selectable"); + context.remove_class(class_name); } } } impl SongModel { - fn set_state(&self, state: RowState) { - self.set_playing(state.is_playing); - self.set_selected(state.is_selected); + fn set_state( + &self, + SongState { + is_playing, + is_selected, + }: SongState, + ) { + self.set_playing(is_playing); + self.set_selected(is_selected); } } @@ -213,22 +209,18 @@ where Model: PlaylistModel + 'static, { fn on_event(&mut self, event: &AppEvent) { - if let Some(diff) = self.model.diff_for_event(event) { - self.list_model.update(diff); - } else { - match event { - AppEvent::SelectionEvent(SelectionEvent::SelectionChanged) => { - self.update_list(); - } - AppEvent::PlaybackEvent(PlaybackEvent::TrackChanged(_)) => { - self.update_list(); - } - AppEvent::SelectionEvent(SelectionEvent::SelectionModeChanged(_)) => { - Self::set_selection_active(&self.listview, self.model.is_selection_enabled()); - self.update_list(); - } - _ => {} + match event { + AppEvent::SelectionEvent(SelectionEvent::SelectionChanged) => { + self.update_list(); + } + AppEvent::PlaybackEvent(PlaybackEvent::TrackChanged(_)) => { + self.update_list(); + } + AppEvent::SelectionEvent(SelectionEvent::SelectionModeChanged(_)) => { + Self::set_selection_active(&self.listview, self.model.is_selection_enabled()); + self.update_list(); } + _ => {} } } } diff --git a/src/app/components/playlist/song.rs b/src/app/components/playlist/song.rs index 21c59edb..c37a8f96 100644 --- a/src/app/components/playlist/song.rs +++ b/src/app/components/playlist/song.rs @@ -9,8 +9,11 @@ use gtk::subclass::prelude::*; use gtk::CompositeTemplate; mod imp { + use super::*; + const SONG_CLASS: &str = "song--playing"; + #[derive(Debug, Default, CompositeTemplate)] #[template(resource = "/dev/alextren/Spot/components/song.ui")] pub struct SongWidget { @@ -54,11 +57,77 @@ mod imp { } } + lazy_static! { + static ref PROPERTIES: [glib::ParamSpec; 2] = [ + glib::ParamSpecBoolean::new( + "playing", + "Playing", + "", + false, + glib::ParamFlags::READWRITE + ), + glib::ParamSpecBoolean::new( + "selected", + "Selected", + "", + false, + glib::ParamFlags::READWRITE, + ), + ]; + } + impl ObjectImpl for SongWidget { + fn properties() -> &'static [glib::ParamSpec] { + &*PROPERTIES + } + + fn set_property( + &self, + obj: &Self::Type, + _id: usize, + value: &glib::Value, + pspec: &glib::ParamSpec, + ) { + match pspec.name() { + "playing" => { + let is_playing = value + .get() + .expect("type conformity checked by `Object::set_property`"); + let context = obj.style_context(); + if is_playing { + context.add_class(SONG_CLASS); + } else { + context.remove_class(SONG_CLASS); + } + } + "selected" => { + let is_selected = value + .get() + .expect("type conformity checked by `Object::set_property`"); + self.song_checkbox.set_active(is_selected); + } + _ => unimplemented!(), + } + } + + fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + match pspec.name() { + "playing" => obj.style_context().has_class(SONG_CLASS).to_value(), + "selected" => self.song_checkbox.is_active().to_value(), + _ => unimplemented!(), + } + } + fn constructed(&self, obj: &Self::Type) { self.parent_constructed(obj); self.song_checkbox.set_sensitive(false); } + + fn dispose(&self, obj: &Self::Type) { + while let Some(child) = obj.first_child() { + child.unparent(); + } + } } impl WidgetImpl for SongWidget {} @@ -79,12 +148,6 @@ impl SongWidget { imp::SongWidget::from_instance(self) } - pub fn for_model(model: SongModel, worker: Worker) -> Self { - let _self = Self::new(); - _self.bind(&model, worker, true); - _self - } - pub fn set_actions(&self, actions: Option<&gio::ActionGroup>) { self.insert_action_group("song", actions); } @@ -100,22 +163,6 @@ impl SongWidget { } } - 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); - } - fn set_show_cover(&self, show_cover: bool) { let song_class = "song--cover"; let context = self.style_context(); @@ -131,7 +178,7 @@ impl SongWidget { } pub fn set_art(&self, model: &SongModel, worker: Worker) { - if let Some(url) = model.cover_url() { + if let Some(url) = model.description().art.clone() { let _self = self.downgrade(); worker.send_local_task(async move { if let Some(_self) = _self.upgrade() { @@ -149,6 +196,8 @@ impl SongWidget { model.bind_title(&*widget.song_title, "label"); model.bind_artist(&*widget.song_artist, "label"); model.bind_duration(&*widget.song_length, "label"); + model.bind_playing(self, "playing"); + model.bind_selected(self, "selected"); self.set_show_cover(show_cover); if show_cover { @@ -156,15 +205,5 @@ impl SongWidget { } else { model.bind_index(&*widget.song_index, "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_details/playlist_details.rs b/src/app/components/playlist_details/playlist_details.rs index 6686e3b2..ad78c4f5 100644 --- a/src/app/components/playlist_details/playlist_details.rs +++ b/src/app/components/playlist_details/playlist_details.rs @@ -164,7 +164,6 @@ impl PlaylistDetails { widget.playlist_tracks_widget().clone(), model.clone(), worker.clone(), - true, )); widget.connect_header(); diff --git a/src/app/components/playlist_details/playlist_details_model.rs b/src/app/components/playlist_details/playlist_details_model.rs index a9444c51..62781748 100644 --- a/src/app/components/playlist_details/playlist_details_model.rs +++ b/src/app/components/playlist_details/playlist_details_model.rs @@ -1,6 +1,5 @@ use gio::prelude::*; use gio::SimpleActionGroup; -use std::cell::Ref; use std::ops::Deref; use std::rc::Rc; @@ -8,12 +7,8 @@ use crate::app::components::SimpleHeaderBarModel; use crate::app::components::{labels, PlaylistModel}; use crate::app::models::*; use crate::app::state::SelectionContext; -use crate::app::state::{ - BrowserAction, BrowserEvent, PlaybackAction, SelectionAction, SelectionState, -}; -use crate::app::{ - ActionDispatcher, AppAction, AppEvent, AppModel, AppState, BatchQuery, ListDiff, SongsSource, -}; +use crate::app::state::{BrowserAction, PlaybackAction, SelectionAction, SelectionState}; +use crate::app::{ActionDispatcher, AppAction, AppEvent, AppModel, BatchQuery, SongsSource}; pub struct PlaylistDetailsModel { pub id: String, @@ -32,27 +27,11 @@ impl PlaylistDetailsModel { } } - fn state(&self) -> Ref<'_, AppState> { - self.app_model.get_state() - } - fn is_playlist_editable(&self) -> bool { let state = self.app_model.get_state(); state.logged_user.playlists.iter().any(|p| p.id == self.id) } - fn songs_ref(&self) -> Option + '_> { - self.app_model.map_state_opt(|s| { - Some( - &s.browser - .playlist_details_state(&self.id)? - .playlist - .as_ref()? - .songs, - ) - }) - } - pub fn get_playlist_info(&self) -> Option + '_> { self.app_model.map_state_opt(|s| { s.browser @@ -74,7 +53,7 @@ impl PlaylistDetailsModel { } pub fn load_more_tracks(&self) -> Option<()> { - let last_batch = self.songs_ref()?.last_batch()?; + let last_batch = self.song_list_model().last_batch()?; let query = BatchQuery { source: SongsSource::Playlist(self.id.clone()), batch: last_batch, @@ -82,6 +61,7 @@ impl PlaylistDetailsModel { let id = self.id.clone(); let next_query = query.next()?; + debug!("next_query = {:?}", &next_query); let loader = self.app_model.get_batch_loader(); self.dispatcher.dispatch_async(Box::pin(async move { @@ -106,13 +86,23 @@ impl PlaylistDetailsModel { } impl PlaylistModel for PlaylistDetailsModel { + fn song_list_model(&self) -> SongListModel { + self.app_model + .get_state() + .browser + .playlist_details_state(&self.id) + .expect("illegal attempt to read playlist_details_state") + .songs + .clone() + } + fn current_song_id(&self) -> Option { - self.state().playback.current_song_id().cloned() + self.app_model.get_state().playback.current_song_id() } fn play_song_at(&self, pos: usize, id: &str) { let source = SongsSource::Playlist(self.id.clone()); - let batch = self.songs_ref().and_then(|songs| songs.song_batch_for(pos)); + let batch = self.song_list_model().song_batch_for(pos); if let Some(batch) = batch { self.dispatcher .dispatch(PlaybackAction::LoadPagedSongs(source, batch).into()); @@ -121,30 +111,9 @@ impl PlaylistModel for PlaylistDetailsModel { } } - fn diff_for_event(&self, event: &AppEvent) -> Option> { - match event { - AppEvent::BrowserEvent(BrowserEvent::PlaylistDetailsLoaded(id)) - | AppEvent::BrowserEvent(BrowserEvent::PlaylistTracksRemoved(id, _)) - if id == &self.id => - { - let songs = self.songs_ref()?; - Some(ListDiff::Set(songs.iter().map(|s| s.into()).collect())) - } - AppEvent::BrowserEvent(BrowserEvent::PlaylistTracksAppended(id, index)) - if id == &self.id => - { - let songs = self.songs_ref()?; - Some(ListDiff::Append( - songs.iter().skip(*index).map(|s| s.into()).collect(), - )) - } - _ => None, - } - } - fn actions_for(&self, id: &str) -> Option { - let songs = self.songs_ref()?; - let song = songs.get(id)?; + let song = self.song_list_model().get(id)?; + let song = song.description(); let group = SimpleActionGroup::new(); @@ -159,8 +128,8 @@ impl PlaylistModel for PlaylistDetailsModel { } fn menu_for(&self, id: &str) -> Option { - let songs = self.songs_ref()?; - let song = songs.get(id)?; + let song = self.song_list_model().get(id)?; + let song = song.description(); let menu = gio::Menu::new(); menu.append(Some(&*labels::VIEW_ALBUM), Some("song.view_album")); @@ -178,10 +147,10 @@ impl PlaylistModel for PlaylistDetailsModel { } fn select_song(&self, id: &str) { - let song = self.songs_ref().and_then(|s| s.get(id).cloned()); + let song = self.song_list_model().get(id); if let Some(song) = song { self.dispatcher - .dispatch(SelectionAction::Select(vec![song]).into()); + .dispatch(SelectionAction::Select(vec![song.into_description()]).into()); } } @@ -220,10 +189,8 @@ impl SimpleHeaderBarModel for PlaylistDetailsModel { } fn select_all(&self) { - if let Some(songs) = self.songs_ref() { - let songs: Vec = songs.iter().cloned().collect(); - self.dispatcher - .dispatch(SelectionAction::Select(songs).into()); - } + let songs: Vec = self.song_list_model().collect(); + self.dispatcher + .dispatch(SelectionAction::Select(songs).into()); } } diff --git a/src/app/components/saved_playlists/saved_playlists.rs b/src/app/components/saved_playlists/saved_playlists.rs index 7849b4e8..3016c44e 100644 --- a/src/app/components/saved_playlists/saved_playlists.rs +++ b/src/app/components/saved_playlists/saved_playlists.rs @@ -70,7 +70,7 @@ impl SavedPlaylistsWidget { fn bind_albums(&self, worker: Worker, store: &ListStore, on_album_pressed: F) where - F: Fn(&String) + Clone + 'static, + F: Fn(String) + Clone + 'static, { imp::SavedPlaylistsWidget::from_instance(self) .flowbox @@ -81,9 +81,7 @@ impl SavedPlaylistsWidget { 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); - } + f(album_model.uri()); })); child.set_child(Some(&album)); @@ -123,7 +121,7 @@ impl SavedPlaylists { self.worker.clone(), &*self.model.get_list_store().unwrap(), clone!(@weak self.model as model => move |id| { - model.open_playlist(id.clone()); + model.open_playlist(id); }), ); } diff --git a/src/app/components/saved_tracks/saved_tracks.rs b/src/app/components/saved_tracks/saved_tracks.rs index 04d4f019..76f8881a 100644 --- a/src/app/components/saved_tracks/saved_tracks.rs +++ b/src/app/components/saved_tracks/saved_tracks.rs @@ -86,12 +86,7 @@ impl SavedTracks { model.load_more(); })); - let playlist = Playlist::new( - widget.song_list_widget().clone(), - model.clone(), - worker, - true, - ); + let playlist = Playlist::new(widget.song_list_widget().clone(), model.clone(), worker); Self { widget, diff --git a/src/app/components/saved_tracks/saved_tracks_model.rs b/src/app/components/saved_tracks/saved_tracks_model.rs index 8d29ea90..6596ede0 100644 --- a/src/app/components/saved_tracks/saved_tracks_model.rs +++ b/src/app/components/saved_tracks/saved_tracks_model.rs @@ -7,10 +7,7 @@ use crate::app::components::{labels, PlaylistModel}; use crate::app::models::*; use crate::app::state::SelectionContext; use crate::app::state::{PlaybackAction, SelectionAction, SelectionState}; -use crate::app::{ - ActionDispatcher, AppAction, AppEvent, AppModel, BatchQuery, BrowserAction, BrowserEvent, - ListDiff, SongsSource, -}; +use crate::app::{ActionDispatcher, AppAction, AppModel, BatchQuery, BrowserAction, SongsSource}; pub struct SavedTracksModel { app_model: Rc, @@ -25,11 +22,6 @@ impl SavedTracksModel { } } - fn songs(&self) -> Option + '_> { - self.app_model - .map_state_opt(|s| Some(&s.browser.home_state()?.saved_tracks)) - } - pub fn load_initial(&self) { let query = BatchQuery { source: SongsSource::SavedTracks, @@ -40,7 +32,7 @@ impl SavedTracksModel { } pub fn load_more(&self) -> Option<()> { - let last_batch = self.songs()?.last_batch()?; + let last_batch = self.song_list_model().last_batch()?; let query = BatchQuery { source: SongsSource::SavedTracks, batch: last_batch, @@ -65,17 +57,23 @@ impl SavedTracksModel { } impl PlaylistModel for SavedTracksModel { - fn current_song_id(&self) -> Option { + fn song_list_model(&self) -> SongListModel { self.app_model .get_state() - .playback - .current_song_id() - .cloned() + .browser + .home_state() + .expect("illegal attempt to read home_state") + .saved_tracks + .clone() + } + + fn current_song_id(&self) -> Option { + self.app_model.get_state().playback.current_song_id() } fn play_song_at(&self, pos: usize, id: &str) { let source = SongsSource::SavedTracks; - let batch = self.songs().and_then(|songs| songs.song_batch_for(pos)); + let batch = self.song_list_model().song_batch_for(pos); if let Some(batch) = batch { self.dispatcher .dispatch(PlaybackAction::LoadPagedSongs(source, batch).into()); @@ -83,26 +81,13 @@ impl PlaylistModel for SavedTracksModel { .dispatch(PlaybackAction::Load(id.to_string()).into()); } } - - fn diff_for_event(&self, event: &AppEvent) -> Option> { - match event { - AppEvent::BrowserEvent(BrowserEvent::SavedTracksAppended(i)) => { - let songs = self.songs()?; - Some(ListDiff::Append( - songs.iter().skip(*i).map(|s| s.into()).collect(), - )) - } - _ => None, - } - } - fn autoscroll_to_playing(&self) -> bool { true } fn actions_for(&self, id: &str) -> Option { - let songs = self.songs()?; - let song = songs.get(id)?; + let song = self.song_list_model().get(id)?; + let song = song.description(); let group = SimpleActionGroup::new(); @@ -116,8 +101,8 @@ impl PlaylistModel for SavedTracksModel { } fn menu_for(&self, id: &str) -> Option { - let songs = self.songs()?; - let song = songs.get(id)?; + let song = self.song_list_model().get(id)?; + let song = song.description(); let menu = gio::Menu::new(); menu.append(Some(&*labels::VIEW_ALBUM), Some("song.view_album")); @@ -134,10 +119,10 @@ impl PlaylistModel for SavedTracksModel { } fn select_song(&self, id: &str) { - let song = self.songs().and_then(|s| s.get(id).cloned()); + let song = self.song_list_model().get(id); if let Some(song) = song { self.dispatcher - .dispatch(SelectionAction::Select(vec![song]).into()); + .dispatch(SelectionAction::Select(vec![song.description().clone()]).into()); } } diff --git a/src/app/components/search/search.rs b/src/app/components/search/search.rs index 65a7314b..0d23727c 100644 --- a/src/app/components/search/search.rs +++ b/src/app/components/search/search.rs @@ -95,7 +95,7 @@ impl SearchResultsWidget { fn bind_albums_results(&self, worker: Worker, store: &gio::ListStore, on_album_pressed: F) where - F: Fn(&String) + Clone + 'static, + F: Fn(String) + Clone + 'static, { self.widget() .albums_results @@ -104,9 +104,7 @@ impl SearchResultsWidget { 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); - } + f(album_model.uri()); })); album }) @@ -115,7 +113,7 @@ impl SearchResultsWidget { fn bind_artists_results(&self, worker: Worker, store: &gio::ListStore, on_artist_pressed: F) where - F: Fn(&String) + Clone + 'static, + F: Fn(String) + Clone + 'static, { self.widget() .artist_results @@ -124,9 +122,7 @@ impl SearchResultsWidget { 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); - } + f(artist_model.id()); })); artist }) @@ -191,7 +187,7 @@ impl SearchResults { &album.artists_name(), &album.title, album.year(), - &album.art, + album.art.as_ref(), &album.id, )); } diff --git a/src/app/components/search/search_model.rs b/src/app/components/search/search_model.rs index f8e36164..8184ec6c 100644 --- a/src/app/components/search/search_model.rs +++ b/src/app/components/search/search_model.rs @@ -56,13 +56,11 @@ impl SearchResultsModel { .map_state_opt(|s| Some(&s.browser.search_state()?.artist_results)) } - pub fn open_album(&self, id: &str) { - self.dispatcher - .dispatch(AppAction::ViewAlbum(id.to_string())); + pub fn open_album(&self, id: String) { + self.dispatcher.dispatch(AppAction::ViewAlbum(id)); } - pub fn open_artist(&self, id: &str) { - self.dispatcher - .dispatch(AppAction::ViewArtist(id.to_string())); + pub fn open_artist(&self, id: String) { + self.dispatcher.dispatch(AppAction::ViewArtist(id)); } } diff --git a/src/app/components/sidebar_listbox/sidebar_icon_widget.rs b/src/app/components/sidebar_listbox/sidebar_icon_widget.rs index 0a925f41..6be73f5b 100644 --- a/src/app/components/sidebar_listbox/sidebar_icon_widget.rs +++ b/src/app/components/sidebar_listbox/sidebar_icon_widget.rs @@ -31,7 +31,14 @@ mod imp { } } - impl ObjectImpl for SideBarItemWidgetIcon {} + impl ObjectImpl for SideBarItemWidgetIcon { + fn dispose(&self, obj: &Self::Type) { + while let Some(child) = obj.first_child() { + child.unparent(); + } + } + } + impl WidgetImpl for SideBarItemWidgetIcon {} impl BoxImpl for SideBarItemWidgetIcon {} } diff --git a/src/app/components/sidebar_listbox/sidebar_item.rs b/src/app/components/sidebar_listbox/sidebar_item.rs index a22d30a0..270fa0d1 100644 --- a/src/app/components/sidebar_listbox/sidebar_item.rs +++ b/src/app/components/sidebar_listbox/sidebar_item.rs @@ -13,31 +13,19 @@ impl SideBarItem { } pub(crate) fn id(&self) -> String { - self.property("id") - .unwrap() - .get::<&str>() - .unwrap() - .to_string() + self.property::("id") } pub(crate) fn title(&self) -> String { - self.property("title") - .unwrap() - .get::<&str>() - .unwrap() - .to_string() + self.property::("title") } pub(crate) fn iconname(&self) -> String { - self.property("iconname") - .unwrap() - .get::<&str>() - .unwrap() - .to_string() + self.property::("iconname") } pub(crate) fn grayedout(&self) -> bool { - self.property("grayedout").unwrap().get::().unwrap() + self.property::("grayedout") } } @@ -72,16 +60,16 @@ mod imp { lazy_static! { static ref PROPERTIES: [glib::ParamSpec; 4] = [ - glib::ParamSpec::new_string("id", "ID", "", None, glib::ParamFlags::READWRITE), - glib::ParamSpec::new_string("title", "Title", "", None, glib::ParamFlags::READWRITE), - glib::ParamSpec::new_string( + glib::ParamSpecString::new("id", "ID", "", None, glib::ParamFlags::READWRITE), + glib::ParamSpecString::new("title", "Title", "", None, glib::ParamFlags::READWRITE), + glib::ParamSpecString::new( "iconname", "IconName", "", None, glib::ParamFlags::READWRITE ), - glib::ParamSpec::new_boolean( + glib::ParamSpecBoolean::new( "grayedout", "GrayedOut", "", diff --git a/src/app/components/sidebar_listbox/sidebar_row.rs b/src/app/components/sidebar_listbox/sidebar_row.rs index 888d9992..c7d6d5c5 100644 --- a/src/app/components/sidebar_listbox/sidebar_row.rs +++ b/src/app/components/sidebar_listbox/sidebar_row.rs @@ -29,11 +29,7 @@ impl SideBarRow { } pub fn id(&self) -> String { - self.property("id") - .unwrap() - .get::<&str>() - .unwrap() - .to_string() + self.property::("id") } } @@ -61,7 +57,7 @@ mod imp { } lazy_static! { - static ref PROPERTIES: [glib::ParamSpec; 1] = [glib::ParamSpec::new_string( + static ref PROPERTIES: [glib::ParamSpec; 1] = [glib::ParamSpecString::new( "id", "ID", "", diff --git a/src/app/components/user_details/user_details.rs b/src/app/components/user_details/user_details.rs index 68f44a4e..671ae894 100644 --- a/src/app/components/user_details/user_details.rs +++ b/src/app/components/user_details/user_details.rs @@ -82,7 +82,7 @@ impl UserDetailsWidget { fn bind_user_playlists(&self, worker: Worker, store: &ListStore, on_pressed: F) where - F: Fn(&String) + Clone + 'static, + F: Fn(String) + Clone + 'static, { self.widget() .user_playlists @@ -91,9 +91,7 @@ impl UserDetailsWidget { 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); - } + f(item.uri()); })); album }) diff --git a/src/app/components/user_details/user_details_model.rs b/src/app/components/user_details/user_details_model.rs index cc7768a5..d47a7549 100644 --- a/src/app/components/user_details/user_details_model.rs +++ b/src/app/components/user_details/user_details_model.rs @@ -39,9 +39,8 @@ impl UserDetailsModel { }); } - pub fn open_playlist(&self, id: &str) { - self.dispatcher - .dispatch(AppAction::ViewPlaylist(id.to_string())); + pub fn open_playlist(&self, id: String) { + self.dispatcher.dispatch(AppAction::ViewPlaylist(id)); } pub fn load_more(&self) -> Option<()> { diff --git a/src/app/components/utils.rs b/src/app/components/utils.rs index 15e20561..6ca2a1bb 100644 --- a/src/app/components/utils.rs +++ b/src/app/components/utils.rs @@ -40,14 +40,14 @@ impl Clock { }, )); if let Some(previous_source) = self.source.replace(new_source) { - glib::source_remove(previous_source); + previous_source.remove(); } } pub fn stop(&self) { let new_source = None; if let Some(previous_source) = self.source.replace(new_source) { - glib::source_remove(previous_source); + previous_source.remove(); } } } @@ -71,7 +71,7 @@ impl Debouncer { glib::Continue(false) }); if let Some(previous_source) = self.0.replace(Some(new_source)) { - glib::source_remove(previous_source); + previous_source.remove(); } } } diff --git a/src/app/list_store.rs b/src/app/list_store.rs index cdeb0dbc..2dce499b 100644 --- a/src/app/list_store.rs +++ b/src/app/list_store.rs @@ -3,18 +3,6 @@ use glib::clone::{Downgrade, Upgrade}; use std::iter::Iterator; use std::marker::PhantomData; -#[derive(Debug, Clone)] -pub enum ListDiff -where - GType: Clone, -{ - Set(Vec), - Insert(usize, Vec), - Append(Vec), - MoveUp(usize), - MoveDown(usize), -} - pub struct ListStore { store: gio::ListStore, _marker: PhantomData, @@ -36,16 +24,6 @@ where } } - pub fn update(&mut self, diff: ListDiff) { - match diff { - ListDiff::Set(elements) => self.replace_all(elements.into_iter()), - ListDiff::Insert(i, elements) => self.insert_multiple(i as u32, 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), - } - } - pub fn unsafe_store(&self) -> &gio::ListStore { &self.store } @@ -60,26 +38,6 @@ 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_multiple(&mut self, position: u32, elements: impl Iterator) { - let upcast_vec: Vec = elements.map(|e| e.upcast::()).collect(); - self.store.splice(position, 0, &upcast_vec[..]); - } - pub fn insert(&mut self, position: u32, element: GType) { self.store.insert(position, &element); } diff --git a/src/app/models/gtypes/album_model.rs b/src/app/models/album_model.rs similarity index 64% rename from src/app/models/gtypes/album_model.rs rename to src/app/models/album_model.rs index 807a1e13..d7b932ff 100644 --- a/src/app/models/gtypes/album_model.rs +++ b/src/app/models/album_model.rs @@ -11,25 +11,25 @@ glib::wrapper! { // initial values for our two properties and then returns the new instance impl AlbumModel { pub fn new( - artist: &str, - album: &str, + artist: &String, + album: &String, year: Option, - cover: &Option, - uri: &str, + cover: Option<&String>, + uri: &String, ) -> AlbumModel { - let year = year.unwrap_or(0); + let year = &year.unwrap_or(0); glib::Object::new::(&[ - ("artist", &artist), - ("album", &album), - ("year", &year), - ("cover", cover), - ("uri", &uri), + ("artist", artist), + ("album", album), + ("year", year), + ("cover", &cover), + ("uri", uri), ]) .expect("Failed to create") } pub fn year(&self) -> Option { - match self.property("year").unwrap().get::().unwrap() { + match self.property::("year") { 0 => None, year => Some(year), } @@ -37,26 +37,14 @@ impl AlbumModel { pub fn cover_url(&self) -> Option { self.property("cover") - .unwrap() - .get::<&str>() - .ok() - .map(|s| s.to_string()) } - pub fn uri(&self) -> Option { + pub fn uri(&self) -> String { self.property("uri") - .unwrap() - .get::<&str>() - .ok() - .map(|s| s.to_string()) } - pub fn album_title(&self) -> Option { + pub fn album_title(&self) -> String { self.property("album") - .unwrap() - .get::<&str>() - .ok() - .map(|s| s.to_string()) } } @@ -64,28 +52,29 @@ mod imp { use super::*; - use std::cell::RefCell; + use std::cell::{Cell, RefCell}; // Static array for defining the properties of the new type. lazy_static! { static ref PROPERTIES: [glib::ParamSpec; 5] = [ - glib::ParamSpec::new_string("artist", "Artist", "", None, glib::ParamFlags::READWRITE), - glib::ParamSpec::new_string("album", "Album", "", None, glib::ParamFlags::READWRITE), - glib::ParamSpec::new_uint("year", "Year", "", 0, 9999, 0, glib::ParamFlags::READWRITE), - glib::ParamSpec::new_string("cover", "Cover", "", None, glib::ParamFlags::READWRITE), - glib::ParamSpec::new_string("uri", "URI", "", None, glib::ParamFlags::READWRITE), + glib::ParamSpecString::new("artist", "Artist", "", None, glib::ParamFlags::READWRITE), + glib::ParamSpecString::new("album", "Album", "", None, glib::ParamFlags::READWRITE), + glib::ParamSpecUInt::new("year", "Year", "", 0, 9999, 0, glib::ParamFlags::READWRITE), + glib::ParamSpecString::new("cover", "Cover", "", None, glib::ParamFlags::READWRITE), + glib::ParamSpecString::new("uri", "URI", "", None, glib::ParamFlags::READWRITE), ]; } // This is the struct containing all state carried with // the new type. Generally this has to make use of // interior mutability. + #[derive(Default)] pub struct AlbumModel { - album: RefCell>, - artist: RefCell>, - year: RefCell>, + album: RefCell, + artist: RefCell, + year: Cell, cover: RefCell>, - uri: RefCell>, + uri: RefCell, } // ObjectSubclass is the trait that defines the new type and @@ -100,21 +89,6 @@ mod imp { // The parent type this one is inheriting from. type ParentType = glib::Object; - - // Interfaces this type implements - type Interfaces = (); - - // Called every time a new instance is created. This should return - // a new instance of our type with its basic values. - fn new() -> Self { - Self { - album: RefCell::new(None), - artist: RefCell::new(None), - year: RefCell::new(None), - cover: RefCell::new(None), - uri: RefCell::new(None), - } - } } // Trait that is used to override virtual methods of glib::Object. @@ -149,13 +123,10 @@ mod imp { let year = value .get() .expect("type conformity checked by `Object::set_property`"); - match year { - 0 => self.year.replace(None), - y => self.year.replace(Some(y)), - }; + self.year.replace(year); } "cover" => { - let cover = value + let cover: Option = value .get() .expect("type conformity checked by `Object::set_property`"); self.cover.replace(cover); @@ -176,7 +147,7 @@ mod imp { match pspec.name() { "album" => self.album.borrow().to_value(), "artist" => self.artist.borrow().to_value(), - "year" => self.year.borrow().unwrap_or(0).to_value(), + "year" => self.year.get().to_value(), "cover" => self.cover.borrow().to_value(), "uri" => self.uri.borrow().to_value(), _ => unimplemented!(), diff --git a/src/app/models/gtypes/artist_model.rs b/src/app/models/artist_model.rs similarity index 72% rename from src/app/models/gtypes/artist_model.rs rename to src/app/models/artist_model.rs index f597cb39..0cf7fcb7 100644 --- a/src/app/models/gtypes/artist_model.rs +++ b/src/app/models/artist_model.rs @@ -17,18 +17,10 @@ impl ArtistModel { pub fn image_url(&self) -> Option { self.property("image") - .unwrap() - .get::<&str>() - .ok() - .map(|s| s.to_string()) } - pub fn id(&self) -> Option { + pub fn id(&self) -> String { self.property("id") - .unwrap() - .get::<&str>() - .ok() - .map(|s| s.to_string()) } } @@ -40,19 +32,20 @@ mod imp { // Static array for defining the properties of the new type. lazy_static! { static ref PROPERTIES: [glib::ParamSpec; 3] = [ - glib::ParamSpec::new_string("artist", "Artist", "", None, glib::ParamFlags::READWRITE), - glib::ParamSpec::new_string("image", "Image", "", None, glib::ParamFlags::READWRITE), - glib::ParamSpec::new_string("id", "id", "", None, glib::ParamFlags::READWRITE), + glib::ParamSpecString::new("artist", "Artist", "", None, glib::ParamFlags::READWRITE), + glib::ParamSpecString::new("image", "Image", "", None, glib::ParamFlags::READWRITE), + glib::ParamSpecString::new("id", "id", "", None, glib::ParamFlags::READWRITE), ]; } // This is the struct containing all state carried with // the new type. Generally this has to make use of // interior mutability. + #[derive(Default)] pub struct ArtistModel { - artist: RefCell>, + artist: RefCell, image: RefCell>, - id: RefCell>, + id: RefCell, } // ObjectSubclass is the trait that defines the new type and @@ -67,19 +60,6 @@ mod imp { // The parent type this one is inheriting from. type ParentType = glib::Object; - - // Interfaces this type implements - type Interfaces = (); - - // Called every time a new instance is created. This should return - // a new instance of our type with its basic values. - fn new() -> Self { - Self { - artist: RefCell::new(None), - image: RefCell::new(None), - id: RefCell::new(None), - } - } } // Trait that is used to override virtual methods of glib::Object. @@ -130,14 +110,5 @@ mod imp { _ => unimplemented!(), } } - - // Called right after construction of the instance. - fn constructed(&self, obj: &Self::Type) { - // Chain up to the parent type's implementation of this virtual - // method. - self.parent_constructed(obj); - - // And here we could do our own initialization. - } } } diff --git a/src/app/models/gtypes/mod.rs b/src/app/models/gtypes/mod.rs deleted file mode 100644 index 47ad0da5..00000000 --- a/src/app/models/gtypes/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -mod song_model; -pub use song_model::SongModel; - -mod album_model; -pub use album_model::AlbumModel; - -mod artist_model; -pub use artist_model::ArtistModel; diff --git a/src/app/models/gtypes/song_model.rs b/src/app/models/gtypes/song_model.rs deleted file mode 100644 index 3fa29288..00000000 --- a/src/app/models/gtypes/song_model.rs +++ /dev/null @@ -1,337 +0,0 @@ -#![allow(clippy::all)] - -use gio::prelude::*; -use glib::{subclass::prelude::*, SignalHandlerId}; - -glib::wrapper! { - pub struct SongModel(ObjectSubclass); -} - -// Constructor for new instances. This simply calls glib::Object::new() with -// initial values for our two properties and then returns the new instance -impl SongModel { - pub fn new( - id: &str, - index: u32, - title: &str, - artist: &str, - duration: &str, - art: &Option, - ) -> SongModel { - glib::Object::new::(&[ - ("index", &index), - ("title", &title), - ("artist", &artist), - ("id", &id), - ("duration", &duration), - ("art", &art), - ]) - .expect("Failed to create") - } - - pub fn cover_url(&self) -> Option { - self.property("art") - .unwrap() - .get::<&str>() - .ok() - .map(|s| s.to_string()) - } - - pub fn set_playing(&self, is_playing: bool) { - self.set_property("playing", is_playing) - .expect("set 'playing' failed"); - } - - pub fn set_selected(&self, is_selected: bool) { - self.set_property("selected", is_selected) - .expect("set 'selected' failed"); - } - - pub fn get_playing(&self) -> bool { - self.property("playing").unwrap().get::().unwrap() - } - - pub fn get_selected(&self) -> bool { - self.property("selected").unwrap().get::().unwrap() - } - - pub fn get_id(&self) -> String { - self.property("id") - .unwrap() - .get::<&str>() - .unwrap() - .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) { - 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) { - 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); - } -} - -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. - pub struct SongModel { - id: RefCell>, - index: RefCell, - title: RefCell>, - artist: RefCell>, - duration: RefCell>, - art: 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 - // contains all information needed by the GObject type system, - // including the new type's name, parent type, etc. - #[glib::object_subclass] - impl ObjectSubclass for SongModel { - // This type name must be unique per process. - const NAME: &'static str = "SongModel"; - - type Type = super::SongModel; - - // The parent type this one is inheriting from. - type ParentType = glib::Object; - - // Interfaces this type implements - type Interfaces = (); - - // Called every time a new instance is created. This should return - // a new instance of our type with its basic values. - fn new() -> Self { - Self { - id: RefCell::new(None), - index: RefCell::new(1), - title: RefCell::new(None), - artist: RefCell::new(None), - duration: RefCell::new(None), - art: RefCell::new(None), - playing: RefCell::new(false), - selected: RefCell::new(false), - bindings: RefCell::new(Default::default()), - } - } - } - - // Static array for defining the properties of the new type. - lazy_static! { - static ref PROPERTIES: [glib::ParamSpec; 8] = [ - glib::ParamSpec::new_uint( - "index", - "Index", - "", - 1, - u32::MAX, - 1, - glib::ParamFlags::READWRITE, - ), - glib::ParamSpec::new_string("title", "Title", "", None, glib::ParamFlags::READWRITE), - glib::ParamSpec::new_string("artist", "Artist", "", None, glib::ParamFlags::READWRITE), - glib::ParamSpec::new_string("id", "id", "", None, glib::ParamFlags::READWRITE), - glib::ParamSpec::new_string( - "duration", - "Duration", - "", - None, - glib::ParamFlags::READWRITE, - ), - glib::ParamSpec::new_boolean( - "playing", - "Playing", - "", - false, - glib::ParamFlags::READWRITE - ), - glib::ParamSpec::new_boolean( - "selected", - "Selected", - "", - false, - glib::ParamFlags::READWRITE, - ), - glib::ParamSpec::new_string("art", "Art", "", None, glib::ParamFlags::READWRITE,), - ]; - } - - // Trait that is used to override virtual methods of glib::Object. - impl ObjectImpl for SongModel { - fn properties() -> &'static [glib::ParamSpec] { - &*PROPERTIES - } - - // Called whenever a property is set on this instance. The id - // is the same as the index of the property in the PROPERTIES array. - fn set_property( - &self, - _obj: &Self::Type, - _id: usize, - value: &glib::Value, - pspec: &glib::ParamSpec, - ) { - match pspec.name() { - "index" => { - let index = value - .get() - .expect("type conformity checked by `Object::set_property`"); - self.index.replace(index); - } - "title" => { - let title = value - .get() - .expect("type conformity checked by `Object::set_property`"); - self.title.replace(title); - } - "artist" => { - let artist = value - .get() - .expect("type conformity checked by `Object::set_property`"); - self.artist.replace(artist); - } - "id" => { - let id = value - .get() - .expect("type conformity checked by `Object::set_property`"); - self.id.replace(id); - } - "duration" => { - let dur = value - .get() - .expect("type conformity checked by `Object::set_property`"); - self.duration.replace(dur); - } - "art" => { - let art = value - .get() - .expect("type conformity checked by `Object::set_property`"); - self.art.replace(art); - } - "playing" => { - let playing = value - .get() - .expect("type conformity checked by `Object::set_property`"); - self.playing.replace(playing); - } - "selected" => { - let selected = value - .get() - .expect("type conformity checked by `Object::set_property`"); - self.selected.replace(selected); - } - _ => unimplemented!(), - } - } - - // Called whenever a property is retrieved from this instance. The id - // is the same as the index of the property in the PROPERTIES array. - fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - match pspec.name() { - "index" => self.index.borrow().to_value(), - "title" => self.title.borrow().to_value(), - "artist" => self.artist.borrow().to_value(), - "id" => self.id.borrow().to_value(), - "duration" => self.duration.borrow().to_value(), - "art" => self.art.borrow().to_value(), - "playing" => self.playing.borrow().to_value(), - "selected" => self.selected.borrow().to_value(), - _ => unimplemented!(), - } - } - - // Called right after construction of the instance. - fn constructed(&self, obj: &Self::Type) { - // Chain up to the parent type's implementation of this virtual - // method. - self.parent_constructed(obj); - - // And here we could do our own initialization. - } - } -} diff --git a/src/app/models/main.rs b/src/app/models/main.rs index f8c7e216..7f81e213 100644 --- a/src/app/models/main.rs +++ b/src/app/models/main.rs @@ -1,64 +1,34 @@ -use std::convert::From; use std::str::FromStr; -use super::core::{Batch, SongList}; -use super::gtypes::*; - -use crate::app::components::utils::format_duration; - -impl From<&AlbumDescription> for AlbumModel { - fn from(album: &AlbumDescription) -> Self { - AlbumModel::new( - &album.artists_name(), - &album.title, - album.year(), - &album.art, - &album.id, - ) - } +#[derive(Clone, Copy, Debug)] +pub struct Batch { + pub offset: usize, + pub batch_size: usize, + pub total: usize, } -impl From for AlbumModel { - fn from(album: AlbumDescription) -> Self { - Self::from(&album) - } -} - -impl From<&PlaylistDescription> for AlbumModel { - fn from(playlist: &PlaylistDescription) -> Self { - AlbumModel::new( - &playlist.owner.display_name, - &playlist.title, - // Playlists do not have their released date since they are expected to be updated anytime. - None, - &playlist.art, - &playlist.id, - ) - } -} - -impl From for AlbumModel { - fn from(playlist: PlaylistDescription) -> Self { - Self::from(&playlist) - } -} - -impl From<&SongDescription> for SongModel { - fn from(song: &SongDescription) -> Self { - SongModel::new( - &song.id, - song.track_number.unwrap_or(1), - &song.title, - &song.artists_name(), - &format_duration(song.duration.into()), - &song.art, - ) +impl Batch { + pub fn first_of_size(batch_size: usize) -> Self { + Self { + offset: 0, + batch_size, + total: 0, + } } -} -impl From for SongModel { - fn from(song: SongDescription) -> Self { - SongModel::from(&song) + pub fn next(self) -> Option { + let Self { + offset, + batch_size, + total, + } = self; + + Some(Self { + offset: offset + batch_size, + batch_size, + total, + }) + .filter(|b| b.offset < total) } } @@ -93,7 +63,7 @@ pub struct AlbumDescription { pub artists: Vec, pub release_date: Option, pub art: Option, - pub songs: SongList, + pub songs: SongBatch, pub is_liked: bool, } @@ -106,11 +76,6 @@ impl AlbumDescription { .join(", ") } - pub fn formatted_time(&self) -> String { - let duration: u32 = self.songs.iter().map(|song| song.duration).sum(); - format_duration(duration.into()) - } - pub fn year(&self) -> Option { self.release_date .as_ref() @@ -129,6 +94,7 @@ pub struct AlbumFullDescription { pub struct AlbumReleaseDetails { pub label: String, pub copyright_text: String, + pub total_tracks: usize, } #[derive(Clone, Debug)] @@ -136,7 +102,7 @@ pub struct PlaylistDescription { pub id: String, pub title: String, pub art: Option, - pub songs: SongList, + pub songs: SongBatch, pub owner: UserRef, } @@ -168,6 +134,12 @@ impl SongDescription { } } +#[derive(Copy, Clone, Default)] +pub struct SongState { + pub is_playing: bool, + pub is_selected: bool, +} + #[derive(Debug, Clone)] pub struct SongBatch { pub songs: Vec, @@ -237,3 +209,38 @@ pub struct UserDescription { pub name: String, pub playlists: Vec, } + +#[cfg(test)] +mod tests { + + use super::*; + + fn song(id: &str) -> SongDescription { + SongDescription { + id: id.to_string(), + uri: "".to_string(), + title: "Title".to_string(), + artists: vec![], + album: AlbumRef { + id: "".to_string(), + name: "".to_string(), + }, + duration: 1000, + art: None, + track_number: None, + } + } + + #[test] + fn resize_batch() { + let batch = SongBatch { + songs: vec![song("1"), song("2"), song("3"), song("4")], + batch: Batch::first_of_size(4), + }; + + let batches = batch.resize(2); + assert_eq!(batches.len(), 2); + assert_eq!(&batches.get(0).unwrap().songs.get(0).unwrap().id, "1"); + assert_eq!(&batches.get(1).unwrap().songs.get(0).unwrap().id, "3"); + } +} diff --git a/src/app/models/mod.rs b/src/app/models/mod.rs index 0f487767..b7b2f6d2 100644 --- a/src/app/models/mod.rs +++ b/src/app/models/mod.rs @@ -1,8 +1,60 @@ mod main; pub use main::*; -mod core; -pub use self::core::*; +mod songs; +pub use songs::*; -mod gtypes; -pub use gtypes::*; +mod album_model; +pub use album_model::*; + +mod artist_model; +pub use artist_model::*; + +impl From<&AlbumDescription> for AlbumModel { + fn from(album: &AlbumDescription) -> Self { + AlbumModel::new( + &album.artists_name(), + &album.title, + album.year(), + album.art.as_ref(), + &album.id, + ) + } +} + +impl From for AlbumModel { + fn from(album: AlbumDescription) -> Self { + Self::from(&album) + } +} + +impl From<&PlaylistDescription> for AlbumModel { + fn from(playlist: &PlaylistDescription) -> Self { + AlbumModel::new( + &playlist.owner.display_name, + &playlist.title, + // Playlists do not have their released date since they are expected to be updated anytime. + None, + playlist.art.as_ref(), + &playlist.id, + ) + } +} + +impl From for AlbumModel { + fn from(playlist: PlaylistDescription) -> Self { + Self::from(&playlist) + } +} + +impl From for SongModel { + fn from(song: SongDescription) -> Self { + SongModel::new(song) + } +} + +impl From<&SongDescription> for SongModel { + fn from(song: &SongDescription) -> Self { + SongModel::new(song.clone()) + } +} diff --git a/src/app/models/songs/mod.rs b/src/app/models/songs/mod.rs new file mode 100644 index 00000000..b156fbeb --- /dev/null +++ b/src/app/models/songs/mod.rs @@ -0,0 +1,7 @@ +mod support; + +mod song_list_model; +pub use song_list_model::*; + +mod song_model; +pub use song_model::*; diff --git a/src/app/models/songs/song_list_model.rs b/src/app/models/songs/song_list_model.rs new file mode 100644 index 00000000..d50e181c --- /dev/null +++ b/src/app/models/songs/song_list_model.rs @@ -0,0 +1,251 @@ +use gio::prelude::*; +use gio::ListModel; +use glib::StaticType; +use gtk::subclass::prelude::*; +use std::cell::{Ref, RefCell, RefMut}; + +use super::support::*; +use crate::app::models::*; + +#[must_use] +pub struct SongListModelPending<'a> { + change: Option, + song_list_model: &'a mut SongListModel, +} + +impl<'a> SongListModelPending<'a> { + fn new(change: Option, song_list_model: &'a mut SongListModel) -> Self { + Self { + change, + song_list_model, + } + } + + pub fn and(self, op: Op) -> Self + where + Op: FnOnce(&mut SongListModel) -> SongListModelPending<'_> + 'static, + { + let Self { + change, + song_list_model, + } = self; + + let new_change = op(song_list_model).change; + + let merged_change = if let (Some(change), Some(new_change)) = (change, new_change) { + Some(change.merge(new_change)) + } else { + change.or(new_change) + }; + + Self { + change: merged_change, + song_list_model, + } + } + + pub fn commit(self) -> bool { + let Self { + change, + song_list_model, + } = self; + song_list_model.notify_changes(change); + change.is_some() + } +} + +glib::wrapper! { + pub struct SongListModel(ObjectSubclass) @implements gio::ListModel; +} + +impl SongListModel { + pub fn new(batch_size: u32) -> Self { + glib::Object::new(&[("batch-size", &batch_size)]).unwrap() + } + + fn inner_mut(&mut self) -> RefMut { + imp::SongListModel::from_instance(self).get_mut() + } + + fn inner(&self) -> Ref { + imp::SongListModel::from_instance(self).get() + } + + fn notify_changes(&self, changes: impl IntoIterator + 'static) { + if cfg!(not(test)) { + glib::source::idle_add_local_once(clone!(@weak self as s => move || { + for ListRangeUpdate(a, b, c) in changes.into_iter() { + debug!("pos {}, removed {}, added {}", a, b, c); + s.items_changed(a as u32, b as u32, c as u32); + } + })); + } + } + + pub fn for_each(&self, f: F) + where + F: Fn(usize, &SongModel), + { + for (i, song) in self.inner().iter().enumerate() { + f(i, song); + } + } + + pub fn collect(&self) -> Vec { + self.inner().iter().map(|s| s.into_description()).collect() + } + + pub fn add(&mut self, song_batch: SongBatch) -> SongListModelPending { + let range = self.inner_mut().add(song_batch); + SongListModelPending::new(range, self) + } + + pub fn get(&self, id: &str) -> Option { + self.inner().get(id).cloned() + } + + pub fn index(&self, i: usize) -> Option { + self.inner().index(i).cloned() + } + + pub fn index_continuous(&self, i: usize) -> Option { + self.inner().index_continuous(i).cloned() + } + + pub fn song_batch_for(&self, i: usize) -> Option { + self.inner().song_batch_for(i) + } + + pub fn last_batch(&self) -> Option { + self.inner().last_batch() + } + + pub fn needed_batch_for(&self, i: usize) -> Option { + self.inner().needed_batch_for(i) + } + + pub fn len(&self) -> usize { + self.inner().len() + } + + pub fn append(&mut self, songs: Vec) -> SongListModelPending { + let range = self.inner_mut().append(songs); + SongListModelPending::new(Some(range), self) + } + + pub fn find_index(&self, song_id: &str) -> Option { + self.inner().find_index(song_id) + } + + pub fn remove(&mut self, ids: &[String]) -> SongListModelPending { + let change = self.inner_mut().remove(ids); + SongListModelPending::new(Some(change), self) + } + + pub fn move_down(&mut self, a: usize) -> SongListModelPending { + let swap = self.inner_mut().swap(a + 1, a); + SongListModelPending::new(swap, self) + } + + pub fn move_up(&mut self, a: usize) -> SongListModelPending { + let swap = self.inner_mut().swap(a - 1, a); + SongListModelPending::new(swap, self) + } + + pub fn clear(&mut self) -> SongListModelPending { + let removed = self.inner_mut().clear(); + SongListModelPending::new(Some(removed), self) + } +} + +mod imp { + + use super::*; + + pub struct SongListModel(RefCell>); + + #[glib::object_subclass] + impl ObjectSubclass for SongListModel { + const NAME: &'static str = "SongList"; + + type Type = super::SongListModel; + type ParentType = glib::Object; + type Interfaces = (ListModel,); + + fn new() -> Self { + Self(RefCell::new(None)) + } + } + + lazy_static! { + static ref PROPERTIES: [glib::ParamSpec; 1] = [glib::ParamSpecUInt::new( + "batch-size", + "Size of the batches", + "", + 1, + u32::MAX, + 1, + glib::ParamFlags::READWRITE, + )]; + } + + impl ObjectImpl for SongListModel { + fn properties() -> &'static [glib::ParamSpec] { + &*PROPERTIES + } + + fn set_property( + &self, + _obj: &Self::Type, + _id: usize, + value: &glib::Value, + pspec: &glib::ParamSpec, + ) { + if "batch-size" == pspec.name() { + let batch_size = value.get::().unwrap(); + *self.0.borrow_mut() = Some(SongList::new_sized(batch_size as usize)) + } else { + unimplemented!() + } + } + + fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + if "batch-size" == pspec.name() { + let size = self.get().batch_size() as u32; + size.to_value() + } else { + unimplemented!() + } + } + } + + impl ListModelImpl for SongListModel { + fn item_type(&self, _: &Self::Type) -> glib::Type { + SongModel::static_type() + } + + fn n_items(&self, _: &Self::Type) -> u32 { + self.get().partial_len() as u32 + } + + fn item(&self, _: &Self::Type, position: u32) -> Option { + self.get() + .index_continuous(position as usize) + .map(|m| m.clone().upcast()) + } + } + + impl SongListModel { + pub fn get_mut(&self) -> RefMut { + RefMut::map(self.0.borrow_mut(), |s| { + s.as_mut().expect("set at construction") + }) + } + + pub fn get(&self) -> Ref { + Ref::map(self.0.borrow(), |s| { + s.as_ref().expect("set at construction") + }) + } + } +} diff --git a/src/app/models/songs/song_model.rs b/src/app/models/songs/song_model.rs new file mode 100644 index 00000000..3141dbe2 --- /dev/null +++ b/src/app/models/songs/song_model.rs @@ -0,0 +1,300 @@ +#![allow(clippy::all)] + +use gio::prelude::*; +use glib::{subclass::prelude::*, SignalHandlerId}; +use std::{cell::Ref, ops::Deref}; + +use crate::app::components::utils::format_duration; +use crate::app::models::*; + +glib::wrapper! { + pub struct SongModel(ObjectSubclass); +} + +impl SongModel { + pub fn new(song: SongDescription) -> Self { + let o: Self = glib::Object::new(&[]).unwrap(); + imp::SongModel::from_instance(&o).song.replace(Some(song)); + o + } + + pub fn set_playing(&self, is_playing: bool) { + self.set_property("playing", is_playing); + } + + pub fn set_selected(&self, is_selected: bool) { + self.set_property("selected", is_selected); + } + + pub fn get_playing(&self) -> bool { + self.property("playing") + } + + pub fn get_selected(&self) -> bool { + self.property("selected") + } + + pub fn get_id(&self) -> String { + self.property("id") + } + + pub fn bind_index(&self, o: &impl ObjectType, 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: &impl ObjectType, 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: &impl ObjectType, 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: &impl ObjectType, 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 bind_playing(&self, o: &impl ObjectType, property: &str) { + imp::SongModel::from_instance(self).push_binding( + self.bind_property("playing", o, property) + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(), + ); + } + + pub fn bind_selected(&self, o: &impl ObjectType, property: &str) { + imp::SongModel::from_instance(self).push_binding( + self.bind_property("selected", o, property) + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(), + ); + } + + pub fn unbind_all(&self) { + imp::SongModel::from_instance(self).unbind_all(self); + } + + pub fn description(&self) -> impl Deref + '_ { + Ref::map(imp::SongModel::from_instance(self).song.borrow(), |s| { + s.as_ref().expect("song set at constructor") + }) + } + + pub fn into_description(&self) -> SongDescription { + imp::SongModel::from_instance(&self) + .song + .borrow() + .as_ref() + .cloned() + .expect("song set at constructor") + } +} + +mod imp { + + use super::*; + use std::cell::{Cell, RefCell}; + + #[derive(Default)] + struct BindingsInner { + pub signals: Vec, + pub bindings: Vec, + } + + #[derive(Default)] + pub struct SongModel { + pub song: RefCell>, + pub state: Cell, + bindings: RefCell, + } + + impl SongModel { + pub fn push_signal(&self, id: SignalHandlerId) { + self.bindings.borrow_mut().signals.push(id); + } + + pub fn push_binding(&self, binding: glib::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()); + } + } + #[glib::object_subclass] + impl ObjectSubclass for SongModel { + const NAME: &'static str = "SongModel"; + type Type = super::SongModel; + type ParentType = glib::Object; + } + + lazy_static! { + static ref PROPERTIES: [glib::ParamSpec; 8] = [ + glib::ParamSpecString::new( + "id", + "Spotify identifier", + "", + None, + glib::ParamFlags::READABLE + ), + glib::ParamSpecUInt::new( + "index", + "Track number within an album", + "", + 1, + u32::MAX, + 1, + glib::ParamFlags::READABLE, + ), + glib::ParamSpecString::new( + "title", + "Title of the track", + "", + None, + glib::ParamFlags::READABLE + ), + glib::ParamSpecString::new( + "artist", + "Artists, comma separated", + "", + None, + glib::ParamFlags::READABLE + ), + glib::ParamSpecString::new( + "duration", + "Duration (formatted)", + "", + None, + glib::ParamFlags::READABLE, + ), + glib::ParamSpecString::new( + "art", + "URL to the cover art", + "", + None, + glib::ParamFlags::READABLE, + ), + glib::ParamSpecBoolean::new( + "playing", + "Playing", + "", + false, + glib::ParamFlags::READWRITE + ), + glib::ParamSpecBoolean::new( + "selected", + "Selected", + "", + false, + glib::ParamFlags::READWRITE, + ), + ]; + } + + impl ObjectImpl for SongModel { + fn properties() -> &'static [glib::ParamSpec] { + &*PROPERTIES + } + + fn set_property( + &self, + _obj: &Self::Type, + _id: usize, + value: &glib::Value, + pspec: &glib::ParamSpec, + ) { + match pspec.name() { + "playing" => { + let is_playing = value + .get() + .expect("type conformity checked by `Object::set_property`"); + let SongState { is_selected, .. } = self.state.get(); + self.state.set(SongState { + is_playing, + is_selected, + }); + } + "selected" => { + let is_selected = value + .get() + .expect("type conformity checked by `Object::set_property`"); + let SongState { is_playing, .. } = self.state.get(); + self.state.set(SongState { + is_playing, + is_selected, + }); + } + _ => unimplemented!(), + } + } + + fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + match pspec.name() { + "index" => self + .song + .borrow() + .as_ref() + .expect("song set at constructor") + .track_number + .unwrap_or(1) + .to_value(), + "title" => self + .song + .borrow() + .as_ref() + .expect("song set at constructor") + .title + .to_value(), + "artist" => self + .song + .borrow() + .as_ref() + .expect("song set at constructor") + .artists_name() + .to_value(), + "id" => self + .song + .borrow() + .as_ref() + .expect("song set at constructor") + .id + .to_value(), + "duration" => self + .song + .borrow() + .as_ref() + .map(|s| format_duration(s.duration.into())) + .expect("song set at constructor") + .to_value(), + "art" => self + .song + .borrow() + .as_ref() + .expect("song set at constructor") + .art + .to_value(), + "playing" => self.state.get().is_playing.to_value(), + "selected" => self.state.get().is_selected.to_value(), + _ => unimplemented!(), + } + } + } +} diff --git a/src/app/models/core.rs b/src/app/models/songs/support.rs similarity index 53% rename from src/app/models/core.rs rename to src/app/models/songs/support.rs index 9e3d6fac..defe392c 100644 --- a/src/app/models/core.rs +++ b/src/app/models/songs/support.rs @@ -1,60 +1,114 @@ -use super::main::*; use std::collections::HashMap; +use std::convert::{TryFrom, TryInto}; + +use crate::app::models::*; #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct InsertionRange(pub usize, pub usize); - -impl InsertionRange { - fn union(a: Option, b: Option) -> Option { - match (a, b) { - (Some(Self(a0, a1)), Some(Self(b0, b1))) => { - let start = usize::min(a0, b0); - let end = usize::max(a0 + b0, a1 + b1) - start; - Some(Self(start, end)) - } - (Some(a), None) | (None, Some(a)) => Some(a), - _ => None, +enum Range { + Empty, + NotEmpty(u32, u32), +} + +impl Range { + fn of(a: impl TryInto, b: impl TryInto) -> Self { + match (a.try_into(), b.try_into()) { + (Ok(a), Ok(b)) if b >= a => Self::NotEmpty(a, b), + _ => Self::Empty, } } -} -#[derive(Clone, Copy, Debug)] -pub struct Batch { - pub offset: usize, - pub batch_size: usize, - pub total: usize, -} + fn len(self) -> u32 { + match self { + Self::Empty => 0, + Self::NotEmpty(a, b) => b - a + 1, + } + } -impl Batch { - pub fn first_of_size(batch_size: usize) -> Self { - Self { - offset: 0, - batch_size, - total: 0, + fn union(self, other: Self) -> Self { + match (self, other) { + (Self::NotEmpty(a0, b0), Self::NotEmpty(a1, b1)) => { + let start = u32::min(a0, a1); + let end = u32::max(b0, b1); + Self::NotEmpty(start, end) + } + (Self::Empty, r) | (r, Self::Empty) => r, } } - pub fn next(self) -> Option { - let Self { - offset, - batch_size, - total, - } = self; + fn offset_by(self, offset: i32) -> Self { + match self { + Self::Empty => Self::Empty, + Self::NotEmpty(a, b) => Self::of((a as i32) + offset, (b as i32) + offset), + } + } - Some(Self { - offset: offset + batch_size, - batch_size, - total, - }) - .filter(|b| b.offset < total) + fn start(self) -> Option + where + Target: TryFrom, + { + match self { + Self::Empty => None, + Self::NotEmpty(a, _) => Some(a.try_into().ok()?), + } } } -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum SongIndexStatus { - Present, - Absent, - OutOfBounds, +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ListRangeUpdate(pub i32, pub i32, pub i32); + +impl ListRangeUpdate { + pub fn inserted(position: impl TryInto, added: impl TryInto) -> Self { + Self( + position.try_into().unwrap_or_default(), + 0, + added.try_into().unwrap_or_default(), + ) + } + + pub fn removed(position: impl TryInto, removed: impl TryInto) -> Self { + Self( + position.try_into().unwrap_or_default(), + removed.try_into().unwrap_or_default(), + 0, + ) + } + + pub fn updated(position: impl TryInto) -> Self { + Self(position.try_into().unwrap_or_default(), 1, 1) + } + + pub fn merge(self, other: Self) -> Self { + // reorder for simplicity + let (left, right) = if self.0 <= other.0 { + (self, other) + } else { + (other, self) + }; + + let Self(p0, r0, a0) = left; + let Self(p1, r1, a1) = right; + + // range [s, e] affected by first update + let ra0 = Range::of(p0, p0 + r0 - 1); + + // ...second update, but only the range affecting existing elements + let ra1 = { + let s1 = i32::max(p0 + a0, p1); + let e1 = i32::max(s1 - 1, p1 + r1 - 1); + Range::of(s1, e1) + }; + + // remap to original + let ra1 = ra1.offset_by(r0 - a0); + + // union + let rau = ra0.union(ra1); + + let removed = rau.len() as i32; + let position = rau.start().unwrap_or(p0); + let added = removed - (r0 - a0) - (r1 - a1); + Self(position, removed, added) + } } #[derive(Clone, Debug)] @@ -64,7 +118,7 @@ pub struct SongList { batch_size: usize, last_batch_key: usize, batches: HashMap>, - indexed_songs: HashMap, + indexed_songs: HashMap, } impl SongList { @@ -79,19 +133,13 @@ impl SongList { } } - pub fn new_from_initial_batch(initial: SongBatch) -> Self { - let mut s = Self::new_sized(initial.batch.batch_size); - s.add(initial); - s - } - - pub fn iter(&self) -> impl Iterator { - self.iter_from(0) + pub fn batch_size(&self) -> usize { + self.batch_size } - fn iter_from(&self, i: usize) -> impl Iterator { + pub fn iter(&self) -> impl Iterator { let indexed_songs = &self.indexed_songs; - self.iter_ids_from(i) + self.iter_ids_from(0) .filter_map(move |(_, id)| indexed_songs.get(id)) } @@ -152,7 +200,14 @@ impl SongList { } } - pub fn remove(&mut self, ids: &[String]) { + pub fn clear(&mut self) -> ListRangeUpdate { + let len = self.partial_len(); + *self = Self::new_sized(self.batch_size); + ListRangeUpdate::removed(0, len) + } + + pub fn remove(&mut self, ids: &[String]) -> ListRangeUpdate { + let len = self.total_loaded; let mut batches = HashMap::>::default(); self.iter_ids_from(0) .filter(|(_, s)| !ids.contains(s)) @@ -161,39 +216,52 @@ impl SongList { }); self.last_batch_key = batches.len().saturating_sub(1); self.batches = batches; - self.total = self.total.saturating_sub(ids.len()); - self.total_loaded = self.total_loaded.saturating_sub(ids.len()); + let removed = ids.len(); + self.total = self.total.saturating_sub(removed); + self.total_loaded = self.total_loaded.saturating_sub(removed); + ListRangeUpdate(0, len as i32, self.total_loaded as i32) } - pub fn append(&mut self, songs: Vec) { - self.total = self.total.saturating_add(songs.len()); - self.total_loaded = self.total_loaded.saturating_add(songs.len()); + pub fn append(&mut self, songs: Vec) -> ListRangeUpdate { + let songs_len = songs.len(); + let insertion_start = self.estimated_len(self.last_batch_key + 1); + self.total = self.total.saturating_add(songs_len); + self.total_loaded = self.total_loaded.saturating_add(songs_len); for song in songs { Self::batches_add(&mut self.batches, self.batch_size, &song.id); - self.indexed_songs.insert(song.id.clone(), song); + self.indexed_songs + .insert(song.id.clone(), SongModel::new(song)); } self.last_batch_key = self.batches.len().saturating_sub(1); + ListRangeUpdate::inserted(insertion_start, songs_len) } - pub fn add(&mut self, song_batch: SongBatch) -> Option { + pub fn add(&mut self, song_batch: SongBatch) -> Option { if song_batch.batch.batch_size != self.batch_size { song_batch .resize(self.batch_size) .into_iter() - .map(|new_batch| self.add_one(new_batch)) - .reduce(InsertionRange::union) + .map(|new_batch| { + debug!("adding batch {:?}", &new_batch.batch); + self.add_one(new_batch) + }) + .reduce(|acc, cur| { + let merged = acc?.merge(cur?); + Some(merged).or(acc).or(cur) + }) .unwrap_or(None) } else { self.add_one(song_batch) } } - fn add_one(&mut self, SongBatch { songs, batch }: SongBatch) -> Option { + fn add_one(&mut self, SongBatch { songs, batch }: SongBatch) -> Option { assert_eq!(batch.batch_size, self.batch_size); let index = batch.offset / batch.batch_size; if self.batches.contains_key(&index) { + debug!("batch already loaded"); return None; } @@ -203,7 +271,8 @@ impl SongList { .into_iter() .map(|song| { let song_id = song.id.clone(); - self.indexed_songs.insert(song_id.clone(), song); + self.indexed_songs + .insert(song_id.clone(), SongModel::new(song)); song_id }) .collect(); @@ -213,7 +282,7 @@ impl SongList { self.total_loaded += len; self.last_batch_key = usize::max(self.last_batch_key, index); - Some(InsertionRange(insertion_start, len)) + Some(ListRangeUpdate::inserted(insertion_start, len)) } fn index_mut(&mut self, i: usize) -> Option<&mut String> { @@ -224,9 +293,9 @@ impl SongList { .and_then(|s| s.get_mut(i % batch_size)) } - pub fn swap(&mut self, a: usize, b: usize) { + pub fn swap(&mut self, a: usize, b: usize) -> Option { if a == b { - return; + return None; } let a_value = self.index_mut(a).map(std::mem::take); let a_value = a_value.as_ref(); @@ -238,9 +307,10 @@ impl SongList { if let (Some(a_mut), Some(a_value)) = (a_mut, new_a_value) { *a_mut = a_value; } + Some(ListRangeUpdate::updated(a).merge(ListRangeUpdate::updated(b))) } - pub fn index(&self, i: usize) -> Option<&SongDescription> { + pub fn index(&self, i: usize) -> Option<&SongModel> { let batch_size = self.batch_size; let batch_id = i / batch_size; let indexed_songs = &self.indexed_songs; @@ -250,6 +320,18 @@ impl SongList { .and_then(move |id| indexed_songs.get(id)) } + pub fn index_continuous(&self, i: usize) -> Option<&SongModel> { + let batch_size = self.batch_size; + let bi = i / batch_size; + let batch = (0..=self.last_batch_key) + .into_iter() + .filter_map(move |i| self.batches.get(&i)) + .nth(bi)?; + batch + .get(i % batch_size) + .and_then(move |id| self.indexed_songs.get(id)) + } + pub fn needed_batch_for(&self, i: usize) -> Option { let total = self.total; let batch_size = self.batch_size; @@ -273,8 +355,7 @@ impl SongList { self.batches.get(&batch_id).map(|songs| SongBatch { songs: songs .iter() - .filter_map(move |id| indexed_songs.get(id)) - .cloned() + .filter_map(move |id| Some(indexed_songs.get(id)?.into_description())) .collect(), batch: Batch { batch_size, @@ -296,25 +377,9 @@ impl SongList { } } - pub fn get(&self, id: &str) -> Option<&SongDescription> { + pub fn get(&self, id: &str) -> Option<&SongModel> { self.indexed_songs.get(id) } - - pub fn status(&self, i: usize) -> SongIndexStatus { - if i >= self.total { - return SongIndexStatus::OutOfBounds; - } - - let batch_size = self.batch_size; - let batch_id = i / batch_size; - self.batches - .get(&batch_id) - .map(|batch| match batch.get(i % batch_size) { - Some(_) => SongIndexStatus::Present, - None => SongIndexStatus::OutOfBounds, - }) - .unwrap_or(SongIndexStatus::Absent) - } } #[cfg(test)] @@ -322,6 +387,16 @@ mod tests { use super::*; + const NO_CHANGE: ListRangeUpdate = ListRangeUpdate(0, 0, 0); + + impl SongList { + fn new_from_initial_batch(initial: SongBatch) -> Self { + let mut s = Self::new_sized(initial.batch.batch_size); + s.add(initial); + s + } + } + fn song(id: &str) -> SongDescription { SongDescription { id: id.to_string(), @@ -353,13 +428,68 @@ mod tests { } } + #[test] + fn test_merge_range() { + // [0, 1, 2, 3, 4, 5] + let change1 = ListRangeUpdate(0, 4, 2); + // [x, x, 4, 5] + let change2 = ListRangeUpdate(1, 1, 2); + // [x, y, y, 4, 5] + assert_eq!(change1.merge(change2), ListRangeUpdate(0, 4, 3)); + assert_eq!(change2.merge(change1), ListRangeUpdate(0, 4, 3)); + + // [0, 1, 2, 3, 4, 5, 6] + let change1 = ListRangeUpdate(0, 2, 3); + // [x, x, x, 2, 3, 4, 5, 6] + let change2 = ListRangeUpdate(4, 1, 1); + // [x, x, x, 2, y, 4, 5, 6] + assert_eq!(change1.merge(change2), ListRangeUpdate(0, 4, 5)); + assert_eq!(change2.merge(change1), ListRangeUpdate(0, 4, 5)); + + // [0, 1, 2, 3, 4, 5, 6] + let change1 = ListRangeUpdate(0, 3, 2); + // [x, x, 3, 4, 5, 6] + let change2 = ListRangeUpdate(4, 1, 1); + // [x, x, 3, 4, y, 6] + assert_eq!(change1.merge(change2), ListRangeUpdate(0, 6, 5)); + assert_eq!(change2.merge(change1), ListRangeUpdate(0, 6, 5)); + + // [0, 1, 2, 3, 4, 5] + let change1 = ListRangeUpdate(0, 4, 2); + // [x, x, 4, 5] + let change2 = ListRangeUpdate(1, 1, 1); + // [x, y, 4, 5] + assert_eq!(change1.merge(change2), ListRangeUpdate(0, 4, 2)); + assert_eq!(change2.merge(change1), ListRangeUpdate(0, 4, 2)); + + // [0, 1, 2, 3, 4, 5] + let change1 = ListRangeUpdate(0, 4, 2); + // [x, x, 4, 5] + let change2 = ListRangeUpdate(0, 4, 2); + // [y, y] + assert_eq!(change1.merge(change2), ListRangeUpdate(0, 6, 2)); + assert_eq!(change2.merge(change1), ListRangeUpdate(0, 6, 2)); + + // [] + let change1 = ListRangeUpdate(0, 0, 2); + // [x, x] + let change2 = ListRangeUpdate(2, 0, 2); + // [x, x, y, y] + assert_eq!(change1.merge(change2), ListRangeUpdate(0, 0, 4)); + assert_eq!(change2.merge(change1), ListRangeUpdate(0, 0, 4)); + + let change1 = ListRangeUpdate(0, 4, 2); + assert_eq!(change1.merge(NO_CHANGE), ListRangeUpdate(0, 4, 2)); + assert_eq!(NO_CHANGE.merge(change1), ListRangeUpdate(0, 4, 2)); + } + #[test] fn test_iter() { let list = SongList::new_from_initial_batch(batch(0)); let mut list_iter = list.iter(); - assert_eq!(list_iter.next().unwrap().id, "song0"); - assert_eq!(list_iter.next().unwrap().id, "song1"); + assert_eq!(list_iter.next().unwrap().description().id, "song0"); + assert_eq!(list_iter.next().unwrap().description().id, "song1"); assert!(list_iter.next().is_none()); } @@ -390,15 +520,15 @@ mod tests { let mut list = SongList::new_from_initial_batch(batch(0)); let range = list.add(batch(1)); - assert_eq!(range, Some(InsertionRange(2, 2))); + assert_eq!(range, Some(ListRangeUpdate::inserted(2, 2))); assert_eq!(list.partial_len(), 4); let range = list.add(batch(3)); - assert_eq!(range, Some(InsertionRange(4, 2))); + assert_eq!(range, Some(ListRangeUpdate::inserted(4, 2))); assert_eq!(list.partial_len(), 6); let range = list.add(batch(2)); - assert_eq!(range, Some(InsertionRange(4, 2))); + assert_eq!(range, Some(ListRangeUpdate::inserted(4, 2))); assert_eq!(list.partial_len(), 8); let range = list.add(batch(2)); @@ -424,21 +554,13 @@ mod tests { assert_eq!(list.partial_len(), 4); let mut list_iter = list.iter(); - assert_eq!(list_iter.next().unwrap().id, "song0"); - assert_eq!(list_iter.next().unwrap().id, "song1"); - assert_eq!(list_iter.next().unwrap().id, "song4"); - assert_eq!(list_iter.next().unwrap().id, "song5"); + assert_eq!(list_iter.next().unwrap().description().id, "song0"); + assert_eq!(list_iter.next().unwrap().description().id, "song1"); + assert_eq!(list_iter.next().unwrap().description().id, "song4"); + assert_eq!(list_iter.next().unwrap().description().id, "song5"); assert!(list_iter.next().is_none()); } - #[test] - fn test_status() { - let list = SongList::new_from_initial_batch(batch(0)); - assert_eq!(list.status(0), SongIndexStatus::Present); - assert_eq!(list.status(6), SongIndexStatus::Absent); - assert_eq!(list.status(10), SongIndexStatus::OutOfBounds); - } - #[test] fn test_remove() { let mut list = SongList::new_from_initial_batch(batch(0)); @@ -449,9 +571,9 @@ mod tests { assert_eq!(list.partial_len(), 3); let mut list_iter = list.iter(); - assert_eq!(list_iter.next().unwrap().id, "song1"); - assert_eq!(list_iter.next().unwrap().id, "song2"); - assert_eq!(list_iter.next().unwrap().id, "song3"); + assert_eq!(list_iter.next().unwrap().description().id, "song1"); + assert_eq!(list_iter.next().unwrap().description().id, "song2"); + assert_eq!(list_iter.next().unwrap().description().id, "song3"); assert!(list_iter.next().is_none()); } @@ -476,11 +598,11 @@ mod tests { list.append(vec![song("song4")]); let mut list_iter = list.iter(); - assert_eq!(list_iter.next().unwrap().id, "song0"); - assert_eq!(list_iter.next().unwrap().id, "song1"); - assert_eq!(list_iter.next().unwrap().id, "song2"); - assert_eq!(list_iter.next().unwrap().id, "song3"); - assert_eq!(list_iter.next().unwrap().id, "song4"); + assert_eq!(list_iter.next().unwrap().description().id, "song0"); + assert_eq!(list_iter.next().unwrap().description().id, "song1"); + assert_eq!(list_iter.next().unwrap().description().id, "song2"); + assert_eq!(list_iter.next().unwrap().description().id, "song3"); + assert_eq!(list_iter.next().unwrap().description().id, "song4"); assert!(list_iter.next().is_none()); } @@ -497,9 +619,9 @@ mod tests { list.swap(2, 3); // should be no-op let mut list_iter = list.iter(); - assert_eq!(list_iter.next().unwrap().id, "song1"); - assert_eq!(list_iter.next().unwrap().id, "song2"); - assert_eq!(list_iter.next().unwrap().id, "song0"); + assert_eq!(list_iter.next().unwrap().description().id, "song1"); + assert_eq!(list_iter.next().unwrap().description().id, "song2"); + assert_eq!(list_iter.next().unwrap().description().id, "song0"); assert!(list_iter.next().is_none()); } } diff --git a/src/app/state/app_state.rs b/src/app/state/app_state.rs index 8a140945..99033014 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}, - PlaylistChange, ScreenName, UpdatableState, + ScreenName, UpdatableState, }; #[derive(Clone, Debug)] @@ -88,22 +88,24 @@ 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); - } + self.playback.queue(self.selection.take_selection()); vec![ SelectionEvent::SelectionModeChanged(false).into(), - PlaybackEvent::PlaylistChanged(PlaylistChange::AppendedAt(append_at)).into(), + PlaybackEvent::PlaylistChanged.into(), ] } AppAction::DequeueSelection => { - for track in self.selection.take_selection() { - self.playback.dequeue(&track.id); - } + let tracks: Vec = self + .selection + .take_selection() + .into_iter() + .map(|s| s.id) + .collect(); + self.playback.dequeue(&tracks); + vec![ SelectionEvent::SelectionModeChanged(false).into(), - PlaybackEvent::PlaylistChanged(PlaylistChange::Reset).into(), + PlaybackEvent::PlaylistChanged.into(), ] } AppAction::MoveDownSelection => { @@ -112,11 +114,7 @@ impl AppState { selection .next() .and_then(|song| playback.move_down(&song.id)) - .map(|index| { - vec![ - PlaybackEvent::PlaylistChanged(PlaylistChange::MovedDown(index)).into(), - ] - }) + .map(|_| vec![PlaybackEvent::PlaylistChanged.into()]) .unwrap_or_else(Vec::new) } AppAction::MoveUpSelection => { @@ -125,9 +123,7 @@ impl AppState { selection .next() .and_then(|song| playback.move_up(&song.id)) - .map(|index| { - vec![PlaybackEvent::PlaylistChanged(PlaylistChange::MovedUp(index)).into()] - }) + .map(|_| vec![PlaybackEvent::PlaylistChanged.into()]) .unwrap_or_else(Vec::new) } AppAction::EnableSelection(context) => { diff --git a/src/app/state/browser_state.rs b/src/app/state/browser_state.rs index 86d10916..726712e1 100644 --- a/src/app/state/browser_state.rs +++ b/src/app/state/browser_state.rs @@ -56,10 +56,10 @@ pub enum BrowserEvent { LibraryUpdated, SavedPlaylistsUpdated, AlbumDetailsLoaded(String), - AlbumTracksAppended(String, usize), + AlbumTracksAppended(String), PlaylistDetailsLoaded(String), - PlaylistTracksAppended(String, usize), - PlaylistTracksRemoved(String, Vec), + PlaylistTracksAppended(String), + PlaylistTracksRemoved(String), SearchUpdated, SearchResultsUpdated, ArtistDetailsUpdated(String), diff --git a/src/app/state/playback_state.rs b/src/app/state/playback_state.rs index c37fff99..75480b0d 100644 --- a/src/app/state/playback_state.rs +++ b/src/app/state/playback_state.rs @@ -1,13 +1,11 @@ -use crate::app::models::{InsertionRange, SongBatch, SongDescription, SongList}; +use crate::app::models::{SongBatch, SongDescription, SongListModel, SongListModelPending}; use crate::app::state::{AppAction, AppEvent, UpdatableState}; use crate::app::{BatchQuery, LazyRandomIndex, SongsSource}; -const RANGE_SIZE: usize = 25; - #[derive(Debug)] pub struct PlaybackState { index: LazyRandomIndex, - songs: SongList, + songs: SongListModel, position: Option, source: Option, repeat: RepeatMode, @@ -16,6 +14,10 @@ pub struct PlaybackState { } impl PlaybackState { + pub fn songs(&self) -> &SongListModel { + &self.songs + } + pub fn is_playing(&self) -> bool { self.is_playing && self.position.is_some() } @@ -44,75 +46,62 @@ impl PlaybackState { } } - pub fn song(&self, id: &str) -> Option<&SongDescription> { - self.songs.get(id) - } - - pub fn len(&self) -> usize { - self.songs.len() - } - - pub fn songs(&self) -> impl Iterator + '_ { - self.songs.iter() - } - - fn index(&self, i: usize) -> Option<&SongDescription> { - if self.is_shuffled { + fn index(&self, i: usize) -> Option { + let song = if self.is_shuffled { self.songs.index(self.index.get(i)?) } else { self.songs.index(i) - } + }; + Some(song?.into_description()) } - pub fn current_song_id(&self) -> Option<&String> { - Some(&self.index(self.position?).as_ref()?.id) + pub fn current_song_id(&self) -> Option { + Some(self.index(self.position?)?.id) } - pub fn current_song(&self) -> Option<&SongDescription> { + pub fn current_song(&self) -> Option { self.index(self.position?) } - pub fn prev_song(&self) -> Option<&SongDescription> { - self.prev_index().and_then(|i| self.index(i)) - } - - pub fn next_song(&self) -> Option<&SongDescription> { - self.next_index().and_then(|i| self.index(i)) - } - - fn set_source(&mut self, source: Option) { - self.songs = SongList::new_sized(2 * RANGE_SIZE); + fn clear(&mut self, source: Option) -> SongListModelPending { self.source = source; self.index = Default::default(); self.position = None; + self.songs.clear() + } + + fn set_batch(&mut self, source: Option, song_batch: SongBatch) -> bool { + let ok = self.clear(source).and(|s| s.add(song_batch)).commit(); + self.index.resize(self.songs.len()); + ok } - fn add_batch(&mut self, song_batch: SongBatch) -> Option { - let SongBatch { songs, batch } = song_batch; - let range = self.songs.add(SongBatch { songs, batch }); + fn add_batch(&mut self, song_batch: SongBatch) -> bool { + let ok = self.songs.add(song_batch).commit(); self.index.resize(self.songs.len()); - range + ok } - pub fn queue(&mut self, track: SongDescription) { - self.songs.append(vec![track]); + pub fn set_queue(&mut self, tracks: Vec) { + self.clear(None).and(|s| s.append(tracks)).commit(); self.index.grow(self.songs.len()); } - pub fn dequeue(&mut self, id: &str) { - let position = self.songs.find_index(id); - self.songs.remove(&[id.to_string()]); - let new_len = self.songs.len(); - self.position = self - .position - .filter(|_| new_len > 0) - .and_then(|p| Some(if p > 0 && p >= position? { p - 1 } else { p })); - self.index.shrink(new_len); + pub fn queue(&mut self, tracks: Vec) { + self.source = None; + self.songs.append(tracks).commit(); + self.index.grow(self.songs.len()); } - fn swap(&mut self, index: usize, other_index: usize) { + pub fn dequeue(&mut self, ids: &[String]) { + let current_id = self.current_song_id(); + self.songs.remove(ids).commit(); + self.position = current_id.and_then(|id| self.songs.find_index(&id)); + self.index.shrink(self.songs.len()); + } + + fn swap_pos(&mut self, index: usize, other_index: usize) { let len = self.songs.len(); - self.songs.swap(index, other_index); self.position = self .position .map(|position| match position { @@ -125,13 +114,15 @@ impl PlaybackState { pub fn move_down(&mut self, id: &str) -> Option { let index = self.songs.find_index(id)?; - self.swap(index, index + 1); + self.songs.move_down(index).commit(); + self.swap_pos(index + 1, index); Some(index) } pub fn move_up(&mut self, id: &str) -> Option { let index = self.songs.find_index(id).filter(|&index| index > 0)?; - self.swap(index - 1, index); + self.songs.move_up(index).commit(); + self.swap_pos(index - 1, index); Some(index) } @@ -160,18 +151,18 @@ impl PlaybackState { self.is_playing = false; } - fn play_index(&mut self, index: usize) -> Option<&String> { + fn play_index(&mut self, index: usize) -> Option { self.is_playing = true; self.position.replace(index); self.index.next_until(index + 1); self.current_song_id() } - fn play_next(&mut self) -> Option<&String> { + fn play_next(&mut self) -> Option { self.next_index().and_then(move |i| self.play_index(i)) } - fn next_index(&self) -> Option { + pub fn next_index(&self) -> Option { let len = self.songs.len(); self.position.and_then(|p| match self.repeat { RepeatMode::Song => Some(p), @@ -181,11 +172,11 @@ impl PlaybackState { }) } - fn play_prev(&mut self) -> Option<&String> { + fn play_prev(&mut self) -> Option { self.prev_index().and_then(move |i| self.play_index(i)) } - fn prev_index(&self) -> Option { + pub fn prev_index(&self) -> Option { let len = self.songs.len(); self.position.and_then(|p| match self.repeat { RepeatMode::Song => Some(p), @@ -215,7 +206,7 @@ impl Default for PlaybackState { fn default() -> Self { Self { index: LazyRandomIndex::default(), - songs: SongList::new_sized(2 * RANGE_SIZE), + songs: SongListModel::new(50), position: None, source: None, repeat: RepeatMode::None, @@ -252,15 +243,6 @@ impl From for AppAction { } } -#[derive(Clone, Debug)] -pub enum PlaylistChange { - Reset, - InsertedAt(usize, usize), - AppendedAt(usize), - MovedUp(usize), - MovedDown(usize), -} - #[derive(Clone, Debug)] pub enum PlaybackEvent { PlaybackPaused, @@ -271,7 +253,7 @@ pub enum PlaybackEvent { VolumeSet(f64), TrackChanged(String), ShuffleChanged, - PlaylistChanged(PlaylistChange), + PlaylistChanged, PlaybackStopped, } @@ -340,7 +322,7 @@ impl UpdatableState for PlaybackState { vec![PlaybackEvent::ShuffleChanged] } PlaybackAction::Next => { - if let Some(id) = self.play_next().cloned() { + if let Some(id) = self.play_next() { make_events(vec![ Some(PlaybackEvent::TrackChanged(id)), Some(PlaybackEvent::PlaybackResumed), @@ -355,7 +337,7 @@ impl UpdatableState for PlaybackState { vec![PlaybackEvent::PlaybackStopped] } PlaybackAction::Previous => { - if let Some(id) = self.play_prev().cloned() { + if let Some(id) = self.play_prev() { make_events(vec![ Some(PlaybackEvent::TrackChanged(id)), Some(PlaybackEvent::PlaybackResumed), @@ -374,20 +356,11 @@ impl UpdatableState for PlaybackState { vec![] } } - PlaybackAction::LoadSongs(tracks) => { - self.set_source(None); - for track in tracks { - self.queue(track); - } - vec![PlaybackEvent::PlaylistChanged(PlaylistChange::Reset)] - } PlaybackAction::LoadPagedSongs(source, batch) if Some(&source) == self.source.as_ref() => { - if let Some(InsertionRange(a, b)) = self.add_batch(batch) { - vec![PlaybackEvent::PlaylistChanged(PlaylistChange::InsertedAt( - a, b, - ))] + if self.add_batch(batch) { + vec![PlaybackEvent::PlaylistChanged] } else { vec![] } @@ -395,23 +368,20 @@ impl UpdatableState for PlaybackState { PlaybackAction::LoadPagedSongs(source, batch) if Some(&source) != self.source.as_ref() => { - self.set_source(Some(source)); - self.add_batch(batch); - vec![PlaybackEvent::PlaylistChanged(PlaylistChange::Reset)] + self.set_batch(Some(source), batch); + vec![PlaybackEvent::PlaylistChanged] + } + PlaybackAction::LoadSongs(tracks) => { + self.set_queue(tracks); + vec![PlaybackEvent::PlaylistChanged] } PlaybackAction::Queue(tracks) => { - let append_at = self.songs.partial_len(); - self.source = None; - for track in tracks { - self.queue(track); - } - vec![PlaybackEvent::PlaylistChanged(PlaylistChange::AppendedAt( - append_at, - ))] + self.queue(tracks); + vec![PlaybackEvent::PlaylistChanged] } PlaybackAction::Dequeue(id) => { - self.dequeue(&id); - vec![PlaybackEvent::PlaylistChanged(PlaylistChange::Reset)] + self.dequeue(&[id]); + vec![PlaybackEvent::PlaylistChanged] } PlaybackAction::Seek(pos) => vec![PlaybackEvent::TrackSeeked(pos)], PlaybackAction::SyncSeek(pos) => vec![PlaybackEvent::SeekSynced(pos)], @@ -448,12 +418,22 @@ mod tests { self.position } - fn song_ids(&self) -> Vec<&str> { - self.songs().map(|s| &s.id[..]).collect() + fn prev_id(&self) -> Option { + self.prev_index() + .and_then(|i| Some(self.songs().index(i)?.description().id.clone())) + } + + fn next_id(&self) -> Option { + self.next_index() + .and_then(|i| Some(self.songs().index(i)?.description().id.clone())) } - fn song_id(&self) -> Option<&str> { - self.current_song().map(|s| &s.id[..]) + fn song_ids(&self) -> Vec { + self.songs() + .collect() + .iter() + .map(|s| s.id.clone()) + .collect() } } @@ -463,21 +443,21 @@ mod tests { assert!(!state.is_playing()); assert!(!state.is_shuffled()); assert!(state.current_song().is_none()); - assert!(state.prev_song().is_none()); - assert!(state.next_song().is_none()); + assert!(state.prev_index().is_none()); + assert!(state.next_index().is_none()); } #[test] fn test_play_one() { let mut state = PlaybackState::default(); - state.queue(song("foo")); + state.queue(vec![song("foo")]); state.play("foo"); assert!(state.is_playing()); - assert_eq!(state.song_id(), Some("foo")); - assert!(state.prev_song().is_none()); - assert!(state.next_song().is_none()); + assert_eq!(state.current_song_id(), Some("foo".to_string())); + assert!(state.prev_index().is_none()); + assert!(state.next_index().is_none()); state.toggle_play(); assert!(!state.is_playing()); @@ -486,33 +466,29 @@ mod tests { #[test] fn test_queue() { let mut state = PlaybackState::default(); - state.queue(song("1")); - state.queue(song("2")); - state.queue(song("3")); + state.queue(vec![song("1"), song("2"), song("3")]); - assert_eq!(state.songs().count(), 3); + assert_eq!(state.songs().len(), 3); state.play("2"); - state.queue(song("4")); - assert_eq!(state.songs().count(), 4); + state.queue(vec![song("4")]); + assert_eq!(state.songs().len(), 4); } #[test] fn test_play_multiple() { let mut state = PlaybackState::default(); - state.queue(song("1")); - state.queue(song("2")); - state.queue(song("3")); - assert_eq!(state.songs().count(), 3); + state.queue(vec![song("1"), song("2"), song("3")]); + assert_eq!(state.songs().len(), 3); state.play("2"); assert!(state.is_playing()); assert_eq!(state.current_position(), Some(1)); - assert_eq!(state.prev_song().map(|s| &s.id[..]), Some("1")); - assert_eq!(state.song_id(), Some("2")); - assert_eq!(state.next_song().map(|s| &s.id[..]), Some("3")); + assert_eq!(state.prev_id(), Some("1".to_string())); + assert_eq!(state.current_song_id(), Some("2".to_string())); + assert_eq!(state.next_id(), Some("3".to_string())); state.toggle_play(); assert!(!state.is_playing()); @@ -520,38 +496,35 @@ mod tests { state.play_next(); assert!(state.is_playing()); assert_eq!(state.current_position(), Some(2)); - assert_eq!(state.prev_song().map(|s| &s.id[..]), Some("2")); - assert_eq!(state.song_id(), Some("3")); - assert!(state.next_song().is_none()); + assert_eq!(state.prev_id(), Some("2".to_string())); + assert_eq!(state.current_song_id(), Some("3".to_string())); + assert!(state.next_index().is_none()); state.play_next(); assert!(state.is_playing()); assert_eq!(state.current_position(), Some(2)); - assert_eq!(state.song_id(), Some("3")); + assert_eq!(state.current_song_id(), Some("3".to_string())); state.play_prev(); state.play_prev(); assert!(state.is_playing()); assert_eq!(state.current_position(), Some(0)); - assert!(state.prev_song().is_none()); - assert_eq!(state.song_id(), Some("1")); - assert_eq!(state.next_song().map(|s| &s.id[..]), Some("2")); + assert!(state.prev_index().is_none()); + assert_eq!(state.current_song_id(), Some("1".to_string())); + assert_eq!(state.next_id(), Some("2".to_string())); state.play_prev(); assert!(state.is_playing()); assert_eq!(state.current_position(), Some(0)); - assert_eq!(state.song_id(), Some("1")); + assert_eq!(state.current_song_id(), Some("1".to_string())); } #[test] fn test_shuffle() { let mut state = PlaybackState::default(); - state.queue(song("1")); - state.queue(song("2")); - state.queue(song("3")); - state.queue(song("4")); + state.queue(vec![song("1"), song("2"), song("3"), song("4")]); - assert_eq!(state.songs().count(), 4); + assert_eq!(state.songs().len(), 4); state.play("2"); assert_eq!(state.current_position(), Some(1)); @@ -567,103 +540,113 @@ mod tests { assert!(!state.is_shuffled()); let ids = state.song_ids(); - assert_eq!(ids, vec!["1", "2", "3", "4"]); + assert_eq!( + ids, + vec![ + "1".to_string(), + "2".to_string(), + "3".to_string(), + "4".to_string() + ] + ); } #[test] fn test_shuffle_queue() { let mut state = PlaybackState::default(); - state.queue(song("1")); - state.queue(song("2")); - state.queue(song("3")); + state.queue(vec![song("1"), song("2"), song("3")]); state.toggle_shuffle(); assert!(state.is_shuffled()); - state.queue(song("4")); + state.queue(vec![song("4")]); state.toggle_shuffle(); assert!(!state.is_shuffled()); let ids = state.song_ids(); - assert_eq!(ids, vec!["1", "2", "3", "4"]); + assert_eq!( + ids, + vec![ + "1".to_string(), + "2".to_string(), + "3".to_string(), + "4".to_string() + ] + ); } #[test] fn test_move() { let mut state = PlaybackState::default(); - state.queue(song("1")); - state.queue(song("2")); - state.queue(song("3")); + state.queue(vec![song("1"), song("2"), song("3")]); state.play("2"); assert!(state.is_playing()); state.move_down("1"); - assert_eq!(state.song_id(), Some("2")); + assert_eq!(state.current_song_id(), Some("2".to_string())); let ids = state.song_ids(); - assert_eq!(ids, vec!["2", "1", "3"]); + assert_eq!(ids, vec!["2".to_string(), "1".to_string(), "3".to_string()]); state.move_down("2"); state.move_down("2"); - assert_eq!(state.song_id(), Some("2")); + assert_eq!(state.current_song_id(), Some("2".to_string())); let ids = state.song_ids(); - assert_eq!(ids, vec!["1", "3", "2"]); + assert_eq!(ids, vec!["1".to_string(), "3".to_string(), "2".to_string()]); state.move_down("2"); - assert_eq!(state.song_id(), Some("2")); + assert_eq!(state.current_song_id(), Some("2".to_string())); let ids = state.song_ids(); - assert_eq!(ids, vec!["1", "3", "2"]); + assert_eq!(ids, vec!["1".to_string(), "3".to_string(), "2".to_string()]); state.move_up("2"); - assert_eq!(state.song_id(), Some("2")); + assert_eq!(state.current_song_id(), Some("2".to_string())); let ids = state.song_ids(); - assert_eq!(ids, vec!["1", "2", "3"]); + assert_eq!(ids, vec!["1".to_string(), "2".to_string(), "3".to_string()]); } #[test] fn test_dequeue_last() { let mut state = PlaybackState::default(); - state.queue(song("1")); - state.queue(song("2")); - state.queue(song("3")); + state.queue(vec![song("1"), song("2"), song("3")]); state.play("3"); assert!(state.is_playing()); - state.dequeue("3"); - assert_eq!(state.song_id(), Some("2")); + state.dequeue(&["3".to_string()]); + assert_eq!(state.current_song_id(), None); } #[test] fn test_dequeue_a_few_songs() { let mut state = PlaybackState::default(); - state.queue(song("1")); - state.queue(song("2")); - state.queue(song("3")); - state.queue(song("4")); - state.queue(song("5")); - state.queue(song("6")); + state.queue(vec![ + song("1"), + song("2"), + song("3"), + song("4"), + song("5"), + song("6"), + ]); state.play("5"); assert!(state.is_playing()); - state.dequeue("1"); - state.dequeue("2"); - state.dequeue("3"); - assert_eq!(state.song_id(), Some("5")); + state.dequeue(&["1".to_string(), "2".to_string(), "3".to_string()]); + assert_eq!(state.current_song_id(), Some("5".to_string())); } #[test] fn test_dequeue_all() { let mut state = PlaybackState::default(); - state.queue(song("3")); + state.queue(vec![song("3")]); state.play("3"); assert!(state.is_playing()); - state.dequeue("3"); - assert_eq!(state.song_id(), None); + state.dequeue(&["3".to_string()]); + assert_eq!(state.current_song_id(), None); } } diff --git a/src/app/state/screen_states.rs b/src/app/state/screen_states.rs index 63f83c38..ee609a42 100644 --- a/src/app/state/screen_states.rs +++ b/src/app/state/screen_states.rs @@ -40,6 +40,7 @@ pub struct DetailsState { pub id: String, pub name: ScreenName, pub content: Option, + pub songs: SongListModel, } impl DetailsState { @@ -48,6 +49,7 @@ impl DetailsState { id: id.clone(), name: ScreenName::AlbumDetails(id), content: None, + songs: SongListModel::new(100), } } } @@ -59,22 +61,14 @@ impl UpdatableState for DetailsState { fn update_with(&mut self, action: Self::Action) -> Vec { match action { BrowserAction::SetAlbumDetails(album) if album.description.id == self.id => { - let id = album.description.id.clone(); + let AlbumDescription { id, songs, .. } = album.description.clone(); + self.songs.add(songs).commit(); self.content = Some(*album); vec![BrowserEvent::AlbumDetailsLoaded(id)] } BrowserAction::AppendAlbumTracks(id, batch) if id == self.id => { - let offset = batch.batch.offset; - if self - .content - .as_mut() - .and_then(|content| content.description.songs.add(*batch)) - .is_some() - { - vec![BrowserEvent::AlbumTracksAppended(id, offset)] - } else { - vec![] - } + self.songs.add(*batch).commit(); + vec![BrowserEvent::AlbumTracksAppended(id)] } BrowserAction::SaveAlbum(album) if album.id == self.id => { let id = album.id; @@ -102,6 +96,7 @@ pub struct PlaylistDetailsState { pub id: String, pub name: ScreenName, pub playlist: Option, + pub songs: SongListModel, } impl PlaylistDetailsState { @@ -110,6 +105,7 @@ impl PlaylistDetailsState { id: id.clone(), name: ScreenName::PlaylistDetails(id), playlist: None, + songs: SongListModel::new(100), } } } @@ -121,31 +117,18 @@ impl UpdatableState for PlaylistDetailsState { fn update_with(&mut self, action: Self::Action) -> Vec { match action { BrowserAction::SetPlaylistDetails(playlist) => { - let id = playlist.id.clone(); + let PlaylistDescription { id, songs, .. } = *playlist.clone(); + self.songs.add(songs).commit(); self.playlist = Some(*playlist); vec![BrowserEvent::PlaylistDetailsLoaded(id)] } BrowserAction::AppendPlaylistTracks(id, song_batch) if id == self.id => { - let offset = song_batch.batch.offset; - if self - .playlist - .as_mut() - .and_then(|playlist| playlist.songs.add(*song_batch)) - .is_some() - { - vec![BrowserEvent::PlaylistTracksAppended(id, offset)] - } else { - vec![] - } + self.songs.add(*song_batch).commit(); + vec![BrowserEvent::PlaylistTracksAppended(id)] } BrowserAction::RemoveTracksFromPlaylist(uris) => { - if let Some(playlist) = self.playlist.as_mut() { - let id = playlist.id.clone(); - playlist.songs.remove(&uris[..]); - vec![BrowserEvent::PlaylistTracksRemoved(id, uris)] - } else { - vec![] - } + self.songs.remove(&uris[..]).commit(); + vec![BrowserEvent::PlaylistTracksRemoved(self.id.clone())] } _ => vec![], } @@ -158,7 +141,7 @@ pub struct ArtistState { pub artist: Option, pub next_page: Pagination, pub albums: ListStore, - pub top_tracks: Vec, + pub top_tracks: SongListModel, } impl ArtistState { @@ -169,7 +152,7 @@ impl ArtistState { artist: None, next_page: Pagination::new(id, 20), albums: ListStore::new(), - top_tracks: vec![], + top_tracks: SongListModel::new(10), } } } @@ -193,7 +176,7 @@ impl UpdatableState for ArtistState { self.next_page.reset_count(self.albums.len()); top_tracks.truncate(5); - self.top_tracks = top_tracks; + self.top_tracks.append(top_tracks).commit(); vec![BrowserEvent::ArtistDetailsUpdated(id)] } @@ -213,7 +196,7 @@ pub struct HomeState { pub albums: ListStore, pub next_playlists_page: Pagination<()>, pub playlists: ListStore, - pub saved_tracks: SongList, + pub saved_tracks: SongListModel, } impl Default for HomeState { @@ -224,7 +207,7 @@ impl Default for HomeState { albums: ListStore::new(), next_playlists_page: Pagination::new((), 30), playlists: ListStore::new(), - saved_tracks: SongList::new_sized(50), + saved_tracks: SongListModel::new(50), } } } @@ -236,10 +219,7 @@ impl UpdatableState for HomeState { fn update_with(&mut self, action: Self::Action) -> Vec { match action { BrowserAction::SetLibraryContent(content) => { - if !self - .albums - .eq(&content, |a, b| a.uri().as_ref() == Some(&b.id)) - { + if !self.albums.eq(&content, |a, b| a.uri() == b.id) { self.albums .replace_all(content.into_iter().map(|a| a.into())); self.next_albums_page.reset_count(self.albums.len()); @@ -255,10 +235,7 @@ impl UpdatableState for HomeState { } BrowserAction::SaveAlbum(album) => { let album_id = album.id.clone(); - let already_present = self - .albums - .iter() - .any(|a| a.uri().as_ref() == Some(&album_id)); + let already_present = self.albums.iter().any(|a| a.uri() == album_id); if already_present { vec![] } else { @@ -268,10 +245,7 @@ impl UpdatableState for HomeState { } } BrowserAction::UnsaveAlbum(id) => { - let position = self - .albums - .iter() - .position(|a| a.uri().as_ref() == Some(&id)); + let position = self.albums.iter().position(|a| a.uri() == id); if let Some(position) = position { self.albums.remove(position as u32); self.next_albums_page.decrement(); @@ -281,10 +255,7 @@ impl UpdatableState for HomeState { } } BrowserAction::SetPlaylistsContent(content) => { - if !self - .playlists - .eq(&content, |a, b| a.uri().as_ref() == Some(&b.id)) - { + if !self.playlists.eq(&content, |a, b| a.uri() == b.id) { self.playlists .replace_all(content.into_iter().map(|a| a.into())); self.next_playlists_page.reset_count(self.playlists.len()); @@ -300,7 +271,7 @@ impl UpdatableState for HomeState { } BrowserAction::AppendSavedTracks(song_batch) => { let offset = song_batch.batch.offset; - if self.saved_tracks.add(*song_batch).is_some() { + if self.saved_tracks.add(*song_batch).commit() { vec![BrowserEvent::SavedTracksAppended(offset)] } else { vec![] @@ -428,7 +399,7 @@ mod tests { artists: vec![], release_date: Some("1970-01-01".to_owned()), art: Some("".to_owned()), - songs: SongList::new_sized(100), + songs: SongBatch::empty(), is_liked: false, }; let mut artist_state = ArtistState::new("id".to_owned()); diff --git a/src/dbus/listener.rs b/src/dbus/listener.rs index bda57021..9038cee3 100644 --- a/src/dbus/listener.rs +++ b/src/dbus/listener.rs @@ -39,36 +39,30 @@ impl AppPlaybackStateListener { } fn make_track_meta(&self) -> Option { - self.app_model - .get_state() - .playback - .current_song() - .cloned() - .map( - |SongDescription { - id, - title, - artists, - album, - duration, - art, - .. - }| TrackMetadata { - id: format!("/dev/alextren/Spot/Track/{}", id), - length: 1000 * duration as u64, - title, - album: album.name, - artist: artists.into_iter().map(|a| a.name).collect(), - art, - }, - ) + let SongDescription { + id, + title, + artists, + album, + duration, + art, + .. + } = self.app_model.get_state().playback.current_song()?; + Some(TrackMetadata { + id: format!("/dev/alextren/Spot/Track/{}", id), + length: 1000 * duration as u64, + title, + album: album.name, + artist: artists.into_iter().map(|a| a.name).collect(), + art, + }) } fn has_prev_next(&self) -> (bool, bool) { let state = self.app_model.get_state(); ( - state.playback.prev_song().is_some(), - state.playback.next_song().is_some(), + state.playback.prev_index().is_some(), + state.playback.next_index().is_some(), ) } diff --git a/src/main.rs b/src/main.rs index 7dd0a5fb..b74a0087 100644 --- a/src/main.rs +++ b/src/main.rs @@ -44,7 +44,6 @@ fn main() { } let context = glib::MainContext::default(); - context.push_thread_default(); let dispatch_loop = DispatchLoop::new(); let sender = dispatch_loop.make_dispatcher(); @@ -78,7 +77,7 @@ fn main() { fn startup(settings: &settings::SpotSettings) { gtk::init().unwrap_or_else(|_| panic!("Failed to initialize GTK")); libadwaita::init(); - let manager = libadwaita::StyleManager::default().unwrap(); + let manager = libadwaita::StyleManager::default(); let res = gio::Resource::load(config::PKGDATADIR.to_owned() + "/spot.gresource") .expect("Could not load resources"); diff --git a/src/meson.build b/src/meson.build index f48fdbb5..f0235a60 100644 --- a/src/meson.build +++ b/src/meson.build @@ -96,7 +96,12 @@ sources = files( './app/components/saved_tracks/saved_tracks.rs', './app/components/saved_tracks/saved_tracks_model.rs', './app/components/saved_tracks/mod.rs', +'./app/components/sidebar_listbox/sidebar_item.rs', +'./app/components/sidebar_listbox/sidebar_row.rs', +'./app/components/sidebar_listbox/mod.rs', +'./app/components/sidebar_listbox/sidebar_icon_widget.rs', './app/components/search/search_model.rs', +'./app/components/search/search_button.rs', './app/components/search/mod.rs', './app/components/search/search.rs', './app/components/artist/mod.rs', @@ -113,12 +118,9 @@ sources = files( './app/components/selection/widget.rs', './app/components/selection/component.rs', './app/components/selection/mod.rs', -'./app/components/sidebar_listbox/mod.rs', -'./app/components/sidebar_listbox/sidebar_item.rs', -'./app/components/sidebar_listbox/sidebar_row.rs', './app/components/mod.rs', -'./app/components/details/release_details.rs', './app/components/details/album_header.rs', +'./app/components/details/release_details.rs', './app/components/details/mod.rs', './app/components/details/details.rs', './app/components/details/details_model.rs', @@ -151,11 +153,12 @@ sources = files( './app/components/library/mod.rs', './app/components/library/library_model.rs', './app/loader.rs', -'./app/models/core.rs', -'./app/models/gtypes/artist_model.rs', -'./app/models/gtypes/song_model.rs', -'./app/models/gtypes/album_model.rs', -'./app/models/gtypes/mod.rs', +'./app/models/songs/support.rs', +'./app/models/songs/song_list_model.rs', +'./app/models/songs/song_model.rs', +'./app/models/songs/mod.rs', +'./app/models/artist_model.rs', +'./app/models/album_model.rs', './app/models/main.rs', './app/models/mod.rs', './app/dispatch.rs',