From 51b789fd0f5f28942e7d9c014c73d992cb0e92a0 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Sat, 10 Feb 2024 19:23:19 -0700 Subject: [PATCH] Refactored tables and loading blocks to use the new dedicated widgets for Tables and Loading blocks --- Cargo.lock | 238 +++++----- src/ui/mod.rs | 109 +---- .../collections/collection_details_ui.rs | 204 ++++----- src/ui/radarr_ui/collections/mod.rs | 118 +++-- src/ui/radarr_ui/downloads/mod.rs | 133 +++--- src/ui/radarr_ui/indexers/edit_indexer_ui.rs | 5 +- .../radarr_ui/indexers/indexer_settings_ui.rs | 5 +- src/ui/radarr_ui/indexers/mod.rs | 147 +++--- .../indexers/test_all_indexers_ui.rs | 85 ++-- src/ui/radarr_ui/library/add_movie_ui.rs | 182 ++++---- src/ui/radarr_ui/library/mod.rs | 162 ++++--- src/ui/radarr_ui/library/movie_details_ui.rs | 433 +++++++++--------- src/ui/radarr_ui/mod.rs | 6 +- src/ui/radarr_ui/root_folders/mod.rs | 89 ++-- src/ui/radarr_ui/system/mod.rs | 152 +++--- src/ui/radarr_ui/system/system_details_ui.rs | 71 ++- src/ui/widgets/loading_block.rs | 33 ++ src/ui/widgets/managarr_table.rs | 148 ++++++ src/ui/widgets/mod.rs | 4 +- 19 files changed, 1174 insertions(+), 1150 deletions(-) create mode 100644 src/ui/widgets/loading_block.rs create mode 100644 src/ui/widgets/managarr_table.rs diff --git a/Cargo.lock b/Cargo.lock index ecf371a..70cb947 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,9 +75,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] name = "anstyle-parse" @@ -224,9 +224,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb" dependencies = [ "android-tzdata", "iana-time-zone", @@ -234,7 +234,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.48.5", + "windows-targets 0.52.0", ] [[package]] @@ -274,7 +274,7 @@ checksum = "15d296c475c6ed4093824c28e222420831d27577aaaf0a1163a3b7fc35b248a5" dependencies = [ "directories", "serde", - "serde_yaml 0.9.30", + "serde_yaml", "thiserror", ] @@ -374,9 +374,9 @@ dependencies = [ [[package]] name = "either" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "encoding_rs" @@ -569,19 +569,13 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.1.0", + "indexmap", "slab", "tokio", "tokio-util", "tracing", ] -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "hashbrown" version = "0.14.3" @@ -600,9 +594,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" +checksum = "d0c62115964e08cb8039170eb33c1d0e2388a256930279edca206fff675f82c3" [[package]] name = "http" @@ -699,9 +693,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.59" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -732,22 +726,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.3" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - -[[package]] -name = "indexmap" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown", ] [[package]] @@ -764,9 +748,9 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "itertools" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] @@ -779,9 +763,9 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" -version = "0.3.67" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" +checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" dependencies = [ "wasm-bindgen", ] @@ -794,9 +778,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.152" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libredox" @@ -809,12 +793,6 @@ dependencies = [ "redox_syscall", ] -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -848,9 +826,9 @@ checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7" [[package]] name = "log4rs" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d36ca1786d9e79b8193a68d480a0907b612f109537115c6ff655a3a1967533fd" +checksum = "0816135ae15bd0391cf284eab37e6e3ee0a6ee63d2ceeb659862bd8d0a984ca6" dependencies = [ "anyhow", "arc-swap", @@ -861,11 +839,13 @@ dependencies = [ "libc", "log", "log-mdc", + "once_cell", "parking_lot", + "rand", "serde", "serde-value", "serde_json", - "serde_yaml 0.8.26", + "serde_yaml", "thiserror", "thread-id", "typemap-ors", @@ -874,11 +854,11 @@ dependencies = [ [[package]] name = "lru" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2994eeba8ed550fd9b47a0b38f0242bc3344e496483c6180b69139cc2fa5d1d7" +checksum = "db2c024b41519440580066ba82aab04092b333e09066a5eb86c7c4890df31f22" dependencies = [ - "hashbrown 0.14.3", + "hashbrown", ] [[package]] @@ -905,7 +885,7 @@ dependencies = [ "rstest", "serde", "serde_json", - "serde_yaml 0.9.30", + "serde_yaml", "strum", "strum_macros", "tokio", @@ -927,9 +907,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] @@ -984,10 +964,16 @@ dependencies = [ ] [[package]] -name = "num-traits" -version = "0.2.17" +name = "num-conv" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", ] @@ -1028,9 +1014,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl" -version = "0.10.62" +version = "0.10.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cde4d2d9200ad5909f8dac647e29482e07c3a35de8a13fce7c9c7747ad9f671" +checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" dependencies = [ "bitflags 2.4.2", "cfg-if", @@ -1060,9 +1046,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.98" +version = "0.9.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1665caf8ab2dc9aef43d1c0023bd904633a6a05cb30b0ad59bec2ae986e57a7" +checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" dependencies = [ "cc", "libc", @@ -1173,9 +1159,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.76" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] @@ -1262,9 +1248,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.2" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", @@ -1274,9 +1260,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ "aho-corasick", "memchr", @@ -1297,9 +1283,9 @@ checksum = "e898588f33fdd5b9420719948f9f2a32c922a246964576f71ba7f24f80610fbc" [[package]] name = "reqwest" -version = "0.11.23" +version = "0.11.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" dependencies = [ "base64", "bytes", @@ -1319,9 +1305,11 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", + "sync_wrapper", "system-configuration", "tokio", "tokio-native-tls", @@ -1379,9 +1367,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.30" +version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ "bitflags 2.4.2", "errno", @@ -1390,6 +1378,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + [[package]] name = "rustversion" version = "1.0.14" @@ -1448,9 +1445,9 @@ checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" [[package]] name = "serde" -version = "1.0.195" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" dependencies = [ "serde_derive", ] @@ -1467,9 +1464,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.195" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", @@ -1478,9 +1475,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.111" +version = "1.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" dependencies = [ "itoa", "ryu", @@ -1510,23 +1507,11 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.8.26" +version = "0.9.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" +checksum = "adf8a49373e98a4c5f0ceb5d05aa7c648d75f63774981ed95b7c7443bbd50c6e" dependencies = [ - "indexmap 1.9.3", - "ryu", - "serde", - "yaml-rust", -] - -[[package]] -name = "serde_yaml" -version = "0.9.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1bf28c79a99f70ee1f1d83d10c875d2e70618417fda01ad1785e027579d9d38" -dependencies = [ - "indexmap 2.1.0", + "indexmap", "itoa", "ryu", "serde", @@ -1654,6 +1639,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "system-configuration" version = "0.5.1" @@ -1677,13 +1668,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.9.0" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", "rustix", "windows-sys 0.52.0", ] @@ -1720,12 +1710,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ "deranged", "libc", + "num-conv", "num_threads", "powerfmt", "serde", @@ -1755,9 +1746,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.1" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", @@ -1809,9 +1800,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" dependencies = [ "serde", "serde_spanned", @@ -1830,11 +1821,11 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.21.0" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +checksum = "0c9ffdf896f8daaabf9b66ba8e77ea1ed5ed0f72821b398aba62352e95062951" dependencies = [ - "indexmap 2.1.0", + "indexmap", "serde", "serde_spanned", "toml_datetime", @@ -1903,9 +1894,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" @@ -1989,9 +1980,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" +checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1999,9 +1990,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" +checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" dependencies = [ "bumpalo", "log", @@ -2014,9 +2005,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" +checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97" dependencies = [ "cfg-if", "js-sys", @@ -2026,9 +2017,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" +checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2036,9 +2027,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" +checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" dependencies = [ "proc-macro2", "quote", @@ -2049,15 +2040,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" +checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" [[package]] name = "web-sys" -version = "0.3.67" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" +checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" dependencies = [ "js-sys", "wasm-bindgen", @@ -2236,15 +2227,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - [[package]] name = "yansi" version = "0.5.1" diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 700c1b5..5da9b92 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -4,15 +4,13 @@ use ratatui::layout::{Alignment, Constraint, Flex, Layout, Rect}; use ratatui::style::{Style, Stylize}; use ratatui::text::{Line, Text}; use ratatui::widgets::Paragraph; -use ratatui::widgets::Row; -use ratatui::widgets::Table; use ratatui::widgets::Tabs; -use ratatui::widgets::{Block, Wrap}; +use ratatui::widgets::Wrap; use ratatui::widgets::{Clear, List, ListItem}; use ratatui::Frame; use crate::app::App; -use crate::models::{HorizontallyScrollableText, Route, StatefulList, StatefulTable, TabState}; +use crate::models::{HorizontallyScrollableText, Route, StatefulList, TabState}; use crate::ui::radarr_ui::RadarrUi; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{ @@ -22,6 +20,7 @@ use crate::ui::utils::{ use crate::ui::widgets::button::Button; use crate::ui::widgets::checkbox::Checkbox; use crate::ui::widgets::input_box::InputBox; +use crate::ui::widgets::loading_block::LoadingBlock; mod radarr_ui; mod styles; @@ -275,14 +274,6 @@ fn draw_tabs(f: &mut Frame<'_>, area: Rect, title: &str, tab_state: &TabState) - content_area } -pub struct TableProps<'a, T> { - pub content: Option<&'a mut StatefulTable>, - pub wrapped_content: Option>>, - pub table_headers: Vec<&'a str>, - pub constraints: Vec, - pub help: Option, -} - pub struct ListProps<'a, T> { pub content: &'a mut StatefulList, pub title: &'static str, @@ -291,94 +282,6 @@ pub struct ListProps<'a, T> { pub help: Option, } -fn draw_table<'a, T, F>( - f: &mut Frame<'_>, - area: Rect, - block: Block<'_>, - table_props: TableProps<'a, T>, - row_mapper: F, - is_loading: bool, - highlight: bool, -) where - F: Fn(&T) -> Row<'a>, -{ - let TableProps { - content, - wrapped_content, - table_headers, - constraints, - help, - } = table_props; - - let content_area = draw_help_footer_and_get_content_area(f, area, help); - - #[allow(clippy::unnecessary_unwrap)] - if wrapped_content.is_some() && wrapped_content.as_ref().unwrap().is_some() { - draw_table_contents( - f, - block, - row_mapper, - highlight, - wrapped_content.unwrap().as_mut().unwrap(), - table_headers, - constraints, - content_area, - ); - } else if content.is_some() && !content.as_ref().unwrap().items.is_empty() { - draw_table_contents( - f, - block, - row_mapper, - highlight, - content.unwrap(), - table_headers, - constraints, - content_area, - ); - } else { - loading(f, block, content_area, is_loading); - } -} - -#[allow(clippy::too_many_arguments)] -fn draw_table_contents<'a, T, F>( - f: &mut Frame<'_>, - block: Block<'_>, - row_mapper: F, - highlight: bool, - content: &mut StatefulTable, - table_headers: Vec<&str>, - constraints: Vec, - area: Rect, -) where - F: Fn(&T) -> Row<'a>, -{ - let rows = content.items.iter().map(row_mapper); - - let headers = Row::new(table_headers).default().bold().bottom_margin(0); - - let mut table = Table::new(rows, &constraints).header(headers).block(block); - - if highlight { - table = table - .highlight_style(Style::new().highlight()) - .highlight_symbol(HIGHLIGHT_SYMBOL); - } - - f.render_stateful_widget(table, area, &mut content.state); -} - -pub fn loading(f: &mut Frame<'_>, block: Block<'_>, area: Rect, is_loading: bool) { - if is_loading { - let paragraph = Paragraph::new(Text::from("\n\n Loading ...\n\n")) - .system_function() - .block(block); - f.render_widget(paragraph, area); - } else { - f.render_widget(block, area) - } -} - pub fn draw_prompt_box( f: &mut Frame<'_>, area: Rect, @@ -532,7 +435,7 @@ pub fn draw_list_box<'a, T>( f.render_stateful_widget(list, content_area, &mut content.state); } else { - loading(f, block, content_area, is_loading); + f.render_widget(LoadingBlock::new(is_loading, block), content_area); } } @@ -543,7 +446,9 @@ fn draw_help_footer_and_get_content_area( ) -> Rect { if let Some(help_string) = help { let [content_area, help_footer_area] = - Layout::vertical([Constraint::Fill(0), Constraint::Length(2)]).areas(area); + Layout::vertical([Constraint::Fill(0), Constraint::Length(2)]) + .margin(1) + .areas(area); let help_paragraph = Paragraph::new(Text::from(format!(" {help_string}").help())) .block(layout_block_top_border()) diff --git a/src/ui/radarr_ui/collections/collection_details_ui.rs b/src/ui/radarr_ui/collections/collection_details_ui.rs index 9c8c142..9343d8f 100644 --- a/src/ui/radarr_ui/collections/collection_details_ui.rs +++ b/src/ui/radarr_ui/collections/collection_details_ui.rs @@ -18,7 +18,8 @@ use crate::ui::utils::{ borderless_block, get_width_from_percentage, layout_block_top_border_with_title, title_block, title_style, }; -use crate::ui::{draw_large_popup_over, draw_small_popup_over, draw_table, DrawUi, TableProps}; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::{draw_large_popup_over, draw_small_popup_over, DrawUi}; use crate::utils::convert_runtime; #[cfg(test)] @@ -67,13 +68,10 @@ impl DrawUi for CollectionDetailsUi { } pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - let [description_area, table_area, help_footer_area] = Layout::vertical([ - Constraint::Percentage(25), - Constraint::Percentage(70), - Constraint::Percentage(5), - ]) - .margin(1) - .areas(area); + let [description_area, table_area] = + Layout::vertical([Constraint::Percentage(25), Constraint::Fill(0)]) + .margin(1) + .areas(area); let collection_selection = if let Some(filtered_collections) = app.data.radarr_data.filtered_collections.as_ref() { filtered_collections.current_selection() @@ -97,13 +95,63 @@ pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) .current_selection() .clone() }; - let help_text = Text::from( - format!( - "<↑↓> scroll table | {}", - build_context_clue_string(&COLLECTION_DETAILS_CONTEXT_CLUES) - ) - .help(), - ); + let movie_row_mapper = |movie: &CollectionMovie| { + let in_library = if app + .data + .radarr_data + .movies + .items + .iter() + .any(|mov| mov.tmdb_id == movie.tmdb_id) + { + "✔" + } else { + "" + }; + movie.title.scroll_left_or_reset( + get_width_from_percentage(table_area, 20), + current_selection == *movie, + app.tick_count % app.ticks_until_scroll == 0, + ); + let (hours, minutes) = convert_runtime(movie.runtime); + let imdb_rating = movie + .ratings + .imdb + .clone() + .unwrap_or_default() + .value + .as_f64() + .unwrap(); + let rotten_tomatoes_rating = movie + .ratings + .rotten_tomatoes + .clone() + .unwrap_or_default() + .value + .as_u64() + .unwrap(); + let imdb_rating = if imdb_rating == 0.0 { + String::new() + } else { + format!("{imdb_rating:.1}") + }; + let rotten_tomatoes_rating = if rotten_tomatoes_rating == 0 { + String::new() + } else { + format!("{rotten_tomatoes_rating}%") + }; + + Row::new(vec![ + Cell::from(in_library), + Cell::from(movie.title.to_string()), + Cell::from(movie.year.to_string()), + Cell::from(format!("{hours}h {minutes}m")), + Cell::from(imdb_rating), + Cell::from(rotten_tomatoes_rating), + Cell::from(movie.genres.join(", ")), + ]) + .primary() + }; let monitored = if collection_selection.monitored { "Yes" } else { @@ -115,10 +163,14 @@ pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) "No" }; let minimum_availability = collection_selection.minimum_availability.to_display_str(); + let help_footer = format!( + "<↑↓> scroll table | {}", + build_context_clue_string(&COLLECTION_DETAILS_CONTEXT_CLUES) + ); let collection_description = Text::from(vec![ Line::from(vec![ - "Overview ".primary().bold(), + "Overview: ".primary().bold(), collection_selection .overview .clone() @@ -151,102 +203,36 @@ pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) let description_paragraph = Paragraph::new(collection_description) .block(borderless_block()) .wrap(Wrap { trim: false }); - let help_paragraph = Paragraph::new(help_text) - .block(borderless_block()) - .alignment(Alignment::Center); + let movies_table = ManagarrTable::new( + Some(&mut app.data.radarr_data.collection_movies), + movie_row_mapper, + ) + .block(layout_block_top_border_with_title(title_style("Movies"))) + .loading(app.is_loading) + .footer_alignment(Alignment::Center) + .footer(Some(help_footer)) + .headers([ + "✔", + "Title", + "Year", + "Runtime", + "IMDB Rating", + "Rotten Tomatoes Rating", + "Genres", + ]) + .constraints([ + Constraint::Percentage(2), + Constraint::Percentage(20), + Constraint::Percentage(8), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(18), + Constraint::Percentage(28), + ]); f.render_widget(title_block(&collection_selection.title.text), area); - f.render_widget(description_paragraph, description_area); - f.render_widget(help_paragraph, help_footer_area); - - draw_table( - f, - table_area, - layout_block_top_border_with_title(title_style("Movies")), - TableProps { - content: Some(&mut app.data.radarr_data.collection_movies), - wrapped_content: None, - table_headers: vec![ - "✔", - "Title", - "Year", - "Runtime", - "IMDB Rating", - "Rotten Tomatoes Rating", - "Genres", - ], - constraints: vec![ - Constraint::Percentage(2), - Constraint::Percentage(20), - Constraint::Percentage(8), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(18), - Constraint::Percentage(28), - ], - help: None, - }, - |movie| { - let in_library = if app - .data - .radarr_data - .movies - .items - .iter() - .any(|mov| mov.tmdb_id == movie.tmdb_id) - { - "✔" - } else { - "" - }; - movie.title.scroll_left_or_reset( - get_width_from_percentage(table_area, 20), - current_selection == *movie, - app.tick_count % app.ticks_until_scroll == 0, - ); - let (hours, minutes) = convert_runtime(movie.runtime); - let imdb_rating = movie - .ratings - .imdb - .clone() - .unwrap_or_default() - .value - .as_f64() - .unwrap(); - let rotten_tomatoes_rating = movie - .ratings - .rotten_tomatoes - .clone() - .unwrap_or_default() - .value - .as_u64() - .unwrap(); - let imdb_rating = if imdb_rating == 0.0 { - String::new() - } else { - format!("{imdb_rating:.1}") - }; - let rotten_tomatoes_rating = if rotten_tomatoes_rating == 0 { - String::new() - } else { - format!("{rotten_tomatoes_rating}%") - }; - - Row::new(vec![ - Cell::from(in_library), - Cell::from(movie.title.to_string()), - Cell::from(movie.year.to_string()), - Cell::from(format!("{hours}h {minutes}m")), - Cell::from(imdb_rating), - Cell::from(rotten_tomatoes_rating), - Cell::from(movie.genres.join(", ")), - ]) - .primary() - }, - app.is_loading, - true, - ); + f.render_widget(movies_table, table_area); } fn draw_movie_overview(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { diff --git a/src/ui/radarr_ui/collections/mod.rs b/src/ui/radarr_ui/collections/mod.rs index 1a1f92a..0f4fc77 100644 --- a/src/ui/radarr_ui/collections/mod.rs +++ b/src/ui/radarr_ui/collections/mod.rs @@ -12,9 +12,10 @@ use crate::ui::radarr_ui::collections::collection_details_ui::CollectionDetailsU use crate::ui::radarr_ui::collections::edit_collection_ui::EditCollectionUi; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; +use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::{ draw_error_message_popup, draw_input_box_popup, draw_popup_over, draw_prompt_box, - draw_prompt_popup_over, draw_table, DrawUi, TableProps, + draw_prompt_popup_over, DrawUi, }; mod collection_details_ui; @@ -113,67 +114,62 @@ pub(super) fn draw_collections(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) Some(filtered_collections) if !app.data.radarr_data.is_filtering => Some(filtered_collections), _ => Some(&mut app.data.radarr_data.collections), }; - draw_table( - f, - area, - layout_block_top_border(), - TableProps { - content, - wrapped_content: None, - table_headers: vec![ - "Collection", - "Number of Movies", - "Root Folder Path", - "Quality Profile", - "Search on Add", - "Monitored", - ], - constraints: vec![ - Constraint::Percentage(25), - Constraint::Percentage(15), - Constraint::Percentage(15), - Constraint::Percentage(15), - Constraint::Percentage(15), - Constraint::Percentage(15), - ], - help: app - .data - .radarr_data - .main_tabs - .get_active_tab_contextual_help(), - }, - |collection| { - let number_of_movies = collection.movies.clone().unwrap_or_default().len(); - collection.title.scroll_left_or_reset( - get_width_from_percentage(area, 25), - *collection == current_selection, - app.tick_count % app.ticks_until_scroll == 0, - ); - let monitored = if collection.monitored { "🏷" } else { "" }; - let search_on_add = if collection.search_on_add { - "Yes" - } else { - "No" - }; + let collections_table_footer = app + .data + .radarr_data + .main_tabs + .get_active_tab_contextual_help(); + let collection_row_mapping = |collection: &Collection| { + let number_of_movies = collection.movies.clone().unwrap_or_default().len(); + collection.title.scroll_left_or_reset( + get_width_from_percentage(area, 25), + *collection == current_selection, + app.tick_count % app.ticks_until_scroll == 0, + ); + let monitored = if collection.monitored { "🏷" } else { "" }; + let search_on_add = if collection.search_on_add { + "Yes" + } else { + "No" + }; - Row::new(vec![ - Cell::from(collection.title.to_string()), - Cell::from(number_of_movies.to_string()), - Cell::from(collection.root_folder_path.clone().unwrap_or_default()), - Cell::from( - quality_profile_map - .get_by_left(&collection.quality_profile_id) - .unwrap() - .to_owned(), - ), - Cell::from(search_on_add), - Cell::from(monitored), - ]) - .primary() - }, - app.is_loading, - true, - ); + Row::new(vec![ + Cell::from(collection.title.to_string()), + Cell::from(number_of_movies.to_string()), + Cell::from(collection.root_folder_path.clone().unwrap_or_default()), + Cell::from( + quality_profile_map + .get_by_left(&collection.quality_profile_id) + .unwrap() + .to_owned(), + ), + Cell::from(search_on_add), + Cell::from(monitored), + ]) + .primary() + }; + let collections_table = ManagarrTable::new(content, collection_row_mapping) + .loading(app.is_loading) + .footer(collections_table_footer) + .block(layout_block_top_border()) + .headers([ + "Collection", + "Number of Movies", + "Root Folder Path", + "Quality Profile", + "Search on Add", + "Monitored", + ]) + .constraints([ + Constraint::Percentage(25), + Constraint::Percentage(15), + Constraint::Percentage(15), + Constraint::Percentage(15), + Constraint::Percentage(15), + Constraint::Percentage(15), + ]); + + f.render_widget(collections_table, area); } fn draw_update_all_collections_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { diff --git a/src/ui/radarr_ui/downloads/mod.rs b/src/ui/radarr_ui/downloads/mod.rs index 1a48e7e..a5cae55 100644 --- a/src/ui/radarr_ui/downloads/mod.rs +++ b/src/ui/radarr_ui/downloads/mod.rs @@ -8,7 +8,8 @@ use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLO use crate::models::{HorizontallyScrollableText, Route}; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; -use crate::ui::{draw_prompt_box, draw_prompt_popup_over, draw_table, DrawUi, TableProps}; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::{draw_prompt_box, draw_prompt_popup_over, DrawUi}; use crate::utils::convert_to_gb; #[cfg(test)] @@ -48,76 +49,74 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } else { app.data.radarr_data.downloads.current_selection().clone() }; + let downloads_table_footer = app + .data + .radarr_data + .main_tabs + .get_active_tab_contextual_help(); - draw_table( - f, - area, - layout_block_top_border(), - TableProps { - content: Some(&mut app.data.radarr_data.downloads), - wrapped_content: None, - table_headers: vec![ - "Title", - "Percent Complete", - "Size", - "Output Path", - "Indexer", - "Download Client", - ], - constraints: vec![ - Constraint::Percentage(30), - Constraint::Percentage(11), - Constraint::Percentage(11), - Constraint::Percentage(18), - Constraint::Percentage(17), - Constraint::Percentage(13), - ], - help: app - .data - .radarr_data - .main_tabs - .get_active_tab_contextual_help(), - }, - |download_record| { - let DownloadRecord { - title, - size, - sizeleft, - download_client, - indexer, - output_path, - .. - } = download_record; + let downloads_row_mapping = |download_record: &DownloadRecord| { + let DownloadRecord { + title, + size, + sizeleft, + download_client, + indexer, + output_path, + .. + } = download_record; - if output_path.is_some() { - output_path.as_ref().unwrap().scroll_left_or_reset( - get_width_from_percentage(area, 18), - current_selection == *download_record, - app.tick_count % app.ticks_until_scroll == 0, - ); - } + if output_path.is_some() { + output_path.as_ref().unwrap().scroll_left_or_reset( + get_width_from_percentage(area, 18), + current_selection == *download_record, + app.tick_count % app.ticks_until_scroll == 0, + ); + } - let percent = 1f64 - (*sizeleft as f64 / *size as f64); - let file_size: f64 = convert_to_gb(*size); + let percent = 1f64 - (*sizeleft as f64 / *size as f64); + let file_size: f64 = convert_to_gb(*size); - Row::new(vec![ - Cell::from(title.to_owned()), - Cell::from(format!("{:.0}%", percent * 100.0)), - Cell::from(format!("{file_size:.2} GB")), - Cell::from( - output_path - .as_ref() - .unwrap_or(&HorizontallyScrollableText::default()) - .to_string(), - ), - Cell::from(indexer.to_owned()), - Cell::from(download_client.to_owned()), - ]) - .primary() - }, - app.is_loading, - true, - ); + Row::new(vec![ + Cell::from(title.to_owned()), + Cell::from(format!("{:.0}%", percent * 100.0)), + Cell::from(format!("{file_size:.2} GB")), + Cell::from( + output_path + .as_ref() + .unwrap_or(&HorizontallyScrollableText::default()) + .to_string(), + ), + Cell::from(indexer.to_owned()), + Cell::from(download_client.to_owned()), + ]) + .primary() + }; + let downloads_table = ManagarrTable::new( + Some(&mut app.data.radarr_data.downloads), + downloads_row_mapping, + ) + .block(layout_block_top_border()) + .loading(app.is_loading) + .footer(downloads_table_footer) + .headers([ + "Title", + "Percent Complete", + "Size", + "Output Path", + "Indexer", + "Download Client", + ]) + .constraints([ + Constraint::Percentage(30), + Constraint::Percentage(11), + Constraint::Percentage(11), + Constraint::Percentage(18), + Constraint::Percentage(17), + Constraint::Percentage(13), + ]); + + f.render_widget(downloads_table, area); } fn draw_delete_download_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { diff --git a/src/ui/radarr_ui/indexers/edit_indexer_ui.rs b/src/ui/radarr_ui/indexers/edit_indexer_ui.rs index 2268b28..8f0d150 100644 --- a/src/ui/radarr_ui/indexers/edit_indexer_ui.rs +++ b/src/ui/radarr_ui/indexers/edit_indexer_ui.rs @@ -8,7 +8,8 @@ use crate::ui::utils::title_block_centered; use crate::ui::widgets::button::Button; use crate::ui::widgets::checkbox::Checkbox; use crate::ui::widgets::input_box::InputBox; -use crate::ui::{draw_popup_over, loading, DrawUi}; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::{draw_popup_over, DrawUi}; use ratatui::layout::{Constraint, Flex, Layout, Rect}; use ratatui::Frame; @@ -160,6 +161,6 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { f.render_widget(cancel_button, cancel_area); } } else { - loading(f, block, area, app.is_loading); + f.render_widget(LoadingBlock::new(app.is_loading, block), area); } } diff --git a/src/ui/radarr_ui/indexers/indexer_settings_ui.rs b/src/ui/radarr_ui/indexers/indexer_settings_ui.rs index 384782c..6fb0d83 100644 --- a/src/ui/radarr_ui/indexers/indexer_settings_ui.rs +++ b/src/ui/radarr_ui/indexers/indexer_settings_ui.rs @@ -13,7 +13,8 @@ use crate::ui::utils::title_block_centered; use crate::ui::widgets::button::Button; use crate::ui::widgets::checkbox::Checkbox; use crate::ui::widgets::input_box::InputBox; -use crate::ui::{draw_popup_over, loading, DrawUi}; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::{draw_popup_over, DrawUi}; #[cfg(test)] #[path = "indexer_settings_ui_tests.rs"] @@ -155,6 +156,6 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: f.render_widget(save_button, save_area); f.render_widget(cancel_button, cancel_area); } else { - loading(f, block, area, app.is_loading); + f.render_widget(LoadingBlock::new(app.is_loading, block), area); } } diff --git a/src/ui/radarr_ui/indexers/mod.rs b/src/ui/radarr_ui/indexers/mod.rs index 6c025c4..096c011 100644 --- a/src/ui/radarr_ui/indexers/mod.rs +++ b/src/ui/radarr_ui/indexers/mod.rs @@ -12,7 +12,8 @@ use crate::ui::radarr_ui::indexers::indexer_settings_ui::IndexerSettingsUi; use crate::ui::radarr_ui::indexers::test_all_indexers_ui::TestAllIndexersUi; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::layout_block_top_border; -use crate::ui::{draw_prompt_box, draw_prompt_popup_over, draw_table, DrawUi, TableProps}; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::{draw_prompt_box, draw_prompt_popup_over, DrawUi}; mod edit_indexer_ui; mod indexer_settings_ui; @@ -59,83 +60,81 @@ impl DrawUi for IndexersUi { } fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - draw_table( - f, - area, - layout_block_top_border(), - TableProps { - content: Some(&mut app.data.radarr_data.indexers), - wrapped_content: None, - table_headers: vec![ - "Indexer", - "RSS", - "Automatic Search", - "Interactive Search", - "Priority", - "Tags", - ], - constraints: vec![ - Constraint::Percentage(25), - Constraint::Percentage(13), - Constraint::Percentage(13), - Constraint::Percentage(13), - Constraint::Percentage(13), - Constraint::Percentage(23), - ], - help: app - .data - .radarr_data - .main_tabs - .get_active_tab_contextual_help(), - }, - |indexer: &'_ Indexer| { - let Indexer { - name, - enable_rss, - enable_automatic_search, - enable_interactive_search, - priority, - tags, - .. - } = indexer; - let bool_to_text = |flag: bool| { - if flag { - return Text::from("Enabled").success(); - } + let indexers_row_mapping = |indexer: &'_ Indexer| { + let Indexer { + name, + enable_rss, + enable_automatic_search, + enable_interactive_search, + priority, + tags, + .. + } = indexer; + let bool_to_text = |flag: bool| { + if flag { + return Text::from("Enabled").success(); + } - Text::from("Disabled").failure() - }; + Text::from("Disabled").failure() + }; - let rss = bool_to_text(*enable_rss); - let automatic_search = bool_to_text(*enable_automatic_search); - let interactive_search = bool_to_text(*enable_interactive_search); - let tags: String = tags - .iter() - .map(|tag_id| { - app - .data - .radarr_data - .tags_map - .get_by_left(&tag_id.as_i64().unwrap()) - .unwrap() - .clone() - }) - .collect::>() - .join(", "); + let rss = bool_to_text(*enable_rss); + let automatic_search = bool_to_text(*enable_automatic_search); + let interactive_search = bool_to_text(*enable_interactive_search); + let tags: String = tags + .iter() + .map(|tag_id| { + app + .data + .radarr_data + .tags_map + .get_by_left(&tag_id.as_i64().unwrap()) + .unwrap() + .clone() + }) + .collect::>() + .join(", "); - Row::new(vec![ - Cell::from(name.clone().unwrap_or_default()), - Cell::from(rss), - Cell::from(automatic_search), - Cell::from(interactive_search), - Cell::from(priority.to_string()), - Cell::from(tags), - ]) - .primary() - }, - app.is_loading, - true, + Row::new(vec![ + Cell::from(name.clone().unwrap_or_default()), + Cell::from(rss), + Cell::from(automatic_search), + Cell::from(interactive_search), + Cell::from(priority.to_string()), + Cell::from(tags), + ]) + .primary() + }; + let indexers_table_footer = app + .data + .radarr_data + .main_tabs + .get_active_tab_contextual_help(); + let indexers_table = ManagarrTable::new( + Some(&mut app.data.radarr_data.indexers), + indexers_row_mapping, ) + .block(layout_block_top_border()) + .footer(indexers_table_footer) + .loading(app.is_loading) + .headers([ + "Indexer", + "RSS", + "Automatic Search", + "Interactive Search", + "Priority", + "Tags", + ]) + .constraints([ + Constraint::Percentage(25), + Constraint::Percentage(13), + Constraint::Percentage(13), + Constraint::Percentage(13), + Constraint::Percentage(13), + Constraint::Percentage(23), + ]); + + f.render_widget(indexers_table, area); } fn draw_delete_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { diff --git a/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs b/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs index a42203b..b7c6faf 100644 --- a/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs +++ b/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs @@ -6,10 +6,9 @@ use crate::models::Route; use crate::ui::radarr_ui::indexers::draw_indexers; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{borderless_block, get_width_from_percentage, title_block}; -use crate::ui::{ - draw_help_footer_and_get_content_area, draw_large_popup_over, draw_table, DrawUi, TableProps, -}; -use ratatui::layout::{Constraint, Rect}; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::{draw_large_popup_over, DrawUi}; +use ratatui::layout::{Alignment, Constraint, Rect}; use ratatui::widgets::{Cell, Row}; use ratatui::Frame; @@ -47,47 +46,45 @@ fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, are IndexerTestResultModalItem::default() }; f.render_widget(title_block("Test All Indexers"), area); - let help = Some(format!( + let help_footer = format!( "<↑↓> scroll | {}", build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES) - )); - let content_area = draw_help_footer_and_get_content_area(f, area, help); - - draw_table( - f, - content_area, - borderless_block(), - TableProps { - content: app.data.radarr_data.indexer_test_all_results.as_mut(), - wrapped_content: None, - table_headers: vec!["Indexer", "Pass/Fail", "Failure Messages"], - constraints: vec![ - Constraint::Percentage(20), - Constraint::Percentage(10), - Constraint::Percentage(70), - ], - help: None, - }, - |result| { - result.validation_failures.scroll_left_or_reset( - get_width_from_percentage(area, 86), - *result == current_selection, - app.tick_count % app.ticks_until_scroll == 0, - ); - let pass_fail = if result.is_valid { "✔" } else { "❌" }; - let row = Row::new(vec![ - Cell::from(result.name.to_owned()), - Cell::from(pass_fail.to_owned()), - Cell::from(result.validation_failures.to_string()), - ]); - - if result.is_valid { - row.success() - } else { - row.failure() - } - }, - app.is_loading, - true, ); + let test_results_row_mapping = |result: &IndexerTestResultModalItem| { + result.validation_failures.scroll_left_or_reset( + get_width_from_percentage(area, 86), + *result == current_selection, + app.tick_count % app.ticks_until_scroll == 0, + ); + let pass_fail = if result.is_valid { "✔" } else { "❌" }; + let row = Row::new(vec![ + Cell::from(result.name.to_owned()), + Cell::from(pass_fail.to_owned()), + Cell::from(result.validation_failures.to_string()), + ]); + + if result.is_valid { + row.success() + } else { + row.failure() + } + }; + + let indexers_test_results_table = ManagarrTable::new( + app.data.radarr_data.indexer_test_all_results.as_mut(), + test_results_row_mapping, + ) + .block(borderless_block()) + .loading(app.is_loading) + .footer(Some(help_footer)) + .footer_alignment(Alignment::Center) + .margin(1) + .headers(["Indexer", "Pass/Fail", "Failure Messages"]) + .constraints([ + Constraint::Percentage(20), + Constraint::Percentage(10), + Constraint::Percentage(70), + ]); + + f.render_widget(indexers_test_results_table, area); } diff --git a/src/ui/radarr_ui/library/add_movie_ui.rs b/src/ui/radarr_ui/library/add_movie_ui.rs index c716bec..a063a43 100644 --- a/src/ui/radarr_ui/library/add_movie_ui.rs +++ b/src/ui/radarr_ui/library/add_movie_ui.rs @@ -18,9 +18,10 @@ use crate::ui::utils::{ }; use crate::ui::widgets::button::Button; use crate::ui::widgets::input_box::InputBox; +use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::{ draw_drop_down_popup, draw_error_popup, draw_error_popup_over, draw_large_popup_over, - draw_medium_popup_over, draw_selectable_list, draw_table, DrawUi, TableProps, + draw_medium_popup_over, draw_selectable_list, DrawUi, }; use crate::utils::convert_runtime; use crate::{render_selectable_input_box, App}; @@ -116,6 +117,64 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .unwrap() .offset .borrow(); + let search_results_row_mapping = |movie: &AddMovieSearchResult| { + let (hours, minutes) = convert_runtime(movie.runtime); + let imdb_rating = movie + .ratings + .imdb + .clone() + .unwrap_or_default() + .value + .as_f64() + .unwrap(); + let rotten_tomatoes_rating = movie + .ratings + .rotten_tomatoes + .clone() + .unwrap_or_default() + .value + .as_u64() + .unwrap(); + let imdb_rating = if imdb_rating == 0.0 { + String::new() + } else { + format!("{imdb_rating:.1}") + }; + let rotten_tomatoes_rating = if rotten_tomatoes_rating == 0 { + String::new() + } else { + format!("{rotten_tomatoes_rating}%") + }; + let in_library = if app + .data + .radarr_data + .movies + .items + .iter() + .any(|mov| mov.tmdb_id == movie.tmdb_id) + { + "✔" + } else { + "" + }; + + movie.title.scroll_left_or_reset( + get_width_from_percentage(area, 27), + *movie == current_selection, + app.tick_count % app.ticks_until_scroll == 0, + ); + + Row::new(vec![ + Cell::from(in_library), + Cell::from(movie.title.to_string()), + Cell::from(movie.year.to_string()), + Cell::from(format!("{hours}h {minutes}m")), + Cell::from(imdb_rating), + Cell::from(rotten_tomatoes_rating), + Cell::from(movie.genres.join(", ")), + ]) + .primary() + }; if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { match active_radarr_block { @@ -134,8 +193,14 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { f.render_widget(help_paragraph, help_area); } ActiveRadarrBlock::AddMovieEmptySearchResults => { + let help_text = Text::from(build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES).help()); + let help_paragraph = Paragraph::new(help_text) + .block(borderless_block()) + .alignment(Alignment::Center); + f.render_widget(layout_block(), results_area); draw_error_popup(f, "No movies found matching your query!"); + f.render_widget(help_paragraph, help_area); } ActiveRadarrBlock::AddMovieSearchResults | ActiveRadarrBlock::AddMoviePrompt @@ -150,96 +215,33 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let help_paragraph = Paragraph::new(help_text) .block(borderless_block()) .alignment(Alignment::Center); + let search_results_table = ManagarrTable::new( + app.data.radarr_data.add_searched_movies.as_mut(), + search_results_row_mapping, + ) + .loading(is_loading) + .block(layout_block()) + .headers([ + "✔", + "Title", + "Year", + "Runtime", + "IMDB", + "Rotten Tomatoes", + "Genres", + ]) + .constraints([ + Constraint::Percentage(2), + Constraint::Percentage(27), + Constraint::Percentage(8), + Constraint::Percentage(10), + Constraint::Percentage(8), + Constraint::Percentage(14), + Constraint::Percentage(28), + ]); + + f.render_widget(search_results_table, results_area); f.render_widget(help_paragraph, help_area); - - draw_table( - f, - results_area, - layout_block(), - TableProps { - content: None, - wrapped_content: Some(app.data.radarr_data.add_searched_movies.as_mut()), - table_headers: vec![ - "✔", - "Title", - "Year", - "Runtime", - "IMDB", - "Rotten Tomatoes", - "Genres", - ], - constraints: vec![ - Constraint::Percentage(2), - Constraint::Percentage(27), - Constraint::Percentage(8), - Constraint::Percentage(10), - Constraint::Percentage(8), - Constraint::Percentage(14), - Constraint::Percentage(28), - ], - help: None, - }, - |movie| { - let (hours, minutes) = convert_runtime(movie.runtime); - let imdb_rating = movie - .ratings - .imdb - .clone() - .unwrap_or_default() - .value - .as_f64() - .unwrap(); - let rotten_tomatoes_rating = movie - .ratings - .rotten_tomatoes - .clone() - .unwrap_or_default() - .value - .as_u64() - .unwrap(); - let imdb_rating = if imdb_rating == 0.0 { - String::new() - } else { - format!("{imdb_rating:.1}") - }; - let rotten_tomatoes_rating = if rotten_tomatoes_rating == 0 { - String::new() - } else { - format!("{rotten_tomatoes_rating}%") - }; - let in_library = if app - .data - .radarr_data - .movies - .items - .iter() - .any(|mov| mov.tmdb_id == movie.tmdb_id) - { - "✔" - } else { - "" - }; - - movie.title.scroll_left_or_reset( - get_width_from_percentage(area, 27), - *movie == current_selection, - app.tick_count % app.ticks_until_scroll == 0, - ); - - Row::new(vec![ - Cell::from(in_library), - Cell::from(movie.title.to_string()), - Cell::from(movie.year.to_string()), - Cell::from(format!("{hours}h {minutes}m")), - Cell::from(imdb_rating), - Cell::from(rotten_tomatoes_rating), - Cell::from(movie.genres.join(", ")), - ]) - .primary() - }, - is_loading, - true, - ); } _ => (), } diff --git a/src/ui/radarr_ui/library/mod.rs b/src/ui/radarr_ui/library/mod.rs index e515b56..708368f 100644 --- a/src/ui/radarr_ui/library/mod.rs +++ b/src/ui/radarr_ui/library/mod.rs @@ -12,9 +12,10 @@ use crate::ui::radarr_ui::library::delete_movie_ui::DeleteMovieUi; use crate::ui::radarr_ui::library::edit_movie_ui::EditMovieUi; use crate::ui::radarr_ui::library::movie_details_ui::MovieDetailsUi; use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; +use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::{ draw_error_message_popup, draw_input_box_popup, draw_popup_over, draw_prompt_box, - draw_prompt_popup_over, draw_table, DrawUi, TableProps, + draw_prompt_popup_over, DrawUi, }; use crate::utils::{convert_runtime, convert_to_gb}; @@ -106,90 +107,85 @@ pub(super) fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { Some(filtered_movies) if !app.data.radarr_data.is_filtering => Some(filtered_movies), _ => Some(&mut app.data.radarr_data.movies), }; + let help_footer = app + .data + .radarr_data + .main_tabs + .get_active_tab_contextual_help(); - draw_table( - f, - area, - layout_block_top_border(), - TableProps { - content, - wrapped_content: None, - table_headers: vec![ - "Title", - "Year", - "Studio", - "Runtime", - "Rating", - "Language", - "Size", - "Quality Profile", - "Monitored", - "Tags", - ], - constraints: vec![ - Constraint::Percentage(27), - Constraint::Percentage(4), - Constraint::Percentage(17), - Constraint::Percentage(6), - Constraint::Percentage(6), - Constraint::Percentage(6), - Constraint::Percentage(6), - Constraint::Percentage(10), - Constraint::Percentage(6), - Constraint::Percentage(12), - ], - help: app - .data - .radarr_data - .main_tabs - .get_active_tab_contextual_help(), - }, - |movie| { - movie.title.scroll_left_or_reset( - get_width_from_percentage(area, 27), - *movie == current_selection, - app.tick_count % app.ticks_until_scroll == 0, - ); - let monitored = if movie.monitored { "🏷" } else { "" }; - let (hours, minutes) = convert_runtime(movie.runtime); - let file_size: f64 = convert_to_gb(movie.size_on_disk); - let certification = movie.certification.clone().unwrap_or_default(); - let quality_profile = quality_profile_map - .get_by_left(&movie.quality_profile_id) - .unwrap() - .to_owned(); - let tags = movie - .tags - .iter() - .map(|tag_id| { - tags_map - .get_by_left(&tag_id.as_i64().unwrap()) - .unwrap() - .clone() - }) - .collect::>() - .join(", "); + let library_table_row_mapping = |movie: &Movie| { + movie.title.scroll_left_or_reset( + get_width_from_percentage(area, 27), + *movie == current_selection, + app.tick_count % app.ticks_until_scroll == 0, + ); + let monitored = if movie.monitored { "🏷" } else { "" }; + let (hours, minutes) = convert_runtime(movie.runtime); + let file_size: f64 = convert_to_gb(movie.size_on_disk); + let certification = movie.certification.clone().unwrap_or_default(); + let quality_profile = quality_profile_map + .get_by_left(&movie.quality_profile_id) + .unwrap() + .to_owned(); + let tags = movie + .tags + .iter() + .map(|tag_id| { + tags_map + .get_by_left(&tag_id.as_i64().unwrap()) + .unwrap() + .clone() + }) + .collect::>() + .join(", "); - decorate_with_row_style( - downloads_vec, - movie, - Row::new(vec![ - Cell::from(movie.title.to_string()), - Cell::from(movie.year.to_string()), - Cell::from(movie.studio.to_string()), - Cell::from(format!("{hours}h {minutes}m")), - Cell::from(certification), - Cell::from(movie.original_language.name.to_owned()), - Cell::from(format!("{file_size:.2} GB")), - Cell::from(quality_profile), - Cell::from(monitored.to_owned()), - Cell::from(tags), - ]), - ) - }, - app.is_loading, - true, - ); + decorate_with_row_style( + downloads_vec, + movie, + Row::new(vec![ + Cell::from(movie.title.to_string()), + Cell::from(movie.year.to_string()), + Cell::from(movie.studio.to_string()), + Cell::from(format!("{hours}h {minutes}m")), + Cell::from(certification), + Cell::from(movie.original_language.name.to_owned()), + Cell::from(format!("{file_size:.2} GB")), + Cell::from(quality_profile), + Cell::from(monitored.to_owned()), + Cell::from(tags), + ]), + ) + }; + let library_table = ManagarrTable::new(content, library_table_row_mapping) + .block(layout_block_top_border()) + .loading(app.is_loading) + .footer(help_footer) + .headers([ + "Title", + "Year", + "Studio", + "Runtime", + "Rating", + "Language", + "Size", + "Quality Profile", + "Monitored", + "Tags", + ]) + .constraints([ + Constraint::Percentage(27), + Constraint::Percentage(4), + Constraint::Percentage(17), + Constraint::Percentage(6), + Constraint::Percentage(6), + Constraint::Percentage(6), + Constraint::Percentage(6), + Constraint::Percentage(10), + Constraint::Percentage(6), + Constraint::Percentage(12), + ]); + + f.render_widget(library_table, area); } fn draw_update_all_movies_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { diff --git a/src/ui/radarr_ui/library/movie_details_ui.rs b/src/ui/radarr_ui/library/movie_details_ui.rs index ea8259e..e1fd620 100644 --- a/src/ui/radarr_ui/library/movie_details_ui.rs +++ b/src/ui/radarr_ui/library/movie_details_ui.rs @@ -15,10 +15,11 @@ use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{ borderless_block, get_width_from_percentage, layout_block_bottom_border, layout_block_top_border, }; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::{ draw_drop_down_popup, draw_large_popup_over, draw_prompt_box, draw_prompt_box_with_content, - draw_prompt_popup_over, draw_selectable_list, draw_small_popup_over, draw_table, draw_tabs, - loading, DrawUi, TableProps, + draw_prompt_popup_over, draw_selectable_list, draw_small_popup_over, draw_tabs, DrawUi, }; use crate::utils::convert_to_gb; @@ -189,7 +190,10 @@ fn draw_file_info(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { f.render_widget(video_details_title_paragraph, video_details_title_area); f.render_widget(video_details_paragraph, video_details_area); } - _ => loading(f, layout_block_top_border(), area, app.is_loading), + _ => f.render_widget( + LoadingBlock::new(app.is_loading, layout_block_top_border()), + area, + ), } } @@ -230,11 +234,12 @@ fn draw_movie_details(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { f.render_widget(paragraph, area); } - _ => loading( - f, - block, + _ => f.render_widget( + LoadingBlock::new( + app.is_loading || app.data.radarr_data.movie_details_modal.is_none(), + block, + ), area, - app.is_loading || app.data.radarr_data.movie_details_modal.is_none(), ), } } @@ -249,148 +254,137 @@ fn draw_movie_history(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .current_selection() .clone() }; + let history_row_mapping = |movie_history_item: &MovieHistoryItem| { + let MovieHistoryItem { + source_title, + quality, + languages, + date, + event_type, + } = movie_history_item; - draw_table( - f, - area, - layout_block_top_border(), - TableProps { - content: Some(&mut movie_details_modal.movie_history), - wrapped_content: None, - table_headers: vec!["Source Title", "Event Type", "Languages", "Quality", "Date"], - constraints: vec![ - Constraint::Percentage(34), - Constraint::Percentage(17), - Constraint::Percentage(14), - Constraint::Percentage(14), - Constraint::Percentage(21), - ], - help: app - .data - .radarr_data - .movie_info_tabs - .get_active_tab_contextual_help(), - }, - |movie_history_item| { - let MovieHistoryItem { - source_title, - quality, - languages, - date, - event_type, - } = movie_history_item; + movie_history_item.source_title.scroll_left_or_reset( + get_width_from_percentage(area, 34), + current_selection == *movie_history_item, + app.tick_count % app.ticks_until_scroll == 0, + ); - movie_history_item.source_title.scroll_left_or_reset( - get_width_from_percentage(area, 34), - current_selection == *movie_history_item, - app.tick_count % app.ticks_until_scroll == 0, - ); + Row::new(vec![ + Cell::from(source_title.to_string()), + Cell::from(event_type.to_owned()), + Cell::from( + languages + .iter() + .map(|language| language.name.to_owned()) + .collect::>() + .join(","), + ), + Cell::from(quality.quality.name.to_owned()), + Cell::from(date.to_string()), + ]) + .success() + }; + let help_footer = app + .data + .radarr_data + .movie_info_tabs + .get_active_tab_contextual_help(); + let history_table = ManagarrTable::new( + Some(&mut movie_details_modal.movie_history), + history_row_mapping, + ) + .block(layout_block_top_border()) + .loading(app.is_loading) + .footer(help_footer) + .headers(["Source Title", "Event Type", "Languages", "Quality", "Date"]) + .constraints([ + Constraint::Percentage(34), + Constraint::Percentage(17), + Constraint::Percentage(14), + Constraint::Percentage(14), + Constraint::Percentage(21), + ]); - Row::new(vec![ - Cell::from(source_title.to_string()), - Cell::from(event_type.to_owned()), - Cell::from( - languages - .iter() - .map(|language| language.name.to_owned()) - .collect::>() - .join(","), - ), - Cell::from(quality.quality.name.to_owned()), - Cell::from(date.to_string()), - ]) - .success() - }, - app.is_loading, - true, - ); + f.render_widget(history_table, area); } } fn draw_movie_cast(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - draw_table( - f, - area, - layout_block_top_border(), - TableProps { - content: Some( - &mut app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_cast, - ), - wrapped_content: None, - constraints: iter::repeat(Constraint::Ratio(1, 2)).take(2).collect(), - table_headers: vec!["Cast Member", "Character"], - help: app - .data - .radarr_data - .movie_info_tabs - .get_active_tab_contextual_help(), - }, - |cast_member| { - let Credit { - person_name, - character, - .. - } = cast_member; + let cast_row_mapping = |cast_member: &Credit| { + let Credit { + person_name, + character, + .. + } = cast_member; - Row::new(vec![ - Cell::from(person_name.to_owned()), - Cell::from(character.clone().unwrap_or_default()), - ]) - .success() - }, - app.is_loading, - true, + Row::new(vec![ + Cell::from(person_name.to_owned()), + Cell::from(character.clone().unwrap_or_default()), + ]) + .success() + }; + let content = Some( + &mut app + .data + .radarr_data + .movie_details_modal + .as_mut() + .unwrap() + .movie_cast, ); + let help_footer = app + .data + .radarr_data + .movie_info_tabs + .get_active_tab_contextual_help(); + let cast_table = ManagarrTable::new(content, cast_row_mapping) + .block(layout_block_top_border()) + .footer(help_footer) + .loading(app.is_loading) + .headers(["Cast Member", "Character"]) + .constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]); + + f.render_widget(cast_table, area); } fn draw_movie_crew(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - draw_table( - f, - area, - layout_block_top_border(), - TableProps { - content: Some( - &mut app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_crew, - ), - wrapped_content: None, - constraints: iter::repeat(Constraint::Ratio(1, 3)).take(3).collect(), - table_headers: vec!["Crew Member", "Job", "Department"], - help: app - .data - .radarr_data - .movie_info_tabs - .get_active_tab_contextual_help(), - }, - |crew_member| { - let Credit { - person_name, - job, - department, - .. - } = crew_member; + let crew_row_mapping = |crew_member: &Credit| { + let Credit { + person_name, + job, + department, + .. + } = crew_member; - Row::new(vec![ - Cell::from(person_name.to_owned()), - Cell::from(job.clone().unwrap_or_default()), - Cell::from(department.clone().unwrap_or_default()), - ]) - .success() - }, - app.is_loading, - true, + Row::new(vec![ + Cell::from(person_name.to_owned()), + Cell::from(job.clone().unwrap_or_default()), + Cell::from(department.clone().unwrap_or_default()), + ]) + .success() + }; + let content = Some( + &mut app + .data + .radarr_data + .movie_details_modal + .as_mut() + .unwrap() + .movie_crew, ); + let help_footer = app + .data + .radarr_data + .movie_info_tabs + .get_active_tab_contextual_help(); + let crew_table = ManagarrTable::new(content, crew_row_mapping) + .block(layout_block_top_border()) + .loading(app.is_loading) + .headers(["Crew Member", "Job", "Department"]) + .constraints(iter::repeat(Constraint::Ratio(1, 3)).take(3)) + .footer(help_footer); + + f.render_widget(crew_table, area); } fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { @@ -407,6 +401,11 @@ fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { _ => (Release::default(), true, None), }; let current_route = *app.get_current_route(); + let help_footer = app + .data + .radarr_data + .movie_info_tabs + .get_active_tab_contextual_help(); let mut table_headers_vec = vec![ "Source".to_owned(), "Age".to_owned(), @@ -442,99 +441,89 @@ fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ReleaseField::Quality => table_headers_vec[8].push_str(direction), } } + let content = Some( + &mut app + .data + .radarr_data + .movie_details_modal + .as_mut() + .unwrap() + .movie_releases, + ); + let releases_row_mapping = |release: &Release| { + let Release { + protocol, + age, + title, + indexer, + size, + rejected, + seeders, + leechers, + languages, + quality, + .. + } = release; + let age = format!("{age} days"); + title.scroll_left_or_reset( + get_width_from_percentage(area, 30), + current_selection == *release + && current_route != ActiveRadarrBlock::ManualSearchConfirmPrompt.into(), + app.tick_count % app.ticks_until_scroll == 0, + ); + let size = convert_to_gb(*size); + let rejected_str = if *rejected { "⛔" } else { "" }; + let peers = if seeders.is_none() || leechers.is_none() { + Text::from("") + } else { + let seeders = seeders.clone().unwrap().as_u64().unwrap(); + let leechers = leechers.clone().unwrap().as_u64().unwrap(); - draw_table( - f, - area, - layout_block_top_border(), - TableProps { - content: Some( - &mut app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_releases, - ), - wrapped_content: None, - constraints: vec![ - Constraint::Length(9), - Constraint::Length(10), - Constraint::Length(5), - Constraint::Percentage(30), - Constraint::Percentage(18), - Constraint::Length(12), - Constraint::Length(12), - Constraint::Percentage(7), - Constraint::Percentage(10), - ], - table_headers: table_headers_vec.iter().map(|s| &**s).collect(), - help: app - .data - .radarr_data - .movie_info_tabs - .get_active_tab_contextual_help(), - }, - |release| { - let Release { - protocol, - age, - title, - indexer, - size, - rejected, + decorate_peer_style( seeders, leechers, - languages, - quality, - .. - } = release; - let age = format!("{age} days"); - title.scroll_left_or_reset( - get_width_from_percentage(area, 30), - current_selection == *release - && current_route != ActiveRadarrBlock::ManualSearchConfirmPrompt.into(), - app.tick_count % app.ticks_until_scroll == 0, - ); - let size = convert_to_gb(*size); - let rejected_str = if *rejected { "⛔" } else { "" }; - let peers = if seeders.is_none() || leechers.is_none() { - Text::from("") - } else { - let seeders = seeders.clone().unwrap().as_u64().unwrap(); - let leechers = leechers.clone().unwrap().as_u64().unwrap(); + Text::from(format!("{seeders} / {leechers}")), + ) + }; - decorate_peer_style( - seeders, - leechers, - Text::from(format!("{seeders} / {leechers}")), - ) - }; + let language = if languages.is_some() { + languages.clone().unwrap()[0].name.clone() + } else { + String::new() + }; + let quality = quality.quality.name.clone(); - let language = if languages.is_some() { - languages.clone().unwrap()[0].name.clone() - } else { - String::new() - }; - let quality = quality.quality.name.clone(); + Row::new(vec![ + Cell::from(protocol.clone()), + Cell::from(age), + Cell::from(rejected_str), + Cell::from(title.to_string()), + Cell::from(indexer.clone()), + Cell::from(format!("{size:.1} GB")), + Cell::from(peers), + Cell::from(language), + Cell::from(quality), + ]) + .primary() + }; + let releases_table = ManagarrTable::new(content, releases_row_mapping) + .block(layout_block_top_border()) + .loading(app.is_loading || is_empty) + .footer(help_footer) + .headers(table_headers_vec.iter().map(|s| &**s)) + .constraints([ + Constraint::Length(9), + Constraint::Length(10), + Constraint::Length(5), + Constraint::Percentage(30), + Constraint::Percentage(18), + Constraint::Length(12), + Constraint::Length(12), + Constraint::Percentage(7), + Constraint::Percentage(10), + ]); - Row::new(vec![ - Cell::from(protocol.clone()), - Cell::from(age), - Cell::from(rejected_str), - Cell::from(title.to_string()), - Cell::from(indexer.clone()), - Cell::from(format!("{size:.1} GB")), - Cell::from(peers), - Cell::from(language), - Cell::from(quality), - ]) - .primary() - }, - app.is_loading || is_empty, - true, - ); + f.render_widget(releases_table, area); } fn draw_manual_search_confirm_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { diff --git a/src/ui/radarr_ui/mod.rs b/src/ui/radarr_ui/mod.rs index f3d9899..68095f3 100644 --- a/src/ui/radarr_ui/mod.rs +++ b/src/ui/radarr_ui/mod.rs @@ -13,7 +13,6 @@ use crate::models::radarr_models::{DiskSpace, DownloadRecord, Movie, RootFolder} use crate::models::servarr_data::radarr::radarr_data::RadarrData; use crate::models::Route; use crate::ui::draw_tabs; -use crate::ui::loading; use crate::ui::radarr_ui::collections::CollectionsUi; use crate::ui::radarr_ui::downloads::DownloadsUi; use crate::ui::radarr_ui::indexers::IndexersUi; @@ -24,6 +23,7 @@ use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{ borderless_block, layout_block, line_gauge_with_label, line_gauge_with_title, title_block, }; +use crate::ui::widgets::loading_block::LoadingBlock; use crate::ui::DrawUi; use crate::utils::convert_to_gb; @@ -163,7 +163,7 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { ) } } else { - loading(f, block, area, app.is_loading); + f.render_widget(LoadingBlock::new(app.is_loading, block), area); } } @@ -195,7 +195,7 @@ fn draw_downloads_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { f.render_widget(download_gauge, download_item_areas[i]); } } else { - loading(f, block, area, app.is_loading); + f.render_widget(LoadingBlock::new(app.is_loading, block), area); } } diff --git a/src/ui/radarr_ui/root_folders/mod.rs b/src/ui/radarr_ui/root_folders/mod.rs index d132552..cb92603 100644 --- a/src/ui/radarr_ui/root_folders/mod.rs +++ b/src/ui/radarr_ui/root_folders/mod.rs @@ -8,9 +8,9 @@ use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ROOT_F use crate::models::Route; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::layout_block_top_border; +use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::{ - draw_input_box_popup, draw_popup_over, draw_prompt_box, draw_prompt_popup_over, draw_table, - DrawUi, TableProps, + draw_input_box_popup, draw_popup_over, draw_prompt_box, draw_prompt_popup_over, DrawUi, }; use crate::utils::convert_to_gb; @@ -56,51 +56,50 @@ impl DrawUi for RootFoldersUi { } fn draw_root_folders(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - draw_table( - f, - area, - layout_block_top_border(), - TableProps { - content: Some(&mut app.data.radarr_data.root_folders), - wrapped_content: None, - table_headers: vec!["Path", "Free Space", "Unmapped Folders"], - constraints: vec![ - Constraint::Percentage(60), - Constraint::Percentage(20), - Constraint::Percentage(20), - ], - help: app - .data - .radarr_data - .main_tabs - .get_active_tab_contextual_help(), - }, - |root_folders| { - let RootFolder { - path, - free_space, - unmapped_folders, - .. - } = root_folders; + let help_footer = app + .data + .radarr_data + .main_tabs + .get_active_tab_contextual_help(); + let root_folders_row_mapping = |root_folders: &RootFolder| { + let RootFolder { + path, + free_space, + unmapped_folders, + .. + } = root_folders; - let space: f64 = convert_to_gb(*free_space); + let space: f64 = convert_to_gb(*free_space); - Row::new(vec![ - Cell::from(path.to_owned()), - Cell::from(format!("{space:.2} GB")), - Cell::from( - unmapped_folders - .as_ref() - .unwrap_or(&Vec::new()) - .len() - .to_string(), - ), - ]) - .primary() - }, - app.is_loading, - true, - ); + Row::new(vec![ + Cell::from(path.to_owned()), + Cell::from(format!("{space:.2} GB")), + Cell::from( + unmapped_folders + .as_ref() + .unwrap_or(&Vec::new()) + .len() + .to_string(), + ), + ]) + .primary() + }; + + let root_folders_table = ManagarrTable::new( + Some(&mut app.data.radarr_data.root_folders), + root_folders_row_mapping, + ) + .block(layout_block_top_border()) + .loading(app.is_loading) + .footer(help_footer) + .headers(["Path", "Free Space", "Unmapped Folders"]) + .constraints([ + Constraint::Ratio(3, 5), + Constraint::Ratio(1, 5), + Constraint::Ratio(1, 5), + ]); + + f.render_widget(root_folders_table, area); } fn draw_add_root_folder_prompt_box(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { diff --git a/src/ui/radarr_ui/system/mod.rs b/src/ui/radarr_ui/system/mod.rs index e550329..1fafb27 100644 --- a/src/ui/radarr_ui/system/mod.rs +++ b/src/ui/radarr_ui/system/mod.rs @@ -11,13 +11,14 @@ use ratatui::{ }; use crate::app::App; -use crate::models::radarr_models::Task; +use crate::models::radarr_models::{QueueEvent, Task}; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::ui::radarr_ui::radarr_ui_utils::{convert_to_minutes_hours_days, style_log_list_item}; use crate::ui::radarr_ui::system::system_details_ui::SystemDetailsUi; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::layout_block_top_border; -use crate::ui::{draw_table, ListProps, TableProps}; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::ListProps; use crate::{ models::Route, ui::{draw_list_box, utils::title_block, DrawUi}, @@ -87,92 +88,83 @@ pub(super) fn draw_system_ui_layout(f: &mut Frame<'_>, app: &mut App<'_>, area: } fn draw_tasks(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - draw_table( - f, - area, - title_block("Tasks"), - TableProps { - content: Some(&mut app.data.radarr_data.tasks), - wrapped_content: None, - table_headers: TASK_TABLE_HEADERS.to_vec(), - constraints: TASK_TABLE_CONSTRAINTS.to_vec(), - help: None, - }, - |task| { - let task_props = extract_task_props(task); + let tasks_row_mapping = |task: &Task| { + let task_props = extract_task_props(task); - Row::new(vec![ - Cell::from(task_props.name), - Cell::from(task_props.interval), - Cell::from(task_props.last_execution), - Cell::from(task_props.last_duration), - Cell::from(task_props.next_execution), - ]) - .primary() - }, - app.is_loading, - false, - ); + Row::new(vec![ + Cell::from(task_props.name), + Cell::from(task_props.interval), + Cell::from(task_props.last_execution), + Cell::from(task_props.last_duration), + Cell::from(task_props.next_execution), + ]) + .primary() + }; + let tasks_table = ManagarrTable::new(Some(&mut app.data.radarr_data.tasks), tasks_row_mapping) + .block(title_block("Tasks")) + .loading(app.is_loading) + .highlight_rows(false) + .headers(TASK_TABLE_HEADERS) + .constraints(TASK_TABLE_CONSTRAINTS); + + f.render_widget(tasks_table, area); } pub(super) fn draw_queued_events(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - draw_table( - f, - area, - title_block("Queued Events"), - TableProps { - content: Some(&mut app.data.radarr_data.queued_events), - wrapped_content: None, - table_headers: vec!["Trigger", "Status", "Name", "Queued", "Started", "Duration"], - constraints: vec![ - Constraint::Percentage(13), - Constraint::Percentage(13), - Constraint::Percentage(30), - Constraint::Percentage(16), - Constraint::Percentage(14), - Constraint::Percentage(14), - ], - help: None, - }, - |event| { - let queued = convert_to_minutes_hours_days(Utc::now().sub(event.queued).num_minutes()); - let queued_string = if queued != "now" { - format!("{queued} ago") - } else { - queued - }; - let started_string = if event.started.is_some() { - let started = - convert_to_minutes_hours_days(Utc::now().sub(event.started.unwrap()).num_minutes()); + let events_row_mapping = |event: &QueueEvent| { + let queued = convert_to_minutes_hours_days(Utc::now().sub(event.queued).num_minutes()); + let queued_string = if queued != "now" { + format!("{queued} ago") + } else { + queued + }; + let started_string = if event.started.is_some() { + let started = + convert_to_minutes_hours_days(Utc::now().sub(event.started.unwrap()).num_minutes()); - if started != "now" { - format!("{started} ago") - } else { - started - } + if started != "now" { + format!("{started} ago") } else { - String::new() - }; + started + } + } else { + String::new() + }; - let duration = if event.duration.is_some() { - &event.duration.as_ref().unwrap()[..8] - } else { - "" - }; + let duration = if event.duration.is_some() { + &event.duration.as_ref().unwrap()[..8] + } else { + "" + }; - Row::new(vec![ - Cell::from(event.trigger.clone()), - Cell::from(event.status.clone()), - Cell::from(event.command_name.clone()), - Cell::from(queued_string), - Cell::from(started_string), - Cell::from(duration.to_owned()), - ]) - .primary() - }, - app.is_loading, - false, - ); + Row::new(vec![ + Cell::from(event.trigger.clone()), + Cell::from(event.status.clone()), + Cell::from(event.command_name.clone()), + Cell::from(queued_string), + Cell::from(started_string), + Cell::from(duration.to_owned()), + ]) + .primary() + }; + let events_table = ManagarrTable::new( + Some(&mut app.data.radarr_data.queued_events), + events_row_mapping, + ) + .block(title_block("Queued Events")) + .loading(app.is_loading) + .highlight_rows(false) + .headers(["Trigger", "Status", "Name", "Queued", "Started", "Duration"]) + .constraints([ + Constraint::Percentage(13), + Constraint::Percentage(13), + Constraint::Percentage(30), + Constraint::Percentage(16), + Constraint::Percentage(14), + Constraint::Percentage(14), + ]); + + f.render_widget(events_table, area); } fn draw_logs(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { diff --git a/src/ui/radarr_ui/system/system_details_ui.rs b/src/ui/radarr_ui/system/system_details_ui.rs index e948bd8..980bb35 100644 --- a/src/ui/radarr_ui/system/system_details_ui.rs +++ b/src/ui/radarr_ui/system/system_details_ui.rs @@ -1,4 +1,4 @@ -use ratatui::layout::Rect; +use ratatui::layout::{Alignment, Rect}; use ratatui::text::{Span, Text}; use ratatui::widgets::{Cell, ListItem, Paragraph, Row}; use ratatui::Frame; @@ -6,6 +6,7 @@ use ratatui::Frame; use crate::app::context_clues::{build_context_clue_string, BARE_POPUP_CONTEXT_CLUES}; use crate::app::radarr::radarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES; use crate::app::App; +use crate::models::radarr_models::Task; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS}; use crate::models::Route; use crate::ui::radarr_ui::radarr_ui_utils::style_log_list_item; @@ -15,10 +16,11 @@ use crate::ui::radarr_ui::system::{ }; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{borderless_block, title_block}; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::{ draw_help_footer_and_get_content_area, draw_large_popup_over, draw_list_box, - draw_medium_popup_over, draw_prompt_box, draw_prompt_popup_over, draw_table, loading, DrawUi, - ListProps, TableProps, + draw_medium_popup_over, draw_prompt_box, draw_prompt_popup_over, DrawUi, ListProps, }; #[cfg(test)] @@ -82,40 +84,35 @@ fn draw_logs_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_tasks_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let tasks_popup_table = |f: &mut Frame<'_>, app: &mut App<'_>, area: Rect| { + let help_footer = Some(build_context_clue_string(&SYSTEM_TASKS_CONTEXT_CLUES)); + // let context_area = draw_help_footer_and_get_content_area( + // f, + // area, + // help_footer, + // ); + let tasks_row_mapping = |task: &Task| { + let task_props = extract_task_props(task); + + Row::new(vec![ + Cell::from(task_props.name), + Cell::from(task_props.interval), + Cell::from(task_props.last_execution), + Cell::from(task_props.last_duration), + Cell::from(task_props.next_execution), + ]) + .primary() + }; + let tasks_table = ManagarrTable::new(Some(&mut app.data.radarr_data.tasks), tasks_row_mapping) + .block(borderless_block()) + .loading(app.is_loading) + .margin(1) + .footer(help_footer) + .footer_alignment(Alignment::Center) + .headers(TASK_TABLE_HEADERS) + .constraints(TASK_TABLE_CONSTRAINTS); + f.render_widget(title_block("Tasks"), area); - - let context_area = draw_help_footer_and_get_content_area( - f, - area, - Some(build_context_clue_string(&SYSTEM_TASKS_CONTEXT_CLUES)), - ); - - draw_table( - f, - context_area, - borderless_block(), - TableProps { - content: Some(&mut app.data.radarr_data.tasks), - wrapped_content: None, - table_headers: TASK_TABLE_HEADERS.to_vec(), - constraints: TASK_TABLE_CONSTRAINTS.to_vec(), - help: None, - }, - |task| { - let task_props = extract_task_props(task); - - Row::new(vec![ - Cell::from(task_props.name), - Cell::from(task_props.interval), - Cell::from(task_props.last_execution), - Cell::from(task_props.last_duration), - Cell::from(task_props.next_execution), - ]) - .primary() - }, - app.is_loading, - true, - ) + f.render_widget(tasks_table, area); }; if matches!( @@ -163,6 +160,6 @@ fn draw_updates_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { f.render_widget(updates_paragraph, content_area); } else { - loading(f, block, content_area, app.is_loading); + f.render_widget(LoadingBlock::new(app.is_loading, block), content_area); } } diff --git a/src/ui/widgets/loading_block.rs b/src/ui/widgets/loading_block.rs new file mode 100644 index 0000000..7dfb5c4 --- /dev/null +++ b/src/ui/widgets/loading_block.rs @@ -0,0 +1,33 @@ +use crate::ui::styles::ManagarrStyle; +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::prelude::Text; +use ratatui::widgets::{Block, Paragraph, Widget}; + +pub struct LoadingBlock<'a> { + is_loading: bool, + block: Block<'a>, +} + +impl<'a> LoadingBlock<'a> { + pub fn new(is_loading: bool, block: Block<'a>) -> Self { + Self { is_loading, block } + } + + fn render_loading_block(&self, area: Rect, buf: &mut Buffer) { + if self.is_loading { + Paragraph::new(Text::from("\n\n Loading ...\n\n")) + .system_function() + .block(self.block.clone()) + .render(area, buf); + } else { + self.block.clone().render(area, buf); + } + } +} + +impl<'a> Widget for LoadingBlock<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + self.render_loading_block(area, buf); + } +} diff --git a/src/ui/widgets/managarr_table.rs b/src/ui/widgets/managarr_table.rs new file mode 100644 index 0000000..e7c8f69 --- /dev/null +++ b/src/ui/widgets/managarr_table.rs @@ -0,0 +1,148 @@ +use crate::models::StatefulTable; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::layout_block_top_border; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::HIGHLIGHT_SYMBOL; +use ratatui::buffer::Buffer; +use ratatui::layout::{Alignment, Constraint, Layout, Rect}; +use ratatui::prelude::{Style, Stylize, Text}; +use ratatui::widgets::{Block, Paragraph, Row, StatefulWidget, Table, Widget}; + +pub struct ManagarrTable<'a, T, F> +where + F: Fn(&T) -> Row<'a>, +{ + content: Option<&'a mut StatefulTable>, + table_headers: Vec>, + constraints: Vec, + row_mapper: F, + footer: Option, + footer_alignment: Alignment, + block: Block<'a>, + margin: u16, + is_loading: bool, + highlight_rows: bool, +} + +impl<'a, T, F> ManagarrTable<'a, T, F> +where + F: Fn(&T) -> Row<'a>, +{ + pub fn new(content: Option<&'a mut StatefulTable>, row_mapper: F) -> Self { + Self { + content, + table_headers: Vec::new(), + constraints: Vec::new(), + row_mapper, + footer: None, + footer_alignment: Alignment::Left, + block: Block::new(), + margin: 0, + is_loading: false, + highlight_rows: true, + } + } + + pub fn headers(mut self, headers: I) -> Self + where + I: IntoIterator, + I::Item: Into>, + { + self.table_headers = headers.into_iter().map(Into::into).collect(); + self + } + + pub fn constraints(mut self, constraints: I) -> Self + where + I: IntoIterator, + I::Item: Into, + { + self.constraints = constraints.into_iter().map(Into::into).collect(); + self + } + + pub fn footer(mut self, footer: Option) -> Self { + self.footer = footer; + self + } + + pub fn footer_alignment(mut self, alignment: Alignment) -> Self { + self.footer_alignment = alignment; + self + } + + pub fn block(mut self, block: Block<'a>) -> Self { + self.block = block; + self + } + + pub fn margin(mut self, margin: u16) -> Self { + self.margin = margin; + self + } + + pub fn loading(mut self, is_loading: bool) -> Self { + self.is_loading = is_loading; + self + } + + pub fn highlight_rows(mut self, hightlight_rows: bool) -> Self { + self.highlight_rows = hightlight_rows; + self + } + + fn render_table(&mut self, area: Rect, buf: &mut Buffer) { + let table_area = if let Some(ref footer) = self.footer { + let [content_area, footer_area] = + Layout::vertical([Constraint::Fill(0), Constraint::Length(2)]) + .margin(self.margin) + .areas(area); + + Paragraph::new(Text::from(format!(" {footer}").help())) + .block(layout_block_top_border()) + .alignment(self.footer_alignment) + .render(footer_area, buf); + + content_area + } else { + area + }; + let loading_block = LoadingBlock::new(self.is_loading, self.block.clone()); + + if let Some(ref mut content) = self.content { + if !content.items.is_empty() { + let rows = content.items.iter().map(&self.row_mapper); + + let headers = Row::new(self.table_headers.clone()) + .default() + .bold() + .bottom_margin(0); + + let mut table = Table::new(rows, &self.constraints) + .header(headers) + .block(self.block.clone()); + + if self.highlight_rows { + table = table + .highlight_style(Style::new().highlight()) + .highlight_symbol(HIGHLIGHT_SYMBOL); + } + + StatefulWidget::render(table, table_area, buf, &mut content.state); + } else { + loading_block.render(table_area, buf); + } + } else { + loading_block.render(table_area, buf); + } + } +} + +impl<'a, T, F> Widget for ManagarrTable<'a, T, F> +where + F: Fn(&T) -> Row<'a>, +{ + fn render(mut self, area: Rect, buf: &mut Buffer) { + self.render_table(area, buf); + } +} diff --git a/src/ui/widgets/mod.rs b/src/ui/widgets/mod.rs index 9fa08be..19659b2 100644 --- a/src/ui/widgets/mod.rs +++ b/src/ui/widgets/mod.rs @@ -1,3 +1,5 @@ pub(super) mod button; -pub(super) mod input_box; pub(super) mod checkbox; +pub(super) mod input_box; +pub(super) mod loading_block; +pub(super) mod managarr_table;