From 7dbae71bba2a4cf129ee177c5bea85966f826e8e Mon Sep 17 00:00:00 2001 From: dhonus Date: Mon, 10 Feb 2025 14:28:59 +0100 Subject: [PATCH 01/26] chore: updated dependencies --- Cargo.lock | 265 +++++++++++++++++++++++++++++++++------------------ Cargo.toml | 4 +- src/queue.rs | 2 +- 3 files changed, 176 insertions(+), 95 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 22cb6da..3915716 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,7 +127,7 @@ dependencies = [ "futures-lite 2.6.0", "parking", "polling 3.7.4", - "rustix 0.38.43", + "rustix 0.38.44", "slab", "tracing", "windows-sys 0.59.0", @@ -166,7 +166,7 @@ dependencies = [ "cfg-if", "event-listener 3.1.0", "futures-lite 1.13.0", - "rustix 0.38.43", + "rustix 0.38.44", "windows-sys 0.48.0", ] @@ -178,7 +178,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -193,7 +193,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 0.38.43", + "rustix 0.38.44", "signal-hook-registry", "slab", "windows-sys 0.59.0", @@ -207,13 +207,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.85" +version = "0.1.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" +checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -300,9 +300,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "bytemuck" @@ -324,9 +324,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" [[package]] name = "cassowary" @@ -345,9 +345,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.10" +version = "1.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" +checksum = "c7777341816418c02e033934a09f20dc0ccaf65a5201ef8a450ae0105a573fda" dependencies = [ "shlex", ] @@ -477,9 +477,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -509,7 +509,7 @@ dependencies = [ "crossterm_winapi", "mio", "parking_lot", - "rustix 0.38.43", + "rustix 0.38.44", "signal-hook", "signal-hook-mio", "winapi", @@ -555,7 +555,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -566,7 +566,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -625,7 +625,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -661,7 +661,7 @@ checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -845,7 +845,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -895,7 +895,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", ] [[package]] @@ -994,15 +1006,15 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.5" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" [[package]] name = "hyper" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes", "futures-channel", @@ -1208,7 +1220,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1271,9 +1283,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown", @@ -1295,7 +1307,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1320,9 +1332,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.10.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "itertools" @@ -1341,7 +1353,7 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jellyfin-tui" -version = "1.1.1" +version = "1.1.2-dev" dependencies = [ "chrono", "color-thief", @@ -1349,7 +1361,7 @@ dependencies = [ "dirs", "image", "libmpv2", - "rand", + "rand 0.9.0", "ratatui", "ratatui-image", "reqwest", @@ -1501,15 +1513,15 @@ checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] [[package]] name = "native-tls" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" dependencies = [ "libc", "log", @@ -1563,15 +1575,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" [[package]] name = "openssl" -version = "0.10.68" +version = "0.10.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" dependencies = [ "bitflags 2.8.0", "cfg-if", @@ -1590,20 +1602,20 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.104" +version = "0.9.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc" dependencies = [ "cc", "libc", @@ -1736,7 +1748,7 @@ dependencies = [ "concurrent-queue", "hermit-abi 0.4.0", "pin-project-lite", - "rustix 0.38.43", + "rustix 0.38.44", "tracing", "windows-sys 0.59.0", ] @@ -1753,7 +1765,7 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -1797,8 +1809,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.0", + "zerocopy 0.8.17", ] [[package]] @@ -1808,7 +1831,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.0", ] [[package]] @@ -1817,7 +1850,17 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" +dependencies = [ + "getrandom 0.3.1", + "zerocopy 0.8.17", ] [[package]] @@ -1851,9 +1894,9 @@ dependencies = [ "base64 0.21.7", "icy_sixel", "image", - "rand", + "rand 0.8.5", "ratatui", - "rustix 0.38.43", + "rustix 0.38.44", "thiserror 1.0.69", "windows 0.58.0", ] @@ -1873,7 +1916,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ - "getrandom", + "getrandom 0.2.15", "libredox", "thiserror 2.0.11", ] @@ -1970,7 +2013,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.15", "libc", "spin", "untrusted", @@ -1999,9 +2042,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.43" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags 2.8.0", "errno", @@ -2012,9 +2055,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.21" +version = "0.23.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" +checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7" dependencies = [ "once_cell", "rustls-pki-types", @@ -2034,9 +2077,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" [[package]] name = "rustls-webpki" @@ -2057,9 +2100,9 @@ checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" [[package]] name = "schannel" @@ -2116,14 +2159,14 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] name = "serde_json" -version = "1.0.136" +version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336a0c23cf42a38d9eaa7cd22c7040d04e1228a19a933890805ffd00a16437d2" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" dependencies = [ "itoa", "memchr", @@ -2139,7 +2182,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2317,7 +2360,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2339,9 +2382,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.96" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", @@ -2365,7 +2408,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2391,15 +2434,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.15.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" dependencies = [ "cfg-if", "fastrand 2.3.0", - "getrandom", + "getrandom 0.3.1", "once_cell", - "rustix 0.38.43", + "rustix 0.38.44", "windows-sys 0.59.0", ] @@ -2429,7 +2472,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2440,7 +2483,7 @@ checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2479,7 +2522,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2578,7 +2621,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2615,9 +2658,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" [[package]] name = "unicode-segmentation" @@ -2716,6 +2759,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -2738,7 +2790,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "wasm-bindgen-shared", ] @@ -2773,7 +2825,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2881,7 +2933,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2892,7 +2944,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -3139,6 +3191,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.8.0", +] + [[package]] name = "write16" version = "1.0.0" @@ -3181,7 +3242,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "synstructure", ] @@ -3212,7 +3273,7 @@ dependencies = [ "nix", "once_cell", "ordered-stream", - "rand", + "rand 0.8.5", "serde", "serde_repr", "sha1", @@ -3258,7 +3319,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa91407dacce3a68c56de03abe2760159582b846c6a4acd2f456618087f12713" +dependencies = [ + "zerocopy-derive 0.8.17", ] [[package]] @@ -3269,7 +3339,18 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06718a168365cad3d5ff0bb133aad346959a2074bd4a85c121255a11304a8626" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", ] [[package]] @@ -3289,7 +3370,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "synstructure", ] @@ -3318,7 +3399,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e7badfd..e82a986 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jellyfin-tui" -version = "1.1.1" +version = "1.1.2-dev" edition = "2021" [dependencies] @@ -18,4 +18,4 @@ dirs = "6.0.0" chrono = "0.4" souvlaki = { version = "0.8.0", default-features = false, features = ["use_zbus"] } color-thief = "0.2" -rand = "0.8.5" +rand = "0.9.0" diff --git a/src/queue.rs b/src/queue.rs index 4c84ab4..4ee2da7 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -502,7 +502,7 @@ impl App { // self.original_sublist = local_current.clone(); let mut desired_order = local_current.clone(); - desired_order.shuffle(&mut rand::thread_rng()); + desired_order.shuffle(&mut rand::rng()); // find in current and move it needed for i in 0..desired_order.len() { From aa656559a505eda8750ec45c691c4e86ad50817e Mon Sep 17 00:00:00 2001 From: dhonus Date: Thu, 13 Feb 2025 15:09:53 +0100 Subject: [PATCH 02/26] fix: cover art borders stylized --- src/library.rs | 2 +- src/playlists.rs | 2 +- src/tui.rs | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/library.rs b/src/library.rs index 49b5db3..5e361f0 100644 --- a/src/library.rs +++ b/src/library.rs @@ -51,7 +51,7 @@ impl App { let outer_area = outer_layout[0]; let block_bottom = Block::default() .borders(Borders::ALL) - .title("Cover art"); + .title("Cover art").white().border_style(style::Color::White); let chunk_area = block_bottom.inner(outer_area); let font_size = picker.font_size(); diff --git a/src/playlists.rs b/src/playlists.rs index 9256ccb..1b746fc 100644 --- a/src/playlists.rs +++ b/src/playlists.rs @@ -37,7 +37,7 @@ impl App { let outer_area = outer_layout[0]; let block_bottom = Block::default() .borders(Borders::ALL) - .title("Cover art"); + .title("Cover art").white().border_style(style::Color::White); let chunk_area = block_bottom.inner(outer_area); let font_size = picker.font_size(); diff --git a/src/tui.rs b/src/tui.rs index 4a44b52..275fd0a 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -472,8 +472,7 @@ impl App { if let Some(metadata) = self.metadata.as_mut() { if client.transcoding.enabled && state.audio_bitrate > 0 - && self.state.queue.get(state.current_index as usize) - .and_then(|s| Some(s.is_transcoded)).unwrap_or(false) + && self.state.queue.get(state.current_index as usize).map(|s| s.is_transcoded).unwrap_or(false) { metadata.bit_rate = state.audio_bitrate as u64; } @@ -1001,7 +1000,7 @@ impl App { let current_artist_id = self.state.current_artist.id.clone(); let current_playlist_id = self.state.current_playlist.id.clone(); - let active_section = self.state.active_section.clone(); + let active_section = self.state.active_section; self.discography(¤t_artist_id).await; self.playlist(¤t_playlist_id).await; From e70a9b36d48b61e003057f6abc4035a0c0fc6f69 Mon Sep 17 00:00:00 2001 From: dhonus Date: Sat, 15 Feb 2025 16:35:38 +0100 Subject: [PATCH 03/26] feat!: album tab and related changes --- src/client.rs | 105 ++++++- src/helpers.rs | 11 +- src/keyboard.rs | 333 +++++++++++++++++++++- src/library.rs | 717 ++++++++++++++++++++++++++++++----------------- src/playlists.rs | 2 +- src/tui.rs | 93 +++++- 6 files changed, 984 insertions(+), 277 deletions(-) diff --git a/src/client.rs b/src/client.rs index 6be3a61..557f436 100644 --- a/src/client.rs +++ b/src/client.rs @@ -266,6 +266,82 @@ impl Client { Ok(artists.items) } + /// Produces a list of all albums + /// + pub async fn albums(&self) -> Result, reqwest::Error> { + let url = format!("{}/Users/{}/Items", self.base_url, self.user_id); + + let response = self.http_client + .get(url) + .header("X-MediaBrowser-Token", self.access_token.to_string()) + .header("x-emby-authorization", "MediaBrowser Client=\"jellyfin-tui\", Device=\"jellyfin-tui\", DeviceId=\"None\", Version=\"10.4.3\"") + .header("Content-Type", "text/json") + .query(&[ + ("SortBy", "DateCreated,SortName"), + ("SortOrder", "Ascending"), + ("Recursive", "true"), + ("IncludeItemTypes", "MusicAlbum"), + ("Fields", "DateCreated, ParentId"), + ("ImageTypeLimit", "1") + ]) + .query(&[("StartIndex", "0")]) + .send() + .await; + + let albums = match response { + Ok(json) => { + let albums: Albums = json.json().await.unwrap_or_else(|_| Albums { + items: vec![], + }); + albums + }, + Err(_) => { + return Ok(vec![]); + } + }; + + Ok(albums.items) + } + + /// Produces a list of songs in an album + /// + pub async fn album_tracks(&self, id: &str) -> Result, reqwest::Error> { + let url = format!("{}/Users/{}/Items", self.base_url, self.user_id); + + let response = self.http_client + .get(url) + .header("X-MediaBrowser-Token", self.access_token.to_string()) + .header("x-emby-authorization", "MediaBrowser Client=\"jellyfin-tui\", Device=\"jellyfin-tui\", DeviceId=\"None\", Version=\"10.4.3\"") + .header("Content-Type", "text/json") + .query(&[ + ("SortBy", "ParentIndexNumber,IndexNumber,SortName"), + ("SortOrder", "Ascending"), + ("Recursive", "true"), + ("IncludeItemTypes", "Audio"), + ("Fields", "Genres, DateCreated, MediaSources, ParentId"), + ("ImageTypeLimit", "1"), + ("ParentId", id) + ]) + .query(&[("StartIndex", "0")]) + .send() + .await; + + let songs = match response { + Ok(json) => { + let songs: Discography = json.json().await.unwrap_or_else(|_| Discography { + items: vec![], + }); + songs.items + }, + Err(_) => { + return Ok(vec![]); + } + }; + + Ok(songs) + } + + /// Produces a list of songs by an artist sorted by album and index /// pub async fn discography(&self, id: &str, recently_added: bool) -> Result { @@ -298,7 +374,7 @@ impl Client { // group the songs by album let mut albums: Vec = vec![]; - let mut current_album = DiscographyAlbum { songs: vec![] }; + let mut current_album = DiscographyAlbum { songs: vec![], id: "".to_string() }; for song in discog.items { // push songs until we find a different album if current_album.songs.is_empty() { @@ -310,7 +386,8 @@ impl Client { continue; } albums.push(current_album); - current_album = DiscographyAlbum { songs: vec![song] }; + let album_id = song.album_id.clone(); + current_album = DiscographyAlbum { songs: vec![song], id: album_id }; } albums.push(current_album); @@ -334,7 +411,7 @@ impl Client { // now we flatten the albums back into a list of songs let mut songs: Vec = vec![]; - for (i, album) in albums.iter().enumerate() { + for album in albums.iter() { if album.songs.is_empty() { continue; } @@ -343,7 +420,7 @@ impl Client { let mut album_song = album.songs[0].clone(); // let name be Artist - Album - Year album_song.name = format!("{} ({})", album.songs[0].album, album.songs[0].production_year); - album_song.id = format!("_album_{}", i); + album_song.id = format!("_album_{}", album.id); album_song.album_artists = album.songs[0].album_artists.clone(); album_song.album_id = "".to_string(); album_song.album_artists = vec![]; @@ -440,7 +517,7 @@ impl Client { let albums = match response { Ok(json) => { - let albums: SearchAlbums = json.json().await.unwrap_or_else(|_| SearchAlbums { + let albums: Albums = json.json().await.unwrap_or_else(|_| Albums { items: vec![], }); albums.items @@ -780,8 +857,9 @@ impl Client { /// Sends an update to favorite of a track. POST is true, DELETE is false /// - pub async fn set_favorite(&self, song_id: &String, favorite: bool) -> Result<(), reqwest::Error> { - let url = format!("{}/Users/{}/FavoriteItems/{}", self.base_url, self.user_id, song_id); + pub async fn set_favorite(&self, item_id: &String, favorite: bool) -> Result<(), reqwest::Error> { + let id = item_id.replace("_album_", ""); + let url = format!("{}/Users/{}/FavoriteItems/{}", self.base_url, self.user_id, id); let response = if favorite { self.http_client .post(url) @@ -1176,6 +1254,7 @@ pub struct Discography { #[derive(Debug, Serialize, Deserialize)] pub struct DiscographyAlbum { + id: String, songs: Vec, } @@ -1374,19 +1453,25 @@ pub struct ProgressReport { } #[derive(Debug, Deserialize)] -pub struct SearchAlbums { +pub struct Albums { #[serde(rename = "Items", default)] pub items: Vec, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct Album { #[serde(rename = "Name", default)] pub name: String, #[serde(rename = "Id",default )] pub id: String, - #[serde(rename = "AlbumArtists")] + #[serde(rename = "AlbumArtists", default)] pub album_artists: Vec, + #[serde(rename = "UserData", default)] + pub user_data: UserData, + #[serde(rename = "DateCreated", default)] + pub date_created: String, + #[serde(rename = "ParentId", default)] + pub parent_id: String, } impl Searchable for Album { diff --git a/src/helpers.rs b/src/helpers.rs index e5ef2af..3063195 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -3,7 +3,7 @@ use dirs::cache_dir; use ratatui::widgets::{ListState, ScrollbarState, TableState}; use crate::{ - client::{Artist, Playlist}, keyboard::{ActiveSection, ActiveTab, SearchSection}, popup::PopupMenu, tui::{Filter, MpvPlaybackState, Repeat, Sort} + client::{Album, Artist, Playlist}, keyboard::{ActiveSection, ActiveTab, SearchSection}, popup::PopupMenu, tui::{Filter, MpvPlaybackState, Repeat, Sort} }; pub fn find_all_subsequences(needle: &str, haystack: &str) -> Vec<(usize, usize)> { @@ -42,12 +42,17 @@ impl crate::tui::State { search_section: SearchSection::default(), active_tab: ActiveTab::default(), current_artist: Artist::default(), + current_album: Album::default(), current_playlist: Playlist::default(), selected_artist: ListState::default(), selected_track: TableState::default(), + selected_album: ListState::default(), + selected_album_track: TableState::default(), selected_playlist_track: TableState::default(), selected_playlist: ListState::default(), tracks_scroll_state: ScrollbarState::default(), + albums_scroll_state: ScrollbarState::default(), + album_tracks_scroll_state: ScrollbarState::default(), artists_scroll_state: ScrollbarState::default(), playlists_scroll_state: ScrollbarState::default(), playlist_tracks_scroll_state: ScrollbarState::default(), @@ -62,6 +67,8 @@ impl crate::tui::State { selected_search_track: ListState::default(), artists_search_term: String::from(""), + albums_search_term: String::from(""), + album_tracks_search_term: String::from(""), tracks_search_term: String::from(""), playlist_tracks_search_term: String::from(""), playlists_search_term: String::from(""), @@ -76,6 +83,8 @@ impl crate::tui::State { artist_filter: Filter::default(), artist_sort: Sort::default(), + album_filter: Filter::default(), + album_sort: Sort::default(), playlist_filter: Filter::default(), playlist_sort: Sort::default(), diff --git a/src/keyboard.rs b/src/keyboard.rs index e327c4f..df28347 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -5,7 +5,7 @@ Keyboard related functions - Also used for searching -------------------------- */ -use crate::{client::{Artist, Playlist}, helpers, tui::{App, Repeat, State}}; +use crate::{client::{Album, Artist, Playlist}, helpers, tui::{App, Repeat, State}}; use std::io; use std::time::Duration; @@ -19,6 +19,8 @@ pub trait Searchable { pub enum Selectable { Artist, + Album, + AlbumTrack, Track, Playlist, PlaylistTrack, @@ -104,12 +106,16 @@ impl App { pub fn reposition_cursor(&mut self, id: &str, selectable: Selectable) { let search_term = match selectable { Selectable::Artist => &self.state.artists_search_term, + Selectable::Album => &self.state.albums_search_term, + Selectable::AlbumTrack => &self.state.album_tracks_search_term, Selectable::Track => &self.state.tracks_search_term, Selectable::Playlist => &self.state.playlists_search_term, Selectable::PlaylistTrack => &self.state.playlist_tracks_search_term, }; let ids = match selectable { Selectable::Artist => self.artists.iter().map(|a| a.id.clone()).collect::>(), + Selectable::Album => self.albums.iter().map(|a| a.id.clone()).collect::>(), + Selectable::AlbumTrack => self.album_tracks.iter().map(|t| t.id.clone()).collect::>(), Selectable::Track => self.tracks.iter().map(|t| t.id.clone()).collect::>(), Selectable::Playlist => self.playlists.iter().map(|p| p.id.clone()).collect::>(), Selectable::PlaylistTrack => self.tracks_playlist.iter().map(|t| t.id.clone()).collect::>(), @@ -119,6 +125,8 @@ impl App { if !ids.is_empty() { match selectable { Selectable::Artist => self.artist_select_by_index(0), + Selectable::Album => self.album_select_by_index(0), + Selectable::AlbumTrack => self.album_track_select_by_index(0), Selectable::Track => self.track_select_by_index(0), Selectable::Playlist => self.playlist_select_by_index(0), Selectable::PlaylistTrack => self.playlist_track_select_by_index(0), @@ -130,6 +138,8 @@ impl App { if !search_term.is_empty() { let items = match selectable { Selectable::Artist => search_results(&self.artists, search_term, false), + Selectable::Album => search_results(&self.albums, search_term, false), + Selectable::AlbumTrack => search_results(&self.album_tracks, search_term, false), Selectable::Track => search_results(&self.tracks, search_term, false), Selectable::Playlist => search_results(&self.playlists, search_term, false), Selectable::PlaylistTrack => search_results(&self.tracks_playlist, search_term, false), @@ -137,6 +147,8 @@ impl App { if let Some(index) = items.iter().position(|i| i == id) { match selectable { Selectable::Artist => self.artist_select_by_index(index), + Selectable::Album => self.album_select_by_index(index), + Selectable::AlbumTrack => self.album_track_select_by_index(index), Selectable::Track => self.track_select_by_index(index), Selectable::Playlist => self.playlist_select_by_index(index), Selectable::PlaylistTrack => self.playlist_track_select_by_index(index), @@ -147,6 +159,8 @@ impl App { if let Some(index) = ids.iter().position(|i| i == id) { match selectable { Selectable::Artist => self.artist_select_by_index(index), + Selectable::Album => self.album_select_by_index(index), + Selectable::AlbumTrack => self.album_track_select_by_index(index), Selectable::Track => self.track_select_by_index(index), Selectable::Playlist => self.playlist_select_by_index(index), Selectable::PlaylistTrack => self.playlist_track_select_by_index(index), @@ -157,12 +171,16 @@ impl App { pub fn get_id_of_selected(&self, items: &Vec, selectable: Selectable) -> String { let search_term = match selectable { Selectable::Artist => &self.state.artists_search_term, + Selectable::Album => &self.state.albums_search_term, + Selectable::AlbumTrack => &self.state.album_tracks_search_term, Selectable::Track => &self.state.tracks_search_term, Selectable::Playlist => &self.state.playlists_search_term, Selectable::PlaylistTrack => &self.state.playlist_tracks_search_term, }; let selected = match selectable { Selectable::Artist => self.state.selected_artist.selected(), + Selectable::Album => self.state.selected_album.selected(), + Selectable::AlbumTrack => self.state.selected_album_track.selected(), Selectable::Track => self.state.selected_track.selected(), Selectable::Playlist => self.state.selected_playlist.selected(), Selectable::PlaylistTrack => self.state.selected_playlist_track.selected(), @@ -175,7 +193,7 @@ impl App { let selected = selected.unwrap_or(0); return items[selected].clone(); } - if items.is_empty() { + if items.is_empty() || items.len() <= selected.unwrap_or(0) { return String::from(""); } let selected = selected.unwrap_or(0); @@ -201,6 +219,26 @@ impl App { self.state.selected_track.select(Some(index)); self.state.tracks_scroll_state = self.state.tracks_scroll_state.content_length(items.len()).position(index); } + + pub fn album_select_by_index(&mut self, index: usize) { + let items = search_results(&self.albums, &self.state.albums_search_term, true); + if items.is_empty() { + return; + } + let index = std::cmp::min(index, items.len() - 1); + self.state.selected_album.select(Some(index)); + self.state.albums_scroll_state = self.state.albums_scroll_state.content_length(items.len()).position(index); + } + + pub fn album_track_select_by_index(&mut self, index: usize) { + let items = search_results(&self.album_tracks, &self.state.album_tracks_search_term, true); + if items.is_empty() { + return; + } + let index = std::cmp::min(index, items.len() - 1); + self.state.selected_album_track.select(Some(index)); + self.state.album_tracks_scroll_state = self.state.album_tracks_scroll_state.content_length(items.len()).position(index); + } pub fn playlist_track_select_by_index(&mut self, index: usize) { let items = search_results(&self.tracks_playlist, &self.state.playlist_tracks_search_term, true); @@ -242,6 +280,8 @@ impl App { self.locally_searching = false; let artist_id = self.get_id_of_selected(&self.artists, Selectable::Artist); let track_id = self.get_id_of_selected(&self.tracks, Selectable::Track); + let album_id = self.get_id_of_selected(&self.albums, Selectable::Album); + let album_track_id = self.get_id_of_selected(&self.album_tracks, Selectable::AlbumTrack); let playlist_id = self.get_id_of_selected(&self.playlists, Selectable::Playlist); let playlist_track_id = self.get_id_of_selected(&self.tracks_playlist, Selectable::PlaylistTrack); @@ -259,6 +299,19 @@ impl App { _ => {} } } + ActiveTab::Albums => { + match self.state.active_section { + ActiveSection::Artists => { + self.state.albums_search_term = String::from(""); + self.reposition_cursor(&album_id, Selectable::Album); + } + ActiveSection::Tracks => { + self.state.album_tracks_search_term = String::from(""); + self.reposition_cursor(&album_track_id, Selectable::AlbumTrack); + } + _ => {} + } + } ActiveTab::Playlists => { match self.state.active_section { ActiveSection::Artists => { @@ -285,6 +338,12 @@ impl App { self.state.tracks_search_term = String::from(""); } } + ActiveTab::Albums => { + self.locally_searching = false; + if self.state.active_section == ActiveSection::Artists { + self.state.album_tracks_search_term = String::from(""); + } + } ActiveTab::Playlists => { self.locally_searching = false; if self.state.active_section == ActiveSection::Artists { @@ -312,6 +371,21 @@ impl App { _ => {} } } + ActiveTab::Albums => { + match self.state.active_section { + ActiveSection::Artists => { + let selected_id = self.get_id_of_selected(&self.albums, Selectable::Album); + self.state.albums_search_term.pop(); + self.reposition_cursor(&selected_id, Selectable::Album); + } + ActiveSection::Tracks => { + let selected_id = self.get_id_of_selected(&self.album_tracks, Selectable::AlbumTrack); + self.state.album_tracks_search_term.pop(); + self.reposition_cursor(&selected_id, Selectable::AlbumTrack); + } + _ => {} + } + } ActiveTab::Playlists => { match self.state.active_section { ActiveSection::Artists => { @@ -347,6 +421,21 @@ impl App { _ => {} } } + ActiveTab::Albums => { + match self.state.active_section { + ActiveSection::Artists => { + let selected_id = self.get_id_of_selected(&self.albums, Selectable::Album); + self.state.albums_search_term.clear(); + self.reposition_cursor(&selected_id, Selectable::Album); + } + ActiveSection::Tracks => { + let selected_id = self.get_id_of_selected(&self.album_tracks, Selectable::AlbumTrack); + self.state.album_tracks_search_term.clear(); + self.reposition_cursor(&selected_id, Selectable::AlbumTrack); + } + _ => {} + } + } ActiveTab::Playlists => { match self.state.active_section { ActiveSection::Artists => { @@ -380,6 +469,19 @@ impl App { _ => {} } } + ActiveTab::Albums => { + match self.state.active_section { + ActiveSection::Artists => { + self.state.albums_search_term.push(c); + self.album_select_by_index(0); + } + ActiveSection::Tracks => { + self.state.album_tracks_search_term.push(c); + self.album_track_select_by_index(0); + } + _ => {} + } + } ActiveTab::Playlists => { match self.state.active_section { ActiveSection::Artists => { @@ -482,7 +584,11 @@ impl App { self.state.selected_track.select_first(); self.state.selected_playlist.select_first(); self.state.selected_playlist_track.select_first(); + self.state.selected_album.select_first(); + self.state.selected_album_track.select_first(); + self.state.artists_scroll_state = self.state.artists_scroll_state.content_length(self.artists.len()); + self.state.albums_scroll_state = self.state.albums_scroll_state.content_length(self.albums.len()); self.state.playlists_scroll_state = self.state.playlists_scroll_state.content_length(self.playlists.len()); self.tracks.clear(); @@ -552,6 +658,25 @@ impl App { } self.artist_select_by_index(selected + 1); } + if self.state.active_tab == ActiveTab::Albums { + if !self.state.albums_search_term.is_empty() { + let items = search_results(&self.albums, &self.state.albums_search_term, false); + let selected = self.state.selected_album.selected().unwrap_or(items.len() - 1); + if selected == items.len() - 1 { + self.album_select_by_index(selected); + return; + } + self.album_select_by_index(selected + 1); + return; + } + + let selected = self.state.selected_album.selected().unwrap_or(self.albums.len() - 1); + if selected == self.albums.len() - 1 { + self.album_select_by_index(selected); + return; + } + self.album_select_by_index(selected + 1); + } if self.state.active_tab == ActiveTab::Playlists { if !self.state.playlists_search_term.is_empty() { let items = search_results(&self.playlists, &self.state.playlists_search_term, false); @@ -598,6 +723,31 @@ impl App { } self.track_select_by_index(selected + 1); } + if self.state.active_tab == ActiveTab::Albums { + if !self.state.album_tracks_search_term.is_empty() { + let items = search_results(&self.album_tracks, &self.state.album_tracks_search_term, false); + let selected = self + .state.selected_album_track + .selected() + .unwrap_or(items.len() - 1); + if selected == items.len() - 1 { + self.album_track_select_by_index(selected); + return; + } + self.album_track_select_by_index(selected + 1); + return; + } + + let selected = self + .state.selected_album_track + .selected() + .unwrap_or(self.album_tracks.len() - 1); + if selected == self.album_tracks.len() - 1 { + self.album_track_select_by_index(selected); + return; + } + self.album_track_select_by_index(selected + 1); + } if self.state.active_tab == ActiveTab::Playlists { if !self.state.playlist_tracks_search_term.is_empty() { let items = search_results(&self.tracks_playlist, &self.state.playlist_tracks_search_term, false); @@ -676,6 +826,27 @@ impl App { } self.artist_select_by_index(selected - 1); } + if self.state.active_tab == ActiveTab::Albums { + if !self.state.albums_search_term.is_empty() { + let selected = self + .state.selected_album + .selected() + .unwrap_or(0); + if selected == 0 { + self.album_select_by_index(selected); + return; + } + self.album_select_by_index(selected - 1); + return; + } + + let selected = self.state.selected_album.selected().unwrap_or(0); + if selected == 0 { + self.album_select_by_index(selected); + return; + } + self.album_select_by_index(selected - 1); + } if self.state.active_tab == ActiveTab::Playlists { if !self.state.playlists_search_term.is_empty() { let selected = self @@ -714,6 +885,19 @@ impl App { let selected = self.state.selected_track.selected().unwrap_or(0); self.track_select_by_index(std::cmp::max(selected as i32 - 1, 0) as usize); } + ActiveTab::Albums => { + if !self.state.album_tracks_search_term.is_empty() { + let selected = self + .state.selected_album_track + .selected() + .unwrap_or(0); + self.album_track_select_by_index(std::cmp::max(selected as i32 - 1, 0) as usize); + return; + } + + let selected = self.state.selected_album_track.selected().unwrap_or(0); + self.album_track_select_by_index(std::cmp::max(selected as i32 - 1, 0) as usize); + } ActiveTab::Playlists => { if !self.state.playlist_tracks_search_term.is_empty() { let selected = self @@ -753,6 +937,9 @@ impl App { ActiveTab::Library => { self.artist_select_by_index(0); } + ActiveTab::Albums => { + self.album_select_by_index(0); + } ActiveTab::Playlists => { self.playlist_select_by_index(0); } @@ -766,6 +953,11 @@ impl App { self.track_select_by_index(0); } } + ActiveTab::Albums => { + if !self.album_tracks.is_empty() { + self.album_track_select_by_index(0); + } + } ActiveTab::Playlists => { if !self.tracks_playlist.is_empty() { self.playlist_track_select_by_index(0); @@ -794,6 +986,11 @@ impl App { self.artist_select_by_index(self.artists.len() - 1); } } + ActiveTab::Albums => { + if !self.albums.is_empty() { + self.album_select_by_index(self.albums.len() - 1); + } + } ActiveTab::Playlists => { if !self.playlists.is_empty() { self.playlist_select_by_index(self.playlists.len() - 1); @@ -809,6 +1006,11 @@ impl App { self.track_select_by_index(self.tracks.len() - 1); } } + ActiveTab::Albums => { + if !self.album_tracks.is_empty() { + self.album_track_select_by_index(self.album_tracks.len() - 1); + } + } ActiveTab::Playlists => { if !self.tracks_playlist.is_empty() { self.playlist_track_select_by_index(self.tracks_playlist.len() - 1); @@ -875,6 +1077,27 @@ impl App { _ => {} } } + ActiveTab::Albums => { + if matches!(self.state.active_section, ActiveSection::Artists) { + if self.albums.is_empty() { + return; + } + let ids = search_results(&self.albums, &self.state.albums_search_term, false); + let mut albums = self.albums.iter().filter(|album| ids.contains(&album.id)).collect::>(); + if albums.is_empty() { + albums = self.albums.iter().collect::>(); + } + if let Some(selected) = self.state.selected_album.selected() { + let current_album = albums[selected].name[0..1].to_lowercase(); + let next_album = albums.iter().skip(selected).find(|a| a.name[0..1].to_lowercase() != current_album); + + if let Some(next_album) = next_album { + let index = albums.iter().position(|a| a.id == next_album.id).unwrap_or(0); + self.album_select_by_index(index); + } + } + } + } ActiveTab::Playlists => { if matches!(self.state.active_section, ActiveSection::Artists) { if self.playlists.is_empty() { @@ -971,10 +1194,10 @@ impl App { match self.state.active_section { ActiveSection::Artists => { - self.state.tracks_search_term = String::from(""); - self.state.selected_track.select(Some(0)); - if self.state.active_tab == ActiveTab::Library { + self.state.tracks_search_term = String::from(""); + self.state.selected_track.select(Some(0)); + let search_results = search_results(&self.artists, &self.state.artists_search_term, true); let artists = search_results .iter() @@ -986,6 +1209,25 @@ impl App { } self.discography(&artists[selected].id.clone()).await; } + + if self.state.active_tab == ActiveTab::Albums { + + self.state.album_tracks_search_term = String::from(""); + self.state.selected_album_track.select(Some(0)); + + let search_results = search_results(&self.albums, &self.state.albums_search_term, true); + let albums = search_results + .iter() + .map(|id| self.albums.iter().find(|album| album.id == *id).unwrap()) + .collect::>(); + + let selected = self.state.selected_album.selected().unwrap_or(0); + if albums.is_empty() { + return; + } + self.album_tracks(&albums[selected].id.clone()).await; + } + if self.state.active_tab == ActiveTab::Playlists { self.state.playlist_tracks_search_term = String::from(""); @@ -1017,6 +1259,14 @@ impl App { .collect(); items } + ActiveTab::Albums => { + let ids = search_results(&self.album_tracks, &self.state.album_tracks_search_term, true); + let items = ids.iter() + .map(|id| self.album_tracks.iter().find(|t| t.id == *id).unwrap()) + .cloned() + .collect(); + items + } ActiveTab::Playlists => { let ids = search_results(&self.tracks_playlist, &self.state.playlist_tracks_search_term, false); let items: Vec = self.tracks_playlist.iter() @@ -1030,6 +1280,7 @@ impl App { let selected = match self.state.active_tab { ActiveTab::Library => self.state.selected_track.selected().unwrap_or(0), + ActiveTab::Albums => self.state.selected_album_track.selected().unwrap_or(0), ActiveTab::Playlists => self.state.selected_playlist_track.selected().unwrap_or(0), _ => 0 }; @@ -1079,6 +1330,14 @@ impl App { .collect(); items } + ActiveTab::Albums => { + let ids = search_results(&self.album_tracks, &self.state.album_tracks_search_term, true); + let items = ids.iter() + .map(|id| self.album_tracks.iter().find(|t| t.id == *id).unwrap()) + .cloned() + .collect(); + items + } ActiveTab::Playlists => { let ids = search_results(&self.tracks_playlist, &self.state.playlist_tracks_search_term, false); let items: Vec = self.tracks_playlist.iter() @@ -1117,6 +1376,18 @@ impl App { self.reposition_cursor(&id, Selectable::Artist); } } + ActiveTab::Albums => { + let id = self.get_id_of_selected(&self.albums, Selectable::Album); + if let Some(album) = self.original_albums.iter_mut().find(|a| a.id == id) { + let _ = client.set_favorite(&album.id, !album.user_data.is_favorite).await; + album.user_data.is_favorite = !album.user_data.is_favorite; + self.reorder_lists(); + self.reposition_cursor(&id, Selectable::Album); + } + if let Some(album) = self.tracks.iter_mut().find(|a| a.id == format!("_album_{}", id)) { + album.user_data.is_favorite = !album.user_data.is_favorite; + } + } ActiveTab::Playlists => { let id = self.get_id_of_selected(&self.playlists, Selectable::Playlist); if let Some(playlist) = self.original_playlists.iter_mut().find(|a| a.id == id) { @@ -1141,6 +1412,26 @@ impl App { if let Some(tr) = self.state.queue.iter_mut().find(|t| t.id == track.id) { tr.is_favorite = !tr.is_favorite; } + if track.id.starts_with("_album_") { + let id = track.id.replace("_album_", ""); + if let Some(album) = self.albums.iter_mut().find(|a| a.id == id) { + album.user_data.is_favorite = !album.user_data.is_favorite; + } + if let Some(album) = self.original_albums.iter_mut().find(|a| a.id == id) { + album.user_data.is_favorite = !album.user_data.is_favorite; + } + self.reorder_lists(); + } + } + } + ActiveTab::Albums => { + let id = self.get_id_of_selected(&self.album_tracks, Selectable::AlbumTrack); + if let Some(track) = self.album_tracks.iter_mut().find(|t| t.id == id) { + let _ = client.set_favorite(&track.id, !track.user_data.is_favorite).await; + track.user_data.is_favorite = !track.user_data.is_favorite; + if let Some(tr) = self.state.queue.iter_mut().find(|t| t.id == track.id) { + tr.is_favorite = !tr.is_favorite; + } } } ActiveTab::Playlists => { @@ -1252,6 +1543,8 @@ impl App { return; } let artist_id = self.get_id_of_selected(&self.artists, Selectable::Artist); + let album_id = self.get_id_of_selected(&self.albums, Selectable::Album); + let album_track_id = self.get_id_of_selected(&self.album_tracks, Selectable::AlbumTrack); let track_id = self.get_id_of_selected(&self.tracks, Selectable::Track); let playlist_id = self.get_id_of_selected(&self.playlists, Selectable::Playlist); let playlist_track_id = self.get_id_of_selected(&self.tracks_playlist, Selectable::PlaylistTrack); @@ -1270,6 +1563,19 @@ impl App { _ => {} } } + ActiveTab::Albums => { + match self.state.active_section { + ActiveSection::Artists => { + self.state.albums_search_term = String::from(""); + self.reposition_cursor(&album_id, Selectable::Album); + } + ActiveSection::Tracks => { + self.state.album_tracks_search_term = String::from(""); + self.reposition_cursor(&album_track_id, Selectable::AlbumTrack); + } + _ => {} + } + } ActiveTab::Playlists => { match self.state.active_section { ActiveSection::Artists => { @@ -1300,12 +1606,18 @@ impl App { } } KeyCode::F(2) | KeyCode::Char('2') => { - self.state.active_tab = ActiveTab::Playlists; + self.state.active_tab = ActiveTab::Albums; if self.tracks_playlist.is_empty() { self.state.active_section = ActiveSection::Artists; } } KeyCode::F(3) | KeyCode::Char('3') => { + self.state.active_tab = ActiveTab::Playlists; + if self.tracks_playlist.is_empty() { + self.state.active_section = ActiveSection::Artists; + } + } + KeyCode::F(4) | KeyCode::Char('4') => { self.state.active_tab = ActiveTab::Search; self.searching = true; } @@ -1326,12 +1638,18 @@ impl App { self.state.active_tab = ActiveTab::Library; } KeyCode::F(2) => { - self.state.active_tab = ActiveTab::Playlists; + self.state.active_tab = ActiveTab::Albums; if self.tracks_playlist.is_empty() { self.state.active_section = ActiveSection::Artists; } } KeyCode::F(3) => { + self.state.active_tab = ActiveTab::Playlists; + if self.tracks_playlist.is_empty() { + self.state.active_section = ActiveSection::Artists; + } + } + KeyCode::F(4) => { self.searching = true; } KeyCode::Backspace => { @@ -1708,6 +2026,7 @@ impl App { pub enum ActiveTab { #[default] Library, + Albums, Playlists, Search, } diff --git a/src/library.rs b/src/library.rs index 5e361f0..2bbc19d 100644 --- a/src/library.rs +++ b/src/library.rs @@ -11,7 +11,7 @@ Main Library tab right[1]: Queue list -------------------------- */ -use crate::client::{Artist, DiscographySong}; +use crate::client::{Album, Artist, DiscographySong}; use crate::helpers; use crate::tui::{App, Repeat}; use crate::keyboard::{*}; @@ -43,6 +43,69 @@ impl App { ]) .split(app_container); + // create a wrapper, to get the width. After that create the inner 'left' and split it + let center = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![Constraint::Percentage(100), Constraint::Length(8)]) + .split(outer_layout[1]); + + let show_lyrics = self.lyrics.as_ref().is_some_and(|(_, lyrics, _)| !lyrics.is_empty()); + let right = Layout::default() + .direction(Direction::Vertical) + .constraints(if show_lyrics && !self.lyrics.as_ref().map_or(true, |(_, lyrics, _)| lyrics.len() == 1) { + vec![Constraint::Percentage(68), Constraint::Percentage(32)] + } else { + vec![Constraint::Min(3), Constraint::Percentage(100)] + }) + .split(outer_layout[2]); + + // update mpris metadata + if self.active_song_id != self.mpris_active_song_id && self.state.current_playback_state.current_index != self.state.current_playback_state.last_index && self.state.current_playback_state.duration > 0.0 { + self.mpris_active_song_id = self.active_song_id.clone(); + let cover_url = format!("file://{}", self.cover_art_path); + let metadata = match self + .state.queue + .get(self.state.current_playback_state.current_index as usize) + { + Some(song) => { + let metadata = MediaMetadata { + title: Some(song.name.as_str()), + artist: Some(song.artist.as_str()), + album: Some(song.album.as_str()), + cover_url: Some(cover_url.as_str()), + duration: Some(Duration::from_secs((self.state.current_playback_state.duration) as u64)), + }; + metadata + } + None => MediaMetadata { + title: None, + artist: None, + album: None, + cover_url: None, + duration: None, + }, + }; + + if let Some(ref mut controls) = self.controls { + let _ = controls.set_metadata(metadata); + } + } + if self.paused != self.mpris_paused && self.state.current_playback_state.duration > 0.0 { + self.mpris_paused = self.paused; + if let Some(ref mut controls) = self.controls { + let progress = self.state.current_playback_state.duration * self.state.current_playback_state.percentage / 100.0; + let _ = controls.set_playback(if self.paused { souvlaki::MediaPlayback::Paused { progress: Some(MediaPosition(Duration::from_secs_f64(progress))) } } else { souvlaki::MediaPlayback::Playing { progress: Some(MediaPosition(Duration::from_secs_f64(progress))) } }); + } + } + + self.render_library_left(frame, outer_layout); + self.render_library_center(frame, ¢er); + self.render_player(frame, ¢er); + self.render_library_right(frame, right); + self.create_popup(frame); + } + + fn render_library_left(&mut self, frame: &mut Frame, outer_layout: std::rc::Rc<[Rect]>) { // LEFT sidebar construct. large_art flag determines the split let left = if self.state.large_art { // this is a temporary hack to get the image area size. @@ -120,22 +183,19 @@ impl App { .split(outer_layout[0]) }; - // create a wrapper, to get the width. After that create the inner 'left' and split it - let center = Layout::default() - .direction(Direction::Vertical) - .constraints(vec![Constraint::Percentage(100), Constraint::Length(8)]) - .split(outer_layout[1]); - - let show_lyrics = self.lyrics.as_ref().is_some_and(|(_, lyrics, _)| !lyrics.is_empty()); - let right = Layout::default() - .direction(Direction::Vertical) - .constraints(if show_lyrics && !self.lyrics.as_ref().map_or(true, |(_, lyrics, _)| lyrics.len() == 1) { - vec![Constraint::Percentage(68), Constraint::Percentage(32)] - } else { - vec![Constraint::Min(3), Constraint::Percentage(100)] - }) - .split(outer_layout[2]); - + match self.state.active_tab { + ActiveTab::Library => { + self.render_library_artists(frame, left); + } + ActiveTab::Albums => { + self.render_library_albums(frame, left); + } + _ => {} + } + } + + fn render_library_artists(&mut self, frame: &mut Frame, left: std::rc::Rc<[Rect]>) { + let artist_block = match self.state.active_section { ActiveSection::Artists => Block::new() .borders(Borders::ALL) @@ -163,7 +223,8 @@ impl App { artist_highlight_style = artist_highlight_style.add_modifier(Modifier::ITALIC); } } - + + let artists = search_results(&self.artists, &self.state.artists_search_term, true) .iter() .map(|id| self.artists.iter().find(|artist| artist.id == *id).unwrap()) @@ -257,231 +318,125 @@ impl App { }), &mut self.state.artists_scroll_state, ); - - let track_block = match self.state.active_section { - ActiveSection::Tracks => Block::new() + + if self.locally_searching { + if self.state.active_section == ActiveSection::Artists { + frame.render_widget( + Block::default() + .borders(Borders::ALL) + .title(format!("Searching: {}", self.state.artists_search_term)) + .border_style(self.primary_color), + left[0], + ); + } + } + } + + fn render_library_albums(&mut self, frame: &mut Frame, left: std::rc::Rc<[Rect]>) { + let album_block = match self.state.active_section { + ActiveSection::Artists => Block::new() .borders(Borders::ALL) .border_style(self.primary_color), _ => Block::new() .borders(Borders::ALL) - .border_style(style::Color::White), + .border_style(Color::White), }; - let selected_track = self.get_id_of_selected(&self.tracks, Selectable::Track); - let current_track = self.state.queue.get(self.state.current_playback_state.current_index as usize); - - let mut track_highlight_style = match self.state.active_section { - ActiveSection::Tracks => Style::default() + let selected_album = self.get_id_of_selected(&self.albums, Selectable::Album); + + let mut album_highlight_style = match self.state.active_section { + ActiveSection::Artists => Style::default() .add_modifier(Modifier::BOLD) - .fg(Color::Indexed(232)) - .bg(Color::White), + .bg(Color::White) + .fg(Color::Indexed(232)), _ => Style::default() .add_modifier(Modifier::BOLD) - .fg(Color::White) - .bg(Color::DarkGray), + .bg(Color::DarkGray) + .fg(Color::White) }; - if current_track.is_some() && current_track.unwrap().id == selected_track { - track_highlight_style = track_highlight_style.add_modifier(Modifier::ITALIC); + if let Some(song) = self.state.queue.get(self.state.current_playback_state.current_index as usize) { + if song.parent_id == selected_album { + album_highlight_style = album_highlight_style.add_modifier(Modifier::ITALIC); + } } - let tracks = search_results(&self.tracks, &self.state.tracks_search_term, true) + let albums = search_results(&self.albums, &self.state.albums_search_term, true) .iter() - .map(|id| self.tracks.iter().find(|t| t.id == *id).unwrap()) - .collect::>(); + .map(|id| self.albums.iter().find(|album| album.id == *id).unwrap()) + .collect::>(); - let items = tracks + let items = albums .iter() - .map(|track| { - let title = track.name.to_string(); - - if track.id.starts_with("_album_") { - let total_time = track.run_time_ticks / 10_000_000; - let seconds = total_time % 60; - let minutes = (total_time / 60) % 60; - let hours = total_time / 60 / 60; - let hours_optional_text = match hours { - 0 => String::from(""), - _ => format!("{}:", hours), - }; - let duration = format!("{}{:02}:{:02}", hours_optional_text, minutes, seconds); - // this is the dummy that symbolizes the name of the album - return Row::new(vec![ - Cell::from(">>"), - Cell::from(title), - Cell::from(""), - Cell::from(""), - Cell::from(""), - Cell::from(""), - Cell::from(""), - Cell::from(duration), - ]).style(Style::default().fg(Color::White)).bold(); - } - - // track.run_time_ticks is in microseconds - let seconds = (track.run_time_ticks / 10_000_000) % 60; - let minutes = (track.run_time_ticks / 10_000_000 / 60) % 60; - let hours = (track.run_time_ticks / 10_000_000 / 60) / 60; - let hours_optional_text = match hours { - 0 => String::from(""), - _ => format!("{}:", hours), - }; + .map(|album| { + let color = if let Some(song) = self.state.queue.get(self.state.current_playback_state.current_index as usize) { + if song.parent_id == album.id { + self.primary_color + } else { Color::White } + } else { Color::White }; + // underline the matching search subsequence ranges + let mut item = Text::default(); + let mut last_end = 0; let all_subsequences = helpers::find_all_subsequences( - &self.state.tracks_search_term.to_lowercase(), - &track.name.to_lowercase(), + &&self.state.albums_search_term.to_lowercase(), + &album.name.to_lowercase(), ); - - let mut title = vec![]; - let mut last_end = 0; - let color = if track.id == self.active_song_id { - self.primary_color - } else { - Color::White - }; - for (start, end) in &all_subsequences { - if &last_end < start { - title.push(Span::styled( - &track.name[last_end..*start], + for (start, end) in all_subsequences { + if last_end < start { + item.push_span(Span::styled( + &album.name[last_end..start], Style::default().fg(color), )); } - title.push(Span::styled( - &track.name[*start..*end], + item.push_span(Span::styled( + &album.name[start..end], Style::default().fg(color).underlined() )); - last_end = *end; + last_end = end; } - if last_end < track.name.len() { - title.push(Span::styled( - &track.name[last_end..], + if last_end < album.name.len() { + item.push_span(Span::styled( + &album.name[last_end..], Style::default().fg(color), )); } - - Row::new(vec![ - Cell::from(format!("{}.", track.index_number)).style(if track.id == self.active_song_id { - Style::default().fg(color) - } else { - Style::default().fg(Color::DarkGray) - }), - Cell::from(if all_subsequences.is_empty() { - track.name.to_string().into() - } else { - Line::from(title) - }), - Cell::from(track.album.clone()), - Cell::from(if track.user_data.is_favorite { - "♥".to_string() - } else { - "".to_string() - }).style(Style::default().fg(self.primary_color)), - Cell::from(format!("{}", track.user_data.play_count)), - Cell::from(if track.parent_index_number > 0 { - format!("{}", track.parent_index_number) - } else { - String::from("1") - }), - Cell::from(if track.has_lyrics { - "✓".to_string() - } else { - "".to_string() - }), - Cell::from(format!("{}{:02}:{:02}", hours_optional_text, minutes, seconds)), - ]).style(if track.id == self.active_song_id { - Style::default().fg(self.primary_color).italic() - } else { - Style::default().fg(Color::White) - }) - }).collect::>(); - let track_instructions = Line::from(vec![ - " Help ".white(), - "".fg(self.primary_color).bold(), - " Quit ".white(), - " ".fg(self.primary_color).bold(), - ]); - - let widths = [ - Constraint::Length(items.len().to_string().len() as u16 + 1), - Constraint::Percentage(50), // title and track even width - Constraint::Percentage(50), - Constraint::Length(2), - Constraint::Length(5), - Constraint::Length(5), - Constraint::Length(6), - Constraint::Length(10), - ]; + if album.user_data.is_favorite { + item.push_span(Span::styled(" ♥", Style::default().fg(self.primary_color))); + } - if self.tracks.is_empty() { - let message_paragraph = Paragraph::new("jellyfin-tui") - .block( - track_block.title("Tracks").padding(Padding::new( - 0, 0, center[0].height / 2, 0, - )).title_bottom(track_instructions.alignment(Alignment::Center)) - ) - .wrap(Wrap { trim: false }) - .alignment(Alignment::Center); - frame.render_widget(message_paragraph, center[0]); - } else { - let items_len = items.len(); - let table = Table::new(items, widths) - .block(if self.state.tracks_search_term.is_empty() && !self.state.current_artist.name.is_empty() { - track_block - .title(format!("{}", self.state.current_artist.name)) - .title_top(Line::from(format!("({} tracks)", self.tracks.iter().filter(|t| !t.id.starts_with("_album_")).count())).right_aligned()) - .title_bottom(track_instructions.alignment(Alignment::Center)) - } else { - track_block - .title(format!("Matching: {}", self.state.tracks_search_term)) - .title_top(Line::from(format!("({} tracks)", items_len)).right_aligned()) - .title_bottom(track_instructions.alignment(Alignment::Center)) - }) - .row_highlight_style(track_highlight_style) - .highlight_symbol(">>") - .style( - Style::default().bg(Color::Reset) - ) - .header( - Row::new(vec!["#", "Title", "Album", "♥", "Plays", "Disc", "Lyrics", "Duration"]) - .style(Style::new().bold().white()) - .bottom_margin(0), - ); - frame.render_widget(Clear, center[0]); - frame.render_stateful_widget(table, center[0], &mut self.state.selected_track); - } + ListItem::new(item) + }) + .collect::>(); - // change section Title to 'Searching: TERM' if locally searching - if self.locally_searching { - let searching_instructions = Line::from(vec![ - " Confirm ".white(), - "".fg(self.primary_color).bold(), - " Clear and keep selection ".white(), - " ".fg(self.primary_color).bold(), - ]); - if self.state.active_section == ActiveSection::Tracks { - frame.render_widget( - Block::default() - .borders(Borders::ALL) - .title(format!("Searching: {}", self.state.tracks_search_term)) - .title_bottom(searching_instructions.alignment(Alignment::Center)) - .border_style(self.primary_color), - center[0], - ); - } - if self.state.active_section == ActiveSection::Artists { - frame.render_widget( - Block::default() - .borders(Borders::ALL) - .title(format!("Searching: {}", self.state.artists_search_term)) - .border_style(self.primary_color), - left[0], - ); - } - } + let items_len = items.len(); + + let list = List::new(items) + .block(if self.state.albums_search_term.is_empty() { + album_block + .title_alignment(Alignment::Right) + .title_top(Line::from("All").left_aligned()) + .title_top(format!("({} albums)", self.albums.len())).title_position(block::Position::Bottom) + } else { + album_block + .title_alignment(Alignment::Right) + .title_top(Line::from( + format!("Matching: {}", self.state.albums_search_term) + ).left_aligned()) + .title_top(format!("({} albums)", items_len)).title_position(block::Position::Bottom) + }) + .highlight_symbol(">>") + .highlight_style( + album_highlight_style + ) + .scroll_padding(10) + .repeat_highlight_symbol(true); + + frame.render_stateful_widget(list, left[0], &mut self.state.selected_album); frame.render_stateful_widget( Scrollbar::default() @@ -490,56 +445,24 @@ impl App { .end_symbol(Some("↓")) .track_style(Style::default().fg(Color::DarkGray)) .thumb_style(Style::default().fg(Color::Gray)), - center[0].inner(Margin { + left[0].inner(Margin { vertical: 1, horizontal: 1, }), - &mut self.state.tracks_scroll_state, + &mut self.state.albums_scroll_state, ); - - // update mpris metadata - if self.active_song_id != self.mpris_active_song_id && self.state.current_playback_state.current_index != self.state.current_playback_state.last_index && self.state.current_playback_state.duration > 0.0 { - self.mpris_active_song_id = self.active_song_id.clone(); - let cover_url = format!("file://{}", self.cover_art_path); - let metadata = match self - .state.queue - .get(self.state.current_playback_state.current_index as usize) - { - Some(song) => { - let metadata = MediaMetadata { - title: Some(song.name.as_str()), - artist: Some(song.artist.as_str()), - album: Some(song.album.as_str()), - cover_url: Some(cover_url.as_str()), - duration: Some(Duration::from_secs((self.state.current_playback_state.duration) as u64)), - }; - metadata - } - None => MediaMetadata { - title: None, - artist: None, - album: None, - cover_url: None, - duration: None, - }, - }; - if let Some(ref mut controls) = self.controls { - let _ = controls.set_metadata(metadata); - } - } - if self.paused != self.mpris_paused && self.state.current_playback_state.duration > 0.0 { - self.mpris_paused = self.paused; - if let Some(ref mut controls) = self.controls { - let progress = self.state.current_playback_state.duration * self.state.current_playback_state.percentage / 100.0; - let _ = controls.set_playback(if self.paused { souvlaki::MediaPlayback::Paused { progress: Some(MediaPosition(Duration::from_secs_f64(progress))) } } else { souvlaki::MediaPlayback::Playing { progress: Some(MediaPosition(Duration::from_secs_f64(progress))) } }); + if self.locally_searching { + if self.state.active_section == ActiveSection::Artists { + frame.render_widget( + Block::default() + .borders(Borders::ALL) + .title(format!("Searching: {}", self.state.albums_search_term)) + .border_style(self.primary_color), + left[0], + ); } } - - self.render_player(frame, center); - self.render_library_right(frame, right); - self.create_popup(frame); - } /// Individual widget rendering functions @@ -700,7 +623,295 @@ impl App { frame.render_stateful_widget(list, right[1], &mut self.state.selected_queue_item); } - pub fn render_player(&mut self, frame: &mut Frame, center: std::rc::Rc<[Rect]>) { + fn render_library_center(&mut self, frame: &mut Frame, center: &std::rc::Rc<[Rect]>) { + let track_block = match self.state.active_section { + ActiveSection::Tracks => Block::new() + .borders(Borders::ALL) + .border_style(self.primary_color), + _ => Block::new() + .borders(Borders::ALL) + .border_style(style::Color::White), + }; + + // let selected_track = self.get_id_of_selected(&self.tracks, Selectable::Track); + let selected_track = match self.state.active_tab { + ActiveTab::Library => self.get_id_of_selected(&self.tracks, Selectable::Track), + ActiveTab::Albums => self.get_id_of_selected(&self.album_tracks, Selectable::Track), + _ => return, + }; + let current_track = self.state.queue.get(self.state.current_playback_state.current_index as usize); + + let mut track_highlight_style = match self.state.active_section { + ActiveSection::Tracks => Style::default() + .add_modifier(Modifier::BOLD) + .fg(Color::Indexed(232)) + .bg(Color::White), + _ => Style::default() + .add_modifier(Modifier::BOLD) + .fg(Color::White) + .bg(Color::DarkGray), + }; + + if current_track.is_some() && current_track.unwrap().id == selected_track { + track_highlight_style = track_highlight_style.add_modifier(Modifier::ITALIC); + } + + // let tracks = search_results(&self.tracks, &self.state.tracks_search_term, true) + // .iter() + // .map(|id| self.tracks.iter().find(|t| t.id == *id).unwrap()) + // .collect::>(); + + let tracks = match self.state.active_tab { + ActiveTab::Library => search_results(&self.tracks, &self.state.tracks_search_term, true) + .iter() + .map(|id| self.tracks.iter().find(|t| t.id == *id).unwrap()) + .collect::>(), + ActiveTab::Albums => search_results(&self.album_tracks, &self.state.album_tracks_search_term, true) + .iter() + .map(|id| self.album_tracks.iter().find(|t| t.id == *id).unwrap()) + .collect::>(), + _ => return, + }; + + let items = tracks + .iter() + .map(|track| { + let title = track.name.to_string(); + + if track.id.starts_with("_album_") { + let total_time = track.run_time_ticks / 10_000_000; + let seconds = total_time % 60; + let minutes = (total_time / 60) % 60; + let hours = total_time / 60 / 60; + let hours_optional_text = match hours { + 0 => String::from(""), + _ => format!("{}:", hours), + }; + let duration = format!("{}{:02}:{:02}", hours_optional_text, minutes, seconds); + // this is the dummy that symbolizes the name of the album + return Row::new(vec![ + Cell::from(">>"), + Cell::from(title), + Cell::from(""), + Cell::from(if track.user_data.is_favorite { + "♥".to_string() + } else { + "".to_string() + }).style(Style::default().fg(self.primary_color)), + Cell::from(format!("{}", track.user_data.play_count)), + Cell::from(""), + Cell::from(""), + Cell::from(duration), + ]).style(Style::default().fg(Color::White)).bold(); + } + + // track.run_time_ticks is in microseconds + let seconds = (track.run_time_ticks / 10_000_000) % 60; + let minutes = (track.run_time_ticks / 10_000_000 / 60) % 60; + let hours = (track.run_time_ticks / 10_000_000 / 60) / 60; + let hours_optional_text = match hours { + 0 => String::from(""), + _ => format!("{}:", hours), + }; + + let all_subsequences = helpers::find_all_subsequences( + &self.state.tracks_search_term.to_lowercase(), + &track.name.to_lowercase(), + ); + + let mut title = vec![]; + let mut last_end = 0; + let color = if track.id == self.active_song_id { + self.primary_color + } else { + Color::White + }; + for (start, end) in &all_subsequences { + if &last_end < start { + title.push(Span::styled( + &track.name[last_end..*start], + Style::default().fg(color), + )); + } + + title.push(Span::styled( + &track.name[*start..*end], + Style::default().fg(color).underlined() + )); + + last_end = *end; + } + + if last_end < track.name.len() { + title.push(Span::styled( + &track.name[last_end..], + Style::default().fg(color), + )); + } + + Row::new(vec![ + Cell::from(format!("{}.", track.index_number)).style(if track.id == self.active_song_id { + Style::default().fg(color) + } else { + Style::default().fg(Color::DarkGray) + }), + Cell::from(if all_subsequences.is_empty() { + track.name.to_string().into() + } else { + Line::from(title) + }), + Cell::from(track.album.clone()), + Cell::from(if track.user_data.is_favorite { + "♥".to_string() + } else { + "".to_string() + }).style(Style::default().fg(self.primary_color)), + Cell::from(format!("{}", track.user_data.play_count)), + Cell::from(if track.parent_index_number > 0 { + format!("{}", track.parent_index_number) + } else { + String::from("1") + }), + Cell::from(if track.has_lyrics { + "✓".to_string() + } else { + "".to_string() + }), + Cell::from(format!("{}{:02}:{:02}", hours_optional_text, minutes, seconds)), + ]).style(if track.id == self.active_song_id { + Style::default().fg(self.primary_color).italic() + } else { + Style::default().fg(Color::White) + }) + }).collect::>(); + + let track_instructions = Line::from(vec![ + " Help ".white(), + "".fg(self.primary_color).bold(), + " Quit ".white(), + " ".fg(self.primary_color).bold(), + ]); + + let widths = [ + Constraint::Length(items.len().to_string().len() as u16 + 1), + Constraint::Percentage(50), // title and track even width + Constraint::Percentage(50), + Constraint::Length(2), + Constraint::Length(5), + Constraint::Length(5), + Constraint::Length(6), + Constraint::Length(10), + ]; + + let show_tracks = match self.state.active_tab { + ActiveTab::Library => !self.tracks.is_empty(), + ActiveTab::Albums => !self.album_tracks.is_empty(), + _ => false, + }; + + if !show_tracks { + let message_paragraph = Paragraph::new("jellyfin-tui") + .block( + track_block.title("Tracks").padding(Padding::new( + 0, 0, center[0].height / 2, 0, + )).title_bottom(track_instructions.alignment(Alignment::Center)) + ) + .wrap(Wrap { trim: false }) + .alignment(Alignment::Center); + frame.render_widget(message_paragraph, center[0]); + } else { + let items_len = items.len(); + let table = match self.state.active_tab { + ActiveTab::Library => Table::new(items, widths) + .block(if self.state.tracks_search_term.is_empty() && !self.state.current_artist.name.is_empty() { + track_block + .title(format!("{}", self.state.current_artist.name)) + .title_top(Line::from(format!("({} tracks)", self.tracks.iter().filter(|t| !t.id.starts_with("_album_")).count())).right_aligned()) + .title_bottom(track_instructions.alignment(Alignment::Center)) + } else { + track_block + .title(format!("Matching: {}", self.state.tracks_search_term)) + .title_top(Line::from(format!("({} tracks)", items_len)).right_aligned()) + .title_bottom(track_instructions.alignment(Alignment::Center)) + }) + .row_highlight_style(track_highlight_style) + .highlight_symbol(">>") + .style( + Style::default().bg(Color::Reset) + ) + .header( + Row::new(vec!["#", "Title", "Album", "♥", "Plays", "Disc", "Lyrics", "Duration"]) + .style(Style::new().bold().white()) + .bottom_margin(0), + ), + ActiveTab::Albums => Table::new(items, widths) + .block(if self.state.album_tracks_search_term.is_empty() && !self.state.current_album.name.is_empty() { + track_block + .title(format!("{}", self.state.current_album.name)) + .title_top(Line::from(format!("({} tracks)", self.album_tracks.iter().filter(|t| !t.id.starts_with("_album_")).count())).right_aligned()) + .title_bottom(track_instructions.alignment(Alignment::Center)) + } else { + track_block + .title(format!("Matching: {}", self.state.album_tracks_search_term)) + .title_top(Line::from(format!("({} tracks)", items_len)).right_aligned()) + .title_bottom(track_instructions.alignment(Alignment::Center)) + }) + .row_highlight_style(track_highlight_style) + .highlight_symbol(">>") + .style( + Style::default().bg(Color::Reset) + ) + .header( + Row::new(vec!["#", "Title", "Album", "♥", "Plays", "Disc", "Lyrics", "Duration"]) + .style(Style::new().bold().white()) + .bottom_margin(0), + ), + _ => return, + }; + frame.render_widget(Clear, center[0]); + match self.state.active_tab { + ActiveTab::Library => frame.render_stateful_widget(table, center[0], &mut self.state.selected_track), + ActiveTab::Albums => frame.render_stateful_widget(table, center[0], &mut self.state.selected_album_track), + _ => return, + } + } + + // change section Title to 'Searching: TERM' if locally searching + if self.locally_searching { + let searching_instructions = Line::from(vec![ + " Confirm ".white(), + "".fg(self.primary_color).bold(), + " Clear and keep selection ".white(), + " ".fg(self.primary_color).bold(), + ]); + if self.state.active_section == ActiveSection::Tracks { + frame.render_widget( + Block::default() + .borders(Borders::ALL) + .title(format!("Searching: {}", self.state.tracks_search_term)) + .title_bottom(searching_instructions.alignment(Alignment::Center)) + .border_style(self.primary_color), + center[0], + ); + } + } + + frame.render_stateful_widget( + Scrollbar::default() + .orientation(ScrollbarOrientation::VerticalRight) + .begin_symbol(Some("↑")) + .end_symbol(Some("↓")) + .track_style(Style::default().fg(Color::DarkGray)) + .thumb_style(Style::default().fg(Color::Gray)), + center[0].inner(Margin { + vertical: 1, + horizontal: 1, + }), + &mut self.state.tracks_scroll_state, + ); + } + + pub fn render_player(&mut self, frame: &mut Frame, center: &std::rc::Rc<[Rect]>) { let current_song = match self .state.queue .get(self.state.current_playback_state.current_index as usize) diff --git a/src/playlists.rs b/src/playlists.rs index 1b746fc..060e492 100644 --- a/src/playlists.rs +++ b/src/playlists.rs @@ -465,7 +465,7 @@ impl App { &mut self.state.playlist_tracks_scroll_state, ); - self.render_player(frame, center); + self.render_player(frame, ¢er); self.render_library_right(frame, right); self.create_popup(frame); diff --git a/src/tui.rs b/src/tui.rs index 275fd0a..11e177a 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -93,6 +93,7 @@ pub enum Sort { #[default] Ascending, Descending, + DateCreated, } pub struct App { pub exit: bool, @@ -106,8 +107,11 @@ pub struct App { pub auto_color: bool, // grab color from cover art (coolest feature ever omg) pub artists: Vec, // all artists - pub original_artists: Vec, // all artists + pub albums: Vec, // all albums + pub album_tracks: Vec, // current album's tracks pub playlists: Vec, // playlists + pub original_artists: Vec, // all artists + pub original_albums: Vec, // all albums pub original_playlists: Vec, // playlists pub tracks: Vec, // current artist's tracks @@ -199,8 +203,11 @@ impl Default for App { auto_color: config.as_ref().and_then(|c| c.get("auto_color")).and_then(|a| a.as_bool()).unwrap_or(true), artists: vec![], + albums: vec![], + album_tracks: vec![], playlists: vec![], original_artists: vec![], + original_albums: vec![], original_playlists: vec![], tracks: vec![], @@ -269,15 +276,20 @@ pub struct State { // active tab (Music, Search) pub active_tab: ActiveTab, pub current_artist: Artist, + pub current_album: Album, pub current_playlist: Playlist, // ratatui list indexes pub selected_artist: ListState, pub selected_track: TableState, + pub selected_album: ListState, + pub selected_album_track: TableState, pub selected_playlist_track: TableState, pub selected_playlist: ListState, - pub tracks_scroll_state: ScrollbarState, pub artists_scroll_state: ScrollbarState, + pub tracks_scroll_state: ScrollbarState, + pub albums_scroll_state: ScrollbarState, + pub album_tracks_scroll_state: ScrollbarState, pub playlists_scroll_state: ScrollbarState, pub playlist_tracks_scroll_state: ScrollbarState, pub selected_queue_item: ListState, @@ -290,6 +302,8 @@ pub struct State { pub selected_search_track: ListState, pub artists_search_term: String, + pub albums_search_term: String, + pub album_tracks_search_term: String, pub tracks_search_term: String, pub playlist_tracks_search_term: String, pub playlists_search_term: String, @@ -306,6 +320,8 @@ pub struct State { pub artist_filter: Filter, pub artist_sort: Sort, + pub album_filter: Filter, + pub album_sort: Sort, pub playlist_filter: Filter, pub playlist_sort: Sort, @@ -372,14 +388,19 @@ impl App { self.original_artists = artists; self.state.artists_scroll_state = ScrollbarState::new(self.artists.len() - 1); self.state.active_section = ActiveSection::Artists; - self.state.selected_artist.select(Some(0)); - self.state.selected_playlist.select(Some(0)); + self.state.selected_artist.select_first(); + self.state.selected_album.select_first(); + self.state.selected_playlist.select_first(); if let Some(client) = &self.client { if let Ok(playlists) = client.playlists(String::from("")).await { self.original_playlists = playlists; self.state.playlists_scroll_state = ScrollbarState::new(self.original_playlists.len() - 1); } + if let Ok(albums) = client.albums().await { + self.original_albums = albums; + self.state.albums_scroll_state = ScrollbarState::new(self.original_albums.len() - 1); + } } self.register_controls(self.mpv_state.clone()); @@ -400,9 +421,11 @@ impl App { /// This will re-compute the order of any list that allows sorting and filtering pub fn reorder_lists(&mut self) { self.artists = self.original_artists.clone(); + self.albums = self.original_albums.clone(); self.playlists = self.original_playlists.clone(); self.artists.sort_by(|a, b| a.name.to_ascii_lowercase().cmp(&b.name.to_ascii_lowercase())); + self.albums.sort_by(|a, b| a.name.to_ascii_lowercase().cmp(&b.name.to_ascii_lowercase())); self.playlists.sort_by(|a, b| a.name.to_ascii_lowercase().cmp(&b.name.to_ascii_lowercase())); match self.state.artist_filter { @@ -423,6 +446,35 @@ impl App { } } } + match self.state.album_filter { + Filter::FavoritesFirst => { + let mut favorites: Vec<_> = self.albums.iter() + .filter(|a| a.user_data.is_favorite).cloned().collect(); + let mut non_favorites: Vec<_> = self.albums.iter() + .filter(|a: &&Album| !a.user_data.is_favorite).cloned().collect(); + + // sort by preference + match self.state.album_sort { + Sort::Ascending => { + // this is the default + } + Sort::Descending => { + favorites.reverse(); + non_favorites.reverse(); + } + Sort::DateCreated => { + favorites.sort_by(|a, b| b.date_created.cmp(&a.date_created)); + non_favorites.sort_by(|a, b| b.date_created.cmp(&a.date_created)); + } + } + self.albums = favorites.into_iter().chain(non_favorites).collect(); + } + Filter::Normal => { + if matches!(self.state.album_sort, Sort::Descending) { + self.albums.reverse(); + } + } + } match self.state.playlist_filter { Filter::FavoritesFirst => { let mut favorites: Vec<_> = self.playlists.iter() @@ -653,6 +705,13 @@ impl App { self.render_home(app_container[1], frame); } } + ActiveTab::Albums => { + if self.show_help { + self.render_home_help(app_container[1], frame); + } else { + self.render_home(app_container[1], frame); + } + } ActiveTab::Playlists => { if self.show_help { self.render_playlists_help(app_container[1], frame); @@ -685,7 +744,7 @@ impl App { Constraint::Min(15), ]) .split(area); - Tabs::new(vec!["Artists", "Playlists", "Search"]) + Tabs::new(vec!["Artists", "Albums", "Playlists", "Search"]) .style(Style::default().white().dim()) .highlight_style(Style::default().white().not_dim()) .select(self.state.active_tab as usize) @@ -774,6 +833,24 @@ impl App { } } + pub async fn album_tracks(&mut self, id: &String) { + if id.is_empty() { + return; + } + if let Some(client) = self.client.as_ref() { + if let Ok(album) = client.album_tracks(id).await { + self.state.active_section = ActiveSection::Tracks; + self.album_tracks = album; + self.state.album_tracks_scroll_state = ScrollbarState::new( + std::cmp::max(0, self.album_tracks.len() as i32 - 1) as usize + ); + self.state.current_album = self.albums.iter() + .find(|a| a.id == *id) + .cloned().unwrap_or_default(); + } + } + } + pub async fn playlist(&mut self, id: &String) { if id.is_empty() { return; @@ -998,11 +1075,13 @@ impl App { self.buffering = true; let current_artist_id = self.state.current_artist.id.clone(); + let current_album_id = self.state.current_album.id.clone(); let current_playlist_id = self.state.current_playlist.id.clone(); let active_section = self.state.active_section; self.discography(¤t_artist_id).await; + self.album_tracks(¤t_album_id).await; self.playlist(¤t_playlist_id).await; self.state.active_section = active_section; @@ -1016,6 +1095,10 @@ impl App { self.track_select_by_index(index); let index = self.state.selected_playlist_track.selected().unwrap_or(0); self.playlist_track_select_by_index(index); + let index = self.state.selected_album.selected().unwrap_or(0); + self.album_select_by_index(index); + let index = self.state.selected_album_track.selected().unwrap_or(0); + self.album_track_select_by_index(index); // handle expired session token in urls if let Some(client) = self.client.as_mut() { From 6b36aea39719f9c8df8ded1882472879898e1445 Mon Sep 17 00:00:00 2001 From: dhonus Date: Sat, 15 Feb 2025 16:58:53 +0100 Subject: [PATCH 04/26] fix: g/G a/A movement tweaks --- src/keyboard.rs | 62 ++++++++++++++++++++++++++++++++++++++----------- src/popup.rs | 4 ++-- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index df28347..f3d5bfe 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -931,7 +931,7 @@ impl App { self.popup.selected.select_previous(); } }, - KeyCode::Char('g') => match self.state.active_section { + KeyCode::Char('g') | KeyCode::Home => match self.state.active_section { ActiveSection::Artists => { match self.state.active_tab { ActiveTab::Library => { @@ -978,7 +978,7 @@ impl App { self.popup.selected.select_first(); } }, - KeyCode::Char('G') => match self.state.active_section { + KeyCode::Char('G') | KeyCode::End => match self.state.active_section { ActiveSection::Artists => { match self.state.active_tab { ActiveTab::Library => { @@ -1051,8 +1051,10 @@ impl App { artists = self.artists.iter().collect::>(); } let selected = self.state.selected_artist.selected().unwrap_or(0); - let current_artist = artists[selected].name[0..1].to_lowercase(); - let next_artist = artists.iter().skip(selected).find(|a| a.name[0..1].to_lowercase() != current_artist); + let current_artist = artists[selected].name.chars().next().unwrap().to_string().to_lowercase(); + let next_artist = artists + .iter().skip(selected) + .find(|a| a.name.chars().next().unwrap().to_string().to_lowercase() != current_artist); if let Some(next_artist) = next_artist { let index = artists.iter().position(|a| a.id == next_artist.id).unwrap_or(0); @@ -1088,8 +1090,10 @@ impl App { albums = self.albums.iter().collect::>(); } if let Some(selected) = self.state.selected_album.selected() { - let current_album = albums[selected].name[0..1].to_lowercase(); - let next_album = albums.iter().skip(selected).find(|a| a.name[0..1].to_lowercase() != current_album); + let current_album = albums[selected].name.chars().next().unwrap().to_string().to_lowercase(); + let next_album = albums + .iter().skip(selected) + .find(|a| a.name.chars().next().unwrap().to_string().to_lowercase() != current_album); if let Some(next_album) = next_album { let index = albums.iter().position(|a| a.id == next_album.id).unwrap_or(0); @@ -1109,8 +1113,10 @@ impl App { playlists = self.playlists.iter().collect::>(); } if let Some(selected) = self.state.selected_playlist.selected() { - let current_playlist = playlists[selected].name[0..1].to_lowercase(); - let next_playlist = playlists.iter().skip(selected).find(|a| a.name[0..1].to_lowercase() != current_playlist); + let current_playlist = playlists[selected].name.chars().next().unwrap().to_string().to_lowercase(); + let next_playlist = playlists + .iter().skip(selected) + .find(|a| a.name.chars().next().unwrap().to_string().to_lowercase() != current_playlist); if let Some(next_playlist) = next_playlist { let index = playlists.iter().position(|a| a.id == next_playlist.id).unwrap_or(0); @@ -1135,8 +1141,10 @@ impl App { artists = self.artists.iter().collect::>(); } let selected = self.state.selected_artist.selected().unwrap_or(0); - let current_artist = artists[selected].name[0..1].to_lowercase(); - let prev_artist = artists.iter().rev().skip(artists.len() - selected).find(|a| a.name[0..1].to_lowercase() != current_artist); + let current_artist = artists[selected].name.chars().next().unwrap().to_string().to_lowercase(); + let prev_artist = artists + .iter().rev().skip(artists.len() - selected) + .find(|a| a.name.chars().next().unwrap().to_string().to_lowercase() != current_artist); if let Some(prev_artist) = prev_artist { let index = artists.iter().position(|a| a.id == prev_artist.id).unwrap_or(0); @@ -1167,6 +1175,30 @@ impl App { _ => {} } } + ActiveTab::Albums => { + if matches!(self.state.active_section, ActiveSection::Artists) { + if self.albums.is_empty() { + return; + } + let ids = search_results(&self.albums, &self.state.albums_search_term, false); + let mut albums = self.albums.iter().filter(|album| ids.contains(&album.id)).collect::>(); + if albums.is_empty() { + albums = self.albums.iter().collect::>(); + } + if let Some(selected) = self.state.selected_album.selected() { + let current_album = albums[selected].name.chars().next().unwrap().to_string().to_lowercase(); + let prev_album = albums + .iter().rev() + .skip(albums.len() - selected) + .find(|a| a.name.chars().next().unwrap().to_string().to_lowercase() != current_album); + + if let Some(prev_album) = prev_album { + let index = albums.iter().position(|a| a.id == prev_album.id).unwrap_or(0); + self.album_select_by_index(index); + } + } + } + } ActiveTab::Playlists => { if matches!(self.state.active_section, ActiveSection::Artists) { if self.playlists.is_empty() { @@ -1178,8 +1210,10 @@ impl App { playlists = self.playlists.iter().collect::>(); } if let Some(selected) = self.state.selected_playlist.selected() { - let current_playlist = playlists[selected].name[0..1].to_lowercase(); - let prev_playlist = playlists.iter().rev().skip(playlists.len() - selected).find(|a| a.name[0..1].to_lowercase() != current_playlist); + let current_playlist = playlists[selected].name.chars().next().unwrap().to_string().to_lowercase(); + let prev_playlist = playlists + .iter().rev().skip(playlists.len() - selected) + .find(|a| a.name.chars().next().unwrap().to_string().to_lowercase() != current_playlist); if let Some(prev_playlist) = prev_playlist { let index = playlists.iter().position(|a| a.id == prev_playlist.id).unwrap_or(0); @@ -1922,7 +1956,7 @@ impl App { self.state.search_track_scroll_state = self.state.search_track_scroll_state.position(selected - 1); } }, - KeyCode::Char('g') => match self.state.search_section { + KeyCode::Char('g') | KeyCode::Home => match self.state.search_section { SearchSection::Artists => { self.state.selected_search_artist.select(Some(0)); self.state.search_artist_scroll_state = self.state.search_artist_scroll_state.position(0); @@ -1936,7 +1970,7 @@ impl App { self.state.search_track_scroll_state = self.state.search_track_scroll_state.position(0); } }, - KeyCode::Char('G') => match self.state.search_section { + KeyCode::Char('G') | KeyCode::End => match self.state.search_section { SearchSection::Artists => { self.state.selected_search_artist.select(Some(self.search_result_artists.len() - 1)); self.state.search_artist_scroll_state = self.state.search_artist_scroll_state.position(self.search_result_artists.len() - 1); diff --git a/src/popup.rs b/src/popup.rs index 05668dd..a9872ee 100644 --- a/src/popup.rs +++ b/src/popup.rs @@ -643,10 +643,10 @@ impl crate::tui::App { KeyCode::Char('k') | KeyCode::Up => { self.popup.selected.select_previous(); } - KeyCode::Char('g') => { + KeyCode::Char('g') | KeyCode::Home => { self.popup.selected.select_first(); } - KeyCode::Char('G') => { + KeyCode::Char('G') | KeyCode::End => { self.popup.selected.select_last(); } From e11eee36f2ff4fe5730e2c5598ad7a348f740748 Mon Sep 17 00:00:00 2001 From: dhonus Date: Sat, 15 Feb 2025 21:56:44 +0100 Subject: [PATCH 05/26] fix: removed album plays column --- src/library.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/library.rs b/src/library.rs index 2bbc19d..c74e403 100644 --- a/src/library.rs +++ b/src/library.rs @@ -224,7 +224,6 @@ impl App { } } - let artists = search_results(&self.artists, &self.state.artists_search_term, true) .iter() .map(|id| self.artists.iter().find(|artist| artist.id == *id).unwrap()) @@ -698,7 +697,7 @@ impl App { } else { "".to_string() }).style(Style::default().fg(self.primary_color)), - Cell::from(format!("{}", track.user_data.play_count)), + Cell::from(""), Cell::from(""), Cell::from(""), Cell::from(duration), From 3b04357d67b8f18b9de927086fa537e5cdb4a232 Mon Sep 17 00:00:00 2001 From: dhonus Date: Wed, 19 Feb 2025 17:18:02 +0100 Subject: [PATCH 06/26] fix: simplified some scroll states --- src/keyboard.rs | 270 ++++++++++++++++++++---------------------------- src/library.rs | 5 +- src/popup.rs | 2 +- src/tui.rs | 14 ++- 4 files changed, 127 insertions(+), 164 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index f3d5bfe..e3834a6 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -639,62 +639,67 @@ impl App { // Move down KeyCode::Down | KeyCode::Char('j') => match self.state.active_section { ActiveSection::Artists => { - if self.state.active_tab == ActiveTab::Library { - if !self.state.artists_search_term.is_empty() { - let items = search_results(&self.artists, &self.state.artists_search_term, false); - let selected = self.state.selected_artist.selected().unwrap_or(items.len() - 1); - if selected == items.len() - 1 { + match self.state.active_tab { + ActiveTab::Library => { + if !self.state.artists_search_term.is_empty() { + let items = search_results(&self.artists, &self.state.artists_search_term, false); + let selected = self.state.selected_artist.selected().unwrap_or(items.len() - 1); + if selected == items.len() - 1 { + self.artist_select_by_index(selected); + return; + } + self.artist_select_by_index(selected + 1); + return; + } + + let selected = self.state.selected_artist.selected().unwrap_or(self.artists.len() - 1); + if selected == self.artists.len() - 1 { self.artist_select_by_index(selected); return; } self.artist_select_by_index(selected + 1); - return; } + ActiveTab::Albums => { + if !self.state.albums_search_term.is_empty() { + let items = search_results(&self.albums, &self.state.albums_search_term, false); + let selected = self.state.selected_album.selected().unwrap_or(items.len() - 1); + if selected == items.len() - 1 { + self.album_select_by_index(selected); + return; + } + self.album_select_by_index(selected + 1); + return; + } - let selected = self.state.selected_artist.selected().unwrap_or(self.artists.len() - 1); - if selected == self.artists.len() - 1 { - self.artist_select_by_index(selected); - return; - } - self.artist_select_by_index(selected + 1); - } - if self.state.active_tab == ActiveTab::Albums { - if !self.state.albums_search_term.is_empty() { - let items = search_results(&self.albums, &self.state.albums_search_term, false); - let selected = self.state.selected_album.selected().unwrap_or(items.len() - 1); - if selected == items.len() - 1 { + let selected = self.state.selected_album.selected().unwrap_or(self.albums.len() - 1); + if selected == self.albums.len() - 1 { self.album_select_by_index(selected); return; } self.album_select_by_index(selected + 1); - return; } + ActiveTab::Playlists => { + if !self.state.playlists_search_term.is_empty() { + let items = search_results(&self.playlists, &self.state.playlists_search_term, false); + let selected = self.state.selected_playlist.selected().unwrap_or(items.len() - 1); + if selected == items.len() - 1 { + self.playlist_select_by_index(selected); + return; + } + self.playlist_select_by_index(selected + 1); + return; + } - let selected = self.state.selected_album.selected().unwrap_or(self.albums.len() - 1); - if selected == self.albums.len() - 1 { - self.album_select_by_index(selected); - return; - } - self.album_select_by_index(selected + 1); - } - if self.state.active_tab == ActiveTab::Playlists { - if !self.state.playlists_search_term.is_empty() { - let items = search_results(&self.playlists, &self.state.playlists_search_term, false); - let selected = self.state.selected_playlist.selected().unwrap_or(items.len() - 1); - if selected == items.len() - 1 { + let selected = self.state.selected_playlist.selected().unwrap_or(self.playlists.len() - 1); + if selected == self.playlists.len() - 1 { self.playlist_select_by_index(selected); return; } self.playlist_select_by_index(selected + 1); - return; } - - let selected = self.state.selected_playlist.selected().unwrap_or(self.playlists.len() - 1); - if selected == self.playlists.len() - 1 { - self.playlist_select_by_index(selected); - return; + ActiveTab::Search => { + // handle_search_tab_events() } - self.playlist_select_by_index(selected + 1); } } ActiveSection::Tracks => { @@ -805,68 +810,73 @@ impl App { }, KeyCode::Up | KeyCode::Char('k') => match self.state.active_section { ActiveSection::Artists => { - if self.state.active_tab == ActiveTab::Library { - if !self.state.artists_search_term.is_empty() { - let selected = self - .state.selected_artist - .selected() - .unwrap_or(0); + match self.state.active_tab { + ActiveTab::Library => { + if !self.state.artists_search_term.is_empty() { + let selected = self + .state.selected_artist + .selected() + .unwrap_or(0); + if selected == 0 { + self.artist_select_by_index(selected); + return; + } + self.artist_select_by_index(selected - 1); + return; + } + + let selected = self.state.selected_artist.selected().unwrap_or(0); if selected == 0 { self.artist_select_by_index(selected); return; } self.artist_select_by_index(selected - 1); - return; } + ActiveTab::Albums => { + if !self.state.albums_search_term.is_empty() { + let selected = self + .state.selected_album + .selected() + .unwrap_or(0); + if selected == 0 { + self.album_select_by_index(selected); + return; + } + self.album_select_by_index(selected - 1); + return; + } - let selected = self.state.selected_artist.selected().unwrap_or(0); - if selected == 0 { - self.artist_select_by_index(selected); - return; - } - self.artist_select_by_index(selected - 1); - } - if self.state.active_tab == ActiveTab::Albums { - if !self.state.albums_search_term.is_empty() { - let selected = self - .state.selected_album - .selected() - .unwrap_or(0); + let selected = self.state.selected_album.selected().unwrap_or(0); if selected == 0 { self.album_select_by_index(selected); return; } self.album_select_by_index(selected - 1); - return; } + ActiveTab::Playlists => { + if !self.state.playlists_search_term.is_empty() { + let selected = self + .state.selected_playlist + .selected() + .unwrap_or(0); + if selected == 0 { + self.playlist_select_by_index(selected); + return; + } + self.playlist_select_by_index(selected - 1); + return; + } - let selected = self.state.selected_album.selected().unwrap_or(0); - if selected == 0 { - self.album_select_by_index(selected); - return; - } - self.album_select_by_index(selected - 1); - } - if self.state.active_tab == ActiveTab::Playlists { - if !self.state.playlists_search_term.is_empty() { - let selected = self - .state.selected_playlist - .selected() - .unwrap_or(0); + let selected = self.state.selected_playlist.selected().unwrap_or(0); if selected == 0 { self.playlist_select_by_index(selected); return; } self.playlist_select_by_index(selected - 1); - return; } - - let selected = self.state.selected_playlist.selected().unwrap_or(0); - if selected == 0 { - self.playlist_select_by_index(selected); - return; + ActiveTab::Search => { + // handle_search_tab_events() } - self.playlist_select_by_index(selected - 1); } } ActiveSection::Tracks => { @@ -1876,112 +1886,58 @@ impl App { } KeyCode::Down | KeyCode::Char('j') => match self.state.search_section { SearchSection::Artists => { - let selected = self - .state.selected_search_artist - .selected() - .unwrap_or(self.search_result_artists.len() - 1); - if selected == self.search_result_artists.len() - 1 { - self.state.selected_search_artist.select(Some(selected)); - self.state.search_artist_scroll_state = self.state.search_artist_scroll_state.position(selected); - return; - } - self.state.selected_search_artist.select(Some(selected + 1)); - self.state.search_artist_scroll_state = self.state.search_artist_scroll_state.position(selected + 1); + self.state.selected_search_artist.select_next(); + self.state.search_artist_scroll_state.next(); } SearchSection::Albums => { - let selected = self - .state.selected_search_album - .selected() - .unwrap_or(self.search_result_albums.len() - 1); - if selected == self.search_result_albums.len() - 1 { - self.state.selected_search_album.select(Some(selected)); - self.state.search_album_scroll_state = self.state.search_album_scroll_state.position(selected); - return; - } - self.state.selected_search_album.select(Some(selected + 1)); - self.state.search_album_scroll_state = self.state.search_album_scroll_state.position(selected + 1); + self.state.selected_search_album.select_next(); + self.state.search_album_scroll_state.next(); } SearchSection::Tracks => { - let selected = self - .state.selected_search_track - .selected() - .unwrap_or(self.search_result_tracks.len() - 1); - if selected == self.search_result_tracks.len() - 1 { - self.state.selected_search_track.select(Some(selected)); - self.state.search_track_scroll_state = self.state.search_track_scroll_state.position(selected); - return; - } - self.state.selected_search_track.select(Some(selected + 1)); - self.state.search_track_scroll_state = self.state.search_track_scroll_state.position(selected + 1); + self.state.selected_search_track.select_next(); + self.state.search_track_scroll_state.next(); } }, KeyCode::Up | KeyCode::Char('k') => match self.state.search_section { SearchSection::Artists => { - let selected = self - .state.selected_search_artist - .selected() - .unwrap_or(0); - if selected == 0 { - self.state.selected_search_artist.select(Some(selected)); - self.state.search_artist_scroll_state = self.state.search_artist_scroll_state.position(selected); - return; - } - self.state.selected_search_artist.select(Some(selected - 1)); - self.state.search_artist_scroll_state = self.state.search_artist_scroll_state.position(selected - 1); + self.state.selected_search_artist.select_previous(); + self.state.search_artist_scroll_state.prev(); } SearchSection::Albums => { - let selected = self - .state.selected_search_album - .selected() - .unwrap_or(0); - if selected == 0 { - self.state.selected_search_album.select(Some(selected)); - self.state.search_album_scroll_state = self.state.search_album_scroll_state.position(selected); - return; - } - self.state.selected_search_album.select(Some(selected - 1)); - self.state.search_album_scroll_state = self.state.search_album_scroll_state.position(selected - 1); + self.state.selected_search_album.select_previous(); + self.state.search_album_scroll_state.prev(); } SearchSection::Tracks => { - let selected = self - .state.selected_search_track - .selected() - .unwrap_or(0); - if selected == 0 { - self.state.selected_search_track.select(Some(selected)); - self.state.search_track_scroll_state = self.state.search_track_scroll_state.position(selected); - return; - } - self.state.selected_search_track.select(Some(selected - 1)); - self.state.search_track_scroll_state = self.state.search_track_scroll_state.position(selected - 1); + self.state.selected_search_track.select_previous(); + self.state.search_track_scroll_state.prev(); } }, KeyCode::Char('g') | KeyCode::Home => match self.state.search_section { SearchSection::Artists => { - self.state.selected_search_artist.select(Some(0)); - self.state.search_artist_scroll_state = self.state.search_artist_scroll_state.position(0); + self.state.selected_search_artist.select_first(); + self.state.search_artist_scroll_state.first(); } SearchSection::Albums => { - self.state.selected_search_album.select(Some(0)); - self.state.search_album_scroll_state = self.state.search_album_scroll_state.position(0); + self.state.selected_search_album.select_first(); + self.state.search_album_scroll_state.first(); } SearchSection::Tracks => { - self.state.selected_search_track.select(Some(0)); - self.state.search_track_scroll_state = self.state.search_track_scroll_state.position(0); + self.state.selected_search_track.select_first(); + self.state.search_track_scroll_state.first(); } }, KeyCode::Char('G') | KeyCode::End => match self.state.search_section { SearchSection::Artists => { - self.state.selected_search_artist.select(Some(self.search_result_artists.len() - 1)); - self.state.search_artist_scroll_state = self.state.search_artist_scroll_state.position(self.search_result_artists.len() - 1); + self.state.selected_search_artist.select_last(); + self.state.search_artist_scroll_state.last(); } SearchSection::Albums => { - self.state.selected_search_album.select(Some(self.search_result_albums.len() - 1)); - self.state.search_album_scroll_state = self.state.search_album_scroll_state.position(self.search_result_albums.len() - 1); + self.state.selected_search_album.select_last(); + self.state.search_album_scroll_state.last(); } SearchSection::Tracks => { - self.state.selected_search_track.select(Some(self.search_result_tracks.len() - 1)); - self.state.search_track_scroll_state = self.state.search_track_scroll_state.position(self.search_result_tracks.len() - 1); + self.state.selected_search_track.select_last(); + self.state.search_track_scroll_state.last(); } }, KeyCode::Char('h') => { diff --git a/src/library.rs b/src/library.rs index c74e403..d812025 100644 --- a/src/library.rs +++ b/src/library.rs @@ -887,7 +887,10 @@ impl App { frame.render_widget( Block::default() .borders(Borders::ALL) - .title(format!("Searching: {}", self.state.tracks_search_term)) + .title(format!("Searching: {}", + if self.state.active_tab == ActiveTab::Library { self.state.tracks_search_term.clone() } + else { self.state.album_tracks_search_term.clone() }) + ) .title_bottom(searching_instructions.alignment(Alignment::Center)) .border_style(self.primary_color), center[0], diff --git a/src/popup.rs b/src/popup.rs index a9872ee..a7ddea2 100644 --- a/src/popup.rs +++ b/src/popup.rs @@ -1192,7 +1192,7 @@ impl crate::tui::App { if let Ok(_) = client.delete_playlist(&id).await { self.playlists.retain(|p| p.id != id); let items = search_results(&self.playlists, &self.state.playlists_search_term, false); - let _ = self.state.playlists_scroll_state.content_length(items.len() - 1); + let _ = self.state.playlists_scroll_state.content_length(items.len().saturating_sub(1)); self.popup.current_menu = Some(PopupMenu::GenericMessage { title: "Playlist deleted".to_string(), diff --git a/src/tui.rs b/src/tui.rs index 11e177a..b338521 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -386,7 +386,7 @@ impl App { } self.client = Some(client); self.original_artists = artists; - self.state.artists_scroll_state = ScrollbarState::new(self.artists.len() - 1); + self.state.artists_scroll_state = ScrollbarState::new(self.artists.len().saturating_sub(1)); self.state.active_section = ActiveSection::Artists; self.state.selected_artist.select_first(); self.state.selected_album.select_first(); @@ -395,11 +395,11 @@ impl App { if let Some(client) = &self.client { if let Ok(playlists) = client.playlists(String::from("")).await { self.original_playlists = playlists; - self.state.playlists_scroll_state = ScrollbarState::new(self.original_playlists.len() - 1); + self.state.playlists_scroll_state = ScrollbarState::new(self.original_playlists.len().saturating_sub(1)); } if let Ok(albums) = client.albums().await { self.original_albums = albums; - self.state.albums_scroll_state = ScrollbarState::new(self.original_albums.len() - 1); + self.state.albums_scroll_state = ScrollbarState::new(self.original_albums.len().saturating_sub(1)); } } self.register_controls(self.mpv_state.clone()); @@ -1039,7 +1039,9 @@ impl App { }; // let current_artist_id = self.get_id_of_selected(&self.artists, Selectable::Artist); self.artists = artists; - self.state.artists_scroll_state = self.state.artists_scroll_state.content_length(self.artists.len() - 1); + self.state.artists_scroll_state = self.state.artists_scroll_state.content_length( + self.artists.len().saturating_sub(1) + ); let playlists = match client.playlists(String::from("")).await { Ok(playlists) => playlists, @@ -1048,7 +1050,9 @@ impl App { } }; self.playlists = playlists; - self.state.playlists_scroll_state = self.state.playlists_scroll_state.content_length(self.playlists.len() - 1); + self.state.playlists_scroll_state = self.state.playlists_scroll_state.content_length( + self.playlists.len().saturating_sub(1) + ); } self.reorder_lists(); From c3c80dedce6ee70c74b098ca5e75e74de06c6182 Mon Sep 17 00:00:00 2001 From: dhonus Date: Wed, 19 Feb 2025 20:10:11 +0100 Subject: [PATCH 07/26] feat: allow changing of albums sort order --- src/popup.rs | 130 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/src/popup.rs b/src/popup.rs index a7ddea2..6549855 100644 --- a/src/popup.rs +++ b/src/popup.rs @@ -116,6 +116,12 @@ pub enum PopupMenu { }, ArtistsChangeFilter {}, ArtistsChangeSort {}, + /** + * Albums related popups + */ + AlbumsRoot {}, + AlbumsChangeFilter {}, + AlbumsChangeSort {}, } enum Action { @@ -141,6 +147,7 @@ enum Action { ChangeOrder, Ascending, Descending, + DateCreated, Normal, ShowFavoritesFirst, RunScheduledTask, @@ -185,6 +192,10 @@ impl PopupMenu { } PopupMenu::ArtistsChangeFilter { } => "Change filter".to_string(), PopupMenu::ArtistsChangeSort { } => "Change sort".to_string(), + // ---------- Albums ---------- // + PopupMenu::AlbumsRoot { } => "Albums".to_string(), + PopupMenu::AlbumsChangeFilter { } => "Change filter".to_string(), + PopupMenu::AlbumsChangeSort { } => "Change sort".to_string(), } } @@ -550,6 +561,48 @@ impl PopupMenu { style: Style::default(), }, ], + // ---------- Albums ---------- // + PopupMenu::AlbumsRoot { } => vec![ + PopupAction { + label: "Change filter".to_string(), + action: Action::ChangeFilter, + style: Style::default(), + }, + PopupAction { + label: "Change sort order".to_string(), + action: Action::ChangeOrder, + style: Style::default(), + }, + ], + PopupMenu::AlbumsChangeFilter { } => vec![ + PopupAction { + label: "Normal".to_string(), + action: Action::Normal, + style: Style::default(), + }, + PopupAction { + label: "Show favorites first".to_string(), + action: Action::ShowFavoritesFirst, + style: Style::default(), + }, + ], + PopupMenu::AlbumsChangeSort { } => vec![ + PopupAction { + label: "Ascending".to_string(), + action: Action::Ascending, + style: Style::default(), + }, + PopupAction { + label: "Descending".to_string(), + action: Action::Descending, + style: Style::default(), + }, + PopupAction { + label: "Date created".to_string(), + action: Action::DateCreated, + style: Style::default(), + }, + ], } } } @@ -703,6 +756,12 @@ impl crate::tui::App { } _ => {} }, + ActiveTab::Albums => match self.state.last_section { + ActiveSection::Artists => { + self.apply_album_action(&action, menu.clone()); + } + _ => {} + }, ActiveTab::Playlists => match self.state.last_section { ActiveSection::Artists => { if let None = self.apply_playlist_action(&action, menu.clone()).await { @@ -904,6 +963,66 @@ impl crate::tui::App { Some(()) } + fn apply_album_action(&mut self, action: &Action, menu: PopupMenu) -> Option<()> { + match menu { + PopupMenu::AlbumsRoot { .. } => match action { + Action::ChangeFilter => { + self.popup.current_menu = Some(PopupMenu::AlbumsChangeFilter {}); + self.popup.selected.select( + match self.state.album_filter { + Filter::Normal => Some(0), + Filter::FavoritesFirst => Some(1), + } + ) + } + Action::ChangeOrder => { + self.popup.current_menu = Some(PopupMenu::AlbumsChangeSort {}); + self.popup.selected.select(Some( + match self.state.album_sort { + Sort::Ascending => 0, + Sort::Descending => 1, + Sort::DateCreated => 2, + } + )); + } + _ => {} + }, + PopupMenu::AlbumsChangeFilter { .. } => match action { + Action::Normal => { + self.state.album_filter = Filter::Normal; + self.reorder_lists(); + self.close_popup(); + } + Action::ShowFavoritesFirst => { + self.state.album_filter = Filter::FavoritesFirst; + self.reorder_lists(); + self.close_popup(); + } + _ => {} + }, + PopupMenu::AlbumsChangeSort { .. } => match action { + Action::Ascending => { + self.state.album_sort = Sort::Ascending; + self.reorder_lists(); + self.close_popup(); + } + Action::Descending => { + self.state.album_sort = Sort::Descending; + self.reorder_lists(); + self.close_popup(); + } + Action::DateCreated => { + self.state.album_sort = Sort::DateCreated; + self.reorder_lists(); + self.close_popup(); + } + _ => {} + }, + _ => {} + } + Some(()) + } + async fn apply_playlist_tracks_action( &mut self, action: &Action, @@ -1426,6 +1545,17 @@ impl crate::tui::App { self.close_popup(); } }, + ActiveTab::Albums => match self.state.last_section { + ActiveSection::Artists => { + if self.popup.current_menu.is_none() { + self.popup.current_menu = Some(PopupMenu::AlbumsRoot {}); + self.popup.selected.select(Some(0)); + } + } + _ => { + self.close_popup(); + } + }, ActiveTab::Playlists => match self.state.last_section { ActiveSection::Artists => { if self.popup.current_menu.is_none() { From 70110d2fea2b5c40bbb4ac87a700ff52ed18bb13 Mon Sep 17 00:00:00 2001 From: dhonus Date: Wed, 19 Feb 2025 21:07:39 +0100 Subject: [PATCH 08/26] refactor: separated track table renders --- src/library.rs | 359 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 243 insertions(+), 116 deletions(-) diff --git a/src/library.rs b/src/library.rs index d812025..0795b1d 100644 --- a/src/library.rs +++ b/src/library.rs @@ -632,14 +632,8 @@ impl App { .border_style(style::Color::White), }; - // let selected_track = self.get_id_of_selected(&self.tracks, Selectable::Track); - let selected_track = match self.state.active_tab { - ActiveTab::Library => self.get_id_of_selected(&self.tracks, Selectable::Track), - ActiveTab::Albums => self.get_id_of_selected(&self.album_tracks, Selectable::Track), - _ => return, - }; let current_track = self.state.queue.get(self.state.current_playback_state.current_index as usize); - + let mut track_highlight_style = match self.state.active_section { ActiveSection::Tracks => Style::default() .add_modifier(Modifier::BOLD) @@ -651,26 +645,77 @@ impl App { .bg(Color::DarkGray), }; + // let selected_track = self.get_id_of_selected(&self.tracks, Selectable::Track); + let selected_track = match self.state.active_tab { + ActiveTab::Library => self.get_id_of_selected(&self.tracks, Selectable::Track), + ActiveTab::Albums => self.get_id_of_selected(&self.album_tracks, Selectable::Track), + _ => return, + }; if current_track.is_some() && current_track.unwrap().id == selected_track { track_highlight_style = track_highlight_style.add_modifier(Modifier::ITALIC); } - // let tracks = search_results(&self.tracks, &self.state.tracks_search_term, true) - // .iter() - // .map(|id| self.tracks.iter().find(|t| t.id == *id).unwrap()) - // .collect::>(); + match self.state.active_tab { + ActiveTab::Library => { + self.render_library_tracks_table(frame, center, track_block, track_highlight_style); + } + ActiveTab::Albums => { + self.render_album_tracks_table(frame, center, track_block, track_highlight_style); + } + _ => {} + } - let tracks = match self.state.active_tab { - ActiveTab::Library => search_results(&self.tracks, &self.state.tracks_search_term, true) - .iter() - .map(|id| self.tracks.iter().find(|t| t.id == *id).unwrap()) - .collect::>(), - ActiveTab::Albums => search_results(&self.album_tracks, &self.state.album_tracks_search_term, true) - .iter() - .map(|id| self.album_tracks.iter().find(|t| t.id == *id).unwrap()) - .collect::>(), - _ => return, - }; + // change section Title to 'Searching: TERM' if locally searching + if self.locally_searching { + let searching_instructions = Line::from(vec![ + " Confirm ".white(), + "".fg(self.primary_color).bold(), + " Clear and keep selection ".white(), + " ".fg(self.primary_color).bold(), + ]); + if self.state.active_section == ActiveSection::Tracks { + frame.render_widget( + Block::default() + .borders(Borders::ALL) + .title(format!("Searching: {}", + if self.state.active_tab == ActiveTab::Library { self.state.tracks_search_term.clone() } + else { self.state.album_tracks_search_term.clone() }) + ) + .title_bottom(searching_instructions.alignment(Alignment::Center)) + .border_style(self.primary_color), + center[0], + ); + } + } + + frame.render_stateful_widget( + Scrollbar::default() + .orientation(ScrollbarOrientation::VerticalRight) + .begin_symbol(Some("↑")) + .end_symbol(Some("↓")) + .track_style(Style::default().fg(Color::DarkGray)) + .thumb_style(Style::default().fg(Color::Gray)), + center[0].inner(Margin { + vertical: 1, + horizontal: 1, + }), + &mut self.state.tracks_scroll_state, + ); + } + + /// These are split into two basically the same functions because the tracks are rendered differently + /// + fn render_library_tracks_table( + &mut self, + frame: &mut Frame, + center: &std::rc::Rc<[Rect]>, + track_block: Block, + track_highlight_style: Style + ) { + let tracks = search_results(&self.tracks, &self.state.tracks_search_term, true) + .iter() + .map(|id| self.tracks.iter().find(|t| t.id == *id).unwrap()) + .collect::>(); let items = tracks .iter() @@ -802,13 +847,7 @@ impl App { Constraint::Length(10), ]; - let show_tracks = match self.state.active_tab { - ActiveTab::Library => !self.tracks.is_empty(), - ActiveTab::Albums => !self.album_tracks.is_empty(), - _ => false, - }; - - if !show_tracks { + if self.tracks.is_empty() { let message_paragraph = Paragraph::new("jellyfin-tui") .block( track_block.title("Tracks").padding(Padding::new( @@ -818,99 +857,187 @@ impl App { .wrap(Wrap { trim: false }) .alignment(Alignment::Center); frame.render_widget(message_paragraph, center[0]); - } else { - let items_len = items.len(); - let table = match self.state.active_tab { - ActiveTab::Library => Table::new(items, widths) - .block(if self.state.tracks_search_term.is_empty() && !self.state.current_artist.name.is_empty() { - track_block - .title(format!("{}", self.state.current_artist.name)) - .title_top(Line::from(format!("({} tracks)", self.tracks.iter().filter(|t| !t.id.starts_with("_album_")).count())).right_aligned()) - .title_bottom(track_instructions.alignment(Alignment::Center)) + return; + } + + let items_len = items.len(); + let table = Table::new(items, widths) + .block(if self.state.tracks_search_term.is_empty() && !self.state.current_artist.name.is_empty() { + track_block + .title(format!("{}", self.state.current_artist.name)) + .title_top(Line::from(format!("({} tracks)", self.tracks.iter().filter(|t| !t.id.starts_with("_album_")).count())).right_aligned()) + .title_bottom(track_instructions.alignment(Alignment::Center)) + } else { + track_block + .title(format!("Matching: {}", self.state.tracks_search_term)) + .title_top(Line::from(format!("({} tracks)", items_len)).right_aligned()) + .title_bottom(track_instructions.alignment(Alignment::Center)) + }) + .row_highlight_style(track_highlight_style) + .highlight_symbol(">>") + .style( + Style::default().bg(Color::Reset) + ) + .header( + Row::new(vec!["#", "Title", "Album", "♥", "Plays", "Disc", "Lyrics", "Duration"]) + .style(Style::new().bold().white()) + .bottom_margin(0), + ); + + frame.render_widget(Clear, center[0]); + frame.render_stateful_widget(table, center[0], &mut self.state.selected_track); + } + + fn render_album_tracks_table( + &mut self, + frame: &mut Frame, + center: &std::rc::Rc<[Rect]>, + track_block: Block, + track_highlight_style: Style + ) { + let tracks = search_results(&self.album_tracks, &self.state.album_tracks_search_term, true) + .iter() + .map(|id| self.album_tracks.iter().find(|t| t.id == *id).unwrap()) + .collect::>(); + + let items = tracks + .iter() + .map(|track| { + // track.run_time_ticks is in microseconds + let seconds = (track.run_time_ticks / 10_000_000) % 60; + let minutes = (track.run_time_ticks / 10_000_000 / 60) % 60; + let hours = (track.run_time_ticks / 10_000_000 / 60) / 60; + let hours_optional_text = match hours { + 0 => String::from(""), + _ => format!("{}:", hours), + }; + + let all_subsequences = helpers::find_all_subsequences( + &self.state.tracks_search_term.to_lowercase(), + &track.name.to_lowercase(), + ); + + let mut title = vec![]; + let mut last_end = 0; + let color = if track.id == self.active_song_id { + self.primary_color + } else { + Color::White + }; + for (start, end) in &all_subsequences { + if &last_end < start { + title.push(Span::styled( + &track.name[last_end..*start], + Style::default().fg(color), + )); + } + + title.push(Span::styled( + &track.name[*start..*end], + Style::default().fg(color).underlined() + )); + + last_end = *end; + } + + if last_end < track.name.len() { + title.push(Span::styled( + &track.name[last_end..], + Style::default().fg(color), + )); + } + + Row::new(vec![ + Cell::from(format!("{}.", track.index_number)).style(if track.id == self.active_song_id { + Style::default().fg(color) } else { - track_block - .title(format!("Matching: {}", self.state.tracks_search_term)) - .title_top(Line::from(format!("({} tracks)", items_len)).right_aligned()) - .title_bottom(track_instructions.alignment(Alignment::Center)) - }) - .row_highlight_style(track_highlight_style) - .highlight_symbol(">>") - .style( - Style::default().bg(Color::Reset) - ) - .header( - Row::new(vec!["#", "Title", "Album", "♥", "Plays", "Disc", "Lyrics", "Duration"]) - .style(Style::new().bold().white()) - .bottom_margin(0), - ), - ActiveTab::Albums => Table::new(items, widths) - .block(if self.state.album_tracks_search_term.is_empty() && !self.state.current_album.name.is_empty() { - track_block - .title(format!("{}", self.state.current_album.name)) - .title_top(Line::from(format!("({} tracks)", self.album_tracks.iter().filter(|t| !t.id.starts_with("_album_")).count())).right_aligned()) - .title_bottom(track_instructions.alignment(Alignment::Center)) + Style::default().fg(Color::DarkGray) + }), + Cell::from(if all_subsequences.is_empty() { + track.name.to_string().into() } else { - track_block - .title(format!("Matching: {}", self.state.album_tracks_search_term)) - .title_top(Line::from(format!("({} tracks)", items_len)).right_aligned()) - .title_bottom(track_instructions.alignment(Alignment::Center)) - }) - .row_highlight_style(track_highlight_style) - .highlight_symbol(">>") - .style( - Style::default().bg(Color::Reset) - ) - .header( - Row::new(vec!["#", "Title", "Album", "♥", "Plays", "Disc", "Lyrics", "Duration"]) - .style(Style::new().bold().white()) - .bottom_margin(0), - ), - _ => return, - }; - frame.render_widget(Clear, center[0]); - match self.state.active_tab { - ActiveTab::Library => frame.render_stateful_widget(table, center[0], &mut self.state.selected_track), - ActiveTab::Albums => frame.render_stateful_widget(table, center[0], &mut self.state.selected_album_track), - _ => return, - } + Line::from(title) + }), + Cell::from(if track.user_data.is_favorite { + "♥".to_string() + } else { + "".to_string() + }).style(Style::default().fg(self.primary_color)), + Cell::from(format!("{}", track.user_data.play_count)), + Cell::from(if track.parent_index_number > 0 { + format!("{}", track.parent_index_number) + } else { + String::from("1") + }), + Cell::from(if track.has_lyrics { + "✓".to_string() + } else { + "".to_string() + }), + Cell::from(format!("{}{:02}:{:02}", hours_optional_text, minutes, seconds)), + ]).style(if track.id == self.active_song_id { + Style::default().fg(self.primary_color).italic() + } else { + Style::default().fg(Color::White) + }) + }).collect::>(); + + let track_instructions = Line::from(vec![ + " Help ".white(), + "".fg(self.primary_color).bold(), + " Quit ".white(), + " ".fg(self.primary_color).bold(), + ]); + + let widths = [ + Constraint::Length(items.len().to_string().len() as u16 + 1), + Constraint::Percentage(100), // title and track even width + Constraint::Length(2), + Constraint::Length(5), + Constraint::Length(5), + Constraint::Length(6), + Constraint::Length(10), + ]; + + if self.album_tracks.is_empty() { + let message_paragraph = Paragraph::new("jellyfin-tui") + .block( + track_block.title("Tracks").padding(Padding::new( + 0, 0, center[0].height / 2, 0, + )).title_bottom(track_instructions.alignment(Alignment::Center)) + ) + .wrap(Wrap { trim: false }) + .alignment(Alignment::Center); + frame.render_widget(message_paragraph, center[0]); + return; } - // change section Title to 'Searching: TERM' if locally searching - if self.locally_searching { - let searching_instructions = Line::from(vec![ - " Confirm ".white(), - "".fg(self.primary_color).bold(), - " Clear and keep selection ".white(), - " ".fg(self.primary_color).bold(), - ]); - if self.state.active_section == ActiveSection::Tracks { - frame.render_widget( - Block::default() - .borders(Borders::ALL) - .title(format!("Searching: {}", - if self.state.active_tab == ActiveTab::Library { self.state.tracks_search_term.clone() } - else { self.state.album_tracks_search_term.clone() }) - ) - .title_bottom(searching_instructions.alignment(Alignment::Center)) - .border_style(self.primary_color), - center[0], + let items_len = items.len(); + let table = Table::new(items, widths) + .block(if self.state.tracks_search_term.is_empty() && !self.state.current_artist.name.is_empty() { + track_block + .title(format!("{}", self.state.current_artist.name)) + .title_top(Line::from(format!("({} tracks)", self.tracks.iter().filter(|t| !t.id.starts_with("_album_")).count())).right_aligned()) + .title_bottom(track_instructions.alignment(Alignment::Center)) + } else { + track_block + .title(format!("Matching: {}", self.state.tracks_search_term)) + .title_top(Line::from(format!("({} tracks)", items_len)).right_aligned()) + .title_bottom(track_instructions.alignment(Alignment::Center)) + }) + .row_highlight_style(track_highlight_style) + .highlight_symbol(">>") + .style( + Style::default().bg(Color::Reset) + ) + .header( + Row::new(vec!["#", "Title", "♥", "Plays", "Disc", "Lyrics", "Duration"]) + .style(Style::new().bold().white()) + .bottom_margin(0), ); - } - } - frame.render_stateful_widget( - Scrollbar::default() - .orientation(ScrollbarOrientation::VerticalRight) - .begin_symbol(Some("↑")) - .end_symbol(Some("↓")) - .track_style(Style::default().fg(Color::DarkGray)) - .thumb_style(Style::default().fg(Color::Gray)), - center[0].inner(Margin { - vertical: 1, - horizontal: 1, - }), - &mut self.state.tracks_scroll_state, - ); + frame.render_widget(Clear, center[0]); + frame.render_stateful_widget(table, center[0], &mut self.state.selected_album_track); } pub fn render_player(&mut self, frame: &mut Frame, center: &std::rc::Rc<[Rect]>) { From 64f968b86cc235c4af50d5d7cdc18d01b61cf146 Mon Sep 17 00:00:00 2001 From: dhonus Date: Wed, 19 Feb 2025 21:30:07 +0100 Subject: [PATCH 09/26] feat: album jumping --- src/library.rs | 42 +++++++++++++++++++++--------------------- src/popup.rs | 31 ++++++++++++++++++++++++++++--- 2 files changed, 49 insertions(+), 24 deletions(-) diff --git a/src/library.rs b/src/library.rs index 0795b1d..566a7fe 100644 --- a/src/library.rs +++ b/src/library.rs @@ -913,7 +913,7 @@ impl App { }; let all_subsequences = helpers::find_all_subsequences( - &self.state.tracks_search_term.to_lowercase(), + &&self.state.album_tracks_search_term.to_lowercase(), &track.name.to_lowercase(), ); @@ -1014,26 +1014,26 @@ impl App { let items_len = items.len(); let table = Table::new(items, widths) - .block(if self.state.tracks_search_term.is_empty() && !self.state.current_artist.name.is_empty() { - track_block - .title(format!("{}", self.state.current_artist.name)) - .title_top(Line::from(format!("({} tracks)", self.tracks.iter().filter(|t| !t.id.starts_with("_album_")).count())).right_aligned()) - .title_bottom(track_instructions.alignment(Alignment::Center)) - } else { - track_block - .title(format!("Matching: {}", self.state.tracks_search_term)) - .title_top(Line::from(format!("({} tracks)", items_len)).right_aligned()) - .title_bottom(track_instructions.alignment(Alignment::Center)) - }) - .row_highlight_style(track_highlight_style) - .highlight_symbol(">>") - .style( - Style::default().bg(Color::Reset) - ) - .header( - Row::new(vec!["#", "Title", "♥", "Plays", "Disc", "Lyrics", "Duration"]) - .style(Style::new().bold().white()) - .bottom_margin(0), + .block(if self.state.album_tracks_search_term.is_empty() && !self.state.current_album.name.is_empty() { + track_block + .title(format!("{}", self.state.current_album.name)) + .title_top(Line::from(format!("({} tracks)", self.album_tracks.iter().filter(|t| !t.id.starts_with("_album_")).count())).right_aligned()) + .title_bottom(track_instructions.alignment(Alignment::Center)) + } else { + track_block + .title(format!("Matching: {}", self.state.album_tracks_search_term)) + .title_top(Line::from(format!("({} tracks)", items_len)).right_aligned()) + .title_bottom(track_instructions.alignment(Alignment::Center)) + }) + .row_highlight_style(track_highlight_style) + .highlight_symbol(">>") + .style( + Style::default().bg(Color::Reset) + ) + .header( + Row::new(vec!["#", "Title", "♥", "Plays", "Disc", "Lyrics", "Duration"]) + .style(Style::new().bold().white()) + .bottom_margin(0), ); frame.render_widget(Clear, center[0]); diff --git a/src/popup.rs b/src/popup.rs index 6549855..f2487ec 100644 --- a/src/popup.rs +++ b/src/popup.rs @@ -452,7 +452,7 @@ impl PopupMenu { // ---------- Playlist tracks ---------- // PopupMenu::PlaylistTracksRoot { .. } => vec![ PopupAction { - label: "Go to album".to_string(), + label: "Jump to album".to_string(), action: Action::GoAlbum, style: Style::default(), }, @@ -563,6 +563,11 @@ impl PopupMenu { ], // ---------- Albums ---------- // PopupMenu::AlbumsRoot { } => vec![ + PopupAction { + label: "Jump to current album".to_string(), + action: Action::JumpToCurrent, + style: Style::default(), + }, PopupAction { label: "Change filter".to_string(), action: Action::ChangeFilter, @@ -758,7 +763,7 @@ impl crate::tui::App { }, ActiveTab::Albums => match self.state.last_section { ActiveSection::Artists => { - self.apply_album_action(&action, menu.clone()); + self.apply_album_action(&action, menu.clone()).await; } _ => {} }, @@ -963,9 +968,29 @@ impl crate::tui::App { Some(()) } - fn apply_album_action(&mut self, action: &Action, menu: PopupMenu) -> Option<()> { + async fn apply_album_action(&mut self, action: &Action, menu: PopupMenu) -> Option<()> { match menu { PopupMenu::AlbumsRoot { .. } => match action { + Action::JumpToCurrent => { + let current_track = self.state.queue.get(self.state.current_playback_state.current_index as usize)?; + + if !self.state.albums_search_term.is_empty() { + let items = search_results(&self.albums, &self.state.albums_search_term, true); + if let Some(album) = items.into_iter().position(|a| *a == current_track.parent_id) { + self.album_select_by_index(album); + self.close_popup(); + return Some(()); + } + } + let album = self.albums.iter().find( + |a| current_track.parent_id == a.id + )?; + self.state.albums_search_term = String::from(""); + let album_id = album.id.clone(); + let index = self.albums.iter().position(|a| a.id == album_id).unwrap_or(0); + self.album_select_by_index(index); + self.close_popup(); + } Action::ChangeFilter => { self.popup.current_menu = Some(PopupMenu::AlbumsChangeFilter {}); self.popup.selected.select( From dd56ef721c7acb25d1761defece6ba31158c0c09 Mon Sep 17 00:00:00 2001 From: dhonus Date: Wed, 19 Feb 2025 21:37:52 +0100 Subject: [PATCH 10/26] fix: some kind of index error --- src/keyboard.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index e3834a6..7b6858c 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -185,18 +185,17 @@ impl App { Selectable::Playlist => self.state.selected_playlist.selected(), Selectable::PlaylistTrack => self.state.selected_playlist_track.selected(), }; + let selected = selected.unwrap_or(0); if !search_term.is_empty() { let items = search_results(items, search_term, false); - if items.is_empty() { + if items.is_empty() || items.len() <= selected { return String::from(""); } - let selected = selected.unwrap_or(0); return items[selected].clone(); } - if items.is_empty() || items.len() <= selected.unwrap_or(0) { + if items.is_empty() || items.len() <= selected { return String::from(""); } - let selected = selected.unwrap_or(0); String::from(items[selected].id()) } From 166c8569ab845563d52901bdb8be94e48313ad17 Mon Sep 17 00:00:00 2001 From: dhonus Date: Thu, 20 Feb 2025 16:26:39 +0100 Subject: [PATCH 11/26] fix: annoying tabs in names! --- src/client.rs | 17 +++++++++++++---- src/library.rs | 4 ++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/client.rs b/src/client.rs index 557f436..42165ee 100644 --- a/src/client.rs +++ b/src/client.rs @@ -194,7 +194,6 @@ impl Client { .send() .await; - // TODO: some offline state handling. Implement when adding offline caching match response { Ok(json) => { let value = match json.json::().await { @@ -326,7 +325,7 @@ impl Client { .send() .await; - let songs = match response { + let mut songs = match response { Ok(json) => { let songs: Discography = json.json().await.unwrap_or_else(|_| Discography { items: vec![], @@ -338,6 +337,11 @@ impl Client { } }; + for song in songs.iter_mut() { + song.name.retain(|c| c != '\t' && c != '\n'); + song.name = song.name.trim().to_string(); + } + Ok(songs) } @@ -375,7 +379,12 @@ impl Client { // group the songs by album let mut albums: Vec = vec![]; let mut current_album = DiscographyAlbum { songs: vec![], id: "".to_string() }; - for song in discog.items { + for mut song in discog.items { + + // you wouldn't believe the kind of things i have to deal with + song.name.retain(|c| c != '\t' && c != '\n'); + song.name = song.name.trim().to_string(); + // push songs until we find a different album if current_album.songs.is_empty() { current_album.songs.push(song); @@ -1552,4 +1561,4 @@ pub struct ScheduledTask { // pub is_hidden: bool, // #[serde(rename = "Key")] // pub key: String, -} \ No newline at end of file +} diff --git a/src/library.rs b/src/library.rs index 566a7fe..24b4a70 100644 --- a/src/library.rs +++ b/src/library.rs @@ -838,8 +838,8 @@ impl App { let widths = [ Constraint::Length(items.len().to_string().len() as u16 + 1), - Constraint::Percentage(50), // title and track even width - Constraint::Percentage(50), + Constraint::Percentage(75), // title and track even width + Constraint::Percentage(25), Constraint::Length(2), Constraint::Length(5), Constraint::Length(5), From 05f4c2170dbdb7fc4347d567c5ef499bb90ea3e5 Mon Sep 17 00:00:00 2001 From: dhonus Date: Thu, 20 Feb 2025 19:38:37 +0100 Subject: [PATCH 12/26] refactor!: make load state stable between versions --- src/helpers.rs | 2 +- src/keyboard.rs | 11 ++++++-- src/popup.rs | 4 +-- src/tui.rs | 69 ++++++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/helpers.rs b/src/helpers.rs index 3063195..f6e8fe9 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -88,7 +88,7 @@ impl crate::tui::State { playlist_filter: Filter::default(), playlist_sort: Sort::default(), - preffered_global_shuffle: PopupMenu::GlobalShuffle { tracks_n: 100, only_played: true, only_unplayed: false }, + preffered_global_shuffle: Some(PopupMenu::GlobalShuffle { tracks_n: 100, only_played: true, only_unplayed: false }), current_playback_state: MpvPlaybackState { percentage: 0.0, diff --git a/src/keyboard.rs b/src/keyboard.rs index 7b6858c..4577694 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -5,7 +5,7 @@ Keyboard related functions - Also used for searching -------------------------- */ -use crate::{client::{Album, Artist, Playlist}, helpers, tui::{App, Repeat, State}}; +use crate::{client::{Album, Artist, Playlist}, helpers, popup::PopupMenu, tui::{App, Repeat, State}}; use std::io; use std::time::Duration; @@ -1546,7 +1546,14 @@ impl App { if key_event.modifiers == KeyModifiers::CONTROL { self.state.last_section = self.state.active_section; self.state.active_section = ActiveSection::Popup; - self.popup.current_menu = Some(self.state.preffered_global_shuffle.clone()); + self.popup.current_menu = self.state.preffered_global_shuffle.clone(); + if self.popup.current_menu.is_none() { + self.popup.current_menu = Some(PopupMenu::GlobalShuffle { + tracks_n: 100, + only_played: true, + only_unplayed: false, + }); + } self.popup.global = true; self.popup.selected.select_last(); return; diff --git a/src/popup.rs b/src/popup.rs index f2487ec..c629117 100644 --- a/src/popup.rs +++ b/src/popup.rs @@ -876,11 +876,11 @@ impl crate::tui::App { let tracks = self.client.as_ref()?.random_tracks(tracks_n, only_played, only_unplayed).await.unwrap_or(vec![]); self.replace_queue(&tracks, 0).await; self.close_popup(); - self.state.preffered_global_shuffle = PopupMenu::GlobalShuffle { + self.state.preffered_global_shuffle = Some(PopupMenu::GlobalShuffle { tracks_n, only_played, only_unplayed, - }; + }); } _ => { self.close_popup(); diff --git a/src/tui.rs b/src/tui.rs index b338521..88e5c46 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -58,6 +58,20 @@ pub struct MpvPlaybackState { pub file_format: String, } +impl Default for MpvPlaybackState { + fn default() -> Self { + MpvPlaybackState { + percentage: 0.0, + duration: 0.0, + current_index: 0, + last_index: -1, + volume: 100, + audio_bitrate: 0, + file_format: String::from(""), + } + } +} + /// Internal song representation. Used in the queue and passed to MPV #[derive(Clone, Default, Serialize, Deserialize)] pub struct Song { @@ -74,10 +88,11 @@ pub struct Song { pub is_favorite: bool, pub original_index: i64, } -#[derive(PartialEq, Serialize, Deserialize)] +#[derive(PartialEq, Serialize, Deserialize, Default)] pub enum Repeat { None, One, + #[default] All, } @@ -266,67 +281,115 @@ impl Default for App { #[derive(serde::Serialize, serde::Deserialize)] pub struct State { // (URL, Title, Artist, Album) + #[serde(default)] pub queue: Vec, // Music - active section (Artists, Tracks, Queue) + #[serde(default)] pub active_section: ActiveSection, // current active section (Artists, Tracks, Queue) + #[serde(default)] pub last_section: ActiveSection, // last active section // Search - active section (Artists, Albums, Tracks) + #[serde(default)] pub search_section: SearchSection, // current active section (Artists, Albums, Tracks) // active tab (Music, Search) + #[serde(default)] pub active_tab: ActiveTab, + #[serde(default)] pub current_artist: Artist, + #[serde(default)] pub current_album: Album, + #[serde(default)] pub current_playlist: Playlist, // ratatui list indexes + #[serde(default)] pub selected_artist: ListState, + #[serde(default)] pub selected_track: TableState, + #[serde(default)] pub selected_album: ListState, + #[serde(default)] pub selected_album_track: TableState, + #[serde(default)] pub selected_playlist_track: TableState, + #[serde(default)] pub selected_playlist: ListState, + #[serde(default)] pub artists_scroll_state: ScrollbarState, + #[serde(default)] pub tracks_scroll_state: ScrollbarState, + #[serde(default)] pub albums_scroll_state: ScrollbarState, + #[serde(default)] pub album_tracks_scroll_state: ScrollbarState, + #[serde(default)] pub playlists_scroll_state: ScrollbarState, + #[serde(default)] pub playlist_tracks_scroll_state: ScrollbarState, + #[serde(default)] pub selected_queue_item: ListState, + #[serde(default)] pub selected_queue_item_manual_override: bool, + #[serde(default)] pub selected_lyric: ListState, + #[serde(default)] pub selected_lyric_manual_override: bool, + #[serde(default)] pub current_lyric: usize, + #[serde(default)] pub selected_search_artist: ListState, + #[serde(default)] pub selected_search_album: ListState, + #[serde(default)] pub selected_search_track: ListState, - + + #[serde(default)] pub artists_search_term: String, + #[serde(default)] pub albums_search_term: String, + #[serde(default)] pub album_tracks_search_term: String, + #[serde(default)] pub tracks_search_term: String, + #[serde(default)] pub playlist_tracks_search_term: String, + #[serde(default)] pub playlists_search_term: String, // scrollbars for search results + #[serde(default)] pub search_artist_scroll_state: ScrollbarState, + #[serde(default)] pub search_album_scroll_state: ScrollbarState, + #[serde(default)] pub search_track_scroll_state: ScrollbarState, // repeat mode + #[serde(default)] pub repeat: Repeat, + #[serde(default)] pub shuffle: bool, + #[serde(default)] pub large_art: bool, + #[serde(default)] pub artist_filter: Filter, + #[serde(default)] pub artist_sort: Sort, + #[serde(default)] pub album_filter: Filter, + #[serde(default)] pub album_sort: Sort, + #[serde(default)] pub playlist_filter: Filter, + #[serde(default)] pub playlist_sort: Sort, - pub preffered_global_shuffle: PopupMenu, + #[serde(default)] + pub preffered_global_shuffle: Option, + #[serde(default)] pub current_playback_state: MpvPlaybackState, } From 0169d1376c731efe96a3c055fac34c11714f7011 Mon Sep 17 00:00:00 2001 From: dhonus Date: Thu, 20 Feb 2025 19:44:19 +0100 Subject: [PATCH 13/26] refactor: moved the ugly state struct to a separate file --- src/helpers.rs | 131 +++++++++++++++++++++++++++++++++++++++++++++--- src/keyboard.rs | 2 +- src/tui.rs | 120 +------------------------------------------- 3 files changed, 127 insertions(+), 126 deletions(-) diff --git a/src/helpers.rs b/src/helpers.rs index f6e8fe9..11ddd54 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -3,7 +3,7 @@ use dirs::cache_dir; use ratatui::widgets::{ListState, ScrollbarState, TableState}; use crate::{ - client::{Album, Artist, Playlist}, keyboard::{ActiveSection, ActiveTab, SearchSection}, popup::PopupMenu, tui::{Filter, MpvPlaybackState, Repeat, Sort} + client::{Album, Artist, Playlist}, keyboard::{ActiveSection, ActiveTab, SearchSection}, popup::PopupMenu, tui::{Filter, MpvPlaybackState, Repeat, Song, Sort} }; pub fn find_all_subsequences(needle: &str, haystack: &str) -> Vec<(usize, usize)> { @@ -32,10 +32,127 @@ pub fn find_all_subsequences(needle: &str, haystack: &str) -> Vec<(usize, usize) } } +/// This struct should contain all the values that should **PERSIST** when the app is closed and reopened. +/// +#[derive(serde::Serialize, serde::Deserialize)] +pub struct State { + // (URL, Title, Artist, Album) + #[serde(default)] + pub queue: Vec, + // Music - active section (Artists, Tracks, Queue) + #[serde(default)] + pub active_section: ActiveSection, // current active section (Artists, Tracks, Queue) + #[serde(default)] + pub last_section: ActiveSection, // last active section + // Search - active section (Artists, Albums, Tracks) + #[serde(default)] + pub search_section: SearchSection, // current active section (Artists, Albums, Tracks) + + // active tab (Music, Search) + #[serde(default)] + pub active_tab: ActiveTab, + #[serde(default)] + pub current_artist: Artist, + #[serde(default)] + pub current_album: Album, + #[serde(default)] + pub current_playlist: Playlist, -impl crate::tui::State { - pub fn new() -> crate::tui::State { - crate::tui::State { + // ratatui list indexes + #[serde(default)] + pub selected_artist: ListState, + #[serde(default)] + pub selected_track: TableState, + #[serde(default)] + pub selected_album: ListState, + #[serde(default)] + pub selected_album_track: TableState, + #[serde(default)] + pub selected_playlist_track: TableState, + #[serde(default)] + pub selected_playlist: ListState, + #[serde(default)] + pub artists_scroll_state: ScrollbarState, + #[serde(default)] + pub tracks_scroll_state: ScrollbarState, + #[serde(default)] + pub albums_scroll_state: ScrollbarState, + #[serde(default)] + pub album_tracks_scroll_state: ScrollbarState, + #[serde(default)] + pub playlists_scroll_state: ScrollbarState, + #[serde(default)] + pub playlist_tracks_scroll_state: ScrollbarState, + #[serde(default)] + pub selected_queue_item: ListState, + #[serde(default)] + pub selected_queue_item_manual_override: bool, + #[serde(default)] + pub selected_lyric: ListState, + #[serde(default)] + pub selected_lyric_manual_override: bool, + #[serde(default)] + pub current_lyric: usize, + #[serde(default)] + pub selected_search_artist: ListState, + #[serde(default)] + pub selected_search_album: ListState, + #[serde(default)] + pub selected_search_track: ListState, + + #[serde(default)] + pub artists_search_term: String, + #[serde(default)] + pub albums_search_term: String, + #[serde(default)] + pub album_tracks_search_term: String, + #[serde(default)] + pub tracks_search_term: String, + #[serde(default)] + pub playlist_tracks_search_term: String, + #[serde(default)] + pub playlists_search_term: String, + + // scrollbars for search results + #[serde(default)] + pub search_artist_scroll_state: ScrollbarState, + #[serde(default)] + pub search_album_scroll_state: ScrollbarState, + #[serde(default)] + pub search_track_scroll_state: ScrollbarState, + + // repeat mode + #[serde(default)] + pub repeat: Repeat, + #[serde(default)] + pub shuffle: bool, + #[serde(default)] + pub large_art: bool, + + #[serde(default)] + pub artist_filter: Filter, + #[serde(default)] + pub artist_sort: Sort, + #[serde(default)] + pub album_filter: Filter, + #[serde(default)] + pub album_sort: Sort, + #[serde(default)] + pub playlist_filter: Filter, + #[serde(default)] + pub playlist_sort: Sort, + + #[serde(default)] + pub preffered_global_shuffle: Option, + + #[serde(default)] + pub current_playback_state: MpvPlaybackState, +} + + +impl State { + pub fn new() -> State { + State { queue: vec![], active_section: ActiveSection::default(), last_section: ActiveSection::default(), @@ -125,7 +242,7 @@ impl crate::tui::State { Ok(()) } - pub fn load_state() -> Result> { + pub fn load_state() -> Result> { let cache_dir = match cache_dir() { Some(dir) => dir, None => { @@ -136,11 +253,11 @@ impl crate::tui::State { .read(true) .open(cache_dir.join("jellyfin-tui").join("state.json")) { Ok(file) => { - let state: crate::tui::State = serde_json::from_reader(file)?; + let state: State = serde_json::from_reader(file)?; Ok(state) } Err(_) => { - Ok(crate::tui::State::new()) + Ok(State::new()) } } } diff --git a/src/keyboard.rs b/src/keyboard.rs index 4577694..6af711d 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -5,7 +5,7 @@ Keyboard related functions - Also used for searching -------------------------- */ -use crate::{client::{Album, Artist, Playlist}, helpers, popup::PopupMenu, tui::{App, Repeat, State}}; +use crate::{client::{Album, Artist, Playlist}, helpers::{self, State}, popup::PopupMenu, tui::{App, Repeat}}; use std::io; use std::time::Duration; diff --git a/src/tui.rs b/src/tui.rs index 88e5c46..549ebb0 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -12,9 +12,10 @@ Notable fields: -------------------------- */ use crate::client::{self, report_progress, Album, Artist, Client, DiscographySong, Lyric, Playlist, ProgressReport}; +use crate::helpers::State; use crate::keyboard::{*}; use crate::mpris; -use crate::popup::{PopupMenu, PopupState}; +use crate::popup::PopupState; use libmpv2::{*}; use serde::{Deserialize, Serialize}; @@ -276,123 +277,6 @@ impl Default for App { } } -/// This struct should contain all the values that should **PERSIST** when the app is closed and reopened. -/// -#[derive(serde::Serialize, serde::Deserialize)] -pub struct State { - // (URL, Title, Artist, Album) - #[serde(default)] - pub queue: Vec, - // Music - active section (Artists, Tracks, Queue) - #[serde(default)] - pub active_section: ActiveSection, // current active section (Artists, Tracks, Queue) - #[serde(default)] - pub last_section: ActiveSection, // last active section - // Search - active section (Artists, Albums, Tracks) - #[serde(default)] - pub search_section: SearchSection, // current active section (Artists, Albums, Tracks) - - // active tab (Music, Search) - #[serde(default)] - pub active_tab: ActiveTab, - #[serde(default)] - pub current_artist: Artist, - #[serde(default)] - pub current_album: Album, - #[serde(default)] - pub current_playlist: Playlist, - - // ratatui list indexes - #[serde(default)] - pub selected_artist: ListState, - #[serde(default)] - pub selected_track: TableState, - #[serde(default)] - pub selected_album: ListState, - #[serde(default)] - pub selected_album_track: TableState, - #[serde(default)] - pub selected_playlist_track: TableState, - #[serde(default)] - pub selected_playlist: ListState, - #[serde(default)] - pub artists_scroll_state: ScrollbarState, - #[serde(default)] - pub tracks_scroll_state: ScrollbarState, - #[serde(default)] - pub albums_scroll_state: ScrollbarState, - #[serde(default)] - pub album_tracks_scroll_state: ScrollbarState, - #[serde(default)] - pub playlists_scroll_state: ScrollbarState, - #[serde(default)] - pub playlist_tracks_scroll_state: ScrollbarState, - #[serde(default)] - pub selected_queue_item: ListState, - #[serde(default)] - pub selected_queue_item_manual_override: bool, - #[serde(default)] - pub selected_lyric: ListState, - #[serde(default)] - pub selected_lyric_manual_override: bool, - #[serde(default)] - pub current_lyric: usize, - #[serde(default)] - pub selected_search_artist: ListState, - #[serde(default)] - pub selected_search_album: ListState, - #[serde(default)] - pub selected_search_track: ListState, - - #[serde(default)] - pub artists_search_term: String, - #[serde(default)] - pub albums_search_term: String, - #[serde(default)] - pub album_tracks_search_term: String, - #[serde(default)] - pub tracks_search_term: String, - #[serde(default)] - pub playlist_tracks_search_term: String, - #[serde(default)] - pub playlists_search_term: String, - - // scrollbars for search results - #[serde(default)] - pub search_artist_scroll_state: ScrollbarState, - #[serde(default)] - pub search_album_scroll_state: ScrollbarState, - #[serde(default)] - pub search_track_scroll_state: ScrollbarState, - - // repeat mode - #[serde(default)] - pub repeat: Repeat, - #[serde(default)] - pub shuffle: bool, - #[serde(default)] - pub large_art: bool, - - #[serde(default)] - pub artist_filter: Filter, - #[serde(default)] - pub artist_sort: Sort, - #[serde(default)] - pub album_filter: Filter, - #[serde(default)] - pub album_sort: Sort, - #[serde(default)] - pub playlist_filter: Filter, - #[serde(default)] - pub playlist_sort: Sort, - - #[serde(default)] - pub preffered_global_shuffle: Option, - - #[serde(default)] - pub current_playback_state: MpvPlaybackState, -} - pub struct MpvState { pub mpris_events: Vec, pub mpv: Mpv, From 1092e5f07479c8df4f4b0a0d63c903b2f0f5f85b Mon Sep 17 00:00:00 2001 From: dhonus Date: Thu, 20 Feb 2025 20:06:23 +0100 Subject: [PATCH 14/26] feat: some album actions --- src/popup.rs | 120 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/src/popup.rs b/src/popup.rs index c629117..efc3e00 100644 --- a/src/popup.rs +++ b/src/popup.rs @@ -122,6 +122,13 @@ pub enum PopupMenu { AlbumsRoot {}, AlbumsChangeFilter {}, AlbumsChangeSort {}, + /** + * Album tracks related popups + */ + AlbumTrackRoot { + track_id: String, + track_name: String, + }, } enum Action { @@ -196,6 +203,8 @@ impl PopupMenu { PopupMenu::AlbumsRoot { } => "Albums".to_string(), PopupMenu::AlbumsChangeFilter { } => "Change filter".to_string(), PopupMenu::AlbumsChangeSort { } => "Change sort".to_string(), + // ---------- Album tracks ---------- // + PopupMenu::AlbumTrackRoot { track_name, .. } => format!("{}", track_name), } } @@ -608,6 +617,19 @@ impl PopupMenu { style: Style::default(), }, ], + // ---------- Album tracks ---------- // + PopupMenu::AlbumTrackRoot { .. } => vec![ + PopupAction { + label: "Jump to album of current song".to_string(), + action: Action::JumpToCurrent, + style: Style::default(), + }, + PopupAction { + label: "Add to playlist".to_string(), + action: Action::AddToPlaylist, + style: Style::default(), + }, + ], } } } @@ -765,6 +787,9 @@ impl crate::tui::App { ActiveSection::Artists => { self.apply_album_action(&action, menu.clone()).await; } + ActiveSection::Tracks => { + self.apply_album_track_action(&action, menu.clone()).await; + } _ => {} }, ActiveTab::Playlists => match self.state.last_section { @@ -1048,6 +1073,91 @@ impl crate::tui::App { Some(()) } + async fn apply_album_track_action(&mut self, action: &Action, menu: PopupMenu) -> Option<()> { + match menu { + PopupMenu::AlbumTrackRoot { .. } => { + let selected = match self.state.selected_album_track.selected() { + Some(i) => i, + None => { + self.close_popup(); + return None; + } + }; + let items = search_results(&self.album_tracks, &self.state.album_tracks_search_term, true); + let track = match items.get(selected) { + Some(track) => { + let track = self.album_tracks.iter().find(|t| t.id == *track)?; + track.clone() + } + None => { + return None; + } + }; + match action { + Action::AddToPlaylist => { + self.popup.current_menu = Some(PopupMenu::TrackAddToPlaylist { + track_name: track.name.clone(), + track_id: track.id.clone(), + playlists: self.playlists.clone(), + }); + self.popup.selected.select(Some(0)); + } + Action::JumpToCurrent => { + let current_track = self.state.queue.get(self.state.current_playback_state.current_index as usize)?; + let album = self.albums.iter().find( + |a| current_track.parent_id == a.id + )?; + let album_id = album.id.clone(); + if album_id != self.state.current_album.id { + let index = self.albums.iter().position(|a| a.id == album_id).unwrap_or(0); + self.artist_select_by_index(index); + self.album_tracks(&album_id).await; + } + if let Some(index) = self.album_tracks.iter().position(|t| t.id == track.id) { + self.track_select_by_index(index); + } + self.close_popup(); + } + _ => {} + } + } + PopupMenu::TrackAddToPlaylist { + track_name, + track_id, + playlists, + } => match action { + Action::AddToPlaylist => { + let selected = self.popup.selected.selected()?; + let playlist_id = &playlists[selected].id; + if let Some(client) = self.client.as_ref() { + if let Ok(_) = client.add_to_playlist(&track_id, playlist_id).await { + self.popup.current_menu = Some(PopupMenu::GenericMessage { + title: "Track added".to_string(), + message: format!( + "Track {} successfully added to playlist {}.", + track_name, playlists[selected].name + ), + }); + } else { + self.popup.current_menu = Some(PopupMenu::GenericMessage { + title: "Error adding track".to_string(), + message: format!( + "Failed to add track {} to playlist {}.", + track_name, playlists[selected].name + ), + }); + } + } + }, + _ => { + self.close_popup(); + } + }, + _ => {} + } + Some(()) + } + async fn apply_playlist_tracks_action( &mut self, action: &Action, @@ -1577,6 +1687,16 @@ impl crate::tui::App { self.popup.selected.select(Some(0)); } } + ActiveSection::Tracks => { + let id = self.get_id_of_selected(&self.album_tracks, Selectable::AlbumTrack); + if self.popup.current_menu.is_none() { + self.popup.current_menu = Some(PopupMenu::AlbumTrackRoot { + track_id: id.clone(), + track_name: self.album_tracks.iter().find(|t| t.id == id)?.name.clone(), + }); + self.popup.selected.select(Some(0)); + } + } _ => { self.close_popup(); } From 0c23a9301264300925777e23474a681ff9a9fa97 Mon Sep 17 00:00:00 2001 From: dhonus Date: Thu, 20 Feb 2025 20:08:47 +0100 Subject: [PATCH 15/26] style: renamed ambiguous section enum variant --- src/keyboard.rs | 96 ++++++++++++++++++++++++------------------------ src/library.rs | 12 +++--- src/playlists.rs | 6 +-- src/popup.rs | 14 +++---- src/tui.rs | 2 +- 5 files changed, 65 insertions(+), 65 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 6af711d..473302d 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -287,7 +287,7 @@ impl App { match self.state.active_tab { ActiveTab::Library => { match self.state.active_section { - ActiveSection::Artists => { + ActiveSection::List => { self.state.artists_search_term = String::from(""); self.reposition_cursor(&artist_id, Selectable::Artist); } @@ -300,7 +300,7 @@ impl App { } ActiveTab::Albums => { match self.state.active_section { - ActiveSection::Artists => { + ActiveSection::List => { self.state.albums_search_term = String::from(""); self.reposition_cursor(&album_id, Selectable::Album); } @@ -313,7 +313,7 @@ impl App { } ActiveTab::Playlists => { match self.state.active_section { - ActiveSection::Artists => { + ActiveSection::List => { self.state.playlists_search_term = String::from(""); self.reposition_cursor(&playlist_id, Selectable::Playlist); } @@ -333,19 +333,19 @@ impl App { match self.state.active_tab { ActiveTab::Library => { self.locally_searching = false; - if self.state.active_section == ActiveSection::Artists { + if self.state.active_section == ActiveSection::List { self.state.tracks_search_term = String::from(""); } } ActiveTab::Albums => { self.locally_searching = false; - if self.state.active_section == ActiveSection::Artists { + if self.state.active_section == ActiveSection::List { self.state.album_tracks_search_term = String::from(""); } } ActiveTab::Playlists => { self.locally_searching = false; - if self.state.active_section == ActiveSection::Artists { + if self.state.active_section == ActiveSection::List { self.state.playlist_tracks_search_term = String::from(""); } } @@ -357,7 +357,7 @@ impl App { match self.state.active_tab { ActiveTab::Library => { match self.state.active_section { - ActiveSection::Artists => { + ActiveSection::List => { let selected_id = self.get_id_of_selected(&self.artists, Selectable::Artist); self.state.artists_search_term.pop(); self.reposition_cursor(&selected_id, Selectable::Artist); @@ -372,7 +372,7 @@ impl App { } ActiveTab::Albums => { match self.state.active_section { - ActiveSection::Artists => { + ActiveSection::List => { let selected_id = self.get_id_of_selected(&self.albums, Selectable::Album); self.state.albums_search_term.pop(); self.reposition_cursor(&selected_id, Selectable::Album); @@ -387,7 +387,7 @@ impl App { } ActiveTab::Playlists => { match self.state.active_section { - ActiveSection::Artists => { + ActiveSection::List => { let selected_id = self.get_id_of_selected(&self.playlists, Selectable::Playlist); self.state.playlists_search_term.pop(); self.reposition_cursor(&selected_id, Selectable::Playlist); @@ -407,7 +407,7 @@ impl App { match self.state.active_tab { ActiveTab::Library => { match self.state.active_section { - ActiveSection::Artists => { + ActiveSection::List => { let selected_id = self.get_id_of_selected(&self.artists, Selectable::Artist); self.state.artists_search_term.clear(); self.reposition_cursor(&selected_id, Selectable::Artist); @@ -422,7 +422,7 @@ impl App { } ActiveTab::Albums => { match self.state.active_section { - ActiveSection::Artists => { + ActiveSection::List => { let selected_id = self.get_id_of_selected(&self.albums, Selectable::Album); self.state.albums_search_term.clear(); self.reposition_cursor(&selected_id, Selectable::Album); @@ -437,7 +437,7 @@ impl App { } ActiveTab::Playlists => { match self.state.active_section { - ActiveSection::Artists => { + ActiveSection::List => { let selected_id = self.get_id_of_selected(&self.playlists, Selectable::Playlist); self.state.playlists_search_term.clear(); self.reposition_cursor(&selected_id, Selectable::Playlist); @@ -457,7 +457,7 @@ impl App { match self.state.active_tab { ActiveTab::Library => { match self.state.active_section { - ActiveSection::Artists => { + ActiveSection::List => { self.state.artists_search_term.push(c); self.artist_select_by_index(0); } @@ -470,7 +470,7 @@ impl App { } ActiveTab::Albums => { match self.state.active_section { - ActiveSection::Artists => { + ActiveSection::List => { self.state.albums_search_term.push(c); self.album_select_by_index(0); } @@ -483,7 +483,7 @@ impl App { } ActiveTab::Playlists => { match self.state.active_section { - ActiveSection::Artists => { + ActiveSection::List => { self.state.playlists_search_term.push(c); self.playlist_select_by_index(0); } @@ -637,7 +637,7 @@ impl App { } // Move down KeyCode::Down | KeyCode::Char('j') => match self.state.active_section { - ActiveSection::Artists => { + ActiveSection::List => { match self.state.active_tab { ActiveTab::Library => { if !self.state.artists_search_term.is_empty() { @@ -808,7 +808,7 @@ impl App { }, }, KeyCode::Up | KeyCode::Char('k') => match self.state.active_section { - ActiveSection::Artists => { + ActiveSection::List => { match self.state.active_tab { ActiveTab::Library => { if !self.state.artists_search_term.is_empty() { @@ -941,7 +941,7 @@ impl App { } }, KeyCode::Char('g') | KeyCode::Home => match self.state.active_section { - ActiveSection::Artists => { + ActiveSection::List => { match self.state.active_tab { ActiveTab::Library => { self.artist_select_by_index(0); @@ -988,7 +988,7 @@ impl App { } }, KeyCode::Char('G') | KeyCode::End => match self.state.active_section { - ActiveSection::Artists => { + ActiveSection::List => { match self.state.active_tab { ActiveTab::Library => { if !self.artists.is_empty() { @@ -1050,7 +1050,7 @@ impl App { ActiveTab::Library => { match self.state.active_section { // first artist with following letter - ActiveSection::Artists => { + ActiveSection::List => { if self.artists.is_empty() { return; } @@ -1089,7 +1089,7 @@ impl App { } } ActiveTab::Albums => { - if matches!(self.state.active_section, ActiveSection::Artists) { + if matches!(self.state.active_section, ActiveSection::List) { if self.albums.is_empty() { return; } @@ -1112,7 +1112,7 @@ impl App { } } ActiveTab::Playlists => { - if matches!(self.state.active_section, ActiveSection::Artists) { + if matches!(self.state.active_section, ActiveSection::List) { if self.playlists.is_empty() { return; } @@ -1140,7 +1140,7 @@ impl App { ActiveTab::Library => { match self.state.active_section { // first artist with previous letter - ActiveSection::Artists => { + ActiveSection::List => { if self.artists.is_empty() { return; } @@ -1185,7 +1185,7 @@ impl App { } } ActiveTab::Albums => { - if matches!(self.state.active_section, ActiveSection::Artists) { + if matches!(self.state.active_section, ActiveSection::List) { if self.albums.is_empty() { return; } @@ -1209,7 +1209,7 @@ impl App { } } ActiveTab::Playlists => { - if matches!(self.state.active_section, ActiveSection::Artists) { + if matches!(self.state.active_section, ActiveSection::List) { if self.playlists.is_empty() { return; } @@ -1235,7 +1235,7 @@ impl App { }, KeyCode::Enter => { match self.state.active_section { - ActiveSection::Artists => { + ActiveSection::List => { if self.state.active_tab == ActiveTab::Library { self.state.tracks_search_term = String::from(""); @@ -1407,7 +1407,7 @@ impl App { // mark as favorite (works on anything) KeyCode::Char('f') => { match self.state.active_section { - ActiveSection::Artists => { + ActiveSection::List => { if let Some(client) = &self.client { match self.state.active_tab { ActiveTab::Library => { @@ -1602,7 +1602,7 @@ impl App { match self.state.active_tab { ActiveTab::Library => { match self.state.active_section { - ActiveSection::Artists => { + ActiveSection::List => { self.state.artists_search_term = String::from(""); self.reposition_cursor(&artist_id, Selectable::Artist); } @@ -1615,7 +1615,7 @@ impl App { } ActiveTab::Albums => { match self.state.active_section { - ActiveSection::Artists => { + ActiveSection::List => { self.state.albums_search_term = String::from(""); self.reposition_cursor(&album_id, Selectable::Album); } @@ -1628,7 +1628,7 @@ impl App { } ActiveTab::Playlists => { match self.state.active_section { - ActiveSection::Artists => { + ActiveSection::List => { self.state.playlists_search_term = String::from(""); self.reposition_cursor(&playlist_id, Selectable::Playlist); } @@ -1652,19 +1652,19 @@ impl App { KeyCode::F(1) | KeyCode::Char('1') => { self.state.active_tab = ActiveTab::Library; if self.tracks.is_empty() { - self.state.active_section = ActiveSection::Artists; + self.state.active_section = ActiveSection::List; } } KeyCode::F(2) | KeyCode::Char('2') => { self.state.active_tab = ActiveTab::Albums; if self.tracks_playlist.is_empty() { - self.state.active_section = ActiveSection::Artists; + self.state.active_section = ActiveSection::List; } } KeyCode::F(3) | KeyCode::Char('3') => { self.state.active_tab = ActiveTab::Playlists; if self.tracks_playlist.is_empty() { - self.state.active_section = ActiveSection::Artists; + self.state.active_section = ActiveSection::List; } } KeyCode::F(4) | KeyCode::Char('4') => { @@ -1690,13 +1690,13 @@ impl App { KeyCode::F(2) => { self.state.active_tab = ActiveTab::Albums; if self.tracks_playlist.is_empty() { - self.state.active_section = ActiveSection::Artists; + self.state.active_section = ActiveSection::List; } } KeyCode::F(3) => { self.state.active_tab = ActiveTab::Playlists; if self.tracks_playlist.is_empty() { - self.state.active_section = ActiveSection::Artists; + self.state.active_section = ActiveSection::List; } } KeyCode::F(4) => { @@ -1760,7 +1760,7 @@ impl App { // in the Music tab, select this artist self.state.active_tab = ActiveTab::Library; - self.state.active_section = ActiveSection::Artists; + self.state.active_section = ActiveSection::List; self.artist_select_by_index(0); // find the artist in the artists list using .id @@ -1786,7 +1786,7 @@ impl App { // in the Music tab, select this artist self.state.active_tab = ActiveTab::Library; - self.state.active_section = ActiveSection::Artists; + self.state.active_section = ActiveSection::List; let album_id = album.id.clone(); let artist_id = if !album.album_artists.is_empty() { @@ -1823,7 +1823,7 @@ impl App { // in the Music tab, select this artist self.state.active_tab = ActiveTab::Library; - self.state.active_section = ActiveSection::Artists; + self.state.active_section = ActiveSection::List; let track_id = track.id.clone(); let album_artists = track.album_artists.clone(); @@ -1969,22 +1969,22 @@ impl App { fn toggle_section(&mut self, forwards: bool) { match forwards { true => match self.state.active_section { - ActiveSection::Artists => self.state.active_section = ActiveSection::Tracks, - ActiveSection::Tracks => self.state.active_section = ActiveSection::Artists, + ActiveSection::List => self.state.active_section = ActiveSection::Tracks, + ActiveSection::Tracks => self.state.active_section = ActiveSection::List, ActiveSection::Queue => { match self.state.last_section { - ActiveSection::Artists => self.state.active_section = ActiveSection::Artists, + ActiveSection::List => self.state.active_section = ActiveSection::List, ActiveSection::Tracks => self.state.active_section = ActiveSection::Tracks, - _ => self.state.active_section = ActiveSection::Artists, + _ => self.state.active_section = ActiveSection::List, } self.state.last_section = ActiveSection::Queue; self.state.selected_queue_item_manual_override = false; } ActiveSection::Lyrics => { match self.state.last_section { - ActiveSection::Artists => self.state.active_section = ActiveSection::Artists, + ActiveSection::List => self.state.active_section = ActiveSection::List, ActiveSection::Tracks => self.state.active_section = ActiveSection::Tracks, - _ => self.state.active_section = ActiveSection::Artists, + _ => self.state.active_section = ActiveSection::List, } self.state.last_section = ActiveSection::Lyrics; self.state.selected_lyric_manual_override = false; @@ -1992,10 +1992,10 @@ impl App { _ => {} }, false => match self.state.active_section { - ActiveSection::Artists => { - self.state.last_section = ActiveSection::Artists; + ActiveSection::List => { + self.state.last_section = ActiveSection::List; self.state.active_section = ActiveSection::Lyrics; - self.state.last_section = ActiveSection::Artists; + self.state.last_section = ActiveSection::List; } ActiveSection::Tracks => { self.state.last_section = ActiveSection::Tracks; @@ -2031,7 +2031,7 @@ pub enum ActiveTab { #[derive(Debug, PartialEq, Clone, Copy, Default, Serialize, Deserialize)] pub enum ActiveSection { #[default] - Artists, + List, Tracks, Queue, Lyrics, diff --git a/src/library.rs b/src/library.rs index 24b4a70..11d685a 100644 --- a/src/library.rs +++ b/src/library.rs @@ -197,7 +197,7 @@ impl App { fn render_library_artists(&mut self, frame: &mut Frame, left: std::rc::Rc<[Rect]>) { let artist_block = match self.state.active_section { - ActiveSection::Artists => Block::new() + ActiveSection::List => Block::new() .borders(Borders::ALL) .border_style(self.primary_color), _ => Block::new() @@ -208,7 +208,7 @@ impl App { let selected_artist = self.get_id_of_selected(&self.artists, Selectable::Artist); let mut artist_highlight_style = match self.state.active_section { - ActiveSection::Artists => Style::default() + ActiveSection::List => Style::default() .add_modifier(Modifier::BOLD) .bg(Color::White) .fg(Color::Indexed(232)), @@ -319,7 +319,7 @@ impl App { ); if self.locally_searching { - if self.state.active_section == ActiveSection::Artists { + if self.state.active_section == ActiveSection::List { frame.render_widget( Block::default() .borders(Borders::ALL) @@ -333,7 +333,7 @@ impl App { fn render_library_albums(&mut self, frame: &mut Frame, left: std::rc::Rc<[Rect]>) { let album_block = match self.state.active_section { - ActiveSection::Artists => Block::new() + ActiveSection::List => Block::new() .borders(Borders::ALL) .border_style(self.primary_color), _ => Block::new() @@ -344,7 +344,7 @@ impl App { let selected_album = self.get_id_of_selected(&self.albums, Selectable::Album); let mut album_highlight_style = match self.state.active_section { - ActiveSection::Artists => Style::default() + ActiveSection::List => Style::default() .add_modifier(Modifier::BOLD) .bg(Color::White) .fg(Color::Indexed(232)), @@ -452,7 +452,7 @@ impl App { ); if self.locally_searching { - if self.state.active_section == ActiveSection::Artists { + if self.state.active_section == ActiveSection::List { frame.render_widget( Block::default() .borders(Borders::ALL) diff --git a/src/playlists.rs b/src/playlists.rs index 060e492..5cc31d3 100644 --- a/src/playlists.rs +++ b/src/playlists.rs @@ -123,7 +123,7 @@ impl App { .split(outer_layout[2]); let playlist_block = match self.state.active_section { - ActiveSection::Artists => Block::new() + ActiveSection::List => Block::new() .borders(Borders::ALL) .border_style(self.primary_color), _ => Block::new() @@ -133,7 +133,7 @@ impl App { let selected_playlist = self.get_id_of_selected(&self.playlists, Selectable::Playlist); let mut playlist_highlight_style = match self.state.active_section { - ActiveSection::Artists => Style::default() + ActiveSection::List => Style::default() .bg(Color::White) .fg(Color::Indexed(232)) .add_modifier(Modifier::BOLD), @@ -440,7 +440,7 @@ impl App { center[0], ); } - if self.state.active_section == ActiveSection::Artists { + if self.state.active_section == ActiveSection::List { frame.render_widget( Block::default() .borders(Borders::ALL) diff --git a/src/popup.rs b/src/popup.rs index efc3e00..b2cf5c5 100644 --- a/src/popup.rs +++ b/src/popup.rs @@ -778,13 +778,13 @@ impl crate::tui::App { ActiveSection::Tracks => { self.apply_track_action(&action, menu.clone()).await; } - ActiveSection::Artists => { + ActiveSection::List => { self.apply_artist_action(&action, menu.clone()); } _ => {} }, ActiveTab::Albums => match self.state.last_section { - ActiveSection::Artists => { + ActiveSection::List => { self.apply_album_action(&action, menu.clone()).await; } ActiveSection::Tracks => { @@ -793,7 +793,7 @@ impl crate::tui::App { _ => {} }, ActiveTab::Playlists => match self.state.last_section { - ActiveSection::Artists => { + ActiveSection::List => { if let None = self.apply_playlist_action(&action, menu.clone()).await { self.close_popup(); } @@ -1187,7 +1187,7 @@ impl crate::tui::App { self.close_popup(); // in the Music tab, select this artist self.state.active_tab = ActiveTab::Library; - self.state.active_section = ActiveSection::Artists; + self.state.active_section = ActiveSection::List; self.state.tracks_search_term = String::from(""); let track_id = track.id.clone(); @@ -1662,7 +1662,7 @@ impl crate::tui::App { self.popup.selected.select(Some(0)); } } - ActiveSection::Artists => { + ActiveSection::List => { if self.popup.current_menu.is_none() { let artists = self.get_id_of_selected(&self.artists, Selectable::Artist); let artist = self.artists.iter().find(|a| a.id == artists)?.clone(); @@ -1681,7 +1681,7 @@ impl crate::tui::App { } }, ActiveTab::Albums => match self.state.last_section { - ActiveSection::Artists => { + ActiveSection::List => { if self.popup.current_menu.is_none() { self.popup.current_menu = Some(PopupMenu::AlbumsRoot {}); self.popup.selected.select(Some(0)); @@ -1702,7 +1702,7 @@ impl crate::tui::App { } }, ActiveTab::Playlists => match self.state.last_section { - ActiveSection::Artists => { + ActiveSection::List => { if self.popup.current_menu.is_none() { let id = self.get_id_of_selected(&self.playlists, Selectable::Playlist); let playlist = self.playlists.iter().find(|p| p.id == id)?.clone(); diff --git a/src/tui.rs b/src/tui.rs index 549ebb0..e7fe4bd 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -334,7 +334,7 @@ impl App { self.client = Some(client); self.original_artists = artists; self.state.artists_scroll_state = ScrollbarState::new(self.artists.len().saturating_sub(1)); - self.state.active_section = ActiveSection::Artists; + self.state.active_section = ActiveSection::List; self.state.selected_artist.select_first(); self.state.selected_album.select_first(); self.state.selected_playlist.select_first(); From b0700002cf11477920b3efb43fd64aa1833de7cd Mon Sep 17 00:00:00 2001 From: dhonus Date: Thu, 20 Feb 2025 20:22:09 +0100 Subject: [PATCH 16/26] style: renamed var --- src/keyboard.rs | 53 ++++++++++++++++++++++++------------------------ src/playlists.rs | 10 ++++----- src/popup.rs | 10 ++++----- src/tui.rs | 8 ++++---- 4 files changed, 41 insertions(+), 40 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 473302d..4fb61be 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -118,7 +118,7 @@ impl App { Selectable::AlbumTrack => self.album_tracks.iter().map(|t| t.id.clone()).collect::>(), Selectable::Track => self.tracks.iter().map(|t| t.id.clone()).collect::>(), Selectable::Playlist => self.playlists.iter().map(|p| p.id.clone()).collect::>(), - Selectable::PlaylistTrack => self.tracks_playlist.iter().map(|t| t.id.clone()).collect::>(), + Selectable::PlaylistTrack => self.playlist_tracks.iter().map(|t| t.id.clone()).collect::>(), }; if id.is_empty() { @@ -142,7 +142,7 @@ impl App { Selectable::AlbumTrack => search_results(&self.album_tracks, search_term, false), Selectable::Track => search_results(&self.tracks, search_term, false), Selectable::Playlist => search_results(&self.playlists, search_term, false), - Selectable::PlaylistTrack => search_results(&self.tracks_playlist, search_term, false), + Selectable::PlaylistTrack => search_results(&self.playlist_tracks, search_term, false), }; if let Some(index) = items.iter().position(|i| i == id) { match selectable { @@ -240,7 +240,7 @@ impl App { } pub fn playlist_track_select_by_index(&mut self, index: usize) { - let items = search_results(&self.tracks_playlist, &self.state.playlist_tracks_search_term, true); + let items = search_results(&self.playlist_tracks, &self.state.playlist_tracks_search_term, true); if items.is_empty() { return; } @@ -282,7 +282,7 @@ impl App { let album_id = self.get_id_of_selected(&self.albums, Selectable::Album); let album_track_id = self.get_id_of_selected(&self.album_tracks, Selectable::AlbumTrack); let playlist_id = self.get_id_of_selected(&self.playlists, Selectable::Playlist); - let playlist_track_id = self.get_id_of_selected(&self.tracks_playlist, Selectable::PlaylistTrack); + let playlist_track_id = self.get_id_of_selected(&self.playlist_tracks, Selectable::PlaylistTrack); match self.state.active_tab { ActiveTab::Library => { @@ -393,7 +393,7 @@ impl App { self.reposition_cursor(&selected_id, Selectable::Playlist); } ActiveSection::Tracks => { - let selected_id = self.get_id_of_selected(&self.tracks_playlist, Selectable::PlaylistTrack); + let selected_id = self.get_id_of_selected(&self.playlist_tracks, Selectable::PlaylistTrack); self.state.playlist_tracks_search_term.pop(); self.reposition_cursor(&selected_id, Selectable::PlaylistTrack); } @@ -443,7 +443,7 @@ impl App { self.reposition_cursor(&selected_id, Selectable::Playlist); } ActiveSection::Tracks => { - let selected_id = self.get_id_of_selected(&self.tracks_playlist, Selectable::PlaylistTrack); + let selected_id = self.get_id_of_selected(&self.playlist_tracks, Selectable::PlaylistTrack); self.state.playlist_tracks_search_term.clear(); self.reposition_cursor(&selected_id, Selectable::PlaylistTrack); } @@ -591,7 +591,8 @@ impl App { self.state.playlists_scroll_state = self.state.playlists_scroll_state.content_length(self.playlists.len()); self.tracks.clear(); - self.tracks_playlist.clear(); + self.album_tracks.clear(); + self.playlist_tracks.clear(); self.paused = true; } KeyCode::Char('T') => { @@ -754,7 +755,7 @@ impl App { } if self.state.active_tab == ActiveTab::Playlists { if !self.state.playlist_tracks_search_term.is_empty() { - let items = search_results(&self.tracks_playlist, &self.state.playlist_tracks_search_term, false); + let items = search_results(&self.playlist_tracks, &self.state.playlist_tracks_search_term, false); let selected = self .state.selected_playlist_track .selected() @@ -770,8 +771,8 @@ impl App { let selected = self .state.selected_playlist_track .selected() - .unwrap_or(self.tracks_playlist.len() - 1); - if selected == self.tracks_playlist.len() - 1 { + .unwrap_or(self.playlist_tracks.len() - 1); + if selected == self.playlist_tracks.len() - 1 { self.playlist_track_select_by_index(selected); return; } @@ -968,7 +969,7 @@ impl App { } } ActiveTab::Playlists => { - if !self.tracks_playlist.is_empty() { + if !self.playlist_tracks.is_empty() { self.playlist_track_select_by_index(0); } } @@ -1021,8 +1022,8 @@ impl App { } } ActiveTab::Playlists => { - if !self.tracks_playlist.is_empty() { - self.playlist_track_select_by_index(self.tracks_playlist.len() - 1); + if !self.playlist_tracks.is_empty() { + self.playlist_track_select_by_index(self.playlist_tracks.len() - 1); } } _ => {} @@ -1284,12 +1285,12 @@ impl App { } let selected = self.state.selected_playlist.selected().unwrap_or(0); self.playlist(&ids[selected]).await; - let _ = self.state.playlist_tracks_scroll_state.content_length(self.tracks_playlist.len() - 1); + let _ = self.state.playlist_tracks_scroll_state.content_length(self.playlist_tracks.len() - 1); return; } let selected = self.state.selected_playlist.selected().unwrap_or(0); self.playlist(&self.playlists[selected].id.clone()).await; - let _ = self.state.playlist_tracks_scroll_state.content_length(self.tracks_playlist.len() - 1); + let _ = self.state.playlist_tracks_scroll_state.content_length(self.playlist_tracks.len() - 1); } } ActiveSection::Tracks => { @@ -1311,8 +1312,8 @@ impl App { items } ActiveTab::Playlists => { - let ids = search_results(&self.tracks_playlist, &self.state.playlist_tracks_search_term, false); - let items: Vec = self.tracks_playlist.iter() + let ids = search_results(&self.playlist_tracks, &self.state.playlist_tracks_search_term, false); + let items: Vec = self.playlist_tracks.iter() .filter(|t| ids.contains(&t.id) || ids.is_empty()) .cloned() .collect(); @@ -1382,8 +1383,8 @@ impl App { items } ActiveTab::Playlists => { - let ids = search_results(&self.tracks_playlist, &self.state.playlist_tracks_search_term, false); - let items: Vec = self.tracks_playlist.iter() + let ids = search_results(&self.playlist_tracks, &self.state.playlist_tracks_search_term, false); + let items: Vec = self.playlist_tracks.iter() .filter(|t| ids.contains(&t.id) || ids.is_empty()) .cloned() .collect(); @@ -1478,8 +1479,8 @@ impl App { } } ActiveTab::Playlists => { - let id = self.get_id_of_selected(&self.tracks_playlist, Selectable::PlaylistTrack); - if let Some(track) = self.tracks_playlist.iter_mut().find(|t| t.id == id) { + let id = self.get_id_of_selected(&self.playlist_tracks, Selectable::PlaylistTrack); + if let Some(track) = self.playlist_tracks.iter_mut().find(|t| t.id == id) { let _ = client.set_favorite(&track.id, !track.user_data.is_favorite).await; track.user_data.is_favorite = !track.user_data.is_favorite; if let Some(tr) = self.state.queue.iter_mut().find(|t| t.id == track.id) { @@ -1597,7 +1598,7 @@ impl App { let album_track_id = self.get_id_of_selected(&self.album_tracks, Selectable::AlbumTrack); let track_id = self.get_id_of_selected(&self.tracks, Selectable::Track); let playlist_id = self.get_id_of_selected(&self.playlists, Selectable::Playlist); - let playlist_track_id = self.get_id_of_selected(&self.tracks_playlist, Selectable::PlaylistTrack); + let playlist_track_id = self.get_id_of_selected(&self.playlist_tracks, Selectable::PlaylistTrack); match self.state.active_tab { ActiveTab::Library => { @@ -1657,13 +1658,13 @@ impl App { } KeyCode::F(2) | KeyCode::Char('2') => { self.state.active_tab = ActiveTab::Albums; - if self.tracks_playlist.is_empty() { + if self.playlist_tracks.is_empty() { self.state.active_section = ActiveSection::List; } } KeyCode::F(3) | KeyCode::Char('3') => { self.state.active_tab = ActiveTab::Playlists; - if self.tracks_playlist.is_empty() { + if self.playlist_tracks.is_empty() { self.state.active_section = ActiveSection::List; } } @@ -1689,13 +1690,13 @@ impl App { } KeyCode::F(2) => { self.state.active_tab = ActiveTab::Albums; - if self.tracks_playlist.is_empty() { + if self.playlist_tracks.is_empty() { self.state.active_section = ActiveSection::List; } } KeyCode::F(3) => { self.state.active_tab = ActiveTab::Playlists; - if self.tracks_playlist.is_empty() { + if self.playlist_tracks.is_empty() { self.state.active_section = ActiveSection::List; } } diff --git a/src/playlists.rs b/src/playlists.rs index 5cc31d3..9dba484 100644 --- a/src/playlists.rs +++ b/src/playlists.rs @@ -255,12 +255,12 @@ impl App { .add_modifier(Modifier::BOLD), }; - let tracks_playlist = search_results(&self.tracks_playlist, &self.state.playlist_tracks_search_term, true) + let playlist_tracks = search_results(&self.playlist_tracks, &self.state.playlist_tracks_search_term, true) .iter() - .map(|id| self.tracks_playlist.iter().find(|t| t.id == *id).unwrap()) + .map(|id| self.playlist_tracks.iter().find(|t| t.id == *id).unwrap()) .collect::>(); - let items = tracks_playlist + let items = playlist_tracks .iter() .enumerate() .map(|(index, track)| { @@ -371,7 +371,7 @@ impl App { Constraint::Length(10), ]; - if self.tracks_playlist.is_empty() { + if self.playlist_tracks.is_empty() { let message_paragraph = Paragraph::new(if self.state.current_playlist.id.is_empty() { "jellyfin-tui".to_string() } else { @@ -401,7 +401,7 @@ impl App { .block(if self.state.playlist_tracks_search_term.is_empty() && !self.state.current_playlist.name.is_empty() { track_block .title(self.state.current_playlist.name.to_string()) - .title_top(Line::from(format!("({} tracks - {})", self.tracks_playlist.len(), duration)).right_aligned()) + .title_top(Line::from(format!("({} tracks - {})", self.playlist_tracks.len(), duration)).right_aligned()) .title_bottom(track_instructions.alignment(Alignment::Center)) } else { track_block diff --git a/src/popup.rs b/src/popup.rs index b2cf5c5..99f714b 100644 --- a/src/popup.rs +++ b/src/popup.rs @@ -1172,10 +1172,10 @@ impl crate::tui::App { return None; } }; - let items = search_results(&self.tracks_playlist, &self.state.playlist_tracks_search_term, true); + let items = search_results(&self.playlist_tracks, &self.state.playlist_tracks_search_term, true); let track = match items.get(selected) { Some(track) => { - let track = self.tracks_playlist.iter().find(|t| t.id == *track)?; + let track = self.playlist_tracks.iter().find(|t| t.id == *track)?; track.clone() } None => { @@ -1282,7 +1282,7 @@ impl crate::tui::App { Action::Yes => { if let Some(client) = self.client.as_ref() { if let Ok(_) = client.remove_from_playlist(&track_id, &playlist_id).await { - self.tracks_playlist + self.playlist_tracks .retain(|t| t.playlist_item_id != track_id); self.popup.current_menu = Some(PopupMenu::GenericMessage { title: format!("{} removed", track_name), @@ -1714,11 +1714,11 @@ impl crate::tui::App { } ActiveSection::Tracks => { let id = - self.get_id_of_selected(&self.tracks_playlist, Selectable::PlaylistTrack); + self.get_id_of_selected(&self.playlist_tracks, Selectable::PlaylistTrack); if self.popup.current_menu.is_none() { self.popup.current_menu = Some(PopupMenu::PlaylistTracksRoot { track_name: self - .tracks_playlist + .playlist_tracks .iter() .find(|t| t.id == id)? .name diff --git a/src/tui.rs b/src/tui.rs index e7fe4bd..b3a3bb6 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -131,7 +131,7 @@ pub struct App { pub original_playlists: Vec, // playlists pub tracks: Vec, // current artist's tracks - pub tracks_playlist: Vec, // current playlist tracks + pub playlist_tracks: Vec, // current playlist tracks pub lyrics: Option<(String, Vec, bool)>, // ID, lyrics, time_synced pub previous_song_parent_id: String, pub active_song_id: String, @@ -227,7 +227,7 @@ impl Default for App { original_playlists: vec![], tracks: vec![], - tracks_playlist: vec![], + playlist_tracks: vec![], lyrics: None, previous_song_parent_id: String::from(""), metadata: None, @@ -805,9 +805,9 @@ impl App { if let Some(client) = self.client.as_ref() { if let Ok(playlist) = client.playlist(id).await { self.state.active_section = ActiveSection::Tracks; - self.tracks_playlist = playlist.items; + self.playlist_tracks = playlist.items; self.state.playlist_tracks_scroll_state = ScrollbarState::new( - std::cmp::max(0, self.tracks_playlist.len() as i32 - 1) as usize + std::cmp::max(0, self.playlist_tracks.len() as i32 - 1) as usize ); self.state.current_playlist = self.playlists.iter() .find(|a| a.id == *id) From 19d0248d48dc164d13e33f37634d9bb65589ebad Mon Sep 17 00:00:00 2001 From: dhonus Date: Thu, 20 Feb 2025 20:31:56 +0100 Subject: [PATCH 17/26] fix: color tweaks --- src/keyboard.rs | 7 +++++-- src/library.rs | 8 ++++---- src/playlists.rs | 4 ++-- src/search.rs | 6 +++--- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 4fb61be..7fbf68d 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -1658,7 +1658,7 @@ impl App { } KeyCode::F(2) | KeyCode::Char('2') => { self.state.active_tab = ActiveTab::Albums; - if self.playlist_tracks.is_empty() { + if self.album_tracks.is_empty() { self.state.active_section = ActiveSection::List; } } @@ -1886,9 +1886,12 @@ impl App { self.state.active_tab = ActiveTab::Library; } KeyCode::Char('2') => { - self.state.active_tab = ActiveTab::Playlists; + self.state.active_tab = ActiveTab::Albums; } KeyCode::Char('3') => { + self.state.active_tab = ActiveTab::Playlists; + } + KeyCode::Char('4') => { self.searching = true; } KeyCode::Down | KeyCode::Char('j') => match self.state.search_section { diff --git a/src/library.rs b/src/library.rs index 11d685a..edbefe6 100644 --- a/src/library.rs +++ b/src/library.rs @@ -214,7 +214,7 @@ impl App { .fg(Color::Indexed(232)), _ => Style::default() .add_modifier(Modifier::BOLD) - .bg(Color::DarkGray) + .bg(Color::Indexed(236)) .fg(Color::White) }; @@ -350,7 +350,7 @@ impl App { .fg(Color::Indexed(232)), _ => Style::default() .add_modifier(Modifier::BOLD) - .bg(Color::DarkGray) + .bg(Color::Indexed(236)) .fg(Color::White) }; @@ -641,8 +641,8 @@ impl App { .bg(Color::White), _ => Style::default() .add_modifier(Modifier::BOLD) - .fg(Color::White) - .bg(Color::DarkGray), + .bg(Color::Indexed(236)) + .fg(Color::White), }; // let selected_track = self.get_id_of_selected(&self.tracks, Selectable::Track); diff --git a/src/playlists.rs b/src/playlists.rs index 9dba484..04f850f 100644 --- a/src/playlists.rs +++ b/src/playlists.rs @@ -139,7 +139,7 @@ impl App { .add_modifier(Modifier::BOLD), _ => Style::default() .add_modifier(Modifier::BOLD) - .bg(Color::DarkGray) + .bg(Color::Indexed(236)) .fg(Color::White) .add_modifier(Modifier::BOLD), }; @@ -250,7 +250,7 @@ impl App { .fg(Color::Indexed(232)) .add_modifier(Modifier::BOLD), _ => Style::default() - .bg(Color::DarkGray) + .bg(Color::Indexed(236)) .fg(Color::White) .add_modifier(Modifier::BOLD), }; diff --git a/src/search.rs b/src/search.rs index e585bd1..80765aa 100644 --- a/src/search.rs +++ b/src/search.rs @@ -175,7 +175,7 @@ impl App { .highlight_style( Style::default() .add_modifier(Modifier::BOLD) - .bg(Color::DarkGray) + .bg(Color::Indexed(236)) .fg(Color::White) ) .scroll_padding(10) @@ -203,7 +203,7 @@ impl App { .highlight_style( Style::default() .add_modifier(Modifier::BOLD) - .bg(Color::DarkGray) + .bg(Color::Indexed(236)) .fg(Color::White) ) .repeat_highlight_symbol(true), @@ -231,7 +231,7 @@ impl App { .highlight_style( Style::default() .add_modifier(Modifier::BOLD) - .bg(Color::DarkGray) + .bg(Color::Indexed(236)) .fg(Color::White) ) .repeat_highlight_symbol(true), From 02af31c53317d24a7e613ea8305a4e4649257770 Mon Sep 17 00:00:00 2001 From: dhonus Date: Fri, 21 Feb 2025 15:59:24 +0100 Subject: [PATCH 18/26] fix: some linter warnings --- src/client.rs | 12 ++++++------ src/keyboard.rs | 22 ++++++++++------------ src/library.rs | 40 ++++++++++++++++++---------------------- src/popup.rs | 42 +++++++++++++++++++++--------------------- src/queue.rs | 12 ++++++------ 5 files changed, 61 insertions(+), 67 deletions(-) diff --git a/src/client.rs b/src/client.rs index 42165ee..f6afd6b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -65,7 +65,7 @@ impl Client { let mut username = String::new(); let mut password = String::new(); - println!(""); + println!(); println!(" - <3 Thank you for trying out jellyfin-tui! It is still beta-quality software, so please report any issues you find or ideas you have here:"); println!(" - https://github.com/dhonus/jellyfin-tui/issues"); println!("\n ! The configuration file does not exist. Please fill in the following details:\n"); @@ -851,14 +851,14 @@ impl Client { pub fn song_url_sync(&self, song_id: String) -> String { let mut url = format!("{}/Audio/{}/universal", self.base_url, song_id); url += &format!("?UserId={}&api_key={}&StartTimeTicks=0&EnableRedirection=true&EnableRemoteMedia=false", self.user_id, self.access_token); - url += &format!("&container=opus,webm|opus,mp3,aac,m4a|aac,m4b|aac,flac,webma,webm|webma,wav,ogg"); + url += "&container=opus,webm|opus,mp3,aac,m4a|aac,m4b|aac,flac,webma,webm|webma,wav,ogg"; if self.transcoding.enabled { url += &format!("&transcodingContainer={}&transcodingProtocol=http&audioCodec={}", self.transcoding.container, self.transcoding.container); if self.transcoding.bitrate > 0 { url += &format!("&maxStreamingBitrate={}", self.transcoding.bitrate * 1000); } else { - url += &format!("&MaxStreamingBitrate=320000"); + url += "&MaxStreamingBitrate=320000"; } } url @@ -866,7 +866,7 @@ impl Client { /// Sends an update to favorite of a track. POST is true, DELETE is false /// - pub async fn set_favorite(&self, item_id: &String, favorite: bool) -> Result<(), reqwest::Error> { + pub async fn set_favorite(&self, item_id: &str, favorite: bool) -> Result<(), reqwest::Error> { let id = item_id.replace("_album_", ""); let url = format!("{}/Users/{}/FavoriteItems/{}", self.base_url, self.user_id, id); let response = if favorite { @@ -1042,7 +1042,7 @@ impl Client { /// Adds a track to a playlist /// /// https://jelly.danielhonus.com/Playlists/60efcb22e97a01f2b2a59f4d7b4a48ee/Items?ids=818923889708a83351a8a381af78310b&userId=aca06460269248d5bbe12e5ae7ceac8b - pub async fn add_to_playlist(&self, track_id: &String, playlist_id: &String) -> Result { + pub async fn add_to_playlist(&self, track_id: &str, playlist_id: &String) -> Result { let url = format!("{}/Playlists/{}/Items", self.base_url, playlist_id); self.http_client @@ -1051,7 +1051,7 @@ impl Client { .header("x-emby-authorization", "MediaBrowser Client=\"jellyfin-tui\", Device=\"jellyfin-tui\", DeviceId=\"None\", Version=\"10.4.3\"") .header("Content-Type", "application/json") .query(&[ - ("ids", track_id.as_str()), + ("ids", track_id), ("userId", self.user_id.as_str()) ]) .send() diff --git a/src/keyboard.rs b/src/keyboard.rs index 7fbf68d..da23202 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -121,18 +121,16 @@ impl App { Selectable::PlaylistTrack => self.playlist_tracks.iter().map(|t| t.id.clone()).collect::>(), }; - if id.is_empty() { - if !ids.is_empty() { - match selectable { - Selectable::Artist => self.artist_select_by_index(0), - Selectable::Album => self.album_select_by_index(0), - Selectable::AlbumTrack => self.album_track_select_by_index(0), - Selectable::Track => self.track_select_by_index(0), - Selectable::Playlist => self.playlist_select_by_index(0), - Selectable::PlaylistTrack => self.playlist_track_select_by_index(0), - } - return; + if id.is_empty() && !ids.is_empty() { + match selectable { + Selectable::Artist => self.artist_select_by_index(0), + Selectable::Album => self.album_select_by_index(0), + Selectable::AlbumTrack => self.album_track_select_by_index(0), + Selectable::Track => self.track_select_by_index(0), + Selectable::Playlist => self.playlist_select_by_index(0), + Selectable::PlaylistTrack => self.playlist_track_select_by_index(0), } + return; } if !search_term.is_empty() { @@ -168,7 +166,7 @@ impl App { } } - pub fn get_id_of_selected(&self, items: &Vec, selectable: Selectable) -> String { + pub fn get_id_of_selected(&self, items: &[T], selectable: Selectable) -> String { let search_term = match selectable { Selectable::Artist => &self.state.artists_search_term, Selectable::Album => &self.state.albums_search_term, diff --git a/src/library.rs b/src/library.rs index edbefe6..26c1aba 100644 --- a/src/library.rs +++ b/src/library.rs @@ -318,16 +318,14 @@ impl App { &mut self.state.artists_scroll_state, ); - if self.locally_searching { - if self.state.active_section == ActiveSection::List { - frame.render_widget( - Block::default() + if self.locally_searching && self.state.active_section == ActiveSection::List { + frame.render_widget( + Block::default() .borders(Borders::ALL) - .title(format!("Searching: {}", self.state.artists_search_term)) - .border_style(self.primary_color), - left[0], - ); - } + .title(format!("Searching: {}", self.state.artists_search_term)) + .border_style(self.primary_color), + left[0], + ); } } @@ -378,7 +376,7 @@ impl App { let mut item = Text::default(); let mut last_end = 0; let all_subsequences = helpers::find_all_subsequences( - &&self.state.albums_search_term.to_lowercase(), + &self.state.albums_search_term.to_lowercase(), &album.name.to_lowercase(), ); for (start, end) in all_subsequences { @@ -451,16 +449,14 @@ impl App { &mut self.state.albums_scroll_state, ); - if self.locally_searching { - if self.state.active_section == ActiveSection::List { - frame.render_widget( - Block::default() - .borders(Borders::ALL) - .title(format!("Searching: {}", self.state.albums_search_term)) - .border_style(self.primary_color), - left[0], - ); - } + if self.locally_searching && self.state.active_section == ActiveSection::List { + frame.render_widget( + Block::default() + .borders(Borders::ALL) + .title(format!("Searching: {}", self.state.albums_search_term)) + .border_style(self.primary_color), + left[0], + ); } } @@ -913,7 +909,7 @@ impl App { }; let all_subsequences = helpers::find_all_subsequences( - &&self.state.album_tracks_search_term.to_lowercase(), + &self.state.album_tracks_search_term.to_lowercase(), &track.name.to_lowercase(), ); @@ -1016,7 +1012,7 @@ impl App { let table = Table::new(items, widths) .block(if self.state.album_tracks_search_term.is_empty() && !self.state.current_album.name.is_empty() { track_block - .title(format!("{}", self.state.current_album.name)) + .title(self.state.current_album.name.to_string()) .title_top(Line::from(format!("({} tracks)", self.album_tracks.iter().filter(|t| !t.id.starts_with("_album_")).count())).right_aligned()) .title_bottom(track_instructions.alignment(Alignment::Center)) } else { diff --git a/src/popup.rs b/src/popup.rs index 99f714b..35bc4b9 100644 --- a/src/popup.rs +++ b/src/popup.rs @@ -172,13 +172,13 @@ struct PopupAction { impl PopupMenu { fn title(&self) -> String { match self { - PopupMenu::GenericMessage { title, .. } => format!("{}", title), + PopupMenu::GenericMessage { title, .. } => title.to_string(), // ---------- Global commands ---------- // PopupMenu::GlobalRoot { .. }=> "Global Commands".to_string(), PopupMenu::GlobalRunScheduledTask { .. } => "Run a scheduled task".to_string(), PopupMenu::GlobalShuffle { .. } => "Global Shuffle".to_string(), // ---------- Playlists ---------- // - PopupMenu::PlaylistRoot { playlist_name, .. } => format!("{}", playlist_name), + PopupMenu::PlaylistRoot { playlist_name, .. } => playlist_name.to_string(), PopupMenu::PlaylistSetName { .. } => "Type to change name".to_string(), PopupMenu::PlaylistConfirmRename { .. } => "Confirm Rename".to_string(), PopupMenu::PlaylistConfirmDelete { .. } => "Confirm Delete".to_string(), @@ -186,14 +186,14 @@ impl PopupMenu { PopupMenu::PlaylistsChangeSort { } => "Change sort order".to_string(), PopupMenu::PlaylistsChangeFilter { } => "Change filter".to_string(), // ---------- Tracks ---------- // - PopupMenu::TrackRoot { track_name, .. } => format!("{}", track_name), - PopupMenu::TrackAddToPlaylist { track_name, .. } => format!("{}", track_name), + PopupMenu::TrackRoot { track_name, .. } => track_name.to_string(), + PopupMenu::TrackAddToPlaylist { track_name, .. } => track_name.to_string(), // ---------- Playlist tracks ---------- // - PopupMenu::PlaylistTracksRoot { track_name, .. } => format!("{}", track_name), - PopupMenu::PlaylistTrackAddToPlaylist { track_name, .. } => format!("{}", track_name), - PopupMenu::PlaylistTracksRemove { track_name, .. } => format!("{}", track_name), + PopupMenu::PlaylistTracksRoot { track_name, .. } => track_name.to_string(), + PopupMenu::PlaylistTrackAddToPlaylist { track_name, .. } => track_name.to_string(), + PopupMenu::PlaylistTracksRemove { track_name, .. } => track_name.to_string(), // ---------- Artists ---------- // - PopupMenu::ArtistRoot { artist, .. } => format!("{}", artist.name), + PopupMenu::ArtistRoot { artist, .. } => artist.name.to_string(), PopupMenu::ArtistJumpToCurrent { artists, .. } => { format!("Which of these {} to jump to?", artists.len()) } @@ -204,7 +204,7 @@ impl PopupMenu { PopupMenu::AlbumsChangeFilter { } => "Change filter".to_string(), PopupMenu::AlbumsChangeSort { } => "Change sort".to_string(), // ---------- Album tracks ---------- // - PopupMenu::AlbumTrackRoot { track_name, .. } => format!("{}", track_name), + PopupMenu::AlbumTrackRoot { track_name, .. } => track_name.to_string(), } } @@ -213,7 +213,7 @@ impl PopupMenu { match self { PopupMenu::GenericMessage { message, .. } => vec![ PopupAction { - label: format!("{}", message), + label: message.to_string(), action: Action::Ok, style: Style::default(), }, @@ -387,7 +387,7 @@ impl PopupMenu { PopupMenu::PlaylistCreate { name, public } => vec![ PopupAction { label: if name.is_empty() { - format!("Type in the new playlist name") + "Type in the new playlist name".into() } else { format!("Name: {}", name) }, @@ -539,7 +539,7 @@ impl PopupMenu { let mut actions = vec![]; for artist in artists { actions.push(PopupAction { - label: format!("{}", artist.name), + label: artist.name.to_string(), action: Action::JumpToCurrent, style: Style::default(), }); @@ -776,30 +776,30 @@ impl crate::tui::App { match self.state.active_tab { ActiveTab::Library => match self.state.last_section { ActiveSection::Tracks => { - self.apply_track_action(&action, menu.clone()).await; + self.apply_track_action(action, menu.clone()).await; } ActiveSection::List => { - self.apply_artist_action(&action, menu.clone()); + self.apply_artist_action(action, menu.clone()); } _ => {} }, ActiveTab::Albums => match self.state.last_section { ActiveSection::List => { - self.apply_album_action(&action, menu.clone()).await; + self.apply_album_action(action, menu.clone()).await; } ActiveSection::Tracks => { - self.apply_album_track_action(&action, menu.clone()).await; + self.apply_album_track_action(action, menu.clone()).await; } _ => {} }, ActiveTab::Playlists => match self.state.last_section { ActiveSection::List => { - if let None = self.apply_playlist_action(&action, menu.clone()).await { + if let None = self.apply_playlist_action(action, menu.clone()).await { self.close_popup(); } } ActiveSection::Tracks => { - self.apply_playlist_tracks_action(&action, menu.clone()) + self.apply_playlist_tracks_action(action, menu.clone()) .await; } _ => {} @@ -853,7 +853,7 @@ impl crate::tui::App { if let Ok(_) = self.client.as_ref()?.run_scheduled_task(&task.id).await { self.popup.current_menu = Some(PopupMenu::GenericMessage { title: format!("Task {} executed successfully", task.name), - message: format!("Try reloading your library to see changes."), + message: "Try reloading your library to see changes.".to_string(), }); } else { self.popup.current_menu = Some(PopupMenu::GenericMessage { @@ -932,7 +932,7 @@ impl crate::tui::App { Action::JumpToCurrent => { let current_track = self.state.queue.get(self.state.current_playback_state.current_index as usize)?; let artist = self.artists.iter().find( - |a| current_track.artist_items.get(0).is_some_and(|item| a.id == item.id) + |a| current_track.artist_items.first().is_some_and(|item| a.id == item.id) )?; let artist_id = artist.id.clone(); let current_track_id = current_track.id.clone(); @@ -1765,7 +1765,7 @@ impl crate::tui::App { } else { style::Color::White }) - .fg(style::Color::Black) + .fg(style::Color::Indexed(232)) .bold(), ) .style(Style::default().fg(style::Color::White)) diff --git a/src/queue.rs b/src/queue.rs index 4ee2da7..e7da48c 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -505,7 +505,7 @@ impl App { desired_order.shuffle(&mut rand::rng()); // find in current and move it needed - for i in 0..desired_order.len() { + for (i, _) in desired_order.iter().enumerate() { let target_song_id = &desired_order[i].id; if let Some(j) = local_current .iter() @@ -527,7 +527,7 @@ impl App { } for (i, song) in local_current.into_iter().enumerate() { - self.state.queue[(shuffle_after as usize) + i] = song; + self.state.queue[shuffle_after + i] = song; } } } @@ -543,18 +543,18 @@ impl App { } let mut shuffle_after = current_index - + self.state.queue.iter().skip(current_index as usize).filter(|s| s.is_in_queue).count() + 1; + + self.state.queue.iter().skip(current_index).filter(|s| s.is_in_queue).count() + 1; if self.state.queue[current_index].is_in_queue { shuffle_after -= 1; } - let mut local_current: Vec = self.state.queue[shuffle_after as usize..].to_vec(); + let mut local_current: Vec = self.state.queue[shuffle_after..].to_vec(); let mut desired_order = local_current.clone(); desired_order.sort_by_key(|s| s.original_index); - for i in 0..desired_order.len() { + for (i, _) in desired_order.iter().enumerate() { let target_song_id = &desired_order[i].id; if let Some(j) = local_current @@ -577,7 +577,7 @@ impl App { } for (i, song) in local_current.into_iter().enumerate() { - self.state.queue[(shuffle_after as usize) + i] = song; + self.state.queue[shuffle_after + i] = song; } } } From 4de6dad09247e0a72825c3285c86e86a61b35179 Mon Sep 17 00:00:00 2001 From: dhonus Date: Fri, 21 Feb 2025 16:31:15 +0100 Subject: [PATCH 19/26] fix: pesky unwraps() --- src/keyboard.rs | 98 +++++++++++++++++++++++++------------------------ 1 file changed, 51 insertions(+), 47 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index da23202..8b3787e 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -1059,14 +1059,16 @@ impl App { artists = self.artists.iter().collect::>(); } let selected = self.state.selected_artist.selected().unwrap_or(0); - let current_artist = artists[selected].name.chars().next().unwrap().to_string().to_lowercase(); - let next_artist = artists - .iter().skip(selected) - .find(|a| a.name.chars().next().unwrap().to_string().to_lowercase() != current_artist); - - if let Some(next_artist) = next_artist { - let index = artists.iter().position(|a| a.id == next_artist.id).unwrap_or(0); - self.artist_select_by_index(index); + if let Some(current_artist) = artists[selected].name.chars().next() { + let current_artist = current_artist.to_ascii_lowercase(); + let next_artist = artists + .iter().skip(selected) + .find(|a| a.name.chars().next().map(|c| c.to_ascii_lowercase()) != Some(current_artist)); + + if let Some(next_artist) = next_artist { + let index = artists.iter().position(|a| a.id == next_artist.id).unwrap_or(0); + self.artist_select_by_index(index); + } } } // this will go to the first song of the next album @@ -1098,12 +1100,7 @@ impl App { albums = self.albums.iter().collect::>(); } if let Some(selected) = self.state.selected_album.selected() { - let current_album = albums[selected].name.chars().next().unwrap().to_string().to_lowercase(); - let next_album = albums - .iter().skip(selected) - .find(|a| a.name.chars().next().unwrap().to_string().to_lowercase() != current_album); - - if let Some(next_album) = next_album { + if let Some(next_album) = albums.iter().skip(selected).find(|a| a.name.chars().next() != albums[selected].name.chars().next()) { let index = albums.iter().position(|a| a.id == next_album.id).unwrap_or(0); self.album_select_by_index(index); } @@ -1121,14 +1118,16 @@ impl App { playlists = self.playlists.iter().collect::>(); } if let Some(selected) = self.state.selected_playlist.selected() { - let current_playlist = playlists[selected].name.chars().next().unwrap().to_string().to_lowercase(); - let next_playlist = playlists - .iter().skip(selected) - .find(|a| a.name.chars().next().unwrap().to_string().to_lowercase() != current_playlist); - - if let Some(next_playlist) = next_playlist { - let index = playlists.iter().position(|a| a.id == next_playlist.id).unwrap_or(0); - self.playlist_select_by_index(index); + if let Some(current_playlist) = playlists[selected].name.chars().next() { + let current_playlist = current_playlist.to_ascii_lowercase(); + let next_playlist = playlists + .iter().skip(selected) + .find(|a| a.name.chars().next().map(|c| c.to_ascii_lowercase()) != Some(current_playlist)); + + if let Some(next_playlist) = next_playlist { + let index = playlists.iter().position(|a| a.id == next_playlist.id).unwrap_or(0); + self.playlist_select_by_index(index); + } } } } @@ -1149,14 +1148,16 @@ impl App { artists = self.artists.iter().collect::>(); } let selected = self.state.selected_artist.selected().unwrap_or(0); - let current_artist = artists[selected].name.chars().next().unwrap().to_string().to_lowercase(); - let prev_artist = artists - .iter().rev().skip(artists.len() - selected) - .find(|a| a.name.chars().next().unwrap().to_string().to_lowercase() != current_artist); - - if let Some(prev_artist) = prev_artist { - let index = artists.iter().position(|a| a.id == prev_artist.id).unwrap_or(0); - self.artist_select_by_index(index); + if let Some(current_artist) = artists[selected].name.chars().next() { + let current_artist = current_artist.to_ascii_lowercase(); + let prev_artist = artists + .iter().rev().skip(artists.len() - selected) + .find(|a| a.name.chars().next().map(|c| c.to_ascii_lowercase()) != Some(current_artist)); + + if let Some(prev_artist) = prev_artist { + let index = artists.iter().position(|a| a.id == prev_artist.id).unwrap_or(0); + self.artist_select_by_index(index); + } } } // this will go to the first song of the previous album @@ -1194,15 +1195,16 @@ impl App { albums = self.albums.iter().collect::>(); } if let Some(selected) = self.state.selected_album.selected() { - let current_album = albums[selected].name.chars().next().unwrap().to_string().to_lowercase(); - let prev_album = albums - .iter().rev() - .skip(albums.len() - selected) - .find(|a| a.name.chars().next().unwrap().to_string().to_lowercase() != current_album); - - if let Some(prev_album) = prev_album { - let index = albums.iter().position(|a| a.id == prev_album.id).unwrap_or(0); - self.album_select_by_index(index); + if let Some(current_album) = albums[selected].name.chars().next() { + let current_album = current_album.to_ascii_lowercase(); + let prev_album = albums + .iter().rev().skip(albums.len() - selected) + .find(|a| a.name.chars().next().map(|c| c.to_ascii_lowercase()) != Some(current_album)); + + if let Some(prev_album) = prev_album { + let index = albums.iter().position(|a| a.id == prev_album.id).unwrap_or(0); + self.album_select_by_index(index); + } } } } @@ -1218,14 +1220,16 @@ impl App { playlists = self.playlists.iter().collect::>(); } if let Some(selected) = self.state.selected_playlist.selected() { - let current_playlist = playlists[selected].name.chars().next().unwrap().to_string().to_lowercase(); - let prev_playlist = playlists - .iter().rev().skip(playlists.len() - selected) - .find(|a| a.name.chars().next().unwrap().to_string().to_lowercase() != current_playlist); - - if let Some(prev_playlist) = prev_playlist { - let index = playlists.iter().position(|a| a.id == prev_playlist.id).unwrap_or(0); - self.playlist_select_by_index(index); + if let Some(current_playlist) = playlists[selected].name.chars().next() { + let current_playlist = current_playlist.to_ascii_lowercase(); + let prev_playlist = playlists + .iter().rev().skip(playlists.len() - selected) + .find(|a| a.name.chars().next().map(|c| c.to_ascii_lowercase()) != Some(current_playlist)); + + if let Some(prev_playlist) = prev_playlist { + let index = playlists.iter().position(|a| a.id == prev_playlist.id).unwrap_or(0); + self.playlist_select_by_index(index); + } } } } From f6bafc60d3d104ce19e32b36d714ee4c3f8136ee Mon Sep 17 00:00:00 2001 From: dhonus Date: Fri, 21 Feb 2025 17:15:23 +0100 Subject: [PATCH 20/26] feat: show duration of all lists --- src/client.rs | 4 +++- src/library.rs | 22 ++++++++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/client.rs b/src/client.rs index f6afd6b..49907bc 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1209,7 +1209,7 @@ pub struct Artist { #[serde(rename = "Id", default)] pub id: String, #[serde(rename = "RunTimeTicks", default)] - run_time_ticks: u64, + pub run_time_ticks: u64, #[serde(rename = "Type", default)] type_: String, #[serde(rename = "UserData", default)] @@ -1481,6 +1481,8 @@ pub struct Album { pub date_created: String, #[serde(rename = "ParentId", default)] pub parent_id: String, + #[serde(rename = "RunTimeTicks", default)] + pub run_time_ticks: u64, } impl Searchable for Album { diff --git a/src/library.rs b/src/library.rs index 26c1aba..58384ad 100644 --- a/src/library.rs +++ b/src/library.rs @@ -857,11 +857,20 @@ impl App { } let items_len = items.len(); + let totaltime = self.tracks.iter().filter(|t| !t.id.starts_with("_album_")).map(|t| t.run_time_ticks / 10_000_000).sum::(); + let seconds = totaltime % 60; + let minutes = (totaltime / 60) % 60; + let hours = totaltime / 60 / 60; + let hours_optional_text = match hours { + 0 => String::from(""), + _ => format!("{}:", hours), + }; + let duration = format!("{}{:02}:{:02}", hours_optional_text, minutes, seconds); let table = Table::new(items, widths) .block(if self.state.tracks_search_term.is_empty() && !self.state.current_artist.name.is_empty() { track_block .title(format!("{}", self.state.current_artist.name)) - .title_top(Line::from(format!("({} tracks)", self.tracks.iter().filter(|t| !t.id.starts_with("_album_")).count())).right_aligned()) + .title_top(Line::from(format!("({} tracks - {})", self.tracks.iter().filter(|t| !t.id.starts_with("_album_")).count(), duration)).right_aligned()) .title_bottom(track_instructions.alignment(Alignment::Center)) } else { track_block @@ -1009,11 +1018,20 @@ impl App { } let items_len = items.len(); + let totaltime = self.album_tracks.iter().map(|t| t.run_time_ticks).sum::() / 10_000_000; + let seconds = totaltime % 60; + let minutes = (totaltime / 60) % 60; + let hours = totaltime / 60 / 60; + let hours_optional_text = match hours { + 0 => String::from(""), + _ => format!("{}:", hours), + }; + let duration = format!("{}{:02}:{:02}", hours_optional_text, minutes, seconds); let table = Table::new(items, widths) .block(if self.state.album_tracks_search_term.is_empty() && !self.state.current_album.name.is_empty() { track_block .title(self.state.current_album.name.to_string()) - .title_top(Line::from(format!("({} tracks)", self.album_tracks.iter().filter(|t| !t.id.starts_with("_album_")).count())).right_aligned()) + .title_top(Line::from(format!("({} tracks - {})", self.album_tracks.iter().filter(|t| !t.id.starts_with("_album_")).count(), duration)).right_aligned()) .title_bottom(track_instructions.alignment(Alignment::Center)) } else { track_block From 28584049c5350386e9ab50e0f195859bca1b70c7 Mon Sep 17 00:00:00 2001 From: dhonus Date: Fri, 21 Feb 2025 17:23:58 +0100 Subject: [PATCH 21/26] style: reformated the whole codebase --- src/client.rs | 339 +++++++---- src/config.rs | 2 +- src/helpers.rs | 64 +- src/keyboard.rs | 1479 ++++++++++++++++++++++++++++------------------ src/library.rs | 611 +++++++++++-------- src/main.rs | 31 +- src/mpris.rs | 72 ++- src/playlists.rs | 232 +++++--- src/popup.rs | 347 +++++++---- src/queue.rs | 275 ++++++--- src/search.rs | 109 ++-- src/tui.rs | 393 +++++++----- 12 files changed, 2493 insertions(+), 1461 deletions(-) diff --git a/src/client.rs b/src/client.rs index 49907bc..7c77f3a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -5,19 +5,19 @@ HTTP client for Jellyfin API -------------------------- */ use crate::keyboard::Searchable; +use chrono::NaiveDate; +use dirs::cache_dir; +use dirs::config_dir; use serde::{Deserialize, Serialize}; use serde_json::Value; -use dirs::config_dir; -use dirs::cache_dir; -use std::io::Write; -use std::path::PathBuf; -use std::io::Cursor; use std::error::Error; -use chrono::NaiveDate; use std::fs::File; -use std::io::Read; use std::fs::OpenOptions; +use std::io::Cursor; +use std::io::Read; +use std::io::Write; use std::os::unix::fs::OpenOptionsExt; +use std::path::PathBuf; #[derive(Debug)] pub struct Client { @@ -49,7 +49,6 @@ impl Client { /// If the configuration file does not exist, it will be created with stdin input /// pub async fn new(quiet: bool) -> Self { - let config_dir = match config_dir() { Some(dir) => dir, None => { @@ -91,14 +90,25 @@ impl Client { } } println!("username: "); - std::io::stdin().read_line(&mut username).expect("[XX] Failed to read username"); + std::io::stdin() + .read_line(&mut username) + .expect("[XX] Failed to read username"); println!("password: "); - std::io::stdin().read_line(&mut password).expect("[XX] Failed to read password"); - - println!("\nHost: '{}' Username: '{}' Password: '{}'", server.trim(), username.trim(), password.trim()); + std::io::stdin() + .read_line(&mut password) + .expect("[XX] Failed to read password"); + + println!( + "\nHost: '{}' Username: '{}' Password: '{}'", + server.trim(), + username.trim(), + password.trim() + ); println!(" ? Is this correct? (Y/n)"); let mut confirm = String::new(); - std::io::stdin().read_line(&mut confirm).expect("[XX] Failed to read confirmation"); + std::io::stdin() + .read_line(&mut confirm) + .expect("[XX] Failed to read confirmation"); // y is default if confirm.contains("n") || confirm.contains("N") { server = "".to_string(); @@ -114,26 +124,39 @@ impl Client { "server": server.trim(), "username": username.trim(), "password": password.trim(), - })).expect(" ! Could not serialize default config"); + })) + .expect(" ! Could not serialize default config"); match std::fs::create_dir_all(config_dir.join("jellyfin-tui")) { Ok(_) => { let mut file = OpenOptions::new() - .write(true).create_new(true).mode(0o600) + .write(true) + .create_new(true) + .mode(0o600) .open(config_file.clone()) .expect(" ! Could not create config file"); file.write_all(default_config.as_bytes()) .expect(" ! Could not write default config"); - println!("\n - Created default config file at: {}", config_file.to_str().expect(" ! Could not convert config path to string")); - }, + println!( + "\n - Created default config file at: {}", + config_file + .to_str() + .expect(" ! Could not convert config path to string") + ); + } Err(_) => { println!(" ! Could not create config directory"); std::process::exit(1); } } } else if !quiet { - println!(" - Found config file at: {}", config_file.to_str().expect(" ! Could not convert config path to string")); + println!( + " - Found config file at: {}", + config_file + .to_str() + .expect(" ! Could not convert config path to string") + ); } let config = crate::config::get_config(); @@ -159,9 +182,7 @@ impl Client { std::process::exit(1); } }; - Credentials { - username, password - } + Credentials { username, password } }; let server = match d["server"].as_str() { @@ -179,7 +200,10 @@ impl Client { let transcoding = Transcoding { enabled: d["transcoding"]["enabled"].as_bool().unwrap_or(false), bitrate: d["transcoding"]["bitrate"].as_u64().unwrap_or(320) as u32, - container: d["transcoding"]["container"].as_str().unwrap_or("mp3").to_string(), + container: d["transcoding"]["container"] + .as_str() + .unwrap_or("mp3") + .to_string(), }; let url: String = String::new() + server + "/Users/authenticatebyname"; @@ -219,7 +243,7 @@ impl Client { user_name: _credentials.username.to_string(), transcoding, } - }, + } Err(e) => { println!(" ! Error authenticating: {}", e); std::process::exit(1); @@ -256,7 +280,7 @@ impl Client { total_record_count: 0, }); artists - }, + } Err(_) => { return Ok(vec![]); } @@ -266,7 +290,7 @@ impl Client { } /// Produces a list of all albums - /// + /// pub async fn albums(&self) -> Result, reqwest::Error> { let url = format!("{}/Users/{}/Items", self.base_url, self.user_id); @@ -289,11 +313,12 @@ impl Client { let albums = match response { Ok(json) => { - let albums: Albums = json.json().await.unwrap_or_else(|_| Albums { - items: vec![], - }); + let albums: Albums = json + .json() + .await + .unwrap_or_else(|_| Albums { items: vec![] }); albums - }, + } Err(_) => { return Ok(vec![]); } @@ -303,7 +328,7 @@ impl Client { } /// Produces a list of songs in an album - /// + /// pub async fn album_tracks(&self, id: &str) -> Result, reqwest::Error> { let url = format!("{}/Users/{}/Items", self.base_url, self.user_id); @@ -327,11 +352,12 @@ impl Client { let mut songs = match response { Ok(json) => { - let songs: Discography = json.json().await.unwrap_or_else(|_| Discography { - items: vec![], - }); + let songs: Discography = json + .json() + .await + .unwrap_or_else(|_| Discography { items: vec![] }); songs.items - }, + } Err(_) => { return Ok(vec![]); } @@ -344,11 +370,14 @@ impl Client { Ok(songs) } - /// Produces a list of songs by an artist sorted by album and index /// - pub async fn discography(&self, id: &str, recently_added: bool) -> Result { + pub async fn discography( + &self, + id: &str, + recently_added: bool, + ) -> Result { let url = format!("{}/Users/{}/Items", self.base_url, self.user_id); let response = self.http_client @@ -372,15 +401,18 @@ impl Client { let discog = match response { Ok(json) => { - let discog: Discography = json.json().await.unwrap_or_else(|_| Discography { - items: vec![], - }); + let discog: Discography = json + .json() + .await + .unwrap_or_else(|_| Discography { items: vec![] }); // group the songs by album let mut albums: Vec = vec![]; - let mut current_album = DiscographyAlbum { songs: vec![], id: "".to_string() }; + let mut current_album = DiscographyAlbum { + songs: vec![], + id: "".to_string(), + }; for mut song in discog.items { - // you wouldn't believe the kind of things i have to deal with song.name.retain(|c| c != '\t' && c != '\n'); song.name = song.name.trim().to_string(); @@ -396,18 +428,32 @@ impl Client { } albums.push(current_album); let album_id = song.album_id.clone(); - current_album = DiscographyAlbum { songs: vec![song], id: album_id }; + current_album = DiscographyAlbum { + songs: vec![song], + id: album_id, + }; } albums.push(current_album); // sort the songs within each album by indexnumber for album in albums.iter_mut() { - album.songs.sort_by(|a, b| a.index_number.cmp(&b.index_number)); + album + .songs + .sort_by(|a, b| a.index_number.cmp(&b.index_number)); } albums.sort_by(|a, b| { // sort albums by release date, if that fails fall back to just the year. Albums with no date will be at the end - match (NaiveDate::parse_from_str(&a.songs[0].premiere_date, "%Y-%m-%dT%H:%M:%S.%fZ"), NaiveDate::parse_from_str(&b.songs[0].premiere_date, "%Y-%m-%dT%H:%M:%S.%fZ")) { + match ( + NaiveDate::parse_from_str( + &a.songs[0].premiere_date, + "%Y-%m-%dT%H:%M:%S.%fZ", + ), + NaiveDate::parse_from_str( + &b.songs[0].premiere_date, + "%Y-%m-%dT%H:%M:%S.%fZ", + ), + ) { (Ok(a_date), Ok(b_date)) => b_date.cmp(&a_date), _ => b.songs[0].production_year.cmp(&a.songs[0].production_year), } @@ -415,7 +461,9 @@ impl Client { // sort over parent_index_number to separate into separate disks for album in albums.iter_mut() { - album.songs.sort_by(|a, b| a.parent_index_number.cmp(&b.parent_index_number)); + album + .songs + .sort_by(|a, b| a.parent_index_number.cmp(&b.parent_index_number)); } // now we flatten the albums back into a list of songs @@ -428,7 +476,10 @@ impl Client { // push a dummy song with the album name let mut album_song = album.songs[0].clone(); // let name be Artist - Album - Year - album_song.name = format!("{} ({})", album.songs[0].album, album.songs[0].production_year); + album_song.name = format!( + "{} ({})", + album.songs[0].album, album.songs[0].production_year + ); album_song.id = format!("_album_{}", album.id); album_song.album_artists = album.songs[0].album_artists.clone(); album_song.album_id = "".to_string(); @@ -479,14 +530,14 @@ impl Client { if let Err(e) = writeln!(file, "{}", id) { _ = e; } - }, + } Err(_) => { return Ok(Discography { items: songs }); } } Discography { items: songs } - }, + } Err(_) => { return Ok(Discography { items: vec![] }); } @@ -526,11 +577,12 @@ impl Client { let albums = match response { Ok(json) => { - let albums: Albums = json.json().await.unwrap_or_else(|_| Albums { - items: vec![], - }); + let albums: Albums = json + .json() + .await + .unwrap_or_else(|_| Albums { items: vec![] }); albums.items - }, + } Err(_) => { return Ok(vec![]); } @@ -541,7 +593,10 @@ impl Client { /// This for the search functionality, it will poll songs based on the search term /// - pub async fn search_tracks(&self, search_term: String) -> Result, reqwest::Error> { + pub async fn search_tracks( + &self, + search_term: String, + ) -> Result, reqwest::Error> { let url = format!("{}/Users/{}/Items", self.base_url, self.user_id); let response = self.http_client @@ -570,13 +625,18 @@ impl Client { let songs = match response { Ok(json) => { - let songs: Discography = json.json().await.unwrap_or_else(|_| Discography { - items: vec![], - }); + let songs: Discography = json + .json() + .await + .unwrap_or_else(|_| Discography { items: vec![] }); // remove those where album_artists is empty - let songs: Vec = songs.items.into_iter().filter(|s| !s.album_artists.is_empty()).collect(); + let songs: Vec = songs + .items + .into_iter() + .filter(|s| !s.album_artists.is_empty()) + .collect(); songs - }, + } Err(_) => { return Ok(vec![]); } @@ -586,8 +646,13 @@ impl Client { } /// Returns a randomized list of tracks based on the preferences - /// - pub async fn random_tracks(&self, tracks_n: usize, only_played: bool, only_unplayed: bool) -> Result, Box> { + /// + pub async fn random_tracks( + &self, + tracks_n: usize, + only_played: bool, + only_unplayed: bool, + ) -> Result, Box> { let url = format!("{}/Users/{}/Items", self.base_url, self.user_id); let response = self.http_client @@ -618,13 +683,18 @@ impl Client { let songs = match response { Ok(json) => { - let songs: Discography = json.json().await.unwrap_or_else(|_| Discography { - items: vec![], - }); + let songs: Discography = json + .json() + .await + .unwrap_or_else(|_| Discography { items: vec![] }); // remove those where album_artists is empty - let songs: Vec = songs.items.into_iter().filter(|s| !s.album_artists.is_empty()).collect(); + let songs: Vec = songs + .items + .into_iter() + .filter(|s| !s.album_artists.is_empty()) + .collect(); songs - }, + } Err(_) => { return Ok(vec![]); } @@ -634,7 +704,7 @@ impl Client { } /// Returns a list of artists with recently added albums - /// + /// pub async fn new_artists(&self) -> Result, Box> { let url = format!("{}/Artists", self.base_url); @@ -662,7 +732,7 @@ impl Client { total_record_count: 0, }); artists - }, + } Err(_) => { return Ok(vec![]); } @@ -700,13 +770,15 @@ impl Client { } if seen_artists_file.exists() { - { // read the file + { + // read the file let mut file = File::open(&seen_artists_file)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; seen_artists = contents.lines().map(|s| s.to_string()).collect(); } - { // wipe it and write the new artists + { + // wipe it and write the new artists let mut file = OpenOptions::new().write(true).open(&seen_artists_file)?; for artist in artists.items.iter() { if seen_artists.contains(&artist.id) { @@ -735,7 +807,7 @@ impl Client { .await; match response { - Ok(_) => {}, + Ok(_) => {} Err(_) => { return Ok(vec![]); } @@ -748,11 +820,12 @@ impl Client { lyrics: vec![], }); lyrics - }, + } Err(_) => { return Ok(vec![]); } - }.lyrics; + } + .lyrics; Ok(lyric) } @@ -818,9 +891,7 @@ impl Client { let cache_dir = match cache_dir() { Some(dir) => dir, - None => { - PathBuf::from("./") - } + None => PathBuf::from("./"), }; if !cache_dir.join("jellyfin-tui").exists() { @@ -837,11 +908,11 @@ impl Client { let mut file = std::fs::File::create( cache_dir - .join("jellyfin-tui") - .join("covers") - .join(album_id.to_string() + "." + extension) + .join("jellyfin-tui") + .join("covers") + .join(album_id.to_string() + "." + extension), )?; - let mut content = Cursor::new(response.bytes().await?); + let mut content = Cursor::new(response.bytes().await?); std::io::copy(&mut content, &mut file)?; Ok(album_id.to_string() + "." + extension) @@ -850,11 +921,17 @@ impl Client { /// Produces URL of a song from its ID pub fn song_url_sync(&self, song_id: String) -> String { let mut url = format!("{}/Audio/{}/universal", self.base_url, song_id); - url += &format!("?UserId={}&api_key={}&StartTimeTicks=0&EnableRedirection=true&EnableRemoteMedia=false", self.user_id, self.access_token); + url += &format!( + "?UserId={}&api_key={}&StartTimeTicks=0&EnableRedirection=true&EnableRemoteMedia=false", + self.user_id, self.access_token + ); url += "&container=opus,webm|opus,mp3,aac,m4a|aac,m4b|aac,flac,webma,webm|webma,wav,ogg"; if self.transcoding.enabled { - url += &format!("&transcodingContainer={}&transcodingProtocol=http&audioCodec={}", self.transcoding.container, self.transcoding.container); + url += &format!( + "&transcodingContainer={}&transcodingProtocol=http&audioCodec={}", + self.transcoding.container, self.transcoding.container + ); if self.transcoding.bitrate > 0 { url += &format!("&maxStreamingBitrate={}", self.transcoding.bitrate * 1000); } else { @@ -868,7 +945,10 @@ impl Client { /// pub async fn set_favorite(&self, item_id: &str, favorite: bool) -> Result<(), reqwest::Error> { let id = item_id.replace("_album_", ""); - let url = format!("{}/Users/{}/FavoriteItems/{}", self.base_url, self.user_id, id); + let url = format!( + "{}/Users/{}/FavoriteItems/{}", + self.base_url, self.user_id, id + ); let response = if favorite { self.http_client .post(url) @@ -888,7 +968,7 @@ impl Client { }; match response { - Ok(_) => {}, + Ok(_) => {} Err(_) => { return Ok(()); } @@ -898,7 +978,7 @@ impl Client { } /// Produces a list of all playlists - /// + /// pub async fn playlists(&self, search_term: String) -> Result, reqwest::Error> { let url = format!("{}/Users/{}/Items", self.base_url, self.user_id); let response = self.http_client @@ -920,22 +1000,22 @@ impl Client { let playlists = match response { Ok(json) => { - let playlists: Playlists = json.json().await.unwrap_or_else(|_| Playlists { - items: vec![], - }); + let playlists: Playlists = json + .json() + .await + .unwrap_or_else(|_| Playlists { items: vec![] }); playlists.items - }, + } Err(_) => { return Ok(vec![]); } }; Ok(playlists) - } /// Gets a single playlist - /// + /// /// https://jelly.danielhonus.com/playlists/636d3c3e246dc4f24718480d4316ef2d/items?Fields=Genres%2C%20DateCreated%2C%20MediaSources%2C%20UserData%2C%20ParentId&IncludeItemTypes=Audio&Limit=300&SortOrder=Ascending&StartIndex=0&UserId=aca06460269248d5bbe12e5ae7ceac8b pub async fn playlist(&self, playlist_id: &String) -> Result { let url = format!("{}/Playlists/{}/Items", self.base_url, playlist_id); @@ -959,11 +1039,12 @@ impl Client { let playlist = match response { Ok(json) => { - let playlist: Discography = json.json().await.unwrap_or_else(|_| Discography { - items: vec![], - }); + let playlist: Discography = json + .json() + .await + .unwrap_or_else(|_| Discography { items: vec![] }); playlist - }, + } Err(_) => { return Ok(Discography { items: vec![] }); } @@ -973,9 +1054,13 @@ impl Client { } /// Creates a new playlist on the server - /// + /// /// We can pass Ids[] to add songs to the playlist as well! Todo - pub async fn create_playlist(&self, playlist_name: &String, is_public: bool) -> Result { + pub async fn create_playlist( + &self, + playlist_name: &String, + is_public: bool, + ) -> Result { let url = format!("{}/Playlists", self.base_url); let response = self.http_client @@ -992,13 +1077,19 @@ impl Client { .send() .await; - let playlist_id = response?.json::().await?["Id"].as_str().unwrap_or("").to_string(); + let playlist_id = response?.json::().await?["Id"] + .as_str() + .unwrap_or("") + .to_string(); Ok(playlist_id) } /// Deletes a playlist on the server - /// - pub async fn delete_playlist(&self, playlist_id: &String) -> Result { + /// + pub async fn delete_playlist( + &self, + playlist_id: &String, + ) -> Result { let url = format!("{}/Items/{}", self.base_url, playlist_id); self.http_client @@ -1011,8 +1102,11 @@ impl Client { } /// Updates a playlist on the server by sending the full definition - /// - pub async fn update_playlist(&self, playlist: &Playlist) -> Result { + /// + pub async fn update_playlist( + &self, + playlist: &Playlist, + ) -> Result { let url = format!("{}/Items/{}", self.base_url, playlist.id); // i do this because my Playlist struct is not the full playlist and i don't want to lose data :) @@ -1040,9 +1134,13 @@ impl Client { } /// Adds a track to a playlist - /// + /// /// https://jelly.danielhonus.com/Playlists/60efcb22e97a01f2b2a59f4d7b4a48ee/Items?ids=818923889708a83351a8a381af78310b&userId=aca06460269248d5bbe12e5ae7ceac8b - pub async fn add_to_playlist(&self, track_id: &str, playlist_id: &String) -> Result { + pub async fn add_to_playlist( + &self, + track_id: &str, + playlist_id: &String, + ) -> Result { let url = format!("{}/Playlists/{}/Items", self.base_url, playlist_id); self.http_client @@ -1060,7 +1158,11 @@ impl Client { /// Removes a track from a playlist /// - pub async fn remove_from_playlist(&self, track_id: &String, playlist_id: &String) -> Result { + pub async fn remove_from_playlist( + &self, + track_id: &String, + playlist_id: &String, + ) -> Result { let url = format!("{}/Playlists/{}/Items", self.base_url, playlist_id); self.http_client @@ -1076,7 +1178,7 @@ impl Client { } /// Returns a list of all server tasks - /// + /// pub async fn scheduled_tasks(&self) -> Result, reqwest::Error> { let url = format!("{}/ScheduledTasks", self.base_url); @@ -1095,7 +1197,7 @@ impl Client { Ok(json) => { let tasks: Vec = json.json().await.unwrap_or_else(|_| vec![]); tasks - }, + } Err(_) => { return Ok(vec![]); } @@ -1105,8 +1207,11 @@ impl Client { } /// Runs a scheduled task - /// - pub async fn run_scheduled_task(&self, task_id: &String) -> Result { + /// + pub async fn run_scheduled_task( + &self, + task_id: &String, + ) -> Result { let url = format!("{}/ScheduledTasks/Running/{}", self.base_url, task_id); self.http_client @@ -1139,7 +1244,11 @@ impl Client { /// Sends a 'stopped' event to the server. Needed for scrobbling /// - pub async fn stopped(&self, song_id: &String, position_ticks: u64) -> Result<(), reqwest::Error> { + pub async fn stopped( + &self, + song_id: &String, + position_ticks: u64, + ) -> Result<(), reqwest::Error> { let url = format!("{}/Sessions/Playing/Stopped", self.base_url); let _response = self.http_client .post(url) @@ -1158,8 +1267,12 @@ impl Client { } /// Reports progress to the server using the info we have from mpv -/// -pub async fn report_progress(base_url: String, access_token: String, pr: ProgressReport) -> Result<(), reqwest::Error> { +/// +pub async fn report_progress( + base_url: String, + access_token: String, + pr: ProgressReport, +) -> Result<(), reqwest::Error> { let url = format!("{}/Sessions/Playing/Progress", base_url); // new http client, this is a pure function so we can create a new one let client = reqwest::Client::new(); @@ -1186,7 +1299,7 @@ pub async fn report_progress(base_url: String, access_token: String, pr: Progres .send() .await; - Ok(()) + Ok(()) } /// TYPES /// @@ -1471,7 +1584,7 @@ pub struct Albums { pub struct Album { #[serde(rename = "Name", default)] pub name: String, - #[serde(rename = "Id",default )] + #[serde(rename = "Id", default)] pub id: String, #[serde(rename = "AlbumArtists", default)] pub album_artists: Vec, diff --git a/src/config.rs b/src/config.rs index b3a626d..cfc8a23 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,5 @@ -use serde_json::Value; use dirs::config_dir; +use serde_json::Value; use ratatui::style::Color; use std::str::FromStr; diff --git a/src/helpers.rs b/src/helpers.rs index 11ddd54..391a47b 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,9 +1,12 @@ -use std::fs::OpenOptions; use dirs::cache_dir; use ratatui::widgets::{ListState, ScrollbarState, TableState}; +use std::fs::OpenOptions; use crate::{ - client::{Album, Artist, Playlist}, keyboard::{ActiveSection, ActiveTab, SearchSection}, popup::PopupMenu, tui::{Filter, MpvPlaybackState, Repeat, Song, Sort} + client::{Album, Artist, Playlist}, + keyboard::{ActiveSection, ActiveTab, SearchSection}, + popup::PopupMenu, + tui::{Filter, MpvPlaybackState, Repeat, Song, Sort}, }; pub fn find_all_subsequences(needle: &str, haystack: &str) -> Vec<(usize, usize)> { @@ -16,9 +19,10 @@ pub fn find_all_subsequences(needle: &str, haystack: &str) -> Vec<(usize, usize) for haystack_char in haystack.chars() { if let Some(needle_char) = current_needle_char { if haystack_char == needle_char { - ranges.push( - (current_byte_index, current_byte_index + haystack_char.len_utf8()) - ); + ranges.push(( + current_byte_index, + current_byte_index + haystack_char.len_utf8(), + )); current_needle_char = needle_chars.next(); } } @@ -33,7 +37,7 @@ pub fn find_all_subsequences(needle: &str, haystack: &str) -> Vec<(usize, usize) } /// This struct should contain all the values that should **PERSIST** when the app is closed and reopened. -/// +/// #[derive(serde::Serialize, serde::Deserialize)] pub struct State { // (URL, Title, Artist, Album) @@ -47,7 +51,7 @@ pub struct State { // Search - active section (Artists, Albums, Tracks) #[serde(default)] pub search_section: SearchSection, // current active section (Artists, Albums, Tracks) - + // active tab (Music, Search) #[serde(default)] pub active_tab: ActiveTab, @@ -99,7 +103,7 @@ pub struct State { pub selected_search_album: ListState, #[serde(default)] pub selected_search_track: ListState, - + #[serde(default)] pub artists_search_term: String, #[serde(default)] @@ -149,7 +153,6 @@ pub struct State { pub current_playback_state: MpvPlaybackState, } - impl State { pub fn new() -> State { State { @@ -182,7 +185,7 @@ impl State { selected_search_artist: ListState::default(), selected_search_album: ListState::default(), selected_search_track: ListState::default(), - + artists_search_term: String::from(""), albums_search_term: String::from(""), album_tracks_search_term: String::from(""), @@ -196,7 +199,7 @@ impl State { repeat: Repeat::All, shuffle: false, - large_art: false, + large_art: false, artist_filter: Filter::default(), artist_sort: Sort::default(), @@ -205,7 +208,11 @@ impl State { playlist_filter: Filter::default(), playlist_sort: Sort::default(), - preffered_global_shuffle: Some(PopupMenu::GlobalShuffle { tracks_n: 100, only_played: true, only_unplayed: false }), + preffered_global_shuffle: Some(PopupMenu::GlobalShuffle { + tracks_n: 100, + only_played: true, + only_unplayed: false, + }), current_playback_state: MpvPlaybackState { percentage: 0.0, @@ -231,14 +238,15 @@ impl State { .write(true) .truncate(true) .append(false) - .open(cache_dir.join("jellyfin-tui").join("state.json")) { - Ok(file) => { - serde_json::to_writer(file, &self)?; - } - Err(_) => { - return Err("Could not open state file".into()); - } + .open(cache_dir.join("jellyfin-tui").join("state.json")) + { + Ok(file) => { + serde_json::to_writer(file, &self)?; } + Err(_) => { + return Err("Could not open state file".into()); + } + } Ok(()) } @@ -251,15 +259,13 @@ impl State { }; match OpenOptions::new() .read(true) - .open(cache_dir.join("jellyfin-tui").join("state.json")) { - Ok(file) => { - let state: State = serde_json::from_reader(file)?; - Ok(state) - } - Err(_) => { - Ok(State::new()) - } + .open(cache_dir.join("jellyfin-tui").join("state.json")) + { + Ok(file) => { + let state: State = serde_json::from_reader(file)?; + Ok(state) } + Err(_) => Ok(State::new()), + } } - -} \ No newline at end of file +} diff --git a/src/keyboard.rs b/src/keyboard.rs index 8b3787e..f73b167 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -5,12 +5,17 @@ Keyboard related functions - Also used for searching -------------------------- */ -use crate::{client::{Album, Artist, Playlist}, helpers::{self, State}, popup::PopupMenu, tui::{App, Repeat}}; - +use crate::{ + client::{Album, Artist, Playlist}, + helpers::{self, State}, + popup::PopupMenu, + tui::{App, Repeat}, +}; + +use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers}; +use serde::{Deserialize, Serialize}; use std::io; use std::time::Duration; -use crossterm::event::{self, Event, KeyEvent, KeyModifiers, KeyCode}; -use serde::{Deserialize, Serialize}; pub trait Searchable { fn id(&self) -> &str; @@ -28,7 +33,11 @@ pub enum Selectable { /// Search results as a vector of IDs. Used in all searchable areas /// -pub fn search_results(items: &[T], search_term: &str, empty_returns_all: bool) -> Vec { +pub fn search_results( + items: &[T], + search_term: &str, + empty_returns_all: bool, +) -> Vec { if empty_returns_all && search_term.is_empty() { return items.iter().map(|item| String::from(item.id())).collect(); } @@ -38,7 +47,7 @@ pub fn search_results(items: &[T], search_term: &str, empty_retur .filter_map(|item| { let name = item.name().to_lowercase(); let matches = helpers::find_all_subsequences(&search_term.to_lowercase(), &name); - + if matches.is_empty() { None } else { @@ -113,12 +122,36 @@ impl App { Selectable::PlaylistTrack => &self.state.playlist_tracks_search_term, }; let ids = match selectable { - Selectable::Artist => self.artists.iter().map(|a| a.id.clone()).collect::>(), - Selectable::Album => self.albums.iter().map(|a| a.id.clone()).collect::>(), - Selectable::AlbumTrack => self.album_tracks.iter().map(|t| t.id.clone()).collect::>(), - Selectable::Track => self.tracks.iter().map(|t| t.id.clone()).collect::>(), - Selectable::Playlist => self.playlists.iter().map(|p| p.id.clone()).collect::>(), - Selectable::PlaylistTrack => self.playlist_tracks.iter().map(|t| t.id.clone()).collect::>(), + Selectable::Artist => self + .artists + .iter() + .map(|a| a.id.clone()) + .collect::>(), + Selectable::Album => self + .albums + .iter() + .map(|a| a.id.clone()) + .collect::>(), + Selectable::AlbumTrack => self + .album_tracks + .iter() + .map(|t| t.id.clone()) + .collect::>(), + Selectable::Track => self + .tracks + .iter() + .map(|t| t.id.clone()) + .collect::>(), + Selectable::Playlist => self + .playlists + .iter() + .map(|p| p.id.clone()) + .collect::>(), + Selectable::PlaylistTrack => self + .playlist_tracks + .iter() + .map(|t| t.id.clone()) + .collect::>(), }; if id.is_empty() && !ids.is_empty() { @@ -140,7 +173,9 @@ impl App { Selectable::AlbumTrack => search_results(&self.album_tracks, search_term, false), Selectable::Track => search_results(&self.tracks, search_term, false), Selectable::Playlist => search_results(&self.playlists, search_term, false), - Selectable::PlaylistTrack => search_results(&self.playlist_tracks, search_term, false), + Selectable::PlaylistTrack => { + search_results(&self.playlist_tracks, search_term, false) + } }; if let Some(index) = items.iter().position(|i| i == id) { match selectable { @@ -204,9 +239,13 @@ impl App { } let index = std::cmp::min(index, items.len() - 1); self.state.selected_artist.select(Some(index)); - self.state.artists_scroll_state = self.state.artists_scroll_state.content_length(items.len()).position(index); + self.state.artists_scroll_state = self + .state + .artists_scroll_state + .content_length(items.len()) + .position(index); } - + pub fn track_select_by_index(&mut self, index: usize) { let items = search_results(&self.tracks, &self.state.tracks_search_term, true); if items.is_empty() { @@ -214,7 +253,11 @@ impl App { } let index = std::cmp::min(index, items.len() - 1); self.state.selected_track.select(Some(index)); - self.state.tracks_scroll_state = self.state.tracks_scroll_state.content_length(items.len()).position(index); + self.state.tracks_scroll_state = self + .state + .tracks_scroll_state + .content_length(items.len()) + .position(index); } pub fn album_select_by_index(&mut self, index: usize) { @@ -224,27 +267,47 @@ impl App { } let index = std::cmp::min(index, items.len() - 1); self.state.selected_album.select(Some(index)); - self.state.albums_scroll_state = self.state.albums_scroll_state.content_length(items.len()).position(index); + self.state.albums_scroll_state = self + .state + .albums_scroll_state + .content_length(items.len()) + .position(index); } pub fn album_track_select_by_index(&mut self, index: usize) { - let items = search_results(&self.album_tracks, &self.state.album_tracks_search_term, true); + let items = search_results( + &self.album_tracks, + &self.state.album_tracks_search_term, + true, + ); if items.is_empty() { return; } let index = std::cmp::min(index, items.len() - 1); self.state.selected_album_track.select(Some(index)); - self.state.album_tracks_scroll_state = self.state.album_tracks_scroll_state.content_length(items.len()).position(index); + self.state.album_tracks_scroll_state = self + .state + .album_tracks_scroll_state + .content_length(items.len()) + .position(index); } - + pub fn playlist_track_select_by_index(&mut self, index: usize) { - let items = search_results(&self.playlist_tracks, &self.state.playlist_tracks_search_term, true); + let items = search_results( + &self.playlist_tracks, + &self.state.playlist_tracks_search_term, + true, + ); if items.is_empty() { return; } let index = std::cmp::min(index, items.len() - 1); self.state.selected_playlist_track.select(Some(index)); - self.state.playlist_tracks_scroll_state = self.state.playlist_tracks_scroll_state.content_length(items.len()).position(index); + self.state.playlist_tracks_scroll_state = self + .state + .playlist_tracks_scroll_state + .content_length(items.len()) + .position(index); } pub fn playlist_select_by_index(&mut self, index: usize) { @@ -254,11 +317,14 @@ impl App { } let index = std::cmp::min(index, items.len() - 1); self.state.selected_playlist.select(Some(index)); - self.state.playlists_scroll_state = self.state.playlists_scroll_state.content_length(items.len()).position(index); + self.state.playlists_scroll_state = self + .state + .playlists_scroll_state + .content_length(items.len()) + .position(index); } - - async fn handle_key_event(&mut self, key_event: KeyEvent) { + async fn handle_key_event(&mut self, key_event: KeyEvent) { self.dirty = true; if key_event.code == KeyCode::Char('c') && key_event.modifiers == KeyModifiers::CONTROL { @@ -278,50 +344,50 @@ impl App { let artist_id = self.get_id_of_selected(&self.artists, Selectable::Artist); let track_id = self.get_id_of_selected(&self.tracks, Selectable::Track); let album_id = self.get_id_of_selected(&self.albums, Selectable::Album); - let album_track_id = self.get_id_of_selected(&self.album_tracks, Selectable::AlbumTrack); - let playlist_id = self.get_id_of_selected(&self.playlists, Selectable::Playlist); - let playlist_track_id = self.get_id_of_selected(&self.playlist_tracks, Selectable::PlaylistTrack); + let album_track_id = + self.get_id_of_selected(&self.album_tracks, Selectable::AlbumTrack); + let playlist_id = + self.get_id_of_selected(&self.playlists, Selectable::Playlist); + let playlist_track_id = + self.get_id_of_selected(&self.playlist_tracks, Selectable::PlaylistTrack); match self.state.active_tab { - ActiveTab::Library => { - match self.state.active_section { - ActiveSection::List => { - self.state.artists_search_term = String::from(""); - self.reposition_cursor(&artist_id, Selectable::Artist); - } - ActiveSection::Tracks => { - self.state.tracks_search_term = String::from(""); - self.reposition_cursor(&track_id, Selectable::Track); - } - _ => {} + ActiveTab::Library => match self.state.active_section { + ActiveSection::List => { + self.state.artists_search_term = String::from(""); + self.reposition_cursor(&artist_id, Selectable::Artist); } - } - ActiveTab::Albums => { - match self.state.active_section { - ActiveSection::List => { - self.state.albums_search_term = String::from(""); - self.reposition_cursor(&album_id, Selectable::Album); - } - ActiveSection::Tracks => { - self.state.album_tracks_search_term = String::from(""); - self.reposition_cursor(&album_track_id, Selectable::AlbumTrack); - } - _ => {} + ActiveSection::Tracks => { + self.state.tracks_search_term = String::from(""); + self.reposition_cursor(&track_id, Selectable::Track); } - } - ActiveTab::Playlists => { - match self.state.active_section { - ActiveSection::List => { - self.state.playlists_search_term = String::from(""); - self.reposition_cursor(&playlist_id, Selectable::Playlist); - } - ActiveSection::Tracks => { - self.state.playlist_tracks_search_term = String::from(""); - self.reposition_cursor(&playlist_track_id, Selectable::PlaylistTrack); - } - _ => {} + _ => {} + }, + ActiveTab::Albums => match self.state.active_section { + ActiveSection::List => { + self.state.albums_search_term = String::from(""); + self.reposition_cursor(&album_id, Selectable::Album); } - } + ActiveSection::Tracks => { + self.state.album_tracks_search_term = String::from(""); + self.reposition_cursor(&album_track_id, Selectable::AlbumTrack); + } + _ => {} + }, + ActiveTab::Playlists => match self.state.active_section { + ActiveSection::List => { + self.state.playlists_search_term = String::from(""); + self.reposition_cursor(&playlist_id, Selectable::Playlist); + } + ActiveSection::Tracks => { + self.state.playlist_tracks_search_term = String::from(""); + self.reposition_cursor( + &playlist_track_id, + Selectable::PlaylistTrack, + ); + } + _ => {} + }, _ => {} } @@ -345,156 +411,148 @@ impl App { self.locally_searching = false; if self.state.active_section == ActiveSection::List { self.state.playlist_tracks_search_term = String::from(""); - } + } } _ => {} } return; } - KeyCode::Backspace => { - match self.state.active_tab { - ActiveTab::Library => { - match self.state.active_section { - ActiveSection::List => { - let selected_id = self.get_id_of_selected(&self.artists, Selectable::Artist); - self.state.artists_search_term.pop(); - self.reposition_cursor(&selected_id, Selectable::Artist); - } - ActiveSection::Tracks => { - let selected_id = self.get_id_of_selected(&self.tracks, Selectable::Track); - self.state.tracks_search_term.pop(); - self.reposition_cursor(&selected_id, Selectable::Track); - } - _ => {} - } + KeyCode::Backspace => match self.state.active_tab { + ActiveTab::Library => match self.state.active_section { + ActiveSection::List => { + let selected_id = + self.get_id_of_selected(&self.artists, Selectable::Artist); + self.state.artists_search_term.pop(); + self.reposition_cursor(&selected_id, Selectable::Artist); } - ActiveTab::Albums => { - match self.state.active_section { - ActiveSection::List => { - let selected_id = self.get_id_of_selected(&self.albums, Selectable::Album); - self.state.albums_search_term.pop(); - self.reposition_cursor(&selected_id, Selectable::Album); - } - ActiveSection::Tracks => { - let selected_id = self.get_id_of_selected(&self.album_tracks, Selectable::AlbumTrack); - self.state.album_tracks_search_term.pop(); - self.reposition_cursor(&selected_id, Selectable::AlbumTrack); - } - _ => {} - } + ActiveSection::Tracks => { + let selected_id = + self.get_id_of_selected(&self.tracks, Selectable::Track); + self.state.tracks_search_term.pop(); + self.reposition_cursor(&selected_id, Selectable::Track); } - ActiveTab::Playlists => { - match self.state.active_section { - ActiveSection::List => { - let selected_id = self.get_id_of_selected(&self.playlists, Selectable::Playlist); - self.state.playlists_search_term.pop(); - self.reposition_cursor(&selected_id, Selectable::Playlist); - } - ActiveSection::Tracks => { - let selected_id = self.get_id_of_selected(&self.playlist_tracks, Selectable::PlaylistTrack); - self.state.playlist_tracks_search_term.pop(); - self.reposition_cursor(&selected_id, Selectable::PlaylistTrack); - } - _ => {} - } + _ => {} + }, + ActiveTab::Albums => match self.state.active_section { + ActiveSection::List => { + let selected_id = + self.get_id_of_selected(&self.albums, Selectable::Album); + self.state.albums_search_term.pop(); + self.reposition_cursor(&selected_id, Selectable::Album); + } + ActiveSection::Tracks => { + let selected_id = + self.get_id_of_selected(&self.album_tracks, Selectable::AlbumTrack); + self.state.album_tracks_search_term.pop(); + self.reposition_cursor(&selected_id, Selectable::AlbumTrack); } _ => {} - } - } - KeyCode::Delete => { - match self.state.active_tab { - ActiveTab::Library => { - match self.state.active_section { - ActiveSection::List => { - let selected_id = self.get_id_of_selected(&self.artists, Selectable::Artist); - self.state.artists_search_term.clear(); - self.reposition_cursor(&selected_id, Selectable::Artist); - } - ActiveSection::Tracks => { - let selected_id = self.get_id_of_selected(&self.tracks, Selectable::Track); - self.state.tracks_search_term.clear(); - self.reposition_cursor(&selected_id, Selectable::Track); - } - _ => {} - } + }, + ActiveTab::Playlists => match self.state.active_section { + ActiveSection::List => { + let selected_id = + self.get_id_of_selected(&self.playlists, Selectable::Playlist); + self.state.playlists_search_term.pop(); + self.reposition_cursor(&selected_id, Selectable::Playlist); } - ActiveTab::Albums => { - match self.state.active_section { - ActiveSection::List => { - let selected_id = self.get_id_of_selected(&self.albums, Selectable::Album); - self.state.albums_search_term.clear(); - self.reposition_cursor(&selected_id, Selectable::Album); - } - ActiveSection::Tracks => { - let selected_id = self.get_id_of_selected(&self.album_tracks, Selectable::AlbumTrack); - self.state.album_tracks_search_term.clear(); - self.reposition_cursor(&selected_id, Selectable::AlbumTrack); - } - _ => {} - } + ActiveSection::Tracks => { + let selected_id = self.get_id_of_selected( + &self.playlist_tracks, + Selectable::PlaylistTrack, + ); + self.state.playlist_tracks_search_term.pop(); + self.reposition_cursor(&selected_id, Selectable::PlaylistTrack); } - ActiveTab::Playlists => { - match self.state.active_section { - ActiveSection::List => { - let selected_id = self.get_id_of_selected(&self.playlists, Selectable::Playlist); - self.state.playlists_search_term.clear(); - self.reposition_cursor(&selected_id, Selectable::Playlist); - } - ActiveSection::Tracks => { - let selected_id = self.get_id_of_selected(&self.playlist_tracks, Selectable::PlaylistTrack); - self.state.playlist_tracks_search_term.clear(); - self.reposition_cursor(&selected_id, Selectable::PlaylistTrack); - } - _ => {} - } + _ => {} + }, + _ => {} + }, + KeyCode::Delete => match self.state.active_tab { + ActiveTab::Library => match self.state.active_section { + ActiveSection::List => { + let selected_id = + self.get_id_of_selected(&self.artists, Selectable::Artist); + self.state.artists_search_term.clear(); + self.reposition_cursor(&selected_id, Selectable::Artist); + } + ActiveSection::Tracks => { + let selected_id = + self.get_id_of_selected(&self.tracks, Selectable::Track); + self.state.tracks_search_term.clear(); + self.reposition_cursor(&selected_id, Selectable::Track); } _ => {} - } - } - KeyCode::Char(c) => { - match self.state.active_tab { - ActiveTab::Library => { - match self.state.active_section { - ActiveSection::List => { - self.state.artists_search_term.push(c); - self.artist_select_by_index(0); - } - ActiveSection::Tracks => { - self.state.tracks_search_term.push(c); - self.track_select_by_index(0); - } - _ => {} - } + }, + ActiveTab::Albums => match self.state.active_section { + ActiveSection::List => { + let selected_id = + self.get_id_of_selected(&self.albums, Selectable::Album); + self.state.albums_search_term.clear(); + self.reposition_cursor(&selected_id, Selectable::Album); } - ActiveTab::Albums => { - match self.state.active_section { - ActiveSection::List => { - self.state.albums_search_term.push(c); - self.album_select_by_index(0); - } - ActiveSection::Tracks => { - self.state.album_tracks_search_term.push(c); - self.album_track_select_by_index(0); - } - _ => {} - } + ActiveSection::Tracks => { + let selected_id = + self.get_id_of_selected(&self.album_tracks, Selectable::AlbumTrack); + self.state.album_tracks_search_term.clear(); + self.reposition_cursor(&selected_id, Selectable::AlbumTrack); } - ActiveTab::Playlists => { - match self.state.active_section { - ActiveSection::List => { - self.state.playlists_search_term.push(c); - self.playlist_select_by_index(0); - } - ActiveSection::Tracks => { - self.state.playlist_tracks_search_term.push(c); - self.playlist_track_select_by_index(0); - } - _ => {} - } + _ => {} + }, + ActiveTab::Playlists => match self.state.active_section { + ActiveSection::List => { + let selected_id = + self.get_id_of_selected(&self.playlists, Selectable::Playlist); + self.state.playlists_search_term.clear(); + self.reposition_cursor(&selected_id, Selectable::Playlist); + } + ActiveSection::Tracks => { + let selected_id = self.get_id_of_selected( + &self.playlist_tracks, + Selectable::PlaylistTrack, + ); + self.state.playlist_tracks_search_term.clear(); + self.reposition_cursor(&selected_id, Selectable::PlaylistTrack); } _ => {} - } - } + }, + _ => {} + }, + KeyCode::Char(c) => match self.state.active_tab { + ActiveTab::Library => match self.state.active_section { + ActiveSection::List => { + self.state.artists_search_term.push(c); + self.artist_select_by_index(0); + } + ActiveSection::Tracks => { + self.state.tracks_search_term.push(c); + self.track_select_by_index(0); + } + _ => {} + }, + ActiveTab::Albums => match self.state.active_section { + ActiveSection::List => { + self.state.albums_search_term.push(c); + self.album_select_by_index(0); + } + ActiveSection::Tracks => { + self.state.album_tracks_search_term.push(c); + self.album_track_select_by_index(0); + } + _ => {} + }, + ActiveTab::Playlists => match self.state.active_section { + ActiveSection::List => { + self.state.playlists_search_term.push(c); + self.playlist_select_by_index(0); + } + ActiveSection::Tracks => { + self.state.playlist_tracks_search_term.push(c); + self.playlist_track_select_by_index(0); + } + _ => {} + }, + _ => {} + }, _ => {} } return; @@ -509,7 +567,13 @@ impl App { KeyCode::Char('q') => self.exit(), // Seek backward KeyCode::Left => { - let secs = f64::max(0.0, self.state.current_playback_state.duration * self.state.current_playback_state.percentage / 100.0 - 5.0); + let secs = f64::max( + 0.0, + self.state.current_playback_state.duration + * self.state.current_playback_state.percentage + / 100.0 + - 5.0, + ); self.update_mpris_position(secs); if let Ok(mpv) = self.mpv_state.lock() { @@ -518,7 +582,10 @@ impl App { } // Seek forward KeyCode::Right => { - let secs = self.state.current_playback_state.duration * self.state.current_playback_state.percentage / 100.0 + 5.0; + let secs = self.state.current_playback_state.duration + * self.state.current_playback_state.percentage + / 100.0 + + 5.0; self.update_mpris_position(secs); if let Ok(mpv) = self.mpv_state.lock() { @@ -528,11 +595,15 @@ impl App { // Previous track KeyCode::Char('n') => { if let Some(client) = &self.client { - let _ = client.stopped( - &self.active_song_id, - // position ticks - (self.state.current_playback_state.duration * self.state.current_playback_state.percentage * 100000.0) as u64, - ).await; + let _ = client + .stopped( + &self.active_song_id, + // position ticks + (self.state.current_playback_state.duration + * self.state.current_playback_state.percentage + * 100000.0) as u64, + ) + .await; if let Ok(mpv) = self.mpv_state.lock() { let _ = mpv.mpv.command("playlist_next", &["force"]); } @@ -542,7 +613,9 @@ impl App { // Next track KeyCode::Char('N') => { if let Ok(mpv) = self.mpv_state.lock() { - let current_time = self.state.current_playback_state.duration * self.state.current_playback_state.percentage / 100.0; + let current_time = self.state.current_playback_state.duration + * self.state.current_playback_state.percentage + / 100.0; if current_time > 5.0 { let _ = mpv.mpv.command("seek", &["0.0", "absolute"]); return; @@ -584,9 +657,18 @@ impl App { self.state.selected_album.select_first(); self.state.selected_album_track.select_first(); - self.state.artists_scroll_state = self.state.artists_scroll_state.content_length(self.artists.len()); - self.state.albums_scroll_state = self.state.albums_scroll_state.content_length(self.albums.len()); - self.state.playlists_scroll_state = self.state.playlists_scroll_state.content_length(self.playlists.len()); + self.state.artists_scroll_state = self + .state + .artists_scroll_state + .content_length(self.artists.len()); + self.state.albums_scroll_state = self + .state + .albums_scroll_state + .content_length(self.albums.len()); + self.state.playlists_scroll_state = self + .state + .playlists_scroll_state + .content_length(self.playlists.len()); self.tracks.clear(); self.album_tracks.clear(); @@ -605,11 +687,15 @@ impl App { } self.state.current_playback_state.volume += 5; if let Ok(mpv) = self.mpv_state.lock() { - let _ = mpv.mpv.set_property("volume", self.state.current_playback_state.volume); + let _ = mpv + .mpv + .set_property("volume", self.state.current_playback_state.volume); } - #[cfg(target_os = "linux")] { + #[cfg(target_os = "linux")] + { if let Some(ref mut controls) = self.controls { - let _ = controls.set_volume(self.state.current_playback_state.volume as f64 / 100.0); + let _ = controls + .set_volume(self.state.current_playback_state.volume as f64 / 100.0); } } } @@ -620,11 +706,15 @@ impl App { } self.state.current_playback_state.volume -= 5; if let Ok(mpv) = self.mpv_state.lock() { - let _ = mpv.mpv.set_property("volume", self.state.current_playback_state.volume); + let _ = mpv + .mpv + .set_property("volume", self.state.current_playback_state.volume); } - #[cfg(target_os = "linux")] { + #[cfg(target_os = "linux")] + { if let Some(ref mut controls) = self.controls { - let _ = controls.set_volume(self.state.current_playback_state.volume as f64 / 100.0); + let _ = controls + .set_volume(self.state.current_playback_state.volume as f64 / 100.0); } } } @@ -640,8 +730,16 @@ impl App { match self.state.active_tab { ActiveTab::Library => { if !self.state.artists_search_term.is_empty() { - let items = search_results(&self.artists, &self.state.artists_search_term, false); - let selected = self.state.selected_artist.selected().unwrap_or(items.len() - 1); + let items = search_results( + &self.artists, + &self.state.artists_search_term, + false, + ); + let selected = self + .state + .selected_artist + .selected() + .unwrap_or(items.len() - 1); if selected == items.len() - 1 { self.artist_select_by_index(selected); return; @@ -650,7 +748,11 @@ impl App { return; } - let selected = self.state.selected_artist.selected().unwrap_or(self.artists.len() - 1); + let selected = self + .state + .selected_artist + .selected() + .unwrap_or(self.artists.len() - 1); if selected == self.artists.len() - 1 { self.artist_select_by_index(selected); return; @@ -659,8 +761,16 @@ impl App { } ActiveTab::Albums => { if !self.state.albums_search_term.is_empty() { - let items = search_results(&self.albums, &self.state.albums_search_term, false); - let selected = self.state.selected_album.selected().unwrap_or(items.len() - 1); + let items = search_results( + &self.albums, + &self.state.albums_search_term, + false, + ); + let selected = self + .state + .selected_album + .selected() + .unwrap_or(items.len() - 1); if selected == items.len() - 1 { self.album_select_by_index(selected); return; @@ -669,7 +779,11 @@ impl App { return; } - let selected = self.state.selected_album.selected().unwrap_or(self.albums.len() - 1); + let selected = self + .state + .selected_album + .selected() + .unwrap_or(self.albums.len() - 1); if selected == self.albums.len() - 1 { self.album_select_by_index(selected); return; @@ -678,8 +792,16 @@ impl App { } ActiveTab::Playlists => { if !self.state.playlists_search_term.is_empty() { - let items = search_results(&self.playlists, &self.state.playlists_search_term, false); - let selected = self.state.selected_playlist.selected().unwrap_or(items.len() - 1); + let items = search_results( + &self.playlists, + &self.state.playlists_search_term, + false, + ); + let selected = self + .state + .selected_playlist + .selected() + .unwrap_or(items.len() - 1); if selected == items.len() - 1 { self.playlist_select_by_index(selected); return; @@ -688,7 +810,11 @@ impl App { return; } - let selected = self.state.selected_playlist.selected().unwrap_or(self.playlists.len() - 1); + let selected = self + .state + .selected_playlist + .selected() + .unwrap_or(self.playlists.len() - 1); if selected == self.playlists.len() - 1 { self.playlist_select_by_index(selected); return; @@ -703,9 +829,11 @@ impl App { ActiveSection::Tracks => { if self.state.active_tab == ActiveTab::Library { if !self.state.tracks_search_term.is_empty() { - let items = search_results(&self.tracks, &self.state.tracks_search_term, false); + let items = + search_results(&self.tracks, &self.state.tracks_search_term, false); let selected = self - .state.selected_track + .state + .selected_track .selected() .unwrap_or(items.len() - 1); if selected == items.len() - 1 { @@ -717,7 +845,8 @@ impl App { } let selected = self - .state.selected_track + .state + .selected_track .selected() .unwrap_or(self.tracks.len() - 1); if selected == self.tracks.len() - 1 { @@ -728,9 +857,14 @@ impl App { } if self.state.active_tab == ActiveTab::Albums { if !self.state.album_tracks_search_term.is_empty() { - let items = search_results(&self.album_tracks, &self.state.album_tracks_search_term, false); + let items = search_results( + &self.album_tracks, + &self.state.album_tracks_search_term, + false, + ); let selected = self - .state.selected_album_track + .state + .selected_album_track .selected() .unwrap_or(items.len() - 1); if selected == items.len() - 1 { @@ -742,7 +876,8 @@ impl App { } let selected = self - .state.selected_album_track + .state + .selected_album_track .selected() .unwrap_or(self.album_tracks.len() - 1); if selected == self.album_tracks.len() - 1 { @@ -753,9 +888,14 @@ impl App { } if self.state.active_tab == ActiveTab::Playlists { if !self.state.playlist_tracks_search_term.is_empty() { - let items = search_results(&self.playlist_tracks, &self.state.playlist_tracks_search_term, false); + let items = search_results( + &self.playlist_tracks, + &self.state.playlist_tracks_search_term, + false, + ); let selected = self - .state.selected_playlist_track + .state + .selected_playlist_track .selected() .unwrap_or(items.len() - 1); if selected == items.len() - 1 { @@ -767,7 +907,8 @@ impl App { } let selected = self - .state.selected_playlist_track + .state + .selected_playlist_track .selected() .unwrap_or(self.playlist_tracks.len() - 1); if selected == self.playlist_tracks.len() - 1 { @@ -804,17 +945,14 @@ impl App { } ActiveSection::Popup => { self.popup.selected.select_next(); - }, + } }, KeyCode::Up | KeyCode::Char('k') => match self.state.active_section { ActiveSection::List => { match self.state.active_tab { ActiveTab::Library => { if !self.state.artists_search_term.is_empty() { - let selected = self - .state.selected_artist - .selected() - .unwrap_or(0); + let selected = self.state.selected_artist.selected().unwrap_or(0); if selected == 0 { self.artist_select_by_index(selected); return; @@ -832,10 +970,7 @@ impl App { } ActiveTab::Albums => { if !self.state.albums_search_term.is_empty() { - let selected = self - .state.selected_album - .selected() - .unwrap_or(0); + let selected = self.state.selected_album.selected().unwrap_or(0); if selected == 0 { self.album_select_by_index(selected); return; @@ -853,10 +988,7 @@ impl App { } ActiveTab::Playlists => { if !self.state.playlists_search_term.is_empty() { - let selected = self - .state.selected_playlist - .selected() - .unwrap_or(0); + let selected = self.state.selected_playlist.selected().unwrap_or(0); if selected == 0 { self.playlist_select_by_index(selected); return; @@ -877,51 +1009,52 @@ impl App { } } } - ActiveSection::Tracks => { - - match self.state.active_tab { - ActiveTab::Library => { - if !self.state.tracks_search_term.is_empty() { - let selected = self - .state.selected_track - .selected() - .unwrap_or(0); - self.track_select_by_index(std::cmp::max(selected as i32 - 1, 0) as usize); - return; - } - + ActiveSection::Tracks => match self.state.active_tab { + ActiveTab::Library => { + if !self.state.tracks_search_term.is_empty() { let selected = self.state.selected_track.selected().unwrap_or(0); - self.track_select_by_index(std::cmp::max(selected as i32 - 1, 0) as usize); + self.track_select_by_index( + std::cmp::max(selected as i32 - 1, 0) as usize + ); + return; } - ActiveTab::Albums => { - if !self.state.album_tracks_search_term.is_empty() { - let selected = self - .state.selected_album_track - .selected() - .unwrap_or(0); - self.album_track_select_by_index(std::cmp::max(selected as i32 - 1, 0) as usize); - return; - } + let selected = self.state.selected_track.selected().unwrap_or(0); + self.track_select_by_index(std::cmp::max(selected as i32 - 1, 0) as usize); + } + ActiveTab::Albums => { + if !self.state.album_tracks_search_term.is_empty() { let selected = self.state.selected_album_track.selected().unwrap_or(0); - self.album_track_select_by_index(std::cmp::max(selected as i32 - 1, 0) as usize); + self.album_track_select_by_index( + std::cmp::max(selected as i32 - 1, 0) as usize + ); + return; } - ActiveTab::Playlists => { - if !self.state.playlist_tracks_search_term.is_empty() { - let selected = self - .state.selected_playlist_track - .selected() - .unwrap_or(0); - self.playlist_track_select_by_index(std::cmp::max(selected as i32 - 1, 0) as usize); - return; - } - - let selected = self.state.selected_playlist_track.selected().unwrap_or(0); - self.playlist_track_select_by_index(std::cmp::max(selected as i32 - 1, 0) as usize); + + let selected = self.state.selected_album_track.selected().unwrap_or(0); + self.album_track_select_by_index( + std::cmp::max(selected as i32 - 1, 0) as usize + ); + } + ActiveTab::Playlists => { + if !self.state.playlist_tracks_search_term.is_empty() { + let selected = + self.state.selected_playlist_track.selected().unwrap_or(0); + self.playlist_track_select_by_index(std::cmp::max( + selected as i32 - 1, + 0, + ) + as usize); + return; } - _ => {} + + let selected = self.state.selected_playlist_track.selected().unwrap_or(0); + self.playlist_track_select_by_index( + std::cmp::max(selected as i32 - 1, 0) as usize + ); } - } + _ => {} + }, ActiveSection::Queue => { if key_event.modifiers == KeyModifiers::SHIFT { self.move_queue_item_up().await; @@ -929,7 +1062,9 @@ impl App { } self.state.selected_queue_item_manual_override = true; let selected = self.state.selected_queue_item.selected().unwrap_or(0); - self.state.selected_queue_item.select(Some(std::cmp::max(selected as i32 - 1, 0) as usize)); + self.state + .selected_queue_item + .select(Some(std::cmp::max(selected as i32 - 1, 0) as usize)); } ActiveSection::Lyrics => { self.state.selected_lyric_manual_override = true; @@ -940,40 +1075,36 @@ impl App { } }, KeyCode::Char('g') | KeyCode::Home => match self.state.active_section { - ActiveSection::List => { - match self.state.active_tab { - ActiveTab::Library => { - self.artist_select_by_index(0); - } - ActiveTab::Albums => { - self.album_select_by_index(0); - } - ActiveTab::Playlists => { - self.playlist_select_by_index(0); - } - _ => {} + ActiveSection::List => match self.state.active_tab { + ActiveTab::Library => { + self.artist_select_by_index(0); } - } - ActiveSection::Tracks => { - match self.state.active_tab { - ActiveTab::Library => { - if !self.tracks.is_empty() { - self.track_select_by_index(0); - } + ActiveTab::Albums => { + self.album_select_by_index(0); + } + ActiveTab::Playlists => { + self.playlist_select_by_index(0); + } + _ => {} + }, + ActiveSection::Tracks => match self.state.active_tab { + ActiveTab::Library => { + if !self.tracks.is_empty() { + self.track_select_by_index(0); } - ActiveTab::Albums => { - if !self.album_tracks.is_empty() { - self.album_track_select_by_index(0); - } + } + ActiveTab::Albums => { + if !self.album_tracks.is_empty() { + self.album_track_select_by_index(0); } - ActiveTab::Playlists => { - if !self.playlist_tracks.is_empty() { - self.playlist_track_select_by_index(0); - } + } + ActiveTab::Playlists => { + if !self.playlist_tracks.is_empty() { + self.playlist_track_select_by_index(0); } - _ => {} } - } + _ => {} + }, ActiveSection::Queue => { self.state.selected_queue_item_manual_override = true; self.state.selected_queue_item.select_first(); @@ -987,46 +1118,42 @@ impl App { } }, KeyCode::Char('G') | KeyCode::End => match self.state.active_section { - ActiveSection::List => { - match self.state.active_tab { - ActiveTab::Library => { - if !self.artists.is_empty() { - self.artist_select_by_index(self.artists.len() - 1); - } + ActiveSection::List => match self.state.active_tab { + ActiveTab::Library => { + if !self.artists.is_empty() { + self.artist_select_by_index(self.artists.len() - 1); } - ActiveTab::Albums => { - if !self.albums.is_empty() { - self.album_select_by_index(self.albums.len() - 1); - } + } + ActiveTab::Albums => { + if !self.albums.is_empty() { + self.album_select_by_index(self.albums.len() - 1); } - ActiveTab::Playlists => { - if !self.playlists.is_empty() { - self.playlist_select_by_index(self.playlists.len() - 1); - } + } + ActiveTab::Playlists => { + if !self.playlists.is_empty() { + self.playlist_select_by_index(self.playlists.len() - 1); } - _ => {} } - } - ActiveSection::Tracks => { - match self.state.active_tab { - ActiveTab::Library => { - if !self.tracks.is_empty() { - self.track_select_by_index(self.tracks.len() - 1); - } + _ => {} + }, + ActiveSection::Tracks => match self.state.active_tab { + ActiveTab::Library => { + if !self.tracks.is_empty() { + self.track_select_by_index(self.tracks.len() - 1); } - ActiveTab::Albums => { - if !self.album_tracks.is_empty() { - self.album_track_select_by_index(self.album_tracks.len() - 1); - } + } + ActiveTab::Albums => { + if !self.album_tracks.is_empty() { + self.album_track_select_by_index(self.album_tracks.len() - 1); } - ActiveTab::Playlists => { - if !self.playlist_tracks.is_empty() { - self.playlist_track_select_by_index(self.playlist_tracks.len() - 1); - } + } + ActiveTab::Playlists => { + if !self.playlist_tracks.is_empty() { + self.playlist_track_select_by_index(self.playlist_tracks.len() - 1); } - _ => {} } - } + _ => {} + }, ActiveSection::Queue => { if !self.state.queue.is_empty() { self.state.selected_queue_item_manual_override = true; @@ -1053,20 +1180,32 @@ impl App { if self.artists.is_empty() { return; } - let ids = search_results(&self.artists, &self.state.artists_search_term, false); - let mut artists = self.artists.iter().filter(|artist| ids.contains(&artist.id)).collect::>(); + let ids = search_results( + &self.artists, + &self.state.artists_search_term, + false, + ); + let mut artists = self + .artists + .iter() + .filter(|artist| ids.contains(&artist.id)) + .collect::>(); if artists.is_empty() { artists = self.artists.iter().collect::>(); } let selected = self.state.selected_artist.selected().unwrap_or(0); if let Some(current_artist) = artists[selected].name.chars().next() { let current_artist = current_artist.to_ascii_lowercase(); - let next_artist = artists - .iter().skip(selected) - .find(|a| a.name.chars().next().map(|c| c.to_ascii_lowercase()) != Some(current_artist)); + let next_artist = artists.iter().skip(selected).find(|a| { + a.name.chars().next().map(|c| c.to_ascii_lowercase()) + != Some(current_artist) + }); if let Some(next_artist) = next_artist { - let index = artists.iter().position(|a| a.id == next_artist.id).unwrap_or(0); + let index = artists + .iter() + .position(|a| a.id == next_artist.id) + .unwrap_or(0); self.artist_select_by_index(index); } } @@ -1078,10 +1217,16 @@ impl App { } if let Some(selected) = self.state.selected_track.selected() { let current_album = self.tracks[selected].album_id.clone(); - let next_album = self.tracks.iter().skip(selected).find(|t| t.album_id != current_album && !t.id.starts_with("_album_")); + let next_album = self.tracks.iter().skip(selected).find(|t| { + t.album_id != current_album && !t.id.starts_with("_album_") + }); if let Some(next_album) = next_album { - let index = self.tracks.iter().position(|t| t.album_id == next_album.album_id).unwrap_or(0); + let index = self + .tracks + .iter() + .position(|t| t.album_id == next_album.album_id) + .unwrap_or(0); self.track_select_by_index(index); } } @@ -1094,14 +1239,24 @@ impl App { if self.albums.is_empty() { return; } - let ids = search_results(&self.albums, &self.state.albums_search_term, false); - let mut albums = self.albums.iter().filter(|album| ids.contains(&album.id)).collect::>(); + let ids = + search_results(&self.albums, &self.state.albums_search_term, false); + let mut albums = self + .albums + .iter() + .filter(|album| ids.contains(&album.id)) + .collect::>(); if albums.is_empty() { albums = self.albums.iter().collect::>(); } if let Some(selected) = self.state.selected_album.selected() { - if let Some(next_album) = albums.iter().skip(selected).find(|a| a.name.chars().next() != albums[selected].name.chars().next()) { - let index = albums.iter().position(|a| a.id == next_album.id).unwrap_or(0); + if let Some(next_album) = albums.iter().skip(selected).find(|a| { + a.name.chars().next() != albums[selected].name.chars().next() + }) { + let index = albums + .iter() + .position(|a| a.id == next_album.id) + .unwrap_or(0); self.album_select_by_index(index); } } @@ -1112,20 +1267,33 @@ impl App { if self.playlists.is_empty() { return; } - let ids = search_results(&self.playlists, &self.state.playlists_search_term, false); - let mut playlists = self.playlists.iter().filter(|playlist| ids.contains(&playlist.id)).collect::>(); + let ids = search_results( + &self.playlists, + &self.state.playlists_search_term, + false, + ); + let mut playlists = self + .playlists + .iter() + .filter(|playlist| ids.contains(&playlist.id)) + .collect::>(); if playlists.is_empty() { playlists = self.playlists.iter().collect::>(); } if let Some(selected) = self.state.selected_playlist.selected() { - if let Some(current_playlist) = playlists[selected].name.chars().next() { + if let Some(current_playlist) = playlists[selected].name.chars().next() + { let current_playlist = current_playlist.to_ascii_lowercase(); - let next_playlist = playlists - .iter().skip(selected) - .find(|a| a.name.chars().next().map(|c| c.to_ascii_lowercase()) != Some(current_playlist)); + let next_playlist = playlists.iter().skip(selected).find(|a| { + a.name.chars().next().map(|c| c.to_ascii_lowercase()) + != Some(current_playlist) + }); if let Some(next_playlist) = next_playlist { - let index = playlists.iter().position(|a| a.id == next_playlist.id).unwrap_or(0); + let index = playlists + .iter() + .position(|a| a.id == next_playlist.id) + .unwrap_or(0); self.playlist_select_by_index(index); } } @@ -1142,8 +1310,16 @@ impl App { if self.artists.is_empty() { return; } - let ids = search_results(&self.artists, &self.state.artists_search_term, false); - let mut artists = self.artists.iter().filter(|artist| ids.contains(&artist.id)).collect::>(); + let ids = search_results( + &self.artists, + &self.state.artists_search_term, + false, + ); + let mut artists = self + .artists + .iter() + .filter(|artist| ids.contains(&artist.id)) + .collect::>(); if artists.is_empty() { artists = self.artists.iter().collect::>(); } @@ -1151,11 +1327,19 @@ impl App { if let Some(current_artist) = artists[selected].name.chars().next() { let current_artist = current_artist.to_ascii_lowercase(); let prev_artist = artists - .iter().rev().skip(artists.len() - selected) - .find(|a| a.name.chars().next().map(|c| c.to_ascii_lowercase()) != Some(current_artist)); + .iter() + .rev() + .skip(artists.len() - selected) + .find(|a| { + a.name.chars().next().map(|c| c.to_ascii_lowercase()) + != Some(current_artist) + }); if let Some(prev_artist) = prev_artist { - let index = artists.iter().position(|a| a.id == prev_artist.id).unwrap_or(0); + let index = artists + .iter() + .position(|a| a.id == prev_artist.id) + .unwrap_or(0); self.artist_select_by_index(index); } } @@ -1167,8 +1351,19 @@ impl App { } if let Some(selected) = self.state.selected_track.selected() { let current_album = self.tracks[selected].album_id.clone(); - let first_track_in_current_album = self.tracks.iter().position(|t| t.album_id == current_album).unwrap_or(0); - let prev_album = self.tracks.iter().rev().skip(self.tracks.len() - selected).find(|t| t.album_id != current_album && !t.id.starts_with("_album_")); + let first_track_in_current_album = self + .tracks + .iter() + .position(|t| t.album_id == current_album) + .unwrap_or(0); + let prev_album = self + .tracks + .iter() + .rev() + .skip(self.tracks.len() - selected) + .find(|t| { + t.album_id != current_album && !t.id.starts_with("_album_") + }); if selected != first_track_in_current_album { self.track_select_by_index(first_track_in_current_album); @@ -1176,7 +1371,11 @@ impl App { } if let Some(prev_album) = prev_album { - let index = self.tracks.iter().position(|t| t.album_id == prev_album.album_id).unwrap_or(0); + let index = self + .tracks + .iter() + .position(|t| t.album_id == prev_album.album_id) + .unwrap_or(0); self.track_select_by_index(index); } } @@ -1189,20 +1388,30 @@ impl App { if self.albums.is_empty() { return; } - let ids = search_results(&self.albums, &self.state.albums_search_term, false); - let mut albums = self.albums.iter().filter(|album| ids.contains(&album.id)).collect::>(); + let ids = + search_results(&self.albums, &self.state.albums_search_term, false); + let mut albums = self + .albums + .iter() + .filter(|album| ids.contains(&album.id)) + .collect::>(); if albums.is_empty() { albums = self.albums.iter().collect::>(); } if let Some(selected) = self.state.selected_album.selected() { - if let Some(current_album) = albums[selected].name.chars().next() { + if let Some(current_album) = albums[selected].name.chars().next() { let current_album = current_album.to_ascii_lowercase(); - let prev_album = albums - .iter().rev().skip(albums.len() - selected) - .find(|a| a.name.chars().next().map(|c| c.to_ascii_lowercase()) != Some(current_album)); + let prev_album = + albums.iter().rev().skip(albums.len() - selected).find(|a| { + a.name.chars().next().map(|c| c.to_ascii_lowercase()) + != Some(current_album) + }); if let Some(prev_album) = prev_album { - let index = albums.iter().position(|a| a.id == prev_album.id).unwrap_or(0); + let index = albums + .iter() + .position(|a| a.id == prev_album.id) + .unwrap_or(0); self.album_select_by_index(index); } } @@ -1214,20 +1423,37 @@ impl App { if self.playlists.is_empty() { return; } - let ids = search_results(&self.playlists, &self.state.playlists_search_term, false); - let mut playlists = self.playlists.iter().filter(|playlist| ids.contains(&playlist.id)).collect::>(); + let ids = search_results( + &self.playlists, + &self.state.playlists_search_term, + false, + ); + let mut playlists = self + .playlists + .iter() + .filter(|playlist| ids.contains(&playlist.id)) + .collect::>(); if playlists.is_empty() { playlists = self.playlists.iter().collect::>(); } if let Some(selected) = self.state.selected_playlist.selected() { - if let Some(current_playlist) = playlists[selected].name.chars().next() { + if let Some(current_playlist) = playlists[selected].name.chars().next() + { let current_playlist = current_playlist.to_ascii_lowercase(); let prev_playlist = playlists - .iter().rev().skip(playlists.len() - selected) - .find(|a| a.name.chars().next().map(|c| c.to_ascii_lowercase()) != Some(current_playlist)); + .iter() + .rev() + .skip(playlists.len() - selected) + .find(|a| { + a.name.chars().next().map(|c| c.to_ascii_lowercase()) + != Some(current_playlist) + }); if let Some(prev_playlist) = prev_playlist { - let index = playlists.iter().position(|a| a.id == prev_playlist.id).unwrap_or(0); + let index = playlists + .iter() + .position(|a| a.id == prev_playlist.id) + .unwrap_or(0); self.playlist_select_by_index(index); } } @@ -1239,15 +1465,20 @@ impl App { KeyCode::Enter => { match self.state.active_section { ActiveSection::List => { - if self.state.active_tab == ActiveTab::Library { self.state.tracks_search_term = String::from(""); self.state.selected_track.select(Some(0)); - let search_results = search_results(&self.artists, &self.state.artists_search_term, true); + let search_results = search_results( + &self.artists, + &self.state.artists_search_term, + true, + ); let artists = search_results .iter() - .map(|id| self.artists.iter().find(|artist| artist.id == *id).unwrap()) + .map(|id| { + self.artists.iter().find(|artist| artist.id == *id).unwrap() + }) .collect::>(); let selected = self.state.selected_artist.selected().unwrap_or(0); if artists.is_empty() { @@ -1257,11 +1488,11 @@ impl App { } if self.state.active_tab == ActiveTab::Albums { - self.state.album_tracks_search_term = String::from(""); self.state.selected_album_track.select(Some(0)); - let search_results = search_results(&self.albums, &self.state.albums_search_term, true); + let search_results = + search_results(&self.albums, &self.state.albums_search_term, true); let albums = search_results .iter() .map(|id| self.albums.iter().find(|album| album.id == *id).unwrap()) @@ -1275,60 +1506,91 @@ impl App { } if self.state.active_tab == ActiveTab::Playlists { - self.state.playlist_tracks_search_term = String::from(""); self.state.selected_playlist_track.select(Some(0)); // if we are searching we need to account of the list index offsets caused by the search if !self.state.playlists_search_term.is_empty() { - let ids = search_results(&self.playlists, &self.state.playlists_search_term, false); + let ids = search_results( + &self.playlists, + &self.state.playlists_search_term, + false, + ); if ids.is_empty() { return; } let selected = self.state.selected_playlist.selected().unwrap_or(0); self.playlist(&ids[selected]).await; - let _ = self.state.playlist_tracks_scroll_state.content_length(self.playlist_tracks.len() - 1); + let _ = self + .state + .playlist_tracks_scroll_state + .content_length(self.playlist_tracks.len() - 1); return; } let selected = self.state.selected_playlist.selected().unwrap_or(0); self.playlist(&self.playlists[selected].id.clone()).await; - let _ = self.state.playlist_tracks_scroll_state.content_length(self.playlist_tracks.len() - 1); + let _ = self + .state + .playlist_tracks_scroll_state + .content_length(self.playlist_tracks.len() - 1); } } ActiveSection::Tracks => { let items = match self.state.active_tab { ActiveTab::Library => { - let ids = search_results(&self.tracks, &self.state.tracks_search_term, true); - let items = ids.iter() + let ids = search_results( + &self.tracks, + &self.state.tracks_search_term, + true, + ); + let items = ids + .iter() .map(|id| self.tracks.iter().find(|t| t.id == *id).unwrap()) .cloned() .collect(); items } ActiveTab::Albums => { - let ids = search_results(&self.album_tracks, &self.state.album_tracks_search_term, true); - let items = ids.iter() - .map(|id| self.album_tracks.iter().find(|t| t.id == *id).unwrap()) + let ids = search_results( + &self.album_tracks, + &self.state.album_tracks_search_term, + true, + ); + let items = ids + .iter() + .map(|id| { + self.album_tracks.iter().find(|t| t.id == *id).unwrap() + }) .cloned() .collect(); items } ActiveTab::Playlists => { - let ids = search_results(&self.playlist_tracks, &self.state.playlist_tracks_search_term, false); - let items: Vec = self.playlist_tracks.iter() + let ids = search_results( + &self.playlist_tracks, + &self.state.playlist_tracks_search_term, + false, + ); + let items: Vec = self + .playlist_tracks + .iter() .filter(|t| ids.contains(&t.id) || ids.is_empty()) .cloned() .collect(); items } - _ => vec![] + _ => vec![], }; let selected = match self.state.active_tab { ActiveTab::Library => self.state.selected_track.selected().unwrap_or(0), - ActiveTab::Albums => self.state.selected_album_track.selected().unwrap_or(0), - ActiveTab::Playlists => self.state.selected_playlist_track.selected().unwrap_or(0), - _ => 0 + ActiveTab::Albums => { + self.state.selected_album_track.selected().unwrap_or(0) + } + ActiveTab::Playlists => { + self.state.selected_playlist_track.selected().unwrap_or(0) + } + _ => 0, }; if key_event.modifiers == KeyModifiers::CONTROL { @@ -1342,19 +1604,21 @@ impl App { self.replace_queue(&items, selected).await; } ActiveSection::Queue => { - self.relocate_queue_and_play().await; + self.relocate_queue_and_play().await; } ActiveSection::Lyrics => { // jump to that timestamp if let Some((_, lyrics_vec, _)) = &self.lyrics { let selected = self.state.selected_lyric.selected().unwrap_or(0); - + if let Some(lyric) = lyrics_vec.get(selected) { let time = lyric.start as f64 / 10_000_000.0; - + if time != 0.0 { if let Ok(mpv) = self.mpv_state.lock() { - let _ = mpv.mpv.command("seek", &[&time.to_string(), "absolute"]); + let _ = mpv + .mpv + .command("seek", &[&time.to_string(), "absolute"]); let _ = mpv.mpv.set_property("pause", false); self.paused = false; self.buffering = true; @@ -1369,36 +1633,51 @@ impl App { KeyCode::Char('e') => { let items = match self.state.active_tab { ActiveTab::Library => { - let ids = search_results(&self.tracks, &self.state.tracks_search_term, true); - let items = ids.iter() + let ids = + search_results(&self.tracks, &self.state.tracks_search_term, true); + let items = ids + .iter() .map(|id| self.tracks.iter().find(|t| t.id == *id).unwrap()) .cloned() .collect(); items } ActiveTab::Albums => { - let ids = search_results(&self.album_tracks, &self.state.album_tracks_search_term, true); - let items = ids.iter() + let ids = search_results( + &self.album_tracks, + &self.state.album_tracks_search_term, + true, + ); + let items = ids + .iter() .map(|id| self.album_tracks.iter().find(|t| t.id == *id).unwrap()) .cloned() .collect(); items } ActiveTab::Playlists => { - let ids = search_results(&self.playlist_tracks, &self.state.playlist_tracks_search_term, false); - let items: Vec = self.playlist_tracks.iter() + let ids = search_results( + &self.playlist_tracks, + &self.state.playlist_tracks_search_term, + false, + ); + let items: Vec = self + .playlist_tracks + .iter() .filter(|t| ids.contains(&t.id) || ids.is_empty()) .cloned() .collect(); items } - _ => vec![] + _ => vec![], }; let selected = match self.state.active_tab { ActiveTab::Library => self.state.selected_track.selected().unwrap_or(0), - ActiveTab::Playlists => self.state.selected_playlist_track.selected().unwrap_or(0), - _ => 0 + ActiveTab::Playlists => { + self.state.selected_playlist_track.selected().unwrap_or(0) + } + _ => 0, }; if key_event.modifiers == KeyModifiers::CONTROL { @@ -1408,106 +1687,148 @@ impl App { self.push_to_queue(&items, selected, 1).await; } // mark as favorite (works on anything) - KeyCode::Char('f') => { - match self.state.active_section { - ActiveSection::List => { - if let Some(client) = &self.client { - match self.state.active_tab { - ActiveTab::Library => { - let id = self.get_id_of_selected(&self.artists, Selectable::Artist); - if let Some(artist) = self.original_artists.iter_mut().find(|a| a.id == id) { - let _ = client.set_favorite(&artist.id, !artist.user_data.is_favorite).await; - artist.user_data.is_favorite = !artist.user_data.is_favorite; - self.reorder_lists(); - self.reposition_cursor(&id, Selectable::Artist); - } + KeyCode::Char('f') => match self.state.active_section { + ActiveSection::List => { + if let Some(client) = &self.client { + match self.state.active_tab { + ActiveTab::Library => { + let id = self.get_id_of_selected(&self.artists, Selectable::Artist); + if let Some(artist) = + self.original_artists.iter_mut().find(|a| a.id == id) + { + let _ = client + .set_favorite(&artist.id, !artist.user_data.is_favorite) + .await; + artist.user_data.is_favorite = !artist.user_data.is_favorite; + self.reorder_lists(); + self.reposition_cursor(&id, Selectable::Artist); } - ActiveTab::Albums => { - let id = self.get_id_of_selected(&self.albums, Selectable::Album); - if let Some(album) = self.original_albums.iter_mut().find(|a| a.id == id) { - let _ = client.set_favorite(&album.id, !album.user_data.is_favorite).await; - album.user_data.is_favorite = !album.user_data.is_favorite; - self.reorder_lists(); - self.reposition_cursor(&id, Selectable::Album); - } - if let Some(album) = self.tracks.iter_mut().find(|a| a.id == format!("_album_{}", id)) { - album.user_data.is_favorite = !album.user_data.is_favorite; - } + } + ActiveTab::Albums => { + let id = self.get_id_of_selected(&self.albums, Selectable::Album); + if let Some(album) = + self.original_albums.iter_mut().find(|a| a.id == id) + { + let _ = client + .set_favorite(&album.id, !album.user_data.is_favorite) + .await; + album.user_data.is_favorite = !album.user_data.is_favorite; + self.reorder_lists(); + self.reposition_cursor(&id, Selectable::Album); } - ActiveTab::Playlists => { - let id = self.get_id_of_selected(&self.playlists, Selectable::Playlist); - if let Some(playlist) = self.original_playlists.iter_mut().find(|a| a.id == id) { - let _ = client.set_favorite(&playlist.id, !playlist.user_data.is_favorite).await; - playlist.user_data.is_favorite = !playlist.user_data.is_favorite; - self.reorder_lists(); - self.reposition_cursor(&id, Selectable::Playlist); - } + if let Some(album) = self + .tracks + .iter_mut() + .find(|a| a.id == format!("_album_{}", id)) + { + album.user_data.is_favorite = !album.user_data.is_favorite; + } + } + ActiveTab::Playlists => { + let id = + self.get_id_of_selected(&self.playlists, Selectable::Playlist); + if let Some(playlist) = + self.original_playlists.iter_mut().find(|a| a.id == id) + { + let _ = client + .set_favorite(&playlist.id, !playlist.user_data.is_favorite) + .await; + playlist.user_data.is_favorite = + !playlist.user_data.is_favorite; + self.reorder_lists(); + self.reposition_cursor(&id, Selectable::Playlist); } - _ => {} } + _ => {} } } - ActiveSection::Tracks => { - if let Some(client) = &self.client { - match self.state.active_tab { - ActiveTab::Library => { - let id = self.get_id_of_selected(&self.tracks, Selectable::Track); - if let Some(track) = self.tracks.iter_mut().find(|t| t.id == id) { - let _ = client.set_favorite(&track.id, !track.user_data.is_favorite).await; - track.user_data.is_favorite = !track.user_data.is_favorite; - if let Some(tr) = self.state.queue.iter_mut().find(|t| t.id == track.id) { - tr.is_favorite = !tr.is_favorite; + } + ActiveSection::Tracks => { + if let Some(client) = &self.client { + match self.state.active_tab { + ActiveTab::Library => { + let id = self.get_id_of_selected(&self.tracks, Selectable::Track); + if let Some(track) = self.tracks.iter_mut().find(|t| t.id == id) { + let _ = client + .set_favorite(&track.id, !track.user_data.is_favorite) + .await; + track.user_data.is_favorite = !track.user_data.is_favorite; + if let Some(tr) = + self.state.queue.iter_mut().find(|t| t.id == track.id) + { + tr.is_favorite = !tr.is_favorite; + } + if track.id.starts_with("_album_") { + let id = track.id.replace("_album_", ""); + if let Some(album) = + self.albums.iter_mut().find(|a| a.id == id) + { + album.user_data.is_favorite = + !album.user_data.is_favorite; } - if track.id.starts_with("_album_") { - let id = track.id.replace("_album_", ""); - if let Some(album) = self.albums.iter_mut().find(|a| a.id == id) { - album.user_data.is_favorite = !album.user_data.is_favorite; - } - if let Some(album) = self.original_albums.iter_mut().find(|a| a.id == id) { - album.user_data.is_favorite = !album.user_data.is_favorite; - } - self.reorder_lists(); + if let Some(album) = + self.original_albums.iter_mut().find(|a| a.id == id) + { + album.user_data.is_favorite = + !album.user_data.is_favorite; } + self.reorder_lists(); } } - ActiveTab::Albums => { - let id = self.get_id_of_selected(&self.album_tracks, Selectable::AlbumTrack); - if let Some(track) = self.album_tracks.iter_mut().find(|t| t.id == id) { - let _ = client.set_favorite(&track.id, !track.user_data.is_favorite).await; - track.user_data.is_favorite = !track.user_data.is_favorite; - if let Some(tr) = self.state.queue.iter_mut().find(|t| t.id == track.id) { - tr.is_favorite = !tr.is_favorite; - } + } + ActiveTab::Albums => { + let id = self + .get_id_of_selected(&self.album_tracks, Selectable::AlbumTrack); + if let Some(track) = + self.album_tracks.iter_mut().find(|t| t.id == id) + { + let _ = client + .set_favorite(&track.id, !track.user_data.is_favorite) + .await; + track.user_data.is_favorite = !track.user_data.is_favorite; + if let Some(tr) = + self.state.queue.iter_mut().find(|t| t.id == track.id) + { + tr.is_favorite = !tr.is_favorite; } } - ActiveTab::Playlists => { - let id = self.get_id_of_selected(&self.playlist_tracks, Selectable::PlaylistTrack); - if let Some(track) = self.playlist_tracks.iter_mut().find(|t| t.id == id) { - let _ = client.set_favorite(&track.id, !track.user_data.is_favorite).await; - track.user_data.is_favorite = !track.user_data.is_favorite; - if let Some(tr) = self.state.queue.iter_mut().find(|t| t.id == track.id) { - tr.is_favorite = !tr.is_favorite; - } + } + ActiveTab::Playlists => { + let id = self.get_id_of_selected( + &self.playlist_tracks, + Selectable::PlaylistTrack, + ); + if let Some(track) = + self.playlist_tracks.iter_mut().find(|t| t.id == id) + { + let _ = client + .set_favorite(&track.id, !track.user_data.is_favorite) + .await; + track.user_data.is_favorite = !track.user_data.is_favorite; + if let Some(tr) = + self.state.queue.iter_mut().find(|t| t.id == track.id) + { + tr.is_favorite = !tr.is_favorite; } } - _ => {} } + _ => {} } } - ActiveSection::Queue => { - if let Some(client) = &self.client { - let selected = self.state.selected_queue_item.selected().unwrap_or(0); - let track = &self.state.queue[selected].clone(); - let _ = client.set_favorite(&track.id, !track.is_favorite).await; - self.state.queue[selected].is_favorite = !track.is_favorite; - if let Some(tr) = self.tracks.iter_mut().find(|t| t.id == track.id) { - tr.user_data.is_favorite = !track.is_favorite; - } + } + ActiveSection::Queue => { + if let Some(client) = &self.client { + let selected = self.state.selected_queue_item.selected().unwrap_or(0); + let track = &self.state.queue[selected].clone(); + let _ = client.set_favorite(&track.id, !track.is_favorite).await; + self.state.queue[selected].is_favorite = !track.is_favorite; + if let Some(tr) = self.tracks.iter_mut().find(|t| t.id == track.id) { + tr.user_data.is_favorite = !track.is_favorite; } } - _ => {} } - } + _ => {} + }, KeyCode::Char('r') => { if let Ok(mpv) = self.mpv_state.lock() { match self.state.repeat { @@ -1597,54 +1918,50 @@ impl App { } let artist_id = self.get_id_of_selected(&self.artists, Selectable::Artist); let album_id = self.get_id_of_selected(&self.albums, Selectable::Album); - let album_track_id = self.get_id_of_selected(&self.album_tracks, Selectable::AlbumTrack); + let album_track_id = + self.get_id_of_selected(&self.album_tracks, Selectable::AlbumTrack); let track_id = self.get_id_of_selected(&self.tracks, Selectable::Track); let playlist_id = self.get_id_of_selected(&self.playlists, Selectable::Playlist); - let playlist_track_id = self.get_id_of_selected(&self.playlist_tracks, Selectable::PlaylistTrack); + let playlist_track_id = + self.get_id_of_selected(&self.playlist_tracks, Selectable::PlaylistTrack); match self.state.active_tab { - ActiveTab::Library => { - match self.state.active_section { - ActiveSection::List => { - self.state.artists_search_term = String::from(""); - self.reposition_cursor(&artist_id, Selectable::Artist); - } - ActiveSection::Tracks => { - self.state.tracks_search_term = String::from(""); - self.reposition_cursor(&track_id, Selectable::Track); - } - _ => {} + ActiveTab::Library => match self.state.active_section { + ActiveSection::List => { + self.state.artists_search_term = String::from(""); + self.reposition_cursor(&artist_id, Selectable::Artist); } - } - ActiveTab::Albums => { - match self.state.active_section { - ActiveSection::List => { - self.state.albums_search_term = String::from(""); - self.reposition_cursor(&album_id, Selectable::Album); - } - ActiveSection::Tracks => { - self.state.album_tracks_search_term = String::from(""); - self.reposition_cursor(&album_track_id, Selectable::AlbumTrack); - } - _ => {} + ActiveSection::Tracks => { + self.state.tracks_search_term = String::from(""); + self.reposition_cursor(&track_id, Selectable::Track); } - } - ActiveTab::Playlists => { - match self.state.active_section { - ActiveSection::List => { - self.state.playlists_search_term = String::from(""); - self.reposition_cursor(&playlist_id, Selectable::Playlist); - } - ActiveSection::Tracks => { - self.state.playlist_tracks_search_term = String::from(""); - self.reposition_cursor(&playlist_track_id, Selectable::PlaylistTrack); - } - ActiveSection::Popup => { - self.state.active_section = self.state.last_section; - } - _ => {} + _ => {} + }, + ActiveTab::Albums => match self.state.active_section { + ActiveSection::List => { + self.state.albums_search_term = String::from(""); + self.reposition_cursor(&album_id, Selectable::Album); } - } + ActiveSection::Tracks => { + self.state.album_tracks_search_term = String::from(""); + self.reposition_cursor(&album_track_id, Selectable::AlbumTrack); + } + _ => {} + }, + ActiveTab::Playlists => match self.state.active_section { + ActiveSection::List => { + self.state.playlists_search_term = String::from(""); + self.reposition_cursor(&playlist_id, Selectable::Playlist); + } + ActiveSection::Tracks => { + self.state.playlist_tracks_search_term = String::from(""); + self.reposition_cursor(&playlist_track_id, Selectable::PlaylistTrack); + } + ActiveSection::Popup => { + self.state.active_section = self.state.last_section; + } + _ => {} + }, ActiveTab::Search => { self.searching = false; self.search_term = String::from(""); @@ -1723,17 +2040,26 @@ impl App { if let Ok(artists) = client.artists(self.search_term.clone()).await { self.search_result_artists = artists; self.state.selected_search_artist.select(Some(0)); - self.state.search_artist_scroll_state = self.state.search_artist_scroll_state.content_length(self.search_result_artists.len()); + self.state.search_artist_scroll_state = self + .state + .search_artist_scroll_state + .content_length(self.search_result_artists.len()); } if let Ok(albums) = client.search_albums(self.search_term.clone()).await { self.search_result_albums = albums; self.state.selected_search_album.select(Some(0)); - self.state.search_album_scroll_state = self.state.search_album_scroll_state.content_length(self.search_result_albums.len()); + self.state.search_album_scroll_state = self + .state + .search_album_scroll_state + .content_length(self.search_result_albums.len()); } if let Ok(tracks) = client.search_tracks(self.search_term.clone()).await { self.search_result_tracks = tracks; self.state.selected_search_track.select(Some(0)); - self.state.search_track_scroll_state = self.state.search_track_scroll_state.content_length(self.search_result_tracks.len()); + self.state.search_track_scroll_state = self + .state + .search_track_scroll_state + .content_length(self.search_result_tracks.len()); } self.state.search_section = SearchSection::Artists; @@ -1743,7 +2069,10 @@ impl App { if self.search_result_albums.is_empty() { self.state.search_section = SearchSection::Tracks; } - if self.search_result_tracks.is_empty() && self.search_result_artists.is_empty() && self.search_result_albums.is_empty() { + if self.search_result_tracks.is_empty() + && self.search_result_artists.is_empty() + && self.search_result_albums.is_empty() + { self.state.search_section = SearchSection::Artists; } @@ -1753,9 +2082,10 @@ impl App { // if not searching, we just go to the artist/etc we selected match self.state.search_section { SearchSection::Artists => { - let artist = match self.search_result_artists.get( - self.state.selected_search_artist.selected().unwrap_or(0) - ) { + let artist = match self + .search_result_artists + .get(self.state.selected_search_artist.selected().unwrap_or(0)) + { Some(artist) => artist, None => return, }; @@ -1770,7 +2100,11 @@ impl App { let artist = self.artists.iter().find(|a| a.id == artist_id); if let Some(art) = artist { - let index = self.artists.iter().position(|a| a.id == art.id).unwrap_or(0); + let index = self + .artists + .iter() + .position(|a| a.id == art.id) + .unwrap_or(0); self.artist_select_by_index(index); let selected = self.state.selected_artist.selected().unwrap_or(0); @@ -1780,9 +2114,10 @@ impl App { } } SearchSection::Albums => { - let album = match self.search_result_albums.get( - self.state.selected_search_album.selected().unwrap_or(0) - ) { + let album = match self + .search_result_albums + .get(self.state.selected_search_album.selected().unwrap_or(0)) + { Some(album) => album, None => return, }; @@ -1801,7 +2136,11 @@ impl App { // is rust crazy, or is it me? if let Some(artist) = self.artists.iter().find(|a| a.id == artist_id) { - let index = self.artists.iter().position(|a| a.id == artist.id).unwrap_or(0); + let index = self + .artists + .iter() + .position(|a| a.id == artist.id) + .unwrap_or(0); self.artist_select_by_index(index); let selected = self.state.selected_artist.selected().unwrap_or(0); @@ -1810,16 +2149,23 @@ impl App { self.track_select_by_index(0); // now find the first track that matches this album - if let Some(track) = self.tracks.iter().find(|t| t.album_id == album_id) { - let index = self.tracks.iter().position(|t| t.id == track.id).unwrap_or(0); + if let Some(track) = + self.tracks.iter().find(|t| t.album_id == album_id) + { + let index = self + .tracks + .iter() + .position(|t| t.id == track.id) + .unwrap_or(0); self.track_select_by_index(index); } } } SearchSection::Tracks => { - let track = match self.search_result_tracks.get( - self.state.selected_search_track.selected().unwrap_or(0) - ) { + let track = match self + .search_result_tracks + .get(self.state.selected_search_track.selected().unwrap_or(0)) + { Some(track) => track, None => return, }; @@ -1838,7 +2184,9 @@ impl App { if self.original_artists.iter().any(|a| a.id == artist.id) { let discography = client.discography(&artist.id, false).await; if let Ok(discography) = discography { - if let Some(_) = discography.items.iter().find(|t| t.id == track_id) { + if let Some(_) = + discography.items.iter().find(|t| t.id == track_id) + { artist_id = artist.id.clone(); break; } @@ -1848,7 +2196,9 @@ impl App { if artist_id.is_empty() { // if this fails, let's last attempt to find the artist by name for artist in album_artists { - if let Some(a) = self.original_artists.iter().find(|a| a.name == artist.name) { + if let Some(a) = + self.original_artists.iter().find(|a| a.name == artist.name) + { artist_id = a.id.clone(); break; } @@ -1857,7 +2207,11 @@ impl App { return; } } - let index = self.artists.iter().position(|a| a.id == artist_id).unwrap_or(0); + let index = self + .artists + .iter() + .position(|a| a.id == artist_id) + .unwrap_or(0); self.artist_select_by_index(index); self.state.artists_search_term = String::from(""); @@ -1869,7 +2223,11 @@ impl App { // now find the first track that matches this album if let Some(track) = self.tracks.iter().find(|t| t.id == track_id) { - let index = self.tracks.iter().position(|t| t.id == track.id).unwrap_or(0); + let index = self + .tracks + .iter() + .position(|t| t.id == track.id) + .unwrap_or(0); self.track_select_by_index(index); } } @@ -2052,4 +2410,3 @@ pub enum SearchSection { Albums, Tracks, } - diff --git a/src/library.rs b/src/library.rs index 58384ad..ea5f911 100644 --- a/src/library.rs +++ b/src/library.rs @@ -13,24 +13,20 @@ Main Library tab use crate::client::{Album, Artist, DiscographySong}; use crate::helpers; +use crate::keyboard::*; use crate::tui::{App, Repeat}; -use crate::keyboard::{*}; -use souvlaki::{MediaMetadata, MediaPosition}; -use ratatui_image::{Resize, StatefulImage, protocol::ImageSource}; use image::{DynamicImage, Rgba}; -use std::time::Duration; use layout::Flex; use ratatui::{ - Frame, - widgets::{ - Block, - Borders, - Paragraph - }, prelude::*, widgets::*, + widgets::{Block, Borders, Paragraph}, + Frame, }; +use ratatui_image::{protocol::ImageSource, Resize, StatefulImage}; +use souvlaki::{MediaMetadata, MediaPosition}; +use std::time::Duration; impl App { pub fn render_home(&mut self, app_container: Rect, frame: &mut Frame) { @@ -48,23 +44,38 @@ impl App { .direction(Direction::Vertical) .constraints(vec![Constraint::Percentage(100), Constraint::Length(8)]) .split(outer_layout[1]); - - let show_lyrics = self.lyrics.as_ref().is_some_and(|(_, lyrics, _)| !lyrics.is_empty()); + + let show_lyrics = self + .lyrics + .as_ref() + .is_some_and(|(_, lyrics, _)| !lyrics.is_empty()); let right = Layout::default() .direction(Direction::Vertical) - .constraints(if show_lyrics && !self.lyrics.as_ref().map_or(true, |(_, lyrics, _)| lyrics.len() == 1) { - vec![Constraint::Percentage(68), Constraint::Percentage(32)] - } else { - vec![Constraint::Min(3), Constraint::Percentage(100)] - }) + .constraints( + if show_lyrics + && !self + .lyrics + .as_ref() + .map_or(true, |(_, lyrics, _)| lyrics.len() == 1) + { + vec![Constraint::Percentage(68), Constraint::Percentage(32)] + } else { + vec![Constraint::Min(3), Constraint::Percentage(100)] + }, + ) .split(outer_layout[2]); - + // update mpris metadata - if self.active_song_id != self.mpris_active_song_id && self.state.current_playback_state.current_index != self.state.current_playback_state.last_index && self.state.current_playback_state.duration > 0.0 { + if self.active_song_id != self.mpris_active_song_id + && self.state.current_playback_state.current_index + != self.state.current_playback_state.last_index + && self.state.current_playback_state.duration > 0.0 + { self.mpris_active_song_id = self.active_song_id.clone(); let cover_url = format!("file://{}", self.cover_art_path); let metadata = match self - .state.queue + .state + .queue .get(self.state.current_playback_state.current_index as usize) { Some(song) => { @@ -73,7 +84,9 @@ impl App { artist: Some(song.artist.as_str()), album: Some(song.album.as_str()), cover_url: Some(cover_url.as_str()), - duration: Some(Duration::from_secs((self.state.current_playback_state.duration) as u64)), + duration: Some(Duration::from_secs( + (self.state.current_playback_state.duration) as u64, + )), }; metadata } @@ -93,8 +106,18 @@ impl App { if self.paused != self.mpris_paused && self.state.current_playback_state.duration > 0.0 { self.mpris_paused = self.paused; if let Some(ref mut controls) = self.controls { - let progress = self.state.current_playback_state.duration * self.state.current_playback_state.percentage / 100.0; - let _ = controls.set_playback(if self.paused { souvlaki::MediaPlayback::Paused { progress: Some(MediaPosition(Duration::from_secs_f64(progress))) } } else { souvlaki::MediaPlayback::Playing { progress: Some(MediaPosition(Duration::from_secs_f64(progress))) } }); + let progress = self.state.current_playback_state.duration + * self.state.current_playback_state.percentage + / 100.0; + let _ = controls.set_playback(if self.paused { + souvlaki::MediaPlayback::Paused { + progress: Some(MediaPosition(Duration::from_secs_f64(progress))), + } + } else { + souvlaki::MediaPlayback::Playing { + progress: Some(MediaPosition(Duration::from_secs_f64(progress))), + } + }); } } @@ -108,22 +131,22 @@ impl App { fn render_library_left(&mut self, frame: &mut Frame, outer_layout: std::rc::Rc<[Rect]>) { // LEFT sidebar construct. large_art flag determines the split let left = if self.state.large_art { - // this is a temporary hack to get the image area size. + // this is a temporary hack to get the image area size. // hopefully ratatui-image will let me get it directly at some point - if let (Some(cover_art), Some(picker)) = (self.cover_art.as_mut(), self.picker.as_ref()) { + if let (Some(cover_art), Some(picker)) = (self.cover_art.as_mut(), self.picker.as_ref()) + { let outer_area = outer_layout[0]; let block_bottom = Block::default() .borders(Borders::ALL) - .title("Cover art").white().border_style(style::Color::White); + .title("Cover art") + .white() + .border_style(style::Color::White); let chunk_area = block_bottom.inner(outer_area); let font_size = picker.font_size(); - let image_source = ImageSource::new( - DynamicImage::new_rgba8(1, 1), - font_size, - Rgba([0,0,0,0]), - ); + let image_source = + ImageSource::new(DynamicImage::new_rgba8(1, 1), font_size, Rgba([0, 0, 0, 0])); match Resize::Scale(None).needs_resize( &image_source, @@ -139,7 +162,7 @@ impl App { let layout = Layout::default() .direction(Direction::Vertical) .constraints(vec![ - Constraint::Length(top_height), // artist list + Constraint::Length(top_height), // artist list Constraint::Length(block_total_height), // image ]) .split(outer_area); @@ -161,13 +184,11 @@ impl App { frame.render_stateful_widget(image, final_centered, cover_art); layout - }, - None => { - Layout::default() - .direction(Direction::Vertical) - .constraints(vec![Constraint::Percentage(100)]) - .split(outer_area) - }, + } + None => Layout::default() + .direction(Direction::Vertical) + .constraints(vec![Constraint::Percentage(100)]) + .split(outer_area), } } else { Layout::default() @@ -195,7 +216,6 @@ impl App { } fn render_library_artists(&mut self, frame: &mut Frame, left: std::rc::Rc<[Rect]>) { - let artist_block = match self.state.active_section { ActiveSection::List => Block::new() .borders(Borders::ALL) @@ -215,10 +235,14 @@ impl App { _ => Style::default() .add_modifier(Modifier::BOLD) .bg(Color::Indexed(236)) - .fg(Color::White) + .fg(Color::White), }; - - if let Some(song) = self.state.queue.get(self.state.current_playback_state.current_index as usize) { + + if let Some(song) = self + .state + .queue + .get(self.state.current_playback_state.current_index as usize) + { if song.artist_items.iter().any(|a| a.id == selected_artist) { artist_highlight_style = artist_highlight_style.add_modifier(Modifier::ITALIC); } @@ -233,11 +257,19 @@ impl App { let items = artists .iter() .map(|artist| { - let color = if let Some(song) = self.state.queue.get(self.state.current_playback_state.current_index as usize) { + let color = if let Some(song) = self + .state + .queue + .get(self.state.current_playback_state.current_index as usize) + { if song.artist_items.iter().any(|a| a.id == artist.id) { self.primary_color - } else { Color::White } - } else { Color::White }; + } else { + Color::White + } + } else { + Color::White + }; // underline the matching search subsequence ranges let mut item = Text::default(); @@ -256,7 +288,7 @@ impl App { item.push_span(Span::styled( &artist.name[start..end], - Style::default().fg(color).underlined() + Style::default().fg(color).underlined(), )); last_end = end; @@ -286,22 +318,23 @@ impl App { artist_block .title_alignment(Alignment::Right) .title_top(Line::from("All").left_aligned()) - .title_top(format!("({} artists)", self.artists.len())).title_position(block::Position::Bottom) + .title_top(format!("({} artists)", self.artists.len())) + .title_position(block::Position::Bottom) } else { artist_block .title_alignment(Alignment::Right) - .title_top(Line::from( - format!("Matching: {}", self.state.artists_search_term) - ).left_aligned()) - .title_top(format!("({} artists)", items_len)).title_position(block::Position::Bottom) + .title_top( + Line::from(format!("Matching: {}", self.state.artists_search_term)) + .left_aligned(), + ) + .title_top(format!("({} artists)", items_len)) + .title_position(block::Position::Bottom) }) .highlight_symbol(">>") - .highlight_style( - artist_highlight_style - ) + .highlight_style(artist_highlight_style) .scroll_padding(10) .repeat_highlight_symbol(true); - + frame.render_stateful_widget(list, left[0], &mut self.state.selected_artist); frame.render_stateful_widget( @@ -324,7 +357,7 @@ impl App { .borders(Borders::ALL) .title(format!("Searching: {}", self.state.artists_search_term)) .border_style(self.primary_color), - left[0], + left[0], ); } } @@ -349,10 +382,14 @@ impl App { _ => Style::default() .add_modifier(Modifier::BOLD) .bg(Color::Indexed(236)) - .fg(Color::White) + .fg(Color::White), }; - if let Some(song) = self.state.queue.get(self.state.current_playback_state.current_index as usize) { + if let Some(song) = self + .state + .queue + .get(self.state.current_playback_state.current_index as usize) + { if song.parent_id == selected_album { album_highlight_style = album_highlight_style.add_modifier(Modifier::ITALIC); } @@ -366,11 +403,19 @@ impl App { let items = albums .iter() .map(|album| { - let color = if let Some(song) = self.state.queue.get(self.state.current_playback_state.current_index as usize) { + let color = if let Some(song) = self + .state + .queue + .get(self.state.current_playback_state.current_index as usize) + { if song.parent_id == album.id { self.primary_color - } else { Color::White } - } else { Color::White }; + } else { + Color::White + } + } else { + Color::White + }; // underline the matching search subsequence ranges let mut item = Text::default(); @@ -389,7 +434,7 @@ impl App { item.push_span(Span::styled( &album.name[start..end], - Style::default().fg(color).underlined() + Style::default().fg(color).underlined(), )); last_end = end; @@ -417,19 +462,20 @@ impl App { album_block .title_alignment(Alignment::Right) .title_top(Line::from("All").left_aligned()) - .title_top(format!("({} albums)", self.albums.len())).title_position(block::Position::Bottom) + .title_top(format!("({} albums)", self.albums.len())) + .title_position(block::Position::Bottom) } else { album_block .title_alignment(Alignment::Right) - .title_top(Line::from( - format!("Matching: {}", self.state.albums_search_term) - ).left_aligned()) - .title_top(format!("({} albums)", items_len)).title_position(block::Position::Bottom) + .title_top( + Line::from(format!("Matching: {}", self.state.albums_search_term)) + .left_aligned(), + ) + .title_top(format!("({} albums)", items_len)) + .title_position(block::Position::Bottom) }) .highlight_symbol(">>") - .highlight_style( - album_highlight_style - ) + .highlight_style(album_highlight_style) .scroll_padding(10) .repeat_highlight_symbol(true); @@ -452,22 +498,24 @@ impl App { if self.locally_searching && self.state.active_section == ActiveSection::List { frame.render_widget( Block::default() - .borders(Borders::ALL) + .borders(Borders::ALL) .title(format!("Searching: {}", self.state.albums_search_term)) .border_style(self.primary_color), - left[0], + left[0], ); } } /// Individual widget rendering functions pub fn render_library_right(&mut self, frame: &mut Frame, right: std::rc::Rc<[Rect]>) { - let show_lyrics = self.lyrics.as_ref().is_some_and(|(_, lyrics, _)| !lyrics.is_empty()); + let show_lyrics = self + .lyrics + .as_ref() + .is_some_and(|(_, lyrics, _)| !lyrics.is_empty()); let lyrics_block = match self.state.active_section { ActiveSection::Lyrics => Block::new() .borders(Borders::ALL) - .border_style(self.primary_color) - , + .border_style(self.primary_color), _ => Block::new() .borders(Borders::ALL) .border_style(Color::White), @@ -475,24 +523,21 @@ impl App { if !show_lyrics { let message_paragraph = Paragraph::new("No lyrics available") - .block( - lyrics_block.title("Lyrics"), - ) - .white() - .wrap(Wrap { trim: false }) - .alignment(Alignment::Center); - - frame.render_widget( - message_paragraph, right[0], - ); + .block(lyrics_block.title("Lyrics")) + .white() + .wrap(Wrap { trim: false }) + .alignment(Alignment::Center); + + frame.render_widget(message_paragraph, right[0]); } else if let Some((_, lyrics, time_synced)) = &self.lyrics { // this will show the lyrics in a scrolling list let items = lyrics .iter() .enumerate() .map(|(index, lyric)| { - - let style = if (index == self.state.current_lyric) && (index != self.state.selected_lyric.selected().unwrap_or(0)) { + let style = if (index == self.state.current_lyric) + && (index != self.state.selected_lyric.selected().unwrap_or(0)) + { Style::default().fg(self.primary_color) } else { Style::default().white() @@ -527,17 +572,19 @@ impl App { .highlight_symbol(">>") .highlight_style( Style::default() - .add_modifier(Modifier::BOLD) - .bg(Color::White) - .fg(Color::Indexed(232)) + .add_modifier(Modifier::BOLD) + .bg(Color::White) + .fg(Color::Indexed(232)), ) .repeat_highlight_symbol(false) .scroll_padding(10); frame.render_stateful_widget(list, right[0], &mut self.state.selected_lyric); - + // if lyrics are time synced, we will scroll to the current lyric if *time_synced { - let current_time = self.state.current_playback_state.duration * self.state.current_playback_state.percentage / 100.0; + let current_time = self.state.current_playback_state.duration + * self.state.current_playback_state.percentage + / 100.0; let current_time_microseconds = (current_time * 10_000_000.0) as u64; for (i, lyric) in lyrics.iter().enumerate() { if lyric.start >= current_time_microseconds { @@ -568,7 +615,8 @@ impl App { }; let items = self - .state.queue + .state + .queue .iter() .enumerate() .map(|(index, song)| { @@ -578,33 +626,62 @@ impl App { item.push_span(Span::styled("+ ", Style::default().fg(self.primary_color))); } if index == self.state.current_playback_state.current_index as usize { - item.push_span(Span::styled(song.name.as_str(), Style::default().fg(self.primary_color))); + item.push_span(Span::styled( + song.name.as_str(), + Style::default().fg(self.primary_color), + )); if song.is_favorite { item.push_span(Span::styled(" ♥", Style::default().fg(self.primary_color))); } - return ListItem::new(item) + return ListItem::new(item); } - item.push_span(Span::styled(song.name.as_str(), Style::default().fg( - if self.state.repeat == Repeat::One { Color::DarkGray } else { Color::White } - ))); + item.push_span(Span::styled( + song.name.as_str(), + Style::default().fg(if self.state.repeat == Repeat::One { + Color::DarkGray + } else { + Color::White + }), + )); if song.is_favorite { item.push_span(Span::styled(" ♥", Style::default().fg(self.primary_color))); } - item.push_span(Span::styled(" - ", Style::default().fg(if self.state.repeat == Repeat::One { Color::DarkGray } else { Color::White }))); - item.push_span(Span::styled(song.artist.as_str(), Style::default().fg(Color::DarkGray))); + item.push_span(Span::styled( + " - ", + Style::default().fg(if self.state.repeat == Repeat::One { + Color::DarkGray + } else { + Color::White + }), + )); + item.push_span(Span::styled( + song.artist.as_str(), + Style::default().fg(Color::DarkGray), + )); ListItem::new(item) }) .collect::>(); let list = List::new(items) - .block(queue_block - .title_alignment(Alignment::Right) - .title_top(Line::from("Queue").left_aligned()) - .title_top( - if self.state.queue.is_empty() { String::from("") } else { format!("({}/{})", self.state.current_playback_state.current_index + 1, self.state.queue.len()) } - ).title_position(block::Position::Bottom) - .title_bottom(if self.state.shuffle { Line::from("(shuffle)").right_aligned() } else { Line::from("") }) + .block( + queue_block + .title_alignment(Alignment::Right) + .title_top(Line::from("Queue").left_aligned()) + .title_top(if self.state.queue.is_empty() { + String::from("") + } else { + format!( + "({}/{})", + self.state.current_playback_state.current_index + 1, + self.state.queue.len() + ) + }) + .title_position(block::Position::Bottom) + .title_bottom(if self.state.shuffle { + Line::from("(shuffle)").right_aligned() + } else { + Line::from("") + }), ) - .highlight_symbol(">>") .highlight_style( Style::default() @@ -614,7 +691,7 @@ impl App { ) .scroll_padding(5) .repeat_highlight_symbol(true); - + frame.render_stateful_widget(list, right[1], &mut self.state.selected_queue_item); } @@ -628,8 +705,11 @@ impl App { .border_style(style::Color::White), }; - let current_track = self.state.queue.get(self.state.current_playback_state.current_index as usize); - + let current_track = self + .state + .queue + .get(self.state.current_playback_state.current_index as usize); + let mut track_highlight_style = match self.state.active_section { ActiveSection::Tracks => Style::default() .add_modifier(Modifier::BOLD) @@ -673,13 +753,17 @@ impl App { frame.render_widget( Block::default() .borders(Borders::ALL) - .title(format!("Searching: {}", - if self.state.active_tab == ActiveTab::Library { self.state.tracks_search_term.clone() } - else { self.state.album_tracks_search_term.clone() }) - ) + .title(format!( + "Searching: {}", + if self.state.active_tab == ActiveTab::Library { + self.state.tracks_search_term.clone() + } else { + self.state.album_tracks_search_term.clone() + } + )) .title_bottom(searching_instructions.alignment(Alignment::Center)) .border_style(self.primary_color), - center[0], + center[0], ); } } @@ -700,13 +784,13 @@ impl App { } /// These are split into two basically the same functions because the tracks are rendered differently - /// + /// fn render_library_tracks_table( &mut self, - frame: &mut Frame, - center: &std::rc::Rc<[Rect]>, - track_block: Block, - track_highlight_style: Style + frame: &mut Frame, + center: &std::rc::Rc<[Rect]>, + track_block: Block, + track_highlight_style: Style, ) { let tracks = search_results(&self.tracks, &self.state.tracks_search_term, true) .iter() @@ -737,12 +821,15 @@ impl App { "♥".to_string() } else { "".to_string() - }).style(Style::default().fg(self.primary_color)), + }) + .style(Style::default().fg(self.primary_color)), Cell::from(""), Cell::from(""), Cell::from(""), Cell::from(duration), - ]).style(Style::default().fg(Color::White)).bold(); + ]) + .style(Style::default().fg(Color::White)) + .bold(); } // track.run_time_ticks is in microseconds @@ -776,7 +863,7 @@ impl App { title.push(Span::styled( &track.name[*start..*end], - Style::default().fg(color).underlined() + Style::default().fg(color).underlined(), )); last_end = *end; @@ -788,13 +875,15 @@ impl App { Style::default().fg(color), )); } - + Row::new(vec![ - Cell::from(format!("{}.", track.index_number)).style(if track.id == self.active_song_id { - Style::default().fg(color) - } else { - Style::default().fg(Color::DarkGray) - }), + Cell::from(format!("{}.", track.index_number)).style( + if track.id == self.active_song_id { + Style::default().fg(color) + } else { + Style::default().fg(Color::DarkGray) + }, + ), Cell::from(if all_subsequences.is_empty() { track.name.to_string().into() } else { @@ -805,7 +894,8 @@ impl App { "♥".to_string() } else { "".to_string() - }).style(Style::default().fg(self.primary_color)), + }) + .style(Style::default().fg(self.primary_color)), Cell::from(format!("{}", track.user_data.play_count)), Cell::from(if track.parent_index_number > 0 { format!("{}", track.parent_index_number) @@ -817,13 +907,18 @@ impl App { } else { "".to_string() }), - Cell::from(format!("{}{:02}:{:02}", hours_optional_text, minutes, seconds)), - ]).style(if track.id == self.active_song_id { + Cell::from(format!( + "{}{:02}:{:02}", + hours_optional_text, minutes, seconds + )), + ]) + .style(if track.id == self.active_song_id { Style::default().fg(self.primary_color).italic() } else { Style::default().fg(Color::White) }) - }).collect::>(); + }) + .collect::>(); let track_instructions = Line::from(vec![ " Help ".white(), @@ -831,7 +926,7 @@ impl App { " Quit ".white(), " ".fg(self.primary_color).bold(), ]); - + let widths = [ Constraint::Length(items.len().to_string().len() as u16 + 1), Constraint::Percentage(75), // title and track even width @@ -846,9 +941,10 @@ impl App { if self.tracks.is_empty() { let message_paragraph = Paragraph::new("jellyfin-tui") .block( - track_block.title("Tracks").padding(Padding::new( - 0, 0, center[0].height / 2, 0, - )).title_bottom(track_instructions.alignment(Alignment::Center)) + track_block + .title("Tracks") + .padding(Padding::new(0, 0, center[0].height / 2, 0)) + .title_bottom(track_instructions.alignment(Alignment::Center)), ) .wrap(Wrap { trim: false }) .alignment(Alignment::Center); @@ -857,7 +953,12 @@ impl App { } let items_len = items.len(); - let totaltime = self.tracks.iter().filter(|t| !t.id.starts_with("_album_")).map(|t| t.run_time_ticks / 10_000_000).sum::(); + let totaltime = self + .tracks + .iter() + .filter(|t| !t.id.starts_with("_album_")) + .map(|t| t.run_time_ticks / 10_000_000) + .sum::(); let seconds = totaltime % 60; let minutes = (totaltime / 60) % 60; let hours = totaltime / 60 / 60; @@ -867,43 +968,61 @@ impl App { }; let duration = format!("{}{:02}:{:02}", hours_optional_text, minutes, seconds); let table = Table::new(items, widths) - .block(if self.state.tracks_search_term.is_empty() && !self.state.current_artist.name.is_empty() { - track_block - .title(format!("{}", self.state.current_artist.name)) - .title_top(Line::from(format!("({} tracks - {})", self.tracks.iter().filter(|t| !t.id.starts_with("_album_")).count(), duration)).right_aligned()) - .title_bottom(track_instructions.alignment(Alignment::Center)) - } else { - track_block - .title(format!("Matching: {}", self.state.tracks_search_term)) - .title_top(Line::from(format!("({} tracks)", items_len)).right_aligned()) - .title_bottom(track_instructions.alignment(Alignment::Center)) - }) + .block( + if self.state.tracks_search_term.is_empty() + && !self.state.current_artist.name.is_empty() + { + track_block + .title(format!("{}", self.state.current_artist.name)) + .title_top( + Line::from(format!( + "({} tracks - {})", + self.tracks + .iter() + .filter(|t| !t.id.starts_with("_album_")) + .count(), + duration + )) + .right_aligned(), + ) + .title_bottom(track_instructions.alignment(Alignment::Center)) + } else { + track_block + .title(format!("Matching: {}", self.state.tracks_search_term)) + .title_top(Line::from(format!("({} tracks)", items_len)).right_aligned()) + .title_bottom(track_instructions.alignment(Alignment::Center)) + }, + ) .row_highlight_style(track_highlight_style) .highlight_symbol(">>") - .style( - Style::default().bg(Color::Reset) - ) + .style(Style::default().bg(Color::Reset)) .header( - Row::new(vec!["#", "Title", "Album", "♥", "Plays", "Disc", "Lyrics", "Duration"]) + Row::new(vec![ + "#", "Title", "Album", "♥", "Plays", "Disc", "Lyrics", "Duration", + ]) .style(Style::new().bold().white()) - .bottom_margin(0), + .bottom_margin(0), ); frame.render_widget(Clear, center[0]); frame.render_stateful_widget(table, center[0], &mut self.state.selected_track); - } + } fn render_album_tracks_table( &mut self, - frame: &mut Frame, - center: &std::rc::Rc<[Rect]>, - track_block: Block, - track_highlight_style: Style + frame: &mut Frame, + center: &std::rc::Rc<[Rect]>, + track_block: Block, + track_highlight_style: Style, ) { - let tracks = search_results(&self.album_tracks, &self.state.album_tracks_search_term, true) - .iter() - .map(|id| self.album_tracks.iter().find(|t| t.id == *id).unwrap()) - .collect::>(); + let tracks = search_results( + &self.album_tracks, + &self.state.album_tracks_search_term, + true, + ) + .iter() + .map(|id| self.album_tracks.iter().find(|t| t.id == *id).unwrap()) + .collect::>(); let items = tracks .iter() @@ -939,7 +1058,7 @@ impl App { title.push(Span::styled( &track.name[*start..*end], - Style::default().fg(color).underlined() + Style::default().fg(color).underlined(), )); last_end = *end; @@ -951,13 +1070,15 @@ impl App { Style::default().fg(color), )); } - + Row::new(vec![ - Cell::from(format!("{}.", track.index_number)).style(if track.id == self.active_song_id { - Style::default().fg(color) - } else { - Style::default().fg(Color::DarkGray) - }), + Cell::from(format!("{}.", track.index_number)).style( + if track.id == self.active_song_id { + Style::default().fg(color) + } else { + Style::default().fg(Color::DarkGray) + }, + ), Cell::from(if all_subsequences.is_empty() { track.name.to_string().into() } else { @@ -967,7 +1088,8 @@ impl App { "♥".to_string() } else { "".to_string() - }).style(Style::default().fg(self.primary_color)), + }) + .style(Style::default().fg(self.primary_color)), Cell::from(format!("{}", track.user_data.play_count)), Cell::from(if track.parent_index_number > 0 { format!("{}", track.parent_index_number) @@ -979,13 +1101,18 @@ impl App { } else { "".to_string() }), - Cell::from(format!("{}{:02}:{:02}", hours_optional_text, minutes, seconds)), - ]).style(if track.id == self.active_song_id { + Cell::from(format!( + "{}{:02}:{:02}", + hours_optional_text, minutes, seconds + )), + ]) + .style(if track.id == self.active_song_id { Style::default().fg(self.primary_color).italic() } else { Style::default().fg(Color::White) }) - }).collect::>(); + }) + .collect::>(); let track_instructions = Line::from(vec![ " Help ".white(), @@ -993,7 +1120,7 @@ impl App { " Quit ".white(), " ".fg(self.primary_color).bold(), ]); - + let widths = [ Constraint::Length(items.len().to_string().len() as u16 + 1), Constraint::Percentage(100), // title and track even width @@ -1007,9 +1134,10 @@ impl App { if self.album_tracks.is_empty() { let message_paragraph = Paragraph::new("jellyfin-tui") .block( - track_block.title("Tracks").padding(Padding::new( - 0, 0, center[0].height / 2, 0, - )).title_bottom(track_instructions.alignment(Alignment::Center)) + track_block + .title("Tracks") + .padding(Padding::new(0, 0, center[0].height / 2, 0)) + .title_bottom(track_instructions.alignment(Alignment::Center)), ) .wrap(Wrap { trim: false }) .alignment(Alignment::Center); @@ -1018,7 +1146,12 @@ impl App { } let items_len = items.len(); - let totaltime = self.album_tracks.iter().map(|t| t.run_time_ticks).sum::() / 10_000_000; + let totaltime = self + .album_tracks + .iter() + .map(|t| t.run_time_ticks) + .sum::() + / 10_000_000; let seconds = totaltime % 60; let minutes = (totaltime / 60) % 60; let hours = totaltime / 60 / 60; @@ -1028,27 +1161,41 @@ impl App { }; let duration = format!("{}{:02}:{:02}", hours_optional_text, minutes, seconds); let table = Table::new(items, widths) - .block(if self.state.album_tracks_search_term.is_empty() && !self.state.current_album.name.is_empty() { - track_block - .title(self.state.current_album.name.to_string()) - .title_top(Line::from(format!("({} tracks - {})", self.album_tracks.iter().filter(|t| !t.id.starts_with("_album_")).count(), duration)).right_aligned()) - .title_bottom(track_instructions.alignment(Alignment::Center)) - } else { - track_block - .title(format!("Matching: {}", self.state.album_tracks_search_term)) - .title_top(Line::from(format!("({} tracks)", items_len)).right_aligned()) - .title_bottom(track_instructions.alignment(Alignment::Center)) - }) + .block( + if self.state.album_tracks_search_term.is_empty() + && !self.state.current_album.name.is_empty() + { + track_block + .title(self.state.current_album.name.to_string()) + .title_top( + Line::from(format!( + "({} tracks - {})", + self.album_tracks + .iter() + .filter(|t| !t.id.starts_with("_album_")) + .count(), + duration + )) + .right_aligned(), + ) + .title_bottom(track_instructions.alignment(Alignment::Center)) + } else { + track_block + .title(format!("Matching: {}", self.state.album_tracks_search_term)) + .title_top(Line::from(format!("({} tracks)", items_len)).right_aligned()) + .title_bottom(track_instructions.alignment(Alignment::Center)) + }, + ) .row_highlight_style(track_highlight_style) .highlight_symbol(">>") - .style( - Style::default().bg(Color::Reset) - ) + .style(Style::default().bg(Color::Reset)) .header( - Row::new(vec!["#", "Title", "♥", "Plays", "Disc", "Lyrics", "Duration"]) + Row::new(vec![ + "#", "Title", "♥", "Plays", "Disc", "Lyrics", "Duration", + ]) .style(Style::new().bold().white()) - .bottom_margin(0), - ); + .bottom_margin(0), + ); frame.render_widget(Clear, center[0]); frame.render_stateful_widget(table, center[0], &mut self.state.selected_album_track); @@ -1056,7 +1203,8 @@ impl App { pub fn render_player(&mut self, frame: &mut Frame, center: &std::rc::Rc<[Rect]>) { let current_song = match self - .state.queue + .state + .queue .get(self.state.current_playback_state.current_index as usize) { Some(song) => { @@ -1084,7 +1232,7 @@ impl App { .constraints(if self.cover_art.is_some() && !self.state.large_art { vec![ Constraint::Percentage(2), - Constraint::Length((center[1].height) * 2 + 1), + Constraint::Length((center[1].height) * 2 + 1), Constraint::Percentage(0), Constraint::Percentage(93), Constraint::Percentage(2), @@ -1102,17 +1250,11 @@ impl App { if self.cover_art.is_some() && !self.state.large_art { let image = StatefulImage::default(); - frame.render_stateful_widget( - image, - bottom_split[1], - self.cover_art.as_mut().unwrap(), - ); + frame.render_stateful_widget(image, bottom_split[1], self.cover_art.as_mut().unwrap()); } let duration = match self.state.current_playback_state.duration { - 0.0 => { - "0:00 / 0:00".to_string() - } + 0.0 => "0:00 / 0:00".to_string(), _ => { let current_time = self.state.current_playback_state.duration * self.state.current_playback_state.percentage @@ -1134,11 +1276,13 @@ impl App { // current song frame.render_widget( - Paragraph::new(current_song).block( - Block::bordered() - .borders(Borders::NONE) - .padding(Padding::new(0, 0, 1, 0)), - ).style(Style::default().fg(Color::White)), + Paragraph::new(current_song) + .block( + Block::bordered() + .borders(Borders::NONE) + .padding(Padding::new(0, 0, 1, 0)), + ) + .style(Style::default().fg(Color::White)), layout[0], ); @@ -1153,10 +1297,7 @@ impl App { frame.render_widget( LineGauge::default() - .block( - Block::bordered() - .borders(Borders::NONE), - ) + .block(Block::bordered().borders(Borders::NONE)) .filled_style(if self.buffering { Style::default() .fg(self.primary_color) @@ -1174,15 +1315,17 @@ impl App { .style(Style::default().fg(Color::White)) .line_set(symbols::line::ROUNDED) .ratio(self.state.current_playback_state.percentage / 100_f64) - .label(Line::from( - format!( - "{} {:.0}% ", - if self.buffering { - self.spinner_stages[self.spinner] - } else if self.paused { "⏸︎" } else { "►" }, - self.state.current_playback_state.percentage - ) - )), + .label(Line::from(format!( + "{} {:.0}% ", + if self.buffering { + self.spinner_stages[self.spinner] + } else if self.paused { + "⏸︎" + } else { + "►" + }, + self.state.current_playback_state.percentage + ))), progress_bar_area[0], ); @@ -1190,7 +1333,8 @@ impl App { Some(ref metadata) => { let mut transcoding_text = String::from(""); let current_song = self - .state.queue + .state + .queue .get(self.state.current_playback_state.current_index as usize); if let Some(song) = current_song { if song.is_transcoded { @@ -1223,11 +1367,14 @@ impl App { ); frame.render_widget( - Paragraph::new(duration).centered().block( - Block::bordered() - .borders(Borders::NONE) - .padding(Padding::ZERO), - ).style(Style::default().fg(Color::White)), + Paragraph::new(duration) + .centered() + .block( + Block::bordered() + .borders(Borders::NONE) + .padding(Padding::ZERO), + ) + .style(Style::default().fg(Color::White)), progress_bar_area[1], ); } diff --git a/src/main.rs b/src/main.rs index a2f13de..e2c424f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,22 +11,20 @@ mod queue; mod search; mod tui; -use std::{io::stdout, vec}; use std::env; use std::panic; use std::sync::atomic::{AtomicBool, Ordering}; +use std::{io::stdout, vec}; -use libmpv2::{*}; +use libmpv2::*; use crossterm::{ + execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - execute }; // keyboard enhancement flags are used to allow for certain normally blocked key combinations... e.g. ctrl+enter... use crossterm::event::{ - KeyboardEnhancementFlags, - PushKeyboardEnhancementFlags, - PopKeyboardEnhancementFlags + KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, }; use ratatui::prelude::{CrosstermBackend, Terminal}; @@ -37,11 +35,13 @@ async fn main() { let args = env::args().collect::>(); if args.len() > 1 { if args[1] == "--version" { - println!("jellyfin-tui {version} (libmpv {major}.{minor} {ver})", + println!( + "jellyfin-tui {version} (libmpv {major}.{minor} {ver})", version = version, major = MPV_CLIENT_API_MAJOR, minor = MPV_CLIENT_API_MINOR, - ver = MPV_CLIENT_API_VERSION); + ver = MPV_CLIENT_API_VERSION + ); return; } if args[1] == "--help" { @@ -51,7 +51,8 @@ async fn main() { } if !args.contains(&String::from("--no-splash")) { - println!(" + println!( + " ⠀⠀⠀⠀⡴⠂⢩⡉⠉⠉⡖⢄⠀ ⠀⠀⠀⢸⠪⠄⠀⠀⠀⠀⠐⠂⢧⠀⠀⠀\x1b[94mjellyfin-tui\x1b[0m by dhonus ⠀⠀⠀⠙⢳⣢⢬⣁⠀⠛⠀⠂⡞ @@ -63,7 +64,8 @@ async fn main() { ⠀⠀⠀⠀⠈⠣⠀⢸⠀⠀⢠⠇⠀⠀⠀⠀This is free software (GPLv3). ⠀⠀⠀⠀⠀⠀⢠⠃⠀⠔⠁⠀⠀ ", - version, MPV_CLIENT_API_MAJOR, MPV_CLIENT_API_MINOR, MPV_CLIENT_API_VERSION); + version, MPV_CLIENT_API_MAJOR, MPV_CLIENT_API_MINOR, MPV_CLIENT_API_VERSION + ); } let client = client::Client::new(false).await; @@ -101,7 +103,7 @@ async fn main() { eprintln!("\n ! (×_×) panik: {}", info); eprintln!(" ! If you think this is a bug, please report it at https://github.com/dhonus/jellyfin-tui/issues"); })); - + let mut app = tui::App::default(); app.init(artists).await; @@ -110,10 +112,9 @@ async fn main() { execute!( stdout(), - PushKeyboardEnhancementFlags( - KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES - ) - ).ok(); + PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES) + ) + .ok(); let mut terminal = Terminal::new(CrosstermBackend::new(stdout())).unwrap(); diff --git a/src/mpris.rs b/src/mpris.rs index e574d17..71c0eeb 100644 --- a/src/mpris.rs +++ b/src/mpris.rs @@ -1,8 +1,12 @@ use crate::tui::{App, MpvState}; -use souvlaki::{MediaControlEvent, MediaControls, MediaPosition, SeekDirection}; #[cfg(target_os = "linux")] use souvlaki::PlatformConfig; -use std::{io::{stdout, Write}, sync::{Arc, Mutex}, time::Duration}; +use souvlaki::{MediaControlEvent, MediaControls, MediaPosition, SeekDirection}; +use std::{ + io::{stdout, Write}, + sync::{Arc, Mutex}, + time::Duration, +}; // linux only, macos requires a window and windows is unsupported pub fn mpris() -> Result> { @@ -26,30 +30,38 @@ pub fn mpris() -> Result> { } impl App { - /// Registers the media controls to the MpvState. Called after each mpv thread re-init. pub fn register_controls(&mut self, mpv_state: Arc>) { if let Some(ref mut controls) = self.controls { - controls.attach(move |event: MediaControlEvent| { - let lock = mpv_state.clone(); - let mut mpv = match lock.lock() { - Ok(mpv) => mpv, - Err(_) => { - return; - } - }; + controls + .attach(move |event: MediaControlEvent| { + let lock = mpv_state.clone(); + let mut mpv = match lock.lock() { + Ok(mpv) => mpv, + Err(_) => { + return; + } + }; - mpv.mpris_events.push(event); + mpv.mpris_events.push(event); - drop(mpv); - }) - .ok(); + drop(mpv); + }) + .ok(); } } pub fn update_mpris_position(&mut self, secs: f64) { if let Some(ref mut controls) = self.controls { - let _ = controls.set_playback(if self.paused { souvlaki::MediaPlayback::Paused { progress: Some(MediaPosition(Duration::from_secs_f64(secs))) } } else { souvlaki::MediaPlayback::Playing { progress: Some(MediaPosition(Duration::from_secs_f64(secs))) } }); + let _ = controls.set_playback(if self.paused { + souvlaki::MediaPlayback::Paused { + progress: Some(MediaPosition(Duration::from_secs_f64(secs))), + } + } else { + souvlaki::MediaPlayback::Playing { + progress: Some(MediaPosition(Duration::from_secs_f64(secs))), + } + }); } } @@ -72,7 +84,9 @@ impl App { let _ = client.stopped( &self.active_song_id, // position ticks - (self.state.current_playback_state.duration * self.state.current_playback_state.percentage * 100000.0) as u64, + (self.state.current_playback_state.duration + * self.state.current_playback_state.percentage + * 100000.0) as u64, ); let _ = mpv.mpv.command("playlist_next", &["force"]); if self.paused { @@ -82,7 +96,9 @@ impl App { self.update_mpris_position(0.0); } MediaControlEvent::Previous => { - let current_time = self.state.current_playback_state.duration * self.state.current_playback_state.percentage / 100.0; + let current_time = self.state.current_playback_state.duration + * self.state.current_playback_state.percentage + / 100.0; if current_time > 5.0 { let _ = mpv.mpv.command("seek", &["0.0", "absolute"]); } else { @@ -100,24 +116,32 @@ impl App { MediaControlEvent::Pause => { let _ = mpv.mpv.set_property("pause", true); self.paused = true; - }, + } MediaControlEvent::SeekBy(direction, duration) => { - let rel = duration.as_secs_f64() * (if matches!(direction, SeekDirection::Forward) { 1.0 } else { -1.0 }); + let rel = duration.as_secs_f64() + * (if matches!(direction, SeekDirection::Forward) { + 1.0 + } else { + -1.0 + }); let mut stdout = stdout().lock(); let _ = stdout.write_fmt(format_args!("\nrel{:?} orig{:?}\n", rel, duration)); let _ = stdout.flush(); - let secs = self.state.current_playback_state.duration * self.state.current_playback_state.percentage / 100.0 + rel; + let secs = self.state.current_playback_state.duration + * self.state.current_playback_state.percentage + / 100.0 + + rel; self.update_mpris_position(secs); let _ = mpv.mpv.command("seek", &[&rel.to_string()]); - }, + } MediaControlEvent::SetPosition(position) => { let secs = position.0.as_secs_f64(); self.update_mpris_position(secs); let _ = mpv.mpv.command("seek", &[&secs.to_string(), "absolute"]); - }, + } MediaControlEvent::SetVolume(_volume) => { #[cfg(target_os = "linux")] { @@ -128,7 +152,7 @@ impl App { let _ = controls.set_volume(volume); } } - }, + } _ => {} } } diff --git a/src/playlists.rs b/src/playlists.rs index 04f850f..a2f3577 100644 --- a/src/playlists.rs +++ b/src/playlists.rs @@ -3,18 +3,15 @@ The playlists tab is rendered here. -------------------------- */ use crate::client::Playlist; +use crate::keyboard::*; use crate::tui::App; -use crate::keyboard::{*}; use image::{DynamicImage, Rgba}; use ratatui::{ - Frame, - widgets::{ - Block, - Borders, - }, prelude::*, widgets::*, + widgets::{Block, Borders}, + Frame, }; use ratatui_image::protocol::ImageSource; use ratatui_image::{Resize, StatefulImage}; @@ -29,24 +26,24 @@ impl App { Constraint::Percentage(22), ]) .split(app_container); - + let left = if self.state.large_art { - // this is a temporary hack to get the image area size. + // this is a temporary hack to get the image area size. // hopefully ratatui-image will let me get it directly at some point - if let (Some(cover_art), Some(picker)) = (self.cover_art.as_mut(), self.picker.as_ref()) { + if let (Some(cover_art), Some(picker)) = (self.cover_art.as_mut(), self.picker.as_ref()) + { let outer_area = outer_layout[0]; let block_bottom = Block::default() .borders(Borders::ALL) - .title("Cover art").white().border_style(style::Color::White); + .title("Cover art") + .white() + .border_style(style::Color::White); let chunk_area = block_bottom.inner(outer_area); let font_size = picker.font_size(); - let image_source = ImageSource::new( - DynamicImage::new_rgba8(1, 1), - font_size, - Rgba([0,0,0,0]), - ); + let image_source = + ImageSource::new(DynamicImage::new_rgba8(1, 1), font_size, Rgba([0, 0, 0, 0])); match Resize::Scale(None).needs_resize( &image_source, @@ -62,7 +59,7 @@ impl App { let layout = Layout::default() .direction(Direction::Vertical) .constraints(vec![ - Constraint::Length(top_height), // artist list + Constraint::Length(top_height), // artist list Constraint::Length(block_total_height), // image ]) .split(outer_area); @@ -84,13 +81,11 @@ impl App { frame.render_stateful_widget(image, final_centered, cover_art); layout - }, - None => { - Layout::default() - .direction(Direction::Vertical) - .constraints(vec![Constraint::Percentage(100)]) - .split(outer_area) - }, + } + None => Layout::default() + .direction(Direction::Vertical) + .constraints(vec![Constraint::Percentage(100)]) + .split(outer_area), } } else { Layout::default() @@ -111,15 +106,25 @@ impl App { .direction(Direction::Vertical) .constraints(vec![Constraint::Percentage(100), Constraint::Length(8)]) .split(outer_layout[1]); - - let show_lyrics = self.lyrics.as_ref().is_some_and(|(_, lyrics, _)| !lyrics.is_empty()); + + let show_lyrics = self + .lyrics + .as_ref() + .is_some_and(|(_, lyrics, _)| !lyrics.is_empty()); let right = Layout::default() .direction(Direction::Vertical) - .constraints(if show_lyrics && !self.lyrics.as_ref().map_or(true, |(_, lyrics, _)| lyrics.len() == 1) { - vec![Constraint::Percentage(68), Constraint::Percentage(32)] - } else { - vec![Constraint::Min(3), Constraint::Percentage(100)] - }) + .constraints( + if show_lyrics + && !self + .lyrics + .as_ref() + .map_or(true, |(_, lyrics, _)| lyrics.len() == 1) + { + vec![Constraint::Percentage(68), Constraint::Percentage(32)] + } else { + vec![Constraint::Min(3), Constraint::Percentage(100)] + }, + ) .split(outer_layout[2]); let playlist_block = match self.state.active_section { @@ -130,7 +135,7 @@ impl App { .borders(Borders::ALL) .border_style(style::Color::White), }; - + let selected_playlist = self.get_id_of_selected(&self.playlists, Selectable::Playlist); let mut playlist_highlight_style = match self.state.active_section { ActiveSection::List => Style::default() @@ -149,7 +154,12 @@ impl App { } let playlists = search_results(&self.playlists, &self.state.playlists_search_term, true) .iter() - .map(|id| self.playlists.iter().find(|playlist| playlist.id == *id).unwrap()) + .map(|id| { + self.playlists + .iter() + .find(|playlist| playlist.id == *id) + .unwrap() + }) .collect::>(); let items = playlists @@ -178,7 +188,7 @@ impl App { item.push_span(Span::styled( &playlist.name[start..end], - Style::default().fg(color).underlined() + Style::default().fg(color).underlined(), )); last_end = end; @@ -203,22 +213,23 @@ impl App { playlist_block .title_alignment(Alignment::Right) .title_top(Line::from("All").left_aligned()) - .title_top(format!("({} playlists)", self.playlists.len())).title_position(block::Position::Bottom) + .title_top(format!("({} playlists)", self.playlists.len())) + .title_position(block::Position::Bottom) } else { playlist_block .title_alignment(Alignment::Right) - .title_top(Line::from( - format!("Matching {}", self.state.playlists_search_term) - ).left_aligned()) - .title_top(format!("({} playlists)", items_len)).title_position(block::Position::Bottom) + .title_top( + Line::from(format!("Matching {}", self.state.playlists_search_term)) + .left_aligned(), + ) + .title_top(format!("({} playlists)", items_len)) + .title_position(block::Position::Bottom) }) .highlight_symbol(">>") - .highlight_style( - playlist_highlight_style - ) + .highlight_style(playlist_highlight_style) .scroll_padding(10) .repeat_highlight_symbol(true); - + frame.render_stateful_widget(list, left[0], &mut self.state.selected_playlist); frame.render_stateful_widget( @@ -243,7 +254,7 @@ impl App { .borders(Borders::ALL) .border_style(style::Color::White), }; - + let track_highlight_style = match self.state.active_section { ActiveSection::Tracks => Style::default() .bg(Color::White) @@ -255,10 +266,14 @@ impl App { .add_modifier(Modifier::BOLD), }; - let playlist_tracks = search_results(&self.playlist_tracks, &self.state.playlist_tracks_search_term, true) - .iter() - .map(|id| self.playlist_tracks.iter().find(|t| t.id == *id).unwrap()) - .collect::>(); + let playlist_tracks = search_results( + &self.playlist_tracks, + &self.state.playlist_tracks_search_term, + true, + ) + .iter() + .map(|id| self.playlist_tracks.iter().find(|t| t.id == *id).unwrap()) + .collect::>(); let items = playlist_tracks .iter() @@ -275,7 +290,9 @@ impl App { Cell::from(""), Cell::from(""), Cell::from(""), - ]).style(Style::default().fg(Color::White)).bold(); + ]) + .style(Style::default().fg(Color::White)) + .bold(); } // track.run_time_ticks is in microseconds @@ -309,7 +326,7 @@ impl App { title.push(Span::styled( &track.name[*start..*end], - Style::default().fg(color).underlined() + Style::default().fg(color).underlined(), )); last_end = *end; @@ -321,44 +338,59 @@ impl App { Style::default().fg(color), )); } - + Row::new(vec![ - Cell::from(format!("{}.", index + 1)).style(if track.id == self.active_song_id { - Style::default().fg(color) - } else { - Style::default().fg(Color::DarkGray) - }), + Cell::from(format!("{}.", index + 1)).style( + if track.id == self.active_song_id { + Style::default().fg(color) + } else { + Style::default().fg(Color::DarkGray) + }, + ), Cell::from(if all_subsequences.is_empty() { track.name.to_string().into() } else { Line::from(title) }), - Cell::from(track.artist_items.iter().map(|artist| artist.name.clone()).collect::>().join(", ")), + Cell::from( + track + .artist_items + .iter() + .map(|artist| artist.name.clone()) + .collect::>() + .join(", "), + ), Cell::from(track.album.clone()), Cell::from(if track.user_data.is_favorite { "♥".to_string() } else { "".to_string() - }).style(Style::default().fg(self.primary_color)), + }) + .style(Style::default().fg(self.primary_color)), Cell::from(format!("{}", track.user_data.play_count)), Cell::from(if track.has_lyrics { "✓".to_string() } else { "".to_string() }), - Cell::from(format!("{}{:02}:{:02}", hours_optional_text, minutes, seconds)), - ]).style(if track.id == self.active_song_id { + Cell::from(format!( + "{}{:02}:{:02}", + hours_optional_text, minutes, seconds + )), + ]) + .style(if track.id == self.active_song_id { Style::default().fg(self.primary_color).italic() } else { Style::default().fg(Color::White) }) - }).collect::>(); + }) + .collect::>(); let track_instructions = Line::from(vec![ - " Help ".white(), - "".fg(self.primary_color).bold(), - " Quit ".white(), - " ".fg(self.primary_color).bold(), + " Help ".white(), + "".fg(self.primary_color).bold(), + " Quit ".white(), + " ".fg(self.primary_color).bold(), ]); let widths = [ Constraint::Length(items.len().to_string().len() as u16 + 1), @@ -377,13 +409,14 @@ impl App { } else { "No tracks in the current playlist".to_string() }) - .block( - track_block.title("Tracks").padding(Padding::new( - 0, 0, center[0].height / 2, 0, - )).title_bottom(track_instructions.alignment(Alignment::Center)) - ) - .wrap(Wrap { trim: false }) - .alignment(Alignment::Center); + .block( + track_block + .title("Tracks") + .padding(Padding::new(0, 0, center[0].height / 2, 0)) + .title_bottom(track_instructions.alignment(Alignment::Center)), + ) + .wrap(Wrap { trim: false }) + .alignment(Alignment::Center); frame.render_widget(message_paragraph, center[0]); } else { let items_len = items.len(); @@ -398,26 +431,42 @@ impl App { let duration = format!("{}{:02}:{:02}", hours_optional_text, minutes, seconds); let table = Table::new(items, widths) - .block(if self.state.playlist_tracks_search_term.is_empty() && !self.state.current_playlist.name.is_empty() { - track_block - .title(self.state.current_playlist.name.to_string()) - .title_top(Line::from(format!("({} tracks - {})", self.playlist_tracks.len(), duration)).right_aligned()) - .title_bottom(track_instructions.alignment(Alignment::Center)) - } else { - track_block - .title(format!("Matching: {}", self.state.playlist_tracks_search_term)) - .title_top(Line::from(format!("({} tracks)", items_len)).right_aligned()) - .title_bottom(track_instructions.alignment(Alignment::Center)) - }) + .block( + if self.state.playlist_tracks_search_term.is_empty() + && !self.state.current_playlist.name.is_empty() + { + track_block + .title(self.state.current_playlist.name.to_string()) + .title_top( + Line::from(format!( + "({} tracks - {})", + self.playlist_tracks.len(), + duration + )) + .right_aligned(), + ) + .title_bottom(track_instructions.alignment(Alignment::Center)) + } else { + track_block + .title(format!( + "Matching: {}", + self.state.playlist_tracks_search_term + )) + .title_top( + Line::from(format!("({} tracks)", items_len)).right_aligned(), + ) + .title_bottom(track_instructions.alignment(Alignment::Center)) + }, + ) .row_highlight_style(track_highlight_style) .highlight_symbol(">>") - .style( - Style::default().bg(Color::Reset) - ) + .style(Style::default().bg(Color::Reset)) .header( - Row::new(vec!["#", "Title", "Artist", "Album", "♥", "Plays", "Lyrics", "Duration"]) + Row::new(vec![ + "#", "Title", "Artist", "Album", "♥", "Plays", "Lyrics", "Duration", + ]) .style(Style::new().bold().white()) - .bottom_margin(0), + .bottom_margin(0), ); frame.render_widget(Clear, center[0]); frame.render_stateful_widget(table, center[0], &mut self.state.selected_playlist_track); @@ -434,16 +483,19 @@ impl App { frame.render_widget( Block::default() .borders(Borders::ALL) - .title(format!("Searching: {}", self.state.playlist_tracks_search_term)) + .title(format!( + "Searching: {}", + self.state.playlist_tracks_search_term + )) .title_bottom(searching_instructions.alignment(Alignment::Center)) .border_style(self.primary_color), - center[0], + center[0], ); } if self.state.active_section == ActiveSection::List { frame.render_widget( Block::default() - .borders(Borders::ALL) + .borders(Borders::ALL) .title(format!("Searching: {}", self.state.playlists_search_term)) .border_style(self.primary_color), left[0], diff --git a/src/popup.rs b/src/popup.rs index 35bc4b9..0b9c910 100644 --- a/src/popup.rs +++ b/src/popup.rs @@ -18,7 +18,8 @@ use serde::{Deserialize, Serialize}; use crate::{ client::{Artist, Playlist, ScheduledTask}, - keyboard::{search_results, ActiveSection, ActiveTab, Selectable}, tui::{Filter, Sort}, + keyboard::{search_results, ActiveSection, ActiveTab, Selectable}, + tui::{Filter, Sort}, }; /// helper function to create a centered rect using up certain percentage of the available rect `r` @@ -174,7 +175,7 @@ impl PopupMenu { match self { PopupMenu::GenericMessage { title, .. } => title.to_string(), // ---------- Global commands ---------- // - PopupMenu::GlobalRoot { .. }=> "Global Commands".to_string(), + PopupMenu::GlobalRoot { .. } => "Global Commands".to_string(), PopupMenu::GlobalRunScheduledTask { .. } => "Run a scheduled task".to_string(), PopupMenu::GlobalShuffle { .. } => "Global Shuffle".to_string(), // ---------- Playlists ---------- // @@ -183,8 +184,8 @@ impl PopupMenu { PopupMenu::PlaylistConfirmRename { .. } => "Confirm Rename".to_string(), PopupMenu::PlaylistConfirmDelete { .. } => "Confirm Delete".to_string(), PopupMenu::PlaylistCreate { .. } => "Create Playlist".to_string(), - PopupMenu::PlaylistsChangeSort { } => "Change sort order".to_string(), - PopupMenu::PlaylistsChangeFilter { } => "Change filter".to_string(), + PopupMenu::PlaylistsChangeSort {} => "Change sort order".to_string(), + PopupMenu::PlaylistsChangeFilter {} => "Change filter".to_string(), // ---------- Tracks ---------- // PopupMenu::TrackRoot { track_name, .. } => track_name.to_string(), PopupMenu::TrackAddToPlaylist { track_name, .. } => track_name.to_string(), @@ -197,12 +198,12 @@ impl PopupMenu { PopupMenu::ArtistJumpToCurrent { artists, .. } => { format!("Which of these {} to jump to?", artists.len()) } - PopupMenu::ArtistsChangeFilter { } => "Change filter".to_string(), - PopupMenu::ArtistsChangeSort { } => "Change sort".to_string(), + PopupMenu::ArtistsChangeFilter {} => "Change filter".to_string(), + PopupMenu::ArtistsChangeSort {} => "Change sort".to_string(), // ---------- Albums ---------- // - PopupMenu::AlbumsRoot { } => "Albums".to_string(), - PopupMenu::AlbumsChangeFilter { } => "Change filter".to_string(), - PopupMenu::AlbumsChangeSort { } => "Change sort".to_string(), + PopupMenu::AlbumsRoot {} => "Albums".to_string(), + PopupMenu::AlbumsChangeFilter {} => "Change filter".to_string(), + PopupMenu::AlbumsChangeSort {} => "Change sort".to_string(), // ---------- Album tracks ---------- // PopupMenu::AlbumTrackRoot { track_name, .. } => track_name.to_string(), } @@ -247,7 +248,10 @@ impl PopupMenu { ], PopupMenu::GlobalRunScheduledTask { tasks } => { let mut actions = vec![]; - let mut categories = tasks.iter().map(|t| t.category.clone()).collect::>(); + let mut categories = tasks + .iter() + .map(|t| t.category.clone()) + .collect::>(); categories.sort(); categories.dedup(); for category in categories { @@ -261,19 +265,33 @@ impl PopupMenu { } actions } - PopupMenu::GlobalShuffle { tracks_n, only_played, only_unplayed } => vec![ + PopupMenu::GlobalShuffle { + tracks_n, + only_played, + only_unplayed, + } => vec![ PopupAction { label: format!("Shuffle {} tracks. +/- to change", tracks_n), action: Action::None, style: Style::default(), }, PopupAction { - label: if *only_played { "✓ Only played tracks" } else { " Only played tracks" }.to_string(), + label: if *only_played { + "✓ Only played tracks" + } else { + " Only played tracks" + } + .to_string(), action: Action::OnlyPlayed, style: Style::default(), }, PopupAction { - label: if *only_unplayed { "✓ Only unplayed tracks" } else { " Only unplayed tracks" }.to_string(), + label: if *only_unplayed { + "✓ Only unplayed tracks" + } else { + " Only unplayed tracks" + } + .to_string(), action: Action::OnlyUnplayed, style: Style::default(), }, @@ -281,7 +299,7 @@ impl PopupMenu { label: "Play".to_string(), action: Action::Play, style: Style::default(), - } + }, ], // ---------- Playlists ---------- PopupMenu::PlaylistRoot { .. } => vec![ @@ -410,7 +428,7 @@ impl PopupMenu { style: Style::default(), }, ], - PopupMenu::PlaylistsChangeSort { } => vec![ + PopupMenu::PlaylistsChangeSort {} => vec![ PopupAction { label: "Ascending".to_string(), action: Action::Ascending, @@ -422,7 +440,7 @@ impl PopupMenu { style: Style::default(), }, ], - PopupMenu::PlaylistsChangeFilter { } => vec![ + PopupMenu::PlaylistsChangeFilter {} => vec![ PopupAction { label: "Normal".to_string(), action: Action::Normal, @@ -546,7 +564,7 @@ impl PopupMenu { } actions } - PopupMenu::ArtistsChangeFilter { } => vec![ + PopupMenu::ArtistsChangeFilter {} => vec![ PopupAction { label: "Normal".to_string(), action: Action::Normal, @@ -558,7 +576,7 @@ impl PopupMenu { style: Style::default(), }, ], - PopupMenu::ArtistsChangeSort { } => vec![ + PopupMenu::ArtistsChangeSort {} => vec![ PopupAction { label: "Ascending".to_string(), action: Action::Ascending, @@ -571,7 +589,7 @@ impl PopupMenu { }, ], // ---------- Albums ---------- // - PopupMenu::AlbumsRoot { } => vec![ + PopupMenu::AlbumsRoot {} => vec![ PopupAction { label: "Jump to current album".to_string(), action: Action::JumpToCurrent, @@ -588,7 +606,7 @@ impl PopupMenu { style: Style::default(), }, ], - PopupMenu::AlbumsChangeFilter { } => vec![ + PopupMenu::AlbumsChangeFilter {} => vec![ PopupAction { label: "Normal".to_string(), action: Action::Normal, @@ -600,7 +618,7 @@ impl PopupMenu { style: Style::default(), }, ], - PopupMenu::AlbumsChangeSort { } => vec![ + PopupMenu::AlbumsChangeSort {} => vec![ PopupAction { label: "Ascending".to_string(), action: Action::Ascending, @@ -686,11 +704,16 @@ impl crate::tui::App { } /// This function handles some special keys for the popup menu - /// + /// async fn handle_special_keys(&mut self, key_event: KeyEvent) { match key_event.code { KeyCode::Char('+') => { - if let Some(PopupMenu::GlobalShuffle { tracks_n, only_played, only_unplayed }) = &self.popup.current_menu { + if let Some(PopupMenu::GlobalShuffle { + tracks_n, + only_played, + only_unplayed, + }) = &self.popup.current_menu + { self.popup.current_menu = Some(PopupMenu::GlobalShuffle { tracks_n: tracks_n + 10, only_played: *only_played, @@ -699,7 +722,12 @@ impl crate::tui::App { } } KeyCode::Char('-') => { - if let Some(PopupMenu::GlobalShuffle { tracks_n, only_played, only_unplayed }) = &self.popup.current_menu { + if let Some(PopupMenu::GlobalShuffle { + tracks_n, + only_played, + only_unplayed, + }) = &self.popup.current_menu + { if *tracks_n > 1 { self.popup.current_menu = Some(PopupMenu::GlobalShuffle { tracks_n: tracks_n - 10, @@ -832,7 +860,12 @@ impl crate::tui::App { self.close_popup(); } Action::RunScheduledTask => { - let tasks = self.client.as_ref()?.scheduled_tasks().await.unwrap_or(vec![]); + let tasks = self + .client + .as_ref()? + .scheduled_tasks() + .await + .unwrap_or(vec![]); self.popup.current_menu = Some(PopupMenu::GlobalRunScheduledTask { tasks }); self.popup.selected.select(Some(0)); } @@ -841,7 +874,10 @@ impl crate::tui::App { PopupMenu::GlobalRunScheduledTask { tasks } => { let selected = self.popup.selected.selected()?; let mut mapped_tasks = vec![]; - let mut categories = tasks.iter().map(|t| t.category.clone()).collect::>(); + let mut categories = tasks + .iter() + .map(|t| t.category.clone()) + .collect::>(); categories.sort(); categories.dedup(); for category in categories { @@ -862,7 +898,11 @@ impl crate::tui::App { }); } } - PopupMenu::GlobalShuffle { tracks_n, only_played, only_unplayed } => match action { + PopupMenu::GlobalShuffle { + tracks_n, + only_played, + only_unplayed, + } => match action { Action::None => { self.popup.selected.select_next(); } @@ -898,7 +938,12 @@ impl crate::tui::App { } } Action::Play => { - let tracks = self.client.as_ref()?.random_tracks(tracks_n, only_played, only_unplayed).await.unwrap_or(vec![]); + let tracks = self + .client + .as_ref()? + .random_tracks(tracks_n, only_played, only_unplayed) + .await + .unwrap_or(vec![]); self.replace_queue(&tracks, 0).await; self.close_popup(); self.state.preffered_global_shuffle = Some(PopupMenu::GlobalShuffle { @@ -928,16 +973,26 @@ impl crate::tui::App { playlists: self.playlists.clone(), }); self.popup.selected.select(Some(0)); - }, + } Action::JumpToCurrent => { - let current_track = self.state.queue.get(self.state.current_playback_state.current_index as usize)?; - let artist = self.artists.iter().find( - |a| current_track.artist_items.first().is_some_and(|item| a.id == item.id) - )?; + let current_track = self + .state + .queue + .get(self.state.current_playback_state.current_index as usize)?; + let artist = self.artists.iter().find(|a| { + current_track + .artist_items + .first() + .is_some_and(|item| a.id == item.id) + })?; let artist_id = artist.id.clone(); let current_track_id = current_track.id.clone(); if artist_id != self.state.current_artist.id { - let index = self.artists.iter().position(|a| a.id == artist_id).unwrap_or(0); + let index = self + .artists + .iter() + .position(|a| a.id == artist_id) + .unwrap_or(0); self.artist_select_by_index(index); self.discography(&artist_id).await; self.artists[index].jellyfintui_recently_added = false; @@ -951,7 +1006,7 @@ impl crate::tui::App { self.track_select_by_index(index); } self.close_popup(); - }, + } _ => { self.close_popup(); } @@ -983,7 +1038,7 @@ impl crate::tui::App { }); } } - }, + } _ => { self.close_popup(); } @@ -997,43 +1052,53 @@ impl crate::tui::App { match menu { PopupMenu::AlbumsRoot { .. } => match action { Action::JumpToCurrent => { - let current_track = self.state.queue.get(self.state.current_playback_state.current_index as usize)?; + let current_track = self + .state + .queue + .get(self.state.current_playback_state.current_index as usize)?; if !self.state.albums_search_term.is_empty() { - let items = search_results(&self.albums, &self.state.albums_search_term, true); - if let Some(album) = items.into_iter().position(|a| *a == current_track.parent_id) { + let items = + search_results(&self.albums, &self.state.albums_search_term, true); + if let Some(album) = items + .into_iter() + .position(|a| *a == current_track.parent_id) + { self.album_select_by_index(album); self.close_popup(); return Some(()); } } - let album = self.albums.iter().find( - |a| current_track.parent_id == a.id - )?; + let album = self + .albums + .iter() + .find(|a| current_track.parent_id == a.id)?; self.state.albums_search_term = String::from(""); let album_id = album.id.clone(); - let index = self.albums.iter().position(|a| a.id == album_id).unwrap_or(0); + let index = self + .albums + .iter() + .position(|a| a.id == album_id) + .unwrap_or(0); self.album_select_by_index(index); self.close_popup(); } Action::ChangeFilter => { self.popup.current_menu = Some(PopupMenu::AlbumsChangeFilter {}); - self.popup.selected.select( - match self.state.album_filter { - Filter::Normal => Some(0), - Filter::FavoritesFirst => Some(1), - } - ) + self.popup.selected.select(match self.state.album_filter { + Filter::Normal => Some(0), + Filter::FavoritesFirst => Some(1), + }) } Action::ChangeOrder => { self.popup.current_menu = Some(PopupMenu::AlbumsChangeSort {}); - self.popup.selected.select(Some( - match self.state.album_sort { + self.popup + .selected + .select(Some(match self.state.album_sort { Sort::Ascending => 0, Sort::Descending => 1, Sort::DateCreated => 2, - } - )); + })); } _ => {} }, @@ -1083,7 +1148,11 @@ impl crate::tui::App { return None; } }; - let items = search_results(&self.album_tracks, &self.state.album_tracks_search_term, true); + let items = search_results( + &self.album_tracks, + &self.state.album_tracks_search_term, + true, + ); let track = match items.get(selected) { Some(track) => { let track = self.album_tracks.iter().find(|t| t.id == *track)?; @@ -1103,17 +1172,26 @@ impl crate::tui::App { self.popup.selected.select(Some(0)); } Action::JumpToCurrent => { - let current_track = self.state.queue.get(self.state.current_playback_state.current_index as usize)?; - let album = self.albums.iter().find( - |a| current_track.parent_id == a.id - )?; + let current_track = self + .state + .queue + .get(self.state.current_playback_state.current_index as usize)?; + let album = self + .albums + .iter() + .find(|a| current_track.parent_id == a.id)?; let album_id = album.id.clone(); if album_id != self.state.current_album.id { - let index = self.albums.iter().position(|a| a.id == album_id).unwrap_or(0); + let index = self + .albums + .iter() + .position(|a| a.id == album_id) + .unwrap_or(0); self.artist_select_by_index(index); self.album_tracks(&album_id).await; } - if let Some(index) = self.album_tracks.iter().position(|t| t.id == track.id) { + if let Some(index) = self.album_tracks.iter().position(|t| t.id == track.id) + { self.track_select_by_index(index); } self.close_popup(); @@ -1148,7 +1226,7 @@ impl crate::tui::App { }); } } - }, + } _ => { self.close_popup(); } @@ -1172,7 +1250,11 @@ impl crate::tui::App { return None; } }; - let items = search_results(&self.playlist_tracks, &self.state.playlist_tracks_search_term, true); + let items = search_results( + &self.playlist_tracks, + &self.state.playlist_tracks_search_term, + true, + ); let track = match items.get(selected) { Some(track) => { let track = self.playlist_tracks.iter().find(|t| t.id == *track)?; @@ -1247,29 +1329,31 @@ impl crate::tui::App { track_name, track_id, playlists, - } => if let Action::AddToPlaylist = action { - let selected = self.popup.selected.selected()?; - let playlist_id = &playlists[selected].id; - if let Some(client) = self.client.as_ref() { - if let Ok(_) = client.add_to_playlist(&track_id, playlist_id).await { - self.popup.current_menu = Some(PopupMenu::GenericMessage { - title: "Track added".to_string(), - message: format!( - "Track {} successfully added to playlist {}.", - track_name, playlists[selected].name - ), - }); - } else { - self.popup.current_menu = Some(PopupMenu::GenericMessage { - title: "Error adding track".to_string(), - message: format!( - "Failed to add track {} to playlist {}.", - track_name, playlists[selected].name - ), - }); + } => { + if let Action::AddToPlaylist = action { + let selected = self.popup.selected.selected()?; + let playlist_id = &playlists[selected].id; + if let Some(client) = self.client.as_ref() { + if let Ok(_) = client.add_to_playlist(&track_id, playlist_id).await { + self.popup.current_menu = Some(PopupMenu::GenericMessage { + title: "Track added".to_string(), + message: format!( + "Track {} successfully added to playlist {}.", + track_name, playlists[selected].name + ), + }); + } else { + self.popup.current_menu = Some(PopupMenu::GenericMessage { + title: "Error adding track".to_string(), + message: format!( + "Failed to add track {} to playlist {}.", + track_name, playlists[selected].name + ), + }); + } } } - }, + } PopupMenu::PlaylistTracksRemove { track_name, track_id, @@ -1309,7 +1393,6 @@ impl crate::tui::App { } async fn apply_playlist_action(&mut self, action: &Action, menu: PopupMenu) -> Option<()> { - let id = self.get_id_of_selected(&self.playlists, Selectable::Playlist); let selected_playlist = self.playlists.iter().find(|p| p.id == id)?.clone(); @@ -1343,7 +1426,6 @@ impl crate::tui::App { } } Action::Rename => { - self.popup.current_menu = Some(PopupMenu::PlaylistSetName { playlist_name: selected_playlist.name.clone(), new_name: selected_playlist.name.clone(), @@ -1372,11 +1454,23 @@ impl crate::tui::App { Action::ChangeFilter => { self.popup.current_menu = Some(PopupMenu::PlaylistsChangeFilter {}); // self.popup.selected.select(Some(0)); - self.popup.selected.select(Some(if self.state.playlist_filter == Filter::Normal { 0 } else { 1 })); + self.popup.selected.select(Some( + if self.state.playlist_filter == Filter::Normal { + 0 + } else { + 1 + }, + )); } Action::ChangeOrder => { self.popup.current_menu = Some(PopupMenu::PlaylistsChangeSort {}); - self.popup.selected.select(Some(if self.state.playlist_sort == Sort::Ascending { 0 } else { 1 })); + self.popup.selected.select(Some( + if self.state.playlist_sort == Sort::Ascending { + 0 + } else { + 1 + }, + )); } _ => {} } @@ -1445,8 +1539,15 @@ impl crate::tui::App { if let Some(client) = self.client.as_ref() { if let Ok(_) = client.delete_playlist(&id).await { self.playlists.retain(|p| p.id != id); - let items = search_results(&self.playlists, &self.state.playlists_search_term, false); - let _ = self.state.playlists_scroll_state.content_length(items.len().saturating_sub(1)); + let items = search_results( + &self.playlists, + &self.state.playlists_search_term, + false, + ); + let _ = self + .state + .playlists_scroll_state + .content_length(items.len().saturating_sub(1)); self.popup.current_menu = Some(PopupMenu::GenericMessage { title: "Playlist deleted".to_string(), @@ -1499,11 +1600,7 @@ impl crate::tui::App { return None; } - let index = self - .playlists - .iter() - .position(|p| p.id == id) - .unwrap_or(0); + let index = self.playlists.iter().position(|p| p.id == id).unwrap_or(0); self.state.selected_playlist.select(Some(index)); self.popup.current_menu = Some(PopupMenu::GenericMessage { @@ -1523,7 +1620,7 @@ impl crate::tui::App { } _ => {} }, - PopupMenu::PlaylistsChangeFilter { } => match action { + PopupMenu::PlaylistsChangeFilter {} => match action { Action::Normal => { self.state.playlist_filter = Filter::Normal; self.close_popup(); @@ -1536,7 +1633,7 @@ impl crate::tui::App { } _ => {} }, - PopupMenu::PlaylistsChangeSort { } => match action { + PopupMenu::PlaylistsChangeSort {} => match action { Action::Ascending => { self.state.playlist_sort = Sort::Ascending; self.close_popup(); @@ -1559,7 +1656,8 @@ impl crate::tui::App { PopupMenu::ArtistRoot { .. } => match action { Action::JumpToCurrent => { let artists = match self - .state.queue + .state + .queue .get(self.state.current_playback_state.current_index as usize) { Some(song) => &song.artist_items, @@ -1578,24 +1676,38 @@ impl crate::tui::App { } Action::ChangeFilter => { self.popup.current_menu = Some(PopupMenu::ArtistsChangeFilter {}); - self.popup.selected.select(Some(if self.state.artist_filter == Filter::Normal { 0 } else { 1 })); + self.popup.selected.select(Some( + if self.state.artist_filter == Filter::Normal { + 0 + } else { + 1 + }, + )); } Action::ChangeOrder => { self.popup.current_menu = Some(PopupMenu::ArtistsChangeSort {}); - self.popup.selected.select(Some(if self.state.artist_sort == Sort::Ascending { 0 } else { 1 })); + self.popup.selected.select(Some( + if self.state.artist_sort == Sort::Ascending { + 0 + } else { + 1 + }, + )); } _ => {} }, - PopupMenu::ArtistJumpToCurrent { artists, .. } => if let Action::JumpToCurrent = action { - let selected = match self.popup.selected.selected() { - Some(i) => i, - None => return, - }; - let artist = &artists[selected]; - self.reposition_cursor(&artist.id, Selectable::Artist); - self.close_popup(); - }, - PopupMenu::ArtistsChangeFilter { } => match action { + PopupMenu::ArtistJumpToCurrent { artists, .. } => { + if let Action::JumpToCurrent = action { + let selected = match self.popup.selected.selected() { + Some(i) => i, + None => return, + }; + let artist = &artists[selected]; + self.reposition_cursor(&artist.id, Selectable::Artist); + self.close_popup(); + } + } + PopupMenu::ArtistsChangeFilter {} => match action { Action::Normal => { self.state.artist_filter = Filter::Normal; self.close_popup(); @@ -1608,7 +1720,7 @@ impl crate::tui::App { } _ => {} }, - PopupMenu::ArtistsChangeSort { } => match action { + PopupMenu::ArtistsChangeSort {} => match action { Action::Ascending => { self.state.artist_sort = Sort::Ascending; self.close_popup(); @@ -1643,7 +1755,9 @@ impl crate::tui::App { if self.popup.global { if self.popup.current_menu.is_none() { - self.popup.current_menu = Some(PopupMenu::GlobalRoot { large_art: self.state.large_art }); + self.popup.current_menu = Some(PopupMenu::GlobalRoot { + large_art: self.state.large_art, + }); self.popup.selected.select(Some(0)); } self.render_popup(frame); @@ -1669,7 +1783,8 @@ impl crate::tui::App { self.popup.current_menu = Some(PopupMenu::ArtistRoot { artist: artist.clone(), playing_artists: self - .state.queue + .state + .queue .get(self.state.current_playback_state.current_index as usize) .map(|s| s.artist_items.clone()), }); @@ -1700,7 +1815,7 @@ impl crate::tui::App { _ => { self.close_popup(); } - }, + }, ActiveTab::Playlists => match self.state.last_section { ActiveSection::List => { if self.popup.current_menu.is_none() { @@ -1775,7 +1890,11 @@ impl crate::tui::App { let percent_height = ((options.len() + 2) as f32 / window_height as f32 * 100.0).ceil() as u16; - let width = if let PopupMenu::GlobalRunScheduledTask { .. } = menu { 70 } else { 30 }; + let width = if let PopupMenu::GlobalRunScheduledTask { .. } = menu { + 70 + } else { + 30 + }; let popup_area = popup_area(area, width, percent_height); frame.render_widget(Clear, popup_area); // clears the background diff --git a/src/queue.rs b/src/queue.rs index e7da48c..87dbcfa 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -1,8 +1,10 @@ /// This file has all the queue control functions /// the basic idea is keeping our queue in sync with mpv and doing some basic operations /// - -use crate::{client::DiscographySong, tui::{App, Song}}; +use crate::{ + client::DiscographySong, + tui::{App, Song}, +}; use rand::seq::SliceRandom; impl App { @@ -13,31 +15,33 @@ impl App { return; } if let Some(client) = &self.client { - - let selected_is_album = tracks.get(skip).is_some_and(|t| t.id.starts_with("_album_")); + let selected_is_album = tracks + .get(skip) + .is_some_and(|t| t.id.starts_with("_album_")); // the playlist MPV will be getting self.state.queue = tracks .iter() .skip(skip) - // if selected is an album, this will filter out all the tracks that are not part of the album - .filter(|track| !selected_is_album || track.parent_id == tracks.get(skip + 1).map_or("", |t| &t.parent_id)) + // if selected is an album, this will filter out all the tracks that are not part of the album + .filter(|track| { + !selected_is_album + || track.parent_id == tracks.get(skip + 1).map_or("", |t| &t.parent_id) + }) .filter(|track| !track.id.starts_with("_album_")) // and then we filter out the album itself - .map(|track| { - Song { - id: track.id.clone(), - url: client.song_url_sync(track.id.clone()), - name: track.name.clone(), - artist: track.album_artist.clone(), - artist_items: track.artist_items.clone(), - album: track.album.clone(), - parent_id: track.parent_id.clone(), - production_year: track.production_year, - is_in_queue: false, - is_transcoded: client.transcoding.enabled, - is_favorite: track.user_data.is_favorite, - original_index: 0, - } + .map(|track| Song { + id: track.id.clone(), + url: client.song_url_sync(track.id.clone()), + name: track.name.clone(), + artist: track.album_artist.clone(), + artist_items: track.artist_items.clone(), + album: track.album.clone(), + parent_id: track.parent_id.clone(), + production_year: track.production_year, + is_in_queue: false, + is_transcoded: client.transcoding.enabled, + is_favorite: track.user_data.is_favorite, + original_index: 0, }) .collect(); @@ -54,7 +58,7 @@ impl App { } fn replace_queue_one_track(&mut self, tracks: &[DiscographySong], skip: usize) { - if tracks.is_empty() { + if tracks.is_empty() { return; } @@ -131,7 +135,7 @@ impl App { self.replace_queue_one_track(tracks, skip); return; } - + if let Some(client) = &self.client { let mut songs: Vec = Vec::new(); for i in 0..n { @@ -175,8 +179,17 @@ impl App { }; for song in songs.iter().rev() { - if let Ok(_) = mpv.mpv.command("loadfile", &[song.url.as_str(), "insert-at", (selected_queue_item + 1).to_string().as_str()]) { - self.state.queue.insert((selected_queue_item + 1) as usize, song.clone()); + if let Ok(_) = mpv.mpv.command( + "loadfile", + &[ + song.url.as_str(), + "insert-at", + (selected_queue_item + 1).to_string().as_str(), + ], + ) { + self.state + .queue + .insert((selected_queue_item + 1) as usize, song.clone()); } } } @@ -188,7 +201,12 @@ impl App { let album_id = self.tracks[selected].parent_id.clone(); let album = self.tracks[selected].album.clone(); let album_artist = self.tracks[selected].album_artist.clone(); - let tracks = self.tracks.iter().skip(selected + 1).take_while(|t| t.parent_id == album_id).collect::>(); + let tracks = self + .tracks + .iter() + .skip(selected + 1) + .take_while(|t| t.parent_id == album_id) + .collect::>(); let mut selected_queue_item = -1; for (i, song) in self.state.queue.iter().enumerate() { @@ -222,8 +240,17 @@ impl App { original_index: 0, }; - if let Ok(_) = mpv.mpv.command("loadfile", &[song.url.as_str(), "insert-at", (selected_queue_item + 1).to_string().as_str()]) { - self.state.queue.insert((selected_queue_item + 1) as usize, song); + if let Ok(_) = mpv.mpv.command( + "loadfile", + &[ + song.url.as_str(), + "insert-at", + (selected_queue_item + 1).to_string().as_str(), + ], + ) { + self.state + .queue + .insert((selected_queue_item + 1) as usize, song); } } } @@ -264,7 +291,10 @@ impl App { Err(_) => return, }; - if let Ok(_) = mpv.mpv.command("loadfile", &[song.url.as_str(), "insert-next"]) { + if let Ok(_) = mpv + .mpv + .command("loadfile", &[song.url.as_str(), "insert-next"]) + { self.state.queue.insert(selected_queue_item + 1, song); } @@ -277,7 +307,6 @@ impl App { // println!("So these wont be the same sad sad {second}{:?}", self.state.queue.get(1).unwrap().url); // // compare the strings // println!("{:?}", self.state.queue.get(1).unwrap().url == second); - } } @@ -298,13 +327,16 @@ impl App { Err(_) => return, }; - if let Ok(_) = mpv.mpv.command("playlist-remove", &[selected_queue_item.to_string().as_str()]) { + if let Ok(_) = mpv.mpv.command( + "playlist-remove", + &[selected_queue_item.to_string().as_str()], + ) { self.state.queue.remove(selected_queue_item); } } /// Clear the queue - /// + /// pub async fn clear_queue(&mut self) { if self.state.queue.is_empty() { return; @@ -314,7 +346,10 @@ impl App { if !self.state.queue[i].is_in_queue { continue; } - if let Ok(_) = mpv.mpv.command("playlist-remove", &[i.to_string().as_str()]) { + if let Ok(_) = mpv + .mpv + .command("playlist-remove", &[i.to_string().as_str()]) + { self.state.queue.remove(i); } } @@ -328,7 +363,13 @@ impl App { pub async fn relocate_queue_and_play(&mut self) { if let Ok(mpv) = self.mpv_state.lock() { // get a list of all the songs in the queue - let mut queue: Vec = self.state.queue.iter().filter(|s| s.is_in_queue).cloned().collect(); + let mut queue: Vec = self + .state + .queue + .iter() + .filter(|s| s.is_in_queue) + .cloned() + .collect(); let queue_len = queue.len(); let mut index = self.state.selected_queue_item.selected().unwrap_or(0); @@ -336,7 +377,9 @@ impl App { // early return in case we're within queue bounds if self.state.queue[index].is_in_queue { - let _ = mpv.mpv.command("playlist-play-index", &[&index.to_string()]); + let _ = mpv + .mpv + .command("playlist-play-index", &[&index.to_string()]); if self.paused { let _ = mpv.mpv.set_property("pause", false); self.paused = false; @@ -368,12 +411,21 @@ impl App { let _ = mpv.mpv.command("loadfile", &[song.url.as_str(), "append"]); self.state.queue.push(song); } else { - let _ = mpv.mpv.command("loadfile", &[song.url.as_str(), "insert-at", (index + 1).to_string().as_str()]); + let _ = mpv.mpv.command( + "loadfile", + &[ + song.url.as_str(), + "insert-at", + (index + 1).to_string().as_str(), + ], + ); self.state.queue.insert(index + 1, song); } } - let _ = mpv.mpv.command("playlist-play-index", &[&index.to_string()]); + let _ = mpv + .mpv + .command("playlist-play-index", &[&index.to_string()]); if self.paused { let _ = mpv.mpv.set_property("pause", false); self.paused = false; @@ -402,27 +454,42 @@ impl App { // i don't think i've ever disliked an API more if let Ok(mpv) = self.mpv_state.lock() { - let _ = mpv.mpv.command("playlist-move", &[ - selected_queue_item.to_string().as_str(), - (selected_queue_item - 1).to_string().as_str() - ]).map_err(|e| format!("Failed to move playlist item: {:?}", e)); - } - self.state.selected_queue_item.select(Some(selected_queue_item - 1)); - - self.state.queue.swap(selected_queue_item, selected_queue_item - 1); + let _ = mpv + .mpv + .command( + "playlist-move", + &[ + selected_queue_item.to_string().as_str(), + (selected_queue_item - 1).to_string().as_str(), + ], + ) + .map_err(|e| format!("Failed to move playlist item: {:?}", e)); + } + self.state + .selected_queue_item + .select(Some(selected_queue_item - 1)); + + self.state + .queue + .swap(selected_queue_item, selected_queue_item - 1); // if we moved the current song either directly or by moving the song above it // we need to update the current index if self.state.current_playback_state.current_index == selected_queue_item as i64 { self.state.current_playback_state.current_index -= 1; - } else if self.state.current_playback_state.current_index == (selected_queue_item - 1) as i64 { + } else if self.state.current_playback_state.current_index + == (selected_queue_item - 1) as i64 + { self.state.current_playback_state.current_index += 1; } // discard next poll let _ = self.receiver.try_recv(); - #[cfg(debug_assertions)] { self.__debug_error_corrector_tm(); } + #[cfg(debug_assertions)] + { + self.__debug_error_corrector_tm(); + } } } @@ -446,44 +513,65 @@ impl App { } if let Ok(mpv) = self.mpv_state.lock() { - let _ = mpv.mpv.command("playlist-move", &[ - (selected_queue_item + 1).to_string().as_str(), - selected_queue_item.to_string().as_str(), - ]).map_err(|e| format!("Failed to move playlist item: {:?}", e)); - } - - self.state.queue.swap(selected_queue_item, selected_queue_item + 1); + let _ = mpv + .mpv + .command( + "playlist-move", + &[ + (selected_queue_item + 1).to_string().as_str(), + selected_queue_item.to_string().as_str(), + ], + ) + .map_err(|e| format!("Failed to move playlist item: {:?}", e)); + } + + self.state + .queue + .swap(selected_queue_item, selected_queue_item + 1); // if we moved the current song either directly or by moving the song above it // we need to update the current index if self.state.current_playback_state.current_index == selected_queue_item as i64 { self.state.current_playback_state.current_index += 1; - } else if self.state.current_playback_state.current_index == (selected_queue_item + 1) as i64 { + } else if self.state.current_playback_state.current_index + == (selected_queue_item + 1) as i64 + { self.state.current_playback_state.current_index -= 1; } - self.state.selected_queue_item.select(Some(selected_queue_item + 1)); + self.state + .selected_queue_item + .select(Some(selected_queue_item + 1)); // discard next poll let _ = self.receiver.try_recv(); - #[cfg(debug_assertions)] { self.__debug_error_corrector_tm(); } + #[cfg(debug_assertions)] + { + self.__debug_error_corrector_tm(); + } } } /// Shuffles the queue - /// + /// pub async fn do_shuffle(&mut self, include_current: bool) { if let Ok(mpv) = self.mpv_state.lock() { - let current_index = self.state.current_playback_state.current_index as usize; if current_index >= self.state.queue.len() { return; } - let mut shuffle_after = current_index - + self.state.queue.iter().skip(current_index as usize).filter(|s| s.is_in_queue).count() + 1; + let mut shuffle_after = current_index + + self + .state + .queue + .iter() + .skip(current_index as usize) + .filter(|s| s.is_in_queue) + .count() + + 1; // if we're within the is_in_queue region, we need to subtract 1 if self.state.queue[current_index].is_in_queue { @@ -507,18 +595,21 @@ impl App { // find in current and move it needed for (i, _) in desired_order.iter().enumerate() { let target_song_id = &desired_order[i].id; - if let Some(j) = local_current - .iter() - .position(|s| &s.id == target_song_id) - { + if let Some(j) = local_current.iter().position(|s| &s.id == target_song_id) { if j != i { let from_index_in_mpv = shuffle_after + j; - let to_index_in_mpv = shuffle_after + i; - - let _ = mpv.mpv.command("playlist-move", &[ - from_index_in_mpv.to_string().as_str(), - to_index_in_mpv.to_string().as_str(), - ]).map_err(|e| format!("Failed to move playlist item: {:?}", e)); + let to_index_in_mpv = shuffle_after + i; + + let _ = mpv + .mpv + .command( + "playlist-move", + &[ + from_index_in_mpv.to_string().as_str(), + to_index_in_mpv.to_string().as_str(), + ], + ) + .map_err(|e| format!("Failed to move playlist item: {:?}", e)); let item = local_current.remove(j); local_current.insert(i, item); @@ -533,7 +624,7 @@ impl App { } /// Attempts to unshuffle the queue - /// + /// pub async fn do_unshuffle(&mut self) { if let Ok(mpv) = self.mpv_state.lock() { let current_index = self.state.current_playback_state.current_index as usize; @@ -543,8 +634,15 @@ impl App { } let mut shuffle_after = current_index - + self.state.queue.iter().skip(current_index).filter(|s| s.is_in_queue).count() + 1; - + + self + .state + .queue + .iter() + .skip(current_index) + .filter(|s| s.is_in_queue) + .count() + + 1; + if self.state.queue[current_index].is_in_queue { shuffle_after -= 1; } @@ -557,18 +655,21 @@ impl App { for (i, _) in desired_order.iter().enumerate() { let target_song_id = &desired_order[i].id; - if let Some(j) = local_current - .iter() - .position(|s| &s.id == target_song_id) - { + if let Some(j) = local_current.iter().position(|s| &s.id == target_song_id) { if j != i { let from_index_in_mpv = shuffle_after + j; - let to_index_in_mpv = shuffle_after + i; - - let _ = mpv.mpv.command("playlist-move", &[ - from_index_in_mpv.to_string().as_str(), - to_index_in_mpv.to_string().as_str(), - ]).map_err(|e| format!("Failed to move playlist item: {:?}", e)); + let to_index_in_mpv = shuffle_after + i; + + let _ = mpv + .mpv + .command( + "playlist-move", + &[ + from_index_in_mpv.to_string().as_str(), + to_index_in_mpv.to_string().as_str(), + ], + ) + .map_err(|e| format!("Failed to move playlist item: {:?}", e)); let item = local_current.remove(j); local_current.insert(i, item); @@ -587,12 +688,14 @@ impl App { /// Can be removed from well tested code /// fn __debug_error_corrector_tm(&mut self) { - let mut mpv_playlist = Vec::new(); if let Ok(mpv) = self.mpv_state.lock() { for (i, _) in self.state.queue.iter().enumerate() { - let mpv_url = mpv.mpv.get_property(format!("playlist/{}/filename", i).as_str()).unwrap_or("".to_string()); + let mpv_url = mpv + .mpv + .get_property(format!("playlist/{}/filename", i).as_str()) + .unwrap_or("".to_string()); mpv_playlist.push(mpv_url); } let mut new_queue = Vec::new(); diff --git a/src/search.rs b/src/search.rs index 80765aa..0adc5ec 100644 --- a/src/search.rs +++ b/src/search.rs @@ -5,19 +5,15 @@ Search tab rendering - The results area contains 3 lists, artists, albums, and tracks. -------------------------- */ +use crate::keyboard::*; use crate::tui::App; -use crate::keyboard::{*}; use ratatui::{ - Frame, - symbols::border, - widgets::{ - Block, - Borders, - Paragraph - }, prelude::*, + symbols::border, widgets::*, + widgets::{Block, Borders, Paragraph}, + Frame, }; impl App { @@ -26,10 +22,7 @@ impl App { // split the app container into 2 parts let search_layout = Layout::default() .direction(Direction::Vertical) - .constraints(vec![ - Constraint::Min(3), - Constraint::Percentage(95), - ]) + .constraints(vec![Constraint::Min(3), Constraint::Percentage(95)]) .split(app_container); let search_area = search_layout[0]; @@ -126,28 +119,26 @@ impl App { _ => format!("{}:", hours), }; - let mut time_span_text = format!(" {}{:02}:{:02}", hours_optional_text, minutes, seconds); - if track.has_lyrics{ + let mut time_span_text = + format!(" {}{:02}:{:02}", hours_optional_text, minutes, seconds); + if track.has_lyrics { time_span_text.push_str(" (l)"); } if track.id == self.active_song_id { let mut time: Text = Text::from(title); - time.push_span( - Span::styled( - time_span_text, - Style::default().add_modifier(Modifier::ITALIC), - ) - ); - ListItem::new(time) - .style(Style::default().fg(self.primary_color)) + time.push_span(Span::styled( + time_span_text, + Style::default().add_modifier(Modifier::ITALIC), + )); + ListItem::new(time).style(Style::default().fg(self.primary_color)) } else { let mut time: Text = Text::from(title); - time.push_span( - Span::styled( - time_span_text, - Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC), - ) - ); + time.push_span(Span::styled( + time_span_text, + Style::default() + .fg(Color::DarkGray) + .add_modifier(Modifier::ITALIC), + )); ListItem::new(time) } }) @@ -159,13 +150,13 @@ impl App { Block::default() .borders(Borders::ALL) .border_style(self.primary_color) - .title("Artists") + .title("Artists"), ) .highlight_symbol(">>") .highlight_style( Style::default() - .add_modifier(Modifier::BOLD) - .add_modifier(Modifier::REVERSED) + .add_modifier(Modifier::BOLD) + .add_modifier(Modifier::REVERSED), ) .scroll_padding(10) .repeat_highlight_symbol(true), @@ -174,9 +165,9 @@ impl App { .highlight_symbol(">>") .highlight_style( Style::default() - .add_modifier(Modifier::BOLD) - .bg(Color::Indexed(236)) - .fg(Color::White) + .add_modifier(Modifier::BOLD) + .bg(Color::Indexed(236)) + .fg(Color::White), ) .scroll_padding(10) .repeat_highlight_symbol(true), @@ -188,13 +179,13 @@ impl App { Block::default() .borders(Borders::ALL) .border_style(self.primary_color) - .title("Albums") + .title("Albums"), ) .highlight_symbol(">>") .highlight_style( Style::default() - .add_modifier(Modifier::BOLD) - .add_modifier(Modifier::REVERSED) + .add_modifier(Modifier::BOLD) + .add_modifier(Modifier::REVERSED), ) .repeat_highlight_symbol(true), _ => List::new(albums) @@ -202,9 +193,9 @@ impl App { .highlight_symbol(">>") .highlight_style( Style::default() - .add_modifier(Modifier::BOLD) - .bg(Color::Indexed(236)) - .fg(Color::White) + .add_modifier(Modifier::BOLD) + .bg(Color::Indexed(236)) + .fg(Color::White), ) .repeat_highlight_symbol(true), }; @@ -215,14 +206,14 @@ impl App { Block::default() .borders(Borders::ALL) .border_style(self.primary_color) - .title("Tracks") + .title("Tracks"), ) .highlight_symbol(">>") .highlight_style( Style::default() - .add_modifier(Modifier::BOLD) - .bg(Color::White) - .fg(Color::Indexed(232)) + .add_modifier(Modifier::BOLD) + .bg(Color::White) + .fg(Color::Indexed(232)), ) .repeat_highlight_symbol(true), _ => List::new(tracks) @@ -230,17 +221,29 @@ impl App { .highlight_symbol(">>") .highlight_style( Style::default() - .add_modifier(Modifier::BOLD) - .bg(Color::Indexed(236)) - .fg(Color::White) + .add_modifier(Modifier::BOLD) + .bg(Color::Indexed(236)) + .fg(Color::White), ) .repeat_highlight_symbol(true), }; // frame.render_widget(artists_list, results_layout[0]); - frame.render_stateful_widget(artists_list, results_layout[0], &mut self.state.selected_search_artist); - frame.render_stateful_widget(albums_list, results_layout[1], &mut self.state.selected_search_album); - frame.render_stateful_widget(tracks_list, results_layout[2], &mut self.state.selected_search_track); + frame.render_stateful_widget( + artists_list, + results_layout[0], + &mut self.state.selected_search_artist, + ); + frame.render_stateful_widget( + albums_list, + results_layout[1], + &mut self.state.selected_search_album, + ); + frame.render_stateful_widget( + tracks_list, + results_layout[2], + &mut self.state.selected_search_track, + ); frame.render_stateful_widget( Scrollbar::default() @@ -253,7 +256,7 @@ impl App { vertical: 1, horizontal: 1, }), - &mut self.state.search_artist_scroll_state + &mut self.state.search_artist_scroll_state, ); frame.render_stateful_widget( @@ -267,7 +270,7 @@ impl App { vertical: 1, horizontal: 1, }), - &mut self.state.search_album_scroll_state + &mut self.state.search_album_scroll_state, ); frame.render_stateful_widget( @@ -281,7 +284,7 @@ impl App { vertical: 1, horizontal: 1, }), - &mut self.state.search_track_scroll_state + &mut self.state.search_track_scroll_state, ); // render search results } diff --git a/src/tui.rs b/src/tui.rs index b3a3bb6..556e266 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -11,13 +11,15 @@ Notable fields: - controls = MPRIS controls. We use MPRIS for media controls. -------------------------- */ -use crate::client::{self, report_progress, Album, Artist, Client, DiscographySong, Lyric, Playlist, ProgressReport}; +use crate::client::{ + self, report_progress, Album, Artist, Client, DiscographySong, Lyric, Playlist, ProgressReport, +}; use crate::helpers::State; -use crate::keyboard::{*}; +use crate::keyboard::*; use crate::mpris; use crate::popup::PopupState; -use libmpv2::{*}; +use libmpv2::*; use serde::{Deserialize, Serialize}; use core::panic; @@ -28,12 +30,7 @@ use souvlaki::{MediaControlEvent, MediaControls}; use dirs::cache_dir; use std::path::PathBuf; -use ratatui::{ - Terminal, - Frame, - prelude::*, - widgets::*, -}; +use ratatui::{prelude::*, widgets::*, Frame, Terminal}; use ratatui_image::{picker::Picker, protocol::StatefulProtocol}; @@ -115,13 +112,13 @@ pub struct App { pub exit: bool, pub dirty: bool, // dirty flag for rendering pub dirty_clear: bool, // dirty flag for clearing the screen - + pub state: State, // main persistent state pub primary_color: Color, // primary color pub config: Option, // config pub auto_color: bool, // grab color from cover art (coolest feature ever omg) - + pub artists: Vec, // all artists pub albums: Vec, // all albums pub album_tracks: Vec, // current album's tracks @@ -145,7 +142,7 @@ pub struct App { pub paused: bool, pending_seek: Option, // pending seek pub buffering: bool, // buffering state (spinner) - + pub spinner: usize, // spinner for buffering spinner_skipped: u8, pub spinner_stages: Vec<&'static str>, @@ -159,7 +156,7 @@ pub struct App { pub search_result_artists: Vec, pub search_result_albums: Vec, pub search_result_tracks: Vec, - + pub popup: PopupState, pub client: Option, // jellyfin http client @@ -189,7 +186,11 @@ impl Default for App { let primary_color = crate::config::get_primary_color(); - let is_art_enabled = config.as_ref().and_then(|c| c.get("art")).and_then(|a| a.as_bool()).unwrap_or(true); + let is_art_enabled = config + .as_ref() + .and_then(|c| c.get("art")) + .and_then(|a| a.as_bool()) + .unwrap_or(true); let picker = if is_art_enabled { match Picker::from_query_stdio() { Ok(picker) => Some(picker), @@ -216,7 +217,11 @@ impl Default for App { state: State::new(), primary_color, config: config.clone(), - auto_color: config.as_ref().and_then(|c| c.get("auto_color")).and_then(|a| a.as_bool()).unwrap_or(true), + auto_color: config + .as_ref() + .and_then(|c| c.get("auto_color")) + .and_then(|a| a.as_bool()) + .unwrap_or(true), artists: vec![], albums: vec![], @@ -237,7 +242,12 @@ impl Default for App { cover_art_dir: match cache_dir() { Some(dir) => dir, None => PathBuf::from("./"), - }.join("jellyfin-tui").join("covers").to_str().unwrap_or("").to_string(), + } + .join("jellyfin-tui") + .join("covers") + .to_str() + .unwrap_or("") + .to_string(), picker, paused: true, @@ -245,9 +255,7 @@ impl Default for App { buffering: false, spinner: 0, spinner_skipped: 0, - spinner_stages: vec![ - "◰", "◳", "◲", "◱" - ], + spinner_stages: vec!["◰", "◳", "◲", "◱"], searching: false, show_help: false, search_term: String::from(""), @@ -257,7 +265,7 @@ impl Default for App { search_result_artists: vec![], search_result_albums: vec![], search_result_tracks: vec![], - + popup: PopupState::default(), client: None, @@ -287,15 +295,16 @@ impl MpvState { let mpv = Mpv::with_initializer(|mpv| { mpv.set_option("msg-level", "ffmpeg/demuxer=no").unwrap(); Ok(()) - }).expect("[XX] Failed to initiate mpv context"); + }) + .expect("[XX] Failed to initiate mpv context"); mpv.set_property("vo", "null").unwrap(); mpv.set_property("volume", 100).unwrap(); mpv.set_property("prefetch-playlist", "yes").unwrap(); // gapless playback // no console output (it shifts the tui around) // TODO: can we catch this and show it in a proper area? - mpv.set_property("quiet", "yes").ok(); - mpv.set_property("really-quiet", "yes").ok(); + mpv.set_property("quiet", "yes").ok(); + mpv.set_property("really-quiet", "yes").ok(); // optional mpv options (hah...) if let Some(config) = config { @@ -342,16 +351,23 @@ impl App { if let Some(client) = &self.client { if let Ok(playlists) = client.playlists(String::from("")).await { self.original_playlists = playlists; - self.state.playlists_scroll_state = ScrollbarState::new(self.original_playlists.len().saturating_sub(1)); + self.state.playlists_scroll_state = + ScrollbarState::new(self.original_playlists.len().saturating_sub(1)); } if let Ok(albums) = client.albums().await { self.original_albums = albums; - self.state.albums_scroll_state = ScrollbarState::new(self.original_albums.len().saturating_sub(1)); + self.state.albums_scroll_state = + ScrollbarState::new(self.original_albums.len().saturating_sub(1)); } } self.register_controls(self.mpv_state.clone()); - let persist = self.config.as_ref().and_then(|c| c.get("persist")).and_then(|a| a.as_bool()).unwrap_or(true); + let persist = self + .config + .as_ref() + .and_then(|c| c.get("persist")) + .and_then(|a| a.as_bool()) + .unwrap_or(true); if persist { if let Err(_) = self.load_state().await { self.reorder_lists(); @@ -360,7 +376,8 @@ impl App { #[cfg(target_os = "linux")] { if let Some(ref mut controls) = self.controls { - let _ = controls.set_volume(self.state.current_playback_state.volume as f64 / 100.0); + let _ = + controls.set_volume(self.state.current_playback_state.volume as f64 / 100.0); } } } @@ -371,16 +388,36 @@ impl App { self.albums = self.original_albums.clone(); self.playlists = self.original_playlists.clone(); - self.artists.sort_by(|a, b| a.name.to_ascii_lowercase().cmp(&b.name.to_ascii_lowercase())); - self.albums.sort_by(|a, b| a.name.to_ascii_lowercase().cmp(&b.name.to_ascii_lowercase())); - self.playlists.sort_by(|a, b| a.name.to_ascii_lowercase().cmp(&b.name.to_ascii_lowercase())); + self.artists.sort_by(|a, b| { + a.name + .to_ascii_lowercase() + .cmp(&b.name.to_ascii_lowercase()) + }); + self.albums.sort_by(|a, b| { + a.name + .to_ascii_lowercase() + .cmp(&b.name.to_ascii_lowercase()) + }); + self.playlists.sort_by(|a, b| { + a.name + .to_ascii_lowercase() + .cmp(&b.name.to_ascii_lowercase()) + }); match self.state.artist_filter { Filter::FavoritesFirst => { - let mut favorites: Vec<_> = self.artists.iter() - .filter(|a| a.user_data.is_favorite).cloned().collect(); - let mut non_favorites: Vec<_> = self.artists.iter() - .filter(|a| !a.user_data.is_favorite).cloned().collect(); + let mut favorites: Vec<_> = self + .artists + .iter() + .filter(|a| a.user_data.is_favorite) + .cloned() + .collect(); + let mut non_favorites: Vec<_> = self + .artists + .iter() + .filter(|a| !a.user_data.is_favorite) + .cloned() + .collect(); if matches!(self.state.artist_sort, Sort::Descending) { favorites.reverse(); non_favorites.reverse(); @@ -395,10 +432,18 @@ impl App { } match self.state.album_filter { Filter::FavoritesFirst => { - let mut favorites: Vec<_> = self.albums.iter() - .filter(|a| a.user_data.is_favorite).cloned().collect(); - let mut non_favorites: Vec<_> = self.albums.iter() - .filter(|a: &&Album| !a.user_data.is_favorite).cloned().collect(); + let mut favorites: Vec<_> = self + .albums + .iter() + .filter(|a| a.user_data.is_favorite) + .cloned() + .collect(); + let mut non_favorites: Vec<_> = self + .albums + .iter() + .filter(|a: &&Album| !a.user_data.is_favorite) + .cloned() + .collect(); // sort by preference match self.state.album_sort { @@ -424,10 +469,18 @@ impl App { } match self.state.playlist_filter { Filter::FavoritesFirst => { - let mut favorites: Vec<_> = self.playlists.iter() - .filter(|a| a.user_data.is_favorite).cloned().collect(); - let mut non_favorites: Vec<_> = self.playlists.iter() - .filter(|a| !a.user_data.is_favorite).cloned().collect(); + let mut favorites: Vec<_> = self + .playlists + .iter() + .filter(|a| a.user_data.is_favorite) + .cloned() + .collect(); + let mut non_favorites: Vec<_> = self + .playlists + .iter() + .filter(|a| !a.user_data.is_favorite) + .cloned() + .collect(); if matches!(self.state.playlist_sort, Sort::Descending) { favorites.reverse(); non_favorites.reverse(); @@ -443,8 +496,7 @@ impl App { } pub async fn run<'a>(&mut self) -> std::result::Result<(), Box> { - - if let Some (seek) = self.pending_seek { + if let Some(seek) = self.pending_seek { if let Ok(mpv) = self.mpv_state.lock() { let paused_for_cache = mpv.mpv.get_property("seekable").unwrap_or(false); if paused_for_cache { @@ -469,9 +521,14 @@ impl App { } if let Some(client) = &self.client { if let Some(metadata) = self.metadata.as_mut() { - if client.transcoding.enabled - && state.audio_bitrate > 0 - && self.state.queue.get(state.current_index as usize).map(|s| s.is_transcoded).unwrap_or(false) + if client.transcoding.enabled + && state.audio_bitrate > 0 + && self + .state + .queue + .get(state.current_index as usize) + .map(|s| s.is_transcoded) + .unwrap_or(false) { metadata.bit_rate = state.audio_bitrate as u64; } @@ -480,7 +537,8 @@ impl App { // Queue position if !self.state.selected_queue_item_manual_override { - self.state.selected_queue_item + self.state + .selected_queue_item .select(Some(state.current_index as usize)); } @@ -501,7 +559,12 @@ impl App { } } } - let song = self.state.queue.get(self.state.current_playback_state.current_index as usize).cloned().unwrap_or_default(); + let song = self + .state + .queue + .get(self.state.current_playback_state.current_index as usize) + .cloned() + .unwrap_or_default(); if let Ok(mpv) = self.mpv_state.lock() { let paused_for_cache = mpv.mpv.get_property("paused-for-cache").unwrap_or(false); @@ -513,28 +576,37 @@ impl App { self.old_percentage = self.state.current_playback_state.percentage; // if % > 0.5, report progress - self.scrobble_this = (song.id.clone(), (self.state.current_playback_state.duration * self.state.current_playback_state.percentage * 100000.0) as u64); + self.scrobble_this = ( + song.id.clone(), + (self.state.current_playback_state.duration + * self.state.current_playback_state.percentage + * 100000.0) as u64, + ); let client = self.client.as_ref().ok_or(" ! No client")?; let runit = report_progress( - client.base_url.clone(), client.access_token.clone(), ProgressReport { - volume_level: self.state.current_playback_state.volume as u64, - is_paused: self.paused, - // take into account duratio, percentage and *10000 - position_ticks: (self.state.current_playback_state.duration * self.state.current_playback_state.percentage * 100000.0) as u64, - media_source_id: self.active_song_id.clone(), - playback_start_time_ticks: 0, - can_seek: false, // TODO - item_id: self.active_song_id.clone(), - event_name: "timeupdate".to_string(), - }); + client.base_url.clone(), + client.access_token.clone(), + ProgressReport { + volume_level: self.state.current_playback_state.volume as u64, + is_paused: self.paused, + // take into account duratio, percentage and *10000 + position_ticks: (self.state.current_playback_state.duration + * self.state.current_playback_state.percentage + * 100000.0) as u64, + media_source_id: self.active_song_id.clone(), + playback_start_time_ticks: 0, + can_seek: false, // TODO + item_id: self.active_song_id.clone(), + event_name: "timeupdate".to_string(), + }, + ); tokio::spawn(runit); - } else if self.old_percentage > self.state.current_playback_state.percentage { self.old_percentage = self.state.current_playback_state.percentage; } - + // song has changed self.song_changed = self.song_changed || song.id != self.active_song_id; if self.song_changed { @@ -550,10 +622,12 @@ impl App { let lyrics = client.lyrics(&self.active_song_id).await; self.metadata = client.metadata(&self.active_song_id).await.ok(); - self.lyrics = lyrics.map(|lyrics| { - let time_synced = lyrics.iter().all(|l| l.start != 0); - ( self.active_song_id.clone(), lyrics, time_synced ) - }).ok(); + self.lyrics = lyrics + .map(|lyrics| { + let time_synced = lyrics.iter().all(|l| l.start != 0); + (self.active_song_id.clone(), lyrics, time_synced) + }) + .ok(); self.state.selected_lyric.select(None); @@ -562,7 +636,10 @@ impl App { self.previous_song_parent_id = song.parent_id.clone(); self.cover_art = None; self.cover_art_path = String::from(""); - let cover_image = client.download_cover_art(song.parent_id).await.unwrap_or_default(); + let cover_image = client + .download_cover_art(song.parent_id) + .await + .unwrap_or_default(); if !cover_image.is_empty() && !self.cover_art_dir.is_empty() { // let p = format!("./covers/{}", cover_image); @@ -587,13 +664,12 @@ impl App { } let client = self.client.as_ref().ok_or(" ! No client")?; - // Scrobble. The way to do scrobbling in jellyfin is using the last.fm jellyfin plugin. + // Scrobble. The way to do scrobbling in jellyfin is using the last.fm jellyfin plugin. // Essentially, this event should be sent either way, the scrobbling is purely server side and not something we need to worry about. if !self.scrobble_this.0.is_empty() { - let _ = client.stopped( - &self.scrobble_this.0, - self.scrobble_this.1, - ).await; + let _ = client + .stopped(&self.scrobble_this.0, self.scrobble_this.1) + .await; self.scrobble_this = (String::from(""), 0); } @@ -602,14 +678,15 @@ impl App { Ok(()) } - pub async fn draw<'a>(&mut self, terminal: &'a mut Tui) -> std::result::Result<(), Box> { - + pub async fn draw<'a>( + &mut self, + terminal: &'a mut Tui, + ) -> std::result::Result<(), Box> { // let the rats take over if self.dirty { - terminal - .draw(|frame: &mut Frame| { - self.render_frame(frame); - })?; + terminal.draw(|frame: &mut Frame| { + self.render_frame(frame); + })?; self.dirty = false; } @@ -624,7 +701,7 @@ impl App { // ratatui is an immediate mode tui which is cute, but it will be heavy on the cpu // we use a dirty draw flag and thread::sleep to throttle the bool check a bit - + thread::sleep(Duration::from_millis(2)); Ok(()) @@ -632,13 +709,9 @@ impl App { /// This is the main render function for rataui. It's called every frame. pub fn render_frame<'a>(&mut self, frame: &'a mut Frame) { - let app_container = Layout::default() .direction(Direction::Vertical) - .constraints(vec![ - Constraint::Min(1), - Constraint::Percentage(100), - ]) + .constraints(vec![Constraint::Min(1), Constraint::Percentage(100)]) .split(frame.area()); // render tabs @@ -706,7 +779,10 @@ impl App { }; let transcoding = if let Some(client) = self.client.as_ref() { if client.transcoding.enabled { - format!("[{}@{}]", client.transcoding.container, client.transcoding.bitrate) + format!( + "[{}@{}]", + client.transcoding.container, client.transcoding.bitrate + ) } else { "".to_string() } @@ -719,7 +795,7 @@ impl App { 101..=120 => Color::Yellow, _ => Color::Red, }; - + Paragraph::new(info) .style(Style::default().fg(volume_color)) .alignment(Alignment::Right) @@ -727,18 +803,15 @@ impl App { .render(tabs_layout[1], buf); LineGauge::default() - .block( - Block::default() - .padding(Padding::horizontal(1)) - ) + .block(Block::default().padding(Padding::horizontal(1))) .filled_style( Style::default() .fg(volume_color) - .add_modifier(Modifier::BOLD) + .add_modifier(Modifier::BOLD), ) - .label(Line::from( - format!("{}%", self.state.current_playback_state.volume) - ).style(Style::default().fg(volume_color)) + .label( + Line::from(format!("{}%", self.state.current_playback_state.volume)) + .style(Style::default().fg(volume_color)), ) .unfilled_style( Style::default() @@ -756,16 +829,19 @@ impl App { if id.is_empty() { return; } - let recently_added = self.artists.iter() + let recently_added = self + .artists + .iter() .any(|a| a.id == id && a.jellyfintui_recently_added); if let Some(client) = self.client.as_ref() { if let Ok(artist) = client.discography(id, recently_added).await { self.state.active_section = ActiveSection::Tracks; self.tracks = artist.items; - self.state.tracks_scroll_state = ScrollbarState::new( - std::cmp::max(0, self.tracks.len() as i32 - 1) as usize - ); - self.state.current_artist = self.artists.iter() + self.state.tracks_scroll_state = + ScrollbarState::new(std::cmp::max(0, self.tracks.len() as i32 - 1) as usize); + self.state.current_artist = self + .artists + .iter() .find(|a| a.id == id) .cloned() .unwrap_or_default(); @@ -788,12 +864,16 @@ impl App { if let Ok(album) = client.album_tracks(id).await { self.state.active_section = ActiveSection::Tracks; self.album_tracks = album; - self.state.album_tracks_scroll_state = ScrollbarState::new( - std::cmp::max(0, self.album_tracks.len() as i32 - 1) as usize - ); - self.state.current_album = self.albums.iter() + self.state.album_tracks_scroll_state = + ScrollbarState::new( + std::cmp::max(0, self.album_tracks.len() as i32 - 1) as usize + ); + self.state.current_album = self + .albums + .iter() .find(|a| a.id == *id) - .cloned().unwrap_or_default(); + .cloned() + .unwrap_or_default(); } } } @@ -806,12 +886,16 @@ impl App { if let Ok(playlist) = client.playlist(id).await { self.state.active_section = ActiveSection::Tracks; self.playlist_tracks = playlist.items; - self.state.playlist_tracks_scroll_state = ScrollbarState::new( - std::cmp::max(0, self.playlist_tracks.len() as i32 - 1) as usize - ); - self.state.current_playlist = self.playlists.iter() + self.state.playlist_tracks_scroll_state = + ScrollbarState::new( + std::cmp::max(0, self.playlist_tracks.len() as i32 - 1) as usize + ); + self.state.current_playlist = self + .playlists + .iter() .find(|a| a.id == *id) - .cloned().unwrap_or_default(); + .cloned() + .unwrap_or_default(); } } } @@ -833,10 +917,10 @@ impl App { if self.mpv_thread.is_some() { if let Ok(mpv) = self.mpv_state.lock() { let _ = mpv.mpv.command("stop", &[]); - for song in &songs { + for song in &songs { mpv.mpv - .command("loadfile", &[&[song.url.as_str(), "append-play"].join(" ")]) - .map_err(|e| format!("Failed to load playlist: {:?}", e))?; + .command("loadfile", &[&[song.url.as_str(), "append-play"].join(" ")]) + .map_err(|e| format!("Failed to load playlist: {:?}", e))?; } let _ = mpv.mpv.set_property("pause", false); self.paused = false; @@ -870,14 +954,16 @@ impl App { sender: Sender, state: MpvPlaybackState, ) -> std::result::Result<(), Box> { - let mpv = mpv_state.lock().map_err(|e| format!("Failed to lock mpv_state: {:?}", e))?; + let mpv = mpv_state + .lock() + .map_err(|e| format!("Failed to lock mpv_state: {:?}", e))?; let _ = mpv.mpv.command("playlist_clear", &["force"]); - for song in songs { + for song in songs { mpv.mpv - .command("loadfile", &[&[song.url.as_str(), "append-play"].join(" ")]) - .map_err(|e| format!("Failed to load playlist: {:?}", e))?; + .command("loadfile", &[&[song.url.as_str(), "append-play"].join(" ")]) + .map_err(|e| format!("Failed to load playlist: {:?}", e))?; } mpv.mpv.set_property("volume", state.volume)?; @@ -887,28 +973,32 @@ impl App { loop { // main mpv loop - let mpv = mpv_state.lock().map_err(|e| format!("Failed to lock mpv_state: {:?}", e))?; + let mpv = mpv_state + .lock() + .map_err(|e| format!("Failed to lock mpv_state: {:?}", e))?; let percentage = mpv.mpv.get_property("percent-pos").unwrap_or(0.0); let current_index: i64 = mpv.mpv.get_property("playlist-pos").unwrap_or(0); let duration = mpv.mpv.get_property("duration").unwrap_or(0.0); let volume = mpv.mpv.get_property("volume").unwrap_or(0); let audio_bitrate = mpv.mpv.get_property("audio-bitrate").unwrap_or(0); - let file_format = mpv.mpv.get_property("file-format").unwrap_or(String::from("")); + let file_format = mpv + .mpv + .get_property("file-format") + .unwrap_or(String::from("")); drop(mpv); - let _ = sender - .send({ - MpvPlaybackState { - percentage, - duration, - current_index, - last_index: state.last_index, - volume, - audio_bitrate, - file_format: file_format.to_string(), - } - }); + let _ = sender.send({ + MpvPlaybackState { + percentage, + duration, + current_index, + last_index: state.last_index, + volume, + audio_bitrate, + file_format: file_format.to_string(), + } + }); thread::sleep(Duration::from_secs_f32(0.2)); } @@ -939,7 +1029,8 @@ impl App { .iter() .filter(|&color| { // filter out too dark or light colors - let brightness = 0.299 * color.r as f32 + 0.587 * color.g as f32 + 0.114 * color.b as f32; + let brightness = + 0.299 * color.r as f32 + 0.587 * color.g as f32 + 0.114 * color.b as f32; brightness > 50.0 && brightness < 200.0 }) .max_by_key(|color| { @@ -948,10 +1039,13 @@ impl App { max - min }) .unwrap_or(&colors[0]); - + let max = prominent_color.iter().max().unwrap(); let scale = 255.0 / max as f32; - let mut primary_color = prominent_color.iter().map(|c| (c as f32 * scale) as u8).collect::>(); + let mut primary_color = prominent_color + .iter() + .map(|c| (c as f32 * scale) as u8) + .collect::>(); // enhance contrast against black and white let brightness = 0.299 * primary_color[0] as f32 @@ -986,9 +1080,10 @@ impl App { }; // let current_artist_id = self.get_id_of_selected(&self.artists, Selectable::Artist); self.artists = artists; - self.state.artists_scroll_state = self.state.artists_scroll_state.content_length( - self.artists.len().saturating_sub(1) - ); + self.state.artists_scroll_state = self + .state + .artists_scroll_state + .content_length(self.artists.len().saturating_sub(1)); let playlists = match client.playlists(String::from("")).await { Ok(playlists) => playlists, @@ -997,9 +1092,10 @@ impl App { } }; self.playlists = playlists; - self.state.playlists_scroll_state = self.state.playlists_scroll_state.content_length( - self.playlists.len().saturating_sub(1) - ); + self.state.playlists_scroll_state = self + .state + .playlists_scroll_state + .content_length(self.playlists.len().saturating_sub(1)); } self.reorder_lists(); @@ -1008,12 +1104,20 @@ impl App { } pub fn save_state(&self) { - let persist = self.config.as_ref().and_then(|c| c.get("persist")).and_then(|a| a.as_bool()).unwrap_or(true); + let persist = self + .config + .as_ref() + .and_then(|c| c.get("persist")) + .and_then(|a| a.as_bool()) + .unwrap_or(true); if !persist { return; } if let Err(e) = self.state.save_state() { - eprintln!("[XX] Failed to save state This is most likely a bug: {:?}", e); + eprintln!( + "[XX] Failed to save state This is most likely a bug: {:?}", + e + ); } } @@ -1022,7 +1126,10 @@ impl App { self.reorder_lists(); - let position = Some(self.state.current_playback_state.duration * (self.state.current_playback_state.percentage / 100.0)); + let position = Some( + self.state.current_playback_state.duration + * (self.state.current_playback_state.percentage / 100.0), + ); self.buffering = true; let current_artist_id = self.state.current_artist.id.clone(); From e5f41ad625dd6b843683b130e7380cc2a8de442c Mon Sep 17 00:00:00 2001 From: dhonus Date: Fri, 21 Feb 2025 17:32:51 +0100 Subject: [PATCH 22/26] fix: jump to current album song --- src/popup.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/popup.rs b/src/popup.rs index 0b9c910..d9458b8 100644 --- a/src/popup.rs +++ b/src/popup.rs @@ -638,7 +638,7 @@ impl PopupMenu { // ---------- Album tracks ---------- // PopupMenu::AlbumTrackRoot { .. } => vec![ PopupAction { - label: "Jump to album of current song".to_string(), + label: "Jump to current song".to_string(), action: Action::JumpToCurrent, style: Style::default(), }, @@ -1181,6 +1181,7 @@ impl crate::tui::App { .iter() .find(|a| current_track.parent_id == a.id)?; let album_id = album.id.clone(); + let current_track_id = current_track.id.clone(); if album_id != self.state.current_album.id { let index = self .albums @@ -1190,9 +1191,9 @@ impl crate::tui::App { self.artist_select_by_index(index); self.album_tracks(&album_id).await; } - if let Some(index) = self.album_tracks.iter().position(|t| t.id == track.id) + if let Some(index) = self.album_tracks.iter().position(|t| t.id == current_track_id) { - self.track_select_by_index(index); + self.album_track_select_by_index(index); } self.close_popup(); } From 0ca76e36f79a779e5090965b88f554d9df423340 Mon Sep 17 00:00:00 2001 From: dhonus Date: Fri, 21 Feb 2025 17:33:55 +0100 Subject: [PATCH 23/26] fix: fn name bug --- src/popup.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/popup.rs b/src/popup.rs index d9458b8..39c0c10 100644 --- a/src/popup.rs +++ b/src/popup.rs @@ -1188,7 +1188,7 @@ impl crate::tui::App { .iter() .position(|a| a.id == album_id) .unwrap_or(0); - self.artist_select_by_index(index); + self.album_select_by_index(index); self.album_tracks(&album_id).await; } if let Some(index) = self.album_tracks.iter().position(|t| t.id == current_track_id) From 3556d8006f148bd3832020be66da5c940a00f1bd Mon Sep 17 00:00:00 2001 From: dhonus Date: Fri, 21 Feb 2025 17:38:32 +0100 Subject: [PATCH 24/26] chore: updated dependencies --- Cargo.lock | 101 +++++++++++++++++++++++++---------------------------- 1 file changed, 47 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3915716..77fb36c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -345,9 +345,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.13" +version = "1.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7777341816418c02e033934a09f20dc0ccaf65a5201ef8a450ae0105a573fda" +checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af" dependencies = [ "shlex", ] @@ -666,9 +666,9 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" @@ -918,9 +918,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "h2" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" dependencies = [ "atomic-waker", "bytes", @@ -1443,9 +1443,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.25" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" [[package]] name = "lru" @@ -1497,9 +1497,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" +checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" dependencies = [ "adler2", "simd-adler32", @@ -1519,9 +1519,9 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", @@ -1581,9 +1581,9 @@ checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" [[package]] name = "openssl" -version = "0.10.70" +version = "0.10.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" +checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" dependencies = [ "bitflags 2.8.0", "cfg-if", @@ -1613,9 +1613,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.105" +version = "0.9.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc" +checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" dependencies = [ "cc", "libc", @@ -1820,8 +1820,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.0", - "zerocopy 0.8.17", + "rand_core 0.9.1", + "zerocopy 0.8.20", ] [[package]] @@ -1841,7 +1841,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.0", + "rand_core 0.9.1", ] [[package]] @@ -1855,12 +1855,12 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" +checksum = "a88e0da7a2c97baa202165137c158d0a2e824ac465d13d81046727b34cb247d3" dependencies = [ "getrandom 0.3.1", - "zerocopy 0.8.17", + "zerocopy 0.8.20", ] [[package]] @@ -1903,9 +1903,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f" dependencies = [ "bitflags 2.8.0", ] @@ -2007,15 +2007,14 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.8" +version = "0.17.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "e75ec5e92c4d8aede845126adc388046234541629e76029599ed35a003c7ed24" dependencies = [ "cc", "cfg-if", "getrandom 0.2.15", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] @@ -2055,9 +2054,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.22" +version = "0.23.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7" +checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" dependencies = [ "once_cell", "rustls-pki-types", @@ -2144,18 +2143,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.217" +version = "1.0.218" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.218" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" dependencies = [ "proc-macro2", "quote", @@ -2164,9 +2163,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.138" +version = "1.0.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" dependencies = [ "itoa", "memchr", @@ -2274,9 +2273,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" [[package]] name = "socket2" @@ -2317,12 +2316,6 @@ dependencies = [ "zvariant", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -2434,9 +2427,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.16.0" +version = "3.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" +checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" dependencies = [ "cfg-if", "fastrand 2.3.0", @@ -2641,9 +2634,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "uds_windows" @@ -2658,9 +2651,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" +checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" [[package]] name = "unicode-segmentation" @@ -3324,11 +3317,11 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.17" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa91407dacce3a68c56de03abe2760159582b846c6a4acd2f456618087f12713" +checksum = "dde3bb8c68a8f3f1ed4ac9221aad6b10cece3e60a8e2ea54a6a2dec806d0084c" dependencies = [ - "zerocopy-derive 0.8.17", + "zerocopy-derive 0.8.20", ] [[package]] @@ -3344,9 +3337,9 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.8.17" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06718a168365cad3d5ff0bb133aad346959a2074bd4a85c121255a11304a8626" +checksum = "eea57037071898bf96a6da35fd626f4f27e9cee3ead2a6c703cf09d472b2e700" dependencies = [ "proc-macro2", "quote", From 543784c940ee613ac2d7e93b68013b81445b3983 Mon Sep 17 00:00:00 2001 From: dhonus Date: Fri, 21 Feb 2025 17:43:56 +0100 Subject: [PATCH 25/26] fix: added parse alias --- src/keyboard.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/keyboard.rs b/src/keyboard.rs index f73b167..9215620 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -2395,6 +2395,7 @@ pub enum ActiveTab { #[derive(Debug, PartialEq, Clone, Copy, Default, Serialize, Deserialize)] pub enum ActiveSection { #[default] + #[serde(alias = "Artists")] // TODO: remove -- backwards compatibility List, Tracks, Queue, From ccbbf8d2bc5b906fbd34a99ffd43240c12b8d80c Mon Sep 17 00:00:00 2001 From: dhonus Date: Fri, 21 Feb 2025 17:44:53 +0100 Subject: [PATCH 26/26] chore: bump version --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 77fb36c..0be781c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1353,7 +1353,7 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jellyfin-tui" -version = "1.1.2-dev" +version = "1.1.2" dependencies = [ "chrono", "color-thief", diff --git a/Cargo.toml b/Cargo.toml index e82a986..c1b5ab4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jellyfin-tui" -version = "1.1.2-dev" +version = "1.1.2" edition = "2021" [dependencies]