commit fa673deaa990eb1626502f8af28d584f71404aa8
parent a5091bcaeb53b4250e24651755a8f44f128bc601
Author: triesap <tyson@radroots.org>
Date: Fri, 20 Mar 2026 15:49:27 +0000
app: reinitialize workspace with native egui bootstrap
- replace the legacy leptos workspace layout with a minimal crates/app bootstrap
- move dependency governance to the workspace root and wire native wgpu backends
- add the macos app-name fix for native menu labeling
- refresh contributor docs, agent guidance, and ignore rules for the standalone rust app
Diffstat:
186 files changed, 1991 insertions(+), 37900 deletions(-)
diff --git a/.cargo/config.toml b/.cargo/config.toml
@@ -1,4 +0,0 @@
-# Local dev defaults for compile-time env access (trunk/cargo).
-[env]
-RADROOTS_DEFAULT_RELAYS = "ws://localhost:8080,ws://localhost:8081"
-RADROOTS_RELAY = "ws://localhost:8082"
diff --git a/.gitignore b/.gitignore
@@ -1,33 +1,18 @@
-/target
+/target/
-# Debug
-logs/
-
-# Local env files
+# Local environment files
.env
.env.*
-!.env.example
-!.env.test
-# OS
+# OS and editor files
.DS_Store
-app/node_modules/
Thumbs.db
-# secrets
+# Local development files
+.vscode/
+.idea/
+
+# Local secrets
*.pem
*.crt
*.key
-*.pem
-
-# local
-.local*
-.vscode
-notes*.txt
-justfile
-
-# dev
-*dev*.toml
-*dev*.json
-
-refs
diff --git a/AGENTS.md b/AGENTS.md
@@ -1,20 +1,85 @@
-# Rad Roots - Code Directives
-
-- this repo is a standalone community-edition OSS app and must remain cloneable and usable from this repo root
-- do not treat the outer workspace as the authoritative runtime or release root for this repo
-- during the current integrated development phase, temporary local path dependencies are allowed only to the public `radroots` crate family and approved developer-controlled vendor repos
-- do not add temporary local path dependencies from this repo to archived code, `refs/*`, or private platform crates without explicit approval
-- validate from this repo root with `cargo metadata --format-version 1 --no-deps` and `cargo check`
-
-## Rust Code Directives
-- Toolchain: Rust 1.92, edition 2024; use workspace versions from the root Cargo.toml.
-- Portability: preserve no_std patterns; gate std usage with cfg(feature = "std") and use alloc when needed.
-- Safety: avoid unsafe; prefer safe, explicit APIs. Add #![forbid(unsafe_code)] on new crates/modules.
-- Public API: keep Radroots* prefix; avoid hidden panics; return Result/Option for fallible ops; use precise error enums (thiserror where appropriate).
-- Features: keep serde/typeshare/ts-rs derives behind existing feature gates and in the current style; ensure feature combinations compile (no_std, std, wasm).
-- Generated outputs: treat */bindings/ts/src/types.ts as generated; do not hand-edit.
-- Performance: borrow over clone, avoid intermediate allocations, preallocate when sizes are known, and prefer iterators over indexing loops.
-- DRY: consolidate shared logic into core/types/events-codec or dedicated helpers.
-- Parity: maintain feature parity across native/wasm layers when adding SQL or Tangle APIs.
-- Module layout: keep lib.rs as a module manifest and re-export surface; avoid heavy logic in lib.rs.
-- Testing: add or update unit tests for new behavior and edge cases, especially around parsing, invariants, conversions, and rounding.
+# Rad Roots Application - Agent Specification
+
+## 1. Scope and hierarchy
+
+- This file applies to the full repository.
+- Keep this file concise, durable, and repository-specific.
+- If a closer directory-level `AGENTS.md` is added later, it overrides this file for that subtree.
+- Put detailed procedures, examples, and temporary migration notes in other checked-in docs, not here.
+
+## 2. Repository identity
+
+- This repository is the standalone Rad Roots application repository.
+- Optimize for durable application structure, explicit boundaries, portability, and clear runtime behavior.
+- Treat this as a public open-source application project: commits, docs, and handoff language should read clearly to contributors who only know this repository.
+- Preserve the repository’s top-level identity files and keep the workspace easy to understand from the root.
+
+## 3. Change discipline
+
+- Prefer the smallest coherent change that fully addresses the request.
+- Do not mix unrelated cleanup, speculative refactors, or roadmap work into the same change.
+- Prefer clean target-state changes over temporary compatibility layers unless compatibility is explicitly required.
+- Remove obsolete code, dependencies, and scaffolding when they are clearly replaced.
+- Do not leave hidden task trackers in source comments, markdown checklists, or stray notes.
+
+## 4. Before editing
+
+Before making substantial changes:
+
+- Read this file, `README.md`, and `CONTRIBUTING.md`.
+- Inspect `git status --short` before broad edits, refactors, or file removals.
+- Read the current implementation and nearby tests before changing behavior.
+- Use checked-in documentation and commands as the source of truth.
+- Do not assume contributor-specific local tooling or machine setup beyond what the repository documents.
+- Surface blockers early when the task depends on unresolved product decisions, missing prerequisites, or unclear architecture boundaries.
+
+## 5. Validation and command surface
+
+- Run validation from the repository root.
+- Prefer the narrowest relevant validation for the files or crate being changed.
+- Use documented commands first. When no narrower repo-specific command is documented, use standard Cargo commands such as:
+ - `cargo metadata --format-version 1 --no-deps`
+ - `cargo check`
+ - targeted `cargo test`
+ - targeted `cargo run -p radroots-app`
+- If validation cannot be run, report the blocker clearly.
+
+## 6. Workspace structure
+
+- Keep the repository root as the workspace root.
+- Keep the main application crate under `crates/app`.
+- Add new crates only when they represent a durable architectural boundary.
+- Keep manifests, paths, and crate boundaries simple and intentional.
+- Do not reintroduce obsolete framework scaffolding unless the requested change explicitly requires it.
+
+## 7. Rust engineering rules
+
+- Use Rust `1.92.0`, edition `2024`, and workspace dependency versions from the root `Cargo.toml`.
+- Prefer safe, explicit APIs and avoid `unsafe`.
+- Keep state, data flow, and side effects understandable; prefer typed models and explicit transitions over stringly APIs or loosely typed maps.
+- Avoid hidden panics in non-test code.
+- Keep module layout and manifests clean; remove dead dependencies, dead modules, and unused feature wiring when they are no longer needed.
+- Keep code readable and direct; avoid unnecessary abstraction in early-stage application code.
+- Add or update deterministic tests when behavior, invariants, parsing, or state transitions change.
+
+## 8. Dependency rules
+
+- Prefer root workspace dependencies where possible.
+- Use canonical upstream crate names in manifests and code.
+- Prefer dependency choices that align with the existing Rad Roots Rust ecosystem when practical.
+- Introduce new dependencies only when they are justified by a clear product or architectural need.
+
+## 9. Commit and handoff rules
+
+- Format commits as `<scope>: <imperative summary>`.
+- Use lowercase scopes.
+- Split unrelated changes into separate commits.
+- Keep commit messages and handoff summaries clear and standalone.
+- In handoff, state what changed, what validation ran, and any remaining risks or assumptions.
+
+## 10. Definition of done
+
+- The requested change is implemented.
+- Replaced or obsolete scaffolding is removed when no longer needed.
+- Relevant validation ran, or a concrete blocker is reported.
+- Any affected documentation or structural context is updated with the code change when necessary.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
@@ -1,107 +1,100 @@
# Contributing
-Rad Roots is an open-source project, and we welcome all contributions — including code improvements, bug fixes, translations, new features, and bug reports.
+Rad Roots is an open-source application. Contributions are welcome, including bug fixes, usability improvements, documentation updates, tests, and new features.
-## Translations
+## Scope
-If you would like to contribute by translating the app into another language, please visit our [localisation repository](https://github.com/radrootslabs/packages-locales). To contribute translations:
+This repository is the standalone Rad Roots application repository. The main application code is organized under `crates/`.
-1. *Fork the repository and add your language files following the existing structure.*
+## Prerequisites
-2. *Submit a Pull Request with your changes.*
+Install the Rust toolchain used by this repository:
-
-If the language you would like to add translations for is not yet set up in the repository let us know by [opening an issue](https://github.com/radrootslabs/packages-locales/issues), or [email us](mailto:support@radroots.dev) and we will assist you in adding the required files.
-
-## Development Environment
-
-Ensure the required system dependencies are installed:
-
-*Rust*
```bash
-$ cargo --version
-> cargo 1.88.0 (873a06493 2025-05-10)
+rustup toolchain install 1.92.0
```
-*NodeJS*
-```bash
-$ node --version
-> v20.18.0
-```
+Confirm your environment:
-*Yarn*
```bash
-$ yarn --version
-> 1.22.22
+cargo --version
+rustc --version
```
-## Building
+## Getting Started
-This app is implemented as a progressive web application using [SvelteKit](https://svelte.dev/), and maintained as a monorepository using [Turbo](https://turborepo.com/) with individual packages tracked as Git submodules.
-
-To begin, first clone the repository and set up your local working copy:
+Clone your fork and enter the repository root:
```bash
-mkdir pwa && cd pwa
+git clone https://github.com/<YOUR-USERNAME>/app.git
+cd app
+```
-git clone https://github.com/radrootslabs/pwa .
+To use the repository-pinned toolchain:
-git remote rename origin upstream
+```bash
+rustup override set 1.92.0
+```
-git remote add origin https://github.com/<YOUR-USERNAME>/pwa.git
+## Development Commands
-git push -u origin master
-```
+Run these commands from the repository root.
-Initialize and update Git submodules:
-```bash
-git submodule update --init --recursive
-```
+Inspect workspace metadata:
-Checkout Git submodules branches:
```bash
-git submodule foreach 'git checkout $(git config -f $toplevel/.gitmodules submodule.$name.branch)'
+cargo metadata --format-version 1 --no-deps
```
-Install the application dependencies:
+Check the application:
+
```bash
-yarn
+cargo check
```
+Run tests:
-Configure local environment variables (optional overrides):
```bash
-cat <<'EOF' > app/.env
-RADROOTS_DEFAULT_RELAYS=ws://localhost:8080,ws://localhost:8081
-RADROOTS_RELAY=ws://localhost:8082
-EOF
+cargo test
```
-Build the application:
-```bash
-yarn build
-```
+Run the native application:
-Run the application in development mode:
```bash
-yarn dev
+cargo run -p radroots-app
```
-## Contributing
+## Contribution Guidelines
+
+- Keep changes scoped to a single coherent change.
+- Prefer small, reviewable commits.
+- Update tests when behavior changes.
+- Update documentation when commands, structure, or contributor workflow changes.
+- Remove obsolete code and dependencies when they are clearly replaced.
+- Use workspace-managed dependency versions from the root [Cargo.toml](/Users/treesap/dev/radroots/radroots-platform-v1/domains/community/apps/app/Cargo.toml).
+
+## Reporting Issues
+
+When reporting a bug, include:
+
+- your operating system and version
+- Rust toolchain version
+- the command you ran
+- the observed behavior
+- the expected behavior
+- logs, screenshots, or backtraces if available
+
+## Submitting Changes
-1. Create a feature branch
-2. Make your changes
-3. Submit a Pull Request
-4. Wait for review and address feedback
+1. Create a branch for your change.
+2. Make the smallest coherent update that solves the issue.
+3. Run the relevant validation commands from this document.
+4. Open a pull request with a clear summary of what changed and how it was verified.
-## Additional Resources
+## Code of Conduct
-- [Rust Documentation](https://www.rust-lang.org/tools/install)
-- [NodeJS Documentation](https://github.com/nvm-sh/nvm?tab=readme-ov-file#installing-and-updating)
-- [Yarn Documentation](https://classic.yarnpkg.com/en/docs)
-- [SvelteKit Documentation](https://svelte.dev/docs/kit/introduction)
-- [Turbo Documentation](https://turborepo.com/docs)
+Be respectful, direct, and constructive in issues and reviews.
## License
-Refer to the LICENSE file in the repository for terms of use and distribution.
+By contributing to this repository, you agree that your contributions will be distributed under the repository's license. See [LICENSE](LICENSE).
diff --git a/Cargo.lock b/Cargo.lock
@@ -3,52 +3,77 @@
version = 4
[[package]]
-name = "aead"
-version = "0.5.2"
+name = "ab_glyph"
+version = "0.2.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
+checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2"
dependencies = [
- "crypto-common",
- "generic-array",
+ "ab_glyph_rasterizer",
+ "owned_ttf_parser",
]
[[package]]
-name = "aes"
-version = "0.8.4"
+name = "ab_glyph_rasterizer"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
+
+[[package]]
+name = "accesskit"
+version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+checksum = "cf203f9d3bd8f29f98833d1fbef628df18f759248a547e7e01cfbf63cda36a99"
dependencies = [
- "cfg-if",
- "cipher",
- "cpufeatures",
+ "enumn",
+ "serde",
]
[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
+ "getrandom",
"once_cell",
+ "serde",
"version_check",
"zerocopy",
]
[[package]]
-name = "aho-corasick"
-version = "1.1.4"
+name = "android-activity"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046"
dependencies = [
- "memchr",
+ "android-properties",
+ "bitflags 2.11.0",
+ "cc",
+ "cesu8",
+ "jni 0.21.1",
+ "jni-sys 0.3.0",
+ "libc",
+ "log",
+ "ndk",
+ "ndk-context",
+ "ndk-sys",
+ "num_enum",
+ "thiserror 1.0.69",
]
[[package]]
-name = "allocator-api2"
-version = "0.2.21"
+name = "android-properties"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
[[package]]
name = "android_system_properties"
@@ -60,245 +85,187 @@ dependencies = [
]
[[package]]
-name = "any_spawner"
-version = "0.3.0"
+name = "arboard"
+version = "3.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1384d3fe1eecb464229fcf6eebb72306591c56bf27b373561489458a7c73027d"
+checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf"
dependencies = [
- "futures",
- "thiserror 2.0.18",
- "wasm-bindgen-futures",
+ "clipboard-win",
+ "image",
+ "log",
+ "objc2 0.6.4",
+ "objc2-app-kit 0.3.2",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation 0.3.2",
+ "parking_lot",
+ "percent-encoding",
+ "windows-sys 0.52.0",
+ "x11rb",
]
[[package]]
-name = "anyhow"
-version = "1.0.102"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
-
-[[package]]
-name = "arraydeque"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
-
-[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
-name = "async-lock"
-version = "3.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
-dependencies = [
- "event-listener",
- "event-listener-strategy",
- "pin-project-lite",
-]
-
-[[package]]
-name = "async-once-cell"
-version = "0.5.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a"
-
-[[package]]
-name = "async-trait"
-version = "0.1.89"
+name = "as-raw-xcb-connection"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
+checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
[[package]]
-name = "async-utility"
-version = "0.3.1"
+name = "ash"
+version = "0.38.0+1.3.281"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a34a3b57207a7a1007832416c3e4862378c8451b4e8e093e436f48c2d3d2c151"
+checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f"
dependencies = [
- "futures-util",
- "gloo-timers",
- "tokio",
- "wasm-bindgen-futures",
+ "libloading",
]
[[package]]
-name = "async-wsocket"
-version = "0.13.2"
+name = "atomic-waker"
+version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1c92385c7c8b3eb2de1b78aeca225212e4c9a69a78b802832759b108681a5069"
-dependencies = [
- "async-utility",
- "futures",
- "futures-util",
- "js-sys",
- "tokio",
- "tokio-rustls",
- "tokio-socks",
- "tokio-tungstenite",
- "url",
- "wasm-bindgen",
- "web-sys",
-]
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
-name = "atomic-destructor"
-version = "0.3.0"
+name = "autocfg"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef49f5882e4b6afaac09ad239a4f8c70a24b8f2b0897edb1f706008efd109cf4"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
-name = "attribute-derive"
-version = "0.10.5"
+name = "bit-set"
+version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05832cdddc8f2650cc2cc187cc2e952b8c133a48eb055f35211f61ee81502d77"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [
- "attribute-derive-macro",
- "derive-where",
- "manyhow",
- "proc-macro2",
- "quote",
- "syn",
+ "bit-vec",
]
[[package]]
-name = "attribute-derive-macro"
-version = "0.10.5"
+name = "bit-vec"
+version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a7cdbbd4bd005c5d3e2e9c885e6fa575db4f4a3572335b974d8db853b6beb61"
-dependencies = [
- "collection_literals",
- "interpolator",
- "manyhow",
- "proc-macro-utils",
- "proc-macro2",
- "quote",
- "quote-use",
- "syn",
-]
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]]
-name = "autocfg"
-version = "1.5.0"
+name = "bitflags"
+version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
-name = "base16"
-version = "0.2.1"
+name = "bitflags"
+version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d27c3610c36aee21ce8ac510e6224498de4228ad772a171ed65643a24693a5a8"
+checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
-name = "base64"
-version = "0.21.7"
+name = "block"
+version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
[[package]]
-name = "base64"
-version = "0.22.1"
+name = "block2"
+version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
+dependencies = [
+ "objc2 0.5.2",
+]
[[package]]
-name = "base64ct"
-version = "1.8.3"
+name = "bumpalo"
+version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
-name = "bech32"
-version = "0.11.1"
+name = "bytemuck"
+version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f"
+checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
+dependencies = [
+ "bytemuck_derive",
+]
[[package]]
-name = "bip39"
-version = "2.2.2"
+name = "bytemuck_derive"
+version = "1.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc"
+checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
dependencies = [
- "bitcoin_hashes",
- "serde",
- "unicode-normalization",
+ "proc-macro2",
+ "quote",
+ "syn",
]
[[package]]
-name = "bitcoin-io"
-version = "0.1.4"
+name = "byteorder-lite"
+version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
-name = "bitcoin_hashes"
-version = "0.14.1"
+name = "bytes"
+version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b"
-dependencies = [
- "bitcoin-io",
- "hex-conservative",
- "serde",
-]
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
-name = "bitflags"
-version = "2.11.0"
+name = "calloop"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
+checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
dependencies = [
- "serde_core",
+ "bitflags 2.11.0",
+ "log",
+ "polling",
+ "rustix 0.38.44",
+ "slab",
+ "thiserror 1.0.69",
]
[[package]]
-name = "block-buffer"
-version = "0.10.4"
+name = "calloop"
+version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7"
dependencies = [
- "generic-array",
+ "bitflags 2.11.0",
+ "polling",
+ "rustix 1.1.4",
+ "slab",
+ "tracing",
]
[[package]]
-name = "block-padding"
-version = "0.3.3"
+name = "calloop-wayland-source"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
+checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
dependencies = [
- "generic-array",
+ "calloop 0.13.0",
+ "rustix 0.38.44",
+ "wayland-backend",
+ "wayland-client",
]
[[package]]
-name = "bumpalo"
-version = "3.20.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
-
-[[package]]
-name = "bytes"
-version = "1.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
-
-[[package]]
-name = "camino"
-version = "1.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48"
-
-[[package]]
-name = "cbc"
-version = "0.1.2"
+name = "calloop-wayland-source"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
+checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa"
dependencies = [
- "cipher",
+ "calloop 0.14.4",
+ "rustix 1.1.4",
+ "wayland-backend",
+ "wayland-client",
]
[[package]]
@@ -308,81 +275,67 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
dependencies = [
"find-msvc-tools",
+ "jobserver",
+ "libc",
"shlex",
]
[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
+[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
-name = "chacha20"
-version = "0.9.1"
+name = "cfg_aliases"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
-dependencies = [
- "cfg-if",
- "cipher",
- "cpufeatures",
-]
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
-name = "chacha20poly1305"
-version = "0.10.1"
+name = "cgl"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
+checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff"
dependencies = [
- "aead",
- "chacha20",
- "cipher",
- "poly1305",
- "zeroize",
+ "libc",
]
[[package]]
-name = "chrono"
-version = "0.4.44"
+name = "clipboard-win"
+version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
+checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4"
dependencies = [
- "iana-time-zone",
- "js-sys",
- "num-traits",
- "wasm-bindgen",
- "windows-link",
+ "error-code",
]
[[package]]
-name = "cipher"
-version = "0.4.4"
+name = "codespan-reporting"
+version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81"
dependencies = [
- "crypto-common",
- "inout",
- "zeroize",
+ "unicode-width",
]
[[package]]
-name = "codee"
-version = "0.3.5"
+name = "combine"
+version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9dbbdc4b4d349732bc6690de10a9de952bd39ba6a065c586e26600b6b0b91f5"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
dependencies = [
- "serde",
- "serde_json",
- "thiserror 2.0.18",
+ "bytes",
+ "memchr",
]
[[package]]
-name = "collection_literals"
-version = "1.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084"
-
-[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -392,260 +345,319 @@ dependencies = [
]
[[package]]
-name = "config"
-version = "0.14.1"
+name = "core-foundation"
+version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
- "async-trait",
- "convert_case 0.6.0",
- "json5",
- "nom",
- "pathdiff",
- "ron",
- "rust-ini",
- "serde",
- "serde_json",
- "toml 0.8.23",
- "yaml-rust2",
+ "core-foundation-sys",
+ "libc",
]
[[package]]
-name = "config"
-version = "0.15.22"
+name = "core-foundation"
+version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e68cfe19cd7d23ffde002c24ffa5cda73931913ef394d5eaaa32037dc940c0c"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
- "convert_case 0.6.0",
- "pathdiff",
- "serde_core",
- "toml 1.0.7+spec-1.1.0",
- "winnow 1.0.0",
+ "core-foundation-sys",
+ "libc",
]
[[package]]
-name = "console_error_panic_hook"
-version = "0.1.7"
+name = "core-foundation-sys"
+version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
-dependencies = [
- "cfg-if",
- "wasm-bindgen",
-]
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
-name = "const-random"
-version = "0.1.18"
+name = "core-graphics"
+version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
+checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
dependencies = [
- "const-random-macro",
+ "bitflags 1.3.2",
+ "core-foundation 0.9.4",
+ "core-graphics-types 0.1.3",
+ "foreign-types",
+ "libc",
]
[[package]]
-name = "const-random-macro"
-version = "0.1.16"
+name = "core-graphics-types"
+version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
+checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
dependencies = [
- "getrandom 0.2.17",
- "once_cell",
- "tiny-keccak",
+ "bitflags 1.3.2",
+ "core-foundation 0.9.4",
+ "libc",
]
[[package]]
-name = "const-str"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f"
-
-[[package]]
-name = "const_format"
-version = "0.2.35"
+name = "core-graphics-types"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad"
+checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [
- "const_format_proc_macros",
+ "bitflags 2.11.0",
+ "core-foundation 0.10.1",
+ "libc",
]
[[package]]
-name = "const_format_proc_macros"
-version = "0.2.34"
+name = "crc32fast"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
- "proc-macro2",
- "quote",
- "unicode-xid",
+ "cfg-if",
]
[[package]]
-name = "const_str_slice_concat"
-version = "0.1.0"
+name = "crossbeam-utils"
+version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f67855af358fcb20fac58f9d714c94e2b228fe5694c1c9b4ead4a366343eda1b"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
-name = "convert_case"
-version = "0.6.0"
+name = "crunchy"
+version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
-dependencies = [
- "unicode-segmentation",
-]
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
-name = "convert_case"
-version = "0.11.0"
+name = "cursor-icon"
+version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49"
-dependencies = [
- "unicode-segmentation",
-]
+checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
[[package]]
-name = "convert_case_extras"
+name = "dispatch"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "589c70f0faf8aa9d17787557d5eae854d7755cac50f5c3d12c81d3d57661cebb"
+checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
+
+[[package]]
+name = "dispatch2"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [
- "convert_case 0.11.0",
+ "bitflags 2.11.0",
+ "objc2 0.6.4",
]
[[package]]
-name = "core-foundation-sys"
-version = "0.8.7"
+name = "displaydoc"
+version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
[[package]]
-name = "cpufeatures"
-version = "0.2.17"
+name = "dlib"
+version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a"
dependencies = [
- "libc",
+ "libloading",
]
[[package]]
-name = "crossbeam-channel"
-version = "0.5.15"
+name = "document-features"
+version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
dependencies = [
- "crossbeam-utils",
+ "litrs",
]
[[package]]
-name = "crossbeam-utils"
-version = "0.8.21"
+name = "downcast-rs"
+version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
-name = "crunchy"
-version = "0.2.4"
+name = "dpi"
+version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
[[package]]
-name = "crypto-common"
-version = "0.1.7"
+name = "ecolor"
+version = "0.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+checksum = "71ddb8ac7643d1dba1bb02110e804406dd459a838efcb14011ced10556711a8e"
dependencies = [
- "generic-array",
- "rand_core 0.6.4",
- "typenum",
+ "bytemuck",
+ "emath",
+ "serde",
]
[[package]]
-name = "data-encoding"
-version = "2.10.0"
+name = "eframe"
+version = "0.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
+checksum = "457481173e6db5ca9fa2be93a58df8f4c7be639587aeb4853b526c6cf87db4e6"
+dependencies = [
+ "ahash",
+ "bytemuck",
+ "document-features",
+ "egui",
+ "egui-wgpu",
+ "egui-winit",
+ "egui_glow",
+ "glutin",
+ "glutin-winit",
+ "image",
+ "js-sys",
+ "log",
+ "objc2 0.5.2",
+ "objc2-app-kit 0.2.2",
+ "objc2-foundation 0.2.2",
+ "parking_lot",
+ "percent-encoding",
+ "pollster",
+ "profiling",
+ "raw-window-handle",
+ "static_assertions",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "web-time",
+ "wgpu",
+ "windows-sys 0.61.2",
+ "winit",
+]
[[package]]
-name = "deranged"
-version = "0.5.8"
+name = "egui"
+version = "0.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+checksum = "6a9b567d356674e9a5121ed3fedfb0a7c31e059fe71f6972b691bcd0bfc284e3"
dependencies = [
- "powerfmt",
+ "accesskit",
+ "ahash",
+ "bitflags 2.11.0",
+ "emath",
+ "epaint",
+ "log",
+ "nohash-hasher",
+ "profiling",
+ "serde",
+ "smallvec",
+ "unicode-segmentation",
]
[[package]]
-name = "derive-where"
-version = "1.6.1"
+name = "egui-wgpu"
+version = "0.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534"
+checksum = "5e4d209971c84b2352a06174abdba701af1e552ce56b144d96f2bd50a3c91236"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "ahash",
+ "bytemuck",
+ "document-features",
+ "egui",
+ "epaint",
+ "log",
+ "profiling",
+ "thiserror 2.0.18",
+ "type-map",
+ "web-time",
+ "wgpu",
+ "winit",
]
[[package]]
-name = "digest"
-version = "0.10.7"
+name = "egui-winit"
+version = "0.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+checksum = "ec6687e5bb551702f4ad10ac428bab12acf9d53047ebb1082d4a0ed8c6251a29"
dependencies = [
- "block-buffer",
- "crypto-common",
- "subtle",
+ "arboard",
+ "bytemuck",
+ "egui",
+ "log",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+ "objc2-ui-kit",
+ "profiling",
+ "raw-window-handle",
+ "smithay-clipboard",
+ "web-time",
+ "webbrowser",
+ "winit",
]
[[package]]
-name = "displaydoc"
-version = "0.2.5"
+name = "egui_glow"
+version = "0.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+checksum = "6420863ea1d90e750f75075231a260030ad8a9f30a7cef82cdc966492dc4c4eb"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "bytemuck",
+ "egui",
+ "glow",
+ "log",
+ "memoffset",
+ "profiling",
+ "wasm-bindgen",
+ "web-sys",
+ "winit",
]
[[package]]
-name = "dlv-list"
-version = "0.5.2"
+name = "emath"
+version = "0.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
+checksum = "491bdf728bf25ddd9ad60d4cf1c48588fa82c013a2440b91aa7fc43e34a07c32"
dependencies = [
- "const-random",
+ "bytemuck",
+ "serde",
]
[[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"
+name = "enumn"
+version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
[[package]]
-name = "either_of"
-version = "0.1.8"
+name = "epaint"
+version = "0.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "14f7f86eef3a7e4b9c2107583dbbbe3d9535c4b800796faf1774b82ba22033da"
+checksum = "009d0dd3c2163823a0abdb899451ecbc78798dec545ee91b43aff1fa790bab62"
dependencies = [
- "paste",
- "pin-project-lite",
+ "ab_glyph",
+ "ahash",
+ "bytemuck",
+ "ecolor",
+ "emath",
+ "epaint_default_fonts",
+ "log",
+ "nohash-hasher",
+ "parking_lot",
+ "profiling",
+ "serde",
]
[[package]]
-name = "encoding_rs"
-version = "0.8.35"
+name = "epaint_default_fonts"
+version = "0.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
-dependencies = [
- "cfg-if",
-]
+checksum = "5c4fbe202b6578d3d56428fa185cdf114a05e49da05f477b3c7f0fbb221f1862"
[[package]]
name = "equivalent"
@@ -654,12 +666,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
-name = "erased"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1731451909bde27714eacba19c2566362a7f35224f52b153d3f42cf60f72472"
-
-[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -670,43 +676,39 @@ dependencies = [
]
[[package]]
-name = "event-listener"
-version = "5.4.1"
+name = "error-code"
+version = "3.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
-dependencies = [
- "concurrent-queue",
- "parking",
- "pin-project-lite",
-]
+checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
[[package]]
-name = "event-listener-strategy"
-version = "0.5.4"
+name = "fax"
+version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab"
dependencies = [
- "event-listener",
- "pin-project-lite",
+ "fax_derive",
]
[[package]]
-name = "fallible-iterator"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
-
-[[package]]
-name = "fallible-streaming-iterator"
-version = "0.1.9"
+name = "fax_derive"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
+checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
[[package]]
-name = "fastrand"
-version = "2.3.0"
+name = "fdeflate"
+version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
+dependencies = [
+ "simd-adler32",
+]
[[package]]
name = "find-msvc-tools"
@@ -715,84 +717,68 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
-name = "foldhash"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
-
-[[package]]
-name = "form_urlencoded"
-version = "1.2.2"
+name = "flate2"
+version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
- "percent-encoding",
+ "crc32fast",
+ "miniz_oxide",
]
[[package]]
-name = "futures"
-version = "0.3.32"
+name = "foldhash"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
-dependencies = [
- "futures-channel",
- "futures-core",
- "futures-executor",
- "futures-io",
- "futures-sink",
- "futures-task",
- "futures-util",
-]
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
-name = "futures-channel"
-version = "0.3.32"
+name = "foldhash"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
-dependencies = [
- "futures-core",
- "futures-sink",
-]
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
-name = "futures-core"
-version = "0.3.32"
+name = "foreign-types"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
+dependencies = [
+ "foreign-types-macros",
+ "foreign-types-shared",
+]
[[package]]
-name = "futures-executor"
-version = "0.3.32"
+name = "foreign-types-macros"
+version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
+checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [
- "futures-core",
- "futures-task",
- "futures-util",
+ "proc-macro2",
+ "quote",
+ "syn",
]
[[package]]
-name = "futures-io"
-version = "0.3.32"
+name = "foreign-types-shared"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
-name = "futures-macro"
-version = "0.3.32"
+name = "form_urlencoded"
+version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "percent-encoding",
]
[[package]]
-name = "futures-sink"
+name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-task"
@@ -806,38 +792,20 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
- "futures-channel",
"futures-core",
- "futures-io",
- "futures-macro",
- "futures-sink",
"futures-task",
- "memchr",
"pin-project-lite",
"slab",
]
[[package]]
-name = "generic-array"
-version = "0.14.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
-dependencies = [
- "typenum",
- "version_check",
-]
-
-[[package]]
-name = "getrandom"
-version = "0.2.17"
+name = "gethostname"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
dependencies = [
- "cfg-if",
- "js-sys",
- "libc",
- "wasi",
- "wasm-bindgen",
+ "rustix 1.1.4",
+ "windows-link",
]
[[package]]
@@ -848,252 +816,191 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
- "r-efi 5.3.0",
+ "r-efi",
"wasip2",
]
[[package]]
-name = "getrandom"
-version = "0.4.2"
+name = "gl_generator"
+version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d"
dependencies = [
- "cfg-if",
- "js-sys",
- "libc",
- "r-efi 6.0.0",
- "wasip2",
- "wasip3",
- "wasm-bindgen",
+ "khronos_api",
+ "log",
+ "xml-rs",
]
[[package]]
-name = "gloo-net"
-version = "0.6.0"
+name = "glow"
+version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580"
+checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08"
dependencies = [
- "futures-channel",
- "futures-core",
- "futures-sink",
- "gloo-utils",
- "http",
"js-sys",
- "pin-project",
- "serde",
- "serde_json",
- "thiserror 1.0.69",
+ "slotmap",
"wasm-bindgen",
- "wasm-bindgen-futures",
"web-sys",
]
[[package]]
-name = "gloo-timers"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
-dependencies = [
- "futures-channel",
- "futures-core",
- "js-sys",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "gloo-utils"
-version = "0.2.0"
+name = "glutin"
+version = "0.32.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
+checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325"
dependencies = [
- "js-sys",
- "serde",
- "serde_json",
- "wasm-bindgen",
- "web-sys",
+ "bitflags 2.11.0",
+ "cfg_aliases",
+ "cgl",
+ "dispatch2",
+ "glutin_egl_sys",
+ "glutin_glx_sys",
+ "glutin_wgl_sys",
+ "libloading",
+ "objc2 0.6.4",
+ "objc2-app-kit 0.3.2",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.2",
+ "once_cell",
+ "raw-window-handle",
+ "wayland-sys",
+ "windows-sys 0.52.0",
+ "x11-dl",
]
[[package]]
-name = "guardian"
-version = "1.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f"
-
-[[package]]
-name = "hashbrown"
-version = "0.14.5"
+name = "glutin-winit"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f"
dependencies = [
- "ahash",
- "allocator-api2",
+ "cfg_aliases",
+ "glutin",
+ "raw-window-handle",
+ "winit",
]
[[package]]
-name = "hashbrown"
-version = "0.15.5"
+name = "glutin_egl_sys"
+version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2"
dependencies = [
- "foldhash",
+ "gl_generator",
+ "windows-sys 0.52.0",
]
[[package]]
-name = "hashbrown"
-version = "0.16.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
-
-[[package]]
-name = "hashlink"
-version = "0.8.4"
+name = "glutin_glx_sys"
+version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
+checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185"
dependencies = [
- "hashbrown 0.14.5",
+ "gl_generator",
+ "x11-dl",
]
[[package]]
-name = "hashlink"
-version = "0.9.1"
+name = "glutin_wgl_sys"
+version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
-dependencies = [
- "hashbrown 0.14.5",
-]
-
-[[package]]
-name = "headless-primitives-core"
-version = "0.1.0"
-
-[[package]]
-name = "headless-primitives-leptos"
-version = "0.1.0"
+checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e"
dependencies = [
- "headless-primitives-core",
- "leptos",
- "send_wrapper",
- "wasm-bindgen",
- "web-sys",
+ "gl_generator",
]
[[package]]
-name = "heck"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
-
-[[package]]
-name = "hex"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
-
-[[package]]
-name = "hex-conservative"
-version = "0.2.2"
+name = "gpu-alloc"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f"
+checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171"
dependencies = [
- "arrayvec",
+ "bitflags 2.11.0",
+ "gpu-alloc-types",
]
[[package]]
-name = "hmac"
-version = "0.12.1"
+name = "gpu-alloc-types"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4"
dependencies = [
- "digest",
+ "bitflags 2.11.0",
]
[[package]]
-name = "html-escape"
-version = "0.2.13"
+name = "gpu-allocator"
+version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476"
+checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd"
dependencies = [
- "utf8-width",
+ "log",
+ "presser",
+ "thiserror 1.0.69",
+ "windows",
]
[[package]]
-name = "http"
-version = "1.4.0"
+name = "gpu-descriptor"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
+checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca"
dependencies = [
- "bytes",
- "itoa",
+ "bitflags 2.11.0",
+ "gpu-descriptor-types",
+ "hashbrown 0.15.5",
]
[[package]]
-name = "httparse"
-version = "1.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
-
-[[package]]
-name = "hydration_context"
-version = "0.3.0"
+name = "gpu-descriptor-types"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8714ae4adeaa846d838f380fbd72f049197de629948f91bf045329e0cf0a283"
+checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91"
dependencies = [
- "futures",
- "once_cell",
- "or_poisoned",
- "pin-project-lite",
- "serde",
- "throw_error",
+ "bitflags 2.11.0",
]
[[package]]
-name = "iana-time-zone"
-version = "0.1.65"
+name = "half"
+version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
+checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
- "android_system_properties",
- "core-foundation-sys",
- "iana-time-zone-haiku",
- "js-sys",
- "log",
- "wasm-bindgen",
- "windows-core",
+ "cfg-if",
+ "crunchy",
+ "num-traits",
+ "zerocopy",
]
[[package]]
-name = "iana-time-zone-haiku"
-version = "0.1.2"
+name = "hashbrown"
+version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
- "cc",
+ "foldhash 0.1.5",
]
[[package]]
-name = "icondata"
-version = "0.7.0"
+name = "hashbrown"
+version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "412c95f764f89aad1a8505aa4273d931b66d5a26d1e6609a37243784a9128ad9"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
- "icondata_core",
- "icondata_lu",
+ "foldhash 0.2.0",
]
[[package]]
-name = "icondata_core"
-version = "0.1.0"
+name = "hermit-abi"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c97be924215abd5e630d84e95a47c710138a6559b4c55039f4f33aa897fa859"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
-name = "icondata_lu"
-version = "0.1.0"
+name = "hexf-parse"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f25663a6cc5371e9e33c98bab870f4aa2fd68c88853f1467c8fccfaf112782b1"
-dependencies = [
- "icondata_core",
-]
+checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]]
name = "icu_collections"
@@ -1177,12 +1084,6 @@ dependencies = [
]
[[package]]
-name = "id-arena"
-version = "2.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
-
-[[package]]
name = "idna"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1204,6 +1105,20 @@ dependencies = [
]
[[package]]
+name = "image"
+version = "0.25.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
+dependencies = [
+ "bytemuck",
+ "byteorder-lite",
+ "moxcms",
+ "num-traits",
+ "png",
+ "tiff",
+]
+
+[[package]]
name = "indexmap"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1211,268 +1126,155 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown 0.16.1",
- "serde",
- "serde_core",
]
[[package]]
-name = "inout"
-version = "0.1.4"
+name = "jni"
+version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
dependencies = [
- "block-padding",
- "generic-array",
+ "cesu8",
+ "cfg-if",
+ "combine",
+ "jni-sys 0.3.0",
+ "log",
+ "thiserror 1.0.69",
+ "walkdir",
+ "windows-sys 0.45.0",
]
[[package]]
-name = "instant"
-version = "0.1.13"
+name = "jni"
+version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
+checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
dependencies = [
"cfg-if",
- "js-sys",
- "wasm-bindgen",
- "web-sys",
+ "combine",
+ "jni-macros",
+ "jni-sys 0.4.1",
+ "log",
+ "simd_cesu8",
+ "thiserror 2.0.18",
+ "walkdir",
+ "windows-link",
]
[[package]]
-name = "interpolator"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8"
-
-[[package]]
-name = "itertools"
-version = "0.14.0"
+name = "jni-macros"
+version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
dependencies = [
- "either",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "simd_cesu8",
+ "syn",
]
[[package]]
-name = "itoa"
-version = "1.0.18"
+name = "jni-sys"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
-name = "js-sys"
-version = "0.3.91"
+name = "jni-sys"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
+checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
dependencies = [
- "once_cell",
- "wasm-bindgen",
+ "jni-sys-macros",
]
[[package]]
-name = "json5"
+name = "jni-sys-macros"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
+checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
dependencies = [
- "pest",
- "pest_derive",
- "serde",
+ "quote",
+ "syn",
]
[[package]]
-name = "lazy_static"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
-
-[[package]]
-name = "leb128fmt"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
-
-[[package]]
-name = "leptos"
-version = "0.8.17"
+name = "jobserver"
+version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b540ac2868724738f0f5d00f00ec4640e587223774219c1baddc46bad46fb8e"
+checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
- "any_spawner",
- "cfg-if",
- "either_of",
- "futures",
- "getrandom 0.4.2",
- "hydration_context",
- "leptos_config",
- "leptos_dom",
- "leptos_hot_reload",
- "leptos_macro",
- "leptos_server",
- "oco_ref",
- "or_poisoned",
- "paste",
- "reactive_graph",
- "rustc-hash",
- "rustc_version",
- "send_wrapper",
- "serde",
- "serde_json",
- "serde_qs",
- "server_fn",
- "slotmap",
- "tachys",
- "thiserror 2.0.18",
- "throw_error",
- "typed-builder",
- "typed-builder-macro",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "wasm_split_helpers",
- "web-sys",
+ "getrandom",
+ "libc",
]
[[package]]
-name = "leptos_config"
-version = "0.8.9"
+name = "js-sys"
+version = "0.3.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19a2ac32008dda0d657f2147cc33336f4e743e091597db10f7a99d668e92a46d"
+checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
dependencies = [
- "config 0.15.22",
- "regex",
- "serde",
- "thiserror 2.0.18",
- "typed-builder",
+ "once_cell",
+ "wasm-bindgen",
]
[[package]]
-name = "leptos_dom"
-version = "0.8.8"
+name = "khronos-egl"
+version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35742e9ed8f8aaf9e549b454c68a7ac0992536e06856365639b111f72ab07884"
+checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76"
dependencies = [
- "js-sys",
- "or_poisoned",
- "reactive_graph",
- "send_wrapper",
- "tachys",
- "wasm-bindgen",
- "web-sys",
+ "libc",
+ "libloading",
+ "pkg-config",
]
[[package]]
-name = "leptos_hot_reload"
-version = "0.8.6"
+name = "khronos_api"
+version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d2a0f220c8a5ef3c51199dfb9cdd702bc0eb80d52fbe70c7890adfaaae8a4b1"
-dependencies = [
- "anyhow",
- "camino",
- "indexmap",
- "or_poisoned",
- "proc-macro2",
- "quote",
- "rstml",
- "serde",
- "syn",
- "walkdir",
-]
+checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
-name = "leptos_macro"
-version = "0.8.15"
+name = "libc"
+version = "0.2.183"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712325a77f1d050bf2897061ccaf2b075930aab36954980d658f04452686c474"
-dependencies = [
- "attribute-derive",
- "cfg-if",
- "convert_case 0.11.0",
- "convert_case_extras",
- "html-escape",
- "itertools",
- "leptos_hot_reload",
- "prettyplease",
- "proc-macro-error2",
- "proc-macro2",
- "quote",
- "rstml",
- "rustc_version",
- "server_fn_macro",
- "syn",
- "uuid",
-]
+checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
[[package]]
-name = "leptos_router"
-version = "0.8.12"
+name = "libloading"
+version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35058d4096407b8369843b5b5d227588dfd57ecc9e9bda0567523f084dce69e8"
+checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
dependencies = [
- "any_spawner",
- "either_of",
- "futures",
- "gloo-net",
- "js-sys",
- "leptos",
- "leptos_router_macro",
- "or_poisoned",
- "reactive_graph",
- "rustc_version",
- "send_wrapper",
- "tachys",
- "thiserror 2.0.18",
- "url",
- "wasm-bindgen",
- "web-sys",
+ "cfg-if",
+ "windows-link",
]
[[package]]
-name = "leptos_router_macro"
-version = "0.8.6"
+name = "libm"
+version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "409c0bd99f986c3cfa1a4db2443c835bc602ded1a12784e22ecb28c3ed5a2ae2"
-dependencies = [
- "proc-macro-error2",
- "proc-macro2",
- "quote",
- "syn",
-]
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
-name = "leptos_server"
-version = "0.8.7"
+name = "libredox"
+version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da974775c5ccbb6bd64be7f53f75e8321542e28f21563a416574dbe4d5447eae"
+checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
dependencies = [
- "any_spawner",
- "base64 0.22.1",
- "codee",
- "futures",
- "hydration_context",
- "or_poisoned",
- "reactive_graph",
- "send_wrapper",
- "serde",
- "serde_json",
- "server_fn",
- "tachys",
+ "bitflags 2.11.0",
+ "libc",
+ "plain",
+ "redox_syscall 0.7.3",
]
[[package]]
-name = "libc"
-version = "0.2.183"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
-
-[[package]]
-name = "libsqlite3-sys"
-version = "0.28.0"
+name = "linux-raw-sys"
+version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f"
-dependencies = [
- "cc",
- "pkg-config",
- "vcpkg",
-]
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "linux-raw-sys"
@@ -1487,47 +1289,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
[[package]]
-name = "log"
-version = "0.4.29"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
-
-[[package]]
-name = "lru"
-version = "0.16.3"
+name = "litrs"
+version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
+checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
[[package]]
-name = "manyhow"
-version = "0.11.4"
+name = "lock_api"
+version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
- "manyhow-macros",
- "proc-macro2",
- "quote",
- "syn",
+ "scopeguard",
]
[[package]]
-name = "manyhow-macros"
-version = "0.11.4"
+name = "log"
+version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495"
-dependencies = [
- "proc-macro-utils",
- "proc-macro2",
- "quote",
-]
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
-name = "matchers"
-version = "0.2.0"
+name = "malloc_buf"
+version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
dependencies = [
- "regex-automata",
+ "libc",
]
[[package]]
@@ -1537,147 +1325,119 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
-name = "mf2-i18n-core"
-version = "0.1.0"
-
-[[package]]
-name = "mf2-i18n-embedded"
-version = "0.1.0"
-dependencies = [
- "mf2-i18n-core",
-]
-
-[[package]]
-name = "minimal-lexical"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
-
-[[package]]
-name = "mio"
-version = "1.1.1"
+name = "memmap2"
+version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
+checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3"
dependencies = [
"libc",
- "wasi",
- "windows-sys 0.61.2",
]
[[package]]
-name = "negentropy"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d"
-
-[[package]]
-name = "next_tuple"
-version = "0.1.0"
+name = "memoffset"
+version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "60993920e071b0c9b66f14e2b32740a4e27ffc82854dcd72035887f336a09a28"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
[[package]]
-name = "nom"
-version = "7.1.3"
+name = "metal"
+version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605"
dependencies = [
- "memchr",
- "minimal-lexical",
+ "bitflags 2.11.0",
+ "block",
+ "core-graphics-types 0.2.0",
+ "foreign-types",
+ "log",
+ "objc",
+ "paste",
]
[[package]]
-name = "nostr"
-version = "0.44.2"
+name = "miniz_oxide"
+version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3aa5e3b6a278ed061835fe1ee293b71641e6bf8b401cfe4e1834bbf4ef0a34e1"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
- "aes",
- "base64 0.22.1",
- "bech32",
- "bip39",
- "bitcoin_hashes",
- "cbc",
- "chacha20",
- "chacha20poly1305",
- "getrandom 0.2.17",
- "hex",
- "instant",
- "scrypt",
- "secp256k1",
- "serde",
- "serde_json",
- "unicode-normalization",
- "url",
+ "adler2",
+ "simd-adler32",
]
[[package]]
-name = "nostr-database"
-version = "0.44.0"
+name = "moxcms"
+version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7462c9d8ae5ef6a28d66a192d399ad2530f1f2130b13186296dbb11bdef5b3d1"
+checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
dependencies = [
- "lru",
- "nostr",
- "tokio",
+ "num-traits",
+ "pxfm",
]
[[package]]
-name = "nostr-gossip"
-version = "0.44.0"
+name = "naga"
+version = "27.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ade30de16869618919c6b5efc8258f47b654a98b51541eb77f85e8ec5e3c83a6"
+checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8"
dependencies = [
- "nostr",
+ "arrayvec",
+ "bit-set",
+ "bitflags 2.11.0",
+ "cfg-if",
+ "cfg_aliases",
+ "codespan-reporting",
+ "half",
+ "hashbrown 0.16.1",
+ "hexf-parse",
+ "indexmap",
+ "libm",
+ "log",
+ "num-traits",
+ "once_cell",
+ "rustc-hash 1.1.0",
+ "spirv",
+ "thiserror 2.0.18",
+ "unicode-ident",
]
[[package]]
-name = "nostr-relay-pool"
-version = "0.44.0"
+name = "ndk"
+version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b1073ccfbaea5549fb914a9d52c68dab2aecda61535e5143dd73e95445a804b"
+checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
dependencies = [
- "async-utility",
- "async-wsocket",
- "atomic-destructor",
- "hex",
- "lru",
- "negentropy",
- "nostr",
- "nostr-database",
- "tokio",
- "tracing",
+ "bitflags 2.11.0",
+ "jni-sys 0.3.0",
+ "log",
+ "ndk-sys",
+ "num_enum",
+ "raw-window-handle",
+ "thiserror 1.0.69",
]
[[package]]
-name = "nostr-sdk"
-version = "0.44.1"
+name = "ndk-context"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "471732576710e779b64f04c55e3f8b5292f865fea228436daf19694f0bf70393"
-dependencies = [
- "async-utility",
- "nostr",
- "nostr-database",
- "nostr-gossip",
- "nostr-relay-pool",
- "tokio",
- "tracing",
-]
+checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
[[package]]
-name = "nu-ansi-term"
-version = "0.50.3"
+name = "ndk-sys"
+version = "0.6.0+11769913"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
dependencies = [
- "windows-sys 0.61.2",
+ "jni-sys 0.3.0",
]
[[package]]
-name = "num-conv"
+name = "nohash-hasher"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
+checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
[[package]]
name = "num-traits"
@@ -1686,1534 +1446,893 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
+ "libm",
]
[[package]]
-name = "oco_ref"
-version = "0.2.1"
+name = "num_enum"
+version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed0423ff9973dea4d6bd075934fdda86ebb8c05bdf9d6b0507067d4a1226371d"
+checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
dependencies = [
- "serde",
- "thiserror 2.0.18",
+ "num_enum_derive",
+ "rustversion",
]
[[package]]
-name = "once_cell"
-version = "1.21.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
-
-[[package]]
-name = "opaque-debug"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
-
-[[package]]
-name = "or_poisoned"
-version = "0.1.0"
+name = "num_enum_derive"
+version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c04f5d74368e4d0dfe06c45c8627c81bd7c317d52762d118fb9b3076f6420fd"
+checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
[[package]]
-name = "ordered-multimap"
-version = "0.7.3"
+name = "objc"
+version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
+checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
dependencies = [
- "dlv-list",
- "hashbrown 0.14.5",
+ "malloc_buf",
]
[[package]]
-name = "parking"
-version = "2.2.1"
+name = "objc-sys"
+version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310"
[[package]]
-name = "password-hash"
-version = "0.5.0"
+name = "objc2"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
+checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
dependencies = [
- "base64ct",
- "rand_core 0.6.4",
- "subtle",
+ "objc-sys",
+ "objc2-encode",
]
[[package]]
-name = "paste"
-version = "1.0.15"
+name = "objc2"
+version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
+dependencies = [
+ "objc2-encode",
+]
[[package]]
-name = "pathdiff"
-version = "0.2.3"
+name = "objc2-app-kit"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
+checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
+dependencies = [
+ "bitflags 2.11.0",
+ "block2",
+ "libc",
+ "objc2 0.5.2",
+ "objc2-core-data",
+ "objc2-core-image",
+ "objc2-foundation 0.2.2",
+ "objc2-quartz-core",
+]
[[package]]
-name = "pbkdf2"
-version = "0.12.2"
+name = "objc2-app-kit"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
+checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
dependencies = [
- "digest",
- "hmac",
+ "bitflags 2.11.0",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation 0.3.2",
]
[[package]]
-name = "percent-encoding"
-version = "2.3.2"
+name = "objc2-cloud-kit"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
+dependencies = [
+ "bitflags 2.11.0",
+ "block2",
+ "objc2 0.5.2",
+ "objc2-core-location",
+ "objc2-foundation 0.2.2",
+]
[[package]]
-name = "pest"
-version = "2.8.6"
+name = "objc2-contacts"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662"
+checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
dependencies = [
- "memchr",
- "ucd-trie",
+ "block2",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
]
[[package]]
-name = "pest_derive"
-version = "2.8.6"
+name = "objc2-core-data"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77"
+checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
dependencies = [
- "pest",
- "pest_generator",
+ "bitflags 2.11.0",
+ "block2",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
]
[[package]]
-name = "pest_generator"
-version = "2.8.6"
+name = "objc2-core-foundation"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f"
+checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
- "pest",
- "pest_meta",
- "proc-macro2",
- "quote",
- "syn",
+ "bitflags 2.11.0",
+ "dispatch2",
+ "objc2 0.6.4",
]
[[package]]
-name = "pest_meta"
-version = "2.8.6"
+name = "objc2-core-graphics"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220"
+checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
dependencies = [
- "pest",
- "sha2",
+ "bitflags 2.11.0",
+ "dispatch2",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+ "objc2-io-surface",
]
[[package]]
-name = "pin-project"
-version = "1.1.11"
+name = "objc2-core-image"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
+checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
dependencies = [
- "pin-project-internal",
+ "block2",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+ "objc2-metal",
]
[[package]]
-name = "pin-project-internal"
-version = "1.1.11"
+name = "objc2-core-location"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
+checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "block2",
+ "objc2 0.5.2",
+ "objc2-contacts",
+ "objc2-foundation 0.2.2",
]
[[package]]
-name = "pin-project-lite"
-version = "0.2.17"
+name = "objc2-encode"
+version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
[[package]]
-name = "pkg-config"
-version = "0.3.32"
+name = "objc2-foundation"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
+dependencies = [
+ "bitflags 2.11.0",
+ "block2",
+ "dispatch",
+ "libc",
+ "objc2 0.5.2",
+]
[[package]]
-name = "poly1305"
-version = "0.8.0"
+name = "objc2-foundation"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
+checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
- "cpufeatures",
- "opaque-debug",
- "universal-hash",
+ "bitflags 2.11.0",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
]
[[package]]
-name = "potential_utf"
-version = "0.1.4"
+name = "objc2-io-surface"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
+checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
dependencies = [
- "zerovec",
+ "bitflags 2.11.0",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
]
[[package]]
-name = "powerfmt"
-version = "0.2.0"
+name = "objc2-link-presentation"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
+dependencies = [
+ "block2",
+ "objc2 0.5.2",
+ "objc2-app-kit 0.2.2",
+ "objc2-foundation 0.2.2",
+]
[[package]]
-name = "ppv-lite86"
-version = "0.2.21"
+name = "objc2-metal"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
dependencies = [
- "zerocopy",
+ "bitflags 2.11.0",
+ "block2",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
]
[[package]]
-name = "prettyplease"
-version = "0.2.37"
+name = "objc2-quartz-core"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
dependencies = [
- "proc-macro2",
- "syn",
+ "bitflags 2.11.0",
+ "block2",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+ "objc2-metal",
]
[[package]]
-name = "proc-macro-error-attr2"
-version = "2.0.0"
+name = "objc2-symbols"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
+checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc"
dependencies = [
- "proc-macro2",
- "quote",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
]
[[package]]
-name = "proc-macro-error2"
-version = "2.0.1"
+name = "objc2-ui-kit"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
+checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
dependencies = [
- "proc-macro-error-attr2",
- "proc-macro2",
- "quote",
- "syn",
+ "bitflags 2.11.0",
+ "block2",
+ "objc2 0.5.2",
+ "objc2-cloud-kit",
+ "objc2-core-data",
+ "objc2-core-image",
+ "objc2-core-location",
+ "objc2-foundation 0.2.2",
+ "objc2-link-presentation",
+ "objc2-quartz-core",
+ "objc2-symbols",
+ "objc2-uniform-type-identifiers",
+ "objc2-user-notifications",
]
[[package]]
-name = "proc-macro-utils"
-version = "0.10.0"
+name = "objc2-uniform-type-identifiers"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071"
+checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
dependencies = [
- "proc-macro2",
- "quote",
- "smallvec",
+ "block2",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
]
[[package]]
-name = "proc-macro2"
-version = "1.0.106"
+name = "objc2-user-notifications"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
dependencies = [
- "unicode-ident",
+ "bitflags 2.11.0",
+ "block2",
+ "objc2 0.5.2",
+ "objc2-core-location",
+ "objc2-foundation 0.2.2",
]
[[package]]
-name = "proc-macro2-diagnostics"
-version = "0.10.1"
+name = "once_cell"
+version = "1.21.4"
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.45"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
-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",
- "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 = "r-efi"
-version = "6.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
-
-[[package]]
-name = "radroots-app"
-version = "0.1.0"
-dependencies = [
- "async-trait",
- "chrono",
- "console_error_panic_hook",
- "futures",
- "gloo-timers",
- "hex",
- "js-sys",
- "leptos",
- "leptos_router",
- "mf2-i18n-core",
- "mf2-i18n-embedded",
- "radroots-app-core",
- "radroots-app-lib",
- "radroots-app-ui-components",
- "radroots-log",
- "radroots-nostr",
- "serde",
- "serde_json",
- "sha2",
- "tracing-wasm",
- "uuid",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "web-sys",
-]
-
-[[package]]
-name = "radroots-app-core"
-version = "0.1.0"
-dependencies = [
- "async-trait",
- "base64 0.22.1",
- "chrono",
- "futures",
- "getrandom 0.4.2",
- "hex",
- "js-sys",
- "radroots-nostr",
- "radroots-replica-db",
- "radroots-replica-db-schema",
- "radroots-replica-sync",
- "radroots-sql-core",
- "rusqlite",
- "serde",
- "serde-wasm-bindgen",
- "serde_json",
- "sha2",
- "url",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "web-sys",
-]
-
-[[package]]
-name = "radroots-app-lib"
-version = "0.1.0"
-dependencies = [
- "futures",
- "gloo-timers",
- "js-sys",
- "once_cell",
- "radroots-app-utils",
- "regex",
- "serde",
- "serde-wasm-bindgen",
- "serde_json",
- "url",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "web-sys",
-]
-
-[[package]]
-name = "radroots-app-ui-components"
-version = "0.1.0"
-dependencies = [
- "icondata",
- "leptos",
- "radroots-app-ui-core",
- "radroots-app-ui-primitives",
- "web-sys",
-]
-
-[[package]]
-name = "radroots-app-ui-core"
-version = "0.1.0"
-dependencies = [
- "wasm-bindgen",
- "web-sys",
-]
-
-[[package]]
-name = "radroots-app-ui-primitives"
-version = "0.1.0"
-dependencies = [
- "headless-primitives-core",
- "headless-primitives-leptos",
-]
-
-[[package]]
-name = "radroots-app-ui-tokens"
-version = "0.1.0"
-
-[[package]]
-name = "radroots-app-utils"
-version = "0.1.0"
-dependencies = [
- "base64 0.22.1",
- "futures",
- "getrandom 0.4.2",
- "gloo-timers",
- "js-sys",
- "once_cell",
- "radroots-types",
- "regex",
- "serde_json",
- "uuid",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "web-sys",
-]
-
-[[package]]
-name = "radroots-core"
-version = "0.1.0-alpha.1"
-dependencies = [
- "rust_decimal",
- "rust_decimal_macros",
- "serde",
-]
-
-[[package]]
-name = "radroots-events"
-version = "0.1.0-alpha.1"
-dependencies = [
- "radroots-core",
- "serde",
-]
-
-[[package]]
-name = "radroots-events-codec"
-version = "0.1.0-alpha.1"
-dependencies = [
- "radroots-core",
- "radroots-events",
- "serde",
- "serde_json",
-]
-
-[[package]]
-name = "radroots-identity"
-version = "0.1.0-alpha.1"
-dependencies = [
- "nostr",
- "radroots-events",
- "radroots-runtime",
- "serde",
- "serde_json",
- "thiserror 1.0.69",
- "tracing",
-]
-
-[[package]]
-name = "radroots-log"
-version = "0.1.0-alpha.1"
-dependencies = [
- "thiserror 1.0.69",
- "tracing",
- "tracing-appender",
- "tracing-subscriber",
-]
-
-[[package]]
-name = "radroots-nostr"
-version = "0.1.0-alpha.1"
-dependencies = [
- "nostr",
- "nostr-sdk",
- "radroots-events",
- "radroots-events-codec",
- "radroots-identity",
- "serde",
- "serde_json",
- "thiserror 1.0.69",
-]
-
-[[package]]
-name = "radroots-replica-db"
-version = "0.1.0-alpha.1"
-dependencies = [
- "hex",
- "radroots-replica-db-schema",
- "radroots-sql-core",
- "radroots-types",
- "serde",
- "serde_json",
- "sha2",
-]
-
-[[package]]
-name = "radroots-replica-db-schema"
-version = "0.1.0-alpha.1"
-dependencies = [
- "radroots-types",
- "serde",
- "serde_json",
-]
-
-[[package]]
-name = "radroots-replica-sync"
-version = "0.1.0-alpha.1"
-dependencies = [
- "base64 0.22.1",
- "hex",
- "radroots-events",
- "radroots-events-codec",
- "radroots-replica-db",
- "radroots-replica-db-schema",
- "radroots-sql-core",
- "radroots-types",
- "serde",
- "serde_json",
- "sha2",
- "uuid",
-]
-
-[[package]]
-name = "radroots-runtime"
-version = "0.1.0-alpha.1"
-dependencies = [
- "anyhow",
- "config 0.14.1",
- "radroots-log",
- "serde",
- "serde_json",
- "tempfile",
- "thiserror 1.0.69",
- "tokio",
- "toml 0.8.23",
- "tracing",
-]
-
-[[package]]
-name = "radroots-sql-core"
-version = "0.1.0-alpha.1"
-dependencies = [
- "chrono",
- "rusqlite",
- "serde",
- "serde_json",
- "thiserror 1.0.69",
- "uuid",
-]
-
-[[package]]
-name = "radroots-types"
-version = "0.1.0-alpha.1"
-dependencies = [
- "serde",
- "serde_json",
-]
-
-[[package]]
-name = "rand"
-version = "0.8.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
-dependencies = [
- "libc",
- "rand_chacha 0.3.1",
- "rand_core 0.6.4",
-]
-
-[[package]]
-name = "rand"
-version = "0.9.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
-dependencies = [
- "rand_chacha 0.9.0",
- "rand_core 0.9.5",
-]
-
-[[package]]
-name = "rand_chacha"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
-dependencies = [
- "ppv-lite86",
- "rand_core 0.6.4",
-]
-
-[[package]]
-name = "rand_chacha"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
-dependencies = [
- "ppv-lite86",
- "rand_core 0.9.5",
-]
-
-[[package]]
-name = "rand_core"
-version = "0.6.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
-dependencies = [
- "getrandom 0.2.17",
-]
-
-[[package]]
-name = "rand_core"
-version = "0.9.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
-dependencies = [
- "getrandom 0.3.4",
-]
-
-[[package]]
-name = "reactive_graph"
-version = "0.2.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35774620b3da884a07341e9e36612e1509b1eb0553ef3bb76f1547dd1b797417"
-dependencies = [
- "any_spawner",
- "async-lock",
- "futures",
- "guardian",
- "hydration_context",
- "indexmap",
- "or_poisoned",
- "paste",
- "pin-project-lite",
- "rustc-hash",
- "rustc_version",
- "send_wrapper",
- "serde",
- "slotmap",
- "thiserror 2.0.18",
- "web-sys",
-]
-
-[[package]]
-name = "reactive_stores"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e114642d342893571ff40b4e1da8ccdea907be44c649041eb7d8413b3fd95e8"
-dependencies = [
- "guardian",
- "indexmap",
- "itertools",
- "or_poisoned",
- "paste",
- "reactive_graph",
- "reactive_stores_macro",
- "rustc-hash",
- "send_wrapper",
-]
-
-[[package]]
-name = "reactive_stores_macro"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b024812c47a6867b6cb32767a46182203f94e59eb88c69b032fd9caffa304ce"
-dependencies = [
- "convert_case 0.11.0",
- "proc-macro-error2",
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "regex"
-version = "1.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
-dependencies = [
- "aho-corasick",
- "memchr",
- "regex-automata",
- "regex-syntax",
-]
-
-[[package]]
-name = "regex-automata"
-version = "0.4.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
-dependencies = [
- "aho-corasick",
- "memchr",
- "regex-syntax",
-]
-
-[[package]]
-name = "regex-syntax"
-version = "0.8.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
-
-[[package]]
-name = "ring"
-version = "0.17.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
-dependencies = [
- "cc",
- "cfg-if",
- "getrandom 0.2.17",
- "libc",
- "untrusted",
- "windows-sys 0.52.0",
-]
-
-[[package]]
-name = "ron"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
-dependencies = [
- "base64 0.21.7",
- "bitflags",
- "serde",
- "serde_derive",
-]
-
-[[package]]
-name = "rstml"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61cf4616de7499fc5164570d40ca4e1b24d231c6833a88bff0fe00725080fd56"
-dependencies = [
- "derive-where",
- "proc-macro2",
- "proc-macro2-diagnostics",
- "quote",
- "syn",
- "syn_derive",
- "thiserror 2.0.18",
-]
-
-[[package]]
-name = "rusqlite"
-version = "0.31.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae"
-dependencies = [
- "bitflags",
- "fallible-iterator",
- "fallible-streaming-iterator",
- "hashlink 0.9.1",
- "libsqlite3-sys",
- "smallvec",
-]
-
-[[package]]
-name = "rust-ini"
-version = "0.20.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a"
-dependencies = [
- "cfg-if",
- "ordered-multimap",
-]
-
-[[package]]
-name = "rust_decimal"
-version = "1.40.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0"
-dependencies = [
- "arrayvec",
- "num-traits",
- "serde",
-]
-
-[[package]]
-name = "rust_decimal_macros"
-version = "1.40.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74a5a6f027e892c7a035c6fddb50435a1fbf5a734ffc0c2a9fed4d0221440519"
-dependencies = [
- "quote",
- "syn",
-]
-
-[[package]]
-name = "rustc-hash"
-version = "2.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
-name = "rustc_version"
-version = "0.4.1"
+name = "orbclient"
+version = "0.3.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+checksum = "59aed3b33578edcfa1bc96a321d590d31832b6ad55a26f0313362ce687e9abd6"
dependencies = [
- "semver",
+ "libc",
+ "libredox",
]
[[package]]
-name = "rustix"
-version = "1.1.4"
+name = "ordered-float"
+version = "5.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d"
dependencies = [
- "bitflags",
- "errno",
- "libc",
- "linux-raw-sys",
- "windows-sys 0.61.2",
+ "num-traits",
]
[[package]]
-name = "rustls"
-version = "0.23.37"
+name = "owned_ttf_parser"
+version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
+checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b"
dependencies = [
- "once_cell",
- "ring",
- "rustls-pki-types",
- "rustls-webpki",
- "subtle",
- "zeroize",
+ "ttf-parser",
]
[[package]]
-name = "rustls-pki-types"
-version = "1.14.0"
+name = "parking_lot"
+version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
- "zeroize",
+ "lock_api",
+ "parking_lot_core",
]
[[package]]
-name = "rustls-webpki"
-version = "0.103.9"
+name = "parking_lot_core"
+version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
- "ring",
- "rustls-pki-types",
- "untrusted",
+ "cfg-if",
+ "libc",
+ "redox_syscall 0.5.18",
+ "smallvec",
+ "windows-link",
]
[[package]]
-name = "rustversion"
-version = "1.0.22"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
-
-[[package]]
-name = "salsa20"
-version = "0.10.2"
+name = "paste"
+version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
-dependencies = [
- "cipher",
-]
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
-name = "same-file"
-version = "1.0.6"
+name = "percent-encoding"
+version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
-dependencies = [
- "winapi-util",
-]
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
-name = "scrypt"
-version = "0.11.0"
+name = "pin-project"
+version = "1.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
+checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
dependencies = [
- "password-hash",
- "pbkdf2",
- "salsa20",
- "sha2",
+ "pin-project-internal",
]
[[package]]
-name = "secp256k1"
-version = "0.29.1"
+name = "pin-project-internal"
+version = "1.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
+checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
dependencies = [
- "rand 0.8.5",
- "secp256k1-sys",
- "serde",
+ "proc-macro2",
+ "quote",
+ "syn",
]
[[package]]
-name = "secp256k1-sys"
-version = "0.10.1"
+name = "pin-project-lite"
+version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9"
-dependencies = [
- "cc",
-]
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
-name = "semver"
-version = "1.0.27"
+name = "pkg-config"
+version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
-name = "send_wrapper"
-version = "0.6.0"
+name = "plain"
+version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
-dependencies = [
- "futures-core",
-]
+checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
-name = "serde"
-version = "1.0.228"
+name = "png"
+version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
dependencies = [
- "serde_core",
- "serde_derive",
+ "bitflags 2.11.0",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
]
[[package]]
-name = "serde-wasm-bindgen"
-version = "0.6.5"
+name = "polling"
+version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
dependencies = [
- "js-sys",
- "serde",
- "wasm-bindgen",
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix 1.1.4",
+ "windows-sys 0.61.2",
]
[[package]]
-name = "serde_core"
-version = "1.0.228"
+name = "pollster"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
-dependencies = [
- "serde_derive",
-]
+checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
[[package]]
-name = "serde_derive"
-version = "1.0.228"
+name = "portable-atomic"
+version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
+checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
[[package]]
-name = "serde_json"
-version = "1.0.149"
+name = "portable-atomic-util"
+version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3"
dependencies = [
- "itoa",
- "memchr",
- "serde",
- "serde_core",
- "zmij",
+ "portable-atomic",
]
[[package]]
-name = "serde_qs"
-version = "0.15.0"
+name = "potential_utf"
+version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352"
+checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
dependencies = [
- "percent-encoding",
- "serde",
- "thiserror 2.0.18",
+ "zerovec",
]
[[package]]
-name = "serde_spanned"
-version = "0.6.9"
+name = "presser"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
-dependencies = [
- "serde",
-]
+checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
[[package]]
-name = "serde_spanned"
-version = "1.0.4"
+name = "proc-macro-crate"
+version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
+checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
dependencies = [
- "serde_core",
+ "toml_edit",
]
[[package]]
-name = "server_fn"
-version = "0.8.11"
+name = "proc-macro2"
+version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c799cec4e8e210dfb2f203aa97f0e82232c619e385ef4d011b17a58d6397c7b"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
- "base64 0.22.1",
- "bytes",
- "const-str",
- "const_format",
- "futures",
- "gloo-net",
- "http",
- "js-sys",
- "or_poisoned",
- "pin-project-lite",
- "rustc_version",
- "rustversion",
- "send_wrapper",
- "serde",
- "serde_json",
- "serde_qs",
- "server_fn_macro_default",
- "thiserror 2.0.18",
- "throw_error",
- "url",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "wasm-streams",
- "web-sys",
- "xxhash-rust",
+ "unicode-ident",
]
[[package]]
-name = "server_fn_macro"
-version = "0.8.10"
+name = "profiling"
+version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1295b54815397d30d986b63f93cfd515fa86d5e528e0bb589ce9d530502f9e0f"
-dependencies = [
- "const_format",
- "convert_case 0.11.0",
- "proc-macro2",
- "quote",
- "rustc_version",
- "syn",
- "xxhash-rust",
-]
+checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
[[package]]
-name = "server_fn_macro_default"
-version = "0.8.5"
+name = "pxfm"
+version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63eb08f80db903d3c42f64e60ebb3875e0305be502bdc064ec0a0eab42207f00"
-dependencies = [
- "server_fn_macro",
- "syn",
-]
+checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d"
[[package]]
-name = "sha1"
-version = "0.10.6"
+name = "quick-error"
+version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
-dependencies = [
- "cfg-if",
- "cpufeatures",
- "digest",
-]
+checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
-name = "sha2"
-version = "0.10.9"
+name = "quick-xml"
+version = "0.39.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d"
dependencies = [
- "cfg-if",
- "cpufeatures",
- "digest",
+ "memchr",
]
[[package]]
-name = "sharded-slab"
-version = "0.1.7"
+name = "quote"
+version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
- "lazy_static",
+ "proc-macro2",
]
[[package]]
-name = "shlex"
-version = "1.3.0"
+name = "r-efi"
+version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
-name = "signal-hook-registry"
-version = "1.4.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+name = "radroots-app"
+version = "0.1.0"
dependencies = [
- "errno",
- "libc",
+ "eframe",
+ "egui",
+ "objc2-foundation 0.3.2",
+ "wgpu",
]
[[package]]
-name = "slab"
-version = "0.4.12"
+name = "range-alloc"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08"
[[package]]
-name = "slotmap"
-version = "1.1.1"
+name = "raw-window-handle"
+version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038"
-dependencies = [
- "version_check",
-]
+checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
[[package]]
-name = "smallvec"
-version = "1.15.1"
+name = "redox_syscall"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
+dependencies = [
+ "bitflags 1.3.2",
+]
[[package]]
-name = "socket2"
-version = "0.6.3"
+name = "redox_syscall"
+version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
- "libc",
- "windows-sys 0.61.2",
+ "bitflags 2.11.0",
]
[[package]]
-name = "stable_deref_trait"
-version = "1.2.1"
+name = "redox_syscall"
+version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16"
+dependencies = [
+ "bitflags 2.11.0",
+]
[[package]]
-name = "subtle"
-version = "2.6.1"
+name = "renderdoc-sys"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
[[package]]
-name = "syn"
-version = "2.0.117"
+name = "rustc-hash"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
-name = "syn_derive"
-version = "0.2.0"
+name = "rustc-hash"
+version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdb066a04799e45f5d582e8fc6ec8e6d6896040d00898eb4e6a835196815b219"
-dependencies = [
- "proc-macro-error2",
- "proc-macro2",
- "quote",
- "syn",
-]
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
-name = "synstructure"
-version = "0.13.2"
+name = "rustc_version"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "semver",
]
[[package]]
-name = "tachys"
-version = "0.2.14"
+name = "rustix"
+version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f768750b0d5514f487772187d4b20c66f56faff4541b1faa5aad4975f5aee085"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
- "any_spawner",
- "async-trait",
- "const_str_slice_concat",
- "drain_filter_polyfill",
- "either_of",
- "erased",
- "futures",
- "html-escape",
- "indexmap",
- "itertools",
- "js-sys",
- "next_tuple",
- "oco_ref",
- "or_poisoned",
- "paste",
- "reactive_graph",
- "reactive_stores",
- "rustc-hash",
- "rustc_version",
- "send_wrapper",
- "slotmap",
- "throw_error",
- "wasm-bindgen",
- "web-sys",
+ "bitflags 2.11.0",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.4.15",
+ "windows-sys 0.52.0",
]
[[package]]
-name = "tempfile"
-version = "3.27.0"
+name = "rustix"
+version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
- "fastrand",
- "getrandom 0.4.2",
- "once_cell",
- "rustix",
+ "bitflags 2.11.0",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.12.1",
"windows-sys 0.61.2",
]
[[package]]
-name = "thiserror"
-version = "1.0.69"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
-dependencies = [
- "thiserror-impl 1.0.69",
-]
-
-[[package]]
-name = "thiserror"
-version = "2.0.18"
+name = "rustversion"
+version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
-dependencies = [
- "thiserror-impl 2.0.18",
-]
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
-name = "thiserror-impl"
-version = "1.0.69"
+name = "same-file"
+version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
]
[[package]]
-name = "thiserror-impl"
-version = "2.0.18"
+name = "scoped-tls"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
-name = "thread_local"
-version = "1.1.9"
+name = "scopeguard"
+version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
-dependencies = [
- "cfg-if",
-]
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
-name = "throw_error"
-version = "0.3.1"
+name = "semver"
+version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc0ed6038fcbc0795aca7c92963ddda636573b956679204e044492d2b13c8f64"
-dependencies = [
- "pin-project-lite",
-]
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]]
-name = "time"
-version = "0.3.47"
+name = "serde"
+version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
- "deranged",
- "itoa",
- "num-conv",
- "powerfmt",
"serde_core",
- "time-core",
- "time-macros",
+ "serde_derive",
]
[[package]]
-name = "time-core"
-version = "0.1.8"
+name = "serde_core"
+version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
[[package]]
-name = "time-macros"
-version = "0.2.27"
+name = "serde_derive"
+version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
- "num-conv",
- "time-core",
+ "proc-macro2",
+ "quote",
+ "syn",
]
[[package]]
-name = "tiny-keccak"
-version = "2.0.2"
+name = "shlex"
+version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
-dependencies = [
- "crunchy",
-]
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
-name = "tinystr"
-version = "0.8.2"
+name = "simd-adler32"
+version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
-dependencies = [
- "displaydoc",
- "zerovec",
-]
+checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
[[package]]
-name = "tinyvec"
-version = "1.11.0"
+name = "simd_cesu8"
+version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
+checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
dependencies = [
- "tinyvec_macros",
+ "rustc_version",
+ "simdutf8",
]
[[package]]
-name = "tinyvec_macros"
-version = "0.1.1"
+name = "simdutf8"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
-name = "tokio"
-version = "1.50.0"
+name = "slab"
+version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
-dependencies = [
- "bytes",
- "libc",
- "mio",
- "pin-project-lite",
- "signal-hook-registry",
- "socket2",
- "tokio-macros",
- "windows-sys 0.61.2",
-]
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
-name = "tokio-macros"
-version = "2.6.1"
+name = "slotmap"
+version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
+checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "version_check",
]
[[package]]
-name = "tokio-rustls"
-version = "0.26.4"
+name = "smallvec"
+version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
-dependencies = [
- "rustls",
- "tokio",
-]
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
-name = "tokio-socks"
-version = "0.5.2"
+name = "smithay-client-toolkit"
+version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f"
+checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016"
dependencies = [
- "either",
- "futures-util",
+ "bitflags 2.11.0",
+ "calloop 0.13.0",
+ "calloop-wayland-source 0.3.0",
+ "cursor-icon",
+ "libc",
+ "log",
+ "memmap2",
+ "rustix 0.38.44",
"thiserror 1.0.69",
- "tokio",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-csd-frame",
+ "wayland-cursor",
+ "wayland-protocols",
+ "wayland-protocols-wlr",
+ "wayland-scanner",
+ "xkeysym",
]
[[package]]
-name = "tokio-tungstenite"
-version = "0.26.2"
+name = "smithay-client-toolkit"
+version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
+checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0"
dependencies = [
- "futures-util",
+ "bitflags 2.11.0",
+ "calloop 0.14.4",
+ "calloop-wayland-source 0.4.1",
+ "cursor-icon",
+ "libc",
"log",
- "rustls",
- "rustls-pki-types",
- "tokio",
- "tokio-rustls",
- "tungstenite",
- "webpki-roots 0.26.11",
+ "memmap2",
+ "rustix 1.1.4",
+ "thiserror 2.0.18",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-csd-frame",
+ "wayland-cursor",
+ "wayland-protocols",
+ "wayland-protocols-experimental",
+ "wayland-protocols-misc",
+ "wayland-protocols-wlr",
+ "wayland-scanner",
+ "xkeysym",
]
[[package]]
-name = "toml"
-version = "0.8.23"
+name = "smithay-clipboard"
+version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226"
dependencies = [
- "serde",
- "serde_spanned 0.6.9",
- "toml_datetime 0.6.11",
- "toml_edit",
+ "libc",
+ "smithay-client-toolkit 0.20.0",
+ "wayland-backend",
]
[[package]]
-name = "toml"
-version = "1.0.7+spec-1.1.0"
+name = "smol_str"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd28d57d8a6f6e458bc0b8784f8fdcc4b99a437936056fa122cb234f18656a96"
+checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead"
dependencies = [
- "serde_core",
- "serde_spanned 1.0.4",
- "toml_datetime 1.0.1+spec-1.1.0",
- "toml_parser",
- "winnow 1.0.0",
+ "serde",
]
[[package]]
-name = "toml_datetime"
-version = "0.6.11"
+name = "spirv"
+version = "0.3.0+sdk-1.3.268.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844"
dependencies = [
- "serde",
+ "bitflags 2.11.0",
]
[[package]]
-name = "toml_datetime"
-version = "1.0.1+spec-1.1.0"
+name = "stable_deref_trait"
+version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9"
-dependencies = [
- "serde_core",
-]
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
-name = "toml_edit"
-version = "0.22.27"
+name = "static_assertions"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
-dependencies = [
- "indexmap",
- "serde",
- "serde_spanned 0.6.9",
- "toml_datetime 0.6.11",
- "toml_write",
- "winnow 0.7.15",
-]
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
-name = "toml_parser"
-version = "1.0.10+spec-1.1.0"
+name = "syn"
+version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
- "winnow 1.0.0",
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
]
[[package]]
-name = "toml_write"
-version = "0.1.2"
+name = "synstructure"
+version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
[[package]]
-name = "tracing"
-version = "0.1.44"
+name = "thiserror"
+version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
- "pin-project-lite",
- "tracing-attributes",
- "tracing-core",
+ "thiserror-impl 1.0.69",
]
[[package]]
-name = "tracing-appender"
-version = "0.2.4"
+name = "thiserror"
+version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
- "crossbeam-channel",
- "thiserror 2.0.18",
- "time",
- "tracing-subscriber",
+ "thiserror-impl 2.0.18",
]
[[package]]
-name = "tracing-attributes"
-version = "0.1.31"
+name = "thiserror-impl"
+version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
@@ -3221,120 +2340,107 @@ dependencies = [
]
[[package]]
-name = "tracing-core"
-version = "0.1.36"
+name = "thiserror-impl"
+version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
- "once_cell",
- "valuable",
+ "proc-macro2",
+ "quote",
+ "syn",
]
[[package]]
-name = "tracing-log"
-version = "0.2.0"
+name = "tiff"
+version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
dependencies = [
- "log",
- "once_cell",
- "tracing-core",
+ "fax",
+ "flate2",
+ "half",
+ "quick-error",
+ "weezl",
+ "zune-jpeg",
]
[[package]]
-name = "tracing-subscriber"
-version = "0.3.23"
+name = "tinystr"
+version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
+checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
dependencies = [
- "matchers",
- "nu-ansi-term",
- "once_cell",
- "regex-automata",
- "sharded-slab",
- "smallvec",
- "thread_local",
- "tracing",
- "tracing-core",
- "tracing-log",
+ "displaydoc",
+ "zerovec",
]
[[package]]
-name = "tracing-wasm"
-version = "0.2.1"
+name = "toml_datetime"
+version = "1.0.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07"
+checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9"
dependencies = [
- "tracing",
- "tracing-subscriber",
- "wasm-bindgen",
+ "serde_core",
]
[[package]]
-name = "tungstenite"
-version = "0.26.2"
+name = "toml_edit"
+version = "0.25.5+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
+checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1"
dependencies = [
- "bytes",
- "data-encoding",
- "http",
- "httparse",
- "log",
- "rand 0.9.2",
- "rustls",
- "rustls-pki-types",
- "sha1",
- "thiserror 2.0.18",
- "utf-8",
+ "indexmap",
+ "toml_datetime",
+ "toml_parser",
+ "winnow",
]
[[package]]
-name = "typed-builder"
-version = "0.23.2"
+name = "toml_parser"
+version = "1.0.10+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31aa81521b70f94402501d848ccc0ecaa8f93c8eb6999eb9747e72287757ffda"
+checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420"
dependencies = [
- "typed-builder-macro",
+ "winnow",
]
[[package]]
-name = "typed-builder-macro"
-version = "0.23.2"
+name = "tracing"
+version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "076a02dc54dd46795c2e9c8282ed40bcfb1e22747e955de9389a1de28190fb26"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "log",
+ "pin-project-lite",
+ "tracing-core",
]
[[package]]
-name = "typenum"
-version = "1.19.0"
+name = "tracing-core"
+version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
[[package]]
-name = "ucd-trie"
-version = "0.1.7"
+name = "ttf-parser"
+version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
+checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
[[package]]
-name = "unicode-ident"
-version = "1.0.24"
+name = "type-map"
+version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90"
+dependencies = [
+ "rustc-hash 2.1.1",
+]
[[package]]
-name = "unicode-normalization"
-version = "0.1.25"
+name = "unicode-ident"
+version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
-dependencies = [
- "tinyvec",
-]
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-segmentation"
@@ -3343,26 +2449,10 @@ 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 = "universal-hash"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
-dependencies = [
- "crypto-common",
- "subtle",
-]
-
-[[package]]
-name = "untrusted"
-version = "0.9.0"
+name = "unicode-width"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "url"
@@ -3374,51 +2464,15 @@ dependencies = [
"idna",
"percent-encoding",
"serde",
- "serde_derive",
]
[[package]]
-name = "utf-8"
-version = "0.7.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
-
-[[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.22.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
-dependencies = [
- "getrandom 0.4.2",
- "js-sys",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "valuable"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
-
-[[package]]
-name = "vcpkg"
-version = "0.2.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
-
-[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3428,32 +2482,17 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
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.2+wasi-0.2.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
- "wit-bindgen",
+ "same-file",
+ "winapi-util",
]
[[package]]
-name = "wasip3"
-version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+name = "wasip2"
+version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
dependencies = [
"wit-bindgen",
]
@@ -3518,72 +2557,138 @@ dependencies = [
]
[[package]]
-name = "wasm-encoder"
-version = "0.244.0"
+name = "wayland-backend"
+version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+checksum = "aa75f400b7f719bcd68b3f47cd939ba654cedeef690f486db71331eec4c6a406"
dependencies = [
- "leb128fmt",
- "wasmparser",
+ "cc",
+ "downcast-rs",
+ "rustix 1.1.4",
+ "scoped-tls",
+ "smallvec",
+ "wayland-sys",
]
[[package]]
-name = "wasm-metadata"
-version = "0.244.0"
+name = "wayland-client"
+version = "0.31.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+checksum = "ab51d9f7c071abeee76007e2b742499e535148035bb835f97aaed1338cf516c3"
dependencies = [
- "anyhow",
- "indexmap",
- "wasm-encoder",
- "wasmparser",
+ "bitflags 2.11.0",
+ "rustix 1.1.4",
+ "wayland-backend",
+ "wayland-scanner",
]
[[package]]
-name = "wasm-streams"
-version = "0.5.0"
+name = "wayland-csd-frame"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"
+checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e"
dependencies = [
- "futures-util",
- "js-sys",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "web-sys",
+ "bitflags 2.11.0",
+ "cursor-icon",
+ "wayland-backend",
]
[[package]]
-name = "wasm_split_helpers"
-version = "0.2.0"
+name = "wayland-cursor"
+version = "0.31.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a114b3073258dd5de3d812cdd048cca6842342755e828a14dbf15f843f2d1b84"
+checksum = "4b3298683470fbdc6ca40151dfc48c8f2fd4c41a26e13042f801f85002384091"
dependencies = [
- "async-once-cell",
- "wasm_split_macros",
+ "rustix 1.1.4",
+ "wayland-client",
+ "xcursor",
]
[[package]]
-name = "wasm_split_macros"
-version = "0.2.0"
+name = "wayland-protocols"
+version = "0.32.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b23b5df31ceff1328f06ac607591d5ba360cf58f90c8fad4ac8d3a55a3c4aec7"
+dependencies = [
+ "bitflags 2.11.0",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-protocols-experimental"
+version = "20250721.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1"
+dependencies = [
+ "bitflags 2.11.0",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-protocols-misc"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "429b99200febaf95d4f4e46deff6fe4382bcff3280ee16a41cf887b3c3364984"
+dependencies = [
+ "bitflags 2.11.0",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-protocols-plasma"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d392fc283a87774afc9beefcd6f931582bb97fe0e6ced0b306a62cb1d026527c"
+dependencies = [
+ "bitflags 2.11.0",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-protocols-wlr"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78248e4cc0eff8163370ba5c158630dcae1f3497a586b826eca2ef5f348d6235"
+dependencies = [
+ "bitflags 2.11.0",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-scanner"
+version = "0.31.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56481f8ed1a9f9ae97ea7b08a5e2b12e8adf9a7818a6ba952b918e09c7be8bf0"
+checksum = "c86287151a309799b821ca709b7345a048a2956af05957c89cb824ab919fa4e3"
dependencies = [
- "base16",
+ "proc-macro2",
+ "quick-xml",
"quote",
- "sha2",
- "syn",
]
[[package]]
-name = "wasmparser"
-version = "0.244.0"
+name = "wayland-sys"
+version = "0.31.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+checksum = "374f6b70e8e0d6bf9461a32988fd553b59ff630964924dad6e4a4eb6bd538d17"
dependencies = [
- "bitflags",
- "hashbrown 0.15.5",
- "indexmap",
- "semver",
+ "dlib",
+ "log",
+ "once_cell",
+ "pkg-config",
]
[[package]]
@@ -3597,21 +2702,180 @@ dependencies = [
]
[[package]]
-name = "webpki-roots"
-version = "0.26.11"
+name = "web-time"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
- "webpki-roots 1.0.6",
+ "js-sys",
+ "wasm-bindgen",
]
[[package]]
-name = "webpki-roots"
-version = "1.0.6"
+name = "webbrowser"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe985f41e291eecef5e5c0770a18d28390addb03331c043964d9e916453d6f16"
+dependencies = [
+ "core-foundation 0.10.1",
+ "jni 0.22.4",
+ "log",
+ "ndk-context",
+ "objc2 0.6.4",
+ "objc2-foundation 0.3.2",
+ "url",
+ "web-sys",
+]
+
+[[package]]
+name = "weezl"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
+
+[[package]]
+name = "wgpu"
+version = "27.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77"
+dependencies = [
+ "arrayvec",
+ "bitflags 2.11.0",
+ "cfg-if",
+ "cfg_aliases",
+ "document-features",
+ "hashbrown 0.16.1",
+ "log",
+ "portable-atomic",
+ "profiling",
+ "raw-window-handle",
+ "smallvec",
+ "static_assertions",
+ "wgpu-core",
+ "wgpu-hal",
+ "wgpu-types",
+]
+
+[[package]]
+name = "wgpu-core"
+version = "27.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27a75de515543b1897b26119f93731b385a19aea165a1ec5f0e3acecc229cae7"
+dependencies = [
+ "arrayvec",
+ "bit-set",
+ "bit-vec",
+ "bitflags 2.11.0",
+ "bytemuck",
+ "cfg_aliases",
+ "document-features",
+ "hashbrown 0.16.1",
+ "indexmap",
+ "log",
+ "naga",
+ "once_cell",
+ "parking_lot",
+ "portable-atomic",
+ "profiling",
+ "raw-window-handle",
+ "rustc-hash 1.1.0",
+ "smallvec",
+ "thiserror 2.0.18",
+ "wgpu-core-deps-apple",
+ "wgpu-core-deps-emscripten",
+ "wgpu-core-deps-windows-linux-android",
+ "wgpu-hal",
+ "wgpu-types",
+]
+
+[[package]]
+name = "wgpu-core-deps-apple"
+version = "27.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0772ae958e9be0c729561d5e3fd9a19679bcdfb945b8b1a1969d9bfe8056d233"
+dependencies = [
+ "wgpu-hal",
+]
+
+[[package]]
+name = "wgpu-core-deps-emscripten"
+version = "27.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
+checksum = "b06ac3444a95b0813ecfd81ddb2774b66220b264b3e2031152a4a29fda4da6b5"
dependencies = [
- "rustls-pki-types",
+ "wgpu-hal",
+]
+
+[[package]]
+name = "wgpu-core-deps-windows-linux-android"
+version = "27.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71197027d61a71748e4120f05a9242b2ad142e3c01f8c1b47707945a879a03c3"
+dependencies = [
+ "wgpu-hal",
+]
+
+[[package]]
+name = "wgpu-hal"
+version = "27.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b21cb61c57ee198bc4aff71aeadff4cbb80b927beb912506af9c780d64313ce"
+dependencies = [
+ "android_system_properties",
+ "arrayvec",
+ "ash",
+ "bit-set",
+ "bitflags 2.11.0",
+ "block",
+ "bytemuck",
+ "cfg-if",
+ "cfg_aliases",
+ "core-graphics-types 0.2.0",
+ "glow",
+ "glutin_wgl_sys",
+ "gpu-alloc",
+ "gpu-allocator",
+ "gpu-descriptor",
+ "hashbrown 0.16.1",
+ "js-sys",
+ "khronos-egl",
+ "libc",
+ "libloading",
+ "log",
+ "metal",
+ "naga",
+ "ndk-sys",
+ "objc",
+ "once_cell",
+ "ordered-float",
+ "parking_lot",
+ "portable-atomic",
+ "portable-atomic-util",
+ "profiling",
+ "range-alloc",
+ "raw-window-handle",
+ "renderdoc-sys",
+ "smallvec",
+ "thiserror 2.0.18",
+ "wasm-bindgen",
+ "web-sys",
+ "wgpu-types",
+ "windows",
+ "windows-core",
+]
+
+[[package]]
+name = "wgpu-types"
+version = "27.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb"
+dependencies = [
+ "bitflags 2.11.0",
+ "bytemuck",
+ "js-sys",
+ "log",
+ "thiserror 2.0.18",
+ "web-sys",
]
[[package]]
@@ -3624,23 +2888,33 @@ dependencies = [
]
[[package]]
+name = "windows"
+version = "0.58.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
+dependencies = [
+ "windows-core",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
name = "windows-core"
-version = "0.62.2"
+version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
dependencies = [
"windows-implement",
"windows-interface",
- "windows-link",
"windows-result",
"windows-strings",
+ "windows-targets 0.52.6",
]
[[package]]
name = "windows-implement"
-version = "0.60.2"
+version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
@@ -3649,9 +2923,9 @@ dependencies = [
[[package]]
name = "windows-interface"
-version = "0.59.3"
+version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
@@ -3666,20 +2940,30 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
-version = "0.4.1"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
- "windows-link",
+ "windows-targets 0.52.6",
]
[[package]]
name = "windows-strings"
-version = "0.5.1"
+version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
- "windows-link",
+ "windows-result",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
]
[[package]]
@@ -3688,7 +2972,7 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
- "windows-targets",
+ "windows-targets 0.52.6",
]
[[package]]
@@ -3702,34 +2986,67 @@ dependencies = [
[[package]]
name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
@@ -3742,35 +3059,101 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
-name = "winnow"
-version = "0.7.15"
+name = "winit"
+version = "0.30.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d"
dependencies = [
- "memchr",
+ "ahash",
+ "android-activity",
+ "atomic-waker",
+ "bitflags 2.11.0",
+ "block2",
+ "bytemuck",
+ "calloop 0.13.0",
+ "cfg_aliases",
+ "concurrent-queue",
+ "core-foundation 0.9.4",
+ "core-graphics",
+ "cursor-icon",
+ "dpi",
+ "js-sys",
+ "libc",
+ "memmap2",
+ "ndk",
+ "objc2 0.5.2",
+ "objc2-app-kit 0.2.2",
+ "objc2-foundation 0.2.2",
+ "objc2-ui-kit",
+ "orbclient",
+ "percent-encoding",
+ "pin-project",
+ "raw-window-handle",
+ "redox_syscall 0.4.1",
+ "rustix 0.38.44",
+ "smithay-client-toolkit 0.19.2",
+ "smol_str",
+ "tracing",
+ "unicode-segmentation",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-protocols-plasma",
+ "web-sys",
+ "web-time",
+ "windows-sys 0.52.0",
+ "x11-dl",
+ "x11rb",
+ "xkbcommon-dl",
]
[[package]]
@@ -3787,117 +3170,75 @@ name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
-dependencies = [
- "wit-bindgen-rust-macro",
-]
-
-[[package]]
-name = "wit-bindgen-core"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
-dependencies = [
- "anyhow",
- "heck",
- "wit-parser",
-]
[[package]]
-name = "wit-bindgen-rust"
-version = "0.51.0"
+name = "writeable"
+version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
-dependencies = [
- "anyhow",
- "heck",
- "indexmap",
- "prettyplease",
- "syn",
- "wasm-metadata",
- "wit-bindgen-core",
- "wit-component",
-]
+checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]]
-name = "wit-bindgen-rust-macro"
-version = "0.51.0"
+name = "x11-dl"
+version = "2.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
dependencies = [
- "anyhow",
- "prettyplease",
- "proc-macro2",
- "quote",
- "syn",
- "wit-bindgen-core",
- "wit-bindgen-rust",
+ "libc",
+ "once_cell",
+ "pkg-config",
]
[[package]]
-name = "wit-component"
-version = "0.244.0"
+name = "x11rb"
+version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
dependencies = [
- "anyhow",
- "bitflags",
- "indexmap",
- "log",
- "serde",
- "serde_derive",
- "serde_json",
- "wasm-encoder",
- "wasm-metadata",
- "wasmparser",
- "wit-parser",
+ "as-raw-xcb-connection",
+ "gethostname",
+ "libc",
+ "libloading",
+ "once_cell",
+ "rustix 1.1.4",
+ "x11rb-protocol",
]
[[package]]
-name = "wit-parser"
-version = "0.244.0"
+name = "x11rb-protocol"
+version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
-dependencies = [
- "anyhow",
- "id-arena",
- "indexmap",
- "log",
- "semver",
- "serde",
- "serde_derive",
- "serde_json",
- "unicode-xid",
- "wasmparser",
-]
+checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
[[package]]
-name = "writeable"
-version = "0.6.2"
+name = "xcursor"
+version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
+checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b"
[[package]]
-name = "xxhash-rust"
-version = "0.8.15"
+name = "xkbcommon-dl"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
+checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5"
+dependencies = [
+ "bitflags 2.11.0",
+ "dlib",
+ "log",
+ "once_cell",
+ "xkeysym",
+]
[[package]]
-name = "yaml-rust2"
-version = "0.8.1"
+name = "xkeysym"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8"
-dependencies = [
- "arraydeque",
- "encoding_rs",
- "hashlink 0.8.4",
-]
+checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
[[package]]
-name = "yansi"
-version = "1.0.1"
+name = "xml-rs"
+version = "0.8.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
[[package]]
name = "yoke"
@@ -3964,12 +3305,6 @@ dependencies = [
]
[[package]]
-name = "zeroize"
-version = "1.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
-
-[[package]]
name = "zerotrie"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4003,7 +3338,16 @@ dependencies = [
]
[[package]]
-name = "zmij"
-version = "1.0.21"
+name = "zune-core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
+
+[[package]]
+name = "zune-jpeg"
+version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+checksum = "ec5f41c76397b7da451efd19915684f727d7e1d516384ca6bd0ec43ec94de23c"
+dependencies = [
+ "zune-core",
+]
diff --git a/Cargo.toml b/Cargo.toml
@@ -1,128 +1,30 @@
[workspace]
members = [
- "app",
- "crates/app-lib",
- "crates/core",
- "crates/ui-components",
- "crates/ui-core",
- "crates/ui-primitives",
- "crates/ui-tokens",
- "crates/utils"
+ "crates/app"
]
-exclude = [
-]
-
resolver = "2"
[workspace.package]
version = "0.1.0"
edition = "2024"
-rust-version = "1.92.0"
-license = "AGPL-3.0"
authors = ["Radroots Authors"]
-repository = "https://github.com/triesap/radroots"
+rust-version = "1.92.0"
+license = "GPL-3.0"
+repository = "https://github.com/radrootslabs/app"
+homepage = "https://radroots.org"
readme = "README.md"
+[workspace.dependencies]
+eframe = { version = "0.33.3", default-features = false, features = ["default_fonts", "wgpu", "wayland", "x11"] }
+egui = { version = "0.33.3", features = ["serde"] }
+objc2-foundation = { version = "0.3.2", default-features = false, features = ["std"] }
+wgpu = { version = "27.0.1", default-features = false }
+
[workspace.lints.rust]
unsafe_code = "forbid"
-[workspace.dependencies]
-leptos = { version = "0.8.17", default-features = false }
-leptos_router = { version = "0.8.12", default-features = false }
-icondata = { version = "0.7", default-features = false, features = ["lucide"] }
-wasm-bindgen = "=0.2.114"
-serde = { version = "1", features = ["derive"] }
-serde_json = "1"
-futures = "0.3"
-async-trait = "0.1.89"
-getrandom = "0.4"
-js-sys = "0.3.91"
-gloo-timers = "0.3"
-web-sys = { version = "0.3.91", features = [
- "Crypto",
- "CryptoKey",
- "SubtleCrypto",
- "Window",
- "Document",
- "Element",
- "DomTokenList",
- "DomException",
- "DomStringList",
- "Event",
- "EventTarget",
- "File",
- "FileList",
- "FileReader",
- "Blob",
- "HtmlAnchorElement",
- "HtmlElement",
- "HtmlInputElement",
- "IdbDatabase",
- "IdbFactory",
- "IdbObjectStore",
- "IdbOpenDbRequest",
- "IdbRequest",
- "IdbTransaction",
- "IdbTransactionMode",
- "Coordinates",
- "Cache",
- "CacheStorage",
- "Clipboard",
- "Headers",
- "Geolocation",
- "MediaQueryList",
- "Navigator",
- "Notification",
- "NotificationOptions",
- "NotificationPermission",
- "NodeList",
- "PermissionDescriptor",
- "PermissionName",
- "PermissionState",
- "PermissionStatus",
- "Permissions",
- "Position",
- "PositionError",
- "PositionOptions",
- "Request",
- "RequestCache",
- "RequestInit",
- "Response",
- "ResponseType",
- "Storage",
- "Url",
-] }
-wasm-bindgen-futures = "0.4"
-base64 = "0.22"
-serde-wasm-bindgen = "0.6"
-send_wrapper = "0.6"
-rusqlite = { version = "0.31", default-features = false }
-url = "2"
-chrono = "0.4"
-hex = "0.4"
-sha2 = "0.10"
-uuid = { version = "1.22", features = ["v4", "v7", "js"] }
-regex = "1"
-once_cell = "1"
-radroots-nostr = { path = "../lib/crates/nostr" }
-radroots-types = { path = "../lib/crates/types" }
-radroots-sql-core = { path = "../lib/crates/sql-core" }
-radroots-replica-db = { path = "../lib/crates/replica-db" }
-radroots-replica-db-schema = { path = "../lib/crates/replica-db-schema" }
-radroots-replica-sync = { path = "../lib/crates/replica-sync" }
-mf2-i18n-core = { path = "../../../../vendor/triesap/mf2-i18n/crates/mf2-i18n-core" }
-mf2-i18n-embedded = { path = "../../../../vendor/triesap/mf2-i18n/crates/mf2-i18n-embedded" }
-
[profile.release]
codegen-units = 1
lto = true
opt-level = "z"
strip = true
-
-[profile.wasm-release]
-inherits = "release"
-codegen-units = 1
-lto = true
-opt-level = "z"
-panic = "abort"
-strip = true
diff --git a/app/.env.example b/app/.env.example
@@ -1,2 +0,0 @@
-RADROOTS_DEFAULT_RELAYS=ws://localhost:8080,ws://localhost:8081
-RADROOTS_RELAY=ws://localhost:8082
diff --git a/app/.gitignore b/app/.gitignore
@@ -1,5 +0,0 @@
-/target/
-/dist/
-/pkg/
-stylesheets/app.generated.css
-**/*.rs.bk
diff --git a/app/Cargo.toml b/app/Cargo.toml
@@ -1,41 +0,0 @@
-[package]
-name = "radroots-app"
-authors = ["Radroots Authors"]
-version.workspace = true
-edition.workspace = true
-license.workspace = true
-rust-version.workspace = true
-
-[lib]
-crate-type = ["cdylib", "rlib"]
-
-[dependencies]
-leptos = { workspace = true, features = ["csr"] }
-leptos_router = { workspace = true }
-wasm-bindgen.workspace = true
-wasm-bindgen-futures.workspace = true
-futures.workspace = true
-js-sys.workspace = true
-web-sys.workspace = true
-gloo-timers = { workspace = true, features = ["futures"] }
-radroots-app-core = { path = "../crates/core" }
-radroots-app-ui-components = { path = "../crates/ui-components" }
-radroots-app-lib = { path = "../crates/app-lib" }
-mf2-i18n-core = { path = "../../../../../vendor/triesap/mf2-i18n/crates/mf2-i18n-core" }
-mf2-i18n-embedded = { path = "../../../../../vendor/triesap/mf2-i18n/crates/mf2-i18n-embedded" }
-radroots-log = { path = "../../lib/crates/log", default-features = false }
-radroots-nostr = { workspace = true }
-tracing-wasm = "0.2"
-console_error_panic_hook = "0.1"
-serde.workspace = true
-serde_json.workspace = true
-uuid.workspace = true
-sha2.workspace = true
-hex.workspace = true
-
-[dev-dependencies]
-async-trait.workspace = true
-
-[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
-radroots-log = { path = "../../lib/crates/log", features = ["std"] }
-chrono.workspace = true
diff --git a/app/app.css b/app/app.css
@@ -1,413 +0,0 @@
-@import "tailwindcss";
-@import "../crates/ui-tokens/assets/tokens.css";
-@import "../crates/ui-tokens/assets/themes/styles.css";
-@import "../crates/ui-tokens/assets/themes/layout.css";
-@import "../crates/ui-tokens/assets/themes/screens.css";
-@import "../crates/ui-tokens/assets/themes/theme_os.css";
-@import "../crates/ui-tokens/assets/themes/semantic.css";
-@import "./stylesheets/apps-base.css";
-@import "./stylesheets/apps-ui.css";
-@import "./stylesheets/styles-maplibre-gl.css";
-@import "./stylesheets/styles-superellipse.css";
-@import "../crates/ui-components/assets/list.css";
-@import "../crates/ui-components/assets/form.css";
-@import "../crates/ui-components/assets/nav.css";
-@import "../crates/ui-components/assets/nav_tabs.css";
-
-@custom-variant h-compact "@media (max-height: 540px)";
-@custom-variant se-compact "@media (max-width: 375px) and (max-height: 540px)";
-
-@source "./index.html";
-@source "./src/**/*.rs";
-@source "../crates/**/*.rs";
-
-@theme {
- --font-sans: "SF Pro Rounded", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
- --font-sansd: "SF Pro Display", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
- --font-serif: ui-serif, Georgia, "Times New Roman", serif;
- --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
-
- --text-guide: 1.25rem;
- --text-form_base: 1.08rem;
- --text-line_label: 1.3rem;
- --text-trellis_ti: 0.8rem;
- --text-line_d: 1.05rem;
- --text-nav_prev: 1.09rem;
- --text-nav_curr: 1.09rem;
- --text-env_ti: 1.05rem;
- --text-env_btnc: 1.1rem;
- --text-env_btnl: 1.1rem;
-
- --radius-input_form: 8px;
- --radius-entry: 1.05rem;
- --radius-touch: 1.25rem;
-
- --border-width-line: 1px;
- --border-width-edge: 2px;
-
- --animate-spin-slow: spin 3s linear infinite;
-}
-
-@utility rounded-touch {
- border-radius: var(--radius-xl);
-}
-
-@utility h-line {
- height: var(--size-line);
- min-height: var(--size-line);
-}
-
-@utility h-line_button {
- height: var(--size-line-button);
- min-height: var(--size-line-button);
-}
-
-@utility h-bold_button {
- height: var(--height-bold_button);
- min-height: var(--height-bold_button);
-}
-
-@layer base {
- :root {
- --nav-header-height: var(--padding-h_nav_page_header_ios0);
- --nav-tabs-height: var(--padding-h_nav_tabs_ios0);
- }
-
- html[data-layout="ios1"] {
- --nav-header-height: var(--padding-h_nav_page_header_ios1);
- --nav-tabs-height: var(--padding-h_nav_tabs_ios1);
- }
-
- html {
- font-family: var(--font-sans);
- background: var(--bg-app);
- color: var(--text-primary);
- height: 100%;
- width: 100%;
- overscroll-behavior: none;
- touch-action: manipulation;
- overflow: hidden;
- }
-
- body {
- min-height: 100dvh;
- margin: 0;
- background: var(--bg-app);
- height: 100%;
- width: 100%;
- overscroll-behavior: none;
- overflow: hidden;
- }
-
- * {
- -webkit-tap-highlight-color: transparent;
- }
-
- html[data-input="keyboard"] :focus-visible {
- outline: 2px solid var(--accent);
- outline-offset: 2px;
- border-radius: 8px;
- }
-
- html[data-input="pointer"] :focus {
- outline: none;
- }
-
- [data-disabled] {
- opacity: 0.5;
- pointer-events: none;
- }
-}
-
-@layer base {
- #app-root {
- min-height: 100dvh;
- height: 100dvh;
- width: 100%;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- overscroll-behavior: none;
- }
-
- #app-shell {
- width: 100%;
- display: flex;
- flex-direction: column;
- flex: 1 1 auto;
- min-height: 0;
- overflow: hidden;
- overscroll-behavior: none;
- }
-}
-
-@layer components {
- .app-page {
- position: relative;
- width: 100%;
- display: flex;
- flex-direction: column;
- flex: 1 1 auto;
- min-height: 0;
- height: 100dvh;
- min-height: 100dvh;
- overscroll-behavior: none;
- }
-
- .app-page-fixed {
- overflow: hidden;
- }
-
- .app-page-scroll {
- overflow-y: auto;
- overflow-x: hidden;
- -webkit-overflow-scrolling: touch;
- overscroll-behavior: none;
- scrollbar-width: none;
- -ms-overflow-style: none;
- }
-
- .app-page-scroll::-webkit-scrollbar {
- display: none;
- }
-
- .app-page-chrome {
- padding-bottom: calc(var(--nav-tabs-height) + var(--safe-b));
- }
-
- .app-page-body {
- padding: 0 16px 24px;
- }
-
- .app-page-shell {
- display: contents;
- }
-
- .app-view {
- will-change: opacity;
- position: relative;
- width: 100%;
- flex: 1 1 auto;
- min-height: 0;
- }
-
- .app-view-enter {
- animation: app-view-enter 200ms cubic-bezier(.2,.8,.2,1) both;
- }
-
- @media (prefers-reduced-motion: reduce) {
- .app-view-enter {
- animation: none;
- }
- }
-
- .ui-surface {
- background: var(--bg-elevated);
- color: var(--text-primary);
- border-radius: var(--radius-lg);
- box-shadow: var(--shadow-1);
- }
-
- .ui-card {
- background: var(--bg-elevated);
- color: var(--text-primary);
- border-radius: var(--radius-md);
- box-shadow: var(--shadow-1);
- }
-
- .ui-separator {
- height: 1px;
- background: var(--separator);
- }
-
- .ui-text-secondary {
- color: var(--text-secondary);
- }
-
- .ui-text-tertiary {
- color: var(--text-tertiary);
- }
-
- .ui-material-regular {
- background: var(--material-regular);
- backdrop-filter: blur(18px) saturate(180%);
- }
-
- [data-ui="dialog-overlay"],
- [data-ui="sheet-overlay"] {
- position: fixed;
- inset: 0;
- background: rgba(0, 0, 0, 0.32);
- opacity: 0;
- animation: overlay-fade-out 200ms ease both;
- }
-
- [data-ui="dialog-overlay"][data-state="open"],
- [data-ui="sheet-overlay"][data-state="open"] {
- opacity: 1;
- animation: overlay-fade-in 240ms var(--ease-ios) both;
- }
-
- [data-ui="sheet"] {
- position: fixed;
- left: 0;
- right: 0;
- bottom: 0;
- margin: 0 auto;
- padding: 16px;
- padding-bottom: calc(16px + var(--safe-b));
- border-top-left-radius: var(--radius-xl);
- border-top-right-radius: var(--radius-xl);
- background: var(--material-regular);
- backdrop-filter: blur(18px) saturate(180%);
- box-shadow: var(--shadow-sheet);
- transform: translateY(110%);
- opacity: 0;
- animation: sheet-slide-out 260ms ease both;
- will-change: transform, opacity;
- overscroll-behavior: none;
- }
-
- [data-ui="sheet"][data-state="open"] {
- transform: translateY(0);
- opacity: 1;
- animation: sheet-slide-in 420ms var(--ease-ios) both;
- }
-
- [data-ui="sheet-handle"] {
- width: 36px;
- height: 4px;
- margin: 0 auto 12px;
- border-radius: 999px;
- background: var(--separator);
- }
-
- .home-toggle {
- position: relative;
- display: grid;
- --home-toggle-seg: 98px;
- --home-toggle-pad: 2px;
- --home-toggle-inset-x: 0.75px;
- --home-toggle-border: 1px;
- grid-template-columns: var(--home-toggle-seg) var(--home-toggle-seg);
- align-items: center;
- width: calc(
- (var(--home-toggle-seg) * 2)
- + (var(--home-toggle-pad) * 2)
- + (var(--home-toggle-border) * 2)
- );
- padding: var(--home-toggle-pad);
- height: 32px;
- border-radius: 12px;
- background: var(--material-regular);
- border: var(--home-toggle-border) solid var(--stroke);
- box-shadow: var(--shadow-press);
- gap: 0;
- box-sizing: border-box;
- }
-
- .home-toggle__indicator {
- position: absolute;
- top: var(--home-toggle-pad);
- bottom: var(--home-toggle-pad);
- left: calc(var(--home-toggle-pad) + var(--home-toggle-inset-x));
- width: calc(var(--home-toggle-seg) - (var(--home-toggle-inset-x) * 2));
- border-radius: 8px;
- background: var(--bg-elevated);
- box-shadow: var(--shadow-1);
- transition: transform var(--dur-2) var(--ease-ios);
- will-change: transform;
- box-sizing: border-box;
- }
-
- .home-toggle--right .home-toggle__indicator {
- transform: translateX(var(--home-toggle-seg));
- }
-
- .home-toggle__button {
- position: relative;
- z-index: 1;
- border: none;
- background: transparent;
- height: 28px;
- padding: 0 var(--home-toggle-inset-x);
- border-radius: 8px;
- font-size: 0.95rem;
- font-weight: 600;
- color: var(--text-secondary);
- letter-spacing: 0.01em;
- cursor: pointer;
- transition: color var(--dur-1) var(--ease-ios);
- }
-
- .home-toggle__button.is-active {
- color: var(--text-primary);
- }
-
- .home-toggle__title {
- font-size: 1.1rem;
- font-weight: 600;
- color: var(--text-primary);
- text-align: center;
- }
-
- @media (prefers-reduced-motion: reduce) {
- .home-toggle__indicator {
- transition: none;
- }
- }
-
-}
-
-@keyframes overlay-fade-in {
- from {
- opacity: 0;
- }
- to {
- opacity: 1;
- }
-}
-
-@keyframes overlay-fade-out {
- from {
- opacity: 1;
- }
- to {
- opacity: 0;
- }
-}
-
-@keyframes sheet-slide-in {
- 0% {
- transform: translateY(110%);
- opacity: 0;
- }
- 60% {
- transform: translateY(-2%);
- opacity: 1;
- }
- 100% {
- transform: translateY(0);
- opacity: 1;
- }
-}
-
-@keyframes sheet-slide-out {
- 0% {
- transform: translateY(0);
- opacity: 1;
- }
- 100% {
- transform: translateY(110%);
- opacity: 0;
- }
-}
-
-@keyframes app-view-enter {
- from {
- opacity: 0;
- }
- to {
- opacity: 1;
- }
-}
diff --git a/app/assets/favicon.ico b/app/assets/favicon.ico
Binary files differ.
diff --git a/app/i18n/build/i18n.catalog.json b/app/i18n/build/i18n.catalog.json
@@ -1,2131 +0,0 @@
-{
- "schema": 1,
- "project": "radroots-app",
- "generated_at": "2026-02-02T00:00:00Z",
- "default_locale": "en",
- "messages": [
- {
- "key": "app.common.agree",
- "id": 4244146572,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.common.back",
- "id": 2990764910,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.common.continue",
- "id": 385087683,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.common.disagree",
- "id": 1460316265,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.common.missing",
- "id": 3520194192,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.common.no",
- "id": 1027581146,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.common.unknown",
- "id": 1588621956,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.common.yes",
- "id": 1057379348,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.health.aria",
- "id": 3172671582,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.health.button.checking",
- "id": 3465392175,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.health.button.run",
- "id": 1230595839,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.health.item.active_key",
- "id": 3589833151,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.health.item.bootstrap_state",
- "id": 737539659,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.health.item.datastore_roundtrip",
- "id": 1765955433,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.health.item.key_maps",
- "id": 1556244806,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.health.item.keystore",
- "id": 4253927470,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.health.item.notifications",
- "id": 3066505377,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.health.item.state_active_key",
- "id": 1422267473,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.health.item.tangle",
- "id": 4017335715,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.health.message.mismatch",
- "id": 2731529562,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.health.message.missing",
- "id": 2655522886,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.health.message.unavailable",
- "id": 645848060,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.health.message.uninitialized",
- "id": 3760340840,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.health.status.error",
- "id": 1466113983,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.health.status.ok",
- "id": 828208412,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.health.status.skipped",
- "id": 3849381953,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.health.title",
- "id": 1957618139,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.notifications.aria",
- "id": 1715867969,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.notifications.button.request",
- "id": 3959491343,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.notifications.button.requesting",
- "id": 3188736353,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.notifications.status.default",
- "id": 3760721877,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.notifications.status.denied",
- "id": 4076317564,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.notifications.status.granted",
- "id": 1582331756,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.notifications.status.unavailable",
- "id": 3118891586,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.notifications.title",
- "id": 907277970,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.reset.aria",
- "id": 3577090589,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.reset.button",
- "id": 3119733506,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.reset.status.done",
- "id": 3550097324,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.reset.status.idle",
- "id": 4013677279,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.reset.status.missing_backends",
- "id": 3398209588,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.reset.status.resetting",
- "id": 3968037487,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.status.aria",
- "id": 3526893198,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.home.title",
- "id": 3182100107,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.init.stage.database",
- "id": 3687544718,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.init.stage.download_geo",
- "id": 3254824087,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.init.stage.download_sql",
- "id": 333951479,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.init.stage.error",
- "id": 388972410,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.init.stage.geocoder",
- "id": 192352243,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.init.stage.idle",
- "id": 1446931460,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.init.stage.ready",
- "id": 4200712024,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.init.stage.storage",
- "id": 1242977634,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.action.clear",
- "id": 618410449,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.action.copy_dump",
- "id": 4122246909,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.action.download_dump",
- "id": 4154904867,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.action.refresh",
- "id": 2946508119,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.content.aria",
- "id": 495027300,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.dump.title",
- "id": 1388485668,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.entries.title",
- "id": 684273162,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.error.anchor_cast_failed",
- "id": 2533250272,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.error.anchor_failed",
- "id": 3154831416,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.error.blob_failed",
- "id": 2801667274,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.error.copy_failed",
- "id": 3247522700,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.error.copy_unavailable",
- "id": 4141297599,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.error.document_unavailable",
- "id": 1080722381,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.error.download_unavailable",
- "id": 4264123149,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.error.url_failed",
- "id": 2872906070,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.error.window_unavailable",
- "id": 2350479185,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.filters.aria",
- "id": 2416988381,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.filters.from_ms",
- "id": 1695556959,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.filters.search_placeholder",
- "id": 2459121539,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.filters.to_ms",
- "id": 3879399801,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.level.all",
- "id": 3715000598,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.level.debug",
- "id": 2466649524,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.level.error",
- "id": 135430264,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.level.info",
- "id": 289833569,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.level.warn",
- "id": 3186734388,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.pagination.aria",
- "id": 887319320,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.pagination.next",
- "id": 2509771422,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.pagination.prev",
- "id": 3713405982,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.status.copy_ok",
- "id": 4173530628,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.status.download_ok",
- "id": 251408416,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.status.dump_empty",
- "id": 3480269256,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.status.idle",
- "id": 3608732267,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.status.loading",
- "id": 2750762471,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.status.support_bundle_ready",
- "id": 3166859448,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.status.support_copy_ok",
- "id": 1839220955,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.summary.limit",
- "id": 3457741666,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.summary.of",
- "id": 2155553479,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.summary.page",
- "id": 1355876668,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.summary.showing",
- "id": 2923985603,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.support.button.bundle",
- "id": 2889964186,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.support.button.copy_instructions",
- "id": 3553383813,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.support.instructions.download",
- "id": 483676330,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.support.instructions.notes",
- "id": 3320932212,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.support.instructions.share",
- "id": 1138860050,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.support.instructions.title",
- "id": 3942736448,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.support.title",
- "id": 1704291719,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.logs.title",
- "id": 2765296598,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.nav.home",
- "id": 2360822558,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.nav.logs",
- "id": 952998791,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.nav.primary_aria",
- "id": 3495744422,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.nav.settings",
- "id": 326721352,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.nav.ui",
- "id": 2416341108,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.not_found",
- "id": 3182331848,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.recovery.body",
- "id": 3376325333,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.recovery.reset.button",
- "id": 3705159239,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.recovery.reset.confirm",
- "id": 1689963212,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.recovery.title",
- "id": 853393776,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.settings.actions.export_db",
- "id": 4012031673,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.settings.actions.logout",
- "id": 1314061244,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.settings.appearance.color_mode.label",
- "id": 4036414159,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.settings.appearance.color_mode.option.dark",
- "id": 1500556587,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.settings.appearance.color_mode.option.light",
- "id": 2964032561,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.settings.appearance.color_mode.option.system",
- "id": 116103587,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.settings.appearance.title",
- "id": 410103833,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.settings.status.title",
- "id": 3557645539,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.settings.status.updated",
- "id": 4045760753,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.settings.status.view",
- "id": 1254024536,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.settings.system.status",
- "id": 809595327,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.settings.system.title",
- "id": 797085259,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.settings.title",
- "id": 2068161917,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.business.title",
- "id": 1801429027,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.acceptance.body",
- "id": 2382792662,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.acceptance.title",
- "id": 3507789532,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.changes.body",
- "id": 951360382,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.changes.title",
- "id": 3581217580,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.consequences.body",
- "id": 622716437,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.consequences.title",
- "id": 2552980318,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.contact.body",
- "id": 2214254760,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.contact.title",
- "id": 3880966154,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.disclaimer.body",
- "id": 2436514119,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.disclaimer.title",
- "id": 4158835886,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.introduction.body",
- "id": 1260183649,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.introduction.title",
- "id": 4105467488,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.prohibited_conduct.item.harass",
- "id": 3851004027,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.prohibited_conduct.item.impersonate",
- "id": 469274623,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.prohibited_conduct.item.intimidate",
- "id": 1885763914,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.prohibited_conduct.item.violence",
- "id": 324691776,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.prohibited_conduct.title",
- "id": 2564751006,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.prohibited_content.body",
- "id": 1145716858,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.prohibited_content.item.harass",
- "id": 3479565655,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.prohibited_content.item.hate_speech",
- "id": 2878691915,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.prohibited_content.item.illegal",
- "id": 2187251819,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.prohibited_content.item.impersonate",
- "id": 2234909629,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.prohibited_content.item.minors",
- "id": 3085497859,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.prohibited_content.item.pornographic",
- "id": 929022219,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.prohibited_content.title",
- "id": 1738432508,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.eula.title",
- "id": 2514464102,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.farmer.title",
- "id": 3967778278,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.intro.body",
- "id": 3075641340,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.intro.kicker",
- "id": 397915792,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.intro.welcome",
- "id": 909038201,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.key_add.placeholder",
- "id": 728766925,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.key_add.title",
- "id": 3229091810,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.key_choice.create",
- "id": 963716841,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.key_choice.title",
- "id": 2829853649,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.key_choice.use_existing",
- "id": 3945623834,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.lock.pending.body",
- "id": 1475586291,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.lock.pending.title",
- "id": 2337040532,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.lock.retry",
- "id": 2739457561,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.locked.body",
- "id": 3219932589,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.locked.title",
- "id": 2703779876,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.profile.confirm_no_name",
- "id": 420411302,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.profile.nip05.prefix",
- "id": 3344734641,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.profile.nip05.suffix",
- "id": 853057844,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.profile.placeholder",
- "id": 321152177,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.setup.profile.title",
- "id": 103377718,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.ui_demo.input.label",
- "id": 184074538,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.ui_demo.input.placeholder",
- "id": 2226709883,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.ui_demo.item.notifications",
- "id": 428578162,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.ui_demo.list.title",
- "id": 2975877094,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.ui_demo.sheet.close",
- "id": 3592615847,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.ui_demo.sheet.description",
- "id": 3475136064,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.ui_demo.sheet.open",
- "id": 3618188646,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.ui_demo.sheet.title",
- "id": 17449068,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.ui_demo.status.enabled",
- "id": 2298608892,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.ui_demo.sync.label",
- "id": 1130669379,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.ui_demo.sync.option.daily",
- "id": 410854867,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.ui_demo.sync.option.never",
- "id": 635139478,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.ui_demo.sync.option.weekly",
- "id": 2766276811,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "app.ui_demo.title",
- "id": 570973545,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "error.app.init.assets",
- "id": 172100683,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "error.app.init.config",
- "id": 3737196850,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "error.app.init.datastore",
- "id": 1124445179,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "error.app.init.idb",
- "id": 3385056441,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "error.app.init.keystore",
- "id": 2043630525,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "error.app.state.already_exists",
- "id": 4003177973,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "error.app.state.checksum_invalid",
- "id": 56309183,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "error.app.state.corrupt",
- "id": 2251393452,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "error.app.state.missing",
- "id": 2404167425,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "error.app.state.schema_unsupported",
- "id": 3991204251,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "error.client.notifications.read_failure",
- "id": 350195904,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- },
- {
- "key": "error.client.notifications.unavailable",
- "id": 4206009627,
- "args": [],
- "features": {
- "select": false,
- "plural_cardinal": false,
- "plural_ordinal": false,
- "formatters": []
- }
- }
- ]
-}
-\ No newline at end of file
diff --git a/app/i18n/build/id_map.json b/app/i18n/build/id_map.json
@@ -1,195 +0,0 @@
-{
- "app.common.agree": 4244146572,
- "app.common.back": 2990764910,
- "app.common.continue": 385087683,
- "app.common.disagree": 1460316265,
- "app.common.missing": 3520194192,
- "app.common.no": 1027581146,
- "app.common.unknown": 1588621956,
- "app.common.yes": 1057379348,
- "app.home.health.aria": 3172671582,
- "app.home.health.button.checking": 3465392175,
- "app.home.health.button.run": 1230595839,
- "app.home.health.item.active_key": 3589833151,
- "app.home.health.item.bootstrap_state": 737539659,
- "app.home.health.item.datastore_roundtrip": 1765955433,
- "app.home.health.item.key_maps": 1556244806,
- "app.home.health.item.keystore": 4253927470,
- "app.home.health.item.notifications": 3066505377,
- "app.home.health.item.state_active_key": 1422267473,
- "app.home.health.item.tangle": 4017335715,
- "app.home.health.message.mismatch": 2731529562,
- "app.home.health.message.missing": 2655522886,
- "app.home.health.message.unavailable": 645848060,
- "app.home.health.message.uninitialized": 3760340840,
- "app.home.health.status.error": 1466113983,
- "app.home.health.status.ok": 828208412,
- "app.home.health.status.skipped": 3849381953,
- "app.home.health.title": 1957618139,
- "app.home.notifications.aria": 1715867969,
- "app.home.notifications.button.request": 3959491343,
- "app.home.notifications.button.requesting": 3188736353,
- "app.home.notifications.status.default": 3760721877,
- "app.home.notifications.status.denied": 4076317564,
- "app.home.notifications.status.granted": 1582331756,
- "app.home.notifications.status.unavailable": 3118891586,
- "app.home.notifications.title": 907277970,
- "app.home.reset.aria": 3577090589,
- "app.home.reset.button": 3119733506,
- "app.home.reset.status.done": 3550097324,
- "app.home.reset.status.idle": 4013677279,
- "app.home.reset.status.missing_backends": 3398209588,
- "app.home.reset.status.resetting": 3968037487,
- "app.home.status.aria": 3526893198,
- "app.home.title": 3182100107,
- "app.init.stage.database": 3687544718,
- "app.init.stage.download_geo": 3254824087,
- "app.init.stage.download_sql": 333951479,
- "app.init.stage.error": 388972410,
- "app.init.stage.geocoder": 192352243,
- "app.init.stage.idle": 1446931460,
- "app.init.stage.ready": 4200712024,
- "app.init.stage.storage": 1242977634,
- "app.logs.action.clear": 618410449,
- "app.logs.action.copy_dump": 4122246909,
- "app.logs.action.download_dump": 4154904867,
- "app.logs.action.refresh": 2946508119,
- "app.logs.content.aria": 495027300,
- "app.logs.dump.title": 1388485668,
- "app.logs.entries.title": 684273162,
- "app.logs.error.anchor_cast_failed": 2533250272,
- "app.logs.error.anchor_failed": 3154831416,
- "app.logs.error.blob_failed": 2801667274,
- "app.logs.error.copy_failed": 3247522700,
- "app.logs.error.copy_unavailable": 4141297599,
- "app.logs.error.document_unavailable": 1080722381,
- "app.logs.error.download_unavailable": 4264123149,
- "app.logs.error.url_failed": 2872906070,
- "app.logs.error.window_unavailable": 2350479185,
- "app.logs.filters.aria": 2416988381,
- "app.logs.filters.from_ms": 1695556959,
- "app.logs.filters.search_placeholder": 2459121539,
- "app.logs.filters.to_ms": 3879399801,
- "app.logs.level.all": 3715000598,
- "app.logs.level.debug": 2466649524,
- "app.logs.level.error": 135430264,
- "app.logs.level.info": 289833569,
- "app.logs.level.warn": 3186734388,
- "app.logs.pagination.aria": 887319320,
- "app.logs.pagination.next": 2509771422,
- "app.logs.pagination.prev": 3713405982,
- "app.logs.status.copy_ok": 4173530628,
- "app.logs.status.download_ok": 251408416,
- "app.logs.status.dump_empty": 3480269256,
- "app.logs.status.idle": 3608732267,
- "app.logs.status.loading": 2750762471,
- "app.logs.status.support_bundle_ready": 3166859448,
- "app.logs.status.support_copy_ok": 1839220955,
- "app.logs.summary.limit": 3457741666,
- "app.logs.summary.of": 2155553479,
- "app.logs.summary.page": 1355876668,
- "app.logs.summary.showing": 2923985603,
- "app.logs.support.button.bundle": 2889964186,
- "app.logs.support.button.copy_instructions": 3553383813,
- "app.logs.support.instructions.download": 483676330,
- "app.logs.support.instructions.notes": 3320932212,
- "app.logs.support.instructions.share": 1138860050,
- "app.logs.support.instructions.title": 3942736448,
- "app.logs.support.title": 1704291719,
- "app.logs.title": 2765296598,
- "app.nav.home": 2360822558,
- "app.nav.logs": 952998791,
- "app.nav.primary_aria": 3495744422,
- "app.nav.settings": 326721352,
- "app.nav.ui": 2416341108,
- "app.not_found": 3182331848,
- "app.recovery.body": 3376325333,
- "app.recovery.reset.button": 3705159239,
- "app.recovery.reset.confirm": 1689963212,
- "app.recovery.title": 853393776,
- "app.settings.actions.export_db": 4012031673,
- "app.settings.actions.logout": 1314061244,
- "app.settings.appearance.color_mode.label": 4036414159,
- "app.settings.appearance.color_mode.option.dark": 1500556587,
- "app.settings.appearance.color_mode.option.light": 2964032561,
- "app.settings.appearance.color_mode.option.system": 116103587,
- "app.settings.appearance.title": 410103833,
- "app.settings.status.title": 3557645539,
- "app.settings.status.updated": 4045760753,
- "app.settings.status.view": 1254024536,
- "app.settings.system.status": 809595327,
- "app.settings.system.title": 797085259,
- "app.settings.title": 2068161917,
- "app.setup.business.title": 1801429027,
- "app.setup.eula.acceptance.body": 2382792662,
- "app.setup.eula.acceptance.title": 3507789532,
- "app.setup.eula.changes.body": 951360382,
- "app.setup.eula.changes.title": 3581217580,
- "app.setup.eula.consequences.body": 622716437,
- "app.setup.eula.consequences.title": 2552980318,
- "app.setup.eula.contact.body": 2214254760,
- "app.setup.eula.contact.title": 3880966154,
- "app.setup.eula.disclaimer.body": 2436514119,
- "app.setup.eula.disclaimer.title": 4158835886,
- "app.setup.eula.introduction.body": 1260183649,
- "app.setup.eula.introduction.title": 4105467488,
- "app.setup.eula.prohibited_conduct.item.harass": 3851004027,
- "app.setup.eula.prohibited_conduct.item.impersonate": 469274623,
- "app.setup.eula.prohibited_conduct.item.intimidate": 1885763914,
- "app.setup.eula.prohibited_conduct.item.violence": 324691776,
- "app.setup.eula.prohibited_conduct.title": 2564751006,
- "app.setup.eula.prohibited_content.body": 1145716858,
- "app.setup.eula.prohibited_content.item.harass": 3479565655,
- "app.setup.eula.prohibited_content.item.hate_speech": 2878691915,
- "app.setup.eula.prohibited_content.item.illegal": 2187251819,
- "app.setup.eula.prohibited_content.item.impersonate": 2234909629,
- "app.setup.eula.prohibited_content.item.minors": 3085497859,
- "app.setup.eula.prohibited_content.item.pornographic": 929022219,
- "app.setup.eula.prohibited_content.title": 1738432508,
- "app.setup.eula.title": 2514464102,
- "app.setup.farmer.title": 3967778278,
- "app.setup.intro.body": 3075641340,
- "app.setup.intro.kicker": 397915792,
- "app.setup.intro.welcome": 909038201,
- "app.setup.key_add.placeholder": 728766925,
- "app.setup.key_add.title": 3229091810,
- "app.setup.key_choice.create": 963716841,
- "app.setup.key_choice.title": 2829853649,
- "app.setup.key_choice.use_existing": 3945623834,
- "app.setup.lock.pending.body": 1475586291,
- "app.setup.lock.pending.title": 2337040532,
- "app.setup.lock.retry": 2739457561,
- "app.setup.locked.body": 3219932589,
- "app.setup.locked.title": 2703779876,
- "app.setup.profile.confirm_no_name": 420411302,
- "app.setup.profile.nip05.prefix": 3344734641,
- "app.setup.profile.nip05.suffix": 853057844,
- "app.setup.profile.placeholder": 321152177,
- "app.setup.profile.title": 103377718,
- "app.ui_demo.input.label": 184074538,
- "app.ui_demo.input.placeholder": 2226709883,
- "app.ui_demo.item.notifications": 428578162,
- "app.ui_demo.list.title": 2975877094,
- "app.ui_demo.sheet.close": 3592615847,
- "app.ui_demo.sheet.description": 3475136064,
- "app.ui_demo.sheet.open": 3618188646,
- "app.ui_demo.sheet.title": 17449068,
- "app.ui_demo.status.enabled": 2298608892,
- "app.ui_demo.sync.label": 1130669379,
- "app.ui_demo.sync.option.daily": 410854867,
- "app.ui_demo.sync.option.never": 635139478,
- "app.ui_demo.sync.option.weekly": 2766276811,
- "app.ui_demo.title": 570973545,
- "error.app.init.assets": 172100683,
- "error.app.init.config": 3737196850,
- "error.app.init.datastore": 1124445179,
- "error.app.init.idb": 3385056441,
- "error.app.init.keystore": 2043630525,
- "error.app.state.already_exists": 4003177973,
- "error.app.state.checksum_invalid": 56309183,
- "error.app.state.corrupt": 2251393452,
- "error.app.state.missing": 2404167425,
- "error.app.state.schema_unsupported": 3991204251,
- "error.client.notifications.read_failure": 350195904,
- "error.client.notifications.unavailable": 4206009627
-}
-\ No newline at end of file
diff --git a/app/i18n/build/id_map_hash b/app/i18n/build/id_map_hash
@@ -1 +0,0 @@
-sha256:74c93a0a2572e6ddee22ba8121685c275e5d3b98dad0c713394bf3d72fbc78ad
diff --git a/app/i18n/build/manifest.json b/app/i18n/build/manifest.json
@@ -1 +0,0 @@
-{"schema":1,"release_id":"dev","generated_at":"2026-02-02T00:00:00Z","default_locale":"en","supported_locales":["en"],"id_map_hash":"sha256:74c93a0a2572e6ddee22ba8121685c275e5d3b98dad0c713394bf3d72fbc78ad","mf2_packs":{"en":{"kind":"base","url":"packs/en.mf2pack","hash":"sha256:1e5b94cb8e1e51cece51482becf25da1b42425165934d34fbb3fd61f691c6db7","size":11611,"content_encoding":"identity","pack_schema":0}}}
-\ No newline at end of file
diff --git a/app/i18n/build/packs/en.mf2pack b/app/i18n/build/packs/en.mf2pack
Binary files differ.
diff --git a/app/i18n/id_salt.txt b/app/i18n/id_salt.txt
@@ -1 +0,0 @@
-radroots-app-v1
diff --git a/app/i18n/locales/en/messages.mf2 b/app/i18n/locales/en/messages.mf2
@@ -1,397 +0,0 @@
-# common
-app.common.missing = missing
-
-app.common.unknown = unknown
-
-app.common.back = Back
-
-app.common.continue = Continue
-
-app.common.no = No
-
-app.common.yes = Yes
-
-app.common.agree = Agree
-
-app.common.disagree = Disagree
-
-# nav
-app.nav.primary_aria = primary
-
-app.nav.home = home
-
-app.nav.logs = logs
-
-app.nav.ui = ui
-
-app.nav.settings = settings
-
-# not found
-app.not_found = not found
-
-# recovery
-app.recovery.title = Recovery required
-
-app.recovery.body = This device could not verify its setup state. Reset the device to continue.
-
-app.recovery.reset.button = Reset device
-
-app.recovery.reset.confirm = This will erase local data on this device. Continue?
-
-# init stages
-app.init.stage.idle = idle
-
-app.init.stage.storage = storage
-
-app.init.stage.download_sql = download sql
-
-app.init.stage.download_geo = download geo
-
-app.init.stage.database = database
-
-app.init.stage.geocoder = geocoder
-
-app.init.stage.ready = ready
-
-app.init.stage.error = error
-
-# home
-app.home.title = app
-
-app.home.status.aria = status
-
-app.home.reset.aria = reset
-
-app.home.reset.button = reset
-
-app.home.reset.status.idle = idle
-
-app.home.reset.status.resetting = resetting
-
-app.home.reset.status.missing_backends = backends unavailable
-
-app.home.reset.status.done = reset complete
-
-app.home.notifications.aria = notifications
-
-app.home.notifications.title = notifications
-
-app.home.notifications.button.request = request
-
-app.home.notifications.button.requesting = requesting
-
-app.home.notifications.status.granted = granted
-
-app.home.notifications.status.denied = denied
-
-app.home.notifications.status.default = default
-
-app.home.notifications.status.unavailable = unavailable
-
-app.home.health.aria = health checks
-
-app.home.health.title = health checks
-
-app.home.health.button.checking = checking
-
-app.home.health.button.run = run checks
-
-app.home.health.item.key_maps = key maps
-
-app.home.health.item.bootstrap_state = bootstrap state
-
-app.home.health.item.state_active_key = active key state
-
-app.home.health.item.notifications = notifications
-
-app.home.health.item.tangle = tangle
-
-app.home.health.item.datastore_roundtrip = datastore roundtrip
-
-app.home.health.item.keystore = keystore
-
-app.home.health.item.active_key = active key
-
-app.home.health.status.ok = ok
-
-app.home.health.status.error = error
-
-app.home.health.status.skipped = skipped
-
-app.home.health.message.missing = missing
-
-app.home.health.message.mismatch = mismatch
-
-app.home.health.message.uninitialized = uninitialized
-
-app.home.health.message.unavailable = unavailable
-
-# setup
-app.setup.intro.kicker = Configure
-
-app.setup.intro.welcome = Welcome to Radroots!
-
-app.setup.intro.body = Your device will be configured by the setup wizard.
-
-app.setup.key_choice.title = Configure Device
-
-app.setup.key_choice.create = Create new keypair
-
-app.setup.key_choice.use_existing = Use existing keypair
-
-app.setup.key_add.title = Add existing key
-
-app.setup.key_add.placeholder = Enter nostr nsec/hex
-
-app.setup.profile.title = Add Profile
-
-app.setup.profile.placeholder = Enter profile name
-
-app.setup.profile.nip05.prefix = Create
-
-app.setup.profile.nip05.suffix = NIP-05 address
-
-app.setup.profile.confirm_no_name = Your profile will be created without a name. You can change this later in Settings > Profile
-
-app.setup.lock.pending.title = Preparing setup
-
-app.setup.lock.pending.body = Checking if this device is already being configured.
-
-app.setup.locked.title = Setup in progress
-
-app.setup.locked.body = This device is being configured in another session. Wait a moment and try again.
-
-app.setup.lock.retry = Try again
-
-app.setup.farmer.title = Setup for Farmer
-
-app.setup.business.title = Setup for Business
-
-# eula
-app.setup.eula.title = End User License Agreement
-
-app.setup.eula.introduction.title = Introduction
-
-app.setup.eula.introduction.body = This End User License Agreement ("EULA") is a legal agreement between you and Radroots Inc. for the use of our mobile application Radroots. By installing, accessing, or using our application, you agree to be bound by the terms and conditions of this EULA.
-
-app.setup.eula.prohibited_content.title = Prohibited Content and Conduct
-
-app.setup.eula.prohibited_content.body = You agree not to use our application to create, upload, post, send, or store any content that:
-
-app.setup.eula.prohibited_content.item.illegal = Is illegal, infringing, or fraudulent
-
-app.setup.eula.prohibited_content.item.pornographic = Is pornographic, obscene, or offensive
-
-app.setup.eula.prohibited_content.item.hate_speech = Is discriminatory or promotes hate speech
-
-app.setup.eula.prohibited_content.item.minors = Is harmful to minors
-
-app.setup.eula.prohibited_content.item.harass = Is intended to harass or bully others
-
-app.setup.eula.prohibited_content.item.impersonate = Is intended to impersonate others
-
-app.setup.eula.prohibited_conduct.title = You also agree not to engage in any conduct that:
-
-app.setup.eula.prohibited_conduct.item.harass = Harasses or bullies others
-
-app.setup.eula.prohibited_conduct.item.impersonate = Impersonates others
-
-app.setup.eula.prohibited_conduct.item.intimidate = Is intended to intimidate or threaten others
-
-app.setup.eula.prohibited_conduct.item.violence = Is intended to promote or incite violence
-
-app.setup.eula.consequences.title = Consequences of Violation
-
-app.setup.eula.consequences.body = Any violation of this EULA, including the prohibited content and conduct outlined above, may result in the termination of your access to our application.
-
-app.setup.eula.disclaimer.title = Disclaimer of Warranties and Limitation of Liability
-
-app.setup.eula.disclaimer.body = Our application is provided "as is" and "as available" without warranty of any kind, either express or implied, including but not limited to the implied warranties of merchantability and fitness for a particular purpose. We do not guarantee that our application will be uninterrupted or error-free. In no event shall Radroots Inc. be liable for any damages whatsoever, including but not limited to direct, indirect, special, incidental, or consequential damages, arising out of or in connection with the use or inability to use our application.
-
-app.setup.eula.changes.title = Changes to EULA
-
-app.setup.eula.changes.body = We reserve the right to update or modify this EULA at any time and without prior notice. Your continued use of our application following any changes to this EULA will be deemed to be your acceptance of such changes.
-
-app.setup.eula.contact.title = Contact Information
-
-app.setup.eula.contact.body = If you have any questions about this EULA, please contact us at info@radroots.org.
-
-app.setup.eula.acceptance.title = Acceptance of Terms
-
-app.setup.eula.acceptance.body = By using our application, you signify your acceptance of this EULA. If you do not agree to this EULA, you may not use our application.
-
-# logs
-app.logs.title = logs
-
-app.logs.action.refresh = refresh
-
-app.logs.action.clear = clear
-
-app.logs.action.copy_dump = copy dump
-
-app.logs.action.download_dump = download dump
-
-app.logs.status.idle = idle
-
-app.logs.status.loading = loading
-
-app.logs.status.dump_empty = dump empty
-
-app.logs.status.copy_ok = copied
-
-app.logs.status.download_ok = download ready
-
-app.logs.status.support_copy_ok = instructions copied
-
-app.logs.status.support_bundle_ready = support bundle ready
-
-app.logs.error.copy_unavailable = copy unavailable
-
-app.logs.error.copy_failed = copy failed
-
-app.logs.error.window_unavailable = window unavailable
-
-app.logs.error.download_unavailable = download unavailable
-
-app.logs.error.document_unavailable = document unavailable
-
-app.logs.error.blob_failed = blob creation failed
-
-app.logs.error.url_failed = url creation failed
-
-app.logs.error.anchor_failed = anchor creation failed
-
-app.logs.error.anchor_cast_failed = anchor cast failed
-
-app.logs.filters.aria = filters
-
-app.logs.filters.search_placeholder = search code/message/context
-
-app.logs.filters.from_ms = from ms
-
-app.logs.filters.to_ms = to ms
-
-app.logs.level.all = all
-
-app.logs.level.debug = debug
-
-app.logs.level.info = info
-
-app.logs.level.warn = warn
-
-app.logs.level.error = error
-
-app.logs.summary.showing = showing
-
-app.logs.summary.of = of
-
-app.logs.summary.limit = limit
-
-app.logs.summary.page = page
-
-app.logs.pagination.aria = pagination
-
-app.logs.pagination.prev = prev
-
-app.logs.pagination.next = next
-
-app.logs.support.button.bundle = support bundle
-
-app.logs.support.button.copy_instructions = copy instructions
-
-app.logs.content.aria = log content
-
-app.logs.entries.title = entries
-
-app.logs.dump.title = dump (jsonl)
-
-app.logs.support.title = support instructions
-
-app.logs.support.instructions.title = support bundle
-
-app.logs.support.instructions.download = 1) download the log dump jsonl file
-
-app.logs.support.instructions.share = 2) share the file with support
-
-app.logs.support.instructions.notes = 3) include notes about the issue and time window
-
-# settings
-app.settings.title = settings
-
-app.settings.appearance.title = appearance
-
-app.settings.appearance.color_mode.label = color mode
-
-app.settings.appearance.color_mode.option.system = system
-
-app.settings.appearance.color_mode.option.light = light
-
-app.settings.appearance.color_mode.option.dark = dark
-
-app.settings.actions.export_db = export database
-
-app.settings.actions.logout = logout
-
-app.settings.system.title = system
-
-app.settings.system.status = system status
-
-app.settings.status.title = system status
-
-app.settings.status.updated = last updated
-
-app.settings.status.view = view status
-
-# ui demo
-app.ui_demo.title = ui demo
-
-app.ui_demo.list.title = list preview
-
-app.ui_demo.item.notifications = notifications
-
-app.ui_demo.status.enabled = enabled
-
-app.ui_demo.input.placeholder = add a note
-
-app.ui_demo.input.label = note
-
-app.ui_demo.sync.label = sync frequency
-
-app.ui_demo.sync.option.daily = daily
-
-app.ui_demo.sync.option.weekly = weekly
-
-app.ui_demo.sync.option.never = never
-
-app.ui_demo.sheet.open = open sheet
-
-app.ui_demo.sheet.title = sheet preview
-
-app.ui_demo.sheet.description = this is a placeholder sheet for iOS styling.
-
-app.ui_demo.sheet.close = close
-
-# errors
-error.app.init.idb = storage unavailable
-
-error.app.init.datastore = datastore unavailable
-
-error.app.init.keystore = keystore unavailable
-
-error.app.init.config = configuration error
-
-error.app.init.assets = assets unavailable
-
-error.app.state.missing = setup required
-
-error.app.state.corrupt = stored data is corrupt
-
-error.app.state.checksum_invalid = stored data checksum invalid
-
-error.app.state.schema_unsupported = stored data schema unsupported
-
-error.app.state.already_exists = setup already completed
-
-error.client.notifications.unavailable = notifications unavailable
-
-error.client.notifications.read_failure = notifications permission read failed
diff --git a/app/i18n/mf2-i18n.toml b/app/i18n/mf2-i18n.toml
@@ -1,3 +0,0 @@
-default_locale = "en"
-source_dirs = ["locales"]
-project_salt_path = "id_salt.txt"
diff --git a/app/index.html b/app/index.html
@@ -1,20 +0,0 @@
-<!doctype html>
-<html lang="en">
- <head>
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1" />
- <title>Rad Roots</title>
- <link rel="stylesheet" type="text/css" href="https://static.radroots.io/webfonts/sf-pro-display/styles.css" />
- <link rel="stylesheet" type="text/css" href="https://static.radroots.io/webfonts/sf-pro-rounded/styles.css" />
- <link data-trunk rel="icon" href="assets/favicon.ico" />
- <link data-trunk rel="css" href="stylesheets/app.generated.css" />
- <link
- data-trunk
- rel="rust"
- data-cargo-profile-release="wasm-release"
- data-wasm-opt="z"
- data-wasm-opt-params="--enable-bulk-memory-opt"
- />
- </head>
- <body id="app-root"></body>
-</html>
diff --git a/app/package-lock.json b/app/package-lock.json
@@ -1,1052 +0,0 @@
-{
- "name": "radroots-app",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "radroots-app",
- "devDependencies": {
- "@tailwindcss/cli": "^4.1.18",
- "tailwindcss": "^4.1.18"
- }
- },
- "node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.13",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
- "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.0",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/remapping": {
- "version": "2.3.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
- "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
- "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.31",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
- "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.1.0",
- "@jridgewell/sourcemap-codec": "^1.4.14"
- }
- },
- "node_modules/@parcel/watcher": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
- "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "dependencies": {
- "detect-libc": "^2.0.3",
- "is-glob": "^4.0.3",
- "node-addon-api": "^7.0.0",
- "picomatch": "^4.0.3"
- },
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- },
- "optionalDependencies": {
- "@parcel/watcher-android-arm64": "2.5.6",
- "@parcel/watcher-darwin-arm64": "2.5.6",
- "@parcel/watcher-darwin-x64": "2.5.6",
- "@parcel/watcher-freebsd-x64": "2.5.6",
- "@parcel/watcher-linux-arm-glibc": "2.5.6",
- "@parcel/watcher-linux-arm-musl": "2.5.6",
- "@parcel/watcher-linux-arm64-glibc": "2.5.6",
- "@parcel/watcher-linux-arm64-musl": "2.5.6",
- "@parcel/watcher-linux-x64-glibc": "2.5.6",
- "@parcel/watcher-linux-x64-musl": "2.5.6",
- "@parcel/watcher-win32-arm64": "2.5.6",
- "@parcel/watcher-win32-ia32": "2.5.6",
- "@parcel/watcher-win32-x64": "2.5.6"
- }
- },
- "node_modules/@parcel/watcher-android-arm64": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz",
- "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-darwin-arm64": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz",
- "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-darwin-x64": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz",
- "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-freebsd-x64": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz",
- "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-arm-glibc": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz",
- "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-arm-musl": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz",
- "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-arm64-glibc": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz",
- "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-arm64-musl": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz",
- "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-x64-glibc": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz",
- "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-x64-musl": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz",
- "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-win32-arm64": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz",
- "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-win32-ia32": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz",
- "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-win32-x64": {
- "version": "2.5.6",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz",
- "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@tailwindcss/cli": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.18.tgz",
- "integrity": "sha512-sMZ+lZbDyxwjD2E0L7oRUjJ01Ffjtme5OtjvvnC+cV4CEDcbqzbp25TCpxHj6kWLU9+DlqJOiNgSOgctC2aZmg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@parcel/watcher": "^2.5.1",
- "@tailwindcss/node": "4.1.18",
- "@tailwindcss/oxide": "4.1.18",
- "enhanced-resolve": "^5.18.3",
- "mri": "^1.2.0",
- "picocolors": "^1.1.1",
- "tailwindcss": "4.1.18"
- },
- "bin": {
- "tailwindcss": "dist/index.mjs"
- }
- },
- "node_modules/@tailwindcss/node": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
- "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/remapping": "^2.3.4",
- "enhanced-resolve": "^5.18.3",
- "jiti": "^2.6.1",
- "lightningcss": "1.30.2",
- "magic-string": "^0.30.21",
- "source-map-js": "^1.2.1",
- "tailwindcss": "4.1.18"
- }
- },
- "node_modules/@tailwindcss/oxide": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz",
- "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 10"
- },
- "optionalDependencies": {
- "@tailwindcss/oxide-android-arm64": "4.1.18",
- "@tailwindcss/oxide-darwin-arm64": "4.1.18",
- "@tailwindcss/oxide-darwin-x64": "4.1.18",
- "@tailwindcss/oxide-freebsd-x64": "4.1.18",
- "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18",
- "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18",
- "@tailwindcss/oxide-linux-arm64-musl": "4.1.18",
- "@tailwindcss/oxide-linux-x64-gnu": "4.1.18",
- "@tailwindcss/oxide-linux-x64-musl": "4.1.18",
- "@tailwindcss/oxide-wasm32-wasi": "4.1.18",
- "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18",
- "@tailwindcss/oxide-win32-x64-msvc": "4.1.18"
- }
- },
- "node_modules/@tailwindcss/oxide-android-arm64": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz",
- "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-darwin-arm64": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz",
- "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-darwin-x64": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz",
- "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-freebsd-x64": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz",
- "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz",
- "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz",
- "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz",
- "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz",
- "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-x64-musl": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz",
- "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-wasm32-wasi": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz",
- "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==",
- "bundleDependencies": [
- "@napi-rs/wasm-runtime",
- "@emnapi/core",
- "@emnapi/runtime",
- "@tybys/wasm-util",
- "@emnapi/wasi-threads",
- "tslib"
- ],
- "cpu": [
- "wasm32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/core": "^1.7.1",
- "@emnapi/runtime": "^1.7.1",
- "@emnapi/wasi-threads": "^1.1.0",
- "@napi-rs/wasm-runtime": "^1.1.0",
- "@tybys/wasm-util": "^0.10.1",
- "tslib": "^2.4.0"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
- "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz",
- "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/detect-libc": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
- "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/enhanced-resolve": {
- "version": "5.18.4",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz",
- "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "graceful-fs": "^4.2.4",
- "tapable": "^2.2.0"
- },
- "engines": {
- "node": ">=10.13.0"
- }
- },
- "node_modules/graceful-fs": {
- "version": "4.2.11",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
- "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-glob": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
- "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-extglob": "^2.1.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/jiti": {
- "version": "2.6.1",
- "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
- "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "jiti": "lib/jiti-cli.mjs"
- }
- },
- "node_modules/lightningcss": {
- "version": "1.30.2",
- "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
- "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
- "dev": true,
- "license": "MPL-2.0",
- "dependencies": {
- "detect-libc": "^2.0.3"
- },
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- },
- "optionalDependencies": {
- "lightningcss-android-arm64": "1.30.2",
- "lightningcss-darwin-arm64": "1.30.2",
- "lightningcss-darwin-x64": "1.30.2",
- "lightningcss-freebsd-x64": "1.30.2",
- "lightningcss-linux-arm-gnueabihf": "1.30.2",
- "lightningcss-linux-arm64-gnu": "1.30.2",
- "lightningcss-linux-arm64-musl": "1.30.2",
- "lightningcss-linux-x64-gnu": "1.30.2",
- "lightningcss-linux-x64-musl": "1.30.2",
- "lightningcss-win32-arm64-msvc": "1.30.2",
- "lightningcss-win32-x64-msvc": "1.30.2"
- }
- },
- "node_modules/lightningcss-android-arm64": {
- "version": "1.30.2",
- "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
- "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-darwin-arm64": {
- "version": "1.30.2",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
- "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-darwin-x64": {
- "version": "1.30.2",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
- "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-freebsd-x64": {
- "version": "1.30.2",
- "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
- "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm-gnueabihf": {
- "version": "1.30.2",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
- "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm64-gnu": {
- "version": "1.30.2",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
- "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm64-musl": {
- "version": "1.30.2",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
- "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-x64-gnu": {
- "version": "1.30.2",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
- "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-x64-musl": {
- "version": "1.30.2",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
- "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-win32-arm64-msvc": {
- "version": "1.30.2",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
- "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-win32-x64-msvc": {
- "version": "1.30.2",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
- "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/magic-string": {
- "version": "0.30.21",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
- "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.5"
- }
- },
- "node_modules/mri": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
- "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/node-addon-api": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
- "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/picocolors": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/source-map-js": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
- "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/tailwindcss": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
- "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/tapable": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
- "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- }
- }
- }
-}
diff --git a/app/package.json b/app/package.json
@@ -1,8 +0,0 @@
-{
- "name": "radroots-app",
- "private": true,
- "devDependencies": {
- "@tailwindcss/cli": "^4.1.18",
- "tailwindcss": "^4.1.18"
- }
-}
diff --git a/app/src/app.rs b/app/src/app.rs
@@ -1,2450 +0,0 @@
-use leptos::ev::{KeyboardEvent, MouseEvent};
-use leptos::prelude::*;
-use leptos::task::spawn_local;
-use leptos_router::components::{A, Route, Router, Routes};
-use leptos_router::hooks::{use_location, use_navigate};
-use leptos_router::path;
-use web_sys::HtmlElement;
-
-use radroots_app_core::datastore::RadrootsClientDatastore;
-use radroots_app_core::idb::IDB_CONFIG_LOGS;
-use radroots_app_core::keystore::{
- RadrootsClientKeystoreError,
- RadrootsClientKeystoreNostr,
- RadrootsClientWebKeystoreNostr,
-};
-use radroots_app_ui_components::{
- RadrootsAppUiButtonLayoutAction,
- RadrootsAppUiButtonLayoutBackAction,
- RadrootsAppUiButtonLayoutPair,
- RadrootsAppUiChip,
- RadrootsAppUiChips,
- RadrootsAppUiFormField,
- RadrootsAppUiIcon,
- RadrootsAppUiIconKey,
- RadrootsAppUiList,
- RadrootsAppUiListIcon,
- RadrootsAppUiListItem,
- RadrootsAppUiListItemKind,
- RadrootsAppUiListLabel,
- RadrootsAppUiListLabelText,
- RadrootsAppUiListLabelValue,
- RadrootsAppUiListLabelValueKind,
- RadrootsAppUiListTitle,
- RadrootsAppUiListTitleValue,
- RadrootsAppUiListTouch,
- RadrootsAppUiListTouchEnd,
- RadrootsAppUiListToggle,
- RadrootsAppUiListView,
- RadrootsAppUiNavHeader,
- RadrootsAppUiNavHeaderBgMode,
- RadrootsAppUiNavHeaderCollapseMode,
- RadrootsAppUiNavTabs,
- RadrootsAppUiScrollContainer,
- RadrootsAppUiScrollContext,
- RadrootsAppUiSpinner,
-};
-use uuid::Uuid;
-
-use crate::t;
-use crate::{
- app_init_assets,
- app_init_backends,
- app_init_has_completed,
- app_init_setup_status,
- app_init_state_default,
- app_init_mark_completed,
- app_init_reset,
- app_init_progress_add,
- app_init_stage_set,
- app_init_total_add,
- app_init_total_unknown,
- app_config_flow_build_config,
- app_config_flow_validate,
- app_config_step_default,
- app_context,
- app_i18n_init,
- app_log_buffer_flush_deferred,
- app_log_debug_emit,
- app_log_error_emit,
- app_log_error_store,
- app_config_gate_from_status,
- app_config_default,
- app_config_status,
- app_datastore_create_config,
- app_datastore_read_state,
- app_datastore_update_config,
- app_datastore_clear_setup_draft,
- app_datastore_write_profile_seed,
- app_datastore_write_setup_draft,
- app_keystore_nostr_ensure_key,
- app_setup_flow_role_from_choices,
- app_setup_flow_validate,
- app_setup_lock_acquire,
- app_setup_lock_enabled,
- app_setup_lock_release,
- app_setup_lock_ttl_ms,
- app_state_timestamp_ms,
- app_setup_eula_date,
- app_setup_finalize_with_key,
- app_setup_gate_from_status,
- app_setup_step_default,
- RadrootsAppBackends,
- RadrootsAppConfigFlowDraft,
- RadrootsAppConfigStep,
- RadrootsAppConfigStatus,
- RadrootsAppConfigRecordError,
- RadrootsAppConfigStoreError,
- RadrootsAppInitError,
- RadrootsAppInitStage,
- RadrootsAppNotifications,
- RadrootsAppLogsPage,
- RadrootsAppKeystoreError,
- RadrootsAppProfileSeed,
- RadrootsAppRole,
- RadrootsAppSettingsPage,
- RadrootsAppSetupDraft,
- RadrootsAppSetupFlowDraft,
- RadrootsAppSetupKeyChoice,
- RadrootsAppSetupFarmerChoice,
- RadrootsAppSetupBusinessChoice,
- RadrootsAppSetupLock,
- RadrootsAppSetupLockStatus,
- RadrootsAppSetupStatus,
- RadrootsAppUiDemoPage,
- RadrootsAppSetupStep,
- RadrootsAppSettingsStatusPage,
-};
-
-#[derive(Clone, Copy, PartialEq, Eq)]
-enum HomeView {
- Activity,
- Profile,
-}
-
-impl HomeView {
- fn label(self) -> &'static str {
- match self {
- HomeView::Activity => "Activity",
- HomeView::Profile => "Profile",
- }
- }
-}
-
-#[component]
-pub(crate) fn AppPageChrome(
- title: String,
- #[prop(optional)] header_right: Option<ChildrenFn>,
- #[prop(optional)] show_tabs: Option<bool>,
- #[prop(optional)] bg_mode: Option<RadrootsAppUiNavHeaderBgMode>,
- #[prop(optional)] collapse_mode: Option<RadrootsAppUiNavHeaderCollapseMode>,
- children: Children,
-) -> impl IntoView {
- let scroll_context = RadrootsAppUiScrollContext::new();
- provide_context(scroll_context.clone());
- let show_tabs = show_tabs.unwrap_or(true);
- let bg_mode = bg_mode.unwrap_or(RadrootsAppUiNavHeaderBgMode::AutoBlur);
- let collapse_mode = collapse_mode.unwrap_or(RadrootsAppUiNavHeaderCollapseMode::Scroll);
- let location = use_location();
- let is_home = move || location.pathname.get() == "/";
- let is_test = move || location.pathname.get().starts_with("/test");
- let is_settings = move || location.pathname.get().starts_with("/settings");
- view! {
- <div class="app-page-shell">
- <RadrootsAppUiScrollContainer
- id=None
- classes=Some("app-page app-page-scroll app-page-chrome".to_string())
- collapse_range=None
- context=Some(scroll_context.clone())
- >
- <RadrootsAppUiNavHeader
- label=title
- on_label_click=None
- bg_mode=Some(bg_mode)
- collapse_mode=Some(collapse_mode)
- right=header_right
- id=None
- class=None
- />
- <div class="app-page-body">
- {children()}
- </div>
- </RadrootsAppUiScrollContainer>
- {move || {
- if show_tabs {
- view! {
- <RadrootsAppUiNavTabs>
- <A
- href="/"
- attr:class="nav-tabs__item"
- attr:data-active=move || if is_home() { "true" } else { "false" }
- attr:aria-label=t!("app.nav.home")
- >
- <RadrootsAppUiIcon key=RadrootsAppUiIconKey::Home size=22 />
- </A>
- <A
- href="/test"
- attr:class="nav-tabs__item"
- attr:data-active=move || if is_test() { "true" } else { "false" }
- attr:aria-label=t!("app.nav.ui")
- >
- <RadrootsAppUiIcon key=RadrootsAppUiIconKey::Beaker size=22 />
- </A>
- <A
- href="/settings"
- attr:class="nav-tabs__item"
- attr:data-active=move || if is_settings() { "true" } else { "false" }
- attr:aria-label=t!("app.nav.settings")
- >
- <RadrootsAppUiIcon key=RadrootsAppUiIconKey::Settings size=22 />
- </A>
- </RadrootsAppUiNavTabs>
- }
- .into_any()
- } else {
- view! { <></> }.into_any()
- }
- }}
- </div>
- }
-}
-
-fn error_label(key: &str) -> Option<String> {
- let label = match key {
- "error.app.init.idb" => t!("error.app.init.idb"),
- "error.app.init.datastore" => t!("error.app.init.datastore"),
- "error.app.init.keystore" => t!("error.app.init.keystore"),
- "error.app.init.config" => t!("error.app.init.config"),
- "error.app.init.assets" => t!("error.app.init.assets"),
- "error.app.state.missing" => t!("error.app.state.missing"),
- "error.app.state.corrupt" => t!("error.app.state.corrupt"),
- "error.app.state.checksum_invalid" => t!("error.app.state.checksum_invalid"),
- "error.app.state.schema_unsupported" => t!("error.app.state.schema_unsupported"),
- "error.app.state.already_exists" => t!("error.app.state.already_exists"),
- "error.client.notifications.unavailable" => t!("error.client.notifications.unavailable"),
- "error.client.notifications.read_failure" => t!("error.client.notifications.read_failure"),
- _ => return None,
- };
- Some(label)
-}
-
-fn reset_status_label(value: &str) -> String {
- match value {
- "reset_idle" => t!("app.home.reset.status.idle"),
- "resetting" => t!("app.home.reset.status.resetting"),
- "reset_missing_backends" => t!("app.home.reset.status.missing_backends"),
- "reset_done" => t!("app.home.reset.status.done"),
- _ => error_label(value).unwrap_or_else(|| value.to_string()),
- }
-}
-
-fn setup_touch_callback(action: &'static str) -> Callback<MouseEvent> {
- Callback::new(move |_| {
- let _ = app_log_debug_emit("log.app.setup.choice", action, None);
- })
-}
-
-fn log_init_stage(stage: RadrootsAppInitStage) {
- let _ = app_log_debug_emit("log.app.init.stage", stage.as_str(), None);
-}
-
-fn logs_datastore() -> radroots_app_core::datastore::RadrootsClientWebDatastore {
- radroots_app_core::datastore::RadrootsClientWebDatastore::new(Some(IDB_CONFIG_LOGS))
-}
-
-#[component]
-fn SplashPage() -> impl IntoView {
- view! {
- <main
- id="app-splash"
- class="app-page app-page-fixed"
- style="min-height:100dvh;background:white;display:flex;align-items:center;justify-content:center;"
- >
- </main>
- }
-}
-
-#[component]
-fn LogoCircle() -> impl IntoView {
- view! {
- <div
- id="app-logo-circle"
- class="relative flex flex-col h-[196px] w-full justify-center items-center"
- >
- <div
- id="app-logo-mark"
- class="relative flex flex-row h-36 w-36 justify-center items-center bg-ly2 rounded-full"
- >
- <p class="font-sans font-[900] text-6xl text-ly0-gl -tracking-[0.4rem] -translate-x-[6px]">
- "\u{00BB}`,"
- </p>
- <p class="font-sans font-[900] text-6xl text-ly0-gl translate-x-[8px]">
- "-"
- </p>
- </div>
- </div>
- }
-}
-
-#[component]
-fn SetupPage() -> impl IntoView {
- let context = app_context();
- let fallback_backends = RwSignal::new_local(None::<RadrootsAppBackends>);
- let backends = context
- .as_ref()
- .map(|value| value.backends)
- .unwrap_or(fallback_backends);
- let fallback_setup_status = RwSignal::new_local(RadrootsAppSetupStatus::Unknown);
- let setup_status = context
- .as_ref()
- .map(|value| value.setup_status)
- .unwrap_or(fallback_setup_status);
- let navigate = use_navigate();
- let navigate_guard = navigate.clone();
- let navigate_home = navigate.clone();
- let setup_step = RwSignal::new_local(app_setup_step_default());
- let setup_key_choice = RwSignal::new_local(None::<RadrootsAppSetupKeyChoice>);
- let setup_farmer_choice = RwSignal::new_local(None::<RadrootsAppSetupFarmerChoice>);
- let setup_business_choice = RwSignal::new_local(None::<RadrootsAppSetupBusinessChoice>);
- let setup_eula_scrolled = RwSignal::new_local(false);
- let setup_eula_scroll_ref: NodeRef<leptos::html::Div> = NodeRef::new();
- let nostr_key_add = RwSignal::new_local(String::new());
- let profile_name = RwSignal::new_local(String::new());
- let profile_nip05 = RwSignal::new_local(true);
- let setup_draft_loaded = RwSignal::new_local(false);
- let setup_lock_owner = RwSignal::new_local(Uuid::new_v4().to_string());
- let setup_lock_status = RwSignal::new_local(None::<RadrootsAppSetupLockStatus>);
- let setup_lock_attempted = RwSignal::new_local(false);
- let setup_flow = move || RadrootsAppSetupFlowDraft {
- step: setup_step.get(),
- key_choice: setup_key_choice.get(),
- farmer_choice: setup_farmer_choice.get(),
- business_choice: setup_business_choice.get(),
- profile_name: profile_name.get(),
- profile_nip05: profile_nip05.get(),
- };
- let setup_validation = move || app_setup_flow_validate(&setup_flow());
- let setup_lock_ready = move || {
- !app_setup_lock_enabled()
- || matches!(
- setup_lock_status.get(),
- Some(RadrootsAppSetupLockStatus::Acquired(_))
- )
- };
- let setup_locked = move || {
- matches!(
- setup_lock_status.get(),
- Some(RadrootsAppSetupLockStatus::Locked(_))
- )
- };
- let setup_lock_pending = move || app_setup_lock_enabled() && setup_lock_status.get().is_none();
- let retry_setup_lock: Callback<MouseEvent> = {
- let setup_lock_status = setup_lock_status.clone();
- let setup_lock_attempted = setup_lock_attempted.clone();
- let setup_lock_owner = setup_lock_owner.clone();
- Callback::new(move |_| {
- setup_lock_status.set(None);
- setup_lock_attempted.set(false);
- setup_lock_owner.set(Uuid::new_v4().to_string());
- })
- };
- let on_generate_key = setup_touch_callback("generate_key");
- let on_add_key = setup_touch_callback("add_key");
- Effect::new(move || {
- match setup_status.get() {
- RadrootsAppSetupStatus::Configured => {
- navigate_guard("/", Default::default());
- }
- RadrootsAppSetupStatus::Corrupt => {
- navigate_guard("/recovery", Default::default());
- }
- _ => {}
- }
- });
- Effect::new({
- let backends = backends.clone();
- let setup_lock_status = setup_lock_status.clone();
- let setup_lock_attempted = setup_lock_attempted.clone();
- let setup_lock_owner = setup_lock_owner.clone();
- move |_| {
- if setup_lock_attempted.get() {
- return;
- }
- if !app_setup_lock_enabled() {
- setup_lock_attempted.set(true);
- return;
- }
- let Some((datastore, key_maps)) = backends
- .with(|value| value.as_ref().map(|backends| (backends.datastore.clone(), backends.config.datastore.key_maps.clone())))
- else {
- return;
- };
- setup_lock_attempted.set(true);
- let owner = setup_lock_owner.get();
- let setup_lock_status = setup_lock_status.clone();
- spawn_local(async move {
- let now_ms = u64::try_from(app_state_timestamp_ms()).unwrap_or(0);
- let ttl_ms = app_setup_lock_ttl_ms();
- match app_setup_lock_acquire(
- datastore.as_ref(),
- &key_maps,
- &owner,
- now_ms,
- ttl_ms,
- )
- .await
- {
- Ok(status) => setup_lock_status.set(Some(status)),
- Err(err) => {
- let _ = app_log_error_emit(&err);
- let fallback = RadrootsAppSetupLock {
- owner,
- expires_at_ms: now_ms.saturating_add(ttl_ms),
- };
- setup_lock_status.set(Some(RadrootsAppSetupLockStatus::Acquired(fallback)));
- }
- }
- });
- }
- });
- Effect::new({
- let backends = backends.clone();
- let setup_draft_loaded = setup_draft_loaded.clone();
- let setup_lock_status = setup_lock_status.clone();
- let setup_key_choice = setup_key_choice.clone();
- let setup_farmer_choice = setup_farmer_choice.clone();
- let setup_business_choice = setup_business_choice.clone();
- let nostr_key_add = nostr_key_add.clone();
- let profile_name = profile_name.clone();
- let profile_nip05 = profile_nip05.clone();
- move |_| {
- if app_setup_lock_enabled()
- && !matches!(
- setup_lock_status.get(),
- Some(RadrootsAppSetupLockStatus::Acquired(_))
- )
- {
- return;
- }
- if setup_draft_loaded.get() {
- return;
- }
- let Some((datastore, key_maps)) = backends
- .with(|value| value.as_ref().map(|backends| (backends.datastore.clone(), backends.config.datastore.key_maps.clone())))
- else {
- return;
- };
- setup_draft_loaded.set(true);
- setup_key_choice.set(None);
- setup_farmer_choice.set(None);
- setup_business_choice.set(None);
- nostr_key_add.set(String::new());
- profile_name.set(String::new());
- profile_nip05.set(true);
- spawn_local(async move {
- let _ = app_datastore_clear_setup_draft(datastore.as_ref(), &key_maps).await;
- });
- }
- });
- Effect::new({
- let backends = backends.clone();
- let setup_draft_loaded = setup_draft_loaded.clone();
- let setup_lock_status = setup_lock_status.clone();
- let setup_key_choice = setup_key_choice.clone();
- let setup_farmer_choice = setup_farmer_choice.clone();
- let setup_business_choice = setup_business_choice.clone();
- let nostr_key_add = nostr_key_add.clone();
- let profile_name = profile_name.clone();
- let profile_nip05 = profile_nip05.clone();
- move |_| {
- if app_setup_lock_enabled()
- && !matches!(
- setup_lock_status.get(),
- Some(RadrootsAppSetupLockStatus::Acquired(_))
- )
- {
- return;
- }
- if !setup_draft_loaded.get() {
- return;
- }
- let Some((datastore, key_maps)) = backends
- .with(|value| value.as_ref().map(|backends| (backends.datastore.clone(), backends.config.datastore.key_maps.clone())))
- else {
- return;
- };
- let nostr_public_key = match setup_key_choice.get() {
- Some(RadrootsAppSetupKeyChoice::AddExisting) => {
- let value = nostr_key_add.get();
- let value = value.trim();
- if value.is_empty() {
- None
- } else {
- Some(value.to_string())
- }
- }
- _ => None,
- };
- let profile_value = profile_name.get();
- let profile_name = if profile_value.trim().is_empty() {
- None
- } else {
- Some(profile_value)
- };
- let role = app_setup_flow_role_from_choices(
- setup_farmer_choice.get(),
- setup_business_choice.get(),
- );
- let draft = RadrootsAppSetupDraft {
- nostr_public_key,
- profile_name,
- role,
- nip05_request: Some(profile_nip05.get()),
- };
- spawn_local(async move {
- let _ = app_datastore_write_setup_draft(datastore.as_ref(), &key_maps, &draft).await;
- });
- }
- });
- let advance_step: Callback<()> = {
- let backends = backends.clone();
- let setup_step = setup_step.clone();
- let setup_key_choice = setup_key_choice.clone();
- let setup_farmer_choice = setup_farmer_choice.clone();
- let setup_business_choice = setup_business_choice.clone();
- let setup_lock_status = setup_lock_status.clone();
- let nostr_key_add = nostr_key_add.clone();
- let profile_name = profile_name.clone();
- let setup_status = setup_status.clone();
- Callback::new(move |_| {
- if app_setup_lock_enabled()
- && !matches!(
- setup_lock_status.get(),
- Some(RadrootsAppSetupLockStatus::Acquired(_))
- )
- {
- return;
- }
- let draft = RadrootsAppSetupFlowDraft {
- step: setup_step.get(),
- key_choice: setup_key_choice.get(),
- farmer_choice: setup_farmer_choice.get(),
- business_choice: setup_business_choice.get(),
- profile_name: profile_name.get(),
- profile_nip05: profile_nip05.get(),
- };
- let validation = app_setup_flow_validate(&draft);
- let current_step = draft.step;
- if matches!(current_step, RadrootsAppSetupStep::Eula) {
- let key_choice = draft.key_choice;
- let setup_role = app_setup_flow_role_from_choices(
- setup_farmer_choice.get(),
- setup_business_choice.get(),
- )
- .unwrap_or_else(RadrootsAppRole::default);
- let nostr_key_add = nostr_key_add.get();
- let profile_name = draft.profile_name;
- let profile_nip05 = draft.profile_nip05;
- let eula_date = app_setup_eula_date();
- let setup_status = setup_status.clone();
- let backends = backends.clone();
- spawn_local(async move {
- let Some((datastore, key_maps, keystore_config)) = backends.with_untracked(|value| {
- value.as_ref().map(|backends| {
- (
- backends.datastore.clone(),
- backends.config.datastore.key_maps.clone(),
- backends.nostr_keystore.get_config(),
- )
- })
- }) else {
- return;
- };
- let keystore = RadrootsClientWebKeystoreNostr::new(Some(keystore_config));
- let active_key = match key_choice {
- Some(RadrootsAppSetupKeyChoice::AddExisting) => {
- let secret_key = nostr_key_add.trim();
- if secret_key.is_empty() {
- let err = RadrootsAppInitError::Keystore(
- RadrootsClientKeystoreError::NostrInvalidSecretKey,
- );
- let _ = app_log_error_emit(&err);
- return;
- }
- match keystore.add(secret_key).await {
- Ok(value) => value,
- Err(err) => {
- let init_err = RadrootsAppInitError::Keystore(err);
- let _ = app_log_error_emit(&init_err);
- return;
- }
- }
- }
- _ => match app_keystore_nostr_ensure_key(&keystore).await {
- Ok(value) => value,
- Err(err) => {
- let init_err = match err {
- RadrootsAppKeystoreError::Keystore(inner) => {
- RadrootsAppInitError::Keystore(inner)
- }
- RadrootsAppKeystoreError::KeyMismatch => RadrootsAppInitError::Keystore(
- RadrootsClientKeystoreError::NostrInvalidSecretKey,
- ),
- };
- let _ = app_log_error_emit(&init_err);
- return;
- }
- },
- };
- let nip05_key = if profile_nip05 {
- let profile_name = profile_name.trim();
- if profile_name.is_empty() {
- None
- } else {
- Some(profile_name.to_string())
- }
- } else {
- None
- };
- if !profile_name.trim().is_empty() {
- let profile_seed = RadrootsAppProfileSeed {
- public_key: active_key.clone(),
- name: profile_name.trim().to_string(),
- display_name: Some(profile_name.trim().to_string()),
- nip05_request: profile_nip05,
- };
- if let Err(err) = app_datastore_write_profile_seed(
- datastore.as_ref(),
- &key_maps,
- &profile_seed,
- )
- .await
- {
- let _ = app_log_error_emit(&err);
- return;
- }
- }
- if let Err(err) = app_setup_finalize_with_key(
- datastore.as_ref(),
- &key_maps,
- active_key,
- eula_date,
- nip05_key,
- setup_role,
- )
- .await
- {
- let _ = app_log_error_emit(&err);
- return;
- }
- let _ = app_datastore_clear_setup_draft(datastore.as_ref(), &key_maps).await;
- if app_setup_lock_enabled() {
- let _ = app_setup_lock_release(datastore.as_ref(), &key_maps).await;
- }
- setup_status.set(RadrootsAppSetupStatus::Configured);
- });
- return;
- }
- if !validation.can_continue {
- return;
- }
- if matches!(current_step, RadrootsAppSetupStep::Profile) {
- if draft.profile_name.trim().is_empty() {
- let setup_step = setup_step.clone();
- let confirm_message = t!("app.setup.profile.confirm_no_name");
- let next_step = validation.next_step;
- spawn_local(async move {
- let notifications = RadrootsAppNotifications::new(None);
- let confirm = notifications.confirm_message(&confirm_message).await;
- if confirm {
- setup_step.set(next_step);
- }
- });
- return;
- }
- }
- setup_step.set(validation.next_step);
- })
- };
- let advance_step_click: Callback<MouseEvent> = {
- let advance_step = advance_step.clone();
- Callback::new(move |_| {
- advance_step.run(());
- })
- };
- let rewind_step: Callback<MouseEvent> = {
- let setup_step = setup_step.clone();
- let setup_key_choice = setup_key_choice.clone();
- let setup_farmer_choice = setup_farmer_choice.clone();
- let setup_business_choice = setup_business_choice.clone();
- let setup_lock_status = setup_lock_status.clone();
- let profile_name = profile_name.clone();
- let profile_nip05 = profile_nip05.clone();
- Callback::new(move |_| {
- if app_setup_lock_enabled()
- && !matches!(
- setup_lock_status.get(),
- Some(RadrootsAppSetupLockStatus::Acquired(_))
- )
- {
- return;
- }
- let draft = RadrootsAppSetupFlowDraft {
- step: setup_step.get(),
- key_choice: setup_key_choice.get(),
- farmer_choice: setup_farmer_choice.get(),
- business_choice: setup_business_choice.get(),
- profile_name: profile_name.get(),
- profile_nip05: profile_nip05.get(),
- };
- let validation = app_setup_flow_validate(&draft);
- if !validation.can_back {
- return;
- }
- let prev_step = validation.prev_step;
- setup_step.set(prev_step);
- if matches!(prev_step, RadrootsAppSetupStep::Intro) {
- setup_key_choice.set(None);
- }
- })
- };
- let on_generate_key = on_generate_key.clone();
- let on_add_key = on_add_key.clone();
- Effect::new({
- let setup_step = setup_step.clone();
- let setup_eula_scrolled = setup_eula_scrolled.clone();
- move |_| {
- if !matches!(setup_step.get(), RadrootsAppSetupStep::Eula) {
- setup_eula_scrolled.set(false);
- }
- }
- });
- Effect::new({
- let setup_step = setup_step.clone();
- let setup_eula_scrolled = setup_eula_scrolled.clone();
- let setup_eula_scroll_ref = setup_eula_scroll_ref.clone();
- move |_| {
- if !matches!(setup_step.get(), RadrootsAppSetupStep::Eula) {
- return;
- }
- let Some(target) = setup_eula_scroll_ref.get() else {
- return;
- };
- if target.scroll_height() <= target.client_height() {
- setup_eula_scrolled.set(true);
- }
- }
- });
- view! {
- <main
- id="app-setup"
- class="app-page app-page-fixed relative w-full flex flex-col"
- >
- {move || {
- if setup_lock_pending() {
- return view! {
- <section
- id="app-setup-lock-pending"
- class="app-view app-view-enter flex flex-col h-[100dvh] w-full px-6 pt-10 pb-16"
- >
- <div
- id="app-setup-lock-pending-body"
- class="flex flex-1 w-full flex-col justify-center items-center gap-4"
- >
- <RadrootsAppUiSpinner class="text-[24px]".to_string() />
- <p class="font-sans font-[600] text-ly0-gl text-2xl text-center">
- {t!("app.setup.lock.pending.title")}
- </p>
- <p class="font-mono font-[400] text-ly0-gl text-base text-center">
- {t!("app.setup.lock.pending.body")}
- </p>
- </div>
- </section>
- }
- .into_any();
- }
- if setup_locked() {
- return view! {
- <section
- id="app-setup-lock"
- class="app-view app-view-enter flex flex-col h-[100dvh] w-full px-6 pt-10 pb-16"
- >
- <div
- id="app-setup-lock-body"
- class="flex flex-1 w-full flex-col justify-center items-center gap-4"
- >
- <p class="font-sans font-[600] text-ly0-gl text-2xl text-center">
- {t!("app.setup.locked.title")}
- </p>
- <p class="font-mono font-[400] text-ly0-gl text-base text-center">
- {t!("app.setup.locked.body")}
- </p>
- </div>
- <div
- id="app-setup-lock-actions"
- class="flex flex-col w-full pt-4 justify-center items-center"
- >
- {{
- let retry_action = RadrootsAppUiButtonLayoutAction {
- label: t!("app.setup.lock.retry"),
- disabled: false,
- loading: false,
- on_click: retry_setup_lock.clone(),
- class: None,
- class_label: None,
- style: None,
- };
- view! { <RadrootsAppUiButtonLayoutPair continue_action=retry_action class="gap-2".to_string() /> }
- }}
- </div>
- </section>
- }
- .into_any();
- }
- match setup_step.get() {
- RadrootsAppSetupStep::Intro => {
- let navigate_home = navigate_home.clone();
- view! {
- <section
- id="app-setup-intro"
- class="app-view app-view-enter relative flex flex-col h-[100dvh] w-full justify-start items-center"
- >
- <div
- id="app-setup-intro-body"
- class="flex flex-col h-full w-full justify-start items-center"
- >
- <div
- id="app-setup-intro-stage"
- class="relative flex flex-col h-full w-full justify-center items-center"
- >
- <header
- id="app-setup-intro-header"
- class="flex flex-row w-full justify-start items-center -translate-y-16"
- >
- <button
- type="button"
- id="app-setup-intro-logo-button"
- class="flex flex-row w-full justify-center items-center"
- on:click=move |_| navigate_home("/", Default::default())
- >
- <LogoCircle />
- </button>
- </header>
- <footer
- id="app-setup-intro-footer"
- class="absolute bottom-0 left-0 flex flex-col h-[20rem] w-full gap-2 justify-start items-center"
- >
- <p
- id="app-setup-intro-kicker"
- class="w-full text-left font-sans font-[400] text-sm uppercase text-ly0-gl-label"
- >
- {t!("app.setup.intro.kicker")}
- </p>
- <div
- id="app-setup-intro-copy"
- class="flex flex-col w-full gap-2 justify-start items-center"
- >
- <p
- id="app-setup-intro-line-welcome"
- class="w-full text-left font-mono font-[400] text-[1.1rem] text-ly0-gl"
- >
- {t!("app.setup.intro.welcome")}
- </p>
- <p
- id="app-setup-intro-line-body"
- class="w-full text-left font-mono font-[400] text-[1.1rem] text-ly0-gl"
- >
- {t!("app.setup.intro.body")}
- </p>
- </div>
- </footer>
- </div>
- </div>
- </section>
- }
- .into_any()
- },
- RadrootsAppSetupStep::KeyChoice => view! {
- <section
- id="app-setup-key-choice"
- class="app-view app-view-enter flex flex-col w-full px-6 pt-10 pb-16"
- on:click=move |_| {
- setup_key_choice.set(None);
- }
- >
- <div
- id="app-setup-key-choice-body"
- class="flex flex-1 w-full flex-col justify-center items-center gap-8"
- >
- <div
- id="app-setup-key-choice-title"
- class="flex flex-row w-full justify-center items-center"
- >
- <p class="font-sans font-[600] text-ly0-gl text-3xl">
- {t!("app.setup.key_choice.title")}
- </p>
- </div>
- <div
- id="app-setup-key-choice-actions"
- class="flex flex-col w-full gap-6 justify-center items-center"
- >
- <button
- id="app-setup-key-choice-generate"
- type="button"
- class=move || {
- if setup_key_choice.get()
- == Some(RadrootsAppSetupKeyChoice::Generate)
- {
- "flex flex-col h-bold_button w-lo_ios0 ios1:w-lo_ios1 justify-center items-center rounded-touch ly1-selected-press el-re"
- } else {
- "flex flex-col h-bold_button w-lo_ios0 ios1:w-lo_ios1 justify-center items-center rounded-touch bg-ly1 el-re"
- }
- }
- on:click=move |ev| {
- ev.stop_propagation();
- setup_key_choice.set(Some(RadrootsAppSetupKeyChoice::Generate));
- on_generate_key.run(ev);
- }
- >
- <span class="font-sans font-[600] text-ly0-gl text-xl">
- {t!("app.setup.key_choice.create")}
- </span>
- </button>
- <button
- id="app-setup-key-choice-add"
- type="button"
- class=move || {
- if setup_key_choice.get()
- == Some(RadrootsAppSetupKeyChoice::AddExisting)
- {
- "flex flex-col h-bold_button w-lo_ios0 ios1:w-lo_ios1 justify-center items-center rounded-touch ly1-selected-press el-re"
- } else {
- "flex flex-col h-bold_button w-lo_ios0 ios1:w-lo_ios1 justify-center items-center rounded-touch bg-ly1 el-re"
- }
- }
- on:click=move |ev| {
- ev.stop_propagation();
- setup_key_choice.set(Some(RadrootsAppSetupKeyChoice::AddExisting));
- on_add_key.run(ev);
- }
- >
- <span class="font-sans font-[600] text-ly0-gl text-xl">
- {t!("app.setup.key_choice.use_existing")}
- </span>
- </button>
- </div>
- </div>
- </section>
- }.into_any(),
- RadrootsAppSetupStep::KeyAddExisting => view! {
- <section
- id="app-setup-key-add-existing"
- class="app-view app-view-enter flex flex-col w-full px-6 pt-10 pb-16"
- >
- <div
- id="app-setup-key-add-existing-body"
- class="flex flex-1 w-full flex-col justify-center items-center"
- >
- <div
- id="app-setup-key-add-existing-card"
- class="flex flex-col w-full gap-6 justify-center items-center"
- >
- <p
- id="app-setup-key-add-existing-title"
- class="font-sans font-[600] text-ly0-gl text-3xl capitalize"
- >
- {t!("app.setup.key_add.title")}
- </p>
- <input
- id="app-setup-key-add-existing-input"
- class="input-base w-lo_ios0 ios1:w-lo_ios1 text-[1.25rem] text-center placeholder:opacity-60"
- type="text"
- placeholder=t!("app.setup.key_add.placeholder")
- prop:value=move || nostr_key_add.get()
- on:input=move |ev| {
- nostr_key_add.set(event_target_value(&ev));
- }
- />
- </div>
- </div>
- </section>
- }.into_any(),
- RadrootsAppSetupStep::Profile => view! {
- <section
- id="app-setup-profile"
- class="app-view app-view-enter flex flex-col w-full px-6 pt-10 pb-16"
- >
- <div
- id="app-setup-profile-body"
- class="flex flex-1 w-full flex-col justify-center items-center"
- >
- <div
- id="app-setup-profile-card"
- class="flex flex-col h-[16rem] w-full px-4 gap-6 justify-start items-center"
- >
- <p
- id="app-setup-profile-title"
- class="font-sans font-[600] text-ly0-gl text-3xl"
- >
- {t!("app.setup.profile.title")}
- </p>
- <div
- id="app-setup-profile-fields"
- class="flex flex-col w-full gap-4 justify-center items-center"
- >
- <input
- id="app-setup-profile-name"
- class="input-base w-lo_ios0 ios1:w-lo_ios1 text-[1.25rem] text-center placeholder:opacity-60"
- type="text"
- placeholder=t!("app.setup.profile.placeholder")
- prop:value=move || profile_name.get()
- on:keydown=move |ev: KeyboardEvent| {
- if ev.key() == "Enter" {
- ev.prevent_default();
- advance_step.run(());
- }
- }
- on:input=move |ev| {
- profile_name.set(event_target_value(&ev));
- }
- />
- <div
- id="app-setup-profile-nip05"
- class="flex flex-row w-full gap-2 justify-center items-center"
- >
- <input
- id="app-setup-profile-nip05-toggle"
- type="checkbox"
- prop:checked=move || profile_nip05.get()
- on:change=move |ev| {
- profile_nip05.set(event_target_checked(&ev));
- }
- />
- <label
- for="app-setup-profile-nip05-toggle"
- class="flex flex-row justify-center items-center"
- >
- <span class="font-sans font-[400] text-ly0-gl text-[14px] tracking-wide">
- {t!("app.setup.profile.nip05.prefix")}
- {" "}
- <span class="font-mono font-[500] tracking-tight px-[3px]">
- "@radroots"
- </span>
- {" "}
- {t!("app.setup.profile.nip05.suffix")}
- </span>
- </label>
- </div>
- </div>
- </div>
- </div>
- </section>
- }.into_any(),
- RadrootsAppSetupStep::FarmerSetup => view! {
- <section
- id="app-setup-farmer"
- class="app-view app-view-enter flex flex-col w-full px-6 pt-10 pb-16"
- on:click=move |_| {
- setup_farmer_choice.set(None);
- }
- >
- <div
- id="app-setup-farmer-body"
- class="flex flex-1 w-full flex-col justify-center items-center"
- >
- <div
- id="app-setup-farmer-card"
- class="flex flex-col h-[16rem] w-full gap-10 justify-start items-center"
- >
- <div
- id="app-setup-farmer-title"
- class="flex flex-row w-full justify-center items-center"
- >
- <p class="font-sans font-[600] text-ly0-gl text-3xl">
- {t!("app.setup.farmer.title")}
- </p>
- </div>
- <div
- id="app-setup-farmer-actions"
- class="flex flex-col w-full gap-5 justify-center items-center"
- >
- <button
- id="app-setup-farmer-yes"
- type="button"
- class=move || {
- if setup_farmer_choice.get()
- == Some(RadrootsAppSetupFarmerChoice::Yes)
- {
- "flex flex-col h-bold_button w-lo_ios0 ios1:w-lo_ios1 justify-center items-center rounded-touch ly1-selected-press el-re"
- } else {
- "flex flex-col h-bold_button w-lo_ios0 ios1:w-lo_ios1 justify-center items-center rounded-touch bg-ly1 el-re"
- }
- }
- on:click=move |ev| {
- ev.stop_propagation();
- setup_farmer_choice.set(Some(RadrootsAppSetupFarmerChoice::Yes));
- }
- >
- <span class="font-sans font-[600] text-ly0-gl text-xl">
- {t!("app.common.yes")}
- </span>
- </button>
- <button
- id="app-setup-farmer-no"
- type="button"
- class=move || {
- if setup_farmer_choice.get()
- == Some(RadrootsAppSetupFarmerChoice::No)
- {
- "flex flex-col h-bold_button w-lo_ios0 ios1:w-lo_ios1 justify-center items-center rounded-touch ly1-selected-press el-re"
- } else {
- "flex flex-col h-bold_button w-lo_ios0 ios1:w-lo_ios1 justify-center items-center rounded-touch bg-ly1 el-re"
- }
- }
- on:click=move |ev| {
- ev.stop_propagation();
- setup_farmer_choice.set(Some(RadrootsAppSetupFarmerChoice::No));
- }
- >
- <span class="font-sans font-[600] text-ly0-gl text-xl">
- {t!("app.common.no")}
- </span>
- </button>
- </div>
- </div>
- </div>
- </section>
- }.into_any(),
- RadrootsAppSetupStep::BusinessSetup => view! {
- <section
- id="app-setup-business"
- class="app-view app-view-enter flex flex-col w-full px-6 pt-10 pb-16"
- on:click=move |_| {
- setup_business_choice.set(None);
- }
- >
- <div
- id="app-setup-business-body"
- class="flex flex-1 w-full flex-col justify-center items-center"
- >
- <div
- id="app-setup-business-card"
- class="flex flex-col h-[16rem] w-full gap-10 justify-start items-center"
- >
- <div
- id="app-setup-business-title"
- class="flex flex-row w-full justify-center items-center"
- >
- <p class="font-sans font-[600] text-ly0-gl text-3xl">
- {t!("app.setup.business.title")}
- </p>
- </div>
- <div
- id="app-setup-business-actions"
- class="flex flex-col w-full gap-5 justify-center items-center"
- >
- <button
- id="app-setup-business-yes"
- type="button"
- class=move || {
- if setup_business_choice.get()
- == Some(RadrootsAppSetupBusinessChoice::Yes)
- {
- "flex flex-col h-bold_button w-lo_ios0 ios1:w-lo_ios1 justify-center items-center rounded-touch ly1-selected-press el-re"
- } else {
- "flex flex-col h-bold_button w-lo_ios0 ios1:w-lo_ios1 justify-center items-center rounded-touch bg-ly1 el-re"
- }
- }
- on:click=move |ev| {
- ev.stop_propagation();
- setup_business_choice.set(Some(RadrootsAppSetupBusinessChoice::Yes));
- }
- >
- <span class="font-sans font-[600] text-ly0-gl text-xl">
- {t!("app.common.yes")}
- </span>
- </button>
- <button
- id="app-setup-business-no"
- type="button"
- class=move || {
- if setup_business_choice.get()
- == Some(RadrootsAppSetupBusinessChoice::No)
- {
- "flex flex-col h-bold_button w-lo_ios0 ios1:w-lo_ios1 justify-center items-center rounded-touch ly1-selected-press el-re"
- } else {
- "flex flex-col h-bold_button w-lo_ios0 ios1:w-lo_ios1 justify-center items-center rounded-touch bg-ly1 el-re"
- }
- }
- on:click=move |ev| {
- ev.stop_propagation();
- setup_business_choice.set(Some(RadrootsAppSetupBusinessChoice::No));
- }
- >
- <span class="font-sans font-[600] text-ly0-gl text-xl">
- {t!("app.common.no")}
- </span>
- </button>
- </div>
- </div>
- </div>
- </section>
- }.into_any(),
- RadrootsAppSetupStep::Eula => view! {
- <section
- id="app-setup-eula"
- class="app-view app-view-enter flex flex-col h-full w-full px-6 pt-8 pb-6"
- >
- <div
- id="app-setup-eula-body"
- class="flex flex-col flex-1 min-h-0 w-full gap-5"
- >
- <header
- id="app-setup-eula-header"
- class="flex flex-row w-full justify-center items-center"
- >
- <p class="font-sans font-[600] text-ly0-gl text-2xl text-center">
- {t!("app.setup.eula.title")}
- </p>
- </header>
- <div
- id="app-setup-eula-scroll"
- class="app-page-scroll scroll-hide flex flex-col flex-1 min-h-0 w-full gap-6 px-1 pb-20 se-compact:pb-12 overscroll-contain font-mono"
- node_ref=setup_eula_scroll_ref
- on:scroll=move |ev| {
- if setup_eula_scrolled.get() {
- return;
- }
- let target = event_target::<HtmlElement>(&ev);
- let scroll_top = target.scroll_top();
- let scroll_height = target.scroll_height();
- let client_height = target.client_height();
- if scroll_top + client_height + 1 >= scroll_height {
- setup_eula_scrolled.set(true);
- }
- }
- >
- <section
- id="app-setup-eula-introduction"
- class="flex flex-col gap-2"
- >
- <h3 class="font-sans font-[600] text-ly0-gl text-base">
- {t!("app.setup.eula.introduction.title")}
- </h3>
- <p class="font-mono font-[400] text-ly0-gl text-sm leading-relaxed">
- {t!("app.setup.eula.introduction.body")}
- </p>
- </section>
- <section
- id="app-setup-eula-prohibited-content"
- class="flex flex-col gap-2"
- >
- <h3 class="font-sans font-[600] text-ly0-gl text-base">
- {t!("app.setup.eula.prohibited_content.title")}
- </h3>
- <p class="font-mono font-[400] text-ly0-gl text-sm leading-relaxed">
- {t!("app.setup.eula.prohibited_content.body")}
- </p>
- <ul class="flex flex-col gap-1 pl-5 list-disc text-sm text-ly0-gl leading-relaxed">
- <li>{t!("app.setup.eula.prohibited_content.item.illegal")}</li>
- <li>{t!("app.setup.eula.prohibited_content.item.pornographic")}</li>
- <li>{t!("app.setup.eula.prohibited_content.item.hate_speech")}</li>
- <li>{t!("app.setup.eula.prohibited_content.item.minors")}</li>
- <li>{t!("app.setup.eula.prohibited_content.item.harass")}</li>
- <li>{t!("app.setup.eula.prohibited_content.item.impersonate")}</li>
- </ul>
- </section>
- <section
- id="app-setup-eula-prohibited-conduct"
- class="flex flex-col gap-2"
- >
- <h3 class="font-sans font-[600] text-ly0-gl text-base">
- {t!("app.setup.eula.prohibited_conduct.title")}
- </h3>
- <ul class="flex flex-col gap-1 pl-5 list-disc text-sm text-ly0-gl leading-relaxed">
- <li>{t!("app.setup.eula.prohibited_conduct.item.harass")}</li>
- <li>{t!("app.setup.eula.prohibited_conduct.item.impersonate")}</li>
- <li>{t!("app.setup.eula.prohibited_conduct.item.intimidate")}</li>
- <li>{t!("app.setup.eula.prohibited_conduct.item.violence")}</li>
- </ul>
- </section>
- <section
- id="app-setup-eula-consequences"
- class="flex flex-col gap-2"
- >
- <h3 class="font-sans font-[600] text-ly0-gl text-base">
- {t!("app.setup.eula.consequences.title")}
- </h3>
- <p class="font-mono font-[400] text-ly0-gl text-sm leading-relaxed">
- {t!("app.setup.eula.consequences.body")}
- </p>
- </section>
- <section
- id="app-setup-eula-disclaimer"
- class="flex flex-col gap-2"
- >
- <h3 class="font-sans font-[600] text-ly0-gl text-base">
- {t!("app.setup.eula.disclaimer.title")}
- </h3>
- <p class="font-mono font-[400] text-ly0-gl text-sm leading-relaxed">
- {t!("app.setup.eula.disclaimer.body")}
- </p>
- </section>
- <section
- id="app-setup-eula-changes"
- class="flex flex-col gap-2"
- >
- <h3 class="font-sans font-[600] text-ly0-gl text-base">
- {t!("app.setup.eula.changes.title")}
- </h3>
- <p class="font-mono font-[400] text-ly0-gl text-sm leading-relaxed">
- {t!("app.setup.eula.changes.body")}
- </p>
- </section>
- <section
- id="app-setup-eula-contact"
- class="flex flex-col gap-2"
- >
- <h3 class="font-sans font-[600] text-ly0-gl text-base">
- {t!("app.setup.eula.contact.title")}
- </h3>
- <p class="font-mono font-[400] text-ly0-gl text-sm leading-relaxed">
- {t!("app.setup.eula.contact.body")}
- </p>
- </section>
- <section
- id="app-setup-eula-acceptance"
- class="flex flex-col gap-2"
- >
- <h3 class="font-sans font-[600] text-ly0-gl text-base">
- {t!("app.setup.eula.acceptance.title")}
- </h3>
- <p class="font-mono font-[400] text-ly0-gl text-sm leading-relaxed">
- {t!("app.setup.eula.acceptance.body")}
- </p>
- </section>
- </div>
- </div>
- <div
- id="app-setup-eula-actions"
- class="flex flex-col w-full pt-4 pb-2 justify-center items-center"
- >
- {move || {
- let continue_action = RadrootsAppUiButtonLayoutAction {
- label: t!("app.common.agree"),
- disabled: !setup_eula_scrolled.get(),
- loading: false,
- on_click: advance_step_click.clone(),
- class: Some("button-layout-accent button-layout-compact".to_string()),
- class_label: Some("text-base".to_string()),
- style: None,
- };
- let back_action = RadrootsAppUiButtonLayoutBackAction {
- visible: true,
- label: Some(t!("app.common.disagree")),
- disabled: false,
- on_click: rewind_step.clone(),
- compact: true,
- };
- view! {
- <RadrootsAppUiButtonLayoutPair
- continue_action=continue_action
- back=back_action
- class="gap-2".to_string()
- />
- }.into_any()
- }}
- </div>
- </section>
- }.into_any(),
- }
- }}
- <footer
- id="app-setup-actions"
- class="z-10 absolute bottom-4 left-0 flex flex-col w-full justify-center items-center se-compact:bottom-0"
- >
- {move || {
- if !setup_lock_ready() {
- return view! { <></> }.into_any();
- }
- let step = setup_step.get();
- if matches!(step, RadrootsAppSetupStep::Eula) {
- return view! { <></> }.into_any();
- }
- let validation = setup_validation();
- let continue_disabled = !validation.can_continue;
- let continue_label = t!("app.common.continue");
- let back_label = t!("app.common.back");
- let continue_action = RadrootsAppUiButtonLayoutAction {
- label: continue_label,
- disabled: continue_disabled,
- loading: false,
- on_click: advance_step_click.clone(),
- class: None,
- class_label: None,
- style: None,
- };
- let back_action = RadrootsAppUiButtonLayoutBackAction {
- visible: validation.can_back,
- label: Some(back_label),
- disabled: false,
- on_click: rewind_step.clone(),
- compact: false,
- };
- view! {
- <RadrootsAppUiButtonLayoutPair
- continue_action=continue_action
- back=back_action
- />
- }.into_any()
- }}
- </footer>
- </main>
- }
-}
-
-#[component]
-fn RecoveryPage() -> impl IntoView {
- let context = app_context();
- let fallback_backends = RwSignal::new_local(None::<RadrootsAppBackends>);
- let fallback_setup_status = RwSignal::new_local(RadrootsAppSetupStatus::Unknown);
- let backends = context
- .as_ref()
- .map(|value| value.backends)
- .unwrap_or(fallback_backends);
- let setup_status = context
- .as_ref()
- .map(|value| value.setup_status)
- .unwrap_or(fallback_setup_status);
- let reset_running = RwSignal::new_local(false);
- let reset_status = RwSignal::new_local(None::<String>);
- let navigate = use_navigate();
- let reset_disabled = move || backends.with(|value| value.is_none()) || reset_running.get();
- let reset_label = move || reset_status.get().as_deref().map(reset_status_label);
- let on_reset: Callback<MouseEvent> = {
- let backends = backends.clone();
- let reset_running = reset_running.clone();
- let reset_status = reset_status.clone();
- let setup_status = setup_status.clone();
- let navigate = navigate.clone();
- Callback::new(move |_| {
- if reset_running.get() {
- return;
- }
- reset_status.set(None);
- let config = backends
- .with_untracked(|value| value.as_ref().map(|backends| backends.config.clone()));
- let reset_running = reset_running.clone();
- let reset_status = reset_status.clone();
- let setup_status = setup_status.clone();
- let navigate = navigate.clone();
- spawn_local(async move {
- let Some(config) = config else {
- reset_status.set(Some("reset_missing_backends".to_string()));
- return;
- };
- let notifications = RadrootsAppNotifications::new(None);
- let confirm_message = t!("app.recovery.reset.confirm");
- let confirm = notifications.confirm_message(&confirm_message).await;
- if !confirm {
- return;
- }
- reset_running.set(true);
- reset_status.set(Some("resetting".to_string()));
- let datastore = radroots_app_core::datastore::RadrootsClientWebDatastore::new(
- Some(config.datastore.idb_config),
- );
- let keystore = radroots_app_core::keystore::RadrootsClientWebKeystoreNostr::new(
- Some(config.keystore.nostr_store),
- );
- match app_init_reset(
- Some(&datastore),
- Some(&config.datastore.key_maps),
- Some(&keystore),
- )
- .await
- {
- Ok(()) => {
- let log_datastore = logs_datastore();
- if let Err(err) = log_datastore.reset().await {
- let reset_err = RadrootsAppInitError::Datastore(err);
- let _ = app_log_error_emit(&reset_err);
- reset_status.set(Some(reset_err.to_string()));
- reset_running.set(false);
- return;
- }
- reset_status.set(Some("reset_done".to_string()));
- setup_status.set(RadrootsAppSetupStatus::Required);
- navigate("/setup", Default::default());
- }
- Err(err) => {
- let log_datastore = logs_datastore();
- let _ = app_log_error_store(
- &log_datastore,
- &config.datastore.key_maps,
- &err,
- )
- .await;
- reset_status.set(Some(err.to_string()));
- }
- }
- reset_running.set(false);
- });
- })
- };
- view! {
- <main id="app-recovery" class="app-page app-page-fixed relative w-full flex flex-col">
- <section
- id="app-recovery-view"
- class="app-view app-view-enter flex flex-col h-[100dvh] w-full px-6 pt-10 pb-16"
- >
- <div
- id="app-recovery-body"
- class="flex flex-1 w-full flex-col justify-center items-center gap-4"
- >
- <p class="font-sans font-[600] text-ly0-gl text-2xl text-center">
- {t!("app.recovery.title")}
- </p>
- <p class="font-mono font-[400] text-ly0-gl text-base text-center">
- {t!("app.recovery.body")}
- </p>
- {move || {
- reset_label()
- .map(|label| {
- view! {
- <p class="font-mono font-[400] text-ly0-gl text-sm text-center">
- {label}
- </p>
- }
- .into_any()
- })
- .unwrap_or_else(|| view! { <></> }.into_any())
- }}
- </div>
- <div
- id="app-recovery-actions"
- class="flex flex-col w-full pt-6 justify-center items-center"
- >
- {move || {
- let reset_action = RadrootsAppUiButtonLayoutAction {
- label: t!("app.recovery.reset.button"),
- disabled: reset_disabled(),
- loading: reset_running.get(),
- on_click: on_reset.clone(),
- class: None,
- class_label: None,
- style: None,
- };
- view! { <RadrootsAppUiButtonLayoutPair continue_action=reset_action class="gap-2".to_string() /> }
- }}
- </div>
- </section>
- </main>
- }
-}
-
-#[component]
-fn ConfigPage() -> impl IntoView {
- let context = app_context();
- let fallback_backends = RwSignal::new_local(None::<RadrootsAppBackends>);
- let fallback_config_status = RwSignal::new_local(RadrootsAppConfigStatus::Unknown);
- let backends = context
- .as_ref()
- .map(|value| value.backends)
- .unwrap_or(fallback_backends);
- let config_status = context
- .as_ref()
- .map(|value| value.config_status)
- .unwrap_or(fallback_config_status);
- let navigate = use_navigate();
- let config_step = RwSignal::new_local(app_config_step_default());
- let profile_name = RwSignal::new_local(String::new());
- let profile_location = RwSignal::new_local(String::new());
- let role = RwSignal::new_local(None::<RadrootsAppRole>);
- let role_loaded = RwSignal::new_local(false);
- let farmer_farm_name = RwSignal::new_local(String::new());
- let farmer_location = RwSignal::new_local(String::new());
- let farmer_products = RwSignal::new_local(Vec::<String>::new());
- let farmer_products_input = RwSignal::new_local(String::new());
- let individual_products = RwSignal::new_local(Vec::<String>::new());
- let individual_products_input = RwSignal::new_local(String::new());
- let business_name = RwSignal::new_local(String::new());
- let business_location = RwSignal::new_local(String::new());
- let business_operations = RwSignal::new_local(String::new());
- let notifications_orders = RwSignal::new_local(true);
- let notifications_messages = RwSignal::new_local(true);
- let config_saving = RwSignal::new_local(false);
- let config_flow = move || RadrootsAppConfigFlowDraft {
- step: config_step.get(),
- profile_name: profile_name.get(),
- profile_location: profile_location.get(),
- role: role.get(),
- farmer_farm_name: farmer_farm_name.get(),
- farmer_location: farmer_location.get(),
- farmer_products: farmer_products.get(),
- individual_name: profile_name.get(),
- individual_location: profile_location.get(),
- individual_products: individual_products.get(),
- business_name: business_name.get(),
- business_location: business_location.get(),
- business_operations: business_operations.get(),
- notifications_orders: notifications_orders.get(),
- notifications_messages: notifications_messages.get(),
- };
- let list_label = |value: String, classes: Option<&str>| RadrootsAppUiListLabelValue {
- classes_wrap: None,
- hide_truncate: false,
- value: RadrootsAppUiListLabelValueKind::Text(RadrootsAppUiListLabelText {
- value,
- classes: classes.map(str::to_string),
- }),
- };
- let config_validation = move || app_config_flow_validate(&config_flow());
- let advance_step = {
- let backends = backends.clone();
- let config_status = config_status.clone();
- let config_saving = config_saving.clone();
- let navigate = navigate.clone();
- Callback::new(move |_| {
- let validation = config_validation();
- if !validation.can_continue || config_saving.get() {
- return;
- }
- if matches!(config_step.get(), RadrootsAppConfigStep::Preferences) {
- let Some(config_data) = app_config_flow_build_config(&config_flow()) else {
- return;
- };
- let Some((datastore, key_maps)) = backends.with(|value| {
- value.as_ref().map(|backends| {
- (
- backends.datastore.clone(),
- backends.config.datastore.key_maps.clone(),
- )
- })
- }) else {
- return;
- };
- let config_status = config_status.clone();
- let config_saving = config_saving.clone();
- let navigate = navigate.clone();
- config_saving.set(true);
- spawn_local(async move {
- let result = match app_datastore_create_config(
- datastore.as_ref(),
- &key_maps,
- &config_data,
- )
- .await
- {
- Ok(_) => Ok(()),
- Err(RadrootsAppConfigStoreError::Record(
- RadrootsAppConfigRecordError::AlreadyExists,
- )) => app_datastore_update_config(
- datastore.as_ref(),
- &key_maps,
- &config_data,
- )
- .await
- .map(|_| ()),
- Err(err) => Err(err),
- };
- match result {
- Ok(()) => {
- config_status.set(RadrootsAppConfigStatus::Configured);
- navigate("/", Default::default());
- }
- Err(err) => {
- let _ = app_log_error_emit(&err);
- config_status.set(RadrootsAppConfigStatus::Corrupt);
- }
- }
- config_saving.set(false);
- });
- return;
- }
- config_step.set(validation.next_step);
- })
- };
- let advance_step_click = {
- let advance_step = advance_step.clone();
- Callback::new(move |_ev: MouseEvent| {
- advance_step.run(());
- })
- };
- let rewind_step = Callback::new(move |_ev: MouseEvent| {
- let validation = config_validation();
- config_step.set(validation.prev_step);
- });
- let add_farmer_product = {
- let farmer_products = farmer_products.clone();
- let farmer_products_input = farmer_products_input.clone();
- move || {
- let entry = farmer_products_input.get_untracked();
- let trimmed = entry.trim().to_string();
- if trimmed.is_empty() {
- return;
- }
- farmer_products.update(|items| {
- if !items.iter().any(|item| item.eq_ignore_ascii_case(&trimmed)) {
- items.push(trimmed.clone());
- }
- });
- farmer_products_input.set(String::new());
- }
- };
- let update_individual_products = {
- let individual_products = individual_products.clone();
- let individual_products_input = individual_products_input.clone();
- move |value: String| {
- individual_products_input.set(value.clone());
- let mut items = Vec::new();
- for entry in value.split(|c| c == ',' || c == '\n') {
- let trimmed = entry.trim();
- if trimmed.is_empty() {
- continue;
- }
- if items.iter().any(|item: &String| item.eq_ignore_ascii_case(trimmed)) {
- continue;
- }
- items.push(trimmed.to_string());
- }
- individual_products.set(items);
- }
- };
- Effect::new(move || {
- if role_loaded.get() {
- return;
- }
- let Some((datastore, key_maps)) = backends.with(|value| {
- value.as_ref().map(|backends| {
- (
- backends.datastore.clone(),
- backends.config.datastore.key_maps.clone(),
- )
- })
- }) else {
- return;
- };
- role_loaded.set(true);
- let role = role.clone();
- let config_status = config_status.clone();
- spawn_local(async move {
- match app_datastore_read_state(datastore.as_ref(), &key_maps).await {
- Ok(state) => {
- role.set(Some(state.role));
- }
- Err(err) => {
- let _ = app_log_error_emit(&err);
- config_status.set(RadrootsAppConfigStatus::Corrupt);
- }
- }
- });
- });
- view! {
- <main id="app-config" class="app-page app-page-fixed relative w-full flex flex-col">
- <section
- id="app-config-body"
- class="app-page-scroll scroll-hide flex flex-col flex-1 min-h-0 w-full gap-6 px-4 pt-6 pb-28 overscroll-contain"
- >
- <header id="app-config-header" class="flex flex-col gap-2">
- <p class="text-[0.7rem] font-semibold uppercase tracking-[0.2em] text-ly1-gl-label/60">
- {"Configuration"}
- </p>
- <h1 id="app-config-title" class="text-2xl font-semibold text-ly0-gl">
- {"Set up your profile"}
- </h1>
- <p id="app-config-step" class="text-sm text-ly1-gl-label/80">
- {move || {
- let step = config_step.get();
- let index = match step {
- RadrootsAppConfigStep::Profile => 1,
- RadrootsAppConfigStep::Role => 2,
- RadrootsAppConfigStep::Preferences => 3,
- };
- format!("Step {index} of 3")
- }}
- </p>
- </header>
- {move || match config_step.get() {
- RadrootsAppConfigStep::Profile => view! {
- <section
- id="app-config-profile"
- class="app-view app-view-enter flex flex-col w-full gap-5"
- >
- <RadrootsAppUiFormField
- label="Profile name".to_string()
- id="app-config-profile-name-field".to_string()
- >
- <input
- id="app-config-profile-name"
- class="input-base"
- type="text"
- placeholder="Your name".to_string()
- prop:value=move || profile_name.get()
- on:input=move |ev| {
- profile_name.set(event_target_value(&ev));
- }
- />
- </RadrootsAppUiFormField>
- <RadrootsAppUiFormField
- label="Location".to_string()
- id="app-config-profile-location-field".to_string()
- hint="City or region".to_string()
- >
- <input
- id="app-config-profile-location"
- class="input-base"
- type="text"
- placeholder="e.g. Sonoma, CA".to_string()
- prop:value=move || profile_location.get()
- on:keydown=move |ev: KeyboardEvent| {
- if ev.key() == "Enter" {
- ev.prevent_default();
- advance_step.run(());
- }
- }
- on:input=move |ev| {
- profile_location.set(event_target_value(&ev));
- }
- />
- </RadrootsAppUiFormField>
- </section>
- }.into_any(),
- RadrootsAppConfigStep::Role => view! {
- <section
- id="app-config-role"
- class="app-view app-view-enter flex flex-col w-full gap-5"
- >
- <div id="app-config-role-spacer"></div>
- {move || match role.get() {
- Some(RadrootsAppRole::Farm) => view! {
- <div
- id="app-config-role-farm"
- class="flex flex-col gap-4"
- >
- <RadrootsAppUiFormField
- label="Farm name".to_string()
- id="app-config-farm-name-field".to_string()
- >
- <input
- id="app-config-farm-name"
- class="input-base"
- type="text"
- placeholder="e.g. Willow Creek Farm".to_string()
- prop:value=move || farmer_farm_name.get()
- on:input=move |ev| {
- farmer_farm_name.set(event_target_value(&ev));
- }
- />
- </RadrootsAppUiFormField>
- <RadrootsAppUiFormField
- label="Location".to_string()
- id="app-config-farm-location-field".to_string()
- >
- <input
- id="app-config-farm-location"
- class="input-base"
- type="text"
- placeholder="City or region".to_string()
- prop:value=move || farmer_location.get()
- on:input=move |ev| {
- farmer_location.set(event_target_value(&ev));
- }
- />
- </RadrootsAppUiFormField>
- <RadrootsAppUiFormField
- label="Products growing".to_string()
- id="app-config-farm-products-field".to_string()
- hint="Press enter to add items".to_string()
- >
- <input
- id="app-config-farm-products-input"
- class="input-base"
- type="text"
- placeholder="Add a product".to_string()
- prop:value=move || farmer_products_input.get()
- on:keydown=move |ev: KeyboardEvent| {
- if ev.key() == "Enter" || ev.key() == "," {
- ev.prevent_default();
- add_farmer_product();
- }
- }
- on:input=move |ev| {
- farmer_products_input.set(event_target_value(&ev));
- }
- />
- <RadrootsAppUiChips id="app-config-farm-products".to_string()>
- <For
- each=move || farmer_products.get()
- key=|value| value.clone()
- children=move |value| {
- let remove_value = value.clone();
- view! {
- <RadrootsAppUiChip
- label=value.clone()
- active=true
- on_click=Callback::new(move |_| {
- farmer_products.update(|items| {
- items.retain(|item| item != &remove_value);
- });
- })
- />
- }
- }
- />
- </RadrootsAppUiChips>
- </RadrootsAppUiFormField>
- </div>
- }.into_any(),
- Some(RadrootsAppRole::Individual) => view! {
- <div
- id="app-config-role-individual"
- class="flex flex-col gap-4"
- >
- <RadrootsAppUiFormField
- label="Products interested in".to_string()
- id="app-config-individual-products-field".to_string()
- hint="Use commas or new lines".to_string()
- >
- <textarea
- id="app-config-individual-products-input"
- class="textarea-base min-h-[15rem]"
- rows="8"
- placeholder="Apples, tomatoes, fresh herbs".to_string()
- prop:value=move || individual_products_input.get()
- on:input=move |ev| {
- update_individual_products(event_target_value(&ev));
- }
- ></textarea>
- </RadrootsAppUiFormField>
- </div>
- }.into_any(),
- Some(RadrootsAppRole::Business) => view! {
- <div
- id="app-config-role-business"
- class="flex flex-col gap-4"
- >
- <RadrootsAppUiFormField
- label="Business name".to_string()
- id="app-config-business-name-field".to_string()
- >
- <input
- id="app-config-business-name"
- class="input-base"
- type="text"
- placeholder="Business name".to_string()
- prop:value=move || business_name.get()
- on:input=move |ev| {
- business_name.set(event_target_value(&ev));
- }
- />
- </RadrootsAppUiFormField>
- <RadrootsAppUiFormField
- label="Location".to_string()
- id="app-config-business-location-field".to_string()
- >
- <input
- id="app-config-business-location"
- class="input-base"
- type="text"
- placeholder="City or region".to_string()
- prop:value=move || business_location.get()
- on:input=move |ev| {
- business_location.set(event_target_value(&ev));
- }
- />
- </RadrootsAppUiFormField>
- <RadrootsAppUiFormField
- label="Type of operations".to_string()
- id="app-config-business-operations-field".to_string()
- hint="What do you coordinate or purchase?".to_string()
- >
- <textarea
- id="app-config-business-operations"
- class="textarea-base"
- rows="3"
- placeholder="Tell us about your operations".to_string()
- prop:value=move || business_operations.get()
- on:input=move |ev| {
- business_operations.set(event_target_value(&ev));
- }
- ></textarea>
- </RadrootsAppUiFormField>
- </div>
- }.into_any(),
- None => view! {
- <p class="text-sm text-ly1-gl-label/80">
- {"Select a role to continue."}
- </p>
- }.into_any(),
- }}
- </section>
- }.into_any(),
- RadrootsAppConfigStep::Preferences => view! {
- <section
- id="app-config-preferences"
- class="app-view app-view-enter flex flex-col w-full gap-5"
- >
- {move || {
- if role.get() == Some(RadrootsAppRole::Individual) {
- let profile_summary = {
- let name = profile_name.get();
- let location = profile_location.get();
- let name = name.trim().to_string();
- let location = location.trim().to_string();
- if name.is_empty() && location.is_empty() {
- "Add name and location".to_string()
- } else if location.is_empty() {
- name
- } else if name.is_empty() {
- location
- } else {
- format!("{name} • {location}")
- }
- };
- let products_summary = {
- let items = individual_products.get();
- if items.is_empty() {
- "Add products".to_string()
- } else {
- items.join(", ")
- }
- };
- let summary_list = RadrootsAppUiList {
- id: Some("app-config-summary-list".to_string()),
- view: Some("app-config-summary".to_string()),
- classes: None,
- title: Some(RadrootsAppUiListTitle {
- value: RadrootsAppUiListTitleValue::Text("Summary".to_string()),
- classes: None,
- mod_value: None,
- link: None,
- on_click: None,
- }),
- default_state: None,
- list: Some(vec![
- Some(RadrootsAppUiListItem {
- kind: RadrootsAppUiListItemKind::Touch(RadrootsAppUiListTouch {
- label: RadrootsAppUiListLabel {
- left: vec![list_label("Profile".to_string(), None)],
- right: vec![list_label(profile_summary, Some("text-xs"))],
- },
- display: None,
- end: Some(RadrootsAppUiListTouchEnd {
- icon: RadrootsAppUiListIcon {
- key: "caret-right".to_string(),
- class: None,
- },
- on_click: None,
- }),
- on_click: Some(Callback::new(move |_| {
- config_step.set(RadrootsAppConfigStep::Profile);
- })),
- }),
- loading: false,
- hide_active: false,
- hide_field: false,
- full_rounded: false,
- offset: None,
- }),
- Some(RadrootsAppUiListItem {
- kind: RadrootsAppUiListItemKind::Touch(RadrootsAppUiListTouch {
- label: RadrootsAppUiListLabel {
- left: vec![list_label("Products interested in".to_string(), None)],
- right: vec![list_label(products_summary, Some("text-xs"))],
- },
- display: None,
- end: Some(RadrootsAppUiListTouchEnd {
- icon: RadrootsAppUiListIcon {
- key: "caret-right".to_string(),
- class: None,
- },
- on_click: None,
- }),
- on_click: Some(Callback::new(move |_| {
- config_step.set(RadrootsAppConfigStep::Role);
- })),
- }),
- loading: false,
- hide_active: false,
- hide_field: false,
- full_rounded: false,
- offset: None,
- }),
- ]),
- hide_offset: false,
- styles: None,
- };
- view! { <RadrootsAppUiListView basis=summary_list /> }.into_any()
- } else {
- view! { <></> }.into_any()
- }
- }}
- {move || {
- let toggle_orders = {
- let notifications_orders = notifications_orders.clone();
- Callback::new(move |value: bool| {
- notifications_orders.set(value);
- })
- };
- let toggle_messages = {
- let notifications_messages = notifications_messages.clone();
- Callback::new(move |value: bool| {
- notifications_messages.set(value);
- })
- };
- let notifications_list = RadrootsAppUiList {
- id: Some("app-config-notifications-list".to_string()),
- view: Some("app-config-notifications".to_string()),
- classes: None,
- title: Some(RadrootsAppUiListTitle {
- value: RadrootsAppUiListTitleValue::Text("Notifications".to_string()),
- classes: None,
- mod_value: None,
- link: None,
- on_click: None,
- }),
- default_state: None,
- list: Some(vec![
- Some(RadrootsAppUiListItem {
- kind: RadrootsAppUiListItemKind::Toggle(RadrootsAppUiListToggle {
- label: RadrootsAppUiListLabel {
- left: vec![list_label("Order updates".to_string(), None)],
- right: Vec::new(),
- },
- checked: notifications_orders.get(),
- disabled: false,
- on_toggle: Some(toggle_orders),
- }),
- loading: false,
- hide_active: true,
- hide_field: false,
- full_rounded: false,
- offset: None,
- }),
- Some(RadrootsAppUiListItem {
- kind: RadrootsAppUiListItemKind::Toggle(RadrootsAppUiListToggle {
- label: RadrootsAppUiListLabel {
- left: vec![list_label("Messages".to_string(), None)],
- right: Vec::new(),
- },
- checked: notifications_messages.get(),
- disabled: false,
- on_toggle: Some(toggle_messages),
- }),
- loading: false,
- hide_active: true,
- hide_field: false,
- full_rounded: false,
- offset: None,
- }),
- ]),
- hide_offset: false,
- styles: None,
- };
- view! { <RadrootsAppUiListView basis=notifications_list /> }
- }}
- </section>
- }.into_any(),
- }}
- </section>
- <footer
- id="app-config-actions"
- class="z-10 absolute bottom-4 left-0 flex flex-col w-full justify-center items-center se-compact:bottom-0"
- >
- {move || {
- let validation = config_validation();
- let continue_disabled = !validation.can_continue;
- let continue_label = if matches!(config_step.get(), RadrootsAppConfigStep::Preferences) {
- "Finish".to_string()
- } else {
- "Continue".to_string()
- };
- let back_label = "Back".to_string();
- let continue_action = RadrootsAppUiButtonLayoutAction {
- label: continue_label,
- disabled: continue_disabled,
- loading: false,
- on_click: advance_step_click.clone(),
- class: None,
- class_label: None,
- style: None,
- };
- let back_action = RadrootsAppUiButtonLayoutBackAction {
- visible: validation.can_back,
- label: Some(back_label),
- disabled: false,
- on_click: rewind_step.clone(),
- compact: false,
- };
- view! {
- <RadrootsAppUiButtonLayoutPair
- continue_action=continue_action
- back=back_action
- />
- }.into_any()
- }}
- </footer>
- </main>
- }
-}
-
-#[component]
-fn HomePage() -> impl IntoView {
- let current_view = RwSignal::new_local(HomeView::Activity);
- let is_activity = move || matches!(current_view.get(), HomeView::Activity);
- let is_profile = move || matches!(current_view.get(), HomeView::Profile);
- view! {
- <AppPageChrome title=t!("app.nav.home")>
- <section
- id="app-home"
- class="flex flex-col items-center justify-start gap-6 pt-6"
- >
- <div
- id="app-home-toggle"
- class="home-toggle"
- class:home-toggle--left=is_activity
- class:home-toggle--right=is_profile
- role="tablist"
- aria-label="Home view"
- >
- <div class="home-toggle__indicator" aria-hidden="true"></div>
- <button
- id="app-home-toggle-activity"
- class="home-toggle__button"
- class:is-active=is_activity
- type="button"
- role="tab"
- aria-selected=move || if is_activity() { "true" } else { "false" }
- on:click=move |_| current_view.set(HomeView::Activity)
- >
- {"Activity"}
- </button>
- <button
- id="app-home-toggle-profile"
- class="home-toggle__button"
- class:is-active=is_profile
- type="button"
- role="tab"
- aria-selected=move || if is_profile() { "true" } else { "false" }
- on:click=move |_| current_view.set(HomeView::Profile)
- >
- {"Profile"}
- </button>
- </div>
- <p id="app-home-view-title" class="home-toggle__title">
- {move || current_view.get().label()}
- </p>
- </section>
- </AppPageChrome>
- }
-}
-
-#[component]
-pub fn RadrootsApp() -> impl IntoView {
- view! {
- <Router>
- <AppShell />
- </Router>
- }
-}
-
-#[component]
-fn AppShell() -> impl IntoView {
- let backends = RwSignal::new_local(None::<RadrootsAppBackends>);
- let init_error = RwSignal::new_local(None::<RadrootsAppInitError>);
- let init_state = RwSignal::new_local(app_init_state_default());
- let setup_status = RwSignal::new_local(RadrootsAppSetupStatus::Unknown);
- let config_status = RwSignal::new_local(RadrootsAppConfigStatus::Unknown);
- let navigate = use_navigate();
- provide_context(backends);
- provide_context(init_error);
- provide_context(init_state);
- provide_context(setup_status);
- provide_context(config_status);
- provide_context(app_i18n_init());
- Effect::new(move || {
- let navigate = navigate.clone();
- spawn_local(async move {
- let stage = RadrootsAppInitStage::Storage;
- init_state.update(|state| app_init_stage_set(state, stage));
- log_init_stage(stage);
- let config = app_config_default();
- if !app_init_has_completed() {
- init_state.update(|state| {
- state.loaded_bytes = 0;
- state.total_bytes = Some(0);
- });
- let assets_result = app_init_assets(
- &config,
- |stage| {
- init_state.update(|state| app_init_stage_set(state, stage));
- log_init_stage(stage);
- },
- |loaded, total| {
- init_state.update(|state| {
- app_init_progress_add(state, loaded);
- match total {
- Some(value) => app_init_total_add(state, value),
- None => app_init_total_unknown(state),
- }
- });
- },
- )
- .await;
- if let Err(err) = assets_result {
- let init_err = RadrootsAppInitError::Assets(err);
- let _ = app_log_error_emit(&init_err);
- init_error.set(Some(init_err));
- let stage = RadrootsAppInitStage::Error;
- init_state.update(|state| app_init_stage_set(state, stage));
- log_init_stage(stage);
- return;
- }
- let stage = RadrootsAppInitStage::Storage;
- init_state.update(|state| app_init_stage_set(state, stage));
- log_init_stage(stage);
- }
- match app_init_backends(config).await {
- Ok(value) => {
- let key_maps = value.config.datastore.key_maps.clone();
- let datastore = value.datastore.clone();
- let keystore_config = value.nostr_keystore.get_config();
- backends.set(Some(value));
- app_init_mark_completed();
- let stage = RadrootsAppInitStage::Ready;
- init_state.update(|state| app_init_stage_set(state, stage));
- log_init_stage(stage);
- let navigate = navigate.clone();
- let setup_status = setup_status.clone();
- let config_status = config_status.clone();
- spawn_local(async move {
- let keystore = radroots_app_core::keystore::RadrootsClientWebKeystoreNostr::new(
- Some(keystore_config),
- );
- match app_init_setup_status(datastore.as_ref(), &keystore, &key_maps).await {
- Ok(status) => {
- setup_status.set(status);
- match status {
- RadrootsAppSetupStatus::Required | RadrootsAppSetupStatus::Locked => {
- config_status.set(RadrootsAppConfigStatus::Unknown);
- navigate("/setup", Default::default());
- }
- RadrootsAppSetupStatus::Corrupt => {
- config_status.set(RadrootsAppConfigStatus::Unknown);
- navigate("/recovery", Default::default());
- }
- RadrootsAppSetupStatus::Configured => {
- let config_state = app_config_status(datastore.as_ref(), &key_maps).await;
- config_status.set(config_state);
- if matches!(
- config_state,
- RadrootsAppConfigStatus::Required | RadrootsAppConfigStatus::Corrupt
- ) {
- navigate("/setup/config", Default::default());
- }
- }
- RadrootsAppSetupStatus::Unknown => {}
- }
- }
- Err(err) => {
- let _ = app_log_error_emit(&err);
- setup_status.set(RadrootsAppSetupStatus::Corrupt);
- config_status.set(RadrootsAppConfigStatus::Unknown);
- navigate("/recovery", Default::default());
- }
- }
- });
- let flush_ctx = backends.with_untracked(|value| {
- value.as_ref().map(|backends| backends.config.datastore.key_maps.clone())
- });
- if let Some(key_maps) = flush_ctx {
- spawn_local(async move {
- let _ = app_log_buffer_flush_deferred(
- &logs_datastore(),
- &key_maps,
- true,
- )
- .await;
- });
- }
- }
- Err(err) => {
- let _ = app_log_error_emit(&err);
- init_error.set(Some(err));
- let stage = RadrootsAppInitStage::Error;
- init_state.update(|state| app_init_stage_set(state, stage));
- log_init_stage(stage);
- }
- }
- })
- });
- let setup_gate = move || app_setup_gate_from_status(setup_status.get());
- let config_gate = move || app_config_gate_from_status(config_status.get());
- let config_ready = move || {
- let status = setup_status.get();
- if matches!(status, RadrootsAppSetupStatus::Configured) {
- !matches!(config_status.get(), RadrootsAppConfigStatus::Unknown)
- } else {
- true
- }
- };
- view! {
- <Show
- when=move || {
- init_state.get().stage == RadrootsAppInitStage::Ready
- && !matches!(setup_status.get(), RadrootsAppSetupStatus::Unknown)
- && config_ready()
- }
- fallback=|| view! { <SplashPage /> }
- >
- {move || {
- let gate = setup_gate();
- if gate.show_recovery {
- return view! { <RecoveryPage /> }.into_any();
- }
- if gate.show_setup {
- return view! { <SetupPage /> }.into_any();
- }
- if gate.show_app {
- let config_gate = config_gate();
- if config_gate.show_config {
- return view! { <ConfigPage /> }.into_any();
- }
- return view! {
- <div id="app-shell">
- <Routes
- fallback=|| view! {
- <main id="app-not-found" class="app-page app-page-fixed">
- <p id="app-not-found-label">{t!("app.not_found")}</p>
- </main>
- }
- >
- <Route path=path!("") view=HomePage />
- <Route path=path!("settings/logs") view=RadrootsAppLogsPage />
- <Route path=path!("test") view=RadrootsAppUiDemoPage />
- <Route
- path=path!("settings/status")
- view=RadrootsAppSettingsStatusPage
- />
- <Route path=path!("settings") view=RadrootsAppSettingsPage />
- </Routes>
- </div>
- }
- .into_any();
- }
- view! { <SplashPage /> }.into_any()
- }}
- </Show>
- }
-}
-
-#[cfg(test)]
-mod tests {
- use crate::app_health_check_delay_ms;
-
- #[test]
- fn health_check_delay_is_positive() {
- assert!(app_health_check_delay_ms() > 0);
- }
-}
diff --git a/app/src/bootstrap.rs b/app/src/bootstrap.rs
@@ -1,1098 +0,0 @@
-#![forbid(unsafe_code)]
-
-use radroots_app_core::datastore::{RadrootsClientDatastore, RadrootsClientDatastoreError};
-use radroots_app_core::notifications::RadrootsClientNotificationsPermission;
-
-use crate::{
- app_datastore_obj_key_state,
- app_datastore_obj_key_setup_draft,
- app_datastore_param_key,
- app_datastore_key_eula_date,
- app_datastore_key_nostr_key,
- app_log_debug_emit,
- app_setup_state_new,
- app_state_record_new,
- app_state_record_validate,
- app_state_timestamp_ms,
- RadrootsAppProfileSeed,
- RadrootsAppRole,
- RadrootsAppState,
- RadrootsAppSetupDraft,
- RadrootsAppStateError,
- RadrootsAppStateRecord,
- RadrootsAppInitError,
- RadrootsAppInitResult,
- RadrootsAppKeyMapConfig,
-};
-
-pub async fn app_datastore_write_state_record<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
- record: &RadrootsAppStateRecord,
-) -> RadrootsAppInitResult<RadrootsAppStateRecord> {
- let key = app_datastore_obj_key_state(key_maps).map_err(RadrootsAppInitError::Config)?;
- let value = datastore
- .set_obj(key, record)
- .await
- .map_err(RadrootsAppInitError::Datastore)?;
- let _ = app_log_debug_emit("log.app.bootstrap.state", "write", Some(key.to_string()));
- Ok(value)
-}
-
-pub async fn app_datastore_read_state_record<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppInitResult<RadrootsAppStateRecord> {
- let key = app_datastore_obj_key_state(key_maps).map_err(RadrootsAppInitError::Config)?;
- match datastore.get_obj::<RadrootsAppStateRecord>(key).await {
- Ok(record) => {
- app_state_record_validate(&record).map_err(RadrootsAppInitError::State)?;
- let _ =
- app_log_debug_emit("log.app.bootstrap.state", "read", Some(key.to_string()));
- Ok(record)
- }
- Err(RadrootsClientDatastoreError::NoResult) => {
- match datastore.get_obj::<RadrootsAppState>(key).await {
- Ok(state) => {
- let record = app_state_record_new(state, 1, app_state_timestamp_ms());
- let value = app_datastore_write_state_record(datastore, key_maps, &record)
- .await?;
- Ok(value)
- }
- Err(RadrootsClientDatastoreError::NoResult) => {
- if let Some(record) = app_datastore_migrate_legacy_state(datastore, key_maps).await? {
- return Ok(record);
- }
- Err(RadrootsAppInitError::State(RadrootsAppStateError::Missing))
- }
- Err(err) => Err(RadrootsAppInitError::Datastore(err)),
- }
- }
- Err(err) => Err(RadrootsAppInitError::Datastore(err)),
- }
-}
-
-async fn app_datastore_migrate_legacy_state<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppInitResult<Option<RadrootsAppStateRecord>> {
- let key_nostr = app_datastore_key_nostr_key(key_maps).map_err(RadrootsAppInitError::Config)?;
- let key_eula = app_datastore_key_eula_date(key_maps).map_err(RadrootsAppInitError::Config)?;
- let active_key = match datastore.get(key_nostr).await {
- Ok(value) => value,
- Err(_) => return Ok(None),
- };
- let eula_date = match datastore.get(key_eula).await {
- Ok(value) => value,
- Err(_) => return Ok(None),
- };
- let state = app_setup_state_new(active_key.clone(), eula_date, RadrootsAppRole::default());
- let record = app_state_record_new(state, 1, app_state_timestamp_ms());
- let stored = app_datastore_write_state_record(datastore, key_maps, &record).await?;
- let _ = datastore.del(key_nostr).await;
- let _ = datastore.del(key_eula).await;
- Ok(Some(stored))
-}
-
-pub async fn app_datastore_write_state<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
- data: &RadrootsAppState,
-) -> RadrootsAppInitResult<RadrootsAppState> {
- app_datastore_update_state(datastore, key_maps, data).await
-}
-
-pub async fn app_datastore_create_state<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
- data: &RadrootsAppState,
-) -> RadrootsAppInitResult<RadrootsAppState> {
- let now_ms = app_state_timestamp_ms();
- match app_datastore_read_state_record(datastore, key_maps).await {
- Ok(_) => Err(RadrootsAppInitError::State(RadrootsAppStateError::AlreadyExists)),
- Err(RadrootsAppInitError::State(RadrootsAppStateError::Missing)) => {
- let record = app_state_record_new(data.clone(), 1, now_ms);
- let value = app_datastore_write_state_record(datastore, key_maps, &record).await?;
- Ok(value.state)
- }
- Err(err) => Err(err),
- }
-}
-
-pub async fn app_datastore_update_state<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
- data: &RadrootsAppState,
-) -> RadrootsAppInitResult<RadrootsAppState> {
- let now_ms = app_state_timestamp_ms();
- let record = match app_datastore_read_state_record(datastore, key_maps).await {
- Ok(existing) => app_state_record_new(data.clone(), existing.revision + 1, now_ms),
- Err(RadrootsAppInitError::State(RadrootsAppStateError::Missing)) => {
- return Err(RadrootsAppInitError::State(RadrootsAppStateError::Missing));
- }
- Err(err) => return Err(err),
- };
- let value = app_datastore_write_state_record(datastore, key_maps, &record).await?;
- Ok(value.state)
-}
-
-pub async fn app_datastore_read_state<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppInitResult<RadrootsAppState> {
- let record = app_datastore_read_state_record(datastore, key_maps).await?;
- Ok(record.state)
-}
-
-pub async fn app_datastore_has_state<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppInitResult<bool> {
- match app_datastore_read_state_record(datastore, key_maps).await {
- Ok(_) => Ok(true),
- Err(RadrootsAppInitError::State(RadrootsAppStateError::Missing)) => Ok(false),
- Err(err) => Err(err),
- }
-}
-
-pub async fn app_datastore_clear_bootstrap<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppInitResult<()> {
- let app_key = app_datastore_obj_key_state(key_maps).map_err(RadrootsAppInitError::Config)?;
- datastore
- .del_obj(app_key)
- .await
- .map_err(RadrootsAppInitError::Datastore)?;
- let _ = app_log_debug_emit("log.app.bootstrap.reset", "clear", None);
- Ok(())
-}
-
-pub async fn app_datastore_read_setup_draft<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppInitResult<Option<RadrootsAppSetupDraft>> {
- let key = app_datastore_obj_key_setup_draft(key_maps).map_err(RadrootsAppInitError::Config)?;
- match datastore.get_obj::<RadrootsAppSetupDraft>(key).await {
- Ok(draft) => Ok(Some(draft)),
- Err(RadrootsClientDatastoreError::NoResult) => Ok(None),
- Err(err) => Err(RadrootsAppInitError::Datastore(err)),
- }
-}
-
-pub async fn app_datastore_write_setup_draft<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
- draft: &RadrootsAppSetupDraft,
-) -> RadrootsAppInitResult<RadrootsAppSetupDraft> {
- let key = app_datastore_obj_key_setup_draft(key_maps).map_err(RadrootsAppInitError::Config)?;
- let value = datastore
- .set_obj(key, draft)
- .await
- .map_err(RadrootsAppInitError::Datastore)?;
- Ok(value)
-}
-
-pub async fn app_datastore_clear_setup_draft<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppInitResult<()> {
- let key = app_datastore_obj_key_setup_draft(key_maps).map_err(RadrootsAppInitError::Config)?;
- match datastore.del_obj(key).await {
- Ok(_) => Ok(()),
- Err(RadrootsClientDatastoreError::NoResult) => Ok(()),
- Err(err) => Err(RadrootsAppInitError::Datastore(err)),
- }
-}
-
-pub async fn app_datastore_write_profile_seed<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
- profile: &RadrootsAppProfileSeed,
-) -> RadrootsAppInitResult<RadrootsAppProfileSeed> {
- let param = app_datastore_param_key(key_maps, "nostr_profile")
- .map_err(RadrootsAppInitError::Config)?;
- let key = param(&profile.public_key);
- let stored = datastore
- .set_obj(&key, profile)
- .await
- .map_err(RadrootsAppInitError::Datastore)?;
- let _ = app_log_debug_emit("log.app.bootstrap.profile", "write", Some(key));
- Ok(stored)
-}
-
-pub async fn app_state_set_notifications_permission<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
- permission: &str,
-) -> RadrootsAppInitResult<RadrootsAppState> {
- let mut data = app_datastore_read_state(datastore, key_maps).await?;
- data.notifications_permission = Some(permission.to_string());
- let value = app_datastore_update_state(datastore, key_maps, &data).await?;
- Ok(value)
-}
-
-pub fn app_state_notifications_permission_value(
- data: &RadrootsAppState,
-) -> Option<RadrootsClientNotificationsPermission> {
- data.notifications_permission
- .as_deref()
- .and_then(RadrootsClientNotificationsPermission::parse)
-}
-
-pub async fn app_state_set_notifications_permission_value<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
- permission: RadrootsClientNotificationsPermission,
-) -> RadrootsAppInitResult<RadrootsAppState> {
- app_state_set_notifications_permission(datastore, key_maps, permission.as_str()).await
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- app_datastore_clear_bootstrap,
- app_datastore_clear_setup_draft,
- app_datastore_create_state,
- app_datastore_has_state,
- app_datastore_read_state_record,
- app_datastore_read_state,
- app_datastore_read_setup_draft,
- app_datastore_update_state,
- app_datastore_write_setup_draft,
- app_datastore_write_profile_seed,
- app_state_set_notifications_permission,
- app_state_set_notifications_permission_value,
- app_state_notifications_permission_value,
- app_datastore_write_state,
- };
- use crate::{
- app_datastore_key_eula_date,
- app_datastore_key_nostr_key,
- app_key_maps_default,
- RadrootsAppInitError,
- RadrootsAppProfileSeed,
- RadrootsAppRole,
- RadrootsAppState,
- RadrootsAppStateError,
- RadrootsAppStateRecord,
- RadrootsAppSetupDraft,
- };
- use async_trait::async_trait;
- use radroots_app_core::backup::RadrootsClientBackupDatastorePayload;
- use radroots_app_core::datastore::{
- RadrootsClientDatastore,
- RadrootsClientDatastoreEntries,
- RadrootsClientDatastoreError,
- RadrootsClientDatastoreResult,
- RadrootsClientWebDatastore,
- };
- use radroots_app_core::idb::{RadrootsClientIdbConfig, IDB_CONFIG_DATASTORE};
- use radroots_app_core::notifications::RadrootsClientNotificationsPermission;
- use serde::de::DeserializeOwned;
- use serde::Serialize;
- use std::cell::RefCell;
- use std::collections::BTreeMap;
-
- struct SetupDraftDatastore {
- draft: RefCell<Option<RadrootsAppSetupDraft>>,
- }
-
- struct ProfileSeedDatastore {
- profile: RefCell<Option<RadrootsAppProfileSeed>>,
- }
-
- #[async_trait(?Send)]
- impl RadrootsClientDatastore for SetupDraftDatastore {
- fn get_config(&self) -> RadrootsClientIdbConfig {
- IDB_CONFIG_DATASTORE
- }
-
- fn get_store_id(&self) -> &str {
- "test"
- }
-
- async fn init(&self) -> RadrootsClientDatastoreResult<()> {
- Ok(())
- }
-
- async fn set(&self, _key: &str, _value: &str) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn get(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn set_obj<T>(
- &self,
- _key: &str,
- value: &T,
- ) -> RadrootsClientDatastoreResult<T>
- where
- T: Serialize + DeserializeOwned + Clone,
- {
- let encoded = serde_json::to_string(value)
- .map_err(|_| RadrootsClientDatastoreError::IdbUndefined)?;
- if let Ok(parsed) = serde_json::from_str::<RadrootsAppSetupDraft>(&encoded) {
- *self.draft.borrow_mut() = Some(parsed);
- return Ok(value.clone());
- }
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn update_obj<T>(
- &self,
- _key: &str,
- _value: &T,
- ) -> RadrootsClientDatastoreResult<T>
- where
- T: Serialize + DeserializeOwned + Clone,
- {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn get_obj<T>(&self, _key: &str) -> RadrootsClientDatastoreResult<T>
- where
- T: DeserializeOwned,
- {
- if let Some(draft) = self.draft.borrow().as_ref() {
- let encoded = serde_json::to_string(draft)
- .map_err(|_| RadrootsClientDatastoreError::NoResult)?;
- return serde_json::from_str(&encoded)
- .map_err(|_| RadrootsClientDatastoreError::NoResult);
- }
- Err(RadrootsClientDatastoreError::NoResult)
- }
-
- async fn del_obj(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
- *self.draft.borrow_mut() = None;
- Ok("cleared".to_string())
- }
-
- async fn del(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn del_pref(&self, _key_prefix: &str) -> RadrootsClientDatastoreResult<Vec<String>> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn set_param(
- &self,
- _key: &str,
- _key_param: &str,
- _value: &str,
- ) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn get_param(
- &self,
- _key: &str,
- _key_param: &str,
- ) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn keys(&self) -> RadrootsClientDatastoreResult<Vec<String>> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn entries(&self) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn entries_pref(
- &self,
- _key_prefix: &str,
- ) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn reset(&self) -> RadrootsClientDatastoreResult<()> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn export_backup(
- &self,
- ) -> RadrootsClientDatastoreResult<RadrootsClientBackupDatastorePayload> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn import_backup(
- &self,
- _payload: RadrootsClientBackupDatastorePayload,
- ) -> RadrootsClientDatastoreResult<()> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
- }
-
- #[async_trait(?Send)]
- impl RadrootsClientDatastore for ProfileSeedDatastore {
- fn get_config(&self) -> RadrootsClientIdbConfig {
- IDB_CONFIG_DATASTORE
- }
-
- fn get_store_id(&self) -> &str {
- "test"
- }
-
- async fn init(&self) -> RadrootsClientDatastoreResult<()> {
- Ok(())
- }
-
- async fn set(&self, _key: &str, _value: &str) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn get(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn set_obj<T>(
- &self,
- _key: &str,
- value: &T,
- ) -> RadrootsClientDatastoreResult<T>
- where
- T: Serialize + DeserializeOwned + Clone,
- {
- let encoded = serde_json::to_string(value)
- .map_err(|_| RadrootsClientDatastoreError::IdbUndefined)?;
- if let Ok(parsed) = serde_json::from_str::<RadrootsAppProfileSeed>(&encoded) {
- *self.profile.borrow_mut() = Some(parsed);
- return Ok(value.clone());
- }
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn update_obj<T>(
- &self,
- _key: &str,
- _value: &T,
- ) -> RadrootsClientDatastoreResult<T>
- where
- T: Serialize + DeserializeOwned + Clone,
- {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn get_obj<T>(&self, _key: &str) -> RadrootsClientDatastoreResult<T>
- where
- T: DeserializeOwned,
- {
- if let Some(profile) = self.profile.borrow().as_ref() {
- let encoded = serde_json::to_string(profile)
- .map_err(|_| RadrootsClientDatastoreError::NoResult)?;
- return serde_json::from_str(&encoded)
- .map_err(|_| RadrootsClientDatastoreError::NoResult);
- }
- Err(RadrootsClientDatastoreError::NoResult)
- }
-
- async fn del_obj(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
- *self.profile.borrow_mut() = None;
- Ok("cleared".to_string())
- }
-
- async fn del(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn del_pref(&self, _key_prefix: &str) -> RadrootsClientDatastoreResult<Vec<String>> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn set_param(
- &self,
- _key: &str,
- _key_param: &str,
- _value: &str,
- ) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn get_param(
- &self,
- _key: &str,
- _key_param: &str,
- ) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn keys(&self) -> RadrootsClientDatastoreResult<Vec<String>> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn entries(&self) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn entries_pref(
- &self,
- _key_prefix: &str,
- ) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn reset(&self) -> RadrootsClientDatastoreResult<()> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn export_backup(
- &self,
- ) -> RadrootsClientDatastoreResult<RadrootsClientBackupDatastorePayload> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn import_backup(
- &self,
- _payload: RadrootsClientBackupDatastorePayload,
- ) -> RadrootsClientDatastoreResult<()> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
- }
-
- struct TestDatastore {
- state: Option<RadrootsAppState>,
- record: RefCell<Option<RadrootsAppStateRecord>>,
- }
-
- struct LegacyKeyDatastore {
- record: RefCell<Option<RadrootsAppStateRecord>>,
- values: RefCell<BTreeMap<String, String>>,
- }
-
- #[async_trait(?Send)]
- impl RadrootsClientDatastore for TestDatastore {
- fn get_config(&self) -> RadrootsClientIdbConfig {
- IDB_CONFIG_DATASTORE
- }
-
- fn get_store_id(&self) -> &str {
- "test"
- }
-
- async fn init(&self) -> RadrootsClientDatastoreResult<()> {
- Ok(())
- }
-
- async fn set(&self, _key: &str, _value: &str) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn get(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn set_obj<T>(
- &self,
- _key: &str,
- value: &T,
- ) -> RadrootsClientDatastoreResult<T>
- where
- T: Serialize + DeserializeOwned + Clone,
- {
- let encoded = serde_json::to_string(value)
- .map_err(|_| RadrootsClientDatastoreError::IdbUndefined)?;
- if let Ok(parsed) = serde_json::from_str::<RadrootsAppStateRecord>(&encoded) {
- *self.record.borrow_mut() = Some(parsed);
- return Ok(value.clone());
- }
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn update_obj<T>(
- &self,
- _key: &str,
- _value: &T,
- ) -> RadrootsClientDatastoreResult<T>
- where
- T: Serialize + DeserializeOwned + Clone,
- {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn get_obj<T>(&self, _key: &str) -> RadrootsClientDatastoreResult<T>
- where
- T: DeserializeOwned,
- {
- if let Some(record) = self.record.borrow().as_ref() {
- let encoded = serde_json::to_string(record)
- .map_err(|_| RadrootsClientDatastoreError::NoResult)?;
- if let Ok(parsed) = serde_json::from_str(&encoded) {
- return Ok(parsed);
- }
- };
- let Some(state) = self.state.as_ref() else {
- return Err(RadrootsClientDatastoreError::NoResult);
- };
- let encoded = serde_json::to_string(state)
- .map_err(|_| RadrootsClientDatastoreError::NoResult)?;
- serde_json::from_str(&encoded).map_err(|_| RadrootsClientDatastoreError::NoResult)
- }
-
- async fn del_obj(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn del(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn del_pref(&self, _key_prefix: &str) -> RadrootsClientDatastoreResult<Vec<String>> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn set_param(
- &self,
- _key: &str,
- _key_param: &str,
- _value: &str,
- ) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn get_param(
- &self,
- _key: &str,
- _key_param: &str,
- ) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn keys(&self) -> RadrootsClientDatastoreResult<Vec<String>> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn entries(&self) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn entries_pref(
- &self,
- _key_prefix: &str,
- ) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn reset(&self) -> RadrootsClientDatastoreResult<()> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn export_backup(
- &self,
- ) -> RadrootsClientDatastoreResult<RadrootsClientBackupDatastorePayload> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn import_backup(
- &self,
- _payload: RadrootsClientBackupDatastorePayload,
- ) -> RadrootsClientDatastoreResult<()> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
- }
-
- #[async_trait(?Send)]
- impl RadrootsClientDatastore for LegacyKeyDatastore {
- fn get_config(&self) -> RadrootsClientIdbConfig {
- IDB_CONFIG_DATASTORE
- }
-
- fn get_store_id(&self) -> &str {
- "test"
- }
-
- async fn init(&self) -> RadrootsClientDatastoreResult<()> {
- Ok(())
- }
-
- async fn set(&self, key: &str, value: &str) -> RadrootsClientDatastoreResult<String> {
- self.values
- .borrow_mut()
- .insert(key.to_string(), value.to_string());
- Ok(value.to_string())
- }
-
- async fn get(&self, key: &str) -> RadrootsClientDatastoreResult<String> {
- self.values
- .borrow()
- .get(key)
- .cloned()
- .ok_or(RadrootsClientDatastoreError::NoResult)
- }
-
- async fn set_obj<T>(
- &self,
- _key: &str,
- value: &T,
- ) -> RadrootsClientDatastoreResult<T>
- where
- T: Serialize + DeserializeOwned + Clone,
- {
- let encoded = serde_json::to_string(value)
- .map_err(|_| RadrootsClientDatastoreError::IdbUndefined)?;
- if let Ok(parsed) = serde_json::from_str::<RadrootsAppStateRecord>(&encoded) {
- *self.record.borrow_mut() = Some(parsed);
- return Ok(value.clone());
- }
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn update_obj<T>(
- &self,
- _key: &str,
- _value: &T,
- ) -> RadrootsClientDatastoreResult<T>
- where
- T: Serialize + DeserializeOwned + Clone,
- {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn get_obj<T>(&self, _key: &str) -> RadrootsClientDatastoreResult<T>
- where
- T: DeserializeOwned,
- {
- if let Some(record) = self.record.borrow().as_ref() {
- let encoded = serde_json::to_string(record)
- .map_err(|_| RadrootsClientDatastoreError::NoResult)?;
- if let Ok(parsed) = serde_json::from_str(&encoded) {
- return Ok(parsed);
- }
- }
- Err(RadrootsClientDatastoreError::NoResult)
- }
-
- async fn del_obj(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
- *self.record.borrow_mut() = None;
- Ok("cleared".to_string())
- }
-
- async fn del(&self, key: &str) -> RadrootsClientDatastoreResult<String> {
- self.values.borrow_mut().remove(key);
- Ok(key.to_string())
- }
-
- async fn del_pref(&self, _key_prefix: &str) -> RadrootsClientDatastoreResult<Vec<String>> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn set_param(
- &self,
- _key: &str,
- _key_param: &str,
- _value: &str,
- ) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn get_param(
- &self,
- _key: &str,
- _key_param: &str,
- ) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn keys(&self) -> RadrootsClientDatastoreResult<Vec<String>> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn entries(&self) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn entries_pref(
- &self,
- _key_prefix: &str,
- ) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn reset(&self) -> RadrootsClientDatastoreResult<()> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn export_backup(
- &self,
- ) -> RadrootsClientDatastoreResult<RadrootsClientBackupDatastorePayload> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn import_backup(
- &self,
- _payload: RadrootsClientBackupDatastorePayload,
- ) -> RadrootsClientDatastoreResult<()> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
- }
-
- #[test]
- fn state_write_maps_idb_errors() {
- let datastore = RadrootsClientWebDatastore::new(None);
- let key_maps = app_key_maps_default();
- let data = RadrootsAppState::default();
- let err = futures::executor::block_on(app_datastore_write_state(
- &datastore,
- &key_maps,
- &data,
- ))
- .expect_err("idb undefined");
- assert_eq!(err, RadrootsAppInitError::Datastore(RadrootsClientDatastoreError::IdbUndefined));
- }
-
- #[test]
- fn state_read_maps_idb_errors() {
- let datastore = RadrootsClientWebDatastore::new(None);
- let key_maps = app_key_maps_default();
- let err = futures::executor::block_on(app_datastore_read_state(
- &datastore,
- &key_maps,
- ))
- .expect_err("idb undefined");
- assert_eq!(err, RadrootsAppInitError::Datastore(RadrootsClientDatastoreError::IdbUndefined));
- }
-
- #[test]
- fn clear_bootstrap_maps_idb_errors() {
- let datastore = RadrootsClientWebDatastore::new(None);
- let key_maps = app_key_maps_default();
- let err = futures::executor::block_on(app_datastore_clear_bootstrap(
- &datastore,
- &key_maps,
- ))
- .expect_err("idb undefined");
- assert_eq!(err, RadrootsAppInitError::Datastore(RadrootsClientDatastoreError::IdbUndefined));
- }
-
- #[test]
- fn has_state_maps_idb_errors() {
- let datastore = RadrootsClientWebDatastore::new(None);
- let key_maps = app_key_maps_default();
- let err = futures::executor::block_on(app_datastore_has_state(&datastore, &key_maps))
- .expect_err("idb undefined");
- assert_eq!(err, RadrootsAppInitError::Datastore(RadrootsClientDatastoreError::IdbUndefined));
- }
-
- #[test]
- fn set_notifications_permission_maps_idb_errors() {
- let datastore = RadrootsClientWebDatastore::new(None);
- let key_maps = app_key_maps_default();
- let err = futures::executor::block_on(app_state_set_notifications_permission(
- &datastore,
- &key_maps,
- "granted",
- ))
- .expect_err("idb undefined");
- assert_eq!(err, RadrootsAppInitError::Datastore(RadrootsClientDatastoreError::IdbUndefined));
- }
-
- #[test]
- fn notifications_permission_value_parses_state() {
- let mut state = RadrootsAppState::default();
- assert!(app_state_notifications_permission_value(&state).is_none());
- state.notifications_permission = Some(String::from("granted"));
- assert_eq!(
- app_state_notifications_permission_value(&state),
- Some(RadrootsClientNotificationsPermission::Granted)
- );
- }
-
- #[test]
- fn notifications_permission_value_maps_idb_errors() {
- let datastore = RadrootsClientWebDatastore::new(None);
- let key_maps = app_key_maps_default();
- let err = futures::executor::block_on(app_state_set_notifications_permission_value(
- &datastore,
- &key_maps,
- RadrootsClientNotificationsPermission::Granted,
- ))
- .expect_err("idb undefined");
- assert_eq!(err, RadrootsAppInitError::Datastore(RadrootsClientDatastoreError::IdbUndefined));
- }
-
- #[test]
- fn set_notifications_permission_requires_state() {
- let datastore = TestDatastore {
- state: None,
- record: RefCell::new(None),
- };
- let key_maps = app_key_maps_default();
- let err = futures::executor::block_on(app_state_set_notifications_permission(
- &datastore,
- &key_maps,
- "granted",
- ))
- .expect_err("missing state");
- assert_eq!(err, RadrootsAppInitError::State(RadrootsAppStateError::Missing));
- }
-
- #[test]
- fn set_notifications_permission_updates_state() {
- let mut state = RadrootsAppState::default();
- state.active_key = "pub".to_string();
- state.eula_date = "2025-01-01T00:00:00Z".to_string();
- let datastore = TestDatastore {
- state: Some(state),
- record: RefCell::new(None),
- };
- let key_maps = app_key_maps_default();
- let updated = futures::executor::block_on(app_state_set_notifications_permission(
- &datastore,
- &key_maps,
- "granted",
- ))
- .expect("updated");
- assert_eq!(updated.notifications_permission.as_deref(), Some("granted"));
- let record = datastore.record.borrow();
- let record = record.as_ref().expect("record");
- assert_eq!(record.state.notifications_permission.as_deref(), Some("granted"));
- }
-
- #[test]
- fn create_state_writes_record() {
- let mut state = RadrootsAppState::default();
- state.active_key = "pub".to_string();
- state.eula_date = "2025-01-01T00:00:00Z".to_string();
- let datastore = TestDatastore {
- state: None,
- record: RefCell::new(None),
- };
- let key_maps = app_key_maps_default();
- let created = futures::executor::block_on(app_datastore_create_state(
- &datastore,
- &key_maps,
- &state,
- ))
- .expect("created");
- assert_eq!(created.active_key, "pub");
- let record = datastore.record.borrow();
- let record = record.as_ref().expect("record");
- assert_eq!(record.state.active_key, "pub");
- }
-
- #[test]
- fn create_state_reports_existing() {
- let mut state = RadrootsAppState::default();
- state.active_key = "pub".to_string();
- state.eula_date = "2025-01-01T00:00:00Z".to_string();
- let datastore = TestDatastore {
- state: Some(state.clone()),
- record: RefCell::new(None),
- };
- let key_maps = app_key_maps_default();
- let err = futures::executor::block_on(app_datastore_create_state(
- &datastore,
- &key_maps,
- &state,
- ))
- .expect_err("exists");
- assert_eq!(err, RadrootsAppInitError::State(RadrootsAppStateError::AlreadyExists));
- }
-
- #[test]
- fn update_state_requires_existing_record() {
- let mut state = RadrootsAppState::default();
- state.active_key = "pub".to_string();
- state.eula_date = "2025-01-01T00:00:00Z".to_string();
- let datastore = TestDatastore {
- state: None,
- record: RefCell::new(None),
- };
- let key_maps = app_key_maps_default();
- let err = futures::executor::block_on(app_datastore_update_state(
- &datastore,
- &key_maps,
- &state,
- ))
- .expect_err("missing");
- assert_eq!(err, RadrootsAppInitError::State(RadrootsAppStateError::Missing));
- }
-
- #[test]
- fn setup_draft_roundtrip() {
- let datastore = SetupDraftDatastore {
- draft: RefCell::new(None),
- };
- let key_maps = app_key_maps_default();
- let draft = RadrootsAppSetupDraft {
- nostr_public_key: Some("pub".to_string()),
- profile_name: Some("radroots".to_string()),
- role: Some(RadrootsAppRole::Individual),
- nip05_request: Some(true),
- };
- let stored = futures::executor::block_on(app_datastore_write_setup_draft(
- &datastore,
- &key_maps,
- &draft,
- ))
- .expect("store draft");
- assert_eq!(stored, draft);
- let loaded = futures::executor::block_on(app_datastore_read_setup_draft(
- &datastore,
- &key_maps,
- ))
- .expect("read draft");
- assert_eq!(loaded, Some(draft));
- let cleared = futures::executor::block_on(app_datastore_clear_setup_draft(
- &datastore,
- &key_maps,
- ));
- assert!(cleared.is_ok());
- let loaded = futures::executor::block_on(app_datastore_read_setup_draft(
- &datastore,
- &key_maps,
- ))
- .expect("read draft");
- assert!(loaded.is_none());
- }
-
- #[test]
- fn profile_seed_write_persists_data() {
- let datastore = ProfileSeedDatastore {
- profile: RefCell::new(None),
- };
- let key_maps = app_key_maps_default();
- let profile = RadrootsAppProfileSeed {
- public_key: "pub".to_string(),
- name: "radroots".to_string(),
- display_name: Some("Radroots".to_string()),
- nip05_request: true,
- };
- let stored = futures::executor::block_on(app_datastore_write_profile_seed(
- &datastore,
- &key_maps,
- &profile,
- ))
- .expect("profile seed");
- assert_eq!(stored, profile);
- let stored_profile = datastore.profile.borrow().clone();
- assert_eq!(stored_profile, Some(profile));
- }
-
- #[test]
- fn state_record_migrates_legacy_keys() {
- let key_maps = app_key_maps_default();
- let key_nostr = app_datastore_key_nostr_key(&key_maps).expect("nostr key");
- let key_eula = app_datastore_key_eula_date(&key_maps).expect("eula key");
- let mut values = BTreeMap::new();
- values.insert(key_nostr.to_string(), "pub".to_string());
- values.insert(key_eula.to_string(), "2025-01-01T00:00:00Z".to_string());
- let datastore = LegacyKeyDatastore {
- record: RefCell::new(None),
- values: RefCell::new(values),
- };
- let record = futures::executor::block_on(app_datastore_read_state_record(
- &datastore,
- &key_maps,
- ))
- .expect("record");
- assert_eq!(record.state.active_key, "pub");
- assert_eq!(record.state.eula_date, "2025-01-01T00:00:00Z");
- assert!(datastore.values.borrow().get(key_nostr).is_none());
- assert!(datastore.values.borrow().get(key_eula).is_none());
- }
-}
diff --git a/app/src/config.rs b/app/src/config.rs
@@ -1,560 +0,0 @@
-#![forbid(unsafe_code)]
-
-use std::collections::{BTreeMap, BTreeSet};
-
-use radroots_app_core::idb::{
- RadrootsClientIdbConfig,
- IDB_CONFIG_DATASTORE,
- IDB_CONFIG_KEYSTORE_NOSTR,
-};
-
-pub type RadrootsAppDatastoreKeyParam = fn(&str) -> String;
-pub type RadrootsAppDatastoreKeyMap = BTreeMap<&'static str, &'static str>;
-pub type RadrootsAppDatastoreKeyParamMap = BTreeMap<&'static str, RadrootsAppDatastoreKeyParam>;
-pub type RadrootsAppDatastoreKeyObjMap = BTreeMap<&'static str, &'static str>;
-pub type RadrootsAppKeystoreKeyMap = BTreeMap<&'static str, &'static str>;
-
-pub const APP_DATASTORE_KEY_NOSTR_KEY: &str = "nostr:key";
-pub const APP_DATASTORE_KEY_EULA_DATE: &str = "app:eula:date";
-pub const APP_DATASTORE_KEY_SETUP_LOCK: &str = "app:setup:lock";
-pub const APP_DATASTORE_KEY_OBJ_STATE: &str = "app:data";
-pub const APP_DATASTORE_KEY_OBJ_SETUP_DRAFT: &str = "setup:draft";
-pub const APP_DATASTORE_KEY_OBJ_CONFIG: &str = "app:config";
-pub const APP_DATASTORE_KEY_LOG_ENTRY: &str = "log:entry";
-pub const APP_KEYSTORE_KEY_NOSTR_DEFAULT: &str = "nostr:default";
-
-pub fn app_datastore_param_nostr_profile(public_key: &str) -> String {
- format!("nostr:{public_key}:profile")
-}
-
-pub fn app_datastore_param_radroots_profile(public_key: &str) -> String {
- format!("radroots:{public_key}:profile")
-}
-
-pub fn app_datastore_param_log_entry(entry_id: &str) -> String {
- format!("{APP_DATASTORE_KEY_LOG_ENTRY}:{entry_id}")
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsAppKeyMapConfig {
- pub key_map: RadrootsAppDatastoreKeyMap,
- pub param_map: RadrootsAppDatastoreKeyParamMap,
- pub obj_map: RadrootsAppDatastoreKeyObjMap,
-}
-
-impl RadrootsAppKeyMapConfig {
- pub fn empty() -> Self {
- Self {
- key_map: BTreeMap::new(),
- param_map: BTreeMap::new(),
- obj_map: BTreeMap::new(),
- }
- }
-}
-
-pub fn app_key_maps_default() -> RadrootsAppKeyMapConfig {
- let mut key_map = BTreeMap::new();
- key_map.insert("nostr_key", APP_DATASTORE_KEY_NOSTR_KEY);
- key_map.insert("eula_date", APP_DATASTORE_KEY_EULA_DATE);
- key_map.insert("setup_lock", APP_DATASTORE_KEY_SETUP_LOCK);
- let mut param_map = BTreeMap::new();
- param_map.insert("nostr_profile", app_datastore_param_nostr_profile as RadrootsAppDatastoreKeyParam);
- param_map.insert(
- "radroots_profile",
- app_datastore_param_radroots_profile as RadrootsAppDatastoreKeyParam,
- );
- param_map.insert("log_entry", app_datastore_param_log_entry as RadrootsAppDatastoreKeyParam);
- let mut obj_map = BTreeMap::new();
- obj_map.insert("state", APP_DATASTORE_KEY_OBJ_STATE);
- obj_map.insert("setup_draft", APP_DATASTORE_KEY_OBJ_SETUP_DRAFT);
- obj_map.insert("config", APP_DATASTORE_KEY_OBJ_CONFIG);
- RadrootsAppKeyMapConfig {
- key_map,
- param_map,
- obj_map,
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppConfigError {
- MissingKeyMap(&'static str),
- MissingParamMap(&'static str),
- MissingObjMap(&'static str),
- MissingKeystoreKeyMap(&'static str),
-}
-
-pub type RadrootsAppConfigResult<T> = Result<T, RadrootsAppConfigError>;
-
-impl RadrootsAppConfigError {
- pub const fn message(&self) -> &'static str {
- match self {
- RadrootsAppConfigError::MissingKeyMap(_) => "error.app.config.key_map_missing",
- RadrootsAppConfigError::MissingParamMap(_) => "error.app.config.param_map_missing",
- RadrootsAppConfigError::MissingObjMap(_) => "error.app.config.obj_map_missing",
- RadrootsAppConfigError::MissingKeystoreKeyMap(_) => "error.app.config.keystore_map_missing",
- }
- }
-}
-
-impl std::fmt::Display for RadrootsAppConfigError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_str(self.message())
- }
-}
-
-impl std::error::Error for RadrootsAppConfigError {}
-
-pub fn app_key_maps_validate(config: &RadrootsAppKeyMapConfig) -> RadrootsAppConfigResult<()> {
- if !config.key_map.contains_key("nostr_key") {
- return Err(RadrootsAppConfigError::MissingKeyMap("nostr_key"));
- }
- if !config.key_map.contains_key("eula_date") {
- return Err(RadrootsAppConfigError::MissingKeyMap("eula_date"));
- }
- if !config.key_map.contains_key("setup_lock") {
- return Err(RadrootsAppConfigError::MissingKeyMap("setup_lock"));
- }
- if !config.param_map.contains_key("nostr_profile") {
- return Err(RadrootsAppConfigError::MissingParamMap("nostr_profile"));
- }
- if !config.param_map.contains_key("radroots_profile") {
- return Err(RadrootsAppConfigError::MissingParamMap("radroots_profile"));
- }
- if !config.param_map.contains_key("log_entry") {
- return Err(RadrootsAppConfigError::MissingParamMap("log_entry"));
- }
- if !config.obj_map.contains_key("state") {
- return Err(RadrootsAppConfigError::MissingObjMap("state"));
- }
- if !config.obj_map.contains_key("setup_draft") {
- return Err(RadrootsAppConfigError::MissingObjMap("setup_draft"));
- }
- if !config.obj_map.contains_key("config") {
- return Err(RadrootsAppConfigError::MissingObjMap("config"));
- }
- Ok(())
-}
-
-pub fn app_keystore_key_maps_validate(config: &RadrootsAppKeystoreKeyMap) -> RadrootsAppConfigResult<()> {
- if !config.contains_key("nostr_default") {
- return Err(RadrootsAppConfigError::MissingKeystoreKeyMap("nostr_default"));
- }
- Ok(())
-}
-
-pub fn app_datastore_key(config: &RadrootsAppKeyMapConfig, key: &'static str) -> RadrootsAppConfigResult<&'static str> {
- config
- .key_map
- .get(key)
- .copied()
- .ok_or(RadrootsAppConfigError::MissingKeyMap(key))
-}
-
-pub fn app_datastore_obj_key(
- config: &RadrootsAppKeyMapConfig,
- key: &'static str,
-) -> RadrootsAppConfigResult<&'static str> {
- config
- .obj_map
- .get(key)
- .copied()
- .ok_or(RadrootsAppConfigError::MissingObjMap(key))
-}
-
-pub fn app_datastore_param_key(
- config: &RadrootsAppKeyMapConfig,
- key: &'static str,
-) -> RadrootsAppConfigResult<RadrootsAppDatastoreKeyParam> {
- config
- .param_map
- .get(key)
- .copied()
- .ok_or(RadrootsAppConfigError::MissingParamMap(key))
-}
-
-pub fn app_datastore_key_nostr_key(config: &RadrootsAppKeyMapConfig) -> RadrootsAppConfigResult<&'static str> {
- app_datastore_key(config, "nostr_key")
-}
-
-pub fn app_datastore_key_eula_date(config: &RadrootsAppKeyMapConfig) -> RadrootsAppConfigResult<&'static str> {
- app_datastore_key(config, "eula_date")
-}
-
-pub fn app_datastore_key_setup_lock(config: &RadrootsAppKeyMapConfig) -> RadrootsAppConfigResult<&'static str> {
- app_datastore_key(config, "setup_lock")
-}
-
-pub fn app_datastore_obj_key_state(config: &RadrootsAppKeyMapConfig) -> RadrootsAppConfigResult<&'static str> {
- app_datastore_obj_key(config, "state")
-}
-
-pub fn app_datastore_obj_key_setup_draft(
- config: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppConfigResult<&'static str> {
- app_datastore_obj_key(config, "setup_draft")
-}
-
-pub fn app_datastore_obj_key_config(
- config: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppConfigResult<&'static str> {
- app_datastore_obj_key(config, "config")
-}
-
-pub fn app_keystore_key(
- config: &RadrootsAppKeystoreKeyMap,
- key: &'static str,
-) -> RadrootsAppConfigResult<&'static str> {
- config
- .get(key)
- .copied()
- .ok_or(RadrootsAppConfigError::MissingKeystoreKeyMap(key))
-}
-
-pub fn app_keystore_key_nostr_default(config: &RadrootsAppKeystoreKeyMap) -> RadrootsAppConfigResult<&'static str> {
- app_keystore_key(config, "nostr_default")
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsAppKeystoreConfig {
- pub nostr_store: RadrootsClientIdbConfig,
- pub key_map: RadrootsAppKeystoreKeyMap,
-}
-
-impl RadrootsAppKeystoreConfig {
- pub fn default_config() -> Self {
- Self {
- nostr_store: IDB_CONFIG_KEYSTORE_NOSTR,
- key_map: app_keystore_key_maps_default(),
- }
- }
-}
-
-pub fn app_keystore_key_maps_default() -> RadrootsAppKeystoreKeyMap {
- let mut map = BTreeMap::new();
- map.insert("nostr_default", APP_KEYSTORE_KEY_NOSTR_DEFAULT);
- map
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsAppDatastoreConfig {
- pub idb_config: RadrootsClientIdbConfig,
- pub key_maps: RadrootsAppKeyMapConfig,
-}
-
-impl RadrootsAppDatastoreConfig {
- pub fn default_config(key_maps: RadrootsAppKeyMapConfig) -> Self {
- Self {
- idb_config: IDB_CONFIG_DATASTORE,
- key_maps,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Default)]
-pub struct RadrootsAppAssetConfig {
- pub sql_wasm_url: Option<String>,
- pub geocoder_db_url: Option<String>,
-}
-
-pub fn app_assets_sql_wasm_url(config: &RadrootsAppConfig) -> Option<&str> {
- config.assets.sql_wasm_url.as_deref()
-}
-
-pub fn app_assets_geocoder_db_url(config: &RadrootsAppConfig) -> Option<&str> {
- config.assets.geocoder_db_url.as_deref()
-}
-
-const APP_DEFAULT_RELAY_FALLBACK: &str = "ws://localhost:8080";
-
-pub fn app_default_relays() -> Vec<String> {
- let raw = option_env!("RADROOTS_DEFAULT_RELAYS")
- .or(option_env!("RADROOTS_RELAY"))
- .unwrap_or(APP_DEFAULT_RELAY_FALLBACK);
- let mut seen = BTreeSet::new();
- let mut relays = Vec::new();
- for relay in raw.split(',') {
- let relay = relay.trim();
- if relay.is_empty() || !seen.insert(relay.to_string()) {
- continue;
- }
- relays.push(relay.to_string());
- }
- if relays.is_empty() {
- relays.push(APP_DEFAULT_RELAY_FALLBACK.to_string());
- }
- relays
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsAppConfig {
- pub datastore: RadrootsAppDatastoreConfig,
- pub keystore: RadrootsAppKeystoreConfig,
- pub assets: RadrootsAppAssetConfig,
-}
-
-impl RadrootsAppConfig {
- pub fn empty() -> Self {
- let key_maps = RadrootsAppKeyMapConfig::empty();
- Self {
- datastore: RadrootsAppDatastoreConfig::default_config(key_maps),
- keystore: RadrootsAppKeystoreConfig::default_config(),
- assets: RadrootsAppAssetConfig::default(),
- }
- }
-
- pub fn from_key_maps(key_maps: RadrootsAppKeyMapConfig) -> Self {
- Self {
- datastore: RadrootsAppDatastoreConfig::default_config(key_maps),
- keystore: RadrootsAppKeystoreConfig::default_config(),
- assets: RadrootsAppAssetConfig::default(),
- }
- }
-
- pub fn validate(&self) -> RadrootsAppConfigResult<()> {
- app_key_maps_validate(&self.datastore.key_maps)?;
- app_keystore_key_maps_validate(&self.keystore.key_map)?;
- Ok(())
- }
-}
-
-pub fn app_config_default() -> RadrootsAppConfig {
- RadrootsAppConfig::from_key_maps(app_key_maps_default())
-}
-
-pub fn app_config_from_env() -> RadrootsAppConfig {
- app_config_default()
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- app_config_default,
- app_config_from_env,
- app_default_relays,
- app_datastore_param_nostr_profile,
- app_datastore_param_log_entry,
- app_datastore_param_radroots_profile,
- app_datastore_key_eula_date,
- app_datastore_key_nostr_key,
- app_datastore_key_setup_lock,
- app_datastore_obj_key_state,
- app_datastore_obj_key_setup_draft,
- app_datastore_obj_key_config,
- app_key_maps_validate,
- app_keystore_key_maps_default,
- app_keystore_key_maps_validate,
- app_keystore_key_nostr_default,
- app_keystore_key,
- app_datastore_param_key,
- app_assets_geocoder_db_url,
- app_assets_sql_wasm_url,
- RadrootsAppAssetConfig,
- RadrootsAppConfig,
- RadrootsAppConfigError,
- RadrootsAppDatastoreConfig,
- RadrootsAppDatastoreKeyParam,
- RadrootsAppKeyMapConfig,
- RadrootsAppKeystoreConfig,
- RadrootsAppKeystoreKeyMap,
- APP_DATASTORE_KEY_EULA_DATE,
- APP_DATASTORE_KEY_NOSTR_KEY,
- APP_DATASTORE_KEY_OBJ_STATE,
- APP_DATASTORE_KEY_OBJ_SETUP_DRAFT,
- APP_DATASTORE_KEY_OBJ_CONFIG,
- APP_DATASTORE_KEY_LOG_ENTRY,
- APP_DATASTORE_KEY_SETUP_LOCK,
- APP_KEYSTORE_KEY_NOSTR_DEFAULT,
- };
- use radroots_app_core::idb::{IDB_CONFIG_DATASTORE, IDB_CONFIG_KEYSTORE_NOSTR};
- use std::collections::BTreeMap;
-
- #[test]
- fn key_map_config_defaults_empty() {
- let config = RadrootsAppKeyMapConfig::empty();
- assert!(config.key_map.is_empty());
- assert!(config.param_map.is_empty());
- assert!(config.obj_map.is_empty());
- }
-
- #[test]
- fn app_config_defaults_empty() {
- let config = RadrootsAppConfig::empty();
- assert!(config.datastore.key_maps.key_map.is_empty());
- }
-
- #[test]
- fn default_relays_are_non_empty() {
- let relays = app_default_relays();
- assert!(!relays.is_empty());
- assert!(relays.iter().all(|relay| !relay.trim().is_empty()));
- }
-
- #[test]
- fn app_config_helpers_return_defaults() {
- let config = app_config_default();
- let from_env = app_config_from_env();
- assert_eq!(config, from_env);
- assert_eq!(
- config.datastore.key_maps.key_map.get("nostr_key"),
- Some(&APP_DATASTORE_KEY_NOSTR_KEY)
- );
- }
-
- #[test]
- fn app_config_validate_uses_key_map_rules() {
- let config = app_config_default();
- assert!(config.validate().is_ok());
- let empty = RadrootsAppConfig::empty();
- assert!(empty.validate().is_err());
- }
-
- #[test]
- fn keystore_config_defaults_to_nostr_store() {
- let config = RadrootsAppKeystoreConfig::default_config();
- assert_eq!(config.nostr_store, IDB_CONFIG_KEYSTORE_NOSTR);
- }
-
- #[test]
- fn keystore_key_maps_defaults_empty() {
- let map = app_keystore_key_maps_default();
- assert_eq!(
- map.get("nostr_default"),
- Some(&APP_KEYSTORE_KEY_NOSTR_DEFAULT)
- );
- }
-
- #[test]
- fn asset_config_defaults_empty() {
- let config = RadrootsAppAssetConfig::default();
- assert!(config.sql_wasm_url.is_none());
- assert!(config.geocoder_db_url.is_none());
- }
-
- #[test]
- fn datastore_config_defaults_to_idb_store() {
- let key_maps = RadrootsAppKeyMapConfig::empty();
- let config = RadrootsAppDatastoreConfig::default_config(key_maps);
- assert_eq!(config.idb_config, IDB_CONFIG_DATASTORE);
- assert!(config.key_maps.key_map.is_empty());
- }
-
- #[test]
- fn key_map_defaults_include_fixture_keys() {
- let config = super::app_key_maps_default();
- assert_eq!(
- config.key_map.get("nostr_key"),
- Some(&APP_DATASTORE_KEY_NOSTR_KEY)
- );
- assert_eq!(
- config.key_map.get("eula_date"),
- Some(&APP_DATASTORE_KEY_EULA_DATE)
- );
- assert_eq!(
- config.key_map.get("setup_lock"),
- Some(&APP_DATASTORE_KEY_SETUP_LOCK)
- );
- assert_eq!(
- config.obj_map.get("state"),
- Some(&APP_DATASTORE_KEY_OBJ_STATE)
- );
- assert_eq!(
- config.obj_map.get("setup_draft"),
- Some(&APP_DATASTORE_KEY_OBJ_SETUP_DRAFT)
- );
- assert_eq!(
- config.obj_map.get("config"),
- Some(&APP_DATASTORE_KEY_OBJ_CONFIG)
- );
- assert_eq!(app_datastore_param_nostr_profile("abc"), "nostr:abc:profile");
- assert_eq!(
- app_datastore_param_log_entry("entry"),
- format!("{APP_DATASTORE_KEY_LOG_ENTRY}:entry")
- );
- }
-
- #[test]
- fn key_map_validation_requires_expected_keys() {
- let config = super::app_key_maps_default();
- assert!(app_key_maps_validate(&config).is_ok());
- let mut missing = RadrootsAppKeyMapConfig::empty();
- missing.key_map.insert("nostr_key", APP_DATASTORE_KEY_NOSTR_KEY);
- let err = app_key_maps_validate(&missing).expect_err("missing keys");
- assert_eq!(err, RadrootsAppConfigError::MissingKeyMap("eula_date"));
- missing.key_map.insert("eula_date", APP_DATASTORE_KEY_EULA_DATE);
- let err = app_key_maps_validate(&missing).expect_err("missing lock");
- assert_eq!(err, RadrootsAppConfigError::MissingKeyMap("setup_lock"));
- missing.key_map.insert("setup_lock", APP_DATASTORE_KEY_SETUP_LOCK);
- missing.obj_map.insert("state", APP_DATASTORE_KEY_OBJ_STATE);
- missing.param_map.insert("nostr_profile", app_datastore_param_nostr_profile as RadrootsAppDatastoreKeyParam);
- missing.param_map.insert("radroots_profile", app_datastore_param_radroots_profile as RadrootsAppDatastoreKeyParam);
- missing.param_map.insert("log_entry", app_datastore_param_log_entry as RadrootsAppDatastoreKeyParam);
- let err = app_key_maps_validate(&missing).expect_err("missing draft");
- assert_eq!(err, RadrootsAppConfigError::MissingObjMap("setup_draft"));
- missing.obj_map.insert("setup_draft", APP_DATASTORE_KEY_OBJ_SETUP_DRAFT);
- let err = app_key_maps_validate(&missing).expect_err("missing config");
- assert_eq!(err, RadrootsAppConfigError::MissingObjMap("config"));
- }
-
- #[test]
- fn keystore_map_validation_requires_expected_keys() {
- let map = app_keystore_key_maps_default();
- assert!(app_keystore_key_maps_validate(&map).is_ok());
- let empty: RadrootsAppKeystoreKeyMap = BTreeMap::new();
- let err = app_keystore_key_maps_validate(&empty)
- .expect_err("missing keys");
- assert_eq!(err, RadrootsAppConfigError::MissingKeystoreKeyMap("nostr_default"));
- }
-
- #[test]
- fn datastore_key_accessors_read_defaults() {
- let config = super::app_key_maps_default();
- assert_eq!(
- app_datastore_key_nostr_key(&config).expect("nostr key"),
- APP_DATASTORE_KEY_NOSTR_KEY
- );
- assert_eq!(
- app_datastore_key_eula_date(&config).expect("eula key"),
- APP_DATASTORE_KEY_EULA_DATE
- );
- assert_eq!(
- app_datastore_key_setup_lock(&config).expect("setup lock key"),
- APP_DATASTORE_KEY_SETUP_LOCK
- );
- assert_eq!(
- app_datastore_obj_key_state(&config).expect("state key"),
- APP_DATASTORE_KEY_OBJ_STATE
- );
- assert_eq!(
- app_datastore_obj_key_setup_draft(&config).expect("draft key"),
- APP_DATASTORE_KEY_OBJ_SETUP_DRAFT
- );
- assert_eq!(
- app_datastore_obj_key_config(&config).expect("config key"),
- APP_DATASTORE_KEY_OBJ_CONFIG
- );
- let nostr_param = app_datastore_param_key(&config, "nostr_profile").expect("param");
- assert_eq!(nostr_param("abc"), "nostr:abc:profile");
- let log_param = app_datastore_param_key(&config, "log_entry").expect("param");
- assert_eq!(log_param("entry"), format!("{APP_DATASTORE_KEY_LOG_ENTRY}:entry"));
- }
-
- #[test]
- fn keystore_key_accessors_read_defaults() {
- let map = app_keystore_key_maps_default();
- assert_eq!(
- app_keystore_key_nostr_default(&map).expect("nostr default"),
- APP_KEYSTORE_KEY_NOSTR_DEFAULT
- );
- assert_eq!(
- app_keystore_key(&map, "nostr_default").expect("nostr default"),
- APP_KEYSTORE_KEY_NOSTR_DEFAULT
- );
- }
-
- #[test]
- fn asset_accessors_read_defaults() {
- let config = app_config_default();
- assert_eq!(app_assets_sql_wasm_url(&config), None);
- assert_eq!(app_assets_geocoder_db_url(&config), None);
- }
-}
diff --git a/app/src/config_flow.rs b/app/src/config_flow.rs
@@ -1,308 +0,0 @@
-#![forbid(unsafe_code)]
-
-use crate::{
- RadrootsAppConfigBusiness,
- RadrootsAppConfigData,
- RadrootsAppConfigFarmer,
- RadrootsAppConfigIndividual,
- RadrootsAppConfigPreferences,
- RadrootsAppConfigProfile,
- RadrootsAppRole,
-};
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppConfigStep {
- Profile,
- Role,
- Preferences,
-}
-
-pub const fn app_config_step_default() -> RadrootsAppConfigStep {
- RadrootsAppConfigStep::Profile
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsAppConfigFlowDraft {
- pub step: RadrootsAppConfigStep,
- pub profile_name: String,
- pub profile_location: String,
- pub role: Option<RadrootsAppRole>,
- pub farmer_farm_name: String,
- pub farmer_location: String,
- pub farmer_products: Vec<String>,
- pub individual_name: String,
- pub individual_location: String,
- pub individual_products: Vec<String>,
- pub business_name: String,
- pub business_location: String,
- pub business_operations: String,
- pub notifications_orders: bool,
- pub notifications_messages: bool,
-}
-
-impl Default for RadrootsAppConfigFlowDraft {
- fn default() -> Self {
- Self {
- step: app_config_step_default(),
- profile_name: String::new(),
- profile_location: String::new(),
- role: None,
- farmer_farm_name: String::new(),
- farmer_location: String::new(),
- farmer_products: Vec::new(),
- individual_name: String::new(),
- individual_location: String::new(),
- individual_products: Vec::new(),
- business_name: String::new(),
- business_location: String::new(),
- business_operations: String::new(),
- notifications_orders: true,
- notifications_messages: true,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsAppConfigFlowValidation {
- pub can_continue: bool,
- pub can_back: bool,
- pub next_step: RadrootsAppConfigStep,
- pub prev_step: RadrootsAppConfigStep,
-}
-
-pub fn app_config_flow_next_step(draft: &RadrootsAppConfigFlowDraft) -> RadrootsAppConfigStep {
- match draft.step {
- RadrootsAppConfigStep::Profile => RadrootsAppConfigStep::Role,
- RadrootsAppConfigStep::Role => RadrootsAppConfigStep::Preferences,
- RadrootsAppConfigStep::Preferences => RadrootsAppConfigStep::Preferences,
- }
-}
-
-pub fn app_config_flow_prev_step(draft: &RadrootsAppConfigFlowDraft) -> RadrootsAppConfigStep {
- match draft.step {
- RadrootsAppConfigStep::Profile => RadrootsAppConfigStep::Profile,
- RadrootsAppConfigStep::Role => RadrootsAppConfigStep::Profile,
- RadrootsAppConfigStep::Preferences => RadrootsAppConfigStep::Role,
- }
-}
-
-fn has_text(value: &str) -> bool {
- !value.trim().is_empty()
-}
-
-fn has_items(values: &[String]) -> bool {
- values.iter().any(|value| !value.trim().is_empty())
-}
-
-fn normalize_text(value: &str) -> Option<String> {
- let trimmed = value.trim();
- if trimmed.is_empty() {
- None
- } else {
- Some(trimmed.to_string())
- }
-}
-
-fn normalize_items(values: &[String]) -> Vec<String> {
- let mut out: Vec<String> = Vec::new();
- for value in values {
- let trimmed = value.trim();
- if trimmed.is_empty() {
- continue;
- }
- if out.iter().any(|item| item.eq_ignore_ascii_case(trimmed)) {
- continue;
- }
- out.push(trimmed.to_string());
- }
- out
-}
-
-fn role_step_valid(draft: &RadrootsAppConfigFlowDraft) -> bool {
- match draft.role {
- Some(RadrootsAppRole::Farm) => {
- has_text(&draft.farmer_farm_name)
- && has_text(&draft.farmer_location)
- && has_items(&draft.farmer_products)
- }
- Some(RadrootsAppRole::Individual) => {
- has_items(&draft.individual_products)
- }
- Some(RadrootsAppRole::Business) => {
- has_text(&draft.business_name)
- && has_text(&draft.business_location)
- && has_text(&draft.business_operations)
- }
- None => false,
- }
-}
-
-pub fn app_config_flow_validate(draft: &RadrootsAppConfigFlowDraft) -> RadrootsAppConfigFlowValidation {
- let can_continue = match draft.step {
- RadrootsAppConfigStep::Profile => {
- has_text(&draft.profile_name) && has_text(&draft.profile_location)
- }
- RadrootsAppConfigStep::Role => role_step_valid(draft),
- RadrootsAppConfigStep::Preferences => true,
- };
- let can_back = !matches!(draft.step, RadrootsAppConfigStep::Profile);
- RadrootsAppConfigFlowValidation {
- can_continue,
- can_back,
- next_step: app_config_flow_next_step(draft),
- prev_step: app_config_flow_prev_step(draft),
- }
-}
-
-pub fn app_config_flow_build_config(
- draft: &RadrootsAppConfigFlowDraft,
-) -> Option<RadrootsAppConfigData> {
- let profile_name = normalize_text(&draft.profile_name)?;
- let profile_location = normalize_text(&draft.profile_location)?;
- let role = draft.role?;
- let profile = RadrootsAppConfigProfile {
- name: profile_name,
- location: profile_location,
- };
- let preferences = RadrootsAppConfigPreferences {
- notifications_orders: draft.notifications_orders,
- notifications_messages: draft.notifications_messages,
- payment_method: None,
- };
- match role {
- RadrootsAppRole::Farm => {
- let farm_name = normalize_text(&draft.farmer_farm_name)?;
- let farm_location = normalize_text(&draft.farmer_location)?;
- let products = normalize_items(&draft.farmer_products);
- if products.is_empty() {
- return None;
- }
- Some(RadrootsAppConfigData {
- profile: profile.clone(),
- role,
- farmer: Some(RadrootsAppConfigFarmer {
- farm_name,
- farm_location,
- products_growing: products,
- }),
- business: None,
- individual: None,
- preferences,
- })
- }
- RadrootsAppRole::Individual => {
- let products = normalize_items(&draft.individual_products);
- if products.is_empty() {
- return None;
- }
- Some(RadrootsAppConfigData {
- profile: profile.clone(),
- role,
- farmer: None,
- business: None,
- individual: Some(RadrootsAppConfigIndividual {
- name: profile.name.clone(),
- location: profile.location.clone(),
- products_interested: products,
- }),
- preferences,
- })
- }
- RadrootsAppRole::Business => {
- let name = normalize_text(&draft.business_name)?;
- let location = normalize_text(&draft.business_location)?;
- let operations = normalize_text(&draft.business_operations)?;
- Some(RadrootsAppConfigData {
- profile: profile.clone(),
- role,
- farmer: None,
- business: Some(RadrootsAppConfigBusiness {
- name,
- location,
- operations,
- }),
- individual: None,
- preferences,
- })
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- app_config_flow_build_config,
- app_config_flow_next_step,
- app_config_flow_prev_step,
- app_config_flow_validate,
- RadrootsAppConfigFlowDraft,
- RadrootsAppConfigStep,
- };
- use crate::RadrootsAppRole;
-
- #[test]
- fn flow_defaults_to_profile() {
- let draft = RadrootsAppConfigFlowDraft::default();
- assert_eq!(draft.step, RadrootsAppConfigStep::Profile);
- assert!(draft.notifications_orders);
- assert!(draft.notifications_messages);
- }
-
- #[test]
- fn flow_step_transitions() {
- let mut draft = RadrootsAppConfigFlowDraft::default();
- assert_eq!(app_config_flow_next_step(&draft), RadrootsAppConfigStep::Role);
- draft.step = RadrootsAppConfigStep::Role;
- assert_eq!(app_config_flow_next_step(&draft), RadrootsAppConfigStep::Preferences);
- draft.step = RadrootsAppConfigStep::Preferences;
- assert_eq!(app_config_flow_next_step(&draft), RadrootsAppConfigStep::Preferences);
- assert_eq!(app_config_flow_prev_step(&draft), RadrootsAppConfigStep::Role);
- }
-
- #[test]
- fn flow_validation_requires_profile_fields() {
- let draft = RadrootsAppConfigFlowDraft::default();
- let validation = app_config_flow_validate(&draft);
- assert!(!validation.can_continue);
- }
-
- #[test]
- fn flow_validation_requires_role_fields() {
- let mut draft = RadrootsAppConfigFlowDraft::default();
- draft.step = RadrootsAppConfigStep::Role;
- draft.role = Some(RadrootsAppRole::Farm);
- draft.farmer_farm_name = String::from("Radroots Farm");
- draft.farmer_location = String::from("Valley");
- let validation = app_config_flow_validate(&draft);
- assert!(!validation.can_continue);
- draft.farmer_products = vec![String::from("tomatoes")];
- let validation = app_config_flow_validate(&draft);
- assert!(validation.can_continue);
- }
-
- #[test]
- fn flow_build_config_requires_role() {
- let draft = RadrootsAppConfigFlowDraft::default();
- assert!(app_config_flow_build_config(&draft).is_none());
- }
-
- #[test]
- fn flow_build_config_maps_farm_values() {
- let mut draft = RadrootsAppConfigFlowDraft::default();
- draft.profile_name = String::from("Radroots");
- draft.profile_location = String::from("Valley");
- draft.role = Some(RadrootsAppRole::Farm);
- draft.farmer_farm_name = String::from("Willow Farm");
- draft.farmer_location = String::from("Valley");
- draft.farmer_products = vec![
- String::from("tomatoes"),
- String::from(" Tomatoes "),
- String::from(" "),
- ];
- let config = app_config_flow_build_config(&draft).expect("config");
- let farmer = config.farmer.expect("farmer");
- assert_eq!(config.profile.name, "Radroots");
- assert_eq!(config.profile.location, "Valley");
- assert_eq!(farmer.products_growing, vec![String::from("tomatoes")]);
- }
-}
diff --git a/app/src/configuration.rs b/app/src/configuration.rs
@@ -1,461 +0,0 @@
-#![forbid(unsafe_code)]
-
-use serde::{Deserialize, Serialize};
-use sha2::{Digest, Sha256};
-
-use radroots_app_core::datastore::{RadrootsClientDatastore, RadrootsClientDatastoreError};
-
-use crate::{
- app_datastore_obj_key_config,
- app_state_timestamp_ms,
- RadrootsAppConfigError,
- RadrootsAppKeyMapConfig,
- RadrootsAppLoggableError,
- RadrootsAppRole,
-};
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsAppConfigProfile {
- pub name: String,
- pub location: String,
-}
-
-impl Default for RadrootsAppConfigProfile {
- fn default() -> Self {
- Self {
- name: String::new(),
- location: String::new(),
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsAppConfigPreferences {
- pub notifications_orders: bool,
- pub notifications_messages: bool,
- pub payment_method: Option<String>,
-}
-
-impl Default for RadrootsAppConfigPreferences {
- fn default() -> Self {
- Self {
- notifications_orders: true,
- notifications_messages: true,
- payment_method: None,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsAppConfigFarmer {
- pub farm_name: String,
- pub farm_location: String,
- pub products_growing: Vec<String>,
-}
-
-impl Default for RadrootsAppConfigFarmer {
- fn default() -> Self {
- Self {
- farm_name: String::new(),
- farm_location: String::new(),
- products_growing: Vec::new(),
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsAppConfigIndividual {
- pub name: String,
- pub location: String,
- pub products_interested: Vec<String>,
-}
-
-impl Default for RadrootsAppConfigIndividual {
- fn default() -> Self {
- Self {
- name: String::new(),
- location: String::new(),
- products_interested: Vec::new(),
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsAppConfigBusiness {
- pub name: String,
- pub location: String,
- pub operations: String,
-}
-
-impl Default for RadrootsAppConfigBusiness {
- fn default() -> Self {
- Self {
- name: String::new(),
- location: String::new(),
- operations: String::new(),
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsAppConfigData {
- pub profile: RadrootsAppConfigProfile,
- pub role: RadrootsAppRole,
- pub farmer: Option<RadrootsAppConfigFarmer>,
- pub business: Option<RadrootsAppConfigBusiness>,
- pub individual: Option<RadrootsAppConfigIndividual>,
- pub preferences: RadrootsAppConfigPreferences,
-}
-
-impl Default for RadrootsAppConfigData {
- fn default() -> Self {
- Self {
- profile: RadrootsAppConfigProfile::default(),
- role: RadrootsAppRole::default(),
- farmer: None,
- business: None,
- individual: None,
- preferences: RadrootsAppConfigPreferences::default(),
- }
- }
-}
-
-pub const APP_CONFIG_SCHEMA_VERSION: u32 = 1;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppConfigStatus {
- Unknown,
- Required,
- Configured,
- Corrupt,
-}
-
-impl Default for RadrootsAppConfigStatus {
- fn default() -> Self {
- RadrootsAppConfigStatus::Unknown
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub struct RadrootsAppConfigGate {
- pub show_config: bool,
- pub show_app: bool,
-}
-
-impl RadrootsAppConfigGate {
- pub const fn splash() -> Self {
- Self {
- show_config: false,
- show_app: false,
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppConfigRecordError {
- Missing,
- Corrupt,
- InvalidChecksum,
- UnsupportedVersion(u32),
- AlreadyExists,
-}
-
-impl RadrootsAppConfigRecordError {
- pub const fn message(&self) -> &'static str {
- match self {
- RadrootsAppConfigRecordError::Missing => "error.app.config.missing",
- RadrootsAppConfigRecordError::Corrupt => "error.app.config.corrupt",
- RadrootsAppConfigRecordError::InvalidChecksum => "error.app.config.checksum_invalid",
- RadrootsAppConfigRecordError::UnsupportedVersion(_) => "error.app.config.schema_unsupported",
- RadrootsAppConfigRecordError::AlreadyExists => "error.app.config.already_exists",
- }
- }
-}
-
-impl std::fmt::Display for RadrootsAppConfigRecordError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_str(self.message())
- }
-}
-
-impl std::error::Error for RadrootsAppConfigRecordError {}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsAppConfigRecord {
- pub schema_version: u32,
- pub revision: u64,
- pub updated_at_ms: i64,
- pub checksum: String,
- pub config: RadrootsAppConfigData,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-struct RadrootsAppConfigChecksumPayload {
- schema_version: u32,
- revision: u64,
- updated_at_ms: i64,
- config: RadrootsAppConfigData,
-}
-
-fn app_config_record_checksum(payload: &RadrootsAppConfigChecksumPayload) -> String {
- let serialized = serde_json::to_vec(payload).unwrap_or_else(|_| Vec::new());
- let hash = Sha256::digest(&serialized);
- hex::encode(hash)
-}
-
-pub fn app_config_record_new(
- config: RadrootsAppConfigData,
- revision: u64,
- updated_at_ms: i64,
-) -> RadrootsAppConfigRecord {
- let payload = RadrootsAppConfigChecksumPayload {
- schema_version: APP_CONFIG_SCHEMA_VERSION,
- revision,
- updated_at_ms,
- config: config.clone(),
- };
- let checksum = app_config_record_checksum(&payload);
- RadrootsAppConfigRecord {
- schema_version: APP_CONFIG_SCHEMA_VERSION,
- revision,
- updated_at_ms,
- checksum,
- config,
- }
-}
-
-pub fn app_config_record_validate(
- record: &RadrootsAppConfigRecord,
-) -> Result<(), RadrootsAppConfigRecordError> {
- if record.schema_version != APP_CONFIG_SCHEMA_VERSION {
- return Err(RadrootsAppConfigRecordError::UnsupportedVersion(
- record.schema_version,
- ));
- }
- let payload = RadrootsAppConfigChecksumPayload {
- schema_version: record.schema_version,
- revision: record.revision,
- updated_at_ms: record.updated_at_ms,
- config: record.config.clone(),
- };
- let expected = app_config_record_checksum(&payload);
- if record.checksum != expected {
- return Err(RadrootsAppConfigRecordError::InvalidChecksum);
- }
- Ok(())
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum RadrootsAppConfigStoreError {
- Datastore(RadrootsClientDatastoreError),
- Config(RadrootsAppConfigError),
- Record(RadrootsAppConfigRecordError),
-}
-
-impl std::fmt::Display for RadrootsAppConfigStoreError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- RadrootsAppConfigStoreError::Datastore(err) => write!(f, "{err}"),
- RadrootsAppConfigStoreError::Config(err) => write!(f, "{err}"),
- RadrootsAppConfigStoreError::Record(err) => write!(f, "{err}"),
- }
- }
-}
-
-impl std::error::Error for RadrootsAppConfigStoreError {}
-
-impl RadrootsAppLoggableError for RadrootsAppConfigStoreError {
- fn log_code(&self) -> &'static str {
- match self {
- RadrootsAppConfigStoreError::Datastore(_) => "error.app.config.datastore",
- RadrootsAppConfigStoreError::Config(err) => err.message(),
- RadrootsAppConfigStoreError::Record(err) => err.message(),
- }
- }
-
- fn log_context(&self) -> Option<String> {
- Some(self.to_string())
- }
-}
-
-pub type RadrootsAppConfigStoreResult<T> = Result<T, RadrootsAppConfigStoreError>;
-
-pub async fn app_config_status<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppConfigStatus {
- match app_datastore_read_config_record(datastore, key_maps).await {
- Ok(_) => RadrootsAppConfigStatus::Configured,
- Err(RadrootsAppConfigStoreError::Record(RadrootsAppConfigRecordError::Missing)) => {
- RadrootsAppConfigStatus::Required
- }
- Err(RadrootsAppConfigStoreError::Record(RadrootsAppConfigRecordError::InvalidChecksum))
- | Err(RadrootsAppConfigStoreError::Record(RadrootsAppConfigRecordError::UnsupportedVersion(_)))
- | Err(RadrootsAppConfigStoreError::Datastore(_)) => RadrootsAppConfigStatus::Corrupt,
- Err(RadrootsAppConfigStoreError::Config(_))
- | Err(RadrootsAppConfigStoreError::Record(RadrootsAppConfigRecordError::Corrupt))
- | Err(RadrootsAppConfigStoreError::Record(RadrootsAppConfigRecordError::AlreadyExists)) => {
- RadrootsAppConfigStatus::Corrupt
- }
- }
-}
-
-pub const fn app_config_gate_from_status(
- status: RadrootsAppConfigStatus,
-) -> RadrootsAppConfigGate {
- match status {
- RadrootsAppConfigStatus::Unknown => RadrootsAppConfigGate::splash(),
- RadrootsAppConfigStatus::Required | RadrootsAppConfigStatus::Corrupt => {
- RadrootsAppConfigGate {
- show_config: true,
- show_app: false,
- }
- }
- RadrootsAppConfigStatus::Configured => RadrootsAppConfigGate {
- show_config: false,
- show_app: true,
- },
- }
-}
-
-pub async fn app_datastore_write_config_record<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
- record: &RadrootsAppConfigRecord,
-) -> RadrootsAppConfigStoreResult<RadrootsAppConfigRecord> {
- let key = app_datastore_obj_key_config(key_maps).map_err(RadrootsAppConfigStoreError::Config)?;
- let value = datastore
- .set_obj(key, record)
- .await
- .map_err(RadrootsAppConfigStoreError::Datastore)?;
- Ok(value)
-}
-
-pub async fn app_datastore_read_config_record<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppConfigStoreResult<RadrootsAppConfigRecord> {
- let key = app_datastore_obj_key_config(key_maps).map_err(RadrootsAppConfigStoreError::Config)?;
- match datastore.get_obj::<RadrootsAppConfigRecord>(key).await {
- Ok(record) => {
- app_config_record_validate(&record).map_err(RadrootsAppConfigStoreError::Record)?;
- Ok(record)
- }
- Err(RadrootsClientDatastoreError::NoResult) => {
- Err(RadrootsAppConfigStoreError::Record(RadrootsAppConfigRecordError::Missing))
- }
- Err(err) => Err(RadrootsAppConfigStoreError::Datastore(err)),
- }
-}
-
-pub async fn app_datastore_create_config<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
- config: &RadrootsAppConfigData,
-) -> RadrootsAppConfigStoreResult<RadrootsAppConfigData> {
- let now_ms = app_state_timestamp_ms();
- match app_datastore_read_config_record(datastore, key_maps).await {
- Ok(_) => Err(RadrootsAppConfigStoreError::Record(
- RadrootsAppConfigRecordError::AlreadyExists,
- )),
- Err(RadrootsAppConfigStoreError::Record(RadrootsAppConfigRecordError::Missing)) => {
- let record = app_config_record_new(config.clone(), 1, now_ms);
- let value = app_datastore_write_config_record(datastore, key_maps, &record).await?;
- Ok(value.config)
- }
- Err(err) => Err(err),
- }
-}
-
-pub async fn app_datastore_update_config<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
- config: &RadrootsAppConfigData,
-) -> RadrootsAppConfigStoreResult<RadrootsAppConfigData> {
- let now_ms = app_state_timestamp_ms();
- let record = match app_datastore_read_config_record(datastore, key_maps).await {
- Ok(existing) => app_config_record_new(config.clone(), existing.revision + 1, now_ms),
- Err(err) => return Err(err),
- };
- let value = app_datastore_write_config_record(datastore, key_maps, &record).await?;
- Ok(value.config)
-}
-
-pub async fn app_datastore_read_config<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppConfigStoreResult<RadrootsAppConfigData> {
- let record = app_datastore_read_config_record(datastore, key_maps).await?;
- Ok(record.config)
-}
-
-pub async fn app_datastore_has_config<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppConfigStoreResult<bool> {
- match app_datastore_read_config_record(datastore, key_maps).await {
- Ok(_) => Ok(true),
- Err(RadrootsAppConfigStoreError::Record(RadrootsAppConfigRecordError::Missing)) => Ok(false),
- Err(err) => Err(err),
- }
-}
-
-pub async fn app_datastore_clear_config<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppConfigStoreResult<()> {
- let key = app_datastore_obj_key_config(key_maps).map_err(RadrootsAppConfigStoreError::Config)?;
- datastore
- .del_obj(key)
- .await
- .map_err(RadrootsAppConfigStoreError::Datastore)?;
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- app_config_gate_from_status,
- app_config_record_new,
- app_config_record_validate,
- RadrootsAppConfigGate,
- RadrootsAppConfigData,
- RadrootsAppConfigRecordError,
- RadrootsAppConfigStatus,
- APP_CONFIG_SCHEMA_VERSION,
- };
-
- #[test]
- fn config_record_roundtrips() {
- let config = RadrootsAppConfigData::default();
- let record = app_config_record_new(config.clone(), 1, 1234);
- assert_eq!(record.schema_version, APP_CONFIG_SCHEMA_VERSION);
- assert_eq!(record.revision, 1);
- assert_eq!(record.updated_at_ms, 1234);
- assert_eq!(record.config, config);
- assert!(app_config_record_validate(&record).is_ok());
- }
-
- #[test]
- fn config_record_detects_invalid_checksum() {
- let config = RadrootsAppConfigData::default();
- let mut record = app_config_record_new(config, 1, 1234);
- record.checksum = String::from("invalid");
- let err = app_config_record_validate(&record).expect_err("checksum");
- assert_eq!(err, RadrootsAppConfigRecordError::InvalidChecksum);
- }
-
- #[test]
- fn config_gate_maps_status() {
- assert_eq!(
- app_config_gate_from_status(RadrootsAppConfigStatus::Unknown),
- RadrootsAppConfigGate::splash()
- );
- let required = app_config_gate_from_status(RadrootsAppConfigStatus::Required);
- assert!(required.show_config);
- assert!(!required.show_app);
- let configured = app_config_gate_from_status(RadrootsAppConfigStatus::Configured);
- assert!(configured.show_app);
- assert!(!configured.show_config);
- }
-}
diff --git a/app/src/context.rs b/app/src/context.rs
@@ -1,86 +0,0 @@
-#![forbid(unsafe_code)]
-
-use leptos::prelude::{use_context, LocalStorage, RwSignal};
-
-use crate::{
- RadrootsAppBackends,
- RadrootsAppConfigStatus,
- RadrootsAppInitError,
- RadrootsAppInitState,
- RadrootsAppSetupStatus,
-};
-
-#[derive(Clone)]
-pub struct RadrootsAppContext {
- pub backends: RwSignal<Option<RadrootsAppBackends>, LocalStorage>,
- pub init_error: RwSignal<Option<RadrootsAppInitError>, LocalStorage>,
- pub init_state: RwSignal<RadrootsAppInitState, LocalStorage>,
- pub setup_status: RwSignal<RadrootsAppSetupStatus, LocalStorage>,
- pub config_status: RwSignal<RadrootsAppConfigStatus, LocalStorage>,
-}
-
-pub fn app_context() -> Option<RadrootsAppContext> {
- Some(RadrootsAppContext {
- backends: use_context::<RwSignal<Option<RadrootsAppBackends>, LocalStorage>>()?,
- init_error: use_context::<RwSignal<Option<RadrootsAppInitError>, LocalStorage>>()?,
- init_state: use_context::<RwSignal<RadrootsAppInitState, LocalStorage>>()?,
- setup_status: use_context::<RwSignal<RadrootsAppSetupStatus, LocalStorage>>()?,
- config_status: use_context::<RwSignal<RadrootsAppConfigStatus, LocalStorage>>()?,
- })
-}
-
-#[cfg(test)]
-mod tests {
- use super::app_context;
- use crate::{
- app_init_state_default,
- RadrootsAppBackends,
- RadrootsAppInitError,
- RadrootsAppInitStage,
- RadrootsAppConfigStatus,
- RadrootsAppSetupStatus,
- };
- use leptos::prelude::{provide_context, Owner, RwSignal, WithUntracked};
-
- #[test]
- fn app_context_is_none_without_providers() {
- let owner = Owner::new();
- owner.set();
- assert!(app_context().is_none());
- }
-
- #[test]
- fn app_context_reads_provided_signals() {
- let owner = Owner::new();
- owner.set();
- let backends = RwSignal::new_local(None::<RadrootsAppBackends>);
- let init_error = RwSignal::new_local(None::<RadrootsAppInitError>);
- let init_state = RwSignal::new_local(app_init_state_default());
- let setup_status = RwSignal::new_local(RadrootsAppSetupStatus::Unknown);
- let config_status = RwSignal::new_local(RadrootsAppConfigStatus::Unknown);
- provide_context(backends);
- provide_context(init_error);
- provide_context(init_state);
- provide_context(setup_status);
- provide_context(config_status);
- let context = app_context().expect("context");
- assert!(context.backends.with_untracked(|value| value.is_none()));
- assert!(context.init_error.with_untracked(|value| value.is_none()));
- assert_eq!(
- context.init_state.with_untracked(|state| state.stage),
- RadrootsAppInitStage::Idle
- );
- assert_eq!(
- context
- .setup_status
- .with_untracked(|value| *value),
- RadrootsAppSetupStatus::Unknown
- );
- assert_eq!(
- context
- .config_status
- .with_untracked(|value| *value),
- RadrootsAppConfigStatus::Unknown
- );
- }
-}
diff --git a/app/src/data.rs b/app/src/data.rs
@@ -1,266 +0,0 @@
-#![forbid(unsafe_code)]
-
-use serde::{Deserialize, Serialize};
-
-use sha2::{Digest, Sha256};
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-pub enum RadrootsAppRole {
- #[serde(rename = "individual", alias = "Public", alias = "public", alias = "Individual")]
- Individual,
- #[serde(rename = "farm")]
- Farm,
- #[serde(rename = "business")]
- Business,
-}
-
-impl Default for RadrootsAppRole {
- fn default() -> Self {
- RadrootsAppRole::Individual
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsAppState {
- pub active_key: String,
- pub role: RadrootsAppRole,
- pub eula_date: String,
- pub eula_version: String,
- pub eula_hash: String,
- pub relays: Vec<String>,
- pub nip05_key: Option<String>,
- pub notifications_permission: Option<String>,
-}
-
-pub const APP_EULA_VERSION: &str = "0.1.0";
-pub const APP_EULA_HASH: &str = "unknown";
-
-impl Default for RadrootsAppState {
- fn default() -> Self {
- Self {
- active_key: String::new(),
- role: RadrootsAppRole::default(),
- eula_date: String::new(),
- eula_version: String::from(APP_EULA_VERSION),
- eula_hash: String::from(APP_EULA_HASH),
- relays: Vec::new(),
- nip05_key: None,
- notifications_permission: None,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
-pub struct RadrootsAppSetupDraft {
- pub nostr_public_key: Option<String>,
- pub profile_name: Option<String>,
- pub role: Option<RadrootsAppRole>,
- pub nip05_request: Option<bool>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsAppProfileSeed {
- pub public_key: String,
- pub name: String,
- pub display_name: Option<String>,
- pub nip05_request: bool,
-}
-
-pub const APP_STATE_SCHEMA_VERSION: u32 = 1;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppStateError {
- Missing,
- Corrupt,
- InvalidChecksum,
- UnsupportedVersion(u32),
- AlreadyExists,
-}
-
-impl RadrootsAppStateError {
- pub const fn message(&self) -> &'static str {
- match self {
- RadrootsAppStateError::Missing => "error.app.state.missing",
- RadrootsAppStateError::Corrupt => "error.app.state.corrupt",
- RadrootsAppStateError::InvalidChecksum => "error.app.state.checksum_invalid",
- RadrootsAppStateError::UnsupportedVersion(_) => "error.app.state.schema_unsupported",
- RadrootsAppStateError::AlreadyExists => "error.app.state.already_exists",
- }
- }
-}
-
-impl std::fmt::Display for RadrootsAppStateError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_str(self.message())
- }
-}
-
-impl std::error::Error for RadrootsAppStateError {}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsAppStateRecord {
- pub schema_version: u32,
- pub revision: u64,
- pub updated_at_ms: i64,
- pub checksum: String,
- pub state: RadrootsAppState,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-struct RadrootsAppStateChecksumPayload {
- schema_version: u32,
- revision: u64,
- updated_at_ms: i64,
- state: RadrootsAppState,
-}
-
-pub fn app_state_timestamp_ms() -> i64 {
- #[cfg(target_arch = "wasm32")]
- {
- js_sys::Date::now() as i64
- }
- #[cfg(not(target_arch = "wasm32"))]
- {
- use std::time::{SystemTime, UNIX_EPOCH};
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map(|value| value.as_millis() as i64)
- .unwrap_or(0)
- }
-}
-
-fn app_state_record_checksum(payload: &RadrootsAppStateChecksumPayload) -> String {
- let serialized =
- serde_json::to_vec(payload).unwrap_or_else(|_| Vec::new());
- let hash = Sha256::digest(&serialized);
- hex::encode(hash)
-}
-
-pub fn app_state_record_new(
- state: RadrootsAppState,
- revision: u64,
- updated_at_ms: i64,
-) -> RadrootsAppStateRecord {
- let payload = RadrootsAppStateChecksumPayload {
- schema_version: APP_STATE_SCHEMA_VERSION,
- revision,
- updated_at_ms,
- state: state.clone(),
- };
- let checksum = app_state_record_checksum(&payload);
- RadrootsAppStateRecord {
- schema_version: APP_STATE_SCHEMA_VERSION,
- revision,
- updated_at_ms,
- checksum,
- state,
- }
-}
-
-pub fn app_state_record_validate(
- record: &RadrootsAppStateRecord,
-) -> Result<(), RadrootsAppStateError> {
- if record.schema_version != APP_STATE_SCHEMA_VERSION {
- return Err(RadrootsAppStateError::UnsupportedVersion(
- record.schema_version,
- ));
- }
- let payload = RadrootsAppStateChecksumPayload {
- schema_version: record.schema_version,
- revision: record.revision,
- updated_at_ms: record.updated_at_ms,
- state: record.state.clone(),
- };
- let expected = app_state_record_checksum(&payload);
- if record.checksum != expected {
- return Err(RadrootsAppStateError::InvalidChecksum);
- }
- Ok(())
-}
-
-pub fn app_state_is_initialized(state: &RadrootsAppState) -> bool {
- !state.active_key.is_empty()
- && !state.eula_date.is_empty()
- && !state.eula_version.is_empty()
- && !state.eula_hash.is_empty()
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- app_state_is_initialized,
- app_state_record_new,
- app_state_record_validate,
- app_state_timestamp_ms,
- RadrootsAppRole,
- RadrootsAppState,
- RadrootsAppStateError,
- APP_EULA_HASH,
- APP_EULA_VERSION,
- APP_STATE_SCHEMA_VERSION,
- };
-
- #[test]
- fn role_defaults_to_individual() {
- assert_eq!(RadrootsAppRole::default(), RadrootsAppRole::Individual);
- }
-
- #[test]
- fn state_defaults_empty() {
- let data = RadrootsAppState::default();
- assert_eq!(data.active_key, "");
- assert_eq!(data.role, RadrootsAppRole::Individual);
- assert_eq!(data.eula_date, "");
- assert_eq!(data.eula_version, APP_EULA_VERSION);
- assert_eq!(data.eula_hash, APP_EULA_HASH);
- assert!(data.relays.is_empty());
- assert!(data.nip05_key.is_none());
- assert!(data.notifications_permission.is_none());
- }
-
- #[test]
- fn state_initialized_requires_key_and_eula() {
- let data = RadrootsAppState::default();
- assert!(!app_state_is_initialized(&data));
- let mut data = RadrootsAppState::default();
- data.active_key = "pub".to_string();
- assert!(!app_state_is_initialized(&data));
- data.eula_date = "2025-01-01T00:00:00Z".to_string();
- data.eula_version.clear();
- assert!(!app_state_is_initialized(&data));
- data.eula_version = APP_EULA_VERSION.to_string();
- data.eula_hash.clear();
- assert!(!app_state_is_initialized(&data));
- data.eula_hash = APP_EULA_HASH.to_string();
- assert!(app_state_is_initialized(&data));
- }
-
- #[test]
- fn state_record_validates_checksum() {
- let mut state = RadrootsAppState::default();
- state.active_key = "pub".to_string();
- let record = app_state_record_new(state, 1, app_state_timestamp_ms());
- assert!(app_state_record_validate(&record).is_ok());
- }
-
- #[test]
- fn state_record_detects_checksum_mismatch() {
- let state = RadrootsAppState::default();
- let mut record = app_state_record_new(state, 1, app_state_timestamp_ms());
- record.checksum = "bad".to_string();
- let err = app_state_record_validate(&record).expect_err("checksum");
- assert_eq!(err, RadrootsAppStateError::InvalidChecksum);
- }
-
- #[test]
- fn state_record_rejects_unsupported_version() {
- let state = RadrootsAppState::default();
- let mut record = app_state_record_new(state, 1, app_state_timestamp_ms());
- record.schema_version = APP_STATE_SCHEMA_VERSION + 1;
- let err = app_state_record_validate(&record).expect_err("version");
- assert_eq!(
- err,
- RadrootsAppStateError::UnsupportedVersion(APP_STATE_SCHEMA_VERSION + 1)
- );
- }
-}
diff --git a/app/src/entry.rs b/app/src/entry.rs
@@ -1,11 +0,0 @@
-use leptos::mount::mount_to_body;
-use wasm_bindgen::prelude::wasm_bindgen;
-
-use crate::{app_logging_init, app_theme_init, RadrootsApp};
-
-#[wasm_bindgen(start)]
-pub fn start() {
- let _ = app_logging_init(None);
- let _ = app_theme_init();
- mount_to_body(RadrootsApp);
-}
diff --git a/app/src/health.rs b/app/src/health.rs
@@ -1,1019 +0,0 @@
-#![forbid(unsafe_code)]
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppHealthCheckStatus {
- Ok,
- Error,
- Skipped,
-}
-
-impl RadrootsAppHealthCheckStatus {
- pub const fn as_str(self) -> &'static str {
- match self {
- RadrootsAppHealthCheckStatus::Ok => "ok",
- RadrootsAppHealthCheckStatus::Error => "error",
- RadrootsAppHealthCheckStatus::Skipped => "skipped",
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsAppHealthCheckResult {
- pub status: RadrootsAppHealthCheckStatus,
- pub message: Option<String>,
-}
-
-impl RadrootsAppHealthCheckResult {
- pub fn ok() -> Self {
- Self {
- status: RadrootsAppHealthCheckStatus::Ok,
- message: None,
- }
- }
-
- pub fn error(message: impl Into<String>) -> Self {
- Self {
- status: RadrootsAppHealthCheckStatus::Error,
- message: Some(message.into()),
- }
- }
-
- pub fn skipped() -> Self {
- Self {
- status: RadrootsAppHealthCheckStatus::Skipped,
- message: None,
- }
- }
-
- pub fn skipped_with_message(message: impl Into<String>) -> Self {
- Self {
- status: RadrootsAppHealthCheckStatus::Skipped,
- message: Some(message.into()),
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsAppHealthReport {
- pub key_maps: RadrootsAppHealthCheckResult,
- pub bootstrap_state: RadrootsAppHealthCheckResult,
- pub state_active_key: RadrootsAppHealthCheckResult,
- pub notifications: RadrootsAppHealthCheckResult,
- pub tangle: RadrootsAppHealthCheckResult,
- pub datastore_roundtrip: RadrootsAppHealthCheckResult,
- pub keystore: RadrootsAppHealthCheckResult,
-}
-
-impl Default for RadrootsAppHealthReport {
- fn default() -> Self {
- Self {
- key_maps: RadrootsAppHealthCheckResult::skipped(),
- bootstrap_state: RadrootsAppHealthCheckResult::skipped(),
- state_active_key: RadrootsAppHealthCheckResult::skipped(),
- notifications: RadrootsAppHealthCheckResult::skipped(),
- tangle: RadrootsAppHealthCheckResult::skipped(),
- datastore_roundtrip: RadrootsAppHealthCheckResult::skipped(),
- keystore: RadrootsAppHealthCheckResult::skipped(),
- }
- }
-}
-
-impl RadrootsAppHealthReport {
- pub fn empty() -> Self {
- Self::default()
- }
-}
-
-use crate::{
- app_datastore_has_state,
- app_datastore_key_nostr_key,
- app_datastore_read_state,
- app_log_buffer_flush_critical,
- app_log_debug_emit,
- app_log_entry_new,
- app_log_entry_record,
- app_key_maps_validate,
- RadrootsAppNotifications,
- RadrootsAppLogLevel,
- RadrootsAppTangleClient,
- RadrootsAppKeyMapConfig,
- RadrootsAppState,
-};
-use futures::join;
-use radroots_app_core::notifications::RadrootsClientNotificationsPermission;
-use radroots_app_core::datastore::{RadrootsClientDatastore, RadrootsClientDatastoreError};
-use radroots_app_core::keystore::{RadrootsClientKeystoreError, RadrootsClientKeystoreNostr};
-
-fn log_health_context(result: &RadrootsAppHealthCheckResult) -> Option<String> {
- match result.message.as_deref() {
- Some(message) => Some(format!("status={},detail={message}", result.status.as_str())),
- None => Some(format!("status={}", result.status.as_str())),
- }
-}
-
-fn log_health_start(name: &str) {
- let _ = app_log_debug_emit("log.app.health.start", name, None);
-}
-
-fn log_health_end(name: &str, result: &RadrootsAppHealthCheckResult) {
- let context = log_health_context(result);
- if result.status == RadrootsAppHealthCheckStatus::Error {
- let entry = app_log_entry_new(RadrootsAppLogLevel::Error, "log.app.health.end", name, context);
- let _ = app_log_entry_record(entry);
- } else {
- let _ = app_log_debug_emit("log.app.health.end", name, context);
- }
-}
-
-pub fn app_health_check_key_maps(key_maps: &RadrootsAppKeyMapConfig) -> RadrootsAppHealthCheckResult {
- match app_key_maps_validate(key_maps) {
- Ok(()) => RadrootsAppHealthCheckResult::ok(),
- Err(err) => RadrootsAppHealthCheckResult::error(err.to_string()),
- }
-}
-
-pub async fn app_health_check_bootstrap_state<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppHealthCheckResult {
- match app_datastore_has_state(datastore, key_maps).await {
- Ok(true) => RadrootsAppHealthCheckResult::ok(),
- Ok(false) => RadrootsAppHealthCheckResult::error("missing"),
- Err(err) => RadrootsAppHealthCheckResult::error(err.to_string()),
- }
-}
-
-pub async fn app_health_check_state_active_key<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppHealthCheckResult {
- app_health_check_state_active_key_with_state(datastore, key_maps, None).await
-}
-
-async fn app_health_check_state_active_key_with_state<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
- state: Option<&RadrootsAppState>,
-) -> RadrootsAppHealthCheckResult {
- let active_key = match state {
- Some(value) => value.active_key.clone(),
- None => {
- let app_data = match app_datastore_read_state(datastore, key_maps).await {
- Ok(value) => value,
- Err(err) => return RadrootsAppHealthCheckResult::error(err.to_string()),
- };
- app_data.active_key
- }
- };
- if active_key.is_empty() {
- return RadrootsAppHealthCheckResult::error("missing");
- }
- let key_name = match app_datastore_key_nostr_key(key_maps) {
- Ok(value) => value,
- Err(err) => return RadrootsAppHealthCheckResult::error(err.to_string()),
- };
- let stored = match datastore.get(key_name).await {
- Ok(value) => value,
- Err(RadrootsClientDatastoreError::NoResult) => return RadrootsAppHealthCheckResult::error("missing"),
- Err(err) => return RadrootsAppHealthCheckResult::error(err.to_string()),
- };
- if stored != active_key {
- return RadrootsAppHealthCheckResult::error("mismatch");
- }
- RadrootsAppHealthCheckResult::ok()
-}
-
-pub async fn app_health_check_notifications(
- notifications: &RadrootsAppNotifications,
-) -> RadrootsAppHealthCheckResult {
- match notifications.permission().await {
- Ok(permission) => app_health_check_notifications_permission(permission),
- Err(err) => RadrootsAppHealthCheckResult::error(err.to_string()),
- }
-}
-
-fn app_health_check_notifications_permission(
- permission: RadrootsClientNotificationsPermission,
-) -> RadrootsAppHealthCheckResult {
- match permission {
- RadrootsClientNotificationsPermission::Granted => RadrootsAppHealthCheckResult::ok(),
- RadrootsClientNotificationsPermission::Denied
- | RadrootsClientNotificationsPermission::Default => RadrootsAppHealthCheckResult::skipped(),
- RadrootsClientNotificationsPermission::Unavailable => {
- RadrootsAppHealthCheckResult::error(permission.as_str())
- }
- }
-}
-
-pub async fn app_health_check_notifications_with_state(
- notifications: &RadrootsAppNotifications,
- stored_permission: Option<&str>,
-) -> RadrootsAppHealthCheckResult {
- if let Some(value) = stored_permission {
- if let Some(permission) = RadrootsClientNotificationsPermission::parse(value) {
- return app_health_check_notifications_permission(permission);
- }
- }
- app_health_check_notifications(notifications).await
-}
-
-pub fn app_health_check_tangle<T: RadrootsAppTangleClient>(tangle: &T) -> RadrootsAppHealthCheckResult {
- match tangle.init() {
- Ok(()) => RadrootsAppHealthCheckResult::ok(),
- Err(crate::RadrootsAppTangleError::NotImplemented) => RadrootsAppHealthCheckResult::skipped(),
- }
-}
-
-const APP_HEALTH_TEMP_KEY: &str = "radroots.health.temp";
-
-pub async fn app_health_check_datastore_roundtrip<T: RadrootsClientDatastore>(
- datastore: &T,
-) -> RadrootsAppHealthCheckResult {
- let value = "ok";
- if let Err(err) = datastore.set(APP_HEALTH_TEMP_KEY, value).await {
- return RadrootsAppHealthCheckResult::error(err.to_string());
- }
- match datastore.get(APP_HEALTH_TEMP_KEY).await {
- Ok(read) => {
- if read != value {
- return RadrootsAppHealthCheckResult::error("mismatch");
- }
- }
- Err(err) => return RadrootsAppHealthCheckResult::error(err.to_string()),
- }
- if let Err(err) = datastore.del(APP_HEALTH_TEMP_KEY).await {
- return RadrootsAppHealthCheckResult::error(err.to_string());
- }
- RadrootsAppHealthCheckResult::ok()
-}
-
-pub async fn app_health_check_keystore_access<T: RadrootsClientDatastore, K: RadrootsClientKeystoreNostr>(
- datastore: &T,
- keystore: &K,
- key_maps: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppHealthCheckResult {
- let key_name = match app_datastore_key_nostr_key(key_maps) {
- Ok(value) => value,
- Err(err) => return RadrootsAppHealthCheckResult::error(err.to_string()),
- };
- let public_key = match datastore.get(key_name).await {
- Ok(value) if !value.is_empty() => value,
- Ok(_) => return RadrootsAppHealthCheckResult::error("missing"),
- Err(RadrootsClientDatastoreError::NoResult) => return RadrootsAppHealthCheckResult::error("missing"),
- Err(err) => return RadrootsAppHealthCheckResult::error(err.to_string()),
- };
- match keystore.read(&public_key).await {
- Ok(_) => RadrootsAppHealthCheckResult::ok(),
- Err(RadrootsClientKeystoreError::MissingKey) => RadrootsAppHealthCheckResult::error("missing"),
- Err(RadrootsClientKeystoreError::NostrNoResults) => RadrootsAppHealthCheckResult::error("missing"),
- Err(err) => RadrootsAppHealthCheckResult::error(err.to_string()),
- }
-}
-
-pub async fn app_health_check_all<T: RadrootsClientDatastore, K: RadrootsClientKeystoreNostr, G: RadrootsAppTangleClient>(
- datastore: &T,
- keystore: &K,
- notifications: &RadrootsAppNotifications,
- tangle: &G,
- key_maps: &RadrootsAppKeyMapConfig,
- setup_required: bool,
-) -> RadrootsAppHealthReport {
- log_health_start("key_maps");
- let key_maps_result = app_health_check_key_maps(key_maps);
- log_health_end("key_maps", &key_maps_result);
- log_health_start("bootstrap_state");
- log_health_start("state_active_key");
- log_health_start("notifications");
- log_health_start("tangle");
- log_health_start("datastore_roundtrip");
- log_health_start("keystore");
- if setup_required {
- let uninitialized =
- RadrootsAppHealthCheckResult::skipped_with_message("uninitialized");
- log_health_end("bootstrap_state", &uninitialized);
- log_health_end("state_active_key", &uninitialized);
- log_health_end("notifications", &uninitialized);
- log_health_end("tangle", &uninitialized);
- log_health_end("datastore_roundtrip", &uninitialized);
- log_health_end("keystore", &uninitialized);
- return RadrootsAppHealthReport {
- key_maps: key_maps_result,
- bootstrap_state: uninitialized.clone(),
- state_active_key: uninitialized.clone(),
- notifications: uninitialized.clone(),
- tangle: uninitialized.clone(),
- datastore_roundtrip: uninitialized.clone(),
- keystore: uninitialized,
- };
- }
- let stored_state = app_datastore_read_state(datastore, key_maps).await.ok();
- let stored_permission = stored_state
- .as_ref()
- .and_then(|data| data.notifications_permission.as_deref());
- let (
- bootstrap_state,
- state_active_key,
- notifications_result,
- tangle_result,
- datastore_roundtrip,
- keystore_result,
- ) = join!(
- app_health_check_bootstrap_state(datastore, key_maps),
- app_health_check_state_active_key_with_state(datastore, key_maps, stored_state.as_ref()),
- app_health_check_notifications_with_state(notifications, stored_permission),
- async { app_health_check_tangle(tangle) },
- app_health_check_datastore_roundtrip(datastore),
- app_health_check_keystore_access(datastore, keystore, key_maps),
- );
- log_health_end("bootstrap_state", &bootstrap_state);
- log_health_end("state_active_key", &state_active_key);
- log_health_end("notifications", ¬ifications_result);
- log_health_end("tangle", &tangle_result);
- log_health_end("datastore_roundtrip", &datastore_roundtrip);
- log_health_end("keystore", &keystore_result);
- RadrootsAppHealthReport {
- key_maps: key_maps_result,
- bootstrap_state,
- state_active_key,
- notifications: notifications_result,
- tangle: tangle_result,
- datastore_roundtrip,
- keystore: keystore_result,
- }
-}
-
-pub async fn app_health_check_all_logged<T: RadrootsClientDatastore, K: RadrootsClientKeystoreNostr, G: RadrootsAppTangleClient>(
- datastore: &T,
- keystore: &K,
- notifications: &RadrootsAppNotifications,
- tangle: &G,
- key_maps: &RadrootsAppKeyMapConfig,
- setup_required: bool,
-) -> RadrootsAppHealthReport {
- let report =
- app_health_check_all(datastore, keystore, notifications, tangle, key_maps, setup_required)
- .await;
- let _ = app_log_buffer_flush_critical(datastore, key_maps).await;
- report
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- app_health_check_state_active_key,
- app_health_check_state_active_key_with_state,
- app_health_check_all,
- app_health_check_all_logged,
- app_health_check_key_maps,
- app_health_check_bootstrap_state,
- app_health_check_datastore_roundtrip,
- app_health_check_keystore_access,
- app_health_check_notifications,
- app_health_check_notifications_with_state,
- app_health_check_notifications_permission,
- app_health_check_tangle,
- log_health_context,
- RadrootsAppHealthCheckResult,
- RadrootsAppHealthCheckStatus,
- RadrootsAppHealthReport,
- };
- use crate::app_log_buffer_drain;
- use crate::{RadrootsAppKeyMapConfig, RadrootsAppStateRecord};
- use async_trait::async_trait;
- use radroots_app_core::datastore::{
- RadrootsClientDatastore,
- RadrootsClientDatastoreEntries,
- RadrootsClientDatastoreEntry,
- RadrootsClientDatastoreError,
- RadrootsClientDatastoreResult,
- RadrootsClientWebDatastore,
- };
- use radroots_app_core::keystore::{
- RadrootsClientKeystoreError,
- RadrootsClientKeystoreNostr,
- RadrootsClientKeystoreResult,
- RadrootsClientWebKeystoreNostr,
- };
- use radroots_app_core::notifications::RadrootsClientNotificationsPermission;
- use radroots_app_core::idb::IDB_CONFIG_DATASTORE;
- use radroots_app_core::backup::RadrootsClientBackupDatastorePayload;
- use radroots_app_core::idb::RadrootsClientIdbConfig;
- use std::cell::RefCell;
- use std::sync::Mutex;
-
- #[test]
- fn health_status_as_str() {
- assert_eq!(RadrootsAppHealthCheckStatus::Ok.as_str(), "ok");
- assert_eq!(RadrootsAppHealthCheckStatus::Error.as_str(), "error");
- assert_eq!(RadrootsAppHealthCheckStatus::Skipped.as_str(), "skipped");
- }
-
- #[test]
- fn health_result_constructors() {
- let ok = RadrootsAppHealthCheckResult::ok();
- assert_eq!(ok.status, RadrootsAppHealthCheckStatus::Ok);
- assert!(ok.message.is_none());
-
- let err = RadrootsAppHealthCheckResult::error("boom");
- assert_eq!(err.status, RadrootsAppHealthCheckStatus::Error);
- assert_eq!(err.message.as_deref(), Some("boom"));
- }
-
- #[test]
- fn health_log_context_formats_error_detail() {
- let result = RadrootsAppHealthCheckResult::error("missing");
- let context = log_health_context(&result);
- assert_eq!(context.as_deref(), Some("status=error,detail=missing"));
- }
-
- #[test]
- fn health_report_defaults_skipped() {
- let report = RadrootsAppHealthReport::default();
- assert_eq!(report.key_maps.status, RadrootsAppHealthCheckStatus::Skipped);
- assert_eq!(report.bootstrap_state.status, RadrootsAppHealthCheckStatus::Skipped);
- assert_eq!(report.state_active_key.status, RadrootsAppHealthCheckStatus::Skipped);
- assert_eq!(report.notifications.status, RadrootsAppHealthCheckStatus::Skipped);
- assert_eq!(report.tangle.status, RadrootsAppHealthCheckStatus::Skipped);
- assert_eq!(report.datastore_roundtrip.status, RadrootsAppHealthCheckStatus::Skipped);
- assert_eq!(report.keystore.status, RadrootsAppHealthCheckStatus::Skipped);
- }
-
- #[test]
- fn health_check_key_maps_reports_errors() {
- let empty = RadrootsAppKeyMapConfig::empty();
- let result = app_health_check_key_maps(&empty);
- assert_eq!(result.status, RadrootsAppHealthCheckStatus::Error);
- assert_eq!(
- result.message.as_deref(),
- Some("error.app.config.key_map_missing")
- );
- }
-
- #[test]
- fn health_check_bootstrap_state_reports_idb_errors() {
- let datastore = RadrootsClientWebDatastore::new(None);
- let key_maps = crate::app_key_maps_default();
- let result = futures::executor::block_on(app_health_check_bootstrap_state(
- &datastore,
- &key_maps,
- ));
- assert_eq!(result.status, RadrootsAppHealthCheckStatus::Error);
- }
-
- #[test]
- fn health_check_roundtrip_reports_idb_errors() {
- let datastore = RadrootsClientWebDatastore::new(None);
- let result =
- futures::executor::block_on(app_health_check_datastore_roundtrip(&datastore));
- assert_eq!(result.status, RadrootsAppHealthCheckStatus::Error);
- }
-
- struct TestDatastore {
- get_result: RadrootsClientDatastoreResult<String>,
- app_data: Option<crate::RadrootsAppState>,
- record: RefCell<Option<RadrootsAppStateRecord>>,
- }
-
- fn datastore_err<T>() -> RadrootsClientDatastoreResult<T> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- #[async_trait(?Send)]
- impl RadrootsClientDatastore for TestDatastore {
- fn get_config(&self) -> RadrootsClientIdbConfig {
- IDB_CONFIG_DATASTORE
- }
-
- fn get_store_id(&self) -> &str {
- "test"
- }
-
- async fn init(&self) -> RadrootsClientDatastoreResult<()> {
- datastore_err()
- }
-
- async fn set(&self, _key: &str, _value: &str) -> RadrootsClientDatastoreResult<String> {
- datastore_err()
- }
-
- async fn get(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
- self.get_result.clone()
- }
-
- async fn set_obj<T>(&self, _key: &str, _value: &T) -> RadrootsClientDatastoreResult<T>
- where
- T: serde::Serialize + serde::de::DeserializeOwned + Clone,
- {
- let encoded = serde_json::to_string(_value)
- .map_err(|_| RadrootsClientDatastoreError::IdbUndefined)?;
- if let Ok(parsed) = serde_json::from_str::<RadrootsAppStateRecord>(&encoded) {
- *self.record.borrow_mut() = Some(parsed);
- return Ok(_value.clone());
- }
- datastore_err()
- }
-
- async fn update_obj<T>(&self, _key: &str, _value: &T) -> RadrootsClientDatastoreResult<T>
- where
- T: serde::Serialize + serde::de::DeserializeOwned + Clone,
- {
- datastore_err()
- }
-
- async fn get_obj<T>(&self, _key: &str) -> RadrootsClientDatastoreResult<T>
- where
- T: serde::de::DeserializeOwned,
- {
- if let Some(record) = self.record.borrow().as_ref() {
- let serialized = serde_json::to_string(record)
- .map_err(|_| RadrootsClientDatastoreError::NoResult)?;
- if let Ok(parsed) = serde_json::from_str(&serialized) {
- return Ok(parsed);
- }
- }
- let Some(data) = self.app_data.as_ref() else {
- return Err(RadrootsClientDatastoreError::NoResult);
- };
- let serialized =
- serde_json::to_string(data).map_err(|_| RadrootsClientDatastoreError::NoResult)?;
- serde_json::from_str(&serialized)
- .map_err(|_| RadrootsClientDatastoreError::NoResult)
- }
-
- async fn del_obj(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
- datastore_err()
- }
-
- async fn del(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
- datastore_err()
- }
-
- async fn del_pref(&self, _key_prefix: &str) -> RadrootsClientDatastoreResult<Vec<String>> {
- datastore_err()
- }
-
- async fn set_param(
- &self,
- _key: &str,
- _key_param: &str,
- _value: &str,
- ) -> RadrootsClientDatastoreResult<String> {
- datastore_err()
- }
-
- async fn get_param(
- &self,
- _key: &str,
- _key_param: &str,
- ) -> RadrootsClientDatastoreResult<String> {
- datastore_err()
- }
-
- async fn keys(&self) -> RadrootsClientDatastoreResult<Vec<String>> {
- datastore_err()
- }
-
- async fn entries(&self) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> {
- datastore_err()
- }
-
- async fn entries_pref(
- &self,
- _key_prefix: &str,
- ) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> {
- datastore_err()
- }
-
- async fn reset(&self) -> RadrootsClientDatastoreResult<()> {
- datastore_err()
- }
-
- async fn export_backup(
- &self,
- ) -> RadrootsClientDatastoreResult<RadrootsClientBackupDatastorePayload> {
- datastore_err()
- }
-
- async fn import_backup(
- &self,
- _payload: RadrootsClientBackupDatastorePayload,
- ) -> RadrootsClientDatastoreResult<()> {
- datastore_err()
- }
- }
-
- struct TestKeystore {
- read_result: RadrootsClientKeystoreResult<String>,
- }
-
- #[async_trait(?Send)]
- impl RadrootsClientKeystoreNostr for TestKeystore {
- async fn generate(&self) -> RadrootsClientKeystoreResult<String> {
- Err(RadrootsClientKeystoreError::IdbUndefined)
- }
-
- async fn add(&self, _secret_key: &str) -> RadrootsClientKeystoreResult<String> {
- Err(RadrootsClientKeystoreError::IdbUndefined)
- }
-
- async fn read(&self, _public_key: &str) -> RadrootsClientKeystoreResult<String> {
- self.read_result.clone()
- }
-
- async fn keys(&self) -> RadrootsClientKeystoreResult<Vec<String>> {
- Err(RadrootsClientKeystoreError::IdbUndefined)
- }
-
- async fn remove(&self, _public_key: &str) -> RadrootsClientKeystoreResult<String> {
- Err(RadrootsClientKeystoreError::IdbUndefined)
- }
-
- async fn reset(&self) -> RadrootsClientKeystoreResult<()> {
- Err(RadrootsClientKeystoreError::IdbUndefined)
- }
- }
-
- #[test]
- fn health_check_keystore_maps_idb_errors() {
- let datastore = RadrootsClientWebDatastore::new(None);
- let keystore = RadrootsClientWebKeystoreNostr::new(None);
- let key_maps = crate::app_key_maps_default();
- let result = futures::executor::block_on(app_health_check_keystore_access(
- &datastore,
- &keystore,
- &key_maps,
- ));
- assert_eq!(result.status, RadrootsAppHealthCheckStatus::Error);
- }
-
- #[test]
- fn health_check_keystore_reports_missing_datastore_key() {
- let datastore = TestDatastore {
- get_result: Err(RadrootsClientDatastoreError::NoResult),
- app_data: None,
- record: RefCell::new(None),
- };
- let keystore = TestKeystore {
- read_result: Err(RadrootsClientKeystoreError::MissingKey),
- };
- let key_maps = crate::app_key_maps_default();
- let result = futures::executor::block_on(app_health_check_keystore_access(
- &datastore,
- &keystore,
- &key_maps,
- ));
- assert_eq!(result.status, RadrootsAppHealthCheckStatus::Error);
- assert_eq!(result.message.as_deref(), Some("missing"));
- }
-
- #[test]
- fn health_check_keystore_reports_missing_keystore_key() {
- let datastore = TestDatastore {
- get_result: Ok("pub".to_string()),
- app_data: None,
- record: RefCell::new(None),
- };
- let keystore = TestKeystore {
- read_result: Err(RadrootsClientKeystoreError::MissingKey),
- };
- let key_maps = crate::app_key_maps_default();
- let result = futures::executor::block_on(app_health_check_keystore_access(
- &datastore,
- &keystore,
- &key_maps,
- ));
- assert_eq!(result.status, RadrootsAppHealthCheckStatus::Error);
- assert_eq!(result.message.as_deref(), Some("missing"));
- }
-
- #[test]
- fn health_check_keystore_accepts_matching_key() {
- let datastore = TestDatastore {
- get_result: Ok("pub".to_string()),
- app_data: None,
- record: RefCell::new(None),
- };
- let keystore = TestKeystore {
- read_result: Ok("secret".to_string()),
- };
- let key_maps = crate::app_key_maps_default();
- let result = futures::executor::block_on(app_health_check_keystore_access(
- &datastore,
- &keystore,
- &key_maps,
- ));
- assert_eq!(result.status, RadrootsAppHealthCheckStatus::Ok);
- }
-
- #[test]
- fn health_check_state_requires_active_key() {
- let datastore = TestDatastore {
- get_result: Ok("pub".to_string()),
- app_data: Some(crate::RadrootsAppState::default()),
- record: RefCell::new(None),
- };
- let key_maps = crate::app_key_maps_default();
- let result = futures::executor::block_on(app_health_check_state_active_key(
- &datastore,
- &key_maps,
- ));
- assert_eq!(result.status, RadrootsAppHealthCheckStatus::Error);
- assert_eq!(result.message.as_deref(), Some("missing"));
- }
-
- #[test]
- fn health_check_state_detects_mismatch() {
- let mut state = crate::RadrootsAppState::default();
- state.active_key = "other".to_string();
- let datastore = TestDatastore {
- get_result: Ok("pub".to_string()),
- app_data: Some(state),
- record: RefCell::new(None),
- };
- let key_maps = crate::app_key_maps_default();
- let result = futures::executor::block_on(app_health_check_state_active_key(
- &datastore,
- &key_maps,
- ));
- assert_eq!(result.status, RadrootsAppHealthCheckStatus::Error);
- assert_eq!(result.message.as_deref(), Some("mismatch"));
- }
-
- #[test]
- fn health_check_state_accepts_match() {
- let mut state = crate::RadrootsAppState::default();
- state.active_key = "pub".to_string();
- let datastore = TestDatastore {
- get_result: Ok("pub".to_string()),
- app_data: Some(state),
- record: RefCell::new(None),
- };
- let key_maps = crate::app_key_maps_default();
- let result = futures::executor::block_on(app_health_check_state_active_key(
- &datastore,
- &key_maps,
- ));
- assert_eq!(result.status, RadrootsAppHealthCheckStatus::Ok);
- }
-
- #[test]
- fn health_check_state_uses_provided_state() {
- let mut state = crate::RadrootsAppState::default();
- state.active_key = "pub".to_string();
- let datastore = TestDatastore {
- get_result: Ok("pub".to_string()),
- app_data: None,
- record: RefCell::new(None),
- };
- let key_maps = crate::app_key_maps_default();
- let result = futures::executor::block_on(app_health_check_state_active_key_with_state(
- &datastore,
- &key_maps,
- Some(&state),
- ));
- assert_eq!(result.status, RadrootsAppHealthCheckStatus::Ok);
- }
-
- #[test]
- fn health_check_all_reports_idb_errors() {
- let datastore = RadrootsClientWebDatastore::new(None);
- let keystore = RadrootsClientWebKeystoreNostr::new(None);
- let notifications = crate::RadrootsAppNotifications::new(None);
- let tangle = crate::RadrootsAppTangleClientStub::new();
- let key_maps = crate::app_key_maps_default();
- let report = futures::executor::block_on(app_health_check_all(
- &datastore,
- &keystore,
- ¬ifications,
- &tangle,
- &key_maps,
- false,
- ));
- assert_eq!(report.key_maps.status, RadrootsAppHealthCheckStatus::Ok);
- assert_eq!(report.bootstrap_state.status, RadrootsAppHealthCheckStatus::Error);
- assert_eq!(report.state_active_key.status, RadrootsAppHealthCheckStatus::Error);
- assert_eq!(report.notifications.status, RadrootsAppHealthCheckStatus::Error);
- assert_eq!(report.tangle.status, RadrootsAppHealthCheckStatus::Skipped);
- assert_eq!(report.datastore_roundtrip.status, RadrootsAppHealthCheckStatus::Error);
- assert_eq!(report.keystore.status, RadrootsAppHealthCheckStatus::Error);
- }
-
- #[test]
- fn health_check_all_skips_when_setup_required() {
- let datastore = RadrootsClientWebDatastore::new(None);
- let keystore = RadrootsClientWebKeystoreNostr::new(None);
- let notifications = crate::RadrootsAppNotifications::new(None);
- let tangle = crate::RadrootsAppTangleClientStub::new();
- let key_maps = crate::app_key_maps_default();
- let report = futures::executor::block_on(app_health_check_all(
- &datastore,
- &keystore,
- ¬ifications,
- &tangle,
- &key_maps,
- true,
- ));
- assert_eq!(report.key_maps.status, RadrootsAppHealthCheckStatus::Ok);
- assert_eq!(report.bootstrap_state.status, RadrootsAppHealthCheckStatus::Skipped);
- assert_eq!(report.state_active_key.status, RadrootsAppHealthCheckStatus::Skipped);
- assert_eq!(report.notifications.status, RadrootsAppHealthCheckStatus::Skipped);
- assert_eq!(report.tangle.status, RadrootsAppHealthCheckStatus::Skipped);
- assert_eq!(report.datastore_roundtrip.status, RadrootsAppHealthCheckStatus::Skipped);
- assert_eq!(report.keystore.status, RadrootsAppHealthCheckStatus::Skipped);
- }
-
- #[test]
- fn health_check_notifications_reports_unavailable() {
- let notifications = crate::RadrootsAppNotifications::new(None);
- let result =
- futures::executor::block_on(app_health_check_notifications(¬ifications));
- assert_eq!(result.status, RadrootsAppHealthCheckStatus::Error);
- assert_eq!(result.message.as_deref(), Some("unavailable"));
- }
-
- #[test]
- fn health_check_notifications_skips_default_and_denied() {
- let default_result =
- app_health_check_notifications_permission(RadrootsClientNotificationsPermission::Default);
- assert_eq!(default_result.status, RadrootsAppHealthCheckStatus::Skipped);
- let denied_result =
- app_health_check_notifications_permission(RadrootsClientNotificationsPermission::Denied);
- assert_eq!(denied_result.status, RadrootsAppHealthCheckStatus::Skipped);
- }
-
- #[test]
- fn health_check_notifications_uses_stored_permission() {
- let notifications = crate::RadrootsAppNotifications::new(None);
- let result = futures::executor::block_on(app_health_check_notifications_with_state(
- ¬ifications,
- Some("granted"),
- ));
- assert_eq!(result.status, RadrootsAppHealthCheckStatus::Ok);
- }
-
- #[test]
- fn health_check_tangle_reports_not_implemented() {
- let tangle = crate::RadrootsAppTangleClientStub::new();
- let result = app_health_check_tangle(&tangle);
- assert_eq!(result.status, RadrootsAppHealthCheckStatus::Skipped);
- assert!(result.message.is_none());
- }
-
- struct FlushDatastore {
- entries: Mutex<Vec<RadrootsClientDatastoreEntry>>,
- }
-
- impl FlushDatastore {
- fn new() -> Self {
- Self {
- entries: Mutex::new(Vec::new()),
- }
- }
-
- fn entry_len(&self) -> usize {
- self.entries.lock().unwrap_or_else(|err| err.into_inner()).len()
- }
- }
-
- #[async_trait(?Send)]
- impl RadrootsClientDatastore for FlushDatastore {
- fn get_config(&self) -> RadrootsClientIdbConfig {
- IDB_CONFIG_DATASTORE
- }
-
- fn get_store_id(&self) -> &str {
- "test"
- }
-
- async fn init(&self) -> RadrootsClientDatastoreResult<()> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn set(&self, _key: &str, _value: &str) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn get(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn set_obj<T>(&self, key: &str, value: &T) -> RadrootsClientDatastoreResult<T>
- where
- T: serde::Serialize + serde::de::DeserializeOwned + Clone,
- {
- let serialized =
- serde_json::to_string(value).map_err(|_| RadrootsClientDatastoreError::NoResult)?;
- let mut entries = self.entries.lock().unwrap_or_else(|err| err.into_inner());
- entries.push(RadrootsClientDatastoreEntry::new(
- key.to_string(),
- Some(serialized),
- ));
- Ok(value.clone())
- }
-
- async fn update_obj<T>(&self, _key: &str, _value: &T) -> RadrootsClientDatastoreResult<T>
- where
- T: serde::Serialize + serde::de::DeserializeOwned + Clone,
- {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn get_obj<T>(&self, _key: &str) -> RadrootsClientDatastoreResult<T>
- where
- T: serde::de::DeserializeOwned,
- {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn del_obj(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn del(&self, key: &str) -> RadrootsClientDatastoreResult<String> {
- let mut entries = self.entries.lock().unwrap_or_else(|err| err.into_inner());
- entries.retain(|entry| entry.key != key);
- Ok(key.to_string())
- }
-
- async fn del_pref(&self, _key_prefix: &str) -> RadrootsClientDatastoreResult<Vec<String>> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn set_param(
- &self,
- _key: &str,
- _key_param: &str,
- _value: &str,
- ) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn get_param(
- &self,
- _key: &str,
- _key_param: &str,
- ) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn keys(&self) -> RadrootsClientDatastoreResult<Vec<String>> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn entries(&self) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> {
- let entries = self.entries.lock().unwrap_or_else(|err| err.into_inner());
- Ok(entries.clone())
- }
-
- async fn entries_pref(
- &self,
- key_prefix: &str,
- ) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> {
- let entries = self.entries.lock().unwrap_or_else(|err| err.into_inner());
- Ok(entries
- .iter()
- .filter(|entry| entry.key.starts_with(key_prefix))
- .cloned()
- .collect())
- }
-
- async fn reset(&self) -> RadrootsClientDatastoreResult<()> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn export_backup(
- &self,
- ) -> RadrootsClientDatastoreResult<RadrootsClientBackupDatastorePayload> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn import_backup(
- &self,
- _payload: RadrootsClientBackupDatastorePayload,
- ) -> RadrootsClientDatastoreResult<()> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
- }
-
- #[test]
- fn health_check_all_logged_flushes_buffer() {
- let _ = app_log_buffer_drain();
- let datastore = FlushDatastore::new();
- let keystore = TestKeystore {
- read_result: Err(RadrootsClientKeystoreError::MissingKey),
- };
- let notifications = crate::RadrootsAppNotifications::new(None);
- let tangle = crate::RadrootsAppTangleClientStub::new();
- let key_maps = crate::app_key_maps_default();
- let report = futures::executor::block_on(app_health_check_all_logged(
- &datastore,
- &keystore,
- ¬ifications,
- &tangle,
- &key_maps,
- false,
- ));
- assert_eq!(report.key_maps.status, RadrootsAppHealthCheckStatus::Ok);
- assert!(datastore.entry_len() > 0);
- }
-}
diff --git a/app/src/health_ui.rs b/app/src/health_ui.rs
@@ -1,160 +0,0 @@
-#![forbid(unsafe_code)]
-
-use leptos::prelude::{LocalStorage, RwSignal, Set};
-use leptos::task::spawn_local;
-
-use crate::{
- app_datastore_read_state,
- app_health_check_all,
- app_log_buffer_flush_deferred,
- app_state_notifications_permission_value,
- app_state_timestamp_ms,
- t,
- RadrootsAppConfig,
- RadrootsAppHealthCheckResult,
- RadrootsAppHealthCheckStatus,
- RadrootsAppHealthReport,
- RadrootsAppNotifications,
- RadrootsAppTangleClientStub,
-};
-use radroots_app_core::idb::IDB_CONFIG_LOGS;
-
-const APP_HEALTH_CHECK_DELAY_MS: u32 = 300;
-
-pub fn app_health_check_delay_ms() -> u32 {
- APP_HEALTH_CHECK_DELAY_MS
-}
-
-pub fn health_status_class(status: RadrootsAppHealthCheckStatus) -> &'static str {
- match status {
- RadrootsAppHealthCheckStatus::Ok => "status-ok",
- RadrootsAppHealthCheckStatus::Error => "status-error",
- RadrootsAppHealthCheckStatus::Skipped => "status-warn",
- }
-}
-
-pub fn health_status_label(status: RadrootsAppHealthCheckStatus) -> String {
- match status {
- RadrootsAppHealthCheckStatus::Ok => t!("app.home.health.status.ok"),
- RadrootsAppHealthCheckStatus::Error => t!("app.home.health.status.error"),
- RadrootsAppHealthCheckStatus::Skipped => t!("app.home.health.status.skipped"),
- }
-}
-
-pub fn health_message_label(message: &str) -> String {
- match message {
- "missing" => t!("app.home.health.message.missing"),
- "mismatch" => t!("app.home.health.message.mismatch"),
- "uninitialized" => t!("app.home.health.message.uninitialized"),
- "unavailable" => t!("app.home.health.message.unavailable"),
- _ => message.to_string(),
- }
-}
-
-pub fn health_result_label(result: &RadrootsAppHealthCheckResult) -> String {
- let status = health_status_label(result.status);
- match result.message.as_deref() {
- Some(message) => format!("{}: {}", status, health_message_label(message)),
- None => status,
- }
-}
-
-pub fn health_report_summary(report: &RadrootsAppHealthReport) -> RadrootsAppHealthCheckStatus {
- let statuses = [
- report.key_maps.status,
- report.bootstrap_state.status,
- report.state_active_key.status,
- report.notifications.status,
- report.tangle.status,
- report.datastore_roundtrip.status,
- report.keystore.status,
- ];
- if statuses
- .iter()
- .any(|status| matches!(status, RadrootsAppHealthCheckStatus::Error))
- {
- return RadrootsAppHealthCheckStatus::Error;
- }
- if statuses
- .iter()
- .any(|status| matches!(status, RadrootsAppHealthCheckStatus::Skipped))
- {
- return RadrootsAppHealthCheckStatus::Skipped;
- }
- RadrootsAppHealthCheckStatus::Ok
-}
-
-pub fn active_key_label(value: Option<String>) -> String {
- let Some(value) = value else {
- return t!("app.common.missing");
- };
- if value.len() <= 12 {
- return value;
- }
- let head = &value[..8];
- let tail = &value[value.len() - 4..];
- format!("{head}...{tail}")
-}
-
-fn logs_datastore() -> radroots_app_core::datastore::RadrootsClientWebDatastore {
- radroots_app_core::datastore::RadrootsClientWebDatastore::new(Some(IDB_CONFIG_LOGS))
-}
-
-pub fn spawn_health_checks(
- config: RadrootsAppConfig,
- setup_required: bool,
- health_report: RwSignal<RadrootsAppHealthReport, LocalStorage>,
- health_running: RwSignal<bool, LocalStorage>,
- active_key: RwSignal<Option<String>, LocalStorage>,
- notifications_status: RwSignal<Option<String>, LocalStorage>,
- last_run: RwSignal<Option<i64>, LocalStorage>,
-) {
- health_running.set(true);
- spawn_local(async move {
- let datastore = radroots_app_core::datastore::RadrootsClientWebDatastore::new(
- Some(config.datastore.idb_config),
- );
- let keystore = radroots_app_core::keystore::RadrootsClientWebKeystoreNostr::new(
- Some(config.keystore.nostr_store),
- );
- let notifications = RadrootsAppNotifications::new(None);
- let tangle = RadrootsAppTangleClientStub::new();
- let report = app_health_check_all(
- &datastore,
- &keystore,
- ¬ifications,
- &tangle,
- &config.datastore.key_maps,
- setup_required,
- )
- .await;
- let mut active_key_value = None;
- let mut notifications_value = None;
- if !setup_required {
- let app_data = app_datastore_read_state(&datastore, &config.datastore.key_maps)
- .await
- .ok();
- active_key_value = app_data.as_ref().and_then(|data| {
- if data.active_key.is_empty() {
- None
- } else {
- Some(data.active_key.clone())
- }
- });
- notifications_value = app_data
- .as_ref()
- .and_then(app_state_notifications_permission_value)
- .map(|permission| permission.as_str().to_string());
- }
- health_report.set(report);
- active_key.set(active_key_value);
- notifications_status.set(notifications_value);
- last_run.set(Some(app_state_timestamp_ms()));
- health_running.set(false);
- let key_maps = config.datastore.key_maps.clone();
- spawn_local(async move {
- let log_datastore = logs_datastore();
- let _ = app_log_buffer_flush_deferred(&log_datastore, &key_maps, true).await;
- });
- });
-}
diff --git a/app/src/i18n.rs b/app/src/i18n.rs
@@ -1,112 +0,0 @@
-#![forbid(unsafe_code)]
-
-use std::collections::BTreeMap;
-
-use leptos::prelude::{use_context, LocalStorage, RwSignal, StoredValue, WithUntracked, WithValue};
-
-use mf2_i18n_core::Args;
-use mf2_i18n_core::MessageId;
-use mf2_i18n_embedded::{EmbeddedPack, EmbeddedRuntime};
-use radroots_app_lib::get_locale;
-
-#[derive(Clone, Copy)]
-pub struct RadrootsAppI18nContext {
- pub locale: RwSignal<String, LocalStorage>,
- pub runtime: StoredValue<Option<EmbeddedRuntime>>,
-}
-
-pub fn app_i18n_init() -> RadrootsAppI18nContext {
- let locale = get_locale(&["en"]);
- let locale = RwSignal::new_local(locale);
- let runtime = StoredValue::new(load_embedded_runtime());
- RadrootsAppI18nContext { locale, runtime }
-}
-
-pub fn app_i18n() -> Option<RadrootsAppI18nContext> {
- use_context::<RadrootsAppI18nContext>()
-}
-
-pub fn translate(key: &str) -> String {
- let Some(ctx) = app_i18n() else {
- return key.to_string();
- };
- let locale = ctx.locale.with_untracked(|value| value.clone());
- ctx.runtime.with_value(|runtime: &Option<EmbeddedRuntime>| {
- if let Some(runtime) = runtime.as_ref() {
- let args = Args::new();
- runtime
- .format(&locale, key, &args)
- .unwrap_or_else(|_| key.to_string())
- } else {
- key.to_string()
- }
- })
-}
-
-fn load_embedded_runtime() -> Option<EmbeddedRuntime> {
- let id_map = load_id_map()?;
- let id_map_hash = load_id_map_hash()?;
- let packs = [EmbeddedPack {
- locale: "en",
- bytes: load_pack_en(),
- }];
- EmbeddedRuntime::new(id_map, id_map_hash, &packs, "en").ok()
-}
-
-fn load_id_map() -> Option<BTreeMap<String, MessageId>> {
- let raw = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/i18n/build/id_map.json"));
- let parsed: BTreeMap<String, u32> = serde_json::from_slice(raw).ok()?;
- let mut map = BTreeMap::new();
- for (key, id) in parsed {
- map.insert(key, MessageId::new(id));
- }
- Some(map)
-}
-
-fn load_id_map_hash() -> Option<[u8; 32]> {
- let raw = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/i18n/build/id_map_hash"));
- let text = std::str::from_utf8(raw).ok()?;
- let value = text.trim();
- let hex_value = value.strip_prefix("sha256:").unwrap_or(value);
- let bytes = hex::decode(hex_value).ok()?;
- if bytes.len() != 32 {
- return None;
- }
- let mut out = [0u8; 32];
- out.copy_from_slice(&bytes);
- Some(out)
-}
-
-fn load_pack_en() -> &'static [u8] {
- include_bytes!(concat!(
- env!("CARGO_MANIFEST_DIR"),
- "/i18n/build/packs/en.mf2pack"
- ))
-}
-
-#[macro_export]
-macro_rules! t {
- ($key:literal) => {
- $crate::i18n::translate($key)
- };
-}
-
-#[cfg(test)]
-mod tests {
- use super::{app_i18n, app_i18n_init, translate};
- use leptos::prelude::{provide_context, Owner};
-
- #[test]
- fn translate_falls_back_without_context() {
- assert_eq!(translate("hello"), "hello");
- }
-
- #[test]
- fn translate_reads_context() {
- let owner = Owner::new();
- owner.set();
- provide_context(app_i18n_init());
- assert!(app_i18n().is_some());
- assert_eq!(translate("hello"), "hello");
- }
-}
diff --git a/app/src/init.rs b/app/src/init.rs
@@ -1,1024 +0,0 @@
-#![forbid(unsafe_code)]
-
-use std::fmt;
-use std::rc::Rc;
-
-use radroots_app_core::datastore::{
- RadrootsClientDatastore,
- RadrootsClientDatastoreError,
- RadrootsClientWebDatastore,
-};
-use radroots_app_core::idb::{
- idb_store_bootstrap,
- RadrootsClientIdbStoreError,
- RADROOTS_IDB_DATABASE,
-};
-use radroots_app_core::keystore::{
- RadrootsClientKeystoreError,
- RadrootsClientKeystoreNostr,
- RadrootsClientWebKeystoreNostr,
-};
-
-use crate::{
- app_datastore_has_state,
- app_datastore_read_state,
- app_assets_geocoder_db_url,
- app_assets_sql_wasm_url,
- app_log_debug_emit,
- app_state_is_initialized,
- RadrootsAppStateError,
- RadrootsAppConfig,
- RadrootsAppConfigError,
- RadrootsAppKeyMapConfig,
- RadrootsAppSetupStatus,
- APP_EULA_HASH,
- APP_EULA_VERSION,
-};
-
-#[cfg(target_arch = "wasm32")]
-use leptos::prelude::window;
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen::JsCast;
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen_futures::JsFuture;
-#[cfg(target_arch = "wasm32")]
-use js_sys::Uint8Array;
-#[cfg(target_arch = "wasm32")]
-use js_sys::Date;
-#[cfg(target_arch = "wasm32")]
-use web_sys::Response;
-#[cfg(not(target_arch = "wasm32"))]
-use std::time::Instant;
-
-pub const APP_INIT_STORAGE_KEY: &str = "radroots.app.init.ready";
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppInitStage {
- Idle,
- Storage,
- DownloadSql,
- DownloadGeo,
- Database,
- Geocoder,
- Ready,
- Error,
-}
-
-impl RadrootsAppInitStage {
- pub const fn as_str(self) -> &'static str {
- match self {
- RadrootsAppInitStage::Idle => "idle",
- RadrootsAppInitStage::Storage => "storage",
- RadrootsAppInitStage::DownloadSql => "download_sql",
- RadrootsAppInitStage::DownloadGeo => "download_geo",
- RadrootsAppInitStage::Database => "database",
- RadrootsAppInitStage::Geocoder => "geocoder",
- RadrootsAppInitStage::Ready => "ready",
- RadrootsAppInitStage::Error => "error",
- }
- }
-
- pub fn parse(value: &str) -> Option<Self> {
- match value {
- "idle" => Some(RadrootsAppInitStage::Idle),
- "storage" => Some(RadrootsAppInitStage::Storage),
- "download_sql" => Some(RadrootsAppInitStage::DownloadSql),
- "download_geo" => Some(RadrootsAppInitStage::DownloadGeo),
- "database" => Some(RadrootsAppInitStage::Database),
- "geocoder" => Some(RadrootsAppInitStage::Geocoder),
- "ready" => Some(RadrootsAppInitStage::Ready),
- "error" => Some(RadrootsAppInitStage::Error),
- _ => None,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsAppInitState {
- pub stage: RadrootsAppInitStage,
- pub loaded_bytes: u64,
- pub total_bytes: Option<u64>,
-}
-
-pub const fn app_init_state_default() -> RadrootsAppInitState {
- RadrootsAppInitState {
- stage: RadrootsAppInitStage::Idle,
- loaded_bytes: 0,
- total_bytes: Some(0),
- }
-}
-
-pub fn app_init_stage_set(state: &mut RadrootsAppInitState, stage: RadrootsAppInitStage) {
- state.stage = stage;
-}
-
-pub fn app_init_progress_add(state: &mut RadrootsAppInitState, bytes: u64) {
- if bytes == 0 {
- return;
- }
- state.loaded_bytes = state.loaded_bytes.saturating_add(bytes);
-}
-
-pub fn app_init_total_add(state: &mut RadrootsAppInitState, bytes: u64) {
- if bytes == 0 {
- return;
- }
- let Some(total) = state.total_bytes else {
- return;
- };
- state.total_bytes = Some(total.saturating_add(bytes));
-}
-
-pub fn app_init_total_unknown(state: &mut RadrootsAppInitState) {
- state.total_bytes = None;
-}
-
-#[cfg(target_arch = "wasm32")]
-fn app_init_timer_start() -> u64 {
- Date::now() as u64
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-fn app_init_timer_start() -> Instant {
- Instant::now()
-}
-
-#[cfg(target_arch = "wasm32")]
-fn app_init_elapsed_ms(start: u64) -> u64 {
- let now = Date::now() as u64;
- now.saturating_sub(start)
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-fn app_init_elapsed_ms(start: Instant) -> u64 {
- start.elapsed().as_millis() as u64
-}
-
-fn app_init_timing_context(label: &str, elapsed_ms: u64) -> String {
- format!("{label}_ms={elapsed_ms}")
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub struct RadrootsAppInitAssetProgress {
- pub loaded_bytes: u64,
- pub total_bytes: Option<u64>,
-}
-
-impl RadrootsAppInitAssetProgress {
- pub const fn empty() -> Self {
- Self {
- loaded_bytes: 0,
- total_bytes: Some(0),
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppInitAssetError {
- MissingUrl,
- FetchUnavailable,
- FetchFailed,
-}
-
-impl RadrootsAppInitAssetError {
- pub const fn message(self) -> &'static str {
- match self {
- RadrootsAppInitAssetError::MissingUrl => "error.app.init.asset_missing_url",
- RadrootsAppInitAssetError::FetchUnavailable => "error.app.init.asset_unavailable",
- RadrootsAppInitAssetError::FetchFailed => "error.app.init.asset_fetch_failed",
- }
- }
-}
-
-impl fmt::Display for RadrootsAppInitAssetError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.write_str(self.message())
- }
-}
-
-impl std::error::Error for RadrootsAppInitAssetError {}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn app_init_fetch_asset<F>(
- url: &str,
- mut on_progress: F,
-) -> Result<RadrootsAppInitAssetProgress, RadrootsAppInitAssetError>
-where
- F: FnMut(u64, Option<u64>),
-{
- if url.is_empty() {
- return Err(RadrootsAppInitAssetError::MissingUrl);
- }
- let response_value = JsFuture::from(window().fetch_with_str(url))
- .await
- .map_err(|_| RadrootsAppInitAssetError::FetchFailed)?;
- let response: Response = response_value
- .dyn_into()
- .map_err(|_| RadrootsAppInitAssetError::FetchFailed)?;
- let total_bytes = response
- .headers()
- .get("content-length")
- .ok()
- .flatten()
- .and_then(|value| value.parse::<u64>().ok());
- let buffer_value = JsFuture::from(response.array_buffer().map_err(|_| RadrootsAppInitAssetError::FetchFailed)?)
- .await
- .map_err(|_| RadrootsAppInitAssetError::FetchFailed)?;
- let buffer = Uint8Array::new(&buffer_value);
- let loaded_bytes = buffer.length() as u64;
- on_progress(loaded_bytes, total_bytes);
- Ok(RadrootsAppInitAssetProgress {
- loaded_bytes,
- total_bytes,
- })
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub async fn app_init_fetch_asset<F>(
- url: &str,
- _on_progress: F,
-) -> Result<RadrootsAppInitAssetProgress, RadrootsAppInitAssetError>
-where
- F: FnMut(u64, Option<u64>),
-{
- if url.is_empty() {
- return Err(RadrootsAppInitAssetError::MissingUrl);
- }
- Err(RadrootsAppInitAssetError::FetchUnavailable)
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppInitError {
- Idb(RadrootsClientIdbStoreError),
- Datastore(RadrootsClientDatastoreError),
- Keystore(RadrootsClientKeystoreError),
- Config(RadrootsAppConfigError),
- Assets(RadrootsAppInitAssetError),
- State(RadrootsAppStateError),
-}
-
-pub type RadrootsAppInitErrorMessage = &'static str;
-
-impl RadrootsAppInitError {
- pub const fn message(&self) -> RadrootsAppInitErrorMessage {
- match self {
- RadrootsAppInitError::Idb(_) => "error.app.init.idb",
- RadrootsAppInitError::Datastore(_) => "error.app.init.datastore",
- RadrootsAppInitError::Keystore(_) => "error.app.init.keystore",
- RadrootsAppInitError::Config(_) => "error.app.init.config",
- RadrootsAppInitError::Assets(_) => "error.app.init.assets",
- RadrootsAppInitError::State(err) => err.message(),
- }
- }
-}
-
-impl fmt::Display for RadrootsAppInitError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.write_str(self.message())
- }
-}
-
-impl std::error::Error for RadrootsAppInitError {}
-
-pub struct RadrootsAppBackends {
- pub config: RadrootsAppConfig,
- pub datastore: Rc<RadrootsClientWebDatastore>,
- pub nostr_keystore: RadrootsClientWebKeystoreNostr,
-}
-
-pub type RadrootsAppInitResult<T> = Result<T, RadrootsAppInitError>;
-
-pub async fn app_init_assets<F, G>(
- config: &RadrootsAppConfig,
- mut on_stage: F,
- mut on_progress: G,
-) -> Result<(), RadrootsAppInitAssetError>
-where
- F: FnMut(RadrootsAppInitStage),
- G: FnMut(u64, Option<u64>),
-{
- let _ = app_log_debug_emit("log.app.init.assets", "start", None);
- if let Some(url) = app_assets_sql_wasm_url(config).filter(|value| !value.is_empty()) {
- let _ = app_log_debug_emit("log.app.init.assets.sql", "download_start", Some(url.to_string()));
- on_stage(RadrootsAppInitStage::DownloadSql);
- app_init_fetch_asset(url, |loaded, total| {
- on_progress(loaded, total);
- })
- .await?;
- let _ = app_log_debug_emit("log.app.init.assets.sql", "download_done", None);
- }
- if let Some(url) = app_assets_geocoder_db_url(config).filter(|value| !value.is_empty()) {
- let _ = app_log_debug_emit("log.app.init.assets.geo", "download_start", Some(url.to_string()));
- on_stage(RadrootsAppInitStage::DownloadGeo);
- app_init_fetch_asset(url, |loaded, total| {
- on_progress(loaded, total);
- })
- .await?;
- let _ = app_log_debug_emit("log.app.init.assets.geo", "download_done", None);
- }
- let _ = app_log_debug_emit("log.app.init.assets", "done", None);
- Ok(())
-}
-
-pub fn app_init_has_completed() -> bool {
- #[cfg(target_arch = "wasm32")]
- {
- let window = window();
- match window.local_storage() {
- Ok(Some(storage)) => match storage.get_item(APP_INIT_STORAGE_KEY) {
- Ok(Some(value)) => value == "1",
- _ => false,
- },
- _ => false,
- }
- }
- #[cfg(not(target_arch = "wasm32"))]
- {
- false
- }
-}
-
-pub fn app_init_mark_completed() {
- #[cfg(target_arch = "wasm32")]
- {
- let window = window();
- if let Ok(Some(storage)) = window.local_storage() {
- let _ = storage.set_item(APP_INIT_STORAGE_KEY, "1");
- }
- }
-}
-
-pub async fn app_init_reset<T: RadrootsClientDatastore, K: RadrootsClientKeystoreNostr>(
- datastore: Option<&T>,
- _key_maps: Option<&RadrootsAppKeyMapConfig>,
- keystore: Option<&K>,
-) -> RadrootsAppInitResult<()> {
- let _ = app_log_debug_emit("log.app.init.reset", "start", None);
- if let Some(datastore) = datastore {
- datastore.reset().await.map_err(RadrootsAppInitError::Datastore)?;
- }
- if let Some(keystore) = keystore {
- keystore.reset().await.map_err(RadrootsAppInitError::Keystore)?;
- }
- #[cfg(target_arch = "wasm32")]
- {
- let window = window();
- if let Ok(Some(storage)) = window.local_storage() {
- let _ = storage.remove_item(APP_INIT_STORAGE_KEY);
- }
- }
- let _ = app_log_debug_emit("log.app.init.reset", "done", None);
- Ok(())
-}
-
-pub async fn app_init_needs_setup<T: RadrootsClientDatastore, K: RadrootsClientKeystoreNostr>(
- datastore: &T,
- keystore: &K,
- key_maps: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppInitResult<bool> {
- let has_state = app_datastore_has_state(datastore, key_maps).await?;
- if !has_state {
- return Ok(true);
- }
- let state = app_datastore_read_state(datastore, key_maps).await?;
- if !app_state_is_initialized(&state) {
- return Ok(true);
- }
- match keystore.read(&state.active_key).await {
- Ok(_) => Ok(false),
- Err(RadrootsClientKeystoreError::MissingKey) => Ok(true),
- Err(RadrootsClientKeystoreError::NostrNoResults) => Ok(true),
- Err(err) => Err(RadrootsAppInitError::Keystore(err)),
- }
-}
-
-pub async fn app_init_setup_status<T: RadrootsClientDatastore, K: RadrootsClientKeystoreNostr>(
- datastore: &T,
- keystore: &K,
- key_maps: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppInitResult<RadrootsAppSetupStatus> {
- let has_state = app_datastore_has_state(datastore, key_maps).await?;
- if !has_state {
- return Ok(RadrootsAppSetupStatus::Required);
- }
- let state = match app_datastore_read_state(datastore, key_maps).await {
- Ok(state) => state,
- Err(RadrootsAppInitError::State(RadrootsAppStateError::Corrupt))
- | Err(RadrootsAppInitError::State(RadrootsAppStateError::UnsupportedVersion(_))) => {
- return Ok(RadrootsAppSetupStatus::Corrupt);
- }
- Err(err) => return Err(err),
- };
- if !app_state_is_initialized(&state) {
- return Ok(RadrootsAppSetupStatus::Required);
- }
- if state.eula_version != APP_EULA_VERSION || state.eula_hash != APP_EULA_HASH {
- return Ok(RadrootsAppSetupStatus::Corrupt);
- }
- match keystore.read(&state.active_key).await {
- Ok(_) => Ok(RadrootsAppSetupStatus::Configured),
- Err(RadrootsClientKeystoreError::MissingKey)
- | Err(RadrootsClientKeystoreError::NostrNoResults) => {
- Ok(RadrootsAppSetupStatus::Corrupt)
- }
- Err(err) => Err(RadrootsAppInitError::Keystore(err)),
- }
-}
-
-pub async fn app_init_backends(config: RadrootsAppConfig) -> RadrootsAppInitResult<RadrootsAppBackends> {
- let _ = app_log_debug_emit("log.app.init.backends", "start", None);
- config.validate().map_err(RadrootsAppInitError::Config)?;
- let idb_start = app_init_timer_start();
- idb_store_bootstrap(RADROOTS_IDB_DATABASE, None)
- .await
- .map_err(RadrootsAppInitError::Idb)?;
- let idb_ms = app_init_elapsed_ms(idb_start);
- let _ = app_log_debug_emit(
- "log.app.init.backends",
- "idb_bootstrap",
- Some(app_init_timing_context("elapsed", idb_ms)),
- );
- let datastore = Rc::new(RadrootsClientWebDatastore::new(Some(config.datastore.idb_config)));
- datastore
- .init()
- .await
- .map_err(RadrootsAppInitError::Datastore)?;
- let _ = app_log_debug_emit("log.app.init.backends", "datastore_ready", None);
- let nostr_keystore = RadrootsClientWebKeystoreNostr::new(Some(config.keystore.nostr_store));
- Ok(RadrootsAppBackends {
- config,
- datastore,
- nostr_keystore,
- })
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- app_init_backends,
- app_init_assets,
- app_init_needs_setup,
- app_init_setup_status,
- app_init_timing_context,
- app_init_progress_add,
- app_init_state_default,
- app_init_stage_set,
- app_init_total_add,
- app_init_total_unknown,
- RadrootsAppInitError,
- RadrootsAppInitErrorMessage,
- RadrootsAppInitStage,
- RadrootsAppInitAssetError,
- };
- use crate::{
- app_config_default,
- app_key_maps_default,
- RadrootsAppConfig,
- RadrootsAppState,
- RadrootsAppStateError,
- RadrootsAppStateRecord,
- RadrootsAppSetupStatus,
- APP_EULA_HASH,
- APP_EULA_VERSION,
- };
- use radroots_app_core::datastore::{
- RadrootsClientDatastore,
- RadrootsClientDatastoreEntries,
- RadrootsClientDatastoreError,
- RadrootsClientDatastoreResult,
- };
- use radroots_app_core::idb::RadrootsClientIdbStoreError;
- use radroots_app_core::keystore::{
- RadrootsClientKeystoreError,
- RadrootsClientKeystoreNostr,
- RadrootsClientKeystoreResult,
- };
- use async_trait::async_trait;
- use crate::RadrootsAppConfigError;
- use radroots_app_core::backup::RadrootsClientBackupDatastorePayload;
- use radroots_app_core::idb::{RadrootsClientIdbConfig, IDB_CONFIG_DATASTORE};
- use serde::{de::DeserializeOwned, Serialize};
-
- #[test]
- fn app_init_error_messages_match_spec() {
- let cases: &[(RadrootsAppInitError, RadrootsAppInitErrorMessage)] = &[
- (
- RadrootsAppInitError::Idb(RadrootsClientIdbStoreError::IdbUndefined),
- "error.app.init.idb",
- ),
- (
- RadrootsAppInitError::Datastore(RadrootsClientDatastoreError::IdbUndefined),
- "error.app.init.datastore",
- ),
- (
- RadrootsAppInitError::Keystore(RadrootsClientKeystoreError::IdbUndefined),
- "error.app.init.keystore",
- ),
- (
- RadrootsAppInitError::Config(RadrootsAppConfigError::MissingKeyMap("nostr_key")),
- "error.app.init.config",
- ),
- (
- RadrootsAppInitError::Assets(RadrootsAppInitAssetError::FetchUnavailable),
- "error.app.init.assets",
- ),
- (
- RadrootsAppInitError::State(RadrootsAppStateError::Missing),
- "error.app.state.missing",
- ),
- ];
- for (err, expected) in cases {
- assert_eq!(err.message(), *expected);
- assert_eq!(err.to_string(), *expected);
- }
- }
-
- #[test]
- fn app_init_backends_maps_idb_errors() {
- let err = match futures::executor::block_on(app_init_backends(app_config_default())) {
- Ok(_) => panic!("idb bootstrap should error on non-wasm"),
- Err(err) => err,
- };
- assert_eq!(
- err,
- RadrootsAppInitError::Idb(RadrootsClientIdbStoreError::IdbUndefined)
- );
- }
-
- #[test]
- fn app_init_timing_context_formats_elapsed() {
- let context = app_init_timing_context("idb", 123);
- assert_eq!(context, "idb_ms=123");
- }
-
- #[test]
- fn app_init_has_completed_is_false_on_native() {
- assert!(!super::app_init_has_completed());
- }
-
- #[test]
- fn app_init_reset_is_noop_on_native() {
- super::app_init_mark_completed();
- let result = futures::executor::block_on(super::app_init_reset::<
- radroots_app_core::datastore::RadrootsClientWebDatastore,
- TestKeystore,
- >(None, None, None));
- assert!(result.is_ok());
- }
-
- #[test]
- fn app_init_reset_maps_datastore_errors() {
- let datastore = radroots_app_core::datastore::RadrootsClientWebDatastore::new(None);
- let err = futures::executor::block_on(super::app_init_reset::<
- radroots_app_core::datastore::RadrootsClientWebDatastore,
- TestKeystore,
- >(Some(&datastore), None, None))
- .expect_err("datastore reset should error on native");
- assert_eq!(
- err,
- RadrootsAppInitError::Datastore(RadrootsClientDatastoreError::IdbUndefined)
- );
- }
-
- struct TestKeystore;
-
- #[async_trait(?Send)]
- impl RadrootsClientKeystoreNostr for TestKeystore {
- async fn generate(&self) -> RadrootsClientKeystoreResult<String> {
- Err(RadrootsClientKeystoreError::IdbUndefined)
- }
-
- async fn add(&self, _secret_key: &str) -> RadrootsClientKeystoreResult<String> {
- Err(RadrootsClientKeystoreError::IdbUndefined)
- }
-
- async fn read(&self, _public_key: &str) -> RadrootsClientKeystoreResult<String> {
- Err(RadrootsClientKeystoreError::IdbUndefined)
- }
-
- async fn keys(&self) -> RadrootsClientKeystoreResult<Vec<String>> {
- Err(RadrootsClientKeystoreError::IdbUndefined)
- }
-
- async fn remove(&self, _public_key: &str) -> RadrootsClientKeystoreResult<String> {
- Err(RadrootsClientKeystoreError::IdbUndefined)
- }
-
- async fn reset(&self) -> RadrootsClientKeystoreResult<()> {
- Err(RadrootsClientKeystoreError::IdbUndefined)
- }
- }
-
- #[test]
- fn app_init_reset_maps_keystore_errors() {
- let keystore = TestKeystore;
- let err = futures::executor::block_on(super::app_init_reset::<
- radroots_app_core::datastore::RadrootsClientWebDatastore,
- TestKeystore,
- >(None, None, Some(&keystore)))
- .expect_err("keystore reset should error on native");
- assert_eq!(
- err,
- RadrootsAppInitError::Keystore(RadrootsClientKeystoreError::IdbUndefined)
- );
- }
-
- #[test]
- fn app_init_stage_roundtrip() {
- let stage = RadrootsAppInitStage::Ready;
- assert_eq!(stage.as_str(), "ready");
- assert_eq!(RadrootsAppInitStage::parse("ready"), Some(stage));
- assert_eq!(RadrootsAppInitStage::parse("unknown"), None);
- }
-
- #[test]
- fn app_init_state_defaults_match_spec() {
- let state = app_init_state_default();
- assert_eq!(state.stage, RadrootsAppInitStage::Idle);
- assert_eq!(state.loaded_bytes, 0);
- assert_eq!(state.total_bytes, Some(0));
- }
-
- #[test]
- fn app_init_progress_helpers_update_state() {
- let mut state = app_init_state_default();
- app_init_stage_set(&mut state, RadrootsAppInitStage::Storage);
- assert_eq!(state.stage, RadrootsAppInitStage::Storage);
- app_init_progress_add(&mut state, 0);
- assert_eq!(state.loaded_bytes, 0);
- app_init_progress_add(&mut state, 5);
- assert_eq!(state.loaded_bytes, 5);
- app_init_total_add(&mut state, 10);
- assert_eq!(state.total_bytes, Some(10));
- app_init_total_unknown(&mut state);
- assert_eq!(state.total_bytes, None);
- app_init_total_add(&mut state, 5);
- assert_eq!(state.total_bytes, None);
- }
-
- #[test]
- fn app_init_assets_skips_when_empty() {
- let config = app_config_default();
- let mut stages = Vec::new();
- let mut progress = Vec::new();
- let result = futures::executor::block_on(app_init_assets(
- &config,
- |stage| stages.push(stage),
- |loaded, total| progress.push((loaded, total)),
- ));
- assert!(result.is_ok());
- assert!(stages.is_empty());
- assert!(progress.is_empty());
- }
-
- #[test]
- fn app_init_assets_reports_unavailable_on_native() {
- let mut config = RadrootsAppConfig::empty();
- config.assets.sql_wasm_url = Some("http://example.com/sql.wasm".to_string());
- let result = futures::executor::block_on(app_init_assets(
- &config,
- |_stage| {},
- |_loaded, _total| {},
- ))
- .expect_err("asset fetch should error on native");
- assert_eq!(result, RadrootsAppInitAssetError::FetchUnavailable);
- }
-
- use std::cell::RefCell;
-
- struct SetupDatastore {
- state: Option<RadrootsAppState>,
- record: RefCell<Option<RadrootsAppStateRecord>>,
- }
-
- #[async_trait(?Send)]
- impl RadrootsClientDatastore for SetupDatastore {
- fn get_config(&self) -> RadrootsClientIdbConfig {
- IDB_CONFIG_DATASTORE
- }
-
- fn get_store_id(&self) -> &str {
- "test"
- }
-
- async fn init(&self) -> RadrootsClientDatastoreResult<()> {
- Ok(())
- }
-
- async fn set(&self, _key: &str, _value: &str) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn get(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn set_obj<T>(
- &self,
- _key: &str,
- value: &T,
- ) -> RadrootsClientDatastoreResult<T>
- where
- T: Serialize + DeserializeOwned + Clone,
- {
- let encoded = serde_json::to_string(value)
- .map_err(|_| RadrootsClientDatastoreError::IdbUndefined)?;
- if let Ok(parsed) = serde_json::from_str::<RadrootsAppStateRecord>(&encoded) {
- *self.record.borrow_mut() = Some(parsed);
- return Ok(value.clone());
- }
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn update_obj<T>(
- &self,
- _key: &str,
- _value: &T,
- ) -> RadrootsClientDatastoreResult<T>
- where
- T: Serialize + DeserializeOwned + Clone,
- {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn get_obj<T>(&self, _key: &str) -> RadrootsClientDatastoreResult<T>
- where
- T: DeserializeOwned,
- {
- if let Some(record) = self.record.borrow().as_ref() {
- let encoded = serde_json::to_string(record)
- .map_err(|_| RadrootsClientDatastoreError::NoResult)?;
- if let Ok(parsed) = serde_json::from_str(&encoded) {
- return Ok(parsed);
- }
- };
- let Some(state) = self.state.as_ref() else {
- return Err(RadrootsClientDatastoreError::NoResult);
- };
- let encoded = serde_json::to_string(state)
- .map_err(|_| RadrootsClientDatastoreError::NoResult)?;
- serde_json::from_str(&encoded).map_err(|_| RadrootsClientDatastoreError::NoResult)
- }
-
- async fn del_obj(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn del(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn del_pref(&self, _key_prefix: &str) -> RadrootsClientDatastoreResult<Vec<String>> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn set_param(
- &self,
- _key: &str,
- _key_param: &str,
- _value: &str,
- ) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn get_param(
- &self,
- _key: &str,
- _key_param: &str,
- ) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn keys(&self) -> RadrootsClientDatastoreResult<Vec<String>> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn entries(&self) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn entries_pref(
- &self,
- _key_prefix: &str,
- ) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn reset(&self) -> RadrootsClientDatastoreResult<()> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn export_backup(
- &self,
- ) -> RadrootsClientDatastoreResult<RadrootsClientBackupDatastorePayload> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn import_backup(
- &self,
- _payload: RadrootsClientBackupDatastorePayload,
- ) -> RadrootsClientDatastoreResult<()> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
- }
-
- struct SetupKeystore {
- read_result: RadrootsClientKeystoreResult<String>,
- }
-
- #[async_trait(?Send)]
- impl RadrootsClientKeystoreNostr for SetupKeystore {
- async fn generate(&self) -> RadrootsClientKeystoreResult<String> {
- Err(RadrootsClientKeystoreError::IdbUndefined)
- }
-
- async fn add(&self, _secret_key: &str) -> RadrootsClientKeystoreResult<String> {
- Err(RadrootsClientKeystoreError::IdbUndefined)
- }
-
- async fn read(&self, _public_key: &str) -> RadrootsClientKeystoreResult<String> {
- self.read_result.clone()
- }
-
- async fn keys(&self) -> RadrootsClientKeystoreResult<Vec<String>> {
- Err(RadrootsClientKeystoreError::IdbUndefined)
- }
-
- async fn remove(&self, _public_key: &str) -> RadrootsClientKeystoreResult<String> {
- Err(RadrootsClientKeystoreError::IdbUndefined)
- }
-
- async fn reset(&self) -> RadrootsClientKeystoreResult<()> {
- Err(RadrootsClientKeystoreError::IdbUndefined)
- }
- }
-
- fn ready_state() -> RadrootsAppState {
- let mut state = RadrootsAppState::default();
- state.active_key = "pub".to_string();
- state.eula_date = "2025-01-01T00:00:00Z".to_string();
- state.eula_version = APP_EULA_VERSION.to_string();
- state.eula_hash = APP_EULA_HASH.to_string();
- state
- }
-
- #[test]
- fn app_init_needs_setup_when_state_missing() {
- let datastore = SetupDatastore {
- state: None,
- record: RefCell::new(None),
- };
- let keystore = SetupKeystore {
- read_result: Ok("secret".to_string()),
- };
- let key_maps = app_key_maps_default();
- let needs_setup = futures::executor::block_on(app_init_needs_setup(
- &datastore,
- &keystore,
- &key_maps,
- ))
- .expect("needs setup");
- assert!(needs_setup);
- }
-
- #[test]
- fn app_init_needs_setup_when_state_incomplete() {
- let datastore = SetupDatastore {
- state: Some(RadrootsAppState::default()),
- record: RefCell::new(None),
- };
- let keystore = SetupKeystore {
- read_result: Ok("secret".to_string()),
- };
- let key_maps = app_key_maps_default();
- let needs_setup = futures::executor::block_on(app_init_needs_setup(
- &datastore,
- &keystore,
- &key_maps,
- ))
- .expect("needs setup");
- assert!(needs_setup);
- }
-
- #[test]
- fn app_init_needs_setup_when_keystore_missing() {
- let mut state = RadrootsAppState::default();
- state.active_key = "pub".to_string();
- state.eula_date = "2025-01-01T00:00:00Z".to_string();
- let datastore = SetupDatastore {
- state: Some(state),
- record: RefCell::new(None),
- };
- let keystore = SetupKeystore {
- read_result: Err(RadrootsClientKeystoreError::MissingKey),
- };
- let key_maps = app_key_maps_default();
- let needs_setup = futures::executor::block_on(app_init_needs_setup(
- &datastore,
- &keystore,
- &key_maps,
- ))
- .expect("needs setup");
- assert!(needs_setup);
- }
-
- #[test]
- fn app_init_needs_setup_is_false_when_ready() {
- let state = ready_state();
- let datastore = SetupDatastore {
- state: Some(state),
- record: RefCell::new(None),
- };
- let keystore = SetupKeystore {
- read_result: Ok("secret".to_string()),
- };
- let key_maps = app_key_maps_default();
- let needs_setup = futures::executor::block_on(app_init_needs_setup(
- &datastore,
- &keystore,
- &key_maps,
- ))
- .expect("needs setup");
- assert!(!needs_setup);
- }
-
- #[test]
- fn app_init_setup_status_required_when_state_missing() {
- let datastore = SetupDatastore {
- state: None,
- record: RefCell::new(None),
- };
- let keystore = SetupKeystore {
- read_result: Ok("secret".to_string()),
- };
- let key_maps = app_key_maps_default();
- let status = futures::executor::block_on(app_init_setup_status(
- &datastore,
- &keystore,
- &key_maps,
- ))
- .expect("setup status");
- assert_eq!(status, RadrootsAppSetupStatus::Required);
- }
-
- #[test]
- fn app_init_setup_status_corrupt_when_eula_mismatch() {
- let mut state = ready_state();
- state.eula_version = "0.0.0".to_string();
- let datastore = SetupDatastore {
- state: Some(state),
- record: RefCell::new(None),
- };
- let keystore = SetupKeystore {
- read_result: Ok("secret".to_string()),
- };
- let key_maps = app_key_maps_default();
- let status = futures::executor::block_on(app_init_setup_status(
- &datastore,
- &keystore,
- &key_maps,
- ))
- .expect("setup status");
- assert_eq!(status, RadrootsAppSetupStatus::Corrupt);
- }
-
- #[test]
- fn app_init_setup_status_corrupt_when_keystore_missing() {
- let state = ready_state();
- let datastore = SetupDatastore {
- state: Some(state),
- record: RefCell::new(None),
- };
- let keystore = SetupKeystore {
- read_result: Err(RadrootsClientKeystoreError::MissingKey),
- };
- let key_maps = app_key_maps_default();
- let status = futures::executor::block_on(app_init_setup_status(
- &datastore,
- &keystore,
- &key_maps,
- ))
- .expect("setup status");
- assert_eq!(status, RadrootsAppSetupStatus::Corrupt);
- }
-
- #[test]
- fn app_init_setup_status_configured_when_ready() {
- let state = ready_state();
- let datastore = SetupDatastore {
- state: Some(state),
- record: RefCell::new(None),
- };
- let keystore = SetupKeystore {
- read_result: Ok("secret".to_string()),
- };
- let key_maps = app_key_maps_default();
- let status = futures::executor::block_on(app_init_setup_status(
- &datastore,
- &keystore,
- &key_maps,
- ))
- .expect("setup status");
- assert_eq!(status, RadrootsAppSetupStatus::Configured);
- }
-}
diff --git a/app/src/keystore.rs b/app/src/keystore.rs
@@ -1,257 +0,0 @@
-#![forbid(unsafe_code)]
-
-use radroots_app_core::keystore::{RadrootsClientKeystoreError, RadrootsClientKeystoreNostr};
-use radroots_nostr::prelude::{RadrootsNostrKeys, RadrootsNostrSecretKey};
-
-use crate::app_log_debug_emit;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppKeystoreError {
- Keystore(RadrootsClientKeystoreError),
- KeyMismatch,
-}
-
-pub type RadrootsAppKeystoreResult<T> = Result<T, RadrootsAppKeystoreError>;
-
-impl RadrootsAppKeystoreError {
- pub const fn message(&self) -> &'static str {
- match self {
- RadrootsAppKeystoreError::Keystore(err) => err.message(),
- RadrootsAppKeystoreError::KeyMismatch => "error.app.keystore.key_mismatch",
- }
- }
-}
-
-impl std::fmt::Display for RadrootsAppKeystoreError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_str(self.message())
- }
-}
-
-impl std::error::Error for RadrootsAppKeystoreError {}
-
-impl From<RadrootsClientKeystoreError> for RadrootsAppKeystoreError {
- fn from(err: RadrootsClientKeystoreError) -> Self {
- RadrootsAppKeystoreError::Keystore(err)
- }
-}
-
-pub async fn app_keystore_nostr_keys<T: RadrootsClientKeystoreNostr>(
- keystore: &T,
-) -> RadrootsAppKeystoreResult<Vec<String>> {
- let result = keystore.keys().await.map_err(RadrootsAppKeystoreError::from);
- let context = match &result {
- Ok(keys) => Some(format!("count={}", keys.len())),
- Err(err) => Some(err.to_string()),
- };
- let _ = app_log_debug_emit("log.app.keystore.keys", "fetch", context);
- result
-}
-
-pub async fn app_keystore_nostr_public_key<T: RadrootsClientKeystoreNostr>(
- keystore: &T,
-) -> RadrootsAppKeystoreResult<Option<String>> {
- let _ = app_log_debug_emit("log.app.keystore.public_key", "start", None);
- match keystore.keys().await {
- Ok(mut keys) => {
- let key = keys.pop();
- let context = key.as_ref().map(|value| format!("key={value}"));
- let _ = app_log_debug_emit("log.app.keystore.public_key", "resolved", context);
- Ok(key)
- }
- Err(RadrootsClientKeystoreError::NostrNoResults) => Ok(None),
- Err(err) => Err(RadrootsAppKeystoreError::from(err)),
- }
-}
-
-pub async fn app_keystore_nostr_ensure_key<T: RadrootsClientKeystoreNostr>(
- keystore: &T,
-) -> RadrootsAppKeystoreResult<String> {
- match app_keystore_nostr_public_key(keystore).await? {
- Some(key) => {
- let _ = app_log_debug_emit("log.app.keystore.ensure", "existing", None);
- Ok(key)
- }
- None => {
- let generated = keystore.generate().await.map_err(RadrootsAppKeystoreError::from)?;
- let _ = app_log_debug_emit("log.app.keystore.ensure", "generated", None);
- Ok(generated)
- }
- }
-}
-
-pub async fn app_keystore_nostr_verify_key<T: RadrootsClientKeystoreNostr>(
- keystore: &T,
- public_key: &str,
-) -> RadrootsAppKeystoreResult<()> {
- let secret_hex = keystore.read(public_key).await.map_err(RadrootsAppKeystoreError::from)?;
- let secret_key = RadrootsNostrSecretKey::parse(&secret_hex)
- .map_err(|_| RadrootsAppKeystoreError::Keystore(RadrootsClientKeystoreError::NostrInvalidSecretKey))?;
- let keys = RadrootsNostrKeys::new(secret_key);
- let derived = keys.public_key().to_hex();
- if derived != public_key {
- return Err(RadrootsAppKeystoreError::KeyMismatch);
- }
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- app_keystore_nostr_ensure_key,
- app_keystore_nostr_public_key,
- app_keystore_nostr_keys,
- app_keystore_nostr_verify_key,
- RadrootsAppKeystoreError,
- };
- use async_trait::async_trait;
- use radroots_app_core::keystore::{
- RadrootsClientKeystoreError,
- RadrootsClientKeystoreNostr,
- RadrootsClientKeystoreResult,
- };
- use radroots_nostr::prelude::{RadrootsNostrKeys, RadrootsNostrSecretKey};
-
- struct TestKeystore {
- keys_result: RadrootsClientKeystoreResult<Vec<String>>,
- generate_result: RadrootsClientKeystoreResult<String>,
- read_result: RadrootsClientKeystoreResult<String>,
- }
-
- #[async_trait(?Send)]
- impl RadrootsClientKeystoreNostr for TestKeystore {
- async fn generate(&self) -> RadrootsClientKeystoreResult<String> {
- self.generate_result.clone()
- }
-
- async fn add(&self, _secret_key: &str) -> RadrootsClientKeystoreResult<String> {
- Err(RadrootsClientKeystoreError::IdbUndefined)
- }
-
- async fn read(&self, _public_key: &str) -> RadrootsClientKeystoreResult<String> {
- self.read_result.clone()
- }
-
- async fn keys(&self) -> RadrootsClientKeystoreResult<Vec<String>> {
- self.keys_result.clone()
- }
-
- async fn remove(&self, _public_key: &str) -> RadrootsClientKeystoreResult<String> {
- Err(RadrootsClientKeystoreError::IdbUndefined)
- }
-
- async fn reset(&self) -> RadrootsClientKeystoreResult<()> {
- Err(RadrootsClientKeystoreError::IdbUndefined)
- }
- }
-
- #[test]
- fn keystore_public_key_maps_empty_to_none() {
- let keystore = TestKeystore {
- keys_result: Err(RadrootsClientKeystoreError::NostrNoResults),
- generate_result: Ok("generated".to_string()),
- read_result: Err(RadrootsClientKeystoreError::IdbUndefined),
- };
- let result = futures::executor::block_on(app_keystore_nostr_public_key(&keystore))
- .expect("nostr key");
- assert!(result.is_none());
- }
-
- #[test]
- fn keystore_public_key_returns_existing() {
- let keystore = TestKeystore {
- keys_result: Ok(vec!["a".to_string(), "b".to_string()]),
- generate_result: Ok("generated".to_string()),
- read_result: Err(RadrootsClientKeystoreError::IdbUndefined),
- };
- let result = futures::executor::block_on(app_keystore_nostr_public_key(&keystore))
- .expect("nostr key");
- assert_eq!(result.as_deref(), Some("b"));
- }
-
- #[test]
- fn keystore_keys_maps_errors() {
- let keystore = TestKeystore {
- keys_result: Err(RadrootsClientKeystoreError::IdbUndefined),
- generate_result: Ok("generated".to_string()),
- read_result: Err(RadrootsClientKeystoreError::IdbUndefined),
- };
- let err = futures::executor::block_on(app_keystore_nostr_keys(&keystore))
- .expect_err("nostr key");
- assert_eq!(
- err,
- RadrootsAppKeystoreError::Keystore(RadrootsClientKeystoreError::IdbUndefined)
- );
- }
-
- #[test]
- fn keystore_ensure_generates_when_empty() {
- let keystore = TestKeystore {
- keys_result: Err(RadrootsClientKeystoreError::NostrNoResults),
- generate_result: Ok("generated".to_string()),
- read_result: Err(RadrootsClientKeystoreError::IdbUndefined),
- };
- let result = futures::executor::block_on(app_keystore_nostr_ensure_key(&keystore))
- .expect("nostr key");
- assert_eq!(result, "generated");
- }
-
- #[test]
- fn keystore_ensure_returns_existing() {
- let keystore = TestKeystore {
- keys_result: Ok(vec!["a".to_string()]),
- generate_result: Ok("generated".to_string()),
- read_result: Err(RadrootsClientKeystoreError::IdbUndefined),
- };
- let result = futures::executor::block_on(app_keystore_nostr_ensure_key(&keystore))
- .expect("nostr key");
- assert_eq!(result, "a");
- }
-
- #[test]
- fn keystore_verify_matches_public_key() {
- let secret_key = RadrootsNostrSecretKey::generate();
- let secret_hex = secret_key.to_secret_hex();
- let keys = RadrootsNostrKeys::new(secret_key);
- let public_key = keys.public_key().to_hex();
- let keystore = TestKeystore {
- keys_result: Ok(vec![]),
- generate_result: Ok("generated".to_string()),
- read_result: Ok(secret_hex),
- };
- let result = futures::executor::block_on(app_keystore_nostr_verify_key(&keystore, &public_key))
- .expect("nostr key");
- assert_eq!(result, ());
- }
-
- #[test]
- fn keystore_verify_rejects_mismatch() {
- let secret_key = RadrootsNostrSecretKey::generate();
- let secret_hex = secret_key.to_secret_hex();
- let other_keys = RadrootsNostrKeys::new(RadrootsNostrSecretKey::generate());
- let public_key = other_keys.public_key().to_hex();
- let keystore = TestKeystore {
- keys_result: Ok(vec![]),
- generate_result: Ok("generated".to_string()),
- read_result: Ok(secret_hex),
- };
- let err = futures::executor::block_on(app_keystore_nostr_verify_key(&keystore, &public_key))
- .expect_err("nostr key");
- assert_eq!(err, RadrootsAppKeystoreError::KeyMismatch);
- }
-
- #[test]
- fn keystore_verify_rejects_invalid_secret() {
- let keystore = TestKeystore {
- keys_result: Ok(vec![]),
- generate_result: Ok("generated".to_string()),
- read_result: Ok("not-a-key".to_string()),
- };
- let err = futures::executor::block_on(app_keystore_nostr_verify_key(&keystore, "pub"))
- .expect_err("nostr key");
- assert_eq!(
- err,
- RadrootsAppKeystoreError::Keystore(RadrootsClientKeystoreError::NostrInvalidSecretKey)
- );
- }
-}
diff --git a/app/src/lib.rs b/app/src/lib.rs
@@ -1,290 +0,0 @@
-#![forbid(unsafe_code)]
-
-mod app;
-mod bootstrap;
-mod context;
-mod config;
-mod config_flow;
-mod configuration;
-mod data;
-mod health;
-mod health_ui;
-mod init;
-mod i18n;
-mod keystore;
-mod logging;
-mod logs;
-mod notifications;
-mod settings;
-mod settings_status;
-mod setup;
-mod setup_flow;
-mod setup_lock;
-mod setup_status;
-mod theme;
-mod tangle;
-mod ui_demo;
-mod entry;
-
-pub use app::RadrootsApp;
-pub use bootstrap::{
- app_datastore_clear_bootstrap,
- app_datastore_create_state,
- app_datastore_has_state,
- app_datastore_read_state,
- app_datastore_read_setup_draft,
- app_datastore_write_setup_draft,
- app_datastore_clear_setup_draft,
- app_datastore_write_profile_seed,
- app_datastore_update_state,
- app_state_set_notifications_permission,
- app_state_set_notifications_permission_value,
- app_state_notifications_permission_value,
- app_datastore_write_state,
-};
-pub use context::{app_context, RadrootsAppContext};
-pub use data::{
- app_state_is_initialized,
- app_state_record_new,
- app_state_record_validate,
- app_state_timestamp_ms,
- RadrootsAppProfileSeed,
- RadrootsAppRole,
- RadrootsAppState,
- RadrootsAppSetupDraft,
- RadrootsAppStateError,
- RadrootsAppStateRecord,
- APP_EULA_HASH,
- APP_EULA_VERSION,
- APP_STATE_SCHEMA_VERSION,
-};
-pub use configuration::{
- app_config_record_new,
- app_config_record_validate,
- app_config_gate_from_status,
- app_config_status,
- app_datastore_clear_config,
- app_datastore_create_config,
- app_datastore_has_config,
- app_datastore_read_config,
- app_datastore_read_config_record,
- app_datastore_update_config,
- app_datastore_write_config_record,
- RadrootsAppConfigBusiness,
- RadrootsAppConfigData,
- RadrootsAppConfigFarmer,
- RadrootsAppConfigIndividual,
- RadrootsAppConfigGate,
- RadrootsAppConfigPreferences,
- RadrootsAppConfigProfile,
- RadrootsAppConfigRecord,
- RadrootsAppConfigRecordError,
- RadrootsAppConfigStatus,
- RadrootsAppConfigStoreError,
- RadrootsAppConfigStoreResult,
- APP_CONFIG_SCHEMA_VERSION,
-};
-pub use health::{
- app_health_check_all,
- app_health_check_all_logged,
- app_health_check_state_active_key,
- app_health_check_bootstrap_state,
- app_health_check_datastore_roundtrip,
- app_health_check_keystore_access,
- app_health_check_notifications,
- app_health_check_tangle,
- app_health_check_key_maps,
- RadrootsAppHealthCheckResult,
- RadrootsAppHealthCheckStatus,
- RadrootsAppHealthReport,
-};
-pub use health_ui::{
- active_key_label,
- app_health_check_delay_ms,
- health_message_label,
- health_report_summary,
- health_result_label,
- health_status_class,
- health_status_label,
- spawn_health_checks,
-};
-pub use keystore::{
- app_keystore_nostr_ensure_key,
- app_keystore_nostr_keys,
- app_keystore_nostr_public_key,
- app_keystore_nostr_verify_key,
- RadrootsAppKeystoreError,
- RadrootsAppKeystoreResult,
-};
-pub use logs::RadrootsAppLogsPage;
-pub use settings::RadrootsAppSettingsPage;
-pub use settings_status::RadrootsAppSettingsStatusPage;
-pub use setup_status::{
- app_setup_gate_from_status,
- RadrootsAppSetupGate,
- RadrootsAppSetupStatus,
-};
-pub use ui_demo::RadrootsAppUiDemoPage;
-pub use theme::{
- app_theme_apply_mode,
- app_theme_init,
- app_theme_read_mode,
- app_theme_store_mode,
- app_theme_mode_from_value,
- app_theme_mode_to_name,
- RadrootsAppThemeError,
- RadrootsAppThemeMode,
- RadrootsAppThemeResult,
- APP_THEME_STORAGE_KEY,
-};
-pub use logging::{
- app_log_entry_error,
- app_log_entry_emit,
- app_log_entry_new,
- app_log_entry_record,
- app_log_entry_store,
- app_log_buffer_drain,
- app_log_buffer_flush_critical,
- app_log_buffer_flush_deferred,
- app_log_buffer_flush,
- app_log_buffer_flush_no_prune,
- app_log_buffer_push,
- app_log_entries_dump,
- app_log_entries_clear,
- app_log_entries_load,
- app_log_entries_prune,
- app_log_error_emit,
- app_log_error_store,
- app_log_entry_key,
- app_log_entry_prefix,
- app_log_debug_emit,
- app_log_dump_header,
- app_log_info_emit,
- app_log_metadata,
- app_log_timestamp_ms,
- app_log_warn_emit,
- app_logging_init,
- RadrootsAppLogDumpMeta,
- RadrootsAppLogEntry,
- RadrootsAppLogError,
- RadrootsAppLogLevel,
- RadrootsAppLogResult,
- RadrootsAppLoggableError,
- RadrootsAppLogMetadata,
- RadrootsAppLoggingError,
- RadrootsAppLoggingResult,
- APP_LOG_BUFFER_MAX_ENTRIES,
- APP_LOG_MAX_ENTRIES,
-};
-pub use notifications::{RadrootsAppNotifications, RadrootsAppNotificationsError, RadrootsAppNotificationsResult};
-pub use setup::{
- app_setup_eula_date,
- app_setup_commit,
- app_setup_finalize_with_key,
- app_setup_initialize,
- app_setup_state_new,
- app_setup_step_default,
- RadrootsAppSetupStep,
-};
-pub use setup_flow::{
- app_setup_flow_next_step,
- app_setup_flow_prev_step,
- app_setup_flow_role_from_choices,
- app_setup_flow_validate,
- RadrootsAppSetupBusinessChoice,
- RadrootsAppSetupFarmerChoice,
- RadrootsAppSetupFlowDraft,
- RadrootsAppSetupFlowValidation,
- RadrootsAppSetupKeyChoice,
-};
-pub use setup_lock::{
- app_setup_lock_acquire,
- app_setup_lock_enabled,
- app_setup_lock_is_expired,
- app_setup_lock_release,
- app_setup_lock_ttl_ms,
- RadrootsAppSetupLock,
- RadrootsAppSetupLockStatus,
- APP_SETUP_LOCK_TTL_MS,
-};
-pub use tangle::{RadrootsAppTangleClient, RadrootsAppTangleClientStub, RadrootsAppTangleError, RadrootsAppTangleResult};
-pub use config::{
- app_config_default,
- app_config_from_env,
- app_default_relays,
- app_datastore_key,
- app_datastore_key_eula_date,
- app_datastore_key_nostr_key,
- app_datastore_param_nostr_profile,
- app_datastore_param_log_entry,
- app_datastore_param_radroots_profile,
- app_datastore_param_key,
- app_datastore_obj_key,
- app_datastore_obj_key_state,
- app_datastore_obj_key_setup_draft,
- app_datastore_obj_key_config,
- app_assets_geocoder_db_url,
- app_assets_sql_wasm_url,
- app_keystore_key_maps_default,
- app_keystore_key_maps_validate,
- app_keystore_key,
- app_keystore_key_nostr_default,
- app_key_maps_default,
- app_key_maps_validate,
- RadrootsAppConfig,
- RadrootsAppConfigError,
- RadrootsAppConfigResult,
- RadrootsAppAssetConfig,
- RadrootsAppDatastoreConfig,
- RadrootsAppDatastoreKeyMap,
- RadrootsAppDatastoreKeyObjMap,
- RadrootsAppDatastoreKeyParam,
- RadrootsAppDatastoreKeyParamMap,
- RadrootsAppKeystoreConfig,
- RadrootsAppKeystoreKeyMap,
- RadrootsAppKeyMapConfig,
- APP_DATASTORE_KEY_EULA_DATE,
- APP_DATASTORE_KEY_LOG_ENTRY,
- APP_DATASTORE_KEY_NOSTR_KEY,
- APP_DATASTORE_KEY_OBJ_STATE,
- APP_DATASTORE_KEY_OBJ_SETUP_DRAFT,
- APP_DATASTORE_KEY_OBJ_CONFIG,
- APP_DATASTORE_KEY_SETUP_LOCK,
- APP_KEYSTORE_KEY_NOSTR_DEFAULT,
- app_datastore_key_setup_lock,
-};
-pub use config_flow::{
- app_config_flow_build_config,
- app_config_flow_next_step,
- app_config_flow_prev_step,
- app_config_flow_validate,
- app_config_step_default,
- RadrootsAppConfigFlowDraft,
- RadrootsAppConfigFlowValidation,
- RadrootsAppConfigStep,
-};
-pub use init::{
- app_init_assets,
- app_init_backends,
- app_init_fetch_asset,
- app_init_has_completed,
- app_init_needs_setup,
- app_init_setup_status,
- app_init_mark_completed,
- app_init_progress_add,
- app_init_reset,
- app_init_state_default,
- app_init_stage_set,
- app_init_total_add,
- app_init_total_unknown,
- RadrootsAppBackends,
- RadrootsAppInitAssetError,
- RadrootsAppInitAssetProgress,
- RadrootsAppInitError,
- RadrootsAppInitErrorMessage,
- RadrootsAppInitResult,
- RadrootsAppInitStage,
- RadrootsAppInitState,
- APP_INIT_STORAGE_KEY,
-};
-pub use i18n::{app_i18n, app_i18n_init, RadrootsAppI18nContext};
diff --git a/app/src/logging.rs b/app/src/logging.rs
@@ -1,1087 +0,0 @@
-#![forbid(unsafe_code)]
-
-use std::sync::OnceLock;
-
-#[cfg(not(test))]
-use std::sync::Mutex;
-
-#[cfg(test)]
-use std::cell::RefCell;
-
-#[cfg(not(target_arch = "wasm32"))]
-use std::path::PathBuf;
-#[cfg(not(target_arch = "wasm32"))]
-use std::time::{SystemTime, UNIX_EPOCH};
-
-#[cfg(target_arch = "wasm32")]
-use js_sys::Date;
-use serde::{Deserialize, Serialize};
-use uuid::Uuid;
-
-use radroots_app_core::datastore::{RadrootsClientDatastore, RadrootsClientDatastoreError};
-
-use crate::{
- app_datastore_param_key,
- RadrootsAppConfigError,
- RadrootsAppInitAssetError,
- RadrootsAppInitError,
- RadrootsAppKeystoreError,
- RadrootsAppKeyMapConfig,
- RadrootsAppNotificationsError,
- RadrootsAppTangleError,
-};
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsAppLogMetadata {
- pub app_name: String,
- pub app_version: String,
- pub app_hash: String,
- pub target: String,
-}
-
-impl Default for RadrootsAppLogMetadata {
- fn default() -> Self {
- let app_name = String::from(env!("CARGO_PKG_NAME"));
- let app_version = String::from(env!("CARGO_PKG_VERSION"));
- let app_hash = String::from(option_env!("RADROOTS_GIT_HASH").unwrap_or("unknown"));
- let target = if cfg!(target_arch = "wasm32") {
- String::from("wasm32")
- } else {
- String::from("native")
- };
- Self {
- app_name,
- app_version,
- app_hash,
- target,
- }
- }
-}
-
-static LOG_META: OnceLock<RadrootsAppLogMetadata> = OnceLock::new();
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsAppLogDumpMeta {
- pub kind: String,
- pub generated_at_ms: i64,
- pub metadata: RadrootsAppLogMetadata,
-}
-
-impl RadrootsAppLogDumpMeta {
- pub fn new() -> Self {
- Self {
- kind: String::from("radroots_log_dump"),
- generated_at_ms: app_log_timestamp_ms(),
- metadata: app_log_metadata().clone(),
- }
- }
-}
-
-#[cfg(not(test))]
-static LOG_BUFFER: OnceLock<Mutex<Vec<RadrootsAppLogEntry>>> = OnceLock::new();
-
-#[cfg(test)]
-thread_local! {
- static LOG_BUFFER: RefCell<Vec<RadrootsAppLogEntry>> = RefCell::new(Vec::new());
-}
-
-pub const APP_LOG_BUFFER_MAX_ENTRIES: usize = 512;
-pub const APP_LOG_MAX_ENTRIES: usize = 2000;
-const APP_LOG_PRUNE_MIN_INTERVAL_MS: i64 = 60_000;
-
-#[cfg(not(test))]
-static LOG_PRUNE_LAST_MS: OnceLock<Mutex<Option<i64>>> = OnceLock::new();
-
-#[cfg(test)]
-thread_local! {
- static LOG_PRUNE_LAST_MS: RefCell<Option<i64>> = RefCell::new(None);
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-pub enum RadrootsAppLogLevel {
- Debug,
- Info,
- Warn,
- Error,
-}
-
-impl RadrootsAppLogLevel {
- pub const fn as_str(self) -> &'static str {
- match self {
- RadrootsAppLogLevel::Debug => "debug",
- RadrootsAppLogLevel::Info => "info",
- RadrootsAppLogLevel::Warn => "warn",
- RadrootsAppLogLevel::Error => "error",
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsAppLogEntry {
- pub id: String,
- pub timestamp_ms: i64,
- pub level: RadrootsAppLogLevel,
- pub code: String,
- pub message: String,
- pub context: Option<String>,
- pub metadata: RadrootsAppLogMetadata,
-}
-
-pub trait RadrootsAppLoggableError: std::fmt::Display {
- fn log_code(&self) -> &'static str;
- fn log_context(&self) -> Option<String> {
- None
- }
-}
-
-impl RadrootsAppLoggableError for RadrootsAppInitAssetError {
- fn log_code(&self) -> &'static str {
- self.message()
- }
-}
-
-impl RadrootsAppLoggableError for RadrootsAppConfigError {
- fn log_code(&self) -> &'static str {
- self.message()
- }
-
- fn log_context(&self) -> Option<String> {
- match self {
- RadrootsAppConfigError::MissingKeyMap(key) => Some(format!("key_map={key}")),
- RadrootsAppConfigError::MissingParamMap(key) => Some(format!("param_map={key}")),
- RadrootsAppConfigError::MissingObjMap(key) => Some(format!("obj_map={key}")),
- RadrootsAppConfigError::MissingKeystoreKeyMap(key) => Some(format!("keystore_map={key}")),
- }
- }
-}
-
-impl RadrootsAppLoggableError for RadrootsAppInitError {
- fn log_code(&self) -> &'static str {
- self.message()
- }
-
- fn log_context(&self) -> Option<String> {
- match self {
- RadrootsAppInitError::Idb(err) => Some(err.to_string()),
- RadrootsAppInitError::Datastore(err) => Some(err.to_string()),
- RadrootsAppInitError::Keystore(err) => Some(err.to_string()),
- RadrootsAppInitError::Config(err) => err.log_context().or_else(|| Some(err.message().to_string())),
- RadrootsAppInitError::Assets(err) => Some(err.message().to_string()),
- RadrootsAppInitError::State(err) => Some(err.message().to_string()),
- }
- }
-}
-
-impl RadrootsAppLoggableError for RadrootsAppKeystoreError {
- fn log_code(&self) -> &'static str {
- self.message()
- }
-
- fn log_context(&self) -> Option<String> {
- match self {
- RadrootsAppKeystoreError::Keystore(err) => Some(err.to_string()),
- RadrootsAppKeystoreError::KeyMismatch => Some(self.message().to_string()),
- }
- }
-}
-
-impl RadrootsAppLoggableError for RadrootsAppNotificationsError {
- fn log_code(&self) -> &'static str {
- self.message()
- }
-
- fn log_context(&self) -> Option<String> {
- match self {
- RadrootsAppNotificationsError::Notifications(err) => Some(err.message().to_string()),
- }
- }
-}
-
-impl RadrootsAppLoggableError for RadrootsAppTangleError {
- fn log_code(&self) -> &'static str {
- self.message()
- }
-}
-
-#[derive(Debug)]
-pub enum RadrootsAppLogError {
- Config(RadrootsAppConfigError),
- Datastore(RadrootsClientDatastoreError),
-}
-
-pub type RadrootsAppLogResult<T> = Result<T, RadrootsAppLogError>;
-
-impl std::fmt::Display for RadrootsAppLogError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- RadrootsAppLogError::Config(err) => write!(f, "{err}"),
- RadrootsAppLogError::Datastore(err) => write!(f, "{err}"),
- }
- }
-}
-
-impl std::error::Error for RadrootsAppLogError {}
-
-impl From<RadrootsAppConfigError> for RadrootsAppLogError {
- fn from(err: RadrootsAppConfigError) -> Self {
- RadrootsAppLogError::Config(err)
- }
-}
-
-impl From<RadrootsClientDatastoreError> for RadrootsAppLogError {
- fn from(err: RadrootsClientDatastoreError) -> Self {
- RadrootsAppLogError::Datastore(err)
- }
-}
-
-pub fn app_log_timestamp_ms() -> i64 {
- #[cfg(target_arch = "wasm32")]
- {
- Date::now() as i64
- }
- #[cfg(not(target_arch = "wasm32"))]
- {
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map(|value| value.as_millis() as i64)
- .unwrap_or(0)
- }
-}
-
-pub fn app_log_entry_error<E: RadrootsAppLoggableError>(err: &E) -> RadrootsAppLogEntry {
- RadrootsAppLogEntry {
- id: Uuid::new_v4().to_string(),
- timestamp_ms: app_log_timestamp_ms(),
- level: RadrootsAppLogLevel::Error,
- code: err.log_code().to_string(),
- message: err.to_string(),
- context: err.log_context(),
- metadata: app_log_metadata().clone(),
- }
-}
-
-pub fn app_log_entry_new(
- level: RadrootsAppLogLevel,
- code: &str,
- message: &str,
- context: Option<String>,
-) -> RadrootsAppLogEntry {
- RadrootsAppLogEntry {
- id: Uuid::new_v4().to_string(),
- timestamp_ms: app_log_timestamp_ms(),
- level,
- code: code.to_string(),
- message: message.to_string(),
- context,
- metadata: app_log_metadata().clone(),
- }
-}
-
-pub fn app_log_entry_emit(entry: &RadrootsAppLogEntry) {
- let payload = serde_json::to_string(entry)
- .unwrap_or_else(|_| format!("{}: {}", entry.code, entry.message));
- match entry.level {
- RadrootsAppLogLevel::Error => radroots_log::log_error(payload),
- RadrootsAppLogLevel::Warn => radroots_log::log_info(payload),
- RadrootsAppLogLevel::Info => radroots_log::log_info(payload),
- RadrootsAppLogLevel::Debug => radroots_log::log_debug(payload),
- }
-}
-
-pub fn app_log_entry_record(entry: RadrootsAppLogEntry) -> RadrootsAppLogEntry {
- app_log_entry_emit(&entry);
- app_log_buffer_push(entry.clone());
- entry
-}
-
-pub fn app_log_error_emit<E: RadrootsAppLoggableError>(err: &E) -> RadrootsAppLogEntry {
- app_log_entry_record(app_log_entry_error(err))
-}
-
-pub fn app_log_debug_emit(code: &str, message: &str, context: Option<String>) -> RadrootsAppLogEntry {
- app_log_entry_record(app_log_entry_new(
- RadrootsAppLogLevel::Debug,
- code,
- message,
- context,
- ))
-}
-
-pub fn app_log_info_emit(code: &str, message: &str, context: Option<String>) -> RadrootsAppLogEntry {
- app_log_entry_record(app_log_entry_new(
- RadrootsAppLogLevel::Info,
- code,
- message,
- context,
- ))
-}
-
-pub fn app_log_warn_emit(code: &str, message: &str, context: Option<String>) -> RadrootsAppLogEntry {
- app_log_entry_record(app_log_entry_new(
- RadrootsAppLogLevel::Warn,
- code,
- message,
- context,
- ))
-}
-
-pub fn app_log_entry_key(
- key_maps: &RadrootsAppKeyMapConfig,
- entry_id: &str,
-) -> RadrootsAppLogResult<String> {
- let param = app_datastore_param_key(key_maps, "log_entry")?;
- Ok(param(entry_id))
-}
-
-pub async fn app_log_entry_store<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
- entry: &RadrootsAppLogEntry,
-) -> RadrootsAppLogResult<RadrootsAppLogEntry> {
- let key = app_log_entry_key(key_maps, &entry.id)?;
- datastore
- .set_obj(&key, entry)
- .await
- .map_err(RadrootsAppLogError::Datastore)
-}
-
-pub async fn app_log_error_store<T: RadrootsClientDatastore, E: RadrootsAppLoggableError>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
- err: &E,
-) -> RadrootsAppLogResult<RadrootsAppLogEntry> {
- let entry = app_log_error_emit(err);
- app_log_entry_store(datastore, key_maps, &entry).await
-}
-
-pub fn app_log_buffer_push(entry: RadrootsAppLogEntry) {
- #[cfg(test)]
- {
- LOG_BUFFER.with(|buffer| {
- let mut entries = buffer.borrow_mut();
- entries.push(entry);
- if entries.len() > APP_LOG_BUFFER_MAX_ENTRIES {
- let drop = entries.len() - APP_LOG_BUFFER_MAX_ENTRIES;
- entries.drain(0..drop);
- }
- });
- }
- #[cfg(not(test))]
- {
- let buffer = LOG_BUFFER.get_or_init(|| Mutex::new(Vec::new()));
- let mut entries = buffer.lock().unwrap_or_else(|err| err.into_inner());
- entries.push(entry);
- if entries.len() > APP_LOG_BUFFER_MAX_ENTRIES {
- let drop = entries.len() - APP_LOG_BUFFER_MAX_ENTRIES;
- entries.drain(0..drop);
- }
- }
-}
-
-pub fn app_log_buffer_drain() -> Vec<RadrootsAppLogEntry> {
- #[cfg(test)]
- {
- LOG_BUFFER.with(|buffer| buffer.borrow_mut().drain(..).collect())
- }
- #[cfg(not(test))]
- {
- let buffer = LOG_BUFFER.get_or_init(|| Mutex::new(Vec::new()));
- let mut entries = buffer.lock().unwrap_or_else(|err| err.into_inner());
- entries.drain(..).collect()
- }
-}
-
-fn app_log_prune_should_run(now_ms: i64) -> bool {
- #[cfg(test)]
- {
- return LOG_PRUNE_LAST_MS.with(|value| {
- let mut last = value.borrow_mut();
- let should_run = match *last {
- Some(prev) if now_ms >= prev && now_ms - prev < APP_LOG_PRUNE_MIN_INTERVAL_MS => false,
- _ => true,
- };
- if should_run {
- *last = Some(now_ms);
- }
- should_run
- });
- }
- #[cfg(not(test))]
- {
- let lock = LOG_PRUNE_LAST_MS.get_or_init(|| Mutex::new(None));
- let mut last = lock.lock().unwrap_or_else(|err| err.into_inner());
- let should_run = match *last {
- Some(prev) if now_ms >= prev && now_ms - prev < APP_LOG_PRUNE_MIN_INTERVAL_MS => false,
- _ => true,
- };
- if should_run {
- *last = Some(now_ms);
- }
- should_run
- }
-}
-
-#[cfg(test)]
-fn app_log_prune_reset() {
- LOG_PRUNE_LAST_MS.with(|value| {
- *value.borrow_mut() = None;
- });
-}
-
-fn app_log_entry_should_persist(level: RadrootsAppLogLevel) -> bool {
- matches!(level, RadrootsAppLogLevel::Warn | RadrootsAppLogLevel::Error)
-}
-
-async fn app_log_buffer_flush_internal<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
- prune: bool,
-) -> RadrootsAppLogResult<usize> {
- let entries = app_log_buffer_drain();
- let mut stored = 0;
- let mut iter = entries.into_iter();
- while let Some(entry) = iter.next() {
- if let Err(err) = app_log_entry_store(datastore, key_maps, &entry).await {
- app_log_buffer_push(entry);
- for remaining in iter {
- app_log_buffer_push(remaining);
- }
- return Err(err);
- }
- stored += 1;
- }
- if prune {
- let _ = app_log_entries_prune(datastore, key_maps, APP_LOG_MAX_ENTRIES).await?;
- }
- Ok(stored)
-}
-
-pub async fn app_log_buffer_flush<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppLogResult<usize> {
- app_log_buffer_flush_internal(datastore, key_maps, true).await
-}
-
-pub async fn app_log_buffer_flush_no_prune<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppLogResult<usize> {
- app_log_buffer_flush_internal(datastore, key_maps, false).await
-}
-
-pub async fn app_log_buffer_flush_deferred<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
- prune: bool,
-) -> RadrootsAppLogResult<usize> {
- if prune && !app_log_prune_should_run(app_log_timestamp_ms()) {
- return app_log_buffer_flush_no_prune(datastore, key_maps).await;
- }
- app_log_buffer_flush_internal(datastore, key_maps, prune).await
-}
-
-pub async fn app_log_buffer_flush_critical<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppLogResult<usize> {
- let entries = app_log_buffer_drain();
- let mut keep = Vec::new();
- let mut persist = Vec::new();
- for entry in entries {
- if app_log_entry_should_persist(entry.level) {
- persist.push(entry);
- } else {
- keep.push(entry);
- }
- }
- let mut stored = 0;
- let mut iter = persist.into_iter();
- while let Some(entry) = iter.next() {
- if let Err(err) = app_log_entry_store(datastore, key_maps, &entry).await {
- app_log_buffer_push(entry);
- for remaining in iter {
- app_log_buffer_push(remaining);
- }
- for remaining in keep {
- app_log_buffer_push(remaining);
- }
- return Err(err);
- }
- stored += 1;
- }
- for entry in keep {
- app_log_buffer_push(entry);
- }
- let _ = app_log_entries_prune(datastore, key_maps, APP_LOG_MAX_ENTRIES).await?;
- Ok(stored)
-}
-
-pub fn app_log_entry_prefix(key_maps: &RadrootsAppKeyMapConfig) -> RadrootsAppLogResult<String> {
- let param = app_datastore_param_key(key_maps, "log_entry")?;
- Ok(param(""))
-}
-
-pub async fn app_log_entries_load<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppLogResult<Vec<RadrootsAppLogEntry>> {
- let prefix = app_log_entry_prefix(key_maps)?;
- let entries = datastore
- .entries_pref(&prefix)
- .await
- .map_err(RadrootsAppLogError::Datastore)?;
- let mut out = Vec::new();
- for entry in entries {
- let Some(value) = entry.value else {
- continue;
- };
- if let Ok(parsed) = serde_json::from_str::<RadrootsAppLogEntry>(&value) {
- out.push(parsed);
- }
- }
- Ok(out)
-}
-
-pub async fn app_log_entries_clear<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppLogResult<usize> {
- let prefix = app_log_entry_prefix(key_maps)?;
- let removed = datastore
- .del_pref(&prefix)
- .await
- .map_err(RadrootsAppLogError::Datastore)?;
- Ok(removed.len())
-}
-
-pub fn app_log_entries_dump(entries: &[RadrootsAppLogEntry]) -> String {
- let mut out = String::new();
- for (idx, entry) in entries.iter().enumerate() {
- if idx > 0 {
- out.push('\n');
- }
- match serde_json::to_string(entry) {
- Ok(line) => out.push_str(&line),
- Err(_) => out.push_str("{\"error\":\"log_entry_encode_failed\"}"),
- }
- }
- out
-}
-
-pub async fn app_log_entries_prune<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
- max_entries: usize,
-) -> RadrootsAppLogResult<usize> {
- let mut entries = app_log_entries_load(datastore, key_maps).await?;
- if entries.len() <= max_entries {
- return Ok(0);
- }
- entries.sort_by_key(|entry| entry.timestamp_ms);
- let prune_count = entries.len().saturating_sub(max_entries);
- let mut removed = 0;
- for entry in entries.into_iter().take(prune_count) {
- let key = app_log_entry_key(key_maps, &entry.id)?;
- let _ = datastore.del(&key).await.map_err(RadrootsAppLogError::Datastore)?;
- removed += 1;
- }
- Ok(removed)
-}
-
-#[derive(Debug)]
-pub enum RadrootsAppLoggingError {
- Logging(radroots_log::Error),
-}
-
-pub type RadrootsAppLoggingResult<T> = Result<T, RadrootsAppLoggingError>;
-
-impl std::fmt::Display for RadrootsAppLoggingError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- RadrootsAppLoggingError::Logging(err) => write!(f, "{err:?}"),
- }
- }
-}
-
-impl std::error::Error for RadrootsAppLoggingError {}
-
-pub fn app_log_metadata() -> &'static RadrootsAppLogMetadata {
- LOG_META.get_or_init(RadrootsAppLogMetadata::default)
-}
-
-pub fn app_log_dump_header() -> String {
- let header = RadrootsAppLogDumpMeta::new();
- serde_json::to_string(&header)
- .unwrap_or_else(|_| String::from("{\"error\":\"log_dump_header_failed\"}"))
-}
-
-pub fn app_logging_init(meta: Option<RadrootsAppLogMetadata>) -> RadrootsAppLoggingResult<()> {
- if LOG_META.get().is_none() {
- let _ = LOG_META.set(meta.unwrap_or_default());
- }
- #[cfg(target_arch = "wasm32")]
- {
- console_error_panic_hook::set_once();
- let _ = tracing_wasm::set_as_global_default();
- Ok(())
- }
- #[cfg(not(target_arch = "wasm32"))]
- {
- let opts = radroots_log::LoggingOptions {
- dir: Some(PathBuf::from("logs")),
- file_name: "radroots-app.log".into(),
- stdout: true,
- default_level: Some(String::from("info")),
- };
- match radroots_log::init_logging(opts) {
- Ok(()) => Ok(()),
- Err(err) => {
- radroots_log::init_stdout().map_err(RadrootsAppLoggingError::Logging)?;
- radroots_log::log_error(format!("logging_init_failed: {err}"));
- Ok(())
- }
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- app_log_entries_clear,
- app_log_entries_dump,
- app_log_entries_load,
- app_log_entries_prune,
- app_log_entry_error,
- app_log_entry_new,
- app_log_entry_key,
- app_log_entry_prefix,
- app_log_buffer_drain,
- app_log_prune_reset,
- app_log_prune_should_run,
- app_log_buffer_flush_critical,
- app_log_buffer_flush_no_prune,
- app_log_buffer_flush,
- app_log_buffer_push,
- app_log_dump_header,
- app_log_metadata,
- app_log_timestamp_ms,
- RadrootsAppLogLevel,
- RadrootsAppLogEntry,
- RadrootsAppLogDumpMeta,
- RadrootsAppLogMetadata,
- APP_LOG_PRUNE_MIN_INTERVAL_MS,
- };
- use crate::{
- app_key_maps_default,
- RadrootsAppConfigError,
- APP_DATASTORE_KEY_LOG_ENTRY,
- };
- use async_trait::async_trait;
- use radroots_app_core::backup::RadrootsClientBackupDatastorePayload;
- use radroots_app_core::datastore::{
- RadrootsClientDatastore,
- RadrootsClientDatastoreEntries,
- RadrootsClientDatastoreEntry,
- RadrootsClientDatastoreError,
- RadrootsClientDatastoreResult,
- };
- use radroots_app_core::idb::{RadrootsClientIdbConfig, IDB_CONFIG_DATASTORE};
- use serde::{de::DeserializeOwned, Serialize};
- use std::sync::Mutex;
-
- static LOG_TEST_LOCK: Mutex<()> = Mutex::new(());
-
- struct TestDatastore {
- entries: Mutex<Vec<RadrootsClientDatastoreEntry>>,
- }
-
- #[test]
- fn log_prune_throttles() {
- let _guard = LOG_TEST_LOCK.lock().unwrap_or_else(|err| err.into_inner());
- app_log_prune_reset();
- let base = 1_000_000;
- assert!(app_log_prune_should_run(base));
- assert!(!app_log_prune_should_run(base + APP_LOG_PRUNE_MIN_INTERVAL_MS - 1));
- assert!(app_log_prune_should_run(base + APP_LOG_PRUNE_MIN_INTERVAL_MS));
- }
-
- impl TestDatastore {
- fn new(entries: Vec<RadrootsClientDatastoreEntry>) -> Self {
- Self {
- entries: Mutex::new(entries),
- }
- }
-
- fn len(&self) -> usize {
- self.entries.lock().unwrap_or_else(|err| err.into_inner()).len()
- }
- }
-
- #[async_trait(?Send)]
- impl RadrootsClientDatastore for TestDatastore {
- fn get_config(&self) -> RadrootsClientIdbConfig {
- IDB_CONFIG_DATASTORE
- }
-
- fn get_store_id(&self) -> &str {
- "test"
- }
-
- async fn init(&self) -> RadrootsClientDatastoreResult<()> {
- Ok(())
- }
-
- async fn set(&self, _key: &str, _value: &str) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn get(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn set_obj<T>(
- &self,
- key: &str,
- value: &T,
- ) -> RadrootsClientDatastoreResult<T>
- where
- T: Serialize + DeserializeOwned + Clone,
- {
- let encoded = serde_json::to_string(value)
- .map_err(|_| RadrootsClientDatastoreError::NoResult)?;
- let mut entries = self.entries.lock().unwrap_or_else(|err| err.into_inner());
- entries.retain(|entry| entry.key != key);
- entries.push(RadrootsClientDatastoreEntry::new(
- key.to_string(),
- Some(encoded),
- ));
- Ok(value.clone())
- }
-
- async fn update_obj<T>(
- &self,
- _key: &str,
- _value: &T,
- ) -> RadrootsClientDatastoreResult<T>
- where
- T: Serialize + DeserializeOwned + Clone,
- {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn get_obj<T>(&self, _key: &str) -> RadrootsClientDatastoreResult<T>
- where
- T: DeserializeOwned,
- {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn del_obj(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn del(&self, key: &str) -> RadrootsClientDatastoreResult<String> {
- let mut entries = self.entries.lock().unwrap_or_else(|err| err.into_inner());
- entries.retain(|entry| entry.key != key);
- Ok(key.to_string())
- }
-
- async fn del_pref(&self, key_prefix: &str) -> RadrootsClientDatastoreResult<Vec<String>> {
- let mut entries = self.entries.lock().unwrap_or_else(|err| err.into_inner());
- let mut removed = Vec::new();
- let mut kept = Vec::new();
- for entry in entries.drain(..) {
- if entry.key.starts_with(key_prefix) {
- removed.push(entry.key);
- } else {
- kept.push(entry);
- }
- }
- *entries = kept;
- Ok(removed)
- }
-
- async fn set_param(
- &self,
- _key: &str,
- _key_param: &str,
- _value: &str,
- ) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn get_param(
- &self,
- _key: &str,
- _key_param: &str,
- ) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn keys(&self) -> RadrootsClientDatastoreResult<Vec<String>> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn entries(&self) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> {
- Ok(self
- .entries
- .lock()
- .unwrap_or_else(|err| err.into_inner())
- .clone())
- }
-
- async fn entries_pref(
- &self,
- key_prefix: &str,
- ) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> {
- Ok(self
- .entries
- .lock()
- .unwrap_or_else(|err| err.into_inner())
- .iter()
- .filter(|entry| entry.key.starts_with(key_prefix))
- .cloned()
- .collect())
- }
-
- async fn reset(&self) -> RadrootsClientDatastoreResult<()> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn export_backup(
- &self,
- ) -> RadrootsClientDatastoreResult<RadrootsClientBackupDatastorePayload> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn import_backup(
- &self,
- _payload: RadrootsClientBackupDatastorePayload,
- ) -> RadrootsClientDatastoreResult<()> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
- }
-
- #[test]
- fn log_metadata_defaults_populated() {
- let meta = RadrootsAppLogMetadata::default();
- assert!(!meta.app_name.is_empty());
- assert!(!meta.app_version.is_empty());
- assert!(!meta.app_hash.is_empty());
- assert!(!meta.target.is_empty());
- }
-
- #[test]
- fn log_metadata_once_lock_returns_default() {
- let meta = app_log_metadata();
- assert!(!meta.app_name.is_empty());
- }
-
- #[test]
- fn log_dump_header_serializes() {
- let header = app_log_dump_header();
- let parsed: RadrootsAppLogDumpMeta =
- serde_json::from_str(&header).expect("header");
- assert_eq!(parsed.kind, "radroots_log_dump");
- assert!(!parsed.metadata.app_name.is_empty());
- }
-
- #[test]
- fn log_entry_error_includes_context() {
- let err = RadrootsAppConfigError::MissingKeyMap("nostr_key");
- let entry = app_log_entry_error(&err);
- assert_eq!(entry.level, RadrootsAppLogLevel::Error);
- assert_eq!(entry.code, err.message());
- assert_eq!(entry.message, err.to_string());
- assert_eq!(entry.context.as_deref(), Some("key_map=nostr_key"));
- assert!(entry.timestamp_ms >= app_log_timestamp_ms() - 10_000);
- }
-
- #[test]
- fn log_error_key_uses_param_map() {
- let key_maps = app_key_maps_default();
- let key = app_log_entry_key(&key_maps, "entry").expect("key");
- assert_eq!(key, format!("{APP_DATASTORE_KEY_LOG_ENTRY}:entry"));
- }
-
- #[test]
- fn log_entry_new_populates_fields() {
- let entry = app_log_entry_new(
- RadrootsAppLogLevel::Info,
- "log.code.test",
- "hello",
- Some(String::from("ctx")),
- );
- assert_eq!(entry.level, RadrootsAppLogLevel::Info);
- assert_eq!(entry.code, "log.code.test");
- assert_eq!(entry.message, "hello");
- assert_eq!(entry.context.as_deref(), Some("ctx"));
- assert!(!entry.id.is_empty());
- }
-
- #[test]
- fn log_buffer_drains_entries() {
- let _guard = LOG_TEST_LOCK.lock().unwrap_or_else(|err| err.into_inner());
- let _ = app_log_buffer_drain();
- let entry = app_log_entry_new(RadrootsAppLogLevel::Debug, "log.code.test", "buf", None);
- app_log_buffer_push(entry.clone());
- let drained = app_log_buffer_drain();
- assert_eq!(drained.len(), 1);
- assert_eq!(drained[0].id, entry.id);
- assert!(app_log_buffer_drain().is_empty());
- }
-
- #[test]
- fn log_buffer_flush_critical_keeps_debug_entries() {
- let _guard = LOG_TEST_LOCK.lock().unwrap_or_else(|err| err.into_inner());
- let _ = app_log_buffer_drain();
- let debug = app_log_entry_new(RadrootsAppLogLevel::Debug, "log.code.debug", "debug", None);
- let error = app_log_entry_new(RadrootsAppLogLevel::Error, "log.code.error", "error", None);
- app_log_buffer_push(debug.clone());
- app_log_buffer_push(error.clone());
- let datastore = TestDatastore::new(Vec::new());
- let key_maps = app_key_maps_default();
- let stored = futures::executor::block_on(app_log_buffer_flush_critical(
- &datastore,
- &key_maps,
- ))
- .expect("flush");
- assert_eq!(stored, 1);
- let remaining = app_log_buffer_drain();
- assert_eq!(remaining.len(), 1);
- assert_eq!(remaining[0].id, debug.id);
- }
-
- #[test]
- fn log_entry_prefix_uses_log_key() {
- let key_maps = app_key_maps_default();
- let prefix = app_log_entry_prefix(&key_maps).expect("prefix");
- assert_eq!(prefix, format!("{APP_DATASTORE_KEY_LOG_ENTRY}:"));
- }
-
- #[test]
- fn log_entries_clear_removes_prefixed_keys() {
- let key_maps = app_key_maps_default();
- let key_a = app_log_entry_key(&key_maps, "a").expect("key");
- let entries = vec![
- RadrootsClientDatastoreEntry::new(key_a, Some(String::from("{}"))),
- RadrootsClientDatastoreEntry::new(String::from("other:1"), Some(String::from("{}"))),
- ];
- let datastore = TestDatastore::new(entries);
- let removed = futures::executor::block_on(app_log_entries_clear(&datastore, &key_maps))
- .expect("clear");
- assert_eq!(removed, 1);
- assert_eq!(datastore.len(), 1);
- }
-
- #[test]
- fn log_entries_dump_serializes_jsonl() {
- let entries = vec![RadrootsAppLogEntry {
- id: String::from("a"),
- timestamp_ms: 1,
- level: RadrootsAppLogLevel::Info,
- code: String::from("code"),
- message: String::from("hello"),
- context: None,
- metadata: RadrootsAppLogMetadata::default(),
- }];
- let dump = app_log_entries_dump(&entries);
- assert!(dump.contains("\"code\":\"code\""));
- assert_eq!(dump.lines().count(), 1);
- }
-
- #[test]
- fn log_entries_load_filters_by_prefix() {
- let key_maps = app_key_maps_default();
- let entry = RadrootsAppLogEntry {
- id: String::from("a"),
- timestamp_ms: 1,
- level: RadrootsAppLogLevel::Info,
- code: String::from("code"),
- message: String::from("hello"),
- context: None,
- metadata: RadrootsAppLogMetadata::default(),
- };
- let key = app_log_entry_key(&key_maps, &entry.id).expect("key");
- let entries = vec![
- RadrootsClientDatastoreEntry::new(
- key,
- Some(serde_json::to_string(&entry).expect("json")),
- ),
- RadrootsClientDatastoreEntry::new(String::from("other"), Some(String::from("{}"))),
- ];
- let datastore = TestDatastore::new(entries);
- let loaded = futures::executor::block_on(app_log_entries_load(&datastore, &key_maps))
- .expect("load");
- assert_eq!(loaded.len(), 1);
- assert_eq!(loaded[0].id, "a");
- }
-
- #[test]
- fn log_entries_prune_enforces_limit() {
- let key_maps = app_key_maps_default();
- let entries = (0..3)
- .map(|idx| RadrootsAppLogEntry {
- id: format!("id-{idx}"),
- timestamp_ms: idx,
- level: RadrootsAppLogLevel::Info,
- code: String::from("code"),
- message: String::from("hello"),
- context: None,
- metadata: RadrootsAppLogMetadata::default(),
- })
- .collect::<Vec<_>>();
- let mut stored = Vec::new();
- for entry in entries {
- let key = app_log_entry_key(&key_maps, &entry.id).expect("key");
- stored.push(RadrootsClientDatastoreEntry::new(
- key,
- Some(serde_json::to_string(&entry).expect("json")),
- ));
- }
- let datastore = TestDatastore::new(stored);
- let removed =
- futures::executor::block_on(app_log_entries_prune(&datastore, &key_maps, 2))
- .expect("prune");
- assert_eq!(removed, 1);
- assert_eq!(datastore.len(), 2);
- }
-
- #[test]
- fn log_buffer_flush_stores_entries() {
- let _guard = LOG_TEST_LOCK.lock().unwrap_or_else(|err| err.into_inner());
- let _ = app_log_buffer_drain();
- let key_maps = app_key_maps_default();
- let datastore = TestDatastore::new(Vec::new());
- app_log_buffer_push(app_log_entry_new(
- RadrootsAppLogLevel::Info,
- "log.code.flush",
- "flush",
- None,
- ));
- let stored = futures::executor::block_on(app_log_buffer_flush(&datastore, &key_maps))
- .expect("flush");
- assert_eq!(stored, 1);
- assert_eq!(datastore.len(), 1);
- }
-
- #[test]
- fn log_buffer_flush_no_prune_stores_entries() {
- let _guard = LOG_TEST_LOCK.lock().unwrap_or_else(|err| err.into_inner());
- let _ = app_log_buffer_drain();
- let key_maps = app_key_maps_default();
- let datastore = TestDatastore::new(Vec::new());
- app_log_buffer_push(app_log_entry_new(
- RadrootsAppLogLevel::Info,
- "log.code.flush.noprune",
- "flush",
- None,
- ));
- let stored =
- futures::executor::block_on(app_log_buffer_flush_no_prune(&datastore, &key_maps))
- .expect("flush");
- assert_eq!(stored, 1);
- assert_eq!(datastore.len(), 1);
- }
-}
diff --git a/app/src/logs.rs b/app/src/logs.rs
@@ -1,979 +0,0 @@
-#![forbid(unsafe_code)]
-
-use futures::future::{AbortHandle, Abortable};
-use futures::StreamExt;
-use gloo_timers::future::IntervalStream;
-use leptos::prelude::*;
-use serde::Serialize;
-use leptos::task::spawn_local;
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen::JsCast;
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen_futures::JsFuture;
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen::JsValue;
-
-use radroots_app_core::datastore::RadrootsClientWebDatastore;
-use radroots_app_core::idb::IDB_CONFIG_LOGS;
-
-use crate::{
- app::AppPageChrome,
- app_context,
- app_log_buffer_flush_no_prune,
- app_log_entries_clear,
- app_log_entries_dump,
- app_log_entries_load,
- app_log_metadata,
- RadrootsAppContext,
- RadrootsAppKeyMapConfig,
- RadrootsAppLogEntry,
- RadrootsAppLogLevel,
-};
-use crate::t;
-
-#[cfg(target_arch = "wasm32")]
-use js_sys::Array;
-
-const LOGS_AUTO_REFRESH_MS: u32 = 5000;
-const LOGS_MAX_VISIBLE: usize = 500;
-const LOGS_PAGE_SIZE: usize = 100;
-
-fn logs_auto_refresh_ms() -> u32 {
- LOGS_AUTO_REFRESH_MS
-}
-
-fn logs_max_visible() -> usize {
- LOGS_MAX_VISIBLE
-}
-
-fn logs_page_size_default() -> usize {
- LOGS_PAGE_SIZE
-}
-
-fn logs_datastore() -> RadrootsClientWebDatastore {
- RadrootsClientWebDatastore::new(Some(IDB_CONFIG_LOGS))
-}
-
-fn logs_resolve_backends(
- context: &Option<RadrootsAppContext>,
-) -> Option<(RadrootsClientWebDatastore, RadrootsAppKeyMapConfig)> {
- context.as_ref().and_then(|context| {
- context.backends.with_untracked(|value| {
- value.as_ref().map(|backends| {
- (logs_datastore(), backends.config.datastore.key_maps.clone())
- })
- })
- })
-}
-
-fn logs_refresh(
- context: &Option<RadrootsAppContext>,
- entries: RwSignal<Vec<RadrootsAppLogEntry>, LocalStorage>,
- loading: RwSignal<bool, LocalStorage>,
- dump_error: RwSignal<Option<String>, LocalStorage>,
-) {
- let Some((datastore, key_maps)) = logs_resolve_backends(context) else {
- entries.set(Vec::new());
- dump_error.set(None);
- return;
- };
- loading.set(true);
- spawn_local(async move {
- let _ = app_log_buffer_flush_no_prune(&datastore, &key_maps).await;
- let result = app_log_entries_load(&datastore, &key_maps).await;
- match result {
- Ok(mut items) => {
- items.sort_by(|a, b| b.timestamp_ms.cmp(&a.timestamp_ms));
- dump_error.set(None);
- entries.set(items);
- }
- Err(err) => {
- dump_error.set(Some(format!("error: {err}")));
- entries.set(Vec::new());
- }
- }
- loading.set(false);
- });
-}
-
-fn logs_clear(
- context: &Option<RadrootsAppContext>,
- entries: RwSignal<Vec<RadrootsAppLogEntry>, LocalStorage>,
- loading: RwSignal<bool, LocalStorage>,
- dump_error: RwSignal<Option<String>, LocalStorage>,
-) {
- let Some((datastore, key_maps)) = logs_resolve_backends(context) else {
- entries.set(Vec::new());
- dump_error.set(None);
- return;
- };
- loading.set(true);
- spawn_local(async move {
- let _ = app_log_entries_clear(&datastore, &key_maps).await;
- let _ = app_log_buffer_flush_no_prune(&datastore, &key_maps).await;
- let result = app_log_entries_load(&datastore, &key_maps).await;
- match result {
- Ok(mut items) => {
- items.sort_by(|a, b| b.timestamp_ms.cmp(&a.timestamp_ms));
- dump_error.set(None);
- entries.set(items);
- }
- Err(err) => {
- dump_error.set(Some(format!("error: {err}")));
- entries.set(Vec::new());
- }
- }
- loading.set(false);
- });
-}
-
-fn log_level_color(level: RadrootsAppLogLevel) -> &'static str {
- match level {
- RadrootsAppLogLevel::Debug => "#6b7280",
- RadrootsAppLogLevel::Info => "#0f172a",
- RadrootsAppLogLevel::Warn => "#b45309",
- RadrootsAppLogLevel::Error => "#b91c1c",
- }
-}
-
-fn log_status_label(value: &str) -> String {
- match value {
- "idle" => t!("app.logs.status.idle"),
- "loading" => t!("app.logs.status.loading"),
- "dump_empty" => t!("app.logs.status.dump_empty"),
- "copy_ok" => t!("app.logs.status.copy_ok"),
- "download_ok" => t!("app.logs.status.download_ok"),
- "support_copy_ok" => t!("app.logs.status.support_copy_ok"),
- "support_bundle_ready" => t!("app.logs.status.support_bundle_ready"),
- "copy_unavailable" => t!("app.logs.error.copy_unavailable"),
- "copy_failed" => t!("app.logs.error.copy_failed"),
- "window_unavailable" => t!("app.logs.error.window_unavailable"),
- "download_unavailable" => t!("app.logs.error.download_unavailable"),
- "document_unavailable" => t!("app.logs.error.document_unavailable"),
- "blob_failed" => t!("app.logs.error.blob_failed"),
- "url_failed" => t!("app.logs.error.url_failed"),
- "anchor_failed" => t!("app.logs.error.anchor_failed"),
- "anchor_cast_failed" => t!("app.logs.error.anchor_cast_failed"),
- _ => value.to_string(),
- }
-}
-
-fn log_level_matches(level: RadrootsAppLogLevel, filter: &str) -> bool {
- if filter.is_empty() || filter == "all" {
- return true;
- }
- level.as_str() == filter
-}
-
-fn log_query_matches(entry: &RadrootsAppLogEntry, query: &str) -> bool {
- let trimmed = query.trim();
- if trimmed.is_empty() {
- return true;
- }
- let needle = trimmed.to_lowercase();
- if entry.code.to_lowercase().contains(&needle) {
- return true;
- }
- if entry.message.to_lowercase().contains(&needle) {
- return true;
- }
- if let Some(context) = entry.context.as_ref() {
- return context.to_lowercase().contains(&needle);
- }
- false
-}
-
-fn parse_log_timestamp(value: &str) -> Option<i64> {
- let trimmed = value.trim();
- if trimmed.is_empty() {
- return None;
- }
- trimmed.parse::<i64>().ok()
-}
-
-fn log_timestamp_matches(timestamp_ms: i64, from_ms: Option<i64>, to_ms: Option<i64>) -> bool {
- if let Some(from_ms) = from_ms {
- if timestamp_ms < from_ms {
- return false;
- }
- }
- if let Some(to_ms) = to_ms {
- if timestamp_ms > to_ms {
- return false;
- }
- }
- true
-}
-
-fn log_entry_matches(
- entry: &RadrootsAppLogEntry,
- level_filter: &str,
- query: &str,
- from_ms: Option<i64>,
- to_ms: Option<i64>,
-) -> bool {
- log_level_matches(entry.level, level_filter)
- && log_query_matches(entry, query)
- && log_timestamp_matches(entry.timestamp_ms, from_ms, to_ms)
-}
-
-fn log_page_count(total: usize, page_size: usize) -> usize {
- if page_size == 0 {
- return 0;
- }
- (total + page_size - 1) / page_size
-}
-
-fn log_entries_page(
- entries: &[RadrootsAppLogEntry],
- page_index: usize,
- page_size: usize,
-) -> Vec<RadrootsAppLogEntry> {
- if page_size == 0 {
- return Vec::new();
- }
- let start = page_index.saturating_mul(page_size);
- if start >= entries.len() {
- return Vec::new();
- }
- let end = (start + page_size).min(entries.len());
- entries[start..end].to_vec()
-}
-
-fn log_page_index_clamp(page_index: usize, total_pages: usize) -> usize {
- if total_pages == 0 {
- return 0;
- }
- if page_index >= total_pages {
- return total_pages - 1;
- }
- page_index
-}
-
-fn log_dump_config_from_app(config: &crate::RadrootsAppConfig) -> RadrootsAppLogDumpConfig {
- RadrootsAppLogDumpConfig {
- datastore_database: config.datastore.idb_config.database.to_string(),
- datastore_store: config.datastore.idb_config.store.to_string(),
- keystore_nostr_database: config.keystore.nostr_store.database.to_string(),
- keystore_nostr_store: config.keystore.nostr_store.store.to_string(),
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-struct RadrootsAppLogDumpConfig {
- datastore_database: String,
- datastore_store: String,
- keystore_nostr_database: String,
- keystore_nostr_store: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-struct RadrootsAppLogDumpFilters {
- level: String,
- query: String,
- from_ms: Option<i64>,
- to_ms: Option<i64>,
- page_size: usize,
- page_index: usize,
- limit: usize,
-}
-
-#[derive(Debug, Clone, Serialize)]
-struct RadrootsAppLogDumpStats {
- total_entries: usize,
- filtered_entries: usize,
- visible_entries: usize,
-}
-
-#[derive(Debug, Clone, Serialize)]
-struct RadrootsAppLogDumpContext {
- kind: String,
- generated_at_ms: i64,
- metadata: crate::RadrootsAppLogMetadata,
- config: Option<RadrootsAppLogDumpConfig>,
- filters: RadrootsAppLogDumpFilters,
- stats: RadrootsAppLogDumpStats,
-}
-
-fn log_dump_header_with_context(context: RadrootsAppLogDumpContext) -> String {
- serde_json::to_string(&context)
- .unwrap_or_else(|_| String::from("{\"error\":\"log_dump_header_failed\"}"))
-}
-
-fn log_dump_with_context(entries: &[RadrootsAppLogEntry], header: String) -> String {
- if entries.is_empty() {
- return String::new();
- }
- format!("{header}\n{}", app_log_entries_dump(entries))
-}
-
-fn support_instructions_text() -> String {
- let lines = [
- t!("app.logs.support.instructions.title"),
- t!("app.logs.support.instructions.download"),
- t!("app.logs.support.instructions.share"),
- t!("app.logs.support.instructions.notes"),
- ];
- lines.join("\n")
-}
-
-#[cfg(any(test, target_arch = "wasm32"))]
-fn log_dump_filename_from_ms(timestamp_ms: i64) -> String {
- format!("radroots-logs-{timestamp_ms}.jsonl")
-}
-
-#[cfg(target_arch = "wasm32")]
-fn log_dump_filename() -> String {
- log_dump_filename_from_ms(crate::app_log_timestamp_ms())
-}
-
-async fn log_dump_copy(text: String) -> Result<(), String> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = text;
- return Err(String::from("copy_unavailable"));
- }
- #[cfg(target_arch = "wasm32")]
- {
- let Some(window) = web_sys::window() else {
- return Err(String::from("window_unavailable"));
- };
- let clipboard = window.navigator().clipboard();
- let promise = clipboard.write_text(&text);
- JsFuture::from(promise)
- .await
- .map_err(|_| String::from("copy_failed"))?;
- Ok(())
- }
-}
-
-async fn log_dump_download(text: String) -> Result<(), String> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = text;
- return Err(String::from("download_unavailable"));
- }
- #[cfg(target_arch = "wasm32")]
- {
- let Some(window) = web_sys::window() else {
- return Err(String::from("window_unavailable"));
- };
- let Some(document) = window.document() else {
- return Err(String::from("document_unavailable"));
- };
- let parts = Array::new();
- parts.push(&JsValue::from_str(&text));
- let blob = web_sys::Blob::new_with_str_sequence(&parts)
- .map_err(|_| String::from("blob_failed"))?;
- let url = web_sys::Url::create_object_url_with_blob(&blob)
- .map_err(|_| String::from("url_failed"))?;
- let anchor = document
- .create_element("a")
- .map_err(|_| String::from("anchor_failed"))?
- .dyn_into::<web_sys::HtmlAnchorElement>()
- .map_err(|_| String::from("anchor_cast_failed"))?;
- anchor.set_href(&url);
- anchor.set_download(&log_dump_filename());
- anchor.click();
- let _ = web_sys::Url::revoke_object_url(&url);
- Ok(())
- }
-}
-
-#[component]
-pub fn RadrootsAppLogsPage() -> impl IntoView {
- let entries = RwSignal::new_local(Vec::<RadrootsAppLogEntry>::new());
- let dump_error = RwSignal::new_local(None::<String>);
- let loading = RwSignal::new_local(false);
- let dump_status = RwSignal::new_local(None::<String>);
- let dump_action_running = RwSignal::new_local(false);
- let support_status = RwSignal::new_local(None::<String>);
- let support_running = RwSignal::new_local(false);
- let did_load = RwSignal::new_local(false);
- let interval_started = RwSignal::new_local(false);
- let filter_query = RwSignal::new_local(String::new());
- let filter_level = RwSignal::new_local(String::from("all"));
- let filter_from = RwSignal::new_local(String::new());
- let filter_to = RwSignal::new_local(String::new());
- let filter_limit = RwSignal::new_local(logs_max_visible());
- let page_size = RwSignal::new_local(logs_page_size_default());
- let page_index = RwSignal::new_local(0usize);
- let dump_config = RwSignal::new_local(None::<RadrootsAppLogDumpConfig>);
- let context = app_context();
- let filtered_entries = Memo::new(move |_| {
- let level_filter = filter_level.get();
- let query = filter_query.get();
- let from_ms = parse_log_timestamp(&filter_from.get());
- let to_ms = parse_log_timestamp(&filter_to.get());
- let limit = filter_limit.get();
- entries.with(|items| {
- items
- .iter()
- .filter(|entry| log_entry_matches(entry, &level_filter, &query, from_ms, to_ms))
- .take(limit)
- .cloned()
- .collect::<Vec<_>>()
- })
- });
- let paged_entries = Memo::new(move |_| {
- let items = filtered_entries.get();
- log_entries_page(&items, page_index.get(), page_size.get())
- });
- let page_total = Memo::new(move |_| {
- log_page_count(filtered_entries.get().len(), page_size.get())
- });
- Effect::new(move || {
- let _ = filter_query.get();
- let _ = filter_level.get();
- let _ = filter_from.get();
- let _ = filter_to.get();
- page_index.set(0);
- });
- Effect::new(move || {
- let total_pages = page_total.get();
- let next = log_page_index_clamp(page_index.get(), total_pages);
- if next != page_index.get() {
- page_index.set(next);
- }
- });
- let dump_text = Memo::new(move |_| {
- if let Some(err) = dump_error.get() {
- return err;
- }
- let items = filtered_entries.get();
- let total_entries = entries.get().len();
- let filtered_len = filtered_entries.get().len();
- let visible_len = paged_entries.get().len();
- let filters = RadrootsAppLogDumpFilters {
- level: filter_level.get(),
- query: filter_query.get(),
- from_ms: parse_log_timestamp(&filter_from.get()),
- to_ms: parse_log_timestamp(&filter_to.get()),
- page_size: page_size.get(),
- page_index: page_index.get(),
- limit: filter_limit.get(),
- };
- let stats = RadrootsAppLogDumpStats {
- total_entries,
- filtered_entries: filtered_len,
- visible_entries: visible_len,
- };
- let context = RadrootsAppLogDumpContext {
- kind: String::from("radroots_log_dump"),
- generated_at_ms: crate::app_log_timestamp_ms(),
- metadata: app_log_metadata().clone(),
- config: dump_config.get(),
- filters,
- stats,
- };
- let header = log_dump_header_with_context(context);
- log_dump_with_context(&items, header)
- });
- let refresh_action = {
- let context = context.clone();
- move || {
- logs_refresh(&context, entries, loading, dump_error);
- }
- };
- let clear_action = {
- let context = context.clone();
- move || {
- logs_clear(&context, entries, loading, dump_error);
- }
- };
- let copy_dump = {
- let dump_text = dump_text.clone();
- move || {
- let text = dump_text.get();
- if text.is_empty() {
- dump_status.set(Some(String::from("dump_empty")));
- return;
- }
- dump_action_running.set(true);
- spawn_local(async move {
- let status = match log_dump_copy(text).await {
- Ok(()) => String::from("copy_ok"),
- Err(err) => err,
- };
- dump_status.set(Some(status));
- dump_action_running.set(false);
- });
- }
- };
- let download_dump = {
- let dump_text = dump_text.clone();
- move || {
- let text = dump_text.get();
- if text.is_empty() {
- dump_status.set(Some(String::from("dump_empty")));
- return;
- }
- dump_action_running.set(true);
- spawn_local(async move {
- let status = match log_dump_download(text).await {
- Ok(()) => String::from("download_ok"),
- Err(err) => err,
- };
- dump_status.set(Some(status));
- dump_action_running.set(false);
- });
- }
- };
- let copy_support = move || {
- let text = support_instructions_text();
- support_running.set(true);
- spawn_local(async move {
- let status = match log_dump_copy(text).await {
- Ok(()) => String::from("support_copy_ok"),
- Err(err) => err,
- };
- support_status.set(Some(status));
- support_running.set(false);
- });
- };
- let support_bundle = {
- let dump_text = dump_text.clone();
- move || {
- let text = dump_text.get();
- if text.is_empty() {
- support_status.set(Some(String::from("dump_empty")));
- return;
- }
- support_running.set(true);
- let instructions = support_instructions_text();
- spawn_local(async move {
- let download = log_dump_download(text).await;
- let copy = log_dump_copy(instructions).await;
- let status = match (download, copy) {
- (Ok(()), Ok(())) => String::from("support_bundle_ready"),
- (Err(err), _) => err,
- (_, Err(err)) => err,
- };
- support_status.set(Some(status));
- support_running.set(false);
- });
- }
- };
- Effect::new({
- let context = context.clone();
- move || {
- let Some(context_ref) = context.as_ref() else {
- return;
- };
- if did_load.get() {
- return;
- }
- let has_backends = context_ref.backends.with(|value| value.is_some());
- if !has_backends {
- return;
- }
- did_load.set(true);
- logs_refresh(&context, entries, loading, dump_error);
- }
- });
- Effect::new({
- let context = context.clone();
- move || {
- let Some(context) = context.as_ref() else {
- return;
- };
- let config = context
- .backends
- .with(|value| value.as_ref().map(|backends| backends.config.clone()));
- let Some(config) = config else {
- return;
- };
- dump_config.set(Some(log_dump_config_from_app(&config)));
- }
- });
- Effect::new({
- let context = context.clone();
- move || {
- if interval_started.get() {
- return;
- }
- interval_started.set(true);
- let context = context.clone();
- let entries = entries;
- let loading = loading;
- let dump_error = dump_error;
- let (abort_handle, abort_reg) = AbortHandle::new_pair();
- let abort_handle_cleanup = abort_handle.clone();
- spawn_local(async move {
- let mut ticks = IntervalStream::new(logs_auto_refresh_ms());
- let task = async move {
- while ticks.next().await.is_some() {
- logs_refresh(&context, entries, loading, dump_error);
- }
- };
- let _ = Abortable::new(task, abort_reg).await;
- });
- on_cleanup(move || abort_handle_cleanup.abort());
- }
- });
- let status_label = move || {
- if loading.get() {
- t!("app.logs.status.loading")
- } else {
- t!("app.logs.status.idle")
- }
- };
- let dump_action_label = move || {
- dump_status
- .get()
- .as_deref()
- .map(log_status_label)
- .unwrap_or_else(|| t!("app.logs.status.idle"))
- };
- let dump_action_disabled = move || dump_action_running.get();
- let support_label = move || {
- support_status
- .get()
- .as_deref()
- .map(log_status_label)
- .unwrap_or_else(|| t!("app.logs.status.idle"))
- };
- let support_disabled = move || support_running.get();
- let prev_disabled = move || page_index.get() == 0;
- let next_disabled = move || {
- let total = page_total.get();
- total == 0 || page_index.get() + 1 >= total
- };
- view! {
- <AppPageChrome title=t!("app.logs.title")>
- <header id="app-logs-header" style="display:flex;align-items:center;gap:12px;">
- <button on:click=move |_| refresh_action()>{t!("app.logs.action.refresh")}</button>
- <button on:click=move |_| clear_action()>{t!("app.logs.action.clear")}</button>
- <button on:click=move |_| copy_dump() disabled=dump_action_disabled>{t!("app.logs.action.copy_dump")}</button>
- <button on:click=move |_| download_dump() disabled=dump_action_disabled>{t!("app.logs.action.download_dump")}</button>
- <div id="app-logs-status" style="font-size:12px;color:#6b7280;">{status_label}</div>
- <div id="app-logs-dump-status" style="font-size:12px;color:#6b7280;">{dump_action_label}</div>
- </header>
- <section id="app-logs-filters" aria-label=t!("app.logs.filters.aria") style="margin-top:12px;display:flex;flex-wrap:wrap;gap:12px;align-items:center;">
- <input
- type="text"
- placeholder=t!("app.logs.filters.search_placeholder")
- prop:value=move || filter_query.get()
- on:input=move |ev| {
- filter_query.set(event_target_value(&ev));
- }
- style="flex:1 1 260px;border:1px solid #e5e7eb;border-radius:8px;padding:6px 8px;font-size:12px;"
- />
- <select
- prop:value=move || filter_level.get()
- on:change=move |ev| {
- filter_level.set(event_target_value(&ev));
- }
- style="border:1px solid #e5e7eb;border-radius:8px;padding:6px 8px;font-size:12px;"
- >
- <option value="all">{t!("app.logs.level.all")}</option>
- <option value="debug">{t!("app.logs.level.debug")}</option>
- <option value="info">{t!("app.logs.level.info")}</option>
- <option value="warn">{t!("app.logs.level.warn")}</option>
- <option value="error">{t!("app.logs.level.error")}</option>
- </select>
- <input
- type="number"
- placeholder=t!("app.logs.filters.from_ms")
- prop:value=move || filter_from.get()
- on:input=move |ev| {
- filter_from.set(event_target_value(&ev));
- }
- style="width:130px;border:1px solid #e5e7eb;border-radius:8px;padding:6px 8px;font-size:12px;"
- />
- <input
- type="number"
- placeholder=t!("app.logs.filters.to_ms")
- prop:value=move || filter_to.get()
- on:input=move |ev| {
- filter_to.set(event_target_value(&ev));
- }
- style="width:130px;border:1px solid #e5e7eb;border-radius:8px;padding:6px 8px;font-size:12px;"
- />
- <select
- prop:value=move || page_size.get().to_string()
- on:change=move |ev| {
- if let Ok(size) = event_target_value(&ev).parse::<usize>() {
- page_size.set(size);
- page_index.set(0);
- }
- }
- style="border:1px solid #e5e7eb;border-radius:8px;padding:6px 8px;font-size:12px;"
- >
- <option value="25">"25"</option>
- <option value="50">"50"</option>
- <option value="100">"100"</option>
- <option value="250">"250"</option>
- </select>
- <div id="app-logs-filter-summary" style="font-size:12px;color:#6b7280;">
- {move || {
- let total = entries.get().len();
- let visible = filtered_entries.get().len();
- let limit = filter_limit.get();
- let pages = page_total.get();
- let page = if pages == 0 { 0 } else { page_index.get() + 1 };
- format!(
- "{} {} {} {} ({} {}) {} {}/{}",
- t!("app.logs.summary.showing"),
- visible,
- t!("app.logs.summary.of"),
- total,
- t!("app.logs.summary.limit"),
- limit,
- t!("app.logs.summary.page"),
- page,
- pages
- )
- }}
- </div>
- </section>
- <section id="app-logs-pagination" aria-label=t!("app.logs.pagination.aria") style="margin-top:8px;display:flex;align-items:center;gap:8px;">
- <button
- on:click=move |_| {
- let next = page_index.get().saturating_sub(1);
- page_index.set(next);
- }
- disabled=prev_disabled
- >
- {t!("app.logs.pagination.prev")}
- </button>
- <button
- on:click=move |_| {
- let next = page_index.get() + 1;
- page_index.set(next);
- }
- disabled=next_disabled
- >
- {t!("app.logs.pagination.next")}
- </button>
- <button on:click=move |_| support_bundle() disabled=support_disabled>
- {t!("app.logs.support.button.bundle")}
- </button>
- <button on:click=move |_| copy_support() disabled=support_disabled>
- {t!("app.logs.support.button.copy_instructions")}
- </button>
- <div id="app-logs-support-status" style="font-size:12px;color:#6b7280;">{support_label}</div>
- </section>
- <section id="app-logs-content" aria-label=t!("app.logs.content.aria") style="margin-top:12px;display:flex;flex-wrap:wrap;gap:16px;">
- <section id="app-logs-entries" style="flex:1 1 520px;min-width:280px;">
- <h2 id="app-logs-entries-title" style="font-weight:600;font-size:14px;">{t!("app.logs.entries.title")}</h2>
- <div id="app-logs-entries-list" style="margin-top:8px;border:1px solid #e5e7eb;border-radius:8px;height:60vh;overflow:auto;padding:10px;display:flex;flex-direction:column;gap:10px;">
- <For
- each=move || paged_entries.get()
- key=|entry| entry.id.clone()
- children=move |entry| {
- let level = entry.level;
- let timestamp_ms = entry.timestamp_ms;
- let code = entry.code;
- let message = entry.message;
- let context = entry.context;
- view! {
- <div style="display:flex;flex-direction:column;gap:4px;">
- <div style="display:flex;align-items:baseline;gap:8px;">
- <span style="font-size:11px;color:#6b7280;">
- {timestamp_ms}
- </span>
- <span
- style=move || format!(
- "font-size:11px;font-weight:600;color:{};",
- log_level_color(level)
- )
- >
- {level.as_str()}
- </span>
- <span style="font-size:12px;font-weight:600;color:#111827;">
- {code}
- </span>
- </div>
- <div style="font-size:13px;color:#111827;">
- {message}
- </div>
- {context.map(|context| {
- view! {
- <div style="font-size:12px;color:#6b7280;">
- {context}
- </div>
- }
- })}
- </div>
- }
- }
- />
- </div>
- </section>
- <section id="app-logs-dump" style="flex:1 1 320px;min-width:260px;">
- <h2 id="app-logs-dump-title" style="font-weight:600;font-size:14px;">{t!("app.logs.dump.title")}</h2>
- <textarea
- readonly
- prop:value=move || dump_text.get()
- style="margin-top:8px;width:100%;height:60vh;border:1px solid #e5e7eb;border-radius:8px;padding:8px;font-size:12px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace;"
- ></textarea>
- <h3 id="app-logs-support-title" style="margin-top:12px;font-weight:600;font-size:14px;">{t!("app.logs.support.title")}</h3>
- <textarea
- readonly
- prop:value=move || support_instructions_text()
- style="margin-top:8px;width:100%;height:140px;border:1px solid #e5e7eb;border-radius:8px;padding:8px;font-size:12px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace;"
- ></textarea>
- </section>
- </section>
- </AppPageChrome>
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- log_dump_filename_from_ms,
- log_dump_header_with_context,
- log_dump_with_context,
- log_entry_matches,
- log_entries_page,
- RadrootsAppLogDumpContext,
- RadrootsAppLogDumpFilters,
- RadrootsAppLogDumpStats,
- log_page_index_clamp,
- log_page_count,
- log_timestamp_matches,
- parse_log_timestamp,
- support_instructions_text,
- logs_auto_refresh_ms,
- logs_max_visible,
- logs_page_size_default,
- };
- use crate::{app_i18n_init, RadrootsAppLogEntry, RadrootsAppLogLevel, RadrootsAppLogMetadata};
- use leptos::prelude::{provide_context, Owner};
-
- #[test]
- fn logs_auto_refresh_is_positive() {
- assert!(logs_auto_refresh_ms() > 0);
- }
-
- #[test]
- fn log_dump_filename_uses_timestamp() {
- let name = log_dump_filename_from_ms(123);
- assert_eq!(name, "radroots-logs-123.jsonl");
- }
-
- #[test]
- fn logs_max_visible_is_positive() {
- assert!(logs_max_visible() > 0);
- }
-
- #[test]
- fn logs_page_size_default_is_positive() {
- assert!(logs_page_size_default() > 0);
- }
-
- #[test]
- fn log_entry_matches_filters_level_and_query() {
- let entry = RadrootsAppLogEntry {
- id: String::from("a"),
- timestamp_ms: 1,
- level: RadrootsAppLogLevel::Info,
- code: String::from("log.code.test"),
- message: String::from("Hello World"),
- context: Some(String::from("context")),
- metadata: RadrootsAppLogMetadata::default(),
- };
- assert!(log_entry_matches(&entry, "info", "hello", None, None));
- assert!(!log_entry_matches(&entry, "error", "hello", None, None));
- assert!(!log_entry_matches(&entry, "info", "missing", None, None));
- }
-
- #[test]
- fn log_dump_with_context_prefixes_dump() {
- let entry = RadrootsAppLogEntry {
- id: String::from("a"),
- timestamp_ms: 1,
- level: RadrootsAppLogLevel::Info,
- code: String::from("log.code.test"),
- message: String::from("Hello"),
- context: None,
- metadata: RadrootsAppLogMetadata::default(),
- };
- let context = RadrootsAppLogDumpContext {
- kind: String::from("radroots_log_dump"),
- generated_at_ms: 1,
- metadata: RadrootsAppLogMetadata::default(),
- config: None,
- filters: RadrootsAppLogDumpFilters {
- level: String::from("all"),
- query: String::new(),
- from_ms: None,
- to_ms: None,
- page_size: 100,
- page_index: 0,
- limit: 500,
- },
- stats: RadrootsAppLogDumpStats {
- total_entries: 1,
- filtered_entries: 1,
- visible_entries: 1,
- },
- };
- let header = log_dump_header_with_context(context);
- let dump = log_dump_with_context(&[entry], header);
- let mut lines = dump.lines();
- let header = lines.next().expect("header");
- assert!(header.contains("radroots_log_dump"));
- let entry_line = lines.next().expect("entry");
- assert!(entry_line.contains("\"log.code.test\""));
- }
-
- #[test]
- fn parse_log_timestamp_accepts_integers() {
- assert_eq!(parse_log_timestamp("123"), Some(123));
- assert_eq!(parse_log_timestamp(""), None);
- assert_eq!(parse_log_timestamp("abc"), None);
- }
-
- #[test]
- fn log_timestamp_matches_respects_bounds() {
- assert!(log_timestamp_matches(100, None, None));
- assert!(log_timestamp_matches(100, Some(50), None));
- assert!(log_timestamp_matches(100, None, Some(150)));
- assert!(!log_timestamp_matches(100, Some(120), None));
- assert!(!log_timestamp_matches(100, None, Some(80)));
- }
-
- #[test]
- fn log_page_count_rounds_up() {
- assert_eq!(log_page_count(0, 10), 0);
- assert_eq!(log_page_count(1, 10), 1);
- assert_eq!(log_page_count(11, 10), 2);
- }
-
- #[test]
- fn log_entries_page_slices() {
- let entries = (0..5)
- .map(|idx| RadrootsAppLogEntry {
- id: format!("id-{idx}"),
- timestamp_ms: idx,
- level: RadrootsAppLogLevel::Info,
- code: String::from("log.code.test"),
- message: String::from("Hello"),
- context: None,
- metadata: RadrootsAppLogMetadata::default(),
- })
- .collect::<Vec<_>>();
- let page = log_entries_page(&entries, 1, 2);
- assert_eq!(page.len(), 2);
- assert_eq!(page[0].id, "id-2");
- }
-
- #[test]
- fn log_page_index_clamp_bounds() {
- assert_eq!(log_page_index_clamp(0, 0), 0);
- assert_eq!(log_page_index_clamp(3, 2), 1);
- assert_eq!(log_page_index_clamp(1, 3), 1);
- }
-
- #[test]
- fn support_instructions_are_present() {
- let owner = Owner::new();
- owner.set();
- provide_context(app_i18n_init());
- let text = support_instructions_text();
- assert!(text.contains("support bundle"));
- }
-}
diff --git a/app/src/notifications.rs b/app/src/notifications.rs
@@ -1,165 +0,0 @@
-#![forbid(unsafe_code)]
-
-use radroots_app_core::notifications::{
- RadrootsClientNotifications,
- RadrootsClientNotificationsConfig,
- RadrootsClientNotificationsDialogConfirmOpts,
- RadrootsClientNotificationsError,
- RadrootsClientNotificationsPermission,
- RadrootsClientWebNotifications,
-};
-
-use crate::app_log_debug_emit;
-
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen::JsValue;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppNotificationsError {
- Notifications(RadrootsClientNotificationsError),
-}
-
-pub type RadrootsAppNotificationsResult<T> = Result<T, RadrootsAppNotificationsError>;
-
-impl RadrootsAppNotificationsError {
- pub const fn message(self) -> &'static str {
- match self {
- RadrootsAppNotificationsError::Notifications(err) => err.message(),
- }
- }
-}
-
-impl std::fmt::Display for RadrootsAppNotificationsError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_str(self.message())
- }
-}
-
-impl std::error::Error for RadrootsAppNotificationsError {}
-
-impl From<RadrootsClientNotificationsError> for RadrootsAppNotificationsError {
- fn from(err: RadrootsClientNotificationsError) -> Self {
- RadrootsAppNotificationsError::Notifications(err)
- }
-}
-
-pub struct RadrootsAppNotifications {
- client: RadrootsClientWebNotifications,
-}
-
-impl RadrootsAppNotifications {
- pub fn new(config: Option<RadrootsClientNotificationsConfig>) -> Self {
- Self {
- client: RadrootsClientWebNotifications::new(config),
- }
- }
-
- pub fn get_config(&self) -> &RadrootsClientNotificationsConfig {
- self.client.get_config()
- }
-
- #[cfg(target_arch = "wasm32")]
- fn notification_available(window: &web_sys::Window) -> bool {
- js_sys::Reflect::has(window.as_ref(), &JsValue::from_str("Notification"))
- .unwrap_or(false)
- }
-
- #[cfg(target_arch = "wasm32")]
- fn permission_from_web(
- permission: web_sys::NotificationPermission,
- ) -> RadrootsClientNotificationsPermission {
- match permission {
- web_sys::NotificationPermission::Granted => {
- RadrootsClientNotificationsPermission::Granted
- }
- web_sys::NotificationPermission::Denied => RadrootsClientNotificationsPermission::Denied,
- web_sys::NotificationPermission::Default => {
- RadrootsClientNotificationsPermission::Default
- }
- _ => RadrootsClientNotificationsPermission::Unavailable,
- }
- }
-
- pub async fn permission(
- &self,
- ) -> RadrootsAppNotificationsResult<RadrootsClientNotificationsPermission> {
- let _ = app_log_debug_emit("log.app.notifications.permission", "start", None);
- #[cfg(not(target_arch = "wasm32"))]
- {
- return Ok(RadrootsClientNotificationsPermission::Unavailable);
- }
- #[cfg(target_arch = "wasm32")]
- {
- let window =
- web_sys::window().ok_or(RadrootsClientNotificationsError::Unavailable)?;
- if !Self::notification_available(&window) {
- return Ok(RadrootsClientNotificationsPermission::Unavailable);
- }
- let permission = Self::permission_from_web(web_sys::Notification::permission());
- let _ = app_log_debug_emit(
- "log.app.notifications.permission",
- "resolved",
- Some(permission.as_str().to_string()),
- );
- Ok(permission)
- }
- }
-
- pub async fn request_permission(
- &self,
- ) -> RadrootsAppNotificationsResult<RadrootsClientNotificationsPermission> {
- let _ = app_log_debug_emit("log.app.notifications.request", "start", None);
- let result = self.client
- .notify_init()
- .await
- .map_err(RadrootsAppNotificationsError::from);
- if let Ok(permission) = &result {
- let _ = app_log_debug_emit(
- "log.app.notifications.request",
- "resolved",
- Some(permission.as_str().to_string()),
- );
- }
- result
- }
-
- pub async fn confirm_message(&self, message: &str) -> bool {
- self.client
- .confirm(RadrootsClientNotificationsDialogConfirmOpts::Message(
- message.to_string(),
- ))
- .await
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{RadrootsAppNotifications, RadrootsAppNotificationsError};
- use radroots_app_core::notifications::{
- RadrootsClientNotificationsConfig,
- RadrootsClientNotificationsError,
- RadrootsClientNotificationsPermission,
- };
-
- #[test]
- fn permission_is_unavailable_on_native() {
- let app = RadrootsAppNotifications::new(Some(RadrootsClientNotificationsConfig {
- app_name: String::from("Radroots"),
- }));
- let permission = futures::executor::block_on(app.permission())
- .expect("permission");
- assert_eq!(permission, RadrootsClientNotificationsPermission::Unavailable);
- }
-
- #[test]
- fn request_permission_maps_errors() {
- let app = RadrootsAppNotifications::new(None);
- let err = futures::executor::block_on(app.request_permission())
- .expect_err("permission request error");
- assert_eq!(
- err,
- RadrootsAppNotificationsError::Notifications(RadrootsClientNotificationsError::Unavailable)
- );
- assert_eq!(err.to_string(), "error.client.notifications.unavailable");
- }
-}
diff --git a/app/src/settings.rs b/app/src/settings.rs
@@ -1,369 +0,0 @@
-#![forbid(unsafe_code)]
-
-use leptos::ev::MouseEvent;
-use leptos::prelude::*;
-use leptos::task::spawn_local;
-use leptos_router::hooks::use_navigate;
-
-use crate::{
- app::AppPageChrome,
- app_context,
- app_datastore_clear_config,
- app_log_error_emit,
- app_theme_apply_mode,
- app_theme_mode_from_value,
- app_theme_read_mode,
- app_theme_store_mode,
- t,
- RadrootsAppBackends,
- RadrootsAppConfigStatus,
- RadrootsAppThemeMode,
-};
-use radroots_app_ui_components::{
- RadrootsAppUiList,
- RadrootsAppUiListIcon,
- RadrootsAppUiListItem,
- RadrootsAppUiListItemKind,
- RadrootsAppUiListLabel,
- RadrootsAppUiListLabelText,
- RadrootsAppUiListLabelValue,
- RadrootsAppUiListLabelValueKind,
- RadrootsAppUiListSelect,
- RadrootsAppUiListSelectField,
- RadrootsAppUiListSelectOption,
- RadrootsAppUiListTitle,
- RadrootsAppUiListTitleValue,
- RadrootsAppUiListTouch,
- RadrootsAppUiListTouchEnd,
- RadrootsAppUiListView,
-};
-
-fn log_settings_action(action: &str) {
- #[cfg(target_arch = "wasm32")]
- {
- web_sys::console::log_1(&action.into());
- }
- #[cfg(not(target_arch = "wasm32"))]
- {
- println!("{action}");
- }
-}
-
-fn settings_touch_callback(action: &'static str) -> Callback<MouseEvent> {
- Callback::new(move |_| log_settings_action(action))
-}
-
-fn settings_capitalize(value: &str) -> String {
- let mut chars = value.chars();
- let Some(first) = chars.next() else {
- return String::new();
- };
- let mut out = String::new();
- out.extend(first.to_uppercase());
- out.push_str(chars.as_str());
- out
-}
-
-fn settings_label(value: String, classes: Option<&str>) -> RadrootsAppUiListLabelValue {
- RadrootsAppUiListLabelValue {
- classes_wrap: None,
- hide_truncate: false,
- value: RadrootsAppUiListLabelValueKind::Text(RadrootsAppUiListLabelText {
- value,
- classes: classes.map(str::to_string),
- }),
- }
-}
-
-#[component]
-pub fn RadrootsAppSettingsPage() -> impl IntoView {
- let context = app_context();
- let fallback_backends = RwSignal::new_local(None::<RadrootsAppBackends>);
- let fallback_config_status = RwSignal::new_local(RadrootsAppConfigStatus::Unknown);
- let backends = context
- .as_ref()
- .map(|value| value.backends)
- .unwrap_or(fallback_backends);
- let config_status = context
- .as_ref()
- .map(|value| value.config_status)
- .unwrap_or(fallback_config_status);
- let navigate = use_navigate();
- let initial_mode = app_theme_read_mode().unwrap_or(RadrootsAppThemeMode::System);
- let color_mode_value = initial_mode.as_str().to_string();
- let color_mode_callback = Callback::new(move |value: String| {
- log_settings_action("settings_color_mode");
- let Some(mode) = app_theme_mode_from_value(&value) else {
- return;
- };
- let _ = app_theme_store_mode(mode);
- let _ = app_theme_apply_mode(mode);
- });
- let appearance_list = RadrootsAppUiList {
- id: Some("settings-appearance".to_string()),
- view: Some("settings".to_string()),
- classes: None,
- title: Some(RadrootsAppUiListTitle {
- value: RadrootsAppUiListTitleValue::Text(t!("app.settings.appearance.title")),
- classes: None,
- mod_value: None,
- link: None,
- on_click: None,
- }),
- default_state: None,
- list: Some(vec![Some(RadrootsAppUiListItem {
- kind: RadrootsAppUiListItemKind::Select(RadrootsAppUiListSelect {
- field: RadrootsAppUiListSelectField {
- value: color_mode_value,
- options: vec![
- RadrootsAppUiListSelectOption {
- label: settings_capitalize(
- &t!("app.settings.appearance.color_mode.option.system"),
- ),
- value: "system".to_string(),
- classes: None,
- },
- RadrootsAppUiListSelectOption {
- label: settings_capitalize(
- &t!("app.settings.appearance.color_mode.option.light"),
- ),
- value: "light".to_string(),
- classes: None,
- },
- RadrootsAppUiListSelectOption {
- label: settings_capitalize(
- &t!("app.settings.appearance.color_mode.option.dark"),
- ),
- value: "dark".to_string(),
- classes: None,
- },
- ],
- disabled: false,
- classes: None,
- id: Some("settings-color-mode".to_string()),
- on_change: Some(color_mode_callback),
- },
- label: RadrootsAppUiListLabel {
- left: vec![settings_label(
- t!("app.settings.appearance.color_mode.label"),
- Some("capitalize"),
- )],
- right: Vec::new(),
- },
- display: None,
- end: Some(RadrootsAppUiListTouchEnd {
- icon: RadrootsAppUiListIcon {
- key: "chevrons-up-down".to_string(),
- class: None,
- },
- on_click: None,
- }),
- loading: false,
- on_click: None,
- }),
- loading: false,
- hide_active: true,
- hide_field: false,
- full_rounded: false,
- offset: None,
- })]),
- hide_offset: false,
- styles: None,
- };
- let logs_navigate = navigate.clone();
- let reconfigure_action = {
- let navigate = navigate.clone();
- let backends = backends.clone();
- let config_status = config_status.clone();
- Callback::new(move |_| {
- let Some((datastore, key_maps)) = backends.with(|value| {
- value.as_ref().map(|backends| {
- (
- backends.datastore.clone(),
- backends.config.datastore.key_maps.clone(),
- )
- })
- }) else {
- return;
- };
- let navigate = navigate.clone();
- let config_status = config_status.clone();
- spawn_local(async move {
- match app_datastore_clear_config(datastore.as_ref(), &key_maps).await {
- Ok(()) => {
- config_status.set(RadrootsAppConfigStatus::Required);
- navigate("/setup/config", Default::default());
- }
- Err(err) => {
- let _ = app_log_error_emit(&err);
- }
- }
- });
- })
- };
- let actions_list = RadrootsAppUiList {
- id: Some("settings-actions".to_string()),
- view: Some("settings".to_string()),
- classes: None,
- title: None,
- default_state: None,
- list: Some(vec![
- Some(RadrootsAppUiListItem {
- kind: RadrootsAppUiListItemKind::Touch(RadrootsAppUiListTouch {
- label: RadrootsAppUiListLabel {
- left: vec![settings_label(
- t!("app.settings.actions.export_db"),
- Some("capitalize"),
- )],
- right: Vec::new(),
- },
- display: None,
- end: Some(RadrootsAppUiListTouchEnd {
- icon: RadrootsAppUiListIcon {
- key: "caret-right".to_string(),
- class: None,
- },
- on_click: None,
- }),
- on_click: Some(settings_touch_callback("settings_export_database")),
- }),
- loading: false,
- hide_active: true,
- hide_field: false,
- full_rounded: false,
- offset: None,
- }),
- Some(RadrootsAppUiListItem {
- kind: RadrootsAppUiListItemKind::Touch(RadrootsAppUiListTouch {
- label: RadrootsAppUiListLabel {
- left: vec![settings_label(
- "update configuration".to_string(),
- Some("capitalize"),
- )],
- right: Vec::new(),
- },
- display: None,
- end: Some(RadrootsAppUiListTouchEnd {
- icon: RadrootsAppUiListIcon {
- key: "caret-right".to_string(),
- class: None,
- },
- on_click: None,
- }),
- on_click: Some(reconfigure_action),
- }),
- loading: false,
- hide_active: true,
- hide_field: false,
- full_rounded: false,
- offset: None,
- }),
- Some(RadrootsAppUiListItem {
- kind: RadrootsAppUiListItemKind::Touch(RadrootsAppUiListTouch {
- label: RadrootsAppUiListLabel {
- left: vec![settings_label(t!("app.nav.logs"), Some("capitalize"))],
- right: Vec::new(),
- },
- display: None,
- end: Some(RadrootsAppUiListTouchEnd {
- icon: RadrootsAppUiListIcon {
- key: "caret-right".to_string(),
- class: None,
- },
- on_click: None,
- }),
- on_click: Some(Callback::new(move |_| {
- logs_navigate("/settings/logs", Default::default());
- })),
- }),
- loading: false,
- hide_active: true,
- hide_field: false,
- full_rounded: false,
- offset: None,
- }),
- Some(RadrootsAppUiListItem {
- kind: RadrootsAppUiListItemKind::Touch(RadrootsAppUiListTouch {
- label: RadrootsAppUiListLabel {
- left: vec![settings_label(
- t!("app.settings.actions.logout"),
- Some("capitalize"),
- )],
- right: Vec::new(),
- },
- display: None,
- end: Some(RadrootsAppUiListTouchEnd {
- icon: RadrootsAppUiListIcon {
- key: "caret-right".to_string(),
- class: None,
- },
- on_click: None,
- }),
- on_click: Some(settings_touch_callback("settings_logout")),
- }),
- loading: false,
- hide_active: true,
- hide_field: false,
- full_rounded: false,
- offset: None,
- }),
- ]),
- hide_offset: false,
- styles: None,
- };
- let system_status_action = {
- let navigate = navigate.clone();
- Callback::new(move |_| {
- navigate("/settings/status", Default::default());
- })
- };
- let system_list = RadrootsAppUiList {
- id: Some("settings-system".to_string()),
- view: Some("settings".to_string()),
- classes: None,
- title: Some(RadrootsAppUiListTitle {
- value: RadrootsAppUiListTitleValue::Text(t!("app.settings.system.title")),
- classes: None,
- mod_value: None,
- link: None,
- on_click: None,
- }),
- default_state: None,
- list: Some(vec![Some(RadrootsAppUiListItem {
- kind: RadrootsAppUiListItemKind::Touch(RadrootsAppUiListTouch {
- label: RadrootsAppUiListLabel {
- left: vec![settings_label(
- t!("app.settings.system.status"),
- Some("capitalize"),
- )],
- right: Vec::new(),
- },
- display: None,
- end: Some(RadrootsAppUiListTouchEnd {
- icon: RadrootsAppUiListIcon {
- key: "chevron-right".to_string(),
- class: None,
- },
- on_click: None,
- }),
- on_click: Some(system_status_action),
- }),
- loading: false,
- hide_active: false,
- hide_field: false,
- full_rounded: false,
- offset: None,
- })]),
- hide_offset: false,
- styles: None,
- };
- view! {
- <AppPageChrome title=t!("app.settings.title")>
- <section id="app-settings-content" class="flex flex-col gap-4">
- <RadrootsAppUiListView basis=appearance_list />
- <RadrootsAppUiListView basis=actions_list />
- <RadrootsAppUiListView basis=system_list />
- </section>
- </AppPageChrome>
- }
-}
diff --git a/app/src/settings_status.rs b/app/src/settings_status.rs
@@ -1,355 +0,0 @@
-#![forbid(unsafe_code)]
-
-use gloo_timers::future::TimeoutFuture;
-use leptos::prelude::*;
-use leptos::task::spawn_local;
-
-use crate::{
- app::AppPageChrome,
- active_key_label,
- app_context,
- app_health_check_delay_ms,
- health_result_label,
- health_status_class,
- spawn_health_checks,
- t,
- RadrootsAppBackends,
- RadrootsAppConfigStatus,
- RadrootsAppHealthCheckResult,
- RadrootsAppHealthReport,
- RadrootsAppSetupStatus,
-};
-use radroots_app_ui_components::{
- RadrootsAppUiList,
- RadrootsAppUiListItem,
- RadrootsAppUiListItemKind,
- RadrootsAppUiListLabel,
- RadrootsAppUiListLabelText,
- RadrootsAppUiListLabelValue,
- RadrootsAppUiListLabelValueKind,
- RadrootsAppUiListTitle,
- RadrootsAppUiListTitleValue,
- RadrootsAppUiListTouch,
- RadrootsAppUiListView,
-};
-
-fn status_dot(status_class: &str) -> RadrootsAppUiListLabelValue {
- RadrootsAppUiListLabelValue {
- classes_wrap: Some("pr-1".to_string()),
- hide_truncate: true,
- value: RadrootsAppUiListLabelValueKind::Text(RadrootsAppUiListLabelText {
- value: "●".to_string(),
- classes: Some(format!("status-dot {}", status_class)),
- }),
- }
-}
-
-fn status_text(value: String) -> RadrootsAppUiListLabelValue {
- RadrootsAppUiListLabelValue {
- classes_wrap: None,
- hide_truncate: false,
- value: RadrootsAppUiListLabelValueKind::Text(RadrootsAppUiListLabelText {
- value,
- classes: None,
- }),
- }
-}
-
-fn config_status_label(status: RadrootsAppConfigStatus) -> String {
- match status {
- RadrootsAppConfigStatus::Unknown => t!("app.common.unknown"),
- RadrootsAppConfigStatus::Required => String::from("required"),
- RadrootsAppConfigStatus::Configured => String::from("configured"),
- RadrootsAppConfigStatus::Corrupt => String::from("corrupt"),
- }
-}
-
-fn config_status_class(status: RadrootsAppConfigStatus) -> &'static str {
- match status {
- RadrootsAppConfigStatus::Configured => "status-good",
- RadrootsAppConfigStatus::Required => "status-warn",
- RadrootsAppConfigStatus::Corrupt => "status-error",
- RadrootsAppConfigStatus::Unknown => "status-neutral",
- }
-}
-
-fn setup_status_label(status: RadrootsAppSetupStatus) -> String {
- match status {
- RadrootsAppSetupStatus::Unknown => t!("app.common.unknown"),
- RadrootsAppSetupStatus::Required => String::from("required"),
- RadrootsAppSetupStatus::Configured => String::from("configured"),
- RadrootsAppSetupStatus::Corrupt => String::from("corrupt"),
- RadrootsAppSetupStatus::Locked => String::from("locked"),
- }
-}
-
-fn setup_status_class(status: RadrootsAppSetupStatus) -> &'static str {
- match status {
- RadrootsAppSetupStatus::Configured => "status-good",
- RadrootsAppSetupStatus::Required | RadrootsAppSetupStatus::Locked => "status-warn",
- RadrootsAppSetupStatus::Corrupt => "status-error",
- RadrootsAppSetupStatus::Unknown => "status-neutral",
- }
-}
-
-fn status_row(label: String, result: RadrootsAppHealthCheckResult) -> RadrootsAppUiListItem {
- let status_label = health_result_label(&result);
- let status_class = health_status_class(result.status);
- RadrootsAppUiListItem {
- kind: RadrootsAppUiListItemKind::Touch(RadrootsAppUiListTouch {
- label: RadrootsAppUiListLabel {
- left: vec![status_text(label)],
- right: vec![status_text(status_label), status_dot(status_class)],
- },
- display: None,
- end: None,
- on_click: None,
- }),
- loading: false,
- hide_active: true,
- hide_field: false,
- full_rounded: false,
- offset: None,
- }
-}
-
-fn format_timestamp(ms: i64) -> String {
- #[cfg(target_arch = "wasm32")]
- {
- use leptos::wasm_bindgen::JsValue;
-
- let date = js_sys::Date::new(&JsValue::from_f64(ms as f64));
- return date
- .to_locale_string("en-US", &JsValue::UNDEFINED)
- .as_string()
- .unwrap_or_else(|| ms.to_string());
- }
- #[cfg(not(target_arch = "wasm32"))]
- {
- ms.to_string()
- }
-}
-
-#[component]
-pub fn RadrootsAppSettingsStatusPage() -> impl IntoView {
- let context = app_context();
- let fallback_backends = RwSignal::new_local(None::<RadrootsAppBackends>);
- let fallback_setup_status = RwSignal::new_local(RadrootsAppSetupStatus::Unknown);
- let fallback_config_status = RwSignal::new_local(RadrootsAppConfigStatus::Unknown);
- let backends = context
- .as_ref()
- .map(|value| value.backends)
- .unwrap_or(fallback_backends);
- let setup_status = context
- .as_ref()
- .map(|value| value.setup_status)
- .unwrap_or(fallback_setup_status);
- let config_status = context
- .as_ref()
- .map(|value| value.config_status)
- .unwrap_or(fallback_config_status);
- let health_report = RwSignal::new_local(RadrootsAppHealthReport::empty());
- let health_running = RwSignal::new_local(false);
- let health_autorun = RwSignal::new_local(false);
- let active_key = RwSignal::new_local(None::<String>);
- let notifications_status = RwSignal::new_local(None::<String>);
- let last_run = RwSignal::new_local(None::<i64>);
- Effect::new(move || {
- if health_autorun.get() {
- return;
- }
- let setup_status = setup_status.get();
- if matches!(setup_status, RadrootsAppSetupStatus::Unknown) {
- return;
- }
- let setup_required_value = !matches!(setup_status, RadrootsAppSetupStatus::Configured);
- let config = backends.with_untracked(|value| value.as_ref().map(|backends| backends.config.clone()));
- let Some(config) = config else {
- return;
- };
- health_autorun.set(true);
- let delay_ms = app_health_check_delay_ms();
- spawn_local(async move {
- TimeoutFuture::new(delay_ms).await;
- spawn_health_checks(
- config,
- setup_required_value,
- health_report,
- health_running,
- active_key,
- notifications_status,
- last_run,
- );
- });
- });
- let health_disabled = move || {
- backends.with(|value| value.is_none())
- || health_running.get()
- || matches!(setup_status.get(), RadrootsAppSetupStatus::Unknown)
- };
- let last_updated_label = move || {
- let value = last_run.get().map(format_timestamp);
- value.unwrap_or_else(|| t!("app.common.unknown"))
- };
- view! {
- <AppPageChrome title=t!("app.settings.status.title")>
- <header id="app-settings-status-header" class="flex flex-col gap-2">
- <div class="flex flex-row items-center gap-4">
- <button
- on:click=move |_| {
- let config = backends.with_untracked(|value| value.as_ref().map(|backends| backends.config.clone()));
- let Some(config) = config else {
- return;
- };
- let setup_required_value =
- !matches!(setup_status.get(), RadrootsAppSetupStatus::Configured);
- spawn_health_checks(
- config,
- setup_required_value,
- health_report,
- health_running,
- active_key,
- notifications_status,
- last_run,
- );
- }
- disabled=health_disabled
- >
- {move || {
- if health_running.get() {
- t!("app.home.health.button.checking")
- } else {
- t!("app.home.health.button.run")
- }
- }}
- </button>
- <div id="app-settings-status-updated" class="text-xs text-[var(--text-secondary)]">
- {move || format!("{}: {}", t!("app.settings.status.updated"), last_updated_label())}
- </div>
- </div>
- </header>
- <section id="app-settings-status-content" class="flex flex-col gap-4 mt-3">
- {move || {
- let report = health_report.get();
- let active = active_key_label(active_key.get());
- let setup_value = setup_status.get();
- let config_value = config_status.get();
- let config_list = RadrootsAppUiList {
- id: Some("settings-config-status-list".to_string()),
- view: Some("settings-config-status".to_string()),
- classes: None,
- title: Some(RadrootsAppUiListTitle {
- value: RadrootsAppUiListTitleValue::Text(String::from("Configuration")),
- classes: None,
- mod_value: None,
- link: None,
- on_click: None,
- }),
- default_state: None,
- list: Some(vec![
- Some(RadrootsAppUiListItem {
- kind: RadrootsAppUiListItemKind::Touch(RadrootsAppUiListTouch {
- label: RadrootsAppUiListLabel {
- left: vec![status_text(String::from("setup status"))],
- right: vec![
- status_text(setup_status_label(setup_value)),
- status_dot(setup_status_class(setup_value)),
- ],
- },
- display: None,
- end: None,
- on_click: None,
- }),
- loading: false,
- hide_active: true,
- hide_field: false,
- full_rounded: false,
- offset: None,
- }),
- Some(RadrootsAppUiListItem {
- kind: RadrootsAppUiListItemKind::Touch(RadrootsAppUiListTouch {
- label: RadrootsAppUiListLabel {
- left: vec![status_text(String::from("config status"))],
- right: vec![
- status_text(config_status_label(config_value)),
- status_dot(config_status_class(config_value)),
- ],
- },
- display: None,
- end: None,
- on_click: None,
- }),
- loading: false,
- hide_active: true,
- hide_field: false,
- full_rounded: false,
- offset: None,
- }),
- ]),
- hide_offset: false,
- styles: None,
- };
- let list = RadrootsAppUiList {
- id: Some("settings-status-list".to_string()),
- view: Some("settings-status".to_string()),
- classes: None,
- title: Some(RadrootsAppUiListTitle {
- value: RadrootsAppUiListTitleValue::Text(t!("app.home.health.title")),
- classes: None,
- mod_value: None,
- link: None,
- on_click: None,
- }),
- default_state: None,
- list: Some(vec![
- Some(status_row(t!("app.home.health.item.key_maps"), report.key_maps)),
- Some(status_row(
- t!("app.home.health.item.bootstrap_state"),
- report.bootstrap_state,
- )),
- Some(status_row(
- t!("app.home.health.item.state_active_key"),
- report.state_active_key,
- )),
- Some(status_row(
- t!("app.home.health.item.notifications"),
- report.notifications,
- )),
- Some(status_row(t!("app.home.health.item.tangle"), report.tangle)),
- Some(status_row(
- t!("app.home.health.item.datastore_roundtrip"),
- report.datastore_roundtrip,
- )),
- Some(status_row(t!("app.home.health.item.keystore"), report.keystore)),
- Some(RadrootsAppUiListItem {
- kind: RadrootsAppUiListItemKind::Touch(RadrootsAppUiListTouch {
- label: RadrootsAppUiListLabel {
- left: vec![status_text(t!("app.home.health.item.active_key"))],
- right: vec![status_text(active), status_dot("status-neutral")],
- },
- display: None,
- end: None,
- on_click: None,
- }),
- loading: false,
- hide_active: true,
- hide_field: false,
- full_rounded: false,
- offset: None,
- }),
- ]),
- hide_offset: false,
- styles: None,
- };
- view! {
- <div class="flex flex-col gap-4">
- <RadrootsAppUiListView basis=config_list />
- <RadrootsAppUiListView basis=list />
- </div>
- }
- .into_any()
- }}
- </section>
- </AppPageChrome>
- }
-}
diff --git a/app/src/setup.rs b/app/src/setup.rs
@@ -1,662 +0,0 @@
-#![forbid(unsafe_code)]
-
-use radroots_app_core::datastore::{RadrootsClientDatastore, RadrootsClientDatastoreEntry};
-use radroots_app_core::keystore::{RadrootsClientKeystoreError, RadrootsClientKeystoreNostr};
-
-#[cfg(target_arch = "wasm32")]
-use js_sys::Date;
-
-#[cfg(not(target_arch = "wasm32"))]
-use chrono::{SecondsFormat, Utc};
-
-use crate::{
- app_datastore_key_nostr_key,
- app_datastore_obj_key_state,
- app_datastore_read_state,
- app_default_relays,
- app_keystore_nostr_ensure_key,
- app_keystore_nostr_verify_key,
- app_log_debug_emit,
- app_state_record_new,
- app_state_timestamp_ms,
- RadrootsAppInitError,
- RadrootsAppInitResult,
- RadrootsAppKeyMapConfig,
- RadrootsAppKeystoreError,
- RadrootsAppRole,
- RadrootsAppState,
- RadrootsAppStateError,
- APP_EULA_HASH,
- APP_EULA_VERSION,
-};
-
-#[cfg(target_arch = "wasm32")]
-pub fn app_setup_eula_date() -> String {
- Date::new_0().to_iso_string().into()
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub fn app_setup_eula_date() -> String {
- Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true)
-}
-
-pub fn app_setup_state_new(
- active_key: String,
- eula_date: String,
- role: RadrootsAppRole,
-) -> RadrootsAppState {
- RadrootsAppState {
- active_key,
- role,
- eula_date,
- eula_version: String::from(APP_EULA_VERSION),
- eula_hash: String::from(APP_EULA_HASH),
- relays: app_default_relays(),
- nip05_key: None,
- notifications_permission: None,
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppSetupStep {
- Intro,
- KeyChoice,
- KeyAddExisting,
- Profile,
- FarmerSetup,
- BusinessSetup,
- Eula,
-}
-
-impl RadrootsAppSetupStep {
- pub const fn next(self) -> Self {
- match self {
- RadrootsAppSetupStep::Intro => RadrootsAppSetupStep::KeyChoice,
- RadrootsAppSetupStep::KeyChoice => RadrootsAppSetupStep::KeyAddExisting,
- RadrootsAppSetupStep::KeyAddExisting => RadrootsAppSetupStep::Profile,
- RadrootsAppSetupStep::Profile => RadrootsAppSetupStep::FarmerSetup,
- RadrootsAppSetupStep::FarmerSetup => RadrootsAppSetupStep::BusinessSetup,
- RadrootsAppSetupStep::BusinessSetup => RadrootsAppSetupStep::Eula,
- RadrootsAppSetupStep::Eula => RadrootsAppSetupStep::Eula,
- }
- }
-
- pub const fn prev(self) -> Self {
- match self {
- RadrootsAppSetupStep::Intro => RadrootsAppSetupStep::Intro,
- RadrootsAppSetupStep::KeyChoice => RadrootsAppSetupStep::Intro,
- RadrootsAppSetupStep::KeyAddExisting => RadrootsAppSetupStep::KeyChoice,
- RadrootsAppSetupStep::Profile => RadrootsAppSetupStep::KeyAddExisting,
- RadrootsAppSetupStep::FarmerSetup => RadrootsAppSetupStep::Profile,
- RadrootsAppSetupStep::BusinessSetup => RadrootsAppSetupStep::FarmerSetup,
- RadrootsAppSetupStep::Eula => RadrootsAppSetupStep::BusinessSetup,
- }
- }
-
- pub const fn is_terminal(self) -> bool {
- matches!(self, RadrootsAppSetupStep::Eula)
- }
-}
-
-pub const fn app_setup_step_default() -> RadrootsAppSetupStep {
- RadrootsAppSetupStep::Intro
-}
-
-pub async fn app_setup_initialize<T: RadrootsClientDatastore, K: RadrootsClientKeystoreNostr>(
- datastore: &T,
- keystore: &K,
- key_maps: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppInitResult<RadrootsAppState> {
- let active_key = app_keystore_nostr_ensure_key(keystore)
- .await
- .map_err(|err| match err {
- RadrootsAppKeystoreError::Keystore(inner) => RadrootsAppInitError::Keystore(inner),
- RadrootsAppKeystoreError::KeyMismatch => {
- RadrootsAppInitError::Keystore(RadrootsClientKeystoreError::NostrInvalidSecretKey)
- }
- })?;
- app_keystore_nostr_verify_key(keystore, &active_key)
- .await
- .map_err(|err| match err {
- RadrootsAppKeystoreError::Keystore(inner) => RadrootsAppInitError::Keystore(inner),
- RadrootsAppKeystoreError::KeyMismatch => {
- RadrootsAppInitError::Keystore(RadrootsClientKeystoreError::NostrInvalidSecretKey)
- }
- })?;
- app_setup_finalize_with_key(
- datastore,
- key_maps,
- active_key,
- app_setup_eula_date(),
- None,
- RadrootsAppRole::default(),
- )
- .await
-}
-
-pub async fn app_setup_finalize_with_key<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
- active_key: String,
- eula_date: String,
- nip05_key: Option<String>,
- role: RadrootsAppRole,
-) -> RadrootsAppInitResult<RadrootsAppState> {
- let stored_state = app_setup_commit(
- datastore,
- key_maps,
- active_key.clone(),
- eula_date,
- nip05_key,
- role,
- )
- .await?;
- let _ = app_log_debug_emit("log.app.setup", "created", Some(format!("key={active_key}")));
- Ok(stored_state)
-}
-
-pub async fn app_setup_commit<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
- active_key: String,
- eula_date: String,
- nip05_key: Option<String>,
- role: RadrootsAppRole,
-) -> RadrootsAppInitResult<RadrootsAppState> {
- let mut state = app_setup_state_new(active_key.clone(), eula_date, role);
- state.nip05_key = nip05_key;
- match app_datastore_read_state(datastore, key_maps).await {
- Ok(existing) => {
- if existing == state {
- let key_name =
- app_datastore_key_nostr_key(key_maps).map_err(RadrootsAppInitError::Config)?;
- let stored_key = datastore
- .get(key_name)
- .await
- .map_err(RadrootsAppInitError::Datastore)?;
- if stored_key != active_key {
- return Err(RadrootsAppInitError::State(RadrootsAppStateError::Corrupt));
- }
- return Ok(existing);
- }
- return Err(RadrootsAppInitError::State(
- RadrootsAppStateError::AlreadyExists,
- ));
- }
- Err(RadrootsAppInitError::State(RadrootsAppStateError::Missing)) => {}
- Err(err) => return Err(err),
- }
- let now_ms = app_state_timestamp_ms();
- let record = app_state_record_new(state.clone(), 1, now_ms);
- let encoded = serde_json::to_string(&record)
- .map_err(|_| RadrootsAppInitError::State(RadrootsAppStateError::Corrupt))?;
- let key_name = app_datastore_key_nostr_key(key_maps).map_err(RadrootsAppInitError::Config)?;
- let state_key =
- app_datastore_obj_key_state(key_maps).map_err(RadrootsAppInitError::Config)?;
- let entries = [
- RadrootsClientDatastoreEntry::new(state_key, Some(encoded)),
- RadrootsClientDatastoreEntry::new(key_name, Some(active_key.clone())),
- ];
- datastore
- .set_entries(&entries)
- .await
- .map_err(RadrootsAppInitError::Datastore)?;
- let stored_state = app_datastore_read_state(datastore, key_maps).await?;
- if stored_state != state {
- return Err(RadrootsAppInitError::State(RadrootsAppStateError::Corrupt));
- }
- let stored_key = datastore
- .get(key_name)
- .await
- .map_err(RadrootsAppInitError::Datastore)?;
- if stored_key != active_key {
- return Err(RadrootsAppInitError::State(RadrootsAppStateError::Corrupt));
- }
- Ok(stored_state)
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- app_setup_commit,
- app_setup_eula_date,
- app_setup_finalize_with_key,
- app_setup_initialize,
- app_setup_state_new,
- app_setup_step_default,
- RadrootsAppSetupStep,
- };
- use crate::{
- app_datastore_key_nostr_key,
- app_key_maps_default,
- RadrootsAppInitError,
- RadrootsAppRole,
- RadrootsAppStateError,
- RadrootsAppStateRecord,
- APP_EULA_HASH,
- APP_EULA_VERSION,
- };
- use async_trait::async_trait;
- use radroots_app_core::backup::RadrootsClientBackupDatastorePayload;
- use radroots_app_core::datastore::{
- RadrootsClientDatastore,
- RadrootsClientDatastoreEntries,
- RadrootsClientDatastoreEntry,
- RadrootsClientDatastoreError,
- RadrootsClientDatastoreResult,
- };
- use radroots_app_core::idb::{RadrootsClientIdbConfig, IDB_CONFIG_DATASTORE};
- use radroots_app_core::keystore::{
- RadrootsClientKeystoreError,
- RadrootsClientKeystoreNostr,
- RadrootsClientKeystoreResult,
- };
- use radroots_nostr::prelude::{RadrootsNostrKeys, RadrootsNostrSecretKey};
- use serde::de::DeserializeOwned;
- use serde::Serialize;
- use std::cell::RefCell;
- use std::collections::BTreeMap;
-
- struct TestKeystore {
- keys_result: RadrootsClientKeystoreResult<Vec<String>>,
- generate_result: RadrootsClientKeystoreResult<String>,
- read_result: RadrootsClientKeystoreResult<String>,
- }
-
- #[async_trait(?Send)]
- impl RadrootsClientKeystoreNostr for TestKeystore {
- async fn generate(&self) -> RadrootsClientKeystoreResult<String> {
- self.generate_result.clone()
- }
-
- async fn add(&self, _secret_key: &str) -> RadrootsClientKeystoreResult<String> {
- Err(RadrootsClientKeystoreError::IdbUndefined)
- }
-
- async fn read(&self, _public_key: &str) -> RadrootsClientKeystoreResult<String> {
- self.read_result.clone()
- }
-
- async fn keys(&self) -> RadrootsClientKeystoreResult<Vec<String>> {
- self.keys_result.clone()
- }
-
- async fn remove(&self, _public_key: &str) -> RadrootsClientKeystoreResult<String> {
- Err(RadrootsClientKeystoreError::IdbUndefined)
- }
-
- async fn reset(&self) -> RadrootsClientKeystoreResult<()> {
- Err(RadrootsClientKeystoreError::IdbUndefined)
- }
- }
-
- struct TestDatastore {
- record: RefCell<Option<RadrootsAppStateRecord>>,
- values: RefCell<BTreeMap<String, String>>,
- }
-
- #[async_trait(?Send)]
- impl RadrootsClientDatastore for TestDatastore {
- fn get_config(&self) -> RadrootsClientIdbConfig {
- IDB_CONFIG_DATASTORE
- }
-
- fn get_store_id(&self) -> &str {
- "test"
- }
-
- async fn init(&self) -> RadrootsClientDatastoreResult<()> {
- Ok(())
- }
-
- async fn set(&self, key: &str, value: &str) -> RadrootsClientDatastoreResult<String> {
- self.values.borrow_mut().insert(key.to_string(), value.to_string());
- Ok(value.to_string())
- }
-
- async fn get(&self, key: &str) -> RadrootsClientDatastoreResult<String> {
- self.values
- .borrow()
- .get(key)
- .cloned()
- .ok_or(RadrootsClientDatastoreError::NoResult)
- }
-
- async fn set_entries(
- &self,
- entries: &[RadrootsClientDatastoreEntry],
- ) -> RadrootsClientDatastoreResult<()> {
- for entry in entries {
- let Some(value) = entry.value.as_deref() else {
- continue;
- };
- if let Ok(parsed) = serde_json::from_str::<RadrootsAppStateRecord>(value) {
- *self.record.borrow_mut() = Some(parsed);
- continue;
- }
- self.values
- .borrow_mut()
- .insert(entry.key.clone(), value.to_string());
- }
- Ok(())
- }
-
- async fn set_obj<T>(
- &self,
- _key: &str,
- value: &T,
- ) -> RadrootsClientDatastoreResult<T>
- where
- T: Serialize + DeserializeOwned + Clone,
- {
- let encoded = serde_json::to_string(value)
- .map_err(|_| RadrootsClientDatastoreError::IdbUndefined)?;
- if let Ok(parsed) = serde_json::from_str::<RadrootsAppStateRecord>(&encoded) {
- *self.record.borrow_mut() = Some(parsed);
- return Ok(value.clone());
- }
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn update_obj<T>(
- &self,
- _key: &str,
- _value: &T,
- ) -> RadrootsClientDatastoreResult<T>
- where
- T: Serialize + DeserializeOwned + Clone,
- {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn get_obj<T>(&self, _key: &str) -> RadrootsClientDatastoreResult<T>
- where
- T: DeserializeOwned,
- {
- if let Some(record) = self.record.borrow().as_ref() {
- let encoded = serde_json::to_string(record)
- .map_err(|_| RadrootsClientDatastoreError::NoResult)?;
- if let Ok(parsed) = serde_json::from_str(&encoded) {
- return Ok(parsed);
- }
- };
- Err(RadrootsClientDatastoreError::NoResult)
- }
-
- async fn del_obj(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn del(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn del_pref(&self, _key_prefix: &str) -> RadrootsClientDatastoreResult<Vec<String>> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn set_param(
- &self,
- _key: &str,
- _key_param: &str,
- _value: &str,
- ) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn get_param(
- &self,
- _key: &str,
- _key_param: &str,
- ) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn keys(&self) -> RadrootsClientDatastoreResult<Vec<String>> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn entries(&self) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn entries_pref(
- &self,
- _key_prefix: &str,
- ) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn reset(&self) -> RadrootsClientDatastoreResult<()> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn export_backup(
- &self,
- ) -> RadrootsClientDatastoreResult<RadrootsClientBackupDatastorePayload> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn import_backup(
- &self,
- _payload: RadrootsClientBackupDatastorePayload,
- ) -> RadrootsClientDatastoreResult<()> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
- }
-
- #[test]
- fn setup_state_new_populates_defaults() {
- let state = app_setup_state_new(
- "pub".to_string(),
- "2025-01-01T00:00:00Z".to_string(),
- RadrootsAppRole::default(),
- );
- assert_eq!(state.active_key, "pub");
- assert_eq!(state.role, RadrootsAppRole::Individual);
- assert_eq!(state.eula_date, "2025-01-01T00:00:00Z");
- assert_eq!(state.eula_version, APP_EULA_VERSION);
- assert_eq!(state.eula_hash, APP_EULA_HASH);
- assert!(!state.relays.is_empty());
- assert!(state.nip05_key.is_none());
- assert!(state.notifications_permission.is_none());
- }
-
- #[test]
- fn setup_eula_date_is_non_empty() {
- let value = app_setup_eula_date();
- assert!(!value.is_empty());
- }
-
- #[test]
- fn setup_step_default_is_intro() {
- assert_eq!(app_setup_step_default(), RadrootsAppSetupStep::Intro);
- }
-
- #[test]
- fn setup_step_next_advances_once() {
- assert_eq!(
- RadrootsAppSetupStep::Intro.next(),
- RadrootsAppSetupStep::KeyChoice
- );
- assert_eq!(
- RadrootsAppSetupStep::KeyChoice.next(),
- RadrootsAppSetupStep::KeyAddExisting
- );
- assert_eq!(
- RadrootsAppSetupStep::KeyAddExisting.next(),
- RadrootsAppSetupStep::Profile
- );
- assert_eq!(
- RadrootsAppSetupStep::Profile.next(),
- RadrootsAppSetupStep::FarmerSetup
- );
- assert_eq!(
- RadrootsAppSetupStep::FarmerSetup.next(),
- RadrootsAppSetupStep::BusinessSetup
- );
- assert_eq!(
- RadrootsAppSetupStep::BusinessSetup.next(),
- RadrootsAppSetupStep::Eula
- );
- assert_eq!(
- RadrootsAppSetupStep::Eula.next(),
- RadrootsAppSetupStep::Eula
- );
- }
-
- #[test]
- fn setup_step_prev_rewinds_once() {
- assert_eq!(
- RadrootsAppSetupStep::Intro.prev(),
- RadrootsAppSetupStep::Intro
- );
- assert_eq!(
- RadrootsAppSetupStep::KeyChoice.prev(),
- RadrootsAppSetupStep::Intro
- );
- assert_eq!(
- RadrootsAppSetupStep::KeyAddExisting.prev(),
- RadrootsAppSetupStep::KeyChoice
- );
- assert_eq!(
- RadrootsAppSetupStep::Profile.prev(),
- RadrootsAppSetupStep::KeyAddExisting
- );
- assert_eq!(
- RadrootsAppSetupStep::FarmerSetup.prev(),
- RadrootsAppSetupStep::Profile
- );
- assert_eq!(
- RadrootsAppSetupStep::BusinessSetup.prev(),
- RadrootsAppSetupStep::FarmerSetup
- );
- assert_eq!(
- RadrootsAppSetupStep::Eula.prev(),
- RadrootsAppSetupStep::BusinessSetup
- );
- }
-
- #[test]
- fn setup_step_terminal_matches_eula() {
- assert!(!RadrootsAppSetupStep::Intro.is_terminal());
- assert!(!RadrootsAppSetupStep::KeyChoice.is_terminal());
- assert!(!RadrootsAppSetupStep::KeyAddExisting.is_terminal());
- assert!(!RadrootsAppSetupStep::Profile.is_terminal());
- assert!(!RadrootsAppSetupStep::FarmerSetup.is_terminal());
- assert!(!RadrootsAppSetupStep::BusinessSetup.is_terminal());
- assert!(RadrootsAppSetupStep::Eula.is_terminal());
- }
-
- #[test]
- fn setup_initialize_creates_state_and_key() {
- let secret_key = RadrootsNostrSecretKey::generate();
- let secret_hex = secret_key.to_secret_hex();
- let keys = RadrootsNostrKeys::new(secret_key);
- let public_key = keys.public_key().to_hex();
- let datastore = TestDatastore {
- record: RefCell::new(None),
- values: RefCell::new(BTreeMap::new()),
- };
- let keystore = TestKeystore {
- keys_result: Err(RadrootsClientKeystoreError::NostrNoResults),
- generate_result: Ok(public_key.clone()),
- read_result: Ok(secret_hex),
- };
- let key_maps = app_key_maps_default();
- let state = futures::executor::block_on(app_setup_initialize(
- &datastore,
- &keystore,
- &key_maps,
- ))
- .expect("setup");
- assert_eq!(state.active_key, public_key);
- let key_name = app_datastore_key_nostr_key(&key_maps).expect("key name");
- let stored = futures::executor::block_on(datastore.get(key_name)).expect("stored");
- assert_eq!(stored, public_key);
- assert!(datastore.record.borrow().is_some());
- }
-
- #[test]
- fn setup_finalize_with_key_writes_state() {
- let datastore = TestDatastore {
- record: RefCell::new(None),
- values: RefCell::new(BTreeMap::new()),
- };
- let key_maps = app_key_maps_default();
- let state = futures::executor::block_on(app_setup_finalize_with_key(
- &datastore,
- &key_maps,
- "pub".to_string(),
- "2025-01-01T00:00:00Z".to_string(),
- None,
- RadrootsAppRole::default(),
- ))
- .expect("finalize");
- assert_eq!(state.active_key, "pub");
- let key_name = app_datastore_key_nostr_key(&key_maps).expect("key name");
- let stored = futures::executor::block_on(datastore.get(key_name)).expect("stored");
- assert_eq!(stored, "pub");
- assert!(datastore.record.borrow().is_some());
- }
-
- #[test]
- fn setup_commit_is_idempotent() {
- let datastore = TestDatastore {
- record: RefCell::new(None),
- values: RefCell::new(BTreeMap::new()),
- };
- let key_maps = app_key_maps_default();
- let state = futures::executor::block_on(app_setup_commit(
- &datastore,
- &key_maps,
- "pub".to_string(),
- "2025-01-01T00:00:00Z".to_string(),
- None,
- RadrootsAppRole::default(),
- ))
- .expect("commit");
- assert_eq!(state.active_key, "pub");
- let state_again = futures::executor::block_on(app_setup_commit(
- &datastore,
- &key_maps,
- "pub".to_string(),
- "2025-01-01T00:00:00Z".to_string(),
- None,
- RadrootsAppRole::default(),
- ))
- .expect("commit");
- assert_eq!(state_again.active_key, "pub");
- }
-
- #[test]
- fn setup_commit_rejects_mismatch() {
- let datastore = TestDatastore {
- record: RefCell::new(None),
- values: RefCell::new(BTreeMap::new()),
- };
- let key_maps = app_key_maps_default();
- let _state = futures::executor::block_on(app_setup_commit(
- &datastore,
- &key_maps,
- "pub".to_string(),
- "2025-01-01T00:00:00Z".to_string(),
- None,
- RadrootsAppRole::default(),
- ))
- .expect("commit");
- let err = futures::executor::block_on(app_setup_commit(
- &datastore,
- &key_maps,
- "other".to_string(),
- "2025-01-01T00:00:00Z".to_string(),
- None,
- RadrootsAppRole::default(),
- ))
- .expect_err("mismatch");
- assert_eq!(
- err,
- RadrootsAppInitError::State(RadrootsAppStateError::AlreadyExists)
- );
- }
-}
diff --git a/app/src/setup_flow.rs b/app/src/setup_flow.rs
@@ -1,225 +0,0 @@
-#![forbid(unsafe_code)]
-
-use crate::{app_setup_step_default, RadrootsAppRole, RadrootsAppSetupStep};
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppSetupKeyChoice {
- Generate,
- AddExisting,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppSetupFarmerChoice {
- Yes,
- No,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppSetupBusinessChoice {
- Yes,
- No,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsAppSetupFlowDraft {
- pub step: RadrootsAppSetupStep,
- pub key_choice: Option<RadrootsAppSetupKeyChoice>,
- pub farmer_choice: Option<RadrootsAppSetupFarmerChoice>,
- pub business_choice: Option<RadrootsAppSetupBusinessChoice>,
- pub profile_name: String,
- pub profile_nip05: bool,
-}
-
-impl Default for RadrootsAppSetupFlowDraft {
- fn default() -> Self {
- Self {
- step: app_setup_step_default(),
- key_choice: None,
- farmer_choice: None,
- business_choice: None,
- profile_name: String::new(),
- profile_nip05: true,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsAppSetupFlowValidation {
- pub can_continue: bool,
- pub can_back: bool,
- pub next_step: RadrootsAppSetupStep,
- pub prev_step: RadrootsAppSetupStep,
-}
-
-pub fn app_setup_flow_role_from_choices(
- farmer_choice: Option<RadrootsAppSetupFarmerChoice>,
- business_choice: Option<RadrootsAppSetupBusinessChoice>,
-) -> Option<RadrootsAppRole> {
- match farmer_choice? {
- RadrootsAppSetupFarmerChoice::Yes => Some(RadrootsAppRole::Farm),
- RadrootsAppSetupFarmerChoice::No => match business_choice? {
- RadrootsAppSetupBusinessChoice::Yes => Some(RadrootsAppRole::Business),
- RadrootsAppSetupBusinessChoice::No => Some(RadrootsAppRole::Individual),
- },
- }
-}
-
-pub fn app_setup_flow_next_step(draft: &RadrootsAppSetupFlowDraft) -> RadrootsAppSetupStep {
- match draft.step {
- RadrootsAppSetupStep::Intro => RadrootsAppSetupStep::KeyChoice,
- RadrootsAppSetupStep::KeyChoice => match draft.key_choice {
- Some(RadrootsAppSetupKeyChoice::Generate) => RadrootsAppSetupStep::Profile,
- Some(RadrootsAppSetupKeyChoice::AddExisting) => RadrootsAppSetupStep::KeyAddExisting,
- None => RadrootsAppSetupStep::KeyChoice,
- },
- RadrootsAppSetupStep::KeyAddExisting => RadrootsAppSetupStep::Profile,
- RadrootsAppSetupStep::Profile => RadrootsAppSetupStep::FarmerSetup,
- RadrootsAppSetupStep::FarmerSetup => match draft.farmer_choice {
- Some(RadrootsAppSetupFarmerChoice::Yes) => RadrootsAppSetupStep::Eula,
- Some(RadrootsAppSetupFarmerChoice::No) => RadrootsAppSetupStep::BusinessSetup,
- None => RadrootsAppSetupStep::FarmerSetup,
- },
- RadrootsAppSetupStep::BusinessSetup => RadrootsAppSetupStep::Eula,
- RadrootsAppSetupStep::Eula => RadrootsAppSetupStep::Eula,
- }
-}
-
-pub fn app_setup_flow_prev_step(draft: &RadrootsAppSetupFlowDraft) -> RadrootsAppSetupStep {
- match draft.step {
- RadrootsAppSetupStep::Intro => RadrootsAppSetupStep::Intro,
- RadrootsAppSetupStep::KeyChoice => RadrootsAppSetupStep::Intro,
- RadrootsAppSetupStep::KeyAddExisting => RadrootsAppSetupStep::KeyChoice,
- RadrootsAppSetupStep::Profile => match draft.key_choice {
- Some(RadrootsAppSetupKeyChoice::AddExisting) => RadrootsAppSetupStep::KeyAddExisting,
- _ => RadrootsAppSetupStep::KeyChoice,
- },
- RadrootsAppSetupStep::FarmerSetup => RadrootsAppSetupStep::Profile,
- RadrootsAppSetupStep::BusinessSetup => RadrootsAppSetupStep::FarmerSetup,
- RadrootsAppSetupStep::Eula => match draft.farmer_choice {
- Some(RadrootsAppSetupFarmerChoice::No) => RadrootsAppSetupStep::BusinessSetup,
- _ => RadrootsAppSetupStep::FarmerSetup,
- },
- }
-}
-
-pub fn app_setup_flow_validate(draft: &RadrootsAppSetupFlowDraft) -> RadrootsAppSetupFlowValidation {
- let can_continue = match draft.step {
- RadrootsAppSetupStep::KeyChoice => draft.key_choice.is_some(),
- RadrootsAppSetupStep::FarmerSetup => draft.farmer_choice.is_some(),
- RadrootsAppSetupStep::BusinessSetup => draft.business_choice.is_some(),
- RadrootsAppSetupStep::Profile => {
- !(draft.profile_nip05 && draft.profile_name.trim().is_empty())
- }
- _ => true,
- };
- let can_back = !matches!(draft.step, RadrootsAppSetupStep::Intro);
- RadrootsAppSetupFlowValidation {
- can_continue,
- can_back,
- next_step: app_setup_flow_next_step(draft),
- prev_step: app_setup_flow_prev_step(draft),
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- app_setup_flow_next_step,
- app_setup_flow_prev_step,
- app_setup_flow_role_from_choices,
- app_setup_flow_validate,
- RadrootsAppSetupBusinessChoice,
- RadrootsAppSetupFarmerChoice,
- RadrootsAppSetupFlowDraft,
- RadrootsAppSetupKeyChoice,
- };
- use crate::{RadrootsAppRole, RadrootsAppSetupStep};
-
- #[test]
- fn flow_defaults_to_intro() {
- let draft = RadrootsAppSetupFlowDraft::default();
- assert_eq!(draft.step, RadrootsAppSetupStep::Intro);
- assert!(draft.profile_nip05);
- }
-
- #[test]
- fn flow_role_from_choices_maps_roles() {
- assert_eq!(
- app_setup_flow_role_from_choices(
- Some(RadrootsAppSetupFarmerChoice::Yes),
- None,
- ),
- Some(RadrootsAppRole::Farm)
- );
- assert_eq!(
- app_setup_flow_role_from_choices(
- Some(RadrootsAppSetupFarmerChoice::No),
- Some(RadrootsAppSetupBusinessChoice::Yes),
- ),
- Some(RadrootsAppRole::Business)
- );
- assert_eq!(
- app_setup_flow_role_from_choices(
- Some(RadrootsAppSetupFarmerChoice::No),
- Some(RadrootsAppSetupBusinessChoice::No),
- ),
- Some(RadrootsAppRole::Individual)
- );
- assert_eq!(
- app_setup_flow_role_from_choices(
- Some(RadrootsAppSetupFarmerChoice::No),
- None,
- ),
- None
- );
- }
-
- #[test]
- fn flow_next_step_respects_choices() {
- let mut draft = RadrootsAppSetupFlowDraft::default();
- draft.step = RadrootsAppSetupStep::KeyChoice;
- draft.key_choice = Some(RadrootsAppSetupKeyChoice::Generate);
- assert_eq!(app_setup_flow_next_step(&draft), RadrootsAppSetupStep::Profile);
- draft.key_choice = Some(RadrootsAppSetupKeyChoice::AddExisting);
- assert_eq!(
- app_setup_flow_next_step(&draft),
- RadrootsAppSetupStep::KeyAddExisting
- );
- draft.step = RadrootsAppSetupStep::FarmerSetup;
- draft.farmer_choice = Some(RadrootsAppSetupFarmerChoice::No);
- assert_eq!(
- app_setup_flow_next_step(&draft),
- RadrootsAppSetupStep::BusinessSetup
- );
- }
-
- #[test]
- fn flow_prev_step_respects_choices() {
- let mut draft = RadrootsAppSetupFlowDraft::default();
- draft.step = RadrootsAppSetupStep::Profile;
- draft.key_choice = Some(RadrootsAppSetupKeyChoice::AddExisting);
- assert_eq!(
- app_setup_flow_prev_step(&draft),
- RadrootsAppSetupStep::KeyAddExisting
- );
- draft.step = RadrootsAppSetupStep::Eula;
- draft.farmer_choice = Some(RadrootsAppSetupFarmerChoice::No);
- assert_eq!(
- app_setup_flow_prev_step(&draft),
- RadrootsAppSetupStep::BusinessSetup
- );
- }
-
- #[test]
- fn flow_validation_disables_continue_for_missing_name() {
- let mut draft = RadrootsAppSetupFlowDraft::default();
- draft.step = RadrootsAppSetupStep::Profile;
- draft.profile_name = String::new();
- draft.profile_nip05 = true;
- let validation = app_setup_flow_validate(&draft);
- assert!(!validation.can_continue);
- draft.profile_nip05 = false;
- let validation = app_setup_flow_validate(&draft);
- assert!(validation.can_continue);
- }
-}
diff --git a/app/src/setup_lock.rs b/app/src/setup_lock.rs
@@ -1,341 +0,0 @@
-#![forbid(unsafe_code)]
-
-use serde::{Deserialize, Serialize};
-
-use radroots_app_core::datastore::{RadrootsClientDatastore, RadrootsClientDatastoreError};
-
-use crate::{
- app_datastore_key_setup_lock,
- RadrootsAppInitError,
- RadrootsAppInitResult,
- RadrootsAppKeyMapConfig,
- RadrootsAppStateError,
-};
-
-pub const APP_SETUP_LOCK_TTL_MS: u64 = 10 * 60 * 1000;
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsAppSetupLock {
- pub owner: String,
- pub expires_at_ms: u64,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum RadrootsAppSetupLockStatus {
- Acquired(RadrootsAppSetupLock),
- Locked(RadrootsAppSetupLock),
-}
-
-pub const fn app_setup_lock_enabled() -> bool {
- cfg!(target_arch = "wasm32")
-}
-
-pub const fn app_setup_lock_ttl_ms() -> u64 {
- APP_SETUP_LOCK_TTL_MS
-}
-
-pub fn app_setup_lock_is_expired(lock: &RadrootsAppSetupLock, now_ms: u64) -> bool {
- lock.expires_at_ms <= now_ms
-}
-
-fn app_setup_lock_new(owner: &str, now_ms: u64, ttl_ms: u64) -> RadrootsAppSetupLock {
- RadrootsAppSetupLock {
- owner: owner.to_string(),
- expires_at_ms: now_ms.saturating_add(ttl_ms),
- }
-}
-
-pub async fn app_setup_lock_acquire<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
- owner: &str,
- now_ms: u64,
- ttl_ms: u64,
-) -> RadrootsAppInitResult<RadrootsAppSetupLockStatus> {
- let key = app_datastore_key_setup_lock(key_maps).map_err(RadrootsAppInitError::Config)?;
- let existing = match datastore.get(key).await {
- Ok(value) => serde_json::from_str::<RadrootsAppSetupLock>(&value).ok(),
- Err(RadrootsClientDatastoreError::NoResult) => None,
- Err(err) => return Err(RadrootsAppInitError::Datastore(err)),
- };
- if let Some(lock) = existing.as_ref() {
- if !app_setup_lock_is_expired(lock, now_ms) && lock.owner != owner {
- return Ok(RadrootsAppSetupLockStatus::Locked(lock.clone()));
- }
- }
- let lock = app_setup_lock_new(owner, now_ms, ttl_ms);
- let encoded = serde_json::to_string(&lock)
- .map_err(|_| RadrootsAppInitError::State(RadrootsAppStateError::Corrupt))?;
- datastore
- .set(key, &encoded)
- .await
- .map_err(RadrootsAppInitError::Datastore)?;
- Ok(RadrootsAppSetupLockStatus::Acquired(lock))
-}
-
-pub async fn app_setup_lock_release<T: RadrootsClientDatastore>(
- datastore: &T,
- key_maps: &RadrootsAppKeyMapConfig,
-) -> RadrootsAppInitResult<()> {
- let key = app_datastore_key_setup_lock(key_maps).map_err(RadrootsAppInitError::Config)?;
- match datastore.del(key).await {
- Ok(_) => Ok(()),
- Err(RadrootsClientDatastoreError::NoResult) => Ok(()),
- Err(err) => Err(RadrootsAppInitError::Datastore(err)),
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- app_setup_lock_acquire,
- app_setup_lock_enabled,
- app_setup_lock_is_expired,
- app_setup_lock_release,
- app_setup_lock_ttl_ms,
- RadrootsAppSetupLock,
- RadrootsAppSetupLockStatus,
- APP_SETUP_LOCK_TTL_MS,
- };
- use crate::{app_key_maps_default, RadrootsAppKeyMapConfig};
- use async_trait::async_trait;
- use radroots_app_core::backup::RadrootsClientBackupDatastorePayload;
- use radroots_app_core::datastore::{
- RadrootsClientDatastore,
- RadrootsClientDatastoreEntries,
- RadrootsClientDatastoreError,
- RadrootsClientDatastoreResult,
- };
- use radroots_app_core::idb::{RadrootsClientIdbConfig, IDB_CONFIG_DATASTORE};
- use serde::{de::DeserializeOwned, Serialize};
- use std::cell::RefCell;
- use std::collections::BTreeMap;
-
- #[test]
- fn lock_enabled_matches_target_arch() {
- assert_eq!(app_setup_lock_enabled(), cfg!(target_arch = "wasm32"));
- }
-
- #[test]
- fn lock_ttl_defaults_to_constant() {
- assert_eq!(app_setup_lock_ttl_ms(), APP_SETUP_LOCK_TTL_MS);
- }
-
- #[test]
- fn lock_expired_checks_timestamp() {
- let lock = RadrootsAppSetupLock {
- owner: "owner".to_string(),
- expires_at_ms: 10,
- };
- assert!(!app_setup_lock_is_expired(&lock, 5));
- assert!(app_setup_lock_is_expired(&lock, 10));
- }
-
- struct LockDatastore {
- values: RefCell<BTreeMap<String, String>>,
- }
-
- #[async_trait(?Send)]
- impl RadrootsClientDatastore for LockDatastore {
- fn get_config(&self) -> RadrootsClientIdbConfig {
- IDB_CONFIG_DATASTORE
- }
-
- fn get_store_id(&self) -> &str {
- "test"
- }
-
- async fn init(&self) -> RadrootsClientDatastoreResult<()> {
- Ok(())
- }
-
- async fn set(&self, key: &str, value: &str) -> RadrootsClientDatastoreResult<String> {
- self.values.borrow_mut().insert(key.to_string(), value.to_string());
- Ok(value.to_string())
- }
-
- async fn get(&self, key: &str) -> RadrootsClientDatastoreResult<String> {
- self.values
- .borrow()
- .get(key)
- .cloned()
- .ok_or(RadrootsClientDatastoreError::NoResult)
- }
-
- async fn set_obj<T>(
- &self,
- _key: &str,
- _value: &T,
- ) -> RadrootsClientDatastoreResult<T>
- where
- T: Serialize + DeserializeOwned + Clone,
- {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn update_obj<T>(
- &self,
- _key: &str,
- _value: &T,
- ) -> RadrootsClientDatastoreResult<T>
- where
- T: Serialize + DeserializeOwned + Clone,
- {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn get_obj<T>(&self, _key: &str) -> RadrootsClientDatastoreResult<T>
- where
- T: DeserializeOwned,
- {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn del_obj(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn del(&self, key: &str) -> RadrootsClientDatastoreResult<String> {
- let removed = self.values.borrow_mut().remove(key);
- match removed {
- Some(value) => Ok(value),
- None => Err(RadrootsClientDatastoreError::NoResult),
- }
- }
-
- async fn del_pref(&self, _key_prefix: &str) -> RadrootsClientDatastoreResult<Vec<String>> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn set_param(
- &self,
- _key: &str,
- _key_param: &str,
- _value: &str,
- ) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn get_param(
- &self,
- _key: &str,
- _key_param: &str,
- ) -> RadrootsClientDatastoreResult<String> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn keys(&self) -> RadrootsClientDatastoreResult<Vec<String>> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn entries(&self) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn entries_pref(
- &self,
- _key_prefix: &str,
- ) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn reset(&self) -> RadrootsClientDatastoreResult<()> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn export_backup(
- &self,
- ) -> RadrootsClientDatastoreResult<RadrootsClientBackupDatastorePayload> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
-
- async fn import_backup(
- &self,
- _payload: RadrootsClientBackupDatastorePayload,
- ) -> RadrootsClientDatastoreResult<()> {
- Err(RadrootsClientDatastoreError::IdbUndefined)
- }
- }
-
- fn lock_datastore() -> LockDatastore {
- LockDatastore {
- values: RefCell::new(BTreeMap::new()),
- }
- }
-
- fn lock_key_maps() -> RadrootsAppKeyMapConfig {
- app_key_maps_default()
- }
-
- #[test]
- fn acquire_returns_locked_for_other_owner() {
- let datastore = lock_datastore();
- let key_maps = lock_key_maps();
- let acquired = futures::executor::block_on(app_setup_lock_acquire(
- &datastore,
- &key_maps,
- "owner-a",
- 100,
- 50,
- ))
- .expect("acquire");
- assert!(matches!(acquired, RadrootsAppSetupLockStatus::Acquired(_)));
- let locked = futures::executor::block_on(app_setup_lock_acquire(
- &datastore,
- &key_maps,
- "owner-b",
- 120,
- 50,
- ))
- .expect("acquire");
- assert!(matches!(locked, RadrootsAppSetupLockStatus::Locked(_)));
- }
-
- #[test]
- fn acquire_refreshes_for_same_owner() {
- let datastore = lock_datastore();
- let key_maps = lock_key_maps();
- let _ = futures::executor::block_on(app_setup_lock_acquire(
- &datastore,
- &key_maps,
- "owner-a",
- 100,
- 50,
- ))
- .expect("acquire");
- let refreshed = futures::executor::block_on(app_setup_lock_acquire(
- &datastore,
- &key_maps,
- "owner-a",
- 140,
- 50,
- ))
- .expect("refresh");
- assert!(matches!(refreshed, RadrootsAppSetupLockStatus::Acquired(_)));
- }
-
- #[test]
- fn release_clears_lock() {
- let datastore = lock_datastore();
- let key_maps = lock_key_maps();
- let _ = futures::executor::block_on(app_setup_lock_acquire(
- &datastore,
- &key_maps,
- "owner-a",
- 100,
- 50,
- ))
- .expect("acquire");
- futures::executor::block_on(app_setup_lock_release(&datastore, &key_maps))
- .expect("release");
- let acquired = futures::executor::block_on(app_setup_lock_acquire(
- &datastore,
- &key_maps,
- "owner-b",
- 200,
- 50,
- ))
- .expect("acquire");
- assert!(matches!(acquired, RadrootsAppSetupLockStatus::Acquired(_)));
- }
-}
diff --git a/app/src/setup_status.rs b/app/src/setup_status.rs
@@ -1,114 +0,0 @@
-#![forbid(unsafe_code)]
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppSetupStatus {
- Unknown,
- Required,
- Configured,
- Corrupt,
- Locked,
-}
-
-impl Default for RadrootsAppSetupStatus {
- fn default() -> Self {
- RadrootsAppSetupStatus::Unknown
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub struct RadrootsAppSetupGate {
- pub show_app: bool,
- pub show_setup: bool,
- pub show_setup_nav: bool,
- pub show_recovery: bool,
-}
-
-impl RadrootsAppSetupGate {
- pub const fn splash() -> Self {
- Self {
- show_app: false,
- show_setup: false,
- show_setup_nav: false,
- show_recovery: false,
- }
- }
-}
-
-pub const fn app_setup_gate_from_status(status: RadrootsAppSetupStatus) -> RadrootsAppSetupGate {
- match status {
- RadrootsAppSetupStatus::Unknown => RadrootsAppSetupGate::splash(),
- RadrootsAppSetupStatus::Required => RadrootsAppSetupGate {
- show_app: false,
- show_setup: true,
- show_setup_nav: false,
- show_recovery: false,
- },
- RadrootsAppSetupStatus::Configured => RadrootsAppSetupGate {
- show_app: true,
- show_setup: false,
- show_setup_nav: false,
- show_recovery: false,
- },
- RadrootsAppSetupStatus::Corrupt => RadrootsAppSetupGate {
- show_app: false,
- show_setup: false,
- show_setup_nav: false,
- show_recovery: true,
- },
- RadrootsAppSetupStatus::Locked => RadrootsAppSetupGate {
- show_app: false,
- show_setup: true,
- show_setup_nav: false,
- show_recovery: false,
- },
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{app_setup_gate_from_status, RadrootsAppSetupGate, RadrootsAppSetupStatus};
-
- #[test]
- fn unknown_status_routes_to_splash() {
- assert_eq!(
- app_setup_gate_from_status(RadrootsAppSetupStatus::Unknown),
- RadrootsAppSetupGate::splash()
- );
- }
-
- #[test]
- fn required_status_shows_setup() {
- let gate = app_setup_gate_from_status(RadrootsAppSetupStatus::Required);
- assert!(gate.show_setup);
- assert!(!gate.show_app);
- assert!(!gate.show_setup_nav);
- assert!(!gate.show_recovery);
- }
-
- #[test]
- fn configured_status_shows_app() {
- let gate = app_setup_gate_from_status(RadrootsAppSetupStatus::Configured);
- assert!(gate.show_app);
- assert!(!gate.show_setup);
- assert!(!gate.show_setup_nav);
- assert!(!gate.show_recovery);
- }
-
- #[test]
- fn corrupt_status_shows_recovery() {
- let gate = app_setup_gate_from_status(RadrootsAppSetupStatus::Corrupt);
- assert!(gate.show_recovery);
- assert!(!gate.show_app);
- assert!(!gate.show_setup);
- assert!(!gate.show_setup_nav);
- }
-
- #[test]
- fn locked_status_shows_setup() {
- let gate = app_setup_gate_from_status(RadrootsAppSetupStatus::Locked);
- assert!(gate.show_setup);
- assert!(!gate.show_app);
- assert!(!gate.show_setup_nav);
- assert!(!gate.show_recovery);
- }
-}
diff --git a/app/src/tangle.rs b/app/src/tangle.rs
@@ -1,58 +0,0 @@
-#![forbid(unsafe_code)]
-
-use crate::app_log_debug_emit;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppTangleError {
- NotImplemented,
-}
-
-pub type RadrootsAppTangleResult<T> = Result<T, RadrootsAppTangleError>;
-
-impl RadrootsAppTangleError {
- pub const fn message(self) -> &'static str {
- match self {
- RadrootsAppTangleError::NotImplemented => "error.app.tangle.not_implemented",
- }
- }
-}
-
-impl std::fmt::Display for RadrootsAppTangleError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_str(self.message())
- }
-}
-
-impl std::error::Error for RadrootsAppTangleError {}
-
-pub trait RadrootsAppTangleClient {
- fn init(&self) -> RadrootsAppTangleResult<()>;
-}
-
-pub struct RadrootsAppTangleClientStub;
-
-impl RadrootsAppTangleClientStub {
- pub fn new() -> Self {
- Self
- }
-}
-
-impl RadrootsAppTangleClient for RadrootsAppTangleClientStub {
- fn init(&self) -> RadrootsAppTangleResult<()> {
- let _ = app_log_debug_emit("log.app.tangle.init", "stub", None);
- Err(RadrootsAppTangleError::NotImplemented)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{RadrootsAppTangleClient, RadrootsAppTangleClientStub, RadrootsAppTangleError};
-
- #[test]
- fn tangle_stub_reports_not_implemented() {
- let client = RadrootsAppTangleClientStub::new();
- let err = client.init().expect_err("not implemented");
- assert_eq!(err, RadrootsAppTangleError::NotImplemented);
- assert_eq!(err.to_string(), "error.app.tangle.not_implemented");
- }
-}
diff --git a/app/src/theme.rs b/app/src/theme.rs
@@ -1,189 +0,0 @@
-#![forbid(unsafe_code)]
-
-pub const APP_THEME_STORAGE_KEY: &str = "app:theme:mode";
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppThemeMode {
- System,
- Light,
- Dark,
-}
-
-impl RadrootsAppThemeMode {
- pub const fn as_str(self) -> &'static str {
- match self {
- RadrootsAppThemeMode::System => "system",
- RadrootsAppThemeMode::Light => "light",
- RadrootsAppThemeMode::Dark => "dark",
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppThemeError {
- Unavailable,
- Storage,
-}
-
-pub type RadrootsAppThemeResult<T> = Result<T, RadrootsAppThemeError>;
-
-impl RadrootsAppThemeError {
- pub const fn message(&self) -> &'static str {
- match self {
- RadrootsAppThemeError::Unavailable => "error.app.theme.unavailable",
- RadrootsAppThemeError::Storage => "error.app.theme.storage",
- }
- }
-}
-
-impl std::fmt::Display for RadrootsAppThemeError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}", self.message())
- }
-}
-
-impl std::error::Error for RadrootsAppThemeError {}
-
-pub fn app_theme_mode_from_value(value: &str) -> Option<RadrootsAppThemeMode> {
- match value {
- "system" => Some(RadrootsAppThemeMode::System),
- "light" => Some(RadrootsAppThemeMode::Light),
- "dark" => Some(RadrootsAppThemeMode::Dark),
- _ => None,
- }
-}
-
-pub fn app_theme_mode_to_name(mode: RadrootsAppThemeMode, prefers_dark: bool) -> &'static str {
- match mode {
- RadrootsAppThemeMode::System => {
- if prefers_dark { "os_dark" } else { "os_light" }
- }
- RadrootsAppThemeMode::Light => "os_light",
- RadrootsAppThemeMode::Dark => "os_dark",
- }
-}
-
-#[cfg(target_arch = "wasm32")]
-fn app_theme_prefers_dark() -> bool {
- let Some(window) = web_sys::window() else {
- return false;
- };
- match window.match_media("(prefers-color-scheme: dark)") {
- Ok(Some(query)) => query.matches(),
- _ => false,
- }
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-fn app_theme_prefers_dark() -> bool {
- false
-}
-
-#[cfg(target_arch = "wasm32")]
-fn app_theme_apply_name(name: &str) -> RadrootsAppThemeResult<()> {
- use leptos::wasm_bindgen::JsCast;
- let Some(window) = web_sys::window() else {
- return Err(RadrootsAppThemeError::Unavailable);
- };
- let Some(document) = window.document() else {
- return Err(RadrootsAppThemeError::Unavailable);
- };
- let Some(root) = document.document_element() else {
- return Err(RadrootsAppThemeError::Unavailable);
- };
- root.set_attribute("data-theme", name)
- .map_err(|_| RadrootsAppThemeError::Unavailable)?;
- let color_scheme = if name == "os_dark" { "dark" } else { "light" };
- let html = root
- .dyn_into::<web_sys::HtmlElement>()
- .map_err(|_| RadrootsAppThemeError::Unavailable)?;
- html.style()
- .set_property("color-scheme", color_scheme)
- .map_err(|_| RadrootsAppThemeError::Unavailable)?;
- Ok(())
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-fn app_theme_apply_name(_name: &str) -> RadrootsAppThemeResult<()> {
- Ok(())
-}
-
-#[cfg(target_arch = "wasm32")]
-fn app_theme_read_storage() -> Option<String> {
- let window = web_sys::window()?;
- let storage = window.local_storage().ok()??;
- storage.get_item(APP_THEME_STORAGE_KEY).ok().flatten()
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-fn app_theme_read_storage() -> Option<String> {
- None
-}
-
-#[cfg(target_arch = "wasm32")]
-fn app_theme_write_storage(value: &str) -> RadrootsAppThemeResult<()> {
- let window = web_sys::window().ok_or(RadrootsAppThemeError::Unavailable)?;
- let storage = window
- .local_storage()
- .map_err(|_| RadrootsAppThemeError::Storage)?
- .ok_or(RadrootsAppThemeError::Storage)?;
- storage
- .set_item(APP_THEME_STORAGE_KEY, value)
- .map_err(|_| RadrootsAppThemeError::Storage)?;
- Ok(())
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-fn app_theme_write_storage(_value: &str) -> RadrootsAppThemeResult<()> {
- Ok(())
-}
-
-pub fn app_theme_read_mode() -> Option<RadrootsAppThemeMode> {
- app_theme_read_storage()
- .as_deref()
- .and_then(app_theme_mode_from_value)
-}
-
-pub fn app_theme_init() -> RadrootsAppThemeResult<&'static str> {
- let prefers_dark = app_theme_prefers_dark();
- let mode = app_theme_read_mode().unwrap_or(RadrootsAppThemeMode::System);
- let theme_name = app_theme_mode_to_name(mode, prefers_dark);
- app_theme_apply_name(theme_name)?;
- Ok(theme_name)
-}
-
-pub fn app_theme_apply_mode(mode: RadrootsAppThemeMode) -> RadrootsAppThemeResult<&'static str> {
- let prefers_dark = app_theme_prefers_dark();
- let name = app_theme_mode_to_name(mode, prefers_dark);
- app_theme_apply_name(name)?;
- Ok(name)
-}
-
-pub fn app_theme_store_mode(mode: RadrootsAppThemeMode) -> RadrootsAppThemeResult<()> {
- app_theme_write_storage(mode.as_str())
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- app_theme_mode_from_value,
- app_theme_mode_to_name,
- RadrootsAppThemeMode,
- };
-
- #[test]
- fn theme_mode_from_value_parses_known_values() {
- assert_eq!(app_theme_mode_from_value("system"), Some(RadrootsAppThemeMode::System));
- assert_eq!(app_theme_mode_from_value("light"), Some(RadrootsAppThemeMode::Light));
- assert_eq!(app_theme_mode_from_value("dark"), Some(RadrootsAppThemeMode::Dark));
- assert_eq!(app_theme_mode_from_value("other"), None);
- }
-
- #[test]
- fn theme_mode_to_name_respects_preference() {
- assert_eq!(app_theme_mode_to_name(RadrootsAppThemeMode::System, true), "os_dark");
- assert_eq!(app_theme_mode_to_name(RadrootsAppThemeMode::System, false), "os_light");
- assert_eq!(app_theme_mode_to_name(RadrootsAppThemeMode::Light, true), "os_light");
- assert_eq!(app_theme_mode_to_name(RadrootsAppThemeMode::Dark, false), "os_dark");
- }
-}
diff --git a/app/src/ui_demo.rs b/app/src/ui_demo.rs
@@ -1,244 +0,0 @@
-use leptos::prelude::*;
-
-use crate::{app::AppPageChrome, t};
-use radroots_app_ui_components::{
- RadrootsAppUiList,
- RadrootsAppUiListDisplay,
- RadrootsAppUiListDisplayValue,
- RadrootsAppUiListIcon,
- RadrootsAppUiListInput,
- RadrootsAppUiListInputAction,
- RadrootsAppUiListInputField,
- RadrootsAppUiListInputLineLabel,
- RadrootsAppUiListItem,
- RadrootsAppUiListItemKind,
- RadrootsAppUiListLabel,
- RadrootsAppUiListLabelText,
- RadrootsAppUiListLabelValue,
- RadrootsAppUiListLabelValueKind,
- RadrootsAppUiListSelect,
- RadrootsAppUiListSelectField,
- RadrootsAppUiListSelectOption,
- RadrootsAppUiListStyles,
- RadrootsAppUiListTitle,
- RadrootsAppUiListTitleValue,
- RadrootsAppUiListTouch,
- RadrootsAppUiListTouchEnd,
- RadrootsAppUiListView,
- RadrootsAppUiSheetClose,
- RadrootsAppUiSheetContent,
- RadrootsAppUiSheetDescription,
- RadrootsAppUiSheetOverlay,
- RadrootsAppUiSheetPortal,
- RadrootsAppUiSheetRoot,
- RadrootsAppUiSheetTitle,
- RadrootsAppUiSheetTrigger,
-};
-
-#[component]
-pub fn RadrootsAppUiDemoPage() -> impl IntoView {
- let sheet_open = RwSignal::new(false);
- let sheet_open_read = sheet_open.read_only();
- let sheet_open_set = Callback::new(move |value| sheet_open.set(value));
- let input_value = RwSignal::new(String::new());
- let select_value = RwSignal::new("daily".to_string());
- let on_input = Callback::new(move |value| input_value.set(value));
- let on_select = Callback::new(move |value| select_value.set(value));
- let text_label = |value: String| RadrootsAppUiListLabelValue {
- classes_wrap: None,
- hide_truncate: false,
- value: RadrootsAppUiListLabelValueKind::Text(RadrootsAppUiListLabelText {
- value,
- classes: None,
- }),
- };
- let list = RadrootsAppUiList {
- id: Some("ui-demo-list".to_string()),
- view: Some("ui-demo".to_string()),
- classes: None,
- title: Some(RadrootsAppUiListTitle {
- value: RadrootsAppUiListTitleValue::Text(t!("app.ui_demo.list.title")),
- classes: None,
- mod_value: None,
- link: None,
- on_click: None,
- }),
- default_state: None,
- list: Some(vec![
- Some(RadrootsAppUiListItem {
- kind: RadrootsAppUiListItemKind::Touch(RadrootsAppUiListTouch {
- label: RadrootsAppUiListLabel {
- left: vec![text_label(t!("app.ui_demo.item.notifications"))],
- right: Vec::new(),
- },
- display: Some(RadrootsAppUiListDisplay {
- value: RadrootsAppUiListDisplayValue::Label(RadrootsAppUiListLabelText {
- value: t!("app.ui_demo.status.enabled"),
- classes: None,
- }),
- loading: false,
- on_click: None,
- }),
- end: Some(RadrootsAppUiListTouchEnd {
- icon: RadrootsAppUiListIcon {
- key: "chevron-right".to_string(),
- class: None,
- },
- on_click: None,
- }),
- on_click: None,
- }),
- loading: false,
- hide_active: false,
- hide_field: false,
- full_rounded: false,
- offset: None,
- }),
- Some(RadrootsAppUiListItem {
- kind: RadrootsAppUiListItemKind::Input(RadrootsAppUiListInput {
- field: RadrootsAppUiListInputField {
- value: input_value.get_untracked(),
- placeholder: Some(t!("app.ui_demo.input.placeholder")),
- disabled: false,
- classes: None,
- id: Some("ui-demo-note".to_string()),
- on_input: Some(on_input),
- },
- line_label: Some(RadrootsAppUiListInputLineLabel {
- value: t!("app.ui_demo.input.label"),
- classes: None,
- }),
- action: Some(RadrootsAppUiListInputAction {
- visible: true,
- loading: false,
- icon: Some(RadrootsAppUiListIcon {
- key: "plus".to_string(),
- class: None,
- }),
- on_click: None,
- }),
- }),
- loading: false,
- hide_active: true,
- hide_field: false,
- full_rounded: false,
- offset: None,
- }),
- Some(RadrootsAppUiListItem {
- kind: RadrootsAppUiListItemKind::Select(RadrootsAppUiListSelect {
- field: RadrootsAppUiListSelectField {
- value: select_value.get_untracked(),
- options: vec![
- RadrootsAppUiListSelectOption {
- label: t!("app.ui_demo.sync.option.daily"),
- value: "daily".to_string(),
- classes: None,
- },
- RadrootsAppUiListSelectOption {
- label: t!("app.ui_demo.sync.option.weekly"),
- value: "weekly".to_string(),
- classes: None,
- },
- RadrootsAppUiListSelectOption {
- label: t!("app.ui_demo.sync.option.never"),
- value: "never".to_string(),
- classes: None,
- },
- ],
- disabled: false,
- classes: None,
- id: Some("ui-demo-sync".to_string()),
- on_change: Some(on_select),
- },
- label: RadrootsAppUiListLabel {
- left: vec![text_label(t!("app.ui_demo.sync.label"))],
- right: Vec::new(),
- },
- display: None,
- end: Some(RadrootsAppUiListTouchEnd {
- icon: RadrootsAppUiListIcon {
- key: "chevrons-up-down".to_string(),
- class: None,
- },
- on_click: None,
- }),
- loading: false,
- on_click: None,
- }),
- loading: false,
- hide_active: false,
- hide_field: false,
- full_rounded: false,
- offset: None,
- }),
- ]),
- hide_offset: false,
- styles: Some(RadrootsAppUiListStyles {
- hide_border_top: None,
- hide_border_bottom: None,
- hide_rounded: None,
- set_title_background: Some(true),
- set_default_background: None,
- }),
- };
- view! {
- <AppPageChrome title=t!("app.ui_demo.title")>
- <section id="app-ui-demo-content">
- <RadrootsAppUiListView basis=list />
-
- <RadrootsAppUiSheetRoot
- open=Some(sheet_open_read)
- default_open=false
- modal=None
- on_open_change=Some(sheet_open_set)
- >
- <RadrootsAppUiSheetTrigger
- disabled=false
- class=Some("ui-card".to_string())
- id=None
- style=Some("padding:12px 16px; width: 100%; text-align: left;".to_string())
- >
- {t!("app.ui_demo.sheet.open")}
- </RadrootsAppUiSheetTrigger>
- <RadrootsAppUiSheetPortal>
- <RadrootsAppUiSheetOverlay
- close_on_click=None
- class=None
- id=None
- style=None
- />
- <RadrootsAppUiSheetContent
- disable_outside_pointer_events=false
- show_handle=true
- class=None
- id=None
- style=None
- >
- <RadrootsAppUiSheetTitle
- class=None
- id=None
- style=None
- >
- {t!("app.ui_demo.sheet.title")}
- </RadrootsAppUiSheetTitle>
- <RadrootsAppUiSheetDescription
- class=None
- id=None
- style=Some("margin-top: 6px;".to_string())
- >
- {t!("app.ui_demo.sheet.description")}
- </RadrootsAppUiSheetDescription>
- <RadrootsAppUiSheetClose
- class=Some("ui-card".to_string())
- id=None
- style=Some("margin-top: 16px; padding: 10px 14px;".to_string())
- >
- {t!("app.ui_demo.sheet.close")}
- </RadrootsAppUiSheetClose>
- </RadrootsAppUiSheetContent>
- </RadrootsAppUiSheetPortal>
- </RadrootsAppUiSheetRoot>
- </section>
- </AppPageChrome>
- }
-}
diff --git a/app/stylesheets/apps-base.css b/app/stylesheets/apps-base.css
@@ -1,70 +0,0 @@
-:root {
- background-color: var(--bg-app);
-}
-
-html {
- @apply select-none cursor-none;
-}
-
-select:focus {
- outline: none;
-}
-
-button:focus {
- outline: none;
-}
-
-select {
- appearance: none;
- -webkit-appearance: none;
- -moz-appearance: none;
- background: transparent;
- background-image: none;
-}
-
-select::-ms-expand {
- display: none;
-}
-
-div:focus {
- outline: 4px solid transparent;
-}
-
-.scroll-hide::-webkit-scrollbar {
- display: none;
-}
-
-.scroll-hide {
- -ms-overflow-style: none;
- scrollbar-width: none;
-}
-
-@keyframes fade-in {
- from {
- opacity: 0;
- }
-
- to {
- opacity: 1;
- }
-}
-
-.fade-in {
- opacity: 0;
- animation: fade-in 250ms ease-in-out forwards;
-}
-
-.fade-in-long {
- opacity: 0;
- animation: fade-in 350ms ease-in-out forwards;
-}
-
-.pre-wrap-text {
- white-space: pre-wrap;
-}
-
-.flex-fluid {
- width: 100%;
- height: 100%;
- flex: 1 0 100%;
-}
diff --git a/app/stylesheets/apps-ui.css b/app/stylesheets/apps-ui.css
@@ -1,441 +0,0 @@
-.carousel-container {
- display: flex;
- flex-grow: 1;
- overflow-x: hidden;
- scroll-snap-type: x mandatory;
- list-style: none;
- scroll-behavior: smooth;
- -webkit-overflow-scrolling: touch;
-}
-
-.carousel-item {
- scroll-snap-align: start;
-}
-
-@utility carousel-container-trellis {
- @apply flex flex-grow h-full w-full;
-}
-
-@utility carousel-item-trellis {
- @apply flex flex-col w-fit px-4 gap-4 justify-start items-center;
-}
-
-@utility button-base {
- @apply flex flex-row justify-center items-center font-mono text-sm lowercase transition-all select-none cursor-none;
-}
-
-@utility button-simple {
- @apply button-base h-line_button w-fit bg-ly1 text-ly2-gl;
-}
-
-@utility button-submit {
- @apply button-base h-line_button min-w-[82px] w-fit rounded-2xl active:bg-ly1-a bg-ly1 text-ly2-gl;
-}
-
-@utility button-layout {
- @apply flex flex-row h-touch_guide w-lo_ios0 ios1:w-lo_ios1 justify-center items-center bg-ly1 rounded-touch el-re ly1-active-press disabled:opacity-60;
-}
-
-@utility button-layout-label {
- @apply font-sans font-[600] tracking-wide text-ly1-gl-shade group-active:text-ly1-gl/40 el-re;
-}
-
-@layer utilities {
- .button-layout-compact {
- height: 48px;
- min-height: 48px;
- }
-
- .button-layout-accent {
- background: hsl(var(--ly0-gl-hl) / 1);
- color: #fff;
- }
-
- .button-layout-accent:active {
- background: hsl(var(--ly0-gl-hl) / 0.85);
- }
-
- .button-layout-accent:disabled {
- opacity: 1;
- background: hsl(var(--ly0-gl-hl) / 0.6);
- color: #fff;
- }
-
- .button-layout-accent .button-layout-label {
- color: #fff;
- }
-
- .button-layout-accent:active .button-layout-label {
- color: rgba(255, 255, 255, 0.8);
- }
-}
-
-@utility input-base {
- @apply flex w-full items-center rounded-touch border border-ly1-edge/60 bg-ly1 px-3 py-2 font-sans text-form_base text-ly1-gl placeholder:text-ly1-gl-label/70 transition-colors focus:outline-none focus:bg-ly1-focus focus:border-ly1-edge/80 disabled:opacity-60;
- @apply focus:shadow-[inset_0_0_0_1px_hsl(var(--ly1-edge)/0.6)];
-}
-
-@utility input-search {
- @apply input-base pl-10 bg-ly1-focus;
-}
-
-@utility select-ghost {
- @apply input-base bg-transparent border-transparent pr-8;
-}
-
-@utility textarea-base {
- @apply input-base min-h-[8rem] resize-none;
-}
-
-@utility entry-line-fluid {
- @apply h-full w-full rounded-2xl;
-}
-
-@utility entry-textarea-wrap {
- @apply flex flex-row w-full items-center rounded-touch;
-}
-
-@utility entry-line-wrap {
- @apply flex flex-row w-full px-2 items-center;
-}
-
-@utility el-textarea {
- @apply flex flex-row w-full p-0 pl-2 py-3 justify-center items-center rounded-touch border-0 focus:border-0 outline-0 focus:outline-0 bg-transparent disabled:bg-transparent font-sans font-[400] placeholder:font-[400] text-form_base;
-}
-
-.el-textarea-ly1:focus {
- border-color: hsl(var(--ly1-focus));
-}
-
-.el-textarea {
- width: 100%;
- height: max-content;
- outline: none;
- border-radius: 1rem;
- text-wrap: wrap;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-@utility el-input {
- @apply flex flex-row w-full p-0 justify-center items-center border-0 focus:border-0 outline-0 focus:outline-0 bg-transparent disabled:bg-transparent font-sans font-[400] placeholder:font-[400] text-form_base;
-}
-
-@utility el-select {
- @apply flex flex-row w-full p-0 justify-center items-center border-0 focus:border-0 outline-0 focus:outline-0 bg-transparent disabled:bg-transparent font-sans font-[400] placeholder:font-[400] text-form_base;
-}
-
-@utility el-select-centered {
- @apply text-center [text-align-last:center];
-}
-
-@utility el-re {
- @apply ease-in-out transition-all;
-}
-
-@utility opacity-active {
- @apply active:opacity-80 group-active:opacity-80;
-}
-
-@layer components {
- .ios-switch {
- display: inline-flex;
- align-items: center;
- justify-content: flex-start;
- width: 44px;
- height: 26px;
- padding: 2px;
- border-radius: 999px;
- border: 1px solid hsl(var(--ly1-edge) / 0.6);
- background: hsl(var(--ly1-edge) / 0.3);
- transition: background 160ms ease, border-color 160ms ease;
- }
-
- .ios-switch__thumb {
- width: 22px;
- height: 22px;
- border-radius: 999px;
- background: #fff;
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18);
- transition: transform 160ms ease, background 160ms ease;
- transform: translateX(0);
- }
-
- .ios-switch--checked {
- background: #34c759;
- border-color: #34c759;
- }
-
- .ios-switch--checked .ios-switch__thumb {
- transform: translateX(18px);
- background: #fff;
- }
-}
-
-@utility ly1-apply-active {
- @apply bg-ly1-focus;
-}
-
-@utility ly1-selected-press {
- @apply bg-ly1-focus ring-[0.2rem] ring-ly1-edge/40;
- box-shadow: var(--shadow-press);
-}
-
-@utility ly1-ring-apply {
- @apply ring-[0.3rem] ring-ly1-edge;
-}
-
-@utility ly1-raise-apply {
- @apply scale-[102%];
-}
-
-@utility ly1-raise-apply-less {
- @apply scale-[101%];
-}
-
-@utility ly1-active-gl {
- @apply active:text-ly1-gl-a group-active:text-ly1-gl-a;
-}
-
-@utility ly1-active-surface {
- @apply active:bg-ly1-focus group-active:bg-ly1-focus;
-}
-
-@utility ly1-active-press {
- @apply active:bg-ly1-focus group-active:bg-ly1-focus active:opacity-[var(--opacity-press)] group-active:opacity-[var(--opacity-press)] active:shadow-[var(--shadow-press)] group-active:shadow-[var(--shadow-press)];
-}
-
-@utility ly1-active-raise {
- @apply active:scale-[102%] group-active:scale-[102%];
-}
-
-@utility ly1-active-raise-less {
- @apply active:scale-[101%] group-active:scale-[101%];
-}
-
-@utility ly1-active-ring {
- @apply ring-ly2/60 active:ring-[0.3rem] group-active:ring-[0.3rem] delay-[75ms] duration-[350ms];
-}
-
-@utility ly1-active-ring-less {
- @apply ring-ly2/60 active:ring-[0.25rem] group-active:ring-[0.25rem] delay-[50ms] duration-[350ms];
-}
-
-@utility ly1-focus-surface {
- @apply focus:bg-ly1-focus group-focus:bg-ly1-focus;
-}
-
-@utility ly1-focus-raise {
- @apply focus:scale-[102%] group-focus:scale-[102%];
-}
-
-@utility ly1-focus-raise-less {
- @apply focus:scale-[101%] group-focus:scale-[101%];
-}
-
-@utility ly1-focus-ring {
- @apply ring-ly2/60 focus:ring-[0.3rem] group-focus:ring-[0.3rem] delay-[75ms] duration-[350ms];
-}
-
-@utility ly1-focus-ring-less {
- @apply ring-ly2/60 focus:ring-[0.25rem] group-focus:ring-[0.25rem] delay-[50ms] duration-[350ms];
-}
-
-@keyframes spinner-fade-white {
- 0% {
- background-color: white;
- }
- 100% {
- background-color: transparent;
- }
-}
-
-@keyframes spinner-fade-base {
- 0% {
- background-color: hsl(var(--ly2-gl));
- }
- 100% {
- background-color: transparent;
- }
-}
-
-.spinner8 {
- position: relative;
- display: inline-block;
- width: 1em;
- height: 1em;
-}
-
-.spinner8.center {
- position: absolute;
- left: 0;
- right: 0;
- top: 0;
- bottom: 0;
- margin: auto;
-}
-
-.spinner8 .spinner8-blade {
- position: absolute;
- left: 0.4629em;
- bottom: 0;
- width: 0.1em;
- height: 0.2777em;
- border-radius: 0.0555em;
- background-color: transparent;
- transform-origin: center -0.2222em;
- animation: spinner-fade-base 1s infinite linear;
-}
-
-.spinner8-white .spinner8-blade-white {
- position: absolute;
- left: 0.4629em;
- bottom: 0;
- width: 0.1em;
- height: 0.2777em;
- border-radius: 0.0555em;
- background-color: transparent;
- transform-origin: center -0.2222em;
- animation: spinner-fade-white 1s infinite linear;
-}
-
-.spinner8 .spinner8-blade:nth-child(1) {
- animation-delay: 0s;
- transform: rotate(0deg);
-}
-
-.spinner8 .spinner8-blade:nth-child(2) {
- animation-delay: 0.125s;
- transform: rotate(45deg);
-}
-
-.spinner8 .spinner8-blade:nth-child(3) {
- animation-delay: 0.25s;
- transform: rotate(90deg);
-}
-
-.spinner8 .spinner8-blade:nth-child(4) {
- animation-delay: 0.375s;
- transform: rotate(135deg);
-}
-
-.spinner8 .spinner8-blade:nth-child(5) {
- animation-delay: 0.5s;
- transform: rotate(180deg);
-}
-
-.spinner8 .spinner8-blade:nth-child(6) {
- animation-delay: 0.625s;
- transform: rotate(225deg);
-}
-
-.spinner8 .spinner8-blade:nth-child(7) {
- animation-delay: 0.75s;
- transform: rotate(270deg);
-}
-
-.spinner8 .spinner8-blade:nth-child(8) {
- animation-delay: 0.875s;
- transform: rotate(315deg);
-}
-
-.spinner12 {
- position: relative;
- display: inline-block;
- width: 1em;
- height: 1em;
-}
-
-.spinner12.center {
- position: absolute;
- left: 0;
- right: 0;
- top: 0;
- bottom: 0;
- margin: auto;
-}
-
-.spinner12 .spinner12-blade {
- position: absolute;
- left: 0.4629em;
- bottom: 0;
- width: 0.074em;
- height: 0.2777em;
- border-radius: 0.0555em;
- background-color: transparent;
- transform-origin: center -0.2222em;
- animation: spinner-fade-base 1s infinite linear;
-}
-
-.spinner12-white .spinner12-blade-white {
- position: absolute;
- left: 0.4629em;
- bottom: 0;
- width: 0.074em;
- height: 0.2777em;
- border-radius: 0.0555em;
- background-color: transparent;
- transform-origin: center -0.2222em;
- animation: spinner-fade-white 1s infinite linear;
-}
-
-.spinner12 .spinner12-blade:nth-child(1) {
- animation-delay: 0s;
- transform: rotate(0deg);
-}
-
-.spinner12 .spinner12-blade:nth-child(2) {
- animation-delay: 0.083s;
- transform: rotate(30deg);
-}
-
-.spinner12 .spinner12-blade:nth-child(3) {
- animation-delay: 0.166s;
- transform: rotate(60deg);
-}
-
-.spinner12 .spinner12-blade:nth-child(4) {
- animation-delay: 0.249s;
- transform: rotate(90deg);
-}
-
-.spinner12 .spinner12-blade:nth-child(5) {
- animation-delay: 0.332s;
- transform: rotate(120deg);
-}
-
-.spinner12 .spinner12-blade:nth-child(6) {
- animation-delay: 0.415s;
- transform: rotate(150deg);
-}
-
-.spinner12 .spinner12-blade:nth-child(7) {
- animation-delay: 0.498s;
- transform: rotate(180deg);
-}
-
-.spinner12 .spinner12-blade:nth-child(8) {
- animation-delay: 0.581s;
- transform: rotate(210deg);
-}
-
-.spinner12 .spinner12-blade:nth-child(9) {
- animation-delay: 0.664s;
- transform: rotate(240deg);
-}
-
-.spinner12 .spinner12-blade:nth-child(10) {
- animation-delay: 0.747s;
- transform: rotate(270deg);
-}
-
-.spinner12 .spinner12-blade:nth-child(11) {
- animation-delay: 0.83s;
- transform: rotate(300deg);
-}
-
-.spinner12 .spinner12-blade:nth-child(12) {
- animation-delay: 0.913s;
- transform: rotate(330deg);
-}
diff --git a/app/stylesheets/styles-maplibre-gl.css b/app/stylesheets/styles-maplibre-gl.css
@@ -1,12 +0,0 @@
-.maplibregl-popup-tip {
- display: none !important;
-}
-
-.maplibregl-popup-content {
- background: hsl(var(--ly1)) !important;
- border-radius: 16px !important;
- box-shadow: 0 4px 8px rgba(0,0,0,.2) !important;
- padding: 0px !important;
- transition: background-color 250ms ease-in-out !important;
-}
-
diff --git a/app/stylesheets/styles-superellipse.css b/app/stylesheets/styles-superellipse.css
@@ -1,59 +0,0 @@
-.round-8 {
- mask-image: paint(squircle);
- --squircle-smooth: 0.6;
- --squircle-radius: 8px;
-}
-
-.round-12 {
- mask-image: paint(squircle);
- --squircle-smooth: 0.6;
- --squircle-radius: 12px;
-}
-
-.round-16 {
- mask-image: paint(squircle);
- --squircle-smooth: 0.6;
- --squircle-radius: 16px;
-}
-
-.round-20 {
- mask-image: paint(squircle);
- --squircle-smooth: 0.6;
- --squircle-radius: 20px;
-}
-
-.round-24 {
- mask-image: paint(squircle);
- --squircle-smooth: 0.6;
- --squircle-radius: 24px;
-}
-
-.round-32 {
- mask-image: paint(squircle);
- --squircle-smooth: 0.6;
- --squircle-radius: 32px;
-}
-
-.round-36 {
- mask-image: paint(squircle);
- --squircle-smooth: 0.6;
- --squircle-radius: 36px;
-}
-
-.round-40 {
- mask-image: paint(squircle);
- --squircle-smooth: 0.6;
- --squircle-radius: 40px;
-}
-
-.round-44 {
- mask-image: paint(squircle);
- --squircle-smooth: 0.6;
- --squircle-radius: 44px;
-}
-
-.round-48 {
- mask-image: paint(squircle);
- --squircle-smooth: 0.6;
- --squircle-radius: 48px;
-}
diff --git a/crates/app-lib/Cargo.toml b/crates/app-lib/Cargo.toml
@@ -1,27 +0,0 @@
-[package]
-name = "radroots-app-lib"
-authors = ["Radroots Authors"]
-version.workspace = true
-edition.workspace = true
-license.workspace = true
-rust-version.workspace = true
-
-[lib]
-crate-type = ["rlib"]
-
-[dependencies]
-serde = { workspace = true }
-serde_json = { workspace = true }
-serde-wasm-bindgen = { workspace = true }
-js-sys = { workspace = true }
-wasm-bindgen = { workspace = true }
-wasm-bindgen-futures = { workspace = true }
-url = { workspace = true }
-futures = { workspace = true }
-radroots-app-utils = { path = "../utils" }
-once_cell = { workspace = true }
-regex = { workspace = true }
-web-sys = { workspace = true }
-
-[target.'cfg(target_arch = "wasm32")'.dependencies]
-gloo-timers = { workspace = true }
diff --git a/crates/app-lib/src/browser.rs b/crates/app-lib/src/browser.rs
@@ -1,137 +0,0 @@
-#![forbid(unsafe_code)]
-
-#[cfg(any(test, target_arch = "wasm32"))]
-use once_cell::sync::Lazy;
-#[cfg(any(test, target_arch = "wasm32"))]
-use regex::Regex;
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct BrowserPlatformInfo {
- pub os: String,
- pub browser: String,
- pub version: String,
-}
-
-#[cfg(any(test, target_arch = "wasm32"))]
-static REMOVE_EXCESS_MOZILLA_AND_VERSION: Lazy<Regex> =
- Lazy::new(|| Regex::new(r"^mozilla/\d\.\d\W").expect("regex"));
-#[cfg(any(test, target_arch = "wasm32"))]
-static BROWSER_PATTERN: Lazy<Regex> = Lazy::new(|| {
- Regex::new(r"(\w+)/(\d+\.\d+(?:\.\d+)?(?:\.\d+)?)").expect("regex")
-});
-#[cfg(any(test, target_arch = "wasm32"))]
-static ENGINE_AND_VERSION_PATTERN: Lazy<Regex> =
- Lazy::new(|| Regex::new(r"^(ver|cri|gec)").expect("regex"));
-#[cfg(any(test, target_arch = "wasm32"))]
-static VERSION_PATTERN: Lazy<Regex> =
- Lazy::new(|| Regex::new(r"version/(\d+(\.\d+)*)").expect("regex"));
-#[cfg(any(test, target_arch = "wasm32"))]
-static MOBILE_OS_IPHONE: Lazy<Regex> = Lazy::new(|| Regex::new("iphone").expect("regex"));
-#[cfg(any(test, target_arch = "wasm32"))]
-static MOBILE_OS_IPAD: Lazy<Regex> = Lazy::new(|| Regex::new("ipad|macintosh").expect("regex"));
-#[cfg(any(test, target_arch = "wasm32"))]
-static MOBILE_OS_ANDROID: Lazy<Regex> = Lazy::new(|| Regex::new("android").expect("regex"));
-#[cfg(any(test, target_arch = "wasm32"))]
-static DESKTOP_OS_WINDOWS: Lazy<Regex> = Lazy::new(|| Regex::new("win").expect("regex"));
-#[cfg(any(test, target_arch = "wasm32"))]
-static DESKTOP_OS_MAC: Lazy<Regex> = Lazy::new(|| Regex::new("macintosh").expect("regex"));
-#[cfg(any(test, target_arch = "wasm32"))]
-static DESKTOP_OS_LINUX: Lazy<Regex> = Lazy::new(|| Regex::new("linux").expect("regex"));
-
-pub fn browser_platform() -> Option<BrowserPlatformInfo> {
- #[cfg(target_arch = "wasm32")]
- {
- let window = web_sys::window()?;
- let navigator = window.navigator();
- let ua = navigator.user_agent().ok()?;
- let max_touch_points = navigator.max_touch_points();
- return Some(parse_user_agent_string(&ua, max_touch_points));
- }
- #[cfg(not(target_arch = "wasm32"))]
- {
- None
- }
-}
-
-#[cfg(any(test, target_arch = "wasm32"))]
-fn parse_user_agent_string(ua_string: &str, max_touch_points: i32) -> BrowserPlatformInfo {
- let ua = REMOVE_EXCESS_MOZILLA_AND_VERSION
- .replace(&ua_string.to_lowercase(), "")
- .to_string();
-
- let mobile_os = if MOBILE_OS_IPHONE.is_match(&ua) && max_touch_points >= 1 {
- Some("iphone")
- } else if MOBILE_OS_IPAD.is_match(&ua) && max_touch_points >= 1 {
- Some("ipad")
- } else if MOBILE_OS_ANDROID.is_match(&ua) && max_touch_points >= 1 {
- Some("android")
- } else {
- None
- };
- let desktop_os = if DESKTOP_OS_WINDOWS.is_match(&ua) {
- Some("windows")
- } else if DESKTOP_OS_MAC.is_match(&ua) {
- Some("mac")
- } else if DESKTOP_OS_LINUX.is_match(&ua) {
- Some("linux")
- } else {
- None
- };
- let os = mobile_os.or(desktop_os).unwrap_or("");
-
- let browser_matches = BROWSER_PATTERN
- .find_iter(&ua)
- .map(|capture| capture.as_str().to_string())
- .collect::<Vec<_>>();
- let safari_version = VERSION_PATTERN
- .captures(&ua)
- .and_then(|caps| caps.get(1).map(|match_value| match_value.as_str().to_string()));
- let browser_offset = if browser_matches.len() > 2 {
- browser_matches
- .get(1)
- .map(|match_value| !ENGINE_AND_VERSION_PATTERN.is_match(match_value))
- .unwrap_or(false)
- } else {
- false
- };
- let browser_index = browser_matches
- .len()
- .saturating_sub(1 + if browser_offset { 1 } else { 0 });
- let (browser, version) = browser_matches
- .get(browser_index)
- .and_then(|match_value| {
- let mut parts = match_value.split('/');
- let browser = parts.next().unwrap_or("").to_string();
- let version = parts.next().unwrap_or("").to_string();
- Some((browser, version))
- })
- .unwrap_or_else(|| (String::new(), String::new()));
- let version = safari_version.unwrap_or(version);
-
- BrowserPlatformInfo {
- os: os.to_string(),
- browser,
- version,
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{parse_user_agent_string, BrowserPlatformInfo};
-
- #[test]
- fn parse_user_agent_detects_browser() {
- let info = parse_user_agent_string(
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
- 0,
- );
- assert_eq!(
- info,
- BrowserPlatformInfo {
- os: "mac".to_string(),
- browser: "chrome".to_string(),
- version: "120.0.0.0".to_string()
- }
- );
- }
-}
diff --git a/crates/app-lib/src/dom.rs b/crates/app-lib/src/dom.rs
@@ -1,91 +0,0 @@
-#![forbid(unsafe_code)]
-
-use std::fmt;
-
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen::JsCast;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum DomError {
- WindowUnavailable,
- DocumentUnavailable,
- QueryFailure,
- ClassListFailure,
-}
-
-impl fmt::Display for DomError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- DomError::WindowUnavailable => f.write_str("error.app.dom.window_unavailable"),
- DomError::DocumentUnavailable => f.write_str("error.app.dom.document_unavailable"),
- DomError::QueryFailure => f.write_str("error.app.dom.query_failure"),
- DomError::ClassListFailure => f.write_str("error.app.dom.class_list_failure"),
- }
- }
-}
-
-impl std::error::Error for DomError {}
-
-pub fn view_effect(view: &str) -> Result<(), DomError> {
- #[cfg(target_arch = "wasm32")]
- {
- let window = web_sys::window().ok_or(DomError::WindowUnavailable)?;
- let document = window.document().ok_or(DomError::DocumentUnavailable)?;
- let nodes = document
- .query_selector_all("[data-view]")
- .map_err(|_| DomError::QueryFailure)?;
- for idx in 0..nodes.length() {
- let Some(node) = nodes.get(idx) else {
- continue;
- };
- let element: web_sys::Element = node.unchecked_into();
- let attr = element.get_attribute("data-view").unwrap_or_default();
- let class_list = element.class_list();
- if attr != view {
- class_list
- .add_1("hidden")
- .map_err(|_| DomError::ClassListFailure)?;
- } else {
- class_list
- .remove_1("hidden")
- .map_err(|_| DomError::ClassListFailure)?;
- }
- }
- Ok(())
- }
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = view;
- Err(DomError::WindowUnavailable)
- }
-}
-
-pub fn el_id(id: &str) -> Option<web_sys::Element> {
- #[cfg(target_arch = "wasm32")]
- {
- let window = web_sys::window()?;
- let document = window.document()?;
- document.get_element_by_id(id)
- }
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = id;
- None
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{el_id, view_effect, DomError};
-
- #[test]
- fn view_effect_errors_on_non_wasm() {
- let err = view_effect("home").expect_err("non-wasm");
- assert_eq!(err, DomError::WindowUnavailable);
- }
-
- #[test]
- fn el_id_returns_none_on_non_wasm() {
- assert!(el_id("missing").is_none());
- }
-}
diff --git a/crates/app-lib/src/fetch.rs b/crates/app-lib/src/fetch.rs
@@ -1,158 +0,0 @@
-#![forbid(unsafe_code)]
-
-use serde::de::DeserializeOwned;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum FetchJsonErrorKind {
- Http,
- Network,
- Parse,
- Unavailable,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct FetchJsonError {
- pub kind: FetchJsonErrorKind,
- pub url: String,
- pub message: String,
- pub status: Option<u16>,
- pub status_text: Option<String>,
-}
-
-pub type FetchJsonResult<T> = Result<T, FetchJsonError>;
-
-impl FetchJsonError {
- pub fn http(url: &str, status: u16, status_text: Option<String>) -> Self {
- let message = status_text
- .clone()
- .filter(|text| !text.is_empty())
- .unwrap_or_else(|| "http_error".to_string());
- Self {
- kind: FetchJsonErrorKind::Http,
- url: url.to_string(),
- message,
- status: Some(status),
- status_text,
- }
- }
-
- pub fn network(url: &str, message: Option<String>) -> Self {
- let message = message.filter(|text| !text.is_empty())
- .unwrap_or_else(|| "network_error".to_string());
- Self {
- kind: FetchJsonErrorKind::Network,
- url: url.to_string(),
- message,
- status: None,
- status_text: None,
- }
- }
-
- pub fn parse(url: &str, message: Option<String>) -> Self {
- let message = message.filter(|text| !text.is_empty())
- .unwrap_or_else(|| "parse_error".to_string());
- Self {
- kind: FetchJsonErrorKind::Parse,
- url: url.to_string(),
- message,
- status: None,
- status_text: None,
- }
- }
-
- pub fn unavailable(url: &str) -> Self {
- Self {
- kind: FetchJsonErrorKind::Unavailable,
- url: url.to_string(),
- message: "fetch_unavailable".to_string(),
- status: None,
- status_text: None,
- }
- }
-}
-
-pub async fn fetch_json<T>(url: &str) -> FetchJsonResult<T>
-where
- T: DeserializeOwned,
-{
- #[cfg(target_arch = "wasm32")]
- {
- fetch_json_wasm(url).await
- }
- #[cfg(not(target_arch = "wasm32"))]
- {
- Err(FetchJsonError::unavailable(url))
- }
-}
-
-#[cfg(target_arch = "wasm32")]
-async fn fetch_json_wasm<T>(url: &str) -> FetchJsonResult<T>
-where
- T: DeserializeOwned,
-{
- use wasm_bindgen::JsCast;
-
- let window = web_sys::window().ok_or_else(|| FetchJsonError::unavailable(url))?;
- let resp_value = wasm_bindgen_futures::JsFuture::from(window.fetch_with_str(url))
- .await
- .map_err(|err| FetchJsonError::network(url, js_error_message(err)))?;
- let response: web_sys::Response = resp_value
- .dyn_into()
- .map_err(|_| FetchJsonError::network(url, Some("network_error".to_string())))?;
- if !response.ok() {
- let status_text = response.status_text();
- return Err(FetchJsonError::http(
- url,
- response.status(),
- if status_text.is_empty() { None } else { Some(status_text) },
- ));
- }
- let json_promise = response
- .json()
- .map_err(|err| FetchJsonError::parse(url, js_error_message(err)))?;
- let json_value = wasm_bindgen_futures::JsFuture::from(json_promise)
- .await
- .map_err(|err| FetchJsonError::parse(url, js_error_message(err)))?;
- serde_wasm_bindgen::from_value(json_value)
- .map_err(|err| FetchJsonError::parse(url, Some(err.to_string())))
-}
-
-#[cfg(target_arch = "wasm32")]
-fn js_error_message(err: wasm_bindgen::JsValue) -> Option<String> {
- err.as_string().filter(|text| !text.is_empty())
-}
-
-#[cfg(test)]
-mod tests {
- use super::{fetch_json, FetchJsonError, FetchJsonErrorKind};
-
- #[derive(Debug, serde::Deserialize)]
- struct DummyPayload {
- #[serde(rename = "value")]
- _value: String,
- }
-
- #[test]
- fn fetch_json_http_error_sets_fields() {
- let err = FetchJsonError::http("https://example", 404, Some("Not Found".to_string()));
- assert_eq!(err.kind, FetchJsonErrorKind::Http);
- assert_eq!(err.url, "https://example");
- assert_eq!(err.status, Some(404));
- assert_eq!(err.status_text.as_deref(), Some("Not Found"));
- }
-
- #[test]
- fn fetch_json_network_error_defaults_message() {
- let err = FetchJsonError::network("https://example", None);
- assert_eq!(err.kind, FetchJsonErrorKind::Network);
- assert_eq!(err.message, "network_error");
- }
-
- #[test]
- fn non_wasm_fetch_is_unavailable() {
- let err = futures::executor::block_on(fetch_json::<DummyPayload>("https://example"))
- .expect_err("unavailable");
- assert_eq!(err.kind, FetchJsonErrorKind::Unavailable);
- assert_eq!(err.url, "https://example");
- }
-}
diff --git a/crates/app-lib/src/file.rs b/crates/app-lib/src/file.rs
@@ -1,212 +0,0 @@
-#![forbid(unsafe_code)]
-
-use std::fmt;
-
-use radroots_app_utils::types::{FilePath, FilePathBlob, WebFilePath};
-use serde::de::DeserializeOwned;
-use serde::Serialize;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum FileError {
- WindowUnavailable,
- DocumentUnavailable,
- ElementUnavailable,
- BlobFailure,
- UrlFailure,
- SerializeFailure,
- ReadFailure,
- ParseFailure,
- EmptyFile,
- PickerFailure,
-}
-
-impl fmt::Display for FileError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- FileError::WindowUnavailable => f.write_str("error.app.file.window_unavailable"),
- FileError::DocumentUnavailable => f.write_str("error.app.file.document_unavailable"),
- FileError::ElementUnavailable => f.write_str("error.app.file.element_unavailable"),
- FileError::BlobFailure => f.write_str("error.app.file.blob_failure"),
- FileError::UrlFailure => f.write_str("error.app.file.url_failure"),
- FileError::SerializeFailure => f.write_str("error.app.file.serialize_failure"),
- FileError::ReadFailure => f.write_str("error.app.file.read_failure"),
- FileError::ParseFailure => f.write_str("error.app.file.parse_failure"),
- FileError::EmptyFile => f.write_str("error.app.file.empty_file"),
- FileError::PickerFailure => f.write_str("error.app.file.picker_failure"),
- }
- }
-}
-
-impl std::error::Error for FileError {}
-
-pub fn parse_file_path(file_path: &str) -> Option<WebFilePath> {
- if file_path.starts_with("blob:") {
- let blob_name = file_path.replace("blob:", "").replace("http://", "");
- return Some(WebFilePath::Blob(FilePathBlob {
- blob_path: file_path.to_string(),
- blob_name,
- mime_type: None,
- }));
- }
- let file_path_file = file_path.rsplit('/').next().unwrap_or("");
- let mut parts = file_path_file.split('.');
- let file_name = parts.next().unwrap_or("");
- let mime_type = parts.next().unwrap_or("");
- if file_name.is_empty() || mime_type.is_empty() {
- return None;
- }
- Some(WebFilePath::File(FilePath {
- file_path: file_path.to_string(),
- file_name: file_name.to_string(),
- mime_type: mime_type.to_string(),
- }))
-}
-
-pub fn download_json<T: Serialize>(data: &T, filename: &str) -> Result<(), FileError> {
- #[cfg(target_arch = "wasm32")]
- {
- use wasm_bindgen::JsCast;
-
- let json = serde_json::to_string_pretty(data).map_err(|_| FileError::SerializeFailure)?;
- let array = js_sys::Array::new();
- array.push(&wasm_bindgen::JsValue::from_str(&json));
- let blob = web_sys::Blob::new_with_str_sequence(&array).map_err(|_| FileError::BlobFailure)?;
- let url =
- web_sys::Url::create_object_url_with_blob(&blob).map_err(|_| FileError::UrlFailure)?;
- let window = web_sys::window().ok_or(FileError::WindowUnavailable)?;
- let document = window.document().ok_or(FileError::DocumentUnavailable)?;
- let anchor = document
- .create_element("a")
- .map_err(|_| FileError::ElementUnavailable)?;
- let anchor: web_sys::HtmlAnchorElement =
- anchor.dyn_into().map_err(|_| FileError::ElementUnavailable)?;
- anchor.set_href(&url);
- anchor.set_download(filename);
- anchor.click();
- web_sys::Url::revoke_object_url(&url).map_err(|_| FileError::UrlFailure)?;
- Ok(())
- }
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = data;
- let _ = filename;
- Err(FileError::WindowUnavailable)
- }
-}
-
-pub async fn select_file() -> Result<Option<web_sys::File>, FileError> {
- #[cfg(target_arch = "wasm32")]
- {
- use std::cell::RefCell;
- use std::rc::Rc;
- use wasm_bindgen::JsCast;
-
- let window = web_sys::window().ok_or(FileError::WindowUnavailable)?;
- let document = window.document().ok_or(FileError::DocumentUnavailable)?;
- let input = document
- .create_element("input")
- .map_err(|_| FileError::ElementUnavailable)?;
- let input: web_sys::HtmlInputElement =
- input.dyn_into().map_err(|_| FileError::ElementUnavailable)?;
- input.set_type("file");
- input.set_accept("*/*");
-
- let (sender, receiver) = futures::channel::oneshot::channel();
- let sender = Rc::new(RefCell::new(Some(sender)));
- let closure_holder: Rc<RefCell<Option<wasm_bindgen::closure::Closure<dyn FnMut(_)>>>> =
- Rc::new(RefCell::new(None));
- let closure_ref = closure_holder.clone();
- let sender_ref = Rc::clone(&sender);
- let input_clone = input.clone();
- *closure_holder.borrow_mut() = Some(wasm_bindgen::closure::Closure::wrap(Box::new(
- move |_event: web_sys::Event| {
- let file = input_clone.files().and_then(|list| list.get(0));
- if let Some(sender) = sender_ref.borrow_mut().take() {
- let _ = sender.send(file);
- }
- closure_ref.borrow_mut().take();
- },
- ) as Box<dyn FnMut(_)>));
- if let Some(closure) = closure_holder.borrow().as_ref() {
- input.set_onchange(Some(closure.as_ref().unchecked_ref()));
- }
- input.click();
- receiver.await.map_err(|_| FileError::PickerFailure)
- }
- #[cfg(not(target_arch = "wasm32"))]
- {
- Err(FileError::WindowUnavailable)
- }
-}
-
-pub async fn get_file_text(file: Option<web_sys::File>) -> Result<Option<String>, FileError> {
- let Some(file) = file else {
- return Ok(None);
- };
- #[cfg(target_arch = "wasm32")]
- {
- let text = wasm_bindgen_futures::JsFuture::from(file.text())
- .await
- .map_err(|_| FileError::ReadFailure)?;
- Ok(text.as_string())
- }
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = file;
- Err(FileError::WindowUnavailable)
- }
-}
-
-pub async fn parse_file_json<T: DeserializeOwned>(
- file: Option<web_sys::File>,
-) -> Result<T, FileError> {
- let contents = get_file_text(file).await?;
- let Some(contents) = contents else {
- return Err(FileError::EmptyFile);
- };
- if contents.is_empty() {
- return Err(FileError::EmptyFile);
- }
- serde_json::from_str(&contents).map_err(|_| FileError::ParseFailure)
-}
-
-#[cfg(test)]
-mod tests {
- use super::{get_file_text, parse_file_path, FileError};
-
- #[test]
- fn parse_file_path_handles_blob_paths() {
- let parsed = parse_file_path("blob:http://example").expect("parsed");
- match parsed {
- radroots_app_utils::types::WebFilePath::Blob(blob) => {
- assert_eq!(blob.blob_name, "example");
- }
- _ => panic!("expected blob"),
- }
- }
-
- #[test]
- fn parse_file_path_handles_files() {
- let parsed = parse_file_path("/path/file.txt").expect("parsed");
- match parsed {
- radroots_app_utils::types::WebFilePath::File(file) => {
- assert_eq!(file.file_name, "file");
- assert_eq!(file.mime_type, "txt");
- }
- _ => panic!("expected file"),
- }
- }
-
- #[test]
- fn get_file_text_none_returns_none() {
- let result = futures::executor::block_on(get_file_text(None)).expect("ok");
- assert!(result.is_none());
- }
-
- #[test]
- fn parse_file_json_errors_without_file() {
- let err = futures::executor::block_on(super::parse_file_json::<serde_json::Value>(None))
- .expect_err("err");
- assert_eq!(err, FileError::EmptyFile);
- }
-}
diff --git a/crates/app-lib/src/geo.rs b/crates/app-lib/src/geo.rs
@@ -1,31 +0,0 @@
-#![forbid(unsafe_code)]
-
-#[derive(Debug, Clone, Copy, PartialEq)]
-pub struct AppGeolocationPoint {
- pub lat: f64,
- pub lng: f64,
-}
-
-pub fn geop_is_valid(point: Option<AppGeolocationPoint>) -> bool {
- if let Some(point) = point {
- !(point.lat == 0.0 && point.lng == 0.0)
- } else {
- false
- }
-}
-
-pub fn geop_init() -> AppGeolocationPoint {
- AppGeolocationPoint { lat: 0.0, lng: 0.0 }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{geop_init, geop_is_valid, AppGeolocationPoint};
-
- #[test]
- fn geop_is_valid_checks_coords() {
- assert!(!geop_is_valid(None));
- assert!(!geop_is_valid(Some(geop_init())));
- assert!(geop_is_valid(Some(AppGeolocationPoint { lat: 1.0, lng: 1.0 })));
- }
-}
diff --git a/crates/app-lib/src/lib.rs b/crates/app-lib/src/lib.rs
@@ -1,31 +0,0 @@
-#![forbid(unsafe_code)]
-
-pub mod browser;
-pub mod dom;
-pub mod file;
-pub mod fetch;
-pub mod geo;
-pub mod locale;
-pub mod path;
-pub mod query;
-pub mod sleep;
-pub mod storage;
-pub mod symbols;
-pub mod theme;
-
-pub use browser::{browser_platform, BrowserPlatformInfo};
-pub use dom::{el_id, view_effect, DomError};
-pub use file::{download_json, get_file_text, parse_file_json, parse_file_path, select_file, FileError};
-pub use fetch::{fetch_json, FetchJsonError, FetchJsonErrorKind, FetchJsonResult};
-pub use geo::{geop_init, geop_is_valid, AppGeolocationPoint};
-pub use locale::{get_locale, resolve_locale};
-pub use path::{normalize_path, sanitize_path, trim_slashes};
-pub use query::{encode_query_params, encode_route};
-pub use sleep::sleep;
-pub use storage::{build_storage_key, build_storage_key_with_prefix, fmt_id, fmt_id_from_path};
-pub use symbols::{
- fmt_cl, value_constrain, SYMBOL_BULLET, SYMBOL_DASH, SYMBOL_DOWN, SYMBOL_PERCENT, SYMBOL_UP,
-};
-pub use theme::{
- get_system_theme, parse_layer, theme_set, ThemeError, ThemeLayer, ThemeMode,
-};
diff --git a/crates/app-lib/src/locale.rs b/crates/app-lib/src/locale.rs
@@ -1,62 +0,0 @@
-#![forbid(unsafe_code)]
-
-const DEFAULT_LOCALE: &str = "en";
-
-pub fn resolve_locale(locales: &[&str], navigator_locale: Option<&str>) -> String {
- let fallback = locales.first().copied().unwrap_or(DEFAULT_LOCALE);
- let fallback_lower = fallback.to_ascii_lowercase();
- let Some(nav_locale) = navigator_locale else {
- return fallback_lower;
- };
- let nav_lower = nav_locale.to_ascii_lowercase();
- if locales
- .iter()
- .any(|locale| locale.eq_ignore_ascii_case(&nav_lower))
- {
- return nav_lower;
- }
- let prefix = nav_lower.chars().take(2).collect::<String>();
- if !prefix.is_empty()
- && locales
- .iter()
- .any(|locale| locale.eq_ignore_ascii_case(&prefix))
- {
- return prefix;
- }
- fallback_lower
-}
-
-pub fn get_locale(locales: &[&str]) -> String {
- #[cfg(target_arch = "wasm32")]
- {
- let navigator_locale = web_sys::window().and_then(|window| window.navigator().language());
- resolve_locale(locales, navigator_locale.as_deref())
- }
- #[cfg(not(target_arch = "wasm32"))]
- {
- resolve_locale(locales, None)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::resolve_locale;
-
- #[test]
- fn resolve_locale_prefers_exact_match() {
- let locales = ["en", "fr"];
- assert_eq!(resolve_locale(&locales, Some("fr")), "fr");
- }
-
- #[test]
- fn resolve_locale_prefers_prefix_match() {
- let locales = ["en", "fr"];
- assert_eq!(resolve_locale(&locales, Some("fr-CA")), "fr");
- }
-
- #[test]
- fn resolve_locale_falls_back() {
- let locales = ["en", "fr"];
- assert_eq!(resolve_locale(&locales, Some("es-ES")), "en");
- }
-}
diff --git a/crates/app-lib/src/path.rs b/crates/app-lib/src/path.rs
@@ -1,50 +0,0 @@
-#![forbid(unsafe_code)]
-
-pub fn trim_slashes(path: &str) -> String {
- path.trim_matches('/').to_string()
-}
-
-pub fn normalize_path(path: &str) -> String {
- let mut output = String::with_capacity(path.len());
- for ch in path.chars() {
- let mapped = match ch {
- '-' => '_',
- '/' => '-',
- _ => ch,
- };
- if mapped == '-' && output.ends_with('-') {
- continue;
- }
- output.push(mapped);
- }
- output
-}
-
-pub fn sanitize_path(raw: &str) -> String {
- raw.chars()
- .filter(|ch| ch.is_ascii_alphanumeric() || *ch == '_' || *ch == '-')
- .collect()
-}
-
-#[cfg(test)]
-mod tests {
- use super::{normalize_path, sanitize_path, trim_slashes};
-
- #[test]
- fn trim_slashes_removes_edge_slashes() {
- assert_eq!(trim_slashes("/a/b/"), "a/b");
- assert_eq!(trim_slashes("///a///"), "a");
- }
-
- #[test]
- fn normalize_path_replaces_chars() {
- assert_eq!(normalize_path("a-b/c"), "a_b-c");
- assert_eq!(normalize_path("a//b"), "a-b");
- }
-
- #[test]
- fn sanitize_path_strips_invalid_chars() {
- assert_eq!(sanitize_path("ab/c$%"), "abc");
- assert_eq!(sanitize_path("a_b-1"), "a_b-1");
- }
-}
diff --git a/crates/app-lib/src/query.rs b/crates/app-lib/src/query.rs
@@ -1,73 +0,0 @@
-#![forbid(unsafe_code)]
-
-pub fn encode_query_params<K: AsRef<str>, V: AsRef<str>>(params: &[(K, V)]) -> String {
- let mut output = String::new();
- for (key, value) in params {
- let key = key.as_ref().trim();
- let value = value.as_ref().trim();
- if key.is_empty() || value.is_empty() {
- continue;
- }
- if !output.is_empty() {
- output.push('&');
- }
- output.push_str(key);
- output.push('=');
- for part in url::form_urlencoded::byte_serialize(value.as_bytes()) {
- for ch in part.chars() {
- if ch == '+' {
- output.push_str("%20");
- } else {
- output.push(ch);
- }
- }
- }
- }
- if output.is_empty() {
- String::new()
- } else {
- format!("?{output}")
- }
-}
-
-pub fn encode_route<K: AsRef<str>, V: AsRef<str>>(route: &str, params: &[(K, V)]) -> String {
- let query = encode_query_params(params);
- if query.is_empty() {
- return route.to_string();
- }
- let base = if route == "/" {
- route.to_string()
- } else {
- let trimmed = route.trim_end_matches('/');
- if trimmed.is_empty() {
- "/".to_string()
- } else {
- trimmed.to_string()
- }
- };
- format!("{base}{query}")
-}
-
-#[cfg(test)]
-mod tests {
- use super::{encode_query_params, encode_route};
-
- #[test]
- fn encode_query_params_skips_empty_entries() {
- let params = [("a", "b c"), ("", "skip"), ("c", "")];
- assert_eq!(encode_query_params(¶ms), "?a=b%20c");
- }
-
- #[test]
- fn encode_route_appends_query() {
- let params = [("q", "1")];
- assert_eq!(encode_route("/path/", ¶ms), "/path?q=1");
- assert_eq!(encode_route("/", ¶ms), "/?q=1");
- }
-
- #[test]
- fn encode_route_preserves_route_without_params() {
- let params: [(&str, &str); 0] = [];
- assert_eq!(encode_route("/path/", ¶ms), "/path/");
- }
-}
diff --git a/crates/app-lib/src/sleep.rs b/crates/app-lib/src/sleep.rs
@@ -1,23 +0,0 @@
-#![forbid(unsafe_code)]
-
-pub async fn sleep(ms: u64) {
- #[cfg(target_arch = "wasm32")]
- {
- let delay = ms.min(u32::MAX as u64) as u32;
- gloo_timers::future::TimeoutFuture::new(delay).await;
- }
- #[cfg(not(target_arch = "wasm32"))]
- {
- std::thread::sleep(std::time::Duration::from_millis(ms));
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::sleep;
-
- #[test]
- fn sleep_returns() {
- futures::executor::block_on(sleep(0));
- }
-}
diff --git a/crates/app-lib/src/storage.rs b/crates/app-lib/src/storage.rs
@@ -1,65 +0,0 @@
-#![forbid(unsafe_code)]
-
-use crate::path::{normalize_path, sanitize_path, trim_slashes};
-
-pub fn fmt_id_from_path(pathname: &str, raw_id: Option<&str>) -> String {
- let trimmed = trim_slashes(pathname);
- let prefix = normalize_path(&trimmed);
- let suffix = raw_id
- .map(|id| format!("-{}", sanitize_path(id)))
- .unwrap_or_default();
- format!("*{prefix}{suffix}")
-}
-
-pub fn fmt_id(raw_id: Option<&str>) -> Option<String> {
- #[cfg(target_arch = "wasm32")]
- {
- let window = web_sys::window()?;
- let location = window.location();
- let pathname = location.pathname().ok()?;
- Some(fmt_id_from_path(&pathname, raw_id))
- }
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = raw_id;
- None
- }
-}
-
-pub fn build_storage_key_with_prefix(prefix: &str, raw_id: &str, base_prefix: &str) -> String {
- let mut output = format!("{prefix}-{}", sanitize_path(raw_id));
- let base_prefix = normalize_path(&trim_slashes(base_prefix));
- if base_prefix.is_empty() {
- return output;
- }
- let base = format!("*{base_prefix}");
- let base_with_dash = format!("{base}-");
- if output.starts_with(&base_with_dash) {
- output.replace_range(..base_with_dash.len(), "*");
- } else if output.starts_with(&base) {
- output.replace_range(..base.len(), "*");
- }
- output
-}
-
-pub fn build_storage_key(raw_id: &str, base_prefix: &str) -> Option<String> {
- let prefix = fmt_id(None)?;
- Some(build_storage_key_with_prefix(&prefix, raw_id, base_prefix))
-}
-
-#[cfg(test)]
-mod tests {
- use super::{build_storage_key_with_prefix, fmt_id_from_path};
-
- #[test]
- fn fmt_id_from_path_formats_prefix() {
- assert_eq!(fmt_id_from_path("/app/home", None), "*app-home");
- assert_eq!(fmt_id_from_path("/app/home", Some("id")), "*app-home-id");
- }
-
- #[test]
- fn build_storage_key_with_prefix_replaces_base_prefix() {
- let key = build_storage_key_with_prefix("*app-home", "raw", "/app");
- assert_eq!(key, "*home-raw");
- }
-}
diff --git a/crates/app-lib/src/symbols.rs b/crates/app-lib/src/symbols.rs
@@ -1,54 +0,0 @@
-#![forbid(unsafe_code)]
-
-use regex::Regex;
-
-pub const SYMBOL_BULLET: &str = "\u{2022}";
-pub const SYMBOL_DASH: &str = "\u{2014}";
-pub const SYMBOL_UP: &str = "\u{2191}";
-pub const SYMBOL_DOWN: &str = "\u{2193}";
-pub const SYMBOL_PERCENT: &str = "%";
-
-pub fn fmt_cl(classes: Option<&str>) -> String {
- classes.unwrap_or("").to_string()
-}
-
-pub fn value_constrain(regex_charset: &Regex, value: &str) -> String {
- let mut output = String::with_capacity(value.len());
- let mut buf = [0u8; 4];
- for ch in value.chars() {
- let encoded = ch.encode_utf8(&mut buf);
- if regex_charset.is_match(encoded) {
- output.push(ch);
- }
- }
- output
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- fmt_cl, value_constrain, SYMBOL_BULLET, SYMBOL_DASH, SYMBOL_DOWN, SYMBOL_PERCENT,
- SYMBOL_UP,
- };
-
- #[test]
- fn symbols_match_expected_values() {
- assert_eq!(SYMBOL_BULLET, "\u{2022}");
- assert_eq!(SYMBOL_DASH, "\u{2014}");
- assert_eq!(SYMBOL_UP, "\u{2191}");
- assert_eq!(SYMBOL_DOWN, "\u{2193}");
- assert_eq!(SYMBOL_PERCENT, "%");
- }
-
- #[test]
- fn fmt_cl_handles_none() {
- assert_eq!(fmt_cl(None), "");
- assert_eq!(fmt_cl(Some("a b")), "a b");
- }
-
- #[test]
- fn value_constrain_filters_chars() {
- let regex = regex::Regex::new("[0-9]").expect("regex");
- assert_eq!(value_constrain(®ex, "a1b2c"), "12");
- }
-}
diff --git a/crates/app-lib/src/theme.rs b/crates/app-lib/src/theme.rs
@@ -1,127 +0,0 @@
-#![forbid(unsafe_code)]
-
-use std::fmt;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum ThemeMode {
- Light,
- Dark,
-}
-
-impl ThemeMode {
- pub const fn as_str(self) -> &'static str {
- match self {
- ThemeMode::Light => "light",
- ThemeMode::Dark => "dark",
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum ThemeLayer {
- Layer0,
- Layer1,
- Layer2,
-}
-
-impl ThemeLayer {
- pub const fn as_u8(self) -> u8 {
- match self {
- ThemeLayer::Layer0 => 0,
- ThemeLayer::Layer1 => 1,
- ThemeLayer::Layer2 => 2,
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum ThemeError {
- WindowUnavailable,
- DocumentUnavailable,
- ElementUnavailable,
-}
-
-impl fmt::Display for ThemeError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- ThemeError::WindowUnavailable => f.write_str("error.app.theme.window_unavailable"),
- ThemeError::DocumentUnavailable => f.write_str("error.app.theme.document_unavailable"),
- ThemeError::ElementUnavailable => f.write_str("error.app.theme.element_unavailable"),
- }
- }
-}
-
-impl std::error::Error for ThemeError {}
-
-pub fn parse_layer(layer: Option<i32>, fallback: Option<ThemeLayer>) -> ThemeLayer {
- match layer {
- Some(0) => ThemeLayer::Layer0,
- Some(1) => ThemeLayer::Layer1,
- Some(2) => ThemeLayer::Layer2,
- _ => fallback.unwrap_or(ThemeLayer::Layer0),
- }
-}
-
-pub fn get_system_theme(fallback: ThemeMode) -> ThemeMode {
- #[cfg(target_arch = "wasm32")]
- {
- if let Some(window) = web_sys::window() {
- if let Ok(Some(query)) = window.match_media("(prefers-color-scheme: dark)") {
- if query.matches() {
- return ThemeMode::Dark;
- }
- return ThemeMode::Light;
- }
- }
- fallback
- }
- #[cfg(not(target_arch = "wasm32"))]
- {
- fallback
- }
-}
-
-pub fn theme_set(theme_key: &str, color_mode: ThemeMode) -> Result<(), ThemeError> {
- #[cfg(target_arch = "wasm32")]
- {
- let window = web_sys::window().ok_or(ThemeError::WindowUnavailable)?;
- let document = window.document().ok_or(ThemeError::DocumentUnavailable)?;
- let element = document.document_element().ok_or(ThemeError::ElementUnavailable)?;
- let value = format!("{theme_key}_{}", color_mode.as_str());
- element
- .set_attribute("data-theme", &value)
- .map_err(|_| ThemeError::ElementUnavailable)?;
- Ok(())
- }
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = theme_key;
- let _ = color_mode;
- Err(ThemeError::WindowUnavailable)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{get_system_theme, parse_layer, theme_set, ThemeError, ThemeLayer, ThemeMode};
-
- #[test]
- fn parse_layer_handles_fallback() {
- assert_eq!(parse_layer(Some(2), None).as_u8(), 2);
- assert_eq!(
- parse_layer(Some(4), Some(ThemeLayer::Layer1)),
- ThemeLayer::Layer1
- );
- }
-
- #[test]
- fn get_system_theme_uses_fallback() {
- assert_eq!(get_system_theme(ThemeMode::Dark), ThemeMode::Dark);
- }
-
- #[test]
- fn theme_set_errors_on_non_wasm() {
- let err = theme_set("radroots", ThemeMode::Light).expect_err("non-wasm error");
- assert_eq!(err, ThemeError::WindowUnavailable);
- }
-}
diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml
@@ -0,0 +1,30 @@
+[package]
+name = "radroots-app"
+authors.workspace = true
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+rust-version.workspace = true
+repository.workspace = true
+homepage.workspace = true
+description = "Rad Roots app"
+publish = false
+
+[lints]
+workspace = true
+
+[dependencies]
+eframe.workspace = true
+egui.workspace = true
+
+[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
+wgpu = { workspace = true, features = ["metal", "wgsl"] }
+
+[target.'cfg(target_os = "macos")'.dependencies]
+objc2-foundation = { workspace = true, features = ["NSProcessInfo", "NSString"] }
+
+[target.'cfg(target_os = "windows")'.dependencies]
+wgpu = { workspace = true, features = ["dx12", "wgsl"] }
+
+[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
+wgpu = { workspace = true, features = ["vulkan", "gles", "wgsl"] }
diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs
@@ -0,0 +1,51 @@
+#![forbid(unsafe_code)]
+#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+
+use eframe::egui;
+
+const APP_NAME: &str = "Rad Roots";
+
+#[cfg(target_os = "macos")]
+fn set_macos_app_name() {
+ use objc2_foundation::{NSProcessInfo, NSString};
+
+ let process_info = NSProcessInfo::processInfo();
+ let process_name = NSString::from_str(APP_NAME);
+ process_info.setProcessName(&process_name);
+}
+
+#[cfg(not(target_os = "macos"))]
+fn set_macos_app_name() {}
+
+#[derive(Default)]
+struct RadrootsApp;
+
+impl eframe::App for RadrootsApp {
+ fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
+ egui::CentralPanel::default().show(ctx, |ui| {
+ ui.vertical_centered(|ui| {
+ ui.add_space(48.0);
+ ui.heading(APP_NAME);
+ ui.add_space(12.0);
+ ui.label("radroots app");
+ });
+ });
+ }
+}
+
+fn main() -> eframe::Result<()> {
+ set_macos_app_name();
+
+ let options = eframe::NativeOptions {
+ viewport: egui::ViewportBuilder::default()
+ .with_inner_size([1280.0, 820.0])
+ .with_min_inner_size([480.0, 320.0]),
+ ..Default::default()
+ };
+
+ eframe::run_native(
+ APP_NAME,
+ options,
+ Box::new(|_cc| Ok(Box::new(RadrootsApp))),
+ )
+}
diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml
@@ -1,39 +0,0 @@
-[package]
-name = "radroots-app-core"
-authors = ["Radroots Authors"]
-version.workspace = true
-edition.workspace = true
-license.workspace = true
-rust-version.workspace = true
-
-[lib]
-crate-type = ["rlib"]
-
-[dependencies]
-async-trait = { workspace = true }
-serde = { workspace = true }
-serde_json = { workspace = true }
-getrandom = { workspace = true }
-base64 = { workspace = true }
-radroots-nostr = { workspace = true, features = ["client", "events"] }
-url = { workspace = true }
-chrono = { workspace = true }
-hex = { workspace = true }
-sha2 = { workspace = true }
-
-[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
-rusqlite = { workspace = true, features = ["bundled", "serialize"] }
-radroots-sql-core = { workspace = true, features = ["native"] }
-radroots-replica-db = { workspace = true }
-radroots-replica-db-schema = { workspace = true }
-radroots-replica-sync = { workspace = true }
-
-[target.'cfg(target_arch = "wasm32")'.dependencies]
-js-sys = { workspace = true }
-web-sys = { workspace = true }
-wasm-bindgen-futures = { workspace = true }
-wasm-bindgen = { workspace = true }
-serde-wasm-bindgen = { workspace = true }
-
-[dev-dependencies]
-futures = "0.3"
diff --git a/crates/core/src/backup/bundle.rs b/crates/core/src/backup/bundle.rs
@@ -1,439 +0,0 @@
-use std::fmt;
-
-use crate::crypto::{
- RadrootsClientCryptoError,
- RadrootsClientCryptoRegistryExport,
- RadrootsClientKeyMaterialProvider,
- RadrootsClientWebCryptoService,
-};
-
-use super::{
- backup_bundle_decode,
- backup_bundle_encode,
- RadrootsClientBackupBundle,
- RadrootsClientBackupBundleManifest,
- RadrootsClientBackupBundlePayload,
- RadrootsClientBackupDatastoreStore,
- RadrootsClientBackupError,
- RadrootsClientBackupKeystoreStore,
- RadrootsClientBackupSqlStore,
- RadrootsClientBackupStoreRef,
- RADROOTS_CLIENT_BACKUP_BUNDLE_VERSION,
-};
-
-#[derive(Debug)]
-pub enum RadrootsClientBackupBundleError<SqlErr, KeystoreErr, DatastoreErr> {
- Backup(RadrootsClientBackupError),
- Crypto(RadrootsClientCryptoError),
- Sql(SqlErr),
- Keystore(KeystoreErr),
- Datastore(DatastoreErr),
-}
-
-impl<SqlErr: fmt::Display, KeystoreErr: fmt::Display, DatastoreErr: fmt::Display> fmt::Display
- for RadrootsClientBackupBundleError<SqlErr, KeystoreErr, DatastoreErr>
-{
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- RadrootsClientBackupBundleError::Backup(err) => err.fmt(f),
- RadrootsClientBackupBundleError::Crypto(err) => err.fmt(f),
- RadrootsClientBackupBundleError::Sql(err) => err.fmt(f),
- RadrootsClientBackupBundleError::Keystore(err) => err.fmt(f),
- RadrootsClientBackupBundleError::Datastore(err) => err.fmt(f),
- }
- }
-}
-
-impl<SqlErr, KeystoreErr, DatastoreErr> std::error::Error
- for RadrootsClientBackupBundleError<SqlErr, KeystoreErr, DatastoreErr>
-where
- SqlErr: std::error::Error + 'static,
- KeystoreErr: std::error::Error + 'static,
- DatastoreErr: std::error::Error + 'static,
-{
-}
-
-pub type RadrootsClientBackupBundleResult<T, SqlErr, KeystoreErr, DatastoreErr> =
- Result<T, RadrootsClientBackupBundleError<SqlErr, KeystoreErr, DatastoreErr>>;
-
-pub struct RadrootsClientBackupBundleBuildOpts<'a, SqlStore, KeystoreStore, DatastoreStore>
-where
- SqlStore: RadrootsClientBackupSqlStore + ?Sized,
- KeystoreStore: RadrootsClientBackupKeystoreStore + ?Sized,
- DatastoreStore: RadrootsClientBackupDatastoreStore + ?Sized,
-{
- pub sql_store: Option<&'a SqlStore>,
- pub keystore_store: Option<&'a KeystoreStore>,
- pub datastore_store: Option<&'a DatastoreStore>,
- pub app_version: Option<&'a str>,
- pub crypto_service: Option<&'a dyn RadrootsClientWebCryptoService>,
- pub key_material_provider: Option<&'a dyn RadrootsClientKeyMaterialProvider>,
-}
-
-pub struct RadrootsClientBackupBundleImportOpts<'a, SqlStore, KeystoreStore, DatastoreStore>
-where
- SqlStore: RadrootsClientBackupSqlStore + ?Sized,
- KeystoreStore: RadrootsClientBackupKeystoreStore + ?Sized,
- DatastoreStore: RadrootsClientBackupDatastoreStore + ?Sized,
-{
- pub sql_store: Option<&'a SqlStore>,
- pub keystore_store: Option<&'a KeystoreStore>,
- pub datastore_store: Option<&'a DatastoreStore>,
- pub crypto_service: Option<&'a dyn RadrootsClientWebCryptoService>,
- pub key_material_provider: Option<&'a dyn RadrootsClientKeyMaterialProvider>,
- pub import_registry: bool,
-}
-
-fn now_millis() -> u64 {
- #[cfg(target_arch = "wasm32")]
- {
- return js_sys::Date::now() as u64;
- }
- #[cfg(not(target_arch = "wasm32"))]
- {
- use std::time::{SystemTime, UNIX_EPOCH};
- return SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map(|duration| duration.as_millis() as u64)
- .unwrap_or(0);
- }
-}
-
-async fn collect_payloads<SqlStore, KeystoreStore, DatastoreStore>(
- opts: &RadrootsClientBackupBundleBuildOpts<'_, SqlStore, KeystoreStore, DatastoreStore>,
-) -> RadrootsClientBackupBundleResult<
- Vec<RadrootsClientBackupBundlePayload>,
- SqlStore::Error,
- KeystoreStore::Error,
- DatastoreStore::Error,
->
-where
- SqlStore: RadrootsClientBackupSqlStore + ?Sized,
- KeystoreStore: RadrootsClientBackupKeystoreStore + ?Sized,
- DatastoreStore: RadrootsClientBackupDatastoreStore + ?Sized,
-{
- let mut payloads = Vec::new();
- if let Some(store) = opts.sql_store {
- let data = store
- .export_backup()
- .await
- .map_err(RadrootsClientBackupBundleError::Sql)?;
- payloads.push(RadrootsClientBackupBundlePayload::Sql {
- store_id: store.store_id().to_string(),
- data,
- });
- }
- if let Some(store) = opts.keystore_store {
- let data = store
- .export_backup()
- .await
- .map_err(RadrootsClientBackupBundleError::Keystore)?;
- payloads.push(RadrootsClientBackupBundlePayload::Keystore {
- store_id: store.store_id().to_string(),
- data,
- });
- }
- if let Some(store) = opts.datastore_store {
- let data = store
- .export_backup()
- .await
- .map_err(RadrootsClientBackupBundleError::Datastore)?;
- payloads.push(RadrootsClientBackupBundlePayload::Datastore {
- store_id: store.store_id().to_string(),
- data,
- });
- }
- Ok(payloads)
-}
-
-pub async fn backup_bundle_build<SqlStore, KeystoreStore, DatastoreStore>(
- opts: &RadrootsClientBackupBundleBuildOpts<'_, SqlStore, KeystoreStore, DatastoreStore>,
-) -> RadrootsClientBackupBundleResult<
- RadrootsClientBackupBundle,
- SqlStore::Error,
- KeystoreStore::Error,
- DatastoreStore::Error,
->
-where
- SqlStore: RadrootsClientBackupSqlStore + ?Sized,
- KeystoreStore: RadrootsClientBackupKeystoreStore + ?Sized,
- DatastoreStore: RadrootsClientBackupDatastoreStore + ?Sized,
-{
- let payloads = collect_payloads(opts).await?;
- let stores = payloads
- .iter()
- .map(|payload| RadrootsClientBackupStoreRef {
- store_id: payload.store_id().to_string(),
- store_type: payload.store_type(),
- })
- .collect();
- let crypto_registry = match opts.crypto_service {
- Some(crypto) => crypto
- .export_registry()
- .await
- .map_err(RadrootsClientBackupBundleError::Crypto)?,
- None => RadrootsClientCryptoRegistryExport {
- stores: Vec::new(),
- keys: Vec::new(),
- },
- };
- Ok(RadrootsClientBackupBundle {
- manifest: RadrootsClientBackupBundleManifest {
- version: RADROOTS_CLIENT_BACKUP_BUNDLE_VERSION,
- created_at: now_millis(),
- app_version: opts.app_version.map(str::to_string),
- stores,
- crypto_registry,
- },
- payloads,
- })
-}
-
-pub async fn backup_bundle_export<SqlStore, KeystoreStore, DatastoreStore>(
- opts: &RadrootsClientBackupBundleBuildOpts<'_, SqlStore, KeystoreStore, DatastoreStore>,
-) -> RadrootsClientBackupBundleResult<
- Vec<u8>,
- SqlStore::Error,
- KeystoreStore::Error,
- DatastoreStore::Error,
->
-where
- SqlStore: RadrootsClientBackupSqlStore + ?Sized,
- KeystoreStore: RadrootsClientBackupKeystoreStore + ?Sized,
- DatastoreStore: RadrootsClientBackupDatastoreStore + ?Sized,
-{
- let provider = opts
- .key_material_provider
- .ok_or(RadrootsClientBackupBundleError::Backup(
- RadrootsClientBackupError::ProviderMissing,
- ))?;
- let bundle = backup_bundle_build(opts).await?;
- backup_bundle_encode(&bundle, provider)
- .await
- .map_err(RadrootsClientBackupBundleError::Backup)
-}
-
-pub async fn backup_bundle_import<SqlStore, KeystoreStore, DatastoreStore>(
- blob: &[u8],
- opts: &RadrootsClientBackupBundleImportOpts<'_, SqlStore, KeystoreStore, DatastoreStore>,
-) -> RadrootsClientBackupBundleResult<
- RadrootsClientBackupBundle,
- SqlStore::Error,
- KeystoreStore::Error,
- DatastoreStore::Error,
->
-where
- SqlStore: RadrootsClientBackupSqlStore + ?Sized,
- KeystoreStore: RadrootsClientBackupKeystoreStore + ?Sized,
- DatastoreStore: RadrootsClientBackupDatastoreStore + ?Sized,
-{
- let provider = opts
- .key_material_provider
- .ok_or(RadrootsClientBackupBundleError::Backup(
- RadrootsClientBackupError::ProviderMissing,
- ))?;
- let bundle = backup_bundle_decode(blob, provider)
- .await
- .map_err(RadrootsClientBackupBundleError::Backup)?;
- if opts.import_registry {
- if let Some(crypto) = opts.crypto_service {
- crypto
- .import_registry(bundle.manifest.crypto_registry.clone())
- .await
- .map_err(RadrootsClientBackupBundleError::Crypto)?;
- }
- }
- for payload in &bundle.payloads {
- match payload {
- RadrootsClientBackupBundlePayload::Sql { store_id, data } => {
- if let Some(store) = opts.sql_store {
- if store.store_id() == store_id {
- store
- .import_backup(data.clone())
- .await
- .map_err(RadrootsClientBackupBundleError::Sql)?;
- }
- }
- }
- RadrootsClientBackupBundlePayload::Keystore { store_id, data } => {
- if let Some(store) = opts.keystore_store {
- if store.store_id() == store_id {
- store
- .import_backup(data.clone())
- .await
- .map_err(RadrootsClientBackupBundleError::Keystore)?;
- }
- }
- }
- RadrootsClientBackupBundlePayload::Datastore { store_id, data } => {
- if let Some(store) = opts.datastore_store {
- if store.store_id() == store_id {
- store
- .import_backup(data.clone())
- .await
- .map_err(RadrootsClientBackupBundleError::Datastore)?;
- }
- }
- }
- }
- }
- Ok(bundle)
-}
-
-#[cfg(test)]
-mod tests {
- use async_trait::async_trait;
-
- use super::{
- backup_bundle_build,
- backup_bundle_export,
- RadrootsClientBackupBundleBuildOpts,
- RadrootsClientBackupBundleError,
- };
- use crate::backup::{
- RadrootsClientBackupBundlePayload,
- RadrootsClientBackupDatastorePayload,
- RadrootsClientBackupDatastoreStore,
- RadrootsClientBackupError,
- RadrootsClientBackupKeystorePayload,
- RadrootsClientBackupKeystoreStore,
- RadrootsClientBackupSqlPayload,
- RadrootsClientBackupSqlStore,
- };
-
- #[derive(Debug)]
- struct StubError;
-
- impl std::fmt::Display for StubError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_str("stub")
- }
- }
-
- impl std::error::Error for StubError {}
-
- struct StubSqlStore;
- struct StubKeystoreStore;
- struct StubDatastoreStore;
-
- #[async_trait(?Send)]
- impl RadrootsClientBackupSqlStore for StubSqlStore {
- type Error = StubError;
-
- async fn export_backup(&self) -> Result<RadrootsClientBackupSqlPayload, Self::Error> {
- Ok(RadrootsClientBackupSqlPayload {
- bytes_b64: "sql".to_string(),
- })
- }
-
- async fn import_backup(
- &self,
- _payload: RadrootsClientBackupSqlPayload,
- ) -> Result<(), Self::Error> {
- Ok(())
- }
-
- fn store_id(&self) -> &str {
- "sql-store"
- }
- }
-
- #[async_trait(?Send)]
- impl RadrootsClientBackupKeystoreStore for StubKeystoreStore {
- type Error = StubError;
-
- async fn export_backup(
- &self,
- ) -> Result<RadrootsClientBackupKeystorePayload, Self::Error> {
- Ok(RadrootsClientBackupKeystorePayload {
- entries: Vec::new(),
- })
- }
-
- async fn import_backup(
- &self,
- _payload: RadrootsClientBackupKeystorePayload,
- ) -> Result<(), Self::Error> {
- Ok(())
- }
-
- fn store_id(&self) -> &str {
- "keystore"
- }
- }
-
- #[async_trait(?Send)]
- impl RadrootsClientBackupDatastoreStore for StubDatastoreStore {
- type Error = StubError;
-
- async fn export_backup(
- &self,
- ) -> Result<RadrootsClientBackupDatastorePayload, Self::Error> {
- Ok(RadrootsClientBackupDatastorePayload {
- entries: Vec::new(),
- })
- }
-
- async fn import_backup(
- &self,
- _payload: RadrootsClientBackupDatastorePayload,
- ) -> Result<(), Self::Error> {
- Ok(())
- }
-
- fn store_id(&self) -> &str {
- "datastore"
- }
- }
-
- #[test]
- fn build_collects_payloads() {
- let sql = StubSqlStore;
- let keystore = StubKeystoreStore;
- let datastore = StubDatastoreStore;
- let opts = RadrootsClientBackupBundleBuildOpts {
- sql_store: Some(&sql),
- keystore_store: Some(&keystore),
- datastore_store: Some(&datastore),
- app_version: Some("1.2.3"),
- crypto_service: None,
- key_material_provider: None,
- };
- let bundle = futures::executor::block_on(backup_bundle_build(&opts))
- .expect("bundle");
- assert_eq!(bundle.payloads.len(), 3);
- assert_eq!(bundle.manifest.stores.len(), 3);
- assert_eq!(bundle.manifest.app_version.as_deref(), Some("1.2.3"));
- assert!(bundle.manifest.crypto_registry.stores.is_empty());
- assert!(bundle.manifest.crypto_registry.keys.is_empty());
- assert!(matches!(
- bundle.payloads[0],
- RadrootsClientBackupBundlePayload::Sql { .. }
- ));
- }
-
- #[test]
- fn export_requires_provider() {
- let sql = StubSqlStore;
- let opts: RadrootsClientBackupBundleBuildOpts<
- StubSqlStore,
- StubKeystoreStore,
- StubDatastoreStore,
- > = RadrootsClientBackupBundleBuildOpts {
- sql_store: Some(&sql),
- keystore_store: None,
- datastore_store: None,
- app_version: None,
- crypto_service: None,
- key_material_provider: None,
- };
- let err = futures::executor::block_on(backup_bundle_export(&opts))
- .expect_err("missing provider");
- match err {
- RadrootsClientBackupBundleError::Backup(
- RadrootsClientBackupError::ProviderMissing,
- ) => {}
- other => panic!("unexpected error: {other}"),
- }
- }
-}
diff --git a/crates/core/src/backup/codec.rs b/crates/core/src/backup/codec.rs
@@ -1,201 +0,0 @@
-use base64::engine::general_purpose::STANDARD;
-use base64::Engine as _;
-
-#[cfg(target_arch = "wasm32")]
-use crate::crypto::RadrootsClientCryptoError;
-#[cfg(target_arch = "wasm32")]
-use crate::crypto::{
- crypto_kdf_iterations_default,
- crypto_kdf_salt_create,
-};
-use crate::crypto::RadrootsClientKeyMaterialProvider;
-
-use super::{
- RadrootsClientBackupBundle,
- RadrootsClientBackupError,
-};
-#[cfg(target_arch = "wasm32")]
-use super::RadrootsClientBackupBundleEnvelope;
-
-pub fn backup_bytes_to_b64(bytes: &[u8]) -> Result<String, RadrootsClientBackupError> {
- Ok(STANDARD.encode(bytes))
-}
-
-pub fn backup_b64_to_bytes(value: &str) -> Result<Vec<u8>, RadrootsClientBackupError> {
- STANDARD
- .decode(value)
- .map_err(|_| RadrootsClientBackupError::DecodeFailure)
-}
-
-#[cfg(target_arch = "wasm32")]
-fn map_crypto_error(
- err: RadrootsClientCryptoError,
- fallback: RadrootsClientBackupError,
-) -> RadrootsClientBackupError {
- match err {
- RadrootsClientCryptoError::CryptoUndefined => {
- RadrootsClientBackupError::CryptoUndefined
- }
- _ => fallback,
- }
-}
-
-#[cfg(target_arch = "wasm32")]
-async fn encrypt_bytes(
- key: &web_sys::CryptoKey,
- iv: &[u8],
- plaintext: &[u8],
-) -> Result<Vec<u8>, RadrootsClientBackupError> {
- let window = web_sys::window().ok_or(RadrootsClientBackupError::CryptoUndefined)?;
- let crypto = window
- .crypto()
- .map_err(|_| RadrootsClientBackupError::CryptoUndefined)?;
- let subtle = crypto.subtle();
- let algo = js_sys::Object::new();
- js_sys::Reflect::set(&algo, &"name".into(), &"AES-GCM".into())
- .map_err(|_| RadrootsClientBackupError::EncodeFailure)?;
- let iv_array = js_sys::Uint8Array::from(iv);
- js_sys::Reflect::set(&algo, &"iv".into(), &iv_array.into())
- .map_err(|_| RadrootsClientBackupError::EncodeFailure)?;
- let promise = subtle
- .encrypt_with_object_and_u8_array(&algo, key, plaintext)
- .map_err(|_| RadrootsClientBackupError::EncodeFailure)?;
- let value = wasm_bindgen_futures::JsFuture::from(promise)
- .await
- .map_err(|_| RadrootsClientBackupError::EncodeFailure)?;
- let array = js_sys::Uint8Array::new(&value);
- let mut out = vec![0u8; array.length() as usize];
- array.copy_to(&mut out);
- Ok(out)
-}
-
-#[cfg(target_arch = "wasm32")]
-async fn decrypt_bytes(
- key: &web_sys::CryptoKey,
- iv: &[u8],
- ciphertext: &[u8],
-) -> Result<Vec<u8>, RadrootsClientBackupError> {
- let window = web_sys::window().ok_or(RadrootsClientBackupError::CryptoUndefined)?;
- let crypto = window
- .crypto()
- .map_err(|_| RadrootsClientBackupError::CryptoUndefined)?;
- let subtle = crypto.subtle();
- let algo = js_sys::Object::new();
- js_sys::Reflect::set(&algo, &"name".into(), &"AES-GCM".into())
- .map_err(|_| RadrootsClientBackupError::DecodeFailure)?;
- let iv_array = js_sys::Uint8Array::from(iv);
- js_sys::Reflect::set(&algo, &"iv".into(), &iv_array.into())
- .map_err(|_| RadrootsClientBackupError::DecodeFailure)?;
- let promise = subtle
- .decrypt_with_object_and_u8_array(&algo, key, ciphertext)
- .map_err(|_| RadrootsClientBackupError::DecodeFailure)?;
- let value = wasm_bindgen_futures::JsFuture::from(promise)
- .await
- .map_err(|_| RadrootsClientBackupError::DecodeFailure)?;
- let array = js_sys::Uint8Array::new(&value);
- let mut out = vec![0u8; array.length() as usize];
- array.copy_to(&mut out);
- Ok(out)
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn backup_bundle_encode(
- bundle: &RadrootsClientBackupBundle,
- provider: &dyn RadrootsClientKeyMaterialProvider,
-) -> Result<Vec<u8>, RadrootsClientBackupError> {
- let json = serde_json::to_string(bundle)
- .map_err(|_| RadrootsClientBackupError::EncodeFailure)?;
- let plaintext = json.into_bytes();
- let salt = crypto_kdf_salt_create(16)
- .map_err(|e| map_crypto_error(e, RadrootsClientBackupError::EncodeFailure))?;
- let iterations = crypto_kdf_iterations_default();
- let mut material = provider
- .get_key_material()
- .await
- .map_err(|e| map_crypto_error(e, RadrootsClientBackupError::EncodeFailure))?;
- let kek = crate::crypto::crypto_kdf_derive_kek(&material, &salt, iterations)
- .await
- .map_err(|e| map_crypto_error(e, RadrootsClientBackupError::EncodeFailure))?;
- material.fill(0);
- let mut iv = vec![0u8; 12];
- crate::crypto::random::fill_random(&mut iv)
- .map_err(|e| map_crypto_error(e, RadrootsClientBackupError::EncodeFailure))?;
- let ciphertext = encrypt_bytes(&kek, &iv, &plaintext).await?;
- let envelope = RadrootsClientBackupBundleEnvelope {
- version: 1,
- created_at: js_sys::Date::now() as u64,
- kdf_salt_b64: backup_bytes_to_b64(&salt)?,
- kdf_iterations: iterations,
- iv_b64: backup_bytes_to_b64(&iv)?,
- ciphertext_b64: backup_bytes_to_b64(&ciphertext)?,
- };
- let encoded = serde_json::to_string(&envelope)
- .map_err(|_| RadrootsClientBackupError::EncodeFailure)?;
- Ok(encoded.into_bytes())
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub async fn backup_bundle_encode(
- _bundle: &RadrootsClientBackupBundle,
- _provider: &dyn RadrootsClientKeyMaterialProvider,
-) -> Result<Vec<u8>, RadrootsClientBackupError> {
- Err(RadrootsClientBackupError::CryptoUndefined)
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn backup_bundle_decode(
- blob: &[u8],
- provider: &dyn RadrootsClientKeyMaterialProvider,
-) -> Result<RadrootsClientBackupBundle, RadrootsClientBackupError> {
- let json = std::str::from_utf8(blob)
- .map_err(|_| RadrootsClientBackupError::DecodeFailure)?;
- let envelope: RadrootsClientBackupBundleEnvelope = serde_json::from_str(json)
- .map_err(|_| RadrootsClientBackupError::InvalidBundle)?;
- let salt = backup_b64_to_bytes(&envelope.kdf_salt_b64)?;
- let iv = backup_b64_to_bytes(&envelope.iv_b64)?;
- let ciphertext = backup_b64_to_bytes(&envelope.ciphertext_b64)?;
- let mut material = provider
- .get_key_material()
- .await
- .map_err(|e| map_crypto_error(e, RadrootsClientBackupError::DecodeFailure))?;
- let kek = crate::crypto::crypto_kdf_derive_kek(
- &material,
- &salt,
- envelope.kdf_iterations,
- )
- .await
- .map_err(|e| map_crypto_error(e, RadrootsClientBackupError::DecodeFailure))?;
- material.fill(0);
- let plaintext = decrypt_bytes(&kek, &iv, &ciphertext).await?;
- let payload = std::str::from_utf8(&plaintext)
- .map_err(|_| RadrootsClientBackupError::DecodeFailure)?;
- serde_json::from_str(payload)
- .map_err(|_| RadrootsClientBackupError::InvalidBundle)
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub async fn backup_bundle_decode(
- _blob: &[u8],
- _provider: &dyn RadrootsClientKeyMaterialProvider,
-) -> Result<RadrootsClientBackupBundle, RadrootsClientBackupError> {
- Err(RadrootsClientBackupError::CryptoUndefined)
-}
-
-#[cfg(test)]
-mod tests {
- use super::{backup_b64_to_bytes, backup_bytes_to_b64};
-
- #[test]
- fn base64_roundtrip() {
- let data = b"radroots";
- let encoded = backup_bytes_to_b64(data).expect("encode");
- let decoded = backup_b64_to_bytes(&encoded).expect("decode");
- assert_eq!(decoded, data);
- }
-
- #[test]
- fn base64_decode_rejects_invalid() {
- let err = backup_b64_to_bytes("not-base64").expect_err("invalid");
- assert_eq!(err, super::RadrootsClientBackupError::DecodeFailure);
- }
-}
diff --git a/crates/core/src/backup/error.rs b/crates/core/src/backup/error.rs
@@ -1,67 +0,0 @@
-use std::fmt;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsClientBackupError {
- CryptoUndefined,
- InvalidBundle,
- DecodeFailure,
- EncodeFailure,
- ProviderMissing,
-}
-
-pub type RadrootsClientBackupErrorMessage = &'static str;
-
-impl RadrootsClientBackupError {
- pub const fn message(self) -> RadrootsClientBackupErrorMessage {
- match self {
- RadrootsClientBackupError::CryptoUndefined => "error.client.backup.crypto_undefined",
- RadrootsClientBackupError::InvalidBundle => "error.client.backup.invalid_bundle",
- RadrootsClientBackupError::DecodeFailure => "error.client.backup.decode_failure",
- RadrootsClientBackupError::EncodeFailure => "error.client.backup.encode_failure",
- RadrootsClientBackupError::ProviderMissing => "error.client.backup.provider_missing",
- }
- }
-}
-
-impl fmt::Display for RadrootsClientBackupError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.write_str(self.message())
- }
-}
-
-impl std::error::Error for RadrootsClientBackupError {}
-
-#[cfg(test)]
-mod tests {
- use super::RadrootsClientBackupError;
-
- #[test]
- fn message_matches_spec() {
- let cases = [
- (
- RadrootsClientBackupError::CryptoUndefined,
- "error.client.backup.crypto_undefined",
- ),
- (
- RadrootsClientBackupError::InvalidBundle,
- "error.client.backup.invalid_bundle",
- ),
- (
- RadrootsClientBackupError::DecodeFailure,
- "error.client.backup.decode_failure",
- ),
- (
- RadrootsClientBackupError::EncodeFailure,
- "error.client.backup.encode_failure",
- ),
- (
- RadrootsClientBackupError::ProviderMissing,
- "error.client.backup.provider_missing",
- ),
- ];
- for (err, expected) in cases {
- assert_eq!(err.message(), expected);
- assert_eq!(err.to_string(), expected);
- }
- }
-}
diff --git a/crates/core/src/backup/mod.rs b/crates/core/src/backup/mod.rs
@@ -1,39 +0,0 @@
-pub mod error;
-pub mod types;
-pub mod codec;
-pub mod bundle;
-
-pub use error::{RadrootsClientBackupError, RadrootsClientBackupErrorMessage};
-pub use types::{
- RadrootsClientBackupBundle,
- RadrootsClientBackupBundleEnvelope,
- RadrootsClientBackupBundleManifest,
- RadrootsClientBackupBundlePayload,
- RadrootsClientBackupBundleStoreType,
- RadrootsClientBackupBundleVersion,
- RadrootsClientBackupDatastoreEntry,
- RadrootsClientBackupDatastorePayload,
- RadrootsClientBackupDatastoreStore,
- RadrootsClientBackupKeystoreEntry,
- RadrootsClientBackupKeystorePayload,
- RadrootsClientBackupKeystoreStore,
- RadrootsClientBackupSqlPayload,
- RadrootsClientBackupSqlStore,
- RadrootsClientBackupStoreRef,
- RADROOTS_CLIENT_BACKUP_BUNDLE_VERSION,
-};
-pub use bundle::{
- backup_bundle_build,
- backup_bundle_export,
- backup_bundle_import,
- RadrootsClientBackupBundleBuildOpts,
- RadrootsClientBackupBundleError,
- RadrootsClientBackupBundleImportOpts,
- RadrootsClientBackupBundleResult,
-};
-pub use codec::{
- backup_b64_to_bytes,
- backup_bytes_to_b64,
- backup_bundle_decode,
- backup_bundle_encode,
-};
diff --git a/crates/core/src/backup/types.rs b/crates/core/src/backup/types.rs
@@ -1,212 +0,0 @@
-use async_trait::async_trait;
-use serde::{Deserialize, Serialize};
-
-use crate::crypto::RadrootsClientCryptoRegistryExport;
-
-pub type RadrootsClientBackupBundleVersion = u8;
-
-pub const RADROOTS_CLIENT_BACKUP_BUNDLE_VERSION: RadrootsClientBackupBundleVersion = 1;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "lowercase")]
-pub enum RadrootsClientBackupBundleStoreType {
- Sql,
- Keystore,
- Datastore,
-}
-
-impl RadrootsClientBackupBundleStoreType {
- pub const fn as_str(self) -> &'static str {
- match self {
- RadrootsClientBackupBundleStoreType::Sql => "sql",
- RadrootsClientBackupBundleStoreType::Keystore => "keystore",
- RadrootsClientBackupBundleStoreType::Datastore => "datastore",
- }
- }
-
- pub fn parse(value: &str) -> Option<Self> {
- match value {
- "sql" => Some(RadrootsClientBackupBundleStoreType::Sql),
- "keystore" => Some(RadrootsClientBackupBundleStoreType::Keystore),
- "datastore" => Some(RadrootsClientBackupBundleStoreType::Datastore),
- _ => None,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsClientBackupSqlPayload {
- pub bytes_b64: String,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsClientBackupKeystoreEntry {
- pub key: String,
- pub value: String,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsClientBackupKeystorePayload {
- pub entries: Vec<RadrootsClientBackupKeystoreEntry>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsClientBackupDatastoreEntry {
- pub key: String,
- pub value: String,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsClientBackupDatastorePayload {
- pub entries: Vec<RadrootsClientBackupDatastoreEntry>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(tag = "store_type", rename_all = "lowercase")]
-pub enum RadrootsClientBackupBundlePayload {
- Sql {
- store_id: String,
- data: RadrootsClientBackupSqlPayload,
- },
- Keystore {
- store_id: String,
- data: RadrootsClientBackupKeystorePayload,
- },
- Datastore {
- store_id: String,
- data: RadrootsClientBackupDatastorePayload,
- },
-}
-
-impl RadrootsClientBackupBundlePayload {
- pub fn store_type(&self) -> RadrootsClientBackupBundleStoreType {
- match self {
- RadrootsClientBackupBundlePayload::Sql { .. } => {
- RadrootsClientBackupBundleStoreType::Sql
- }
- RadrootsClientBackupBundlePayload::Keystore { .. } => {
- RadrootsClientBackupBundleStoreType::Keystore
- }
- RadrootsClientBackupBundlePayload::Datastore { .. } => {
- RadrootsClientBackupBundleStoreType::Datastore
- }
- }
- }
-
- pub fn store_id(&self) -> &str {
- match self {
- RadrootsClientBackupBundlePayload::Sql { store_id, .. } => store_id,
- RadrootsClientBackupBundlePayload::Keystore { store_id, .. } => store_id,
- RadrootsClientBackupBundlePayload::Datastore { store_id, .. } => store_id,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsClientBackupStoreRef {
- pub store_id: String,
- pub store_type: RadrootsClientBackupBundleStoreType,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsClientBackupBundleManifest {
- pub version: RadrootsClientBackupBundleVersion,
- pub created_at: u64,
- pub app_version: Option<String>,
- pub stores: Vec<RadrootsClientBackupStoreRef>,
- pub crypto_registry: RadrootsClientCryptoRegistryExport,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsClientBackupBundle {
- pub manifest: RadrootsClientBackupBundleManifest,
- pub payloads: Vec<RadrootsClientBackupBundlePayload>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsClientBackupBundleEnvelope {
- pub version: u8,
- pub created_at: u64,
- pub kdf_salt_b64: String,
- pub kdf_iterations: u32,
- pub iv_b64: String,
- pub ciphertext_b64: String,
-}
-
-#[async_trait(?Send)]
-pub trait RadrootsClientBackupSqlStore {
- type Error;
-
- async fn export_backup(&self) -> Result<RadrootsClientBackupSqlPayload, Self::Error>;
- async fn import_backup(&self, payload: RadrootsClientBackupSqlPayload)
- -> Result<(), Self::Error>;
- fn store_id(&self) -> &str;
-}
-
-#[async_trait(?Send)]
-pub trait RadrootsClientBackupKeystoreStore {
- type Error;
-
- async fn export_backup(&self) -> Result<RadrootsClientBackupKeystorePayload, Self::Error>;
- async fn import_backup(
- &self,
- payload: RadrootsClientBackupKeystorePayload,
- ) -> Result<(), Self::Error>;
- fn store_id(&self) -> &str;
-}
-
-#[async_trait(?Send)]
-pub trait RadrootsClientBackupDatastoreStore {
- type Error;
-
- async fn export_backup(&self) -> Result<RadrootsClientBackupDatastorePayload, Self::Error>;
- async fn import_backup(
- &self,
- payload: RadrootsClientBackupDatastorePayload,
- ) -> Result<(), Self::Error>;
- fn store_id(&self) -> &str;
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- RadrootsClientBackupBundlePayload,
- RadrootsClientBackupBundleStoreType,
- };
-
- #[test]
- fn store_type_roundtrip() {
- let sql = RadrootsClientBackupBundleStoreType::Sql;
- let keystore = RadrootsClientBackupBundleStoreType::Keystore;
- let datastore = RadrootsClientBackupBundleStoreType::Datastore;
-
- assert_eq!(sql.as_str(), "sql");
- assert_eq!(keystore.as_str(), "keystore");
- assert_eq!(datastore.as_str(), "datastore");
- assert_eq!(RadrootsClientBackupBundleStoreType::parse("sql"), Some(sql));
- assert_eq!(
- RadrootsClientBackupBundleStoreType::parse("keystore"),
- Some(keystore)
- );
- assert_eq!(
- RadrootsClientBackupBundleStoreType::parse("datastore"),
- Some(datastore)
- );
- assert_eq!(RadrootsClientBackupBundleStoreType::parse("other"), None);
- }
-
- #[test]
- fn payload_store_helpers() {
- let payload = RadrootsClientBackupBundlePayload::Sql {
- store_id: "store".to_string(),
- data: super::RadrootsClientBackupSqlPayload {
- bytes_b64: "bytes".to_string(),
- },
- };
- assert_eq!(payload.store_id(), "store");
- assert_eq!(
- payload.store_type(),
- RadrootsClientBackupBundleStoreType::Sql
- );
- }
-}
diff --git a/crates/core/src/cipher/error.rs b/crates/core/src/cipher/error.rs
@@ -1,61 +0,0 @@
-use std::fmt;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsClientCipherError {
- IdbUndefined,
- CryptoUndefined,
- InvalidCiphertext,
- DecryptFailure,
-}
-
-pub type RadrootsClientCipherErrorMessage = &'static str;
-
-impl RadrootsClientCipherError {
- pub const fn message(self) -> RadrootsClientCipherErrorMessage {
- match self {
- RadrootsClientCipherError::IdbUndefined => "error.client.cipher.idb_undefined",
- RadrootsClientCipherError::CryptoUndefined => "error.client.cipher.crypto_undefined",
- RadrootsClientCipherError::InvalidCiphertext => "error.client.cipher.invalid_ciphertext",
- RadrootsClientCipherError::DecryptFailure => "error.client.cipher.decrypt_failure",
- }
- }
-}
-
-impl fmt::Display for RadrootsClientCipherError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.write_str(self.message())
- }
-}
-
-impl std::error::Error for RadrootsClientCipherError {}
-
-#[cfg(test)]
-mod tests {
- use super::RadrootsClientCipherError;
-
- #[test]
- fn message_matches_spec() {
- let cases = [
- (
- RadrootsClientCipherError::IdbUndefined,
- "error.client.cipher.idb_undefined",
- ),
- (
- RadrootsClientCipherError::CryptoUndefined,
- "error.client.cipher.crypto_undefined",
- ),
- (
- RadrootsClientCipherError::InvalidCiphertext,
- "error.client.cipher.invalid_ciphertext",
- ),
- (
- RadrootsClientCipherError::DecryptFailure,
- "error.client.cipher.decrypt_failure",
- ),
- ];
- for (err, expected) in cases {
- assert_eq!(err.message(), expected);
- assert_eq!(err.to_string(), expected);
- }
- }
-}
diff --git a/crates/core/src/cipher/mod.rs b/crates/core/src/cipher/mod.rs
@@ -1,11 +0,0 @@
-pub mod error;
-pub mod types;
-
-pub use error::{RadrootsClientCipherError, RadrootsClientCipherErrorMessage};
-pub use types::{
- RadrootsClientCipher,
- RadrootsClientCipherConfig,
- RadrootsClientCipherDecryptResult,
- RadrootsClientCipherEncryptResult,
- RadrootsClientCipherResetResult,
-};
diff --git a/crates/core/src/cipher/types.rs b/crates/core/src/cipher/types.rs
@@ -1,42 +0,0 @@
-use async_trait::async_trait;
-
-use crate::idb::RadrootsClientIdbConfig;
-
-use super::RadrootsClientCipherError;
-
-pub type RadrootsClientCipherEncryptResult = Result<Vec<u8>, RadrootsClientCipherError>;
-pub type RadrootsClientCipherDecryptResult = Result<Vec<u8>, RadrootsClientCipherError>;
-pub type RadrootsClientCipherResetResult = Result<(), RadrootsClientCipherError>;
-
-#[derive(Debug, Clone, Default, PartialEq, Eq)]
-pub struct RadrootsClientCipherConfig {
- pub idb_config: Option<RadrootsClientIdbConfig>,
- pub key_name: Option<String>,
- pub key_length: Option<u32>,
- pub iv_length: Option<u32>,
- pub algorithm: Option<String>,
-}
-
-#[async_trait(?Send)]
-pub trait RadrootsClientCipher {
- fn get_config(&self) -> RadrootsClientIdbConfig;
-
- async fn reset(&self) -> RadrootsClientCipherResetResult;
- async fn encrypt(&self, data: &[u8]) -> RadrootsClientCipherEncryptResult;
- async fn decrypt(&self, blob: &[u8]) -> RadrootsClientCipherDecryptResult;
-}
-
-#[cfg(test)]
-mod tests {
- use super::RadrootsClientCipherConfig;
-
- #[test]
- fn default_config_is_empty() {
- let config = RadrootsClientCipherConfig::default();
- assert!(config.idb_config.is_none());
- assert!(config.key_name.is_none());
- assert!(config.key_length.is_none());
- assert!(config.iv_length.is_none());
- assert!(config.algorithm.is_none());
- }
-}
diff --git a/crates/core/src/crypto/envelope.rs b/crates/core/src/crypto/envelope.rs
@@ -1,181 +0,0 @@
-use super::{RadrootsClientCryptoEnvelope, RadrootsClientCryptoError};
-
-const ENVELOPE_MAGIC: [u8; 4] = [0x52, 0x52, 0x43, 0x45];
-const ENVELOPE_VERSION: u8 = 1;
-const ENVELOPE_HEADER_LENGTH: usize = 4 + 1 + 1 + 1 + 8;
-
-pub fn crypto_envelope_encode(
- envelope: &RadrootsClientCryptoEnvelope,
-) -> Result<Vec<u8>, RadrootsClientCryptoError> {
- let key_bytes = envelope.key_id.as_bytes();
- if key_bytes.len() > u8::MAX as usize {
- return Err(RadrootsClientCryptoError::InvalidKeyId);
- }
- let total_len = ENVELOPE_HEADER_LENGTH
- + key_bytes.len()
- + envelope.iv.len()
- + envelope.ciphertext.len();
- let mut out = vec![0u8; total_len];
- let mut offset = 0;
- out[offset..offset + ENVELOPE_MAGIC.len()].copy_from_slice(&ENVELOPE_MAGIC);
- offset += ENVELOPE_MAGIC.len();
- out[offset] = ENVELOPE_VERSION;
- offset += 1;
- out[offset] = key_bytes.len() as u8;
- offset += 1;
- out[offset] = envelope.iv.len() as u8;
- offset += 1;
- out[offset..offset + 8].copy_from_slice(&envelope.created_at.to_be_bytes());
- offset += 8;
- out[offset..offset + key_bytes.len()].copy_from_slice(key_bytes);
- offset += key_bytes.len();
- out[offset..offset + envelope.iv.len()].copy_from_slice(&envelope.iv);
- offset += envelope.iv.len();
- out[offset..offset + envelope.ciphertext.len()].copy_from_slice(&envelope.ciphertext);
- Ok(out)
-}
-
-pub fn crypto_envelope_decode(
- blob: &[u8],
-) -> Result<Option<RadrootsClientCryptoEnvelope>, RadrootsClientCryptoError> {
- if blob.len() < ENVELOPE_HEADER_LENGTH {
- return Ok(None);
- }
- if blob[..ENVELOPE_MAGIC.len()] != ENVELOPE_MAGIC {
- return Ok(None);
- }
- let mut offset = ENVELOPE_MAGIC.len();
- let version = blob[offset];
- offset += 1;
- if version != ENVELOPE_VERSION {
- return Err(RadrootsClientCryptoError::InvalidEnvelope);
- }
- let key_len = blob[offset] as usize;
- offset += 1;
- let iv_len = blob[offset] as usize;
- offset += 1;
- if blob.len() < offset + 8 {
- return Err(RadrootsClientCryptoError::InvalidEnvelope);
- }
- let created_at = u64::from_be_bytes(
- blob[offset..offset + 8]
- .try_into()
- .map_err(|_| RadrootsClientCryptoError::InvalidEnvelope)?,
- );
- offset += 8;
- let remaining = blob.len() - offset;
- if remaining < key_len + iv_len + 1 {
- return Err(RadrootsClientCryptoError::InvalidEnvelope);
- }
- let key_end = offset + key_len;
- let iv_end = key_end + iv_len;
- let key_bytes = &blob[offset..key_end];
- let iv = blob[key_end..iv_end].to_vec();
- let ciphertext = blob[iv_end..].to_vec();
- let key_id = std::str::from_utf8(key_bytes)
- .map_err(|_| RadrootsClientCryptoError::InvalidKeyId)?
- .to_string();
- if key_id.is_empty() {
- return Err(RadrootsClientCryptoError::InvalidKeyId);
- }
- Ok(Some(RadrootsClientCryptoEnvelope {
- version,
- key_id,
- iv,
- created_at,
- ciphertext,
- }))
-}
-
-#[cfg(test)]
-mod tests {
- use super::{crypto_envelope_decode, crypto_envelope_encode};
- use crate::crypto::{RadrootsClientCryptoEnvelope, RadrootsClientCryptoError};
-
- #[test]
- fn encode_decode_roundtrip() -> Result<(), RadrootsClientCryptoError> {
- let envelope = RadrootsClientCryptoEnvelope {
- version: 1,
- key_id: String::from("key"),
- iv: vec![1, 2, 3],
- created_at: 42,
- ciphertext: vec![4, 5, 6],
- };
- let encoded = crypto_envelope_encode(&envelope)?;
- let decoded = crypto_envelope_decode(&encoded)?.ok_or(RadrootsClientCryptoError::InvalidEnvelope)?;
- assert_eq!(decoded.version, envelope.version);
- assert_eq!(decoded.key_id, envelope.key_id);
- assert_eq!(decoded.iv, envelope.iv);
- assert_eq!(decoded.created_at, envelope.created_at);
- assert_eq!(decoded.ciphertext, envelope.ciphertext);
- Ok(())
- }
-
- #[test]
- fn decode_rejects_wrong_magic() -> Result<(), RadrootsClientCryptoError> {
- let mut blob = vec![0u8; 16];
- blob[0] = 0x00;
- blob[1] = 0x00;
- blob[2] = 0x00;
- blob[3] = 0x00;
- assert!(crypto_envelope_decode(&blob)?.is_none());
- Ok(())
- }
-
- #[test]
- fn decode_rejects_short_blob() -> Result<(), RadrootsClientCryptoError> {
- let blob = vec![0u8; 4];
- assert!(crypto_envelope_decode(&blob)?.is_none());
- Ok(())
- }
-
- #[test]
- fn decode_rejects_missing_ciphertext() {
- let envelope = RadrootsClientCryptoEnvelope {
- version: 1,
- key_id: String::from("key"),
- iv: vec![1, 2, 3],
- created_at: 42,
- ciphertext: Vec::new(),
- };
- let encoded = crypto_envelope_encode(&envelope).expect("encode");
- let err = crypto_envelope_decode(&encoded)
- .expect_err("should fail");
- assert_eq!(err, RadrootsClientCryptoError::InvalidEnvelope);
- }
-
- #[test]
- fn encode_rejects_large_key_id() {
- let envelope = RadrootsClientCryptoEnvelope {
- version: 1,
- key_id: "k".repeat(256),
- iv: vec![1, 2, 3],
- created_at: 42,
- ciphertext: vec![4],
- };
- let err = crypto_envelope_encode(&envelope).expect_err("should fail");
- assert_eq!(err, RadrootsClientCryptoError::InvalidKeyId);
- }
-
- #[test]
- fn decode_rejects_empty_key_id() {
- let envelope = RadrootsClientCryptoEnvelope {
- version: 1,
- key_id: String::new(),
- iv: vec![1, 2, 3],
- created_at: 42,
- ciphertext: vec![4, 5, 6],
- };
- let encoded = crypto_envelope_encode(&envelope).expect("encode");
- let err = crypto_envelope_decode(&encoded).expect_err("should fail");
- assert_eq!(err, RadrootsClientCryptoError::InvalidKeyId);
- }
-
- #[test]
- fn decode_rejects_wrong_version() {
- let mut blob = vec![0x52, 0x52, 0x43, 0x45, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, b'a', 0, 0];
- let err = crypto_envelope_decode(&blob).expect_err("should fail");
- assert_eq!(err, RadrootsClientCryptoError::InvalidEnvelope);
- blob[4] = 1;
- }
-}
diff --git a/crates/core/src/crypto/error.rs b/crates/core/src/crypto/error.rs
@@ -1,109 +0,0 @@
-use std::fmt;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsClientCryptoError {
- IdbUndefined,
- CryptoUndefined,
- InvalidEnvelope,
- InvalidKeyId,
- KeyNotFound,
- UnwrapFailure,
- WrapFailure,
- LegacyKeyMissing,
- EncryptFailure,
- DecryptFailure,
- KdfFailure,
- RegistryFailure,
-}
-
-pub type RadrootsClientCryptoErrorMessage = &'static str;
-
-impl RadrootsClientCryptoError {
- pub const fn message(self) -> RadrootsClientCryptoErrorMessage {
- match self {
- RadrootsClientCryptoError::IdbUndefined => "error.client.crypto.idb_undefined",
- RadrootsClientCryptoError::CryptoUndefined => "error.client.crypto.crypto_undefined",
- RadrootsClientCryptoError::InvalidEnvelope => "error.client.crypto.invalid_envelope",
- RadrootsClientCryptoError::InvalidKeyId => "error.client.crypto.invalid_key_id",
- RadrootsClientCryptoError::KeyNotFound => "error.client.crypto.key_not_found",
- RadrootsClientCryptoError::UnwrapFailure => "error.client.crypto.unwrap_failure",
- RadrootsClientCryptoError::WrapFailure => "error.client.crypto.wrap_failure",
- RadrootsClientCryptoError::LegacyKeyMissing => "error.client.crypto.legacy_key_missing",
- RadrootsClientCryptoError::EncryptFailure => "error.client.crypto.encrypt_failure",
- RadrootsClientCryptoError::DecryptFailure => "error.client.crypto.decrypt_failure",
- RadrootsClientCryptoError::KdfFailure => "error.client.crypto.kdf_failure",
- RadrootsClientCryptoError::RegistryFailure => "error.client.crypto.registry_failure",
- }
- }
-}
-
-impl fmt::Display for RadrootsClientCryptoError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.write_str(self.message())
- }
-}
-
-impl std::error::Error for RadrootsClientCryptoError {}
-
-#[cfg(test)]
-mod tests {
- use super::RadrootsClientCryptoError;
-
- #[test]
- fn message_matches_spec() {
- let cases = [
- (
- RadrootsClientCryptoError::IdbUndefined,
- "error.client.crypto.idb_undefined",
- ),
- (
- RadrootsClientCryptoError::CryptoUndefined,
- "error.client.crypto.crypto_undefined",
- ),
- (
- RadrootsClientCryptoError::InvalidEnvelope,
- "error.client.crypto.invalid_envelope",
- ),
- (
- RadrootsClientCryptoError::InvalidKeyId,
- "error.client.crypto.invalid_key_id",
- ),
- (
- RadrootsClientCryptoError::KeyNotFound,
- "error.client.crypto.key_not_found",
- ),
- (
- RadrootsClientCryptoError::UnwrapFailure,
- "error.client.crypto.unwrap_failure",
- ),
- (
- RadrootsClientCryptoError::WrapFailure,
- "error.client.crypto.wrap_failure",
- ),
- (
- RadrootsClientCryptoError::LegacyKeyMissing,
- "error.client.crypto.legacy_key_missing",
- ),
- (
- RadrootsClientCryptoError::EncryptFailure,
- "error.client.crypto.encrypt_failure",
- ),
- (
- RadrootsClientCryptoError::DecryptFailure,
- "error.client.crypto.decrypt_failure",
- ),
- (
- RadrootsClientCryptoError::KdfFailure,
- "error.client.crypto.kdf_failure",
- ),
- (
- RadrootsClientCryptoError::RegistryFailure,
- "error.client.crypto.registry_failure",
- ),
- ];
- for (err, expected) in cases {
- assert_eq!(err.message(), expected);
- assert_eq!(err.to_string(), expected);
- }
- }
-}
diff --git a/crates/core/src/crypto/kdf.rs b/crates/core/src/crypto/kdf.rs
@@ -1,145 +0,0 @@
-use super::RadrootsClientCryptoError;
-use crate::crypto::random::fill_random;
-
-#[cfg(target_arch = "wasm32")]
-use js_sys::{Array, Object, Reflect, Uint8Array};
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen::JsCast;
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen_futures::JsFuture;
-#[cfg(target_arch = "wasm32")]
-use web_sys::{CryptoKey, SubtleCrypto};
-
-const DEFAULT_KDF_ITERATIONS: u32 = 210_000;
-#[cfg(target_arch = "wasm32")]
-const KDF_HASH: &str = "SHA-256";
-
-pub fn crypto_kdf_iterations_default() -> u32 {
- DEFAULT_KDF_ITERATIONS
-}
-
-pub fn crypto_kdf_salt_create(length: usize) -> Result<Vec<u8>, RadrootsClientCryptoError> {
- let mut salt = vec![0u8; length];
- fill_random(&mut salt)?;
- Ok(salt)
-}
-
-#[cfg(target_arch = "wasm32")]
-fn subtle_crypto() -> Result<SubtleCrypto, RadrootsClientCryptoError> {
- let window = web_sys::window().ok_or(RadrootsClientCryptoError::CryptoUndefined)?;
- let crypto = window
- .crypto()
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
- Ok(crypto.subtle())
-}
-
-#[cfg(target_arch = "wasm32")]
-fn derive_key_usages() -> Array {
- let usages = Array::new();
- usages.push(&"deriveKey".into());
- usages
-}
-
-#[cfg(target_arch = "wasm32")]
-fn encrypt_decrypt_usages() -> Array {
- let usages = Array::new();
- usages.push(&"encrypt".into());
- usages.push(&"decrypt".into());
- usages
-}
-
-#[cfg(target_arch = "wasm32")]
-fn pbkdf2_params(
- salt: &[u8],
- iterations: u32,
-) -> Result<Object, RadrootsClientCryptoError> {
- let params = Object::new();
- Reflect::set(¶ms, &"name".into(), &"PBKDF2".into())
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
- let salt_array = Uint8Array::from(salt);
- Reflect::set(¶ms, &"salt".into(), &salt_array.into())
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
- Reflect::set(
- ¶ms,
- &"iterations".into(),
- &wasm_bindgen::JsValue::from_f64(iterations as f64),
- )
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
- Reflect::set(¶ms, &"hash".into(), &KDF_HASH.into())
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
- Ok(params)
-}
-
-#[cfg(target_arch = "wasm32")]
-fn aes_gcm_algorithm() -> Result<Object, RadrootsClientCryptoError> {
- let algo = Object::new();
- Reflect::set(&algo, &"name".into(), &"AES-GCM".into())
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
- Reflect::set(
- &algo,
- &"length".into(),
- &wasm_bindgen::JsValue::from_f64(256.0),
- )
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
- Ok(algo)
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn crypto_kdf_derive_kek(
- material: &[u8],
- salt: &[u8],
- iterations: u32,
-) -> Result<CryptoKey, RadrootsClientCryptoError> {
- let subtle = subtle_crypto()?;
- let key_data = Uint8Array::from(material);
- let key_data_obj = key_data.unchecked_ref::<Object>();
- let base_promise = subtle
- .import_key_with_str(
- "raw",
- key_data_obj,
- "PBKDF2",
- false,
- &derive_key_usages().into(),
- )
- .map_err(|_| RadrootsClientCryptoError::KdfFailure)?;
- let base_value = JsFuture::from(base_promise)
- .await
- .map_err(|_| RadrootsClientCryptoError::KdfFailure)?;
- let base_key = base_value
- .dyn_into::<CryptoKey>()
- .map_err(|_| RadrootsClientCryptoError::KdfFailure)?;
-
- let pbkdf2 = pbkdf2_params(salt, iterations).map_err(|_| RadrootsClientCryptoError::KdfFailure)?;
- let aes_gcm = aes_gcm_algorithm().map_err(|_| RadrootsClientCryptoError::KdfFailure)?;
- let promise = subtle
- .derive_key_with_object_and_object(
- &pbkdf2,
- &base_key,
- &aes_gcm,
- false,
- &encrypt_decrypt_usages().into(),
- )
- .map_err(|_| RadrootsClientCryptoError::KdfFailure)?;
- let value = JsFuture::from(promise)
- .await
- .map_err(|_| RadrootsClientCryptoError::KdfFailure)?;
- value
- .dyn_into::<CryptoKey>()
- .map_err(|_| RadrootsClientCryptoError::KdfFailure)
-}
-
-#[cfg(test)]
-mod tests {
- use super::{crypto_kdf_iterations_default, crypto_kdf_salt_create};
-
- #[test]
- fn kdf_defaults_match_spec() {
- assert_eq!(crypto_kdf_iterations_default(), 210_000);
- }
-
- #[test]
- fn kdf_salt_length_matches_request() {
- let salt = crypto_kdf_salt_create(16).expect("salt");
- assert_eq!(salt.len(), 16);
- }
-}
diff --git a/crates/core/src/crypto/keys.rs b/crates/core/src/crypto/keys.rs
@@ -1,181 +0,0 @@
-use super::RadrootsClientCryptoError;
-use crate::crypto::random::fill_random;
-
-#[cfg(target_arch = "wasm32")]
-use js_sys::{Array, Object, Reflect, Uint8Array};
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen::JsCast;
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen_futures::JsFuture;
-#[cfg(target_arch = "wasm32")]
-use web_sys::{CryptoKey, SubtleCrypto};
-
-const KEY_ID_BYTES_LENGTH: usize = 16;
-#[cfg(target_arch = "wasm32")]
-const WRAP_IV_LENGTH: usize = 12;
-
-fn bytes_to_hex(bytes: &[u8]) -> String {
- let mut out = String::with_capacity(bytes.len() * 2);
- for byte in bytes {
- use std::fmt::Write;
- let _ = write!(out, "{:02x}", byte);
- }
- out
-}
-
-pub fn crypto_key_id_create() -> Result<String, RadrootsClientCryptoError> {
- let mut bytes = [0u8; KEY_ID_BYTES_LENGTH];
- fill_random(&mut bytes)?;
- Ok(bytes_to_hex(&bytes))
-}
-
-#[cfg(target_arch = "wasm32")]
-fn subtle_crypto() -> Result<SubtleCrypto, RadrootsClientCryptoError> {
- let window = web_sys::window().ok_or(RadrootsClientCryptoError::CryptoUndefined)?;
- let crypto = window
- .crypto()
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
- Ok(crypto.subtle())
-}
-
-#[cfg(target_arch = "wasm32")]
-fn encrypt_decrypt_usages() -> Array {
- let usages = Array::new();
- usages.push(&"encrypt".into());
- usages.push(&"decrypt".into());
- usages
-}
-
-#[cfg(target_arch = "wasm32")]
-fn aes_gcm_algorithm(length: u32) -> Result<Object, RadrootsClientCryptoError> {
- let algo = Object::new();
- Reflect::set(&algo, &"name".into(), &"AES-GCM".into())
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
- Reflect::set(
- &algo,
- &"length".into(),
- &wasm_bindgen::JsValue::from_f64(length as f64),
- )
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
- Ok(algo)
-}
-
-#[cfg(target_arch = "wasm32")]
-fn aes_gcm_params(iv: &[u8]) -> Result<Object, RadrootsClientCryptoError> {
- let algo = Object::new();
- Reflect::set(&algo, &"name".into(), &"AES-GCM".into())
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
- let iv_array = Uint8Array::from(iv);
- Reflect::set(&algo, &"iv".into(), &iv_array.into())
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
- Ok(algo)
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn crypto_key_generate() -> Result<CryptoKey, RadrootsClientCryptoError> {
- let subtle = subtle_crypto()?;
- let algo = aes_gcm_algorithm(256)?;
- let usages = encrypt_decrypt_usages();
- let promise = subtle
- .generate_key_with_object(&algo, true, &usages.into())
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
- let value = JsFuture::from(promise)
- .await
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
- value
- .dyn_into::<CryptoKey>()
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn crypto_key_export_raw(
- key: &CryptoKey,
-) -> Result<Vec<u8>, RadrootsClientCryptoError> {
- let subtle = subtle_crypto()?;
- let promise = subtle
- .export_key("raw", key)
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
- let value = JsFuture::from(promise)
- .await
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
- let array = Uint8Array::new(&value);
- let mut out = vec![0u8; array.length() as usize];
- array.copy_to(&mut out);
- Ok(out)
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn crypto_key_import_raw(
- raw: &[u8],
-) -> Result<CryptoKey, RadrootsClientCryptoError> {
- let subtle = subtle_crypto()?;
- let algo = aes_gcm_algorithm(256)?;
- let usages = encrypt_decrypt_usages();
- let data = Uint8Array::from(raw);
- let data_obj = data.unchecked_ref::<Object>();
- let promise = subtle
- .import_key_with_object("raw", data_obj, &algo, false, &usages.into())
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
- let value = JsFuture::from(promise)
- .await
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
- value
- .dyn_into::<CryptoKey>()
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn crypto_key_wrap(
- kek: &CryptoKey,
- raw_key: &mut [u8],
-) -> Result<(Vec<u8>, Vec<u8>), RadrootsClientCryptoError> {
- let subtle = subtle_crypto().map_err(|_| RadrootsClientCryptoError::WrapFailure)?;
- let mut wrap_iv = vec![0u8; WRAP_IV_LENGTH];
- fill_random(&mut wrap_iv).map_err(|_| RadrootsClientCryptoError::WrapFailure)?;
- let algo = aes_gcm_params(&wrap_iv).map_err(|_| RadrootsClientCryptoError::WrapFailure)?;
- let promise = subtle
- .encrypt_with_object_and_u8_array(&algo, kek, raw_key)
- .map_err(|_| RadrootsClientCryptoError::WrapFailure)?;
- let value = JsFuture::from(promise)
- .await
- .map_err(|_| RadrootsClientCryptoError::WrapFailure)?;
- let array = Uint8Array::new(&value);
- let mut wrapped = vec![0u8; array.length() as usize];
- array.copy_to(&mut wrapped);
- raw_key.fill(0);
- Ok((wrapped, wrap_iv))
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn crypto_key_unwrap(
- kek: &CryptoKey,
- wrapped_key: &[u8],
- wrap_iv: &[u8],
-) -> Result<CryptoKey, RadrootsClientCryptoError> {
- let subtle = subtle_crypto().map_err(|_| RadrootsClientCryptoError::UnwrapFailure)?;
- let algo = aes_gcm_params(wrap_iv).map_err(|_| RadrootsClientCryptoError::UnwrapFailure)?;
- let promise = subtle
- .decrypt_with_object_and_u8_array(&algo, kek, wrapped_key)
- .map_err(|_| RadrootsClientCryptoError::UnwrapFailure)?;
- let value = JsFuture::from(promise)
- .await
- .map_err(|_| RadrootsClientCryptoError::UnwrapFailure)?;
- let array = Uint8Array::new(&value);
- let mut raw = vec![0u8; array.length() as usize];
- array.copy_to(&mut raw);
- crypto_key_import_raw(&raw)
- .await
- .map_err(|_| RadrootsClientCryptoError::UnwrapFailure)
-}
-
-#[cfg(test)]
-mod tests {
- use super::crypto_key_id_create;
-
- #[test]
- fn key_id_is_hex() {
- let key_id = crypto_key_id_create().expect("key id");
- assert_eq!(key_id.len(), 32);
- assert!(key_id.chars().all(|c| c.is_ascii_hexdigit()));
- }
-}
diff --git a/crates/core/src/crypto/mod.rs b/crates/core/src/crypto/mod.rs
@@ -1,56 +0,0 @@
-pub mod error;
-pub mod types;
-pub mod envelope;
-pub mod random;
-pub mod keys;
-pub mod kdf;
-pub mod registry;
-pub mod provider;
-pub mod service;
-
-pub use error::{RadrootsClientCryptoError, RadrootsClientCryptoErrorMessage};
-pub use types::{
- RadrootsClientCryptoAlgorithm,
- RadrootsClientCryptoDecryptOutcome,
- RadrootsClientCryptoEnvelope,
- RadrootsClientCryptoKeyEntry,
- RadrootsClientCryptoKeyStatus,
- RadrootsClientCryptoRegistryExport,
- RadrootsClientCryptoStoreConfig,
- RadrootsClientCryptoStoreIndex,
- RadrootsClientKeyMaterialProvider,
- RadrootsClientLegacyKeyConfig,
- RadrootsClientWebCryptoService,
-};
-pub use provider::RadrootsClientDeviceKeyMaterialProvider;
-pub use service::{
- RadrootsClientWebCryptoServiceConfig,
- RadrootsClientWebCryptoServiceImpl,
-};
-pub use envelope::{crypto_envelope_decode, crypto_envelope_encode};
-pub use keys::crypto_key_id_create;
-pub use kdf::{crypto_kdf_iterations_default, crypto_kdf_salt_create};
-pub use registry::{
- crypto_registry_clear_key_entry,
- crypto_registry_clear_store_index,
- crypto_registry_export,
- crypto_registry_get_device_material,
- crypto_registry_get_key_entry,
- crypto_registry_get_store_index,
- crypto_registry_import,
- crypto_registry_list_key_entries,
- crypto_registry_list_store_indices,
- crypto_registry_set_device_material,
- crypto_registry_set_key_entry,
- crypto_registry_set_store_index,
-};
-#[cfg(target_arch = "wasm32")]
-pub use keys::{
- crypto_key_export_raw,
- crypto_key_generate,
- crypto_key_import_raw,
- crypto_key_unwrap,
- crypto_key_wrap,
-};
-#[cfg(target_arch = "wasm32")]
-pub use kdf::crypto_kdf_derive_kek;
diff --git a/crates/core/src/crypto/provider.rs b/crates/core/src/crypto/provider.rs
@@ -1,63 +0,0 @@
-use async_trait::async_trait;
-
-#[cfg(target_arch = "wasm32")]
-use crate::crypto::random::fill_random;
-#[cfg(target_arch = "wasm32")]
-use crate::crypto::registry::{
- crypto_registry_get_device_material,
- crypto_registry_set_device_material,
-};
-
-use super::{RadrootsClientCryptoError, RadrootsClientKeyMaterialProvider};
-
-const DEVICE_PROVIDER_ID: &str = "device";
-#[cfg(target_arch = "wasm32")]
-const DEVICE_MATERIAL_BYTES: usize = 32;
-
-pub struct RadrootsClientDeviceKeyMaterialProvider;
-
-#[async_trait(?Send)]
-impl RadrootsClientKeyMaterialProvider for RadrootsClientDeviceKeyMaterialProvider {
- async fn get_key_material(&self) -> Result<Vec<u8>, RadrootsClientCryptoError> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- return Err(RadrootsClientCryptoError::CryptoUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- if let Some(existing) = crypto_registry_get_device_material().await? {
- return Ok(existing);
- }
- let mut material = vec![0u8; DEVICE_MATERIAL_BYTES];
- fill_random(&mut material)?;
- crypto_registry_set_device_material(&material).await?;
- Ok(material)
- }
- }
-
- async fn get_provider_id(&self) -> Result<String, RadrootsClientCryptoError> {
- Ok(String::from(DEVICE_PROVIDER_ID))
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::RadrootsClientDeviceKeyMaterialProvider;
- use crate::crypto::{RadrootsClientCryptoError, RadrootsClientKeyMaterialProvider};
-
- #[test]
- fn provider_id_is_device() {
- let provider = RadrootsClientDeviceKeyMaterialProvider;
- let id = futures::executor::block_on(provider.get_provider_id())
- .expect("provider id");
- assert_eq!(id, "device");
- }
-
- #[test]
- fn non_wasm_material_errors() {
- let provider = RadrootsClientDeviceKeyMaterialProvider;
- let err = futures::executor::block_on(provider.get_key_material())
- .expect_err("missing crypto");
- assert_eq!(err, RadrootsClientCryptoError::CryptoUndefined);
- }
-}
diff --git a/crates/core/src/crypto/random.rs b/crates/core/src/crypto/random.rs
@@ -1,34 +0,0 @@
-use super::RadrootsClientCryptoError;
-
-pub fn fill_random(bytes: &mut [u8]) -> Result<(), RadrootsClientCryptoError> {
- if bytes.is_empty() {
- return Ok(());
- }
- fill_random_inner(bytes)
-}
-
-#[cfg(target_arch = "wasm32")]
-fn fill_random_inner(bytes: &mut [u8]) -> Result<(), RadrootsClientCryptoError> {
- let window = web_sys::window().ok_or(RadrootsClientCryptoError::CryptoUndefined)?;
- let crypto = window.crypto().map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
- crypto
- .get_random_values_with_u8_array(bytes)
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
- Ok(())
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-fn fill_random_inner(bytes: &mut [u8]) -> Result<(), RadrootsClientCryptoError> {
- getrandom::fill(bytes).map_err(|_| RadrootsClientCryptoError::CryptoUndefined)
-}
-
-#[cfg(test)]
-mod tests {
- use super::fill_random;
-
- #[test]
- fn fill_random_noop_for_empty() {
- let mut bytes = [];
- assert!(fill_random(&mut bytes).is_ok());
- }
-}
diff --git a/crates/core/src/crypto/registry.rs b/crates/core/src/crypto/registry.rs
@@ -1,403 +0,0 @@
-use crate::crypto::{RadrootsClientCryptoError, RadrootsClientCryptoRegistryExport};
-use crate::crypto::{RadrootsClientCryptoKeyEntry, RadrootsClientCryptoStoreIndex};
-
-#[cfg(any(test, target_arch = "wasm32"))]
-const STORE_INDEX_PREFIX: &str = "store:";
-#[cfg(any(test, target_arch = "wasm32"))]
-const KEY_ENTRY_PREFIX: &str = "key:";
-#[cfg(target_arch = "wasm32")]
-const DEVICE_MATERIAL_KEY: &str = "device:material";
-
-#[cfg(any(test, target_arch = "wasm32"))]
-fn store_index_key(store_id: &str) -> String {
- format!("{STORE_INDEX_PREFIX}{store_id}")
-}
-
-#[cfg(any(test, target_arch = "wasm32"))]
-fn key_entry_key(key_id: &str) -> String {
- format!("{KEY_ENTRY_PREFIX}{key_id}")
-}
-
-#[cfg(target_arch = "wasm32")]
-use crate::idb::{
- idb_del,
- idb_get,
- idb_keys,
- idb_set,
- idb_store_ensure,
- idb_value_as_bytes,
- IDB_CONFIG_CRYPTO_REGISTRY,
- RadrootsClientIdbStoreError,
-};
-#[cfg(target_arch = "wasm32")]
-use js_sys::Uint8Array;
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen::JsValue;
-
-#[cfg(target_arch = "wasm32")]
-fn map_idb_error(err: RadrootsClientIdbStoreError) -> RadrootsClientCryptoError {
- match err {
- RadrootsClientIdbStoreError::IdbUndefined => RadrootsClientCryptoError::IdbUndefined,
- _ => RadrootsClientCryptoError::RegistryFailure,
- }
-}
-
-#[cfg(target_arch = "wasm32")]
-async fn ensure_idb() -> Result<(), RadrootsClientCryptoError> {
- idb_store_ensure(
- IDB_CONFIG_CRYPTO_REGISTRY.database,
- IDB_CONFIG_CRYPTO_REGISTRY.store,
- )
- .await
- .map_err(map_idb_error)
-}
-
-#[cfg(target_arch = "wasm32")]
-fn decode_store_index(value: &JsValue) -> Result<RadrootsClientCryptoStoreIndex, RadrootsClientCryptoError> {
- if let Some(text) = value.as_string() {
- return serde_json::from_str(&text)
- .map_err(|_| RadrootsClientCryptoError::RegistryFailure);
- }
- serde_wasm_bindgen::from_value(value.clone())
- .map_err(|_| RadrootsClientCryptoError::RegistryFailure)
-}
-
-#[cfg(target_arch = "wasm32")]
-fn decode_key_entry(value: &JsValue) -> Result<RadrootsClientCryptoKeyEntry, RadrootsClientCryptoError> {
- if let Some(text) = value.as_string() {
- return serde_json::from_str(&text)
- .map_err(|_| RadrootsClientCryptoError::RegistryFailure);
- }
- serde_wasm_bindgen::from_value(value.clone())
- .map_err(|_| RadrootsClientCryptoError::RegistryFailure)
-}
-
-#[cfg(target_arch = "wasm32")]
-fn encode_store_index(
- index: &RadrootsClientCryptoStoreIndex,
-) -> Result<JsValue, RadrootsClientCryptoError> {
- serde_wasm_bindgen::to_value(index)
- .map_err(|_| RadrootsClientCryptoError::RegistryFailure)
-}
-
-#[cfg(target_arch = "wasm32")]
-fn encode_key_entry(
- entry: &RadrootsClientCryptoKeyEntry,
-) -> Result<JsValue, RadrootsClientCryptoError> {
- serde_wasm_bindgen::to_value(entry)
- .map_err(|_| RadrootsClientCryptoError::RegistryFailure)
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn crypto_registry_get_store_index(
- store_id: &str,
-) -> Result<Option<RadrootsClientCryptoStoreIndex>, RadrootsClientCryptoError> {
- ensure_idb().await?;
- let key = store_index_key(store_id);
- let value = idb_get(
- IDB_CONFIG_CRYPTO_REGISTRY.database,
- IDB_CONFIG_CRYPTO_REGISTRY.store,
- &key,
- )
- .await
- .map_err(map_idb_error)?;
- let Some(value) = value else {
- return Ok(None);
- };
- decode_store_index(&value).map(Some)
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn crypto_registry_set_store_index(
- index: RadrootsClientCryptoStoreIndex,
-) -> Result<(), RadrootsClientCryptoError> {
- ensure_idb().await?;
- let key = store_index_key(&index.store_id);
- let value = encode_store_index(&index)?;
- idb_set(
- IDB_CONFIG_CRYPTO_REGISTRY.database,
- IDB_CONFIG_CRYPTO_REGISTRY.store,
- &key,
- &value,
- )
- .await
- .map_err(map_idb_error)
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn crypto_registry_get_key_entry(
- key_id: &str,
-) -> Result<Option<RadrootsClientCryptoKeyEntry>, RadrootsClientCryptoError> {
- ensure_idb().await?;
- let key = key_entry_key(key_id);
- let value = idb_get(
- IDB_CONFIG_CRYPTO_REGISTRY.database,
- IDB_CONFIG_CRYPTO_REGISTRY.store,
- &key,
- )
- .await
- .map_err(map_idb_error)?;
- let Some(value) = value else {
- return Ok(None);
- };
- decode_key_entry(&value).map(Some)
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn crypto_registry_set_key_entry(
- entry: RadrootsClientCryptoKeyEntry,
-) -> Result<(), RadrootsClientCryptoError> {
- ensure_idb().await?;
- let key = key_entry_key(&entry.key_id);
- let value = encode_key_entry(&entry)?;
- idb_set(
- IDB_CONFIG_CRYPTO_REGISTRY.database,
- IDB_CONFIG_CRYPTO_REGISTRY.store,
- &key,
- &value,
- )
- .await
- .map_err(map_idb_error)
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn crypto_registry_list_store_indices(
-) -> Result<Vec<RadrootsClientCryptoStoreIndex>, RadrootsClientCryptoError> {
- ensure_idb().await?;
- let keys = idb_keys(
- IDB_CONFIG_CRYPTO_REGISTRY.database,
- IDB_CONFIG_CRYPTO_REGISTRY.store,
- )
- .await
- .map_err(map_idb_error)?;
- let mut out = Vec::new();
- for key in keys {
- if !key.starts_with(STORE_INDEX_PREFIX) {
- continue;
- }
- let value = idb_get(
- IDB_CONFIG_CRYPTO_REGISTRY.database,
- IDB_CONFIG_CRYPTO_REGISTRY.store,
- &key,
- )
- .await
- .map_err(map_idb_error)?;
- let Some(value) = value else {
- continue;
- };
- let index = decode_store_index(&value)?;
- out.push(index);
- }
- Ok(out)
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn crypto_registry_list_key_entries(
-) -> Result<Vec<RadrootsClientCryptoKeyEntry>, RadrootsClientCryptoError> {
- ensure_idb().await?;
- let keys = idb_keys(
- IDB_CONFIG_CRYPTO_REGISTRY.database,
- IDB_CONFIG_CRYPTO_REGISTRY.store,
- )
- .await
- .map_err(map_idb_error)?;
- let mut out = Vec::new();
- for key in keys {
- if !key.starts_with(KEY_ENTRY_PREFIX) {
- continue;
- }
- let value = idb_get(
- IDB_CONFIG_CRYPTO_REGISTRY.database,
- IDB_CONFIG_CRYPTO_REGISTRY.store,
- &key,
- )
- .await
- .map_err(map_idb_error)?;
- let Some(value) = value else {
- continue;
- };
- let entry = decode_key_entry(&value)?;
- out.push(entry);
- }
- Ok(out)
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn crypto_registry_export(
-) -> Result<RadrootsClientCryptoRegistryExport, RadrootsClientCryptoError> {
- let stores = crypto_registry_list_store_indices().await?;
- let keys = crypto_registry_list_key_entries().await?;
- Ok(RadrootsClientCryptoRegistryExport { stores, keys })
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn crypto_registry_import(
- registry: RadrootsClientCryptoRegistryExport,
-) -> Result<(), RadrootsClientCryptoError> {
- ensure_idb().await?;
- for store_index in registry.stores {
- crypto_registry_set_store_index(store_index).await?;
- }
- for entry in registry.keys {
- crypto_registry_set_key_entry(entry).await?;
- }
- Ok(())
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn crypto_registry_get_device_material(
-) -> Result<Option<Vec<u8>>, RadrootsClientCryptoError> {
- ensure_idb().await?;
- let value = idb_get(
- IDB_CONFIG_CRYPTO_REGISTRY.database,
- IDB_CONFIG_CRYPTO_REGISTRY.store,
- DEVICE_MATERIAL_KEY,
- )
- .await
- .map_err(map_idb_error)?;
- let Some(value) = value else {
- return Ok(None);
- };
- idb_value_as_bytes(&value).ok_or(RadrootsClientCryptoError::RegistryFailure).map(Some)
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn crypto_registry_set_device_material(
- material: &[u8],
-) -> Result<(), RadrootsClientCryptoError> {
- ensure_idb().await?;
- let value = Uint8Array::from(material);
- idb_set(
- IDB_CONFIG_CRYPTO_REGISTRY.database,
- IDB_CONFIG_CRYPTO_REGISTRY.store,
- DEVICE_MATERIAL_KEY,
- &value.into(),
- )
- .await
- .map_err(map_idb_error)
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn crypto_registry_clear_store_index(
- store_id: &str,
-) -> Result<(), RadrootsClientCryptoError> {
- ensure_idb().await?;
- let key = store_index_key(store_id);
- idb_del(
- IDB_CONFIG_CRYPTO_REGISTRY.database,
- IDB_CONFIG_CRYPTO_REGISTRY.store,
- &key,
- )
- .await
- .map_err(map_idb_error)
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn crypto_registry_clear_key_entry(
- key_id: &str,
-) -> Result<(), RadrootsClientCryptoError> {
- ensure_idb().await?;
- let key = key_entry_key(key_id);
- idb_del(
- IDB_CONFIG_CRYPTO_REGISTRY.database,
- IDB_CONFIG_CRYPTO_REGISTRY.store,
- &key,
- )
- .await
- .map_err(map_idb_error)
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub async fn crypto_registry_get_store_index(
- _store_id: &str,
-) -> Result<Option<RadrootsClientCryptoStoreIndex>, RadrootsClientCryptoError> {
- Err(RadrootsClientCryptoError::IdbUndefined)
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub async fn crypto_registry_set_store_index(
- _index: RadrootsClientCryptoStoreIndex,
-) -> Result<(), RadrootsClientCryptoError> {
- Err(RadrootsClientCryptoError::IdbUndefined)
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub async fn crypto_registry_get_key_entry(
- _key_id: &str,
-) -> Result<Option<RadrootsClientCryptoKeyEntry>, RadrootsClientCryptoError> {
- Err(RadrootsClientCryptoError::IdbUndefined)
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub async fn crypto_registry_set_key_entry(
- _entry: RadrootsClientCryptoKeyEntry,
-) -> Result<(), RadrootsClientCryptoError> {
- Err(RadrootsClientCryptoError::IdbUndefined)
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub async fn crypto_registry_list_store_indices(
-) -> Result<Vec<RadrootsClientCryptoStoreIndex>, RadrootsClientCryptoError> {
- Err(RadrootsClientCryptoError::IdbUndefined)
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub async fn crypto_registry_list_key_entries(
-) -> Result<Vec<RadrootsClientCryptoKeyEntry>, RadrootsClientCryptoError> {
- Err(RadrootsClientCryptoError::IdbUndefined)
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub async fn crypto_registry_export(
-) -> Result<RadrootsClientCryptoRegistryExport, RadrootsClientCryptoError> {
- Err(RadrootsClientCryptoError::IdbUndefined)
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub async fn crypto_registry_import(
- _registry: RadrootsClientCryptoRegistryExport,
-) -> Result<(), RadrootsClientCryptoError> {
- Err(RadrootsClientCryptoError::IdbUndefined)
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub async fn crypto_registry_get_device_material(
-) -> Result<Option<Vec<u8>>, RadrootsClientCryptoError> {
- Err(RadrootsClientCryptoError::IdbUndefined)
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub async fn crypto_registry_set_device_material(
- _material: &[u8],
-) -> Result<(), RadrootsClientCryptoError> {
- Err(RadrootsClientCryptoError::IdbUndefined)
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub async fn crypto_registry_clear_store_index(
- _store_id: &str,
-) -> Result<(), RadrootsClientCryptoError> {
- Err(RadrootsClientCryptoError::IdbUndefined)
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub async fn crypto_registry_clear_key_entry(
- _key_id: &str,
-) -> Result<(), RadrootsClientCryptoError> {
- Err(RadrootsClientCryptoError::IdbUndefined)
-}
-
-#[cfg(test)]
-mod tests {
- use super::{key_entry_key, store_index_key};
-
- #[test]
- fn store_index_key_prefixes() {
- assert_eq!(store_index_key("alpha"), "store:alpha");
- }
-
- #[test]
- fn key_entry_key_prefixes() {
- assert_eq!(key_entry_key("beta"), "key:beta");
- }
-}
diff --git a/crates/core/src/crypto/service.rs b/crates/core/src/crypto/service.rs
@@ -1,628 +0,0 @@
-use std::cell::RefCell;
-use std::collections::HashMap;
-
-use async_trait::async_trait;
-
-use crate::crypto::{crypto_registry_export, crypto_registry_import};
-
-#[cfg(target_arch = "wasm32")]
-use crate::crypto::{
- crypto_envelope_decode,
- crypto_envelope_encode,
- crypto_kdf_derive_kek,
- crypto_kdf_iterations_default,
- crypto_kdf_salt_create,
- crypto_key_export_raw,
- crypto_key_generate,
- crypto_key_id_create,
- crypto_key_import_raw,
- crypto_key_unwrap,
- crypto_key_wrap,
- crypto_registry_get_key_entry,
- crypto_registry_get_store_index,
- crypto_registry_set_key_entry,
- crypto_registry_set_store_index,
-};
-#[cfg(target_arch = "wasm32")]
-use crate::crypto::random::fill_random;
-#[cfg(target_arch = "wasm32")]
-use crate::idb::{idb_get, idb_store_ensure, idb_store_exists, idb_value_as_bytes};
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen::JsCast;
-
-use super::{
- RadrootsClientCryptoDecryptOutcome,
- RadrootsClientCryptoError,
- RadrootsClientCryptoRegistryExport,
- RadrootsClientCryptoStoreConfig,
- RadrootsClientKeyMaterialProvider,
- RadrootsClientWebCryptoService,
-};
-use super::provider::RadrootsClientDeviceKeyMaterialProvider;
-
-#[cfg(target_arch = "wasm32")]
-use super::{
- RadrootsClientCryptoAlgorithm,
- RadrootsClientCryptoEnvelope,
- RadrootsClientCryptoKeyEntry,
- RadrootsClientCryptoKeyStatus,
- RadrootsClientCryptoStoreIndex,
- RadrootsClientLegacyKeyConfig,
-};
-
-const DEFAULT_IV_LENGTH: u32 = 12;
-#[cfg(target_arch = "wasm32")]
-const DEFAULT_KDF_SALT_BYTES: usize = 16;
-
-pub struct RadrootsClientWebCryptoServiceConfig {
- pub key_material_provider: Option<Box<dyn RadrootsClientKeyMaterialProvider>>,
-}
-
-pub struct RadrootsClientWebCryptoServiceImpl {
- store_configs: RefCell<HashMap<String, RadrootsClientCryptoStoreConfig>>,
- #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
- key_material_provider: Box<dyn RadrootsClientKeyMaterialProvider>,
-}
-
-impl RadrootsClientWebCryptoServiceImpl {
- pub fn new(config: Option<RadrootsClientWebCryptoServiceConfig>) -> Self {
- let provider = config
- .and_then(|config| config.key_material_provider)
- .unwrap_or_else(|| Box::new(RadrootsClientDeviceKeyMaterialProvider));
- Self {
- store_configs: RefCell::new(HashMap::new()),
- key_material_provider: provider,
- }
- }
-
- #[cfg(any(test, target_arch = "wasm32"))]
- fn resolve_store_config(&self, store_id: &str) -> RadrootsClientCryptoStoreConfig {
- if let Some(existing) = self.store_configs.borrow().get(store_id) {
- return existing.clone();
- }
- let config = RadrootsClientCryptoStoreConfig {
- store_id: store_id.to_string(),
- legacy_key: None,
- iv_length: Some(DEFAULT_IV_LENGTH),
- };
- self.store_configs
- .borrow_mut()
- .insert(store_id.to_string(), config.clone());
- config
- }
-
- #[cfg(target_arch = "wasm32")]
- async fn resolve_active_key(
- &self,
- store_id: &str,
- ) -> Result<ResolvedKey, RadrootsClientCryptoError> {
- let index = crypto_registry_get_store_index(store_id).await?;
- let config = self.resolve_store_config(store_id);
- let Some(index) = index else {
- return self.create_store_key(store_id, &config).await;
- };
- let entry = crypto_registry_get_key_entry(&index.active_key_id).await?;
- let Some(entry) = entry else {
- return self.create_store_key(store_id, &config).await;
- };
- let key = self.unwrap_key_entry(&entry).await?;
- Ok(ResolvedKey { key, entry, index })
- }
-
- #[cfg(target_arch = "wasm32")]
- async fn resolve_key_by_id(
- &self,
- store_id: &str,
- key_id: &str,
- ) -> Result<ResolvedKey, RadrootsClientCryptoError> {
- let entry = crypto_registry_get_key_entry(key_id).await?;
- let Some(entry) = entry else {
- return Err(RadrootsClientCryptoError::KeyNotFound);
- };
- let index = match crypto_registry_get_store_index(store_id).await? {
- Some(index) => index,
- None => {
- let next_index = RadrootsClientCryptoStoreIndex {
- store_id: store_id.to_string(),
- active_key_id: entry.key_id.clone(),
- key_ids: vec![entry.key_id.clone()],
- created_at: entry.created_at,
- };
- crypto_registry_set_store_index(next_index.clone()).await?;
- next_index
- }
- };
- let key = self.unwrap_key_entry(&entry).await?;
- Ok(ResolvedKey {
- key,
- entry,
- index,
- })
- }
-
- #[cfg(target_arch = "wasm32")]
- async fn create_store_key(
- &self,
- store_id: &str,
- config: &RadrootsClientCryptoStoreConfig,
- ) -> Result<ResolvedKey, RadrootsClientCryptoError> {
- let created = self.create_key_entry(store_id, config).await?;
- let index = RadrootsClientCryptoStoreIndex {
- store_id: store_id.to_string(),
- active_key_id: created.entry.key_id.clone(),
- key_ids: vec![created.entry.key_id.clone()],
- created_at: created.entry.created_at,
- };
- crypto_registry_set_store_index(index.clone()).await?;
- Ok(ResolvedKey {
- key: created.key,
- entry: created.entry,
- index,
- })
- }
-
- #[cfg(target_arch = "wasm32")]
- async fn create_key_entry(
- &self,
- store_id: &str,
- config: &RadrootsClientCryptoStoreConfig,
- ) -> Result<CreatedKey, RadrootsClientCryptoError> {
- let key_id = crypto_key_id_create()?;
- let created_at = js_sys::Date::now() as u64;
- let kdf_salt = crypto_kdf_salt_create(DEFAULT_KDF_SALT_BYTES)?;
- let kdf_iterations = crypto_kdf_iterations_default();
- let mut material = self.key_material_provider.get_key_material().await?;
- let provider_id = self.key_material_provider.get_provider_id().await?;
- let kek = crypto_kdf_derive_kek(&material, &kdf_salt, kdf_iterations).await?;
- material.fill(0);
- let key = crypto_key_generate().await?;
- let mut raw_key = crypto_key_export_raw(&key).await?;
- let (wrapped_key, wrap_iv) = crypto_key_wrap(&kek, &mut raw_key).await?;
- let iv_length = config.iv_length.unwrap_or(DEFAULT_IV_LENGTH);
- let entry = RadrootsClientCryptoKeyEntry {
- key_id,
- store_id: store_id.to_string(),
- created_at,
- status: RadrootsClientCryptoKeyStatus::Active,
- wrapped_key,
- wrap_iv,
- kdf_salt,
- kdf_iterations,
- iv_length,
- algorithm: RadrootsClientCryptoAlgorithm::AesGcm,
- provider_id,
- };
- crypto_registry_set_key_entry(entry.clone()).await?;
- Ok(CreatedKey { key, entry })
- }
-
- #[cfg(target_arch = "wasm32")]
- async fn unwrap_key_entry(
- &self,
- entry: &RadrootsClientCryptoKeyEntry,
- ) -> Result<web_sys::CryptoKey, RadrootsClientCryptoError> {
- let mut material = self.key_material_provider.get_key_material().await?;
- let kek = crypto_kdf_derive_kek(&material, &entry.kdf_salt, entry.kdf_iterations).await?;
- material.fill(0);
- crypto_key_unwrap(&kek, &entry.wrapped_key, &entry.wrap_iv).await
- }
-
- #[cfg(target_arch = "wasm32")]
- async fn decrypt_envelope(
- &self,
- store_id: &str,
- envelope: RadrootsClientCryptoEnvelope,
- ) -> Result<RadrootsClientCryptoDecryptOutcome, RadrootsClientCryptoError> {
- let resolved = self.resolve_key_by_id(store_id, &envelope.key_id).await?;
- let plaintext =
- decrypt_bytes(&resolved.key, &envelope.iv, &envelope.ciphertext).await?;
- let needs_reencrypt = resolved.index.active_key_id != envelope.key_id;
- if !needs_reencrypt {
- return Ok(RadrootsClientCryptoDecryptOutcome {
- plaintext,
- needs_reencrypt,
- reencrypted: None,
- });
- }
- let reencrypted = self.encrypt(store_id, &plaintext).await?;
- Ok(RadrootsClientCryptoDecryptOutcome {
- plaintext,
- needs_reencrypt,
- reencrypted: Some(reencrypted),
- })
- }
-
- #[cfg(target_arch = "wasm32")]
- async fn decrypt_legacy(
- &self,
- store_id: &str,
- blob: &[u8],
- legacy_key: Option<RadrootsClientLegacyKeyConfig>,
- iv_length: u32,
- ) -> Result<RadrootsClientCryptoDecryptOutcome, RadrootsClientCryptoError> {
- let Some(legacy_key) = legacy_key else {
- return Err(RadrootsClientCryptoError::LegacyKeyMissing);
- };
- let legacy_crypto_key = self.load_legacy_key(&legacy_key).await?;
- let Some(legacy_crypto_key) = legacy_crypto_key else {
- return Err(RadrootsClientCryptoError::LegacyKeyMissing);
- };
- let iv_len = iv_length as usize;
- if blob.len() <= iv_len {
- return Err(RadrootsClientCryptoError::InvalidEnvelope);
- }
- let iv = &blob[..iv_len];
- let ciphertext = &blob[iv_len..];
- let plaintext = decrypt_bytes_with_algorithm(
- &legacy_crypto_key,
- &legacy_key.algorithm,
- iv,
- ciphertext,
- )
- .await?;
- let reencrypted = self.encrypt(store_id, &plaintext).await?;
- Ok(RadrootsClientCryptoDecryptOutcome {
- plaintext,
- needs_reencrypt: true,
- reencrypted: Some(reencrypted),
- })
- }
-
- #[cfg(target_arch = "wasm32")]
- async fn load_legacy_key(
- &self,
- legacy: &RadrootsClientLegacyKeyConfig,
- ) -> Result<Option<web_sys::CryptoKey>, RadrootsClientCryptoError> {
- let exists = idb_store_exists(legacy.idb_config.database, legacy.idb_config.store)
- .await
- .map_err(map_idb_error)?;
- if !exists {
- return Ok(None);
- }
- idb_store_ensure(legacy.idb_config.database, legacy.idb_config.store)
- .await
- .map_err(map_idb_error)?;
- let stored = idb_get(
- legacy.idb_config.database,
- legacy.idb_config.store,
- &legacy.key_name,
- )
- .await
- .map_err(map_idb_error)?;
- let Some(stored) = stored else {
- return Ok(None);
- };
- if let Ok(key) = stored.clone().dyn_into::<web_sys::CryptoKey>() {
- return Ok(Some(key));
- }
- let Some(bytes) = idb_value_as_bytes(&stored) else {
- return Ok(None);
- };
- crypto_key_import_raw(&bytes).await.map(Some)
- }
-}
-
-impl Default for RadrootsClientWebCryptoServiceImpl {
- fn default() -> Self {
- Self::new(None)
- }
-}
-
-#[async_trait(?Send)]
-impl RadrootsClientWebCryptoService for RadrootsClientWebCryptoServiceImpl {
- fn register_store_config(&mut self, config: RadrootsClientCryptoStoreConfig) {
- let store_id = config.store_id.clone();
- let mut configs = self.store_configs.borrow_mut();
- if let Some(existing) = configs.get(&store_id).cloned() {
- configs.insert(
- store_id,
- RadrootsClientCryptoStoreConfig {
- store_id: config.store_id,
- iv_length: config.iv_length.or(existing.iv_length),
- legacy_key: config.legacy_key.or_else(|| existing.legacy_key.clone()),
- },
- );
- return;
- }
- configs.insert(
- store_id,
- RadrootsClientCryptoStoreConfig {
- store_id: config.store_id,
- iv_length: Some(config.iv_length.unwrap_or(DEFAULT_IV_LENGTH)),
- legacy_key: config.legacy_key,
- },
- );
- }
-
- async fn encrypt(
- &self,
- store_id: &str,
- plaintext: &[u8],
- ) -> Result<Vec<u8>, RadrootsClientCryptoError> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = (store_id, plaintext);
- return Err(RadrootsClientCryptoError::CryptoUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- let resolved = self.resolve_active_key(store_id).await?;
- let iv_length = if resolved.entry.iv_length == 0 {
- DEFAULT_IV_LENGTH
- } else {
- resolved.entry.iv_length
- };
- let mut iv = vec![0u8; iv_length as usize];
- fill_random(&mut iv)?;
- let ciphertext = encrypt_bytes(&resolved.key, &iv, plaintext).await?;
- let envelope = RadrootsClientCryptoEnvelope {
- version: 1,
- key_id: resolved.entry.key_id.clone(),
- iv,
- created_at: js_sys::Date::now() as u64,
- ciphertext,
- };
- crypto_envelope_encode(&envelope)
- }
- }
-
- async fn decrypt(
- &self,
- store_id: &str,
- blob: &[u8],
- ) -> Result<Vec<u8>, RadrootsClientCryptoError> {
- let outcome = self.decrypt_record(store_id, blob).await?;
- Ok(outcome.plaintext)
- }
-
- async fn decrypt_record(
- &self,
- store_id: &str,
- blob: &[u8],
- ) -> Result<RadrootsClientCryptoDecryptOutcome, RadrootsClientCryptoError> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = (store_id, blob);
- return Err(RadrootsClientCryptoError::CryptoUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- let config = self.resolve_store_config(store_id);
- let envelope = crypto_envelope_decode(blob)?;
- if let Some(envelope) = envelope {
- return self.decrypt_envelope(store_id, envelope).await;
- }
- let iv_length = config.iv_length.unwrap_or(DEFAULT_IV_LENGTH);
- return self
- .decrypt_legacy(store_id, blob, config.legacy_key, iv_length)
- .await;
- }
- }
-
- async fn rotate_store_key(
- &self,
- store_id: &str,
- ) -> Result<String, RadrootsClientCryptoError> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = store_id;
- return Err(RadrootsClientCryptoError::CryptoUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- let config = self.resolve_store_config(store_id);
- let index = match crypto_registry_get_store_index(store_id).await? {
- Some(index) => index,
- None => {
- let created = self.create_store_key(store_id, &config).await?;
- return Ok(created.entry.key_id);
- }
- };
- if let Some(entry) = crypto_registry_get_key_entry(&index.active_key_id).await? {
- let rotated = RadrootsClientCryptoKeyEntry {
- status: RadrootsClientCryptoKeyStatus::Rotated,
- ..entry
- };
- crypto_registry_set_key_entry(rotated).await?;
- }
- let created = self.create_key_entry(store_id, &config).await?;
- let next_index = RadrootsClientCryptoStoreIndex {
- store_id: index.store_id,
- active_key_id: created.entry.key_id.clone(),
- key_ids: merge_key_ids(&index.key_ids, &created.entry.key_id),
- created_at: index.created_at,
- };
- crypto_registry_set_store_index(next_index).await?;
- Ok(created.entry.key_id)
- }
- }
-
- async fn export_registry(
- &self,
- ) -> Result<RadrootsClientCryptoRegistryExport, RadrootsClientCryptoError> {
- crypto_registry_export().await
- }
-
- async fn import_registry(
- &self,
- registry: RadrootsClientCryptoRegistryExport,
- ) -> Result<(), RadrootsClientCryptoError> {
- crypto_registry_import(registry).await
- }
-}
-
-#[cfg(target_arch = "wasm32")]
-struct ResolvedKey {
- key: web_sys::CryptoKey,
- entry: RadrootsClientCryptoKeyEntry,
- index: RadrootsClientCryptoStoreIndex,
-}
-
-#[cfg(target_arch = "wasm32")]
-struct CreatedKey {
- key: web_sys::CryptoKey,
- entry: RadrootsClientCryptoKeyEntry,
-}
-
-#[cfg(target_arch = "wasm32")]
-fn merge_key_ids(current: &[String], next_key_id: &str) -> Vec<String> {
- if current.iter().any(|key_id| key_id == next_key_id) {
- return current.to_vec();
- }
- let mut merged = current.to_vec();
- merged.push(next_key_id.to_string());
- merged
-}
-
-#[cfg(target_arch = "wasm32")]
-fn map_idb_error(err: crate::idb::RadrootsClientIdbStoreError) -> RadrootsClientCryptoError {
- match err {
- crate::idb::RadrootsClientIdbStoreError::IdbUndefined => {
- RadrootsClientCryptoError::IdbUndefined
- }
- _ => RadrootsClientCryptoError::RegistryFailure,
- }
-}
-
-#[cfg(target_arch = "wasm32")]
-fn subtle_crypto() -> Result<web_sys::SubtleCrypto, RadrootsClientCryptoError> {
- let window = web_sys::window().ok_or(RadrootsClientCryptoError::CryptoUndefined)?;
- let crypto = window
- .crypto()
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
- Ok(crypto.subtle())
-}
-
-#[cfg(target_arch = "wasm32")]
-fn aes_gcm_params(iv: &[u8]) -> Result<js_sys::Object, RadrootsClientCryptoError> {
- let algo = js_sys::Object::new();
- js_sys::Reflect::set(&algo, &"name".into(), &"AES-GCM".into())
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
- let iv_array = js_sys::Uint8Array::from(iv);
- js_sys::Reflect::set(&algo, &"iv".into(), &iv_array.into())
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
- Ok(algo)
-}
-
-#[cfg(target_arch = "wasm32")]
-fn algorithm_params(
- name: &str,
- iv: &[u8],
-) -> Result<js_sys::Object, RadrootsClientCryptoError> {
- let algo = js_sys::Object::new();
- js_sys::Reflect::set(&algo, &"name".into(), &name.into())
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
- let iv_array = js_sys::Uint8Array::from(iv);
- js_sys::Reflect::set(&algo, &"iv".into(), &iv_array.into())
- .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?;
- Ok(algo)
-}
-
-#[cfg(target_arch = "wasm32")]
-async fn encrypt_bytes(
- key: &web_sys::CryptoKey,
- iv: &[u8],
- plaintext: &[u8],
-) -> Result<Vec<u8>, RadrootsClientCryptoError> {
- let subtle = subtle_crypto()?;
- let algo = aes_gcm_params(iv)?;
- let promise = subtle
- .encrypt_with_object_and_u8_array(&algo, key, plaintext)
- .map_err(|_| RadrootsClientCryptoError::EncryptFailure)?;
- let value = wasm_bindgen_futures::JsFuture::from(promise)
- .await
- .map_err(|_| RadrootsClientCryptoError::EncryptFailure)?;
- let array = js_sys::Uint8Array::new(&value);
- let mut out = vec![0u8; array.length() as usize];
- array.copy_to(&mut out);
- Ok(out)
-}
-
-#[cfg(target_arch = "wasm32")]
-async fn decrypt_bytes(
- key: &web_sys::CryptoKey,
- iv: &[u8],
- ciphertext: &[u8],
-) -> Result<Vec<u8>, RadrootsClientCryptoError> {
- let subtle = subtle_crypto()?;
- let algo = aes_gcm_params(iv)?;
- let promise = subtle
- .decrypt_with_object_and_u8_array(&algo, key, ciphertext)
- .map_err(|_| RadrootsClientCryptoError::DecryptFailure)?;
- let value = wasm_bindgen_futures::JsFuture::from(promise)
- .await
- .map_err(|_| RadrootsClientCryptoError::DecryptFailure)?;
- let array = js_sys::Uint8Array::new(&value);
- let mut out = vec![0u8; array.length() as usize];
- array.copy_to(&mut out);
- Ok(out)
-}
-
-#[cfg(target_arch = "wasm32")]
-async fn decrypt_bytes_with_algorithm(
- key: &web_sys::CryptoKey,
- algorithm: &str,
- iv: &[u8],
- ciphertext: &[u8],
-) -> Result<Vec<u8>, RadrootsClientCryptoError> {
- let subtle = subtle_crypto()?;
- let algo = algorithm_params(algorithm, iv)?;
- let promise = subtle
- .decrypt_with_object_and_u8_array(&algo, key, ciphertext)
- .map_err(|_| RadrootsClientCryptoError::DecryptFailure)?;
- let value = wasm_bindgen_futures::JsFuture::from(promise)
- .await
- .map_err(|_| RadrootsClientCryptoError::DecryptFailure)?;
- let array = js_sys::Uint8Array::new(&value);
- let mut out = vec![0u8; array.length() as usize];
- array.copy_to(&mut out);
- Ok(out)
-}
-
-#[cfg(test)]
-mod tests {
- use crate::crypto::{
- RadrootsClientCryptoStoreConfig,
- RadrootsClientLegacyKeyConfig,
- RadrootsClientWebCryptoService,
- };
- use crate::idb::RadrootsClientIdbConfig;
-
- use super::{RadrootsClientWebCryptoServiceImpl, DEFAULT_IV_LENGTH};
-
- #[test]
- fn register_store_config_defaults_iv_length() {
- let mut service = RadrootsClientWebCryptoServiceImpl::default();
- service.register_store_config(RadrootsClientCryptoStoreConfig {
- store_id: "store".to_string(),
- legacy_key: None,
- iv_length: None,
- });
- let config = service.resolve_store_config("store");
- assert_eq!(config.iv_length, Some(DEFAULT_IV_LENGTH));
- }
-
- #[test]
- fn register_store_config_merges_updates() {
- let mut service = RadrootsClientWebCryptoServiceImpl::default();
- service.register_store_config(RadrootsClientCryptoStoreConfig {
- store_id: "store".to_string(),
- legacy_key: None,
- iv_length: Some(16),
- });
- let legacy = RadrootsClientLegacyKeyConfig {
- idb_config: RadrootsClientIdbConfig::new("db", "store"),
- key_name: "key".to_string(),
- iv_length: 12,
- algorithm: "AES-GCM".to_string(),
- };
- service.register_store_config(RadrootsClientCryptoStoreConfig {
- store_id: "store".to_string(),
- legacy_key: Some(legacy.clone()),
- iv_length: None,
- });
- let config = service.resolve_store_config("store");
- assert_eq!(config.iv_length, Some(16));
- assert_eq!(config.legacy_key, Some(legacy));
- }
-}
diff --git a/crates/core/src/crypto/types.rs b/crates/core/src/crypto/types.rs
@@ -1,180 +0,0 @@
-use async_trait::async_trait;
-use serde::{Deserialize, Serialize};
-
-use crate::idb::RadrootsClientIdbConfig;
-
-use super::RadrootsClientCryptoError;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "lowercase")]
-pub enum RadrootsClientCryptoKeyStatus {
- Active,
- Rotated,
-}
-
-impl RadrootsClientCryptoKeyStatus {
- pub const fn as_str(self) -> &'static str {
- match self {
- RadrootsClientCryptoKeyStatus::Active => "active",
- RadrootsClientCryptoKeyStatus::Rotated => "rotated",
- }
- }
-
- pub fn parse(value: &str) -> Option<Self> {
- match value {
- "active" => Some(RadrootsClientCryptoKeyStatus::Active),
- "rotated" => Some(RadrootsClientCryptoKeyStatus::Rotated),
- _ => None,
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-pub enum RadrootsClientCryptoAlgorithm {
- #[serde(rename = "AES-GCM")]
- AesGcm,
-}
-
-impl RadrootsClientCryptoAlgorithm {
- pub const fn as_str(self) -> &'static str {
- match self {
- RadrootsClientCryptoAlgorithm::AesGcm => "AES-GCM",
- }
- }
-
- pub fn parse(value: &str) -> Option<Self> {
- match value {
- "AES-GCM" => Some(RadrootsClientCryptoAlgorithm::AesGcm),
- _ => None,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsClientCryptoEnvelope {
- pub version: u8,
- pub key_id: String,
- pub iv: Vec<u8>,
- pub created_at: u64,
- pub ciphertext: Vec<u8>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsClientCryptoKeyEntry {
- pub key_id: String,
- pub store_id: String,
- pub created_at: u64,
- pub status: RadrootsClientCryptoKeyStatus,
- pub wrapped_key: Vec<u8>,
- pub wrap_iv: Vec<u8>,
- pub kdf_salt: Vec<u8>,
- pub kdf_iterations: u32,
- pub iv_length: u32,
- pub algorithm: RadrootsClientCryptoAlgorithm,
- pub provider_id: String,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsClientCryptoStoreIndex {
- pub store_id: String,
- pub active_key_id: String,
- pub key_ids: Vec<String>,
- pub created_at: u64,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsClientCryptoRegistryExport {
- pub stores: Vec<RadrootsClientCryptoStoreIndex>,
- pub keys: Vec<RadrootsClientCryptoKeyEntry>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsClientCryptoDecryptOutcome {
- pub plaintext: Vec<u8>,
- pub needs_reencrypt: bool,
- pub reencrypted: Option<Vec<u8>>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsClientLegacyKeyConfig {
- pub idb_config: RadrootsClientIdbConfig,
- pub key_name: String,
- pub iv_length: u32,
- pub algorithm: String,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsClientCryptoStoreConfig {
- pub store_id: String,
- pub legacy_key: Option<RadrootsClientLegacyKeyConfig>,
- pub iv_length: Option<u32>,
-}
-
-#[async_trait(?Send)]
-pub trait RadrootsClientKeyMaterialProvider {
- async fn get_key_material(&self) -> Result<Vec<u8>, RadrootsClientCryptoError>;
- async fn get_provider_id(&self) -> Result<String, RadrootsClientCryptoError>;
-}
-
-#[async_trait(?Send)]
-pub trait RadrootsClientWebCryptoService {
- fn register_store_config(&mut self, config: RadrootsClientCryptoStoreConfig);
-
- async fn encrypt(
- &self,
- store_id: &str,
- plaintext: &[u8],
- ) -> Result<Vec<u8>, RadrootsClientCryptoError>;
-
- async fn decrypt(
- &self,
- store_id: &str,
- blob: &[u8],
- ) -> Result<Vec<u8>, RadrootsClientCryptoError>;
-
- async fn decrypt_record(
- &self,
- store_id: &str,
- blob: &[u8],
- ) -> Result<RadrootsClientCryptoDecryptOutcome, RadrootsClientCryptoError>;
-
- async fn rotate_store_key(&self, store_id: &str) -> Result<String, RadrootsClientCryptoError>;
-
- async fn export_registry(
- &self,
- ) -> Result<RadrootsClientCryptoRegistryExport, RadrootsClientCryptoError>;
-
- async fn import_registry(
- &self,
- registry: RadrootsClientCryptoRegistryExport,
- ) -> Result<(), RadrootsClientCryptoError>;
-}
-
-#[cfg(test)]
-mod tests {
- use super::{RadrootsClientCryptoAlgorithm, RadrootsClientCryptoKeyStatus};
-
- #[test]
- fn key_status_roundtrip() {
- let active = RadrootsClientCryptoKeyStatus::Active;
- let rotated = RadrootsClientCryptoKeyStatus::Rotated;
-
- assert_eq!(active.as_str(), "active");
- assert_eq!(rotated.as_str(), "rotated");
- assert_eq!(RadrootsClientCryptoKeyStatus::parse("active"), Some(active));
- assert_eq!(RadrootsClientCryptoKeyStatus::parse("rotated"), Some(rotated));
- assert_eq!(RadrootsClientCryptoKeyStatus::parse("unknown"), None);
- }
-
- #[test]
- fn algorithm_roundtrip() {
- let algo = RadrootsClientCryptoAlgorithm::AesGcm;
-
- assert_eq!(algo.as_str(), "AES-GCM");
- assert_eq!(
- RadrootsClientCryptoAlgorithm::parse("AES-GCM"),
- Some(algo)
- );
- assert_eq!(RadrootsClientCryptoAlgorithm::parse("AES-CBC"), None);
- }
-}
diff --git a/crates/core/src/datastore/error.rs b/crates/core/src/datastore/error.rs
@@ -1,49 +0,0 @@
-use std::fmt;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsClientDatastoreError {
- IdbUndefined,
- NoResult,
-}
-
-pub type RadrootsClientDatastoreErrorMessage = &'static str;
-
-impl RadrootsClientDatastoreError {
- pub const fn message(self) -> RadrootsClientDatastoreErrorMessage {
- match self {
- RadrootsClientDatastoreError::IdbUndefined => "error.client.datastore.idb_undefined",
- RadrootsClientDatastoreError::NoResult => "error.client.datastore.no_result",
- }
- }
-}
-
-impl fmt::Display for RadrootsClientDatastoreError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.write_str(self.message())
- }
-}
-
-impl std::error::Error for RadrootsClientDatastoreError {}
-
-#[cfg(test)]
-mod tests {
- use super::RadrootsClientDatastoreError;
-
- #[test]
- fn message_matches_spec() {
- let cases = [
- (
- RadrootsClientDatastoreError::IdbUndefined,
- "error.client.datastore.idb_undefined",
- ),
- (
- RadrootsClientDatastoreError::NoResult,
- "error.client.datastore.no_result",
- ),
- ];
- for (err, expected) in cases {
- assert_eq!(err.message(), expected);
- assert_eq!(err.to_string(), expected);
- }
- }
-}
diff --git a/crates/core/src/datastore/mod.rs b/crates/core/src/datastore/mod.rs
@@ -1,13 +0,0 @@
-pub mod error;
-pub mod types;
-pub mod web;
-
-pub use error::{RadrootsClientDatastoreError, RadrootsClientDatastoreErrorMessage};
-pub use types::{
- RadrootsClientDatastore,
- RadrootsClientDatastoreEntries,
- RadrootsClientDatastoreEntry,
- RadrootsClientDatastoreResult,
- RadrootsClientDatastoreValue,
-};
-pub use web::RadrootsClientWebDatastore;
diff --git a/crates/core/src/datastore/types.rs b/crates/core/src/datastore/types.rs
@@ -1,103 +0,0 @@
-use async_trait::async_trait;
-use serde::de::DeserializeOwned;
-use serde::Serialize;
-
-use crate::backup::RadrootsClientBackupDatastorePayload;
-use crate::idb::RadrootsClientIdbConfig;
-
-use super::RadrootsClientDatastoreError;
-
-pub type RadrootsClientDatastoreValue = Option<String>;
-pub type RadrootsClientDatastoreResult<T> = Result<T, RadrootsClientDatastoreError>;
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsClientDatastoreEntry {
- pub key: String,
- pub value: RadrootsClientDatastoreValue,
-}
-
-impl RadrootsClientDatastoreEntry {
- pub fn new(key: impl Into<String>, value: RadrootsClientDatastoreValue) -> Self {
- Self {
- key: key.into(),
- value,
- }
- }
-}
-
-pub type RadrootsClientDatastoreEntries = Vec<RadrootsClientDatastoreEntry>;
-
-#[async_trait(?Send)]
-pub trait RadrootsClientDatastore {
- fn get_config(&self) -> RadrootsClientIdbConfig;
- fn get_store_id(&self) -> &str;
-
- async fn init(&self) -> RadrootsClientDatastoreResult<()>;
- async fn set(&self, key: &str, value: &str) -> RadrootsClientDatastoreResult<String>;
- async fn get(&self, key: &str) -> RadrootsClientDatastoreResult<String>;
- async fn set_entries(
- &self,
- entries: &[RadrootsClientDatastoreEntry],
- ) -> RadrootsClientDatastoreResult<()> {
- for entry in entries {
- match entry.value.as_deref() {
- Some(value) => {
- let _ = self.set(&entry.key, value).await?;
- }
- None => {
- let _ = self.del(&entry.key).await?;
- }
- }
- }
- Ok(())
- }
- async fn set_obj<T>(&self, key: &str, value: &T) -> RadrootsClientDatastoreResult<T>
- where
- T: Serialize + DeserializeOwned + Clone;
- async fn update_obj<T>(&self, key: &str, value: &T) -> RadrootsClientDatastoreResult<T>
- where
- T: Serialize + DeserializeOwned + Clone;
- async fn get_obj<T>(&self, key: &str) -> RadrootsClientDatastoreResult<T>
- where
- T: DeserializeOwned;
- async fn del_obj(&self, key: &str) -> RadrootsClientDatastoreResult<String>;
- async fn del(&self, key: &str) -> RadrootsClientDatastoreResult<String>;
- async fn del_pref(&self, key_prefix: &str) -> RadrootsClientDatastoreResult<Vec<String>>;
- async fn set_param(
- &self,
- key: &str,
- key_param: &str,
- value: &str,
- ) -> RadrootsClientDatastoreResult<String>;
- async fn get_param(
- &self,
- key: &str,
- key_param: &str,
- ) -> RadrootsClientDatastoreResult<String>;
- async fn keys(&self) -> RadrootsClientDatastoreResult<Vec<String>>;
- async fn entries(&self) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries>;
- async fn entries_pref(
- &self,
- key_prefix: &str,
- ) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries>;
- async fn reset(&self) -> RadrootsClientDatastoreResult<()>;
- async fn export_backup(
- &self,
- ) -> RadrootsClientDatastoreResult<RadrootsClientBackupDatastorePayload>;
- async fn import_backup(
- &self,
- payload: RadrootsClientBackupDatastorePayload,
- ) -> RadrootsClientDatastoreResult<()>;
-}
-
-#[cfg(test)]
-mod tests {
- use super::RadrootsClientDatastoreEntry;
-
- #[test]
- fn entry_builder_preserves_values() {
- let entry = RadrootsClientDatastoreEntry::new("key", Some(String::from("value")));
- assert_eq!(entry.key, "key");
- assert_eq!(entry.value.as_deref(), Some("value"));
- }
-}
diff --git a/crates/core/src/datastore/web.rs b/crates/core/src/datastore/web.rs
@@ -1,577 +0,0 @@
-use async_trait::async_trait;
-use serde::de::DeserializeOwned;
-use serde::Serialize;
-
-use crate::backup::RadrootsClientBackupDatastorePayload;
-#[cfg(target_arch = "wasm32")]
-use crate::crypto::RadrootsClientCryptoError;
-use crate::idb::{IDB_CONFIG_DATASTORE, RadrootsClientIdbConfig};
-#[cfg(target_arch = "wasm32")]
-use crate::idb::RadrootsClientIdbStoreError;
-use crate::idb::{RadrootsClientWebEncryptedStore, RadrootsClientWebEncryptedStoreConfig};
-#[cfg(target_arch = "wasm32")]
-use crate::idb::idb_set_entries;
-
-use super::{
- RadrootsClientDatastore,
- RadrootsClientDatastoreEntry,
- RadrootsClientDatastoreEntries,
- RadrootsClientDatastoreError,
- RadrootsClientDatastoreResult,
-};
-
-const DATASTORE_STORE_PREFIX: &str = "datastore";
-const DEFAULT_IV_LENGTH: u32 = 12;
-
-pub struct RadrootsClientWebDatastore {
- encrypted_store: RadrootsClientWebEncryptedStore,
-}
-
-impl RadrootsClientWebDatastore {
- pub fn new(config: Option<RadrootsClientIdbConfig>) -> Self {
- let idb_config = config.unwrap_or(IDB_CONFIG_DATASTORE);
- let store_id = format!(
- "{}:{}:{}",
- DATASTORE_STORE_PREFIX, idb_config.database, idb_config.store
- );
- let encrypted_store = RadrootsClientWebEncryptedStore::new(
- RadrootsClientWebEncryptedStoreConfig {
- idb_config,
- store_id,
- legacy_key: None,
- iv_length: Some(DEFAULT_IV_LENGTH),
- crypto_service: None,
- },
- );
- Self { encrypted_store }
- }
-
- #[cfg(target_arch = "wasm32")]
- async fn decrypt_value(
- &self,
- store_key: &str,
- stored: crate::idb::RadrootsClientIdbValue,
- ) -> RadrootsClientDatastoreResult<String> {
- if let Some(text) = stored.as_string() {
- let encrypted = self
- .encrypted_store
- .encrypt_bytes(text.as_bytes())
- .await
- .map_err(map_crypto_error)?;
- self.store_encrypted(store_key, &encrypted).await?;
- return Ok(text);
- }
- let Some(bytes) = crate::idb::idb_value_as_bytes(&stored) else {
- return Err(RadrootsClientDatastoreError::NoResult);
- };
- let outcome = self
- .encrypted_store
- .decrypt_record(&bytes)
- .await
- .map_err(map_crypto_error)?;
- if let Some(reencrypted) = outcome.reencrypted {
- self.store_encrypted(store_key, &reencrypted).await?;
- }
- String::from_utf8(outcome.plaintext)
- .map_err(|_| RadrootsClientDatastoreError::NoResult)
- }
-
- #[cfg(target_arch = "wasm32")]
- async fn store_encrypted(
- &self,
- store_key: &str,
- bytes: &[u8],
- ) -> RadrootsClientDatastoreResult<()> {
- let value = js_sys::Uint8Array::from(bytes);
- crate::idb::idb_set(
- self.encrypted_store.get_config().database,
- self.encrypted_store.get_config().store,
- store_key,
- &value.into(),
- )
- .await
- .map_err(map_idb_error)
- }
-
- fn param_key(key: &str, key_param: &str) -> String {
- format!("{key}:{key_param}")
- }
-}
-
-#[async_trait(?Send)]
-impl RadrootsClientDatastore for RadrootsClientWebDatastore {
- fn get_config(&self) -> RadrootsClientIdbConfig {
- self.encrypted_store.get_config()
- }
-
- fn get_store_id(&self) -> &str {
- self.encrypted_store.get_store_id()
- }
-
- async fn init(&self) -> RadrootsClientDatastoreResult<()> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- return Err(RadrootsClientDatastoreError::IdbUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- self.encrypted_store
- .ensure_store()
- .await
- .map_err(map_crypto_error)?;
- Ok(())
- }
- }
-
- async fn set(&self, key: &str, value: &str) -> RadrootsClientDatastoreResult<String> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = (key, value);
- return Err(RadrootsClientDatastoreError::IdbUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- let encrypted = self
- .encrypted_store
- .encrypt_bytes(value.as_bytes())
- .await
- .map_err(map_crypto_error)?;
- self.store_encrypted(key, &encrypted).await?;
- Ok(value.to_string())
- }
- }
-
- async fn get(&self, key: &str) -> RadrootsClientDatastoreResult<String> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = key;
- return Err(RadrootsClientDatastoreError::IdbUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- let stored = crate::idb::idb_get(
- self.encrypted_store.get_config().database,
- self.encrypted_store.get_config().store,
- key,
- )
- .await
- .map_err(map_idb_error)?;
- let Some(stored) = stored else {
- return Err(RadrootsClientDatastoreError::NoResult);
- };
- self.decrypt_value(key, stored).await
- }
- }
-
- async fn set_entries(
- &self,
- entries: &[RadrootsClientDatastoreEntry],
- ) -> RadrootsClientDatastoreResult<()> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = entries;
- return Err(RadrootsClientDatastoreError::IdbUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- let mut encrypted_entries = Vec::with_capacity(entries.len());
- for entry in entries {
- let value = match entry.value.as_deref() {
- Some(value) => {
- let encrypted = self
- .encrypted_store
- .encrypt_bytes(value.as_bytes())
- .await
- .map_err(map_crypto_error)?;
- Some(js_sys::Uint8Array::from(&encrypted[..]).into())
- }
- None => None,
- };
- encrypted_entries.push((entry.key.clone(), value));
- }
- idb_set_entries(
- self.encrypted_store.get_config().database,
- self.encrypted_store.get_config().store,
- &encrypted_entries,
- )
- .await
- .map_err(map_idb_error)?;
- Ok(())
- }
- }
-
- async fn set_obj<T>(&self, key: &str, value: &T) -> RadrootsClientDatastoreResult<T>
- where
- T: Serialize + DeserializeOwned + Clone,
- {
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = (key, value);
- return Err(RadrootsClientDatastoreError::IdbUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- let serialized = serde_json::to_string(value)
- .map_err(|_| RadrootsClientDatastoreError::NoResult)?;
- let encrypted = self
- .encrypted_store
- .encrypt_bytes(serialized.as_bytes())
- .await
- .map_err(map_crypto_error)?;
- self.store_encrypted(key, &encrypted).await?;
- Ok(value.clone())
- }
- }
-
- async fn update_obj<T>(&self, key: &str, value: &T) -> RadrootsClientDatastoreResult<T>
- where
- T: Serialize + DeserializeOwned + Clone,
- {
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = (key, value);
- return Err(RadrootsClientDatastoreError::IdbUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- let stored = crate::idb::idb_get(
- self.encrypted_store.get_config().database,
- self.encrypted_store.get_config().store,
- key,
- )
- .await
- .map_err(map_idb_error)?;
- let mut base = if let Some(stored) = stored {
- let decrypted = self.decrypt_value(key, stored).await?;
- serde_json::from_str(&decrypted)
- .map_err(|_| RadrootsClientDatastoreError::NoResult)?
- } else {
- serde_json::Value::Object(Default::default())
- };
- let update = serde_json::to_value(value)
- .map_err(|_| RadrootsClientDatastoreError::NoResult)?;
- merge_json(&mut base, update);
- let updated: T = serde_json::from_value(base)
- .map_err(|_| RadrootsClientDatastoreError::NoResult)?;
- let serialized = serde_json::to_string(&updated)
- .map_err(|_| RadrootsClientDatastoreError::NoResult)?;
- let encrypted = self
- .encrypted_store
- .encrypt_bytes(serialized.as_bytes())
- .await
- .map_err(map_crypto_error)?;
- self.store_encrypted(key, &encrypted).await?;
- Ok(updated)
- }
- }
-
- async fn get_obj<T>(&self, key: &str) -> RadrootsClientDatastoreResult<T>
- where
- T: DeserializeOwned,
- {
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = key;
- return Err(RadrootsClientDatastoreError::IdbUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- let stored = crate::idb::idb_get(
- self.encrypted_store.get_config().database,
- self.encrypted_store.get_config().store,
- key,
- )
- .await
- .map_err(map_idb_error)?;
- let Some(stored) = stored else {
- return Err(RadrootsClientDatastoreError::NoResult);
- };
- let decrypted = self.decrypt_value(key, stored).await?;
- serde_json::from_str(&decrypted)
- .map_err(|_| RadrootsClientDatastoreError::NoResult)
- }
- }
-
- async fn del_obj(&self, key: &str) -> RadrootsClientDatastoreResult<String> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = key;
- return Err(RadrootsClientDatastoreError::IdbUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- crate::idb::idb_del(
- self.encrypted_store.get_config().database,
- self.encrypted_store.get_config().store,
- key,
- )
- .await
- .map_err(map_idb_error)?;
- Ok(key.to_string())
- }
- }
-
- async fn del(&self, key: &str) -> RadrootsClientDatastoreResult<String> {
- self.del_obj(key).await
- }
-
- async fn del_pref(&self, key_prefix: &str) -> RadrootsClientDatastoreResult<Vec<String>> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = key_prefix;
- return Err(RadrootsClientDatastoreError::IdbUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- let keys = crate::idb::idb_keys(
- self.encrypted_store.get_config().database,
- self.encrypted_store.get_config().store,
- )
- .await
- .map_err(map_idb_error)?;
- let prefixed: Vec<String> = keys
- .into_iter()
- .filter(|key| key.starts_with(key_prefix))
- .collect();
- for key in &prefixed {
- crate::idb::idb_del(
- self.encrypted_store.get_config().database,
- self.encrypted_store.get_config().store,
- key,
- )
- .await
- .map_err(map_idb_error)?;
- }
- Ok(prefixed)
- }
- }
-
- async fn set_param(
- &self,
- key: &str,
- key_param: &str,
- value: &str,
- ) -> RadrootsClientDatastoreResult<String> {
- let store_key = Self::param_key(key, key_param);
- self.set(&store_key, value).await
- }
-
- async fn get_param(
- &self,
- key: &str,
- key_param: &str,
- ) -> RadrootsClientDatastoreResult<String> {
- let store_key = Self::param_key(key, key_param);
- self.get(&store_key).await
- }
-
- async fn keys(&self) -> RadrootsClientDatastoreResult<Vec<String>> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- return Err(RadrootsClientDatastoreError::IdbUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- crate::idb::idb_keys(
- self.encrypted_store.get_config().database,
- self.encrypted_store.get_config().store,
- )
- .await
- .map_err(map_idb_error)
- }
- }
-
- async fn entries(&self) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> {
- self.entries_pref("").await
- }
-
- async fn entries_pref(
- &self,
- key_prefix: &str,
- ) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = key_prefix;
- return Err(RadrootsClientDatastoreError::IdbUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- let keys = crate::idb::idb_keys(
- self.encrypted_store.get_config().database,
- self.encrypted_store.get_config().store,
- )
- .await
- .map_err(map_idb_error)?;
- let prefixed: Vec<String> = keys
- .into_iter()
- .filter(|key| key.starts_with(key_prefix))
- .collect();
- let mut out = Vec::with_capacity(prefixed.len());
- for key in prefixed {
- let stored = crate::idb::idb_get(
- self.encrypted_store.get_config().database,
- self.encrypted_store.get_config().store,
- &key,
- )
- .await
- .map_err(map_idb_error)?;
- let value = if let Some(stored) = stored {
- Some(self.decrypt_value(&key, stored).await?)
- } else {
- None
- };
- out.push(RadrootsClientDatastoreEntry::new(key, value));
- }
- Ok(out)
- }
- }
-
- async fn reset(&self) -> RadrootsClientDatastoreResult<()> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- return Err(RadrootsClientDatastoreError::IdbUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- crate::idb::idb_clear(
- self.encrypted_store.get_config().database,
- self.encrypted_store.get_config().store,
- )
- .await
- .map_err(map_idb_error)?;
- let index = crate::crypto::crypto_registry_get_store_index(
- self.encrypted_store.get_store_id(),
- )
- .await
- .map_err(map_crypto_error)?;
- if let Some(index) = index {
- crate::crypto::crypto_registry_clear_store_index(
- self.encrypted_store.get_store_id(),
- )
- .await
- .map_err(map_crypto_error)?;
- for key_id in index.key_ids {
- crate::crypto::crypto_registry_clear_key_entry(&key_id)
- .await
- .map_err(map_crypto_error)?;
- }
- }
- Ok(())
- }
- }
-
- async fn export_backup(
- &self,
- ) -> RadrootsClientDatastoreResult<RadrootsClientBackupDatastorePayload> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- return Err(RadrootsClientDatastoreError::IdbUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- let keys = self.keys().await?;
- let mut entries = Vec::new();
- for key in keys {
- let stored = crate::idb::idb_get(
- self.encrypted_store.get_config().database,
- self.encrypted_store.get_config().store,
- &key,
- )
- .await
- .map_err(map_idb_error)?;
- let Some(stored) = stored else {
- return Err(RadrootsClientDatastoreError::NoResult);
- };
- let value = self.decrypt_value(&key, stored).await?;
- entries.push(crate::backup::RadrootsClientBackupDatastoreEntry {
- key,
- value,
- });
- }
- Ok(RadrootsClientBackupDatastorePayload { entries })
- }
- }
-
- async fn import_backup(
- &self,
- payload: RadrootsClientBackupDatastorePayload,
- ) -> RadrootsClientDatastoreResult<()> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = payload;
- return Err(RadrootsClientDatastoreError::IdbUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- for entry in payload.entries {
- let encrypted = self
- .encrypted_store
- .encrypt_bytes(entry.value.as_bytes())
- .await
- .map_err(map_crypto_error)?;
- self.store_encrypted(&entry.key, &encrypted).await?;
- }
- Ok(())
- }
- }
-}
-
-#[cfg(target_arch = "wasm32")]
-fn map_crypto_error(err: RadrootsClientCryptoError) -> RadrootsClientDatastoreError {
- match err {
- RadrootsClientCryptoError::IdbUndefined | RadrootsClientCryptoError::CryptoUndefined => {
- RadrootsClientDatastoreError::IdbUndefined
- }
- _ => RadrootsClientDatastoreError::NoResult,
- }
-}
-
-#[cfg(target_arch = "wasm32")]
-fn map_idb_error(err: RadrootsClientIdbStoreError) -> RadrootsClientDatastoreError {
- match err {
- RadrootsClientIdbStoreError::IdbUndefined => RadrootsClientDatastoreError::IdbUndefined,
- _ => RadrootsClientDatastoreError::NoResult,
- }
-}
-
-#[cfg(target_arch = "wasm32")]
-fn merge_json(base: &mut serde_json::Value, update: serde_json::Value) {
- match (base, update) {
- (serde_json::Value::Object(base_map), serde_json::Value::Object(update_map)) => {
- for (key, value) in update_map {
- base_map.insert(key, value);
- }
- }
- (base_value, update_value) => {
- *base_value = update_value;
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::RadrootsClientWebDatastore;
- use crate::datastore::RadrootsClientDatastore;
-
- #[test]
- fn param_key_uses_colon_separator() {
- let key = RadrootsClientWebDatastore::param_key("alpha", "beta");
- assert_eq!(key, "alpha:beta");
- }
-
- #[test]
- fn non_wasm_get_errors() {
- let store = RadrootsClientWebDatastore::new(None);
- let err = futures::executor::block_on(store.get("key"))
- .expect_err("idb undefined");
- assert_eq!(err, crate::datastore::RadrootsClientDatastoreError::IdbUndefined);
- }
-
- #[test]
- fn non_wasm_entries_pref_errors() {
- let store = RadrootsClientWebDatastore::new(None);
- let err = futures::executor::block_on(store.entries_pref("log:"))
- .expect_err("idb undefined");
- assert_eq!(err, crate::datastore::RadrootsClientDatastoreError::IdbUndefined);
- }
-}
diff --git a/crates/core/src/fs/error.rs b/crates/core/src/fs/error.rs
@@ -1,49 +0,0 @@
-use std::fmt;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsClientFsError {
- NotFound,
- RequestFailure,
-}
-
-pub type RadrootsClientFsErrorMessage = &'static str;
-
-impl RadrootsClientFsError {
- pub const fn message(self) -> RadrootsClientFsErrorMessage {
- match self {
- RadrootsClientFsError::NotFound => "error.client.fs.not_found",
- RadrootsClientFsError::RequestFailure => "error.client.fs.request_failure",
- }
- }
-}
-
-impl fmt::Display for RadrootsClientFsError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.write_str(self.message())
- }
-}
-
-impl std::error::Error for RadrootsClientFsError {}
-
-#[cfg(test)]
-mod tests {
- use super::RadrootsClientFsError;
-
- #[test]
- fn message_matches_spec() {
- let cases = [
- (
- RadrootsClientFsError::NotFound,
- "error.client.fs.not_found",
- ),
- (
- RadrootsClientFsError::RequestFailure,
- "error.client.fs.request_failure",
- ),
- ];
- for (err, expected) in cases {
- assert_eq!(err.message(), expected);
- assert_eq!(err.to_string(), expected);
- }
- }
-}
diff --git a/crates/core/src/fs/mod.rs b/crates/core/src/fs/mod.rs
@@ -1,11 +0,0 @@
-pub mod error;
-pub mod types;
-
-pub use error::{RadrootsClientFsError, RadrootsClientFsErrorMessage};
-pub use types::{
- RadrootsClientFs,
- RadrootsClientFsFileInfo,
- RadrootsClientFsOpenResult,
- RadrootsClientFsReadBinResult,
- RadrootsClientFsResult,
-};
diff --git a/crates/core/src/fs/types.rs b/crates/core/src/fs/types.rs
@@ -1,60 +0,0 @@
-use async_trait::async_trait;
-
-use super::RadrootsClientFsError;
-
-pub type RadrootsClientFsResult<T> = Result<T, RadrootsClientFsError>;
-pub type RadrootsClientFsReadBinResult = RadrootsClientFsResult<Vec<u8>>;
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsClientFsOpenResult {
- pub path: String,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsClientFsFileInfo {
- pub size: u64,
- pub is_file: bool,
- pub is_directory: bool,
- pub accessed_at: Option<u64>,
- pub modified_at: Option<u64>,
- pub created_at: Option<u64>,
-}
-
-#[async_trait(?Send)]
-pub trait RadrootsClientFs {
- async fn exists(&self, path: &str) -> RadrootsClientFsResult<bool>;
- async fn open(&self, path: &str) -> RadrootsClientFsResult<RadrootsClientFsOpenResult>;
- async fn info(&self, path: &str) -> RadrootsClientFsResult<RadrootsClientFsFileInfo>;
- async fn read_bin(&self, path: &str) -> RadrootsClientFsReadBinResult;
-}
-
-#[cfg(test)]
-mod tests {
- use super::{RadrootsClientFsFileInfo, RadrootsClientFsOpenResult};
-
- #[test]
- fn file_info_tracks_flags() {
- let info = RadrootsClientFsFileInfo {
- size: 42,
- is_file: true,
- is_directory: false,
- accessed_at: Some(1),
- modified_at: Some(2),
- created_at: None,
- };
- assert!(info.is_file);
- assert!(!info.is_directory);
- assert_eq!(info.size, 42);
- assert_eq!(info.accessed_at, Some(1));
- assert_eq!(info.modified_at, Some(2));
- assert_eq!(info.created_at, None);
- }
-
- #[test]
- fn open_result_preserves_path() {
- let open = RadrootsClientFsOpenResult {
- path: String::from("path"),
- };
- assert_eq!(open.path, "path");
- }
-}
diff --git a/crates/core/src/geolocation/error.rs b/crates/core/src/geolocation/error.rs
@@ -1,85 +0,0 @@
-use std::fmt;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsClientGeolocationError {
- PermissionDenied,
- LocationUnavailable,
- PositionUnavailable,
- Timeout,
- BlockedByPermissionsPolicy,
- UnknownError,
-}
-
-pub type RadrootsClientGeolocationErrorMessage = &'static str;
-
-impl RadrootsClientGeolocationError {
- pub const fn message(self) -> RadrootsClientGeolocationErrorMessage {
- match self {
- RadrootsClientGeolocationError::PermissionDenied => {
- "error.client.geolocation.permission_denied"
- }
- RadrootsClientGeolocationError::LocationUnavailable => {
- "error.client.geolocation.location_unavailable"
- }
- RadrootsClientGeolocationError::PositionUnavailable => {
- "error.client.geolocation.position_unavailable"
- }
- RadrootsClientGeolocationError::Timeout => {
- "error.client.geolocation.timeout"
- }
- RadrootsClientGeolocationError::BlockedByPermissionsPolicy => {
- "error.client.geolocation.blocked_by_permissions_policy"
- }
- RadrootsClientGeolocationError::UnknownError => {
- "error.client.geolocation.unknown_error"
- }
- }
- }
-}
-
-impl fmt::Display for RadrootsClientGeolocationError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.write_str(self.message())
- }
-}
-
-impl std::error::Error for RadrootsClientGeolocationError {}
-
-#[cfg(test)]
-mod tests {
- use super::RadrootsClientGeolocationError;
-
- #[test]
- fn message_matches_spec() {
- let cases = [
- (
- RadrootsClientGeolocationError::PermissionDenied,
- "error.client.geolocation.permission_denied",
- ),
- (
- RadrootsClientGeolocationError::LocationUnavailable,
- "error.client.geolocation.location_unavailable",
- ),
- (
- RadrootsClientGeolocationError::PositionUnavailable,
- "error.client.geolocation.position_unavailable",
- ),
- (
- RadrootsClientGeolocationError::Timeout,
- "error.client.geolocation.timeout",
- ),
- (
- RadrootsClientGeolocationError::BlockedByPermissionsPolicy,
- "error.client.geolocation.blocked_by_permissions_policy",
- ),
- (
- RadrootsClientGeolocationError::UnknownError,
- "error.client.geolocation.unknown_error",
- ),
- ];
- for (err, expected) in cases {
- assert_eq!(err.message(), expected);
- assert_eq!(err.to_string(), expected);
- }
- }
-}
diff --git a/crates/core/src/geolocation/mod.rs b/crates/core/src/geolocation/mod.rs
@@ -1,11 +0,0 @@
-pub mod error;
-pub mod types;
-pub mod web;
-
-pub use error::{RadrootsClientGeolocationError, RadrootsClientGeolocationErrorMessage};
-pub use types::{
- RadrootsClientGeolocation,
- RadrootsClientGeolocationPosition,
- RadrootsClientGeolocationResult,
-};
-pub use web::RadrootsClientWebGeolocation;
diff --git a/crates/core/src/geolocation/types.rs b/crates/core/src/geolocation/types.rs
@@ -1,43 +0,0 @@
-use async_trait::async_trait;
-
-use super::RadrootsClientGeolocationError;
-
-pub type RadrootsClientGeolocationResult<T> =
- Result<T, RadrootsClientGeolocationError>;
-
-#[derive(Debug, Clone, Copy, PartialEq)]
-pub struct RadrootsClientGeolocationPosition {
- pub lat: f64,
- pub lng: f64,
- pub accuracy: Option<f64>,
- pub altitude: Option<f64>,
- pub altitude_accuracy: Option<f64>,
-}
-
-#[async_trait(?Send)]
-pub trait RadrootsClientGeolocation {
- async fn current(
- &self,
- ) -> RadrootsClientGeolocationResult<RadrootsClientGeolocationPosition>;
-}
-
-#[cfg(test)]
-mod tests {
- use super::RadrootsClientGeolocationPosition;
-
- #[test]
- fn position_tracks_optional_fields() {
- let position = RadrootsClientGeolocationPosition {
- lat: 1.0,
- lng: 2.0,
- accuracy: Some(3.0),
- altitude: None,
- altitude_accuracy: Some(4.0),
- };
- assert_eq!(position.lat, 1.0);
- assert_eq!(position.lng, 2.0);
- assert_eq!(position.accuracy, Some(3.0));
- assert_eq!(position.altitude, None);
- assert_eq!(position.altitude_accuracy, Some(4.0));
- }
-}
diff --git a/crates/core/src/geolocation/web.rs b/crates/core/src/geolocation/web.rs
@@ -1,173 +0,0 @@
-use async_trait::async_trait;
-
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen::{JsCast, JsValue};
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen_futures::JsFuture;
-
-use super::{
- RadrootsClientGeolocation,
- RadrootsClientGeolocationError,
- RadrootsClientGeolocationPosition,
- RadrootsClientGeolocationResult,
-};
-
-pub struct RadrootsClientWebGeolocation;
-
-impl RadrootsClientWebGeolocation {
- #[cfg(target_arch = "wasm32")]
- fn policy_allows_geolocation(
- document: &web_sys::Document,
- ) -> Option<bool> {
- let policy = js_sys::Reflect::get(document.as_ref(), &JsValue::from_str("permissionsPolicy"))
- .ok()?;
- if policy.is_null() || policy.is_undefined() {
- return None;
- }
- let allows = js_sys::Reflect::get(&policy, &JsValue::from_str("allowsFeature"))
- .ok()?;
- let allows = allows.dyn_into::<js_sys::Function>().ok()?;
- let result = allows
- .call1(&policy, &JsValue::from_str("geolocation"))
- .ok()?;
- result.as_bool()
- }
-
- #[cfg(target_arch = "wasm32")]
- async fn permission_state(
- navigator: &web_sys::Navigator,
- ) -> Option<web_sys::PermissionState> {
- let permissions = navigator.permissions().ok()?;
- let descriptor = web_sys::PermissionDescriptor::new(web_sys::PermissionName::Geolocation);
- let promise = permissions.query(&descriptor).ok()?;
- let status = JsFuture::from(promise).await.ok()?;
- let status: web_sys::PermissionStatus = status.dyn_into().ok()?;
- Some(status.state())
- }
-
- #[cfg(target_arch = "wasm32")]
- async fn get_current_position(
- geolocation: &web_sys::Geolocation,
- ) -> Result<web_sys::Position, JsValue> {
- let geolocation = geolocation.clone();
- let promise = js_sys::Promise::new(&mut |resolve, reject| {
- let success = wasm_bindgen::closure::Closure::once(
- move |position: web_sys::Position| {
- let _ = resolve.call1(&JsValue::NULL, &position);
- },
- );
- let reject_failure = reject.clone();
- let failure = wasm_bindgen::closure::Closure::once(
- move |error: web_sys::PositionError| {
- let _ = reject_failure.call1(&JsValue::NULL, &error);
- },
- );
- let options = web_sys::PositionOptions::new();
- options.set_enable_high_accuracy(true);
- options.set_timeout(10_000);
- options.set_maximum_age(30_000);
- if geolocation
- .get_current_position_with_error_callback_and_options(
- success.as_ref().unchecked_ref(),
- Some(failure.as_ref().unchecked_ref()),
- &options,
- )
- .is_err()
- {
- let _ = reject.call0(&JsValue::NULL);
- }
- success.forget();
- failure.forget();
- });
- let value = JsFuture::from(promise).await?;
- value.dyn_into::<web_sys::Position>()
- }
-
- #[cfg(target_arch = "wasm32")]
- fn map_error(
- policy_allows: Option<bool>,
- error: &web_sys::PositionError,
- ) -> RadrootsClientGeolocationError {
- match error.code() {
- web_sys::PositionError::PERMISSION_DENIED => {
- if policy_allows == Some(false) {
- RadrootsClientGeolocationError::BlockedByPermissionsPolicy
- } else {
- RadrootsClientGeolocationError::PermissionDenied
- }
- }
- web_sys::PositionError::POSITION_UNAVAILABLE => {
- RadrootsClientGeolocationError::PositionUnavailable
- }
- web_sys::PositionError::TIMEOUT => RadrootsClientGeolocationError::Timeout,
- _ => RadrootsClientGeolocationError::UnknownError,
- }
- }
-}
-
-#[async_trait(?Send)]
-impl RadrootsClientGeolocation for RadrootsClientWebGeolocation {
- async fn current(
- &self,
- ) -> RadrootsClientGeolocationResult<RadrootsClientGeolocationPosition> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- return Err(RadrootsClientGeolocationError::LocationUnavailable);
- }
- #[cfg(target_arch = "wasm32")]
- {
- let window =
- web_sys::window().ok_or(RadrootsClientGeolocationError::LocationUnavailable)?;
- let document = window
- .document()
- .ok_or(RadrootsClientGeolocationError::LocationUnavailable)?;
- let navigator = window.navigator();
- let geolocation = navigator
- .geolocation()
- .map_err(|_| RadrootsClientGeolocationError::LocationUnavailable)?;
-
- let policy_allows = Self::policy_allows_geolocation(&document);
- let _ = Self::permission_state(&navigator).await;
-
- if policy_allows == Some(false) {
- return Err(RadrootsClientGeolocationError::BlockedByPermissionsPolicy);
- }
-
- match Self::get_current_position(&geolocation).await {
- Ok(position) => {
- let coords = position.coords();
- Ok(RadrootsClientGeolocationPosition {
- lat: coords.latitude(),
- lng: coords.longitude(),
- accuracy: Some(coords.accuracy()),
- altitude: coords.altitude(),
- altitude_accuracy: coords.altitude_accuracy(),
- })
- }
- Err(err) => {
- if let Ok(position_error) = err.dyn_into::<web_sys::PositionError>() {
- return Err(Self::map_error(policy_allows, &position_error));
- }
- Err(RadrootsClientGeolocationError::UnknownError)
- }
- }
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::RadrootsClientWebGeolocation;
- use crate::geolocation::{
- RadrootsClientGeolocation,
- RadrootsClientGeolocationError,
- };
-
- #[test]
- fn non_wasm_current_errors() {
- let geo = RadrootsClientWebGeolocation;
- let err = futures::executor::block_on(geo.current())
- .expect_err("location unavailable");
- assert_eq!(err, RadrootsClientGeolocationError::LocationUnavailable);
- }
-}
diff --git a/crates/core/src/idb/config.rs b/crates/core/src/idb/config.rs
@@ -1,81 +0,0 @@
-use super::RadrootsClientIdbConfig;
-
-pub const RADROOTS_IDB_DATABASE: &str = "radroots-app-v1";
-
-pub const IDB_STORE_DATASTORE: &str = "radroots.app.data";
-pub const IDB_STORE_LOGS: &str = "radroots.app.logs";
-pub const IDB_STORE_KEYSTORE: &str = "radroots.security.keystore";
-pub const IDB_STORE_KEYSTORE_NOSTR: &str = "radroots.security.keystore.nostr";
-pub const IDB_STORE_CRYPTO_REGISTRY: &str = "radroots.security.crypto.registry";
-pub const IDB_STORE_CIPHER_AES_GCM: &str = "radroots.security.cipher.aes-gcm";
-pub const IDB_STORE_CIPHER_SQL: &str = "radroots.security.cipher.sql";
-pub const IDB_STORE_TANGLE: &str = "radroots.storage.tangle.sql";
-pub const IDB_STORE_CIPHER_SUFFIX: &str = ".cipher";
-
-pub const IDB_STORE_KEYSTORE_CIPHER: &str = "radroots.security.keystore.cipher";
-pub const IDB_STORE_KEYSTORE_NOSTR_CIPHER: &str = "radroots.security.keystore.nostr.cipher";
-
-pub const IDB_CONFIG_DATASTORE: RadrootsClientIdbConfig =
- RadrootsClientIdbConfig::new(RADROOTS_IDB_DATABASE, IDB_STORE_DATASTORE);
-pub const IDB_CONFIG_LOGS: RadrootsClientIdbConfig =
- RadrootsClientIdbConfig::new(RADROOTS_IDB_DATABASE, IDB_STORE_LOGS);
-pub const IDB_CONFIG_KEYSTORE: RadrootsClientIdbConfig =
- RadrootsClientIdbConfig::new(RADROOTS_IDB_DATABASE, IDB_STORE_KEYSTORE);
-pub const IDB_CONFIG_KEYSTORE_NOSTR: RadrootsClientIdbConfig =
- RadrootsClientIdbConfig::new(RADROOTS_IDB_DATABASE, IDB_STORE_KEYSTORE_NOSTR);
-pub const IDB_CONFIG_CRYPTO_REGISTRY: RadrootsClientIdbConfig =
- RadrootsClientIdbConfig::new(RADROOTS_IDB_DATABASE, IDB_STORE_CRYPTO_REGISTRY);
-pub const IDB_CONFIG_CIPHER_AES_GCM: RadrootsClientIdbConfig =
- RadrootsClientIdbConfig::new(RADROOTS_IDB_DATABASE, IDB_STORE_CIPHER_AES_GCM);
-pub const IDB_CONFIG_CIPHER_SQL: RadrootsClientIdbConfig =
- RadrootsClientIdbConfig::new(RADROOTS_IDB_DATABASE, IDB_STORE_CIPHER_SQL);
-pub const IDB_CONFIG_TANGLE: RadrootsClientIdbConfig =
- RadrootsClientIdbConfig::new(RADROOTS_IDB_DATABASE, IDB_STORE_TANGLE);
-
-pub const RADROOTS_IDB_CONFIGS: &[RadrootsClientIdbConfig] = &[
- IDB_CONFIG_DATASTORE,
- IDB_CONFIG_LOGS,
- IDB_CONFIG_KEYSTORE,
- IDB_CONFIG_KEYSTORE_NOSTR,
- IDB_CONFIG_CRYPTO_REGISTRY,
- IDB_CONFIG_CIPHER_AES_GCM,
- IDB_CONFIG_CIPHER_SQL,
- IDB_CONFIG_TANGLE,
-];
-
-pub const RADROOTS_IDB_STORES: &[&str] = &[
- IDB_STORE_DATASTORE,
- IDB_STORE_LOGS,
- IDB_STORE_KEYSTORE,
- IDB_STORE_KEYSTORE_NOSTR,
- IDB_STORE_CRYPTO_REGISTRY,
- IDB_STORE_CIPHER_AES_GCM,
- IDB_STORE_CIPHER_SQL,
- IDB_STORE_TANGLE,
- IDB_STORE_KEYSTORE_CIPHER,
- IDB_STORE_KEYSTORE_NOSTR_CIPHER,
-];
-
-#[cfg(test)]
-mod tests {
- use super::{
- IDB_STORE_KEYSTORE_CIPHER,
- IDB_STORE_KEYSTORE_NOSTR_CIPHER,
- RADROOTS_IDB_CONFIGS,
- RADROOTS_IDB_DATABASE,
- RADROOTS_IDB_STORES,
- };
-
- #[test]
- fn configs_share_database_name() {
- for config in RADROOTS_IDB_CONFIGS {
- assert_eq!(config.database, RADROOTS_IDB_DATABASE);
- }
- }
-
- #[test]
- fn stores_include_cipher_variants() {
- assert!(RADROOTS_IDB_STORES.contains(&IDB_STORE_KEYSTORE_CIPHER));
- assert!(RADROOTS_IDB_STORES.contains(&IDB_STORE_KEYSTORE_NOSTR_CIPHER));
- }
-}
diff --git a/crates/core/src/idb/encrypted_store.rs b/crates/core/src/idb/encrypted_store.rs
@@ -1,115 +0,0 @@
-use crate::crypto::{
- RadrootsClientCryptoDecryptOutcome,
- RadrootsClientCryptoError,
- RadrootsClientCryptoStoreConfig,
- RadrootsClientLegacyKeyConfig,
- RadrootsClientWebCryptoService,
- RadrootsClientWebCryptoServiceImpl,
-};
-use crate::idb::{idb_store_ensure, RadrootsClientIdbConfig, RadrootsClientIdbStoreError};
-
-pub struct RadrootsClientWebEncryptedStoreConfig {
- pub idb_config: RadrootsClientIdbConfig,
- pub store_id: String,
- pub legacy_key: Option<RadrootsClientLegacyKeyConfig>,
- pub iv_length: Option<u32>,
- pub crypto_service: Option<Box<dyn RadrootsClientWebCryptoService>>,
-}
-
-pub struct RadrootsClientWebEncryptedStore {
- config: RadrootsClientIdbConfig,
- store_id: String,
- crypto: Box<dyn RadrootsClientWebCryptoService>,
-}
-
-impl RadrootsClientWebEncryptedStore {
- pub fn new(config: RadrootsClientWebEncryptedStoreConfig) -> Self {
- let mut crypto = config
- .crypto_service
- .unwrap_or_else(|| Box::new(RadrootsClientWebCryptoServiceImpl::default()));
- let store_config = RadrootsClientCryptoStoreConfig {
- store_id: config.store_id.clone(),
- legacy_key: config.legacy_key,
- iv_length: config.iv_length,
- };
- crypto.register_store_config(store_config);
- Self {
- config: config.idb_config,
- store_id: config.store_id,
- crypto,
- }
- }
-
- pub fn get_config(&self) -> RadrootsClientIdbConfig {
- self.config
- }
-
- pub fn get_store_id(&self) -> &str {
- &self.store_id
- }
-
- pub async fn ensure_store(&self) -> Result<(), RadrootsClientCryptoError> {
- idb_store_ensure(self.config.database, self.config.store)
- .await
- .map_err(map_idb_error)
- }
-
- pub async fn encrypt_bytes(
- &self,
- bytes: &[u8],
- ) -> Result<Vec<u8>, RadrootsClientCryptoError> {
- self.crypto.encrypt(&self.store_id, bytes).await
- }
-
- pub async fn decrypt_record(
- &self,
- blob: &[u8],
- ) -> Result<RadrootsClientCryptoDecryptOutcome, RadrootsClientCryptoError> {
- self.crypto.decrypt_record(&self.store_id, blob).await
- }
-}
-
-fn map_idb_error(err: RadrootsClientIdbStoreError) -> RadrootsClientCryptoError {
- match err {
- RadrootsClientIdbStoreError::IdbUndefined => RadrootsClientCryptoError::IdbUndefined,
- _ => RadrootsClientCryptoError::RegistryFailure,
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{RadrootsClientWebEncryptedStore, RadrootsClientWebEncryptedStoreConfig};
- use crate::crypto::RadrootsClientCryptoError;
- use crate::idb::RadrootsClientIdbConfig;
-
- #[test]
- fn encrypted_store_exposes_ids() {
- let config = RadrootsClientWebEncryptedStoreConfig {
- idb_config: RadrootsClientIdbConfig::new("db", "store"),
- store_id: "store-id".to_string(),
- legacy_key: None,
- iv_length: None,
- crypto_service: None,
- };
- let store = RadrootsClientWebEncryptedStore::new(config);
- let idb_config = store.get_config();
- assert_eq!(idb_config.database, "db");
- assert_eq!(idb_config.store, "store");
- assert_eq!(store.get_store_id(), "store-id");
- }
-
- #[test]
- fn non_wasm_store_ensure_errors() {
- let config = RadrootsClientWebEncryptedStoreConfig {
- idb_config: RadrootsClientIdbConfig::new("db", "store"),
- store_id: "store-id".to_string(),
- legacy_key: None,
- iv_length: None,
- crypto_service: None,
- };
- let store = RadrootsClientWebEncryptedStore::new(config);
- let err = futures::executor::block_on(store.ensure_store())
- .expect_err("idb undefined");
- assert_eq!(err, RadrootsClientCryptoError::IdbUndefined);
- }
-}
diff --git a/crates/core/src/idb/error.rs b/crates/core/src/idb/error.rs
@@ -1,57 +0,0 @@
-use std::fmt;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsClientIdbStoreError {
- IdbUndefined,
- OperationFailure,
- VersionError,
-}
-
-pub type RadrootsClientIdbStoreErrorMessage = &'static str;
-
-impl RadrootsClientIdbStoreError {
- pub const fn message(self) -> RadrootsClientIdbStoreErrorMessage {
- match self {
- RadrootsClientIdbStoreError::IdbUndefined => "error.client.idb.idb_undefined",
- RadrootsClientIdbStoreError::OperationFailure => {
- "error.client.idb.operation_failure"
- }
- RadrootsClientIdbStoreError::VersionError => "error.client.idb.version_error",
- }
- }
-}
-
-impl fmt::Display for RadrootsClientIdbStoreError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.write_str(self.message())
- }
-}
-
-impl std::error::Error for RadrootsClientIdbStoreError {}
-
-#[cfg(test)]
-mod tests {
- use super::RadrootsClientIdbStoreError;
-
- #[test]
- fn message_matches_spec() {
- let cases = [
- (
- RadrootsClientIdbStoreError::IdbUndefined,
- "error.client.idb.idb_undefined",
- ),
- (
- RadrootsClientIdbStoreError::OperationFailure,
- "error.client.idb.operation_failure",
- ),
- (
- RadrootsClientIdbStoreError::VersionError,
- "error.client.idb.version_error",
- ),
- ];
- for (err, expected) in cases {
- assert_eq!(err.message(), expected);
- assert_eq!(err.to_string(), expected);
- }
- }
-}
diff --git a/crates/core/src/idb/keyval.rs b/crates/core/src/idb/keyval.rs
@@ -1,296 +0,0 @@
-use super::{RadrootsClientIdbStoreError, RadrootsClientIdbValue};
-
-#[cfg(target_arch = "wasm32")]
-use js_sys::{Array, Promise};
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen::closure::Closure;
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen::{JsCast, JsValue};
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen_futures::JsFuture;
-#[cfg(target_arch = "wasm32")]
-use web_sys::{IdbRequest, IdbTransaction, IdbTransactionMode};
-
-#[cfg(target_arch = "wasm32")]
-use super::store::idb_open;
-
-#[cfg(target_arch = "wasm32")]
-async fn idb_request(request: IdbRequest) -> Result<JsValue, RadrootsClientIdbStoreError> {
- let promise = Promise::new(&mut |resolve, reject| {
- let request_success = request.clone();
- let resolve = resolve.clone();
- let reject_success = reject.clone();
- let on_success = Closure::wrap(Box::new(move |_event: web_sys::Event| {
- match request_success.result() {
- Ok(value) => {
- let _ = resolve.call1(&JsValue::UNDEFINED, &value);
- }
- Err(err) => {
- let _ = reject_success.call1(&JsValue::UNDEFINED, &err);
- }
- }
- }) as Box<dyn FnMut(_)>);
- request.set_onsuccess(Some(on_success.as_ref().unchecked_ref()));
- on_success.forget();
-
- let request_error = request.clone();
- let reject_error = reject.clone();
- let on_error = Closure::wrap(Box::new(move |_event: web_sys::Event| {
- let err = request_error
- .error()
- .map(JsValue::from)
- .unwrap_or_else(|_| JsValue::from_str("idb_request_failed"));
- let _ = reject_error.call1(&JsValue::UNDEFINED, &err);
- }) as Box<dyn FnMut(_)>);
- request.set_onerror(Some(on_error.as_ref().unchecked_ref()));
- on_error.forget();
- });
- JsFuture::from(promise)
- .await
- .map_err(|_| RadrootsClientIdbStoreError::OperationFailure)
-}
-
-#[cfg(target_arch = "wasm32")]
-async fn idb_store_request(
- database: &str,
- store: &str,
- mode: IdbTransactionMode,
- build_request: impl FnOnce(web_sys::IdbObjectStore) -> Result<IdbRequest, JsValue>,
-) -> Result<JsValue, RadrootsClientIdbStoreError> {
- let db = idb_open(database, None, &[]).await?;
- let transaction = db
- .transaction_with_str_and_mode(store, mode)
- .map_err(|_| RadrootsClientIdbStoreError::OperationFailure)?;
- let object_store = transaction
- .object_store(store)
- .map_err(|_| RadrootsClientIdbStoreError::OperationFailure)?;
- let request = build_request(object_store)
- .map_err(|_| RadrootsClientIdbStoreError::OperationFailure)?;
- let value = idb_request(request).await?;
- db.close();
- Ok(value)
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn idb_get(
- database: &str,
- store: &str,
- key: &str,
-) -> Result<Option<RadrootsClientIdbValue>, RadrootsClientIdbStoreError> {
- let value = idb_store_request(database, store, IdbTransactionMode::Readonly, |object_store| {
- object_store.get(&JsValue::from_str(key))
- })
- .await?;
- if value.is_null() || value.is_undefined() {
- return Ok(None);
- }
- Ok(Some(value))
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn idb_set(
- database: &str,
- store: &str,
- key: &str,
- value: &RadrootsClientIdbValue,
-) -> Result<(), RadrootsClientIdbStoreError> {
- let _ = idb_store_request(database, store, IdbTransactionMode::Readwrite, |object_store| {
- object_store.put_with_key(value, &JsValue::from_str(key))
- })
- .await?;
- Ok(())
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn idb_set_entries(
- database: &str,
- store: &str,
- entries: &[(String, Option<RadrootsClientIdbValue>)],
-) -> Result<(), RadrootsClientIdbStoreError> {
- let db = idb_open(database, None, &[]).await?;
- let transaction = db
- .transaction_with_str_and_mode(store, IdbTransactionMode::Readwrite)
- .map_err(|_| RadrootsClientIdbStoreError::OperationFailure)?;
- let object_store = transaction
- .object_store(store)
- .map_err(|_| RadrootsClientIdbStoreError::OperationFailure)?;
- let promise = idb_transaction_complete(transaction.clone())?;
- for (key, value) in entries {
- let key = JsValue::from_str(key);
- match value {
- Some(value) => {
- object_store
- .put_with_key(value, &key)
- .map_err(|_| RadrootsClientIdbStoreError::OperationFailure)?;
- }
- None => {
- object_store
- .delete(&key)
- .map_err(|_| RadrootsClientIdbStoreError::OperationFailure)?;
- }
- }
- }
- JsFuture::from(promise)
- .await
- .map_err(|_| RadrootsClientIdbStoreError::OperationFailure)?;
- db.close();
- Ok(())
-}
-
-#[cfg(target_arch = "wasm32")]
-fn idb_transaction_complete(
- transaction: IdbTransaction,
-) -> Result<Promise, RadrootsClientIdbStoreError> {
- let promise = Promise::new(&mut |resolve, reject| {
- let resolve = resolve.clone();
- let on_complete = Closure::wrap(Box::new(move |_event: web_sys::Event| {
- let _ = resolve.call0(&JsValue::UNDEFINED);
- }) as Box<dyn FnMut(_)>);
- transaction.set_oncomplete(Some(on_complete.as_ref().unchecked_ref()));
- on_complete.forget();
-
- let reject = reject.clone();
- let on_error = Closure::wrap(Box::new(move |_event: web_sys::Event| {
- let _ = reject.call1(&JsValue::UNDEFINED, &JsValue::from_str("idb_tx_failed"));
- }) as Box<dyn FnMut(_)>);
- transaction.set_onerror(Some(on_error.as_ref().unchecked_ref()));
- transaction.set_onabort(Some(on_error.as_ref().unchecked_ref()));
- on_error.forget();
- });
- Ok(promise)
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn idb_del(
- database: &str,
- store: &str,
- key: &str,
-) -> Result<(), RadrootsClientIdbStoreError> {
- let _ = idb_store_request(database, store, IdbTransactionMode::Readwrite, |object_store| {
- object_store.delete(&JsValue::from_str(key))
- })
- .await?;
- Ok(())
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn idb_clear(
- database: &str,
- store: &str,
-) -> Result<(), RadrootsClientIdbStoreError> {
- let _ = idb_store_request(database, store, IdbTransactionMode::Readwrite, |object_store| {
- object_store.clear()
- })
- .await?;
- Ok(())
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn idb_keys(
- database: &str,
- store: &str,
-) -> Result<Vec<String>, RadrootsClientIdbStoreError> {
- let value = idb_store_request(database, store, IdbTransactionMode::Readonly, |object_store| {
- object_store.get_all_keys()
- })
- .await?;
- let array = Array::from(&value);
- let mut out = Vec::new();
- for entry in array.iter() {
- if let Some(key) = entry.as_string() {
- out.push(key);
- }
- }
- Ok(out)
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub async fn idb_get(
- _database: &str,
- _store: &str,
- _key: &str,
-) -> Result<Option<RadrootsClientIdbValue>, RadrootsClientIdbStoreError> {
- Err(RadrootsClientIdbStoreError::IdbUndefined)
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub async fn idb_set(
- _database: &str,
- _store: &str,
- _key: &str,
- _value: &RadrootsClientIdbValue,
-) -> Result<(), RadrootsClientIdbStoreError> {
- Err(RadrootsClientIdbStoreError::IdbUndefined)
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub async fn idb_set_entries(
- _database: &str,
- _store: &str,
- _entries: &[(String, Option<RadrootsClientIdbValue>)],
-) -> Result<(), RadrootsClientIdbStoreError> {
- Err(RadrootsClientIdbStoreError::IdbUndefined)
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub async fn idb_del(
- _database: &str,
- _store: &str,
- _key: &str,
-) -> Result<(), RadrootsClientIdbStoreError> {
- Err(RadrootsClientIdbStoreError::IdbUndefined)
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub async fn idb_clear(
- _database: &str,
- _store: &str,
-) -> Result<(), RadrootsClientIdbStoreError> {
- Err(RadrootsClientIdbStoreError::IdbUndefined)
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub async fn idb_keys(
- _database: &str,
- _store: &str,
-) -> Result<Vec<String>, RadrootsClientIdbStoreError> {
- Err(RadrootsClientIdbStoreError::IdbUndefined)
-}
-
-#[cfg(test)]
-mod tests {
- use super::{idb_clear, idb_del, idb_get, idb_keys, idb_set, idb_set_entries};
- use crate::idb::RadrootsClientIdbStoreError;
-
- #[test]
- fn non_wasm_keyval_returns_idb_undefined() {
- let err = futures::executor::block_on(idb_get("db", "store", "key"))
- .expect_err("idb undefined");
- assert_eq!(err, RadrootsClientIdbStoreError::IdbUndefined);
- }
-
- #[test]
- fn non_wasm_keyval_batch_returns_idb_undefined() {
- let entries = Vec::new();
- let err = futures::executor::block_on(idb_set_entries("db", "store", &entries))
- .expect_err("idb undefined");
- assert_eq!(err, RadrootsClientIdbStoreError::IdbUndefined);
- }
-
- #[test]
- fn non_wasm_keyval_mutations_return_idb_undefined() {
- let value = ();
- let err = futures::executor::block_on(idb_set("db", "store", "key", &value))
- .expect_err("idb undefined");
- assert_eq!(err, RadrootsClientIdbStoreError::IdbUndefined);
- let err = futures::executor::block_on(idb_del("db", "store", "key"))
- .expect_err("idb undefined");
- assert_eq!(err, RadrootsClientIdbStoreError::IdbUndefined);
- let err = futures::executor::block_on(idb_clear("db", "store"))
- .expect_err("idb undefined");
- assert_eq!(err, RadrootsClientIdbStoreError::IdbUndefined);
- let err = futures::executor::block_on(idb_keys("db", "store"))
- .expect_err("idb undefined");
- assert_eq!(err, RadrootsClientIdbStoreError::IdbUndefined);
- }
-}
diff --git a/crates/core/src/idb/mod.rs b/crates/core/src/idb/mod.rs
@@ -1,41 +0,0 @@
-pub mod config;
-pub mod encrypted_store;
-pub mod error;
-pub mod keyval;
-pub mod store;
-pub mod types;
-pub mod value;
-
-pub use config::{
- IDB_CONFIG_CIPHER_AES_GCM,
- IDB_CONFIG_CIPHER_SQL,
- IDB_CONFIG_CRYPTO_REGISTRY,
- IDB_CONFIG_DATASTORE,
- IDB_CONFIG_LOGS,
- IDB_CONFIG_KEYSTORE,
- IDB_CONFIG_KEYSTORE_NOSTR,
- IDB_CONFIG_TANGLE,
- IDB_STORE_CIPHER_AES_GCM,
- IDB_STORE_CIPHER_SQL,
- IDB_STORE_CIPHER_SUFFIX,
- IDB_STORE_CRYPTO_REGISTRY,
- IDB_STORE_DATASTORE,
- IDB_STORE_LOGS,
- IDB_STORE_KEYSTORE,
- IDB_STORE_KEYSTORE_CIPHER,
- IDB_STORE_KEYSTORE_NOSTR,
- IDB_STORE_KEYSTORE_NOSTR_CIPHER,
- IDB_STORE_TANGLE,
- RADROOTS_IDB_CONFIGS,
- RADROOTS_IDB_DATABASE,
- RADROOTS_IDB_STORES,
-};
-pub use types::RadrootsClientIdbConfig;
-pub use value::{idb_value_as_bytes, RadrootsClientIdbValue};
-pub use error::{RadrootsClientIdbStoreError, RadrootsClientIdbStoreErrorMessage};
-pub use keyval::{idb_clear, idb_del, idb_get, idb_keys, idb_set, idb_set_entries};
-pub use encrypted_store::{
- RadrootsClientWebEncryptedStore,
- RadrootsClientWebEncryptedStoreConfig,
-};
-pub use store::{idb_store_bootstrap, idb_store_ensure, idb_store_exists};
diff --git a/crates/core/src/idb/store.rs b/crates/core/src/idb/store.rs
@@ -1,304 +0,0 @@
-#[cfg(target_arch = "wasm32")]
-use crate::idb::{RADROOTS_IDB_DATABASE, RADROOTS_IDB_STORES};
-
-use super::RadrootsClientIdbStoreError;
-
-#[cfg(target_arch = "wasm32")]
-use js_sys::{Array, Function, Promise, Reflect};
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen::closure::Closure;
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen::{JsCast, JsValue};
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen_futures::JsFuture;
-#[cfg(target_arch = "wasm32")]
-use web_sys::{IdbDatabase, IdbFactory};
-
-#[cfg(target_arch = "wasm32")]
-fn idb_factory() -> Result<IdbFactory, RadrootsClientIdbStoreError> {
- let window = web_sys::window().ok_or(RadrootsClientIdbStoreError::IdbUndefined)?;
- let factory = window
- .indexed_db()
- .map_err(|_| RadrootsClientIdbStoreError::OperationFailure)?;
- factory.ok_or(RadrootsClientIdbStoreError::IdbUndefined)
-}
-
-#[cfg(target_arch = "wasm32")]
-async fn idb_database_exists(
- factory: &IdbFactory,
- database: &str,
-) -> Result<bool, RadrootsClientIdbStoreError> {
- let databases = Reflect::get(factory.as_ref(), &JsValue::from_str("databases"))
- .ok()
- .and_then(|value| value.dyn_into::<Function>().ok());
- let Some(databases) = databases else {
- return Ok(true);
- };
- let promise = databases
- .call0(factory.as_ref())
- .map_err(|_| RadrootsClientIdbStoreError::OperationFailure)?;
- let promise: Promise = promise
- .dyn_into()
- .map_err(|_| RadrootsClientIdbStoreError::OperationFailure)?;
- let value = JsFuture::from(promise)
- .await
- .map_err(|_| RadrootsClientIdbStoreError::OperationFailure)?;
- let list = Array::from(&value);
- for entry in list.iter() {
- let name = Reflect::get(&entry, &JsValue::from_str("name"))
- .ok()
- .and_then(|value| value.as_string());
- if name.as_deref() == Some(database) {
- return Ok(true);
- }
- }
- Ok(false)
-}
-
-#[cfg(target_arch = "wasm32")]
-fn idb_missing_stores(db: &IdbDatabase, stores: &[String]) -> Vec<String> {
- let names = db.object_store_names();
- stores
- .iter()
- .filter(|store| !names.contains(store))
- .cloned()
- .collect()
-}
-
-#[cfg(target_arch = "wasm32")]
-fn map_open_error(err: JsValue) -> RadrootsClientIdbStoreError {
- let Some(exception) = err.dyn_ref::<web_sys::DomException>() else {
- return RadrootsClientIdbStoreError::OperationFailure;
- };
- if exception.name() == "VersionError" {
- RadrootsClientIdbStoreError::VersionError
- } else {
- RadrootsClientIdbStoreError::OperationFailure
- }
-}
-
-#[cfg(target_arch = "wasm32")]
-pub(crate) async fn idb_open(
- database: &str,
- version: Option<u32>,
- stores: &[String],
-) -> Result<IdbDatabase, RadrootsClientIdbStoreError> {
- let factory = idb_factory()?;
- let request = match version {
- Some(version) => factory
- .open_with_u32(database, version)
- .map_err(|_| RadrootsClientIdbStoreError::OperationFailure)?,
- None => factory
- .open(database)
- .map_err(|_| RadrootsClientIdbStoreError::OperationFailure)?,
- };
- let stores = stores.to_vec();
- let promise = Promise::new(&mut |resolve, reject| {
- let request_success = request.clone();
- let resolve = resolve.clone();
- let reject_success = reject.clone();
- let on_success = Closure::wrap(Box::new(move |_event: web_sys::Event| {
- match request_success.result() {
- Ok(value) => {
- let _ = resolve.call1(&JsValue::UNDEFINED, &value);
- }
- Err(err) => {
- let _ = reject_success.call1(&JsValue::UNDEFINED, &err);
- }
- }
- }) as Box<dyn FnMut(_)>);
- request.set_onsuccess(Some(on_success.as_ref().unchecked_ref()));
- on_success.forget();
-
- let request_error = request.clone();
- let reject_error = reject.clone();
- let on_error = Closure::wrap(Box::new(move |_event: web_sys::Event| {
- let err = request_error
- .error()
- .map(JsValue::from)
- .unwrap_or_else(|_| JsValue::from_str("idb_open_failed"));
- let _ = reject_error.call1(&JsValue::UNDEFINED, &err);
- }) as Box<dyn FnMut(_)>);
- request.set_onerror(Some(on_error.as_ref().unchecked_ref()));
- on_error.forget();
-
- let request_upgrade = request.clone();
- let stores_upgrade = stores.clone();
- let reject_upgrade = reject.clone();
- let on_upgrade = Closure::wrap(Box::new(move |_event: web_sys::Event| {
- if stores_upgrade.is_empty() {
- return;
- }
- let Ok(value) = request_upgrade.result() else {
- let _ = reject_upgrade.call1(
- &JsValue::UNDEFINED,
- &JsValue::from_str("idb_open_failed"),
- );
- return;
- };
- let Ok(db) = value.dyn_into::<IdbDatabase>() else {
- let _ = reject_upgrade.call1(
- &JsValue::UNDEFINED,
- &JsValue::from_str("idb_open_failed"),
- );
- return;
- };
- let names = db.object_store_names();
- for store in &stores_upgrade {
- if names.contains(store) {
- continue;
- }
- if db.create_object_store(store).is_err() {
- let _ = reject_upgrade.call1(
- &JsValue::UNDEFINED,
- &JsValue::from_str("idb_store_create_failed"),
- );
- return;
- }
- }
- }) as Box<dyn FnMut(_)>);
- request.set_onupgradeneeded(Some(on_upgrade.as_ref().unchecked_ref()));
- on_upgrade.forget();
- });
- let value = JsFuture::from(promise).await.map_err(map_open_error)?;
- value
- .dyn_into::<IdbDatabase>()
- .map_err(|_| RadrootsClientIdbStoreError::OperationFailure)
-}
-
-#[cfg(target_arch = "wasm32")]
-async fn idb_store_ensure_all(
- database: &str,
- stores: &[String],
-) -> Result<(), RadrootsClientIdbStoreError> {
- if stores.is_empty() {
- return Ok(());
- }
- let mut target_stores = stores.to_vec();
- target_stores.sort();
- target_stores.dedup();
- let mut attempt = 0;
- while attempt < 5 {
- attempt += 1;
- let db = idb_open(database, None, &[]).await?;
- let missing = idb_missing_stores(&db, &target_stores);
- let version = db.version() as u32;
- db.close();
- if missing.is_empty() {
- return Ok(());
- }
- let next_version = version.saturating_add(1);
- match idb_open(database, Some(next_version), &missing).await {
- Ok(upgraded) => {
- let still_missing = idb_missing_stores(&upgraded, &target_stores);
- upgraded.close();
- if still_missing.is_empty() {
- return Ok(());
- }
- }
- Err(RadrootsClientIdbStoreError::VersionError) => continue,
- Err(err) => return Err(err),
- }
- }
- Err(RadrootsClientIdbStoreError::OperationFailure)
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn idb_store_ensure(
- database: &str,
- store: &str,
-) -> Result<(), RadrootsClientIdbStoreError> {
- if database == RADROOTS_IDB_DATABASE {
- idb_store_bootstrap(database, None).await?;
- if RADROOTS_IDB_STORES.contains(&store) {
- return Ok(());
- }
- }
- idb_store_ensure_all(database, &[store.to_string()]).await
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn idb_store_bootstrap(
- database: &str,
- stores: Option<&[&str]>,
-) -> Result<(), RadrootsClientIdbStoreError> {
- let target_stores: Vec<String> = match stores {
- Some(stores) => stores.iter().map(|store| (*store).to_string()).collect(),
- None if database == RADROOTS_IDB_DATABASE => RADROOTS_IDB_STORES
- .iter()
- .map(|store| (*store).to_string())
- .collect(),
- None => Vec::new(),
- };
- if target_stores.is_empty() {
- return Ok(());
- }
- idb_store_ensure_all(database, &target_stores).await
-}
-
-#[cfg(target_arch = "wasm32")]
-pub async fn idb_store_exists(
- database: &str,
- store: &str,
-) -> Result<bool, RadrootsClientIdbStoreError> {
- let factory = idb_factory()?;
- let known = idb_database_exists(&factory, database).await?;
- if !known {
- return Ok(false);
- }
- let db = idb_open(database, None, &[]).await?;
- let exists = db.object_store_names().contains(store);
- db.close();
- Ok(exists)
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub async fn idb_store_ensure(
- _database: &str,
- _store: &str,
-) -> Result<(), RadrootsClientIdbStoreError> {
- Err(RadrootsClientIdbStoreError::IdbUndefined)
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub async fn idb_store_bootstrap(
- _database: &str,
- _stores: Option<&[&str]>,
-) -> Result<(), RadrootsClientIdbStoreError> {
- Err(RadrootsClientIdbStoreError::IdbUndefined)
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub async fn idb_store_exists(
- _database: &str,
- _store: &str,
-) -> Result<bool, RadrootsClientIdbStoreError> {
- Ok(false)
-}
-
-#[cfg(test)]
-mod tests {
- use super::{idb_store_bootstrap, idb_store_ensure, idb_store_exists};
- use crate::idb::RadrootsClientIdbStoreError;
-
- #[test]
- fn non_wasm_returns_idb_undefined() {
- let err = futures::executor::block_on(idb_store_ensure("db", "store"))
- .expect_err("idb undefined");
- assert_eq!(err, RadrootsClientIdbStoreError::IdbUndefined);
- }
-
- #[test]
- fn non_wasm_bootstrap_returns_idb_undefined() {
- let err = futures::executor::block_on(idb_store_bootstrap("db", None))
- .expect_err("idb undefined");
- assert_eq!(err, RadrootsClientIdbStoreError::IdbUndefined);
- }
-
- #[test]
- fn non_wasm_exists_returns_false() {
- let exists = futures::executor::block_on(idb_store_exists("db", "store"))
- .expect("exists");
- assert!(!exists);
- }
-}
diff --git a/crates/core/src/idb/types.rs b/crates/core/src/idb/types.rs
@@ -1,11 +0,0 @@
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub struct RadrootsClientIdbConfig {
- pub database: &'static str,
- pub store: &'static str,
-}
-
-impl RadrootsClientIdbConfig {
- pub const fn new(database: &'static str, store: &'static str) -> Self {
- Self { database, store }
- }
-}
diff --git a/crates/core/src/idb/value.rs b/crates/core/src/idb/value.rs
@@ -1,37 +0,0 @@
-#[cfg(target_arch = "wasm32")]
-pub type RadrootsClientIdbValue = wasm_bindgen::JsValue;
-#[cfg(not(target_arch = "wasm32"))]
-pub type RadrootsClientIdbValue = ();
-
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen::JsCast;
-
-#[cfg(target_arch = "wasm32")]
-pub fn idb_value_as_bytes(value: &RadrootsClientIdbValue) -> Option<Vec<u8>> {
- if value.is_instance_of::<js_sys::Uint8Array>()
- || value.is_instance_of::<js_sys::ArrayBuffer>()
- || js_sys::ArrayBuffer::is_view(value)
- {
- let array = js_sys::Uint8Array::new(value);
- let mut out = vec![0u8; array.length() as usize];
- array.copy_to(&mut out);
- return Some(out);
- }
- None
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub fn idb_value_as_bytes(_value: &RadrootsClientIdbValue) -> Option<Vec<u8>> {
- None
-}
-
-#[cfg(test)]
-mod tests {
- use super::{idb_value_as_bytes, RadrootsClientIdbValue};
-
- #[test]
- fn non_wasm_returns_none() {
- let value: RadrootsClientIdbValue = ();
- assert!(idb_value_as_bytes(&value).is_none());
- }
-}
diff --git a/crates/core/src/keystore/error.rs b/crates/core/src/keystore/error.rs
@@ -1,69 +0,0 @@
-use std::fmt;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsClientKeystoreError {
- IdbUndefined,
- MissingKey,
- CorruptData,
- NostrInvalidSecretKey,
- NostrNoResults,
-}
-
-pub type RadrootsClientKeystoreErrorMessage = &'static str;
-
-impl RadrootsClientKeystoreError {
- pub const fn message(self) -> RadrootsClientKeystoreErrorMessage {
- match self {
- RadrootsClientKeystoreError::IdbUndefined => "error.client.keystore.idb_undefined",
- RadrootsClientKeystoreError::MissingKey => "error.client.keystore.missing_key",
- RadrootsClientKeystoreError::CorruptData => "error.client.keystore.corrupt_data",
- RadrootsClientKeystoreError::NostrInvalidSecretKey => {
- "error.client.keystore.nostr_invalid_secret_key"
- }
- RadrootsClientKeystoreError::NostrNoResults => "error.client.keystore.nostr_no_results",
- }
- }
-}
-
-impl fmt::Display for RadrootsClientKeystoreError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.write_str(self.message())
- }
-}
-
-impl std::error::Error for RadrootsClientKeystoreError {}
-
-#[cfg(test)]
-mod tests {
- use super::RadrootsClientKeystoreError;
-
- #[test]
- fn message_matches_spec() {
- let cases = [
- (
- RadrootsClientKeystoreError::IdbUndefined,
- "error.client.keystore.idb_undefined",
- ),
- (
- RadrootsClientKeystoreError::MissingKey,
- "error.client.keystore.missing_key",
- ),
- (
- RadrootsClientKeystoreError::CorruptData,
- "error.client.keystore.corrupt_data",
- ),
- (
- RadrootsClientKeystoreError::NostrInvalidSecretKey,
- "error.client.keystore.nostr_invalid_secret_key",
- ),
- (
- RadrootsClientKeystoreError::NostrNoResults,
- "error.client.keystore.nostr_no_results",
- ),
- ];
- for (err, expected) in cases {
- assert_eq!(err.message(), expected);
- assert_eq!(err.to_string(), expected);
- }
- }
-}
diff --git a/crates/core/src/keystore/mod.rs b/crates/core/src/keystore/mod.rs
@@ -1,14 +0,0 @@
-pub mod error;
-pub mod types;
-pub mod web;
-pub mod web_nostr;
-
-pub use error::{RadrootsClientKeystoreError, RadrootsClientKeystoreErrorMessage};
-pub use types::{
- RadrootsClientKeystore,
- RadrootsClientKeystoreNostr,
- RadrootsClientKeystoreResult,
- RadrootsClientKeystoreValue,
-};
-pub use web::RadrootsClientWebKeystore;
-pub use web_nostr::RadrootsClientWebKeystoreNostr;
diff --git a/crates/core/src/keystore/types.rs b/crates/core/src/keystore/types.rs
@@ -1,46 +0,0 @@
-use async_trait::async_trait;
-
-use crate::backup::RadrootsClientBackupKeystorePayload;
-
-use super::RadrootsClientKeystoreError;
-
-pub type RadrootsClientKeystoreValue = Option<String>;
-pub type RadrootsClientKeystoreResult<T> = Result<T, RadrootsClientKeystoreError>;
-
-#[async_trait(?Send)]
-pub trait RadrootsClientKeystore {
- async fn add(&self, key: &str, value: &str) -> RadrootsClientKeystoreResult<String>;
- async fn remove(&self, key: &str) -> RadrootsClientKeystoreResult<String>;
- async fn read(&self, key: Option<&str>) -> RadrootsClientKeystoreResult<RadrootsClientKeystoreValue>;
- async fn keys(&self) -> RadrootsClientKeystoreResult<Vec<String>>;
- async fn reset(&self) -> RadrootsClientKeystoreResult<()>;
- fn get_store_id(&self) -> &str;
- async fn export_backup(
- &self,
- ) -> RadrootsClientKeystoreResult<RadrootsClientBackupKeystorePayload>;
- async fn import_backup(
- &self,
- payload: RadrootsClientBackupKeystorePayload,
- ) -> RadrootsClientKeystoreResult<()>;
-}
-
-#[async_trait(?Send)]
-pub trait RadrootsClientKeystoreNostr {
- async fn generate(&self) -> RadrootsClientKeystoreResult<String>;
- async fn add(&self, secret_key: &str) -> RadrootsClientKeystoreResult<String>;
- async fn read(&self, public_key: &str) -> RadrootsClientKeystoreResult<String>;
- async fn keys(&self) -> RadrootsClientKeystoreResult<Vec<String>>;
- async fn remove(&self, public_key: &str) -> RadrootsClientKeystoreResult<String>;
- async fn reset(&self) -> RadrootsClientKeystoreResult<()>;
-}
-
-#[cfg(test)]
-mod tests {
- use super::RadrootsClientKeystoreValue;
-
- #[test]
- fn keystore_value_allows_none() {
- let value: RadrootsClientKeystoreValue = None;
- assert!(value.is_none());
- }
-}
diff --git a/crates/core/src/keystore/web.rs b/crates/core/src/keystore/web.rs
@@ -1,270 +0,0 @@
-use async_trait::async_trait;
-
-use crate::backup::RadrootsClientBackupKeystorePayload;
-#[cfg(target_arch = "wasm32")]
-use crate::crypto::RadrootsClientCryptoError;
-use crate::crypto::RadrootsClientLegacyKeyConfig;
-use crate::idb::{
- IDB_CONFIG_KEYSTORE,
- IDB_STORE_KEYSTORE_CIPHER,
- IDB_STORE_KEYSTORE_NOSTR,
- IDB_STORE_KEYSTORE_NOSTR_CIPHER,
- RadrootsClientIdbConfig,
-};
-#[cfg(target_arch = "wasm32")]
-use crate::idb::RadrootsClientIdbStoreError;
-use crate::idb::{RadrootsClientWebEncryptedStore, RadrootsClientWebEncryptedStoreConfig};
-
-use super::{
- RadrootsClientKeystore,
- RadrootsClientKeystoreError,
- RadrootsClientKeystoreResult,
-};
-
-const DEFAULT_IV_LENGTH: u32 = 12;
-
-pub struct RadrootsClientWebKeystore {
- config: RadrootsClientIdbConfig,
- store_id: String,
- #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
- encrypted_store: RadrootsClientWebEncryptedStore,
-}
-
-impl RadrootsClientWebKeystore {
- pub fn new(config: Option<RadrootsClientIdbConfig>) -> Self {
- let config = config.unwrap_or(IDB_CONFIG_KEYSTORE);
- let store_id = format!("keystore:{}:{}", config.database, config.store);
- let legacy_store = if config.store == IDB_STORE_KEYSTORE_NOSTR {
- IDB_STORE_KEYSTORE_NOSTR_CIPHER
- } else {
- IDB_STORE_KEYSTORE_CIPHER
- };
- let legacy_key_config = RadrootsClientLegacyKeyConfig {
- idb_config: RadrootsClientIdbConfig::new(config.database, legacy_store),
- key_name: format!("radroots.keystore.{}.aes-gcm.key", config.store),
- iv_length: DEFAULT_IV_LENGTH,
- algorithm: "AES-GCM".to_string(),
- };
- let encrypted_store = RadrootsClientWebEncryptedStore::new(
- RadrootsClientWebEncryptedStoreConfig {
- idb_config: config,
- store_id: store_id.clone(),
- legacy_key: Some(legacy_key_config.clone()),
- iv_length: Some(DEFAULT_IV_LENGTH),
- crypto_service: None,
- },
- );
- Self {
- config,
- store_id,
- encrypted_store,
- }
- }
-
- pub fn get_config(&self) -> RadrootsClientIdbConfig {
- self.config
- }
-
- pub fn get_store_id(&self) -> &str {
- &self.store_id
- }
-
- #[cfg(target_arch = "wasm32")]
- async fn store_encrypted(&self, key: &str, bytes: &[u8]) -> RadrootsClientKeystoreResult<()> {
- let value = js_sys::Uint8Array::from(bytes);
- crate::idb::idb_set(
- self.config.database,
- self.config.store,
- key,
- &value.into(),
- )
- .await
- .map_err(map_idb_error)
- }
-}
-
-#[async_trait(?Send)]
-impl RadrootsClientKeystore for RadrootsClientWebKeystore {
- async fn add(&self, key: &str, value: &str) -> RadrootsClientKeystoreResult<String> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = (key, value);
- return Err(RadrootsClientKeystoreError::IdbUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- let encrypted = self
- .encrypted_store
- .encrypt_bytes(value.as_bytes())
- .await
- .map_err(map_crypto_error)?;
- self.store_encrypted(key, &encrypted).await?;
- Ok(key.to_string())
- }
- }
-
- async fn remove(&self, key: &str) -> RadrootsClientKeystoreResult<String> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = key;
- return Err(RadrootsClientKeystoreError::IdbUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- crate::idb::idb_del(self.config.database, self.config.store, key)
- .await
- .map_err(map_idb_error)?;
- Ok(key.to_string())
- }
- }
-
- async fn read(&self, key: Option<&str>) -> RadrootsClientKeystoreResult<Option<String>> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = key;
- return Err(RadrootsClientKeystoreError::IdbUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- let Some(key) = key else {
- return Err(RadrootsClientKeystoreError::MissingKey);
- };
- let stored = crate::idb::idb_get(self.config.database, self.config.store, key)
- .await
- .map_err(map_idb_error)?;
- let Some(stored) = stored else {
- return Err(RadrootsClientKeystoreError::CorruptData);
- };
- let Some(bytes) = crate::idb::idb_value_as_bytes(&stored) else {
- return Err(RadrootsClientKeystoreError::CorruptData);
- };
- let outcome = self
- .encrypted_store
- .decrypt_record(&bytes)
- .await
- .map_err(map_crypto_error)?;
- if let Some(reencrypted) = outcome.reencrypted {
- self.store_encrypted(key, &reencrypted).await?;
- }
- let plain =
- String::from_utf8(outcome.plaintext).map_err(|_| RadrootsClientKeystoreError::CorruptData)?;
- Ok(Some(plain))
- }
- }
-
- async fn keys(&self) -> RadrootsClientKeystoreResult<Vec<String>> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- return Err(RadrootsClientKeystoreError::IdbUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- crate::idb::idb_keys(self.config.database, self.config.store)
- .await
- .map_err(map_idb_error)
- }
- }
-
- async fn reset(&self) -> RadrootsClientKeystoreResult<()> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- return Err(RadrootsClientKeystoreError::IdbUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- crate::idb::idb_clear(self.config.database, self.config.store)
- .await
- .map_err(map_idb_error)?;
- let index = crate::crypto::crypto_registry_get_store_index(&self.store_id)
- .await
- .map_err(map_crypto_error)?;
- if let Some(index) = index {
- crate::crypto::crypto_registry_clear_store_index(&self.store_id)
- .await
- .map_err(map_crypto_error)?;
- for key_id in index.key_ids {
- crate::crypto::crypto_registry_clear_key_entry(&key_id)
- .await
- .map_err(map_crypto_error)?;
- }
- }
- Ok(())
- }
- }
-
- fn get_store_id(&self) -> &str {
- &self.store_id
- }
-
- async fn export_backup(
- &self,
- ) -> RadrootsClientKeystoreResult<RadrootsClientBackupKeystorePayload> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- return Err(RadrootsClientKeystoreError::IdbUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- let keys = self.keys().await?;
- let mut entries = Vec::new();
- for key in keys {
- let value = self.read(Some(&key)).await?;
- let Some(value) = value else {
- return Err(RadrootsClientKeystoreError::CorruptData);
- };
- entries.push(crate::backup::RadrootsClientBackupKeystoreEntry { key, value });
- }
- Ok(RadrootsClientBackupKeystorePayload { entries })
- }
- }
-
- async fn import_backup(
- &self,
- payload: RadrootsClientBackupKeystorePayload,
- ) -> RadrootsClientKeystoreResult<()> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = payload;
- return Err(RadrootsClientKeystoreError::IdbUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- for entry in payload.entries {
- self.add(&entry.key, &entry.value).await?;
- }
- Ok(())
- }
- }
-}
-
-#[cfg(target_arch = "wasm32")]
-fn map_crypto_error(err: RadrootsClientCryptoError) -> RadrootsClientKeystoreError {
- match err {
- RadrootsClientCryptoError::IdbUndefined | RadrootsClientCryptoError::CryptoUndefined => {
- RadrootsClientKeystoreError::IdbUndefined
- }
- _ => RadrootsClientKeystoreError::CorruptData,
- }
-}
-
-#[cfg(target_arch = "wasm32")]
-fn map_idb_error(err: RadrootsClientIdbStoreError) -> RadrootsClientKeystoreError {
- match err {
- RadrootsClientIdbStoreError::IdbUndefined => RadrootsClientKeystoreError::IdbUndefined,
- _ => RadrootsClientKeystoreError::CorruptData,
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::RadrootsClientWebKeystore;
- use crate::keystore::RadrootsClientKeystore;
-
- #[test]
- fn non_wasm_add_errors() {
- let store = RadrootsClientWebKeystore::new(None);
- let err = futures::executor::block_on(store.add("key", "value"))
- .expect_err("idb undefined");
- assert_eq!(err, crate::keystore::RadrootsClientKeystoreError::IdbUndefined);
- }
-}
diff --git a/crates/core/src/keystore/web_nostr.rs b/crates/core/src/keystore/web_nostr.rs
@@ -1,97 +0,0 @@
-use async_trait::async_trait;
-
-use radroots_nostr::prelude::{RadrootsNostrKeys, RadrootsNostrSecretKey};
-
-use crate::idb::{IDB_CONFIG_KEYSTORE_NOSTR, RadrootsClientIdbConfig};
-
-use super::{
- RadrootsClientKeystore,
- RadrootsClientKeystoreError,
- RadrootsClientKeystoreNostr,
- RadrootsClientKeystoreResult,
- RadrootsClientWebKeystore,
-};
-
-pub struct RadrootsClientWebKeystoreNostr {
- keystore: RadrootsClientWebKeystore,
-}
-
-impl RadrootsClientWebKeystoreNostr {
- pub fn new(config: Option<RadrootsClientIdbConfig>) -> Self {
- let config = config.unwrap_or(IDB_CONFIG_KEYSTORE_NOSTR);
- let keystore = RadrootsClientWebKeystore::new(Some(config));
- Self { keystore }
- }
-
- pub fn get_config(&self) -> RadrootsClientIdbConfig {
- self.keystore.get_config()
- }
-
- async fn add_secret_key(
- &self,
- secret_key: RadrootsNostrSecretKey,
- ) -> RadrootsClientKeystoreResult<String> {
- let secret_hex = secret_key.to_secret_hex();
- let keys = RadrootsNostrKeys::new(secret_key);
- let public_key = keys.public_key.to_hex();
- let _ = self.keystore.add(&public_key, &secret_hex).await?;
- Ok(public_key)
- }
-}
-
-#[async_trait(?Send)]
-impl RadrootsClientKeystoreNostr for RadrootsClientWebKeystoreNostr {
- async fn generate(&self) -> RadrootsClientKeystoreResult<String> {
- let secret_key = RadrootsNostrSecretKey::generate();
- self.add_secret_key(secret_key).await
- }
-
- async fn add(&self, secret_key: &str) -> RadrootsClientKeystoreResult<String> {
- let secret_key = RadrootsNostrSecretKey::parse(secret_key)
- .map_err(|_| RadrootsClientKeystoreError::NostrInvalidSecretKey)?;
- self.add_secret_key(secret_key).await
- }
-
- async fn read(&self, public_key: &str) -> RadrootsClientKeystoreResult<String> {
- let value = self.keystore.read(Some(public_key)).await?;
- value.ok_or(RadrootsClientKeystoreError::MissingKey)
- }
-
- async fn keys(&self) -> RadrootsClientKeystoreResult<Vec<String>> {
- let keys = self.keystore.keys().await?;
- if keys.is_empty() {
- return Err(RadrootsClientKeystoreError::NostrNoResults);
- }
- Ok(keys)
- }
-
- async fn remove(&self, public_key: &str) -> RadrootsClientKeystoreResult<String> {
- let _ = self.keystore.remove(public_key).await?;
- Ok(public_key.to_string())
- }
-
- async fn reset(&self) -> RadrootsClientKeystoreResult<()> {
- self.keystore.reset().await
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::RadrootsClientWebKeystoreNostr;
- use crate::idb::IDB_CONFIG_KEYSTORE_NOSTR;
- use crate::keystore::{RadrootsClientKeystoreError, RadrootsClientKeystoreNostr};
-
- #[test]
- fn default_config_is_nostr_store() {
- let keystore = RadrootsClientWebKeystoreNostr::new(None);
- assert_eq!(keystore.get_config(), IDB_CONFIG_KEYSTORE_NOSTR);
- }
-
- #[test]
- fn invalid_secret_key_errors() {
- let keystore = RadrootsClientWebKeystoreNostr::new(None);
- let err = futures::executor::block_on(keystore.add("not-a-key"))
- .expect_err("invalid secret key");
- assert_eq!(err, RadrootsClientKeystoreError::NostrInvalidSecretKey);
- }
-}
diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs
@@ -1,16 +0,0 @@
-#![forbid(unsafe_code)]
-
-pub mod crypto;
-pub mod cipher;
-pub mod backup;
-pub mod datastore;
-pub mod fs;
-pub mod geolocation;
-pub mod idb;
-pub mod keystore;
-pub mod notifications;
-pub mod radroots;
-#[cfg(not(target_arch = "wasm32"))]
-pub mod sql;
-#[cfg(not(target_arch = "wasm32"))]
-pub mod tangle;
diff --git a/crates/core/src/notifications/error.rs b/crates/core/src/notifications/error.rs
@@ -1,53 +0,0 @@
-use std::fmt;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsClientNotificationsError {
- Unavailable,
- ReadFailure,
-}
-
-pub type RadrootsClientNotificationsErrorMessage = &'static str;
-
-impl RadrootsClientNotificationsError {
- pub const fn message(self) -> RadrootsClientNotificationsErrorMessage {
- match self {
- RadrootsClientNotificationsError::Unavailable => {
- "error.client.notifications.unavailable"
- }
- RadrootsClientNotificationsError::ReadFailure => {
- "error.client.notifications.read_failure"
- }
- }
- }
-}
-
-impl fmt::Display for RadrootsClientNotificationsError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.write_str(self.message())
- }
-}
-
-impl std::error::Error for RadrootsClientNotificationsError {}
-
-#[cfg(test)]
-mod tests {
- use super::RadrootsClientNotificationsError;
-
- #[test]
- fn message_matches_spec() {
- let cases = [
- (
- RadrootsClientNotificationsError::Unavailable,
- "error.client.notifications.unavailable",
- ),
- (
- RadrootsClientNotificationsError::ReadFailure,
- "error.client.notifications.read_failure",
- ),
- ];
- for (err, expected) in cases {
- assert_eq!(err.message(), expected);
- assert_eq!(err.to_string(), expected);
- }
- }
-}
diff --git a/crates/core/src/notifications/mod.rs b/crates/core/src/notifications/mod.rs
@@ -1,15 +0,0 @@
-pub mod error;
-pub mod types;
-pub mod web;
-
-pub use error::{RadrootsClientNotificationsError, RadrootsClientNotificationsErrorMessage};
-pub use types::{
- RadrootsClientNotifications,
- RadrootsClientNotificationsConfig,
- RadrootsClientNotificationsDialogConfirmOpts,
- RadrootsClientNotificationsPermission,
- RadrootsClientNotificationsResult,
- RadrootsClientNotificationsSendOptions,
- RadrootsClientResolveStatus,
-};
-pub use web::RadrootsClientWebNotifications;
diff --git a/crates/core/src/notifications/types.rs b/crates/core/src/notifications/types.rs
@@ -1,143 +0,0 @@
-use async_trait::async_trait;
-
-use super::RadrootsClientNotificationsError;
-
-pub type RadrootsClientNotificationsResult<T> =
- Result<T, RadrootsClientNotificationsError>;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsClientResolveStatus {
- Info,
- Warning,
- Error,
- Success,
-}
-
-impl RadrootsClientResolveStatus {
- pub const fn as_str(self) -> &'static str {
- match self {
- RadrootsClientResolveStatus::Info => "info",
- RadrootsClientResolveStatus::Warning => "warning",
- RadrootsClientResolveStatus::Error => "error",
- RadrootsClientResolveStatus::Success => "success",
- }
- }
-
- pub fn parse(value: &str) -> Option<Self> {
- match value {
- "info" => Some(RadrootsClientResolveStatus::Info),
- "warning" => Some(RadrootsClientResolveStatus::Warning),
- "error" => Some(RadrootsClientResolveStatus::Error),
- "success" => Some(RadrootsClientResolveStatus::Success),
- _ => None,
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsClientNotificationsPermission {
- Granted,
- Denied,
- Default,
- Unavailable,
-}
-
-impl RadrootsClientNotificationsPermission {
- pub const fn as_str(self) -> &'static str {
- match self {
- RadrootsClientNotificationsPermission::Granted => "granted",
- RadrootsClientNotificationsPermission::Denied => "denied",
- RadrootsClientNotificationsPermission::Default => "default",
- RadrootsClientNotificationsPermission::Unavailable => "unavailable",
- }
- }
-
- pub fn parse(value: &str) -> Option<Self> {
- match value {
- "granted" => Some(RadrootsClientNotificationsPermission::Granted),
- "denied" => Some(RadrootsClientNotificationsPermission::Denied),
- "default" => Some(RadrootsClientNotificationsPermission::Default),
- "unavailable" => Some(RadrootsClientNotificationsPermission::Unavailable),
- _ => None,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsClientNotificationsDialogConfirmConfig {
- pub message: String,
- pub title: Option<String>,
- pub status: Option<RadrootsClientResolveStatus>,
- pub cancel: Option<String>,
- pub ok: Option<String>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum RadrootsClientNotificationsDialogConfirmOpts {
- Message(String),
- Config(RadrootsClientNotificationsDialogConfirmConfig),
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsClientNotificationsSendOptions {
- pub id: Option<String>,
- pub channel_id: Option<String>,
- pub title: Option<String>,
- pub body: Option<String>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsClientNotificationsConfig {
- pub app_name: String,
-}
-
-#[async_trait(?Send)]
-pub trait RadrootsClientNotifications {
- async fn alert(
- &self,
- message: &str,
- title: Option<&str>,
- status: Option<RadrootsClientResolveStatus>,
- ) -> bool;
- async fn confirm(
- &self,
- opts: RadrootsClientNotificationsDialogConfirmOpts,
- ) -> bool;
- async fn notify_init(
- &self,
- ) -> RadrootsClientNotificationsResult<RadrootsClientNotificationsPermission>;
- async fn notify_send(
- &self,
- opts: RadrootsClientNotificationsSendOptions,
- ) -> RadrootsClientNotificationsResult<()>;
- async fn open_photos(
- &self,
- ) -> RadrootsClientNotificationsResult<Option<Vec<String>>>;
-}
-
-#[cfg(test)]
-mod tests {
- use super::{RadrootsClientNotificationsPermission, RadrootsClientResolveStatus};
-
- #[test]
- fn resolve_status_roundtrip() {
- let status = RadrootsClientResolveStatus::Warning;
- assert_eq!(status.as_str(), "warning");
- assert_eq!(
- RadrootsClientResolveStatus::parse("warning"),
- Some(status)
- );
- assert_eq!(RadrootsClientResolveStatus::parse("other"), None);
- }
-
- #[test]
- fn notification_permission_roundtrip() {
- let permission = RadrootsClientNotificationsPermission::Granted;
- assert_eq!(permission.as_str(), "granted");
- assert_eq!(
- RadrootsClientNotificationsPermission::parse("granted"),
- Some(permission)
- );
- assert_eq!(RadrootsClientNotificationsPermission::parse("other"), None);
- }
-}
diff --git a/crates/core/src/notifications/web.rs b/crates/core/src/notifications/web.rs
@@ -1,333 +0,0 @@
-use async_trait::async_trait;
-
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen::{JsCast, JsValue};
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen_futures::JsFuture;
-
-use super::{
- RadrootsClientNotifications,
- RadrootsClientNotificationsConfig,
- RadrootsClientNotificationsDialogConfirmOpts,
- RadrootsClientNotificationsError,
- RadrootsClientNotificationsPermission,
- RadrootsClientNotificationsResult,
- RadrootsClientNotificationsSendOptions,
- RadrootsClientResolveStatus,
-};
-
-pub struct RadrootsClientWebNotifications {
- config: RadrootsClientNotificationsConfig,
-}
-
-impl RadrootsClientWebNotifications {
- pub fn new(config: Option<RadrootsClientNotificationsConfig>) -> Self {
- let config = config.unwrap_or(RadrootsClientNotificationsConfig {
- app_name: String::from("Radroots"),
- });
- Self { config }
- }
-
- pub fn get_config(&self) -> &RadrootsClientNotificationsConfig {
- &self.config
- }
-
- #[cfg(target_arch = "wasm32")]
- fn notification_available(window: &web_sys::Window) -> bool {
- js_sys::Reflect::has(window.as_ref(), &JsValue::from_str("Notification"))
- .unwrap_or(false)
- }
-
- #[cfg(target_arch = "wasm32")]
- fn permission_from_web(permission: web_sys::NotificationPermission) -> RadrootsClientNotificationsPermission {
- match permission {
- web_sys::NotificationPermission::Granted => {
- RadrootsClientNotificationsPermission::Granted
- }
- web_sys::NotificationPermission::Denied => RadrootsClientNotificationsPermission::Denied,
- web_sys::NotificationPermission::Default => {
- RadrootsClientNotificationsPermission::Default
- }
- _ => RadrootsClientNotificationsPermission::Unavailable,
- }
- }
-
- #[cfg(target_arch = "wasm32")]
- async fn request_permission(
- &self,
- window: &web_sys::Window,
- ) -> RadrootsClientNotificationsResult<RadrootsClientNotificationsPermission> {
- if !Self::notification_available(window) {
- return Ok(RadrootsClientNotificationsPermission::Unavailable);
- }
- let promise = web_sys::Notification::request_permission()
- .map_err(|_| RadrootsClientNotificationsError::Unavailable)?;
- let result = JsFuture::from(promise)
- .await
- .map_err(|_| RadrootsClientNotificationsError::Unavailable)?;
- if let Some(permission) = result.as_string() {
- if let Some(parsed) = RadrootsClientNotificationsPermission::parse(&permission) {
- return Ok(parsed);
- }
- }
- Ok(Self::permission_from_web(web_sys::Notification::permission()))
- }
-
- #[cfg(target_arch = "wasm32")]
- async fn read_photo_data(
- &self,
- file: web_sys::File,
- ) -> RadrootsClientNotificationsResult<String> {
- let reader =
- web_sys::FileReader::new().map_err(|_| RadrootsClientNotificationsError::ReadFailure)?;
- let reader_load = reader.clone();
- let reader_error = reader.clone();
- let promise = js_sys::Promise::new(&mut |resolve, reject| {
- let reader_load = reader_load.clone();
- let resolve_load = resolve.clone();
- let reject_load = reject.clone();
- let onload = wasm_bindgen::closure::Closure::once(move |_event: web_sys::Event| {
- match reader_load.result() {
- Ok(value) => {
- let _ = resolve_load.call1(&JsValue::NULL, &value);
- }
- Err(err) => {
- let _ = reject_load.call1(&JsValue::NULL, &err);
- }
- }
- });
- let reader_error = reader_error.clone();
- let reject_error = reject.clone();
- let onerror = wasm_bindgen::closure::Closure::once(move |_event: web_sys::Event| {
- let err = reader_error
- .error()
- .map(JsValue::from)
- .unwrap_or_else(|| {
- JsValue::from_str(RadrootsClientNotificationsError::ReadFailure.message())
- });
- let _ = reject_error.call1(&JsValue::NULL, &err);
- });
- reader.set_onload(Some(onload.as_ref().unchecked_ref()));
- reader.set_onerror(Some(onerror.as_ref().unchecked_ref()));
- onload.forget();
- onerror.forget();
- });
- reader
- .read_as_data_url(&file)
- .map_err(|_| RadrootsClientNotificationsError::ReadFailure)?;
- let result = JsFuture::from(promise)
- .await
- .map_err(|_| RadrootsClientNotificationsError::ReadFailure)?;
- result
- .as_string()
- .ok_or(RadrootsClientNotificationsError::ReadFailure)
- }
-
- #[cfg(target_arch = "wasm32")]
- async fn select_photo_files(
- &self,
- ) -> RadrootsClientNotificationsResult<Option<web_sys::FileList>> {
- let window = web_sys::window().ok_or(RadrootsClientNotificationsError::Unavailable)?;
- let document = window
- .document()
- .ok_or(RadrootsClientNotificationsError::Unavailable)?;
- let input = document
- .create_element("input")
- .map_err(|_| RadrootsClientNotificationsError::Unavailable)?;
- let input: web_sys::HtmlInputElement = input
- .dyn_into()
- .map_err(|_| RadrootsClientNotificationsError::Unavailable)?;
- input.set_type("file");
- input.set_multiple(true);
- input.set_accept("image/png,image/jpg");
- let input_handle = input.clone();
- let promise = js_sys::Promise::new(&mut |resolve, _reject| {
- let input_handle = input_handle.clone();
- let input_set = input_handle.clone();
- let resolve_change = resolve.clone();
- let onchange = wasm_bindgen::closure::Closure::once(move |_event: web_sys::Event| {
- let files = input_handle.files();
- let value = files.map(JsValue::from).unwrap_or(JsValue::NULL);
- let _ = resolve_change.call1(&JsValue::NULL, &value);
- });
- input_set.set_onchange(Some(onchange.as_ref().unchecked_ref()));
- input_set.click();
- onchange.forget();
- });
- let value = JsFuture::from(promise)
- .await
- .map_err(|_| RadrootsClientNotificationsError::Unavailable)?;
- if value.is_null() || value.is_undefined() {
- return Ok(None);
- }
- let list = value
- .dyn_into::<web_sys::FileList>()
- .map_err(|_| RadrootsClientNotificationsError::Unavailable)?;
- Ok(Some(list))
- }
-}
-
-#[async_trait(?Send)]
-impl RadrootsClientNotifications for RadrootsClientWebNotifications {
- async fn alert(
- &self,
- message: &str,
- title: Option<&str>,
- _status: Option<RadrootsClientResolveStatus>,
- ) -> bool {
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = (message, title);
- return false;
- }
- #[cfg(target_arch = "wasm32")]
- {
- let window = match web_sys::window() {
- Some(window) => window,
- None => return false,
- };
- let msg = if let Some(title) = title {
- format!("{title}\n\n{message}")
- } else {
- message.to_string()
- };
- window.alert_with_message(&msg).is_ok()
- }
- }
-
- async fn confirm(
- &self,
- opts: RadrootsClientNotificationsDialogConfirmOpts,
- ) -> bool {
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = opts;
- return false;
- }
- #[cfg(target_arch = "wasm32")]
- {
- let window = match web_sys::window() {
- Some(window) => window,
- None => return false,
- };
- let msg = match opts {
- RadrootsClientNotificationsDialogConfirmOpts::Message(message) => message,
- RadrootsClientNotificationsDialogConfirmOpts::Config(config) => config.message,
- };
- window.confirm_with_message(&msg).unwrap_or(false)
- }
- }
-
- async fn notify_init(
- &self,
- ) -> RadrootsClientNotificationsResult<RadrootsClientNotificationsPermission> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- return Err(RadrootsClientNotificationsError::Unavailable);
- }
- #[cfg(target_arch = "wasm32")]
- {
- let window = web_sys::window().ok_or(RadrootsClientNotificationsError::Unavailable)?;
- if !Self::notification_available(&window) {
- return Ok(RadrootsClientNotificationsPermission::Unavailable);
- }
- let permission = Self::permission_from_web(web_sys::Notification::permission());
- match permission {
- RadrootsClientNotificationsPermission::Granted
- | RadrootsClientNotificationsPermission::Denied => Ok(permission),
- RadrootsClientNotificationsPermission::Default => {
- self.request_permission(&window).await
- }
- RadrootsClientNotificationsPermission::Unavailable => Ok(permission),
- }
- }
- }
-
- async fn notify_send(
- &self,
- opts: RadrootsClientNotificationsSendOptions,
- ) -> RadrootsClientNotificationsResult<()> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = opts;
- return Err(RadrootsClientNotificationsError::Unavailable);
- }
- #[cfg(target_arch = "wasm32")]
- {
- let window = web_sys::window().ok_or(RadrootsClientNotificationsError::Unavailable)?;
- if !Self::notification_available(&window) {
- return Err(RadrootsClientNotificationsError::Unavailable);
- }
- let permission = self.notify_init().await?;
- if permission != RadrootsClientNotificationsPermission::Granted {
- return Err(RadrootsClientNotificationsError::Unavailable);
- }
- let title = opts
- .title
- .as_deref()
- .unwrap_or(&self.config.app_name);
- if let Some(body) = opts.body.as_deref() {
- let options = web_sys::NotificationOptions::new();
- options.set_body(body);
- web_sys::Notification::new_with_options(title, &options)
- .map_err(|_| RadrootsClientNotificationsError::Unavailable)?;
- } else {
- web_sys::Notification::new(title)
- .map_err(|_| RadrootsClientNotificationsError::Unavailable)?;
- }
- Ok(())
- }
- }
-
- async fn open_photos(
- &self,
- ) -> RadrootsClientNotificationsResult<Option<Vec<String>>> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- return Err(RadrootsClientNotificationsError::Unavailable);
- }
- #[cfg(target_arch = "wasm32")]
- {
- let files = self.select_photo_files().await?;
- let Some(files) = files else {
- return Ok(None);
- };
- let mut results = Vec::new();
- for idx in 0..files.length() {
- let Some(file) = files.item(idx) else {
- continue;
- };
- let data = self.read_photo_data(file).await?;
- results.push(data);
- }
- Ok(Some(results))
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::RadrootsClientWebNotifications;
- use crate::notifications::{
- RadrootsClientNotifications,
- RadrootsClientNotificationsConfig,
- RadrootsClientNotificationsError,
- };
-
- #[test]
- fn default_config_is_radroots() {
- let client = RadrootsClientWebNotifications::new(None);
- let config = RadrootsClientNotificationsConfig {
- app_name: String::from("Radroots"),
- };
- assert_eq!(client.get_config(), &config);
- }
-
- #[test]
- fn non_wasm_notify_init_errors() {
- let client = RadrootsClientWebNotifications::new(None);
- let err = futures::executor::block_on(client.notify_init())
- .expect_err("notify init errors");
- assert_eq!(err, RadrootsClientNotificationsError::Unavailable);
- }
-}
diff --git a/crates/core/src/radroots/error.rs b/crates/core/src/radroots/error.rs
@@ -1,59 +0,0 @@
-use std::fmt;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsClientRadrootsError {
- MissingBaseUrl,
- AccountRegistered,
- RequestFailure,
-}
-
-pub type RadrootsClientRadrootsErrorMessage = &'static str;
-
-impl RadrootsClientRadrootsError {
- pub const fn message(self) -> RadrootsClientRadrootsErrorMessage {
- match self {
- RadrootsClientRadrootsError::MissingBaseUrl => {
- "error.client.radroots.missing_base_url"
- }
- RadrootsClientRadrootsError::AccountRegistered => {
- "error.client.radroots.account_registered"
- }
- RadrootsClientRadrootsError::RequestFailure => "error.client.radroots.request_failure",
- }
- }
-}
-
-impl fmt::Display for RadrootsClientRadrootsError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.write_str(self.message())
- }
-}
-
-impl std::error::Error for RadrootsClientRadrootsError {}
-
-#[cfg(test)]
-mod tests {
- use super::RadrootsClientRadrootsError;
-
- #[test]
- fn message_matches_spec() {
- let cases = [
- (
- RadrootsClientRadrootsError::MissingBaseUrl,
- "error.client.radroots.missing_base_url",
- ),
- (
- RadrootsClientRadrootsError::AccountRegistered,
- "error.client.radroots.account_registered",
- ),
- (
- RadrootsClientRadrootsError::RequestFailure,
- "error.client.radroots.request_failure",
- ),
- ];
- for (err, expected) in cases {
- assert_eq!(err.message(), expected);
- assert_eq!(err.to_string(), expected);
- }
- }
-}
diff --git a/crates/core/src/radroots/mod.rs b/crates/core/src/radroots/mod.rs
@@ -1,15 +0,0 @@
-pub mod error;
-pub mod types;
-pub mod web;
-
-pub use error::{RadrootsClientRadrootsError, RadrootsClientRadrootsErrorMessage};
-pub use types::{
- RadrootsClientMediaImageUpload,
- RadrootsClientMediaResource,
- RadrootsClientRadroots,
- RadrootsClientRadrootsAccountsActivate,
- RadrootsClientRadrootsAccountsCreate,
- RadrootsClientRadrootsAccountsRequest,
- RadrootsClientRadrootsResult,
-};
-pub use web::RadrootsClientWebRadroots;
diff --git a/crates/core/src/radroots/types.rs b/crates/core/src/radroots/types.rs
@@ -1,74 +0,0 @@
-use async_trait::async_trait;
-
-use super::RadrootsClientRadrootsError;
-
-pub type RadrootsClientRadrootsResult<T> = Result<T, RadrootsClientRadrootsError>;
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsClientRadrootsAccountsRequest {
- pub profile_name: String,
- pub secret_key: String,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsClientRadrootsAccountsCreate {
- pub tok: String,
- pub secret_key: String,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsClientRadrootsAccountsActivate {
- pub id: String,
- pub secret_key: String,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsClientMediaResource {
- pub base_url: String,
- pub hash: String,
- pub ext: String,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsClientMediaImageUpload {
- pub mime_type: Option<String>,
- pub file_data: Vec<u8>,
- pub secret_key: String,
-}
-
-#[async_trait(?Send)]
-pub trait RadrootsClientRadroots {
- async fn accounts_request(
- &self,
- opts: RadrootsClientRadrootsAccountsRequest,
- ) -> RadrootsClientRadrootsResult<String>;
- async fn accounts_create(
- &self,
- opts: RadrootsClientRadrootsAccountsCreate,
- ) -> RadrootsClientRadrootsResult<String>;
- async fn accounts_activate(
- &self,
- opts: RadrootsClientRadrootsAccountsActivate,
- ) -> RadrootsClientRadrootsResult<String>;
- async fn media_image_upload(
- &self,
- opts: RadrootsClientMediaImageUpload,
- ) -> RadrootsClientRadrootsResult<RadrootsClientMediaResource>;
-}
-
-#[cfg(test)]
-mod tests {
- use super::RadrootsClientMediaResource;
-
- #[test]
- fn media_resource_fields_roundtrip() {
- let resource = RadrootsClientMediaResource {
- base_url: String::from("https://example.com"),
- hash: String::from("hash"),
- ext: String::from("png"),
- };
- assert_eq!(resource.base_url, "https://example.com");
- assert_eq!(resource.hash, "hash");
- assert_eq!(resource.ext, "png");
- }
-}
diff --git a/crates/core/src/radroots/web.rs b/crates/core/src/radroots/web.rs
@@ -1,386 +0,0 @@
-use async_trait::async_trait;
-#[cfg(target_arch = "wasm32")]
-use std::str::FromStr;
-#[cfg(target_arch = "wasm32")]
-use base64::engine::general_purpose::STANDARD;
-#[cfg(target_arch = "wasm32")]
-use base64::Engine as _;
-#[cfg(target_arch = "wasm32")]
-use radroots_nostr::prelude::{
- RadrootsNostrEventBuilder,
- RadrootsNostrKeys,
- RadrootsNostrSecretKey,
-};
-#[cfg(target_arch = "wasm32")]
-use serde::Deserialize;
-use url::Url;
-
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen::{JsCast, JsValue};
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen_futures::JsFuture;
-
-#[cfg(target_arch = "wasm32")]
-use serde_json::Value;
-#[cfg(target_arch = "wasm32")]
-use crate::crypto::random::fill_random;
-
-use super::{
- RadrootsClientMediaImageUpload,
- RadrootsClientMediaResource,
- RadrootsClientRadroots,
- RadrootsClientRadrootsAccountsActivate,
- RadrootsClientRadrootsAccountsCreate,
- RadrootsClientRadrootsAccountsRequest,
- RadrootsClientRadrootsError,
- RadrootsClientRadrootsResult,
-};
-
-#[cfg(target_arch = "wasm32")]
-#[derive(Deserialize)]
-struct MediaResourceWire {
- base_url: String,
- hash: String,
- ext: String,
-}
-
-pub struct RadrootsClientWebRadroots {
- base_url: Option<String>,
-}
-
-impl RadrootsClientWebRadroots {
- pub fn new(base_url: Option<&str>) -> Self {
- let base_url = base_url.and_then(sanitize_base_url);
- Self { base_url }
- }
-
- pub fn get_base_url(&self) -> Option<&str> {
- self.base_url.as_deref()
- }
-
- fn require_base_url(&self) -> RadrootsClientRadrootsResult<&str> {
- self.base_url
- .as_deref()
- .ok_or(RadrootsClientRadrootsError::MissingBaseUrl)
- }
-
- #[cfg(target_arch = "wasm32")]
- fn create_x_nostr_event(
- &self,
- secret_key: &str,
- ) -> RadrootsClientRadrootsResult<String> {
- let secret_key = RadrootsNostrSecretKey::from_str(secret_key)
- .map_err(|_| RadrootsClientRadrootsError::RequestFailure)?;
- let keys = RadrootsNostrKeys::new(secret_key);
- let content = random_content()?;
- let event = RadrootsNostrEventBuilder::text_note(content)
- .sign_with_keys(&keys)
- .map_err(|_| RadrootsClientRadrootsError::RequestFailure)?;
- serde_json::to_string(&event)
- .map_err(|_| RadrootsClientRadrootsError::RequestFailure)
- }
-
- #[cfg(target_arch = "wasm32")]
- async fn send_json(
- &self,
- url: &str,
- method: &str,
- headers: Vec<(String, String)>,
- body: Option<Value>,
- ) -> RadrootsClientRadrootsResult<Option<Value>> {
- let window = web_sys::window().ok_or(RadrootsClientRadrootsError::RequestFailure)?;
- let init = web_sys::RequestInit::new();
- init.set_method(method);
- let header_map = web_sys::Headers::new()
- .map_err(|_| RadrootsClientRadrootsError::RequestFailure)?;
- for (key, value) in headers {
- header_map
- .set(&key, &value)
- .map_err(|_| RadrootsClientRadrootsError::RequestFailure)?;
- }
- if let Some(body) = body {
- let body = serde_json::to_string(&body)
- .map_err(|_| RadrootsClientRadrootsError::RequestFailure)?;
- init.set_body(&JsValue::from_str(&body));
- }
- init.set_headers(&header_map);
- let request = web_sys::Request::new_with_str_and_init(url, &init)
- .map_err(|_| RadrootsClientRadrootsError::RequestFailure)?;
- let response = JsFuture::from(window.fetch_with_request(&request))
- .await
- .map_err(|_| RadrootsClientRadrootsError::RequestFailure)?;
- let response: web_sys::Response = response
- .dyn_into()
- .map_err(|_| RadrootsClientRadrootsError::RequestFailure)?;
- if !response.ok() {
- return Err(RadrootsClientRadrootsError::RequestFailure);
- }
- parse_response(response).await
- }
-
- #[cfg(target_arch = "wasm32")]
- async fn send_bytes(
- &self,
- url: &str,
- method: &str,
- headers: Vec<(String, String)>,
- body: &[u8],
- ) -> RadrootsClientRadrootsResult<Option<Value>> {
- let window = web_sys::window().ok_or(RadrootsClientRadrootsError::RequestFailure)?;
- let init = web_sys::RequestInit::new();
- init.set_method(method);
- let header_map = web_sys::Headers::new()
- .map_err(|_| RadrootsClientRadrootsError::RequestFailure)?;
- for (key, value) in headers {
- header_map
- .set(&key, &value)
- .map_err(|_| RadrootsClientRadrootsError::RequestFailure)?;
- }
- let bytes = js_sys::Uint8Array::from(body);
- init.set_body(&bytes.into());
- init.set_headers(&header_map);
- let request = web_sys::Request::new_with_str_and_init(url, &init)
- .map_err(|_| RadrootsClientRadrootsError::RequestFailure)?;
- let response = JsFuture::from(window.fetch_with_request(&request))
- .await
- .map_err(|_| RadrootsClientRadrootsError::RequestFailure)?;
- let response: web_sys::Response = response
- .dyn_into()
- .map_err(|_| RadrootsClientRadrootsError::RequestFailure)?;
- if !response.ok() {
- return Err(RadrootsClientRadrootsError::RequestFailure);
- }
- parse_response(response).await
- }
-}
-
-#[async_trait(?Send)]
-impl RadrootsClientRadroots for RadrootsClientWebRadroots {
- async fn accounts_request(
- &self,
- opts: RadrootsClientRadrootsAccountsRequest,
- ) -> RadrootsClientRadrootsResult<String> {
- let _ = self.require_base_url()?;
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = opts;
- return Err(RadrootsClientRadrootsError::RequestFailure);
- }
- #[cfg(target_arch = "wasm32")]
- {
- let base_url = self.require_base_url()?;
- let url = format!("{base_url}/v1/accounts/request");
- let event = self.create_x_nostr_event(&opts.secret_key)?;
- let headers = vec![
- ("X-Nostr-Event".to_string(), event),
- ("Content-Type".to_string(), "application/json".to_string()),
- ];
- let body = serde_json::json!({ "profile_name": opts.profile_name });
- let data = self.send_json(&url, "POST", headers, Some(body)).await?;
- if let Some(data) = data {
- if is_pass_response(&data) {
- if let Some(tok) = string_field(&data, "tok") {
- return Ok(tok);
- }
- }
- }
- Err(RadrootsClientRadrootsError::AccountRegistered)
- }
- }
-
- async fn accounts_create(
- &self,
- opts: RadrootsClientRadrootsAccountsCreate,
- ) -> RadrootsClientRadrootsResult<String> {
- let _ = self.require_base_url()?;
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = opts;
- return Err(RadrootsClientRadrootsError::RequestFailure);
- }
- #[cfg(target_arch = "wasm32")]
- {
- let base_url = self.require_base_url()?;
- let url = format!("{base_url}/v1/accounts/create");
- let event = self.create_x_nostr_event(&opts.secret_key)?;
- let token = encode_bearer_token(&opts.tok);
- let headers = vec![
- ("X-Nostr-Event".to_string(), event),
- ("Authorization".to_string(), format!("Bearer {token}")),
- ];
- let data = self.send_json(&url, "POST", headers, None).await?;
- if let Some(data) = data {
- if is_pass_response(&data) {
- if let Some(id) = string_field(&data, "id") {
- return Ok(id);
- }
- }
- }
- Err(RadrootsClientRadrootsError::RequestFailure)
- }
- }
-
- async fn accounts_activate(
- &self,
- opts: RadrootsClientRadrootsAccountsActivate,
- ) -> RadrootsClientRadrootsResult<String> {
- let _ = self.require_base_url()?;
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = opts;
- return Err(RadrootsClientRadrootsError::RequestFailure);
- }
- #[cfg(target_arch = "wasm32")]
- {
- let base_url = self.require_base_url()?;
- let url = format!("{base_url}/v1/accounts/activate");
- let event = self.create_x_nostr_event(&opts.secret_key)?;
- let headers = vec![
- ("X-Nostr-Event".to_string(), event),
- ("Content-Type".to_string(), "application/json".to_string()),
- ];
- let body = serde_json::json!({ "id": opts.id });
- let data = self.send_json(&url, "POST", headers, Some(body)).await?;
- if let Some(data) = data {
- if is_pass_response(&data) {
- if let Some(id) = string_field(&data, "id") {
- return Ok(id);
- }
- }
- }
- Err(RadrootsClientRadrootsError::RequestFailure)
- }
- }
-
- async fn media_image_upload(
- &self,
- opts: RadrootsClientMediaImageUpload,
- ) -> RadrootsClientRadrootsResult<RadrootsClientMediaResource> {
- let _ = self.require_base_url()?;
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = opts;
- return Err(RadrootsClientRadrootsError::RequestFailure);
- }
- #[cfg(target_arch = "wasm32")]
- {
- let base_url = self.require_base_url()?;
- let url = format!("{base_url}/v1/media/image/upload");
- let event = self.create_x_nostr_event(&opts.secret_key)?;
- let mime_type = opts
- .mime_type
- .unwrap_or_else(|| String::from("image/png"));
- let headers = vec![
- ("X-Nostr-Event".to_string(), event),
- ("Content-Type".to_string(), mime_type),
- ];
- let data = self
- .send_bytes(&url, "PUT", headers, &opts.file_data)
- .await?;
- if let Some(data) = data {
- if is_pass_response(&data) {
- if let Some(resource) = parse_media_resource(&data) {
- return Ok(resource);
- }
- }
- }
- Err(RadrootsClientRadrootsError::RequestFailure)
- }
- }
-}
-
-fn sanitize_base_url(value: &str) -> Option<String> {
- let trimmed = value.trim();
- if trimmed.is_empty() {
- return None;
- }
- let parsed = Url::parse(trimmed).ok()?;
- let base = format!("{}{}", parsed.origin().ascii_serialization(), parsed.path());
- Some(base.trim_end_matches('/').to_string())
-}
-
-#[cfg(target_arch = "wasm32")]
-fn random_content() -> RadrootsClientRadrootsResult<String> {
- let mut bytes = [0u8; 16];
- fill_random(&mut bytes).map_err(|_| RadrootsClientRadrootsError::RequestFailure)?;
- Ok(STANDARD.encode(bytes))
-}
-
-#[cfg(target_arch = "wasm32")]
-fn is_pass_response(value: &Value) -> bool {
- matches!(value.get("pass"), Some(Value::Bool(true)))
-}
-
-#[cfg(target_arch = "wasm32")]
-fn string_field(value: &Value, key: &str) -> Option<String> {
- value.get(key).and_then(|value| value.as_str()).map(|v| v.to_string())
-}
-
-#[cfg(target_arch = "wasm32")]
-fn parse_media_resource(value: &Value) -> Option<RadrootsClientMediaResource> {
- let resource: MediaResourceWire = serde_json::from_value(value.clone()).ok()?;
- Some(RadrootsClientMediaResource {
- base_url: resource.base_url,
- hash: resource.hash,
- ext: resource.ext,
- })
-}
-
-#[cfg(target_arch = "wasm32")]
-fn encode_bearer_token(value: &str) -> String {
- url::form_urlencoded::byte_serialize(value.as_bytes()).collect()
-}
-
-#[cfg(target_arch = "wasm32")]
-async fn parse_response(
- response: web_sys::Response,
-) -> RadrootsClientRadrootsResult<Option<Value>> {
- let json_response = response.json();
- if let Ok(json_response) = json_response {
- if let Ok(value) = JsFuture::from(json_response).await {
- if let Ok(value) = serde_wasm_bindgen::from_value::<Value>(value) {
- return Ok(Some(value));
- }
- }
- }
- let text_response = response.text().map_err(|_| RadrootsClientRadrootsError::RequestFailure)?;
- let text_value = JsFuture::from(text_response)
- .await
- .map_err(|_| RadrootsClientRadrootsError::RequestFailure)?;
- if let Some(text) = text_value.as_string() {
- if let Ok(value) = serde_json::from_str(&text) {
- return Ok(Some(value));
- }
- return Ok(Some(Value::String(text)));
- }
- Ok(None)
-}
-
-#[cfg(test)]
-mod tests {
- use super::RadrootsClientWebRadroots;
- use crate::radroots::{
- RadrootsClientRadroots,
- RadrootsClientRadrootsAccountsRequest,
- RadrootsClientRadrootsError,
- };
-
- #[test]
- fn base_url_sanitizes_trailing_slash() {
- let client = RadrootsClientWebRadroots::new(Some("https://example.com/app/"));
- assert_eq!(client.get_base_url(), Some("https://example.com/app"));
- }
-
- #[test]
- fn missing_base_url_errors() {
- let client = RadrootsClientWebRadroots::new(None);
- let err = futures::executor::block_on(client.accounts_request(
- RadrootsClientRadrootsAccountsRequest {
- profile_name: "rad".to_string(),
- secret_key: "deadbeef".to_string(),
- },
- ))
- .expect_err("missing base url");
- assert_eq!(err, RadrootsClientRadrootsError::MissingBaseUrl);
- }
-}
diff --git a/crates/core/src/sql/error.rs b/crates/core/src/sql/error.rs
@@ -1,81 +0,0 @@
-use std::fmt;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsClientSqlError {
- IdbUndefined,
- EngineUnavailable,
- InvalidParams,
- QueryFailure,
- ExportFailure,
- ImportFailure,
- BackupFailure,
-}
-
-pub type RadrootsClientSqlErrorMessage = &'static str;
-
-impl RadrootsClientSqlError {
- pub const fn message(self) -> RadrootsClientSqlErrorMessage {
- match self {
- RadrootsClientSqlError::IdbUndefined => "error.client.sql.idb_undefined",
- RadrootsClientSqlError::EngineUnavailable => {
- "error.client.sql.engine_unavailable"
- }
- RadrootsClientSqlError::InvalidParams => "error.client.sql.invalid_params",
- RadrootsClientSqlError::QueryFailure => "error.client.sql.query_failure",
- RadrootsClientSqlError::ExportFailure => "error.client.sql.export_failure",
- RadrootsClientSqlError::ImportFailure => "error.client.sql.import_failure",
- RadrootsClientSqlError::BackupFailure => "error.client.sql.backup_failure",
- }
- }
-}
-
-impl fmt::Display for RadrootsClientSqlError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.write_str(self.message())
- }
-}
-
-impl std::error::Error for RadrootsClientSqlError {}
-
-#[cfg(test)]
-mod tests {
- use super::RadrootsClientSqlError;
-
- #[test]
- fn message_matches_spec() {
- let cases = [
- (
- RadrootsClientSqlError::IdbUndefined,
- "error.client.sql.idb_undefined",
- ),
- (
- RadrootsClientSqlError::EngineUnavailable,
- "error.client.sql.engine_unavailable",
- ),
- (
- RadrootsClientSqlError::InvalidParams,
- "error.client.sql.invalid_params",
- ),
- (
- RadrootsClientSqlError::QueryFailure,
- "error.client.sql.query_failure",
- ),
- (
- RadrootsClientSqlError::ExportFailure,
- "error.client.sql.export_failure",
- ),
- (
- RadrootsClientSqlError::ImportFailure,
- "error.client.sql.import_failure",
- ),
- (
- RadrootsClientSqlError::BackupFailure,
- "error.client.sql.backup_failure",
- ),
- ];
- for (err, expected) in cases {
- assert_eq!(err.message(), expected);
- assert_eq!(err.to_string(), expected);
- }
- }
-}
diff --git a/crates/core/src/sql/mod.rs b/crates/core/src/sql/mod.rs
@@ -1,19 +0,0 @@
-pub mod error;
-pub mod types;
-pub mod web;
-
-pub use error::{RadrootsClientSqlError, RadrootsClientSqlErrorMessage};
-pub use types::{
- RadrootsClientSqlCipherConfig,
- RadrootsClientSqlEncryptedStore,
- RadrootsClientSqlEngine,
- RadrootsClientSqlEngineConfig,
- RadrootsClientSqlExecOutcome,
- RadrootsClientSqlMigrationRow,
- RadrootsClientSqlMigrationState,
- RadrootsClientSqlParams,
- RadrootsClientSqlResultRow,
- RadrootsClientSqlResult,
- RadrootsClientSqlValue,
-};
-pub use web::RadrootsClientWebSqlEngine;
diff --git a/crates/core/src/sql/types.rs b/crates/core/src/sql/types.rs
@@ -1,105 +0,0 @@
-use std::collections::BTreeMap;
-
-use async_trait::async_trait;
-use serde_json::Value;
-
-use crate::backup::RadrootsClientBackupSqlPayload;
-use crate::idb::RadrootsClientIdbConfig;
-
-use super::RadrootsClientSqlError;
-
-pub type RadrootsClientSqlResult<T> = Result<T, RadrootsClientSqlError>;
-pub type RadrootsClientSqlValue = Value;
-pub type RadrootsClientSqlResultRow = BTreeMap<String, Value>;
-
-#[derive(Debug, Clone, PartialEq)]
-pub struct RadrootsClientSqlExecOutcome {
- pub changes: i64,
- pub last_insert_id: i64,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsClientSqlMigrationRow {
- pub id: i64,
- pub name: String,
- pub applied_at: String,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsClientSqlMigrationState {
- pub applied_names: Vec<String>,
- pub applied_count: usize,
-}
-
-#[derive(Debug, Clone, PartialEq)]
-pub enum RadrootsClientSqlParams {
- Named(BTreeMap<String, Value>),
- Positional(Vec<Value>),
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum RadrootsClientSqlCipherConfig {
- Default,
- Disabled,
- Custom(RadrootsClientIdbConfig),
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsClientSqlEngineConfig {
- pub store_key: String,
- pub idb_config: RadrootsClientIdbConfig,
- pub cipher_config: RadrootsClientSqlCipherConfig,
- pub sql_wasm_path: Option<String>,
-}
-
-#[async_trait(?Send)]
-pub trait RadrootsClientSqlEncryptedStore {
- async fn load(&self) -> RadrootsClientSqlResult<Option<Vec<u8>>>;
- async fn save(&self, bytes: &[u8]) -> RadrootsClientSqlResult<()>;
- async fn remove(&self) -> RadrootsClientSqlResult<()>;
-}
-
-#[async_trait(?Send)]
-pub trait RadrootsClientSqlEngine {
- async fn close(&self) -> RadrootsClientSqlResult<()>;
- async fn purge_storage(&self) -> RadrootsClientSqlResult<()>;
- fn exec(
- &self,
- sql: &str,
- params: RadrootsClientSqlParams,
- ) -> RadrootsClientSqlResult<RadrootsClientSqlExecOutcome>;
- fn query(
- &self,
- sql: &str,
- params: RadrootsClientSqlParams,
- ) -> RadrootsClientSqlResult<Vec<RadrootsClientSqlResultRow>>;
- fn export_bytes(&self) -> RadrootsClientSqlResult<Vec<u8>>;
- async fn import_bytes(&self, bytes: &[u8]) -> RadrootsClientSqlResult<()>;
- async fn export_backup(
- &self,
- ) -> RadrootsClientSqlResult<RadrootsClientBackupSqlPayload>;
- async fn import_backup(
- &self,
- payload: RadrootsClientBackupSqlPayload,
- ) -> RadrootsClientSqlResult<()>;
- fn get_store_id(&self) -> &str;
-}
-
-#[cfg(test)]
-mod tests {
- use super::{RadrootsClientSqlParams, RadrootsClientSqlValue};
-
- #[test]
- fn params_accept_positional_values() {
- let params = RadrootsClientSqlParams::Positional(vec![
- RadrootsClientSqlValue::from(1),
- RadrootsClientSqlValue::from("two"),
- ]);
- match params {
- RadrootsClientSqlParams::Positional(values) => {
- assert_eq!(values.len(), 2);
- }
- RadrootsClientSqlParams::Named(_) => panic!("expected positional params"),
- }
- }
-}
diff --git a/crates/core/src/sql/web.rs b/crates/core/src/sql/web.rs
@@ -1,515 +0,0 @@
-use std::collections::BTreeMap;
-use std::sync::{Arc, Mutex};
-
-use async_trait::async_trait;
-use rusqlite::types::{Value as SqlValue, ValueRef as SqlValueRef};
-use rusqlite::{params_from_iter, Connection, DatabaseName};
-use serde_json::Value;
-
-use crate::backup::{backup_b64_to_bytes, backup_bytes_to_b64, RadrootsClientBackupSqlPayload};
-#[cfg(target_arch = "wasm32")]
-use crate::crypto::RadrootsClientCryptoError;
-use crate::crypto::RadrootsClientLegacyKeyConfig;
-use crate::idb::{IDB_CONFIG_CIPHER_SQL, RadrootsClientIdbConfig};
-#[cfg(target_arch = "wasm32")]
-use crate::idb::RadrootsClientIdbStoreError;
-use crate::idb::{RadrootsClientWebEncryptedStore, RadrootsClientWebEncryptedStoreConfig};
-
-use super::{
- RadrootsClientSqlCipherConfig,
- RadrootsClientSqlEncryptedStore,
- RadrootsClientSqlEngine,
- RadrootsClientSqlEngineConfig,
- RadrootsClientSqlError,
- RadrootsClientSqlExecOutcome,
- RadrootsClientSqlParams,
- RadrootsClientSqlResult,
- RadrootsClientSqlResultRow,
- RadrootsClientSqlValue,
-};
-
-const SQL_STORE_PREFIX: &str = "sql";
-const DEFAULT_IV_LENGTH: u32 = 12;
-
-pub struct RadrootsClientWebSqlEncryptedStore {
- #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
- store_key: String,
- store_id: String,
- #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
- encrypted_store: RadrootsClientWebEncryptedStore,
-}
-
-impl RadrootsClientWebSqlEncryptedStore {
- pub fn new(config: &RadrootsClientSqlEngineConfig) -> Self {
- let store_key = config.store_key.clone();
- let store_id = format!("{SQL_STORE_PREFIX}:{store_key}");
- let legacy_idb_config = resolve_cipher_config(&config.cipher_config);
- let legacy_key = legacy_idb_config.map(|idb_config| RadrootsClientLegacyKeyConfig {
- idb_config,
- key_name: format!("radroots.sql.{store_key}.aes-gcm.key"),
- iv_length: DEFAULT_IV_LENGTH,
- algorithm: String::from("AES-GCM"),
- });
- let encrypted_store = RadrootsClientWebEncryptedStore::new(
- RadrootsClientWebEncryptedStoreConfig {
- idb_config: config.idb_config,
- store_id: store_id.clone(),
- legacy_key,
- iv_length: Some(DEFAULT_IV_LENGTH),
- crypto_service: None,
- },
- );
- Self {
- store_key,
- store_id,
- encrypted_store,
- }
- }
-
- pub fn get_store_id(&self) -> &str {
- &self.store_id
- }
-}
-
-#[async_trait(?Send)]
-impl RadrootsClientSqlEncryptedStore for RadrootsClientWebSqlEncryptedStore {
- async fn load(&self) -> RadrootsClientSqlResult<Option<Vec<u8>>> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- return Err(RadrootsClientSqlError::IdbUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- self.encrypted_store
- .ensure_store()
- .await
- .map_err(map_crypto_error)?;
- let stored = crate::idb::idb_get(
- self.encrypted_store.get_config().database,
- self.encrypted_store.get_config().store,
- &self.store_key,
- )
- .await
- .map_err(map_idb_error)?;
- let Some(stored) = stored else {
- return Ok(None);
- };
- let Some(bytes) = crate::idb::idb_value_as_bytes(&stored) else {
- return Ok(None);
- };
- let outcome = self
- .encrypted_store
- .decrypt_record(&bytes)
- .await
- .map_err(map_crypto_error)?;
- if let Some(reencrypted) = outcome.reencrypted {
- let value = js_sys::Uint8Array::from(&reencrypted[..]);
- crate::idb::idb_set(
- self.encrypted_store.get_config().database,
- self.encrypted_store.get_config().store,
- &self.store_key,
- &value.into(),
- )
- .await
- .map_err(map_idb_error)?;
- }
- Ok(Some(outcome.plaintext))
- }
- }
-
- async fn save(&self, bytes: &[u8]) -> RadrootsClientSqlResult<()> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = bytes;
- return Err(RadrootsClientSqlError::IdbUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- self.encrypted_store
- .ensure_store()
- .await
- .map_err(map_crypto_error)?;
- let encrypted = self
- .encrypted_store
- .encrypt_bytes(bytes)
- .await
- .map_err(map_crypto_error)?;
- let value = js_sys::Uint8Array::from(&encrypted[..]);
- crate::idb::idb_set(
- self.encrypted_store.get_config().database,
- self.encrypted_store.get_config().store,
- &self.store_key,
- &value.into(),
- )
- .await
- .map_err(map_idb_error)?;
- Ok(())
- }
- }
-
- async fn remove(&self) -> RadrootsClientSqlResult<()> {
- #[cfg(not(target_arch = "wasm32"))]
- {
- return Err(RadrootsClientSqlError::IdbUndefined);
- }
- #[cfg(target_arch = "wasm32")]
- {
- crate::idb::idb_del(
- self.encrypted_store.get_config().database,
- self.encrypted_store.get_config().store,
- &self.store_key,
- )
- .await
- .map_err(map_idb_error)?;
- Ok(())
- }
- }
-}
-
-pub struct RadrootsClientWebSqlEngine {
- store_id: String,
- store: Arc<RadrootsClientWebSqlEncryptedStore>,
- conn: Arc<Mutex<Connection>>,
-}
-
-impl RadrootsClientWebSqlEngine {
- pub async fn create(
- config: RadrootsClientSqlEngineConfig,
- ) -> RadrootsClientSqlResult<Self> {
- let store = Arc::new(RadrootsClientWebSqlEncryptedStore::new(&config));
- let conn = Connection::open_in_memory().map_err(map_rusqlite_error)?;
- let engine = Self {
- store_id: store.get_store_id().to_string(),
- store,
- conn: Arc::new(Mutex::new(conn)),
- };
- match engine.store.load().await {
- Ok(Some(bytes)) => {
- let _ = engine.import_bytes(&bytes).await?;
- }
- Ok(None) => {}
- Err(RadrootsClientSqlError::IdbUndefined) => {}
- Err(err) => return Err(err),
- }
- Ok(engine)
- }
-
- pub fn get_store_id(&self) -> &str {
- &self.store_id
- }
-
- #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
- pub(crate) fn shared_connection(&self) -> Arc<Mutex<Connection>> {
- Arc::clone(&self.conn)
- }
-
- fn exec_statement(
- &self,
- sql: &str,
- params: RadrootsClientSqlParams,
- ) -> RadrootsClientSqlResult<RadrootsClientSqlExecOutcome> {
- let conn = self.conn.lock().map_err(|_| RadrootsClientSqlError::EngineUnavailable)?;
- let mut stmt = conn.prepare(sql).map_err(map_rusqlite_error)?;
- let changes = match params {
- RadrootsClientSqlParams::Named(named) => {
- let named = build_named_params(&named)?;
- let mut refs = Vec::with_capacity(named.len());
- for (key, value) in &named {
- refs.push((key.as_str(), value as &dyn rusqlite::ToSql));
- }
- stmt.execute(refs.as_slice()).map_err(map_rusqlite_error)?
- }
- RadrootsClientSqlParams::Positional(values) => {
- let values = build_positional_params(&values)?;
- stmt.execute(params_from_iter(values.into_iter()))
- .map_err(map_rusqlite_error)?
- }
- };
- let last_insert_id = conn.last_insert_rowid();
- Ok(RadrootsClientSqlExecOutcome {
- changes: changes as i64,
- last_insert_id,
- })
- }
-
- fn query_statement(
- &self,
- sql: &str,
- params: RadrootsClientSqlParams,
- ) -> RadrootsClientSqlResult<Vec<RadrootsClientSqlResultRow>> {
- let conn = self.conn.lock().map_err(|_| RadrootsClientSqlError::EngineUnavailable)?;
- let mut stmt = conn.prepare(sql).map_err(map_rusqlite_error)?;
- let rows = match params {
- RadrootsClientSqlParams::Named(named) => {
- let named = build_named_params(&named)?;
- let mut refs = Vec::with_capacity(named.len());
- for (key, value) in &named {
- refs.push((key.as_str(), value as &dyn rusqlite::ToSql));
- }
- let mapped = stmt
- .query_map(refs.as_slice(), row_to_map)
- .map_err(map_rusqlite_error)?;
- mapped.collect::<Result<Vec<_>, _>>().map_err(map_rusqlite_error)?
- }
- RadrootsClientSqlParams::Positional(values) => {
- let values = build_positional_params(&values)?;
- let mapped = stmt
- .query_map(params_from_iter(values.into_iter()), row_to_map)
- .map_err(map_rusqlite_error)?;
- mapped.collect::<Result<Vec<_>, _>>().map_err(map_rusqlite_error)?
- }
- };
- Ok(rows)
- }
-
- fn export_bytes_inner(&self) -> RadrootsClientSqlResult<Vec<u8>> {
- let conn = self.conn.lock().map_err(|_| RadrootsClientSqlError::EngineUnavailable)?;
- let data = conn
- .serialize(DatabaseName::Main)
- .map_err(|_| RadrootsClientSqlError::ExportFailure)?;
- Ok(data.to_vec())
- }
-
- async fn persist(&self) -> RadrootsClientSqlResult<()> {
- let bytes = self.export_bytes_inner()?;
- match self.store.save(&bytes).await {
- Ok(()) => Ok(()),
- Err(RadrootsClientSqlError::IdbUndefined) => Ok(()),
- Err(err) => Err(err),
- }
- }
-}
-
-#[async_trait(?Send)]
-impl RadrootsClientSqlEngine for RadrootsClientWebSqlEngine {
- async fn close(&self) -> RadrootsClientSqlResult<()> {
- self.persist().await
- }
-
- async fn purge_storage(&self) -> RadrootsClientSqlResult<()> {
- match self.store.remove().await {
- Ok(()) => Ok(()),
- Err(RadrootsClientSqlError::IdbUndefined) => Ok(()),
- Err(err) => Err(err),
- }
- }
-
- fn exec(
- &self,
- sql: &str,
- params: RadrootsClientSqlParams,
- ) -> RadrootsClientSqlResult<RadrootsClientSqlExecOutcome> {
- self.exec_statement(sql, params)
- }
-
- fn query(
- &self,
- sql: &str,
- params: RadrootsClientSqlParams,
- ) -> RadrootsClientSqlResult<Vec<RadrootsClientSqlResultRow>> {
- self.query_statement(sql, params)
- }
-
- fn export_bytes(&self) -> RadrootsClientSqlResult<Vec<u8>> {
- self.export_bytes_inner()
- }
-
- async fn import_bytes(&self, _bytes: &[u8]) -> RadrootsClientSqlResult<()> {
- Err(RadrootsClientSqlError::ImportFailure)
- }
-
- async fn export_backup(&self) -> RadrootsClientSqlResult<RadrootsClientBackupSqlPayload> {
- let bytes = self.export_bytes_inner()?;
- let bytes_b64 = backup_bytes_to_b64(&bytes)
- .map_err(|_| RadrootsClientSqlError::BackupFailure)?;
- Ok(RadrootsClientBackupSqlPayload { bytes_b64 })
- }
-
- async fn import_backup(
- &self,
- payload: RadrootsClientBackupSqlPayload,
- ) -> RadrootsClientSqlResult<()> {
- let bytes = backup_b64_to_bytes(&payload.bytes_b64)
- .map_err(|_| RadrootsClientSqlError::BackupFailure)?;
- self.import_bytes(&bytes).await
- }
-
- fn get_store_id(&self) -> &str {
- &self.store_id
- }
-}
-
-fn resolve_cipher_config(
- cipher_config: &RadrootsClientSqlCipherConfig,
-) -> Option<RadrootsClientIdbConfig> {
- match cipher_config {
- RadrootsClientSqlCipherConfig::Default => Some(IDB_CONFIG_CIPHER_SQL),
- RadrootsClientSqlCipherConfig::Disabled => None,
- RadrootsClientSqlCipherConfig::Custom(config) => Some(*config),
- }
-}
-
-fn build_positional_params(
- values: &[RadrootsClientSqlValue],
-) -> RadrootsClientSqlResult<Vec<SqlValue>> {
- let mut binds = Vec::with_capacity(values.len());
- for value in values {
- binds.push(map_param_value(value)?);
- }
- Ok(binds)
-}
-
-fn build_named_params(
- values: &BTreeMap<String, RadrootsClientSqlValue>,
-) -> RadrootsClientSqlResult<Vec<(String, SqlValue)>> {
- let mut binds = Vec::with_capacity(values.len());
- for (key, value) in values {
- let key = if key.starts_with(':') || key.starts_with('@') || key.starts_with('$') {
- key.clone()
- } else {
- format!(":{key}")
- };
- binds.push((key, map_param_value(value)?));
- }
- Ok(binds)
-}
-
-fn map_param_value(value: &RadrootsClientSqlValue) -> RadrootsClientSqlResult<SqlValue> {
- match value {
- Value::Null => Ok(SqlValue::Null),
- Value::Bool(value) => Ok(SqlValue::Integer(i64::from(*value))),
- Value::Number(value) => {
- if let Some(v) = value.as_i64() {
- Ok(SqlValue::Integer(v))
- } else if let Some(v) = value.as_u64() {
- Ok(SqlValue::Integer(v as i64))
- } else if let Some(v) = value.as_f64() {
- Ok(SqlValue::Real(v))
- } else {
- Err(RadrootsClientSqlError::InvalidParams)
- }
- }
- Value::String(value) => Ok(SqlValue::Text(value.clone())),
- _ => Err(RadrootsClientSqlError::InvalidParams),
- }
-}
-
-fn row_to_map(row: &rusqlite::Row) -> rusqlite::Result<RadrootsClientSqlResultRow> {
- let stmt = row.as_ref();
- let mut map = BTreeMap::new();
- for i in 0..stmt.column_count() {
- let name = stmt.column_name(i).unwrap_or("").to_string();
- let value = row.get_ref(i)?;
- let json_value = match value {
- SqlValueRef::Null => Value::Null,
- SqlValueRef::Integer(i) => Value::from(i),
- SqlValueRef::Real(f) => Value::from(f),
- SqlValueRef::Text(s) => {
- let s = std::str::from_utf8(s).map_err(|e| {
- rusqlite::Error::FromSqlConversionFailure(
- i,
- rusqlite::types::Type::Text,
- Box::new(e),
- )
- })?;
- Value::from(s.to_string())
- }
- SqlValueRef::Blob(_) => Value::Null,
- };
- map.insert(name, json_value);
- }
- Ok(map)
-}
-
-#[cfg(target_arch = "wasm32")]
-fn map_crypto_error(err: RadrootsClientCryptoError) -> RadrootsClientSqlError {
- match err {
- RadrootsClientCryptoError::IdbUndefined => RadrootsClientSqlError::IdbUndefined,
- RadrootsClientCryptoError::CryptoUndefined => RadrootsClientSqlError::EngineUnavailable,
- _ => RadrootsClientSqlError::QueryFailure,
- }
-}
-
-#[cfg(target_arch = "wasm32")]
-fn map_idb_error(err: RadrootsClientIdbStoreError) -> RadrootsClientSqlError {
- match err {
- RadrootsClientIdbStoreError::IdbUndefined => RadrootsClientSqlError::IdbUndefined,
- _ => RadrootsClientSqlError::QueryFailure,
- }
-}
-
-fn map_rusqlite_error(_err: rusqlite::Error) -> RadrootsClientSqlError {
- RadrootsClientSqlError::QueryFailure
-}
-
-#[cfg(test)]
-mod tests {
- use std::collections::BTreeMap;
-
- use super::RadrootsClientWebSqlEngine;
- use crate::idb::RadrootsClientIdbConfig;
- use crate::sql::{
- RadrootsClientSqlCipherConfig,
- RadrootsClientSqlEngine,
- RadrootsClientSqlEngineConfig,
- RadrootsClientSqlParams,
- RadrootsClientSqlValue,
- };
-
- #[test]
- fn sql_exec_query_roundtrip() {
- let config = RadrootsClientSqlEngineConfig {
- store_key: "test-store".to_string(),
- idb_config: RadrootsClientIdbConfig::new("db", "store"),
- cipher_config: RadrootsClientSqlCipherConfig::Disabled,
- sql_wasm_path: None,
- };
- let engine = futures::executor::block_on(RadrootsClientWebSqlEngine::create(config))
- .expect("engine");
- let _ = engine.exec(
- "CREATE TABLE test_items (id INTEGER PRIMARY KEY, name TEXT)",
- RadrootsClientSqlParams::Positional(Vec::new()),
- );
- let _ = engine.exec(
- "INSERT INTO test_items (name) VALUES (?)",
- RadrootsClientSqlParams::Positional(vec![RadrootsClientSqlValue::from("rad")]),
- );
- let rows = engine
- .query(
- "SELECT name FROM test_items WHERE id = ?",
- RadrootsClientSqlParams::Positional(vec![RadrootsClientSqlValue::from(1)]),
- )
- .expect("query");
- let name = rows
- .first()
- .and_then(|row| row.get("name"))
- .and_then(|value| value.as_str())
- .expect("name");
- assert_eq!(name, "rad");
- }
-
- #[test]
- fn sql_named_params_execute() {
- let config = RadrootsClientSqlEngineConfig {
- store_key: "test-store".to_string(),
- idb_config: RadrootsClientIdbConfig::new("db", "store"),
- cipher_config: RadrootsClientSqlCipherConfig::Disabled,
- sql_wasm_path: None,
- };
- let engine = futures::executor::block_on(RadrootsClientWebSqlEngine::create(config))
- .expect("engine");
- let _ = engine.exec(
- "CREATE TABLE named_items (id INTEGER PRIMARY KEY, name TEXT)",
- RadrootsClientSqlParams::Positional(Vec::new()),
- );
- let mut named = BTreeMap::new();
- named.insert("name".to_string(), RadrootsClientSqlValue::from("rad"));
- let outcome = engine
- .exec(
- "INSERT INTO named_items (name) VALUES (:name)",
- RadrootsClientSqlParams::Named(named),
- )
- .expect("insert");
- assert_eq!(outcome.changes, 1);
- }
-}
diff --git a/crates/core/src/tangle/error.rs b/crates/core/src/tangle/error.rs
@@ -1,73 +0,0 @@
-use std::fmt;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsClientTangleError {
- InitFailure,
- ParseFailure,
- InvalidResponse,
- RuntimeUnavailable,
- CryptoUnavailable,
-}
-
-pub type RadrootsClientTangleErrorMessage = &'static str;
-
-impl RadrootsClientTangleError {
- pub const fn message(self) -> RadrootsClientTangleErrorMessage {
- match self {
- RadrootsClientTangleError::InitFailure => "error.client.tangle.init_failure",
- RadrootsClientTangleError::ParseFailure => "error.client.tangle.parse_failure",
- RadrootsClientTangleError::InvalidResponse => {
- "error.client.tangle.invalid_response"
- }
- RadrootsClientTangleError::RuntimeUnavailable => {
- "error.client.tangle.runtime_unavailable"
- }
- RadrootsClientTangleError::CryptoUnavailable => {
- "error.client.tangle.crypto_unavailable"
- }
- }
- }
-}
-
-impl fmt::Display for RadrootsClientTangleError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.write_str(self.message())
- }
-}
-
-impl std::error::Error for RadrootsClientTangleError {}
-
-#[cfg(test)]
-mod tests {
- use super::RadrootsClientTangleError;
-
- #[test]
- fn message_matches_spec() {
- let cases = [
- (
- RadrootsClientTangleError::InitFailure,
- "error.client.tangle.init_failure",
- ),
- (
- RadrootsClientTangleError::ParseFailure,
- "error.client.tangle.parse_failure",
- ),
- (
- RadrootsClientTangleError::InvalidResponse,
- "error.client.tangle.invalid_response",
- ),
- (
- RadrootsClientTangleError::RuntimeUnavailable,
- "error.client.tangle.runtime_unavailable",
- ),
- (
- RadrootsClientTangleError::CryptoUnavailable,
- "error.client.tangle.crypto_unavailable",
- ),
- ];
- for (err, expected) in cases {
- assert_eq!(err.message(), expected);
- assert_eq!(err.to_string(), expected);
- }
- }
-}
diff --git a/crates/core/src/tangle/mod.rs b/crates/core/src/tangle/mod.rs
@@ -1,22 +0,0 @@
-pub mod error;
-pub mod types;
-pub mod web;
-
-pub use error::{RadrootsClientTangleError, RadrootsClientTangleErrorMessage};
-pub use types::{
- RadrootsClientTangle,
- RadrootsClientTangleConfig,
- RadrootsClientTangleDatabaseExportManifest,
- RadrootsClientTangleDatabaseExportManifestClient,
- RadrootsClientTangleDatabaseExportManifestRs,
- RadrootsClientTangleDatabaseExportOptions,
- RadrootsClientTangleDatabaseExportSnapshot,
- RadrootsClientTangleDatabaseJsonExport,
- RadrootsClientTangleNostrEventDraft,
- RadrootsClientTangleNostrSyncBundle,
- RadrootsClientTangleNostrSyncOptions,
- RadrootsClientTangleNostrSyncSigner,
- RadrootsClientTangleNostrSyncSummary,
- RadrootsClientTangleResult,
-};
-pub use web::RadrootsClientWebTangle;
diff --git a/crates/core/src/tangle/types.rs b/crates/core/src/tangle/types.rs
@@ -1,419 +0,0 @@
-use async_trait::async_trait;
-use serde::{Deserialize, Serialize};
-use serde_json::Value;
-
-use radroots_replica_db::backup::DatabaseBackup;
-use radroots_replica_db::ReplicaDbExportManifestRs;
-pub use radroots_replica_db_schema::{
- farm::*,
- farm_gcs_location::*,
- farm_member::*,
- farm_member_claim::*,
- farm_tag::*,
- gcs_location::*,
- log_error::*,
- media_image::*,
- nostr_event_state::*,
- nostr_profile::*,
- nostr_profile_relay::*,
- nostr_relay::*,
- plot::*,
- plot_gcs_location::*,
- plot_tag::*,
- trade_product::*,
- trade_product_location::*,
- trade_product_media::*,
-};
-use radroots_replica_sync::{RadrootsReplicaEventDraft, RadrootsReplicaSyncBundle};
-
-use crate::idb::RadrootsClientIdbConfig;
-use crate::sql::{RadrootsClientSqlCipherConfig, RadrootsClientSqlMigrationState};
-
-use super::RadrootsClientTangleError;
-
-pub type RadrootsClientTangleResult<T> = Result<T, RadrootsClientTangleError>;
-pub type RadrootsClientTangleDatabaseJsonExport = DatabaseBackup;
-pub type RadrootsClientTangleDatabaseExportManifestRs = ReplicaDbExportManifestRs;
-pub type RadrootsClientTangleNostrEventDraft = RadrootsReplicaEventDraft;
-pub type RadrootsClientTangleNostrSyncBundle = RadrootsReplicaSyncBundle;
-
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
-pub struct RadrootsClientTangleDatabaseExportManifestClient {
- pub app_name: String,
- pub app_version: String,
- pub exported_at: String,
- pub db_sha256: String,
- pub db_size_bytes: u64,
- pub store_key: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub nostr_event: Option<Value>,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct RadrootsClientTangleDatabaseExportManifest {
- pub rust: RadrootsClientTangleDatabaseExportManifestRs,
- pub client: RadrootsClientTangleDatabaseExportManifestClient,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct RadrootsClientTangleDatabaseExportSnapshot {
- pub manifest: RadrootsClientTangleDatabaseExportManifest,
- pub db_bytes: Vec<u8>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsClientTangleDatabaseExportOptions {
- pub app_name: String,
- pub app_version: String,
- pub store_key: Option<String>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsClientTangleNostrSyncSigner {
- pub secret_key: String,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsClientTangleNostrSyncOptions {
- pub relays: Vec<String>,
- pub signers: Vec<RadrootsClientTangleNostrSyncSigner>,
- pub publish_timeout_ms: Option<u64>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsClientTangleNostrSyncSummary {
- pub events_total: usize,
- pub events_published: usize,
- pub events_failed: usize,
- pub events_skipped: usize,
- pub missing_signers: Vec<String>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsClientTangleConfig {
- pub store_key: Option<String>,
- pub idb_config: Option<RadrootsClientIdbConfig>,
- pub cipher_config: Option<RadrootsClientSqlCipherConfig>,
- pub sql_wasm_path: Option<String>,
-}
-
-#[async_trait(?Send)]
-pub trait RadrootsClientTangle {
- async fn init(&self) -> RadrootsClientTangleResult<()>;
- async fn close(&self) -> RadrootsClientTangleResult<()>;
- async fn migration_state(
- &self,
- ) -> RadrootsClientTangleResult<RadrootsClientSqlMigrationState>;
- async fn reset(&self) -> RadrootsClientTangleResult<RadrootsClientSqlMigrationState>;
- async fn reinit(&self) -> RadrootsClientTangleResult<RadrootsClientSqlMigrationState>;
- fn get_store_key(&self) -> &str;
- async fn export_json(
- &self,
- ) -> RadrootsClientTangleResult<RadrootsClientTangleDatabaseJsonExport>;
- async fn import_json(
- &self,
- backup: RadrootsClientTangleDatabaseJsonExport,
- ) -> RadrootsClientTangleResult<()>;
- async fn export_database(
- &self,
- opts: RadrootsClientTangleDatabaseExportOptions,
- ) -> RadrootsClientTangleResult<RadrootsClientTangleDatabaseExportSnapshot>;
- async fn nostr_sync_all(
- &self,
- opts: RadrootsClientTangleNostrSyncOptions,
- ) -> RadrootsClientTangleResult<RadrootsClientTangleNostrSyncSummary>;
- async fn farm_create(&self, opts: IFarmCreate) -> RadrootsClientTangleResult<IFarmCreateResolve>;
- async fn farm_find_one(&self, opts: IFarmFindOne) -> RadrootsClientTangleResult<IFarmFindOneResolve>;
- async fn farm_find_many(&self, opts: IFarmFindMany) -> RadrootsClientTangleResult<IFarmFindManyResolve>;
- async fn farm_delete(&self, opts: IFarmDelete) -> RadrootsClientTangleResult<IFarmDeleteResolve>;
- async fn farm_update(&self, opts: IFarmUpdate) -> RadrootsClientTangleResult<IFarmUpdateResolve>;
- async fn plot_create(&self, opts: IPlotCreate) -> RadrootsClientTangleResult<IPlotCreateResolve>;
- async fn plot_find_one(&self, opts: IPlotFindOne) -> RadrootsClientTangleResult<IPlotFindOneResolve>;
- async fn plot_find_many(&self, opts: IPlotFindMany) -> RadrootsClientTangleResult<IPlotFindManyResolve>;
- async fn plot_delete(&self, opts: IPlotDelete) -> RadrootsClientTangleResult<IPlotDeleteResolve>;
- async fn plot_update(&self, opts: IPlotUpdate) -> RadrootsClientTangleResult<IPlotUpdateResolve>;
- async fn gcs_location_create(
- &self,
- opts: IGcsLocationCreate,
- ) -> RadrootsClientTangleResult<IGcsLocationCreateResolve>;
- async fn gcs_location_find_one(
- &self,
- opts: IGcsLocationFindOne,
- ) -> RadrootsClientTangleResult<IGcsLocationFindOneResolve>;
- async fn gcs_location_find_many(
- &self,
- opts: IGcsLocationFindMany,
- ) -> RadrootsClientTangleResult<IGcsLocationFindManyResolve>;
- async fn gcs_location_delete(
- &self,
- opts: IGcsLocationDelete,
- ) -> RadrootsClientTangleResult<IGcsLocationDeleteResolve>;
- async fn gcs_location_update(
- &self,
- opts: IGcsLocationUpdate,
- ) -> RadrootsClientTangleResult<IGcsLocationUpdateResolve>;
- async fn farm_gcs_location_create(
- &self,
- opts: IFarmGcsLocationCreate,
- ) -> RadrootsClientTangleResult<IFarmGcsLocationCreateResolve>;
- async fn farm_gcs_location_find_one(
- &self,
- opts: IFarmGcsLocationFindOne,
- ) -> RadrootsClientTangleResult<IFarmGcsLocationFindOneResolve>;
- async fn farm_gcs_location_find_many(
- &self,
- opts: IFarmGcsLocationFindMany,
- ) -> RadrootsClientTangleResult<IFarmGcsLocationFindManyResolve>;
- async fn farm_gcs_location_delete(
- &self,
- opts: IFarmGcsLocationDelete,
- ) -> RadrootsClientTangleResult<IFarmGcsLocationDeleteResolve>;
- async fn farm_gcs_location_update(
- &self,
- opts: IFarmGcsLocationUpdate,
- ) -> RadrootsClientTangleResult<IFarmGcsLocationUpdateResolve>;
- async fn plot_gcs_location_create(
- &self,
- opts: IPlotGcsLocationCreate,
- ) -> RadrootsClientTangleResult<IPlotGcsLocationCreateResolve>;
- async fn plot_gcs_location_find_one(
- &self,
- opts: IPlotGcsLocationFindOne,
- ) -> RadrootsClientTangleResult<IPlotGcsLocationFindOneResolve>;
- async fn plot_gcs_location_find_many(
- &self,
- opts: IPlotGcsLocationFindMany,
- ) -> RadrootsClientTangleResult<IPlotGcsLocationFindManyResolve>;
- async fn plot_gcs_location_delete(
- &self,
- opts: IPlotGcsLocationDelete,
- ) -> RadrootsClientTangleResult<IPlotGcsLocationDeleteResolve>;
- async fn plot_gcs_location_update(
- &self,
- opts: IPlotGcsLocationUpdate,
- ) -> RadrootsClientTangleResult<IPlotGcsLocationUpdateResolve>;
- async fn farm_tag_create(
- &self,
- opts: IFarmTagCreate,
- ) -> RadrootsClientTangleResult<IFarmTagCreateResolve>;
- async fn farm_tag_find_one(
- &self,
- opts: IFarmTagFindOne,
- ) -> RadrootsClientTangleResult<IFarmTagFindOneResolve>;
- async fn farm_tag_find_many(
- &self,
- opts: IFarmTagFindMany,
- ) -> RadrootsClientTangleResult<IFarmTagFindManyResolve>;
- async fn farm_tag_delete(
- &self,
- opts: IFarmTagDelete,
- ) -> RadrootsClientTangleResult<IFarmTagDeleteResolve>;
- async fn farm_tag_update(
- &self,
- opts: IFarmTagUpdate,
- ) -> RadrootsClientTangleResult<IFarmTagUpdateResolve>;
- async fn plot_tag_create(
- &self,
- opts: IPlotTagCreate,
- ) -> RadrootsClientTangleResult<IPlotTagCreateResolve>;
- async fn plot_tag_find_one(
- &self,
- opts: IPlotTagFindOne,
- ) -> RadrootsClientTangleResult<IPlotTagFindOneResolve>;
- async fn plot_tag_find_many(
- &self,
- opts: IPlotTagFindMany,
- ) -> RadrootsClientTangleResult<IPlotTagFindManyResolve>;
- async fn plot_tag_delete(
- &self,
- opts: IPlotTagDelete,
- ) -> RadrootsClientTangleResult<IPlotTagDeleteResolve>;
- async fn plot_tag_update(
- &self,
- opts: IPlotTagUpdate,
- ) -> RadrootsClientTangleResult<IPlotTagUpdateResolve>;
- async fn farm_member_create(
- &self,
- opts: IFarmMemberCreate,
- ) -> RadrootsClientTangleResult<IFarmMemberCreateResolve>;
- async fn farm_member_find_one(
- &self,
- opts: IFarmMemberFindOne,
- ) -> RadrootsClientTangleResult<IFarmMemberFindOneResolve>;
- async fn farm_member_find_many(
- &self,
- opts: IFarmMemberFindMany,
- ) -> RadrootsClientTangleResult<IFarmMemberFindManyResolve>;
- async fn farm_member_delete(
- &self,
- opts: IFarmMemberDelete,
- ) -> RadrootsClientTangleResult<IFarmMemberDeleteResolve>;
- async fn farm_member_update(
- &self,
- opts: IFarmMemberUpdate,
- ) -> RadrootsClientTangleResult<IFarmMemberUpdateResolve>;
- async fn farm_member_claim_create(
- &self,
- opts: IFarmMemberClaimCreate,
- ) -> RadrootsClientTangleResult<IFarmMemberClaimCreateResolve>;
- async fn farm_member_claim_find_one(
- &self,
- opts: IFarmMemberClaimFindOne,
- ) -> RadrootsClientTangleResult<IFarmMemberClaimFindOneResolve>;
- async fn farm_member_claim_find_many(
- &self,
- opts: IFarmMemberClaimFindMany,
- ) -> RadrootsClientTangleResult<IFarmMemberClaimFindManyResolve>;
- async fn farm_member_claim_delete(
- &self,
- opts: IFarmMemberClaimDelete,
- ) -> RadrootsClientTangleResult<IFarmMemberClaimDeleteResolve>;
- async fn farm_member_claim_update(
- &self,
- opts: IFarmMemberClaimUpdate,
- ) -> RadrootsClientTangleResult<IFarmMemberClaimUpdateResolve>;
- async fn nostr_event_state_create(
- &self,
- opts: INostrEventStateCreate,
- ) -> RadrootsClientTangleResult<INostrEventStateCreateResolve>;
- async fn nostr_event_state_find_one(
- &self,
- opts: INostrEventStateFindOne,
- ) -> RadrootsClientTangleResult<INostrEventStateFindOneResolve>;
- async fn nostr_event_state_find_many(
- &self,
- opts: INostrEventStateFindMany,
- ) -> RadrootsClientTangleResult<INostrEventStateFindManyResolve>;
- async fn nostr_event_state_delete(
- &self,
- opts: INostrEventStateDelete,
- ) -> RadrootsClientTangleResult<INostrEventStateDeleteResolve>;
- async fn nostr_event_state_update(
- &self,
- opts: INostrEventStateUpdate,
- ) -> RadrootsClientTangleResult<INostrEventStateUpdateResolve>;
- async fn log_error_create(
- &self,
- opts: ILogErrorCreate,
- ) -> RadrootsClientTangleResult<ILogErrorCreateResolve>;
- async fn log_error_find_one(
- &self,
- opts: ILogErrorFindOne,
- ) -> RadrootsClientTangleResult<ILogErrorFindOneResolve>;
- async fn log_error_find_many(
- &self,
- opts: ILogErrorFindMany,
- ) -> RadrootsClientTangleResult<ILogErrorFindManyResolve>;
- async fn log_error_delete(
- &self,
- opts: ILogErrorDelete,
- ) -> RadrootsClientTangleResult<ILogErrorDeleteResolve>;
- async fn log_error_update(
- &self,
- opts: ILogErrorUpdate,
- ) -> RadrootsClientTangleResult<ILogErrorUpdateResolve>;
- async fn media_image_create(
- &self,
- opts: IMediaImageCreate,
- ) -> RadrootsClientTangleResult<IMediaImageCreateResolve>;
- async fn media_image_find_one(
- &self,
- opts: IMediaImageFindOne,
- ) -> RadrootsClientTangleResult<IMediaImageFindOneResolve>;
- async fn media_image_find_many(
- &self,
- opts: IMediaImageFindMany,
- ) -> RadrootsClientTangleResult<IMediaImageFindManyResolve>;
- async fn media_image_delete(
- &self,
- opts: IMediaImageDelete,
- ) -> RadrootsClientTangleResult<IMediaImageDeleteResolve>;
- async fn media_image_update(
- &self,
- opts: IMediaImageUpdate,
- ) -> RadrootsClientTangleResult<IMediaImageUpdateResolve>;
- async fn nostr_profile_create(
- &self,
- opts: INostrProfileCreate,
- ) -> RadrootsClientTangleResult<INostrProfileCreateResolve>;
- async fn nostr_profile_find_one(
- &self,
- opts: INostrProfileFindOne,
- ) -> RadrootsClientTangleResult<INostrProfileFindOneResolve>;
- async fn nostr_profile_find_many(
- &self,
- opts: INostrProfileFindMany,
- ) -> RadrootsClientTangleResult<INostrProfileFindManyResolve>;
- async fn nostr_profile_delete(
- &self,
- opts: INostrProfileDelete,
- ) -> RadrootsClientTangleResult<INostrProfileDeleteResolve>;
- async fn nostr_profile_update(
- &self,
- opts: INostrProfileUpdate,
- ) -> RadrootsClientTangleResult<INostrProfileUpdateResolve>;
- async fn nostr_relay_create(
- &self,
- opts: INostrRelayCreate,
- ) -> RadrootsClientTangleResult<INostrRelayCreateResolve>;
- async fn nostr_relay_find_one(
- &self,
- opts: INostrRelayFindOne,
- ) -> RadrootsClientTangleResult<INostrRelayFindOneResolve>;
- async fn nostr_relay_find_many(
- &self,
- opts: INostrRelayFindMany,
- ) -> RadrootsClientTangleResult<INostrRelayFindManyResolve>;
- async fn nostr_relay_delete(
- &self,
- opts: INostrRelayDelete,
- ) -> RadrootsClientTangleResult<INostrRelayDeleteResolve>;
- async fn nostr_relay_update(
- &self,
- opts: INostrRelayUpdate,
- ) -> RadrootsClientTangleResult<INostrRelayUpdateResolve>;
- async fn trade_product_create(
- &self,
- opts: ITradeProductCreate,
- ) -> RadrootsClientTangleResult<ITradeProductCreateResolve>;
- async fn trade_product_find_one(
- &self,
- opts: ITradeProductFindOne,
- ) -> RadrootsClientTangleResult<ITradeProductFindOneResolve>;
- async fn trade_product_find_many(
- &self,
- opts: ITradeProductFindMany,
- ) -> RadrootsClientTangleResult<ITradeProductFindManyResolve>;
- async fn trade_product_delete(
- &self,
- opts: ITradeProductDelete,
- ) -> RadrootsClientTangleResult<ITradeProductDeleteResolve>;
- async fn trade_product_update(
- &self,
- opts: ITradeProductUpdate,
- ) -> RadrootsClientTangleResult<ITradeProductUpdateResolve>;
- async fn nostr_profile_relay_set(
- &self,
- opts: INostrProfileRelayRelation,
- ) -> RadrootsClientTangleResult<INostrProfileRelayResolve>;
- async fn nostr_profile_relay_unset(
- &self,
- opts: INostrProfileRelayRelation,
- ) -> RadrootsClientTangleResult<INostrProfileRelayResolve>;
- async fn trade_product_location_set(
- &self,
- opts: ITradeProductLocationRelation,
- ) -> RadrootsClientTangleResult<ITradeProductLocationResolve>;
- async fn trade_product_location_unset(
- &self,
- opts: ITradeProductLocationRelation,
- ) -> RadrootsClientTangleResult<ITradeProductLocationResolve>;
- async fn trade_product_media_set(
- &self,
- opts: ITradeProductMediaRelation,
- ) -> RadrootsClientTangleResult<ITradeProductMediaResolve>;
- async fn trade_product_media_unset(
- &self,
- opts: ITradeProductMediaRelation,
- ) -> RadrootsClientTangleResult<ITradeProductMediaResolve>;
-}
diff --git a/crates/core/src/tangle/web.rs b/crates/core/src/tangle/web.rs
@@ -1,1216 +0,0 @@
-use std::cell::RefCell;
-use std::collections::{BTreeMap, BTreeSet};
-use std::rc::Rc;
-use std::str::FromStr;
-use std::sync::{Arc, Mutex};
-
-use async_trait::async_trait;
-use radroots_nostr::prelude::{
- radroots_event_from_nostr,
- radroots_nostr_build_event,
- RadrootsNostrClient,
- RadrootsNostrEvent,
- RadrootsNostrKeys,
- RadrootsNostrSecretKey,
-};
-use radroots_sql_core::error::SqlError;
-use radroots_sql_core::{ExecOutcome, SqlExecutor};
-use radroots_sql_core::sqlite_util;
-use radroots_replica_db::{export_manifest, ReplicaSql};
-use radroots_replica_sync::{
- radroots_replica_ingest_event,
- radroots_replica_sync_all,
- RadrootsReplicaEventDraft as RadrootsTangleEventDraft,
- RadrootsReplicaFarmSelector as RadrootsTangleFarmSelector,
- RadrootsReplicaSyncRequest as RadrootsTangleSyncRequest,
-};
-use rusqlite::{params_from_iter, Connection};
-use sha2::{Digest, Sha256};
-
-use crate::idb::{IDB_CONFIG_TANGLE, RadrootsClientIdbConfig};
-use crate::sql::{
- RadrootsClientSqlCipherConfig,
- RadrootsClientSqlEngine,
- RadrootsClientSqlEngineConfig,
- RadrootsClientSqlError,
- RadrootsClientSqlMigrationState,
- RadrootsClientSqlParams,
- RadrootsClientSqlResultRow,
- RadrootsClientWebSqlEngine,
-};
-
-use super::error::RadrootsClientTangleError;
-use super::types::{
- IFarmCreate,
- IFarmCreateResolve,
- IFarmDelete,
- IFarmDeleteResolve,
- IFarmFindMany,
- IFarmFindManyResolve,
- IFarmFindOne,
- IFarmFindOneResolve,
- IFarmUpdate,
- IFarmUpdateResolve,
- IFarmGcsLocationCreate,
- IFarmGcsLocationCreateResolve,
- IFarmGcsLocationDelete,
- IFarmGcsLocationDeleteResolve,
- IFarmGcsLocationFindMany,
- IFarmGcsLocationFindManyResolve,
- IFarmGcsLocationFindOne,
- IFarmGcsLocationFindOneResolve,
- IFarmGcsLocationUpdate,
- IFarmGcsLocationUpdateResolve,
- IFarmMemberClaimCreate,
- IFarmMemberClaimCreateResolve,
- IFarmMemberClaimDelete,
- IFarmMemberClaimDeleteResolve,
- IFarmMemberClaimFindMany,
- IFarmMemberClaimFindManyResolve,
- IFarmMemberClaimFindOne,
- IFarmMemberClaimFindOneResolve,
- IFarmMemberClaimUpdate,
- IFarmMemberClaimUpdateResolve,
- IFarmMemberCreate,
- IFarmMemberCreateResolve,
- IFarmMemberDelete,
- IFarmMemberDeleteResolve,
- IFarmMemberFindMany,
- IFarmMemberFindManyResolve,
- IFarmMemberFindOne,
- IFarmMemberFindOneResolve,
- IFarmMemberUpdate,
- IFarmMemberUpdateResolve,
- IFarmTagCreate,
- IFarmTagCreateResolve,
- IFarmTagDelete,
- IFarmTagDeleteResolve,
- IFarmTagFindMany,
- IFarmTagFindManyResolve,
- IFarmTagFindOne,
- IFarmTagFindOneResolve,
- IFarmTagUpdate,
- IFarmTagUpdateResolve,
- IGcsLocationCreate,
- IGcsLocationCreateResolve,
- IGcsLocationDelete,
- IGcsLocationDeleteResolve,
- IGcsLocationFindMany,
- IGcsLocationFindManyResolve,
- IGcsLocationFindOne,
- IGcsLocationFindOneResolve,
- IGcsLocationUpdate,
- IGcsLocationUpdateResolve,
- ILogErrorCreate,
- ILogErrorCreateResolve,
- ILogErrorDelete,
- ILogErrorDeleteResolve,
- ILogErrorFindMany,
- ILogErrorFindManyResolve,
- ILogErrorFindOne,
- ILogErrorFindOneResolve,
- ILogErrorUpdate,
- ILogErrorUpdateResolve,
- IMediaImageCreate,
- IMediaImageCreateResolve,
- IMediaImageDelete,
- IMediaImageDeleteResolve,
- IMediaImageFindMany,
- IMediaImageFindManyResolve,
- IMediaImageFindOne,
- IMediaImageFindOneResolve,
- IMediaImageUpdate,
- IMediaImageUpdateResolve,
- INostrEventStateCreate,
- INostrEventStateCreateResolve,
- INostrEventStateDelete,
- INostrEventStateDeleteResolve,
- INostrEventStateFindMany,
- INostrEventStateFindManyResolve,
- INostrEventStateFindOne,
- INostrEventStateFindOneResolve,
- INostrEventStateUpdate,
- INostrEventStateUpdateResolve,
- INostrProfileCreate,
- INostrProfileCreateResolve,
- INostrProfileDelete,
- INostrProfileDeleteResolve,
- INostrProfileFindMany,
- INostrProfileFindManyResolve,
- INostrProfileFindOne,
- INostrProfileFindOneResolve,
- INostrProfileUpdate,
- INostrProfileUpdateResolve,
- INostrRelayCreate,
- INostrRelayCreateResolve,
- INostrRelayDelete,
- INostrRelayDeleteResolve,
- INostrRelayFindMany,
- INostrRelayFindManyResolve,
- INostrRelayFindOne,
- INostrRelayFindOneResolve,
- INostrRelayUpdate,
- INostrRelayUpdateResolve,
- IPlotCreate,
- IPlotCreateResolve,
- IPlotDelete,
- IPlotDeleteResolve,
- IPlotFindMany,
- IPlotFindManyResolve,
- IPlotFindOne,
- IPlotFindOneResolve,
- IPlotUpdate,
- IPlotUpdateResolve,
- IPlotGcsLocationCreate,
- IPlotGcsLocationCreateResolve,
- IPlotGcsLocationDelete,
- IPlotGcsLocationDeleteResolve,
- IPlotGcsLocationFindMany,
- IPlotGcsLocationFindManyResolve,
- IPlotGcsLocationFindOne,
- IPlotGcsLocationFindOneResolve,
- IPlotGcsLocationUpdate,
- IPlotGcsLocationUpdateResolve,
- IPlotTagCreate,
- IPlotTagCreateResolve,
- IPlotTagDelete,
- IPlotTagDeleteResolve,
- IPlotTagFindMany,
- IPlotTagFindManyResolve,
- IPlotTagFindOne,
- IPlotTagFindOneResolve,
- IPlotTagUpdate,
- IPlotTagUpdateResolve,
- ITradeProductCreate,
- ITradeProductCreateResolve,
- ITradeProductDelete,
- ITradeProductDeleteResolve,
- ITradeProductFindMany,
- ITradeProductFindManyResolve,
- ITradeProductFindOne,
- ITradeProductFindOneResolve,
- ITradeProductUpdate,
- ITradeProductUpdateResolve,
- INostrProfileRelayRelation,
- INostrProfileRelayResolve,
- ITradeProductLocationRelation,
- ITradeProductLocationResolve,
- ITradeProductMediaRelation,
- ITradeProductMediaResolve,
- RadrootsClientTangle,
- RadrootsClientTangleConfig,
- RadrootsClientTangleDatabaseExportManifest,
- RadrootsClientTangleDatabaseExportManifestClient,
- RadrootsClientTangleDatabaseExportOptions,
- RadrootsClientTangleDatabaseExportSnapshot,
- RadrootsClientTangleDatabaseJsonExport,
- RadrootsClientTangleNostrSyncOptions,
- RadrootsClientTangleNostrSyncSigner,
- RadrootsClientTangleNostrSyncSummary,
- RadrootsClientTangleResult,
-};
-
-const DEFAULT_TANGLE_STORE_KEY: &str = "radroots-app-v1-tangle-db";
-
-pub struct RadrootsClientWebTangle {
- store_key: String,
- idb_config: RadrootsClientIdbConfig,
- cipher_config: RadrootsClientSqlCipherConfig,
- sql_wasm_path: Option<String>,
- engine: RefCell<Option<Rc<RadrootsClientWebSqlEngine>>>,
- init_in_progress: RefCell<bool>,
-}
-
-impl RadrootsClientWebTangle {
- pub fn new(config: Option<RadrootsClientTangleConfig>) -> Self {
- let config = config.unwrap_or(RadrootsClientTangleConfig {
- store_key: None,
- idb_config: None,
- cipher_config: None,
- sql_wasm_path: None,
- });
- let store_key = config
- .store_key
- .unwrap_or_else(|| DEFAULT_TANGLE_STORE_KEY.to_string());
- let idb_config = config.idb_config.unwrap_or(IDB_CONFIG_TANGLE);
- let cipher_config =
- config
- .cipher_config
- .unwrap_or(RadrootsClientSqlCipherConfig::Disabled);
- let sql_wasm_path = config.sql_wasm_path;
- Self {
- store_key,
- idb_config,
- cipher_config,
- sql_wasm_path,
- engine: RefCell::new(None),
- init_in_progress: RefCell::new(false),
- }
- }
-
- fn engine_config(&self) -> RadrootsClientSqlEngineConfig {
- RadrootsClientSqlEngineConfig {
- store_key: self.store_key.clone(),
- idb_config: self.idb_config,
- cipher_config: self.cipher_config.clone(),
- sql_wasm_path: self.sql_wasm_path.clone(),
- }
- }
-
- async fn init_engine(&self) -> RadrootsClientTangleResult<Rc<RadrootsClientWebSqlEngine>> {
- let engine = RadrootsClientWebSqlEngine::create(self.engine_config())
- .await
- .map_err(map_engine_error)?;
- let tangle = self.tangle(&engine);
- tangle.migrate_up().map_err(|_| RadrootsClientTangleError::InitFailure)?;
- let engine = Rc::new(engine);
- self.engine.borrow_mut().replace(engine.clone());
- Ok(engine)
- }
-
- async fn ensure_ready(&self) -> RadrootsClientTangleResult<Rc<RadrootsClientWebSqlEngine>> {
- if let Some(engine) = self.engine.borrow().as_ref() {
- return Ok(Rc::clone(engine));
- }
- if *self.init_in_progress.borrow() {
- return Err(RadrootsClientTangleError::InitFailure);
- }
- *self.init_in_progress.borrow_mut() = true;
- let result = self.init_engine().await;
- *self.init_in_progress.borrow_mut() = false;
- result
- }
-
- fn tangle(&self, engine: &RadrootsClientWebSqlEngine) -> ReplicaSql<TangleSqlExecutor> {
- ReplicaSql::new(TangleSqlExecutor::new(engine.shared_connection()))
- }
-}
-
-#[async_trait(?Send)]
-impl RadrootsClientTangle for RadrootsClientWebTangle {
- async fn init(&self) -> RadrootsClientTangleResult<()> {
- let _ = self.ensure_ready().await?;
- Ok(())
- }
-
- async fn close(&self) -> RadrootsClientTangleResult<()> {
- if let Some(engine) = self.engine.borrow_mut().take() {
- engine
- .close()
- .await
- .map_err(map_engine_error)?;
- }
- *self.init_in_progress.borrow_mut() = false;
- Ok(())
- }
-
- async fn migration_state(&self) -> RadrootsClientTangleResult<RadrootsClientSqlMigrationState> {
- let engine = self.ensure_ready().await?;
- let rows = engine
- .query(
- "select id, name, applied_at from __migrations order by id asc",
- RadrootsClientSqlParams::Positional(Vec::new()),
- )
- .map_err(map_engine_error)?;
- migration_state_from_rows(rows)
- }
-
- async fn reset(&self) -> RadrootsClientTangleResult<RadrootsClientSqlMigrationState> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- tangle.migrate_down().map_err(|_| RadrootsClientTangleError::InvalidResponse)?;
- tangle.migrate_up().map_err(|_| RadrootsClientTangleError::InvalidResponse)?;
- self.migration_state().await
- }
-
- async fn reinit(&self) -> RadrootsClientTangleResult<RadrootsClientSqlMigrationState> {
- if let Some(engine) = self.engine.borrow_mut().take() {
- engine
- .purge_storage()
- .await
- .map_err(map_engine_error)?;
- engine
- .close()
- .await
- .map_err(map_engine_error)?;
- }
- self.migration_state().await
- }
-
- fn get_store_key(&self) -> &str {
- &self.store_key
- }
-
- async fn export_json(
- &self,
- ) -> RadrootsClientTangleResult<RadrootsClientTangleDatabaseJsonExport> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- tangle
- .backup_database()
- .map_err(|_| RadrootsClientTangleError::InvalidResponse)
- }
-
- async fn import_json(
- &self,
- backup: RadrootsClientTangleDatabaseJsonExport,
- ) -> RadrootsClientTangleResult<()> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- tangle
- .restore_database(&backup)
- .map_err(|_| RadrootsClientTangleError::InvalidResponse)
- }
-
- async fn export_database(
- &self,
- opts: RadrootsClientTangleDatabaseExportOptions,
- ) -> RadrootsClientTangleResult<RadrootsClientTangleDatabaseExportSnapshot> {
- if let Some(store_key) = opts.store_key.clone() {
- if store_key != self.store_key {
- let alt = RadrootsClientWebTangle::new(Some(RadrootsClientTangleConfig {
- store_key: Some(store_key),
- idb_config: Some(self.idb_config),
- cipher_config: Some(self.cipher_config.clone()),
- sql_wasm_path: self.sql_wasm_path.clone(),
- }));
- let mut opts = opts.clone();
- opts.store_key = None;
- let snapshot = alt.export_database(opts).await?;
- let _ = alt.close().await;
- return Ok(snapshot);
- }
- }
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- let manifest_rs = export_manifest(tangle.executor())
- .map_err(|_| RadrootsClientTangleError::InvalidResponse)?;
- let db_bytes = engine.export_bytes().map_err(map_engine_error)?;
- let db_sha256 = sha256_hex(&db_bytes);
- let exported_at = export_timestamp();
- let manifest_client = RadrootsClientTangleDatabaseExportManifestClient {
- app_name: opts.app_name,
- app_version: opts.app_version,
- exported_at,
- db_sha256,
- db_size_bytes: db_bytes.len() as u64,
- store_key: self.store_key.clone(),
- nostr_event: None,
- };
- let manifest = RadrootsClientTangleDatabaseExportManifest {
- rust: manifest_rs,
- client: manifest_client,
- };
- Ok(RadrootsClientTangleDatabaseExportSnapshot { manifest, db_bytes })
- }
-
- async fn nostr_sync_all(
- &self,
- opts: RadrootsClientTangleNostrSyncOptions,
- ) -> RadrootsClientTangleResult<RadrootsClientTangleNostrSyncSummary> {
- let engine = self.ensure_ready().await?;
- let relays = normalize_relays(&opts.relays);
- if relays.is_empty() || opts.signers.is_empty() {
- return Err(RadrootsClientTangleError::InvalidResponse);
- }
- let signer_map = build_signer_map(&opts.signers);
- if signer_map.is_empty() {
- return Err(RadrootsClientTangleError::InvalidResponse);
- }
- let tangle = self.tangle(&engine);
- let farms = map_db_result(tangle.farm_find_many(&IFarmFindMany { filter: None }))?;
- let mut event_map: BTreeMap<String, RadrootsTangleEventDraft> = BTreeMap::new();
- for farm in farms.results {
- let request = RadrootsTangleSyncRequest {
- farm: RadrootsTangleFarmSelector {
- id: Some(farm.id),
- d_tag: None,
- pubkey: None,
- },
- options: None,
- };
- let bundle = radroots_replica_sync_all(tangle.executor(), &request)
- .map_err(|_| RadrootsClientTangleError::InvalidResponse)?;
- for draft in bundle.events {
- let key = tangle_sync_event_key(&draft);
- event_map.entry(key).or_insert(draft);
- }
- }
- if event_map.is_empty() {
- return Ok(RadrootsClientTangleNostrSyncSummary {
- events_total: 0,
- events_published: 0,
- events_failed: 0,
- events_skipped: 0,
- missing_signers: Vec::new(),
- });
- }
- let mut events_published = 0;
- let mut events_failed = 0;
- let mut events_skipped = 0;
- let mut missing_signers = BTreeSet::new();
-
- for draft in event_map.values() {
- let Some(secret_key) = signer_map.get(&draft.author) else {
- missing_signers.insert(draft.author.clone());
- events_skipped += 1;
- continue;
- };
- let event = sign_draft_event(draft, secret_key)?;
- match publish_signed_event(&relays, secret_key, &event).await {
- Ok(()) => {
- let event = radroots_event_from_nostr(&event);
- match radroots_replica_ingest_event(tangle.executor(), &event) {
- Ok(_) => events_published += 1,
- Err(_) => events_failed += 1,
- }
- }
- Err(_) => events_failed += 1,
- }
- }
- let summary = RadrootsClientTangleNostrSyncSummary {
- events_total: event_map.len(),
- events_published,
- events_failed,
- events_skipped,
- missing_signers: missing_signers.into_iter().collect(),
- };
- if !summary.missing_signers.is_empty() || summary.events_failed > 0 {
- return Err(RadrootsClientTangleError::InvalidResponse);
- }
- Ok(summary)
- }
-
- async fn farm_create(&self, opts: IFarmCreate) -> RadrootsClientTangleResult<IFarmCreateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.farm_create(&opts))
- }
-
- async fn farm_find_one(&self, opts: IFarmFindOne) -> RadrootsClientTangleResult<IFarmFindOneResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.farm_find_one(&opts))
- }
-
- async fn farm_find_many(&self, opts: IFarmFindMany) -> RadrootsClientTangleResult<IFarmFindManyResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.farm_find_many(&opts))
- }
-
- async fn farm_delete(&self, opts: IFarmDelete) -> RadrootsClientTangleResult<IFarmDeleteResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.farm_delete(&opts))
- }
-
- async fn farm_update(&self, opts: IFarmUpdate) -> RadrootsClientTangleResult<IFarmUpdateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.farm_update(&opts))
- }
-
- async fn plot_create(&self, opts: IPlotCreate) -> RadrootsClientTangleResult<IPlotCreateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.plot_create(&opts))
- }
-
- async fn plot_find_one(&self, opts: IPlotFindOne) -> RadrootsClientTangleResult<IPlotFindOneResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.plot_find_one(&opts))
- }
-
- async fn plot_find_many(&self, opts: IPlotFindMany) -> RadrootsClientTangleResult<IPlotFindManyResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.plot_find_many(&opts))
- }
-
- async fn plot_delete(&self, opts: IPlotDelete) -> RadrootsClientTangleResult<IPlotDeleteResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.plot_delete(&opts))
- }
-
- async fn plot_update(&self, opts: IPlotUpdate) -> RadrootsClientTangleResult<IPlotUpdateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.plot_update(&opts))
- }
-
- async fn gcs_location_create(&self, opts: IGcsLocationCreate) -> RadrootsClientTangleResult<IGcsLocationCreateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.gcs_location_create(&opts))
- }
-
- async fn gcs_location_find_one(&self, opts: IGcsLocationFindOne) -> RadrootsClientTangleResult<IGcsLocationFindOneResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.gcs_location_find_one(&opts))
- }
-
- async fn gcs_location_find_many(&self, opts: IGcsLocationFindMany) -> RadrootsClientTangleResult<IGcsLocationFindManyResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.gcs_location_find_many(&opts))
- }
-
- async fn gcs_location_delete(&self, opts: IGcsLocationDelete) -> RadrootsClientTangleResult<IGcsLocationDeleteResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.gcs_location_delete(&opts))
- }
-
- async fn gcs_location_update(&self, opts: IGcsLocationUpdate) -> RadrootsClientTangleResult<IGcsLocationUpdateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.gcs_location_update(&opts))
- }
-
- async fn farm_gcs_location_create(&self, opts: IFarmGcsLocationCreate) -> RadrootsClientTangleResult<IFarmGcsLocationCreateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.farm_gcs_location_create(&opts))
- }
-
- async fn farm_gcs_location_find_one(&self, opts: IFarmGcsLocationFindOne) -> RadrootsClientTangleResult<IFarmGcsLocationFindOneResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.farm_gcs_location_find_one(&opts))
- }
-
- async fn farm_gcs_location_find_many(&self, opts: IFarmGcsLocationFindMany) -> RadrootsClientTangleResult<IFarmGcsLocationFindManyResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.farm_gcs_location_find_many(&opts))
- }
-
- async fn farm_gcs_location_delete(&self, opts: IFarmGcsLocationDelete) -> RadrootsClientTangleResult<IFarmGcsLocationDeleteResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.farm_gcs_location_delete(&opts))
- }
-
- async fn farm_gcs_location_update(&self, opts: IFarmGcsLocationUpdate) -> RadrootsClientTangleResult<IFarmGcsLocationUpdateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.farm_gcs_location_update(&opts))
- }
-
- async fn plot_gcs_location_create(&self, opts: IPlotGcsLocationCreate) -> RadrootsClientTangleResult<IPlotGcsLocationCreateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.plot_gcs_location_create(&opts))
- }
-
- async fn plot_gcs_location_find_one(&self, opts: IPlotGcsLocationFindOne) -> RadrootsClientTangleResult<IPlotGcsLocationFindOneResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.plot_gcs_location_find_one(&opts))
- }
-
- async fn plot_gcs_location_find_many(&self, opts: IPlotGcsLocationFindMany) -> RadrootsClientTangleResult<IPlotGcsLocationFindManyResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.plot_gcs_location_find_many(&opts))
- }
-
- async fn plot_gcs_location_delete(&self, opts: IPlotGcsLocationDelete) -> RadrootsClientTangleResult<IPlotGcsLocationDeleteResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.plot_gcs_location_delete(&opts))
- }
-
- async fn plot_gcs_location_update(&self, opts: IPlotGcsLocationUpdate) -> RadrootsClientTangleResult<IPlotGcsLocationUpdateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.plot_gcs_location_update(&opts))
- }
-
- async fn farm_tag_create(&self, opts: IFarmTagCreate) -> RadrootsClientTangleResult<IFarmTagCreateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.farm_tag_create(&opts))
- }
-
- async fn farm_tag_find_one(&self, opts: IFarmTagFindOne) -> RadrootsClientTangleResult<IFarmTagFindOneResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.farm_tag_find_one(&opts))
- }
-
- async fn farm_tag_find_many(&self, opts: IFarmTagFindMany) -> RadrootsClientTangleResult<IFarmTagFindManyResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.farm_tag_find_many(&opts))
- }
-
- async fn farm_tag_delete(&self, opts: IFarmTagDelete) -> RadrootsClientTangleResult<IFarmTagDeleteResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.farm_tag_delete(&opts))
- }
-
- async fn farm_tag_update(&self, opts: IFarmTagUpdate) -> RadrootsClientTangleResult<IFarmTagUpdateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.farm_tag_update(&opts))
- }
-
- async fn plot_tag_create(&self, opts: IPlotTagCreate) -> RadrootsClientTangleResult<IPlotTagCreateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.plot_tag_create(&opts))
- }
-
- async fn plot_tag_find_one(&self, opts: IPlotTagFindOne) -> RadrootsClientTangleResult<IPlotTagFindOneResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.plot_tag_find_one(&opts))
- }
-
- async fn plot_tag_find_many(&self, opts: IPlotTagFindMany) -> RadrootsClientTangleResult<IPlotTagFindManyResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.plot_tag_find_many(&opts))
- }
-
- async fn plot_tag_delete(&self, opts: IPlotTagDelete) -> RadrootsClientTangleResult<IPlotTagDeleteResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.plot_tag_delete(&opts))
- }
-
- async fn plot_tag_update(&self, opts: IPlotTagUpdate) -> RadrootsClientTangleResult<IPlotTagUpdateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.plot_tag_update(&opts))
- }
-
- async fn farm_member_create(&self, opts: IFarmMemberCreate) -> RadrootsClientTangleResult<IFarmMemberCreateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.farm_member_create(&opts))
- }
-
- async fn farm_member_find_one(&self, opts: IFarmMemberFindOne) -> RadrootsClientTangleResult<IFarmMemberFindOneResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.farm_member_find_one(&opts))
- }
-
- async fn farm_member_find_many(&self, opts: IFarmMemberFindMany) -> RadrootsClientTangleResult<IFarmMemberFindManyResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.farm_member_find_many(&opts))
- }
-
- async fn farm_member_delete(&self, opts: IFarmMemberDelete) -> RadrootsClientTangleResult<IFarmMemberDeleteResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.farm_member_delete(&opts))
- }
-
- async fn farm_member_update(&self, opts: IFarmMemberUpdate) -> RadrootsClientTangleResult<IFarmMemberUpdateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.farm_member_update(&opts))
- }
-
- async fn farm_member_claim_create(&self, opts: IFarmMemberClaimCreate) -> RadrootsClientTangleResult<IFarmMemberClaimCreateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.farm_member_claim_create(&opts))
- }
-
- async fn farm_member_claim_find_one(&self, opts: IFarmMemberClaimFindOne) -> RadrootsClientTangleResult<IFarmMemberClaimFindOneResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.farm_member_claim_find_one(&opts))
- }
-
- async fn farm_member_claim_find_many(&self, opts: IFarmMemberClaimFindMany) -> RadrootsClientTangleResult<IFarmMemberClaimFindManyResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.farm_member_claim_find_many(&opts))
- }
-
- async fn farm_member_claim_delete(&self, opts: IFarmMemberClaimDelete) -> RadrootsClientTangleResult<IFarmMemberClaimDeleteResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.farm_member_claim_delete(&opts))
- }
-
- async fn farm_member_claim_update(&self, opts: IFarmMemberClaimUpdate) -> RadrootsClientTangleResult<IFarmMemberClaimUpdateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.farm_member_claim_update(&opts))
- }
-
- async fn nostr_event_state_create(&self, opts: INostrEventStateCreate) -> RadrootsClientTangleResult<INostrEventStateCreateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.nostr_event_state_create(&opts))
- }
-
- async fn nostr_event_state_find_one(&self, opts: INostrEventStateFindOne) -> RadrootsClientTangleResult<INostrEventStateFindOneResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.nostr_event_state_find_one(&opts))
- }
-
- async fn nostr_event_state_find_many(&self, opts: INostrEventStateFindMany) -> RadrootsClientTangleResult<INostrEventStateFindManyResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.nostr_event_state_find_many(&opts))
- }
-
- async fn nostr_event_state_delete(&self, opts: INostrEventStateDelete) -> RadrootsClientTangleResult<INostrEventStateDeleteResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.nostr_event_state_delete(&opts))
- }
-
- async fn nostr_event_state_update(&self, opts: INostrEventStateUpdate) -> RadrootsClientTangleResult<INostrEventStateUpdateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.nostr_event_state_update(&opts))
- }
-
- async fn log_error_create(&self, opts: ILogErrorCreate) -> RadrootsClientTangleResult<ILogErrorCreateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.log_error_create(&opts))
- }
-
- async fn log_error_find_one(&self, opts: ILogErrorFindOne) -> RadrootsClientTangleResult<ILogErrorFindOneResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.log_error_find_one(&opts))
- }
-
- async fn log_error_find_many(&self, opts: ILogErrorFindMany) -> RadrootsClientTangleResult<ILogErrorFindManyResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.log_error_find_many(&opts))
- }
-
- async fn log_error_delete(&self, opts: ILogErrorDelete) -> RadrootsClientTangleResult<ILogErrorDeleteResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.log_error_delete(&opts))
- }
-
- async fn log_error_update(&self, opts: ILogErrorUpdate) -> RadrootsClientTangleResult<ILogErrorUpdateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.log_error_update(&opts))
- }
-
- async fn media_image_create(&self, opts: IMediaImageCreate) -> RadrootsClientTangleResult<IMediaImageCreateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.media_image_create(&opts))
- }
-
- async fn media_image_find_one(&self, opts: IMediaImageFindOne) -> RadrootsClientTangleResult<IMediaImageFindOneResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.media_image_find_one(&opts))
- }
-
- async fn media_image_find_many(&self, opts: IMediaImageFindMany) -> RadrootsClientTangleResult<IMediaImageFindManyResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.media_image_find_many(&opts))
- }
-
- async fn media_image_delete(&self, opts: IMediaImageDelete) -> RadrootsClientTangleResult<IMediaImageDeleteResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.media_image_delete(&opts))
- }
-
- async fn media_image_update(&self, opts: IMediaImageUpdate) -> RadrootsClientTangleResult<IMediaImageUpdateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.media_image_update(&opts))
- }
-
- async fn nostr_profile_create(&self, opts: INostrProfileCreate) -> RadrootsClientTangleResult<INostrProfileCreateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.nostr_profile_create(&opts))
- }
-
- async fn nostr_profile_find_one(&self, opts: INostrProfileFindOne) -> RadrootsClientTangleResult<INostrProfileFindOneResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.nostr_profile_find_one(&opts))
- }
-
- async fn nostr_profile_find_many(&self, opts: INostrProfileFindMany) -> RadrootsClientTangleResult<INostrProfileFindManyResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.nostr_profile_find_many(&opts))
- }
-
- async fn nostr_profile_delete(&self, opts: INostrProfileDelete) -> RadrootsClientTangleResult<INostrProfileDeleteResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.nostr_profile_delete(&opts))
- }
-
- async fn nostr_profile_update(&self, opts: INostrProfileUpdate) -> RadrootsClientTangleResult<INostrProfileUpdateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.nostr_profile_update(&opts))
- }
-
- async fn nostr_relay_create(&self, opts: INostrRelayCreate) -> RadrootsClientTangleResult<INostrRelayCreateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.nostr_relay_create(&opts))
- }
-
- async fn nostr_relay_find_one(&self, opts: INostrRelayFindOne) -> RadrootsClientTangleResult<INostrRelayFindOneResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.nostr_relay_find_one(&opts))
- }
-
- async fn nostr_relay_find_many(&self, opts: INostrRelayFindMany) -> RadrootsClientTangleResult<INostrRelayFindManyResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.nostr_relay_find_many(&opts))
- }
-
- async fn nostr_relay_delete(&self, opts: INostrRelayDelete) -> RadrootsClientTangleResult<INostrRelayDeleteResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.nostr_relay_delete(&opts))
- }
-
- async fn nostr_relay_update(&self, opts: INostrRelayUpdate) -> RadrootsClientTangleResult<INostrRelayUpdateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.nostr_relay_update(&opts))
- }
-
- async fn trade_product_create(&self, opts: ITradeProductCreate) -> RadrootsClientTangleResult<ITradeProductCreateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.trade_product_create(&opts))
- }
-
- async fn trade_product_find_one(&self, opts: ITradeProductFindOne) -> RadrootsClientTangleResult<ITradeProductFindOneResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.trade_product_find_one(&opts))
- }
-
- async fn trade_product_find_many(&self, opts: ITradeProductFindMany) -> RadrootsClientTangleResult<ITradeProductFindManyResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.trade_product_find_many(&opts))
- }
-
- async fn trade_product_delete(&self, opts: ITradeProductDelete) -> RadrootsClientTangleResult<ITradeProductDeleteResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.trade_product_delete(&opts))
- }
-
- async fn trade_product_update(&self, opts: ITradeProductUpdate) -> RadrootsClientTangleResult<ITradeProductUpdateResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.trade_product_update(&opts))
- }
-
- async fn nostr_profile_relay_set(&self, opts: INostrProfileRelayRelation) -> RadrootsClientTangleResult<INostrProfileRelayResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.nostr_profile_relay_set(&opts))
- }
-
- async fn nostr_profile_relay_unset(&self, opts: INostrProfileRelayRelation) -> RadrootsClientTangleResult<INostrProfileRelayResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.nostr_profile_relay_unset(&opts))
- }
-
- async fn trade_product_location_set(&self, opts: ITradeProductLocationRelation) -> RadrootsClientTangleResult<ITradeProductLocationResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.trade_product_location_set(&opts))
- }
-
- async fn trade_product_location_unset(&self, opts: ITradeProductLocationRelation) -> RadrootsClientTangleResult<ITradeProductLocationResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.trade_product_location_unset(&opts))
- }
-
- async fn trade_product_media_set(&self, opts: ITradeProductMediaRelation) -> RadrootsClientTangleResult<ITradeProductMediaResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.trade_product_media_set(&opts))
- }
-
- async fn trade_product_media_unset(&self, opts: ITradeProductMediaRelation) -> RadrootsClientTangleResult<ITradeProductMediaResolve> {
- let engine = self.ensure_ready().await?;
- let tangle = self.tangle(&engine);
- map_db_result(tangle.trade_product_media_unset(&opts))
- }
-
-}
-
-struct TangleSqlExecutor {
- conn: Arc<Mutex<Connection>>,
-}
-
-impl TangleSqlExecutor {
- fn new(conn: Arc<Mutex<Connection>>) -> Self {
- Self { conn }
- }
-}
-
-impl SqlExecutor for TangleSqlExecutor {
- fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError> {
- let binds = sqlite_util::parse_params(params_json)?;
- let conn = self.conn.lock().map_err(|_| SqlError::Internal)?;
- let changes = conn
- .execute(sql, params_from_iter(binds.into_iter()))
- .map_err(SqlError::from)?;
- let last_insert_id = conn.last_insert_rowid();
- Ok(ExecOutcome {
- changes: changes as i64,
- last_insert_id,
- })
- }
-
- fn query_raw(&self, sql: &str, params_json: &str) -> Result<String, SqlError> {
- let binds = sqlite_util::parse_params(params_json)?;
- let rows = {
- let conn = self.conn.lock().map_err(|_| SqlError::Internal)?;
- let mut stmt = conn.prepare(sql).map_err(SqlError::from)?;
- let mapped = stmt.query_map(
- params_from_iter(binds.into_iter()),
- sqlite_util::row_to_json,
- )?;
- mapped
- .collect::<Result<Vec<_>, _>>()
- .map_err(SqlError::from)?
- };
- serde_json::to_string(&rows).map_err(SqlError::from)
- }
-
- fn begin(&self) -> Result<(), SqlError> {
- let conn = self.conn.lock().map_err(|_| SqlError::Internal)?;
- conn.execute("BEGIN", []).map_err(SqlError::from)?;
- Ok(())
- }
-
- fn commit(&self) -> Result<(), SqlError> {
- let conn = self.conn.lock().map_err(|_| SqlError::Internal)?;
- conn.execute("COMMIT", []).map_err(SqlError::from)?;
- Ok(())
- }
-
- fn rollback(&self) -> Result<(), SqlError> {
- let conn = self.conn.lock().map_err(|_| SqlError::Internal)?;
- conn.execute("ROLLBACK", []).map_err(SqlError::from)?;
- Ok(())
- }
-}
-
-fn map_engine_error(err: RadrootsClientSqlError) -> RadrootsClientTangleError {
- match err {
- RadrootsClientSqlError::EngineUnavailable => RadrootsClientTangleError::RuntimeUnavailable,
- RadrootsClientSqlError::IdbUndefined => RadrootsClientTangleError::RuntimeUnavailable,
- RadrootsClientSqlError::ImportFailure => RadrootsClientTangleError::InvalidResponse,
- RadrootsClientSqlError::ExportFailure => RadrootsClientTangleError::InvalidResponse,
- RadrootsClientSqlError::BackupFailure => RadrootsClientTangleError::InvalidResponse,
- RadrootsClientSqlError::InvalidParams => RadrootsClientTangleError::ParseFailure,
- RadrootsClientSqlError::QueryFailure => RadrootsClientTangleError::InvalidResponse,
- }
-}
-
-fn migration_state_from_rows(
- rows: Vec<RadrootsClientSqlResultRow>,
-) -> RadrootsClientTangleResult<RadrootsClientSqlMigrationState> {
- let mut names = Vec::with_capacity(rows.len());
- for row in rows {
- let name = row
- .get("name")
- .and_then(|value| value.as_str())
- .ok_or(RadrootsClientTangleError::ParseFailure)?;
- names.push(name.to_string());
- }
- Ok(RadrootsClientSqlMigrationState {
- applied_names: names.clone(),
- applied_count: names.len(),
- })
-}
-
-fn map_db_result<T, E>(result: Result<T, E>) -> RadrootsClientTangleResult<T> {
- result.map_err(|_| RadrootsClientTangleError::InvalidResponse)
-}
-
-fn normalize_relays(relays: &[String]) -> Vec<String> {
- let mut unique = BTreeSet::new();
- for relay in relays {
- let relay = relay.trim();
- if relay.is_empty() {
- continue;
- }
- unique.insert(relay.to_string());
- }
- unique.into_iter().collect()
-}
-
-fn tangle_sync_event_key(draft: &RadrootsTangleEventDraft) -> String {
- let d_tag = draft_d_tag(&draft.tags);
- format!("{}:{}:{}", draft.kind, draft.author, d_tag.unwrap_or_default())
-}
-
-fn draft_d_tag(tags: &[Vec<String>]) -> Option<String> {
- for tag in tags {
- if tag.first().map(|value| value.as_str()) == Some("d") {
- if let Some(value) = tag.get(1) {
- return Some(value.clone());
- }
- }
- }
- None
-}
-
-fn build_signer_map(
- signers: &[RadrootsClientTangleNostrSyncSigner],
-) -> BTreeMap<String, String> {
- let mut map = BTreeMap::new();
- for signer in signers {
- let secret_key = match RadrootsNostrSecretKey::from_str(&signer.secret_key) {
- Ok(secret_key) => secret_key,
- Err(_) => continue,
- };
- let keys = RadrootsNostrKeys::new(secret_key);
- map.insert(keys.public_key().to_hex(), signer.secret_key.clone());
- }
- map
-}
-
-fn sign_draft_event(
- draft: &RadrootsTangleEventDraft,
- secret_key: &str,
-) -> RadrootsClientTangleResult<RadrootsNostrEvent> {
- let secret_key = RadrootsNostrSecretKey::from_str(secret_key)
- .map_err(|_| RadrootsClientTangleError::CryptoUnavailable)?;
- let keys = RadrootsNostrKeys::new(secret_key);
- let builder = radroots_nostr_build_event(draft.kind, draft.content.clone(), draft.tags.clone())
- .map_err(|_| RadrootsClientTangleError::CryptoUnavailable)?;
- builder
- .sign_with_keys(&keys)
- .map_err(|_| RadrootsClientTangleError::CryptoUnavailable)
-}
-
-async fn build_publish_client(
- relays: &[String],
- secret_key: &str,
-) -> RadrootsClientTangleResult<RadrootsNostrClient> {
- let secret_key = RadrootsNostrSecretKey::from_str(secret_key)
- .map_err(|_| RadrootsClientTangleError::CryptoUnavailable)?;
- let client = RadrootsNostrClient::new(RadrootsNostrKeys::new(secret_key));
- for relay in relays {
- client
- .add_write_relay(relay)
- .await
- .map_err(|_| RadrootsClientTangleError::InvalidResponse)?;
- }
- Ok(client)
-}
-
-async fn publish_signed_event(
- relays: &[String],
- secret_key: &str,
- event: &RadrootsNostrEvent,
-) -> RadrootsClientTangleResult<()> {
- let client = build_publish_client(relays, secret_key).await?;
- client.connect().await;
- client
- .send_event(event)
- .await
- .map_err(|_| RadrootsClientTangleError::InvalidResponse)?;
- Ok(())
-}
-
-fn sha256_hex(bytes: &[u8]) -> String {
- let mut hasher = Sha256::new();
- hasher.update(bytes);
- hex::encode(hasher.finalize())
-}
-
-#[cfg(target_arch = "wasm32")]
-fn export_timestamp() -> String {
- js_sys::Date::new_0().to_iso_string().into()
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-fn export_timestamp() -> String {
- chrono::Utc::now().to_rfc3339()
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- build_publish_client,
- RadrootsClientWebTangle,
- DEFAULT_TANGLE_STORE_KEY,
- };
- use crate::tangle::{
- RadrootsClientTangle,
- RadrootsClientTangleError,
- RadrootsClientTangleNostrSyncOptions,
- RadrootsClientTangleNostrSyncSigner,
- };
-
- #[test]
- fn default_store_key_is_set() {
- let tangle = RadrootsClientWebTangle::new(None);
- assert_eq!(tangle.get_store_key(), DEFAULT_TANGLE_STORE_KEY);
- }
-
- #[test]
- fn nostr_sync_requires_relays() {
- let tangle = RadrootsClientWebTangle::new(None);
- let opts = RadrootsClientTangleNostrSyncOptions {
- relays: Vec::new(),
- signers: vec![RadrootsClientTangleNostrSyncSigner {
- secret_key: "deadbeef".to_string(),
- }],
- publish_timeout_ms: None,
- };
- let err = futures::executor::block_on(tangle.nostr_sync_all(opts))
- .expect_err("invalid response");
- assert_eq!(err, RadrootsClientTangleError::InvalidResponse);
- }
-
- #[test]
- fn build_publish_client_registers_requested_relays() {
- let client = futures::executor::block_on(build_publish_client(
- &[
- "wss://relay.one".to_string(),
- "wss://relay.two".to_string(),
- ],
- "5c4b53425ff8f8734229781dc8b4c40ce6bb6ed15c93dffa50e1399c049b8367",
- ))
- .expect("client");
-
- let relays = futures::executor::block_on(client.relays());
- assert_eq!(relays.len(), 2);
- }
-}
diff --git a/crates/ui-components/Cargo.toml b/crates/ui-components/Cargo.toml
@@ -1,17 +0,0 @@
-[package]
-name = "radroots-app-ui-components"
-authors = ["Radroots Authors"]
-version.workspace = true
-edition.workspace = true
-license.workspace = true
-rust-version.workspace = true
-
-[lib]
-crate-type = ["rlib"]
-
-[dependencies]
-radroots-app-ui-core = { path = "../ui-core" }
-radroots-app-ui-primitives = { path = "../ui-primitives" }
-leptos = { workspace = true, features = ["csr"] }
-icondata.workspace = true
-web-sys.workspace = true
diff --git a/crates/ui-components/assets/form.css b/crates/ui-components/assets/form.css
@@ -1,29 +0,0 @@
-@layer components {
- .form-field {
- @apply flex flex-col gap-2;
- }
-
- .form-field__label {
- @apply text-[0.72rem] font-semibold uppercase tracking-[0.08em] text-ly1-gl-label/80;
- }
-
- .form-field__control {
- @apply flex flex-col gap-2;
- }
-
- .form-field__hint {
- @apply text-xs text-ly1-gl-label/70;
- }
-
- .form-chips {
- @apply flex flex-wrap gap-2;
- }
-
- .form-chip {
- @apply inline-flex items-center justify-center rounded-full border border-ly1-edge/60 bg-ly1 px-3 py-1 text-sm text-ly1-gl transition-colors;
- }
-
- .form-chip[data-active="true"] {
- @apply border-ly1-edge/80 bg-ly1-focus text-ly1-gl;
- }
-}
diff --git a/crates/ui-components/assets/list.css b/crates/ui-components/assets/list.css
@@ -1,191 +0,0 @@
-@layer components {
- .w-trellis_display {
- width: var(--size-trellis-display);
- min-width: var(--size-trellis-display);
- }
-
- .w-trellis_value {
- width: var(--size-trellis-value);
- min-width: var(--size-trellis-value);
- }
-
- .w-trellisOffset {
- width: var(--size-trellis-offset);
- min-width: var(--size-trellis-offset);
- }
-
- .text-trellis_ti {
- font: 0.8rem/1rem var(--font-sans);
- }
-
- .text-line_d {
- font: var(--type-body);
- }
-
- .text-line_d_e {
- font: var(--type-subheadline);
- }
-
- .text-ly0-gl-label {
- color: var(--text-secondary);
- }
-
- .text-form_base {
- font: var(--type-body);
- }
-
- .border-t-line {
- border-top: 1px solid var(--separator);
- }
-
- .border-b-line {
- border-bottom: 1px solid var(--separator);
- }
-
- .el-re {
- transition: all var(--dur-2) var(--ease-ios);
- }
-
- .opacity-active:active,
- .group:active .opacity-active {
- opacity: 0.8;
- }
-
- .el-textarea {
- width: 100%;
- height: max-content;
- outline: none;
- border-radius: var(--radius-xl);
- text-wrap: wrap;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- padding: 0;
- }
-
- .el-input,
- .el-select,
- .el-textarea {
- display: flex;
- flex-direction: row;
- width: 100%;
- justify-content: center;
- align-items: center;
- border: 0;
- outline: 0;
- background: transparent;
- font: var(--type-body);
- }
-
- .el-input::placeholder,
- .el-select::placeholder,
- .el-textarea::placeholder {
- font: var(--type-body);
- }
-
- .el-select-centered {
- text-align: center;
- text-align-last: center;
- }
-
- .list-group-surface {
- background: var(--bg-elevated);
- border-radius: var(--radius-lg);
- box-shadow: var(--shadow-1);
- overflow: hidden;
- }
-
- .list-row-surface {
- background: var(--bg-elevated);
- }
-
- .list-row-surface:active {
- background: var(--material-chrome);
- }
-
- .list-row-surface:focus-within {
- background: var(--material-chrome);
- }
-
- [data-ui="list-group"] {
- width: 100%;
- }
-
- [data-ui="list-row"] {
- width: 100%;
- }
-
- [data-ui="list-row-leading"] {
- font: var(--type-body);
- color: var(--text-primary);
- }
-
- [data-ui="list-row-trailing"] {
- font: var(--type-subheadline);
- color: var(--text-secondary);
- }
-
- [data-ui="list-line"] {
- min-height: var(--size-line);
- }
-
- [data-ui="list-input"] input {
- text-align: left;
- }
-
- [data-ui="list-select"] select {
- min-width: var(--size-trellis-value);
- text-align: right;
- text-align-last: right;
- }
-
- [data-ui="list-select"] option {
- color: var(--text-primary);
- }
-
- .list-select-hit {
- position: absolute;
- inset: 0;
- width: 100%;
- height: 100%;
- opacity: 0;
- cursor: pointer;
- pointer-events: auto;
- border: 0;
- background: transparent;
- appearance: none;
- -webkit-appearance: none;
- -moz-appearance: none;
- z-index: 30;
- }
-
- .status-ok {
- color: var(--status-ok);
- }
-
- .status-warn {
- color: var(--status-warn);
- }
-
- .status-error {
- color: var(--status-error);
- }
-
- .status-neutral {
- color: var(--status-neutral);
- }
-
- .status-dot {
- font-size: 0.6rem;
- line-height: 1;
- }
-
- .status-pill {
- display: inline-flex;
- align-items: center;
- padding: 2px 8px;
- border-radius: 999px;
- font: var(--type-caption1);
- background: color-mix(in srgb, currentColor 12%, transparent);
- }
-}
diff --git a/crates/ui-components/assets/nav.css b/crates/ui-components/assets/nav.css
@@ -1,111 +0,0 @@
-.nav-header {
- position: sticky;
- top: 0;
- z-index: 20;
- display: flex;
- flex-direction: column;
- gap: 6px;
- padding: calc(var(--safe-t) + 6px) 16px 10px;
- background: transparent;
- isolation: isolate;
- --collapse: 0;
-}
-
-.nav-header__background {
- position: absolute;
- inset: 0;
- z-index: 0;
- opacity: 0;
- background: transparent;
- transition: opacity var(--dur-2) var(--ease-ios);
- pointer-events: none;
-}
-
-.nav-header__content {
- position: relative;
- z-index: 1;
- display: flex;
- flex-direction: column;
- gap: 6px;
-}
-
-.nav-header__bar {
- display: flex;
- align-items: center;
- justify-content: space-between;
- min-height: var(--nav-header-height);
-}
-
-.nav-header__compact,
-.nav-header__large {
- display: flex;
- align-items: center;
- justify-content: flex-start;
-}
-
-.nav-header__title-button {
- display: inline-flex;
- align-items: center;
- justify-content: flex-start;
- border: none;
- background: transparent;
- padding: 0;
-}
-
-.nav-header__title-text {
- font-family: var(--font-sansd);
- letter-spacing: -0.01em;
- text-transform: capitalize;
- max-width: min(72vw, 420px);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.nav-header__title-large {
- font-size: 34px;
- line-height: 41px;
- font-weight: 700;
- transform-origin: left top;
- transform: translateY(calc(var(--collapse) * -12px))
- scale(calc(1 - (var(--collapse) * 0.18)));
- opacity: calc(1 - var(--collapse));
- transition: transform var(--dur-2) var(--ease-ios), opacity var(--dur-2) var(--ease-ios);
-}
-
-.nav-header__title-compact {
- font-size: 17px;
- line-height: 22px;
- font-weight: 600;
- opacity: var(--collapse);
- transform: translateY(calc((1 - var(--collapse)) * 8px));
- transition: transform var(--dur-2) var(--ease-ios), opacity var(--dur-2) var(--ease-ios);
-}
-
-.nav-header__actions {
- display: inline-flex;
- align-items: center;
- justify-content: flex-end;
- gap: 8px;
-}
-
-.nav-header[data-bg="opaque"][data-bg-state="active"] .nav-header__background,
-.nav-header[data-bg="auto-opaque"][data-bg-state="active"] .nav-header__background {
- opacity: 1;
- background: var(--bg-app);
-}
-
-.nav-header[data-bg="blur"][data-bg-state="active"] .nav-header__background,
-.nav-header[data-bg="auto-blur"][data-bg-state="active"] .nav-header__background {
- opacity: 1;
- background: var(--material-regular);
- backdrop-filter: blur(18px) saturate(180%);
-}
-
-@media (prefers-reduced-motion: reduce) {
- .nav-header__background,
- .nav-header__title-large,
- .nav-header__title-compact {
- transition: none;
- }
-}
diff --git a/crates/ui-components/assets/nav_tabs.css b/crates/ui-components/assets/nav_tabs.css
@@ -1,53 +0,0 @@
-.nav-tabs {
- position: fixed;
- bottom: 0;
- left: 0;
- z-index: 20;
- width: 100%;
- height: var(--nav-tabs-height);
- padding: 8px 16px calc(var(--safe-b) + 8px);
- display: flex;
- align-items: flex-start;
- justify-content: center;
- transition: transform var(--dur-2) var(--ease-ios), opacity var(--dur-2) var(--ease-ios);
- will-change: transform, opacity;
-}
-
-.nav-tabs__tray {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 20px;
- padding: 8px 20px;
- border-radius: 999px;
- background: var(--material-regular);
- backdrop-filter: blur(18px) saturate(180%);
- box-shadow: var(--shadow-1);
-}
-
-.nav-tabs__item {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- height: 40px;
- width: 40px;
- border-radius: 999px;
- color: var(--text-secondary);
- transition: color var(--dur-1) var(--ease-ios);
-}
-
-.nav-tabs__item[data-active="true"] {
- color: var(--text-primary);
-}
-
-.nav-tabs[data-hidden="true"] {
- transform: translateY(calc(100% + 12px));
- opacity: 0;
- pointer-events: none;
-}
-
-@media (prefers-reduced-motion: reduce) {
- .nav-tabs {
- transition: none;
- }
-}
diff --git a/crates/ui-components/src/button.rs b/crates/ui-components/src/button.rs
@@ -1,25 +0,0 @@
-use leptos::prelude::*;
-
-#[component]
-pub fn RadrootsAppUiButton(
- #[prop(optional)] disabled: bool,
- #[prop(optional)] class: Option<String>,
- #[prop(optional)] id: Option<String>,
- #[prop(optional)] style: Option<String>,
- children: Children,
-) -> impl IntoView {
- let data_disabled = if disabled { Some("".to_string()) } else { None };
- view! {
- <button
- type="button"
- id=id
- class=class
- style=style
- disabled=disabled
- data-ui="button"
- data-disabled=data_disabled
- >
- {children()}
- </button>
- }
-}
diff --git a/crates/ui-components/src/button_layout.rs b/crates/ui-components/src/button_layout.rs
@@ -1,186 +0,0 @@
-#![forbid(unsafe_code)]
-
-use leptos::ev::MouseEvent;
-use leptos::prelude::*;
-
-use crate::RadrootsAppUiSpinner;
-
-fn radroots_app_ui_button_class_merge(parts: &[Option<&str>]) -> String {
- let mut result = String::new();
- for part in parts {
- let Some(value) = part else {
- continue;
- };
- if value.is_empty() {
- continue;
- }
- if !result.is_empty() {
- result.push(' ');
- }
- result.push_str(value);
- }
- result
-}
-
-#[derive(Clone)]
-pub struct RadrootsAppUiButtonLayoutAction {
- pub label: String,
- pub disabled: bool,
- pub loading: bool,
- pub on_click: Callback<MouseEvent>,
- pub class: Option<String>,
- pub class_label: Option<String>,
- pub style: Option<String>,
-}
-
-#[derive(Clone)]
-pub struct RadrootsAppUiButtonLayoutBackAction {
- pub visible: bool,
- pub label: Option<String>,
- pub disabled: bool,
- pub on_click: Callback<MouseEvent>,
- pub compact: bool,
-}
-
-#[component]
-pub fn RadrootsAppUiButtonLayout(
- label: String,
- on_click: Callback<MouseEvent>,
- #[prop(optional)] disabled: bool,
- #[prop(optional)] loading: bool,
- #[prop(optional)] class: Option<String>,
- #[prop(optional)] class_label: Option<String>,
- #[prop(optional)] style: Option<String>,
- #[prop(optional)] hide_active: bool,
-) -> impl IntoView {
- let allow_active = !disabled && !hide_active;
- let base_class = if allow_active {
- "button-layout"
- } else {
- "flex flex-row h-touch_guide w-lo_ios0 ios1:w-lo_ios1 justify-center items-center bg-ly1 rounded-touch el-re disabled:opacity-60"
- };
- let button_class = radroots_app_ui_button_class_merge(&[
- if allow_active { Some("group") } else { None },
- Some(base_class),
- class.as_deref(),
- ]);
- let label_class = radroots_app_ui_button_class_merge(&[
- Some("button-layout-label"),
- class_label.as_deref(),
- ]);
- view! {
- <button
- type="button"
- class=button_class
- style=style
- disabled=disabled
- on:click=move |ev| {
- ev.stop_propagation();
- if disabled {
- return;
- }
- on_click.run(ev);
- }
- >
- {move || {
- if loading {
- view! { <RadrootsAppUiSpinner class="text-[18px]".to_string() /> }.into_any()
- } else {
- view! { <span class=label_class.clone()>{label.clone()}</span> }.into_any()
- }
- }}
- </button>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiButtonLayoutPair(
- continue_action: RadrootsAppUiButtonLayoutAction,
- #[prop(optional)] back: Option<RadrootsAppUiButtonLayoutBackAction>,
- #[prop(optional)] class: Option<String>,
-) -> impl IntoView {
- let wrapper_class = radroots_app_ui_button_class_merge(&[
- Some("flex flex-col gap-1 justify-center items-center"),
- class.as_deref(),
- ]);
- view! {
- <div class=wrapper_class>
- <RadrootsAppUiButtonLayout
- label=continue_action.label
- disabled=continue_action.disabled
- loading=continue_action.loading
- on_click=continue_action.on_click
- class=continue_action.class.unwrap_or_default()
- class_label=continue_action.class_label.unwrap_or_default()
- style=continue_action.style.unwrap_or_default()
- />
- {back.map(|back_action| {
- view! {
- <div class="flex flex-col justify-center items-center">
- {{
- let back_label = back_action.label.clone().unwrap_or_default();
- let back_disabled = back_action.disabled;
- let back_on_click = back_action.on_click.clone();
- let back_visible = back_action.visible;
- let back_compact = back_action.compact;
- let back_text_class = radroots_app_ui_button_class_merge(&[
- Some("font-sans font-[600] tracking-wide text-ly1-gl-shade"),
- if back_disabled { None } else { Some("group-active:text-ly1-gl/40") },
- ]);
- let back_button_class = if back_compact {
- radroots_app_ui_button_class_merge(&[
- if back_disabled { None } else { Some("group") },
- Some("flex flex-row w-fit justify-center items-center py-1 transition-opacity duration-[160ms] ease-[cubic-bezier(.2,.8,.2,1)]"),
- if back_visible { Some("opacity-100") } else { Some("opacity-0 pointer-events-none") },
- ])
- } else {
- radroots_app_ui_button_class_merge(&[
- if back_disabled { None } else { Some("group") },
- Some("flex flex-row h-12 w-lo_ios0 ios1:w-lo_ios1 justify-center items-center -translate-y-[2px] transition-opacity duration-[160ms] ease-[cubic-bezier(.2,.8,.2,1)]"),
- if back_visible { Some("opacity-100") } else { Some("opacity-0 pointer-events-none") },
- ])
- };
- view! {
- <button
- type="button"
- class=back_button_class
- disabled=back_disabled
- on:click=move |ev| {
- ev.stop_propagation();
- if back_disabled {
- return;
- }
- back_on_click.run(ev);
- }
- >
- <span class=back_text_class>{back_label}</span>
- </button>
- }.into_any()
- }}
- </div>
- }
- })}
- </div>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiButtonLayoutBottom(
- #[prop(optional)] hidden: bool,
- #[prop(optional)] class: Option<String>,
- children: Children,
-) -> impl IntoView {
- if hidden {
- view! { <></> }.into_any()
- } else {
- let wrapper_class = radroots_app_ui_button_class_merge(&[
- Some("z-10 absolute bottom-0 h-lo_bottom_button_ios0 ios1:h-lo_bottom_button_ios1 flex flex-col w-full px-4 gap-1 justify-start items-center"),
- class.as_deref(),
- ]);
- view! {
- <div class=wrapper_class>
- {children()}
- </div>
- }.into_any()
- }
-}
diff --git a/crates/ui-components/src/dialog.rs b/crates/ui-components/src/dialog.rs
@@ -1,368 +0,0 @@
-use leptos::ev::MouseEvent;
-use leptos::html;
-use leptos::prelude::*;
-use std::sync::{Arc, Mutex};
-
-use radroots_app_ui_core::RadrootsAppUiId;
-use radroots_app_ui_primitives::{
- dialog_content_attrs,
- dialog_trigger_attrs,
- use_primitive,
- DialogModel,
- RadrootsAppUiDismissableReason,
- RadrootsAppUiDismissableLayer,
- RadrootsAppUiFocusScope,
- RadrootsAppUiModalGuard,
- RadrootsAppUiPresence,
- RadrootsAppUiPortal,
- RadrootsAppUiScrollLockGuard,
-};
-
-#[cfg(target_arch = "wasm32")]
-use radroots_app_ui_primitives::{
- radroots_app_ui_modal_hide_siblings,
- radroots_app_ui_scroll_lock_acquire,
-};
-
-#[derive(Clone)]
-struct RadrootsAppUiDialogContext {
- open: ReadSignal<bool>,
- set_open: Callback<bool>,
- dismiss: Callback<RadrootsAppUiDismissableReason>,
- modal: bool,
- content_id: String,
- title_id: RwSignal<Option<String>>,
- description_id: RwSignal<Option<String>>,
-}
-
-pub fn radroots_app_ui_dialog_state_value(open: bool) -> &'static str {
- if open {
- "open"
- } else {
- "closed"
- }
-}
-
-#[component]
-pub fn RadrootsAppUiDialogRoot(
- open: Option<ReadSignal<bool>>,
- #[prop(optional)] default_open: bool,
- modal: Option<bool>,
- on_open_change: Option<Callback<bool>>,
- children: ChildrenFn,
-) -> impl IntoView {
- let open_state = RwSignal::new(default_open);
- let open_prop = open;
- let is_controlled = open_prop.is_some();
- let open_signal = match open_prop {
- Some(open) => open,
- None => open_state.read_only(),
- };
- let on_open_change = on_open_change.clone();
- let set_open = Callback::new(move |value| {
- if !is_controlled {
- open_state.set(value);
- }
- if let Some(callback) = on_open_change.as_ref() {
- callback.run(value);
- }
- });
- let dismiss = {
- let set_open = set_open.clone();
- Callback::new(move |_reason: RadrootsAppUiDismissableReason| {
- set_open.run(false);
- })
- };
- let content_id = RadrootsAppUiId::next().prefixed("dialog-content");
- let modal = modal.unwrap_or(true);
- let title_id = RwSignal::new(None::<String>);
- let description_id = RwSignal::new(None::<String>);
- provide_context(RadrootsAppUiDialogContext {
- open: open_signal,
- set_open,
- dismiss,
- modal,
- content_id,
- title_id,
- description_id,
- });
- view! { <>{children()}</> }
-}
-
-#[component]
-pub fn RadrootsAppUiDialogTrigger(
- #[prop(optional)] disabled: bool,
- class: Option<String>,
- id: Option<String>,
- style: Option<String>,
- children: Children,
-) -> impl IntoView {
- let context = use_context::<RadrootsAppUiDialogContext>()
- .expect("dialog context");
- let open = context.open;
- let content_id = context.content_id.clone();
- let attrs = Signal::derive(move || {
- let model = DialogModel::new(open.get());
- dialog_trigger_attrs(&model, Some(content_id.as_str()))
- });
- let trigger = use_primitive::<html::Button>(attrs, Vec::new());
- let on_click = move |_event: MouseEvent| {
- if disabled {
- return;
- }
- context.set_open.run(true);
- };
- view! {
- <button
- node_ref=trigger.node_ref()
- type="button"
- id=id
- class=class
- style=style
- disabled=disabled
- data-ui="dialog-trigger"
- on:click=on_click
- >
- {children()}
- </button>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiDialogPortal(children: ChildrenFn) -> impl IntoView {
- let context = use_context::<RadrootsAppUiDialogContext>()
- .expect("dialog context");
- let present = Signal::derive(move || context.open.get());
- let children = StoredValue::new(children);
- view! {
- <RadrootsAppUiPortal>
- <RadrootsAppUiPresence present=present>
- {(children.get_value())()}
- </RadrootsAppUiPresence>
- </RadrootsAppUiPortal>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiDialogOverlay(
- close_on_click: Option<bool>,
- data_ui: Option<String>,
- class: Option<String>,
- id: Option<String>,
- style: Option<String>,
-) -> impl IntoView {
- let context = use_context::<RadrootsAppUiDialogContext>()
- .expect("dialog context");
- let close_on_click = close_on_click.unwrap_or(true);
- let data_ui = StoredValue::new(data_ui.unwrap_or_else(|| "dialog-overlay".to_string()));
- let on_click = move |_event: MouseEvent| {
- if close_on_click {
- context.set_open.run(false);
- }
- };
- view! {
- <div
- id=id
- class=class
- style=style
- data-ui=move || data_ui.get_value()
- data-state=move || radroots_app_ui_dialog_state_value(context.open.get())
- on:click=on_click
- ></div>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiDialogContent(
- #[prop(optional)] disable_outside_pointer_events: bool,
- data_ui: Option<String>,
- class: Option<String>,
- id: Option<String>,
- style: Option<String>,
- children: ChildrenFn,
-) -> impl IntoView {
- let context = use_context::<RadrootsAppUiDialogContext>()
- .expect("dialog context");
- let content_id = context.content_id.clone();
- let open = context.open;
- let title_id = context.title_id;
- let description_id = context.description_id;
- let scroll_guard = Arc::new(Mutex::new(None::<RadrootsAppUiScrollLockGuard>));
- let modal_guard = Arc::new(Mutex::new(None::<RadrootsAppUiModalGuard>));
- let modal = context.modal;
- let attrs = Signal::derive(move || {
- let mut model = DialogModel::new(open.get());
- model.set_modal(modal);
- dialog_content_attrs(
- &model,
- title_id.get().as_deref(),
- description_id.get().as_deref(),
- )
- });
- let primitive = use_primitive::<html::Div>(attrs, Vec::new());
- let node_ref = primitive.node_ref();
-
- #[cfg(target_arch = "wasm32")]
- {
- use leptos::wasm_bindgen::JsCast;
- use leptos::web_sys;
-
- let node_ref = node_ref;
- let scroll_guard = Arc::clone(&scroll_guard);
- let modal_guard = Arc::clone(&modal_guard);
- node_ref.on_load(move |root| {
- if modal {
- if let Ok(guard) = radroots_app_ui_scroll_lock_acquire() {
- let mut state = scroll_guard
- .lock()
- .unwrap_or_else(|poisoned| poisoned.into_inner());
- *state = Some(guard);
- }
- let element: web_sys::Element = root.unchecked_into();
- if let Ok(guard) = radroots_app_ui_modal_hide_siblings(&element) {
- let mut state = modal_guard
- .lock()
- .unwrap_or_else(|poisoned| poisoned.into_inner());
- *state = Some(guard);
- }
- }
- });
- }
-
- let scroll_guard_cleanup = Arc::clone(&scroll_guard);
- let modal_guard_cleanup = Arc::clone(&modal_guard);
- on_cleanup(move || {
- let _ = scroll_guard_cleanup
- .lock()
- .unwrap_or_else(|poisoned| poisoned.into_inner())
- .take();
- let _ = modal_guard_cleanup
- .lock()
- .unwrap_or_else(|poisoned| poisoned.into_inner())
- .take();
- });
-
- let on_dismiss = context.dismiss.clone();
-
- let data_ui = StoredValue::new(data_ui.unwrap_or_else(|| "dialog".to_string()));
- let id_value = StoredValue::new(id.unwrap_or_else(|| content_id.clone()));
- let class_value = StoredValue::new(class);
- let style_value = StoredValue::new(style);
- let children = StoredValue::new(children);
-
- view! {
- <RadrootsAppUiDismissableLayer
- on_dismiss=on_dismiss
- disable_pointer_down_outside_dismiss=disable_outside_pointer_events
- >
- <RadrootsAppUiFocusScope trapped=modal auto_focus=true return_focus=true>
- <div
- node_ref=node_ref
- id=move || id_value.get_value()
- class=move || class_value.get_value()
- style=move || style_value.get_value()
- data-ui=move || data_ui.get_value()
- >
- {(children.get_value())()}
- </div>
- </RadrootsAppUiFocusScope>
- </RadrootsAppUiDismissableLayer>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiDialogTitle(
- class: Option<String>,
- id: Option<String>,
- style: Option<String>,
- children: Children,
-) -> impl IntoView {
- let context = use_context::<RadrootsAppUiDialogContext>()
- .expect("dialog context");
- let title_id = id.unwrap_or_else(|| RadrootsAppUiId::next().prefixed("dialog-title"));
- context.title_id.set(Some(title_id.clone()));
- let title_id_cleanup = title_id.clone();
- let title_signal = context.title_id;
- on_cleanup(move || {
- if title_signal.get_untracked().as_deref() == Some(&title_id_cleanup) {
- title_signal.set(None);
- }
- });
- view! {
- <h2
- id=title_id
- class=class
- style=style
- data-ui="dialog-title"
- >
- {children()}
- </h2>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiDialogDescription(
- class: Option<String>,
- id: Option<String>,
- style: Option<String>,
- children: Children,
-) -> impl IntoView {
- let context = use_context::<RadrootsAppUiDialogContext>()
- .expect("dialog context");
- let description_id = id.unwrap_or_else(|| RadrootsAppUiId::next().prefixed("dialog-desc"));
- context.description_id.set(Some(description_id.clone()));
- let desc_id_cleanup = description_id.clone();
- let desc_signal = context.description_id;
- on_cleanup(move || {
- if desc_signal.get_untracked().as_deref() == Some(&desc_id_cleanup) {
- desc_signal.set(None);
- }
- });
- view! {
- <p
- id=description_id
- class=class
- style=style
- data-ui="dialog-description"
- >
- {children()}
- </p>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiDialogClose(
- class: Option<String>,
- id: Option<String>,
- style: Option<String>,
- children: Children,
-) -> impl IntoView {
- let context = use_context::<RadrootsAppUiDialogContext>()
- .expect("dialog context");
- let on_click = move |_event: MouseEvent| {
- context.set_open.run(false);
- };
- view! {
- <button
- type="button"
- id=id
- class=class
- style=style
- data-ui="dialog-close"
- on:click=on_click
- >
- {children()}
- </button>
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::radroots_app_ui_dialog_state_value;
-
- #[test]
- fn dialog_state_value_matches_open() {
- assert_eq!(radroots_app_ui_dialog_state_value(true), "open");
- assert_eq!(radroots_app_ui_dialog_state_value(false), "closed");
- }
-}
diff --git a/crates/ui-components/src/form.rs b/crates/ui-components/src/form.rs
@@ -1,74 +0,0 @@
-#![forbid(unsafe_code)]
-
-use leptos::ev::MouseEvent;
-use leptos::prelude::*;
-
-use crate::RadrootsAppUiLabel;
-
-#[component]
-pub fn RadrootsAppUiFormField(
- label: String,
- #[prop(optional)] hint: Option<String>,
- #[prop(optional)] id: Option<String>,
- #[prop(optional)] class: Option<String>,
- children: Children,
-) -> impl IntoView {
- view! {
- <section id=id class=class.unwrap_or_else(|| "form-field".to_string())>
- <RadrootsAppUiLabel class="form-field__label".to_string()>
- {label}
- </RadrootsAppUiLabel>
- <div class="form-field__control">{children()}</div>
- {move || {
- hint.clone()
- .map(|value| view! { <p class="form-field__hint">{value}</p> }.into_any())
- .unwrap_or_else(|| view! { <></> }.into_any())
- }}
- </section>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiChips(
- #[prop(optional)] id: Option<String>,
- #[prop(optional)] class: Option<String>,
- children: Children,
-) -> impl IntoView {
- let class_value = match class {
- Some(value) => format!("form-chips {value}"),
- None => "form-chips".to_string(),
- };
- view! {
- <div id=id class=class_value>
- {children()}
- </div>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiChip(
- label: String,
- active: bool,
- #[prop(optional)] class: Option<String>,
- #[prop(optional)] on_click: Option<Callback<MouseEvent>>,
-) -> impl IntoView {
- let class_value = match class {
- Some(value) => format!("form-chip {value}"),
- None => "form-chip".to_string(),
- };
- let on_click = move |ev: MouseEvent| {
- if let Some(handler) = on_click {
- handler.run(ev);
- }
- };
- view! {
- <button
- type="button"
- class=class_value
- attr:data-active=move || if active { "true" } else { "false" }
- on:click=on_click
- >
- {label}
- </button>
- }
-}
diff --git a/crates/ui-components/src/icon.rs b/crates/ui-components/src/icon.rs
@@ -1,147 +0,0 @@
-#![forbid(unsafe_code)]
-
-use icondata::{Icon, LuBeaker, LuChevronRight, LuChevronsUpDown, LuHouse, LuPlus, LuSettings};
-use leptos::prelude::*;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
-pub enum RadrootsAppUiIconKey {
- CaretRight,
- CaretUpDown,
- Plus,
- Settings,
- Home,
- Beaker,
-}
-
-impl RadrootsAppUiIconKey {
- pub const fn as_str(self) -> &'static str {
- match self {
- RadrootsAppUiIconKey::CaretRight => "caret-right",
- RadrootsAppUiIconKey::CaretUpDown => "caret-up-down",
- RadrootsAppUiIconKey::Plus => "plus",
- RadrootsAppUiIconKey::Settings => "settings",
- RadrootsAppUiIconKey::Home => "home",
- RadrootsAppUiIconKey::Beaker => "beaker",
- }
- }
-}
-
-pub fn radroots_app_ui_icon_key_from_name(name: &str) -> Option<RadrootsAppUiIconKey> {
- match name {
- "caret-right" | "chevron-right" => Some(RadrootsAppUiIconKey::CaretRight),
- "caret-up-down" | "chevrons-up-down" => Some(RadrootsAppUiIconKey::CaretUpDown),
- "plus" => Some(RadrootsAppUiIconKey::Plus),
- "settings" | "gear" => Some(RadrootsAppUiIconKey::Settings),
- "home" => Some(RadrootsAppUiIconKey::Home),
- "beaker" | "test" => Some(RadrootsAppUiIconKey::Beaker),
- _ => None,
- }
-}
-
-pub fn radroots_app_ui_icon_data(key: RadrootsAppUiIconKey) -> Icon {
- match key {
- RadrootsAppUiIconKey::CaretRight => LuChevronRight,
- RadrootsAppUiIconKey::CaretUpDown => LuChevronsUpDown,
- RadrootsAppUiIconKey::Plus => LuPlus,
- RadrootsAppUiIconKey::Settings => LuSettings,
- RadrootsAppUiIconKey::Home => LuHouse,
- RadrootsAppUiIconKey::Beaker => LuBeaker,
- }
-}
-
-#[component]
-pub fn RadrootsAppUiIcon(
- key: RadrootsAppUiIconKey,
- #[prop(optional)] class: Option<String>,
- #[prop(optional)] size: Option<u32>,
-) -> impl IntoView {
- let icon = radroots_app_ui_icon_data(key);
- let class_value = class.unwrap_or_default();
- let size_value = size.unwrap_or(20).to_string();
- let view_box = icon.view_box.unwrap_or("0 0 24 24");
- let stroke = icon.stroke.unwrap_or("currentColor");
- let fill = icon.fill.unwrap_or("none");
- let stroke_width = icon.stroke_width.unwrap_or("2");
- let stroke_linecap = icon.stroke_linecap.unwrap_or("round");
- let stroke_linejoin = icon.stroke_linejoin.unwrap_or("round");
- view! {
- <svg
- class=class_value
- width=size_value.clone()
- height=size_value
- viewBox=view_box
- fill=fill
- stroke=stroke
- stroke-width=stroke_width
- stroke-linecap=stroke_linecap
- stroke-linejoin=stroke_linejoin
- xmlns="http://www.w3.org/2000/svg"
- focusable="false"
- aria-hidden="true"
- attr:style=icon.style
- attr:x=icon.x
- attr:y=icon.y
- inner_html=icon.data
- />
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- radroots_app_ui_icon_key_from_name,
- radroots_app_ui_icon_data,
- RadrootsAppUiIconKey,
- };
-
- #[test]
- fn icon_key_parses_names() {
- assert_eq!(
- radroots_app_ui_icon_key_from_name("caret-right"),
- Some(RadrootsAppUiIconKey::CaretRight)
- );
- assert_eq!(
- radroots_app_ui_icon_key_from_name("chevron-right"),
- Some(RadrootsAppUiIconKey::CaretRight)
- );
- assert_eq!(
- radroots_app_ui_icon_key_from_name("caret-up-down"),
- Some(RadrootsAppUiIconKey::CaretUpDown)
- );
- assert_eq!(
- radroots_app_ui_icon_key_from_name("chevrons-up-down"),
- Some(RadrootsAppUiIconKey::CaretUpDown)
- );
- assert_eq!(
- radroots_app_ui_icon_key_from_name("plus"),
- Some(RadrootsAppUiIconKey::Plus)
- );
- assert_eq!(
- radroots_app_ui_icon_key_from_name("settings"),
- Some(RadrootsAppUiIconKey::Settings)
- );
- assert_eq!(
- radroots_app_ui_icon_key_from_name("gear"),
- Some(RadrootsAppUiIconKey::Settings)
- );
- assert_eq!(
- radroots_app_ui_icon_key_from_name("home"),
- Some(RadrootsAppUiIconKey::Home)
- );
- assert_eq!(
- radroots_app_ui_icon_key_from_name("beaker"),
- Some(RadrootsAppUiIconKey::Beaker)
- );
- assert_eq!(
- radroots_app_ui_icon_key_from_name("test"),
- Some(RadrootsAppUiIconKey::Beaker)
- );
- assert_eq!(radroots_app_ui_icon_key_from_name("unknown"), None);
- }
-
- #[test]
- fn icon_data_resolves() {
- let icon = radroots_app_ui_icon_data(RadrootsAppUiIconKey::Settings);
- assert!(!icon.data.is_empty());
- }
-}
diff --git a/crates/ui-components/src/label.rs b/crates/ui-components/src/label.rs
@@ -1,22 +0,0 @@
-use leptos::prelude::*;
-
-#[component]
-pub fn RadrootsAppUiLabel(
- #[prop(optional)] for_id: Option<String>,
- #[prop(optional)] class: Option<String>,
- #[prop(optional)] id: Option<String>,
- #[prop(optional)] style: Option<String>,
- children: Children,
-) -> impl IntoView {
- view! {
- <label
- id=id
- class=class
- style=style
- for=for_id
- data-ui="label"
- >
- {children()}
- </label>
- }
-}
diff --git a/crates/ui-components/src/lib.rs b/crates/ui-components/src/lib.rs
@@ -1,132 +0,0 @@
-#![forbid(unsafe_code)]
-
-mod button;
-mod button_layout;
-mod icon;
-mod label;
-mod list;
-mod list_types;
-mod separator;
-mod spinner;
-mod dialog;
-mod form;
-mod sheet;
-mod scroll;
-mod nav_header;
-mod nav_tabs;
-
-pub use button::RadrootsAppUiButton;
-pub use button_layout::{
- RadrootsAppUiButtonLayout,
- RadrootsAppUiButtonLayoutAction,
- RadrootsAppUiButtonLayoutBackAction,
- RadrootsAppUiButtonLayoutBottom,
- RadrootsAppUiButtonLayoutPair,
-};
-pub use form::{RadrootsAppUiChip, RadrootsAppUiChips, RadrootsAppUiFormField};
-pub use icon::{
- radroots_app_ui_icon_data,
- radroots_app_ui_icon_key_from_name,
- RadrootsAppUiIcon,
- RadrootsAppUiIconKey,
-};
-pub use list::{
- radroots_app_ui_list_group_data_ui_value,
- radroots_app_ui_list_border_classes,
- radroots_app_ui_list_row_data_ui_value,
- radroots_app_ui_list_row_leading_data_ui_value,
- radroots_app_ui_list_row_trailing_data_ui_value,
- radroots_app_ui_list_section_data_ui_value,
- RadrootsAppUiListDefaultLabels,
- RadrootsAppUiListGroup,
- RadrootsAppUiListLine,
- RadrootsAppUiListInputRow,
- RadrootsAppUiListOffsetView,
- RadrootsAppUiListSelectRow,
- RadrootsAppUiListRowDisplayValue,
- RadrootsAppUiListRow,
- RadrootsAppUiListRowLabel,
- RadrootsAppUiListRowLeading,
- RadrootsAppUiListRowTrailing,
- RadrootsAppUiListSection,
- RadrootsAppUiListTitleView,
- RadrootsAppUiListTouchEndView,
- RadrootsAppUiListTouchRow,
- RadrootsAppUiListView,
-};
-pub use list_types::{
- radroots_app_ui_list_icon_key,
- radroots_app_ui_list_styles_resolve,
- RadrootsAppUiList,
- RadrootsAppUiListDefault,
- RadrootsAppUiListDefaultLabel,
- RadrootsAppUiListDisplay,
- RadrootsAppUiListDisplayValue,
- RadrootsAppUiListIcon,
- RadrootsAppUiListInput,
- RadrootsAppUiListInputAction,
- RadrootsAppUiListInputField,
- RadrootsAppUiListInputLineLabel,
- RadrootsAppUiListItem,
- RadrootsAppUiListItemKind,
- RadrootsAppUiListLabel,
- RadrootsAppUiListLabelText,
- RadrootsAppUiListLabelValue,
- RadrootsAppUiListLabelValueKind,
- RadrootsAppUiListOffset,
- RadrootsAppUiListOffsetMod,
- RadrootsAppUiListSelect,
- RadrootsAppUiListSelectField,
- RadrootsAppUiListSelectOption,
- RadrootsAppUiListStyles,
- RadrootsAppUiListStylesResolved,
- RadrootsAppUiListTitle,
- RadrootsAppUiListTitleLink,
- RadrootsAppUiListTitleValue,
- RadrootsAppUiListTouch,
- RadrootsAppUiListTouchEnd,
- RadrootsAppUiListToggle,
-};
-pub use label::RadrootsAppUiLabel;
-pub use separator::{
- radroots_app_ui_separator_orientation_value,
- RadrootsAppUiSeparator,
- RadrootsAppUiSeparatorOrientation,
-};
-pub use spinner::RadrootsAppUiSpinner;
-pub use dialog::{
- radroots_app_ui_dialog_state_value,
- RadrootsAppUiDialogClose,
- RadrootsAppUiDialogContent,
- RadrootsAppUiDialogDescription,
- RadrootsAppUiDialogOverlay,
- RadrootsAppUiDialogPortal,
- RadrootsAppUiDialogRoot,
- RadrootsAppUiDialogTitle,
- RadrootsAppUiDialogTrigger,
-};
-pub use sheet::{
- radroots_app_ui_sheet_data_ui_value,
- radroots_app_ui_sheet_handle_data_ui_value,
- radroots_app_ui_sheet_overlay_data_ui_value,
- RadrootsAppUiSheetClose,
- RadrootsAppUiSheetContent,
- RadrootsAppUiSheetDescription,
- RadrootsAppUiSheetOverlay,
- RadrootsAppUiSheetPortal,
- RadrootsAppUiSheetRoot,
- RadrootsAppUiSheetTitle,
- RadrootsAppUiSheetTrigger,
-};
-pub use scroll::{
- radroots_app_ui_collapse_progress,
- radroots_app_ui_scroll_velocity,
- RadrootsAppUiScrollContainer,
- RadrootsAppUiScrollContext,
-};
-pub use nav_header::{
- RadrootsAppUiNavHeader,
- RadrootsAppUiNavHeaderBgMode,
- RadrootsAppUiNavHeaderCollapseMode,
-};
-pub use nav_tabs::RadrootsAppUiNavTabs;
diff --git a/crates/ui-components/src/list.rs b/crates/ui-components/src/list.rs
@@ -1,1468 +0,0 @@
-#![forbid(unsafe_code)]
-
-use std::sync::Arc;
-
-use leptos::ev::MouseEvent;
-use leptos::prelude::*;
-
-use crate::{
- radroots_app_ui_list_icon_key,
- radroots_app_ui_list_styles_resolve,
- RadrootsAppUiIcon,
- RadrootsAppUiIconKey,
- RadrootsAppUiList,
- RadrootsAppUiListDisplay,
- RadrootsAppUiListDisplayValue,
- RadrootsAppUiListDefault,
- RadrootsAppUiListDefaultLabel,
- RadrootsAppUiListInput,
- RadrootsAppUiListInputAction,
- RadrootsAppUiListItem,
- RadrootsAppUiListItemKind,
- RadrootsAppUiListLabel,
- RadrootsAppUiListLabelValue,
- RadrootsAppUiListLabelValueKind,
- RadrootsAppUiListOffset,
- RadrootsAppUiListOffsetMod,
- RadrootsAppUiListSelect,
- RadrootsAppUiListStylesResolved,
- RadrootsAppUiListTitle,
- RadrootsAppUiListTitleValue,
- RadrootsAppUiListTouch,
- RadrootsAppUiListTouchEnd,
- RadrootsAppUiListToggle,
- RadrootsAppUiSpinner,
-};
-
-pub fn radroots_app_ui_list_group_data_ui_value() -> &'static str {
- "list-group"
-}
-
-pub fn radroots_app_ui_list_section_data_ui_value() -> &'static str {
- "list-section"
-}
-
-pub fn radroots_app_ui_list_row_data_ui_value() -> &'static str {
- "list-row"
-}
-
-pub fn radroots_app_ui_list_row_leading_data_ui_value() -> &'static str {
- "list-row-leading"
-}
-
-pub fn radroots_app_ui_list_row_trailing_data_ui_value() -> &'static str {
- "list-row-trailing"
-}
-
-fn radroots_app_ui_list_base_id(id: Option<&str>, view: Option<&str>) -> String {
- let suffix = id.or(view).unwrap_or("default");
- format!("app-list-{suffix}")
-}
-
-fn radroots_app_ui_list_title_id(base_id: &str) -> String {
- format!("{base_id}-title")
-}
-
-fn radroots_app_ui_list_items_id(base_id: &str) -> String {
- format!("{base_id}-items")
-}
-
-fn radroots_app_ui_list_item_id(base_id: &str, index: usize) -> String {
- format!("{base_id}-item-{index}")
-}
-
-fn radroots_app_ui_list_line_id(base_id: &str, index: usize) -> String {
- format!("{base_id}-line-{index}")
-}
-
-#[component]
-pub fn RadrootsAppUiListGroup(
- class: Option<String>,
- id: Option<String>,
- style: Option<String>,
- children: ChildrenFn,
-) -> impl IntoView {
- view! {
- <section
- id=id
- class=class
- style=style
- data-ui=radroots_app_ui_list_group_data_ui_value()
- >
- {children()}
- </section>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiListSection(
- class: Option<String>,
- id: Option<String>,
- style: Option<String>,
- children: ChildrenFn,
-) -> impl IntoView {
- view! {
- <section
- id=id
- class=class
- style=style
- data-ui=radroots_app_ui_list_section_data_ui_value()
- >
- {children()}
- </section>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiListRow(
- class: Option<String>,
- id: Option<String>,
- style: Option<String>,
- children: ChildrenFn,
-) -> impl IntoView {
- view! {
- <li
- id=id
- class=class
- style=style
- data-ui=radroots_app_ui_list_row_data_ui_value()
- >
- {children()}
- </li>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiListRowLeading(
- class: Option<String>,
- id: Option<String>,
- style: Option<String>,
- children: Children,
-) -> impl IntoView {
- view! {
- <div
- id=id
- class=class
- style=style
- data-ui=radroots_app_ui_list_row_leading_data_ui_value()
- >
- {children()}
- </div>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiListRowTrailing(
- class: Option<String>,
- id: Option<String>,
- style: Option<String>,
- children: Children,
-) -> impl IntoView {
- view! {
- <div
- id=id
- class=class
- style=style
- data-ui=radroots_app_ui_list_row_trailing_data_ui_value()
- >
- {children()}
- </div>
- }
-}
-
-fn radroots_app_ui_list_class_merge(parts: &[Option<&str>]) -> String {
- let mut result = String::new();
- for part in parts {
- if let Some(value) = part {
- if value.is_empty() {
- continue;
- }
- if !result.is_empty() {
- result.push(' ');
- }
- result.push_str(value);
- }
- }
- result
-}
-
-fn radroots_app_ui_list_active_class(hide_active: bool) -> Option<&'static str> {
- if hide_active { None } else { Some("opacity-active") }
-}
-
-pub fn radroots_app_ui_list_border_classes(
- hide_border_top: bool,
- hide_border_bottom: bool,
-) -> String {
- let top = if hide_border_top {
- "group-first:border-t-0"
- } else {
- "group-first:border-t-line"
- };
- let bottom = if hide_border_bottom {
- "group-last:border-b-0"
- } else {
- "group-last:border-b-line"
- };
- format!("{top} {bottom}")
-}
-
-#[component]
-pub fn RadrootsAppUiListLine(
- #[prop(optional)] id: String,
- as_button: bool,
- #[prop(optional)] loading: bool,
- #[prop(optional)] hide_border_top: bool,
- #[prop(optional)] hide_border_bottom: bool,
- on_click: Option<Callback<MouseEvent>>,
- end: Option<ChildrenFn>,
- #[prop(optional)] overlay: Option<ChildrenFn>,
- children: ChildrenFn,
-) -> impl IntoView {
- let border_class = radroots_app_ui_list_border_classes(hide_border_top, hide_border_bottom);
- let line_class = radroots_app_ui_list_class_merge(&[
- Some("flex flex-row h-full w-full justify-center items-center border-t-line el-re"),
- Some(border_class.as_str()),
- ]);
- let line_state = if loading { "loading" } else { "ready" };
- let end_view = end.map(|slot| slot());
- let overlay_view = overlay.map(|slot| slot());
- let id = if id.is_empty() { None } else { Some(id) };
- let line_inner = view! {
- <div class=line_class data-ui="list-line">
- {if loading {
- view! {
- <div class="flex flex-row h-full w-full justify-center items-center">
- <RadrootsAppUiSpinner />
- </div>
- }
- .into_any()
- } else {
- view! {
- <div class="relative group flex flex-row h-line w-full pr-[2px] justify-between items-center el-re">
- <div class="flex flex-row h-full w-trellis_display justify-between items-center">
- {children()}
- </div>
- {end_view}
- {overlay_view}
- </div>
- }
- .into_any()
- }}
- </div>
- };
- let has_click = on_click.is_some();
- let click_handler = move |ev: MouseEvent| {
- if let Some(callback) = &on_click {
- callback.run(ev);
- }
- };
- if as_button {
- view! {
- <button
- type="button"
- id=id
- class="flex flex-row flex-grow overflow-hidden"
- aria-busy=loading
- data-state=line_state
- on:click=click_handler
- >
- {line_inner}
- </button>
- }
- .into_any()
- } else {
- let role = if has_click { Some("button") } else { None };
- let tabindex = if has_click { Some(0) } else { None };
- view! {
- <div
- id=id
- class="flex flex-row flex-grow overflow-hidden"
- aria-busy=loading
- data-state=line_state
- role=role
- tabindex=tabindex
- on:click=click_handler
- >
- {line_inner}
- </div>
- }
- .into_any()
- }
-}
-
-fn radroots_app_ui_list_title_padding_class(mod_value: Option<&RadrootsAppUiListOffsetMod>) -> Option<&'static str> {
- match mod_value {
- Some(RadrootsAppUiListOffsetMod::Small) => Some("pl-[16px]"),
- Some(RadrootsAppUiListOffsetMod::Glyph)
- | Some(RadrootsAppUiListOffsetMod::Icon { .. })
- | Some(RadrootsAppUiListOffsetMod::IconCircle { .. }) => Some("pl-[36px]"),
- None => None,
- }
-}
-
-fn radroots_app_ui_list_default_labels(
- labels: Option<&[RadrootsAppUiListDefaultLabel]>,
-) -> Vec<RadrootsAppUiListDefaultLabel> {
- labels.map_or_else(
- || {
- vec![RadrootsAppUiListDefaultLabel {
- label: "No items to display.".to_string(),
- classes: None,
- on_click: None,
- }]
- },
- |labels| labels.to_vec(),
- )
-}
-
-fn radroots_app_ui_list_offset_mod(
- mod_value: Option<&RadrootsAppUiListOffsetMod>,
-) -> RadrootsAppUiListOffsetMod {
- mod_value.cloned().unwrap_or(RadrootsAppUiListOffsetMod::Small)
-}
-
-fn radroots_app_ui_list_input_action_icon_key(
- action: &RadrootsAppUiListInputAction,
-) -> RadrootsAppUiIconKey {
- action
- .icon
- .as_ref()
- .and_then(radroots_app_ui_list_icon_key)
- .unwrap_or(RadrootsAppUiIconKey::Plus)
-}
-
-fn radroots_app_ui_list_display_loading(display: Option<&RadrootsAppUiListDisplay>) -> bool {
- display.map(|value| value.loading).unwrap_or(false)
-}
-
-fn radroots_app_ui_list_title_visible(
- title: Option<&RadrootsAppUiListTitle>,
- default_state: Option<&RadrootsAppUiListDefault>,
-) -> bool {
- match title {
- None => false,
- Some(_) => default_state.map(|value| value.show_title).unwrap_or(true),
- }
-}
-
-fn radroots_app_ui_list_row_class(
- item: &RadrootsAppUiListItem,
- styles: &RadrootsAppUiListStylesResolved,
-) -> String {
- let active_class = radroots_app_ui_list_active_class(item.hide_active);
- radroots_app_ui_list_class_merge(&[
- Some("group flex flex-row h-full w-full justify-end items-center el-re"),
- Some("list-row-surface"),
- if item.hide_field { Some("hidden") } else { None },
- if item.full_rounded { Some("rounded-touch") } else { None },
- if styles.hide_rounded {
- None
- } else {
- Some("first:rounded-t-2xl last:rounded-b-2xl")
- },
- active_class,
- ])
-}
-
-fn radroots_app_ui_list_label_value_view(
- value: RadrootsAppUiListLabelValue,
- is_right: bool,
- hide_active: bool,
-) -> AnyView {
- let RadrootsAppUiListLabelValue {
- classes_wrap,
- hide_truncate,
- value,
- } = value;
- let active_class = radroots_app_ui_list_active_class(hide_active);
- let wrap_class = radroots_app_ui_list_class_merge(&[
- Some("flex flex-row h-full items-center"),
- if hide_truncate { None } else { Some("truncate") },
- classes_wrap.as_deref(),
- ]);
- let view = match value {
- RadrootsAppUiListLabelValueKind::Text(value) => {
- let text_class = radroots_app_ui_list_class_merge(&[
- Some("text-line_d"),
- if is_right { Some("ui-text-secondary") } else { None },
- active_class,
- if hide_truncate { None } else { Some("truncate") },
- value.classes.as_deref(),
- ]);
- view! { <p class=text_class>{value.value}</p> }.into_any()
- }
- RadrootsAppUiListLabelValueKind::Icon(icon) => {
- let icon_key = radroots_app_ui_list_icon_key(&icon);
- let icon_class = radroots_app_ui_list_class_merge(&[
- if is_right { Some("ui-text-secondary") } else { None },
- active_class,
- icon.class.as_deref(),
- ]);
- if let Some(icon_key) = icon_key {
- view! { <RadrootsAppUiIcon key=icon_key class=icon_class size=16 /> }.into_any()
- } else {
- view! { <div></div> }.into_any()
- }
- }
- };
- view! { <div class=wrap_class>{view}</div> }.into_any()
-}
-
-#[component]
-pub fn RadrootsAppUiListRowLabel(
- basis: RadrootsAppUiListLabel,
- #[prop(optional)] hide_active: bool,
-) -> impl IntoView {
- let left_values = basis.left;
- let right_values = basis.right;
- let left_view = left_values
- .into_iter()
- .map(|value| radroots_app_ui_list_label_value_view(value, false, hide_active))
- .collect_view();
- let right_view = right_values
- .into_iter()
- .rev()
- .map(|value| radroots_app_ui_list_label_value_view(value, true, hide_active))
- .collect_view();
- view! {
- <div class="flex flex-row h-full w-full items-center justify-between">
- <div class="flex flex-row h-full items-center truncate">
- {left_view}
- </div>
- <div class="flex flex-row h-full items-center justify-end pr-4">
- {right_view}
- </div>
- </div>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiListRowDisplayValue(
- basis: RadrootsAppUiListDisplay,
- #[prop(optional)] hide_active: bool,
-) -> impl IntoView {
- let on_click = basis.on_click;
- let display = match basis.value {
- RadrootsAppUiListDisplayValue::Icon(icon) => {
- let icon_key = radroots_app_ui_list_icon_key(&icon);
- let active_class = radroots_app_ui_list_active_class(hide_active);
- let icon_class = radroots_app_ui_list_class_merge(&[
- Some("ui-text-secondary"),
- active_class,
- icon.class.as_deref(),
- ]);
- if let Some(icon_key) = icon_key {
- view! { <RadrootsAppUiIcon key=icon_key class=icon_class size=18 /> }.into_any()
- } else {
- view! { <div></div> }.into_any()
- }
- }
- RadrootsAppUiListDisplayValue::Label(label) => {
- let active_class = radroots_app_ui_list_active_class(hide_active);
- let text_class = radroots_app_ui_list_class_merge(&[
- Some("font-sans text-line_d_e line-clamp-1 text-ly0-gl-label el-re"),
- active_class,
- label.classes.as_deref(),
- ]);
- view! { <p class=text_class>{label.value}</p> }.into_any()
- }
- };
- view! {
- <button
- type="button"
- class="z-10 flex flex-grow justify-end"
- on:click=move |ev: MouseEvent| {
- ev.stop_propagation();
- if let Some(callback) = &on_click {
- callback.run(ev);
- }
- }
- >
- {display}
- </button>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiListOffsetView(
- basis: Option<RadrootsAppUiListOffset>,
- #[prop(optional)] class: Option<String>,
-) -> impl IntoView {
- let basis = basis.unwrap_or(RadrootsAppUiListOffset {
- mod_value: None,
- classes: None,
- hide_space: false,
- hide_offset: false,
- on_click: None,
- });
- if basis.hide_offset {
- return view! { <div></div> }.into_any();
- }
- let mod_value = radroots_app_ui_list_offset_mod(basis.mod_value.as_ref());
- let wrap_class = radroots_app_ui_list_class_merge(&[
- Some("flex flex-row h-full"),
- class.as_deref(),
- basis.classes.as_deref(),
- ]);
- let on_click = basis.on_click;
- match mod_value {
- RadrootsAppUiListOffsetMod::Small => view! {
- <div class=wrap_class>
- <div class="flex flex-row h-full w-[22px]">
- <div class="flex-fluid"></div>
- </div>
- </div>
- }
- .into_any(),
- RadrootsAppUiListOffsetMod::Glyph => view! {
- <div class=wrap_class>
- <div class="flex flex-row pr-[2px]">
- <div class="flex flex-row h-full w-trellisOffset">
- <div class="flex-fluid"></div>
- </div>
- </div>
- </div>
- }
- .into_any(),
- RadrootsAppUiListOffsetMod::Icon { icon, loading } => {
- let icon_key = radroots_app_ui_list_icon_key(&icon);
- let icon_class = radroots_app_ui_list_class_merge(&[
- Some("ui-text-secondary"),
- icon.class.as_deref(),
- ]);
- let button_class = radroots_app_ui_list_class_merge(&[
- Some("fade-in pl-2 translate-x-[3px] translate-y-[1px]"),
- ]);
- let icon_view = if loading {
- view! { <RadrootsAppUiSpinner class="text-[12px]".to_string() /> }.into_any()
- } else if let Some(icon_key) = icon_key {
- view! { <RadrootsAppUiIcon key=icon_key class=icon_class size=16 /> }.into_any()
- } else {
- view! { <div></div> }.into_any()
- };
- view! {
- <div class=wrap_class>
- <div class="flex flex-row h-full min-w-[20px] w-trellisOffset justify-center items-center pr-3">
- <button
- type="button"
- class=button_class
- on:click=move |ev: MouseEvent| {
- if loading {
- return;
- }
- if let Some(callback) = &on_click {
- callback.run(ev);
- }
- }
- >
- {icon_view}
- </button>
- </div>
- </div>
- }
- .into_any()
- }
- RadrootsAppUiListOffsetMod::IconCircle { icon, loading } => {
- let icon_key = radroots_app_ui_list_icon_key(&icon);
- let icon_class = radroots_app_ui_list_class_merge(&[
- Some("ui-text-secondary"),
- icon.class.as_deref(),
- ]);
- let button_class = radroots_app_ui_list_class_merge(&[
- Some("fade-in pl-2 translate-x-[3px] translate-y-[1px] rounded-full"),
- ]);
- let icon_view = if loading {
- view! { <RadrootsAppUiSpinner class="text-[12px]".to_string() /> }.into_any()
- } else if let Some(icon_key) = icon_key {
- view! { <RadrootsAppUiIcon key=icon_key class=icon_class size=16 /> }.into_any()
- } else {
- view! { <div></div> }.into_any()
- };
- view! {
- <div class=wrap_class>
- <div class="flex flex-row h-full min-w-[20px] w-trellisOffset justify-center items-center pr-3">
- <button
- type="button"
- class=button_class
- on:click=move |ev: MouseEvent| {
- if loading {
- return;
- }
- if let Some(callback) = &on_click {
- callback.run(ev);
- }
- }
- >
- {icon_view}
- </button>
- </div>
- </div>
- }
- .into_any()
- }
- }
-}
-
-#[component]
-pub fn RadrootsAppUiListTouchEndView(
- basis: RadrootsAppUiListTouchEnd,
- #[prop(optional)] hide_active: bool,
-) -> impl IntoView {
- let icon_key = radroots_app_ui_list_icon_key(&basis.icon);
- let active_class = radroots_app_ui_list_active_class(hide_active);
- let icon_class = radroots_app_ui_list_class_merge(&[
- Some("ui-text-secondary opacity-70 translate-y-[1px]"),
- active_class,
- basis.icon.class.as_deref(),
- ]);
- let on_click = basis.on_click;
- let icon_view = icon_key.map(|icon_key| {
- view! { <RadrootsAppUiIcon key=icon_key class=icon_class size=14 /> }.into_any()
- });
- view! {
- <div class="absolute top-0 right-0 h-full w-max flex flex-row justify-center items-center">
- <button
- type="button"
- class="flex pr-3"
- on:click=move |ev: MouseEvent| {
- if let Some(callback) = &on_click {
- callback.run(ev);
- }
- }
- >
- {icon_view}
- </button>
- </div>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiListTouchRow(
- basis: RadrootsAppUiListTouch,
- #[prop(optional)] line_id: String,
- #[prop(optional)] hide_active: bool,
- #[prop(optional)] hide_border_top: bool,
- #[prop(optional)] hide_border_bottom: bool,
- #[prop(optional)] loading: bool,
-) -> impl IntoView {
- let label = basis.label;
- let display = basis.display;
- let end = basis.end;
- let on_click = basis.on_click;
- let end_slot = end.map(|end| {
- let hide_active = hide_active;
- Arc::new(move || {
- let end_value = end.clone();
- view! { <RadrootsAppUiListTouchEndView basis=end_value hide_active=hide_active /> }.into_any()
- }) as ChildrenFn
- });
- view! {
- <RadrootsAppUiListLine
- id=line_id
- as_button=true
- loading=loading
- hide_border_top=hide_border_top
- hide_border_bottom=hide_border_bottom
- on_click=on_click
- end=end_slot
- >
- <RadrootsAppUiListRowLabel basis=label.clone() hide_active=hide_active />
- {display.as_ref().map(|display| {
- let display = display.clone();
- view! { <RadrootsAppUiListRowDisplayValue basis=display hide_active=hide_active /> }.into_any()
- })}
- </RadrootsAppUiListLine>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiListToggleRow(
- basis: RadrootsAppUiListToggle,
- #[prop(optional)] line_id: String,
- #[prop(optional)] hide_active: bool,
- #[prop(optional)] hide_border_top: bool,
- #[prop(optional)] hide_border_bottom: bool,
- #[prop(optional)] loading: bool,
-) -> impl IntoView {
- let label = basis.label;
- let checked = basis.checked;
- let disabled = basis.disabled;
- let on_toggle = basis.on_toggle;
- let switch_class = if checked {
- "ios-switch ios-switch--checked"
- } else {
- "ios-switch"
- };
- let end_slot = Arc::new(move || {
- view! {
- <span class="flex flex-row h-full items-center pr-3">
- <span class=switch_class aria-hidden="true">
- <span class="ios-switch__thumb"></span>
- </span>
- </span>
- }
- .into_any()
- }) as ChildrenFn;
- let on_click = if disabled {
- None
- } else {
- let on_toggle = on_toggle.clone();
- Some(Callback::new(move |_ev: MouseEvent| {
- if let Some(callback) = &on_toggle {
- callback.run(!checked);
- }
- }))
- };
- view! {
- <RadrootsAppUiListLine
- id=line_id
- as_button=true
- loading=loading
- hide_border_top=hide_border_top
- hide_border_bottom=hide_border_bottom
- on_click=on_click
- end=Some(end_slot)
- >
- <RadrootsAppUiListRowLabel basis=label.clone() hide_active=hide_active />
- </RadrootsAppUiListLine>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiListInputRow(
- basis: RadrootsAppUiListInput,
- #[prop(optional)] line_id: String,
- #[prop(optional)] hide_border_top: bool,
- #[prop(optional)] hide_border_bottom: bool,
-) -> impl IntoView {
- let RadrootsAppUiListInput {
- field,
- line_label,
- action,
- } = basis;
- let line_id = if line_id.is_empty() { None } else { Some(line_id) };
- let border_class = radroots_app_ui_list_border_classes(hide_border_top, hide_border_bottom);
- let wrap_class = radroots_app_ui_list_class_merge(&[
- Some("flex flex-row h-line w-full justify-start items-center border-t-line overflow-hidden"),
- Some(border_class.as_str()),
- ]);
- let line_label_view = line_label.map(|line_label| {
- let label_class = radroots_app_ui_list_class_merge(&[
- Some("text-form_base ui-text-secondary"),
- line_label.classes.as_deref(),
- ]);
- view! {
- <div class="flex flex-row h-full justify-start items-center overflow-x-hidden">
- <p class=label_class>{line_label.value}</p>
- </div>
- }
- .into_any()
- });
- let input_class = radroots_app_ui_list_class_merge(&[
- Some("el-input"),
- field.classes.as_deref(),
- ]);
- let input_id = field.id;
- let input_value = field.value;
- let input_placeholder = field.placeholder;
- let input_disabled = field.disabled;
- let on_input = field.on_input;
- let action_view = action.and_then(|action| {
- if !action.visible {
- return None;
- }
- let action_loading = action.loading;
- let action_icon_key = radroots_app_ui_list_input_action_icon_key(&action);
- let action_icon_class = radroots_app_ui_list_class_merge(&[
- Some("ui-text-secondary"),
- action.icon.as_ref().and_then(|icon| icon.class.as_deref()),
- ]);
- let on_click = action.on_click;
- Some(
- view! {
- <div class="absolute top-0 right-0 flex flex-row h-full w-12 pr-4 justify-end items-center fade-in">
- {if action_loading {
- view! { <RadrootsAppUiSpinner class="text-[12px]".to_string() /> }
- .into_any()
- } else {
- view! {
- <button
- type="button"
- class="group fade-in-long"
- on:click=move |ev: MouseEvent| {
- if let Some(callback) = &on_click {
- callback.run(ev);
- }
- }
- >
- <RadrootsAppUiIcon key=action_icon_key class=action_icon_class size=18 />
- </button>
- }
- .into_any()
- }}
- </div>
- }
- .into_any(),
- )
- });
- view! {
- <div
- id=line_id
- class="flex flex-row flex-grow h-full w-full"
- data-ui="list-input"
- >
- <div class=wrap_class>
- {line_label_view}
- <div class="relative flex flex-row flex-grow h-full pr-12 justify-start items-center">
- <input
- id=input_id
- class=input_class
- disabled=input_disabled
- placeholder=input_placeholder
- prop:value=input_value
- on:input=move |ev| {
- if let Some(callback) = &on_input {
- callback.run(event_target_value(&ev));
- }
- }
- />
- {action_view}
- </div>
- </div>
- </div>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiListSelectRow(
- basis: RadrootsAppUiListSelect,
- #[prop(optional)] line_id: String,
- #[prop(optional)] hide_active: bool,
- #[prop(optional)] hide_border_top: bool,
- #[prop(optional)] hide_border_bottom: bool,
-) -> impl IntoView {
- let RadrootsAppUiListSelect {
- field,
- label,
- display,
- end,
- loading,
- on_click,
- } = basis;
- let end_slot = end.map(|end| {
- let hide_active = hide_active;
- Arc::new(move || {
- let end_value = end.clone();
- view! { <RadrootsAppUiListTouchEndView basis=end_value hide_active=hide_active /> }.into_any()
- }) as ChildrenFn
- });
- let display_loading = radroots_app_ui_list_display_loading(display.as_ref());
- let select_class = radroots_app_ui_list_class_merge(&[
- Some("el-select"),
- Some("list-select-hit"),
- field.classes.as_deref(),
- ]);
- let select_id = field.id;
- let select_value = field.value.clone();
- let select_disabled = field.disabled || loading;
- let on_change = field.on_change;
- let options = Arc::new(field.options);
- let selected_value = RwSignal::new(select_value.clone());
- let selected_label = RwSignal::new(
- options
- .iter()
- .find(|option| option.value == select_value)
- .map(|option| option.label.clone())
- .unwrap_or_default(),
- );
- let selected_class = radroots_app_ui_list_class_merge(&[
- Some("font-sans text-line_d_e line-clamp-1 text-ly0-gl-label el-re"),
- radroots_app_ui_list_active_class(hide_active),
- ]);
- let select_overlay = {
- let select_class = select_class.clone();
- let select_id = select_id.clone();
- let on_change = on_change.clone();
- let on_click = on_click.clone();
- let options = Arc::clone(&options);
- let selected_label = selected_label;
- Arc::new(move || {
- let options_for_change = Arc::clone(&options);
- let options_for_view = Arc::clone(&options);
- view! {
- <select
- id=select_id.clone()
- class=select_class.clone()
- disabled=select_disabled
- prop:value=move || selected_value.get()
- on:click=move |ev| {
- if let Some(callback) = &on_click {
- callback.run(ev);
- }
- }
- on:change=move |ev| {
- let next_value = event_target_value(&ev);
- selected_value.set(next_value.clone());
- let next_label = options_for_change
- .iter()
- .find(|option| option.value == next_value)
- .map(|option| option.label.clone())
- .unwrap_or_default();
- selected_label.set(next_label);
- if let Some(callback) = &on_change {
- callback.run(next_value);
- }
- #[cfg(target_arch = "wasm32")]
- {
- use leptos::wasm_bindgen::JsCast;
- use leptos::web_sys;
-
- if let Some(target) = ev.target() {
- if let Ok(select) = target.dyn_into::<web_sys::HtmlSelectElement>() {
- let _ = select.blur();
- }
- }
- }
- }
- >
- {options_for_view
- .iter()
- .cloned()
- .map(|option| {
- let class = radroots_app_ui_list_class_merge(&[
- option.classes.as_deref(),
- ]);
- view! { <option value=option.value class=class>{option.label}</option> }
- })
- .collect_view()}
- </select>
- }
- .into_any()
- }) as ChildrenFn
- };
- view! {
- <RadrootsAppUiListLine
- id=line_id
- as_button=false
- loading=loading
- hide_border_top=hide_border_top
- hide_border_bottom=hide_border_bottom
- on_click=None
- end=end_slot
- overlay=select_overlay
- >
- <RadrootsAppUiListRowLabel basis=label.clone() hide_active=hide_active />
- <div class="relative flex flex-row pr-3 justify-center items-end" data-ui="list-select">
- {if display_loading {
- view! { <RadrootsAppUiSpinner class="text-[12px]".to_string() /> }.into_any()
- } else if let Some(display) = display.as_ref() {
- let display = display.clone();
- view! { <RadrootsAppUiListRowDisplayValue basis=display hide_active=hide_active /> }.into_any()
- } else {
- view! {
- <p class=selected_class.clone()>
- {move || selected_label.get()}
- </p>
- }
- .into_any()
- }}
- </div>
- </RadrootsAppUiListLine>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiListTitleView(
- basis: RadrootsAppUiListTitle,
- id: Option<String>,
-) -> impl IntoView {
- let title_class = radroots_app_ui_list_class_merge(&[
- Some("flex flex-row h-[24px] w-full pl-[2px] gap-1 items-center"),
- basis.classes.as_deref(),
- ]);
- let padding_class = radroots_app_ui_list_title_padding_class(basis.mod_value.as_ref());
- let button_class = radroots_app_ui_list_class_merge(&[
- Some("flex flex-row h-full w-max items-center gap-1"),
- padding_class,
- ]);
- let on_click = basis.on_click;
- let has_click = on_click.is_some();
- let title_value = match basis.value {
- RadrootsAppUiListTitleValue::Spacer => {
- view! { <div class="flex-fluid"></div> }.into_any()
- }
- RadrootsAppUiListTitleValue::Text(value) => {
- view! { <p class="text-trellis_ti uppercase ui-text-tertiary">{value}</p> }.into_any()
- }
- };
- let link_view = basis.link.map(|link| {
- let label_view = link.label.map(|label| match label.value {
- RadrootsAppUiListLabelValueKind::Text(text) => {
- let class = radroots_app_ui_list_class_merge(&[
- Some("text-trellis_ti uppercase fade-in"),
- text.classes.as_deref(),
- ]);
- view! { <p class=class>{text.value}</p> }.into_any()
- }
- RadrootsAppUiListLabelValueKind::Icon(icon) => {
- let icon_key = radroots_app_ui_list_icon_key(&icon);
- let icon_class = radroots_app_ui_list_class_merge(&[
- Some("fade-in"),
- icon.class.as_deref(),
- ]);
- if let Some(icon_key) = icon_key {
- view! { <RadrootsAppUiIcon key=icon_key class=icon_class size=16 /> }
- .into_any()
- } else {
- view! { <div></div> }.into_any()
- }
- }
- });
- let icon_view = link.icon.and_then(|icon| {
- radroots_app_ui_list_icon_key(&icon).map(|icon_key| {
- let icon_class = radroots_app_ui_list_class_merge(&[
- Some("fade-in"),
- icon.class.as_deref(),
- ]);
- view! { <RadrootsAppUiIcon key=icon_key class=icon_class size=16 /> }.into_any()
- })
- });
- let link_class = radroots_app_ui_list_class_merge(&[
- Some("group flex flex-row h-full w-max items-center"),
- link.classes.as_deref(),
- ]);
- let on_click = link.on_click;
- view! {
- <button
- type="button"
- class=link_class
- on:click=move |_| {
- if let Some(callback) = &on_click {
- callback.run(());
- }
- }
- >
- {label_view}
- {icon_view}
- </button>
- }
- .into_any()
- });
- let title_button = if has_click {
- view! {
- <button
- type="button"
- id=id.clone()
- class=button_class
- on:click=move |_| {
- if let Some(callback) = &on_click {
- callback.run(());
- }
- }
- >
- {title_value}
- </button>
- }
- .into_any()
- } else {
- view! {
- <div id=id.clone() class=button_class>
- {title_value}
- </div>
- }
- .into_any()
- };
- view! {
- <div class=title_class>
- {title_button}
- {link_view}
- </div>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiListDefaultLabels(
- labels: Option<Vec<RadrootsAppUiListDefaultLabel>>,
- #[prop(optional)] class: Option<String>,
-) -> impl IntoView {
- let labels = radroots_app_ui_list_default_labels(labels.as_deref());
- let wrap_class = radroots_app_ui_list_class_merge(&[
- Some("flex flex-row"),
- class.as_deref(),
- ]);
- let items = labels
- .into_iter()
- .map(|label| {
- let inner_class = radroots_app_ui_list_class_merge(&[
- Some("text-trellis_ti"),
- label.classes.as_deref(),
- ]);
- let on_click = label.on_click;
- if on_click.is_some() {
- view! {
- <button
- type="button"
- class=inner_class
- on:click=move |_| {
- if let Some(callback) = &on_click {
- callback.run(());
- }
- }
- >
- {label.label}
- </button>
- }
- .into_any()
- } else {
- view! { <span class=inner_class>{label.label}</span> }.into_any()
- }
- })
- .collect_view();
- view! {
- <div class=wrap_class>
- <p class="text-trellis_ti ui-text-tertiary">{items}</p>
- </div>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiListView(basis: RadrootsAppUiList) -> impl IntoView {
- let RadrootsAppUiList {
- id,
- view,
- classes,
- title,
- default_state,
- list,
- hide_offset,
- styles,
- } = basis;
- let base_id = radroots_app_ui_list_base_id(id.as_deref(), view.as_deref());
- let title_id = radroots_app_ui_list_title_id(base_id.as_str());
- let items_id = radroots_app_ui_list_items_id(base_id.as_str());
- let resolved_styles = radroots_app_ui_list_styles_resolve(styles.as_ref());
- let wrap_class = radroots_app_ui_list_class_merge(&[
- Some("flex flex-col"),
- classes.as_deref(),
- ]);
- let group_class = radroots_app_ui_list_class_merge(&[
- Some("relative flex flex-col h-auto w-full gap-[3px]"),
- ]);
- let list_class = radroots_app_ui_list_class_merge(&[
- Some("flex flex-col w-full justify-center items-center"),
- if resolved_styles.set_title_background {
- Some("list-group-surface")
- } else {
- None
- },
- ]);
- let view_value = view.unwrap_or_default();
- let title_view = if radroots_app_ui_list_title_visible(title.as_ref(), default_state.as_ref())
- {
- let title = title.map(|title| {
- view! { <RadrootsAppUiListTitleView basis=title id=Some(title_id.clone()) /> }
- .into_any()
- });
- Some(
- view! {
- <header class="flex flex-col w-full" data-ui="list-header">
- {title}
- </header>
- }
- .into_any(),
- )
- } else {
- None
- };
- let content_view = if let Some(default_state) = default_state {
- let default_class = radroots_app_ui_list_class_merge(&[
- Some("flex flex-col h-auto w-full justify-center items-center"),
- if resolved_styles.set_default_background {
- Some("ui-surface")
- } else {
- None
- },
- default_state.classes.as_deref(),
- ]);
- Some(
- view! {
- <div class=default_class>
- <RadrootsAppUiListDefaultLabels labels=default_state.labels />
- </div>
- }
- .into_any(),
- )
- } else if let Some(list) = list {
- let items = list
- .into_iter()
- .enumerate()
- .filter_map(|(index, item)| item.map(|item| (index, item)))
- .map(|(index, item)| {
- let row_class = radroots_app_ui_list_row_class(&item, &resolved_styles);
- let row_id = radroots_app_ui_list_item_id(base_id.as_str(), index);
- let line_id = radroots_app_ui_list_line_id(base_id.as_str(), index);
- let row_state = if item.loading { "loading" } else { "ready" };
- let offset_view = if hide_offset {
- None
- } else {
- Some(
- view! { <RadrootsAppUiListOffsetView basis=item.offset.clone() /> }
- .into_any(),
- )
- };
- let row_view = match item.kind {
- RadrootsAppUiListItemKind::Touch(touch) => view! {
- <RadrootsAppUiListTouchRow
- basis=touch
- loading=item.loading
- hide_active=item.hide_active
- hide_border_top=resolved_styles.hide_border_top
- hide_border_bottom=resolved_styles.hide_border_bottom
- line_id=line_id.clone()
- />
- }
- .into_any(),
- RadrootsAppUiListItemKind::Toggle(toggle) => view! {
- <RadrootsAppUiListToggleRow
- basis=toggle
- loading=item.loading
- hide_active=item.hide_active
- hide_border_top=resolved_styles.hide_border_top
- hide_border_bottom=resolved_styles.hide_border_bottom
- line_id=line_id.clone()
- />
- }
- .into_any(),
- RadrootsAppUiListItemKind::Input(input) => view! {
- <RadrootsAppUiListInputRow
- basis=input
- hide_border_top=resolved_styles.hide_border_top
- hide_border_bottom=resolved_styles.hide_border_bottom
- line_id=line_id.clone()
- />
- }
- .into_any(),
- RadrootsAppUiListItemKind::Select(select) => view! {
- <RadrootsAppUiListSelectRow
- basis=select
- hide_active=item.hide_active
- hide_border_top=resolved_styles.hide_border_top
- hide_border_bottom=resolved_styles.hide_border_bottom
- line_id=line_id.clone()
- />
- }
- .into_any(),
- };
- view! {
- <li
- id=row_id
- class=row_class
- data-ui="list-row"
- data-state=row_state
- >
- <div class="flex flex-row h-full w-full gap-1 items-center overflow-y-hidden">
- {offset_view}
- {row_view}
- </div>
- </li>
- }
- .into_any()
- })
- .collect_view();
- Some(
- view! { <ul id=items_id class=list_class>{items}</ul> }.into_any(),
- )
- } else {
- None
- };
- let has_title = title_view.is_some();
- view! {
- <section
- id=base_id
- class=wrap_class
- data-view=view_value
- data-ui="list-group"
- aria-labelledby=if has_title { Some(title_id.clone()) } else { None }
- >
- <div class=group_class>
- {title_view}
- {content_view}
- </div>
- </section>
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- radroots_app_ui_list_active_class,
- radroots_app_ui_list_class_merge,
- radroots_app_ui_list_border_classes,
- radroots_app_ui_list_group_data_ui_value,
- radroots_app_ui_list_row_data_ui_value,
- radroots_app_ui_list_row_leading_data_ui_value,
- radroots_app_ui_list_row_trailing_data_ui_value,
- radroots_app_ui_list_section_data_ui_value,
- radroots_app_ui_list_default_labels,
- radroots_app_ui_list_offset_mod,
- radroots_app_ui_list_input_action_icon_key,
- radroots_app_ui_list_display_loading,
- radroots_app_ui_list_row_class,
- radroots_app_ui_list_title_visible,
- radroots_app_ui_list_title_padding_class,
- };
- use crate::{
- RadrootsAppUiIconKey,
- RadrootsAppUiListInputAction,
- RadrootsAppUiListInputField,
- RadrootsAppUiListInput,
- RadrootsAppUiListItem,
- RadrootsAppUiListItemKind,
- RadrootsAppUiListOffsetMod,
- RadrootsAppUiListStylesResolved,
- RadrootsAppUiListTitle,
- RadrootsAppUiListTitleValue,
- };
-
- #[test]
- fn list_data_ui_values() {
- assert_eq!(radroots_app_ui_list_group_data_ui_value(), "list-group");
- assert_eq!(radroots_app_ui_list_section_data_ui_value(), "list-section");
- assert_eq!(radroots_app_ui_list_row_data_ui_value(), "list-row");
- assert_eq!(
- radroots_app_ui_list_row_leading_data_ui_value(),
- "list-row-leading"
- );
- assert_eq!(
- radroots_app_ui_list_row_trailing_data_ui_value(),
- "list-row-trailing"
- );
- }
-
- #[test]
- fn list_class_merge_skips_empty_values() {
- let merged = radroots_app_ui_list_class_merge(&[
- Some("alpha"),
- Some(""),
- None,
- Some("beta"),
- ]);
- assert_eq!(merged, "alpha beta");
- }
-
- #[test]
- fn list_active_class_respects_flag() {
- assert_eq!(radroots_app_ui_list_active_class(true), None);
- assert_eq!(radroots_app_ui_list_active_class(false), Some("opacity-active"));
- }
-
- #[test]
- fn list_border_classes_match_flags() {
- let classes = radroots_app_ui_list_border_classes(true, false);
- assert_eq!(classes, "group-first:border-t-0 group-last:border-b-line");
- let classes = radroots_app_ui_list_border_classes(false, true);
- assert_eq!(classes, "group-first:border-t-line group-last:border-b-0");
- }
-
- #[test]
- fn list_title_padding_matches_mod() {
- assert_eq!(
- radroots_app_ui_list_title_padding_class(Some(&RadrootsAppUiListOffsetMod::Small)),
- Some("pl-[16px]")
- );
- assert_eq!(
- radroots_app_ui_list_title_padding_class(Some(&RadrootsAppUiListOffsetMod::Glyph)),
- Some("pl-[36px]")
- );
- assert_eq!(radroots_app_ui_list_title_padding_class(None), None);
- }
-
- #[test]
- fn list_default_labels_fallbacks() {
- let labels = radroots_app_ui_list_default_labels(None);
- assert_eq!(labels.len(), 1);
- assert_eq!(labels[0].label, "No items to display.");
- }
-
- #[test]
- fn list_offset_defaults_to_small() {
- let resolved = radroots_app_ui_list_offset_mod(None);
- assert!(matches!(resolved, RadrootsAppUiListOffsetMod::Small));
- }
-
- #[test]
- fn list_input_action_defaults_to_plus() {
- let action = RadrootsAppUiListInputAction {
- visible: true,
- loading: false,
- icon: None,
- on_click: None,
- };
- assert_eq!(
- radroots_app_ui_list_input_action_icon_key(&action),
- RadrootsAppUiIconKey::Plus
- );
- }
-
- #[test]
- fn list_display_loading_defaults_false() {
- assert!(!radroots_app_ui_list_display_loading(None));
- }
-
- #[test]
- fn list_title_visible_requires_title() {
- assert!(!radroots_app_ui_list_title_visible(None, None));
- let title = RadrootsAppUiListTitle {
- value: RadrootsAppUiListTitleValue::Text("Title".to_string()),
- classes: None,
- mod_value: None,
- link: None,
- on_click: None,
- };
- assert!(radroots_app_ui_list_title_visible(Some(&title), None));
- let default_state = crate::RadrootsAppUiListDefault {
- labels: None,
- show_title: false,
- classes: None,
- };
- assert!(!radroots_app_ui_list_title_visible(
- Some(&title),
- Some(&default_state)
- ));
- }
-
- #[test]
- fn list_row_class_flags_hidden_and_rounding() {
- let item = RadrootsAppUiListItem {
- kind: RadrootsAppUiListItemKind::Input(RadrootsAppUiListInput {
- field: RadrootsAppUiListInputField {
- value: String::new(),
- placeholder: None,
- disabled: false,
- classes: None,
- id: None,
- on_input: None,
- },
- line_label: None,
- action: None,
- }),
- loading: false,
- hide_active: true,
- hide_field: true,
- full_rounded: true,
- offset: None,
- };
- let styles = RadrootsAppUiListStylesResolved {
- hide_border_top: false,
- hide_border_bottom: false,
- hide_rounded: false,
- set_title_background: false,
- set_default_background: false,
- };
- let class = radroots_app_ui_list_row_class(&item, &styles);
- assert!(class.contains("hidden"));
- assert!(class.contains("rounded-touch"));
- assert!(class.contains("first:rounded-t-2xl"));
- }
-}
diff --git a/crates/ui-components/src/list_types.rs b/crates/ui-components/src/list_types.rs
@@ -1,315 +0,0 @@
-#![forbid(unsafe_code)]
-
-use leptos::ev::MouseEvent;
-use leptos::prelude::Callback;
-
-use crate::{radroots_app_ui_icon_key_from_name, RadrootsAppUiIconKey};
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub struct RadrootsAppUiListStylesResolved {
- pub hide_border_top: bool,
- pub hide_border_bottom: bool,
- pub hide_rounded: bool,
- pub set_title_background: bool,
- pub set_default_background: bool,
-}
-
-impl Default for RadrootsAppUiListStylesResolved {
- fn default() -> Self {
- Self {
- hide_border_top: true,
- hide_border_bottom: true,
- hide_rounded: false,
- set_title_background: false,
- set_default_background: false,
- }
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppUiListStyles {
- pub hide_border_top: Option<bool>,
- pub hide_border_bottom: Option<bool>,
- pub hide_rounded: Option<bool>,
- pub set_title_background: Option<bool>,
- pub set_default_background: Option<bool>,
-}
-
-pub fn radroots_app_ui_list_styles_resolve(
- styles: Option<&RadrootsAppUiListStyles>,
-) -> RadrootsAppUiListStylesResolved {
- let defaults = RadrootsAppUiListStylesResolved::default();
- match styles {
- Some(styles) => RadrootsAppUiListStylesResolved {
- hide_border_top: styles.hide_border_top.unwrap_or(defaults.hide_border_top),
- hide_border_bottom: styles
- .hide_border_bottom
- .unwrap_or(defaults.hide_border_bottom),
- hide_rounded: styles.hide_rounded.unwrap_or(defaults.hide_rounded),
- set_title_background: styles
- .set_title_background
- .unwrap_or(defaults.set_title_background),
- set_default_background: styles
- .set_default_background
- .unwrap_or(defaults.set_default_background),
- },
- None => defaults,
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppUiListIcon {
- pub key: String,
- pub class: Option<String>,
-}
-
-pub fn radroots_app_ui_list_icon_key(
- icon: &RadrootsAppUiListIcon,
-) -> Option<RadrootsAppUiIconKey> {
- radroots_app_ui_icon_key_from_name(icon.key.as_str())
-}
-
-#[derive(Debug, Clone)]
-pub enum RadrootsAppUiListTitleValue {
- Text(String),
- Spacer,
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppUiListTitleLink {
- pub label: Option<RadrootsAppUiListLabelValue>,
- pub icon: Option<RadrootsAppUiListIcon>,
- pub classes: Option<String>,
- pub on_click: Option<Callback<()>>,
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppUiListTitle {
- pub value: RadrootsAppUiListTitleValue,
- pub classes: Option<String>,
- pub mod_value: Option<RadrootsAppUiListOffsetMod>,
- pub link: Option<RadrootsAppUiListTitleLink>,
- pub on_click: Option<Callback<()>>,
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppUiListDefaultLabel {
- pub label: String,
- pub classes: Option<String>,
- pub on_click: Option<Callback<()>>,
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppUiListDefault {
- pub labels: Option<Vec<RadrootsAppUiListDefaultLabel>>,
- pub show_title: bool,
- pub classes: Option<String>,
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppUiListLabelText {
- pub value: String,
- pub classes: Option<String>,
-}
-
-#[derive(Debug, Clone)]
-pub enum RadrootsAppUiListLabelValueKind {
- Text(RadrootsAppUiListLabelText),
- Icon(RadrootsAppUiListIcon),
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppUiListLabelValue {
- pub classes_wrap: Option<String>,
- pub hide_truncate: bool,
- pub value: RadrootsAppUiListLabelValueKind,
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppUiListLabel {
- pub left: Vec<RadrootsAppUiListLabelValue>,
- pub right: Vec<RadrootsAppUiListLabelValue>,
-}
-
-#[derive(Debug, Clone)]
-pub enum RadrootsAppUiListDisplayValue {
- Icon(RadrootsAppUiListIcon),
- Label(RadrootsAppUiListLabelText),
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppUiListDisplay {
- pub value: RadrootsAppUiListDisplayValue,
- pub loading: bool,
- pub on_click: Option<Callback<MouseEvent>>,
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppUiListTouchEnd {
- pub icon: RadrootsAppUiListIcon,
- pub on_click: Option<Callback<MouseEvent>>,
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppUiListTouch {
- pub label: RadrootsAppUiListLabel,
- pub display: Option<RadrootsAppUiListDisplay>,
- pub end: Option<RadrootsAppUiListTouchEnd>,
- pub on_click: Option<Callback<MouseEvent>>,
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppUiListToggle {
- pub label: RadrootsAppUiListLabel,
- pub checked: bool,
- pub disabled: bool,
- pub on_toggle: Option<Callback<bool>>,
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppUiListInputAction {
- pub visible: bool,
- pub loading: bool,
- pub icon: Option<RadrootsAppUiListIcon>,
- pub on_click: Option<Callback<MouseEvent>>,
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppUiListInputLineLabel {
- pub value: String,
- pub classes: Option<String>,
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppUiListInputField {
- pub value: String,
- pub placeholder: Option<String>,
- pub disabled: bool,
- pub classes: Option<String>,
- pub id: Option<String>,
- pub on_input: Option<Callback<String>>,
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppUiListInput {
- pub field: RadrootsAppUiListInputField,
- pub line_label: Option<RadrootsAppUiListInputLineLabel>,
- pub action: Option<RadrootsAppUiListInputAction>,
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppUiListSelectOption {
- pub label: String,
- pub value: String,
- pub classes: Option<String>,
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppUiListSelectField {
- pub value: String,
- pub options: Vec<RadrootsAppUiListSelectOption>,
- pub disabled: bool,
- pub classes: Option<String>,
- pub id: Option<String>,
- pub on_change: Option<Callback<String>>,
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppUiListSelect {
- pub field: RadrootsAppUiListSelectField,
- pub label: RadrootsAppUiListLabel,
- pub display: Option<RadrootsAppUiListDisplay>,
- pub end: Option<RadrootsAppUiListTouchEnd>,
- pub loading: bool,
- pub on_click: Option<Callback<MouseEvent>>,
-}
-
-#[derive(Debug, Clone)]
-pub enum RadrootsAppUiListOffsetMod {
- Small,
- Glyph,
- Icon {
- icon: RadrootsAppUiListIcon,
- loading: bool,
- },
- IconCircle {
- icon: RadrootsAppUiListIcon,
- loading: bool,
- },
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppUiListOffset {
- pub mod_value: Option<RadrootsAppUiListOffsetMod>,
- pub classes: Option<String>,
- pub hide_space: bool,
- pub hide_offset: bool,
- pub on_click: Option<Callback<MouseEvent>>,
-}
-
-#[derive(Debug, Clone)]
-pub enum RadrootsAppUiListItemKind {
- Touch(RadrootsAppUiListTouch),
- Toggle(RadrootsAppUiListToggle),
- Input(RadrootsAppUiListInput),
- Select(RadrootsAppUiListSelect),
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppUiListItem {
- pub kind: RadrootsAppUiListItemKind,
- pub loading: bool,
- pub hide_active: bool,
- pub hide_field: bool,
- pub full_rounded: bool,
- pub offset: Option<RadrootsAppUiListOffset>,
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppUiList {
- pub id: Option<String>,
- pub view: Option<String>,
- pub classes: Option<String>,
- pub title: Option<RadrootsAppUiListTitle>,
- pub default_state: Option<RadrootsAppUiListDefault>,
- pub list: Option<Vec<Option<RadrootsAppUiListItem>>>,
- pub hide_offset: bool,
- pub styles: Option<RadrootsAppUiListStyles>,
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- radroots_app_ui_list_styles_resolve,
- RadrootsAppUiListStyles,
- RadrootsAppUiListStylesResolved,
- };
-
- #[test]
- fn list_style_defaults_match_spec() {
- let resolved = radroots_app_ui_list_styles_resolve(None);
- assert_eq!(resolved, RadrootsAppUiListStylesResolved::default());
- }
-
- #[test]
- fn list_style_overrides_apply() {
- let styles = RadrootsAppUiListStyles {
- hide_border_top: Some(false),
- hide_border_bottom: Some(true),
- hide_rounded: Some(true),
- set_title_background: Some(true),
- set_default_background: Some(true),
- };
- let resolved = radroots_app_ui_list_styles_resolve(Some(&styles));
- assert_eq!(
- resolved,
- RadrootsAppUiListStylesResolved {
- hide_border_top: false,
- hide_border_bottom: true,
- hide_rounded: true,
- set_title_background: true,
- set_default_background: true,
- }
- );
- }
-}
diff --git a/crates/ui-components/src/nav_header.rs b/crates/ui-components/src/nav_header.rs
@@ -1,140 +0,0 @@
-#![forbid(unsafe_code)]
-
-use leptos::ev::MouseEvent;
-use leptos::prelude::*;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppUiNavHeaderBgMode {
- Transparent,
- Opaque,
- Blur,
- AutoOpaque,
- AutoBlur,
-}
-
-impl RadrootsAppUiNavHeaderBgMode {
- pub const fn as_str(self) -> &'static str {
- match self {
- RadrootsAppUiNavHeaderBgMode::Transparent => "transparent",
- RadrootsAppUiNavHeaderBgMode::Opaque => "opaque",
- RadrootsAppUiNavHeaderBgMode::Blur => "blur",
- RadrootsAppUiNavHeaderBgMode::AutoOpaque => "auto-opaque",
- RadrootsAppUiNavHeaderBgMode::AutoBlur => "auto-blur",
- }
- }
-
- pub const fn is_auto(self) -> bool {
- matches!(
- self,
- RadrootsAppUiNavHeaderBgMode::AutoOpaque | RadrootsAppUiNavHeaderBgMode::AutoBlur
- )
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppUiNavHeaderCollapseMode {
- None,
- Scroll,
-}
-
-#[component]
-pub fn RadrootsAppUiNavHeader(
- label: String,
- on_label_click: Option<Callback<MouseEvent>>,
- bg_mode: Option<RadrootsAppUiNavHeaderBgMode>,
- collapse_mode: Option<RadrootsAppUiNavHeaderCollapseMode>,
- right: Option<ChildrenFn>,
- id: Option<String>,
- class: Option<String>,
-) -> impl IntoView {
- let bg_mode = bg_mode.unwrap_or(RadrootsAppUiNavHeaderBgMode::AutoBlur);
- let collapse_mode = collapse_mode.unwrap_or(RadrootsAppUiNavHeaderCollapseMode::Scroll);
- let class_value = match class {
- Some(value) => format!("nav-header {value}"),
- None => "nav-header".to_string(),
- };
- let label_large = label.clone();
- let label_compact = label.clone();
- let title_large = nav_header_title_view(
- label_large,
- "nav-header__title-text nav-header__title-large",
- on_label_click.clone(),
- );
- let title_compact = nav_header_title_view(
- label_compact,
- "nav-header__title-text nav-header__title-compact",
- on_label_click,
- );
- let scroll_context = use_context::<crate::RadrootsAppUiScrollContext>();
- let collapse_progress = Signal::derive(move || match collapse_mode {
- RadrootsAppUiNavHeaderCollapseMode::None => 0.0,
- RadrootsAppUiNavHeaderCollapseMode::Scroll => scroll_context
- .as_ref()
- .map(|context| context.collapse_progress.get())
- .unwrap_or(0.0),
- });
- let bg_active = Signal::derive(move || {
- let scrolled = collapse_progress.get() > 0.02;
- match bg_mode {
- RadrootsAppUiNavHeaderBgMode::Transparent => false,
- RadrootsAppUiNavHeaderBgMode::Opaque | RadrootsAppUiNavHeaderBgMode::Blur => true,
- RadrootsAppUiNavHeaderBgMode::AutoOpaque | RadrootsAppUiNavHeaderBgMode::AutoBlur => {
- scrolled
- }
- }
- });
- let show_actions = Signal::derive(move || !bg_active.get());
- let right_slot = right;
- view! {
- <header
- id=id
- class=class_value
- attr:data-bg=bg_mode.as_str()
- attr:data-bg-state=move || if bg_active.get() { "active" } else { "idle" }
- style=move || format!("--collapse: {:.3};", collapse_progress.get())
- >
- <div class="nav-header__background" aria-hidden="true"></div>
- <div class="nav-header__content">
- <div class="nav-header__bar">
- <div class="nav-header__compact">
- {title_compact}
- </div>
- {move || {
- if show_actions.get() {
- right_slot
- .as_ref()
- .map(|slot| slot())
- .map(|view| view! { <div class="nav-header__actions">{view}</div> }.into_any())
- .unwrap_or_else(|| view! { <></> }.into_any())
- } else {
- view! { <></> }.into_any()
- }
- }}
- </div>
- <div class="nav-header__large">
- {title_large}
- </div>
- </div>
- </header>
- }
-}
-
-fn nav_header_title_view(
- label: String,
- class: &str,
- on_label_click: Option<Callback<MouseEvent>>,
-) -> AnyView {
- if let Some(callback) = on_label_click {
- let on_click = move |ev: MouseEvent| {
- callback.run(ev);
- };
- view! {
- <button class="nav-header__title-button" on:click=on_click>
- <span class=class>{label}</span>
- </button>
- }
- .into_any()
- } else {
- view! { <span class=class>{label}</span> }.into_any()
- }
-}
diff --git a/crates/ui-components/src/nav_tabs.rs b/crates/ui-components/src/nav_tabs.rs
@@ -1,55 +0,0 @@
-#![forbid(unsafe_code)]
-
-use leptos::prelude::*;
-
-const NAV_TABS_HIDE_VELOCITY: f64 = 220.0;
-const NAV_TABS_SHOW_VELOCITY: f64 = 120.0;
-const NAV_TABS_HIDE_SCROLL: f64 = 40.0;
-
-#[component]
-pub fn RadrootsAppUiNavTabs(
- #[prop(optional)] id: Option<String>,
- #[prop(optional)] class: Option<String>,
- #[prop(optional)] auto_hide: Option<bool>,
- children: Children,
-) -> impl IntoView {
- let auto_hide = auto_hide.unwrap_or(true);
- let scroll_context = use_context::<crate::RadrootsAppUiScrollContext>();
- let hidden = RwSignal::new(false);
- Effect::new(move || {
- if !auto_hide {
- hidden.set(false);
- return;
- }
- let Some(context) = scroll_context.as_ref() else {
- hidden.set(false);
- return;
- };
- let scroll_top = context.scroll_top.get();
- if scroll_top <= NAV_TABS_HIDE_SCROLL {
- hidden.set(false);
- return;
- }
- let velocity = context.scroll_velocity.get();
- if velocity > NAV_TABS_HIDE_VELOCITY {
- hidden.set(true);
- } else if velocity < -NAV_TABS_SHOW_VELOCITY {
- hidden.set(false);
- }
- });
- let class_value = match class {
- Some(value) => format!("nav-tabs {value}"),
- None => "nav-tabs".to_string(),
- };
- view! {
- <nav
- id=id
- class=class_value
- attr:data-hidden=move || if hidden.get() { "true" } else { "false" }
- >
- <div class="nav-tabs__tray">
- {children()}
- </div>
- </nav>
- }
-}
diff --git a/crates/ui-components/src/scroll.rs b/crates/ui-components/src/scroll.rs
@@ -1,116 +0,0 @@
-#![forbid(unsafe_code)]
-
-use leptos::ev::Event;
-use leptos::prelude::*;
-use web_sys::HtmlElement;
-
-const DEFAULT_COLLAPSE_RANGE: f64 = 120.0;
-
-#[derive(Clone, Copy, Debug)]
-struct RadrootsAppUiScrollSample {
- scroll_top: f64,
- time_ms: f64,
-}
-
-impl Default for RadrootsAppUiScrollSample {
- fn default() -> Self {
- Self {
- scroll_top: 0.0,
- time_ms: 0.0,
- }
- }
-}
-
-#[derive(Clone, Debug)]
-pub struct RadrootsAppUiScrollContext {
- pub scroll_top: RwSignal<f64>,
- pub scroll_velocity: RwSignal<f64>,
- pub collapse_progress: RwSignal<f64>,
-}
-
-impl RadrootsAppUiScrollContext {
- pub fn new() -> Self {
- Self {
- scroll_top: RwSignal::new(0.0),
- scroll_velocity: RwSignal::new(0.0),
- collapse_progress: RwSignal::new(0.0),
- }
- }
-}
-
-pub fn radroots_app_ui_collapse_progress(scroll_top: f64, collapse_range: f64) -> f64 {
- if collapse_range <= 0.0 {
- return 0.0;
- }
- (scroll_top / collapse_range).clamp(0.0, 1.0)
-}
-
-pub fn radroots_app_ui_scroll_velocity(prev_top: f64, next_top: f64, dt_ms: f64) -> f64 {
- if dt_ms <= 0.0 {
- return 0.0;
- }
- (next_top - prev_top) / dt_ms * 1000.0
-}
-
-#[component]
-pub fn RadrootsAppUiScrollContainer(
- id: Option<String>,
- classes: Option<String>,
- collapse_range: Option<f64>,
- context: Option<RadrootsAppUiScrollContext>,
- children: Children,
-) -> impl IntoView {
- let context = context.unwrap_or_else(RadrootsAppUiScrollContext::new);
- provide_context(context.clone());
- let last_sample = RwSignal::new_local(RadrootsAppUiScrollSample::default());
- let collapse_range_value = collapse_range.unwrap_or(DEFAULT_COLLAPSE_RANGE);
- let class_value =
- classes.unwrap_or_else(|| "app-page app-page-scroll app-page-chrome".to_string());
- let on_scroll = move |ev: Event| {
- let target = event_target::<HtmlElement>(&ev);
- let scroll_top = target.scroll_top() as f64;
- let time_ms = ev.time_stamp();
- let prev = last_sample.get_untracked();
- let velocity = radroots_app_ui_scroll_velocity(prev.scroll_top, scroll_top, time_ms - prev.time_ms);
- last_sample.set(RadrootsAppUiScrollSample { scroll_top, time_ms });
- context.scroll_top.set(scroll_top);
- context.scroll_velocity.set(velocity);
- context
- .collapse_progress
- .set(radroots_app_ui_collapse_progress(scroll_top, collapse_range_value));
- };
- view! {
- <div id=id class=class_value on:scroll=on_scroll>
- {children()}
- </div>
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{radroots_app_ui_collapse_progress, radroots_app_ui_scroll_velocity};
-
- #[test]
- fn collapse_progress_clamps_range() {
- assert_eq!(radroots_app_ui_collapse_progress(0.0, 120.0), 0.0);
- assert_eq!(radroots_app_ui_collapse_progress(60.0, 120.0), 0.5);
- assert_eq!(radroots_app_ui_collapse_progress(180.0, 120.0), 1.0);
- assert_eq!(radroots_app_ui_collapse_progress(-10.0, 120.0), 0.0);
- }
-
- #[test]
- fn collapse_progress_handles_zero_range() {
- assert_eq!(radroots_app_ui_collapse_progress(10.0, 0.0), 0.0);
- }
-
- #[test]
- fn scroll_velocity_uses_delta_per_second() {
- assert_eq!(radroots_app_ui_scroll_velocity(0.0, 100.0, 1000.0), 100.0);
- assert_eq!(radroots_app_ui_scroll_velocity(100.0, 0.0, 500.0), -200.0);
- }
-
- #[test]
- fn scroll_velocity_handles_zero_time() {
- assert_eq!(radroots_app_ui_scroll_velocity(0.0, 100.0, 0.0), 0.0);
- }
-}
diff --git a/crates/ui-components/src/separator.rs b/crates/ui-components/src/separator.rs
@@ -1,72 +0,0 @@
-use leptos::prelude::*;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppUiSeparatorOrientation {
- Horizontal,
- Vertical,
-}
-
-impl Default for RadrootsAppUiSeparatorOrientation {
- fn default() -> Self {
- RadrootsAppUiSeparatorOrientation::Horizontal
- }
-}
-
-pub fn radroots_app_ui_separator_orientation_value(
- orientation: RadrootsAppUiSeparatorOrientation,
-) -> &'static str {
- match orientation {
- RadrootsAppUiSeparatorOrientation::Horizontal => "horizontal",
- RadrootsAppUiSeparatorOrientation::Vertical => "vertical",
- }
-}
-
-#[component]
-pub fn RadrootsAppUiSeparator(
- #[prop(optional)] orientation: RadrootsAppUiSeparatorOrientation,
- #[prop(optional)] decorative: bool,
- #[prop(optional)] class: Option<String>,
- #[prop(optional)] id: Option<String>,
- #[prop(optional)] style: Option<String>,
-) -> impl IntoView {
- let data_orientation = radroots_app_ui_separator_orientation_value(orientation);
- let data_decorative = if decorative { Some("".to_string()) } else { None };
- let role = if decorative { "presentation" } else { "separator" };
- let aria_orientation = if decorative {
- None
- } else {
- Some(data_orientation.to_string())
- };
- view! {
- <div
- id=id
- class=class
- style=style
- role=role
- data-ui="separator"
- data-orientation=data_orientation
- data-decorative=data_decorative
- aria-orientation=aria_orientation
- ></div>
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- radroots_app_ui_separator_orientation_value,
- RadrootsAppUiSeparatorOrientation,
- };
-
- #[test]
- fn separator_orientation_values() {
- assert_eq!(
- radroots_app_ui_separator_orientation_value(RadrootsAppUiSeparatorOrientation::Horizontal),
- "horizontal"
- );
- assert_eq!(
- radroots_app_ui_separator_orientation_value(RadrootsAppUiSeparatorOrientation::Vertical),
- "vertical"
- );
- }
-}
diff --git a/crates/ui-components/src/sheet.rs b/crates/ui-components/src/sheet.rs
@@ -1,197 +0,0 @@
-use leptos::prelude::*;
-
-use super::{
- RadrootsAppUiDialogClose,
- RadrootsAppUiDialogContent,
- RadrootsAppUiDialogDescription,
- RadrootsAppUiDialogOverlay,
- RadrootsAppUiDialogPortal,
- RadrootsAppUiDialogRoot,
- RadrootsAppUiDialogTitle,
- RadrootsAppUiDialogTrigger,
-};
-
-pub fn radroots_app_ui_sheet_data_ui_value() -> &'static str {
- "sheet"
-}
-
-pub fn radroots_app_ui_sheet_overlay_data_ui_value() -> &'static str {
- "sheet-overlay"
-}
-
-pub fn radroots_app_ui_sheet_handle_data_ui_value() -> &'static str {
- "sheet-handle"
-}
-
-#[component]
-pub fn RadrootsAppUiSheetRoot(
- open: Option<ReadSignal<bool>>,
- #[prop(optional)] default_open: bool,
- modal: Option<bool>,
- on_open_change: Option<Callback<bool>>,
- children: ChildrenFn,
-) -> impl IntoView {
- view! {
- <RadrootsAppUiDialogRoot
- open=open
- default_open=default_open
- modal=modal
- on_open_change=on_open_change
- >
- {children()}
- </RadrootsAppUiDialogRoot>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiSheetTrigger(
- #[prop(optional)] disabled: bool,
- class: Option<String>,
- id: Option<String>,
- style: Option<String>,
- children: Children,
-) -> impl IntoView {
- view! {
- <RadrootsAppUiDialogTrigger
- disabled=disabled
- class=class
- id=id
- style=style
- >
- {children()}
- </RadrootsAppUiDialogTrigger>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiSheetPortal(children: ChildrenFn) -> impl IntoView {
- view! {
- <RadrootsAppUiDialogPortal>
- {children()}
- </RadrootsAppUiDialogPortal>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiSheetOverlay(
- close_on_click: Option<bool>,
- class: Option<String>,
- id: Option<String>,
- style: Option<String>,
-) -> impl IntoView {
- view! {
- <RadrootsAppUiDialogOverlay
- close_on_click=close_on_click
- data_ui=Some(radroots_app_ui_sheet_overlay_data_ui_value().to_string())
- class=class
- id=id
- style=style
- ></RadrootsAppUiDialogOverlay>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiSheetContent(
- #[prop(optional)] disable_outside_pointer_events: bool,
- #[prop(optional)] show_handle: bool,
- class: Option<String>,
- id: Option<String>,
- style: Option<String>,
- children: ChildrenFn,
-) -> impl IntoView {
- let handle = show_handle;
- let children = StoredValue::new(children);
- let content_children = move || {
- let inner = (children.get_value())();
- if handle {
- view! {
- <div data-ui=radroots_app_ui_sheet_handle_data_ui_value()></div>
- {inner}
- }
- .into_any()
- } else {
- inner
- }
- };
- view! {
- <RadrootsAppUiDialogContent
- disable_outside_pointer_events=disable_outside_pointer_events
- data_ui=Some(radroots_app_ui_sheet_data_ui_value().to_string())
- class=class
- id=id
- style=style
- >
- {content_children}
- </RadrootsAppUiDialogContent>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiSheetTitle(
- class: Option<String>,
- id: Option<String>,
- style: Option<String>,
- children: Children,
-) -> impl IntoView {
- view! {
- <RadrootsAppUiDialogTitle
- class=class
- id=id
- style=style
- >
- {children()}
- </RadrootsAppUiDialogTitle>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiSheetDescription(
- class: Option<String>,
- id: Option<String>,
- style: Option<String>,
- children: Children,
-) -> impl IntoView {
- view! {
- <RadrootsAppUiDialogDescription
- class=class
- id=id
- style=style
- >
- {children()}
- </RadrootsAppUiDialogDescription>
- }
-}
-
-#[component]
-pub fn RadrootsAppUiSheetClose(
- class: Option<String>,
- id: Option<String>,
- style: Option<String>,
- children: Children,
-) -> impl IntoView {
- view! {
- <RadrootsAppUiDialogClose
- class=class
- id=id
- style=style
- >
- {children()}
- </RadrootsAppUiDialogClose>
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- radroots_app_ui_sheet_data_ui_value,
- radroots_app_ui_sheet_handle_data_ui_value,
- radroots_app_ui_sheet_overlay_data_ui_value,
- };
-
- #[test]
- fn sheet_data_ui_values() {
- assert_eq!(radroots_app_ui_sheet_data_ui_value(), "sheet");
- assert_eq!(radroots_app_ui_sheet_overlay_data_ui_value(), "sheet-overlay");
- assert_eq!(radroots_app_ui_sheet_handle_data_ui_value(), "sheet-handle");
- }
-}
diff --git a/crates/ui-components/src/spinner.rs b/crates/ui-components/src/spinner.rs
@@ -1,59 +0,0 @@
-#![forbid(unsafe_code)]
-
-use leptos::prelude::*;
-
-const RADROOTS_APP_UI_SPINNER_BLADE_COUNT: usize = 8;
-
-fn radroots_app_ui_spinner_class_merge(parts: &[Option<&str>]) -> String {
- let mut result = String::new();
- for part in parts {
- if let Some(value) = part {
- if value.is_empty() {
- continue;
- }
- if !result.is_empty() {
- result.push(' ');
- }
- result.push_str(value);
- }
- }
- result
-}
-
-#[component]
-pub fn RadrootsAppUiSpinner(
- #[prop(optional)] class: Option<String>,
- #[prop(optional)] id: Option<String>,
- #[prop(optional)] style: Option<String>,
- #[prop(optional)] white: bool,
- #[prop(optional)] center: bool,
-) -> impl IntoView {
- let class_value = radroots_app_ui_spinner_class_merge(&[
- Some("spinner8"),
- if white { Some("spinner8-white") } else { None },
- if center { Some("center") } else { None },
- class.as_deref(),
- ]);
- let blade_class = radroots_app_ui_spinner_class_merge(&[
- Some("spinner8-blade"),
- if white { Some("spinner8-blade-white") } else { None },
- ]);
- let blades = (0..RADROOTS_APP_UI_SPINNER_BLADE_COUNT)
- .map(|_| view! { <span class=blade_class.clone()></span> })
- .collect_view();
- view! {
- <span id=id class=class_value style=style>
- {blades}
- </span>
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::RADROOTS_APP_UI_SPINNER_BLADE_COUNT;
-
- #[test]
- fn spinner_blade_count_is_expected() {
- assert_eq!(RADROOTS_APP_UI_SPINNER_BLADE_COUNT, 8);
- }
-}
diff --git a/crates/ui-core/Cargo.toml b/crates/ui-core/Cargo.toml
@@ -1,25 +0,0 @@
-[package]
-name = "radroots-app-ui-core"
-authors = ["Radroots Authors"]
-version.workspace = true
-edition.workspace = true
-license.workspace = true
-rust-version.workspace = true
-
-[lib]
-crate-type = ["rlib"]
-
-[dependencies]
-
-[target.'cfg(target_arch = "wasm32")'.dependencies]
-wasm-bindgen = { workspace = true }
-web-sys = { workspace = true, features = [
- "Document",
- "Element",
- "EventTarget",
- "HtmlElement",
- "KeyboardEvent",
- "MediaQueryList",
- "PointerEvent",
- "Window"
-] }
diff --git a/crates/ui-core/src/event.rs b/crates/ui-core/src/event.rs
@@ -1,83 +0,0 @@
-pub fn radroots_app_ui_compose_event_handlers<T, A, B, P>(
- mut first: Option<A>,
- mut second: Option<B>,
- mut is_prevented: P,
-) -> impl FnMut(&T)
-where
- A: FnMut(&T),
- B: FnMut(&T),
- P: FnMut(&T) -> bool,
-{
- move |event| {
- if let Some(handler) = first.as_mut() {
- handler(event);
- }
- if is_prevented(event) {
- return;
- }
- if let Some(handler) = second.as_mut() {
- handler(event);
- }
- }
-}
-
-pub fn radroots_app_ui_compose_event_handlers_unchecked<T, A, B>(
- first: Option<A>,
- second: Option<B>,
-) -> impl FnMut(&T)
-where
- A: FnMut(&T),
- B: FnMut(&T),
-{
- radroots_app_ui_compose_event_handlers(first, second, |_| false)
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- radroots_app_ui_compose_event_handlers,
- radroots_app_ui_compose_event_handlers_unchecked,
- };
-
- #[derive(Default)]
- struct TestEvent {
- calls: core::cell::Cell<usize>,
- prevented: core::cell::Cell<bool>,
- }
-
- impl TestEvent {
- fn mark_called(&self) {
- self.calls.set(self.calls.get() + 1);
- }
-
- fn prevent(&self) {
- self.prevented.set(true);
- }
- }
-
- #[test]
- fn compose_calls_handlers_in_order() {
- let event = TestEvent::default();
- let mut handler = radroots_app_ui_compose_event_handlers_unchecked(
- Some(|evt: &TestEvent| evt.mark_called()),
- Some(|evt: &TestEvent| evt.mark_called()),
- );
- handler(&event);
- assert_eq!(event.calls.get(), 2);
- }
-
- #[test]
- fn compose_skips_second_when_prevented() {
- let event = TestEvent::default();
- let mut handler = radroots_app_ui_compose_event_handlers(
- Some(|evt: &TestEvent| {
- evt.mark_called();
- evt.prevent();
- }),
- Some(|evt: &TestEvent| evt.mark_called()),
- |evt: &TestEvent| evt.prevented.get(),
- );
- handler(&event);
- assert_eq!(event.calls.get(), 1);
- }
-}
diff --git a/crates/ui-core/src/id.rs b/crates/ui-core/src/id.rs
@@ -1,82 +0,0 @@
-use alloc::string::{String, ToString};
-use core::sync::atomic::{AtomicUsize, Ordering};
-
-static RADROOTS_APP_UI_ID_SEQ: AtomicUsize = AtomicUsize::new(0);
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
-pub struct RadrootsAppUiId {
- value: usize,
-}
-
-impl RadrootsAppUiId {
- pub fn next() -> Self {
- let value = RADROOTS_APP_UI_ID_SEQ.fetch_add(1, Ordering::Relaxed);
- Self { value }
- }
-
- pub const fn value(self) -> usize {
- self.value
- }
-
- pub fn prefixed(self, prefix: &str) -> String {
- let mut out = String::with_capacity(prefix.len() + 1 + 20);
- out.push_str(prefix);
- out.push('-');
- out.push_str(self.value.to_string().as_str());
- out
- }
-}
-
-#[derive(Debug, Default, Clone)]
-pub struct RadrootsAppUiIdSequence {
- next: usize,
-}
-
-impl RadrootsAppUiIdSequence {
- pub const fn new() -> Self {
- Self { next: 0 }
- }
-
- pub fn next(&mut self) -> RadrootsAppUiId {
- let value = self.next;
- self.next = self.next.saturating_add(1);
- RadrootsAppUiId { value }
- }
-
- pub const fn peek(&self) -> usize {
- self.next
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{RadrootsAppUiId, RadrootsAppUiIdSequence};
-
- #[test]
- fn id_sequence_increments() {
- let first = RadrootsAppUiId::next().value();
- let second = RadrootsAppUiId::next().value();
- assert!(second > first);
- }
-
- #[test]
- fn id_prefix_builds_value() {
- let id = RadrootsAppUiId { value: 7 };
- assert_eq!(id.prefixed("radroots"), "radroots-7");
- }
-
- #[test]
- fn id_sequence_local_increments() {
- let mut seq = RadrootsAppUiIdSequence::new();
- let first = seq.next();
- let second = seq.next();
- assert_eq!(first.value(), 0);
- assert_eq!(second.value(), 1);
- }
-
- #[test]
- fn id_sequence_peek_is_next_value() {
- let seq = RadrootsAppUiIdSequence::new();
- assert_eq!(seq.peek(), 0);
- }
-}
diff --git a/crates/ui-core/src/input.rs b/crates/ui-core/src/input.rs
@@ -1,130 +0,0 @@
-use core::fmt;
-use core::sync::atomic::{AtomicU8, Ordering};
-
-#[cfg(target_arch = "wasm32")]
-use alloc::boxed::Box;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppUiInputModality {
- Keyboard,
- Pointer,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppUiInputModalityError {
- WindowMissing,
- DocumentMissing,
- RootMissing,
- ListenerFailed,
-}
-
-impl fmt::Display for RadrootsAppUiInputModalityError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- RadrootsAppUiInputModalityError::WindowMissing => {
- write!(f, "input_modality_window_missing")
- }
- RadrootsAppUiInputModalityError::DocumentMissing => {
- write!(f, "input_modality_document_missing")
- }
- RadrootsAppUiInputModalityError::RootMissing => {
- write!(f, "input_modality_root_missing")
- }
- RadrootsAppUiInputModalityError::ListenerFailed => {
- write!(f, "input_modality_listener_failed")
- }
- }
- }
-}
-
-static RADROOTS_APP_UI_INPUT_MODE: AtomicU8 = AtomicU8::new(0);
-
-pub fn radroots_app_ui_input_modality_get() -> Option<RadrootsAppUiInputModality> {
- match RADROOTS_APP_UI_INPUT_MODE.load(Ordering::Relaxed) {
- 1 => Some(RadrootsAppUiInputModality::Keyboard),
- 2 => Some(RadrootsAppUiInputModality::Pointer),
- _ => None,
- }
-}
-
-pub fn radroots_app_ui_input_modality_set(modality: RadrootsAppUiInputModality) {
- let value = match modality {
- RadrootsAppUiInputModality::Keyboard => 1,
- RadrootsAppUiInputModality::Pointer => 2,
- };
- RADROOTS_APP_UI_INPUT_MODE.store(value, Ordering::Relaxed);
-}
-
-#[cfg(target_arch = "wasm32")]
-pub fn radroots_app_ui_input_modality_attach() -> Result<(), RadrootsAppUiInputModalityError> {
- use wasm_bindgen::closure::Closure;
- use wasm_bindgen::JsCast;
-
- let window = web_sys::window().ok_or(RadrootsAppUiInputModalityError::WindowMissing)?;
- let document = window
- .document()
- .ok_or(RadrootsAppUiInputModalityError::DocumentMissing)?;
- let root = document
- .document_element()
- .ok_or(RadrootsAppUiInputModalityError::RootMissing)?;
-
- let root_keyboard = root.clone();
- let keydown = Closure::wrap(Box::new(move |_event: web_sys::KeyboardEvent| {
- radroots_app_ui_input_modality_set(RadrootsAppUiInputModality::Keyboard);
- let _ = root_keyboard.set_attribute("data-input", "keyboard");
- }) as Box<dyn FnMut(_)>);
- document
- .add_event_listener_with_callback("keydown", keydown.as_ref().unchecked_ref())
- .map_err(|_| RadrootsAppUiInputModalityError::ListenerFailed)?;
- keydown.forget();
-
- let root_pointer = root.clone();
- let pointerdown = Closure::wrap(Box::new(move |_event: web_sys::PointerEvent| {
- radroots_app_ui_input_modality_set(RadrootsAppUiInputModality::Pointer);
- let _ = root_pointer.set_attribute("data-input", "pointer");
- }) as Box<dyn FnMut(_)>);
- document
- .add_event_listener_with_callback("pointerdown", pointerdown.as_ref().unchecked_ref())
- .map_err(|_| RadrootsAppUiInputModalityError::ListenerFailed)?;
- pointerdown.forget();
-
- Ok(())
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub fn radroots_app_ui_input_modality_attach() -> Result<(), RadrootsAppUiInputModalityError> {
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- radroots_app_ui_input_modality_get,
- radroots_app_ui_input_modality_set,
- RadrootsAppUiInputModality,
- RADROOTS_APP_UI_INPUT_MODE,
- };
- use core::sync::atomic::Ordering;
-
- fn reset_input_modality() {
- RADROOTS_APP_UI_INPUT_MODE.store(0, Ordering::Relaxed);
- }
-
- #[test]
- fn input_modality_defaults_to_none() {
- reset_input_modality();
- let current = radroots_app_ui_input_modality_get();
- assert!(current.is_none());
- }
-
- #[test]
- fn input_modality_set_roundtrips() {
- reset_input_modality();
- radroots_app_ui_input_modality_set(RadrootsAppUiInputModality::Keyboard);
- assert_eq!(
- radroots_app_ui_input_modality_get(),
- Some(RadrootsAppUiInputModality::Keyboard)
- );
- reset_input_modality();
- }
-}
diff --git a/crates/ui-core/src/lib.rs b/crates/ui-core/src/lib.rs
@@ -1,28 +0,0 @@
-#![forbid(unsafe_code)]
-#![no_std]
-
-extern crate alloc;
-
-mod id;
-mod event;
-mod input;
-mod preference;
-
-pub use event::{
- radroots_app_ui_compose_event_handlers,
- radroots_app_ui_compose_event_handlers_unchecked,
-};
-pub use id::{RadrootsAppUiId, RadrootsAppUiIdSequence};
-pub use input::{
- radroots_app_ui_input_modality_attach,
- radroots_app_ui_input_modality_get,
- radroots_app_ui_input_modality_set,
- RadrootsAppUiInputModality,
- RadrootsAppUiInputModalityError,
-};
-pub use preference::{
- radroots_app_ui_prefers_contrast_more,
- radroots_app_ui_prefers_reduced_motion,
- RadrootsAppUiPreferenceError,
- RadrootsAppUiPreferenceResult,
-};
diff --git a/crates/ui-core/src/preference.rs b/crates/ui-core/src/preference.rs
@@ -1,58 +0,0 @@
-use core::fmt;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppUiPreferenceError {
- WindowMissing,
- MatchMediaFailed,
-}
-
-impl fmt::Display for RadrootsAppUiPreferenceError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- RadrootsAppUiPreferenceError::WindowMissing => {
- write!(f, "preference_window_missing")
- }
- RadrootsAppUiPreferenceError::MatchMediaFailed => {
- write!(f, "preference_match_media_failed")
- }
- }
- }
-}
-
-pub type RadrootsAppUiPreferenceResult<T> = Result<T, RadrootsAppUiPreferenceError>;
-
-#[cfg(target_arch = "wasm32")]
-fn radroots_app_ui_prefers(query: &str) -> RadrootsAppUiPreferenceResult<bool> {
- let window = web_sys::window().ok_or(RadrootsAppUiPreferenceError::WindowMissing)?;
- let media = window
- .match_media(query)
- .map_err(|_| RadrootsAppUiPreferenceError::MatchMediaFailed)?;
- Ok(media.map(|list| list.matches()).unwrap_or(false))
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-fn radroots_app_ui_prefers(_query: &str) -> RadrootsAppUiPreferenceResult<bool> {
- Ok(false)
-}
-
-pub fn radroots_app_ui_prefers_reduced_motion() -> RadrootsAppUiPreferenceResult<bool> {
- radroots_app_ui_prefers("(prefers-reduced-motion: reduce)")
-}
-
-pub fn radroots_app_ui_prefers_contrast_more() -> RadrootsAppUiPreferenceResult<bool> {
- radroots_app_ui_prefers("(prefers-contrast: more)")
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- radroots_app_ui_prefers_contrast_more,
- radroots_app_ui_prefers_reduced_motion,
- };
-
- #[test]
- fn prefers_helpers_return_result() {
- let _ = radroots_app_ui_prefers_reduced_motion().expect("reduced motion");
- let _ = radroots_app_ui_prefers_contrast_more().expect("contrast more");
- }
-}
diff --git a/crates/ui-primitives/Cargo.toml b/crates/ui-primitives/Cargo.toml
@@ -1,14 +0,0 @@
-[package]
-name = "radroots-app-ui-primitives"
-authors = ["Radroots Authors"]
-version.workspace = true
-edition.workspace = true
-license.workspace = true
-rust-version.workspace = true
-
-[lib]
-crate-type = ["rlib"]
-
-[dependencies]
-ui-primitives-core = { package = "headless-primitives-core", path = "../../../../../../vendor/triesap/headless-primitives/crates/headless-primitives-core" }
-ui-primitives-leptos = { package = "headless-primitives-leptos", path = "../../../../../../vendor/triesap/headless-primitives/crates/headless-primitives-leptos" }
diff --git a/crates/ui-primitives/src/lib.rs b/crates/ui-primitives/src/lib.rs
@@ -1,45 +0,0 @@
-#![forbid(unsafe_code)]
-
-pub use ui_primitives_core::dialog::DialogModel;
-pub use ui_primitives_core::roving_focus::{
- roving_focus_action_from_key as radroots_app_ui_roving_focus_action_from_key,
- roving_focus_next_index as radroots_app_ui_roving_focus_next_index,
- RovingFocusAction as RadrootsAppUiRovingFocusAction,
- RovingFocusOrientation as RadrootsAppUiRovingFocusOrientation,
-};
-pub use ui_primitives_leptos::attrs::{
- dialog_content_attrs,
- dialog_trigger_attrs,
-};
-pub use ui_primitives_leptos::{
- dismissible_is_escape as radroots_app_ui_dismissable_is_escape,
- dismissible_is_outside as radroots_app_ui_dismissable_is_outside,
- focus_scope_next_index as radroots_app_ui_focus_scope_next_index,
- focus_scope_selector as radroots_app_ui_focus_scope_selector,
- modal_hide_siblings as radroots_app_ui_modal_hide_siblings,
- modal_restore as radroots_app_ui_modal_restore,
- presence_state_next as radroots_app_ui_presence_state_next,
- scroll_lock_acquire as radroots_app_ui_scroll_lock_acquire,
- scroll_lock_release as radroots_app_ui_scroll_lock_release,
- use_dom_bindings as use_primitive,
- BoundElement as PrimitiveElement,
- DismissibleLayer as RadrootsAppUiDismissableLayer,
- DismissibleReason as RadrootsAppUiDismissableReason,
- DomAttribute as PrimitiveAttribute,
- DomAttributeValue as PrimitiveAttributeValue,
- DomBindingError as PrimitiveError,
- DomBindingResult as PrimitiveResult,
- DomEventBinding as PrimitiveEvent,
- FocusScope as RadrootsAppUiFocusScope,
- ModalError as RadrootsAppUiModalError,
- ModalGuard as RadrootsAppUiModalGuard,
- ModalResult as RadrootsAppUiModalResult,
- ModalTarget as RadrootsAppUiModalTarget,
- Portal as RadrootsAppUiPortal,
- PortalMount as RadrootsAppUiPortalMount,
- Presence as RadrootsAppUiPresence,
- PresenceState as RadrootsAppUiPresenceState,
- ScrollLockError as RadrootsAppUiScrollLockError,
- ScrollLockGuard as RadrootsAppUiScrollLockGuard,
- ScrollLockResult as RadrootsAppUiScrollLockResult,
-};
diff --git a/crates/ui-tokens/Cargo.toml b/crates/ui-tokens/Cargo.toml
@@ -1,12 +0,0 @@
-[package]
-name = "radroots-app-ui-tokens"
-authors = ["Radroots Authors"]
-version.workspace = true
-edition.workspace = true
-license.workspace = true
-rust-version.workspace = true
-
-[lib]
-crate-type = ["rlib"]
-
-[dependencies]
diff --git a/crates/ui-tokens/assets/themes/layout.css b/crates/ui-tokens/assets/themes/layout.css
@@ -1,143 +0,0 @@
-@theme {
- --height-bold_button: 3.75rem;
- --height-entry_line: 48px;
- --height-ios0: 340px;
- --height-ios1: 345px;
- --height-line: 46px;
- --height-line_button: 3.25rem;
- --height-lo_bottom_button_ios0: 90px;
- --height-lo_bottom_button_ios1: 112px;
- --height-lo_view_main_ios0: 22rem;
- --height-lo_view_main_ios1: 28rem;
- --height-nav_page_header_ios0: 62px;
- --height-nav_page_header_ios1: 62px;
- --height-nav_page_toolbar_ios0: 72px;
- --height-nav_page_toolbar_ios1: 120px;
- --height-nav_tabs_ios0: 80px;
- --height-nav_tabs_ios1: 120px;
- --height-touch_guide: 3.4rem;
- --max-height-bold_button: 3.75rem;
- --max-height-entry_line: 48px;
- --max-height-ios0: 340px;
- --max-height-ios1: 345px;
- --max-height-line: 46px;
- --max-height-line_button: 3.25rem;
- --max-height-lo_bottom_button_ios0: 90px;
- --max-height-lo_bottom_button_ios1: 112px;
- --max-height-lo_view_main_ios0: 22rem;
- --max-height-lo_view_main_ios1: 28rem;
- --max-height-nav_page_header_ios0: 62px;
- --max-height-nav_page_header_ios1: 62px;
- --max-height-nav_page_toolbar_ios0: 72px;
- --max-height-nav_page_toolbar_ios1: 120px;
- --max-height-nav_tabs_ios0: 80px;
- --max-height-nav_tabs_ios1: 120px;
- --max-height-touch_guide: 3.4rem;
- --max-width-ios0: 340px;
- --max-width-ios1: 345px;
- --max-width-lo_ios0: 340px;
- --max-width-lo_ios1: 345px;
- --max-width-lo_line_entry_ios0: 349px;
- --max-width-lo_line_entry_ios1: 378px;
- --max-width-lo_textdesc_ios0: 312px;
- --max-width-lo_textdesc_ios1: 312px;
- --max-width-trellis_display: 286px;
- --max-width-trellis_value: 180px;
- --min-height-bold_button: 3.75rem;
- --min-height-entry_line: 48px;
- --min-height-ios0: 340px;
- --min-height-ios1: 345px;
- --min-height-line: 46px;
- --min-height-line_button: 3.25rem;
- --min-height-lo_bottom_button_ios0: 90px;
- --min-height-lo_bottom_button_ios1: 112px;
- --min-height-lo_view_main_ios0: 22rem;
- --min-height-lo_view_main_ios1: 28rem;
- --min-height-nav_page_header_ios0: 62px;
- --min-height-nav_page_header_ios1: 62px;
- --min-height-nav_page_toolbar_ios0: 72px;
- --min-height-nav_page_toolbar_ios1: 120px;
- --min-height-nav_tabs_ios0: 80px;
- --min-height-nav_tabs_ios1: 120px;
- --min-height-touch_guide: 3.4rem;
- --min-width-ios0: 340px;
- --min-width-ios1: 345px;
- --min-width-lo_ios0: 340px;
- --min-width-lo_ios1: 345px;
- --min-width-lo_line_entry_ios0: 349px;
- --min-width-lo_line_entry_ios1: 378px;
- --min-width-lo_textdesc_ios0: 312px;
- --min-width-lo_textdesc_ios1: 312px;
- --min-width-trellis_display: 286px;
- --min-width-trellis_value: 180px;
- --padding-dim_ios0: 340px;
- --padding-dim_ios1: 345px;
- --padding-h_bold_button: 3.75rem;
- --padding-h_entry_line: 48px;
- --padding-h_ios0: 340px;
- --padding-h_ios1: 345px;
- --padding-h_line: 46px;
- --padding-h_line_button: 3.25rem;
- --padding-h_lo_bottom_button_ios0: 90px;
- --padding-h_lo_bottom_button_ios1: 112px;
- --padding-h_lo_view_main_ios0: 22rem;
- --padding-h_lo_view_main_ios1: 28rem;
- --padding-h_nav_page_header_ios0: 62px;
- --padding-h_nav_page_header_ios1: 62px;
- --padding-h_nav_page_toolbar_ios0: 72px;
- --padding-h_nav_page_toolbar_ios1: 120px;
- --padding-h_nav_tabs_ios0: 80px;
- --padding-h_nav_tabs_ios1: 120px;
- --padding-h_touch_guide: 3.4rem;
- --padding-w_ios0: 340px;
- --padding-w_ios1: 345px;
- --padding-w_lo_ios0: 340px;
- --padding-w_lo_ios1: 345px;
- --padding-w_lo_line_entry_ios0: 349px;
- --padding-w_lo_line_entry_ios1: 378px;
- --padding-w_lo_textdesc_ios0: 312px;
- --padding-w_lo_textdesc_ios1: 312px;
- --padding-w_trellis_display: 286px;
- --padding-w_trellis_value: 180px;
- --spacing-dim_ios0: 340px;
- --spacing-dim_ios1: 345px;
- --spacing-edge: 2px;
- --spacing-line: 1px;
- --translate-h_bold_button: 3.75rem;
- --translate-h_entry_line: 48px;
- --translate-h_ios0: 340px;
- --translate-h_ios1: 345px;
- --translate-h_line: 46px;
- --translate-h_line_button: 3.25rem;
- --translate-h_lo_bottom_button_ios0: 90px;
- --translate-h_lo_bottom_button_ios1: 112px;
- --translate-h_lo_view_main_ios0: 22rem;
- --translate-h_lo_view_main_ios1: 28rem;
- --translate-h_nav_page_header_ios0: 62px;
- --translate-h_nav_page_header_ios1: 62px;
- --translate-h_nav_page_toolbar_ios0: 72px;
- --translate-h_nav_page_toolbar_ios1: 120px;
- --translate-h_nav_tabs_ios0: 80px;
- --translate-h_nav_tabs_ios1: 120px;
- --translate-h_touch_guide: 3.4rem;
- --translate-w_ios0: 340px;
- --translate-w_ios1: 345px;
- --translate-w_lo_ios0: 340px;
- --translate-w_lo_ios1: 345px;
- --translate-w_lo_line_entry_ios0: 349px;
- --translate-w_lo_line_entry_ios1: 378px;
- --translate-w_lo_textdesc_ios0: 312px;
- --translate-w_lo_textdesc_ios1: 312px;
- --translate-w_trellis_display: 286px;
- --translate-w_trellis_value: 180px;
- --width-ios0: 340px;
- --width-ios1: 345px;
- --width-lo_ios0: 340px;
- --width-lo_ios1: 345px;
- --width-lo_line_entry_ios0: 349px;
- --width-lo_line_entry_ios1: 378px;
- --width-lo_textdesc_ios0: 312px;
- --width-lo_textdesc_ios1: 312px;
- --width-trellis_display: 286px;
- --width-trellis_value: 180px;
-}
diff --git a/crates/ui-tokens/assets/themes/screens.css b/crates/ui-tokens/assets/themes/screens.css
@@ -1,11 +0,0 @@
-@custom-variant ios1 {
- @media (orientation: portrait) and (min-height: 750px) {
- @slot;
- }
-}
-
-@custom-variant ios0 {
- @media (orientation: portrait) and (max-height: 680px) {
- @slot;
- }
-}
diff --git a/crates/ui-tokens/assets/themes/semantic.css b/crates/ui-tokens/assets/themes/semantic.css
@@ -1,25 +0,0 @@
-:root {
- --bg-app: hsl(var(--ly0) / 1);
- --bg-grouped: hsl(var(--ly1) / 1);
- --bg-elevated: hsl(var(--ly2) / 1);
-
- --text-primary: hsl(var(--ly0-gl) / 1);
- --text-secondary: hsl(var(--ly0-gl-label) / 1);
- --text-tertiary: hsl(var(--ly0-gl-shade) / 1);
-
- --separator: hsl(var(--ly1-edge) / 0.6);
- --stroke: hsl(var(--ly1-edge) / 0.35);
-
- --accent: hsl(var(--ly0-gl-hl) / 1);
- --accent-contrast: hsl(var(--ly1-gl) / 1);
-
- --material-thin: color-mix(in srgb, hsl(var(--ly1) / 1) 55%, transparent);
- --material-regular: color-mix(in srgb, hsl(var(--ly1) / 1) 72%, transparent);
- --material-thick: color-mix(in srgb, hsl(var(--ly1) / 1) 85%, transparent);
- --material-chrome: color-mix(in srgb, hsl(var(--ly0) / 1) 90%, transparent);
-
- --status-ok: var(--success);
- --status-warn: var(--warning);
- --status-error: var(--destructive);
- --status-neutral: var(--text-secondary);
-}
diff --git a/crates/ui-tokens/assets/themes/styles.css b/crates/ui-tokens/assets/themes/styles.css
@@ -1,37 +0,0 @@
-@theme {
- --color-ly0: hsl(var(--ly0) / 1);
- --color-ly0-w: hsl(var(--ly0-w) / 1);
- --color-ly0-a: hsl(var(--ly0-a) / 1);
- --color-ly0-edge: hsl(var(--ly0-edge) / 1);
- --color-ly0-blur: hsl(var(--ly0-blur) / 1);
- --color-ly0-gl: hsl(var(--ly0-gl) / 1);
- --color-ly0-gl-a: hsl(var(--ly0-gl-a) / 1);
- --color-ly0-gl-pl: hsl(var(--ly0-gl-pl) / 1);
- --color-ly0-gl-hl: hsl(var(--ly0-gl-hl) / 1);
- --color-ly0-gl-hl-a: hsl(var(--ly0-gl-hl-a) / 1);
- --color-ly0-gl-shade: hsl(var(--ly0-gl-shade) / 1);
- --color-ly0-gl-label: hsl(var(--ly0-gl-label) / 1);
- --color-ly1: hsl(var(--ly1) / 1);
- --color-ly1-a: hsl(var(--ly1-a) / 1);
- --color-ly1-edge: hsl(var(--ly1-edge) / 1);
- --color-ly1-err: hsl(var(--ly1-err) / 1);
- --color-ly1-focus: hsl(var(--ly1-focus) / 1);
- --color-ly1-gl: hsl(var(--ly1-gl) / 1);
- --color-ly1-gl-a: hsl(var(--ly1-gl-a) / 1);
- --color-ly1-gl-d: hsl(var(--ly1-gl-d) / 1);
- --color-ly1-gl-pl: hsl(var(--ly1-gl-pl) / 1);
- --color-ly1-gl-hl: hsl(var(--ly1-gl-hl) / 1);
- --color-ly1-gl-hl-a: hsl(var(--ly1-gl-hl-a) / 1);
- --color-ly1-gl-shade: hsl(var(--ly1-gl-shade) / 1);
- --color-ly1-gl-label: hsl(var(--ly1-gl-label) / 1);
- --color-ly2: hsl(var(--ly2) / 1);
- --color-ly2-a: hsl(var(--ly2-a) / 1);
- --color-ly2-edge: hsl(var(--ly2-edge) / 1);
- --color-ly2-gl: hsl(var(--ly2-gl) / 1);
- --color-ly2-gl-a: hsl(var(--ly2-gl-a) / 1);
- --color-ly2-gl-d: hsl(var(--ly2-gl-d) / 1);
- --color-ly2-gl-pl: hsl(var(--ly2-gl-pl) / 1);
- --color-ly2-gl-hl: hsl(var(--ly2-gl-hl) / 1);
- --color-ly2-gl-hl-a: hsl(var(--ly2-gl-hl-a) / 1);
- --color-ly2-gl-shade: hsl(var(--ly2-gl-shade) / 1);
-}
diff --git a/crates/ui-tokens/assets/themes/theme_os.css b/crates/ui-tokens/assets/themes/theme_os.css
@@ -1,119 +0,0 @@
-:root,
-:root[data-theme="os_light"] {
- color-scheme: light;
- --ly0: 240 24% 96%;
- --ly0-w: 248 17% 98%;
- --ly0-a: 240 6% 83%;
- --ly0-edge: 0 0% 87%;
- --ly0-blur: 179 7% 96%;
- --ly0-gl: 240 2% 55%;
- --ly0-gl-a: 240 2% 60%;
- --ly0-gl-pl: 240 2% 78%;
- --ly0-gl-hl: 219 92% 59%;
- --ly0-gl-hl-a: 211 100% 40%;
- --ly0-gl-shade: 230 3% 54%;
- --ly0-gl-label: 240 2% 53%;
- --ly1: 0 0% 100%;
- --ly1-a: 240 6% 83%;
- --ly1-edge: 274 4% 90%;
- --ly1-err: 0 0% 0%;
- --ly1-focus: 240 15% 94%;
- --ly1-gl: 0 0% 10%;
- --ly1-gl-a: 0 0% 10%;
- --ly1-gl-d: 0 0% 20%;
- --ly1-gl-pl: 240 2% 78%;
- --ly1-gl-hl: 211 100% 50%;
- --ly1-gl-hl-a: 211 100% 40%;
- --ly1-gl-shade: 240 2% 55%;
- --ly1-gl-label: 240 2% 53%;
- --ly2: 240 5% 90%;
- --ly2-a: 240 5% 95%;
- --ly2-edge: 242 2% 88%;
- --ly2-gl: 240 2% 55%;
- --ly2-gl-a: 240 2% 45%;
- --ly2-gl-d: 240 2% 65%;
- --ly2-gl-pl: 240 2% 78%;
- --ly2-gl-hl: 211 100% 50%;
- --ly2-gl-hl-a: 211 100% 40%;
- --ly2-gl-shade: 240 2% 55%;
-}
-
-@media (prefers-color-scheme: dark) {
- :root:not([data-theme="os_light"]) {
- color-scheme: dark;
- --ly0: 0 0% 7%;
- --ly0-w: 0 0% 7%;
- --ly0-a: 240 2% 23%;
- --ly0-edge: 274 4% 11%;
- --ly0-blur: 0 0% 12%;
- --ly0-gl: 230 3% 56%;
- --ly0-gl-a: 230 3% 51%;
- --ly0-gl-pl: 30 1% 99%;
- --ly0-gl-hl: 210 100% 52%;
- --ly0-gl-hl-a: 210 91% 21%;
- --ly0-gl-shade: 240 3% 57%;
- --ly0-gl-label: 240 3% 55%;
- --ly1: 240 4% 11%;
- --ly1-a: 240 2% 23%;
- --ly1-edge: 240 3% 19%;
- --ly1-err: 0 0% 0%;
- --ly1-focus: 240 4% 20%;
- --ly1-gl: 30 100% 100%;
- --ly1-gl-a: 30 1% 90%;
- --ly1-gl-d: 240 1% 82%;
- --ly1-gl-pl: 30 1% 99%;
- --ly1-gl-hl: 210 100% 52%;
- --ly1-gl-hl-a: 210 100% 62%;
- --ly1-gl-shade: 230 4% 61%;
- --ly1-gl-label: 30 1% 99%;
- --ly2: 240 2% 18%;
- --ly2-a: 240 3% 15%;
- --ly2-edge: 240 2% 23%;
- --ly2-gl: 240 3% 73%;
- --ly2-gl-a: 230 4% 51%;
- --ly2-gl-d: 240 3% 63%;
- --ly2-gl-pl: 240 2% 40%;
- --ly2-gl-hl: 210 100% 52%;
- --ly2-gl-hl-a: 210 100% 42%;
- --ly2-gl-shade: 230 4% 61%;
- }
-}
-
-:root[data-theme="os_dark"] {
- color-scheme: dark;
- --ly0: 0 0% 7%;
- --ly0-w: 0 0% 7%;
- --ly0-a: 240 2% 23%;
- --ly0-edge: 274 4% 11%;
- --ly0-blur: 0 0% 12%;
- --ly0-gl: 230 3% 56%;
- --ly0-gl-a: 230 3% 51%;
- --ly0-gl-pl: 30 1% 99%;
- --ly0-gl-hl: 210 100% 52%;
- --ly0-gl-hl-a: 210 91% 21%;
- --ly0-gl-shade: 240 3% 57%;
- --ly0-gl-label: 240 3% 55%;
- --ly1: 240 4% 11%;
- --ly1-a: 240 2% 23%;
- --ly1-edge: 240 3% 19%;
- --ly1-err: 0 0% 0%;
- --ly1-focus: 240 4% 20%;
- --ly1-gl: 30 100% 100%;
- --ly1-gl-a: 30 1% 90%;
- --ly1-gl-d: 240 1% 82%;
- --ly1-gl-pl: 30 1% 99%;
- --ly1-gl-hl: 210 100% 52%;
- --ly1-gl-hl-a: 210 100% 62%;
- --ly1-gl-shade: 230 4% 61%;
- --ly1-gl-label: 30 1% 99%;
- --ly2: 240 2% 18%;
- --ly2-a: 240 3% 15%;
- --ly2-edge: 240 2% 23%;
- --ly2-gl: 240 3% 73%;
- --ly2-gl-a: 230 4% 51%;
- --ly2-gl-d: 240 3% 63%;
- --ly2-gl-pl: 240 2% 40%;
- --ly2-gl-hl: 210 100% 52%;
- --ly2-gl-hl-a: 210 100% 42%;
- --ly2-gl-shade: 230 4% 61%;
-}
diff --git a/crates/ui-tokens/assets/tokens.css b/crates/ui-tokens/assets/tokens.css
@@ -1,64 +0,0 @@
-:root {
- --font-sans: "SF Pro Rounded", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
- --font-sansd: "SF Pro Display", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
-
- --destructive: #ff3b30;
- --warning: #ff9500;
- --success: #34c759;
-
- --type-largeTitle: 34px/41px var(--font-sans);
- --type-title1: 28px/34px var(--font-sans);
- --type-title2: 22px/28px var(--font-sans);
- --type-title3: 20px/25px var(--font-sans);
- --type-headline: 17px/22px var(--font-sans);
- --type-body: 17px/22px var(--font-sans);
- --type-callout: 16px/21px var(--font-sans);
- --type-subheadline: 15px/20px var(--font-sans);
- --type-footnote: 13px/18px var(--font-sans);
- --type-caption1: 12px/16px var(--font-sans);
- --type-caption2: 11px/13px var(--font-sans);
-
- --space-1: 4px;
- --space-2: 8px;
- --space-3: 12px;
- --space-4: 16px;
- --space-5: 20px;
- --space-6: 24px;
- --space-7: 32px;
- --space-8: 40px;
-
- --size-line: 46px;
- --size-line-button: 52px;
- --size-touch-guide: 54px;
- --size-trellis-display: 286px;
- --size-trellis-value: 180px;
- --size-trellis-offset: 32px;
-
- --radius-sm: 8px;
- --radius-md: 12px;
- --radius-lg: 16px;
- --radius-xl: 20px;
-
- --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.06), 0 3px 8px rgba(0, 0, 0, 0.08);
- --shadow-2: 0 8px 20px rgba(0, 0, 0, 0.12);
- --shadow-popover: 0 12px 30px rgba(0, 0, 0, 0.18);
- --shadow-sheet: 0 -8px 30px rgba(0, 0, 0, 0.18);
- --shadow-press: 0 1px 2px rgba(0, 0, 0, 0.08);
- --opacity-press: 0.92;
-
- --dur-1: 120ms;
- --dur-2: 180ms;
- --dur-3: 240ms;
- --ease-ios: cubic-bezier(0.32, 0.72, 0, 1);
-
- --safe-b: env(safe-area-inset-bottom);
- --safe-t: env(safe-area-inset-top);
-}
-
-@media (prefers-reduced-motion: reduce) {
- :root {
- --dur-1: 1ms;
- --dur-2: 1ms;
- --dur-3: 1ms;
- }
-}
diff --git a/crates/ui-tokens/src/lib.rs b/crates/ui-tokens/src/lib.rs
@@ -1,21 +0,0 @@
-#![forbid(unsafe_code)]
-
-pub const RADROOTS_APP_UI_TOKENS_CSS: &str = include_str!("../assets/tokens.css");
-
-pub struct RadrootsAppUiTokens;
-
-impl RadrootsAppUiTokens {
- pub const fn css() -> &'static str {
- RADROOTS_APP_UI_TOKENS_CSS
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::RADROOTS_APP_UI_TOKENS_CSS;
-
- #[test]
- fn tokens_css_is_not_empty() {
- assert!(!RADROOTS_APP_UI_TOKENS_CSS.trim().is_empty());
- }
-}
diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml
@@ -1,29 +0,0 @@
-[package]
-name = "radroots-app-utils"
-authors = ["Radroots Authors"]
-version.workspace = true
-edition.workspace = true
-license.workspace = true
-rust-version.workspace = true
-
-[lib]
-crate-type = ["rlib"]
-
-[dependencies]
-radroots-types = { workspace = true }
-getrandom = { workspace = true }
-js-sys = { workspace = true }
-web-sys = { workspace = true }
-serde_json = { workspace = true }
-uuid = { workspace = true }
-base64 = { workspace = true }
-regex = { workspace = true }
-once_cell = { workspace = true }
-
-[target.'cfg(target_arch = "wasm32")'.dependencies]
-gloo-timers = { workspace = true }
-wasm-bindgen = { workspace = true }
-wasm-bindgen-futures = { workspace = true }
-
-[dev-dependencies]
-futures = { workspace = true }
diff --git a/crates/utils/src/async/mod.rs b/crates/utils/src/async/mod.rs
@@ -1,64 +0,0 @@
-#![forbid(unsafe_code)]
-
-use crate::error::RadrootsAppUtilsError;
-use std::future::Future;
-#[cfg(not(target_arch = "wasm32"))]
-use std::time::Duration;
-
-pub async fn exe_iter<F, Fut>(
- callback: F,
- num: usize,
- delay_ms: u64,
-) -> Result<(), RadrootsAppUtilsError>
-where
- F: Fn() -> Fut,
- Fut: Future<Output = ()>,
-{
- if num == 0 {
- return Ok(());
- }
- for index in 0..num {
- callback().await;
- if index + 1 < num {
- sleep_ms(delay_ms).await?;
- }
- }
- Ok(())
-}
-
-#[cfg(target_arch = "wasm32")]
-async fn sleep_ms(delay_ms: u64) -> Result<(), RadrootsAppUtilsError> {
- gloo_timers::future::TimeoutFuture::new(delay_ms as u32).await;
- Ok(())
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-async fn sleep_ms(delay_ms: u64) -> Result<(), RadrootsAppUtilsError> {
- std::thread::sleep(Duration::from_millis(delay_ms));
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::exe_iter;
- use std::sync::{Arc, Mutex};
-
- #[test]
- fn exe_iter_runs_callback() {
- let counter = Arc::new(Mutex::new(0usize));
- let counter_ref = Arc::clone(&counter);
- let task = exe_iter(
- move || {
- let counter_ref = Arc::clone(&counter_ref);
- async move {
- let mut guard = counter_ref.lock().expect("lock");
- *guard += 1;
- }
- },
- 3,
- 0,
- );
- futures::executor::block_on(task).expect("exe_iter");
- assert_eq!(*counter.lock().expect("lock"), 3);
- }
-}
diff --git a/crates/utils/src/binary/mod.rs b/crates/utils/src/binary/mod.rs
@@ -1,32 +0,0 @@
-#![forbid(unsafe_code)]
-
-#[cfg(target_arch = "wasm32")]
-pub type RadrootsAppArrayBuffer = js_sys::ArrayBuffer;
-
-#[cfg(not(target_arch = "wasm32"))]
-pub type RadrootsAppArrayBuffer = Vec<u8>;
-
-pub fn as_array_buffer(bytes: &[u8]) -> RadrootsAppArrayBuffer {
- #[cfg(target_arch = "wasm32")]
- {
- let array = js_sys::Uint8Array::from(bytes);
- array.buffer()
- }
- #[cfg(not(target_arch = "wasm32"))]
- {
- bytes.to_vec()
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::as_array_buffer;
-
- #[test]
- #[cfg(not(target_arch = "wasm32"))]
- fn as_array_buffer_clones_bytes() {
- let bytes = vec![1u8, 2u8, 3u8];
- let buffer = as_array_buffer(&bytes);
- assert_eq!(buffer, bytes);
- }
-}
diff --git a/crates/utils/src/cache/mod.rs b/crates/utils/src/cache/mod.rs
@@ -1,224 +0,0 @@
-#![forbid(unsafe_code)]
-
-use crate::error::RadrootsAppUtilsError;
-
-pub const RADROOTS_ASSET_CACHE_NAME: &str = "cache-app-assets-v1";
-pub const RADROOTS_ASSET_CACHE_PREFIX: &str = "cache-app-assets-v";
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum AssetCacheMode {
- Default,
- NoStore,
- Reload,
- NoCache,
- ForceCache,
- OnlyIfCached,
-}
-
-#[derive(Debug, Clone)]
-pub struct AssetCacheRequestInit {
- pub cache: Option<AssetCacheMode>,
-}
-
-#[derive(Debug, Clone)]
-pub struct AssetCacheFetchConfig {
- pub cache_name: Option<String>,
- pub request_init: Option<AssetCacheRequestInit>,
-}
-
-#[cfg(target_arch = "wasm32")]
-pub type AssetResponse = web_sys::Response;
-
-#[cfg(not(target_arch = "wasm32"))]
-pub type AssetResponse = ();
-
-pub type AssetBytes = Vec<u8>;
-
-pub async fn asset_cache_fetch(
- url: &str,
- config: Option<&AssetCacheFetchConfig>,
-) -> Result<AssetResponse, RadrootsAppUtilsError> {
- asset_cache_fetch_impl(url, config).await
-}
-
-pub async fn asset_cache_fetch_bytes(
- url: &str,
- config: Option<&AssetCacheFetchConfig>,
-) -> Result<Option<AssetBytes>, RadrootsAppUtilsError> {
- #[cfg(target_arch = "wasm32")]
- {
- let response = asset_cache_fetch(url, config).await?;
- if !response.ok() {
- return Ok(None);
- }
- let promise = response
- .array_buffer()
- .map_err(|_| RadrootsAppUtilsError::Unavailable)?;
- let buffer = wasm_bindgen_futures::JsFuture::from(promise)
- .await
- .map_err(|_| RadrootsAppUtilsError::Unavailable)?;
- let array = js_sys::Uint8Array::new(&buffer);
- let mut bytes = vec![0u8; array.length() as usize];
- array.copy_to(&mut bytes);
- Ok(Some(bytes))
- }
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = url;
- let _ = config;
- Err(RadrootsAppUtilsError::Unavailable)
- }
-}
-
-#[cfg(any(test, target_arch = "wasm32"))]
-fn cache_name_resolve(config: Option<&AssetCacheFetchConfig>) -> String {
- config
- .and_then(|config| config.cache_name.as_ref().cloned())
- .unwrap_or_else(|| RADROOTS_ASSET_CACHE_NAME.to_string())
-}
-
-#[cfg(any(test, target_arch = "wasm32"))]
-fn cache_key_resolve(url: &str) -> String {
- url.split('#').next().unwrap_or(url).to_string()
-}
-
-#[cfg(target_arch = "wasm32")]
-async fn asset_cache_fetch_impl(
- url: &str,
- config: Option<&AssetCacheFetchConfig>,
-) -> Result<AssetResponse, RadrootsAppUtilsError> {
- let cache_name = cache_name_resolve(config);
- let cache_key = cache_key_resolve(url);
- if let Some(cached) = cache_read(&cache_name, &cache_key).await? {
- return Ok(cached);
- }
- let response = fetch_with_init(url, config).await?;
- if response.ok() || response.type_() == web_sys::ResponseType::Opaque {
- let response = response
- .clone()
- .map_err(|_| RadrootsAppUtilsError::Unavailable)?;
- cache_write(&cache_name, &cache_key, response).await?;
- }
- Ok(response)
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-async fn asset_cache_fetch_impl(
- _url: &str,
- _config: Option<&AssetCacheFetchConfig>,
-) -> Result<AssetResponse, RadrootsAppUtilsError> {
- Err(RadrootsAppUtilsError::Unavailable)
-}
-
-#[cfg(target_arch = "wasm32")]
-async fn fetch_with_init(
- url: &str,
- config: Option<&AssetCacheFetchConfig>,
-) -> Result<AssetResponse, RadrootsAppUtilsError> {
- use wasm_bindgen::JsCast;
-
- let window = web_sys::window().ok_or(RadrootsAppUtilsError::Unavailable)?;
- let init = web_sys::RequestInit::new();
- if let Some(request_init) = config.and_then(|config| config.request_init.as_ref()) {
- if let Some(cache_mode) = request_init.cache {
- init.set_cache(cache_mode.to_request_cache());
- }
- }
- let response = wasm_bindgen_futures::JsFuture::from(window.fetch_with_str_and_init(url, &init))
- .await
- .map_err(|_| RadrootsAppUtilsError::Unavailable)?;
- response
- .dyn_into::<web_sys::Response>()
- .map_err(|_| RadrootsAppUtilsError::Unavailable)
-}
-
-#[cfg(target_arch = "wasm32")]
-async fn cache_read(
- cache_name: &str,
- cache_key: &str,
-) -> Result<Option<AssetResponse>, RadrootsAppUtilsError> {
- use wasm_bindgen::JsCast;
-
- let window = web_sys::window().ok_or(RadrootsAppUtilsError::Unavailable)?;
- let storage = window
- .caches()
- .map_err(|_| RadrootsAppUtilsError::Unavailable)?;
- let cache = wasm_bindgen_futures::JsFuture::from(storage.open(cache_name))
- .await
- .map_err(|_| RadrootsAppUtilsError::Unavailable)?;
- let cache = cache
- .dyn_into::<web_sys::Cache>()
- .map_err(|_| RadrootsAppUtilsError::Unavailable)?;
- let cached = wasm_bindgen_futures::JsFuture::from(cache.match_with_str(cache_key))
- .await
- .map_err(|_| RadrootsAppUtilsError::Unavailable)?;
- if cached.is_undefined() || cached.is_null() {
- return Ok(None);
- }
- let response = cached
- .dyn_into::<web_sys::Response>()
- .map_err(|_| RadrootsAppUtilsError::Unavailable)?;
- Ok(Some(response))
-}
-
-#[cfg(target_arch = "wasm32")]
-async fn cache_write(
- cache_name: &str,
- cache_key: &str,
- response: AssetResponse,
-) -> Result<(), RadrootsAppUtilsError> {
- use wasm_bindgen::JsCast;
-
- let window = web_sys::window().ok_or(RadrootsAppUtilsError::Unavailable)?;
- let storage = window
- .caches()
- .map_err(|_| RadrootsAppUtilsError::Unavailable)?;
- let cache = wasm_bindgen_futures::JsFuture::from(storage.open(cache_name))
- .await
- .map_err(|_| RadrootsAppUtilsError::Unavailable)?;
- let cache = cache
- .dyn_into::<web_sys::Cache>()
- .map_err(|_| RadrootsAppUtilsError::Unavailable)?;
- wasm_bindgen_futures::JsFuture::from(cache.put_with_str(cache_key, &response))
- .await
- .map_err(|_| RadrootsAppUtilsError::Unavailable)?;
- Ok(())
-}
-
-impl AssetCacheMode {
- #[cfg(target_arch = "wasm32")]
- fn to_request_cache(self) -> web_sys::RequestCache {
- match self {
- AssetCacheMode::Default => web_sys::RequestCache::Default,
- AssetCacheMode::NoStore => web_sys::RequestCache::NoStore,
- AssetCacheMode::Reload => web_sys::RequestCache::Reload,
- AssetCacheMode::NoCache => web_sys::RequestCache::NoCache,
- AssetCacheMode::ForceCache => web_sys::RequestCache::ForceCache,
- AssetCacheMode::OnlyIfCached => web_sys::RequestCache::OnlyIfCached,
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{cache_key_resolve, cache_name_resolve, AssetCacheFetchConfig, RADROOTS_ASSET_CACHE_NAME};
-
- #[test]
- fn cache_name_defaults() {
- assert_eq!(cache_name_resolve(None), RADROOTS_ASSET_CACHE_NAME);
- }
-
- #[test]
- fn cache_name_uses_config() {
- let config = AssetCacheFetchConfig {
- cache_name: Some("custom".to_string()),
- request_init: None,
- };
- assert_eq!(cache_name_resolve(Some(&config)), "custom");
- }
-
- #[test]
- fn cache_key_strips_hash() {
- assert_eq!(cache_key_resolve("path#hash"), "path");
- }
-}
diff --git a/crates/utils/src/currency/mod.rs b/crates/utils/src/currency/mod.rs
@@ -1,131 +0,0 @@
-#![forbid(unsafe_code)]
-
-use crate::numbers::parse_float;
-use crate::validation::regex::UtilRegex;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum FiatCurrency {
- Usd,
- Eur,
-}
-
-impl FiatCurrency {
- pub const fn as_str(self) -> &'static str {
- match self {
- FiatCurrency::Usd => "usd",
- FiatCurrency::Eur => "eur",
- }
- }
-
- pub const fn as_upper(self) -> &'static str {
- match self {
- FiatCurrency::Usd => "USD",
- FiatCurrency::Eur => "EUR",
- }
- }
-}
-
-pub const FIAT_CURRENCIES: [FiatCurrency; 2] = [FiatCurrency::Usd, FiatCurrency::Eur];
-
-pub fn price_to_formatted(value: f64) -> f64 {
- (value * 100.0).round() / 100.0
-}
-
-pub fn parse_currency(val: Option<&str>) -> FiatCurrency {
- match val.map(|value| value.trim().to_lowercase()) {
- Some(value) if value == "eur" => FiatCurrency::Eur,
- _ => FiatCurrency::Usd,
- }
-}
-
-pub fn fmt_price(locale: &str, value: &str, currency: &str) -> String {
- let value = parse_float(value, 0.0);
- let currency = parse_currency(Some(currency));
- fmt_price_value(locale, value, currency)
-}
-
-pub fn parse_currency_marker(locale: &str, currency: &str) -> String {
- let currency = parse_currency(Some(currency));
- let formatted = fmt_price_value(locale, 1.0, currency);
- if let Some(match_value) = UtilRegex::currency_marker().find(&formatted) {
- return match_value.as_str().to_string();
- }
- if let Some(match_value) = UtilRegex::currency_symbol().find(&formatted) {
- return match_value.as_str().to_string();
- }
- if let Some(match_value) = formatted
- .find(currency.as_upper())
- .map(|start| &formatted[start..start + currency.as_upper().len()])
- {
- return match_value.to_string();
- }
- currency.as_upper().to_string()
-}
-
-#[cfg(target_arch = "wasm32")]
-fn fmt_price_value(locale: &str, value: f64, currency: FiatCurrency) -> String {
- use js_sys::{Array, Object, Reflect};
- use wasm_bindgen::JsValue;
-
- let locales = Array::new();
- locales.push(&JsValue::from_str(locale));
- let options = Object::new();
- let currency_upper = currency.as_upper();
- let _ = Reflect::set(&options, &JsValue::from_str("style"), &JsValue::from_str("currency"));
- let _ = Reflect::set(
- &options,
- &JsValue::from_str("currency"),
- &JsValue::from_str(currency_upper),
- );
- let _ = Reflect::set(
- &options,
- &JsValue::from_str("minimumFractionDigits"),
- &JsValue::from_f64(2.0),
- );
- let _ = Reflect::set(
- &options,
- &JsValue::from_str("maximumFractionDigits"),
- &JsValue::from_f64(2.0),
- );
- let formatter = js_sys::Intl::NumberFormat::new(&locales, &options);
- let formatted = formatter
- .format()
- .call1(&JsValue::NULL, &JsValue::from_f64(value))
- .ok()
- .and_then(|value| value.as_string());
- formatted.unwrap_or_else(|| format!("{} {:.2}", currency.as_upper(), value))
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-fn fmt_price_value(_locale: &str, value: f64, currency: FiatCurrency) -> String {
- format!("{} {:.2}", currency.as_upper(), value)
-}
-
-#[cfg(test)]
-mod tests {
- use super::{fmt_price, parse_currency, parse_currency_marker, price_to_formatted, FiatCurrency};
-
- #[test]
- fn price_to_formatted_rounds() {
- assert_eq!(price_to_formatted(1.234), 1.23);
- }
-
- #[test]
- fn parse_currency_defaults() {
- assert_eq!(parse_currency(Some("usd")), FiatCurrency::Usd);
- assert_eq!(parse_currency(Some("eur")), FiatCurrency::Eur);
- assert_eq!(parse_currency(None), FiatCurrency::Usd);
- }
-
- #[test]
- fn fmt_price_formats_value() {
- let formatted = fmt_price("en-US", "1.25", "usd");
- assert!(formatted.contains("USD"));
- }
-
- #[test]
- fn parse_currency_marker_returns_token() {
- let marker = parse_currency_marker("en-US", "usd");
- assert!(!marker.is_empty());
- }
-}
diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs
@@ -1,51 +0,0 @@
-#![forbid(unsafe_code)]
-
-use std::fmt;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppUtilsError {
- InvalidInput,
- Unavailable,
-}
-
-pub type RadrootsAppUtilsErrorMessage = &'static str;
-
-impl RadrootsAppUtilsError {
- pub const fn message(self) -> RadrootsAppUtilsErrorMessage {
- match self {
- RadrootsAppUtilsError::InvalidInput => "error.app.utils.invalid_input",
- RadrootsAppUtilsError::Unavailable => "error.app.utils.unavailable",
- }
- }
-}
-
-impl fmt::Display for RadrootsAppUtilsError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.write_str(self.message())
- }
-}
-
-impl std::error::Error for RadrootsAppUtilsError {}
-
-#[cfg(test)]
-mod tests {
- use super::RadrootsAppUtilsError;
-
- #[test]
- fn message_matches_spec() {
- let cases = [
- (
- RadrootsAppUtilsError::InvalidInput,
- "error.app.utils.invalid_input",
- ),
- (
- RadrootsAppUtilsError::Unavailable,
- "error.app.utils.unavailable",
- ),
- ];
- for (err, expected) in cases {
- assert_eq!(err.message(), expected);
- assert_eq!(err.to_string(), expected);
- }
- }
-}
diff --git a/crates/utils/src/errors.rs b/crates/utils/src/errors.rs
@@ -1,101 +0,0 @@
-#![forbid(unsafe_code)]
-
-use std::fmt;
-
-use radroots_types::types::IError;
-
-pub const ERR_PREFIX_APP: &str = "error.app";
-pub const ERR_PREFIX_UTILS: &str = "error.app.utils";
-
-pub enum ErrInput {
- Message(String),
- Error(IError<String>),
-}
-
-impl From<String> for ErrInput {
- fn from(value: String) -> Self {
- ErrInput::Message(value)
- }
-}
-
-impl From<&str> for ErrInput {
- fn from(value: &str) -> Self {
- ErrInput::Message(value.to_string())
- }
-}
-
-impl From<IError<String>> for ErrInput {
- fn from(value: IError<String>) -> Self {
- ErrInput::Error(value)
- }
-}
-
-pub fn err_msg(err: impl Into<ErrInput>) -> IError<String> {
- match err.into() {
- ErrInput::Message(err) => IError { err },
- ErrInput::Error(err) => err,
- }
-}
-
-pub fn throw_err(err: impl Into<ErrInput>) -> ! {
- let err = err_msg(err);
- panic!("{}", err.err);
-}
-
-pub fn handle_err(err: impl fmt::Display, append: Option<&str>) -> IError<String> {
- let mut msg = err.to_string();
- if let Some(append) = append {
- if !append.is_empty() {
- msg = format!("{msg} {append}");
- }
- }
- IError { err: msg }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{err_msg, handle_err, throw_err, ERR_PREFIX_APP, ERR_PREFIX_UTILS};
- use radroots_types::types::IError;
-
- #[test]
- fn err_msg_wraps_string() {
- let err = err_msg("boom");
- assert_eq!(err.err, "boom");
- }
-
- #[test]
- fn err_msg_accepts_error() {
- let err = err_msg(IError { err: "boom".to_string() });
- assert_eq!(err.err, "boom");
- }
-
- #[test]
- #[should_panic(expected = "boom")]
- fn throw_err_panics_with_string() {
- throw_err("boom");
- }
-
- #[test]
- #[should_panic(expected = "boom")]
- fn throw_err_panics_with_error() {
- throw_err(IError { err: "boom".to_string() });
- }
-
- #[test]
- fn handle_err_adds_append() {
- let err = handle_err("boom", Some("context"));
- assert_eq!(err.err, "boom context");
- }
-
- #[test]
- fn handle_err_without_append() {
- let err = handle_err("boom", None);
- assert_eq!(err.err, "boom");
- }
-
- #[test]
- fn error_prefixes_match_spec() {
- assert_eq!(ERR_PREFIX_APP, "error.app");
- assert_eq!(ERR_PREFIX_UTILS, "error.app.utils");
- }
-}
diff --git a/crates/utils/src/id/mod.rs b/crates/utils/src/id/mod.rs
@@ -1,55 +0,0 @@
-#![forbid(unsafe_code)]
-
-use base64::engine::general_purpose::URL_SAFE_NO_PAD;
-use base64::Engine;
-use uuid::Uuid;
-
-pub fn uuidv4() -> String {
- Uuid::new_v4().to_string()
-}
-
-pub fn uuidv7() -> String {
- Uuid::now_v7().to_string()
-}
-
-pub fn uuidv4_b64url() -> String {
- URL_SAFE_NO_PAD.encode(Uuid::new_v4().as_bytes())
-}
-
-pub fn uuidv7_b64url() -> String {
- URL_SAFE_NO_PAD.encode(Uuid::now_v7().as_bytes())
-}
-
-pub fn d_tag_create() -> String {
- uuidv7_b64url()
-}
-
-#[cfg(test)]
-mod tests {
- use super::{d_tag_create, uuidv4, uuidv4_b64url, uuidv7, uuidv7_b64url};
-
- #[test]
- fn uuidv4_has_expected_length() {
- assert_eq!(uuidv4().len(), 36);
- }
-
- #[test]
- fn uuidv7_has_expected_length() {
- assert_eq!(uuidv7().len(), 36);
- }
-
- #[test]
- fn uuidv4_b64url_has_expected_length() {
- assert_eq!(uuidv4_b64url().len(), 22);
- }
-
- #[test]
- fn uuidv7_b64url_has_expected_length() {
- assert_eq!(uuidv7_b64url().len(), 22);
- }
-
- #[test]
- fn d_tag_create_uses_uuidv7_b64url() {
- assert_eq!(d_tag_create().len(), 22);
- }
-}
diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs
@@ -1,65 +0,0 @@
-#![forbid(unsafe_code)]
-
-pub mod error;
-pub mod errors;
-pub mod r#async;
-pub mod binary;
-pub mod cache;
-pub mod currency;
-pub mod id;
-pub mod media;
-pub mod model;
-pub mod numbers;
-pub mod object;
-pub mod path;
-pub mod text;
-pub mod time;
-pub mod types;
-pub mod unit;
-pub mod validation;
-
-pub use r#async::exe_iter;
-pub use binary::{as_array_buffer, RadrootsAppArrayBuffer};
-pub use cache::{
- asset_cache_fetch, asset_cache_fetch_bytes, AssetBytes, AssetCacheFetchConfig, AssetCacheMode,
- AssetCacheRequestInit, AssetResponse, RADROOTS_ASSET_CACHE_NAME, RADROOTS_ASSET_CACHE_PREFIX,
-};
-pub use currency::{
- fmt_price, parse_currency, parse_currency_marker, price_to_formatted, FiatCurrency,
- FIAT_CURRENCIES,
-};
-pub use id::{d_tag_create, uuidv4, uuidv4_b64url, uuidv7, uuidv7_b64url};
-pub use media::{fmt_media_image_upload_result_url, MediaImageUploadResult, MediaResource};
-pub use model::{
- is_model_query_filter_option, is_model_query_filter_option_list, is_model_query_values,
- list_model_query_values_assert, parse_model_filter_map, parse_model_query_value, ModelForm,
- ModelFormErrorTuple, ModelFormValidationTuple, ModelQueryBindValue, ModelQueryBindValueOpt,
- ModelQueryBindValueTuple, ModelQueryFilter, ModelQueryFilterCondition, ModelQueryFilterMap,
- ModelQueryFilterMapParsed, ModelQueryFilterOption, ModelQueryFilterOptionList, ModelQueryParam,
- ModelQueryValue, ModelSchemaErrors, ModelSortCreatedAt,
-};
-pub use errors::{err_msg, handle_err, throw_err, ERR_PREFIX_APP, ERR_PREFIX_UTILS};
-pub use numbers::{num_interval_range, num_str, parse_float, parse_int};
-pub use object::{obj_en, obj_result, obj_results_str, obj_truthy_fields};
-pub use path::{
- parse_route_path,
- resolve_route_path,
- resolve_wasm_path,
- RadrootsAppRoutePathParts,
-};
-pub use text::{str_cap, str_cap_words, text_dec, text_enc, ROOT_SYMBOL};
-pub use time::{time_now_ms, time_now_s};
-pub use types::{
- is_error, resolve_err, resolve_ok, FileBytesFormat, FilePath, FilePathBlob, FileMimeType,
- IdbClientConfig, ResolveError, ResultBool, ResultId, ResultObj, ResultPass, ResultPublicKey,
- ResultSecretKey, ResultsList, ValidationRegex, ValStr, WebFilePath,
-};
-pub use unit::{
- mass_to_g, parse_area_unit, parse_area_unit_default, parse_mass_unit, parse_mass_unit_default,
- AreaUnit, MassUnit, AREA_UNITS, MASS_UNITS,
-};
-pub use validation::regex::{form_fields, FormField, FormFieldsKey, UtilRegex};
-pub use validation::schema::{
- zf_area_unit, zf_email, zf_mass_unit, zf_numf_pos, zf_numi_pos, zf_price, zf_price_amount,
- zf_quantity_amount, zf_username,
-};
diff --git a/crates/utils/src/media/mod.rs b/crates/utils/src/media/mod.rs
@@ -1,40 +0,0 @@
-#![forbid(unsafe_code)]
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct MediaResource {
- pub base_url: String,
- pub hash: String,
- pub ext: String,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct MediaImageUploadResult {
- pub base_url: String,
- pub file_hash: String,
- pub file_ext: String,
-}
-
-pub fn fmt_media_image_upload_result_url(result: &MediaImageUploadResult) -> String {
- format!(
- "{}/{}.{}",
- result.base_url, result.file_hash, result.file_ext
- )
-}
-
-#[cfg(test)]
-mod tests {
- use super::{fmt_media_image_upload_result_url, MediaImageUploadResult};
-
- #[test]
- fn fmt_media_url_builds_path() {
- let result = MediaImageUploadResult {
- base_url: "https://example.com".to_string(),
- file_hash: "hash".to_string(),
- file_ext: "png".to_string(),
- };
- assert_eq!(
- fmt_media_image_upload_result_url(&result),
- "https://example.com/hash.png"
- );
- }
-}
diff --git a/crates/utils/src/model/mod.rs b/crates/utils/src/model/mod.rs
@@ -1,341 +0,0 @@
-#![forbid(unsafe_code)]
-
-use crate::types::ValidationRegex;
-use crate::error::RadrootsAppUtilsError;
-use std::collections::BTreeMap;
-
-#[derive(Debug, Clone, PartialEq)]
-pub enum ModelQueryValue {
- String(String),
- Number(f64),
- Bool(bool),
- Null,
-}
-
-#[derive(Debug, Clone, PartialEq)]
-pub enum ModelQueryBindValue {
- String(String),
- Number(f64),
- Null,
-}
-
-pub type ModelQueryBindValueTuple = (String, ModelQueryValue);
-pub type ModelQueryBindValueOpt = Option<ModelQueryBindValue>;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum ModelQueryFilterOption {
- Equals,
- StartsWith,
- EndsWith,
- Contains,
- NotEquals,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum ModelQueryFilterOptionList {
- Between,
- In,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum ModelQueryFilterCondition {
- And,
- Or,
- Not,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum ModelSortCreatedAt {
- Newest,
- Oldest,
-}
-
-#[derive(Debug, Clone, PartialEq)]
-pub struct ModelQueryParam {
- pub query: String,
- pub bind_values: Vec<ModelQueryBindValue>,
-}
-
-pub type ModelFormErrorTuple = (bool, String);
-pub type ModelFormValidationTuple = (String, String);
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct ModelSchemaErrors {
- pub err_s: Vec<String>,
-}
-
-#[derive(Debug, Clone)]
-pub struct ModelForm {
- pub label: Option<String>,
- pub placeholder: Option<String>,
- pub validate_keypress: Option<bool>,
- pub prevent_focus_rest: Option<bool>,
- pub hidden: Option<bool>,
- pub optional: Option<bool>,
- pub default: Option<ModelQueryValue>,
- pub rxpv: ValidationRegex,
-}
-
-#[derive(Debug, Clone, PartialEq)]
-pub enum ModelQueryFilter {
- Value(ModelQueryValue),
- Single {
- value: ModelQueryValue,
- option: ModelQueryFilterOption,
- condition: Option<ModelQueryFilterCondition>,
- },
- List {
- values: Vec<ModelQueryValue>,
- option: ModelQueryFilterOptionList,
- condition: Option<ModelQueryFilterCondition>,
- },
-}
-
-#[derive(Debug, Clone, PartialEq)]
-pub struct ModelQueryFilterMapParsed {
- pub query_values: Vec<String>,
- pub bind_values: Vec<ModelQueryBindValue>,
-}
-
-pub type ModelQueryFilterMap = BTreeMap<String, ModelQueryFilter>;
-
-pub fn parse_model_query_value(value: &ModelQueryValue) -> ModelQueryBindValue {
- match value {
- ModelQueryValue::Bool(true) => ModelQueryBindValue::String("1".to_string()),
- ModelQueryValue::Bool(false) => ModelQueryBindValue::String("0".to_string()),
- ModelQueryValue::Number(value) => ModelQueryBindValue::Number(*value),
- ModelQueryValue::String(value) => {
- if value.is_empty() {
- ModelQueryBindValue::Null
- } else {
- ModelQueryBindValue::String(value.clone())
- }
- }
- ModelQueryValue::Null => ModelQueryBindValue::Null,
- }
-}
-
-pub fn is_model_query_filter_option(value: &str) -> bool {
- matches!(
- value,
- "equals" | "starts-with" | "ends-with" | "contains" | "ne"
- )
-}
-
-pub fn is_model_query_filter_option_list(value: &str) -> bool {
- matches!(value, "between" | "in")
-}
-
-pub fn is_model_query_values(value: &ModelQueryValue) -> bool {
- !matches!(value, ModelQueryValue::Null)
-}
-
-pub fn list_model_query_values_assert(values: &[Option<ModelQueryValue>]) -> Vec<ModelQueryValue> {
- values.iter().filter_map(|value| value.clone()).collect()
-}
-
-pub fn parse_model_filter_map(
- filters: &ModelQueryFilterMap,
-) -> Result<ModelQueryFilterMapParsed, RadrootsAppUtilsError> {
- let mut bind_values = Vec::new();
- let mut query_values = Vec::new();
- for (index, (field, filter)) in filters.iter().enumerate() {
- let filter_condition = if index == 0 {
- String::new()
- } else if let Some(condition) = filter_condition_for(filter) {
- format!("{} ", condition.as_str())
- } else {
- "AND ".to_string()
- };
- match filter {
- ModelQueryFilter::Value(value) => {
- query_values.push(format!("{filter_condition}{field} = ?"));
- bind_values.push(parse_model_query_value(value));
- }
- ModelQueryFilter::Single {
- value,
- option,
- ..
- } => match option {
- ModelQueryFilterOption::StartsWith => {
- query_values.push(format!("{filter_condition}{field} LIKE ?"));
- bind_values.push(ModelQueryBindValue::String(format!(
- "{}%",
- value_to_string(value)
- )));
- }
- ModelQueryFilterOption::EndsWith => {
- query_values.push(format!("{filter_condition}{field} LIKE ?"));
- bind_values.push(ModelQueryBindValue::String(format!(
- "%{}",
- value_to_string(value)
- )));
- }
- ModelQueryFilterOption::Contains => {
- query_values.push(format!("{filter_condition}{field} LIKE ?"));
- bind_values.push(ModelQueryBindValue::String(format!(
- "%{}%",
- value_to_string(value)
- )));
- }
- ModelQueryFilterOption::NotEquals => {
- query_values.push(format!("{filter_condition}{field} != ?"));
- bind_values.push(parse_model_query_value(value));
- }
- ModelQueryFilterOption::Equals => {
- query_values.push(format!("{filter_condition}{field} = ?"));
- bind_values.push(parse_model_query_value(value));
- }
- },
- ModelQueryFilter::List {
- values,
- option,
- ..
- } => match option {
- ModelQueryFilterOptionList::Between => {
- if values.len() < 2 {
- return Err(RadrootsAppUtilsError::InvalidInput);
- }
- query_values.push(format!("{filter_condition}{field} BETWEEN ? AND ?"));
- bind_values.push(parse_model_query_value(&values[0]));
- bind_values.push(parse_model_query_value(&values[1]));
- }
- ModelQueryFilterOptionList::In => {
- if values.is_empty() {
- return Err(RadrootsAppUtilsError::InvalidInput);
- }
- let placeholders = std::iter::repeat("?")
- .take(values.len())
- .collect::<Vec<_>>()
- .join(", ");
- query_values.push(format!(
- "{filter_condition}{field} IN ({placeholders})"
- ));
- for value in values {
- bind_values.push(parse_model_query_value(value));
- }
- }
- },
- }
- }
- if query_values.is_empty() || bind_values.is_empty() {
- return Err(RadrootsAppUtilsError::InvalidInput);
- }
- Ok(ModelQueryFilterMapParsed {
- query_values,
- bind_values,
- })
-}
-
-fn value_to_string(value: &ModelQueryValue) -> String {
- match value {
- ModelQueryValue::String(value) => value.clone(),
- ModelQueryValue::Number(value) => value.to_string(),
- ModelQueryValue::Bool(true) => "1".to_string(),
- ModelQueryValue::Bool(false) => "0".to_string(),
- ModelQueryValue::Null => String::new(),
- }
-}
-
-fn filter_condition_for(filter: &ModelQueryFilter) -> Option<ModelQueryFilterCondition> {
- match filter {
- ModelQueryFilter::Value(_) => None,
- ModelQueryFilter::Single { condition, .. } => *condition,
- ModelQueryFilter::List { condition, .. } => *condition,
- }
-}
-
-impl ModelQueryFilterCondition {
- pub const fn as_str(self) -> &'static str {
- match self {
- ModelQueryFilterCondition::And => "and",
- ModelQueryFilterCondition::Or => "or",
- ModelQueryFilterCondition::Not => "not",
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- is_model_query_filter_option, is_model_query_filter_option_list, is_model_query_values,
- list_model_query_values_assert, parse_model_filter_map, parse_model_query_value,
- ModelQueryBindValue, ModelQueryFilter, ModelQueryFilterMap, ModelQueryFilterOption,
- ModelQueryValue,
- };
-
- #[test]
- fn parse_model_query_value_handles_bool() {
- assert_eq!(
- parse_model_query_value(&ModelQueryValue::Bool(true)),
- ModelQueryBindValue::String("1".to_string())
- );
- assert_eq!(
- parse_model_query_value(&ModelQueryValue::Bool(false)),
- ModelQueryBindValue::String("0".to_string())
- );
- }
-
- #[test]
- fn parse_model_query_value_handles_string() {
- assert_eq!(
- parse_model_query_value(&ModelQueryValue::String("ok".to_string())),
- ModelQueryBindValue::String("ok".to_string())
- );
- assert_eq!(
- parse_model_query_value(&ModelQueryValue::String(String::new())),
- ModelQueryBindValue::Null
- );
- }
-
- #[test]
- fn filter_option_checks() {
- assert!(is_model_query_filter_option("equals"));
- assert!(!is_model_query_filter_option("other"));
- assert!(is_model_query_filter_option_list("between"));
- assert!(!is_model_query_filter_option_list("other"));
- }
-
- #[test]
- fn query_value_checks() {
- assert!(is_model_query_values(&ModelQueryValue::String("ok".to_string())));
- assert!(!is_model_query_values(&ModelQueryValue::Null));
- }
-
- #[test]
- fn list_model_query_values_filters_none() {
- let values = vec![
- Some(ModelQueryValue::String("a".to_string())),
- None,
- Some(ModelQueryValue::Number(1.0)),
- ];
- let filtered = list_model_query_values_assert(&values);
- assert_eq!(
- filtered,
- vec![
- ModelQueryValue::String("a".to_string()),
- ModelQueryValue::Number(1.0)
- ]
- );
- }
-
- #[test]
- fn parse_model_filter_map_builds_query() {
- let mut filters = ModelQueryFilterMap::new();
- filters.insert(
- "name".to_string(),
- ModelQueryFilter::Single {
- value: ModelQueryValue::String("rad".to_string()),
- option: ModelQueryFilterOption::Contains,
- condition: None,
- },
- );
- filters.insert(
- "status".to_string(),
- ModelQueryFilter::Value(ModelQueryValue::String("ok".to_string())),
- );
- let parsed = parse_model_filter_map(&filters).expect("parsed");
- assert_eq!(parsed.query_values.len(), 2);
- assert_eq!(parsed.bind_values.len(), 2);
- }
-}
diff --git a/crates/utils/src/numbers/mod.rs b/crates/utils/src/numbers/mod.rs
@@ -1,108 +0,0 @@
-#![forbid(unsafe_code)]
-
-use crate::error::RadrootsAppUtilsError;
-
-pub fn parse_int(value: &str, fallback: i64) -> i64 {
- value.trim().parse::<i64>().unwrap_or(fallback)
-}
-
-pub fn parse_float(value: &str, fallback: f64) -> f64 {
- value.trim().parse::<f64>().unwrap_or(fallback)
-}
-
-pub fn num_str<T: ToString>(value: T) -> String {
- value.to_string()
-}
-
-pub fn num_interval_range(min: i64, max: i64) -> Result<i64, RadrootsAppUtilsError> {
- if min > max {
- return Err(RadrootsAppUtilsError::InvalidInput);
- }
- if min == max {
- return Ok(min);
- }
- let min_i128 = i128::from(min);
- let max_i128 = i128::from(max);
- let range = max_i128 - min_i128 + 1;
- if range <= 0 || range > i128::from(u64::MAX) {
- return Err(RadrootsAppUtilsError::InvalidInput);
- }
- let range_u64 = range as u64;
- let max_acceptable = u64::MAX - (u64::MAX % range_u64);
- loop {
- let value = random_u64()?;
- if value < max_acceptable {
- let offset = (value % range_u64) as i128;
- return Ok((min_i128 + offset) as i64);
- }
- }
-}
-
-#[cfg(target_arch = "wasm32")]
-fn random_u64() -> Result<u64, RadrootsAppUtilsError> {
- let window = web_sys::window().ok_or(RadrootsAppUtilsError::Unavailable)?;
- let crypto = window
- .crypto()
- .map_err(|_| RadrootsAppUtilsError::Unavailable)?;
- let mut bytes = [0u8; 8];
- crypto
- .get_random_values_with_u8_array(&mut bytes)
- .map_err(|_| RadrootsAppUtilsError::Unavailable)?;
- Ok(u64::from_le_bytes(bytes))
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-fn random_u64() -> Result<u64, RadrootsAppUtilsError> {
- let mut bytes = [0u8; 8];
- getrandom::fill(&mut bytes).map_err(|_| RadrootsAppUtilsError::Unavailable)?;
- Ok(u64::from_le_bytes(bytes))
-}
-
-#[cfg(test)]
-mod tests {
- use super::{num_interval_range, num_str, parse_float, parse_int};
- use crate::error::RadrootsAppUtilsError;
-
- #[test]
- fn parse_int_returns_fallback_on_invalid() {
- assert_eq!(parse_int("boom", 42), 42);
- }
-
- #[test]
- fn parse_int_parses_numbers() {
- assert_eq!(parse_int("123", 0), 123);
- }
-
- #[test]
- fn parse_float_returns_fallback_on_invalid() {
- assert_eq!(parse_float("boom", 1.5), 1.5);
- }
-
- #[test]
- fn parse_float_parses_numbers() {
- assert_eq!(parse_float("3.5", 0.0), 3.5);
- }
-
- #[test]
- fn num_str_formats_numbers() {
- assert_eq!(num_str(42), "42");
- }
-
- #[test]
- fn num_interval_range_rejects_invalid() {
- let err = num_interval_range(2, 1).unwrap_err();
- assert_eq!(err, RadrootsAppUtilsError::InvalidInput);
- }
-
- #[test]
- fn num_interval_range_single_value() {
- let value = num_interval_range(4, 4).expect("single value");
- assert_eq!(value, 4);
- }
-
- #[test]
- fn num_interval_range_within_bounds() {
- let value = num_interval_range(1, 3).expect("range");
- assert!((1..=3).contains(&value));
- }
-}
diff --git a/crates/utils/src/object/mod.rs b/crates/utils/src/object/mod.rs
@@ -1,80 +0,0 @@
-#![forbid(unsafe_code)]
-
-pub fn obj_en<K, V, F, I>(object: I, parse_function: F) -> Vec<(K, V)>
-where
- I: IntoIterator<Item = (String, V)>,
- F: Fn(&str) -> K,
-{
- object
- .into_iter()
- .map(|(key, value)| (parse_function(&key), value))
- .collect()
-}
-
-pub fn obj_truthy_fields<I, V>(values: I) -> bool
-where
- I: IntoIterator<Item = V>,
- V: AsRef<str>,
-{
- values.into_iter().all(|value| !value.as_ref().is_empty())
-}
-
-pub fn obj_result(value: &serde_json::Value) -> Option<String> {
- let obj = value.as_object()?;
- let result = obj.get("result")?.as_str()?;
- Some(result.to_string())
-}
-
-pub fn obj_results_str(value: &serde_json::Value) -> Option<Vec<String>> {
- let obj = value.as_object()?;
- let results = obj.get("results")?.as_array()?;
- Some(
- results
- .iter()
- .map(|entry| {
- entry
- .as_str()
- .map(ToString::to_string)
- .unwrap_or_else(|| entry.to_string())
- })
- .collect(),
- )
-}
-
-#[cfg(test)]
-mod tests {
- use super::{obj_en, obj_result, obj_results_str, obj_truthy_fields};
- use serde_json::json;
- use std::collections::BTreeMap;
-
- #[test]
- fn obj_en_maps_entries() {
- let mut map = BTreeMap::new();
- map.insert("one".to_string(), 1);
- let entries = obj_en(map.into_iter(), |key| format!("key:{key}"));
- assert_eq!(entries, vec![("key:one".to_string(), 1)]);
- }
-
- #[test]
- fn obj_truthy_fields_checks_values() {
- let values = vec!["one", "two"];
- assert!(obj_truthy_fields(values));
- let values = vec!["one", ""];
- assert!(!obj_truthy_fields(values));
- }
-
- #[test]
- fn obj_result_reads_result_field() {
- let value = json!({ "result": "ok" });
- assert_eq!(obj_result(&value), Some("ok".to_string()));
- }
-
- #[test]
- fn obj_results_str_reads_results_list() {
- let value = json!({ "results": ["a", "b"] });
- assert_eq!(
- obj_results_str(&value),
- Some(vec!["a".to_string(), "b".to_string()])
- );
- }
-}
diff --git a/crates/utils/src/path/mod.rs b/crates/utils/src/path/mod.rs
@@ -1,101 +0,0 @@
-#![forbid(unsafe_code)]
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsAppRoutePathParts {
- pub path: String,
- pub query: String,
- pub hash: String,
-}
-
-pub fn parse_route_path(route_path: &str) -> RadrootsAppRoutePathParts {
- let query_idx = route_path.find('?');
- let hash_idx = route_path.find('#');
- let path_end = match (query_idx, hash_idx) {
- (Some(query_idx), Some(hash_idx)) => query_idx.min(hash_idx),
- (Some(query_idx), None) => query_idx,
- (None, Some(hash_idx)) => hash_idx,
- (None, None) => route_path.len(),
- };
- let path = route_path[..path_end].to_string();
- let query = query_idx
- .map(|start| {
- let end = hash_idx.unwrap_or(route_path.len());
- route_path[start..end].to_string()
- })
- .unwrap_or_default();
- let hash = hash_idx
- .map(|start| route_path[start..].to_string())
- .unwrap_or_default();
- RadrootsAppRoutePathParts { path, query, hash }
-}
-
-fn has_file_extension(route_path: &str, file_exts: &[&str]) -> bool {
- let lower_path = route_path.to_ascii_lowercase();
- file_exts.iter().any(|ext| lower_path.ends_with(ext))
-}
-
-pub fn resolve_route_path(
- route_path: Option<&str>,
- file_name: &str,
- default_route_path: &str,
- file_exts: &[&str],
-) -> String {
- let resolved_route_path = route_path.unwrap_or(default_route_path);
- let parts = parse_route_path(resolved_route_path);
- let mut normalized_path = parts.path.as_str();
- if normalized_path.ends_with('/') {
- normalized_path = &normalized_path[..normalized_path.len().saturating_sub(1)];
- }
- if normalized_path.is_empty() {
- return resolved_route_path.to_string();
- }
- if normalized_path == file_name
- || normalized_path.ends_with(&format!("/{file_name}"))
- || has_file_extension(normalized_path, file_exts)
- {
- return format!("{}{}{}", normalized_path, parts.query, parts.hash);
- }
- format!(
- "{}/{}{}{}",
- normalized_path, file_name, parts.query, parts.hash
- )
-}
-
-pub fn resolve_wasm_path(
- wasm_path: Option<&str>,
- wasm_file: &str,
- default_wasm_path: &str,
-) -> String {
- resolve_route_path(wasm_path, wasm_file, default_wasm_path, &[".wasm"])
-}
-
-#[cfg(test)]
-mod tests {
- use super::{parse_route_path, resolve_route_path, resolve_wasm_path};
-
- #[test]
- fn parse_route_path_splits_parts() {
- let parts = parse_route_path("assets/app.js?cache=1#hash");
- assert_eq!(parts.path, "assets/app.js");
- assert_eq!(parts.query, "?cache=1");
- assert_eq!(parts.hash, "#hash");
- }
-
- #[test]
- fn resolve_route_path_appends_file_name() {
- let path = resolve_route_path(Some("assets"), "app.js", "/app.js", &[".js"]);
- assert_eq!(path, "assets/app.js");
- }
-
- #[test]
- fn resolve_route_path_keeps_file_path() {
- let path = resolve_route_path(Some("assets/app.js"), "app.js", "/app.js", &[".js"]);
- assert_eq!(path, "assets/app.js");
- }
-
- #[test]
- fn resolve_wasm_path_defaults_to_wasm_extension() {
- let path = resolve_wasm_path(Some("pkg"), "app_bg.wasm", "/app_bg.wasm");
- assert_eq!(path, "pkg/app_bg.wasm");
- }
-}
diff --git a/crates/utils/src/text/mod.rs b/crates/utils/src/text/mod.rs
@@ -1,84 +0,0 @@
-#![forbid(unsafe_code)]
-
-pub const ROOT_SYMBOL: &str = "»`,-";
-
-pub fn text_enc(data: &str) -> Vec<u8> {
- data.as_bytes().to_vec()
-}
-
-pub fn text_dec(data: &[u8]) -> String {
- String::from_utf8_lossy(data).to_string()
-}
-
-pub fn str_cap(value: Option<&str>) -> String {
- let Some(value) = value else {
- return String::new();
- };
- let mut chars = value.chars();
- let Some(first) = chars.next() else {
- return String::new();
- };
- let mut output = first.to_uppercase().collect::<String>();
- output.push_str(chars.as_str());
- output
-}
-
-pub fn str_cap_words(value: Option<&str>) -> String {
- let Some(value) = value else {
- return String::new();
- };
- let mut words = Vec::new();
- for word in value.split(' ') {
- if word.is_empty() {
- continue;
- }
- let capped = str_cap(Some(word));
- if !capped.is_empty() {
- words.push(capped);
- }
- }
- words.join(" ")
-}
-
-#[cfg(test)]
-mod tests {
- use super::{str_cap, str_cap_words, text_dec, text_enc, ROOT_SYMBOL};
-
- #[test]
- fn root_symbol_matches_spec() {
- assert_eq!(ROOT_SYMBOL, "»`,-");
- }
-
- #[test]
- fn text_enc_dec_roundtrip() {
- let encoded = text_enc("radroots");
- assert_eq!(encoded, b"radroots");
- let decoded = text_dec(&encoded);
- assert_eq!(decoded, "radroots");
- }
-
- #[test]
- fn str_cap_handles_none() {
- assert_eq!(str_cap(None), "");
- }
-
- #[test]
- fn str_cap_uppercases_first_letter() {
- assert_eq!(str_cap(Some("radroots")), "Radroots");
- }
-
- #[test]
- fn str_cap_words_handles_none() {
- assert_eq!(str_cap_words(None), "");
- }
-
- #[test]
- fn str_cap_words_caps_each_word() {
- assert_eq!(str_cap_words(Some("rad roots")), "Rad Roots");
- }
-
- #[test]
- fn str_cap_words_skips_empty_words() {
- assert_eq!(str_cap_words(Some("rad roots")), "Rad Roots");
- }
-}
diff --git a/crates/utils/src/time/mod.rs b/crates/utils/src/time/mod.rs
@@ -1,31 +0,0 @@
-#![forbid(unsafe_code)]
-
-use std::time::{SystemTime, UNIX_EPOCH};
-
-pub fn time_now_s() -> u64 {
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map(|duration| duration.as_secs())
- .unwrap_or(0)
-}
-
-pub fn time_now_ms() -> u64 {
- time_now_s()
-}
-
-#[cfg(test)]
-mod tests {
- use super::{time_now_ms, time_now_s};
-
- #[test]
- fn time_now_returns_seconds() {
- let now_s = time_now_s();
- let now_ms = time_now_ms();
- let delta = if now_s > now_ms {
- now_s - now_ms
- } else {
- now_ms - now_s
- };
- assert!(delta <= 1);
- }
-}
diff --git a/crates/utils/src/types.rs b/crates/utils/src/types.rs
@@ -1,203 +0,0 @@
-#![forbid(unsafe_code)]
-
-use crate::error::RadrootsAppUtilsError;
-use regex::Regex;
-
-pub type ResolveError<T> = Result<T, RadrootsAppUtilsError>;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum FileBytesFormat {
- Kb,
- Mb,
- Gb,
-}
-
-pub type FileMimeType = String;
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct FilePath {
- pub file_path: String,
- pub file_name: String,
- pub mime_type: FileMimeType,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct FilePathBlob {
- pub blob_path: String,
- pub blob_name: String,
- pub mime_type: Option<FileMimeType>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum WebFilePath {
- File(FilePath),
- Blob(FilePathBlob),
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct IdbClientConfig {
- pub database: String,
- pub store: String,
-}
-
-pub type ValStr = Option<String>;
-
-#[derive(Debug, Clone, Copy)]
-pub struct ValidationRegex {
- pub value: &'static Regex,
- pub charset: &'static Regex,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub struct ResultPass {
- pub pass: bool,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct ResultId {
- pub id: String,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct ResultObj<T> {
- pub result: T,
-}
-
-pub type ResultBool = ResultObj<bool>;
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct ResultsList<T> {
- pub results: Vec<T>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct ResultPublicKey {
- pub public_key: String,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct ResultSecretKey {
- pub secret_key: String,
-}
-
-impl ResultPass {
- pub const fn ok() -> Self {
- Self { pass: true }
- }
-}
-
-pub fn resolve_ok<T>(value: T) -> ResolveError<T> {
- Ok(value)
-}
-
-pub fn resolve_err<T>(err: RadrootsAppUtilsError) -> ResolveError<T> {
- Err(err)
-}
-
-pub fn is_error<T>(value: &ResolveError<T>) -> bool {
- value.is_err()
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- resolve_err, resolve_ok, FileBytesFormat, FilePath, FilePathBlob, IdbClientConfig,
- is_error, ResultBool, ResultId, ResultObj, ResultPass, ResultPublicKey, ResultSecretKey,
- ResultsList, ValidationRegex, ValStr, WebFilePath,
- };
- use crate::error::RadrootsAppUtilsError;
- use regex::Regex;
-
- #[test]
- fn result_pass_is_true() {
- let pass = ResultPass::ok();
- assert!(pass.pass);
- }
-
- #[test]
- fn resolve_ok_returns_value() {
- let value = resolve_ok(5).expect("value");
- assert_eq!(value, 5);
- }
-
- #[test]
- fn resolve_err_returns_error() {
- let err = resolve_err::<()>(RadrootsAppUtilsError::Unavailable)
- .expect_err("err");
- assert_eq!(err, RadrootsAppUtilsError::Unavailable);
- }
-
- #[test]
- fn result_types_store_values() {
- let id = ResultId {
- id: "id".to_string(),
- };
- assert_eq!(id.id, "id");
- let obj = ResultObj { result: 5 };
- assert_eq!(obj.result, 5);
- let bool_obj: ResultBool = ResultObj { result: true };
- assert!(bool_obj.result);
- let list = ResultsList {
- results: vec![1, 2],
- };
- assert_eq!(list.results, vec![1, 2]);
- let public_key = ResultPublicKey {
- public_key: "pub".to_string(),
- };
- assert_eq!(public_key.public_key, "pub");
- let secret_key = ResultSecretKey {
- secret_key: "sec".to_string(),
- };
- assert_eq!(secret_key.secret_key, "sec");
- }
-
- #[test]
- fn file_path_types_store_values() {
- let path = FilePath {
- file_path: "path".to_string(),
- file_name: "name".to_string(),
- mime_type: "text/plain".to_string(),
- };
- let blob = FilePathBlob {
- blob_path: "blob".to_string(),
- blob_name: "blob.bin".to_string(),
- mime_type: None,
- };
- let file_path = WebFilePath::File(path.clone());
- let blob_path = WebFilePath::Blob(blob.clone());
- assert_eq!(file_path, WebFilePath::File(path));
- assert_eq!(blob_path, WebFilePath::Blob(blob));
- assert_eq!(FileBytesFormat::Kb, FileBytesFormat::Kb);
- }
-
- #[test]
- fn idb_config_stores_values() {
- let config = IdbClientConfig {
- database: "db".to_string(),
- store: "store".to_string(),
- };
- assert_eq!(config.database, "db");
- assert_eq!(config.store, "store");
- let value: ValStr = None;
- assert!(value.is_none());
- }
-
- #[test]
- fn is_error_returns_true_for_err() {
- let ok = resolve_ok(1);
- assert!(!is_error(&ok));
- let err = resolve_err::<()>(RadrootsAppUtilsError::InvalidInput);
- assert!(is_error(&err));
- }
-
- #[test]
- fn validation_regex_tracks_patterns() {
- let regex = Regex::new("^a+$").expect("regex");
- let value = Box::leak(Box::new(regex));
- let validation = ValidationRegex {
- value,
- charset: value,
- };
- assert!(validation.value.is_match("aa"));
- }
-}
diff --git a/crates/utils/src/unit/mod.rs b/crates/utils/src/unit/mod.rs
@@ -1,140 +0,0 @@
-#![forbid(unsafe_code)]
-
-use crate::error::RadrootsAppUtilsError;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum AreaUnit {
- Ac,
- Ha,
- Ft2,
- M2,
-}
-
-impl AreaUnit {
- pub const fn as_str(self) -> &'static str {
- match self {
- AreaUnit::Ac => "ac",
- AreaUnit::Ha => "ha",
- AreaUnit::Ft2 => "ft2",
- AreaUnit::M2 => "m2",
- }
- }
-}
-
-pub const AREA_UNITS: [AreaUnit; 4] = [AreaUnit::Ac, AreaUnit::Ha, AreaUnit::Ft2, AreaUnit::M2];
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum MassUnit {
- Kg,
- Lb,
- G,
-}
-
-impl MassUnit {
- pub const fn as_str(self) -> &'static str {
- match self {
- MassUnit::Kg => "kg",
- MassUnit::Lb => "lb",
- MassUnit::G => "g",
- }
- }
-}
-
-pub const MASS_UNITS: [MassUnit; 3] = [MassUnit::Kg, MassUnit::Lb, MassUnit::G];
-
-pub fn parse_mass_unit(val: Option<&str>) -> Option<MassUnit> {
- match val {
- Some("kg") => Some(MassUnit::Kg),
- Some("lb") => Some(MassUnit::Lb),
- Some("g") => Some(MassUnit::G),
- _ => None,
- }
-}
-
-pub fn parse_mass_unit_default(val: Option<&str>) -> MassUnit {
- parse_mass_unit(val).unwrap_or(MassUnit::Kg)
-}
-
-pub fn mass_to_g(value: f64, unit: &str) -> Result<f64, RadrootsAppUtilsError> {
- let mass_unit = parse_mass_unit(Some(unit)).ok_or(RadrootsAppUtilsError::InvalidInput)?;
- let grams = match mass_unit {
- MassUnit::Kg => value * 1000.0,
- MassUnit::Lb => value * 453.592,
- MassUnit::G => value,
- };
- Ok(grams)
-}
-
-pub fn parse_area_unit(val: Option<&str>) -> Option<AreaUnit> {
- match val {
- Some("ac") => Some(AreaUnit::Ac),
- Some("ha") => Some(AreaUnit::Ha),
- Some("ft2") => Some(AreaUnit::Ft2),
- Some("m2") => Some(AreaUnit::M2),
- _ => None,
- }
-}
-
-pub fn parse_area_unit_default(val: Option<&str>) -> AreaUnit {
- parse_area_unit(val).unwrap_or(AreaUnit::Ac)
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- mass_to_g, parse_area_unit, parse_area_unit_default, parse_mass_unit,
- parse_mass_unit_default, AreaUnit, MassUnit, AREA_UNITS, MASS_UNITS,
- };
-
- #[test]
- fn area_units_are_sorted() {
- assert_eq!(AREA_UNITS.len(), 4);
- assert_eq!(AREA_UNITS[0], AreaUnit::Ac);
- assert_eq!(AREA_UNITS[1], AreaUnit::Ha);
- assert_eq!(AREA_UNITS[2], AreaUnit::Ft2);
- assert_eq!(AREA_UNITS[3], AreaUnit::M2);
- }
-
- #[test]
- fn mass_units_are_sorted() {
- assert_eq!(MASS_UNITS.len(), 3);
- assert_eq!(MASS_UNITS[0], MassUnit::Kg);
- assert_eq!(MASS_UNITS[1], MassUnit::Lb);
- assert_eq!(MASS_UNITS[2], MassUnit::G);
- }
-
- #[test]
- fn parse_mass_units() {
- assert_eq!(parse_mass_unit(Some("kg")), Some(MassUnit::Kg));
- assert_eq!(parse_mass_unit(Some("lb")), Some(MassUnit::Lb));
- assert_eq!(parse_mass_unit(Some("g")), Some(MassUnit::G));
- assert_eq!(parse_mass_unit(Some("other")), None);
- }
-
- #[test]
- fn parse_mass_unit_defaults_to_kg() {
- assert_eq!(parse_mass_unit_default(None), MassUnit::Kg);
- }
-
- #[test]
- fn parse_area_units() {
- assert_eq!(parse_area_unit(Some("ac")), Some(AreaUnit::Ac));
- assert_eq!(parse_area_unit(Some("ha")), Some(AreaUnit::Ha));
- assert_eq!(parse_area_unit(Some("ft2")), Some(AreaUnit::Ft2));
- assert_eq!(parse_area_unit(Some("m2")), Some(AreaUnit::M2));
- assert_eq!(parse_area_unit(Some("other")), None);
- }
-
- #[test]
- fn parse_area_unit_defaults_to_ac() {
- assert_eq!(parse_area_unit_default(None), AreaUnit::Ac);
- }
-
- #[test]
- fn mass_to_g_handles_units() {
- let grams = mass_to_g(2.0, "kg").expect("kg");
- assert_eq!(grams, 2000.0);
- let grams = mass_to_g(2.0, "g").expect("g");
- assert_eq!(grams, 2.0);
- }
-}
diff --git a/crates/utils/src/validation/mod.rs b/crates/utils/src/validation/mod.rs
@@ -1,4 +0,0 @@
-#![forbid(unsafe_code)]
-
-pub mod regex;
-pub mod schema;
diff --git a/crates/utils/src/validation/regex.rs b/crates/utils/src/validation/regex.rs
@@ -1,450 +0,0 @@
-#![forbid(unsafe_code)]
-
-use once_cell::sync::Lazy;
-use regex::Regex;
-use std::collections::HashMap;
-
-pub struct UtilRegex;
-
-macro_rules! regex_lazy {
- ($name:ident, $pattern:expr) => {
- static $name: Lazy<Regex> = Lazy::new(|| {
- Regex::new($pattern).expect("regex")
- });
- };
-}
-
-regex_lazy!(EMAIL, r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$");
-regex_lazy!(EMAIL_CH, r"^[A-Za-z0-9._%+@-]*$");
-regex_lazy!(PRODUCT_KEY, r"^[A-Za-z_]+$");
-regex_lazy!(PRODUCT_KEY_CH, r"^[A-Za-z_]$");
-regex_lazy!(PRODUCT_TITLE, r"[A-Za-z0-9 ]+$");
-regex_lazy!(PRODUCT_TITLE_CH, r"[A-Za-z0-9 ]$");
-regex_lazy!(FLOAT, r"^[+-]?(\d+(\.\d*)?|\.\d+)$");
-regex_lazy!(FLOAT_CH, r"^[0-9.+-]$");
-regex_lazy!(FLOAT_POS, r"^\d+(\.\d+)?$");
-regex_lazy!(FLOAT_POS_CH, r"^[0-9.]$");
-regex_lazy!(DESCRIPTION, r"^(?:\S+(?:\s+\S+)*)$");
-regex_lazy!(DESCRIPTION_CH, r#"[^a-zA-Z0-9.,!?;:'"(){}\[\]\s\x{0600}-\x{06FF}\x{0900}-\x{097F}\x{0400}-\x{04FF}\x{0500}-\x{052F}\x{1F00}-\x{1FFF}\x{4E00}-\x{9FFF}\x{AC00}-\x{D7AF}\x{3040}-\x{309F}\x{30A0}-\x{30FF} ]+"#);
-regex_lazy!(NBSP, r"[\x{00A0}]");
-regex_lazy!(NBSP_RP, r"[\x{00A0}]+");
-regex_lazy!(RTLM, r"[\x{200F}]");
-regex_lazy!(RTLM_RP, r"[\x{200F}]+");
-regex_lazy!(COMMAS, r"[,]+");
-regex_lazy!(PERIODS, r"[.]+");
-regex_lazy!(WORD_ONLY, r"^[a-zA-Z]+$");
-regex_lazy!(ALPHA, r"[a-zA-Z ]$");
-regex_lazy!(ALPHA_CH, r"[a-zA-Z ]$");
-regex_lazy!(NUM, r"^[0-9]+$");
-regex_lazy!(LAT, r"^[-+]?([1-8]?[0-9](\.\d{1,6})?|90(\.0{1,6})?)$");
-regex_lazy!(LAT_CH, r"^[0-9.+-]$");
-regex_lazy!(LNG, r"^[-+]?((1[0-7]?[0-9]|180)(\.\d{1,6})?|(\d{1,2})(\.\d{1,6})?)$");
-regex_lazy!(LNG_CH, r"^[0-9.+-]$");
-regex_lazy!(ALPHANUM, r"[a-zA-Z0-9., ]$");
-regex_lazy!(ALPHANUM_CH, r"[a-zA-Z0-9.,\s\x{0600}-\x{06FF}\x{0900}-\x{097F}\x{0400}-\x{04FF}\x{0500}-\x{052F}\x{1F00}-\x{1FFF}\x{4E00}-\x{9FFF}\x{AC00}-\x{D7AF}\x{3040}-\x{309F}\x{30A0}-\x{30FF} ]+");
-regex_lazy!(PRICE, r"^\d+(\.\d+)?$");
-regex_lazy!(PRICE_CH, r"[0-9.]$");
-regex_lazy!(PRICE_CUR, r"^[A-Za-z]{3}$");
-regex_lazy!(PRICE_CUR_CH, r"[A-Za-z]$");
-regex_lazy!(PROFILE_NAME, r"^[a-zA-Z0-9._]{3,30}$");
-regex_lazy!(PROFILE_NAME_CH, r"[a-zA-Z0-9._]");
-regex_lazy!(TRADE_PRODUCT_KEY, r"^(?:[a-zA-Z0-9]+(?:\s+[a-zA-Z0-9]+){0,2})$");
-regex_lazy!(TRADE_PRODUCT_CATEGORY, r"^(?:[a-zA-Z0-9]+(?:\s+[a-zA-Z0-9]+){0,2})$");
-regex_lazy!(CURRENCY_SYMBOL, r"(?:[A-Za-z]{3,5}\$|\p{Sc})");
-regex_lazy!(CURRENCY_MARKER, r"(?:[A-Za-z]{2,4}[^\d\s]+|[^\d\s]{1,3}[A-Za-z]{2,4})");
-regex_lazy!(WS_PROTO, r"^(wss://|ws://)");
-regex_lazy!(BIN_DISPLAY_UNIT, r"^(kg|lb|g)$");
-regex_lazy!(BIN_DISPLAY_UNIT_CH, r"[A-Za-z]$");
-regex_lazy!(URL_IMAGE_UPLOAD, r"^file://.*\.(png|jpg|jpeg|gif|webp|bmp|svg)$");
-regex_lazy!(URL_IMAGE_UPLOAD_DEV, r"^file://.*\.(png|jpg|jpeg|gif|webp|bmp|svg)$");
-regex_lazy!(COUNTRY_CODE_A2, r"^[A-Za-z]{2}$");
-regex_lazy!(ADDR_PRIMARY, r"[a-zA-Z0-9., ]$");
-regex_lazy!(ADDR_ADMIN, r"[a-zA-Z0-9., ]$");
-regex_lazy!(NUM_INT, r"^[0-9]$");
-regex_lazy!(AREA_UNIT, r"^(ac|ha|ft2|m2)$");
-regex_lazy!(AREA_UNIT_CH, r"[A-Za-z2]$");
-
-impl UtilRegex {
- pub fn email() -> &'static Regex {
- &EMAIL
- }
-
- pub fn email_ch() -> &'static Regex {
- &EMAIL_CH
- }
-
- pub fn product_key() -> &'static Regex {
- &PRODUCT_KEY
- }
-
- pub fn product_key_ch() -> &'static Regex {
- &PRODUCT_KEY_CH
- }
-
- pub fn product_title() -> &'static Regex {
- &PRODUCT_TITLE
- }
-
- pub fn product_title_ch() -> &'static Regex {
- &PRODUCT_TITLE_CH
- }
-
- pub fn float() -> &'static Regex {
- &FLOAT
- }
-
- pub fn float_ch() -> &'static Regex {
- &FLOAT_CH
- }
-
- pub fn float_pos() -> &'static Regex {
- &FLOAT_POS
- }
-
- pub fn float_pos_ch() -> &'static Regex {
- &FLOAT_POS_CH
- }
-
- pub fn description() -> &'static Regex {
- &DESCRIPTION
- }
-
- pub fn description_ch() -> &'static Regex {
- &DESCRIPTION_CH
- }
-
- pub fn nbsp() -> &'static Regex {
- &NBSP
- }
-
- pub fn nbsp_rp() -> &'static Regex {
- &NBSP_RP
- }
-
- pub fn rtlm() -> &'static Regex {
- &RTLM
- }
-
- pub fn rtlm_rp() -> &'static Regex {
- &RTLM_RP
- }
-
- pub fn commas() -> &'static Regex {
- &COMMAS
- }
-
- pub fn periods() -> &'static Regex {
- &PERIODS
- }
-
- pub fn word_only() -> &'static Regex {
- &WORD_ONLY
- }
-
- pub fn alpha() -> &'static Regex {
- &ALPHA
- }
-
- pub fn alpha_ch() -> &'static Regex {
- &ALPHA_CH
- }
-
- pub fn num() -> &'static Regex {
- &NUM
- }
-
- pub fn lat() -> &'static Regex {
- &LAT
- }
-
- pub fn lat_ch() -> &'static Regex {
- &LAT_CH
- }
-
- pub fn lng() -> &'static Regex {
- &LNG
- }
-
- pub fn lng_ch() -> &'static Regex {
- &LNG_CH
- }
-
- pub fn alphanum() -> &'static Regex {
- &ALPHANUM
- }
-
- pub fn alphanum_ch() -> &'static Regex {
- &ALPHANUM_CH
- }
-
- pub fn price() -> &'static Regex {
- &PRICE
- }
-
- pub fn price_ch() -> &'static Regex {
- &PRICE_CH
- }
-
- pub fn price_cur() -> &'static Regex {
- &PRICE_CUR
- }
-
- pub fn price_cur_ch() -> &'static Regex {
- &PRICE_CUR_CH
- }
-
- pub fn profile_name() -> &'static Regex {
- &PROFILE_NAME
- }
-
- pub fn profile_name_ch() -> &'static Regex {
- &PROFILE_NAME_CH
- }
-
- pub fn trade_product_key() -> &'static Regex {
- &TRADE_PRODUCT_KEY
- }
-
- pub fn trade_product_category() -> &'static Regex {
- &TRADE_PRODUCT_CATEGORY
- }
-
- pub fn currency_symbol() -> &'static Regex {
- &CURRENCY_SYMBOL
- }
-
- pub fn currency_marker() -> &'static Regex {
- &CURRENCY_MARKER
- }
-
- pub fn ws_proto() -> &'static Regex {
- &WS_PROTO
- }
-
- pub fn bin_display_unit() -> &'static Regex {
- &BIN_DISPLAY_UNIT
- }
-
- pub fn bin_display_unit_ch() -> &'static Regex {
- &BIN_DISPLAY_UNIT_CH
- }
-
- pub fn url_image_upload() -> &'static Regex {
- &URL_IMAGE_UPLOAD
- }
-
- pub fn url_image_upload_dev() -> &'static Regex {
- &URL_IMAGE_UPLOAD_DEV
- }
-
- pub fn country_code_a2() -> &'static Regex {
- &COUNTRY_CODE_A2
- }
-
- pub fn addr_primary() -> &'static Regex {
- &ADDR_PRIMARY
- }
-
- pub fn addr_admin() -> &'static Regex {
- &ADDR_ADMIN
- }
-
- pub fn num_int() -> &'static Regex {
- &NUM_INT
- }
-
- pub fn area_unit() -> &'static Regex {
- &AREA_UNIT
- }
-
- pub fn area_unit_ch() -> &'static Regex {
- &AREA_UNIT_CH
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
-pub enum FormFieldsKey {
- NostrSecretKey,
- ProductTitle,
- ProductKey,
- ProductProcess,
- ProductDescription,
- Price,
- PriceCurrency,
- BinDisplayUnit,
- BinDisplayAmount,
- BinLabel,
- FarmName,
- FarmSize,
- Area,
- AreaUnit,
- ContactName,
- ProfileName,
-}
-
-impl FormFieldsKey {
- pub const fn as_str(self) -> &'static str {
- match self {
- FormFieldsKey::NostrSecretKey => "nostr_secret_key",
- FormFieldsKey::ProductTitle => "product_title",
- FormFieldsKey::ProductKey => "product_key",
- FormFieldsKey::ProductProcess => "product_process",
- FormFieldsKey::ProductDescription => "product_description",
- FormFieldsKey::Price => "price",
- FormFieldsKey::PriceCurrency => "price_currency",
- FormFieldsKey::BinDisplayUnit => "bin_display_unit",
- FormFieldsKey::BinDisplayAmount => "bin_display_amount",
- FormFieldsKey::BinLabel => "bin_label",
- FormFieldsKey::FarmName => "farm_name",
- FormFieldsKey::FarmSize => "farm_size",
- FormFieldsKey::Area => "area",
- FormFieldsKey::AreaUnit => "area_unit",
- FormFieldsKey::ContactName => "contact_name",
- FormFieldsKey::ProfileName => "profile_name",
- }
- }
-}
-
-#[derive(Debug, Clone, Copy)]
-pub struct FormField {
- pub validate: &'static Regex,
- pub charset: &'static Regex,
-}
-
-static FORM_FIELDS: Lazy<HashMap<FormFieldsKey, FormField>> = Lazy::new(|| {
- let mut fields = HashMap::new();
- fields.insert(
- FormFieldsKey::ProfileName,
- FormField {
- validate: UtilRegex::profile_name(),
- charset: UtilRegex::profile_name_ch(),
- },
- );
- fields.insert(
- FormFieldsKey::ProductDescription,
- FormField {
- validate: UtilRegex::alpha(),
- charset: UtilRegex::alpha_ch(),
- },
- );
- fields.insert(
- FormFieldsKey::ProductKey,
- FormField {
- validate: UtilRegex::product_key(),
- charset: UtilRegex::product_key_ch(),
- },
- );
- fields.insert(
- FormFieldsKey::ProductTitle,
- FormField {
- validate: UtilRegex::product_title(),
- charset: UtilRegex::product_title_ch(),
- },
- );
- fields.insert(
- FormFieldsKey::ProductProcess,
- FormField {
- validate: UtilRegex::alphanum(),
- charset: UtilRegex::alphanum_ch(),
- },
- );
- fields.insert(
- FormFieldsKey::Price,
- FormField {
- validate: UtilRegex::price(),
- charset: UtilRegex::price_ch(),
- },
- );
- fields.insert(
- FormFieldsKey::PriceCurrency,
- FormField {
- validate: UtilRegex::price_cur(),
- charset: UtilRegex::price_cur_ch(),
- },
- );
- fields.insert(
- FormFieldsKey::BinDisplayAmount,
- FormField {
- validate: UtilRegex::float_pos(),
- charset: UtilRegex::float_pos_ch(),
- },
- );
- fields.insert(
- FormFieldsKey::BinDisplayUnit,
- FormField {
- validate: UtilRegex::bin_display_unit(),
- charset: UtilRegex::bin_display_unit_ch(),
- },
- );
- fields.insert(
- FormFieldsKey::BinLabel,
- FormField {
- validate: UtilRegex::alphanum(),
- charset: UtilRegex::alphanum_ch(),
- },
- );
- fields.insert(
- FormFieldsKey::Area,
- FormField {
- validate: UtilRegex::float(),
- charset: UtilRegex::float_ch(),
- },
- );
- fields.insert(
- FormFieldsKey::AreaUnit,
- FormField {
- validate: UtilRegex::area_unit(),
- charset: UtilRegex::area_unit_ch(),
- },
- );
- fields.insert(
- FormFieldsKey::FarmName,
- FormField {
- validate: UtilRegex::alpha(),
- charset: UtilRegex::alpha_ch(),
- },
- );
- fields.insert(
- FormFieldsKey::FarmSize,
- FormField {
- validate: UtilRegex::num_int(),
- charset: UtilRegex::num_int(),
- },
- );
- fields.insert(
- FormFieldsKey::ContactName,
- FormField {
- validate: UtilRegex::alpha(),
- charset: UtilRegex::alpha_ch(),
- },
- );
- fields.insert(
- FormFieldsKey::NostrSecretKey,
- FormField {
- validate: UtilRegex::alpha(),
- charset: UtilRegex::alpha_ch(),
- },
- );
- fields
-});
-
-pub fn form_fields() -> &'static HashMap<FormFieldsKey, FormField> {
- &FORM_FIELDS
-}
-
-#[cfg(test)]
-mod tests {
- use super::{form_fields, FormFieldsKey, UtilRegex};
-
- #[test]
- fn email_regex_accepts_valid_email() {
- assert!(UtilRegex::email().is_match("user@example.com"));
- assert!(!UtilRegex::email().is_match("invalid"));
- }
-
- #[test]
- fn form_fields_contains_profile_name() {
- let fields = form_fields();
- assert!(fields.contains_key(&FormFieldsKey::ProfileName));
- }
-}
diff --git a/crates/utils/src/validation/schema.rs b/crates/utils/src/validation/schema.rs
@@ -1,133 +0,0 @@
-#![forbid(unsafe_code)]
-
-use crate::error::RadrootsAppUtilsError;
-use crate::numbers::{parse_float, parse_int};
-use crate::unit::{parse_area_unit, parse_mass_unit, AreaUnit, MassUnit};
-use crate::validation::regex::UtilRegex;
-
-pub fn zf_area_unit(value: &str) -> Result<AreaUnit, RadrootsAppUtilsError> {
- parse_area_unit(Some(value)).ok_or(RadrootsAppUtilsError::InvalidInput)
-}
-
-pub fn zf_mass_unit(value: &str) -> Result<MassUnit, RadrootsAppUtilsError> {
- parse_mass_unit(Some(value)).ok_or(RadrootsAppUtilsError::InvalidInput)
-}
-
-pub fn zf_price_amount(input: &str) -> Result<f64, RadrootsAppUtilsError> {
- let value = parse_float(input, 1.0);
- validate_positive_multiple(value, 0.01)
-}
-
-pub fn zf_quantity_amount(input: &str) -> Result<i64, RadrootsAppUtilsError> {
- let value = parse_int(input, 1);
- if value > 0 {
- Ok(value)
- } else {
- Err(RadrootsAppUtilsError::InvalidInput)
- }
-}
-
-pub fn zf_price(value: f64) -> Result<f64, RadrootsAppUtilsError> {
- validate_positive_multiple(value, 0.01)
-}
-
-pub fn zf_numi_pos(value: i64) -> Result<i64, RadrootsAppUtilsError> {
- if value > 0 {
- Ok(value)
- } else {
- Err(RadrootsAppUtilsError::InvalidInput)
- }
-}
-
-pub fn zf_numf_pos(value: f64) -> Result<f64, RadrootsAppUtilsError> {
- if value > 0.0 {
- Ok(value)
- } else {
- Err(RadrootsAppUtilsError::InvalidInput)
- }
-}
-
-pub fn zf_email(value: &str) -> Result<&str, RadrootsAppUtilsError> {
- if UtilRegex::email().is_match(value) {
- Ok(value)
- } else {
- Err(RadrootsAppUtilsError::InvalidInput)
- }
-}
-
-pub fn zf_username(value: &str) -> Result<&str, RadrootsAppUtilsError> {
- if UtilRegex::profile_name().is_match(value) {
- Ok(value)
- } else {
- Err(RadrootsAppUtilsError::InvalidInput)
- }
-}
-
-fn validate_positive_multiple(value: f64, multiple: f64) -> Result<f64, RadrootsAppUtilsError> {
- if value <= 0.0 {
- return Err(RadrootsAppUtilsError::InvalidInput);
- }
- let scaled = value / multiple;
- if (scaled - scaled.round()).abs() > f64::EPSILON {
- return Err(RadrootsAppUtilsError::InvalidInput);
- }
- Ok(value)
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- zf_area_unit, zf_email, zf_mass_unit, zf_numf_pos, zf_numi_pos, zf_price,
- zf_price_amount, zf_quantity_amount, zf_username,
- };
-
- #[test]
- fn zf_area_unit_accepts_valid() {
- assert!(zf_area_unit("ac").is_ok());
- assert!(zf_area_unit("invalid").is_err());
- }
-
- #[test]
- fn zf_mass_unit_accepts_valid() {
- assert!(zf_mass_unit("kg").is_ok());
- assert!(zf_mass_unit("invalid").is_err());
- }
-
- #[test]
- fn zf_price_amount_validates_positive_multiple() {
- assert!(zf_price_amount("1.25").is_ok());
- assert!(zf_price_amount("-1").is_err());
- }
-
- #[test]
- fn zf_quantity_amount_requires_positive_int() {
- assert!(zf_quantity_amount("2").is_ok());
- assert!(zf_quantity_amount("0").is_err());
- }
-
- #[test]
- fn zf_price_validates_multiple() {
- assert!(zf_price(1.25).is_ok());
- assert!(zf_price(-1.0).is_err());
- }
-
- #[test]
- fn zf_num_pos_validates_positive() {
- assert!(zf_numi_pos(1).is_ok());
- assert!(zf_numi_pos(0).is_err());
- assert!(zf_numf_pos(1.0).is_ok());
- assert!(zf_numf_pos(0.0).is_err());
- }
-
- #[test]
- fn zf_email_validates_format() {
- assert!(zf_email("user@example.com").is_ok());
- assert!(zf_email("bad").is_err());
- }
-
- #[test]
- fn zf_username_validates_profile_name() {
- assert!(zf_username("user_name").is_ok());
- assert!(zf_username("x").is_err());
- }
-}
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
@@ -1,3 +1,2 @@
[toolchain]
channel = "1.92.0"
-targets = ["wasm32-unknown-unknown"]