diff --git a/Cargo.lock b/Cargo.lock index 872d982f4..3a73c4b6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,6 +112,12 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "anymap2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" + [[package]] name = "arc-swap" version = "1.7.1" @@ -232,7 +238,7 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.103", ] [[package]] @@ -262,6 +268,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bincode" version = "2.0.1" @@ -327,6 +342,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "boolinator" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9" + [[package]] name = "brotli" version = "8.0.1" @@ -434,7 +455,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.103", ] [[package]] @@ -458,6 +479,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -506,7 +537,7 @@ checksum = "5877d3fbf742507b66bc2a1945106bd30dd8504019d596901ddd012a4dd01740" dependencies = [ "chrono", "once_cell", - "winnow", + "winnow 0.6.26", ] [[package]] @@ -596,7 +627,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.103", ] [[package]] @@ -607,7 +638,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.103", ] [[package]] @@ -651,7 +682,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 2.0.103", ] [[package]] @@ -661,7 +692,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn", + "syn 2.0.103", ] [[package]] @@ -680,6 +711,12 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + [[package]] name = "displaydoc" version = "0.2.5" @@ -688,7 +725,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.103", ] [[package]] @@ -732,7 +769,7 @@ checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.103", ] [[package]] @@ -857,6 +894,36 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "frontend" +version = "0.1.0" +dependencies = [ + "anyhow", + "futures", + "futures-signals", + "gloo-storage 0.3.0", + "gloo-timers 0.3.0", + "gloo-utils 0.2.0", + "implicit-clone 0.6.0", + "js-sys", + "log", + "prost", + "reqwasm", + "serde", + "serde_json", + "shared", + "thiserror 2.0.12", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "wasm-logger", + "web-sys", + "yew", + "yew-hooks", + "yew-i18n", + "yew-router", +] + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -937,7 +1004,23 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.103", +] + +[[package]] +name = "futures-signals" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70abe9c40a0dccd69bf7c59ba58714ebeb6c15a88143a10c6be7130e895f1696" +dependencies = [ + "discard", + "futures-channel", + "futures-core", + "futures-util", + "gensym", + "log", + "pin-project", + "serde", ] [[package]] @@ -986,6 +1069,18 @@ dependencies = [ "version_check", ] +[[package]] +name = "gensym" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913dce4c5f06c2ea40fc178c06f777ac89fc6b1383e90c254fafb1abe4ba3c82" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.103", + "uuid", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -1019,6 +1114,435 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "gloo" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28999cda5ef6916ffd33fb4a7b87e1de633c47c0dc6d97905fee1cdaa142b94d" +dependencies = [ + "gloo-console 0.2.3", + "gloo-dialogs 0.1.1", + "gloo-events 0.1.2", + "gloo-file 0.2.3", + "gloo-history 0.1.5", + "gloo-net 0.3.1", + "gloo-render 0.1.1", + "gloo-storage 0.2.2", + "gloo-timers 0.2.6", + "gloo-utils 0.1.7", + "gloo-worker 0.2.1", +] + +[[package]] +name = "gloo" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd35526c28cc55c1db77aed6296de58677dbab863b118483a27845631d870249" +dependencies = [ + "gloo-console 0.3.0", + "gloo-dialogs 0.2.0", + "gloo-events 0.2.0", + "gloo-file 0.3.0", + "gloo-history 0.2.2", + "gloo-net 0.4.0", + "gloo-render 0.2.0", + "gloo-storage 0.3.0", + "gloo-timers 0.3.0", + "gloo-utils 0.2.0", + "gloo-worker 0.4.0", +] + +[[package]] +name = "gloo" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15282ece24eaf4bd338d73ef580c6714c8615155c4190c781290ee3fa0fd372" +dependencies = [ + "gloo-console 0.3.0", + "gloo-dialogs 0.2.0", + "gloo-events 0.2.0", + "gloo-file 0.3.0", + "gloo-history 0.2.2", + "gloo-net 0.5.0", + "gloo-render 0.2.0", + "gloo-storage 0.3.0", + "gloo-timers 0.3.0", + "gloo-utils 0.2.0", + "gloo-worker 0.5.0", +] + +[[package]] +name = "gloo-console" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f" +dependencies = [ + "gloo-utils 0.1.7", + "js-sys", + "serde", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-console" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a17868f56b4a24f677b17c8cb69958385102fa879418052d60b50bc1727e261" +dependencies = [ + "gloo-utils 0.2.0", + "js-sys", + "serde", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-dialogs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67062364ac72d27f08445a46cab428188e2e224ec9e37efdba48ae8c289002e6" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-dialogs" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4748e10122b01435750ff530095b1217cf6546173459448b83913ebe7815df" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-events" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-events" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c26fb45f7c385ba980f5fa87ac677e363949e065a083722697ef1b2cc91e41" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-file" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7" +dependencies = [ + "gloo-events 0.1.2", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-file" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f" +dependencies = [ + "futures-channel", + "gloo-events 0.2.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-history" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85725d90bf0ed47063b3930ef28e863658a7905989e9929a8708aab74a1d5e7f" +dependencies = [ + "gloo-events 0.1.2", + "gloo-utils 0.1.7", + "serde", + "serde-wasm-bindgen 0.5.0", + "serde_urlencoded", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-history" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6" +dependencies = [ + "getrandom 0.2.16", + "gloo-events 0.2.0", + "gloo-utils 0.2.0", + "serde", + "serde-wasm-bindgen 0.6.5", + "serde_urlencoded", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2899cb1a13be9020b010967adc6b2a8a343b6f1428b90238c9d53ca24decc6db" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.1.7", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a66b4e3c7d9ed8d315fd6b97c8b1f74a7c6ecbbc2320e65ae7ed38b7068cc620" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.1.7", + "http 0.2.12", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ac9e8288ae2c632fa9f8657ac70bfe38a1530f345282d7ba66a1f70b72b7dc4" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.2.0", + "http 0.2.12", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.2.0", + "http 0.2.12", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-render" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd9306aef67cfd4449823aadcd14e3958e0800aa2183955a309112a84ec7764" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-render" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56008b6744713a8e8d98ac3dcb7d06543d5662358c9c805b4ce2167ad4649833" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-storage" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480" +dependencies = [ + "gloo-utils 0.1.7", + "js-sys", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-storage" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" +dependencies = [ + "gloo-utils 0.2.0", + "js-sys", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-worker" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13471584da78061a28306d1359dd0178d8d6fc1c7c80e5e35d27260346e0516a" +dependencies = [ + "anymap2", + "bincode 1.3.3", + "gloo-console 0.2.3", + "gloo-utils 0.1.7", + "js-sys", + "serde", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-worker" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76495d3dd87de51da268fa3a593da118ab43eb7f8809e17eb38d3319b424e400" +dependencies = [ + "bincode 1.3.3", + "futures", + "gloo-utils 0.2.0", + "gloo-worker-macros", + "js-sys", + "pinned", + "serde", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-worker" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "085f262d7604911c8150162529cefab3782e91adb20202e8658f7275d2aefe5d" +dependencies = [ + "bincode 1.3.3", + "futures", + "gloo-utils 0.2.0", + "gloo-worker-macros", + "js-sys", + "pinned", + "serde", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-worker-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956caa58d4857bc9941749d55e4bd3000032d8212762586fa5705632967140e7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.103", +] + [[package]] name = "governor" version = "0.8.1" @@ -1079,6 +1603,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "http" version = "0.2.12" @@ -1359,6 +1889,35 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "implicit-clone" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a9aa791c7b5a71b636b7a68207fdebf171ddfc593d9c8506ec4cbc527b6a84" +dependencies = [ + "implicit-clone-derive", + "indexmap", +] + +[[package]] +name = "implicit-clone" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1689b939ee35e3a075b0834b5672efd43aec8a6e81a1c6002b76a5ca2f211ae0" +dependencies = [ + "implicit-clone-derive", +] + +[[package]] +name = "implicit-clone-derive" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "699c1b6d335e63d0ba5c1e1c7f647371ce989c3bcbe1f7ed2b85fa56e3bd1a21" +dependencies = [ + "quote", + "syn 2.0.103", +] + [[package]] name = "indexmap" version = "2.9.0" @@ -1449,6 +2008,15 @@ dependencies = [ "waker-fn", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1476,7 +2044,7 @@ checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.103", ] [[package]] @@ -1647,6 +2215,16 @@ dependencies = [ "unicase", ] +[[package]] +name = "minicov" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27fe9f1cc3c22e1687f9446c2083c4c5fc7f0bcf1c7a86bdbded14985895b4b" +dependencies = [ + "cc", + "walkdir", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1771,6 +2349,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "num_threads" version = "0.1.7" @@ -1824,7 +2412,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.103", ] [[package]] @@ -1943,7 +2531,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.103", ] [[package]] @@ -1973,7 +2561,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.103", ] [[package]] @@ -1988,6 +2576,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pinned" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a829027bd95e54cfe13e3e258a1ae7b645960553fb82b75ff852c29688ee595b" +dependencies = [ + "futures", + "rustversion", + "thiserror 1.0.69", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -2049,6 +2648,50 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" +dependencies = [ + "proc-macro2", + "syn 2.0.103", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -2058,6 +2701,46 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prokio" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b55e106e5791fa5a13abd13c85d6127312e8e09098059ca2bc9b03ca4cf488" +dependencies = [ + "futures", + "gloo 0.8.1", + "num_cpus", + "once_cell", + "pin-project", + "pinned", + "tokio", + "tokio-stream", + "wasm-bindgen-futures", +] + +[[package]] +name = "prost" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.103", +] + [[package]] name = "quanta" version = "0.12.6" @@ -2248,6 +2931,15 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwasm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b89870d729c501fa7a68c43bf4d938bbb3a8c156d333d90faa0e8b3e3212fb" +dependencies = [ + "gloo-net 0.1.0", +] + [[package]] name = "reqwest" version = "0.12.20" @@ -2310,6 +3002,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "route-recognizer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" + [[package]] name = "rpassword" version = "7.4.0" @@ -2507,6 +3205,28 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_derive" version = "1.0.219" @@ -2515,7 +3235,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.103", ] [[package]] @@ -2583,18 +3303,18 @@ dependencies = [ "base64 0.22.1", "bitflags 2.9.1", "blake3", - "clap", "enum-iterator", + "fastrand 2.3.0", "indexmap", "log", "path-clean", "pest", "pest_derive", - "rand", "regex", "serde", "serde_json", "url", + "zeroize", ] [[package]] @@ -2675,6 +3395,16 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.103" @@ -2703,7 +3433,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.103", ] [[package]] @@ -2766,7 +3496,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.103", ] [[package]] @@ -2777,7 +3507,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.103", ] [[package]] @@ -2863,7 +3593,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.103", ] [[package]] @@ -2911,6 +3641,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.5.40", +] + [[package]] name = "tower" version = "0.5.2" @@ -3005,7 +3752,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.103", ] [[package]] @@ -3040,8 +3787,7 @@ dependencies = [ "arc-swap", "axum", "base64 0.22.1", - "bincode", - "bitflags 2.9.1", + "bincode 2.0.1", "blake3", "bytes", "chrono", @@ -3093,7 +3839,6 @@ dependencies = [ "url", "vergen", "winapi", - "zeroize", ] [[package]] @@ -3155,6 +3900,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -3167,6 +3918,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -3258,6 +4020,8 @@ dependencies = [ "cfg-if", "once_cell", "rustversion", + "serde", + "serde_json", "wasm-bindgen-macro", ] @@ -3271,7 +4035,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 2.0.103", "wasm-bindgen-shared", ] @@ -3306,7 +4070,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.103", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3320,6 +4084,41 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-bindgen-test" +version = "0.3.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c8d5e33ca3b6d9fa3b4676d774c5778031d27a578c2b007f905acf816152c3" +dependencies = [ + "js-sys", + "minicov", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.103", +] + +[[package]] +name = "wasm-logger" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "074649a66bb306c8f2068c9016395fa65d8e08d2affcbf95acf3c24c3ab19718" +dependencies = [ + "log", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -3414,7 +4213,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.103", ] [[package]] @@ -3425,7 +4224,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.103", ] [[package]] @@ -3611,6 +4410,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "winnow" version = "0.6.26" @@ -3635,6 +4443,102 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "yew" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f1a03f255c70c7aa3e9c62e15292f142ede0564123543c1cc0c7a4f31660cac" +dependencies = [ + "console_error_panic_hook", + "futures", + "gloo 0.10.0", + "implicit-clone 0.4.9", + "indexmap", + "js-sys", + "prokio", + "rustversion", + "serde", + "slab", + "thiserror 1.0.69", + "tokio", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "yew-macro", +] + +[[package]] +name = "yew-hooks" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4a023bfdcec05c9a0f6fd84f0caff8869fed4b0b73ba06e9418c8e45ce7ca5" +dependencies = [ + "gloo 0.11.0", + "js-sys", + "log", + "serde", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "yew", +] + +[[package]] +name = "yew-i18n" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b8a8c5a463e6de4440187b5d8d8cc6d6342c6d7a193c6382b351da6325610a1" +dependencies = [ + "serde_json", + "yew", +] + +[[package]] +name = "yew-macro" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fd8ca5166d69e59f796500a2ce432ff751edecbbb308ca59fd3fe4d0343de2" +dependencies = [ + "boolinator", + "once_cell", + "prettyplease", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.103", +] + +[[package]] +name = "yew-router" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca1d5052c96e6762b4d6209a8aded597758d442e6c479995faf0c7b5538e0c6" +dependencies = [ + "gloo 0.10.0", + "js-sys", + "route-recognizer", + "serde", + "serde_urlencoded", + "tracing", + "urlencoding", + "wasm-bindgen", + "web-sys", + "yew", + "yew-router-macro", +] + +[[package]] +name = "yew-router-macro" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42bfd190a07ca8cfde7cd4c52b3ac463803dc07323db8c34daa697e86365978c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.103", +] + [[package]] name = "yoke" version = "0.8.0" @@ -3655,7 +4559,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.103", "synstructure", ] @@ -3676,7 +4580,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.103", ] [[package]] @@ -3696,7 +4600,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.103", "synstructure", ] @@ -3736,7 +4640,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.103", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 16cce1c76..b6474de40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["backend", "shared"] +members = ["backend", "webui", "shared"] resolver = "2" [profile.release] @@ -9,4 +9,3 @@ lto = true # Enable Link Time Optimization codegen-units = 1 # Reduce number of codegen units to increase optimizations. panic = 'abort' # Abort on panic strip = true - diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 77e2f96f7..c82c0a327 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -46,13 +46,11 @@ tokio-util = "0.7" tempfile = "3.20" ruzstd = "0" filetime = "0.2" -zeroize = "1.8" strsim = "0.11" rphonetic = "3" rayon = "1.10" base64 = "0" paste = "1" -bitflags = "2" indexmap = "2" notify = "8" arc-swap = "1" diff --git a/backend/src/api/endpoints/web_index.rs b/backend/src/api/endpoints/web_index.rs index 0c60009bc..ab11ccd7c 100644 --- a/backend/src/api/endpoints/web_index.rs +++ b/backend/src/api/endpoints/web_index.rs @@ -1,6 +1,6 @@ use crate::api::api_utils::serve_file; use crate::api::model::app_state::AppState; -use crate::auth::{AuthBearer, UserCredential, verify_password, create_jwt_admin, create_jwt_user, is_admin, verify_token}; +use crate::auth::{AuthBearer, verify_password, create_jwt_admin, create_jwt_user, is_admin, verify_token}; use axum::response::IntoResponse; use log::error; use serde_json::json; @@ -8,6 +8,7 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::{Arc}; use tower::Service; +use shared::model::UserCredential; use shared::utils::CONSTANTS; fn no_web_auth_token() -> impl axum::response::IntoResponse + Send { diff --git a/backend/src/auth/mod.rs b/backend/src/auth/mod.rs index 1d79a2830..0ae43c4e1 100644 --- a/backend/src/auth/mod.rs +++ b/backend/src/auth/mod.rs @@ -2,7 +2,6 @@ use axum::http::StatusCode; mod authenticator; mod password; -mod user; mod auth_bearer; mod auth_basic; mod access_token; @@ -11,7 +10,6 @@ type Rejection = (StatusCode, &'static str); pub use self::authenticator::*; pub use self::access_token::*; -pub use self::user::*; pub use self::password::*; pub use self::fingerprint::*; pub use self::auth_basic::*; diff --git a/backend/src/model/config/web_auth.rs b/backend/src/model/config/web_auth.rs index f1b8bd150..9ac884b66 100644 --- a/backend/src/model/config/web_auth.rs +++ b/backend/src/model/config/web_auth.rs @@ -1,9 +1,8 @@ use std::fs::File; use std::io::BufRead; use std::path::PathBuf; -use crate::auth::UserCredential; use shared::error::{TuliproxError, TuliproxErrorKind, create_tuliprox_error_result}; -use shared::model::WebAuthConfigDto; +use shared::model::{UserCredential, WebAuthConfigDto}; use crate::model::macros; use crate::utils; diff --git a/backend/src/repository/user_repository.rs b/backend/src/repository/user_repository.rs index d92167c44..2c9c825a7 100644 --- a/backend/src/repository/user_repository.rs +++ b/backend/src/repository/user_repository.rs @@ -469,7 +469,7 @@ mod tests { ], }; - let mut cfg = AppConfig { + let cfg = AppConfig { config: Arc::new(ArcSwapAny::default()), sources: Arc::new(ArcSwapAny::default()), hdhomerun: Arc::new(ArcSwapAny::default()), diff --git a/frontend/src/scss/_common.scss b/frontend/src/scss/_common.scss index 714a213ef..d44830f99 100644 --- a/frontend/src/scss/_common.scss +++ b/frontend/src/scss/_common.scss @@ -1,5 +1,9 @@ $media-size-mobile: 600px; +.hidden { + display: none; +} + @mixin container_common($justify-content: flex-start, $padding: 12px) { display: flex; justify-content: $justify-content; diff --git a/frontend/src/scss/_panel.scss b/frontend/src/scss/_panel.scss new file mode 100644 index 000000000..1c2408b4b --- /dev/null +++ b/frontend/src/scss/_panel.scss @@ -0,0 +1,6 @@ +@use "./common"; + +.panel { + @include common.container_vert_no_padding(); + overflow: hidden; +} diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 55319a021..169a67c11 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -6,7 +6,6 @@ edition = "2021" [dependencies] bitflags = "2" serde = { version = "1", features = ["derive"] } -rand = "0" regex = "1" enum-iterator = "2" log = "0" @@ -14,8 +13,9 @@ path-clean = "1" url = "2" indexmap = "2" pest = "2" -pest_derive = "2.8" +pest_derive = "2" serde_json = "1" base64 = "0" blake3 = "1" -clap = { version = "4.5.40", features = ["derive"] } \ No newline at end of file +fastrand = "2" +zeroize = "1" diff --git a/shared/src/model/auth/mod.rs b/shared/src/model/auth/mod.rs new file mode 100644 index 000000000..3f6b44c20 --- /dev/null +++ b/shared/src/model/auth/mod.rs @@ -0,0 +1,3 @@ +mod user; + +pub use self::user::*; diff --git a/shared/src/model/auth/user.rs b/shared/src/model/auth/user.rs new file mode 100644 index 000000000..5a427b067 --- /dev/null +++ b/shared/src/model/auth/user.rs @@ -0,0 +1,18 @@ +use zeroize::Zeroize; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct UserCredential { + pub username: String, + pub password: String, +} + +impl UserCredential { + pub fn zeroize(&mut self) { + self.password.zeroize(); + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Eq, PartialEq, Default)] +pub struct TokenResponse { + pub token: String, +} diff --git a/shared/src/model/mod.rs b/shared/src/model/mod.rs index 455c49642..4eb945613 100644 --- a/shared/src/model/mod.rs +++ b/shared/src/model/mod.rs @@ -8,6 +8,7 @@ mod config; mod strm_export_style; mod mapping; pub mod xtream_const; +mod auth; pub use self::cluster_flags::*; pub use self::playlist::*; @@ -18,3 +19,4 @@ pub use self::processing_order::*; pub use self::config::*; pub use self::strm_export_style::*; pub use self::mapping::*; +pub use self::auth::*; diff --git a/shared/src/utils/string_utils.rs b/shared/src/utils/string_utils.rs index 075d97183..572d290db 100644 --- a/shared/src/utils/string_utils.rs +++ b/shared/src/utils/string_utils.rs @@ -1,5 +1,3 @@ -use rand::Rng; - pub trait Capitalize { fn capitalize(&self) -> String; } @@ -30,11 +28,10 @@ pub fn get_trimmed_string(value: &Option) -> Option { pub fn generate_random_string(length: usize) -> String { let charset = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - let mut rng = rand::rng(); - + let range = ..charset.len(); let random_string: String = (0..length) .map(|_| { - let idx = rng.random_range(0..charset.len()); + let idx = fastrand::usize(range); charset[idx] as char }) .collect(); diff --git a/webui/Cargo.toml b/webui/Cargo.toml new file mode 100644 index 000000000..ec41b1ddc --- /dev/null +++ b/webui/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "frontend" +version = "0.1.0" +edition = "2021" + +[dependencies] +shared = { version = "0.1.0", path = "../shared" } +yew = "0.21" +yew-router = "0.18" +yew-hooks = "0.3" +yew-i18n = "0.1" +serde_json = "1" +serde = {version = "1.0", features = ["derive", "rc"] } +wasm-bindgen = {version = "0.2", features = ["serde-serialize"]} +wasm-bindgen-futures = "0.4" +anyhow = "1" +thiserror = "2" +reqwasm = {version = "0.5", features = ["json"]} +log = "0.4.27" +wasm-logger = "0.2" +gloo-storage = "0" +gloo-timers = { version = "0", features = ["futures"] } +implicit-clone = "0" +futures-signals = "0.3" +futures = "0.3" +prost = "0" + +[dependencies.web-sys] +version = "0.3" +features = [ + "Document", + "Element", + "Node", + "Window", + "HtmlCollection", + "HtmlInputElement", +] + +[dev-dependencies] +js-sys = "0.3" +wasm-bindgen-futures = "0.4" +wasm-bindgen-test = "0.3" +gloo-utils = "0.2" diff --git a/webui/Trunk.toml b/webui/Trunk.toml new file mode 100644 index 000000000..8f6af1539 --- /dev/null +++ b/webui/Trunk.toml @@ -0,0 +1,11 @@ +[build] +dist = "dist" + +[serve] +port = 9899 + +[[proxy]] +backend = "http://localhost:9901/api/v1/" + +[[proxy]] +backend = "http://localhost:9901/auth/" diff --git a/webui/index.html b/webui/index.html new file mode 100644 index 000000000..11cc80036 --- /dev/null +++ b/webui/index.html @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + Tuliprox + + + + \ No newline at end of file diff --git a/webui/public/assets/favicon.ico b/webui/public/assets/favicon.ico new file mode 100644 index 000000000..8d861c4c8 Binary files /dev/null and b/webui/public/assets/favicon.ico differ diff --git a/webui/public/assets/fonts.css b/webui/public/assets/fonts.css new file mode 100644 index 000000000..fd07a74a0 --- /dev/null +++ b/webui/public/assets/fonts.css @@ -0,0 +1,59 @@ +/* rubik-arabic-400-normal */ +@font-face { + font-family: 'Rubik'; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url('fonts/rubik-arabic-400-normal.woff2') format('woff2'), url('fonts/rubik-arabic-400-normal.woff') format('woff'); + unicode-range: U+0600-06FF,U+0750-077F,U+0870-088E,U+0890-0891,U+0897-08E1,U+08E3-08FF,U+200C-200E,U+2010-2011,U+204F,U+2E41,U+FB50-FDFF,U+FE70-FE74,U+FE76-FEFC,U+102E0-102FB,U+10E60-10E7E,U+10EC2-10EC4,U+10EFC-10EFF,U+1EE00-1EE03,U+1EE05-1EE1F,U+1EE21-1EE22,U+1EE24,U+1EE27,U+1EE29-1EE32,U+1EE34-1EE37,U+1EE39,U+1EE3B,U+1EE42,U+1EE47,U+1EE49,U+1EE4B,U+1EE4D-1EE4F,U+1EE51-1EE52,U+1EE54,U+1EE57,U+1EE59,U+1EE5B,U+1EE5D,U+1EE5F,U+1EE61-1EE62,U+1EE64,U+1EE67-1EE6A,U+1EE6C-1EE72,U+1EE74-1EE77,U+1EE79-1EE7C,U+1EE7E,U+1EE80-1EE89,U+1EE8B-1EE9B,U+1EEA1-1EEA3,U+1EEA5-1EEA9,U+1EEAB-1EEBB,U+1EEF0-1EEF1; +} + +/* rubik-cyrillic-ext-400-normal */ +@font-face { + font-family: 'Rubik'; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url('fonts/rubik-cyrillic-ext-400-normal.woff2') format('woff2'), url('fonts/rubik-cyrillic-ext-400-normal.woff') format('woff'); + unicode-range: U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F; +} + +/* rubik-cyrillic-400-normal */ +@font-face { + font-family: 'Rubik'; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url('fonts/rubik-cyrillic-400-normal.woff2') format('woff2'), url('fonts/rubik-cyrillic-400-normal.woff') format('woff'); + unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116; +} + +/* rubik-hebrew-400-normal */ +@font-face { + font-family: 'Rubik'; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url('fonts/rubik-hebrew-400-normal.woff2') format('woff2'), url('fonts/rubik-hebrew-400-normal.woff') format('woff'); + unicode-range: U+0307-0308,U+0590-05FF,U+200C-2010,U+20AA,U+25CC,U+FB1D-FB4F; +} + +/* rubik-latin-ext-400-normal */ +@font-face { + font-family: 'Rubik'; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url('fonts/rubik-latin-ext-400-normal.woff2') format('woff2'), url('fonts/rubik-latin-ext-400-normal.woff') format('woff'); + unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF; +} + +/* rubik-latin-400-normal */ +@font-face { + font-family: 'Rubik'; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url('fonts/rubik-latin-400-normal.woff2') format('woff2'), url('fonts/rubik-latin-400-normal.woff') format('woff'); + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; +} \ No newline at end of file diff --git a/webui/public/assets/fonts/rubik-arabic-400-italic.woff b/webui/public/assets/fonts/rubik-arabic-400-italic.woff new file mode 100644 index 000000000..fca1c231b Binary files /dev/null and b/webui/public/assets/fonts/rubik-arabic-400-italic.woff differ diff --git a/webui/public/assets/fonts/rubik-arabic-400-italic.woff2 b/webui/public/assets/fonts/rubik-arabic-400-italic.woff2 new file mode 100644 index 000000000..b42c25f4f Binary files /dev/null and b/webui/public/assets/fonts/rubik-arabic-400-italic.woff2 differ diff --git a/webui/public/assets/fonts/rubik-arabic-400-normal.woff b/webui/public/assets/fonts/rubik-arabic-400-normal.woff new file mode 100644 index 000000000..3b48a3e5e Binary files /dev/null and b/webui/public/assets/fonts/rubik-arabic-400-normal.woff differ diff --git a/webui/public/assets/fonts/rubik-arabic-400-normal.woff2 b/webui/public/assets/fonts/rubik-arabic-400-normal.woff2 new file mode 100644 index 000000000..673f79567 Binary files /dev/null and b/webui/public/assets/fonts/rubik-arabic-400-normal.woff2 differ diff --git a/webui/public/assets/fonts/rubik-cyrillic-400-italic.woff b/webui/public/assets/fonts/rubik-cyrillic-400-italic.woff new file mode 100644 index 000000000..b11f2dc6d Binary files /dev/null and b/webui/public/assets/fonts/rubik-cyrillic-400-italic.woff differ diff --git a/webui/public/assets/fonts/rubik-cyrillic-400-italic.woff2 b/webui/public/assets/fonts/rubik-cyrillic-400-italic.woff2 new file mode 100644 index 000000000..d6d59de0a Binary files /dev/null and b/webui/public/assets/fonts/rubik-cyrillic-400-italic.woff2 differ diff --git a/webui/public/assets/fonts/rubik-cyrillic-400-normal.woff b/webui/public/assets/fonts/rubik-cyrillic-400-normal.woff new file mode 100644 index 000000000..0506d064a Binary files /dev/null and b/webui/public/assets/fonts/rubik-cyrillic-400-normal.woff differ diff --git a/webui/public/assets/fonts/rubik-cyrillic-400-normal.woff2 b/webui/public/assets/fonts/rubik-cyrillic-400-normal.woff2 new file mode 100644 index 000000000..8a166d4df Binary files /dev/null and b/webui/public/assets/fonts/rubik-cyrillic-400-normal.woff2 differ diff --git a/webui/public/assets/fonts/rubik-cyrillic-ext-400-italic.woff b/webui/public/assets/fonts/rubik-cyrillic-ext-400-italic.woff new file mode 100644 index 000000000..42098f8a8 Binary files /dev/null and b/webui/public/assets/fonts/rubik-cyrillic-ext-400-italic.woff differ diff --git a/webui/public/assets/fonts/rubik-cyrillic-ext-400-italic.woff2 b/webui/public/assets/fonts/rubik-cyrillic-ext-400-italic.woff2 new file mode 100644 index 000000000..109a7ca52 Binary files /dev/null and b/webui/public/assets/fonts/rubik-cyrillic-ext-400-italic.woff2 differ diff --git a/webui/public/assets/fonts/rubik-cyrillic-ext-400-normal.woff b/webui/public/assets/fonts/rubik-cyrillic-ext-400-normal.woff new file mode 100644 index 000000000..d47ad615c Binary files /dev/null and b/webui/public/assets/fonts/rubik-cyrillic-ext-400-normal.woff differ diff --git a/webui/public/assets/fonts/rubik-cyrillic-ext-400-normal.woff2 b/webui/public/assets/fonts/rubik-cyrillic-ext-400-normal.woff2 new file mode 100644 index 000000000..cca969e32 Binary files /dev/null and b/webui/public/assets/fonts/rubik-cyrillic-ext-400-normal.woff2 differ diff --git a/webui/public/assets/fonts/rubik-hebrew-400-italic.woff b/webui/public/assets/fonts/rubik-hebrew-400-italic.woff new file mode 100644 index 000000000..cb34551ba Binary files /dev/null and b/webui/public/assets/fonts/rubik-hebrew-400-italic.woff differ diff --git a/webui/public/assets/fonts/rubik-hebrew-400-italic.woff2 b/webui/public/assets/fonts/rubik-hebrew-400-italic.woff2 new file mode 100644 index 000000000..798e805e4 Binary files /dev/null and b/webui/public/assets/fonts/rubik-hebrew-400-italic.woff2 differ diff --git a/webui/public/assets/fonts/rubik-hebrew-400-normal.woff b/webui/public/assets/fonts/rubik-hebrew-400-normal.woff new file mode 100644 index 000000000..df2536016 Binary files /dev/null and b/webui/public/assets/fonts/rubik-hebrew-400-normal.woff differ diff --git a/webui/public/assets/fonts/rubik-hebrew-400-normal.woff2 b/webui/public/assets/fonts/rubik-hebrew-400-normal.woff2 new file mode 100644 index 000000000..658d71941 Binary files /dev/null and b/webui/public/assets/fonts/rubik-hebrew-400-normal.woff2 differ diff --git a/webui/public/assets/fonts/rubik-latin-400-italic.woff b/webui/public/assets/fonts/rubik-latin-400-italic.woff new file mode 100644 index 000000000..458d6cd9f Binary files /dev/null and b/webui/public/assets/fonts/rubik-latin-400-italic.woff differ diff --git a/webui/public/assets/fonts/rubik-latin-400-italic.woff2 b/webui/public/assets/fonts/rubik-latin-400-italic.woff2 new file mode 100644 index 000000000..af50274a4 Binary files /dev/null and b/webui/public/assets/fonts/rubik-latin-400-italic.woff2 differ diff --git a/webui/public/assets/fonts/rubik-latin-400-normal.woff b/webui/public/assets/fonts/rubik-latin-400-normal.woff new file mode 100644 index 000000000..25a12df47 Binary files /dev/null and b/webui/public/assets/fonts/rubik-latin-400-normal.woff differ diff --git a/webui/public/assets/fonts/rubik-latin-400-normal.woff2 b/webui/public/assets/fonts/rubik-latin-400-normal.woff2 new file mode 100644 index 000000000..ac6ff6829 Binary files /dev/null and b/webui/public/assets/fonts/rubik-latin-400-normal.woff2 differ diff --git a/webui/public/assets/fonts/rubik-latin-ext-400-italic.woff b/webui/public/assets/fonts/rubik-latin-ext-400-italic.woff new file mode 100644 index 000000000..c7530972a Binary files /dev/null and b/webui/public/assets/fonts/rubik-latin-ext-400-italic.woff differ diff --git a/webui/public/assets/fonts/rubik-latin-ext-400-italic.woff2 b/webui/public/assets/fonts/rubik-latin-ext-400-italic.woff2 new file mode 100644 index 000000000..0d4e484ff Binary files /dev/null and b/webui/public/assets/fonts/rubik-latin-ext-400-italic.woff2 differ diff --git a/webui/public/assets/fonts/rubik-latin-ext-400-normal.woff b/webui/public/assets/fonts/rubik-latin-ext-400-normal.woff new file mode 100644 index 000000000..a97a610d3 Binary files /dev/null and b/webui/public/assets/fonts/rubik-latin-ext-400-normal.woff differ diff --git a/webui/public/assets/fonts/rubik-latin-ext-400-normal.woff2 b/webui/public/assets/fonts/rubik-latin-ext-400-normal.woff2 new file mode 100644 index 000000000..f8a84d783 Binary files /dev/null and b/webui/public/assets/fonts/rubik-latin-ext-400-normal.woff2 differ diff --git a/webui/public/assets/i18n/en.json b/webui/public/assets/i18n/en.json new file mode 100644 index 000000000..d65ee2f76 --- /dev/null +++ b/webui/public/assets/i18n/en.json @@ -0,0 +1,206 @@ +{ + "LABEL": { + "LIVE": "Live", + "VOD": "VOD", + "SERIES": "Series", + "SAVE": "Save", + "SUBMIT": "Submit", + "OK": "Ok", + "CANCEL": "Cancel", + "CLOSE": "Close", + "SHOW_SELECTED": "Show selected", + "SELECT_ALL": "Select All", + "DESELECT_ALL": "Deselect all", + "SEARCH": "Search", + "CLEAR": "Clear", + "REGEXP": "Regexp", + "LOGOUT": "Logout", + "CONFIGURATION": "Configuration", + "HOST": "Host", + "PORT": "Port", + "WEB_ROOT": "Web-Root", + "THREADS": "Threads", + "WORKING_DIR": "Working dir", + "BACKUP_DIR": "Backup dir", + "UPDATE_ON_BOOT": "Update on boot", + "WEB_UI": "Web UI", + "USER_UI": "User UI", + "WEB_UI_PATH": "Path prefix", + "SCHEDULES": "Schedules", + "SANITIZE_SENSITIVE_INFO": "Sanitize sensitive info", + "ACTIVE_USER": "Active User", + "RESOURCE_REWRITE_DISABLE": "Resource Rewrite disable", + "DISABLE_REFERER_HEADER": "Referer header disable", + "RECONNECT": "Reconnect", + "THROTTLE": "Throttle", + "GRACE_PERIOD": "Grace period ms", + "GRACE_PERIOD_TIMEOUT": "Grace period timeout sec", + "BUFFER_ENABLED": "Buffer Enabled", + "BUFFER_SIZE": "Buffer Size", + "ENABLED": "Enabled", + "SIZE": "Size", + "CACHE_DIR": "Cache dir", + "NOTIFY_ON": "Notify on", + "BOT_TOKEN": "Bot Token", + "CHAT_IDS": "Chat Ids", + "URL": "Url", + "USER": "User", + "USERS": "Users", + "TOKEN": "Token", + "WEB_SEARCH_URL": "Web Search Url", + "EXTENSIONS": "Extensions", + "HEADERS": "Headers", + "DIRECTORY": "Directory", + "ORGANIZE_INTO_DIRECTORIES": "Organize Into Directorie", + "EPISODE_PATTERN": "Episode Pattern", + "API": "Api", + "MAIN": "Main", + "REVERSE_PROXY": "ReverseProxy", + "MESSAGING": "Messaging", + "VIDEO": "Video", + "EXAMPLES": "Examples", + "UPDATE": "Update", + "START": "Start", + "CACHE": "Cache", + "STREAM": "Stream", + "LOG": "Log", + "LOG_LEVEL": "Log level", + "CONFIG": "Config", + "TELEGRAM": "Telegram", + "REST": "Rest", + "PUSHOVER": "Pushover", + "PROXY": "Proxy", + "REFRESH": "Refresh", + "STATUS": "Status", + "MAX_CON": "MaxCon", + "EXP_DATE": "ExpDate", + "USERNAME": "Username", + "PASSWORD": "Password", + "SERVER": "Server", + "TIMEZONE": "Timezone", + "PATH": "Path", + "MESSAGE": "Message", + "PROTOCOL": "Protocol", + "NAME": "Name", + "API_PROXY": "Api-Proxy", + "INFINITE": "Infinite", + "ONE_YEAR": "One year", + "SELF_HOSTED": "Self Hosted", + "PROVIDER": "Provider", + "CUSTOM": "Custom", + "XTREAM_CODES": "xtream codes", + "XC": "xc", + "M3U": "m3u", + "PLAYLIST_BROWSER": "Playlist Browser", + "RATE_LIMIT": "Rate Limit", + "PERIOD_MILLIS": "Period Millis", + "BURST_SIZE": "Burst Size", + "WEB_AUTH": "Auth", + "ISSUER": "Issuer", + "SECRET": "Secret", + "USERFILE": "Userfile", + "UI_ENABLED": "UI", + "PLAYER_SERVER": "Player Server", + "VERSION": "Version", + "SERVER_TIME": "Server time", + "BUILD_TIME": "Build time", + "MEMORY": "Memory", + "ACTIVE_USERS": "Active users", + "ACTIVE_USER_CONNECTIONS": "Active user conn...", + "ACTIVE_PROVIDER_CONNECTIONS": "Active provider conn...", + "IP_CHECK": "IP Check", + "URL_IPV4": "Url IPv4", + "URL_IPV6": "Url IPv6", + "PATTERN_IPV4": "Pattern IPv4", + "PATTERN_IPV6": "Pattern IPv6", + "IPv4": "IPv4", + "IPv6": "IPv6", + "IP": "IP", + "TARGET": "Target", + "TARGETS": "Targets", + "NOTES": "Notes" + }, + "TITLE": { + "USER_BOUQUET_EDITOR": "User group editor" + }, + "MESSAGES": { + "NO_CONTENT": "No content", + "INVALID_REGEXP": "Invalid regexp", + "INVALID_FILETYPE": "Invalid filetype", + "TARGET_NOT_EXISTS": "Target does not exist", + "DOWNLOAD": { + "SUCCESS": "Successfully downloaded", + "FAIL": "Failed to download!", + "PLAYLIST": { + "SUCCESS": "Successfully downloaded playlist", + "FAIL": "Failed to download playlist!" + }, + "SERVER_CONFIG": { + "FAIL": "Failed to download server config!" + } + }, + "SAVE": { + "MAIN_CONFIG": { + "SUCCESS": "Main config saved!", + "FAIL": "Failed to save main config!" + }, + "BOUQUET": { + "SUCCESS": "Successfully saved bouquet", + "FAIL": "Failed to save bouquet!" + }, + "USER": { + "SUCCESS": "Successfully saved user", + "FAIL": "Failed to save user!", + "USERNAME_REQUIRED": "Username required", + "DUPLICATE_USERNAME": "Duplicate username!", + "DUPLICATE_TOKEN": "Duplicate token!", + "TARGET_NOT_FOUND": "Target not found!", + "MAX_CONNECTIONS_INVALID": "MaxConnections invalid" + } + }, + "PLAYLIST_UPDATE": { + "SUCCESS": "Successfully started playlist update!", + "FAIL": "Playlist update failed!" + }, + "SOURCE_SELECTOR": { + "MISSING_SELECTION": "No entry selected" + } + }, + "INFO": { + "RESTART_TO_APPLY_CHANGES": "You need to restart to apply changes.", + "SCHEDULE_EXAMPLE": "Schedule example", + "THREADS": "Threads: keep it 0 if you have no problems. Increase it if you have multiple providers. Accessing you provider parallel can cause a ban.", + "WEB_ROOT": "Web-Root: Contains the web-ui files. They are normally placed in the same directory under ./web" + }, + "HINT": { + "CONFIG": { + "API_PROXY": { + "PATH": "URL path prefix" + }, + "USER": { + "TARGET_FORCE_REDIRECT": "Force Redirect is set for target" + }, + "LOG": { + "ACTIVE_USER": "Activates active user info log" + }, + "PROXY": { + "RESOURCE_REWRITE_DISABLE": "If activated, the URL rewrite of resources in reverse proxy mode is disabled", + "DISABLE_REFERER_HEADER": "If activated, the referer header is not sent to the provider", + "THROTTLE": "Supported units are KB/s,MB/s,KiB/s,MiB/s,kbps,mbps,Mibps" + }, + "WEB_UI": { + "PATH": "Configuration of a path prefix, allowing it to be accessed behind a reverse proxy under a custom subpath. like https://yourdomain.com/tuliprox" + } + } + }, + "SETUP":{ + "LABEL": { + "NEXT": "Next", + "BACK": "Back", + "FINISH_SETUP": "Finish Setup" + }, + "MSG": { + "WELCOME": "Welcome to tuliprox setup. This wizard will guide you through the initial setup process." + } + } +} \ No newline at end of file diff --git a/webui/public/assets/icons.json b/webui/public/assets/icons.json new file mode 100644 index 000000000..25d1b4e10 --- /dev/null +++ b/webui/public/assets/icons.json @@ -0,0 +1,280 @@ +[ + { + "keys": [ + "Logo" + ], + "path": "M 14.09194,7.6882684e-4 C 13.973764,9.5800672e-5 13.856738,1.8569152e-4 13.740338,0.00128559 10.134203,0.03533121 7.3722672,0.9073449 5.3120566,2.2058124 2.6199728,3.9025243 1.1166249,6.3549771 0.44196532,8.8193928 4.4918101,7.9934421 7.5738185,8.392102 10.393515,9.4901562 V 3.0538267 c 0,-1.0502871 0.717633,-1.3329492 1.494308,-0.8443969 l 5.939123,3.7362233 C 17.883676,5.8672459 17.940027,5.7886156 17.993602,5.7100075 18.773785,4.4409406 19.517515,2.4795189 20.024457,0.67721647 17.837484,0.22159145 15.864573,0.01086418 14.09194,7.6882684e-4 Z M 12.283121,4.6015429 c -0.352571,-0.00358 -0.610222,0.2693437 -0.610222,0.817525 V 18.544943 c 0,0.716278 0.702783,1.00662 1.134576,0.772566 L 23.366199,12.42229 c 0.321466,-0.209944 0.293977,-0.733308 0,-1.002527 L 12.807475,4.7777605 C 12.623215,4.6618552 12.443381,4.6031688 12.283121,4.6015429 Z M 4.6820187,10.631177 c -1.3670233,0.0065 -2.8875164,0.207303 -4.66075611,0.63924 -0.22262,3.841376 1.29470801,7.532859 4.53424051,9.96688 3.3072703,2.484918 8.4898149,3.647573 15.5090939,1.986968 -0.453335,-1.879096 -1.379185,-3.757389 -2.654289,-5.468942 l -5.522485,3.606515 c -0.568764,0.308297 -1.494308,-0.07453 -1.494308,-1.018031 V 12.036783 C 8.6043863,11.2147 6.6723813,10.706689 4.6820187,10.631177 Z" + }, + { + "keys": [ + "ContentCopy" + ], + "path": "M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" + }, + { + "keys": [ + "DeleteSweep" + ], + "path": "M15 16h4v2h-4zm0-8h7v2h-7zm0 4h6v2h-6zM3 18c0 1.1.9 2 2 2h6c1.1 0 2-.9 2-2V8H3v10zM14 5h-3l-1-1H6L5 5H2v2h12z" + }, + { + "keys": [ + "Delete" + ], + "path": "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" + }, + { + "keys": [ + "Search" + ], + "path": "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" + }, + { + "keys": [ + "Clear" + ], + "path": "M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" + }, + { + "keys": [ + "ClearSearch" + ], + "path": "M 10.82534,3 C 7.3479487,3 4.5022347,5.716944 4.2480387,9.1748729 H 6.3014596 C 6.5556707,6.859298 8.4664087,5.0582909 10.82534,5.0582909 c 2.531786,0 4.575514,2.068585 4.575514,4.6311538 0,2.5625733 -2.043728,4.6311543 -4.575514,4.6311543 -0.172856,0 -0.335527,-0.03153 -0.508391,-0.05226 v 2.080402 c 0.172857,0.02062 0.335527,0.03015 0.508391,0.03015 1.637017,0 3.14233,-0.60752 4.30146,-1.616081 l 0.274054,0.289447 v 0.812061 L 20.484758,21 22,19.466331 16.926025,14.320604 h -0.802303 l -0.28597,-0.277387 c 0.996446,-1.173226 1.596663,-2.696845 1.596663,-4.3537672 0,-3.6946315 -2.958832,-6.6894447 -6.609075,-6.6894447 z M 2.7228671,11.048238 2,11.777886 4.5121637,14.320599 2,16.863312 l 0.7228671,0.729648 2.5101775,-2.540703 2.5121641,2.540703 0.720882,-0.729648 -2.510178,-2.542713 2.510178,-2.542713 -0.720882,-0.729648 -2.5121641,2.542713 z" + }, + { + "keys": [ + "ExpandMore" + ], + "path": "M16.59 8.59 12 13.17 7.41 8.59 6 10l6 6 6-6z" + }, + { + "keys": [ + "ExpandLess" + ], + "path": "m12 8-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z" + }, + { + "keys": [ + "ChevronRight" + ], + "path": "M10 6 8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" + }, + { + "keys": [ + "LinkRounded" + ], + "path": "M17 7h-3c-.55 0-1 .45-1 1s.45 1 1 1h3c1.65 0 3 1.35 3 3s-1.35 3-3 3h-3c-.55 0-1 .45-1 1s.45 1 1 1h3c2.76 0 5-2.24 5-5s-2.24-5-5-5zm-9 5c0 .55.45 1 1 1h6c.55 0 1-.45 1-1s-.45-1-1-1H9c-.55 0-1 .45-1 1zm2 3H7c-1.65 0-3-1.35-3-3s1.35-3 3-3h3c.55 0 1-.45 1-1s-.45-1-1-1H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h3c.55 0 1-.45 1-1s-.45-1-1-1z" + }, + { + "keys": [ + "PlayArrow" + ], + "path": "M8 5v14l11-7z" + }, + { + "keys": [ + "ArrowLeft" + ], + "path": "m14 7-5 5 5 5V7z" + }, + { + "keys": [ + "ArrowRight" + ], + "path": "m10 17 5-5-5-5v10z" + }, + { + "keys": [ + "ArrowDown" + ], + "path": "m7 10 5 5 5-5z" + }, + { + "keys": [ + "ArrowUp" + ], + "path": "m7 14 5-5 5 5z" + }, + { + "keys": [ + "CloudDownload" + ], + "path": "M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM17 13l-5 5-5-5h3V9h4v4h3z" + }, + { + "keys": [ + "Download" + ], + "path": "M5 20h14v-2H5v2zM19 9h-4V3H9v6H5l7 7 7-7z" + }, + { + "keys": [ + "Config" + ], + "path": "M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z" + }, + { + "keys": [ + "PersonAdd" + ], + "path": "M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9 4c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" + }, + { + "keys": [ + "PersonRemove" + ], + "path": "M14 8c0-2.21-1.79-4-4-4S6 5.79 6 8s1.79 4 4 4 4-1.79 4-4zm3 2v2h6v-2h-6zM2 18v2h16v-2c0-2.66-5.33-4-8-4s-8 1.34-8 4z" + }, + { + "keys": [ + "WebSearch" + ], + "path": "M19.3 16.9c.4-.7.7-1.5.7-2.4 0-2.5-2-4.5-4.5-4.5S11 12 11 14.5s2 4.5 4.5 4.5c.9 0 1.7-.3 2.4-.7l3.2 3.2 1.4-1.4-3.2-3.2zm-3.8.1c-1.4 0-2.5-1.1-2.5-2.5s1.1-2.5 2.5-2.5 2.5 1.1 2.5 2.5-1.1 2.5-2.5 2.5zM12 20v2C6.48 22 2 17.52 2 12S6.48 2 12 2c4.84 0 8.87 3.44 9.8 8h-2.07c-.64-2.46-2.4-4.47-4.73-5.41V5c0 1.1-.9 2-2 2h-2v2c0 .55-.45 1-1 1H8v2h2v3H9l-4.79-4.79C4.08 10.79 4 11.38 4 12c0 4.41 3.59 8 8 8z" + }, + { + "keys": [ + "CheckMark" + ], + "path": "M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" + }, + { + "keys": [ + "Checked" + ], + "path": "M 4.2222224,2 C 3,2 2,3.000021 2,4.2222362 V 19.777764 C 2,20.999979 3,22 4.2222224,22 H 19.777778 C 20.999989,22 22,20.999979 22,19.777764 V 4.2222362 C 22,3.000021 20.999989,2 19.777778,2 Z m 0,2.2222362 H 19.777778 V 19.777764 H 4.2222224 Z M 17.288622,7.1063517 10.441833,13.953134 6.7113668,10.233533 5.2465223,11.698352 10.441833,16.89369 18.753467,8.5820472 Z" + }, + { + "keys": [ + "Hourglass" + ], + "path": "M6 2v6h.01L6 8.01 10 12l-4 4 .01.01H6V22h12v-5.99h-.01L18 16l-4-4 4-3.99-.01-.01H18V2H6zm10 14.5V20H8v-3.5l4-4 4 4zm-4-5-4-4V4h8v3.5l-4 4z" + }, + { + "keys": [ + "Error" + ], + "path": "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" + }, + { + "keys": [ + "Regexp" + ], + "path": "M 14.219989,2 V 6.6779027 L 10.218308,4.3991767 8.9526346,6.5626756 13.00229,8.8690562 8.8806751,11.216304 l 1.2656739,2.16227 4.07364,-2.3199 v 4.62413 h 2.532593 v -4.677903 l 3.98081,2.26705 L 22,11.108144 18.066228,8.8681345 21.985982,6.6358063 20.720308,4.4735364 16.752582,6.7332118 V 2 Z M 4.8198059,16.438384 C 3.2775726,16.438384 1.9999995,17.698564 2,19.219808 1.9999995,20.741043 3.2775726,22 4.8198059,22 c 1.542234,0 2.8185603,-1.258957 2.8185597,-2.780192 6e-7,-1.521244 -1.2763257,-2.781424 -2.8185597,-2.781424 z" + }, + { + "keys": [ + "User" + ], + "path": "M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" + }, + { + "keys": [ + "Refresh" + ], + "path": "M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" + }, + { + "keys": [ + "ApiServer" + ], + "path": "m20.2 5.9.8-.8C19.6 3.7 17.8 3 16 3s-3.6.7-5 2.1l.8.8C13 4.8 14.5 4.2 16 4.2s3 .6 4.2 1.7zm-.9.8c-.9-.9-2.1-1.4-3.3-1.4s-2.4.5-3.3 1.4l.8.8c.7-.7 1.6-1 2.5-1 .9 0 1.8.3 2.5 1l.8-.8zM19 13h-2V9h-2v4H5c-1.1 0-2 .9-2 2v4c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-4c0-1.1-.9-2-2-2zM8 18H6v-2h2v2zm3.5 0h-2v-2h2v2zm3.5 0h-2v-2h2v2z" + }, + { + "keys": ["Warn"], + "path": "M 11.999765,2 2,22 h 20 z m 0,4.1999619 6.845654,13.6948891 H 5.1545815 Z m -0.909027,4.2211761 v 5.263416 h 1.818524 v -5.263416 z m 0,6.31599 v 2.105149 h 1.818524 v -2.105149 z" + }, + { + "keys": ["Gallery"], + "path": "M 11.999742,2 C 6.5002947,2 1.9999999,6.5002944 2,11.999742 2,17.499188 6.5002947,22 11.999742,22 17.499187,22 22,17.499188 22,11.999742 22,6.5002944 17.499187,1.9999999 11.999742,2 Z M 7.4796784,5.5010206 H 9.2940081 C 10.454244,5.5013381 11.3944,6.4424302 11.393587,7.6026664 v 1.8143297 c -3.18e-4,1.1594289 -0.94015,2.0992499 -2.0995789,2.0995789 H 7.4796784 C 6.3194422,11.517395 5.3783618,10.577232 5.3780327,9.4169961 V 7.6026664 C 5.3772125,6.4416227 6.3186347,5.5002073 7.4796784,5.5010206 Z m 7.2263136,0 h 1.816396 c 1.160237,3.175e-4 2.100392,0.9414098 2.099579,2.1016458 v 1.8143297 c -3.17e-4,1.1594289 -0.94015,2.0992499 -2.099579,2.0995789 h -1.816396 c -1.159429,-3.18e-4 -2.09925,-0.94015 -2.099579,-2.0995789 V 7.6026664 c -8.2e-4,-1.1602362 0.939342,-2.1013166 2.099579,-2.1016458 z M 7.4796784,12.483425 h 1.8143297 c 1.1594289,3.18e-4 2.0992499,0.94015 2.0995789,2.099579 v 1.816397 c -3.18e-4,1.159428 -0.94015,2.099249 -2.0995789,2.099578 H 7.4796784 C 6.3194422,18.4998 5.3783618,17.559637 5.3780327,16.399401 v -1.816397 c 3.175e-4,-1.160236 0.9414095,-2.100391 2.1016457,-2.099579 z m 7.2263136,0 h 1.816396 c 1.159429,3.18e-4 2.09925,0.94015 2.099579,2.099579 v 1.816397 c -3.17e-4,1.159428 -0.94015,2.099249 -2.099579,2.099578 h -1.816396 c -1.159429,-3.17e-4 -2.09925,-0.94015 -2.099579,-2.099578 v -1.816397 c 3.17e-4,-1.159429 0.94015,-2.09925 2.099579,-2.099579 z" + }, + { + "keys": ["Editor"], + "path": "M8 5v14l11-7z" + }, + { + "keys": ["ScheduleAdd"], + "path": "m 15.427906,2 0.0026,4.3103469 -4.402514,-0.00424 0.0016,2.4026963 4.402514,0.00475 0.0026,4.2565168 2.402815,0.0026 -0.0021,-4.2565172 4.164457,0.00517 -0.0016,-2.403213 -4.164974,-0.00465 -0.0026,-4.3103469 z M 9.2342365,7.5143019 c -3.9841557,5e-7 -7.2343587,3.2535111 -7.2343586,7.2428491 0,3.989335 3.2502029,7.242849 7.2343586,7.242849 3.9841545,0 7.2322915,-3.253514 7.2322915,-7.242849 0,-0.06325 -0.0014,-0.126255 -0.0031,-0.18912 H 14.68171 c 0.0022,0.06278 0.0041,0.125793 0.0041,0.18912 0,3.025716 -2.429816,5.460169 -5.4516058,5.460169 -3.0217902,0 -5.4531561,-2.434453 -5.4531561,-5.460169 0,-3.025718 2.4313659,-5.4586186 5.4531561,-5.4586186 0.1480969,0 0.2937091,0.00764 0.4387121,0.019119 C 9.6657863,8.738416 9.6404593,8.1019918 9.6781163,7.5292876 9.531058,7.5202214 9.3835267,7.5143032 9.2342349,7.5143032 Z M 8.078806,10.651303 v 5.534575 h 5.462975 V 14.71943 H 9.5432468 v -4.068127 z" + }, + { + "keys": ["ScheduleRemove"], + "path": "m 14.051424,1.9998779 -1.742213,1.697054 3.122965,3.0458089 -3.191235,3.1104043 1.741155,1.6975709 3.191765,-3.1098876 3.084332,3.0080846 L 21.999878,9.7518594 18.916075,6.7432575 21.935312,3.8023519 20.194157,2.1042643 17.17439,5.0462036 Z M 9.4085202,7.51427 c -4.0802372,6e-7 -7.4086424,3.253631 -7.4086423,7.242969 0,3.989334 3.3284051,7.242968 7.4086423,7.242968 4.0802358,0 7.4070548,-3.253634 7.4070548,-7.242968 0,-0.06325 -0.0014,-0.126271 -0.0032,-0.189136 h -1.824773 c 0.0023,0.06278 0.0042,0.125808 0.0042,0.189136 0,3.025716 -2.488677,5.460131 -5.5833398,5.460131 -3.0946634,0 -5.5843983,-2.434415 -5.5843983,-5.460131 0,-3.025718 2.4897349,-5.458582 5.5843983,-5.458582 0.1516683,0 0.3008134,0.00764 0.4493133,0.01912 C 9.8504755,8.7385417 9.8245025,8.10196 9.8630655,7.5292556 9.712521,7.5201898 9.5614122,7.51427 9.4085202,7.51427 Z m -1.1833506,3.137276 v 5.534546 H 13.820153 V 14.719515 H 9.7249975 v -4.067969 z" + }, + { + "keys": ["Logout"], + "path": "m17 7-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4z" + }, + { + "keys": ["SelectAll"], + "path": "M 2,9.2105263 H 13.578947 V 11.315789 H 2 Z M 2,5 H 13.578947 V 7.105263 H 2 Z m 0,8.421054 h 7.368421 v 2.105263 H 2 Z m 18.515789,-2.178949 -4.473684,4.46316 -2.231579,-2.23158 -1.48421,1.484211 3.715789,3.726316 L 22,12.726317 Z" + }, + { + "keys": ["DeselectAll"], + "path": "M 2,5 V 7.1538458 H 13.612371 V 5 Z M 2,9.3076922 V 11.461539 H 13.612371 V 9.3076922 Z m 14.082473,1.7899618 -1.265978,1.392431 2.327832,2.559791 -2.327832,2.557693 L 16.082473,19 18.408247,16.442307 20.734022,19 22,17.607569 19.674225,15.049876 22,12.490085 20.734022,11.097654 18.408247,13.655347 Z M 2,13.615385 v 2.153847 h 7.3896896 v -2.153847 z" + }, + { + "keys": ["Visibility"], + "path": "M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5M12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5m0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3" + }, + { + "keys": ["Edit"], + "path": "M3 17.25V21h3.75L17.81 9.94l-3.75-3.75zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.996.996 0 0 0-1.41 0l-1.83 1.83 3.75 3.75z" + }, + { + "keys": ["Calendar"], + "path": "M19 4h-1V2h-2v2H8V2H6v2H5c-1.11 0-1.99.9-1.99 2L3 20c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2m0 16H5V10h14zM9 14H7v-2h2zm4 0h-2v-2h2zm4 0h-2v-2h2zm-8 4H7v-2h2zm4 0h-2v-2h2zm4 0h-2v-2h2z" + }, + { + "keys": [ + "Unlimited" + ], + "path": "M18.6 6.62c-1.44 0-2.8.56-3.77 1.53L12 10.66 10.48 12h.01L7.8 14.39c-.64.64-1.49.99-2.4.99-1.87 0-3.39-1.51-3.39-3.38S3.53 8.62 5.4 8.62c.91 0 1.76.35 2.44 1.03l1.13 1 1.51-1.34L9.22 8.2C8.2 7.18 6.84 6.62 5.4 6.62 2.42 6.62 0 9.04 0 12s2.42 5.38 5.4 5.38c1.44 0 2.8-.56 3.77-1.53l2.83-2.5.01.01L13.52 12h-.01l2.69-2.39c.64-.64 1.49-.99 2.4-.99 1.87 0 3.39 1.51 3.39 3.38s-1.52 3.38-3.39 3.38c-.9 0-1.76-.35-2.44-1.03l-1.14-1.01-1.51 1.34 1.27 1.12c1.02 1.01 2.37 1.57 3.82 1.57 2.98 0 5.4-2.41 5.4-5.38s-2.42-5.37-5.4-5.37" + }, + { + "keys": [ + "Today" + ], + "path": "M16.53 11.06 15.47 10l-4.88 4.88-2.12-2.12-1.06 1.06L10.59 17zM19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2m0 16H5V8h14z" + }, + { + "keys": [ + "Help" + ], + "path": "M11 18h2v-2h-2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8m0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4" + }, + { + "keys": [ + "Live" + ], + "path": "M21 6h-7.59l3.29-3.29L16 2l-4 4-4-4-.71.71L10.59 6H3c-1.1 0-2 .89-2 2v12c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V8c0-1.11-.9-2-2-2m0 14H3V8h18zM9 10v8l7-4z" + }, + { + "keys": [ + "VOD" + ], + "path": "m 19.815223,2.0000416 a 0.58850091,0.49877638 0 0 0 -0.152559,0.017775 L 2.4317328,6.0126382 A 0.58850091,0.49877638 0 0 0 2.0210941,6.6257352 L 2.9224652,9.4191448 A 1.173617,0.82462271 0 0 0 2.09356,10.207336 1.173617,0.82462271 0 0 0 2.9599692,11.002528 V 22 h 1.176613 V 21.97845 H 20.824021 V 22 h 1.175977 V 10.712682 A 0.58850091,0.49877638 0 0 0 21.411374,10.212723 H 4.4410652 a 1.173617,0.82462271 0 0 0 0,-0.0054 1.173617,0.82462271 0 0 0 -0.069287,-0.2785336 l 16.0358828,-3.7173633 0.002,0.00646 1.134023,-0.2629113 -1.157542,-3.5869872 A 0.58850091,0.49877638 0 0 0 19.815315,2.0000062 Z M 19.407126,3.1109398 20.112078,5.2966424 4.0158064,9.0280154 3.3108555,6.8423128 Z m -2.292202,0.8544558 a 0.88775572,0.75240594 0 0 0 -0.19642,0.025866 0.88775572,0.75240594 0 0 0 -0.622314,0.9244907 0.88775572,0.75240594 0 0 0 1.090798,0.5268975 0.88775572,0.75240594 0 0 0 0.621679,-0.9239538 0.88775572,0.75240594 0 0 0 -0.893743,-0.5532946 z m -3.63663,0.8426032 A 0.88775572,0.75240594 0 0 0 13.281873,4.834396 0.88775572,0.75240594 0 0 0 12.660194,5.7588867 0.88775572,0.75240594 0 0 0 13.750358,6.2857842 0.88775572,0.75240594 0 0 0 14.372672,5.3618304 0.88775572,0.75240594 0 0 0 13.478294,4.8079988 Z M 9.7698324,5.6678394 a 0.88775572,0.75240594 0 0 0 -0.19642,0.02586 0.88775572,0.75240594 0 0 0 -0.621679,0.924494 A 0.88775572,0.75240594 0 0 0 10.041896,7.1450878 0.88775572,0.75240594 0 0 0 10.664211,6.2211341 0.88775572,0.75240594 0 0 0 9.7698324,5.6678394 Z M 5.8490599,6.5767097 A 0.88775572,0.75240594 0 0 0 5.6526401,6.6031068 0.88775572,0.75240594 0 0 0 5.0303252,7.5275985 0.88775572,0.75240594 0 0 0 6.1211239,8.054495 0.88775572,0.75240594 0 0 0 6.7434384,7.1305423 0.88775572,0.75240594 0 0 0 5.8490599,6.5767097 Z M 4.1365822,11.210483 H 20.824021 V 13.47646 H 4.1365822 Z m 2.3335209,0.292541 a 0.88775572,0.75240594 0 0 0 -0.8880218,0.752092 0.88775572,0.75240594 0 0 0 0.8880218,0.752633 0.88775572,0.75240594 0 0 0 0.888022,-0.752633 0.88775572,0.75240594 0 0 0 -0.888022,-0.752092 z m 4.0650679,0 a 0.88775572,0.75240594 0 0 0 -0.887386,0.752092 0.88775572,0.75240594 0 0 0 0.887386,0.752633 0.88775572,0.75240594 0 0 0 0.888022,-0.752633 0.88775572,0.75240594 0 0 0 -0.888022,-0.752092 z m 3.844493,0 a 0.88775572,0.75240594 0 0 0 -0.887387,0.752092 0.88775572,0.75240594 0 0 0 0.887387,0.752633 0.88775572,0.75240594 0 0 0 0.888022,-0.752633 0.88775572,0.75240594 0 0 0 -0.888022,-0.752092 z m 3.770121,0 a 0.88775572,0.75240594 0 0 0 -0.888022,0.752092 0.88775572,0.75240594 0 0 0 0.888022,0.752633 0.88775572,0.75240594 0 0 0 0.887386,-0.752633 0.88775572,0.75240594 0 0 0 -0.887386,-0.752092 z M 4.1365822,14.424658 H 20.824021 V 21.03025 H 4.1365822 Z m 5.7317781,0.759096 v 0.700912 4.107958 l 5.2219757,-2.554745 z m 1.0138837,1.402902 2.083069,0.898093 -2.083069,1.018233 z" + }, + { + "keys": [ + "Series" + ], + "path": "M 5.6142482,2 A 0.52258163,0.5109162 0 0 0 5.0926858,2.5115754 V 6.8905936 H 2.521564 A 0.52258163,0.5109162 0 0 0 2.0000014,7.4027207 V 21.488425 A 0.52258163,0.5109162 0 0 0 2.521564,22 h 15.863059 a 0.52258163,0.5109162 0 0 0 0.522692,-0.511575 v -4.37957 h 2.569993 A 0.52258163,0.5109162 0 0 0 22,16.597831 V 2.5115754 A 0.52258163,0.5109162 0 0 0 21.477308,2 Z M 6.137504,3.0214951 H 20.955746 V 5.3426232 H 6.137504 Z M 8.2096417,3.3211558 A 0.78831624,0.7707189 0 0 0 7.4210893,4.0921057 0.78831624,0.7707189 0 0 0 8.2096417,4.8625039 0.78831624,0.7707189 0 0 0 8.9981944,4.0921057 0.78831624,0.7707189 0 0 0 8.2096417,3.3211558 Z m 3.6097313,0 a 0.78831624,0.7707189 0 0 0 -0.787989,0.7709499 0.78831624,0.7707189 0 0 0 0.787989,0.7703982 0.78831624,0.7707189 0 0 0 0.788552,-0.7703982 0.78831624,0.7707189 0 0 0 -0.788552,-0.7709499 z m 3.413862,0 a 0.78831624,0.7707189 0 0 0 -0.787988,0.7709499 0.78831624,0.7707189 0 0 0 0.787988,0.7703982 0.78831624,0.7707189 0 0 0 0.788553,-0.7703982 0.78831624,0.7707189 0 0 0 -0.788553,-0.7709499 z m 3.347821,0 A 0.78831624,0.7707189 0 0 0 17.792504,4.0921057 0.78831624,0.7707189 0 0 0 18.581056,4.8625039 0.78831624,0.7707189 0 0 0 19.369044,4.0921057 0.78831624,0.7707189 0 0 0 18.581056,3.3211558 Z M 6.137504,6.3138988 H 20.955746 V 13.080269 H 18.907315 V 7.4027207 A 0.52258163,0.5109162 0 0 0 18.384623,6.8905936 H 6.137504 Z M 3.0448198,7.9126405 H 17.863061 V 10.233768 H 3.0448198 Z M 5.1169577,8.211749 A 0.78831624,0.7707189 0 0 0 4.3284051,8.9826991 0.78831624,0.7707189 0 0 0 5.1169577,9.7536488 0.78831624,0.7707189 0 0 0 5.9055102,8.9826991 0.78831624,0.7707189 0 0 0 5.1169577,8.211749 Z m 3.6097308,0 A 0.78831624,0.7707189 0 0 0 7.9387002,8.9826991 0.78831624,0.7707189 0 0 0 8.7266885,9.7536488 0.78831624,0.7707189 0 0 0 9.5152413,8.9826991 0.78831624,0.7707189 0 0 0 8.7266885,8.211749 Z m 3.4138625,0 A 0.78831624,0.7707189 0 0 0 11.352563,8.9826991 0.78831624,0.7707189 0 0 0 12.140551,9.7536488 0.78831624,0.7707189 0 0 0 12.929104,8.9826991 0.78831624,0.7707189 0 0 0 12.140551,8.211749 Z m 3.34782,0 A 0.78831624,0.7707189 0 0 0 14.69982,8.9826991 0.78831624,0.7707189 0 0 0 15.488371,9.7536488 0.78831624,0.7707189 0 0 0 16.276361,8.9826991 0.78831624,0.7707189 0 0 0 15.488371,8.211749 Z M 3.0448198,11.205044 H 17.863061 v 6.765818 H 3.0448198 Z m 5.0897487,0.77702 v 0.718524 4.207942 l 4.6370505,-2.616926 z m 0.9003159,1.4376 1.8497406,0.919952 -1.8497406,1.043017 z m 9.8724306,0.632432 h 2.048431 v 2.035816 h -2.048431 v -0.327806 a 0.78831624,0.7707189 0 0 0 0.461729,-0.701967 0.78831624,0.7707189 0 0 0 -0.461729,-0.701416 z M 3.0448198,18.94269 H 17.863061 v 2.035815 H 3.0448198 Z m 2.0721379,0.235644 a 0.78831624,0.7707189 0 0 0 -0.7885526,0.770398 0.78831624,0.7707189 0 0 0 0.7885526,0.77095 0.78831624,0.7707189 0 0 0 0.7885525,-0.77095 0.78831624,0.7707189 0 0 0 -0.7885525,-0.770398 z m 3.6097308,0 a 0.78831624,0.7707189 0 0 0 -0.7879883,0.770398 0.78831624,0.7707189 0 0 0 0.7879883,0.77095 0.78831624,0.7707189 0 0 0 0.7885528,-0.77095 0.78831624,0.7707189 0 0 0 -0.7885528,-0.770398 z m 3.4138625,0 a 0.78831624,0.7707189 0 0 0 -0.787988,0.770398 0.78831624,0.7707189 0 0 0 0.787988,0.77095 0.78831624,0.7707189 0 0 0 0.788553,-0.77095 0.78831624,0.7707189 0 0 0 -0.788553,-0.770398 z m 3.34782,0 a 0.78831624,0.7707189 0 0 0 -0.788551,0.770398 0.78831624,0.7707189 0 0 0 0.788551,0.77095 0.78831624,0.7707189 0 0 0 0.78799,-0.77095 0.78831624,0.7707189 0 0 0 -0.78799,-0.770398 z" + }, + { + "keys": [ + "Back" + ], + "path": "M10 9V5l-7 7 7 7v-4.1c5 0 8.5 1.6 11 5.1-1-5-4-10-11-11" + }, + { + "keys": [ + "TreeView" + ], + "path": "M17 12h2L12 2 5.05 12H7l-3.9 6h6.92v4h3.96v-4H21z" + }, + { + "keys": [ + "Status" + ], + "path": "m 4,3.627 c -1.1,0 -2,0.8999999 -2,2 V 8.6269999 H 4 V 5.627 h 16 v 2.9999999 h 2 V 5.627 c 0,-1.1000001 -0.9,-2 -2,-2 z m 9.994135,3.041016 c -0.362495,0 -0.724532,0.1697839 -0.894533,0.5097642 L 10,13.386764 8.8906064,11.177778 A 0.98799998,0.98799998 0 0 0 8,10.627 H 2 v 2 h 5.3789124 l 1.7206895,3.451167 C 9.2796039,16.418167 9.6199993,16.627 10,16.627 c 0.380001,0 0.720606,-0.208833 0.890606,-0.548833 L 14,9.8672329 15.109393,12.078171 C 15.279391,12.418171 15.619999,12.627 16,12.627 h 6 v -2 H 16.619142 L 14.890606,7.1777802 C 14.720608,6.8377802 14.356611,6.668016 13.994135,6.668016 Z M 2,14.627 v 3 c 0,1.1 0.9,2 2,2 h 16 c 1.1,0 2,-0.9 2,-2 v -3 h -2 v 3 H 4 v -3 z" + } +] \ No newline at end of file diff --git a/webui/public/assets/missing-logo.svg b/webui/public/assets/missing-logo.svg new file mode 100644 index 000000000..1006b8346 --- /dev/null +++ b/webui/public/assets/missing-logo.svg @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/webui/public/assets/placeholder.png b/webui/public/assets/placeholder.png new file mode 100644 index 000000000..d157b8f4e Binary files /dev/null and b/webui/public/assets/placeholder.png differ diff --git a/webui/public/assets/schedule.svg b/webui/public/assets/schedule.svg new file mode 100644 index 000000000..87bff106d --- /dev/null +++ b/webui/public/assets/schedule.svg @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 0 8,20 * * * * + secminhour + + yearday of weekmonthday of month + + + + + + + + + diff --git a/webui/public/assets/tuliprox-logo.svg b/webui/public/assets/tuliprox-logo.svg new file mode 100644 index 000000000..7f5c089eb --- /dev/null +++ b/webui/public/assets/tuliprox-logo.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + diff --git a/webui/public/config.json b/webui/public/config.json new file mode 100644 index 000000000..7bceb8e9e --- /dev/null +++ b/webui/public/config.json @@ -0,0 +1,11 @@ +{ + "tabTitle": "tuliprox", + "appTitle": "tuliprox", + "appLogo": "/assets/tuliprox-logo.svg", + "api": { + "apiUrl": "/api/v1/", + "authUrl": "/auth/" + }, + "wsUrl" : "/ws", + "protocolVersion": 1 +} \ No newline at end of file diff --git a/webui/scss/_app.scss b/webui/scss/_app.scss new file mode 100644 index 000000000..aed3a17e3 --- /dev/null +++ b/webui/scss/_app.scss @@ -0,0 +1,77 @@ +@use 'common'; + +.app { + display: flex; + flex-flow: column; + justify-content: center; + overflow: hidden; + flex: 1 1 auto; + box-sizing: border-box; + max-width: 100%; + color: var(--text-color); + background-color: inherit; + + &-header { + display: flex; + flex-flow: row; + font-size: 2rem; + font-weight: bold; + color: var(--app-header-color); + align-content: center; + padding: 10px; + @media (max-width: common.$media-size-mobile) { + padding: 10px 0; + } + + &__logo { + display: flex; + justify-content: center; + align-items: center; + + svg, img { + width: 2em; + aspect-ratio: 1 / 1; + transform: translate(-10px, 0); + } + } + + &__caption { + display: flex; + flex-flow: row nowrap; + flex: 1 1 auto; + justify-content: center; + align-items: center; + } + + &__toolbar { + display: flex; + align-items: center; + gap: 8px; + button { + height: 60%; + @include common.iconButton(); + } + padding-right: 4px; + } + } + + &-main { + display: flex; + flex-flow: column; + justify-content: center; + flex: 1 1 auto; + overflow: hidden; + box-sizing: border-box; + max-width: 100%; + background-color: inherit; + } + &-content { + display: flex; + flex-flow: column; + flex: 1 1 auto; + box-sizing: border-box; + overflow: hidden; + max-width: 100%; + background-color: inherit; + } +} \ No newline at end of file diff --git a/webui/scss/_common.scss b/webui/scss/_common.scss new file mode 100644 index 000000000..ad666dda8 --- /dev/null +++ b/webui/scss/_common.scss @@ -0,0 +1,136 @@ +$media-size-mobile: 600px; + +@mixin container_common($justify-content: flex-start, $padding: 12px) { + display: flex; + justify-content: $justify-content; + gap: 8px; + box-sizing: border-box; + @media (max-width: $media-size-mobile) { + padding: $padding; + } + @media (min-width: $media-size-mobile) { + padding: 2px; + } +} + +@mixin container_vert() { + flex-flow: column; + @include container_common(); +} + +@mixin container_horz() { + flex-flow: row; + @include container_common(); +} + +@mixin container_horz_nowrap($justify-content: flex-start, $padding: 12px) { + flex-flow: row nowrap; + @media (max-width: $media-size-mobile) { + flex-flow: column; + } + @include container_common($justify-content, $padding); +} + +@mixin container_horz_no_padding() { + flex-flow: row; + @include container_common($padding: 0); +} + +@mixin container_vert_no_padding($justify-content: flex-start) { + flex-flow: column; + @include container_common($justify-content, $padding: 0); +} + +@mixin activeOptionButton { + box-sizing: border-box !important; + background-color: var(--option-active-background-color) !important; + color: var(--option-active-color) !important; + font-weight: bold; + svg { + color: var(--option-active-color) !important; + } +} + +@mixin iconButton { + display: flex; + justify-content: center; + align-items: center; + color: var(--icon-button-color); + background-color: var(--icon-button-background-color); + border: none; + border-radius: var(--icon-button-border-radius); + aspect-ratio: 1 / 1; + cursor: pointer; + box-sizing: border-box; + max-height: 100%; + width: min-content; + + svg { + color: var(--text-color); + path { + fill: var(--text-color); + } + } + &:hover { + svg { + color: var(--text-button-hover-color); + path { + fill: var(--text-button-hover-color); + } + } + } + + &:focus, + &:hover { + color: var(--icon-button-hover-color); + background-color: var(--icon-button-hover-background-color); + outline: none; + } +} + +@mixin textButton { + border: 1px solid var(--text-button-border-color); + font-size: 1.2rem; + border-radius: var(--text-button-border-radius); + padding: 8px 16px; + background-color: var(--text-button-background-color); + color: var(--text-button-color); + &:focus, + &:hover { + background-color: var(--text-button-hover-background-color); + color: var(--text-button-hover-color); + border: 1px solid transparent; + outline: none; + } +} + +@mixin list { + ul { + list-style-type: none; /* Remove bullets */ + padding: 0; /* Remove padding */ + margin: 0; /* Remove margins */ + li { + padding: 8px; + border-radius: var(--border-radius); + } + } +} + +@mixin list-hover { + ul { + list-style-type: none; /* Remove bullets */ + padding: 0; /* Remove padding */ + margin: 0; /* Remove margins */ + li { + padding: 8px; + border-radius: var(--border-radius); + user-select: none; + cursor: pointer; + + &:hover { + color: var(--popup-hover-color); + background-color: var(--popup-hover-background-color); + } + } + } +} \ No newline at end of file diff --git a/webui/scss/_default.scss b/webui/scss/_default.scss new file mode 100644 index 000000000..5568e8142 --- /dev/null +++ b/webui/scss/_default.scss @@ -0,0 +1,124 @@ +@use 'common'; + +::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); + border-radius: 10px; + background-color: var(--scrollbar-track-color); +} + +::-webkit-scrollbar { + width: 12px; + background-color: transparent; +} + +::-webkit-scrollbar-thumb { + border-radius: 10px; + //-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3); + background-color: var(--scrollbar-handle-color); +} + + +button { + cursor: pointer; + //&:active { + // transform: scale(0.90); /* Scaling button to 0.98 to its original size */ + //} +} + +.hidden { + display: none !important; +} + +.card { + display: flex; + flex-flow: column; + flex: 0 1 auto; + align-content: flex-start; + overflow: hidden; + padding: 20px; + border: 1px solid var(--card-border-color); + background-color: var(--card-background-color); + color: var(--card-color); + filter: drop-shadow(3px 3px 4px var(--popup-shadow-color)); + min-width: 200px; + + border-radius:var(--border-radius); + + @media (min-width: common.$media-size-mobile) { + padding: 2px; + } +} + +.vert-group { + display: flex; + flex-flow: column; + gap: 20px; + overflow: hidden; + padding: 0 4px 4px 0; +} + +select, +input { + background-color: transparent; + color: var(--text-color); + &:focus { + background-color: var(--background-color) !important; + } +} + +select { + padding: 2px 4px; +} + +.tool-button { + display: flex; + align-items: center; + justify-content: center; + height: 1.1rem; + width: 1.1rem; + border-radius: var(--border-radius); + background-color: var(--tool-button-background-color); + color: var(--tool-button-color); + cursor: pointer; + + svg { + width: 1rem; + height: 1rem; + } + + &:hover { + color: var(--tool-button-hover-color); + background-color: var(--tool-button-hover-background-color); + } + + &:active { + transform: scale(0.9); + } +} + +:not(input[type="text"]) { + caret-color: transparent; +} +input[type="text"] { + caret-color: auto; +} + +select { + height: 2rem; + border-radius: var(--border-radius); + font-size: 1.2rem; + padding: 0 8px; + cursor: pointer; + &:hover { + color: var(--text-button-hover-color); + background-color: var(--text-button-hover-background-color); + } +} + +option { + cursor: pointer; +} + +dialog { + padding: 0 !important; +} \ No newline at end of file diff --git a/webui/scss/_login.scss b/webui/scss/_login.scss new file mode 100644 index 000000000..65980561f --- /dev/null +++ b/webui/scss/_login.scss @@ -0,0 +1,77 @@ +@use 'common'; + +.login-view { + display: flex; + flex-flow: column; + gap: 8px; + margin: 10% auto auto auto; + border: 1px solid var(--border-color); + border-top-left-radius: 6px; + border-top-right-radius: 6px; + overflow: hidden; + position: relative; + z-index: 1; + + &__header { + display: flex; + flex-flow: row; + align-items: center; + background-color: var(--card-background-color); + padding: 0 8px; + &-title { + color: var(--card-color); + padding: 8px; + font-size: 1.5rem; + } + &-logo { + overflow: hidden; + img, svg { + height: 1.5rem; + width: 1.5rem; + } + } + + } + + &__logo { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + svg { + height: 100%; + width: 100%; + color: color-mix(in srgb, var(--background-color) 80%, black 10%); + } + } + + &__form { + display: flex; + flex-flow: column; + gap: 20px; + padding: 8px; + width: 300px; + + input { + height: 2rem; + border: none; + border-radius: 6px; + font-size: 1.4rem; + background-color: var(--card-background-color); + padding: 4px 8px; + } + input[type="password"] { + caret-color: var(--card-color); + } + + button { + @include common.textButton(); + } + + .error-text { + color: var(--attention-color); + font-size: 1rem; + } + } +} \ No newline at end of file diff --git a/webui/scss/_preferences.scss b/webui/scss/_preferences.scss new file mode 100644 index 000000000..f3b44bc64 --- /dev/null +++ b/webui/scss/_preferences.scss @@ -0,0 +1,172 @@ +@use 'common'; + +@mixin prefsRoot { + @include common.container_vert_no_padding(flex-start); + align-content: flex-start; + overflow: hidden; +} + +@mixin prefsInput { + color: var(--text-color); + background-color: transparent; + padding: 0.2rem 0.5rem; + flex: 1 1 auto; + border: 1px solid var(--border-color); + + &:focus, + &:hover { + outline: none; + } +} + +@mixin prefsToolbar() { + &__toolbar { + display: flex; + flex-flow: row wrap; + flex: 0 0 auto; + justify-content: flex-end; + align-items: center; + gap: 12px; + background-color: var(--toolbar-bg-color); + padding: 8px 16px; + + label { + flex: 1 1 auto; + font-size: 2rem; + font-weight: bold; + color: var(--title-label-color); + } + + button { + @include common.textButton(); + } + } +} + +@mixin prefsForm() { + &__content { + display: flex; + flex-flow: column; + + &-help { + display: flex; + flex-flow: column; + gap:12px; + border-top: 1px solid var(--border-light-color); + padding-top: 12px; + color: var(--label-color); + + pre { + margin: 0; + } + } + + } + +} + + +.preferences { + @include common.container_horz_no_padding(); + overflow: hidden; + flex: 1 1 auto; + + &__content { + @include common.container_horz_nowrap(flex-start, 0); + flex: 1 1 auto; + overflow: hidden; + gap: 0; + } + + &__panels { + @include common.container_vert_no_padding(flex-start); + overflow: hidden; + flex: 1 1 auto; + + .panel { + @include common.container_vert(); + flex: 1 1 auto; + overflow: hidden; + .card { + @include common.container_vert(); + flex: 1 1 auto; + overflow: hidden; + } + } + } + + &__sidebar { + display: flex; + flex-flow: column; + box-sizing: border-box; + @media (max-width: common.$media-size-mobile) { + flex-flow: row; + } + //max-width: 48px; + //min-width: 48px; + //width: 48px; + @media (max-width: common.$media-size-mobile) { + max-width: 32px; + min-width: 32px; + } + gap: 12px; + padding: 8px 4px; + + button { + @include common.iconButton(); + border: 1px solid transparent; + } + + button.selected { + color: var(--icon-button-hover-color); + background-color: var(--icon-button-hover-background-color); + border: 1px solid var(--icon-button-hover-border-color); + } + + &-menu-action { + display: flex; + flex-flow: row nowrap; + gap: 8px; + align-items: center; + cursor: pointer; + color: var(--menu-button-color); + background-color: var(--menu-button-background-color); + border: 1px solid transparent; + border-radius: var(--border-radius); + padding: 8px 16px; + user-select: none; + white-space: nowrap; + + svg { + width: 2rem; + } + + &_update { svg { path { fill: var(--menu-icon-color-update); } } } + &_user { svg { path { fill: var(--menu-icon-color-user); } } } + &_apiserver { svg { path { fill: var(--menu-icon-color-api_server); } } } + &_mainconfig { svg { path { fill: var(--menu-icon-color-main_config); } } } + &_status { svg { path { fill: var(--menu-icon-color-status); } } } + + + * { + pointer-events: none; + } + + @media (max-width: common.$media-size-mobile) { + label { + display: none; + } + } + + &:hover { + border: 1px solid var(--icon-button-hover-border-color); + } + } + + &-menu-action.selected { + color: var(--menu-button-hover-color); + background-color: var(--menu-button-hover-background-color); + } + } + +} \ No newline at end of file diff --git a/webui/scss/_theme.scss b/webui/scss/_theme.scss new file mode 100644 index 000000000..fcc544b10 --- /dev/null +++ b/webui/scss/_theme.scss @@ -0,0 +1,110 @@ +:root { + --media-size-mobile: 600px; + --text-color: #ffffff; + --hint-color: #888888; + --label-color: #73d6fe; + --background-color: #303946; //#121212; + --app-header-color: #73d6fe; //#6495b1; + --sidebar-toggle-color: #ffffff; + --sidebar-toggle-hover-color: #47495d; + --playlist-color: #aeb0b2; + --border-color: #47495d; + --border-light-color: #68686c; + --hover-border-color: #aaaab0; + --spinner-color: #ffffff66; + --spinner-highlight-color: #5599f0; + --tree-count-color: #7b7b7e; + --tree-hover-background-color: #004db4; + --tree-hover-color: white; + --tree-expander-hover-color: white; + --tool-button-background-color: #434343; + --tool-button-color: #fff; + --tool-button-hover-color: #ffffff; + --tool-button-hover-background-color: #0c8a13; + --default-button-background-color: #124a9b; + + --toolbar-background-color: #282A36; + --toolbar-button-hover-color: #ffffff; + --toolbar-button-hover-background-color: #004db4; + + --popup-background-color: #282A36; + --popup-shadow-color: #06070a; + --popup-border-color: #646464; + --popup-color: #fff; + --popup-hover-background-color: #5599f0; + --popup-hover-color: #fff; + + --attention-color: red; + --warn-color: orange; + --ok-color: #21e821; + + --card-background-color: #202733; //#282828; + --card-background-color-darker: #1c2026; + --card-color: #ffffff; + --card-border-color: #47495d; + + --title-label-color: #6495b1; + + --border-radius: 8px; + + --icon-button-border-radius: 4px; + --icon-button-background-color: var(--default-button-background-color); + --icon-button-color: #ffffff; + --icon-button-hover-background-color: #0d6efd; + --icon-button-hover-color: #ffffff; + --icon-button-hover-border-color: #ced9fb; + + --menu-button-color:#fffff; + --menu-button-background-color: #20273344; + + --menu-button-hover-color:#fffff; + --menu-button-hover-background-color: #202733; + + --text-button-border-radius: 4px; + --text-button-border-color: transparent; + --text-button-background-color: var(--default-button-background-color); + --text-button-color: #ffffff; + --text-button-hover-background-color: #0d6efd; + --text-button-hover-color: #ffffff; + + + --scrollbar-track-color: #838383; + --scrollbar-handle-color: #505050; + + --tabset-background-color: #44475a47; // #282A36; + --tabset-border-color: #535353; + --tab-background-color: #44475A; + --tab-color: #fff; + --tab-hover-background-color: #6272A4; + --tab-hover-color: #50FA7B; + --tab-avtive-indicator-color: #50FA7B; + --tab-active-background-color: #54547a; + --tab-active-color: #50FA7B; + + --table-border-color: #44475a; + + --checkbox-background-color: #44475A; + --checkbox-hover-background-color: #006fee; + --checkbox-checked-background-color: #006fee; + --checkbox-border-radius: 4px; + + --option-active-background-color: #50FA7B; + --option-active-color: #000; + + --shocase-card-background: #4b4b4b; + + --breadcrumbs-background-color: #202733; + --breadcrumb-background-color: #4e545d; + --breadcrumb-color: #c4c4c4; + --breadcrumb-active-background-color: #4ad96e; + --breadcrumb-active-color: #000; + + --menu-icon-color-update: #A3D8FF; + --menu-icon-color-user: #0ABAB5; + --menu-icon-color-api_server: #FF9D23; + --menu-icon-color-main_config: #FF2DF1; + --menu-icon-color-status : #F9E400; + + --toolbar-bg-color: #161b25; + +} diff --git a/webui/scss/main.scss b/webui/scss/main.scss new file mode 100644 index 000000000..7b1988904 --- /dev/null +++ b/webui/scss/main.scss @@ -0,0 +1,56 @@ +@use "theme"; +@use "default"; +@use "common"; +@use "app"; +@use "login"; +@use "preferences"; + +html { + height: 100%; +} +body { + margin: 0; + padding: 0; + font-family: "Rubik", sans-serif; + background-color: var(--background-color); + height: 100%; + max-width: 100vw; + overflow: hidden; + box-sizing: border-box; + display: flex; + align-content: stretch; + align-items: stretch; + + input, textarea, select, button { + font-family: inherit; + } + + input { + height: 2rem; + border-radius: 6px; + } + + select { + border-color: var(--border-color); + } +} + +#root { + display: flex; + flex-flow: column; + flex: 1 1 auto; + box-sizing: border-box; + overflow: hidden; + max-width: 100%; + background-color: inherit; +} + +dialog { + border: none; + border-radius: var(--border-radius); + background-color: var(--card-background-color); + filter: drop-shadow(6px 6px 6px var(--popup-shadow-color)); + &::backdrop { + background: rgba(0, 0, 0, 0.3); /* Schwarzer, halbtransparenter Hintergrund */ + } +} diff --git a/webui/src/app/components/authentication.rs b/webui/src/app/components/authentication.rs new file mode 100644 index 000000000..f1ffd4c41 --- /dev/null +++ b/webui/src/app/components/authentication.rs @@ -0,0 +1,54 @@ +use crate::app::components::login::Login; +use crate::hooks::use_service_context; +use std::future; +use yew::prelude::*; +use yew::suspense::use_future; +use yew_hooks::{use_async_with_options, UseAsyncOptions}; + +#[derive(Properties, Clone, PartialEq)] +pub struct AuthenticationProps { + pub children: Children, +} + +#[function_component] +pub fn Authentication(props: &AuthenticationProps) -> Html { + let services = use_service_context(); + let loading = use_state(|| true); + let authenticated = use_state(|| false); + + { + let services_ctx = services.clone(); + let authenticated_state = authenticated.clone(); + let _ = use_future(|| async move { + services_ctx.auth.auth_subscribe( + &mut |success| { + authenticated_state.set(success); + future::ready(()) + } + ).await + }); + } + + { + let services_ctx = services.clone(); + let authenticated_state = authenticated.clone(); + let loading_state = loading.clone(); + use_async_with_options(async move { + let result = services_ctx.auth.refresh().await; + let success = result.is_ok(); + authenticated_state.set(success); + loading_state.set(false); + result + }, UseAsyncOptions::enable_auto()); + } + + if *loading { + html! {} + } else if *authenticated { + html! { + { for props.children.iter() } + } + } else { + html! {} + } +} \ No newline at end of file diff --git a/webui/src/app/components/home.rs b/webui/src/app/components/home.rs new file mode 100644 index 000000000..6ffeac786 --- /dev/null +++ b/webui/src/app/components/home.rs @@ -0,0 +1,67 @@ +use crate::app::components::preferences::Preferences; +use crate::app::components::svg_icon::AppIcon; +use crate::hooks::use_service_context; +use yew::prelude::*; + +#[function_component] +pub fn Home() -> Html { + let services = use_service_context(); + let app_title = services.config.ui_config.app_title.as_ref().map_or("tuliprox", |v| v.as_str()); + + let app_logo = if let Some(logo) = services.config.ui_config.app_logo.as_ref() { + html! { logo } + } else { + html! { } + }; + + let preferences_visible = use_state(|| true); + + let handle_logout = { + let services_ctx = services.clone(); + Callback::from(move |_| services_ctx.auth.logout()) + }; + let handle_view_change = { + let prefs_vis = preferences_visible.clone(); + Callback::from(move |_| prefs_vis.set(!&*prefs_vis)) + }; + + //
+ //
+ + html! { +
+
+
+ + {app_title} +
+
+ // + + +
+
+
+ { if *preferences_visible { + html! {
} + } else { + html! {
{"Plylist"}
} + //html! { } + } + } +
+
+ } +} diff --git a/webui/src/app/components/login.rs b/webui/src/app/components/login.rs new file mode 100644 index 000000000..e90183b00 --- /dev/null +++ b/webui/src/app/components/login.rs @@ -0,0 +1,85 @@ +use web_sys::HtmlInputElement; + +use crate::app::components::svg_icon::AppIcon; +use crate::hooks::use_service_context; +use yew::prelude::*; +use yew_hooks::use_async; + +#[function_component] +pub fn Login() -> Html { + let services = use_service_context(); + let username_ref = use_node_ref(); + let password_ref = use_node_ref(); + let auth_success = use_state(|| true); + + let app_title = services.config.ui_config.app_title.as_ref().map_or("tuliprox", |v| v.as_str()); + + let services_ctx = services.clone(); + let app_logo = use_memo(services_ctx,|service| { + match service.config.ui_config.app_logo.as_ref() { + Some(logo) => html! { logo }, + None => html! { }, + } + }); + + let authenticate = { + let services_ctx = services.clone(); + let authorized_state = auth_success.clone(); + let u_ref = username_ref.clone(); + let p_ref = password_ref.clone(); + use_async(async move { + let username_input: HtmlInputElement = u_ref.cast::().unwrap(); + let password_input: HtmlInputElement = p_ref.cast::().unwrap(); + let username = username_input.value(); + let password = password_input.value(); + let result = services_ctx.auth.authenticate(username, password).await; + let success = result.is_ok(); + authorized_state.set(success); + result + }) + }; + + let do_login = { + let authenticator = authenticate.clone(); + Callback::from(move |_| { + authenticator.run(); + }) + }; + + let handle_login = { + let login = do_login.clone(); + Callback::from(move |e: MouseEvent| { + e.prevent_default(); + login.emit(()); + }) + }; + + let handle_key_down = { + let login = do_login.clone(); + Callback::from(move |e: KeyboardEvent| { + if e.key() == "Enter" { + login.emit(()); + } + }) + }; + + html! { + <> + //
+ + + } +} \ No newline at end of file diff --git a/webui/src/app/components/mod.rs b/webui/src/app/components/mod.rs new file mode 100644 index 000000000..72dafb0ed --- /dev/null +++ b/webui/src/app/components/mod.rs @@ -0,0 +1,13 @@ +mod panel; +mod svg_icon; +mod authentication; +mod home; +mod login; +mod preferences; + +pub use self::svg_icon::*; +pub use self::panel::*; +pub use self::authentication::*; +pub use self::login::*; +pub use self::home::*; +pub use self::preferences::*; diff --git a/webui/src/app/components/panel.rs b/webui/src/app/components/panel.rs new file mode 100644 index 000000000..63bb97911 --- /dev/null +++ b/webui/src/app/components/panel.rs @@ -0,0 +1,17 @@ +use yew::{function_component, html, Children, Html, Properties}; + +#[derive(Properties, Clone, PartialEq, Debug)] +pub struct PanelProps { + pub value: String, + pub active: String, + pub children: Children, +} + +#[function_component] +pub fn Panel(props: &PanelProps) -> Html { + html! { +
+ { for props.children.iter() } +
+ } +} \ No newline at end of file diff --git a/webui/src/app/components/preferences.rs b/webui/src/app/components/preferences.rs new file mode 100644 index 000000000..7f8e6274d --- /dev/null +++ b/webui/src/app/components/preferences.rs @@ -0,0 +1,122 @@ +use crate::app::components::AppIcon; +use crate::app::components::Panel; +use std::fmt; +use std::str::FromStr; +use wasm_bindgen::JsCast; +use yew::prelude::*; +use yew_i18n::use_translation; + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum PrefPage { + Update, + User, + ApiServer, + MainConfig, + Status, +} + +impl fmt::Display for PrefPage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + PrefPage::Update => "update", + PrefPage::User => "user", + PrefPage::ApiServer => "apiserver", + PrefPage::MainConfig => "mainconfig", + PrefPage::Status => "status", + }; + write!(f, "{s}") + } +} + +impl FromStr for PrefPage { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "update" => Ok(PrefPage::Update), + "user" => Ok(PrefPage::User), + "apiserver" => Ok(PrefPage::ApiServer), + "mainconfig" => Ok(PrefPage::MainConfig), + "status" => Ok(PrefPage::Status), + _ => Err(()) + } + } +} + +struct SidebarAction { + page: PrefPage, + icon: &'static str, + label: &'static str, +} + +const SIDEBAR_ACTIONS: &[SidebarAction] = &[ + SidebarAction { page: PrefPage::Update, icon: "Refresh", label: "LABEL.REFRESH" }, + SidebarAction { page: PrefPage::User, icon: "User", label: "LABEL.USERS" }, + SidebarAction { page: PrefPage::ApiServer, icon: "ApiServer", label: "LABEL.PROXY" }, + SidebarAction { page: PrefPage::MainConfig, icon: "Config", label: "LABEL.CONFIG" }, + SidebarAction { page: PrefPage::Status, icon: "Status", label: "LABEL.STATUS" }, +]; + +#[function_component] +pub fn Preferences() -> Html { + let active_page = use_state(|| PrefPage::Update); + + let translation = use_translation(); + + let handle_sidebar_action = { + let active_page = active_page.clone(); + Callback::from(move |e: MouseEvent| { + if let Some(target) = e.target() { + if let Ok(element) = target.dyn_into::() { + if let Some(data_page) = element.get_attribute("data-page") { + if let Ok(page) = PrefPage::from_str(&data_page) { + active_page.set(page); + } + } + } + } + }) + }; + + html! { +
+
+
+ { + SIDEBAR_ACTIONS.iter().map(|action| { + html! { +
+ + +
+ } + }).collect::>() + } +
+
+ +
{"Update"}
+
+ +
{"User"}
+
+ +
{"Api"}
+
+ +
{"MainConfig"}
+
+ +
{"Status"}
+
+
+
+
+ } +} \ No newline at end of file diff --git a/webui/src/app/components/svg_icon.rs b/webui/src/app/components/svg_icon.rs new file mode 100644 index 000000000..6ffc073b3 --- /dev/null +++ b/webui/src/app/components/svg_icon.rs @@ -0,0 +1,51 @@ +use yew::prelude::*; +use crate::hooks::use_icon_context; + +#[derive(Properties, Clone, PartialEq, Eq, Debug)] +pub struct SvgIconProps { + name: AttrValue, + path: AttrValue, + #[prop_or(AttrValue::Static("24px"))] + pub width: AttrValue, + #[prop_or(AttrValue::Static("24px"))] + pub height: AttrValue, + #[prop_or(AttrValue::Static("0 0 24 24"))] + pub viewport:AttrValue, +} +#[function_component] +pub fn SvgIcon(props: &SvgIconProps) -> Html { + // info!("SvgIcon props {:?}", props); + html! { + + } +} + +#[derive(Properties, Clone, PartialEq, Eq)] +pub struct AppIconProps { + pub name: AttrValue, + #[prop_or(AttrValue::Static("100%"))] + pub width: AttrValue, + #[prop_or(AttrValue::Static("100%"))] + pub height: AttrValue, +} + +#[function_component] +pub fn AppIcon(props: &AppIconProps) -> Html { + let icon_ctx = use_icon_context(); + let name = props.name.to_string(); + let icon_def = use_memo((icon_ctx, name.clone()), |(icon_ctx, name)| { + icon_ctx.get_icon(name) + }); + + match &*icon_def { + Some(definition) => { + let viewport = definition.viewport.as_ref().map_or_else(|| AttrValue::Static("0 0 24 24"), |vp|AttrValue::from(vp.to_string())); + html! { + + } + }, + None => html! {} + } +} diff --git a/webui/src/app/mod.rs b/webui/src/app/mod.rs new file mode 100644 index 000000000..09fdfb2c0 --- /dev/null +++ b/webui/src/app/mod.rs @@ -0,0 +1,146 @@ +pub mod components; + +use std::collections::HashMap; +use std::rc::Rc; +use futures::future::join_all; +use log::{error}; +use serde_json::Value; +use crate::app::components::Authentication; +use crate::app::components::Home; +use crate::provider::icon_context_provider::IconContextProvider; +use crate::provider::service_context_provider::ServiceContextProvider; +use components::Login; +use yew_i18n::I18nProvider; +use yew::prelude::*; +use yew_hooks::{use_async_with_options, UseAsyncOptions}; +use yew_router::prelude::*; +use crate::app::components::Preferences; +use crate::config::Config; +use crate::error::Error; +use crate::hooks::IconDefinition; +use crate::services::request_get; + +fn flatten_json(value: &Value, prefix: String, map: &mut HashMap) { + match value { + Value::Object(obj) => { + for (key, val) in obj { + let new_prefix = if prefix.is_empty() { + key.clone() + } else { + format!("{prefix}.{key}") + }; + flatten_json(val, new_prefix, map); + } + } + Value::Array(arr) => { + for (i, val) in arr.iter().enumerate() { + let new_prefix = format!("{prefix}[{i}]"); + flatten_json(val, new_prefix, map); + } + } + other => { + map.insert(prefix, other.clone()); + } + } +} + +/// App routes +#[derive(Routable, Debug, Clone, PartialEq, Eq)] +pub enum AppRoute { + #[at("/login")] + Login, + #[at("/preferecnes")] + Preferences, + #[at("/")] + Home, + #[not_found] + #[at("/404")] + NotFound, +} + +pub fn switch(route: AppRoute) -> Html { + match route { + AppRoute::Login => html! {}, + AppRoute::Home => html! {}, + AppRoute::Preferences => html! {}, + AppRoute::NotFound => html! { "Page not found" }, + } +} + +#[function_component] +pub fn App() -> Html { + let supported_languages = vec!["en"]; + let translations_state = use_state(|| None); + let configuration_state = use_state(|| None); + let icon_state = use_state(|| None); + + { + let trans_state = translations_state.clone(); + let languages = supported_languages.clone(); + use_async_with_options::<_, (), Error>(async move { + let futures = languages.iter() + .map(|lang| async move { + let url = format!("assets/i18n/{lang}.json"); + let result: Result = request_get(&url).await; + (lang.to_string(), result) + }) + .collect::>(); + let results = join_all(futures).await; + let mut translations = HashMap::::new(); + for (lang, result) in results { + if let Ok(i18n) = result { + let mut lang_translations = HashMap::::new(); + flatten_json(&i18n, String::new(), &mut lang_translations); + let map: serde_json::Map = lang_translations.into_iter().collect(); + translations.insert(lang, Value::Object(map)); + } + } + trans_state.set(Some(translations)); + Ok(()) + }, UseAsyncOptions::enable_auto()); + } + + { + let config_state = configuration_state.clone(); + use_async_with_options::<_, (), Error>(async move { + match request_get("config.json").await { + Ok(cfg) => config_state.set(Some(cfg)), + Err(err) => error!("Failed to load config {err}"), + } + Ok(()) + }, UseAsyncOptions::enable_auto()); + } + + { + let icon_state = icon_state.clone(); + use_async_with_options::<_, (), Error>(async move { + match request_get("assets/icons.json").await { + Ok(icons) => icon_state.set(Some(icons)), + Err(err) => error!("Failed to load icons {err}"), + } + Ok(()) + }, UseAsyncOptions::enable_auto()); + } + + if translations_state.as_ref().is_none() || configuration_state.as_ref().is_none() + || icon_state.as_ref().is_none(){ + return html! {
{ "Loading..." }
}; + } + let transl = translations_state.as_ref().unwrap(); + let config: &Config = configuration_state.as_ref().unwrap(); + let icons: &Vec> = icon_state.as_ref().unwrap(); + + html! { + + + + + + render={switch} /> + + + + + + } +} \ No newline at end of file diff --git a/webui/src/config.rs b/webui/src/config.rs new file mode 100644 index 000000000..71be9468a --- /dev/null +++ b/webui/src/config.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; + + +#[derive(PartialEq, Eq, Clone, Serialize, Deserialize, Debug)] +pub struct ApiConfig { + #[serde(alias = "apiUrl")] + pub api_url: String, + #[serde(alias = "authUrl")] + pub auth_url: String, +} + +#[derive(PartialEq, Eq, Clone, Serialize, Deserialize, Debug)] +pub struct Config { + #[serde(alias = "tabTitle")] + pub tab_title: Option, + #[serde(alias = "appTitle")] + pub app_title: Option, + #[serde(alias = "appLogo")] + pub app_logo: Option, + pub api: ApiConfig, + #[serde(alias = "wsUrl")] + pub ws_url: String, + #[serde(alias = "protocolVersion")] + pub protocol_version: u8, + +} diff --git a/webui/src/error.rs b/webui/src/error.rs new file mode 100644 index 000000000..8e85f48d4 --- /dev/null +++ b/webui/src/error.rs @@ -0,0 +1,43 @@ +use std::collections::HashMap; +use serde::{Deserialize, Serialize}; +use thiserror::Error as ThisError; + +/// Conduit api error info for Unprocessable Entity error +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct ErrorInfo { + pub errors: HashMap>, +} + + +/// Define all possible errors +#[derive(ThisError, Clone, Debug, PartialEq, Eq)] +pub enum Error { + /// 401 + #[error("Unauthorized")] + Unauthorized, + + /// 403 + #[error("Forbidden")] + Forbidden, + + /// 404 + #[error("Not Found")] + NotFound, + + /// 422 + #[error("Unprocessable Entity: {0:?}")] + UnprocessableEntity(ErrorInfo), + + /// 500 + #[error("Internal Server Error")] + InternalServerError, + + /// serde deserialize error + #[error("Deserialize Error")] + DeserializeError, + + /// request error + #[error("Http Request Error")] + RequestError, +} \ No newline at end of file diff --git a/webui/src/hooks/mod.rs b/webui/src/hooks/mod.rs new file mode 100644 index 000000000..770682e92 --- /dev/null +++ b/webui/src/hooks/mod.rs @@ -0,0 +1,5 @@ +mod use_service_context; +mod use_icon_context; + +pub use use_service_context::*; +pub use use_icon_context::*; \ No newline at end of file diff --git a/webui/src/hooks/use_icon_context.rs b/webui/src/hooks/use_icon_context.rs new file mode 100644 index 000000000..937df4f0e --- /dev/null +++ b/webui/src/hooks/use_icon_context.rs @@ -0,0 +1,70 @@ +use std::collections::HashMap; +use std::rc::Rc; +use yew::prelude::*; + +#[derive(PartialEq, Eq, Clone, serde::Deserialize, Debug)] +pub struct IconDefinition { + keys: Vec, + pub(crate) path: String, + pub(crate) viewport: Option, +} + +#[derive(PartialEq, Debug)] +pub struct Icons { + definitions: Option>>, +} + +impl Icons { + pub fn new() -> Self { + Self { + definitions: None + } + } + + pub fn new_with(definitions: &Vec>) -> Self { + let mut map = HashMap::new(); + + for icon in definitions { + for key in &icon.keys { + map.insert(key.clone(), icon.clone()); + } + } + + Self { definitions: Some(map) } + } + + pub fn get_icon(&self, name: &str) -> Option> { + if let Some(icon_defs) = &self.definitions { + return icon_defs.get(name).cloned(); + } + None + } +} + +impl Default for Icons { + fn default() -> Self { + Self::new() + } +} + +#[derive(PartialEq, Debug, Clone)] +pub struct IconContext { + pub(crate) icons: Rc, +} + +impl IconContext { + pub fn new(definitions: &Vec>) -> Self { + Self { + icons: Rc::new(Icons::new_with(definitions)) + } + } + + pub fn icons(&self) -> Rc { + self.icons.clone() + } +} + +#[hook] +pub fn use_icon_context() -> Rc { + use_context::>().unwrap().icons() +} \ No newline at end of file diff --git a/webui/src/hooks/use_service_context.rs b/webui/src/hooks/use_service_context.rs new file mode 100644 index 000000000..4adaf6c62 --- /dev/null +++ b/webui/src/hooks/use_service_context.rs @@ -0,0 +1,51 @@ +use std::rc::Rc; +use yew::prelude::*; +use crate::config::Config; +use crate::services::auth_service::AuthService; +use crate::services::config_service::ConfigService; + +pub struct Services { + pub auth: Rc, + pub config: Rc, +} + +impl Services { + pub fn new(config: &Config) -> Self { + let auth = Rc::new(AuthService::new()); + let config = Rc::new(ConfigService::new(config)); + Self { + auth, + config + } + } +} + +impl PartialEq for Services { + fn eq(&self, _other: &Self) -> bool { + true + } +} + +impl Eq for Services {} + +#[derive(PartialEq, Eq, Clone)] +pub struct ServiceContext { + services: Rc, +} + +impl ServiceContext { + pub fn new(config: &Config) -> Self { + Self { + services: Rc::new(Services::new(config)) + } + } + + pub fn services(&self) -> Rc { + self.services.clone() + } +} + +#[hook] +pub fn use_service_context() -> Rc { + use_context::>().unwrap().services() +} \ No newline at end of file diff --git a/webui/src/lib.rs b/webui/src/lib.rs new file mode 100644 index 000000000..300ea3da2 --- /dev/null +++ b/webui/src/lib.rs @@ -0,0 +1,6 @@ +pub mod config; +pub mod app; +pub mod error; +pub mod hooks; +pub mod services; +pub mod provider; \ No newline at end of file diff --git a/webui/src/main.rs b/webui/src/main.rs new file mode 100644 index 000000000..802537461 --- /dev/null +++ b/webui/src/main.rs @@ -0,0 +1,6 @@ +use frontend::app::App; + +pub fn main() { + wasm_logger::init(wasm_logger::Config::default()); + yew::Renderer::::new().render(); +} \ No newline at end of file diff --git a/webui/src/provider/icon_context_provider.rs b/webui/src/provider/icon_context_provider.rs new file mode 100644 index 000000000..f7761bde1 --- /dev/null +++ b/webui/src/provider/icon_context_provider.rs @@ -0,0 +1,21 @@ +use crate::hooks::{IconContext, IconDefinition}; +use std::rc::Rc; +use yew::prelude::*; + +#[derive(Properties, Clone, PartialEq)] +pub struct IconContextProps { + pub children: Children, + pub icons: Vec>, +} + +/// User context provider. +#[function_component] +pub fn IconContextProvider(props: &IconContextProps) -> Html { + let icon_ctx = use_state(||IconContext::new(&props.icons)); + + html! { + > context={icon_ctx}> + { for props.children.iter() } + >> + } +} \ No newline at end of file diff --git a/webui/src/provider/mod.rs b/webui/src/provider/mod.rs new file mode 100644 index 000000000..1b0891af8 --- /dev/null +++ b/webui/src/provider/mod.rs @@ -0,0 +1,2 @@ +pub mod icon_context_provider; +pub mod service_context_provider; \ No newline at end of file diff --git a/webui/src/provider/service_context_provider.rs b/webui/src/provider/service_context_provider.rs new file mode 100644 index 000000000..7ed502e33 --- /dev/null +++ b/webui/src/provider/service_context_provider.rs @@ -0,0 +1,22 @@ + +use yew::prelude::*; +use crate::config::Config; +use crate::hooks::ServiceContext; + +#[derive(Properties, Clone, PartialEq)] +pub struct ServiceContextProps { + pub children: Children, + pub config: Config +} + +/// User context provider. +#[function_component] +pub fn ServiceContextProvider(props: &ServiceContextProps) -> Html { + let service_ctx = use_state(||ServiceContext::new(&props.config)); + + html! { + > context={service_ctx}> + { for props.children.iter() } + >> + } +} \ No newline at end of file diff --git a/webui/src/services/auth_service.rs b/webui/src/services/auth_service.rs new file mode 100644 index 000000000..5c536d560 --- /dev/null +++ b/webui/src/services/auth_service.rs @@ -0,0 +1,76 @@ +use super::request_post; +use crate::error::Error; +use crate::services::requests::set_token; +use futures_signals::signal::Mutable; +use futures_signals::signal::SignalExt; +use shared::model::{TokenResponse, UserCredential}; +use std::future::Future; + +#[derive(Debug)] +pub struct AuthService { + auth_channel: Mutable, +} + +const AUTH_PATH: &str = "/auth"; + +impl AuthService { + pub fn new() -> Self { + Self { + auth_channel: Mutable::new(false), + } + } + + pub async fn auth_subscribe(&self, callback: &mut F) + where + U: Future, + F: FnMut(bool) -> U, + { + let fut = self.auth_channel.signal_cloned().for_each(callback); + fut.await + } + + pub fn logout(&self) { + set_token(None); + self.auth_channel.set(false); + } + + pub async fn authenticate(&self, username: String, password: String) -> Result { + let credentials = UserCredential { + username, + password, + }; + match request_post::(&format!("{AUTH_PATH}/token"), credentials).await { + Ok(token) => { + self.auth_channel.set(true); + set_token(Some(&token.token)); + Ok(token) + } + Err(e) => { + self.auth_channel.set(false); + set_token(None); + Err(e) + } + } + } + + pub async fn refresh(&self) -> Result { + match request_post::<(), TokenResponse>(&format!("{AUTH_PATH}/refresh"), ()).await { + Ok(token) => { + self.auth_channel.set(true); + set_token(Some(&token.token)); + Ok(token) + } + Err(e) => { + self.auth_channel.set(false); + set_token(None); + Err(e) + } + } + } +} + +impl Default for AuthService { + fn default() -> Self { + Self::new() + } +} diff --git a/webui/src/services/config_service.rs b/webui/src/services/config_service.rs new file mode 100644 index 000000000..a2ea36104 --- /dev/null +++ b/webui/src/services/config_service.rs @@ -0,0 +1,24 @@ +use std::rc::Rc; +use shared::model::{ConfigDto}; +use crate::config::Config; +use crate::services::{request_get}; + +const CONFIG_PATH: &str = "/api/v1/config"; + + +pub struct ConfigService { + pub ui_config: Rc, +} + +impl ConfigService { + pub fn new(config: &Config) -> Self { + Self { + ui_config: Rc::new(config.clone()), + } + } + + pub async fn get_server_config(&self) -> Result { + request_get::(CONFIG_PATH).await + } + +} \ No newline at end of file diff --git a/webui/src/services/mod.rs b/webui/src/services/mod.rs new file mode 100644 index 000000000..ca16f9798 --- /dev/null +++ b/webui/src/services/mod.rs @@ -0,0 +1,7 @@ +pub mod auth_service; +pub mod config_service; +pub mod requests; + +pub use requests::{ + limit, request_delete, request_get, request_post, request_put +}; \ No newline at end of file diff --git a/webui/src/services/requests.rs b/webui/src/services/requests.rs new file mode 100644 index 000000000..250114ff0 --- /dev/null +++ b/webui/src/services/requests.rs @@ -0,0 +1,126 @@ +use gloo_storage::{LocalStorage, Storage}; +use crate::error::{Error, ErrorInfo}; +use log::{error}; +use serde::{de::DeserializeOwned, Serialize}; +use reqwasm::http::Request; + +enum RequestMethod { + Get, + Post, + Put, + // PATCH, + Delete +} + +const API_ROOT: &str = "/api/v1"; + +const TOKEN_KEY: &str = "tuliprox.token"; +pub fn get_token() -> Option { + LocalStorage::get(TOKEN_KEY).ok() +} + +pub fn set_token(token: Option<&str>) { + if let Some(t) = token { + LocalStorage::set(TOKEN_KEY, String::from(t)).expect("failed to set"); + } else { + LocalStorage::delete(TOKEN_KEY); + } +} + +/// build all kinds of http request: post/get/delete etc. +async fn request(method: RequestMethod, url: &str, body: B) -> Result +where + T: DeserializeOwned + 'static + std::fmt::Debug, + B: Serialize + std::fmt::Debug, +{ + let mut request = match method { + RequestMethod::Get => Request::get(url), + RequestMethod::Post => Request::post(url).body(serde_json::to_string(&body).unwrap()), + RequestMethod::Put => Request::put(url).body(serde_json::to_string(&body).unwrap()), + // RequestMethod::PATCH => Request::patch(&url).body(serde_json::to_string(&body).unwrap()), + RequestMethod::Delete => Request::delete(url), + }.header("Content-Type", "application/json"); + if let Some(token) = get_token() { + request = request.header("Authorization", format!("Bearer {token}").as_str()); + } + match request.send().await { + + Ok(response) => { + match response.status() { + 200 => { + let data: Result = response.json::().await; + if let Ok(data) = data { + // debug!("Response: {:?}", data); + Ok(data) + } else { + Err(Error::DeserializeError) + } + }, + 401 => Err(Error::Unauthorized), + 403 => Err(Error::Forbidden), + 404 => Err(Error::NotFound), + 500 => Err(Error::InternalServerError), + 422 => { + let data: Result = response.json::().await; + if let Ok(data) = data { + Err(Error::UnprocessableEntity(data)) + } else { + Err(Error::DeserializeError) + } + } + _ => Err(Error::RequestError), + } + } + Err(e) => { + error!("{e}"); + Err(Error::RequestError) + } + } +} + +/// Delete request +pub async fn request_delete(url: &str) -> Result +where + T: DeserializeOwned + 'static + std::fmt::Debug, +{ + request(RequestMethod::Delete, url, ()).await +} + +/// Get request +pub async fn request_get(url: &str) -> Result +where + T: DeserializeOwned + 'static + std::fmt::Debug, +{ + request(RequestMethod::Get, url, ()).await +} + +pub async fn request_get_api(url: &str) -> Result +where + T: DeserializeOwned + 'static + std::fmt::Debug, +{ + request(RequestMethod::Get, format!("{API_ROOT}{url}").as_str(), ()).await +} + +/// Post request with a body +pub async fn request_post(url: &str, body: B) -> Result +where + T: DeserializeOwned + 'static + std::fmt::Debug, + B: Serialize + std::fmt::Debug, +{ + request(RequestMethod::Post, url, body).await +} + +/// Put request with a body +pub async fn request_put(url: &str, body: B) -> Result +where + T: DeserializeOwned + 'static + std::fmt::Debug, + B: Serialize + std::fmt::Debug, +{ + request(RequestMethod::Put, url, body).await +} + +/// Set limit for pagination +pub fn limit(count: u32, p: u32) -> String { + let offset = if p > 0 { p * count } else { 0 }; + format!("limit={count}&offset={offset}") +}