diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..0e465b2 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.wasm32-unknown-unknown] +rustflags = ["--cfg", "getrandom_backend=\"wasm_js\""] diff --git a/.helix/languages.toml b/.helix/languages.toml index b868472..6cbd9aa 100644 --- a/.helix/languages.toml +++ b/.helix/languages.toml @@ -6,5 +6,8 @@ language-servers = ["rust-analyzer", "tailwindcss-ls"] # procMacro = { ignored = { leptos_macro = ["server"] } } cargo = { features = ["ssr", "hydrate"] } +[language-server.rust-analyzer.config.check] +command = "clippy" + [language-server.tailwindcss-ls] config = { userLanguages = { rust = "html", "*.rs" = "html" } } diff --git a/Cargo.lock b/Cargo.lock index d9f64fe..521cd03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,27 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned-vec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.18" @@ -100,23 +121,53 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "ascend" version = "0.0.0" dependencies = [ "axum", + "bincode", "camino", + "chrono", "clap", + "codee", "confik", "console_error_panic_hook", "derive_more", + "error_reporter", + "getrandom 0.3.1", "http 1.2.0", + "image", "leptos", "leptos_axum", "leptos_meta", "leptos_router", "moonboard-parser", - "rand", + "rand 0.9.0", + "redb", "ron", "serde", "serde_json", @@ -129,6 +180,7 @@ dependencies = [ "tracing-subscriber", "tracing-subscriber-wasm", "type-toppings", + "uuid", "wasm-bindgen", "web-sys", "xdg", @@ -147,9 +199,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.85" +version = "0.1.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" +checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" dependencies = [ "proc-macro2", "quote", @@ -192,6 +244,29 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "av1-grain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62" +dependencies = [ + "arrayvec", +] + [[package]] name = "axum" version = "0.7.9" @@ -284,6 +359,18 @@ dependencies = [ "serde", ] +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.8.0" @@ -294,10 +381,28 @@ dependencies = [ ] [[package]] -name = "bumpalo" -version = "3.16.0" +name = "bitstream-io" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" + +[[package]] +name = "built" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "bytemuck" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" [[package]] name = "byteorder" @@ -306,10 +411,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] -name = "bytes" -version = "1.9.0" +name = "byteorder-lite" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" [[package]] name = "camino" @@ -320,12 +431,48 @@ dependencies = [ "serde", ] +[[package]] +name = "cc" +version = "1.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "755717a7de9ec452bf7f3f1a3099085deabd7f2962b861dae91ecd7a365903d2" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -355,9 +502,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.27" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" +checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff" dependencies = [ "clap_builder", "clap_derive", @@ -377,9 +524,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.24" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" +checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" dependencies = [ "heck", "proc-macro2", @@ -395,13 +542,13 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "codee" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d3ad3122b0001c7f140cf4d605ef9a9e2c24d96ab0b4fb4347b76de2425f445" +checksum = "0f18d705321923b1a9358e3fc3c57c3b50171196827fc7f5f10b053242aca627" dependencies = [ "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.11", ] [[package]] @@ -410,6 +557,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186dce98367766de751c42c4f03970fc60fc012296e706ccbb9d5df9b6c1e271" +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.3" @@ -510,6 +663,40 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -620,6 +807,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" +[[package]] +name = "dyn-clone" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35" + [[package]] name = "either" version = "1.13.0" @@ -628,10 +821,11 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "either_of" -version = "0.1.2" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2dc0006c5cf511f802ddcffc0a6df9dcc1912f5f0e448f6641b3b035f14f43d" +checksum = "169ae1dd00fb612cf27fd069b3b10f325ea60ac551f08e5b931b4413972a847d" dependencies = [ + "paste", "pin-project-lite", ] @@ -687,6 +881,40 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.73.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -801,10 +1029,34 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.31.1" @@ -1113,9 +1365,9 @@ checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" [[package]] name = "httparse" -version = "1.9.5" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" [[package]] name = "httpdate" @@ -1141,9 +1393,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes", "futures-channel", @@ -1174,6 +1426,29 @@ dependencies = [ "tower-service", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -1319,6 +1594,45 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" + [[package]] name = "indexmap" version = "2.7.1" @@ -1329,6 +1643,17 @@ dependencies = [ "hashbrown 0.15.2", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "interpolator" version = "0.5.0" @@ -1337,9 +1662,9 @@ checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" [[package]] name = "inventory" -version = "0.3.17" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b31349d02fe60f80bbbab1a9402364cad7460626d6030494b08ac4a2075bf81" +checksum = "54b12ebb6799019b044deaf431eadfe23245b259bba5a2c0796acec3943a3cdb" dependencies = [ "rustversion", ] @@ -1350,6 +1675,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -1366,10 +1700,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] -name = "js-sys" -version = "0.3.76" +name = "jobserver" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", @@ -1382,17 +1731,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] -name = "leptos" -version = "0.7.4" +name = "lebe" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21c31c9d022c77702c53e02830d08b28320aca9c0899a19c443096c114623fa5" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "leptos" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78329c12843d64766d8f00216aae665416d804327302ce8e0ab83884dfa91887" dependencies = [ "any_spawner", "base64 0.22.1", "cfg-if", "either_of", "futures", - "getrandom", + "getrandom 0.2.15", "hydration_context", "leptos_config", "leptos_dom", @@ -1402,7 +1757,7 @@ dependencies = [ "oco_ref", "or_poisoned", "paste", - "rand", + "rand 0.8.5", "reactive_graph", "rustc-hash", "send_wrapper", @@ -1422,9 +1777,9 @@ dependencies = [ [[package]] name = "leptos_axum" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b613d5784037baee42a11d21bc263adfc1a55e416556a3d5bfe39c7b87fadf" +checksum = "d59be1dd90fa5102d0a48696c93328c9947aaef19f2dbe9c81f819db3b11a849" dependencies = [ "any_spawner", "axum", @@ -1446,9 +1801,9 @@ dependencies = [ [[package]] name = "leptos_config" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d874993c7664d757677d056c8f46b5cb5365fe622005e1bf26050f4996e7e52" +checksum = "132a18e8ffc4fbe2d624f3743d88a1b4989bff2d5e12be2b0d2749201d9dfb52" dependencies = [ "config", "regex", @@ -1459,9 +1814,9 @@ dependencies = [ [[package]] name = "leptos_dom" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a462aaeec85bc4ecfb26bf324437b92690bf3add1e30eb29b3acc08b20e8b4cb" +checksum = "d468f638f2f13d70d99d9952be98d671a75366034472f3828e586ba62d770049" dependencies = [ "js-sys", "or_poisoned", @@ -1475,9 +1830,9 @@ dependencies = [ [[package]] name = "leptos_hot_reload" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07eb295ad2f3b2af190da62af339b84fd01ce3c71702f09eb69a57310fcf0c6d" +checksum = "8ba37d76693fc6228554e0bb06a9aa41c59e2b5180caf423c7913557b81d01dd" dependencies = [ "anyhow", "camino", @@ -1493,9 +1848,9 @@ dependencies = [ [[package]] name = "leptos_integration_utils" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8652fcd7a1744f85403b95c5520143f3b962d640c8450b8514f9530fb5c4b76" +checksum = "123887e1b34f8fadab385b3192e14a4e5bd84505dec16c31de0b8c4ccc8553d3" dependencies = [ "futures", "hydration_context", @@ -1508,15 +1863,15 @@ dependencies = [ [[package]] name = "leptos_macro" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90291b25ee576bc9c299d3371cc8f09bf60ea939a8de61fa8b744650aff76e24" +checksum = "064d0c8b144b93f8d7e84b30c16d1da0e64a63c7e91b9a872f7be63601c5868b" dependencies = [ "attribute-derive", "cfg-if", "convert_case", "html-escape", - "itertools", + "itertools 0.13.0", "leptos_hot_reload", "prettyplease", "proc-macro-error2", @@ -1531,9 +1886,9 @@ dependencies = [ [[package]] name = "leptos_meta" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7250991b2077ef5869e999c74cf4990926a3c3919b50c9937e101c1c874102db" +checksum = "3abd2ac7fbfeee757fd569f7db4bdbaafe1e899049435476f06d4be1911f6097" dependencies = [ "futures", "indexmap", @@ -1547,9 +1902,9 @@ dependencies = [ [[package]] name = "leptos_router" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a193dbd62b9617a5d7d199ea70c570da01a1bbe798e617373b6351845be6778" +checksum = "8dbc548cc4d127608a79354361df357f53c7d8e89dc14ef9738d789cd338694a" dependencies = [ "any_spawner", "either_of", @@ -1572,20 +1927,21 @@ dependencies = [ [[package]] name = "leptos_router_macro" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34bc3f80ad810b22058f12d278bb0bf929779cc0bc1289a06980d896f62743f0" +checksum = "3010b3a222b5ff3d16421d074f74b52049df4353330373d17f675b2e0f51c9de" dependencies = [ "proc-macro-error2", "proc-macro2", "quote", + "syn", ] [[package]] name = "leptos_server" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18caffe32c245ddb35697edd898ccb3393efce67672a707a14eebd0db2e8249a" +checksum = "fb1779f1f0570915066c132fb11f999add8b13d02ca5221735193eb02b3fa69a" dependencies = [ "any_spawner", "base64 0.22.1", @@ -1608,6 +1964,16 @@ version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +[[package]] +name = "libfuzzer-sys" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "linear-map" version = "1.2.0" @@ -1636,6 +2002,15 @@ version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "manyhow" version = "0.11.4" @@ -1674,6 +2049,16 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1709,6 +2094,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -1718,7 +2104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -1750,6 +2136,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "next_tuple" version = "0.1.0" @@ -1766,6 +2158,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1776,6 +2174,56 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -1807,9 +2255,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" [[package]] name = "or_poisoned" @@ -1872,18 +2320,18 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" +checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" +checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" dependencies = [ "proc-macro2", "quote", @@ -1902,13 +2350,32 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "ppv-lite86" version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -1976,6 +2443,40 @@ dependencies = [ "yansi", ] +[[package]] +name = "profiling" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.38" @@ -2014,8 +2515,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.0", + "zerocopy 0.8.17", ] [[package]] @@ -2025,7 +2537,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.0", ] [[package]] @@ -2034,14 +2556,94 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" +dependencies = [ + "getrandom 0.3.1", + "zerocopy 0.8.17", +] + +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.12.1", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand 0.8.5", + "rand_chacha 0.3.1", + "simd_helpers", + "system-deps", + "thiserror 1.0.69", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", ] [[package]] name = "reactive_graph" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbf210c04505e128fb7f64acecc23c71f82f56c7d481b190e1010b7bada2cb9" +checksum = "059aede5acae8f5c25b1d34b6df34700006418b3c493db3698b7ebcd4a8a6287" dependencies = [ "any_spawner", "async-lock", @@ -2061,12 +2663,12 @@ dependencies = [ [[package]] name = "reactive_stores" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80bb1913eeb71f74028213455ee971550c2b3cb91b6acd5efa8a0f8dc59f5039" +checksum = "4c7edacf4298579a5772285b8e2dc0b9953c8fbaa9c3f56c3dd69d56e5af7a48" dependencies = [ "guardian", - "itertools", + "itertools 0.13.0", "or_poisoned", "paste", "reactive_graph", @@ -2076,9 +2678,9 @@ dependencies = [ [[package]] name = "reactive_stores_macro" -version = "0.1.0" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d86e4f08f361b05d11422398cef4bc4cf356f2fdd2f06a96646b0e9cd902226" +checksum = "178b1cd8b2871a45bfc8e13ff8076049b6e9a5132e72414e5cab3894c4a6adb3" dependencies = [ "convert_case", "proc-macro-error2", @@ -2087,13 +2689,22 @@ dependencies = [ "syn", ] +[[package]] +name = "redb" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0a72cd7140de9fc3e318823b883abf819c20d478ec89ce880466dc2ef263c6" +dependencies = [ + "libc", +] + [[package]] name = "redox_syscall" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags", + "bitflags 2.8.0", ] [[package]] @@ -2140,6 +2751,12 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rgb" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" + [[package]] name = "ron" version = "0.8.1" @@ -2147,7 +2764,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64 0.21.7", - "bitflags", + "bitflags 2.8.0", "serde", "serde_derive", ] @@ -2175,9 +2792,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustversion" @@ -2187,9 +2804,9 @@ checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" [[package]] name = "same-file" @@ -2248,9 +2865,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.137" +version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" dependencies = [ "itoa", "memchr", @@ -2302,9 +2919,9 @@ dependencies = [ [[package]] name = "server_fn" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5dd7fcccd3ef2081da086c1f8595b506627abbbbc9f64be0141d2251219570e" +checksum = "5c183c31152fd00e994a3ea0ca43e6017056ccf7812160b0ae008acc3de8241c" dependencies = [ "axum", "bytes", @@ -2339,9 +2956,9 @@ dependencies = [ [[package]] name = "server_fn_macro" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0bbac4f01a714b0490247ac625bdb7055548210556c39e8f56a2dbbe3abc70b" +checksum = "c43b2266308c118be1a1cc60602f8efb07a64e72deed8d317704d5cfda092ca1" dependencies = [ "const_format", "convert_case", @@ -2353,9 +2970,9 @@ dependencies = [ [[package]] name = "server_fn_macro_default" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f07dfd1744a5f5612f00f69fe035b0bfafdf12bb46d76e785673078a9e56b170" +checksum = "087eca61bc8f93d868b8c10ca058da358fd7aaeb7bc8415b572f9f3f27ce0b93" dependencies = [ "server_fn_macro", "syn", @@ -2370,6 +2987,27 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "slab" version = "0.4.9" @@ -2435,9 +3073,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.96" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", @@ -2474,19 +3112,34 @@ dependencies = [ ] [[package]] -name = "tachys" -version = "0.1.4" +name = "system-deps" +version = "6.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d777e4426a597296b020edcb5c3d8f25a3ccd8adfd22eb5154ac81da946aef9f" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "tachys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a3bbcf8e3b52cad5f0aa860837d4d1796c7c4873b083c9520a1bbba4747973" dependencies = [ "any_spawner", + "async-trait", "const_str_slice_concat", "drain_filter_polyfill", + "dyn-clone", "either_of", "futures", "html-escape", "indexmap", - "itertools", + "itertools 0.13.0", "js-sys", "linear-map", "next_tuple", @@ -2506,6 +3159,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "thiserror" version = "1.0.69" @@ -2565,6 +3224,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -2617,9 +3287,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" dependencies = [ "serde", "serde_spanned", @@ -2638,9 +3308,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" dependencies = [ "indexmap", "serde", @@ -2682,7 +3352,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ - "bitflags", + "bitflags 2.8.0", "bytes", "futures-util", "http 1.2.0", @@ -2707,7 +3377,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" dependencies = [ - "bitflags", + "bitflags 2.8.0", "bytes", "futures-util", "http 1.2.0", @@ -2848,9 +3518,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" [[package]] name = "unicode-segmentation" @@ -2901,11 +3571,25 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.12.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" +checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0" dependencies = [ - "getrandom", + "getrandom 0.3.1", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", ] [[package]] @@ -2914,6 +3598,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + [[package]] name = "version_check" version = "0.9.5" @@ -2937,21 +3627,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "wasm-bindgen" -version = "0.2.99" +name = "wasi" +version = "0.13.3+wasi-0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", @@ -2963,9 +3663,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.49" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", @@ -2976,9 +3676,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2986,9 +3686,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -2999,9 +3699,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "wasm-streams" @@ -3018,14 +3721,20 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.76" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", ] +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + [[package]] name = "winapi" version = "0.3.9" @@ -3057,6 +3766,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -3141,13 +3859,22 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.24" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" +checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f" dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.8.0", +] + [[package]] name = "write16" version = "1.0.0" @@ -3209,7 +3936,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa91407dacce3a68c56de03abe2760159582b846c6a4acd2f456618087f12713" +dependencies = [ + "zerocopy-derive 0.8.17", ] [[package]] @@ -3223,6 +3959,17 @@ dependencies = [ "syn", ] +[[package]] +name = "zerocopy-derive" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06718a168365cad3d5ff0bb133aad346959a2074bd4a85c121255a11304a8626" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.5" @@ -3265,3 +4012,27 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" +dependencies = [ + "zune-core", +] diff --git a/crates/ascend/Cargo.toml b/crates/ascend/Cargo.toml index 974d558..a311d66 100644 --- a/crates/ascend/Cargo.toml +++ b/crates/ascend/Cargo.toml @@ -9,43 +9,59 @@ publish = false crate-type = ["cdylib", "rlib"] [dependencies] -moonboard-parser = { workspace = true, optional = true } axum = { version = "0.7", optional = true } +camino = { version = "1.1", optional = true } +chrono = { version = "0.4.39", features = ["now", "serde"] } +clap = { version = "4.5.7", features = ["derive"] } +confik = { version = "0.12", optional = true, features = ["camino"] } console_error_panic_hook = "0.1" +derive_more = { version = "1", features = [ + "display", + "error", + "from", + "from_str", +] } +http = "1" +image = { version = "0.25", optional = true } leptos = { version = "0.7.4", features = ["tracing"] } -server_fn = { version = "0.7.4", features = ["cbor"] } leptos_axum = { version = "0.7", optional = true } leptos_meta = { version = "0.7" } leptos_router = { version = "0.7.0" } +moonboard-parser = { workspace = true, optional = true } +rand = { version = "0.9", default-features = false, features = ["thread_rng"] } +ron = { version = "0.8" } +serde = { version = "1", features = ["derive"] } +server_fn = { version = "0.7.4", features = ["cbor"] } +smart-default = "0.7.1" tokio = { version = "1", features = ["rt-multi-thread"], optional = true } tower = { version = "0.4", optional = true } tower-http = { version = "0.5", features = ["fs"], optional = true } -wasm-bindgen = "=0.2.99" -http = "1" -serde = { version = "1", features = ["derive"] } -derive_more = { version = "1", features = ["display", "error", "from"] } -clap = { version = "4.5.7", features = ["derive"] } -camino = { version = "1.1", optional = true } -type-toppings = { version = "0.2.1", features = ["result"] } tracing = { version = "0.1" } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } tracing-subscriber-wasm = "0.1.0" -ron = { version = "0.8" } -rand = { version = "0.8", optional = true } +type-toppings = { version = "0.2.1", features = ["result", "iterator"] } +wasm-bindgen = "=0.2.100" web-sys = { version = "0.3.76", features = ["File", "FileList"] } -smart-default = "0.7.1" -confik = { version = "0.12", optional = true, features = ["camino"] } xdg = { version = "2.5", optional = true } +uuid = { version = "1.12", features = ["serde", "v4"] } +redb = { version = "2.4", optional = true } +bincode = { version = "1.3", optional = true } +serde_json = { version = "1" } +codee = { version = "0.3" } +error_reporter = { version = "1" } +getrandom = { version = "0.3.1" } [dev-dependencies.serde_json] version = "1" [features] -hydrate = ["leptos/hydrate"] +hydrate = ["leptos/hydrate", "getrandom/wasm_js", "uuid/js"] ssr = [ "dep:axum", + "dep:redb", + "dep:image", + "dep:bincode", "dep:tokio", - "dep:rand", "dep:tower", "dep:tower-http", "dep:leptos_axum", diff --git a/crates/ascend/src/app.rs b/crates/ascend/src/app.rs index 298eed8..8efd20c 100644 --- a/crates/ascend/src/app.rs +++ b/crates/ascend/src/app.rs @@ -1,3 +1,4 @@ +use crate::codec::ron::Ron; use crate::pages; use leptos::prelude::*; use leptos_router::components::*; @@ -24,7 +25,7 @@ pub fn shell(options: LeptosOptions) -> impl IntoView { } #[component] -pub fn App() -> impl leptos::IntoView { +pub fn App() -> impl IntoView { use leptos_meta::Stylesheet; use leptos_meta::Title; @@ -39,13 +40,48 @@ pub fn App() -> impl leptos::IntoView { <Router> - <main> - <Routes fallback=|| "Not found"> - <Route path=path!("/") view=pages::wall::Wall /> - <Route path=path!("/wall/edit") view=pages::edit_wall::EditWall /> - <Route path=path!("/wall/routes") view=pages::routes::Routes /> - </Routes> - </main> + <Routes fallback=|| "Not found"> + <Route path=path!("/") view=Home /> + <Route path=path!("/wall/:wall_uid") view=pages::wall::Wall /> + <Route path=path!("/wall/:wall_uid/edit") view=pages::edit_wall::EditWall /> + <Route path=path!("/wall/:wall_uid/routes") view=pages::routes::Routes /> + </Routes> </Router> } } + +#[component] +pub fn Home() -> impl IntoView { + // TODO: show cards with walls, and a "new wall" button + + tracing::debug!("Rendering home component"); + + // dbg!(leptos::prelude::Owner::current().map(|o| o.ancestry())); + + let wall_uid = OnceResource::<_, Ron>::new_with_options( + async move { + // dbg!(leptos::prelude::Owner::current().map(|o| o.ancestry())); + let walls = crate::server_functions::get_walls() + .await + .inspect_err(|e| { + dbg!(e); + }) + .expect("failed to get walls") + .into_inner(); + walls.first().map(|wall| wall.uid) + }, + false, + ); + + Effect::new(move || { + tracing::debug!("running effect"); + + if let Some(wall_uid) = wall_uid.get().flatten() { + tracing::debug!("navigating"); + let navigate = leptos_router::hooks::use_navigate(); + let url = format!("/wall/{}", wall_uid); + navigate(&url, Default::default()); + tracing::debug!("navigated"); + } + }); +} diff --git a/crates/ascend/src/codec.rs b/crates/ascend/src/codec.rs index 3f6aed7..2d82e93 100644 --- a/crates/ascend/src/codec.rs +++ b/crates/ascend/src/codec.rs @@ -1,59 +1,129 @@ pub mod ron { //! Wrap T in RonCodec<T> that when serialized, always serializes to a [ron] string. - #[derive(Debug, Clone)] - pub struct RonCodec<T> { - t: T, + use codee::Decoder; + use codee::Encoder; + use serde::Deserialize; + use serde::Serialize; + use serde::de::DeserializeOwned; + use server_fn::ServerFnError; + use server_fn::codec::Encoding; + use server_fn::codec::FromReq; + use server_fn::codec::FromRes; + use server_fn::codec::IntoReq; + use server_fn::codec::IntoRes; + use server_fn::request::ClientReq; + use server_fn::request::Req; + use server_fn::response::ClientRes; + use server_fn::response::Res; + + pub struct Ron; + + impl<T> Encoder<T> for Ron + where + T: Serialize, + { + type Encoded = String; + type Error = ron::Error; + + fn encode(val: &T) -> Result<Self::Encoded, Self::Error> { + ron::to_string(val) + } } - impl<T> RonCodec<T> { + impl<T> Decoder<T> for Ron + where + for<'de> T: Deserialize<'de>, + { + type Encoded = str; + type Error = ron::error::SpannedError; + + fn decode(val: &Self::Encoded) -> Result<T, Self::Error> { + ron::from_str(val) + } + } + + impl Encoding for Ron { + const CONTENT_TYPE: &'static str = "application/ron"; + const METHOD: http::Method = http::Method::POST; + } + + #[derive(Debug, Clone)] + pub struct RonEncoded<T>(pub T); + + impl<T> RonEncoded<T> { pub fn into_inner(self) -> T { - self.t + self.0 } pub fn new(t: T) -> Self { - Self { t } + Self(t) } } - impl<T> std::ops::Deref for RonCodec<T> { + impl<T> std::ops::Deref for RonEncoded<T> { type Target = T; fn deref(&self) -> &Self::Target { - &self.t + &self.0 } } - impl<T> serde::Serialize for RonCodec<T> + // IntoReq + impl<T, Request, Err> IntoReq<Ron, Request, Err> for RonEncoded<T> where - T: serde::Serialize, + Request: ClientReq<Err>, + T: Serialize, { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: serde::Serializer, - { - let serialized = ron::to_string(&self.t).map_err(serde::ser::Error::custom)?; - serializer.serialize_str(&serialized) + fn into_req(self, path: &str, accepts: &str) -> Result<Request, ServerFnError<Err>> { + let data = Ron::encode(&self.0).map_err(|e| ServerFnError::Serialization(e.to_string()))?; + Request::try_new_post(path, Ron::CONTENT_TYPE, accepts, data) } } - impl<'de, T> serde::Deserialize<'de> for RonCodec<T> + // FromReq + impl<T, Request, Err> FromReq<Ron, Request, Err> for RonEncoded<T> where - T: serde::de::DeserializeOwned + 'static, + Request: Req<Err> + Send, + T: DeserializeOwned, { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: serde::Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - let t: T = ron::from_str(&s).map_err(serde::de::Error::custom)?; - Ok(Self { t }) + async fn from_req(req: Request) -> Result<Self, ServerFnError<Err>> { + let data = req.try_into_string().await?; + Ron::decode(&data).map(RonEncoded).map_err(|e| ServerFnError::Args(e.to_string())) + } + } + + // IntoRes + impl<CustErr, T, Response> IntoRes<Ron, Response, CustErr> for RonEncoded<T> + where + Response: Res<CustErr>, + T: Serialize + Send, + { + async fn into_res(self) -> Result<Response, ServerFnError<CustErr>> { + let data = Ron::encode(&self.0).map_err(|e| ServerFnError::Serialization(e.to_string()))?; + Response::try_from_string(Ron::CONTENT_TYPE, data) + } + } + + // FromRes + impl<T, Response, Err> FromRes<Ron, Response, Err> for RonEncoded<T> + where + Response: ClientRes<Err> + Send, + T: DeserializeOwned, + { + async fn from_res(res: Response) -> Result<Self, ServerFnError<Err>> { + let data = res.try_into_string().await?; + Ron::decode(&data) + .map(RonEncoded) + .map_err(|e| ServerFnError::Deserialization(e.to_string())) } } #[cfg(test)] mod tests { - use super::RonCodec; + use super::Ron; + use codee::Decoder; + use codee::Encoder; use serde::Deserialize; use serde::Serialize; @@ -69,19 +139,9 @@ pub mod ron { name: "Test".to_string(), value: 42, }; - - // Wrap in RonCodec - let wrapped = RonCodec::new(original.clone()); - - // Serialize - let serialized = serde_json::to_string(&wrapped).expect("Serialization failed"); - println!("Serialized: {}", serialized); - - // Deserialize - let deserialized: RonCodec<TestStruct> = serde_json::from_str(&serialized).expect("Deserialization failed"); - - // Compare - assert_eq!(deserialized.into_inner(), original); + let enc = Ron::encode(&original).unwrap(); + let dec: TestStruct = Ron::decode(&enc).unwrap(); + assert_eq!(dec, original); } } } diff --git a/crates/ascend/src/components/button.rs b/crates/ascend/src/components/button.rs index 9d2accb..ee2dd67 100644 --- a/crates/ascend/src/components/button.rs +++ b/crates/ascend/src/components/button.rs @@ -2,8 +2,14 @@ use leptos::prelude::*; use web_sys::MouseEvent; #[component] -pub fn Button(#[prop(into)] text: String, onclick: impl Fn(MouseEvent) -> () + 'static) -> impl IntoView { +pub fn Button(#[prop(into)] text: String, onclick: impl Fn(MouseEvent) + 'static) -> impl IntoView { view! { - <button on:click=onclick type="button" class="text-black bg-orange-300 hover:bg-orange-400 focus:ring-4 focus:ring-orange-500 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 focus:outline-none">{ text }</button> + <button + on:click=onclick + type="button" + class="text-black bg-orange-300 hover:bg-orange-400 focus:ring-4 focus:ring-orange-500 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 focus:outline-none" + > + {text} + </button> } } diff --git a/crates/ascend/src/components/header.rs b/crates/ascend/src/components/header.rs index 8db55aa..7ef2d58 100644 --- a/crates/ascend/src/components/header.rs +++ b/crates/ascend/src/components/header.rs @@ -1,11 +1,13 @@ use leptos::prelude::*; +#[derive(Debug, Clone)] pub struct HeaderItems { pub left: Vec<HeaderItem>, pub middle: Vec<HeaderItem>, pub right: Vec<HeaderItem>, } +#[derive(Debug, Clone)] pub struct HeaderItem { pub text: String, pub link: Option<String>, @@ -13,91 +15,46 @@ pub struct HeaderItem { /// Header with background color etc. #[component] -pub fn StyledHeader(items: HeaderItems) -> impl IntoView { - let fancy = false; - - if fancy { - view! { - <div class="flex"> - // Left gradient chunk - <div class="flex-grow"> - <div class="h-2/5" style="background: #eaac53" /> - <div - class="h-3/5" - style="background: linear-gradient(to bottom left, #eaac53 49.5%, rgb(15 23 42) 50.5%)" - /> - </div> - - <div class="flex-none container mx-auto text-black" style="background: #eaac53"> - <Header items /> - </div> - - // Right gradient chunk - <div class="flex-grow" style="background: #eaac53" /> - </div> - <div class="flex"> - // Left gradient chunk - <div class="flex-grow" /> - - <div class="flex-none container mx-auto"> - // Background color gradient - <div - class="h-6" - style="background: linear-gradient(to bottom left, #eaac53 49.5%, rgb(15 23 42) 50.5%)" - /> - </div> - - // Right gradient chunk - <div class="flex-grow"> - <div class="h-4/5" style="background: #eaac53" /> - <div - class="h-1/5" - style="background: linear-gradient(to bottom right, #eaac53 49.5%, rgb(15 23 42) 50.5%)" - /> - </div> - </div> - } - .into_any() - } else { - view! { - <div class="bg-orange-300 text-black border-b-2 border-b-orange-400"> - <div class="container mx-auto" > - <Header items /> - </div> - </div> - } - .into_any() +pub fn StyledHeader(#[prop(into)] items: Signal<HeaderItems>) -> impl IntoView { + view! { + <div class="bg-orange-300 text-black border-b-2 border-b-orange-400"> + // <div class="container mx-auto" > + <Header items /> + // </div> + </div> } } /// Function header without styling #[component] -pub fn Header(items: HeaderItems) -> impl IntoView { - let HeaderItems { left, middle, right } = items; +pub fn Header(#[prop(into)] items: Signal<HeaderItems>) -> impl IntoView { + let left = move || items.read().left.clone(); + let middle = move || items.read().middle.clone(); + let right = move || items.read().right.clone(); view! { <div class="grid grid-cols-[1fr_3fr_1fr] text-xl font-semibold p-4"> // Left side of header <div class="justify-self-start"> - <Items items=left /> + <Items items=Signal::derive(left) /> </div> // Expanding space in the middle <div class="justify-self-center font-semibold"> - <Items items=middle /> + <Items items=Signal::derive(middle) /> </div> // Right side of header <div class="justify-self-end"> - <Items items=right /> + <Items items=Signal::derive(right) /> </div> </div> } } #[component] -fn Items(items: Vec<HeaderItem>) -> impl IntoView { - let items = items.into_iter().map(|item| view! { <Item item /> }).collect_view(); +fn Items(#[prop(into)] items: Signal<Vec<HeaderItem>>) -> impl IntoView { + let items = move || items.get().into_iter().map(|item| view! { <Item item /> }).collect_view(); view! { <div class="flex gap-4">{items}</div> } } diff --git a/crates/ascend/src/components/problem.rs b/crates/ascend/src/components/problem.rs new file mode 100644 index 0000000..dd462c6 --- /dev/null +++ b/crates/ascend/src/components/problem.rs @@ -0,0 +1,51 @@ +use crate::models::HoldRole; +use crate::models::{self}; +use leptos::prelude::*; + +#[component] +#[tracing::instrument(skip_all)] +pub fn Problem(#[prop(into)] dim: Signal<models::WallDimensions>, #[prop(into)] problem: Signal<models::Problem>) -> impl IntoView { + let holds = move || { + let mut holds = vec![]; + for row in 0..dim.get().rows { + for col in 0..dim.get().cols { + let hold_position = models::HoldPosition { row, col }; + let role = move || problem.get().holds.get(&hold_position).copied(); + let role = Signal::derive(role); + let hold = view! { <Hold role /> }; + holds.push(hold); + } + } + holds.into_iter().collect_view() + }; + + let grid_classes = move || format!("grid grid-rows-{} grid-cols-{} gap-3", dim.get().rows, dim.get().cols); + + view! { + <div class="grid grid-cols-[auto,1fr] gap-8"> + <div class=move || { grid_classes }>{holds}</div> + </div> + } +} + +#[component] +#[tracing::instrument(skip_all)] +fn Hold(#[prop(into)] role: Signal<Option<HoldRole>>) -> impl IntoView { + let class = move || { + let role_classes = match role.get() { + Some(HoldRole::Start) => Some("outline outline-offset-2 outline-green-500"), + Some(HoldRole::Normal) => Some("outline outline-offset-2 outline-blue-500"), + Some(HoldRole::Zone) => Some("outline outline-offset-2 outline-amber-500"), + Some(HoldRole::End) => Some("outline outline-offset-2 outline-red-500"), + None => Some("brightness-50"), + }; + let mut s = "min-w-2 bg-sky-100 aspect-square rounded".to_string(); + if let Some(c) = role_classes { + s.push(' '); + s.push_str(c); + } + s + }; + + view! { <div class=class /> } +} diff --git a/crates/ascend/src/lib.rs b/crates/ascend/src/lib.rs index 8ea317e..857e4ea 100644 --- a/crates/ascend/src/lib.rs +++ b/crates/ascend/src/lib.rs @@ -5,17 +5,60 @@ pub mod pages { pub mod wall; } pub mod components { + pub use button::Button; + pub use header::StyledHeader; + pub use problem::Problem; + pub mod button; pub mod header; + pub mod problem; } +pub mod resources { + use crate::codec::ron::Ron; + use crate::codec::ron::RonEncoded; + use crate::models::{self}; + use leptos::prelude::Get; + use leptos::prelude::Signal; + use leptos::server::Resource; + use server_fn::ServerFnError; + type RonResource<T> = Resource<Result<T, ServerFnError>, Ron>; + + pub fn wall_by_uid(wall_uid: Signal<models::WallUid>) -> RonResource<models::Wall> { + Resource::new_with_options( + move || wall_uid.get(), + move |wall_uid| async move { crate::server_functions::get_wall(wall_uid).await.map(RonEncoded::into_inner) }, + false, + ) + } + + pub fn problem_by_uid(wall_uid: Signal<models::WallUid>, problem_uid: Signal<models::ProblemUid>) -> RonResource<models::Problem> { + Resource::new_with_options( + move || (wall_uid.get(), problem_uid.get()), + move |(wall_uid, problem_uid)| async move { + crate::server_functions::get_problem(wall_uid, problem_uid) + .await + .map(RonEncoded::into_inner) + }, + false, + ) + } + + pub fn problems_for_wall(wall_uid: Signal<models::WallUid>) -> RonResource<Vec<models::Problem>> { + Resource::new_with_options( + move || wall_uid.get(), + move |wall_uid| async move { crate::server_functions::get_problems_for_wall(wall_uid).await.map(RonEncoded::into_inner) }, + false, + ) + } +} pub mod codec; +pub mod models; +pub mod server_functions; #[cfg(feature = "ssr")] pub mod server; -pub mod models; - #[cfg(feature = "hydrate")] #[wasm_bindgen::prelude::wasm_bindgen] pub fn hydrate() { @@ -24,6 +67,11 @@ pub fn hydrate() { console_error_panic_hook::set_once(); tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::builder() + .with_default_directive(tracing::level_filters::LevelFilter::DEBUG.into()) + .from_env_lossy(), + ) .with_writer( // To avoide trace events in the browser from showing their JS backtrace tracing_subscriber_wasm::MakeConsoleWriter::default().map_trace_level_to(tracing::Level::DEBUG), diff --git a/crates/ascend/src/models.rs b/crates/ascend/src/models.rs index 9cfbeba..1ef6591 100644 --- a/crates/ascend/src/models.rs +++ b/crates/ascend/src/models.rs @@ -1,71 +1,218 @@ //! Shared models between server and client code. -use serde::Deserialize; -use serde::Serialize; -use std::collections::BTreeMap; +pub use v1::HoldPosition; +pub use v1::HoldRole; +pub use v2::Hold; +pub use v2::Image; +pub use v2::ImageFilename; +pub use v2::ImageResolution; +pub use v2::ImageUid; +pub use v2::Method; +pub use v2::Problem; +pub use v2::ProblemUid; +pub use v2::Root; +pub use v2::Wall; +pub use v2::WallDimensions; +pub use v2::WallUid; -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct Wall { - pub rows: u64, - pub cols: u64, - pub holds: BTreeMap<HoldPosition, Hold>, -} -impl Wall { - pub fn new(rows: u64, cols: u64) -> Self { - let mut holds = BTreeMap::new(); - for row in 0..rows { - for col in 0..cols { - let position = HoldPosition { row, col }; - let hold = Hold { position, image: None }; - holds.insert(position, hold); - } +pub mod v2 { + use super::v1; + use serde::Deserialize; + use serde::Serialize; + use std::collections::BTreeMap; + use std::collections::BTreeSet; + + #[derive(Serialize, Deserialize, Debug)] + pub struct Root { + pub walls: BTreeSet<WallUid>, + } + + #[derive(Serialize, Deserialize, Debug, Clone)] + pub struct Wall { + pub uid: WallUid, + + // TODO: Replace by walldimensions + pub rows: u64, + pub cols: u64, + + pub holds: BTreeMap<v1::HoldPosition, Hold>, + pub problems: BTreeSet<ProblemUid>, + } + impl Wall { + pub fn random_problem(&self) -> Option<ProblemUid> { + use rand::seq::IteratorRandom; + let mut rng = rand::rng(); + self.problems.iter().choose(&mut rng).copied() } - Self { rows, cols, holds } } -} -impl Default for Wall { - fn default() -> Self { - Self::new(12, 12) + + #[derive(Debug, Copy, Clone, PartialEq, Eq)] + pub struct WallDimensions { + pub rows: u64, + pub cols: u64, + } + + #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, derive_more::FromStr, derive_more::Display)] + pub struct WallUid(pub uuid::Uuid); + impl WallUid { + pub fn create() -> Self { + Self(uuid::Uuid::new_v4()) + } + } + + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] + pub struct Problem { + pub uid: ProblemUid, + pub name: String, + pub set_by: String, + pub holds: BTreeMap<v1::HoldPosition, v1::HoldRole>, + pub method: Method, + pub date_added: chrono::DateTime<chrono::Utc>, + } + + #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, derive_more::FromStr, derive_more::Display)] + pub struct ProblemUid(pub uuid::Uuid); + impl ProblemUid { + pub fn create() -> Self { + Self(uuid::Uuid::new_v4()) + } + } + + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] + pub enum Method { + FeetFollowHands, + Footless, + FootlessPlusKickboard, + } + + #[derive(Serialize, Deserialize, Clone, Debug)] + pub struct Hold { + pub position: v1::HoldPosition, + pub image: Option<Image>, + } + + #[derive(Serialize, Deserialize, Clone, Debug)] + pub struct Image { + pub uid: ImageUid, + pub resolutions: BTreeMap<ImageResolution, ImageFilename>, + } + impl Image { + pub(crate) fn srcset(&self) -> String { + self.resolutions + .iter() + .map(|(res, filename)| format!("/files/holds/{} {}w", filename.filename, res.width)) + .collect::<Vec<String>>() + .join(", ") + } + } + + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] + pub struct ImageResolution { + pub width: u64, + pub height: u64, + } + + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] + pub struct ImageFilename { + pub filename: String, + } + + #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, derive_more::FromStr, derive_more::Display)] + pub struct ImageUid(pub uuid::Uuid); + impl ImageUid { + pub fn create() -> Self { + Self(uuid::Uuid::new_v4()) + } } } -#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Copy)] -pub struct HoldPosition { - /// Starting from 0 - pub row: u64, +pub mod v1 { + use serde::Deserialize; + use serde::Serialize; + use smart_default::SmartDefault; + use std::collections::BTreeMap; + use std::collections::BTreeSet; - /// Starting from 0 - pub col: u64, -} - -#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct Problem { - pub holds: BTreeMap<HoldPosition, HoldRole>, -} - -/// The role of a hold on a route -#[derive(Serialize, Deserialize, Clone, Debug, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum HoldRole { - /// Start hold - Start, - - /// Any hold on the route without a specific role - Normal, - - /// Zone hold - Zone, - - /// End hold - End, -} - -#[derive(Serialize, Deserialize, Clone, Debug, Default)] -pub struct Hold { - pub position: HoldPosition, - pub image: Option<Image>, -} - -#[derive(Serialize, Deserialize, Clone, Debug, Default)] -pub struct Image { - pub filename: String, + const STATE_VERSION: u64 = 1; + + #[derive(Serialize, Deserialize, Clone, Debug, SmartDefault)] + pub struct PersistentState { + /// State schema version + #[default(STATE_VERSION)] + pub version: u64, + + pub wall: Wall, + pub problems: Problems, + } + + #[derive(Serialize, Deserialize, Clone, Debug, Default)] + pub struct Problems { + pub problems: BTreeSet<Problem>, + } + + #[derive(Serialize, Deserialize, Clone, Debug)] + pub struct Wall { + pub rows: u64, + pub cols: u64, + pub holds: BTreeMap<HoldPosition, Hold>, + } + impl Wall { + pub fn new(rows: u64, cols: u64) -> Self { + let mut holds = BTreeMap::new(); + for row in 0..rows { + for col in 0..cols { + let position = HoldPosition { row, col }; + let hold = Hold { position, image: None }; + holds.insert(position, hold); + } + } + Self { rows, cols, holds } + } + } + impl Default for Wall { + fn default() -> Self { + Self::new(12, 12) + } + } + + #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Copy)] + pub struct HoldPosition { + /// Starting from 0 + pub row: u64, + + /// Starting from 0 + pub col: u64, + } + + #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] + pub struct Problem { + pub holds: BTreeMap<HoldPosition, HoldRole>, + } + + /// The role of a hold on a route + #[derive(Serialize, Deserialize, Clone, Debug, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] + pub enum HoldRole { + /// Start hold + Start, + + /// Any hold on the route without a specific role + Normal, + + /// Zone hold + Zone, + + /// End hold + End, + } + + #[derive(Serialize, Deserialize, Clone, Debug)] + pub struct Hold { + pub position: HoldPosition, + pub image: Option<Image>, + } + + #[derive(Serialize, Deserialize, Clone, Debug)] + pub struct Image { + pub filename: String, + } } diff --git a/crates/ascend/src/pages/edit_wall.rs b/crates/ascend/src/pages/edit_wall.rs index acdc20d..dd859a2 100644 --- a/crates/ascend/src/pages/edit_wall.rs +++ b/crates/ascend/src/pages/edit_wall.rs @@ -1,35 +1,48 @@ -use crate::codec::ron::RonCodec; +use crate::codec::ron::Ron; +use crate::codec::ron::RonEncoded; +use crate::components::StyledHeader; use crate::components::header::HeaderItem; use crate::components::header::HeaderItems; -use crate::components::header::StyledHeader; use crate::models; use crate::models::HoldPosition; -use crate::models::Wall; +use crate::models::WallUid; +use leptos::Params; use leptos::ev::Event; use leptos::html::Input; use leptos::prelude::*; +use leptos_router::params::Params; use serde::Deserialize; use serde::Serialize; use server_fn::codec::Cbor; -use std::ops::Deref; use wasm_bindgen::JsCast; use wasm_bindgen::prelude::*; use web_sys::FileList; -#[component] -pub fn EditWall() -> impl leptos::IntoView { - let load = async move { - // TODO: What to do about this unwrap? - load_initial_data().await.unwrap() - }; +#[derive(Params, PartialEq, Clone)] +struct RouteParams { + wall_uid: Option<models::WallUid>, +} - let header_items = HeaderItems { +#[component] +pub fn EditWall() -> impl IntoView { + let params = leptos_router::hooks::use_params::<RouteParams>(); + let wall_uid = Signal::derive(move || { + params + .get() + .expect("gets wall_uid from URL") + .wall_uid + .expect("wall_uid param is never None") + }); + + let wall = crate::resources::wall_by_uid(wall_uid); + + let header_items = move || HeaderItems { left: vec![HeaderItem { text: "← Ascend".to_string(), - link: Some("/".to_string()), + link: Some(format!("/wall/{}", wall_uid.get())), }], middle: vec![HeaderItem { - text: "EDIT WALL".to_string(), + text: "HOLDS".to_string(), link: None, }], right: vec![], @@ -37,27 +50,41 @@ pub fn EditWall() -> impl leptos::IntoView { leptos::view! { <div class="min-w-screen min-h-screen bg-slate-900"> - <StyledHeader items=header_items /> + <StyledHeader items=Signal::derive(header_items) /> <div class="container mx-auto mt-2"> - <Await future=load let:data> - <Ready data=data.deref().to_owned() /> - </Await> + <Suspense fallback=move || { + view! { <p>"Loading..."</p> } + }> + {move || Suspend::new(async move { + let wall = wall.await; + view! { + <ErrorBoundary fallback=|_errors| { + "error" + }> + {move || -> Result<_, ServerFnError> { + let wall = wall.clone()?; + Ok(view! { <Ready wall /> }) + }} + </ErrorBoundary> + } + })} + </Suspense> </div> </div> } } #[component] -fn Ready(data: InitialData) -> impl leptos::IntoView { +fn Ready(wall: models::Wall) -> impl IntoView { tracing::debug!("ready"); let mut holds = vec![]; - for hold in data.wall.holds.values().cloned() { - holds.push(view! { <Hold hold /> }); + for hold in wall.holds.values().cloned() { + holds.push(view! { <Hold wall_uid=wall.uid hold /> }); } - let grid_classes = format!("grid grid-rows-{} grid-cols-{} gap-3", data.wall.rows, data.wall.cols); + let grid_classes = format!("grid grid-rows-{} grid-cols-{} gap-3", wall.rows, wall.cols); view! { <div> @@ -68,7 +95,7 @@ fn Ready(data: InitialData) -> impl leptos::IntoView { } #[component] -fn Hold(hold: models::Hold) -> impl leptos::IntoView { +fn Hold(wall_uid: models::WallUid, hold: models::Hold) -> impl IntoView { let hold_position = hold.position; let file_input_ref = NodeRef::<Input>::new(); @@ -82,7 +109,7 @@ fn Hold(hold: models::Hold) -> impl leptos::IntoView { let hold = Signal::derive(move || { let refreshed = upload.value().get().map(Result::unwrap); - refreshed.unwrap_or(hold.clone()) + refreshed.map(RonEncoded::into_inner).unwrap_or(hold.clone()) }); // Callback to handle file selection @@ -108,7 +135,11 @@ fn Hold(hold: models::Hold) -> impl leptos::IntoView { file_contents, }; - upload.dispatch(SetImage { hold_position, image }); + upload.dispatch(SetImage { + wall_uid, + hold_position, + image, + }); }) as Box<dyn FnMut(_)>); file_reader.set_onload(Some(on_load.as_ref().unchecked_ref())); @@ -117,8 +148,8 @@ fn Hold(hold: models::Hold) -> impl leptos::IntoView { let img = move || { hold.read().image.as_ref().map(|img| { - let src = format!("/files/holds/{}", img.filename); - view! { <img class="object-cover w-full h-full" src=src /> } + let srcset = img.srcset(); + view! { <img class="object-cover w-full h-full" srcset=srcset /> } }) }; @@ -138,51 +169,77 @@ fn Hold(hold: models::Hold) -> impl leptos::IntoView { } } -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct InitialData { - wall: Wall, -} - #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Image { file_name: String, file_contents: Vec<u8>, } -#[server] -async fn load_initial_data() -> Result<RonCodec<InitialData>, ServerFnError> { - use crate::server::state::State; +#[server( + name = SetImage, + input = Cbor, + output = Ron, +)] +#[tracing::instrument(skip(image), err)] +async fn set_image(wall_uid: WallUid, hold_position: HoldPosition, image: Image) -> Result<RonEncoded<models::Hold>, ServerFnError> { + use image::ImageDecoder; + use std::collections::BTreeMap; + use std::path::Path; - let state = expect_context::<State>(); - - let wall = state.persistent.with(|s| s.wall.clone()).await; - Ok(RonCodec::new(InitialData { wall })) -} - -#[server(name = SetImage, input = Cbor)] -#[tracing::instrument(skip(image))] -async fn set_image(hold_position: HoldPosition, image: Image) -> Result<models::Hold, ServerFnError> { tracing::info!("Setting image, {}, {} bytes", image.file_name, image.file_contents.len()); - use crate::server::state::State; + let db = expect_context::<crate::server::db::Database>(); - // TODO: Fix file extension presumption, and possibly use uuid - let filename = format!("row{}_col{}.jpg", hold_position.row, hold_position.col); - tokio::fs::create_dir_all("datastore/public/holds").await?; - tokio::fs::write(format!("datastore/public/holds/{filename}"), image.file_contents).await?; + let image = tokio::task::spawn_blocking(move || -> Result<models::Image, ServerFnError> { + let mut decoder = image::ImageReader::new(std::io::Cursor::new(image.file_contents)) + .with_guessed_format()? + .into_decoder()?; + let orientation = decoder.orientation()?; + let mut img = image::DynamicImage::from_decoder(decoder)?; + img.apply_orientation(orientation); - let state = expect_context::<State>(); - state - .persistent - .update(|s| { - if let Some(hold) = s.wall.holds.get_mut(&hold_position) { - hold.image = Some(models::Image { filename }); - } + let holds_dir = Path::new("datastore/public/holds"); + std::fs::create_dir_all(holds_dir)?; + + let targets = [(50, 50), (150, 150), (300, 300), (400, 400)]; + + let uid = models::ImageUid::create(); + let mut resolutions = BTreeMap::new(); + for (width, height) in targets { + let resized = img.resize_to_fill(width, height, image::imageops::FilterType::Lanczos3); + + let filename = format!("hold_row{}_col{}_{width}x{height}_{uid}.webp", hold_position.row, hold_position.col); + let path = holds_dir.join(&filename); + let mut file = std::fs::OpenOptions::new().write(true).append(false).create_new(true).open(&path)?; + resized.write_to(&mut file, image::ImageFormat::WebP)?; + + let res = models::ImageResolution { + width: width.into(), + height: height.into(), + }; + resolutions.insert(res, models::ImageFilename { filename }); + } + + // TODO: Clean up old image? + + Ok(models::Image { uid, resolutions }) + }) + .await??; + + let hold = db + .write(move |txn| { + use redb::ReadableTable; + let mut walls = txn.open_table(crate::server::db::current::TABLE_WALLS)?; + let mut wall = walls.get(wall_uid)?.expect("todo").value(); + + let hold = wall.holds.get_mut(&hold_position).expect("hold"); + hold.image = Some(image); + let hold = hold.clone(); + walls.insert(wall_uid, wall)?; + + Ok(hold) }) .await?; - // Return updated hold - let hold = state.persistent.with(|s| s.wall.holds.get(&hold_position).cloned().unwrap()).await; - - Ok(hold) + Ok(RonEncoded::new(hold)) } diff --git a/crates/ascend/src/pages/routes.rs b/crates/ascend/src/pages/routes.rs index 94208aa..4d8b4e7 100644 --- a/crates/ascend/src/pages/routes.rs +++ b/crates/ascend/src/pages/routes.rs @@ -1,18 +1,35 @@ -use crate::codec::ron::RonCodec; +use crate::components; use crate::components::header::HeaderItem; use crate::components::header::HeaderItems; use crate::components::header::StyledHeader; +use crate::models; +use crate::models::WallUid; +use leptos::Params; use leptos::prelude::*; -use serde::Deserialize; -use serde::Serialize; -use std::ops::Deref; +use leptos_router::params::Params; + +#[derive(Params, PartialEq, Clone)] +struct RouteParams { + // Is never None + wall_uid: Option<models::WallUid>, +} #[component] -pub fn Routes() -> impl leptos::IntoView { - let load = async move { - // TODO: What to do about this unwrap? - load_initial_data().await.unwrap() - }; +#[tracing::instrument(skip_all)] +pub fn Routes() -> impl IntoView { + tracing::debug!("Enter"); + + let params = leptos_router::hooks::use_params::<RouteParams>(); + let wall_uid = Signal::derive(move || { + params + .get() + .expect("gets wall_uid from URL") + .wall_uid + .expect("wall_uid param is never None") + }); + + let wall = crate::resources::wall_by_uid(wall_uid); + let problems = crate::resources::problems_for_wall(wall_uid); let header_items = HeaderItems { left: vec![HeaderItem { @@ -26,27 +43,70 @@ pub fn Routes() -> impl leptos::IntoView { right: vec![], }; - leptos::view! { + let suspend = move || { + Suspend::new(async move { + let wall = wall.await; + let problems = problems.await; + + let v = move || -> Result<_, ServerFnError> { + let wall = wall.clone()?; + let problems = problems.clone()?; + + let wall_dimensions = models::WallDimensions { + rows: wall.rows, + cols: wall.cols, + }; + let problems_sample = move || problems.iter().take(10).cloned().collect::<Vec<_>>(); + + Ok(view! { + <div> + <For + each=problems_sample + key=|problem| problem.uid + children=move |problem: models::Problem| { + view! { <Problem dim=wall_dimensions problem /> } + } + /> + </div> + }) + }; + + view! { <ErrorBoundary fallback=|_errors| "error">{v}</ErrorBoundary> } + }) + }; + + view! { <div class="min-w-screen min-h-screen bg-slate-900"> <StyledHeader items=header_items /> <div class="container mx-auto mt-2"> - <Await future=load let:data> - <Ready data=data.deref().to_owned() /> - </Await> + {move || view! { <Import wall_uid=wall_uid.get() /> }} + <Suspense fallback=|| view! { <p>"loading"</p> }>{suspend}</Suspense> </div> </div> } } #[component] -fn Ready(data: InitialData) -> impl leptos::IntoView { +#[tracing::instrument(skip_all)] +fn Problem(#[prop(into)] dim: Signal<models::WallDimensions>, #[prop(into)] problem: Signal<models::Problem>) -> impl IntoView { + tracing::debug!("Enter"); + + view! { + <components::Problem dim problem /> + <p>{move || problem.get().name.clone()}</p> + } +} + +#[component] +#[tracing::instrument(skip_all)] +fn Import(wall_uid: WallUid) -> impl IntoView { tracing::debug!("ready"); let import_from_mini_moonboard = Action::from(ServerAction::<ImportFromMiniMoonboard>::new()); let onclick = move |_mouse_event| { - import_from_mini_moonboard.dispatch(ImportFromMiniMoonboard {}); + import_from_mini_moonboard.dispatch(ImportFromMiniMoonboard { wall_uid }); }; view! { @@ -55,31 +115,18 @@ fn Ready(data: InitialData) -> impl leptos::IntoView { } } -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct InitialData {} - -#[server] -async fn load_initial_data() -> Result<RonCodec<InitialData>, ServerFnError> { - // use crate::server::state::State; - // let state = expect_context::<State>(); - - // TODO: provide info on current routes set - - Ok(RonCodec::new(InitialData {})) -} - #[server(name = ImportFromMiniMoonboard)] #[tracing::instrument] -async fn import_from_mini_moonboard() -> Result<(), ServerFnError> { +async fn import_from_mini_moonboard(wall_uid: WallUid) -> Result<(), ServerFnError> { use crate::server::config::Config; - use crate::server::state::State; + use crate::server::db::Database; tracing::info!("Importing mini moonboard problems"); let config = expect_context::<Config>(); - let state = expect_context::<State>(); + let db = expect_context::<Database>(); - crate::server::operations::import_mini_moonboard_problems(&config, &state).await?; + crate::server::operations::import_mini_moonboard_problems(&config, db, wall_uid).await?; // TODO: Return information about what was done Ok(()) diff --git a/crates/ascend/src/pages/wall.rs b/crates/ascend/src/pages/wall.rs index 3624947..5930e84 100644 --- a/crates/ascend/src/pages/wall.rs +++ b/crates/ascend/src/pages/wall.rs @@ -1,24 +1,62 @@ -use crate::codec::ron::RonCodec; +use crate::codec::ron::RonEncoded; use crate::components::button::Button; use crate::components::header::HeaderItem; use crate::components::header::HeaderItems; use crate::components::header::StyledHeader; use crate::models; use crate::models::HoldRole; +use leptos::Params; use leptos::prelude::*; -use leptos::reactive::graph::ReactiveNode; -use serde::Deserialize; -use serde::Serialize; -use std::ops::Deref; +use leptos_router::params::Params; + +#[derive(Params, PartialEq, Clone)] +struct RouteParams { + wall_uid: Option<models::WallUid>, +} #[component] -pub fn Wall() -> impl leptos::IntoView { - let load = async move { - // TODO: What to do about this unwrap? - load_initial_data().await.unwrap() - }; +#[tracing::instrument(skip_all)] +pub fn Wall() -> impl IntoView { + tracing::debug!("Enter"); - let header_items = HeaderItems { + let params = leptos_router::hooks::use_params::<RouteParams>(); + let wall_uid = Signal::derive(move || { + params + .get() + .expect("gets wall_uid from URL") + .wall_uid + .expect("wall_uid param is never None") + }); + + let wall = crate::resources::wall_by_uid(wall_uid); + + let problem_action = Action::new(move |&(wall_uid, problem_uid): &(models::WallUid, models::ProblemUid)| async move { + tracing::info!("fetching"); + crate::server_functions::get_problem(wall_uid, problem_uid) + .await + .map(RonEncoded::into_inner) + }); + let problem_signal = Signal::derive(move || { + let v = problem_action.value().read_only().get(); + tracing::debug!("val: {:?}", v); + v.and_then(Result::ok) + }); + + Effect::new(move |_prev_value| { + problem_action.value().write_only().set(None); + match wall.get() { + Some(Ok(wall)) => { + if let Some(problem_uid) = wall.random_problem() { + tracing::debug!("dispatching from effect"); + problem_action.dispatch((wall.uid, problem_uid)); + } + } + Some(Err(_err)) => {} + None => {} + } + }); + + let header_items = move || HeaderItems { left: vec![], middle: vec![HeaderItem { text: "ASCEND".to_string(), @@ -27,67 +65,87 @@ pub fn Wall() -> impl leptos::IntoView { right: vec![ HeaderItem { text: "Routes".to_string(), - link: Some("/wall/routes".to_string()), + link: Some(format!("/wall/{}/routes", wall_uid.get())), }, HeaderItem { text: "Holds".to_string(), - link: Some("/wall/edit".to_string()), + link: Some(format!("/wall/{}/edit", wall_uid.get())), }, ], }; leptos::view! { <div class="min-w-screen min-h-screen bg-slate-900"> - <StyledHeader items=header_items /> + <StyledHeader items=Signal::derive(header_items) /> <div class="m-2"> - <Await future=load let:data> - <Ready data=data.deref().to_owned() /> - </Await> + <Suspense fallback=move || { + view! { <p>"Loading..."</p> } + }> + {move || Suspend::new(async move { + tracing::info!("executing Suspend future"); + let wall = wall.await?; + let v = view! { + <div class="grid grid-cols-[auto,1fr] gap-8"> + <Grid wall=wall.clone() problem=problem_signal /> + + <div> + <div> + <p> + {move || problem_signal.get().map(|p| p.name.clone())} + </p> + <p> + {move || problem_signal.get().map(|p| p.set_by.clone())} + </p> + </div> + + <Button + onclick=move |_| { + if let Some(problem_uid) = wall.random_problem() { + tracing::info!("dispatching from button click handler"); + problem_action.dispatch((wall.uid, problem_uid)); + } + } + text="➤ Next problem" + /> + </div> + </div> + }; + Ok::<_, ServerFnError>(v) + })} + </Suspense> </div> </div> } } #[component] -fn Ready(data: InitialData) -> impl leptos::IntoView { - tracing::debug!("ready"); - - let (current_problem, current_problem_writer) = signal(None::<models::Problem>); - let problem_fetcher = LocalResource::new(move || async move { - tracing::info!("Loading random problem"); - let problem = get_random_problem().await.expect("cannot get random problem"); - if problem.is_none() { - tracing::info!("No problem returned by server in response to request for random problem"); - } - current_problem_writer.set(problem.into_inner()); - }); +#[tracing::instrument(skip_all)] +fn Grid(wall: models::Wall, problem: Signal<Option<models::Problem>>) -> impl IntoView { + tracing::debug!("Enter"); let mut cells = vec![]; - for (&hold_position, hold) in &data.wall.holds { - let role = move || current_problem.get().and_then(|problem| problem.holds.get(&hold_position).copied()); + for (&hold_position, hold) in &wall.holds { + let role = move || problem.get().and_then(|p| p.holds.get(&hold_position).copied()); let role = Signal::derive(role); - let cell = view! { <Hold role hold=hold.clone() /> }; cells.push(cell); } - - let grid_classes = format!("grid grid-rows-{} grid-cols-{} gap-3", data.wall.rows, data.wall.cols); + let grid_classes = format!("grid grid-rows-{} grid-cols-{} gap-3", wall.rows, wall.cols,); view! { <div class="grid grid-cols-[auto,1fr] gap-8"> - // Render the wall - <div style="max-height: 90vh; max-width: 90vh;" class=move || { grid_classes.clone() }>{cells}</div> - - <div> - <Button onclick=move |_| problem_fetcher.mark_dirty() text="Next problem ➤" /> - </ div> + <div style="max-height: 90vh; max-width: 90vh;" class=move || { grid_classes.clone() }> + {cells} + </div> </div> } } #[component] -fn Hold(hold: models::Hold, #[prop(into)] role: Signal<Option<HoldRole>>) -> impl leptos::IntoView { +#[tracing::instrument(skip_all)] +fn Hold(hold: models::Hold, role: Signal<Option<HoldRole>>) -> impl IntoView { + tracing::trace!("Enter"); let class = move || { let role_classes = match role.get() { Some(HoldRole::Start) => Some("outline outline-offset-2 outline-green-500"), @@ -106,45 +164,10 @@ fn Hold(hold: models::Hold, #[prop(into)] role: Signal<Option<HoldRole>>) -> imp }; let img = hold.image.map(|img| { - let src = format!("/files/holds/{}", img.filename); - view! { <img class="object-cover w-full h-full" src=src /> } + let srcset = img.srcset(); + view! { <img class="object-cover w-full h-full" srcset=srcset /> } }); + tracing::trace!("view"); view! { <div class=class>{img}</div> } } - -#[derive(Serialize, Deserialize, Clone)] -pub struct InitialData { - wall: models::Wall, -} - -#[server] -async fn load_initial_data() -> Result<RonCodec<InitialData>, ServerFnError> { - use crate::server::state::State; - - let state = expect_context::<State>(); - - let wall = state.persistent.with(|s| s.wall.clone()).await; - Ok(RonCodec::new(InitialData { wall })) -} - -#[server] -async fn get_random_problem() -> Result<RonCodec<Option<models::Problem>>, ServerFnError> { - use crate::server::state::State; - use rand::seq::IteratorRandom; - - let state = expect_context::<State>(); - - let problem = state - .persistent - .with(|s| { - let problems = &s.problems.problems; - let rng = &mut rand::thread_rng(); - problems.iter().choose(rng).cloned() - }) - .await; - - tracing::debug!("Returning randomized problem: {problem:?}"); - - Ok(RonCodec::new(problem)) -} diff --git a/crates/ascend/src/server.rs b/crates/ascend/src/server.rs index c28e5e8..a870fab 100644 --- a/crates/ascend/src/server.rs +++ b/crates/ascend/src/server.rs @@ -4,21 +4,16 @@ use cli::Cli; use config::Config; use confik::Configuration; use confik::EnvSource; -use persistence::Persistent; -use state::PersistentState; -use state::State; use std::path::Path; -use std::path::PathBuf; use tower_http::services::ServeDir; use tracing::level_filters::LevelFilter; use type_toppings::ResultExt; mod cli; pub mod config; +pub mod db; mod migrations; pub mod operations; -pub mod persistence; -pub mod state; pub const STATE_FILE: &str = "datastore/private/state.ron"; @@ -43,12 +38,6 @@ pub async fn main() { match cli.command { Command::Serve => serve(cli).await.unwrap_or_report(), - Command::ResetState => { - let s = PersistentState::default(); - let p = Path::new(STATE_FILE); - tracing::info!("Resetting state to default: {}", p.display()); - Persistent::persist(p, &s).await.unwrap_or_report(); - } } } @@ -57,32 +46,34 @@ async fn serve(cli: Cli) -> Result<(), Error> { use crate::app::App; use crate::app::shell; use axum::Router; - use leptos::prelude::*; use leptos_axum::LeptosRoutes; use leptos_axum::generate_route_list; - migrations::run_migrations().await; + tracing::debug!("Creating DB"); + let db = db::Database::create()?; + + migrations::run_migrations(&db).await.map_err(Error::Migration)?; // Setting get_configuration(None) means we'll be using cargo-leptos's env values // For deployment these variables are: // <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain> // Alternately a file can be specified such as Some("Cargo.toml") // The file would need to be included with the executable when moved to deployment - let leptos_conf_file = get_configuration(None).unwrap_or_report(); + let leptos_conf_file = leptos::config::get_configuration(None).unwrap_or_report(); let leptos_options = leptos_conf_file.leptos_options; let addr = leptos_options.site_addr; let routes = generate_route_list(App); let config = load_config(cli)?; - let server_state = load_state().await?; + tracing::debug!("Creating app router"); let app = Router::new() .leptos_routes_with_context( &leptos_options, routes, move || { - provide_context(server_state.clone()); - provide_context(config.clone()) + leptos::prelude::provide_context::<db::Database>(db.clone()); + leptos::prelude::provide_context::<Config>(config.clone()) }, { let leptos_options = leptos_options.clone(); @@ -93,7 +84,9 @@ async fn serve(cli: Cli) -> Result<(), Error> { .fallback(leptos_axum::file_and_error_handler(shell)) .with_state(leptos_options); + tracing::debug!("Binding TCP listener"); let listener = tokio::net::TcpListener::bind(&addr).await?; + tracing::info!("Listening on http://{addr}"); axum::serve(listener, app.into_make_service()).await?; @@ -106,22 +99,6 @@ fn file_service(path: impl AsRef<Path>) -> ServeDir { ServeDir::new(path) } -#[tracing::instrument] -async fn load_state() -> Result<State, Error> { - tracing::info!("Loading state"); - - let p = PathBuf::from(STATE_FILE); - - let persistent = if p.try_exists()? { - Persistent::<PersistentState>::load(&p).await? - } else { - tracing::info!("No state found at {STATE_FILE}, creating default state"); - Persistent::<PersistentState>::new(PersistentState::default(), p) - }; - - Ok(State { persistent }) -} - fn load_config(cli: Cli) -> Result<Config, Error> { let mut builder = config::Config::builder(); if cli @@ -139,11 +116,14 @@ fn load_config(cli: Cli) -> Result<Config, Error> { #[display("Server crash: {_variant}")] pub enum Error { Io(std::io::Error), - Persistence(persistence::Error), + Parser(moonboard_parser::Error), #[display("Failed migration")] + #[from(ignore)] Migration(Box<dyn std::error::Error>), Confik(confik::Error), + + Database(redb::Error), } diff --git a/crates/ascend/src/server/cli.rs b/crates/ascend/src/server/cli.rs index 4b25089..3405709 100644 --- a/crates/ascend/src/server/cli.rs +++ b/crates/ascend/src/server/cli.rs @@ -15,9 +15,6 @@ pub struct Cli { pub enum Command { #[default] Serve, - - /// Resets state, replacing it with defaults - ResetState, } fn default_config_location() -> camino::Utf8PathBuf { diff --git a/crates/ascend/src/server/db.rs b/crates/ascend/src/server/db.rs new file mode 100644 index 0000000..ddc03b8 --- /dev/null +++ b/crates/ascend/src/server/db.rs @@ -0,0 +1,107 @@ +use bincode::Bincode; +use redb::ReadTransaction; +use redb::TableDefinition; +use redb::WriteTransaction; +use serde::Deserialize; +use serde::Serialize; +use std::path::PathBuf; +use std::sync::Arc; + +mod bincode; + +const DB_FILE: &str = "datastore/private/ascend.redb"; + +#[derive(Debug, Clone)] +pub struct Database { + db: Arc<redb::Database>, +} + +impl Database { + #[tracing::instrument(skip_all, err)] + pub fn create() -> Result<Database, redb::Error> { + let file = PathBuf::from(DB_FILE); + + // Create parent dirs + if let Some(parent_dir) = file.parent() { + std::fs::create_dir_all(parent_dir)?; + } + + let db = redb::Database::create(file)?; + Ok(Self { db: Arc::new(db) }) + } + + #[tracing::instrument(skip_all, err)] + pub async fn read<T>(&self, f: impl FnOnce(&'_ ReadTransaction) -> Result<T, DatabaseOperationError>) -> Result<T, DatabaseOperationError> { + tokio::task::block_in_place(|| { + let dbtx = self.db.begin_read()?; + f(&dbtx) + }) + } + + #[tracing::instrument(skip_all, err)] + pub async fn write<T>(&self, f: impl FnOnce(&'_ WriteTransaction) -> Result<T, DatabaseOperationError>) -> Result<T, DatabaseOperationError> { + tokio::task::block_in_place(|| { + let dbtx = self.db.begin_write()?; + let res = f(&dbtx)?; + dbtx.commit()?; + Ok(res) + }) + } + + #[tracing::instrument(skip_all)] + pub async fn get_version(&self) -> Result<Option<Version>, DatabaseOperationError> { + self.read(|dbtx| dbtx.open_table(TABLE_VERSION)?.get(()).map(|o| o.map(|v| v.value())).map_err(Into::into)) + .await + } +} + +#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)] +#[display("DB operation error: {_variant}")] +pub enum DatabaseOperationError { + #[display("redb error")] + #[from(forward)] + Redb(#[error(source)] redb::Error), + + #[from(ignore)] + Custom(#[error(source)] Box<dyn std::error::Error + Send + Sync + 'static>), +} +impl DatabaseOperationError { + pub fn custom(err: impl std::error::Error + Send + Sync + 'static) -> Self { + Self::Custom(Box::new(err)) + } +} + +pub const TABLE_VERSION: TableDefinition<(), Bincode<Version>> = TableDefinition::new("version"); +#[derive(Serialize, Deserialize, Debug, derive_more::Display)] +#[display("{version}")] +pub struct Version { + pub version: u64, +} +impl Version { + pub fn current() -> Version { + Version { version: current::VERSION } + } +} + +pub use v2 as current; + +pub mod v2 { + use crate::models; + use crate::server::db::bincode::Bincode; + use redb::TableDefinition; + + pub const VERSION: u64 = 2; + + pub const TABLE_ROOT: TableDefinition<(), Bincode<models::v2::Root>> = TableDefinition::new("root"); + pub const TABLE_WALLS: TableDefinition<Bincode<models::v2::WallUid>, Bincode<models::v2::Wall>> = TableDefinition::new("walls"); + pub const TABLE_PROBLEMS: TableDefinition<Bincode<(models::v2::WallUid, models::v2::ProblemUid)>, Bincode<models::v2::Problem>> = + TableDefinition::new("problems"); +} + +pub mod v1 { + use crate::models; + use crate::server::db::bincode::Bincode; + use redb::TableDefinition; + + pub const TABLE_ROOT: TableDefinition<(), Bincode<models::v1::PersistentState>> = TableDefinition::new("root"); +} diff --git a/crates/ascend/src/server/db/bincode.rs b/crates/ascend/src/server/db/bincode.rs new file mode 100644 index 0000000..0b3529e --- /dev/null +++ b/crates/ascend/src/server/db/bincode.rs @@ -0,0 +1,52 @@ +use redb::Value; + +/// Wrapper type to handle keys and values using bincode serialization +#[derive(Debug)] +pub struct Bincode<T>(pub T); + +impl<T> Value for Bincode<T> +where + T: std::fmt::Debug + serde::Serialize + for<'a> serde::Deserialize<'a>, +{ + type SelfType<'a> + = T + where + Self: 'a; + + type AsBytes<'a> + = Vec<u8> + where + Self: 'a; + + fn fixed_width() -> Option<usize> { + None + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + bincode::deserialize(data).unwrap() + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> + where + Self: 'a, + Self: 'b, + { + bincode::serialize(value).unwrap() + } + + fn type_name() -> redb::TypeName { + redb::TypeName::new(&format!("Bincode<{}>", std::any::type_name::<T>())) + } +} + +impl<T> redb::Key for Bincode<T> +where + T: std::fmt::Debug + serde::Serialize + serde::de::DeserializeOwned + Ord, +{ + fn compare(data1: &[u8], data2: &[u8]) -> std::cmp::Ordering { + Self::from_bytes(data1).cmp(&Self::from_bytes(data2)) + } +} diff --git a/crates/ascend/src/server/migrations.rs b/crates/ascend/src/server/migrations.rs index 7c8e26f..5ddec99 100644 --- a/crates/ascend/src/server/migrations.rs +++ b/crates/ascend/src/server/migrations.rs @@ -1,20 +1,218 @@ -use crate::server::STATE_FILE; +use super::db; +use super::db::Database; +use crate::models; +use image::ImageDecoder; +use redb::ReadableTable; +use redb::ReadableTableMetadata; +use std::collections::BTreeMap; +use std::collections::BTreeSet; +use std::path::Path; use std::path::PathBuf; use type_toppings::ResultExt; -#[tracing::instrument] -pub async fn run_migrations() { - migrate_state_file().await; +#[tracing::instrument(skip_all, err)] +pub async fn run_migrations(db: &Database) -> Result<(), Box<dyn std::error::Error>> { + migrate_from_ron_to_redb(db).await?; + init_at_current_version(db).await?; + migrate_to_v2(db).await?; + Ok(()) } -/// State file moved to datastore/private -#[tracing::instrument] -async fn migrate_state_file() { - let m = PathBuf::from("state.ron"); - if m.try_exists().expect_or_report_with(|| format!("Failed to read {}", m.display())) { - tracing::warn!("MIGRATING STATE FILE"); - let p = PathBuf::from(STATE_FILE); - tokio::fs::create_dir_all(p.parent().unwrap()).await.unwrap_or_report(); - tokio::fs::rename(m, &p).await.unwrap_or_report(); +/// Use redb DB instead of Ron state file +#[tracing::instrument(skip_all, err)] +async fn migrate_from_ron_to_redb(db: &Database) -> Result<(), Box<dyn std::error::Error>> { + let ron_state_file_path = PathBuf::from(super::STATE_FILE); + + if ron_state_file_path + .try_exists() + .expect_or_report_with(|| format!("Failed to read {}", ron_state_file_path.display())) + { + tracing::warn!("MIGRATING"); + + let ron_state: models::v1::PersistentState = { + let content = tokio::fs::read_to_string(&ron_state_file_path).await?; + ron::from_str(&content)? + }; + + db.write(|txn| { + let mut version_table = txn.open_table(db::TABLE_VERSION)?; + assert!(version_table.is_empty()?); + version_table.insert((), db::Version { version: 1 })?; + + let mut root_table = txn.open_table(db::v1::TABLE_ROOT)?; + assert!(root_table.is_empty()?); + + let persistent_state = models::v1::PersistentState { + version: ron_state.version, + wall: ron_state.wall, + problems: ron_state.problems, + }; + + root_table.insert((), persistent_state)?; + Ok(()) + }) + .await?; + + tracing::info!("Removing ron state"); + tokio::fs::remove_file(ron_state_file_path).await?; } + + Ok(()) +} + +// TODO: Move out, is not really a migration +#[tracing::instrument(skip_all, err)] +async fn init_at_current_version(db: &Database) -> Result<(), Box<dyn std::error::Error>> { + db.write(|txn| { + let mut version_table = txn.open_table(db::TABLE_VERSION)?; + let is_missing_version = version_table.get(())?.is_none(); + if is_missing_version { + let v = db::Version::current(); + tracing::warn!("INITIALIZING DATABASE AT VERSION {v}"); + version_table.insert((), v)?; + + // Root table + { + let mut table = txn.open_table(db::current::TABLE_ROOT)?; + assert!(table.is_empty()?); + table.insert((), models::Root { walls: BTreeSet::new() })?; + } + + // Walls table + { + // Opening the table creates the table + let table = txn.open_table(db::current::TABLE_WALLS)?; + assert!(table.is_empty()?); + } + + // Problems table + { + // Opening the table creates the table + let table = txn.open_table(db::current::TABLE_PROBLEMS)?; + assert!(table.is_empty()?); + } + } + + Ok(()) + }) + .await?; + + Ok(()) +} + +#[tracing::instrument(skip_all, err)] +async fn migrate_to_v2(db: &Database) -> Result<(), Box<dyn std::error::Error>> { + use super::db; + + db.write(|txn| { + let mut version_table = txn.open_table(db::TABLE_VERSION)?; + let version = version_table.get(())?.unwrap().value().version; + if version == 1 { + tracing::warn!("MIGRATING"); + version_table.insert((), db::Version { version: 2 })?; + + let root_table_v1 = txn.open_table(db::v1::TABLE_ROOT)?; + let root_v1 = root_table_v1.get(())?.unwrap().value(); + drop(root_table_v1); + txn.delete_table(db::v1::TABLE_ROOT)?; + + let models::v1::PersistentState { version: _, wall, problems } = root_v1; + + // we'll reimport them instead of a lossy conversion. + drop(problems); + + let mut walls = BTreeSet::new(); + let wall_uid = models::v2::WallUid(uuid::Uuid::new_v4()); + let holds = wall + .holds + .into_iter() + .map(|(hold_position, hold)| -> Result<_, Box<dyn std::error::Error + Send + Sync>> { + let image = hold + .image + .map(|i| -> Result<_, Box<dyn std::error::Error + Send + Sync>> { + let holds_dir = Path::new("datastore/public/holds"); + + let p = holds_dir.join(i.filename); + tracing::info!("reading {}", p.display()); + let file_contents = std::fs::read(p)?; + + let mut decoder = image::ImageReader::new(std::io::Cursor::new(file_contents)) + .with_guessed_format()? + .into_decoder()?; + let orientation = decoder.orientation()?; + let mut img = image::DynamicImage::from_decoder(decoder)?; + img.apply_orientation(orientation); + + std::fs::create_dir_all(holds_dir)?; + + let targets = [(50, 50), (150, 150), (300, 300), (400, 400)]; + + let uid = models::ImageUid::create(); + let mut resolutions = BTreeMap::new(); + for (width, height) in targets { + tracing::info!("resizing to {width}x{height}"); + let resized = img.resize_to_fill(width, height, image::imageops::FilterType::Lanczos3); + + let filename = format!("hold_row{}_col{}_{width}x{height}_{uid}.webp", hold_position.row, hold_position.col); + let path = holds_dir.join(&filename); + tracing::info!("opening {}", path.display()); + let mut file = std::fs::OpenOptions::new().write(true).append(false).create_new(true).open(&path)?; + resized.write_to(&mut file, image::ImageFormat::WebP)?; + + let res = models::ImageResolution { + width: width.into(), + height: height.into(), + }; + resolutions.insert(res, models::ImageFilename { filename }); + } + + Ok(models::Image { uid, resolutions }) + }) + .transpose()?; + + Ok(( + models::v1::HoldPosition { + row: hold_position.row, + col: hold_position.col, + }, + models::v2::Hold { + position: models::v1::HoldPosition { + row: hold.position.row, + col: hold.position.col, + }, + image, + }, + )) + }) + .collect::<Result<_, _>>() + .unwrap(); + + let wall_v2 = models::v2::Wall { + uid: wall_uid, + rows: wall.rows, + cols: wall.cols, + holds, + problems: BTreeSet::new(), + }; + + walls.insert(wall_v2.uid); + let root_v2 = models::v2::Root { walls }; + + let mut root_table_v2 = txn.open_table(db::v2::TABLE_ROOT)?; + root_table_v2.insert((), root_v2)?; + drop(root_table_v2); + + let mut walls_table = txn.open_table(db::v2::TABLE_WALLS)?; + walls_table.insert(wall_v2.uid, wall_v2)?; + drop(walls_table); + + let problems_table = txn.open_table(db::v2::TABLE_PROBLEMS)?; + drop(problems_table); + } + + Ok(()) + }) + .await?; + + Ok(()) } diff --git a/crates/ascend/src/server/operations.rs b/crates/ascend/src/server/operations.rs index 54063ec..13e294b 100644 --- a/crates/ascend/src/server/operations.rs +++ b/crates/ascend/src/server/operations.rs @@ -1,15 +1,18 @@ //! Server lib module to host re-usable server operations. +use super::db::Database; +use crate::models; use crate::models::HoldPosition; use crate::models::HoldRole; -use crate::models::Problem; use crate::server::config::Config; -use crate::server::persistence; -use crate::server::state::State; +use crate::server::db; +use redb::ReadableTable; use std::collections::BTreeMap; -#[tracing::instrument(skip(state))] -pub(crate) async fn import_mini_moonboard_problems(config: &Config, state: &State) -> Result<(), Error> { +#[tracing::instrument(skip_all)] +pub(crate) async fn import_mini_moonboard_problems(config: &Config, db: Database, wall_uid: models::WallUid) -> Result<(), Error> { + use moonboard_parser::mini_moonboard; + let mut problems = Vec::new(); let file_name = "problems Mini MoonBoard 2020 40.json"; @@ -17,10 +20,12 @@ pub(crate) async fn import_mini_moonboard_problems(config: &Config, state: &Stat tracing::info!("Parsing mini moonboard problems from {file_path}"); - let mini_moonboard = moonboard_parser::mini_moonboard::parse(file_path.as_std_path()).await?; - for problem in mini_moonboard.problems { + let set_by = "mini-mb-2020-parser"; + + let mini_moonboard = mini_moonboard::parse(file_path.as_std_path()).await?; + for mini_mb_problem in mini_moonboard.problems { let mut holds = BTreeMap::<HoldPosition, HoldRole>::new(); - for mv in problem.moves { + for mv in mini_mb_problem.moves { let row = mv.description.row(); let col = mv.description.column(); let hold_position = HoldPosition { row, col }; @@ -33,16 +38,44 @@ pub(crate) async fn import_mini_moonboard_problems(config: &Config, state: &Stat }; holds.insert(hold_position, role); } - let route = Problem { holds }; - problems.push(route); + + let name = mini_mb_problem.name; + + let method = match mini_mb_problem.method { + mini_moonboard::Method::FeetFollowHands => models::Method::FeetFollowHands, + mini_moonboard::Method::Footless => models::Method::Footless, + mini_moonboard::Method::FootlessPlusKickboard => models::Method::FootlessPlusKickboard, + }; + + let problem_id = models::ProblemUid::create(); + + let problem = models::Problem { + uid: problem_id, + name, + set_by: set_by.to_owned(), + holds, + method, + date_added: chrono::Utc::now(), + }; + problems.push(problem); } - state - .persistent - .update(|s| { - s.problems.problems.extend(problems); - }) - .await?; + db.write(|txn| { + let mut walls_table = txn.open_table(db::current::TABLE_WALLS)?; + let mut problems_table = txn.open_table(db::current::TABLE_PROBLEMS)?; + + let mut wall = walls_table.get(wall_uid)?.unwrap().value(); + wall.problems.extend(problems.iter().map(|p| p.uid)); + walls_table.insert(wall_uid, wall)?; + + for problem in problems { + let key = (wall_uid, problem.uid); + problems_table.insert(key, problem)?; + } + + Ok(()) + }) + .await?; Ok(()) } @@ -50,5 +83,6 @@ pub(crate) async fn import_mini_moonboard_problems(config: &Config, state: &Stat #[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)] pub enum Error { Parser(moonboard_parser::Error), - Persistence(persistence::Error), + Tokio(tokio::task::JoinError), + DbOperation(crate::server::db::DatabaseOperationError), } diff --git a/crates/ascend/src/server/persistence.rs b/crates/ascend/src/server/persistence.rs deleted file mode 100644 index 6104857..0000000 --- a/crates/ascend/src/server/persistence.rs +++ /dev/null @@ -1,134 +0,0 @@ -use serde::Serialize; -use serde::de::DeserializeOwned; -use std::path::Path; -use std::path::PathBuf; -use std::sync::Arc; -use tokio::sync::Mutex; - -#[derive(Debug, Clone)] -pub struct Persistent<T> { - state: Arc<Mutex<T>>, - file_path: PathBuf, -} - -impl<T> Persistent<T> { - #[tracing::instrument(skip(state))] - pub fn new(state: T, file_path: PathBuf) -> Self { - Self { - state: Arc::new(Mutex::new(state)), - file_path, - } - } - - /// Instantiates state from file system - #[tracing::instrument] - pub async fn load(file_path: &Path) -> Result<Self, Error> - where - T: DeserializeOwned, - { - let content = tokio::fs::read_to_string(&file_path).await.map_err(|source| Error::Read { - file_path: file_path.to_owned(), - source, - })?; - - let t = ron::from_str(&content).map_err(|source| Error::Deserialize { - file_path: file_path.to_owned(), - source, - })?; - - let persistent = Self { - state: Arc::new(Mutex::new(t)), - file_path: file_path.to_owned(), - }; - - Ok(persistent) - } - - // TODO: This is pretty poor - clones the entire state on access - /// Returns state - #[tracing::instrument(skip(self))] - pub async fn get(&self) -> T - where - T: Clone, - { - let state = self.state.lock().await; - state.clone() - } - - /// Returns state passed through given function - #[tracing::instrument(skip_all)] - pub async fn with<F, R>(&self, f: F) -> R - where - F: FnOnce(&T) -> R, - { - let state = self.state.lock().await; - f(&state) - } - - /// Updates and persists state - #[tracing::instrument(skip_all)] - pub async fn update<F>(&self, f: F) -> Result<(), Error> - where - F: FnOnce(&mut T), - T: Serialize, - { - let mut state = self.state.lock().await; - f(&mut state); - Self::persist(&self.file_path, &state).await?; - Ok(()) - } - - /// Sets and persists state - #[tracing::instrument(skip_all)] - pub async fn set(&self, new_state: T) -> Result<(), Error> - where - T: Serialize, - { - self.update(move |state| { - *state = new_state; - }) - .await - } - - /// Persist. - /// - /// Implicitly called by `set` and `update`. - #[tracing::instrument(skip_all, err)] - pub async fn persist(file_path: &Path, state: &T) -> Result<(), Error> - where - T: Serialize, - { - tracing::debug!("Persisting state"); - let serialized = ron::ser::to_string_pretty(state, ron::ser::PrettyConfig::default()).map_err(|source| Error::Serialize { source })?; - if let Some(parent) = file_path.parent() { - tokio::fs::create_dir_all(parent).await.map_err(|source| Error::CreateDir { - source, - dir: parent.to_owned(), - })?; - } - tokio::fs::write(file_path, serialized).await.map_err(|source| Error::Write { - file_path: file_path.to_owned(), - source, - })?; - Ok(()) - } -} - -#[derive(Debug, derive_more::Error, derive_more::Display)] -#[display("Persistent state error: {_variant}")] -pub enum Error { - #[display("Failed to read file: {}", file_path.display())] - Read { file_path: PathBuf, source: std::io::Error }, - - #[display("Failed to deserialize state from file: {}", file_path.display())] - Deserialize { file_path: PathBuf, source: ron::error::SpannedError }, - - #[display("Failed to serialize state")] - Serialize { source: ron::Error }, - - #[display("Failed to write file: {}", file_path.display())] - Write { file_path: PathBuf, source: std::io::Error }, - - #[display("Failed to create directory: {}", dir.display())] - CreateDir { dir: PathBuf, source: std::io::Error }, -} diff --git a/crates/ascend/src/server/state.rs b/crates/ascend/src/server/state.rs deleted file mode 100644 index cdf6def..0000000 --- a/crates/ascend/src/server/state.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! Server state - -const STATE_VERSION: u64 = 1; - -use super::persistence::Persistent; -use crate::models; -use crate::models::Wall; -use serde::Deserialize; -use serde::Serialize; -use smart_default::SmartDefault; -use std::collections::BTreeSet; - -#[derive(Clone, Debug)] -pub struct State { - pub persistent: Persistent<PersistentState>, -} - -#[derive(Serialize, Deserialize, Clone, Debug, SmartDefault)] -pub struct PersistentState { - /// State schema version - #[default(STATE_VERSION)] - pub version: u64, - - pub wall: Wall, - pub problems: Problems, -} - -#[derive(Serialize, Deserialize, Clone, Debug, Default)] -pub struct Problems { - pub problems: BTreeSet<models::Problem>, -} diff --git a/crates/ascend/src/server_functions.rs b/crates/ascend/src/server_functions.rs new file mode 100644 index 0000000..fa2249e --- /dev/null +++ b/crates/ascend/src/server_functions.rs @@ -0,0 +1,162 @@ +use crate::codec::ron::Ron; +use crate::codec::ron::RonEncoded; +use crate::models; +use leptos::server; +use server_fn::ServerFnError; + +#[server( + input = Ron, + output = Ron, + custom = RonEncoded +)] +#[tracing::instrument(skip_all, err(Debug))] +pub async fn get_walls() -> Result<RonEncoded<Vec<models::Wall>>, ServerFnError> { + use crate::server::db::Database; + use leptos::prelude::expect_context; + use redb::ReadableTable; + tracing::debug!("Enter"); + + let db = expect_context::<Database>(); + + let walls = db + .read(|txn| { + let walls_table = txn.open_table(crate::server::db::current::TABLE_WALLS)?; + let walls: Vec<models::Wall> = walls_table.iter()?.map(|r| r.map(|(_, v)| v.value())).collect::<Result<_, _>>()?; + Ok(walls) + }) + .await?; + + tracing::debug!("Exit"); + + Ok(RonEncoded::new(walls)) +} + +#[server( + input = Ron, + output = Ron, + custom = RonEncoded +)] +#[tracing::instrument(skip_all, err(Debug))] +pub(crate) async fn get_wall(wall_uid: models::WallUid) -> Result<RonEncoded<models::Wall>, ServerFnError> { + use crate::server::db::Database; + use crate::server::db::DatabaseOperationError; + use leptos::prelude::expect_context; + tracing::debug!("Enter"); + + #[derive(Debug, derive_more::Error, derive_more::Display)] + enum Error { + #[display("Wall not found: {_0:?}")] + NotFound(#[error(not(source))] models::WallUid), + } + + let db = expect_context::<Database>(); + + let wall = db + .read(|txn| { + let walls_table = txn.open_table(crate::server::db::current::TABLE_WALLS)?; + let wall = walls_table + .get(wall_uid)? + .ok_or(Error::NotFound(wall_uid)) + .map_err(DatabaseOperationError::custom)? + .value(); + Ok(wall) + }) + .await?; + + tracing::debug!("ok"); + + Ok(RonEncoded::new(wall)) +} + +#[server( + input = Ron, + output = Ron, + custom = RonEncoded +)] +#[tracing::instrument(err(Debug))] +pub(crate) async fn get_problems_for_wall(wall_uid: models::WallUid) -> Result<RonEncoded<Vec<models::Problem>>, ServerFnError> { + use crate::server::db::Database; + use crate::server::db::DatabaseOperationError; + use leptos::prelude::expect_context; + tracing::debug!("Enter"); + + #[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)] + enum Error { + #[display("Wall not found: {_0:?}")] + WallNotFound(#[error(not(source))] models::WallUid), + + DatabaseOperation(DatabaseOperationError), + } + + async fn inner(wall_uid: models::WallUid) -> Result<Vec<models::Problem>, Error> { + let db = expect_context::<Database>(); + + let problems = db + .read(|txn| { + let walls_table = txn.open_table(crate::server::db::current::TABLE_WALLS)?; + tracing::debug!("getting wall"); + let wall = walls_table + .get(wall_uid)? + .ok_or(Error::WallNotFound(wall_uid)) + .map_err(DatabaseOperationError::custom)? + .value(); + tracing::debug!("got wall"); + drop(walls_table); + + tracing::debug!("open problems table"); + let problems_table = txn.open_table(crate::server::db::current::TABLE_PROBLEMS)?; + tracing::debug!("opened problems table"); + + let mut problems = Vec::new(); + for &problem_uid in &wall.problems { + if let Some(problem) = problems_table.get((wall_uid, problem_uid))? { + problems.push(problem.value()); + } + } + Ok(problems) + }) + .await?; + + Ok(problems) + } + + let problems = inner(wall_uid).await.map_err(error_reporter::Report::new).map_err(ServerFnError::new)?; + + tracing::debug!("ok"); + + Ok(RonEncoded::new(problems)) +} + +#[server( + input = Ron, + output = Ron, + custom = RonEncoded +)] +#[tracing::instrument(skip_all, err(Debug))] +pub(crate) async fn get_problem(wall_uid: models::WallUid, problem_uid: models::ProblemUid) -> Result<RonEncoded<models::Problem>, ServerFnError> { + use crate::server::db::Database; + use crate::server::db::DatabaseOperationError; + use leptos::prelude::expect_context; + tracing::debug!("Enter"); + + #[derive(Debug, derive_more::Error, derive_more::Display)] + enum Error { + #[display("Problem not found: {_0:?}")] + NotFound(#[error(not(source))] models::ProblemUid), + } + + let db = expect_context::<Database>(); + let problem = db + .read(|txn| { + let table = txn.open_table(crate::server::db::current::TABLE_PROBLEMS)?; + let problem = table + .get((wall_uid, problem_uid))? + .ok_or(Error::NotFound(problem_uid)) + .map_err(DatabaseOperationError::custom)? + .value(); + Ok(problem) + }) + .await?; + + Ok(RonEncoded::new(problem)) +} diff --git a/flake.lock b/flake.lock index 62e8c5d..1cc7198 100644 --- a/flake.lock +++ b/flake.lock @@ -10,11 +10,11 @@ ] }, "locked": { - "lastModified": 1729251766, - "narHash": "sha256-/tOGBbFKgIii6L0VZdJ2MFdhzTt0BtEsAFbWITXeIxA=", + "lastModified": 1739793388, + "narHash": "sha256-mf0FJ7JJi5gTUFz0SyWF8bqqonxoFD2DG9D785uyYJM=", "owner": "plul", "repo": "basecamp", - "rev": "aae7006aec576140aadf3fdea4ed7eae904dda14", + "rev": "f0f702ef6d5e8446eb8cd64e56fe1fe3cfbc677d", "type": "github" }, "original": { @@ -25,11 +25,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1736420959, - "narHash": "sha256-dMGNa5UwdtowEqQac+Dr0d2tFO/60ckVgdhZU9q2E2o=", + "lastModified": 1738797219, + "narHash": "sha256-KRwX9Z1XavpgeSDVM/THdFd6uH8rNm/6R+7kIbGa+2s=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "32af3611f6f05655ca166a0b1f47b57c762b5192", + "rev": "1da52dd49a127ad74486b135898da2cef8c62665", "type": "github" }, "original": { @@ -53,11 +53,11 @@ ] }, "locked": { - "lastModified": 1736649126, - "narHash": "sha256-XCw5sv/ePsroqiF3lJM6Y2X9EhPdHeE47gr3Q8b0UQw=", + "lastModified": 1738895285, + "narHash": "sha256-4Ukr4reJfQ67c6QqIxbX47wnPIGxE8BXCAEPu1C3MFM=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "162ab0edc2936508470199b2e8e6c444a2535019", + "rev": "85f3aed5f4b8eb312c6e8fe8c476bac248aed75f", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 14bbba8..7d584f0 100644 --- a/flake.nix +++ b/flake.nix @@ -75,12 +75,7 @@ }; nixosModules.default = - { - config, - pkgs, - lib, - ... - }: + { config, lib, ... }: let cfg = config.services.ascend; in diff --git a/justfile b/justfile index b625400..8c6650e 100644 --- a/justfile +++ b/justfile @@ -9,19 +9,19 @@ fmt: bc-fmt serve: - RUST_LOG=debug RUST_BACKTRACE=1 cargo leptos watch -- serve + RUST_BACKTRACE=1 cargo leptos watch -- serve -build-release: - rm -rf dist - mkdir dist - cargo leptos build --release -vv - cp target/release/ascend dist/ - cp -r target/site dist/ +# build-release: +# rm -rf dist +# mkdir dist +# cargo leptos build --release -vv +# cp target/release/ascend dist/ +# cp -r target/site dist/ -run-release: - #!/usr/bin/env bash - cd dist - LEPTOS_SITE_ROOT="site" LEPTOS_SITE_ADDR="127.0.0.1:1337" ./ascend serve +# run-release: +# #!/usr/bin/env bash +# cd dist +# LEPTOS_SITE_ROOT="site" LEPTOS_SITE_ADDR="127.0.0.1:1337" ./ascend serve reset-state: cargo run --features ssr -- reset-state @@ -42,3 +42,9 @@ prod-deploy: prod-logs: ssh 192.168.1.3 'journalctl --unit ascend.service' + +leptos-discord: + xdg-open "https://discord.com/channels/1031524867910148188/1031524868883218474" + +leptos-issues: + xdg-open "https://github.com/leptos-rs/leptos/issues" diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..b048e86 --- /dev/null +++ b/todo.md @@ -0,0 +1,12 @@ +- save images with a uuid +- downscale images +- associate routes with wall +- group routes by pattern (pattern family has shift/mirror variations) +- generate pattern families of variations when importing problems +- implement pattern challenge (start an "adventure mode" based on a pattern family) +- Record problem success (enum: flash, send, no-send) +- implement routes page to show all routes for a given wall +- implement favorite routes feature +- use wall id in URL. +- decide on routes vs problems terminology +- decide on holds vs wall-edit terminology