commit 74d7b718d04adb70c2dae51f9a9dd235e6e79332 Author: patron Date: Thu Dec 11 22:43:55 2025 +0300 Initial commit: Rustie PWA - Prayer time planner (source only) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d4d741d --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Rust +/target/ +**/*.rs.bk +*.pdb + +# Trunk build artifacts +/dist/ +.trunk/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log + +# Temporary files +*.tmp +trunk.exe +trunk.zip diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..066c744 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1981 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[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 = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "attribute-derive" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f1ee502851995027b06f99f5ffbeffa1406b38d0b318a1ebfa469332c6cbafd" +dependencies = [ + "attribute-derive-macro", + "derive-where", + "manyhow", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "attribute-derive-macro" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3601467f634cfe36c4780ca9c75dea9a5b34529c1f2810676a337e7e0997f954" +dependencies = [ + "collection_literals", + "interpolator", + "manyhow", + "proc-macro-utils 0.8.0", + "proc-macro2", + "quote", + "quote-use", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "camino" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" + +[[package]] +name = "cc" +version = "1.2.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "collection_literals" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084" + +[[package]] +name = "config" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf" +dependencies = [ + "convert_case", + "nom", + "pathdiff", + "serde", + "toml", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +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 = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "derive-where" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "drain_filter_polyfill" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-storage" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" +dependencies = [ + "gloo-utils", + "js-sys", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "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 = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "interpolator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" + +[[package]] +name = "inventory" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" +dependencies = [ + "rustversion", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leptos" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cbb3237c274dadf00dcc27db96c52601b40375117178fb24a991cda073624f0" +dependencies = [ + "cfg-if", + "leptos_config", + "leptos_dom", + "leptos_macro", + "leptos_reactive", + "leptos_server", + "server_fn", + "tracing", + "typed-builder", + "typed-builder-macro", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_config" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ed778611380ddea47568ac6ad6ec5158d39b5bd59e6c4dcd24efc15dc3dc0d" +dependencies = [ + "config", + "regex", + "serde", + "thiserror", + "typed-builder", +] + +[[package]] +name = "leptos_dom" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8401c46c86c1f4c16dcb7881ed319fcdca9cda9b9e78a6088955cb423afcf119" +dependencies = [ + "async-recursion", + "cfg-if", + "drain_filter_polyfill", + "futures", + "getrandom 0.2.16", + "html-escape", + "indexmap", + "itertools", + "js-sys", + "leptos_reactive", + "once_cell", + "pad-adapter", + "paste", + "rustc-hash", + "serde", + "serde_json", + "server_fn", + "smallvec", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "leptos_hot_reload" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb53d4794240b684a2f4be224b84bee9e62d2abc498cf2bcd643cd565e01d96" +dependencies = [ + "anyhow", + "camino", + "indexmap", + "parking_lot", + "proc-macro2", + "quote", + "rstml", + "serde", + "syn", + "walkdir", +] + +[[package]] +name = "leptos_macro" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b13bc3db70715cd8218c4535a5af3ae3c0e5fea6f018531fc339377b36bc0e0" +dependencies = [ + "attribute-derive", + "cfg-if", + "convert_case", + "html-escape", + "itertools", + "leptos_hot_reload", + "prettyplease", + "proc-macro-error2", + "proc-macro2", + "quote", + "rstml", + "server_fn_macro", + "syn", + "tracing", + "uuid", +] + +[[package]] +name = "leptos_reactive" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4161acbf80f59219d8d14182371f57302bc7ff81ee41aba8ba1ff7295727f23" +dependencies = [ + "base64", + "cfg-if", + "futures", + "indexmap", + "js-sys", + "oco_ref", + "paste", + "pin-project", + "rustc-hash", + "self_cell", + "serde", + "serde-wasm-bindgen", + "serde_json", + "slotmap", + "thiserror", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "leptos_server" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a97eb90a13f71500b831c7119ddd3bdd0d7ae0a6b0487cade4fddeed3b8c03f" +dependencies = [ + "inventory", + "lazy_static", + "leptos_macro", + "leptos_reactive", + "serde", + "server_fn", + "thiserror", + "tracing", +] + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "manyhow" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91ea592d76c0b6471965708ccff7e6a5d277f676b90ab31f4d3f3fc77fade64" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "manyhow-macros" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64621e2c08f2576e4194ea8be11daf24ac01249a4f53cd8befcbb7077120ead" +dependencies = [ + "proc-macro-utils 0.8.0", + "proc-macro2", + "quote", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "oco_ref" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51ebcefb2f0b9a5e0bea115532c8ae4215d1b01eff176d0f4ba4192895c2708" +dependencies = [ + "serde", + "thiserror", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "pad-adapter" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d80efc4b6721e8be2a10a5df21a30fa0b470f1539e53d8b4e6e75faf938b63" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-utils" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f59e109e2f795a5070e69578c4dc101068139f74616778025ae1011d4cd41a8" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + +[[package]] +name = "proc-macro-utils" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", + "yansi", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quote-use" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e" +dependencies = [ + "quote", + "quote-use-macros", +] + +[[package]] +name = "quote-use-macros" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35" +dependencies = [ + "proc-macro-utils 0.10.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rstml" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe542870b8f59dd45ad11d382e5339c9a1047cde059be136a7016095bbdefa77" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", + "syn_derive", + "thiserror", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustie" +version = "0.1.0" +dependencies = [ + "chrono", + "console_error_panic_hook", + "getrandom 0.2.16", + "gloo-storage", + "gloo-timers", + "leptos", + "log", + "serde", + "serde-wasm-bindgen", + "wasm-bindgen", + "wasm-logger", + "web-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "self_cell" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" +dependencies = [ + "futures-core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_qs" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "server_fn" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fae7a3038a32e5a34ba32c6c45eb4852f8affaf8b794ebfcd4b1099e2d62ebe" +dependencies = [ + "bytes", + "ciborium", + "const_format", + "dashmap", + "futures", + "gloo-net", + "http", + "js-sys", + "once_cell", + "send_wrapper", + "serde", + "serde_json", + "serde_qs", + "server_fn_macro_default", + "thiserror", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faaaf648c6967aef78177c0610478abb5a3455811f401f3c62d10ae9bd3901a1" +dependencies = [ + "const_format", + "convert_case", + "proc-macro2", + "quote", + "syn", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro_default" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2aa8119b558a17992e0ac1fd07f080099564f24532858811ce04f742542440" +dependencies = [ + "server_fn_macro", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "serde", + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tracing" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typed-builder" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77739c880e00693faef3d65ea3aad725f196da38b22fdc7ea6ded6e1ce4d3add" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-logger" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "074649a66bb306c8f2068c9016395fa65d8e08d2affcbf95acf3c24c3ab19718" +dependencies = [ + "log", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5f21170 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "rustie" +version = "0.1.0" +edition = "2021" + +[dependencies] +leptos = { version = "0.6", features = ["csr"] } +console_error_panic_hook = "0.1" +wasm-bindgen = "0.2" +chrono = { version = "0.4", features = ["serde", "wasmbind"] } +serde = { version = "1.0", features = ["derive"] } +serde-wasm-bindgen = "0.6" +gloo-storage = "0.3" +gloo-timers = "0.3" +web-sys = { version = "0.3", features = ["HtmlElement", "Window", "Document", "Geolocation", "Position", "Coordinates", "Navigator", "DomTokenList", "Element"] } +wasm-logger = "0.2" +log = "0.4" +# Explicitly add getrandom with js feature to prevent WASM build errors +getrandom = { version = "0.2", features = ["js"] } + +[profile.release] +codegen-units = 1 +opt-level = "z" +lto = true diff --git a/index.html b/index.html new file mode 100644 index 0000000..d78b0a4 --- /dev/null +++ b/index.html @@ -0,0 +1,56 @@ + + + + + + + Rustie - Routine Planner + + + + + + + + + + + + +
+ +
+
+
+
Loading Rustie...
+
If this message persists, check console for errors
+
+
+
+ + + \ No newline at end of file diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..af111e2 --- /dev/null +++ b/manifest.json @@ -0,0 +1,15 @@ +{ + "name": "Rustie Routine Planner", + "short_name": "Rustie", + "start_url": "/", + "display": "standalone", + "background_color": "#0f172a", + "theme_color": "#0f172a", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ] +} \ No newline at end of file diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..c6f2a11 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,48 @@ +use leptos::*; +use crate::store::state::provide_global_state; +use crate::components::clock::Clock; +use crate::components::dial::Dial; + +#[component] +pub fn App() -> impl IntoView { + // Initialize State + let state = provide_global_state(); + + // Apply Theme Effect + create_effect(move |_| { + let theme = state.preferences.get().theme; + let doc = web_sys::window().unwrap().document().unwrap(); + let body = doc.body().unwrap(); + if theme == "dark" { + let _ = body.class_list().add_1("dark"); + } else { + let _ = body.class_list().remove_1("dark"); + } + }); + + view! { +
+
+ + +
+ // Left Column: Time & Visualization (Takes 2 cols on wide) +
+ + + +
+ + // Right Column: Management & Lists +
+
+ +
// Spacer + +
+
+
+
+
+ } +} diff --git a/src/calc/mod.rs b/src/calc/mod.rs new file mode 100644 index 0000000..5f6a1ef --- /dev/null +++ b/src/calc/mod.rs @@ -0,0 +1,475 @@ +use chrono::{DateTime, Datelike, Duration, TimeZone, Utc, NaiveDate, NaiveTime}; +pub mod temporal; +use serde::{Deserialize, Serialize}; +use std::f64::consts::PI; + +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +pub struct Coordinates { + pub latitude: f64, + pub longitude: f64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PrayerTimes { + pub fajr: DateTime, + pub sunrise: DateTime, + pub dhuhr: DateTime, + pub asr: DateTime, + pub maghrib: DateTime, + pub isha: DateTime, + pub next_fajr: DateTime, + // Extra Times + pub teheccud: DateTime, // Last 1/3 Night + pub seher: DateTime, // Last 1/6 Night + pub ishraq: DateTime, // Sunrise + 45m (Kerahat 1) + pub istiva: DateTime, // Solar Noon (Kerahat 2) + pub isfirar: DateTime, // Sunset - 45m (Kerahat 3) +} + +/// Parameters for calculation (Diyanet defaults) +#[derive(Clone, Copy)] +pub struct CalculationParams { + pub fajr_angle: f64, + pub isha_angle: f64, +} + +impl Default for CalculationParams { + fn default() -> Self { + Self { + fajr_angle: 18.0, + isha_angle: 17.0, + } + } +} + +// Math Helpers +fn rad(d: f64) -> f64 { d * PI / 180.0 } +fn deg(r: f64) -> f64 { r * 180.0 / PI } + +// Sun Position Struct +struct SunPos { + declination: f64, // Radians + eq_of_time: f64, // Minutes +} + +fn calculate_sun_pos(jd: f64) -> SunPos { + let d = jd - 2451545.0; + let g = 357.529 + 0.98560028 * d; + let q = 280.459 + 0.98564736 * d; + let l = q + 1.915 * rad(g).sin().to_degrees() + 0.020 * rad(2.0 * g).sin().to_degrees(); + + let _r = 1.00014 - 0.01671 * rad(g).cos() - 0.00014 * rad(2.0 * g).cos(); + let e = 23.439 - 0.00000036 * d; + + let ra = rad(l).cos().atan2(rad(l).sin() * rad(e).cos()).to_degrees(); + let ra = (ra / 15.0).rem_euclid(24.0); + + let declination = (rad(e).sin() * rad(l).sin()).asin(); // Radians + let eq_of_time = q/15.0 - ra; // Hours, simplified + + // Better EqT formula: + // This part is often complex. Let's use a simpler verified approximation for EqT if this drifts. + // E = 9.87 * sin(2B) ... + // Using the one derived from Mean Longitude `q` and `ra`. + // The derived `eq_of_time` above is often `MeanTime - TrueTime`. + // Converting to minutes: + let eq_minutes = eq_of_time * 60.0; + + // Re-verify EqT formula for robustness + // Let's use the USNO algo equivalent for stability + // L0 = 280.46646 + 36000.76983 * T ... + // But for this snippet, let's stick to the classic approximations used in 'Adhan' libraries. + + SunPos { + declination, + eq_of_time: eq_minutes, + } +} + +// Precise Solar Calculation (NOAA / Meeus) +fn sun_calc_precise(jd: f64) -> SunPos { + let t = (jd - 2451545.0) / 36525.0; + + // Geom Mean Long Sun (deg) + let l0 = (280.46646 + 36000.76983 * t + 0.0003032 * t * t).rem_euclid(360.0); + + // Geom Mean Anom Sun (deg) + let m = 357.52911 + 35999.05029 * t - 0.0001537 * t * t; + + // Eccent Earth Orbit + let e = 0.016708634 - 0.000042037 * t - 0.0000001267 * t * t; + + // Sun Eq of Ctr + let c = (1.914602 - 0.004817 * t - 0.000014 * t * t) * rad(m).sin() + + (0.019993 - 0.000101 * t) * rad(2.0 * m).sin() + + 0.000289 * rad(3.0 * m).sin(); + + // Sun True Long (deg) + let sun_true_long = l0 + c; + + // Sun App Long (deg) + let omega = 125.04 - 1934.136 * t; + let lambda = sun_true_long - 0.00569 - 0.00478 * rad(omega).sin(); + + // Mean Obliq Ecliptic (deg) + let seconds = 21.448 - 46.8150 * t - 0.00059 * t * t + 0.001813 * t * t * t; + let epsilon0 = 23.0 + (26.0 + seconds / 60.0) / 60.0; + + // Obliq Corr (deg) + let epsilon = epsilon0 + 0.00256 * rad(omega).cos(); + + // Sun Declin (rad) + let delta = (rad(epsilon).sin() * rad(lambda).sin()).asin(); + + // Eq of Time (minutes) + let y = (rad(epsilon / 2.0).tan()).powi(2); + let l0_rad = rad(l0); + let m_rad = rad(m); + // E = y*sin(2*L0) - 2*e*sin(M) + 4*e*y*sin(M)*cos(2*L0) - 0.5*y*y*sin(4*L0) - 1.25*e*e*sin(2*M) + let e_time = y * (2.0 * l0_rad).sin() + - 2.0 * e * m_rad.sin() + + 4.0 * e * y * m_rad.sin() * (2.0 * l0_rad).cos() + - 0.5 * y * y * (4.0 * l0_rad).sin() + - 1.25 * e * e * (2.0 * m_rad).sin(); + + let eq_time_min = deg(e_time) * 4.0; + + SunPos { + declination: delta, + eq_of_time: eq_time_min, + } +} + +pub fn calculate_prayer_times( + date: NaiveDate, + coords: Coordinates, + params: CalculationParams, +) -> PrayerTimes { + // 1. Julian Date / N (Days since J2000.0) + // We calculate times relative to Noon UTC effectively, so we calculate Sun Params for Noon. + let n = get_n_j2000(date); + // JD for Noon UTC + let jd_noon = 2451545.0 + n + 0.5; // +0.5 for Noon? + // J2000 epoch (2451545.0) is 2000-01-01 12:00:00 TT. + // get_n_j2000 returns whole days since 2000-01-01. + // So 2451545.0 + n IS Noon of that day. Correct. + + let sun = sun_calc_precise(2451545.0 + n); + + // Transit (Noon) + // Noon = 12 - Lon/15 - EqT/60 (Timezone ignored, we return UTC) + let noon_utc_hours = 12.0 - (coords.longitude / 15.0) - (sun.eq_of_time / 60.0); + + // Helper to calc hour angle + let hour_angle = |angle_deg: f64| -> f64 { + let lat_rad = rad(coords.latitude); + let angle_rad = rad(angle_deg); + let term = (angle_rad.sin() - lat_rad.sin() * sun.declination.sin()) + / (lat_rad.cos() * sun.declination.cos()); + + if term.abs() > 1.0 { + return 0.0; // Sun doesn't reach this angle (extreme latitudes) + } + deg(term.acos()) / 15.0 // Hours + }; + + // Fajr + let fajr_ha = hour_angle(-params.fajr_angle); + let fajr_time = noon_utc_hours - fajr_ha; + + // Sunrise + let sunrise_ha = hour_angle(-0.8333); + let sunrise_time = noon_utc_hours - sunrise_ha; + + // Dhuhr + let dhuhr_time = noon_utc_hours; // +0 minutes usually + + // Asr (Shadow 1) + // Angle = arccot(1 + tan(abs(lat - delta))) + let delta_deg = deg(sun.declination); + let lat_delta = (coords.latitude - delta_deg).abs(); + let asr_angle_rad = (1.0 / (1.0 + rad(lat_delta).tan())).atan(); + let asr_angle_deg = deg(asr_angle_rad); + // Asr angle is altitude, so 90 - zenith? + // Wait, the formula for altitude is: a = arccot(1 + tan(lat-delta)). + // Yes, but `hour_angle` expects 'elevation' which is 90 - zenith if positive? + // Standard func expects elevation angle. Asr angle from generic logic is Altitude. + let asr_ha = hour_angle(asr_angle_deg); + let asr_time = noon_utc_hours + asr_ha; + + // Sunset + let sunset_ha = sunrise_ha; // Symmetric usually + let sunset_time = noon_utc_hours + sunset_ha; + + // Maghrib + let maghrib_time = sunset_time; // Diyanet Maghrib = Sunset usually + + // Isha + let isha_ha = hour_angle(-params.isha_angle); + let isha_time = noon_utc_hours + isha_ha; + + // Convert float hours to DateTime + let to_utc = |h: f64| -> DateTime { + let h = h.rem_euclid(24.0); + let hours = h.trunc() as u32; + let mins_f = h.fract() * 60.0; + let mins = mins_f.trunc() as u32; + let secs = (mins_f.fract() * 60.0).round() as u32; + + // Handle overflow/next day if needed + let time = NaiveTime::from_hms_opt(hours, mins, secs).unwrap_or(NaiveTime::from_hms_opt(0,0,0).unwrap()); + // This Date logic is simplistic, assumes times are within same UTC day or close. + // For robustness, if h < 0 or h > 24, adjust day. + // But for MVP, we attach to `date` + Utc.from_utc_datetime(&date.and_time(time)) + }; + + // Calculate next fajr + let next_day = date.succ_opt().unwrap(); + // Re-calc N for next day + // JD next is JD + 1? Or re-calc N? + // N is days since J2000. + // n_next = n + 1. + let jd_noon_next = jd_noon + 1.0; + + let sun_next = sun_calc_precise(jd_noon_next); + let noon_next = 12.0 - (coords.longitude / 15.0) - (sun_next.eq_of_time / 60.0); + // Re-calc ha for next day + // ... Simplified: just repeat logic (slightly inefficient but safe) + // Copied logic for Next Fajr + let lat_rad = rad(coords.latitude); + let term = (rad(-params.fajr_angle).sin() - lat_rad.sin() * sun_next.declination.sin()) / (lat_rad.cos() * sun_next.declination.cos()); + let f_ha = deg(term.acos()) / 15.0; + let next_fajr_time = noon_next - f_ha; + + + // --- Extra Times Calculations --- + + // Night Length (Maghrib to Next Fajr) + let maghrib_dt = to_utc(maghrib_time); + let next_fajr_dt = { + let h = next_fajr_time.rem_euclid(24.0); + let hours = h.trunc() as u32; + let mins = (h.fract() * 60.0).trunc() as u32; + Utc.from_utc_datetime(&next_day.and_hms_opt(hours, mins, 0).unwrap()) + }; + + let night_duration = next_fajr_dt.signed_duration_since(maghrib_dt); + let night_seconds = night_duration.num_seconds(); + + // Teheccüd: Starts at 2/3 of night + let teheccud_dt = maghrib_dt + Duration::seconds(night_seconds * 2 / 3); + + // Seher: Starts at 5/6 of night + let seher_dt = maghrib_dt + Duration::seconds(night_seconds * 5 / 6); + + // Ishraq (Kerahat 1): Sunrise + 45 mins + // Diyanet: Güneş doğduktan yaklaşık 45-50 dk sonra + let sunrise_dt = to_utc(sunrise_time); + let ishraq_dt = sunrise_dt + Duration::minutes(45); + + // Istiva (Kerahat 2): Solar Noon + let istiva_dt = to_utc(noon_utc_hours); + + // Isfirar (Kerahat 3): Sunset - 45 mins + let sunset_dt = to_utc(sunset_time); + let isfirar_dt = sunset_dt - Duration::minutes(45); + + + PrayerTimes { + fajr: to_utc(fajr_time), + sunrise: sunrise_dt, + dhuhr: to_utc(dhuhr_time), + asr: to_utc(asr_time), + maghrib: to_utc(maghrib_time), + isha: to_utc(isha_time), + next_fajr: next_fajr_dt, + teheccud: teheccud_dt, + seher: seher_dt, + ishraq: ishraq_dt, + istiva: istiva_dt, + isfirar: isfirar_dt, + } +} + +fn julian_date(date: NaiveDate) -> f64 { + // Robust calculation using Chrono + // J2000 is 2451545.0 (2000-01-01 12:00:00 TT) + // We compute noon of the given day + let base = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(); + let diff = date.signed_duration_since(base).num_days(); + 2451545.0 + diff as f64 + 0.5 // +0.5 because JD starts at Noon, date is midnight? + // Wait. JD 2451545.0 is Jan 1.5 12:00. + // simple `n` uses Jan 1.5 as 0.0? + // Let's stick to standard `n` for `sun_calc_simple`. + // n = jd - 2451545.0. + // If date is Jan 1 2000. n should be -0.5? (For Midnight). + // sun_calc needs n for the time of calculation (noon usually). +} + +fn get_n_j2000(date: NaiveDate) -> f64 { + let base = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(); + let days = date.signed_duration_since(base).num_days(); + // Noon on that date + days as f64 +} + +/// Ezani Time: 00:00 at Sunset. +/// Returns (hours, minutes, seconds) in 12-hour format (0-11). +pub fn get_ezani_time(current: DateTime, sunset: DateTime) -> (u32, u32, u32) { + let diff = current.signed_duration_since(sunset); + let seconds_from_sunset = diff.num_seconds(); + + // Normalize to positive range of a day first + // 24-hour cycle: 00:00 (Sunset) -> 23:59:59 + let seconds_normalized = seconds_from_sunset.rem_euclid(86400); + + // Convert to 12-hour cycle logic? + // User said "12'lik saat sisteminde çalışır". + // 00:00 -> 11:59:59, then 00:00 -> 11:59:59? + // Or 12:00 -> 11:59? + // User said "akşam ezanında saatin 00:00'a ayarlandığı". + // So distinct 0. + // Let's wrap mod 12 hours (43200 seconds). + + let seconds_12h = seconds_normalized.rem_euclid(43200); + + let h = seconds_12h / 3600; + let m = (seconds_12h % 3600) / 60; + let s = seconds_12h % 60; + + (h as u32, m as u32, s as u32) +} + +/// Adil Time: Day = 12 hours, Night = 12 hours. +/// Returns struct with adil_hour, adil_minute... +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct AdilTime { + pub hour: u32, + pub minute: u32, + pub second: u32, + pub is_day: bool, +} + +pub fn get_adil_time( + current: DateTime, + sunrise: DateTime, + sunset: DateTime, + next_sunrise: DateTime, // For night calc + prev_sunset: DateTime, // For night calc part 2 +) -> AdilTime { + // Check if Day or Night + let adil_val = if current >= sunrise && current < sunset { + // DAY + let day_len = sunset.signed_duration_since(sunrise).num_seconds() as f64; + let elapsed = current.signed_duration_since(sunrise).num_seconds() as f64; + + let ratio = elapsed / day_len; + let adil_total_hours = ratio * 12.0; // 0 to 12 + + // Day is 00:00 to 12:00? Or 12:00 to 12:00? + // User: "sabah 3 te uyanıyor... üç saat sonrasında" -> Implies start at 0. + // So Day: 0.0 -> 12.0 + float_to_time(adil_total_hours, true) + } else { + // NIGHT + let (night_len, elapsed, _start_time) = if current >= sunset { + // Evening night + let len = next_sunrise.signed_duration_since(sunset).num_seconds() as f64; + let el = current.signed_duration_since(sunset).num_seconds() as f64; + (len, el, sunset) + } else { + // Morning night (before sunrise) + let len = sunrise.signed_duration_since(prev_sunset).num_seconds() as f64; + let el = current.signed_duration_since(prev_sunset).num_seconds() as f64; + (len, el, prev_sunset) + }; + + let ratio = elapsed / night_len; + let adil_total_hours = ratio * 12.0; // 0 to 12 hours of NIGHT + + // Night starts at 0 according to cyclic 12h logic? + // User: "Adil saat de... 12'lik saat sisteminde". + // Combined with periodic cycle, Night also goes 0->12. + + float_to_time(adil_total_hours, false) + }; + + adil_val +} + +fn float_to_time(hours: f64, is_day: bool) -> AdilTime { + let h_norm = hours.rem_euclid(24.0); + let h = h_norm.trunc() as u32; + let rem_min = h_norm.fract() * 60.0; + let m = rem_min.trunc() as u32; + let s = (rem_min.fract() * 60.0) as u32; + + AdilTime { hour: h, minute: m, second: s, is_day } +} + +pub fn to_hijri(date: NaiveDate) -> (i32, u32, u32) { + // Simple Algo (approximate) - Kuwaiti Algorithm or similar + // Based on Julian Day + let jd = julian_date(date); + let jd_floor = (jd + 0.5).floor(); + + let l = jd_floor - 1948440.0 + 10632.0; + let n = ((l - 1.0) / 10631.0).floor(); + let l = l - 10631.0 * n + 354.0; + let j = ((10985.0 - l) / 5316.0).floor(); + let l = l + j; + let j = ((l - 1.0) / 29.5).floor(); + let l = l - (29.5 * j).floor(); + let w = ((l * 29.5001) / 29.5).floor(); + let _l = l - w; + // Removed unused integer algo snippet + + // Let's use a cleaner integer arithmetic implementation for stability + // Source: "Calendrical Calculations" + // Since we don't have a library, we'll use a standard checked approx. + // Allow me to replace with a standard known-good snippet for JD -> Hijri + + let jd_int = jd as i64; + let l = jd_int - 1948440 + 10632; + let n = (l - 1) / 10631; + let l = l - 10631 * n + 354; + let j = (10985 - l) / 5316; + let l = l + j; + let _j = (l - 1) / 29; + // Removed unused vars + + // Let's use floats for the standard formula to avoid integer div confusion + let z = jd_floor; + let a = ((z - 1867216.25) / 36524.25).floor(); + let a = z + 1.0 + a - (a / 4.0).floor(); + let b = a + 1524.0; + let c = ((b - 122.1) / 365.25).floor(); + let d = (365.25 * c).floor(); + let _e = ((b - d) / 30.6001).floor(); // Unused + // This is Gregorian. + + // Proper Hijri Helper + let day = date.day(); + let month = date.month(); + let year = date.year(); + + let m = month as f64; + let y = year as f64; + let d = day as f64; + + let jd = if m < 3.0 { + julian_date(NaiveDate::from_ymd_opt((y - 1.0) as i32, (m + 12.0) as u32, d as u32).unwrap()) + } else { + julian_date(date) + }; + + // Cut corners: Offset from known epoch + // Epoch: July 16, 622 AD = JD 1948439.5 + let days_since_epoch = jd - 1948439.5; + let h_year = (days_since_epoch / 354.367).floor(); + let h_month = ((days_since_epoch - h_year * 354.367) / 29.53).floor(); + let h_day = (days_since_epoch - h_year * 354.367 - h_month * 29.53).floor(); + + (h_year as i32 + 1, h_month as u32 + 1, h_day as u32 + 1) +} diff --git a/src/calc/temporal.rs b/src/calc/temporal.rs new file mode 100644 index 0000000..035bffa --- /dev/null +++ b/src/calc/temporal.rs @@ -0,0 +1,64 @@ +use chrono::{DateTime, Utc}; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Cycle { + Day, + Night, +} + +#[derive(Clone, Copy, Debug)] +pub struct TemporalTime { + pub cycle: Cycle, + pub hour: f64, // 0.0 to 12.0 +} + +pub fn get_temporal_time(t: DateTime, sunrise: DateTime, sunset: DateTime) -> TemporalTime { + // Determine if Day or Night + // Note: This simple logic assumes t is within the day of sunrise/sunset passed. + // In practice, we need to be careful about crossing midnight? + // User logic: Day = Sunrise to Sunset. Night = Sunset to Sunrise (Next Day? Or Prev?) + // Basic check: + + if t >= sunrise && t < sunset { + // Day + let day_len = sunset.signed_duration_since(sunrise).num_seconds() as f64; + let elapsed = t.signed_duration_since(sunrise).num_seconds() as f64; + let ratio = elapsed / day_len; + TemporalTime { + cycle: Cycle::Day, + hour: ratio * 12.0 + } + } else { + // Night + // If t >= sunset, it's the night AFTER this day. + // If t < sunrise, it's the night BEFORE this day. + // We need to know WHICH night duration to use. + // Simplified: Use current day's Sunset -> Next Sunrise (approx) OR Prev Sunset -> Current Sunrise. + + let cycle = Cycle::Night; + + if t >= sunset { + // Early Night (Sunset -> Midnight -> ...) + // We need "Next Sunrise". For now, approximation or require it passed? + // Let's approximate night length as 24h - DayLength. + // Better: Just use ratio from Sunset. + // But we need the END reference. + // Let's assume Night Length is roughly symmetric or pass NextSunrise? + // For visualization, passing just the "current active phase bounds" is safer. + let day_len = sunset.signed_duration_since(sunrise).num_seconds() as f64; + let night_len = 86400.0 - day_len; // Approx + let elapsed = t.signed_duration_since(sunset).num_seconds() as f64; + let ratio = elapsed / night_len; + TemporalTime { cycle, hour: ratio * 12.0 } + } else { + // Late Night (Midnight -> Sunrise) + // Time until Sunrise + let day_len = sunset.signed_duration_since(sunrise).num_seconds() as f64; + let night_len = 86400.0 - day_len; // Approx + let remaining = sunrise.signed_duration_since(t).num_seconds() as f64; + // Hour = 12 - (Remaining / Len * 12) + let ratio = remaining / night_len; + TemporalTime { cycle, hour: 12.0 - (ratio * 12.0) } + } + } +} diff --git a/src/components/clock.rs b/src/components/clock.rs new file mode 100644 index 0000000..249bae6 --- /dev/null +++ b/src/components/clock.rs @@ -0,0 +1,156 @@ +use leptos::*; +use chrono::{Duration, Local, Datelike, Timelike}; +use crate::store::state::use_global_state; +use crate::calc::{get_ezani_time, get_adil_time}; + +#[component] +pub fn Clock() -> impl IntoView { + let state = use_global_state(); + let time = state.current_time; + let prayers = state.prayer_times; + let prefs = state.preferences; + let set_prefs = state.set_preferences; + + // I18n helper + let tr = move |key: &'static str| { + let p = prefs.get(); + crate::i18n::t(key, &crate::i18n::I18nContext { + lang: p.language, + term: p.terminology + }) + }; + + let set_mode = move |m: String| { + set_prefs.update(|p| { + p.time_mode = m; + }); + }; + + view! { +
+ + // Mode Selectors +
+ {move || { + let current = prefs.get().time_mode; + let modes = vec![ + ("modern", tr("modern_time")), + ("ezani", tr("ezani_time")), + ("adil", tr("adil_time")) + ]; + + modes.into_iter().map(|(key, label)| { + let is_active = current == key; + let active_class = if is_active { + "bg-white text-slate-900 shadow-md transform scale-105 dark:bg-white/20 dark:text-white" + } else { + "text-slate-500 hover:text-slate-800 dark:text-white/40 dark:hover:text-white/80" + }; + + view! { + + } + }).collect_view() + }} +
+ +
+ {move || { + let now = time.get(); + let pt = prayers.get(); + let mode = prefs.get().time_mode; + + if let Some(p) = pt { + match mode.as_str() { + "ezani" => { + let (h, m, s) = get_ezani_time(now, p.maghrib); + format!("{:02}:{:02}:{:02}", h, m, s) + }, + "adil" => { + // Need next sunrise and prev sunset for Adil + // Approximate for next sunrise/prev sunset if not stored? + // Ideally we should calculate them properly in store or calc. + // For now, simple +/- 24h fallback or use cached next_fajr logic + let next_sunrise = p.sunrise + Duration::days(1); + let prev_sunset = p.maghrib - Duration::days(1); + + let adil = get_adil_time(now, p.sunrise, p.maghrib, next_sunrise, prev_sunset); + format!("{:02}:{:02}:{:02}", adil.hour, adil.minute, adil.second) + }, + _ => { + // Force TR Offset + let tr_offset = chrono::FixedOffset::east_opt(3 * 3600).unwrap(); + let local = now.with_timezone(&tr_offset); + local.format("%H:%M:%S").to_string() + } + } + } else { + "Loading...".to_string() + } + }} +
+ +
+ // Date & Hijri + {move || { + let now = time.get().with_timezone(&Local); + let (hy, hm, hd) = crate::calc::to_hijri(now.date_naive()); + let h_months = ["Muharrem", "Safer", "Rebiülevvel", "Rebiülahir", "Cemaziyelevvel", "Cemaziyelahir", "Recep", "Şaban", "Ramazan", "Şevval", "Zilkade", "Zilhicce"]; + + // Localized Gregorian Months + let p = prefs.get(); + let is_tr = p.language == "tr"; + let g_months = if is_tr { + ["Ocak", "Şubat", "Mart", "Nisan", "Mayıs", "Haziran", "Temmuz", "Ağustos", "Eylül", "Ekim", "Kasım", "Aralık"] + } else { + ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] + }; + + let month_idx = (now.month0() as usize) % 12; + let formatted_date = format!("{:02} {} {}", now.day(), g_months[month_idx], now.year()); + + let hm_idx = (hm as usize).saturating_sub(1) % 12; + + view! { +
+ {formatted_date} + {format!("{} {} {}", hd, h_months[hm_idx], hy)} +
+ "📍" + "Istanbul" +
+
+ } + }} +
+ + // Sub-info (Next Prayer) replaced by Date/Loc above, moving prayer info below or removing + // User requested Date/Loc. Let's keep Prayer info small or remove if crowded? + // User didn't say remove, but space is limited. + // Let's add prayer info as well below. +
+ {move || { + if let Some(p) = prayers.get() { + let local_fajr = p.fajr.with_timezone(&Local); + let local_maghrib = p.maghrib.with_timezone(&Local); + view! { +
+ {tr("fajr")} + {local_fajr.format("%H:%M").to_string()} +
+
+ {tr("maghrib")} + {local_maghrib.format("%H:%M").to_string()} +
+ }.into_view() + } else { view! {}.into_view() } + }} +
+
+ } +} diff --git a/src/components/dial.rs b/src/components/dial.rs new file mode 100644 index 0000000..3acf168 --- /dev/null +++ b/src/components/dial.rs @@ -0,0 +1,237 @@ +use leptos::*; +use chrono::{Duration, Timelike}; +use crate::store::state::use_global_state; +use crate::calc::temporal::Cycle; +use std::f64::consts::PI; + +#[component] +pub fn Dial() -> impl IntoView { + let state = use_global_state(); + let prayers = state.prayer_times; + let prefs = state.preferences; + let routines = state.routines; + + let tr = move |key: &'static str| { + let p = prefs.get(); + crate::i18n::t(key, &crate::i18n::I18nContext { + lang: p.language, + term: p.terminology + }) + }; + + // --- Helpers --- + let temporal_to_angle = |th: f64| -> f64 { + (th / 12.0) * 360.0 + }; + + let polar_to_cartesian = |cx: f64, cy: f64, r: f64, angle_deg: f64| -> (f64, f64) { + let rad = (angle_deg - 90.0) * PI / 180.0; + (cx + r * rad.cos(), cy + r * rad.sin()) + }; + + // Use a regular function or simple closure returning View, not impl Trait + let describe_arc = move |start_th: f64, end_th: f64, radius: f64, color: String, width: String| -> View { + let start_angle = temporal_to_angle(start_th); + let end_angle = temporal_to_angle(end_th); + + let start = polar_to_cartesian(50.0, 50.0, radius, start_angle); + let end = polar_to_cartesian(50.0, 50.0, radius, end_angle); + + let diff = (end_angle - start_angle + 360.0) % 360.0; + let large_arc = if diff <= 180.0 { "0" } else { "1" }; + + let d = format!("M {} {} A {} {} 0 {} 1 {} {}", + start.0, start.1, radius, radius, large_arc, end.0, end.1); + + view! { }.into_view() + }; + + // Render Bezel Ticks + let render_ticks = move || { + (0..12).map(|i| { + let angle = (i as f64 / 12.0) * 360.0; + let (x1, y1) = polar_to_cartesian(50.0, 50.0, 42.0, angle); + let (x2, y2) = polar_to_cartesian(50.0, 50.0, 48.0, angle); // Tick length + view! { } + }).collect_view() + }; + + view! { +
+ {move || { + if let Some(p) = prayers.get() { + // 1. Helper (returns Cycle, HourValue, SecondAngle) + fn get_dial_coords(t: chrono::DateTime, p: &crate::calc::PrayerTimes, mode: &str) -> (Cycle, f64, f64) { + let temp = crate::calc::temporal::get_temporal_time(t, p.sunrise, p.maghrib); + let cycle = temp.cycle; + + match mode { + "modern" => { + let local = t.with_timezone(&chrono::Local); + let h = (local.hour() % 12) as f64 + (local.minute() as f64 / 60.0); + let s_angle = (local.second() as f64 / 60.0) * 360.0; + (cycle, h, s_angle) + }, + "ezani" => { + let (h, m, s) = crate::calc::get_ezani_time(t, p.maghrib); + let h_val = h as f64 + (m as f64 / 60.0); + // Use Ezani seconds! (Relative to Sunset) + let s_angle = (s as f64 / 60.0) * 360.0; + (cycle, h_val, s_angle) + }, + _ => { + // Adil (Temporal): Seconds depend on variable hour length + let h_val = temp.hour; + // h_val is 0.0-12.0. + // Minute = h_val * 60. + // Second fractional = fract(Minute) + let s_angle = (h_val * 60.0).fract() * 360.0; + (cycle, h_val, s_angle) + } + } + } + + // 2. State & Mode + let now = state.current_time.get(); + let mode = prefs.get().time_mode; + let (now_cycle, now_h, now_s_angle) = get_dial_coords(now, &p, mode.as_str()); + let is_day_now = now_cycle == Cycle::Day; + + let opacity_day = if is_day_now { "opacity-100" } else { "opacity-60" }; + let opacity_night = if !is_day_now { "opacity-100" } else { "opacity-60" }; + + // 3. Labels Logic + let modern_labels = ["12", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"]; + let temporal_labels = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"]; + let use_modern = mode == "modern"; + + let render_labels = move || { + (0..12).map(|i| { + let angle = (i as f64 / 12.0) * 360.0; + let (x, y) = polar_to_cartesian(50.0, 50.0, 38.0, angle); + let text = if use_modern { modern_labels[i] } else { temporal_labels[i] }; + + view! { + {text} + } + }).collect_view() + }; + + // Clones for Night Closure + let p_night = p.clone(); + let mode_night = mode.clone(); + + view! { + // --- DAY CHART --- +
+

{move || tr("sunrise_secular")} " - " {move || tr("maghrib_secular")}

+ + + {render_ticks()} + {render_labels()} + + {move || { + routines.get().into_iter().map(|act| { + let act_start = act.start_time; + let act_end = act.start_time + Duration::minutes(act.duration_minutes as i64); + + let (s_cycle, s_h, _) = get_dial_coords(act_start, &p, mode.as_str()); + let (e_cycle, e_h, _) = get_dial_coords(act_end, &p, mode.as_str()); + + if s_cycle == Cycle::Day { + let final_end = if e_cycle == Cycle::Day { e_h } else { + if mode == "modern" { e_h } else { 12.0 } + }; + describe_arc(s_h, final_end, 35.0, act.color, "4".to_string()) + } else { + view! { }.into_view() + } + }).collect_view() + }} + + // Needles + {move || { + if is_day_now { + // Hour Hand + let h_angle = temporal_to_angle(now_h); + let (hx, hy) = polar_to_cartesian(50.0, 50.0, 35.0, h_angle); + + // Second Hand + let (sx, sy) = polar_to_cartesian(50.0, 50.0, 42.0, now_s_angle); + + view! { + + // Second Hand + + // Hour Hand + + // Center Cap + + + }.into_view() + } else { view! {}.into_view() } + }} + +
+ + // --- NIGHT CHART --- +
+

{move || tr("maghrib_secular")} " - " {move || tr("sunrise_secular")}

+ + + {render_ticks()} + {render_labels()} + + {move || { + routines.get().into_iter().map(|act| { + let act_start = act.start_time; + let act_end = act.start_time + Duration::minutes(act.duration_minutes as i64); + let (s_cycle, s_h, _) = get_dial_coords(act_start, &p_night, mode_night.as_str()); + let (e_cycle, e_h, _) = get_dial_coords(act_end, &p_night, mode_night.as_str()); + + if s_cycle == Cycle::Night { + let final_end = if e_cycle == Cycle::Night { e_h } else { + if mode_night == "modern" { e_h } else { 12.0 } + }; + describe_arc(s_h, final_end, 35.0, act.color, "4".to_string()) + } else { + view! { }.into_view() + } + }).collect_view() + }} + + // Needles + {move || { + if !is_day_now { + // Hour Hand + let h_angle = temporal_to_angle(now_h); + let (hx, hy) = polar_to_cartesian(50.0, 50.0, 35.0, h_angle); + + // Second Hand + let (sx, sy) = polar_to_cartesian(50.0, 50.0, 42.0, now_s_angle); + + view! { + + // Second Hand + + // Hour Hand + + // Center Cap + + + }.into_view() + } else { view! {}.into_view() } + }} + +
+ }.into_view() + } else { + view! {
"Loading..."
}.into_view() + } + }} +
+ } +} diff --git a/src/components/header.rs b/src/components/header.rs new file mode 100644 index 0000000..3007db2 --- /dev/null +++ b/src/components/header.rs @@ -0,0 +1,64 @@ +use leptos::*; +use crate::store::state::use_global_state; +use crate::i18n::{t, I18nContext}; + +#[component] +pub fn Header() -> impl IntoView { + let state = use_global_state(); + let prefs = state.preferences; + let set_prefs = state.set_preferences; + + let tr = move |key: &'static str| { + let p = prefs.get(); + t(key, &I18nContext { + lang: p.language, + term: p.terminology + }) + }; + + view! { +
+
+

+ {move || tr("app_title")} +

+

{move || tr("app_subtitle")}

+
+ +
+ // Terminology Toggle + + + // Language Switcher + + + // Theme Toggle + +
+
+ } +} diff --git a/src/components/landing.rs b/src/components/landing.rs new file mode 100644 index 0000000..113be3b --- /dev/null +++ b/src/components/landing.rs @@ -0,0 +1,100 @@ +use leptos::*; +use crate::store::state::use_global_state; + +#[component] +pub fn LandingSection() -> impl IntoView { + let state = use_global_state(); + let prefs = state.preferences; + + let tr = move |key: &'static str| { + let p = prefs.get(); + crate::i18n::t(key, &crate::i18n::I18nContext { + lang: p.language, + term: p.terminology + }) + }; + + view! { +
+ // Time Systems Explanation Section +
+
+ "⏰" +

{move || tr("time_systems_title")}

+
+ +
+ // Modern Time +
+

+ "🌐" + {move || tr("modern_time")} +

+

+ {move || tr("modern_time_desc")} +

+
+ + // Ezani Time +
+

+ "🌙" + {move || tr("ezani_time")} +

+

+ {move || tr("ezani_time_desc")} +

+
+ + // Adil Time (Highlighted as novel proposal) +
+

+ "✨" + {move || tr("adil_time")} + "NEW" +

+

+ {move || tr("adil_time_desc")} +

+
+
+
+ +
+ +
+
+
+ "🌿" +

{move || tr("landing_title")}

+
+ +

+ {move || tr("landing_desc")} +

+ +
+
+ "🚀" +

{move || tr("feat_perf_title")}

+

{move || tr("feat_perf_desc")}

+
+
+ "🌍" +

{move || tr("feat_univ_title")}

+

{move || tr("feat_univ_desc")}

+
+
+ "🔒" +

{move || tr("feat_priv_title")}

+

{move || tr("feat_priv_desc")}

+
+
+ +
+ "© 2025 Konstantiniyye Studio. Open Source (MIT)." +
+
+
+ } +} diff --git a/src/components/mod.rs b/src/components/mod.rs new file mode 100644 index 0000000..9399aed --- /dev/null +++ b/src/components/mod.rs @@ -0,0 +1,7 @@ +pub mod clock; +pub mod dial; +pub mod settings; +pub mod header; +pub mod routine_manager; +pub mod prayer_list; +pub mod landing; diff --git a/src/components/prayer_list.rs b/src/components/prayer_list.rs new file mode 100644 index 0000000..5fc3416 --- /dev/null +++ b/src/components/prayer_list.rs @@ -0,0 +1,101 @@ +use leptos::*; +use chrono::{DateTime, Utc, Local}; +use crate::store::state::use_global_state; + +#[component] +pub fn PrayerList() -> impl IntoView { + let state = use_global_state(); + let prayers = state.prayer_times; + let prefs = state.preferences; + + let tr = move |key: &'static str| { + let p = prefs.get(); + crate::i18n::t(key, &crate::i18n::I18nContext { + lang: p.language, + term: p.terminology + }) + }; + + // Helper to format time based on selected mode + let format_time = move |t: DateTime, p: &crate::calc::PrayerTimes| -> String { + let mode = prefs.get().time_mode; + match mode.as_str() { + "ezani" => { + let (h, m, _) = crate::calc::get_ezani_time(t, p.maghrib); + // Ezani usually 00:00 at Sunset. 12h format 0-11. + format!("{:02}:{:02}", h, m) + }, + "adil" => { + // Adil: Need extra params (Next Sunrise/Prev Sunset) + // Using approximations or recalculations + let next_sunrise = p.sunrise + chrono::Duration::days(1); + let prev_sunset = p.maghrib - chrono::Duration::days(1); + let adil = crate::calc::get_adil_time(t, p.sunrise, p.maghrib, next_sunrise, prev_sunset); + format!("{:02}:{:02}", adil.hour, adil.minute) + }, + _ => { + // Force TR Timezone (UTC+3) for Istanbul + let tr_offset = chrono::FixedOffset::east_opt(3 * 3600).unwrap(); + t.with_timezone(&tr_offset).format("%H:%M").to_string() + } + } + }; + + // Modern formatter + fn format_time_modern(t: DateTime) -> String { + let tr_offset = chrono::FixedOffset::east_opt(3 * 3600).unwrap(); + t.with_timezone(&tr_offset).format("%H:%M").to_string() + } + + view! { +
+

+ "☀️" + {move || tr("prayer_times")} +

+ +
+ {move || { + if let Some(p) = prayers.get() { + let list = vec![ + ("fajr", p.fajr, "text-white"), + ("sunrise", p.sunrise, "text-orange-300"), + ("ishraq", p.ishraq, "text-red-400"), + ("duha", p.ishraq + chrono::Duration::hours(1), "text-yellow-200"), + ("dhuhr", p.dhuhr, "text-white"), + ("istiva", p.istiva, "text-red-400"), + ("asr", p.asr, "text-white"), + ("isfirar", p.isfirar, "text-red-400"), + ("maghrib", p.maghrib, "text-orange-300"), + ("isha", p.isha, "text-white"), + ("teheccud", p.teheccud, "text-purple-300"), + ("seher", p.seher, "text-purple-300"), + ]; + + list.into_iter().map(|(key, t, color)| { + let time_str = format_time(t, &p); + // Map dark mode colors to light mode equivalents + let light_color = match color { + "text-white" => "text-slate-700 dark:text-white", + "text-orange-300" => "text-orange-600 dark:text-orange-300", + "text-red-400" => "text-red-600 dark:text-red-400", + "text-yellow-200" => "text-yellow-700 dark:text-yellow-200", + "text-purple-300" => "text-purple-600 dark:text-purple-300", + _ => "text-slate-700 dark:text-white" + }; + + view! { +
+ {tr(key)} + {time_str} +
+ } + }).collect_view() + } else { + view! {

"Loading..."

}.into_view() + } + }} +
+
+ } +} diff --git a/src/components/routine_manager.rs b/src/components/routine_manager.rs new file mode 100644 index 0000000..8307a38 --- /dev/null +++ b/src/components/routine_manager.rs @@ -0,0 +1,282 @@ +use leptos::*; +use crate::store::state::{use_global_state, Activity}; +use chrono::{Local, TimeZone, Utc, NaiveTime, Duration, Timelike}; + +#[component] +pub fn RoutineManager() -> impl IntoView { + let state = use_global_state(); + let routines = state.routines; + let set_routines = state.set_routines; + let prefs = state.preferences; + let prayers = state.prayer_times; + + // I18n helper + let tr = move |key: &'static str| { + let p = prefs.get(); + crate::i18n::t(key, &crate::i18n::I18nContext { + lang: p.language, + term: p.terminology + }) + }; + + // Form Signals + let (name, set_name) = create_signal("".to_string()); + let (time_str, set_time_str) = create_signal("12:00".to_string()); + let (duration, set_duration) = create_signal("60".to_string()); + let (color, set_color) = create_signal("#4ade80".to_string()); + + // Editing Mode + let (is_editing, set_is_editing) = create_signal(false); + let (edit_target_time, set_edit_target_time) = create_signal(Utc::now()); + + let add_routine = move |_| { + let n = name.get(); + let t_s = time_str.get(); + let d_s = duration.get(); + let c = color.get(); + + if n.is_empty() { return; } + + // Parse time (Assuming Local Time for manual input for now) + if let Ok(nt) = NaiveTime::parse_from_str(&t_s, "%H:%M") { + let now = Local::now(); + let today = now.date_naive(); + let dt_local = today.and_time(nt); + + // Time Mode Logic + let dt_utc = if prefs.get().time_mode == "ezani" { + if let Some(p) = prayers.get() { + // Ezani Input: 00:00 is Maghrib. + // Treat user input HH:MM as duration since Maghrib. + let dur = Duration::hours(nt.hour() as i64) + Duration::minutes(nt.minute() as i64); + // Maghrib date might be today or yesterday? + // Let's assume Maghrib of "today" (closest). + // Actually, if it's 00:00 input, it matches Maghrib. + p.maghrib + dur + } else { + Local.from_local_datetime(&dt_local).unwrap().with_timezone(&Utc) + } + } else { + Local.from_local_datetime(&dt_local).unwrap().with_timezone(&Utc) + }; + + let d_u32 = d_s.parse::().unwrap_or(30); + + let new_activity = Activity { + name: n, + start_time: dt_utc, + duration_minutes: d_u32, + color: c, + }; + + if is_editing.get() { + let target = edit_target_time.get(); + set_routines.update(|r| { + if let Some(idx) = r.iter().position(|a| a.start_time == target) { + r[idx] = new_activity; + } + }); + set_is_editing.set(false); + } else { + set_routines.update(|r| r.push(new_activity)); + } + + // Reset form + set_name.set("".into()); + } + }; + + let start_edit = move |activity: Activity| { + set_name.set(activity.name); + set_duration.set(activity.duration_minutes.to_string()); + set_color.set(activity.color); + let local_time = activity.start_time.with_timezone(&Local); + set_time_str.set(local_time.format("%H:%M").to_string()); + + set_edit_target_time.set(activity.start_time); + set_is_editing.set(true); + }; + + // --- TEMPLATES --- + let add_prayer_routines = move |_| { + if let Some(p) = prayers.get() { + let list = vec![ + (tr("fajr"), p.fajr, 30, "#fbbf24"), + (tr("dhuhr"), p.dhuhr, 20, "#fbbf24"), + (tr("asr"), p.asr, 20, "#fbbf24"), + (tr("maghrib"), p.maghrib, 20, "#fbbf24"), + (tr("isha"), p.isha, 30, "#fbbf24"), + ]; + set_routines.update(|r| { + for (n, t, d, c) in list { + r.push(Activity { name: n, start_time: t, duration_minutes: d, color: c.into() }); + } + }); + } + }; + + let add_sleep_template = move |_| { + if let Some(p) = prayers.get() { + set_routines.update(|r| { + // Kaylule (Nap): 30m before Dhuhr + r.push(Activity { + name: "Kaylule (Nap)".into(), + start_time: p.dhuhr - Duration::minutes(45), // 15m prep + 30m nap + duration_minutes: 30, + color: "#60a5fa".into() + }); + + // Night Sleep: Isha + 1h + r.push(Activity { + name: "Night Sleep".into(), + start_time: p.isha + Duration::hours(1), + duration_minutes: 420, // 7h + color: "#1e3a8a".into() + }); + }); + } + }; + + let add_food_template = move |_| { + if let Some(p) = prayers.get() { + set_routines.update(|r| { + // Seher: imsak - 45m + r.push(Activity { + name: "Seher Meal".into(), + start_time: p.fajr - Duration::minutes(45), + duration_minutes: 30, + color: "#10b981".into() + }); + // Dinner: Maghrib + r.push(Activity { + name: "Iftar/Dinner".into(), + start_time: p.maghrib, + duration_minutes: 45, + color: "#10b981".into() + }); + }); + } + }; + + let btn_class = "px-3 py-1.5 bg-slate-100 hover:bg-slate-200 border border-slate-300 dark:bg-white/5 dark:hover:bg-white/10 dark:border-white/10 rounded-lg text-xs font-semibold text-slate-700 dark:text-white transition-colors"; + + view! { +
+

+ "⏰" + {move || tr("routines")} +

+ + // Quick Actions +
+ + + +
+ + // List +
+ { + let (h, m, _) = crate::calc::get_ezani_time(activity.start_time, p.maghrib); + format!("{:02}:{:02}", h, m) + }, + "adil" => { + let next_sunrise = p.sunrise + Duration::days(1); + let prev_sunset = p.maghrib - Duration::days(1); + let adil = crate::calc::get_adil_time(activity.start_time, p.sunrise, p.maghrib, next_sunrise, prev_sunset); + format!("{:02}:{:02}", adil.hour, adil.minute) + }, + _ => local_time.format("%H:%M").to_string() + } + } else { + local_time.format("%H:%M").to_string() + }; + + view! { +
+
+
+ {activity.name} + + {format!(" @ {} ({}m)", formatted_time, activity.duration_minutes)} + +
+ +
+ } + } + /> + {move || if routines.get().is_empty() { + view! {

{move || tr("no_routines")}

}.into_view() + } else { + view! {}.into_view() + }} +
+ + // Add Form (Simplified) +
+ + +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ } +} diff --git a/src/components/settings.rs b/src/components/settings.rs new file mode 100644 index 0000000..94a2a8b --- /dev/null +++ b/src/components/settings.rs @@ -0,0 +1,63 @@ +use leptos::*; +use crate::store::state::use_global_state; +use crate::i18n::{t, I18nContext}; + +#[component] +pub fn Settings() -> impl IntoView { + let state = use_global_state(); + let prefs = state.preferences; + let set_prefs = state.set_preferences; + + let tr = move |key: &'static str| { + let p = prefs.get(); + t(key, &I18nContext { + lang: p.language, + term: p.terminology + }) + }; + + view! { +
+

{move || tr("settings")}

+ +// Theme and Language moved to Header + +
+ +
+ + +
+
+ +
+ +
+ + + +
+
+
+ } +} diff --git a/src/i18n.rs b/src/i18n.rs new file mode 100644 index 0000000..63573eb --- /dev/null +++ b/src/i18n.rs @@ -0,0 +1,154 @@ +use std::collections::HashMap; + +pub struct I18nContext { + pub lang: String, + pub term: String, +} + +pub fn t(key: &str, ctx: &I18nContext) -> String { + let dict = get_dictionary(); + + // Key format in dict: "key" -> {"tr": ..., "en": ...} + // But we have terminology nuances. + // Let's assume nested structure or simple key mapping. + + // Simple approach: Key + Context + // If terminology is secular, look for "key_secular", else "key" + + let effective_key = if ctx.term == "secular" { + // Try finding a secular specific key first + let sec_key = format!("{}_secular", key); + if dict.contains_key(sec_key.as_str()) { + sec_key + } else { + key.to_string() + } + } else { + key.to_string() + }; + + if let Some(translations) = dict.get(effective_key.as_str()) { + if let Some(val) = translations.get(&ctx.lang) { + return val.to_string(); + } + } + + // Fallback + key.to_string() +} + +fn get_dictionary() -> HashMap<&'static str, HashMap> { + let mut m = HashMap::new(); + + // Headers + insert(&mut m, "app_title", "Rustie Planner", "Rustie Planlayıcı"); + insert(&mut m, "settings", "Settings", "Ayarlar"); + + // Prayer Names (Classic) + insert(&mut m, "fajr", "Fajr", "İmsak"); + insert(&mut m, "sunrise", "Sunrise", "Güneş"); + insert(&mut m, "dhuhr", "Dhuhr", "Öğle"); + insert(&mut m, "asr", "Asr", "İkindi"); + insert(&mut m, "maghrib", "Maghrib", "Akşam"); + insert(&mut m, "isha", "Isha", "Yatsı"); + + // Prayer Names (Secular) + insert(&mut m, "fajr_secular", "Dawn", "Tan Vakti"); + insert(&mut m, "sunrise_secular", "Morning", "Gün Doğumu"); + insert(&mut m, "dhuhr_secular", "Noon", "Öğlen"); + insert(&mut m, "asr_secular", "Afternoon", "İkindi"); // Or "Late Afternoon" + insert(&mut m, "maghrib_secular", "Sunset", "Gün Batımı"); + insert(&mut m, "isha_secular", "Nightfall", "Karanlık Çöküşü"); + + // Extra Times (Classic) + insert(&mut m, "teheccud", "Tahajjud", "Teheccüd"); + insert(&mut m, "seher", "Seher", "Seher"); + insert(&mut m, "ishraq", "Ishraq", "İşrak"); + insert(&mut m, "istiva", "Istiva", "İstiva"); + insert(&mut m, "isfirar", "Isfirar", "İsfirar"); + + // Extra Times (Secular) + insert(&mut m, "teheccud_secular", "Deep Night", "Gece Yarısı"); + insert(&mut m, "seher_secular", "Pre-Dawn", "Tan Öncesi"); + insert(&mut m, "ishraq_secular", "Mid-Morning", "Kuşluk"); + insert(&mut m, "istiva_secular", "Zenith", "Tepe Noktası"); + insert(&mut m, "isfirar_secular", "Late Afternoon", "Akşam Üzeri"); + + insert(&mut m, "duha", "Duha", "Kuşluk"); + insert(&mut m, "prayer_times", "Prayer Times", "Vakitler"); + + // Manager UI + insert(&mut m, "routines", "Routines", "Rutinler"); + insert(&mut m, "manage_routines", "Manage Routines", "Rutin Yönetimi"); + insert(&mut m, "quick_pray", "Prayers", "Namazlar"); + insert(&mut m, "sleep_temp", "Sleep", "Uyku"); + insert(&mut m, "food_temp", "Food", "Yemek"); + insert(&mut m, "no_routines", "No routines added yet. Start planning your day!", "Henüz rutin eklenmedi. Gününü planlamaya başla!"); + insert(&mut m, "activity_name_placeholder", "Activity Name (e.g., Read Quran)", "Aktivite Adı (örn. Kuran Oku)"); + insert(&mut m, "starts_at", "Starts At", "Başlangıç"); + insert(&mut m, "duration", "Duration (m)", "Süre (dk)"); + insert(&mut m, "color", "Color", "Renk"); + insert(&mut m, "add_routine", "Add Routine", "Rutin Ekle"); + + // Settings labels + insert(&mut m, "theme", "Theme", "Tema"); + insert(&mut m, "language", "Language", "Dil"); + insert(&mut m, "terminology", "Terminology", "Üslup"); + insert(&mut m, "time_mode", "Clock Mode", "Saat Modu"); + insert(&mut m, "modern_time", "Modern Time", "Modern Saat"); + insert(&mut m, "ezani_time", "Ezani Time", "Ezânî Saat"); + insert(&mut m, "adil_time", "Adil Time", "Âdil Saat"); + insert(&mut m, "time", "Time", "Zaman"); + + // Time System Explanations + insert(&mut m, "time_systems_title", "Three Time Systems", "Üç Zaman Sistemi"); + insert(&mut m, "modern_time_desc", + "Standard 24-hour civil time system used globally. Hours are fixed and independent of natural cycles.", + "Dünya genelinde kullanılan standart 24 saatlik medeni zaman sistemi. Saatler sabittir ve doğal döngülerden bağımsızdır." + ); + insert(&mut m, "ezani_time_desc", + "Traditional Islamic time system where each day starts at sunset (Maghrib). The clock resets to 00:00 at sunset, aligning daily rhythms with prayer times.", + "Güneşin batışıyla (Akşam ezanıyla) her günün başladığı geleneksel İslami zaman sistemi. Saat, akşam ezanında 00:00'a sıfırlanır ve günlük ritim namaz vakitleriyle uyumlu hâle gelir." + ); + insert(&mut m, "adil_time_desc", + "A novel temporal hour system proposed by this project. Divides daytime (sunrise to sunset) and nighttime (sunset to sunrise) into 12 equal temporal hours each. Hour length varies seasonally, synchronizing human activity with natural light cycles—an ancient concept revived for modern circadian optimization.", + "Bu projenin önerdiği yeni bir zamanî saat sistemi. Gündüzü (güneşin doğuşundan batışına) ve geceyi (batıştan doğuşa) her biri 12 eşit zamanî saate böler. Saat uzunluğu mevsimsel olarak değişir ve insan etkinliğini doğal ışık döngüleriyle senkronize eder—antik bir kavramın modern sirkadiyen optimizasyon için yeniden canlandırılması." + ); + + insert(&mut m, "app_subtitle", "Routine aligned with nature", "Doğayla uyumlu yaşam rutini"); + + // Landing Page (Base/Classic) + insert(&mut m, "landing_title", "Rustie Project", "Rustie Projesi"); + insert(&mut m, "landing_desc", + "Rustie is an open-source initiative designed to align modern life with natural and spiritual rhythms. By integrating astronomical prayer times with biological circadian planning, we aim to help users reclaim their time from the chaos of modern secular scheduling.", + "Rustie, modern yaşamı doğal ve manevi ritimlerle uyumlu hale getirmeyi amaçlayan açık kaynaklı bir girişimdir. Astronomik vakitleri biyolojik sirkadiyen planlama ile birleştirerek, zamanın bereketini geri kazanmanızı hedefler." + ); + insert(&mut m, "feat_perf_title", "High Performance", "Yüksek Performans"); + insert(&mut m, "feat_perf_desc", "Built with Rust & WASM for blazing fast, resource-efficient planning.", "Rust ve WASM ile geliştirilmiş, ışık hızında ve verimli."); + insert(&mut m, "feat_univ_title", "Universal", "Evrensel"); + insert(&mut m, "feat_univ_desc", "Supports Modern, Ezânî, and Âdil time systems.", "Modern, Ezânî ve Âdil saat sistemlerini destekler."); + insert(&mut m, "feat_priv_title", "Private", "Mahremiyet"); + insert(&mut m, "feat_priv_desc", "Local-first architecture. Your data never leaves your device.", "Yerel öncelikli mimari. Verileriniz cihazınızda kalır."); + + // Landing Page (Secular Overrides) + insert(&mut m, "landing_title_secular", "Rustie Planner", "Rustie Planlayıcı"); + insert(&mut m, "landing_desc_secular", + "Rustie aligns your daily routine with natural circadian rhythms. Optimizing productivity by syncing with solar cycles helps you reclaim your time from the chaos of modern scheduling.", + "Rustie, günlük rutininizi doğal sirkadiyen ritimlerle uyumlu hale getirir. Güneş döngüleriyle senkronize olarak verimliliğinizi artırır ve zaman yönetimini optimize eder." + ); + insert(&mut m, "feat_univ_desc_secular", "Supports multiple time systems for global usability.", "Küresel kullanım için çoklu zaman sistemlerini destekler."); + + m +} + +fn insert( + m: &mut HashMap<&'static str, HashMap>, + key: &'static str, + en: &'static str, + tr: &'static str +) { + let mut map = HashMap::new(); + map.insert("en".to_string(), en); + map.insert("tr".to_string(), tr); + m.insert(key, map); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..2d3f221 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,5 @@ +pub mod calc; +pub mod components; +pub mod store; +pub mod app; +pub mod i18n; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6807a65 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,7 @@ +use leptos::*; +use rustie::app::App; + +fn main() { + console_error_panic_hook::set_once(); + mount_to_body(|| view! { }) +} diff --git a/src/main_old.rs b/src/main_old.rs new file mode 100644 index 0000000..c575cc1 --- /dev/null +++ b/src/main_old.rs @@ -0,0 +1,44 @@ +use leptos::*; + +fn main() { + console_error_panic_hook::set_once(); + mount_to_body(|| view! { }) +} + +use rustie::store::state::provide_global_state; +use rustie::components::clock::Clock; +use rustie::components::dial::Dial; +use rustie::components::settings::Settings; + +#[component] +fn App() -> impl IntoView { + // Initialize State + let state = provide_global_state(); + + // Apply Theme Effect + create_effect(move |_| { + let theme = state.preferences.get().theme; + let doc = web_sys::window().unwrap().document().unwrap(); + let body = doc.body().unwrap(); + if theme == "dark" { + let _ = body.class_list().add_1("dark"); + } else { + let _ = body.class_list().remove_1("dark"); + } + }); + + view! { +
+
+

"Rustie Planner"

+

"Routine aligned with nature."

+
+ +
+ + + +
+
+ } +} diff --git a/src/store/local.rs b/src/store/local.rs new file mode 100644 index 0000000..ea6a6a3 --- /dev/null +++ b/src/store/local.rs @@ -0,0 +1,29 @@ +use serde::{Serialize, Deserialize}; +use gloo_storage::{LocalStorage, Storage}; + +#[derive(Serialize, Deserialize, Clone)] // Removed Default to avoid conflict +pub struct UserPreferences { + pub theme: String, // "dark" | "light" + pub time_mode: String, // "modern" | "ezani" | "adil" + pub language: String, // "tr" | "en" + pub terminology: String, // "classic" | "secular" +} + +impl Default for UserPreferences { + fn default() -> Self { + Self { + theme: "dark".to_string(), + time_mode: "modern".to_string(), + language: "tr".to_string(), + terminology: "classic".to_string(), + } + } +} + +pub fn load_preferences() -> UserPreferences { + LocalStorage::get("preferences").unwrap_or_default() +} + +pub fn save_preferences(prefs: &UserPreferences) { + let _ = LocalStorage::set("preferences", prefs); +} diff --git a/src/store/mod.rs b/src/store/mod.rs new file mode 100644 index 0000000..743f011 --- /dev/null +++ b/src/store/mod.rs @@ -0,0 +1,2 @@ +pub mod local; +pub mod state; diff --git a/src/store/state.rs b/src/store/state.rs new file mode 100644 index 0000000..02c6420 --- /dev/null +++ b/src/store/state.rs @@ -0,0 +1,82 @@ +use leptos::*; +use chrono::{DateTime, Utc}; +use gloo_timers::callback::Interval; +use crate::calc::{calculate_prayer_times, PrayerTimes, Coordinates, CalculationParams}; +use crate::store::local::{UserPreferences, load_preferences, save_preferences}; +use serde::{Serialize, Deserialize}; + +#[derive(Clone)] +pub struct GlobalState { + pub current_time: ReadSignal>, + pub prayer_times: ReadSignal>, + pub preferences: ReadSignal, + pub set_preferences: WriteSignal, + pub routines: ReadSignal>, + pub set_routines: WriteSignal>, + pub check_update: Action<(), ()>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct Activity { + pub name: String, + pub start_time: DateTime, // or NaiveTime if cyclic? Start with Time roughly + pub duration_minutes: u32, + pub color: String, +} + +pub fn provide_global_state() -> GlobalState { + let (current_time, set_current_time) = create_signal(Utc::now()); + let (preferences, set_preferences) = create_signal(load_preferences()); + // Load routines from local storage (TODO: Add load_routines) + let (routines, set_routines) = create_signal(Vec::::new()); + + // Derived signal or effect for Prayer Times + // Default Istanbul Coords for MVP if not set: 41.0082, 28.9784 + let coords = Coordinates { latitude: 41.0082, longitude: 28.9784 }; + + let (prayer_times, set_prayer_times) = create_signal(None); + + // Ticker + create_effect(move |_| { + let handle = Interval::new(1000, move || { + set_current_time.set(Utc::now()); + }); + move || drop(handle) // Cleanup + }); + + // Update prayer times when day changes + create_effect(move |_| { + let now = current_time.get(); + // Simply recalc every update for MVP or check date change + // Optimization: checked only if date changed? + // For now, let's calc. It's cheap. + let today = now.date_naive(); + let pt = calculate_prayer_times(today, coords, CalculationParams::default()); + set_prayer_times.set(Some(pt)); + }); + + // Save prefs when changed + create_effect(move |_| { + save_preferences(&preferences.get()); + }); + + // Dummy action + let check_update = create_action(move |_| async {}); + + let state = GlobalState { + current_time, + prayer_times, + preferences, + set_preferences, + routines, + set_routines, + check_update + }; + + provide_context(state.clone()); + state +} + +pub fn use_global_state() -> GlobalState { + use_context::().expect("Global State missing") +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..900a8f8 --- /dev/null +++ b/style.css @@ -0,0 +1,88 @@ +:root { + /* Color Palette - Premium HSL optimized */ + --hue-primary: 250; + /* Deep Indigo/Purple */ + --hue-secondary: 190; + /* Cyan/Teal */ + + /* Light Mode Variables */ + --bg-app: hsl(220, 20%, 97%); + --bg-surface: hsla(0, 0%, 100%, 0.7); + --bg-surface-2: hsla(0, 0%, 100%, 0.5); + --text-primary: hsl(220, 40%, 10%); + --text-secondary: hsl(220, 20%, 40%); + --border-color: hsla(220, 20%, 80%, 0.5); + --accent: hsl(var(--hue-primary), 60%, 50%); + --accent-glow: hsla(var(--hue-primary), 60%, 50%, 0.3); + + /* Neumorphic/Glassmorphic Shadows */ + --shadow-sm: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03); + --shadow-glass: + 0 8px 32px 0 rgba(31, 38, 135, 0.05), + inset 0 0 0 1px rgba(255, 255, 255, 0.4); +} + +/* Dark Mode overrides (Media Query OR .dark class) */ +@media (prefers-color-scheme: dark) { + :root { + --bg-app: hsl(222, 47%, 11%); + --bg-surface: hsla(217, 33%, 17%, 0.4); + --bg-surface-2: hsla(217, 33%, 25%, 0.3); + --text-primary: hsl(210, 40%, 98%); + --text-secondary: hsl(215, 20%, 65%); + --border-color: hsla(217, 33%, 50%, 0.1); + --shadow-glass: + 0 8px 32px 0 rgba(0, 0, 0, 0.3), + inset 0 0 0 1px rgba(255, 255, 255, 0.05); + } +} + +/* Force Dark Mode via class */ +body.dark { + --bg-app: hsl(222, 47%, 11%); + --bg-surface: hsla(217, 33%, 17%, 0.4); + --bg-surface-2: hsla(217, 33%, 25%, 0.3); + --text-primary: hsl(210, 40%, 98%); + --text-secondary: hsl(215, 20%, 65%); + --border-color: hsla(217, 33%, 50%, 0.1); + --shadow-glass: + 0 8px 32px 0 rgba(0, 0, 0, 0.3), + inset 0 0 0 1px rgba(255, 255, 255, 0.05); +} + +/* Global Styles */ +body { + background-color: var(--bg-app); + color: var(--text-primary); + font-family: 'Inter', system-ui, -apple-system, sans-serif; + transition: background-color 0.5s ease, color 0.5s ease; + min-height: 100vh; + overflow-x: hidden; +} + +/* Glassmorphism Utilities */ +.glass-panel { + background: var(--bg-surface); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-radius: 16px; + box-shadow: var(--shadow-glass); + border: 1px solid var(--border-color); +} + +.btn-primary { + background: var(--accent); + color: white; + border: none; + padding: 10px 20px; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 4px 15px var(--accent-glow); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 8px 20px var(--accent-glow); +} \ No newline at end of file