Compare commits

26 Commits

Author SHA1 Message Date
ac2fafeb91 feat: UI overhaul (#131) 2026-01-29 19:24:59 +00:00
30fdeae3b2 feat: UI overhaul
All checks were successful
conventional pull request title check / conventional pull request title check (pull_request) Successful in 6s
actionlint check / actionlint check (pull_request) Successful in 20s
conventional commit messages check / conventional commit messages check (pull_request) Successful in 10s
dotenv-linter check / dotenv-linter check (pull_request) Successful in 32s
GitLeaks check / GitLeaks check (pull_request) Successful in 28s
hadolint check / hadolint check (pull_request) Successful in 42s
htmlhint check / htmlhint check (pull_request) Successful in 1m16s
markdownlint check / markdownlint check (pull_request) Successful in 53s
Prettier check / Prettier check (pull_request) Successful in 50s
ShellCheck check / ShellCheck check (pull_request) Successful in 39s
yamllint check / yamllint check (pull_request) Successful in 42s
Stylelint check / Stylelint check (pull_request) Successful in 44s
checkov check / checkov check (pull_request) Successful in 6m52s
Rust check / Rust check (pull_request) Successful in 33m33s
2026-01-29 16:22:45 +01:00
be1a21b746 fix: text wrapping (#128) 2026-01-25 10:21:12 +00:00
fa749b652f fix: text wrapping
Some checks failed
actionlint check / actionlint check (pull_request) Successful in 7s
conventional pull request title check / conventional pull request title check (pull_request) Successful in 3s
dotenv-linter check / dotenv-linter check (pull_request) Successful in 6s
GitLeaks check / GitLeaks check (pull_request) Successful in 9s
conventional commit messages check / conventional commit messages check (pull_request) Failing after 5s
hadolint check / hadolint check (pull_request) Successful in 19s
markdownlint check / markdownlint check (pull_request) Successful in 37s
htmlhint check / htmlhint check (pull_request) Successful in 43s
Prettier check / Prettier check (pull_request) Successful in 40s
ShellCheck check / ShellCheck check (pull_request) Successful in 48s
Stylelint check / Stylelint check (pull_request) Successful in 37s
checkov check / checkov check (pull_request) Successful in 1m46s
yamllint check / yamllint check (pull_request) Successful in 31s
Rust check / Rust check (pull_request) Successful in 16m41s
2026-01-25 10:35:54 +01:00
d9efaaae6e fix: ability to access the someday maybe category list (#127) 2026-01-25 09:27:23 +00:00
428e52ba3d fix: ability to access the someday maybe category list
Some checks failed
actionlint check / actionlint check (pull_request) Successful in 7s
conventional pull request title check / conventional pull request title check (pull_request) Successful in 4s
conventional commit messages check / conventional commit messages check (pull_request) Failing after 6s
dotenv-linter check / dotenv-linter check (pull_request) Successful in 7s
GitLeaks check / GitLeaks check (pull_request) Successful in 15s
hadolint check / hadolint check (pull_request) Successful in 15s
htmlhint check / htmlhint check (pull_request) Successful in 36s
Prettier check / Prettier check (pull_request) Successful in 54s
checkov check / checkov check (pull_request) Successful in 1m20s
markdownlint check / markdownlint check (pull_request) Successful in 57s
ShellCheck check / ShellCheck check (pull_request) Successful in 28s
Stylelint check / Stylelint check (pull_request) Successful in 27s
yamllint check / yamllint check (pull_request) Successful in 30s
Rust check / Rust check (pull_request) Successful in 23m59s
2026-01-25 09:41:46 +01:00
8dbb1dc48e feat: make the cursor of subtask buttons pointer (#124) 2026-01-24 20:53:48 +00:00
ee950aa4a9 feat: make the cursor of subtask buttons pointer
All checks were successful
actionlint check / actionlint check (pull_request) Successful in 7s
conventional commit messages check / conventional commit messages check (pull_request) Successful in 6s
conventional pull request title check / conventional pull request title check (pull_request) Successful in 3s
dotenv-linter check / dotenv-linter check (pull_request) Successful in 8s
GitLeaks check / GitLeaks check (pull_request) Successful in 12s
hadolint check / hadolint check (pull_request) Successful in 41s
htmlhint check / htmlhint check (pull_request) Successful in 1m1s
checkov check / checkov check (pull_request) Successful in 1m18s
markdownlint check / markdownlint check (pull_request) Successful in 59s
Prettier check / Prettier check (pull_request) Successful in 27s
ShellCheck check / ShellCheck check (pull_request) Successful in 34s
Stylelint check / Stylelint check (pull_request) Successful in 32s
yamllint check / yamllint check (pull_request) Successful in 34s
Rust check / Rust check (pull_request) Successful in 16m14s
2026-01-24 21:34:30 +01:00
439cc012f0 chore: make the Rust builds locked (#123) 2026-01-24 20:08:34 +00:00
803fe36ed1 feat: update Font Awesome to 7.1.0 (#122) 2026-01-24 20:08:26 +00:00
80918b98d9 chore: make the Rust builds locked
All checks were successful
actionlint check / actionlint check (pull_request) Successful in 7s
conventional pull request title check / conventional pull request title check (pull_request) Successful in 4s
conventional commit messages check / conventional commit messages check (pull_request) Successful in 7s
dotenv-linter check / dotenv-linter check (pull_request) Successful in 10s
GitLeaks check / GitLeaks check (pull_request) Successful in 13s
hadolint check / hadolint check (pull_request) Successful in 39s
checkov check / checkov check (pull_request) Successful in 1m36s
htmlhint check / htmlhint check (pull_request) Successful in 1m19s
markdownlint check / markdownlint check (pull_request) Successful in 1m14s
Prettier check / Prettier check (pull_request) Successful in 50s
ShellCheck check / ShellCheck check (pull_request) Successful in 2m1s
Stylelint check / Stylelint check (pull_request) Successful in 2m4s
yamllint check / yamllint check (pull_request) Successful in 2m20s
Rust check / Rust check (pull_request) Successful in 21m31s
2026-01-24 20:00:16 +01:00
d4235ef2ab feat: update Font Awesome to 7.1.0
All checks were successful
actionlint check / actionlint check (pull_request) Successful in 39s
conventional pull request title check / conventional pull request title check (pull_request) Successful in 8s
conventional commit messages check / conventional commit messages check (pull_request) Successful in 10s
GitLeaks check / GitLeaks check (pull_request) Successful in 46s
dotenv-linter check / dotenv-linter check (pull_request) Successful in 48s
hadolint check / hadolint check (pull_request) Successful in 51s
checkov check / checkov check (pull_request) Successful in 2m37s
htmlhint check / htmlhint check (pull_request) Successful in 1m9s
markdownlint check / markdownlint check (pull_request) Successful in 50s
Prettier check / Prettier check (pull_request) Successful in 30s
ShellCheck check / ShellCheck check (pull_request) Successful in 55s
Stylelint check / Stylelint check (pull_request) Successful in 54s
yamllint check / yamllint check (pull_request) Successful in 1m6s
Rust check / Rust check (pull_request) Successful in 24m52s
2026-01-24 17:52:43 +01:00
d0a124ee75 fix: font displaying (#120) 2026-01-24 11:30:45 +00:00
67a8a3fae6 fix: font displaying
All checks were successful
actionlint check / actionlint check (pull_request) Successful in 6s
dotenv-linter check / dotenv-linter check (pull_request) Successful in 6s
conventional pull request title check / conventional pull request title check (pull_request) Successful in 3s
conventional commit messages check / conventional commit messages check (pull_request) Successful in 6s
GitLeaks check / GitLeaks check (pull_request) Successful in 13s
hadolint check / hadolint check (pull_request) Successful in 33s
htmlhint check / htmlhint check (pull_request) Successful in 40s
checkov check / checkov check (pull_request) Successful in 1m18s
markdownlint check / markdownlint check (pull_request) Successful in 57s
Prettier check / Prettier check (pull_request) Successful in 34s
ShellCheck check / ShellCheck check (pull_request) Successful in 32s
Stylelint check / Stylelint check (pull_request) Successful in 30s
yamllint check / yamllint check (pull_request) Successful in 34s
Rust check / Rust check (pull_request) Successful in 11m1s
2026-01-24 12:16:16 +01:00
baef08475f chore: bump the Dioxus and Diesel CLI (#117) 2026-01-24 09:44:33 +00:00
265516c1a6 chore: bump the Dioxus and Diesel CLI
All checks were successful
conventional pull request title check / conventional pull request title check (pull_request) Successful in 10s
dotenv-linter check / dotenv-linter check (pull_request) Successful in 26s
GitLeaks check / GitLeaks check (pull_request) Successful in 23s
hadolint check / hadolint check (pull_request) Successful in 52s
htmlhint check / htmlhint check (pull_request) Successful in 1m3s
markdownlint check / markdownlint check (pull_request) Successful in 1m1s
Prettier check / Prettier check (pull_request) Successful in 52s
yamllint check / yamllint check (pull_request) Successful in 27s
Stylelint check / Stylelint check (pull_request) Successful in 33s
ShellCheck check / ShellCheck check (pull_request) Successful in 9m9s
Rust check / Rust check (pull_request) Successful in 29m35s
actionlint check / actionlint check (pull_request) Successful in 18s
conventional commit messages check / conventional commit messages check (pull_request) Successful in 13s
checkov check / checkov check (pull_request) Successful in 1m37s
2026-01-24 00:44:51 +01:00
01c9b2d0fb fix: automatically reconnect after losing a WebSocket connection (#112) 2026-01-23 19:32:12 +00:00
4ad96fe92f fix: Android bundle Docker compose build (#114) 2026-01-23 19:32:05 +00:00
df1d23c0e3 chore: bump dependencies (#116) 2026-01-23 19:31:59 +00:00
ce73256133 fix: automatically reconnect after losing a WebSocket connection
All checks were successful
actionlint check / actionlint check (pull_request) Successful in 12s
conventional commit messages check / conventional commit messages check (pull_request) Successful in 22s
conventional pull request title check / conventional pull request title check (pull_request) Successful in 4s
checkov check / checkov check (pull_request) Successful in 1m19s
dotenv-linter check / dotenv-linter check (pull_request) Successful in 10s
GitLeaks check / GitLeaks check (pull_request) Successful in 13s
hadolint check / hadolint check (pull_request) Successful in 11s
htmlhint check / htmlhint check (pull_request) Successful in 41s
markdownlint check / markdownlint check (pull_request) Successful in 34s
Prettier check / Prettier check (pull_request) Successful in 37s
ShellCheck check / ShellCheck check (pull_request) Successful in 25s
Stylelint check / Stylelint check (pull_request) Successful in 40s
yamllint check / yamllint check (pull_request) Successful in 29s
Rust check / Rust check (pull_request) Successful in 1h1m8s
2026-01-23 18:01:39 +01:00
fd661ba7e4 fix: Android bundle Docker compose build
All checks were successful
actionlint check / actionlint check (pull_request) Successful in 12s
conventional pull request title check / conventional pull request title check (pull_request) Successful in 6s
conventional commit messages check / conventional commit messages check (pull_request) Successful in 12s
dotenv-linter check / dotenv-linter check (pull_request) Successful in 14s
GitLeaks check / GitLeaks check (pull_request) Successful in 20s
hadolint check / hadolint check (pull_request) Successful in 17s
markdownlint check / markdownlint check (pull_request) Successful in 39s
checkov check / checkov check (pull_request) Successful in 1m23s
htmlhint check / htmlhint check (pull_request) Successful in 49s
ShellCheck check / ShellCheck check (pull_request) Successful in 28s
Prettier check / Prettier check (pull_request) Successful in 33s
Stylelint check / Stylelint check (pull_request) Successful in 30s
yamllint check / yamllint check (pull_request) Successful in 36s
Rust check / Rust check (pull_request) Successful in 1h58m0s
2026-01-23 18:01:23 +01:00
316d45fe0c chore: bump dependencies
All checks were successful
conventional pull request title check / conventional pull request title check (pull_request) Successful in 8s
actionlint check / actionlint check (pull_request) Successful in 15s
conventional commit messages check / conventional commit messages check (pull_request) Successful in 16s
dotenv-linter check / dotenv-linter check (pull_request) Successful in 23s
GitLeaks check / GitLeaks check (pull_request) Successful in 23s
hadolint check / hadolint check (pull_request) Successful in 24s
Prettier check / Prettier check (pull_request) Successful in 53s
markdownlint check / markdownlint check (pull_request) Successful in 56s
htmlhint check / htmlhint check (pull_request) Successful in 1m0s
checkov check / checkov check (pull_request) Successful in 1m47s
ShellCheck check / ShellCheck check (pull_request) Successful in 35s
yamllint check / yamllint check (pull_request) Successful in 37s
Stylelint check / Stylelint check (pull_request) Successful in 43s
Rust check / Rust check (pull_request) Successful in 1h12m38s
2026-01-23 18:00:32 +01:00
00bb8d7951 ci: update the Rust check dependencies (#115) 2026-01-23 16:59:59 +00:00
393173f218 ci: update the Rust check dependencies
All checks were successful
actionlint check / actionlint check (pull_request) Successful in 6s
conventional commit messages check / conventional commit messages check (pull_request) Successful in 7s
conventional pull request title check / conventional pull request title check (pull_request) Successful in 6s
dotenv-linter check / dotenv-linter check (pull_request) Successful in 10s
GitLeaks check / GitLeaks check (pull_request) Successful in 10s
hadolint check / hadolint check (pull_request) Successful in 30s
htmlhint check / htmlhint check (pull_request) Successful in 44s
checkov check / checkov check (pull_request) Successful in 1m6s
markdownlint check / markdownlint check (pull_request) Successful in 48s
Prettier check / Prettier check (pull_request) Successful in 27s
ShellCheck check / ShellCheck check (pull_request) Successful in 25s
yamllint check / yamllint check (pull_request) Successful in 25s
Stylelint check / Stylelint check (pull_request) Successful in 27s
Rust check / Rust check (pull_request) Successful in 14m21s
2026-01-23 17:28:58 +01:00
8fd5fe2d4e chore: remove unused CSS configuration (#110) 2026-01-11 08:53:33 +00:00
d165e58443 chore: remove unused CSS configuration
All checks were successful
actionlint check / actionlint check (pull_request) Successful in 6s
conventional pull request title check / conventional pull request title check (pull_request) Successful in 3s
conventional commit messages check / conventional commit messages check (pull_request) Successful in 5s
dotenv-linter check / dotenv-linter check (pull_request) Successful in 9s
GitLeaks check / GitLeaks check (pull_request) Successful in 11s
hadolint check / hadolint check (pull_request) Successful in 18s
htmlhint check / htmlhint check (pull_request) Successful in 46s
markdownlint check / markdownlint check (pull_request) Successful in 42s
Prettier check / Prettier check (pull_request) Successful in 33s
checkov check / checkov check (pull_request) Successful in 1m30s
ShellCheck check / ShellCheck check (pull_request) Successful in 32s
Stylelint check / Stylelint check (pull_request) Successful in 34s
yamllint check / yamllint check (pull_request) Successful in 32s
Rust check / Rust check (pull_request) Successful in 23m25s
2026-01-10 13:00:51 +01:00
89 changed files with 1075 additions and 3670 deletions

View File

@@ -21,9 +21,9 @@ jobs:
run: >
apt-get update && apt-get install -y
libgtk-3-dev=3.24.33-1ubuntu2.2
libjavascriptcoregtk-4.1-dev=2.50.3-0ubuntu0.22.04.1
libjavascriptcoregtk-4.1-dev=2.50.4-0ubuntu0.22.04.1
libsoup-3.0-dev=3.0.7-0ubuntu1
libwebkit2gtk-4.1-dev=2.50.3-0ubuntu0.22.04.1
libwebkit2gtk-4.1-dev=2.50.4-0ubuntu0.22.04.1
libxdo-dev=1:3.20160805.1-4
- name: Rust toolchain installation
uses: dtolnay/rust-toolchain@9bc92bc5598b4f3bec5d910d352094982cb0c3b9 # 1.92.0
@@ -40,6 +40,6 @@ jobs:
- name: rustfmt check
run: cargo fmt --all --check
- name: Clippy check
run: cargo clippy --all-targets --all-features -- --deny warnings
run: cargo clippy --locked --all-targets --all-features -- --deny warnings
- name: test check
run: cargo test --all --all-targets --all-features
run: cargo --locked test --all --all-targets --all-features

177
Cargo.lock generated
View File

@@ -534,9 +534,9 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.42"
version = "0.4.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
dependencies = [
"iana-time-zone",
"js-sys",
@@ -634,7 +634,8 @@ dependencies = [
[[package]]
name = "const-serialize"
version = "0.8.0-alpha.0"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e42cd5aabba86f128b3763da1fec1491c0f728ce99245062cd49b6f9e6d235b"
dependencies = [
"const-serialize 0.7.2",
"const-serialize-macro 0.8.0-alpha.0",
@@ -655,7 +656,8 @@ dependencies = [
[[package]]
name = "const-serialize-macro"
version = "0.8.0-alpha.0"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42571ed01eb46d2e1adcf99c8ca576f081e46f2623d13500eba70d1d99a4c439"
dependencies = [
"proc-macro2",
"quote",
@@ -1014,9 +1016,9 @@ dependencies = [
[[package]]
name = "diesel"
version = "2.3.4"
version = "2.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c415189028b232660655e4893e8bc25ca7aee8e96888db66d9edb400535456a"
checksum = "d9b6c2fc184a6fb6ebcf5f9a5e3bbfa84d8fd268cdfcce4ed508979a6259494d"
dependencies = [
"bitflags 2.10.0",
"byteorder",
@@ -1074,7 +1076,8 @@ dependencies = [
[[package]]
name = "dioxus"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92b583b48ac77158495e6678fe3a2b5954fc8866fc04cb9695dd146e88bc329d"
dependencies = [
"dioxus-asset-resolver",
"dioxus-cli-config",
@@ -1107,7 +1110,8 @@ dependencies = [
[[package]]
name = "dioxus-asset-resolver"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0161af1d3cfc8ff31503ff1b7ee0068c97771fc38d0cc6566e23483142ddf4f"
dependencies = [
"dioxus-cli-config",
"http",
@@ -1127,7 +1131,8 @@ dependencies = [
[[package]]
name = "dioxus-cli-config"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccd67ab405e1915a47df9769cd5408545d1b559d5c01ce7a0f442caef520d1f3"
dependencies = [
"wasm-bindgen",
]
@@ -1135,7 +1140,8 @@ dependencies = [
[[package]]
name = "dioxus-config-macro"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f040ec7c41aa5428283f56bb0670afba9631bfe3ffd885f4814807f12c8c9d91"
dependencies = [
"proc-macro2",
"quote",
@@ -1144,12 +1150,14 @@ dependencies = [
[[package]]
name = "dioxus-config-macros"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10c41b47b55a433b61f7c12327c85ba650572bacbcc42c342ba2e87a57975264"
[[package]]
name = "dioxus-core"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b389b0e3cc01c7da292ad9b884b088835fdd1671d45fbd2f737506152b22eef0"
dependencies = [
"anyhow",
"const_format",
@@ -1170,7 +1178,8 @@ dependencies = [
[[package]]
name = "dioxus-core-macro"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82d65f0024fc86f01911a16156d280eea583be5a82a3bed85e7e8e4194302d"
dependencies = [
"convert_case 0.8.0",
"dioxus-rsx",
@@ -1182,12 +1191,14 @@ dependencies = [
[[package]]
name = "dioxus-core-types"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfc4b8cdc440a55c17355542fc2089d97949bba674255d84cac77805e1db8c9f"
[[package]]
name = "dioxus-desktop"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e6ec66749d1556636c5b4f661495565c155a7f78a46d4d007d7478c6bdc288c"
dependencies = [
"async-trait",
"base64",
@@ -1241,7 +1252,8 @@ dependencies = [
[[package]]
name = "dioxus-devtools"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf89488bad8fb0f18b9086ee2db01f95f709801c10c68be42691a36378a0f2d"
dependencies = [
"dioxus-cli-config",
"dioxus-core",
@@ -1260,7 +1272,8 @@ dependencies = [
[[package]]
name = "dioxus-devtools-types"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e7381d9d7d0a0f66b9d5082d584853c3d53be21d34007073daca98ddf26fc4d"
dependencies = [
"dioxus-core",
"serde",
@@ -1270,7 +1283,8 @@ dependencies = [
[[package]]
name = "dioxus-document"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba0aeeff26d9d06441f59fd8d7f4f76098ba30ca9728e047c94486161185ceb"
dependencies = [
"dioxus-core",
"dioxus-core-macro",
@@ -1285,10 +1299,19 @@ dependencies = [
"tracing",
]
[[package]]
name = "dioxus-free-icons"
version = "0.10.0"
source = "git+https://github.com/matous-volf/dioxus-free-icons?rev=6488400003a3d6829e771a84a565c5c5f08a9aa0#6488400003a3d6829e771a84a565c5c5f08a9aa0"
dependencies = [
"dioxus",
]
[[package]]
name = "dioxus-fullstack"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7db1f8b70338072ec408b48d09c96559cf071f87847465d8161294197504c498"
dependencies = [
"anyhow",
"async-stream",
@@ -1352,7 +1375,8 @@ dependencies = [
[[package]]
name = "dioxus-fullstack-core"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda8b152e85121243741b9d5f2a3d8cb3c47a7b2299e902f98b6a7719915b0a2"
dependencies = [
"anyhow",
"axum-core",
@@ -1379,7 +1403,8 @@ dependencies = [
[[package]]
name = "dioxus-fullstack-macro"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "255104d4a4f278f1a8482fa30536c91d22260c561c954b753e72987df8d65b2e"
dependencies = [
"const_format",
"convert_case 0.8.0",
@@ -1392,7 +1417,8 @@ dependencies = [
[[package]]
name = "dioxus-history"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d00ba43bfe6e5ca226fef6128f240ca970bea73cac0462416188026360ccdcf"
dependencies = [
"dioxus-core",
"tracing",
@@ -1401,7 +1427,8 @@ dependencies = [
[[package]]
name = "dioxus-hooks"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dab2da4f038c33cb38caa37ffc3f5d6dfbc018f05da35b238210a533bb075823"
dependencies = [
"dioxus-core",
"dioxus-signals",
@@ -1416,7 +1443,8 @@ dependencies = [
[[package]]
name = "dioxus-html"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded5fa6d2e677b7442a93f4228bf3c0ad2597a8bd3292cae50c869d015f3a99"
dependencies = [
"async-trait",
"bytes",
@@ -1442,7 +1470,8 @@ dependencies = [
[[package]]
name = "dioxus-html-internal-macro"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45462ab85fe059a36841508d40545109fd0e25855012d22583a61908eb5cd02a"
dependencies = [
"convert_case 0.8.0",
"proc-macro2",
@@ -1453,6 +1482,8 @@ dependencies = [
[[package]]
name = "dioxus-i18n"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ceebf715471a986307cdfe422d645c0784602003758171102ba9225624be9f55"
dependencies = [
"dioxus",
"fluent",
@@ -1464,7 +1495,8 @@ dependencies = [
[[package]]
name = "dioxus-interpreter-js"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a42a7f73ad32a5054bd8c1014f4ac78cca3b7f6889210ee2b57ea31b33b6d32f"
dependencies = [
"dioxus-core",
"dioxus-core-types",
@@ -1483,7 +1515,8 @@ dependencies = [
[[package]]
name = "dioxus-liveview"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3f7a1cfe6f8e9f2e303607c8ae564d11932fd80714c8a8c97e3860d55538997"
dependencies = [
"axum",
"dioxus-cli-config",
@@ -1510,7 +1543,8 @@ dependencies = [
[[package]]
name = "dioxus-logger"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1eeab114cb009d9e6b85ea10639a18cfc54bb342f3b837770b004c4daeb89c2"
dependencies = [
"dioxus-cli-config",
"tracing",
@@ -1521,7 +1555,8 @@ dependencies = [
[[package]]
name = "dioxus-router"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d5b31f9e27231389bf5a117b7074d22d8c58358b484a2558e56fbab20e64ca4"
dependencies = [
"dioxus-cli-config",
"dioxus-core",
@@ -1541,7 +1576,8 @@ dependencies = [
[[package]]
name = "dioxus-router-macro"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "838b9b441a95da62b39cae4defd240b5ebb0ec9f2daea1126099e00a838dc86f"
dependencies = [
"base16",
"digest",
@@ -1555,7 +1591,8 @@ dependencies = [
[[package]]
name = "dioxus-rsx"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53128858f0ccca9de54292a4d48409fda1df75fd5012c6243f664042f0225d68"
dependencies = [
"proc-macro2",
"proc-macro2-diagnostics",
@@ -1567,7 +1604,8 @@ dependencies = [
[[package]]
name = "dioxus-server"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adb2d4e0f0f3a157bda6af2d90f22bac40070e509a66e3ea58abf3b35f904c"
dependencies = [
"anyhow",
"async-trait",
@@ -1624,7 +1662,8 @@ dependencies = [
[[package]]
name = "dioxus-signals"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f48020bc23bc9766e7cce986c0fd6de9af0b8cbfd432652ec6b1094439c1ec6"
dependencies = [
"dioxus-core",
"futures-channel",
@@ -1639,7 +1678,8 @@ dependencies = [
[[package]]
name = "dioxus-ssr"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44cf9294a21fcd1098e02ad7a3ba61b99be8310ad3395fecf8210387c83f26b9"
dependencies = [
"askama_escape",
"dioxus-core",
@@ -1650,7 +1690,8 @@ dependencies = [
[[package]]
name = "dioxus-stores"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77aaa9ac56d781bb506cf3c0d23bea96b768064b89fe50d3b4d4659cc6bd8058"
dependencies = [
"dioxus-core",
"dioxus-signals",
@@ -1661,7 +1702,8 @@ dependencies = [
[[package]]
name = "dioxus-stores-macro"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b1a728622e7b63db45774f75e71504335dd4e6115b235bbcff272980499493a"
dependencies = [
"convert_case 0.8.0",
"proc-macro2",
@@ -1672,7 +1714,8 @@ dependencies = [
[[package]]
name = "dioxus-web"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b33fe739fed4e8143dac222a9153593f8e2451662ce8fc4c9d167a9d6ec0923"
dependencies = [
"dioxus-cli-config",
"dioxus-core",
@@ -2302,7 +2345,8 @@ dependencies = [
[[package]]
name = "generational-box"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4ed190b9de8e734d47a70be59b1e7588b9e8e0d0036e332f4c014e8aed1bc5"
dependencies = [
"parking_lot",
"tracing",
@@ -3106,7 +3150,8 @@ dependencies = [
[[package]]
name = "lazy-js-bundle"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7b88b715ab1496c6e6b8f5e927be961c4235196121b6ae59bcb51077a21dd36"
[[package]]
name = "lazy_static"
@@ -3288,7 +3333,8 @@ dependencies = [
[[package]]
name = "manganis"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cce7d688848bf9d034168513b9a2ffbfe5f61df2ff14ae15e6cfc866efdd344"
dependencies = [
"const-serialize 0.7.2",
"const-serialize 0.8.0-alpha.0",
@@ -3299,7 +3345,8 @@ dependencies = [
[[package]]
name = "manganis-core"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84ce917b978268fe8a7db49e216343ec7c8f471f7e686feb70940d67293f19d4"
dependencies = [
"const-serialize 0.7.2",
"const-serialize 0.8.0-alpha.0",
@@ -3312,7 +3359,8 @@ dependencies = [
[[package]]
name = "manganis-macro"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad513e990f7c0bca86aa68659a7a3dc4c705572ed4c22fd6af32ccf261334cc2"
dependencies = [
"dunce",
"macro-string",
@@ -4768,15 +4816,15 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.145"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
"zmij",
]
[[package]]
@@ -5096,7 +5144,8 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subsecond"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8438668e545834d795d04c4335aafc332ce046106521a29f0a5c6501de34187c"
dependencies = [
"js-sys",
"libc",
@@ -5114,7 +5163,8 @@ dependencies = [
[[package]]
name = "subsecond-types"
version = "0.7.3"
source = "git+https://github.com/matous-volf/dioxus?rev=627d5ca5b80aeed57c23e253024665f103117f5e#627d5ca5b80aeed57c23e253024665f103117f5e"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e72f747606fc19fe81d6c59e491af93ed7dcbcb6aad9d1d18b05129914ec298"
dependencies = [
"serde",
]
@@ -5334,30 +5384,30 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.44"
version = "0.3.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.6"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca"
[[package]]
name = "time-macros"
version = "0.2.24"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd"
dependencies = [
"num-conv",
"time-core",
@@ -5398,6 +5448,7 @@ dependencies = [
"diesel",
"diesel_migrations",
"dioxus",
"dioxus-free-icons",
"dioxus-html",
"dioxus-i18n",
"feruca",
@@ -5416,9 +5467,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.48.0"
version = "1.49.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
dependencies = [
"bytes",
"libc",
@@ -5647,9 +5698,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.43"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"log",
"pin-project-lite",
@@ -5670,9 +5721,9 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.35"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
]
@@ -6896,3 +6947,9 @@ dependencies = [
"quote",
"syn 2.0.111",
]
[[package]]
name = "zmij"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65"

View File

@@ -7,19 +7,21 @@ edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
chrono = { version = "0.4.42", features = ["serde", "unstable-locales"] }
dioxus = { git = "https://github.com/matous-volf/dioxus", rev = "627d5ca5b80aeed57c23e253024665f103117f5e", features = ["fullstack", "router"] }
chrono = { version = "0.4.43", features = ["serde", "unstable-locales"] }
# Remember to update the CLI as well.
dioxus = { version = "0.7.3", features = ["fullstack", "router"] }
# TODO: Remove this once https://github.com/DioxusLabs/dioxus/issues/4765 is resolved.
dioxus-html = { git = "https://github.com/matous-volf/dioxus", rev = "627d5ca5b80aeed57c23e253024665f103117f5e", features = ["serialize"] }
dioxus-html = { version = "0.7.3", features = ["serialize"] }
feruca = { version = "0.11.5" }
serde = { version = "1.0.228" }
serde_json = { version = "1.0.145" }
serde_json = { version = "1.0.149" }
serde_with = { version = "3.16.1", features = ["chrono_0_4"] }
tracing = "0.1.43"
tracing = "0.1.44"
unic-langid-impl = { version = "0.9.6", features = ["serde"] }
validator = { version = "0.20.0", features = ["derive"] }
diesel = { version = "2.3.4", features = [
# Remember to update the CLI as well.
diesel = { version = "2.3.6", features = [
"chrono",
"postgres",
"postgres_backend",
@@ -29,13 +31,18 @@ diesel_migrations = { version = "2.3.1", features = [
"postgres",
], optional = true }
rand = { version = "0.9.2", optional = true }
time = { version = "0.3.44", optional = true }
tokio = { version = "1.48.0", optional = true }
time = { version = "0.3.45", optional = true }
tokio = { version = "1.49.0", optional = true }
async-std = { version = "1.13.2", optional = true }
dioxus-i18n = { path = "dioxus-i18n" }
dioxus-i18n = "0.5.1"
voca_rs = "1.15.2"
load-dotenv = "0.1.2"
# TODO: Switch to upstream once it merges the changes.
dioxus-free-icons = { git = "https://github.com/matous-volf/dioxus-free-icons", rev = "6488400003a3d6829e771a84a565c5c5f08a9aa0", features = [
"font-awesome-regular",
"font-awesome-solid",
] }
[features]
default = ["web"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,203 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="512"
height="512"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
sodipodi:docname="icon.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
inkscape:zoom="1.1020922"
inkscape:cx="188.27826"
inkscape:cy="204.15715"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect1"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="display:inline;fill:#27272a;fill-opacity:1;stroke:none;stroke-width:32;stroke-dasharray:none;stroke-opacity:1"
id="rect1"
width="512"
height="512"
x="0"
y="0"
sodipodi:insensitive="true"
inkscape:label="background"
ry="128.00018"
sodipodi:type="rect"
rx="129.98714" />
<g
id="g17"
inkscape:label="logo"
transform="translate(8)">
<g
id="g8"
inkscape:label="ring">
<g
id="g7"
inkscape:label="back">
<circle
style="fill:#d97706;fill-opacity:1;stroke:#d97706;stroke-width:32;stroke-dasharray:none;stroke-opacity:1"
id="path1"
cx="224"
cy="256"
r="128"
inkscape:label="ring back" />
<rect
style="fill:#d97706;fill-opacity:1;stroke:none;stroke-width:21.8936;stroke-dasharray:none;stroke-opacity:1"
id="rect2"
width="48"
height="288"
x="224"
y="112"
inkscape:label="rect2" />
</g>
<g
id="g1"
transform="translate(-4.163147,-0.69235229)"
inkscape:label="front">
<circle
style="fill:#27272a;fill-opacity:1;stroke:none;stroke-width:32;stroke-dasharray:none;stroke-opacity:1"
id="path1-5-2"
cx="276.16315"
cy="256.69235"
r="128" />
<circle
style="fill:none;fill-opacity:1;stroke:#fbbf24;stroke-width:32;stroke-dasharray:none;stroke-opacity:1"
id="path1-5"
cx="276.16315"
cy="256.69235"
r="128" />
</g>
</g>
<g
id="g16"
inkscape:label="tick"
transform="translate(16.000231,-8.3918418e-5)">
<g
id="g6"
transform="rotate(45,-57.96574,415.4208)"
style="fill:#d97706;fill-opacity:1;stroke:none;stroke-opacity:1"
inkscape:label="back">
<rect
style="fill:#d97706;fill-opacity:1;stroke:none;stroke-width:32;stroke-dasharray:none;stroke-opacity:1"
id="rect9"
width="32.000458"
height="32"
x="273.94067"
y="210.74516"
transform="rotate(-45,-57.96574,415.4208)" />
<rect
style="fill:#d97706;fill-opacity:1;stroke:none;stroke-width:32;stroke-dasharray:none;stroke-opacity:1"
id="rect8"
width="32.000458"
height="32"
x="206.05841"
y="233.37257"
transform="rotate(-45,-57.96574,415.4208)" />
<rect
style="fill:#d97706;fill-opacity:1;stroke:none;stroke-width:32;stroke-dasharray:none;stroke-opacity:1"
id="rect7"
height="16"
x="228.686"
y="285.255"
width="32"
transform="rotate(-45,-57.96574,415.4208)" />
<rect
style="fill:#d97706;fill-opacity:1;stroke:none;stroke-width:32;stroke-dasharray:none;stroke-opacity:1"
id="rect5"
width="64"
height="32"
x="0"
y="100" />
<rect
style="fill:#d97706;fill-opacity:1;stroke:none;stroke-width:39.1918;stroke-dasharray:none;stroke-opacity:1"
id="rect6"
width="32"
height="96"
x="32"
y="36" />
</g>
<g
id="g4"
transform="rotate(45,-41.965512,454.04877)"
style="fill:#fbbf24;fill-opacity:1;stroke:none;stroke-opacity:1"
inkscape:label="front">
<rect
style="fill:#fbbf24;fill-opacity:1;stroke:none;stroke-width:32;stroke-dasharray:none;stroke-opacity:1"
id="rect3"
width="64"
height="32"
x="0"
y="100" />
<rect
style="fill:#fbbf24;fill-opacity:1;stroke:none;stroke-width:39.1918;stroke-dasharray:none;stroke-opacity:1"
id="rect4"
width="32"
height="96"
x="32"
y="36" />
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -3,8 +3,8 @@
"short_name": "Todo Baggins",
"start_url": "/",
"display": "standalone",
"background_color": "#27272a",
"theme_color": "#27272a",
"background_color": "#101828",
"theme_color": "#b89a2e",
"icons": [
{
"src": "/assets/images/icon.png",

View File

@@ -1,17 +0,0 @@
@layer base {
@font-face {
font-family: Inter;
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url("/assets/fonts/inter_variable.woff2") format("woff2");
}
@font-face {
font-family: Inter;
font-style: italic;
font-weight: 100 900;
font-display: swap;
src: url("/assets/fonts/inter_variable_italic.woff2") format("woff2");
}
}

View File

@@ -8,56 +8,37 @@ input[type="range"] {
background: transparent;
}
input[type="range"]::-moz-range-thumb {
input[type="range"]::-moz-range-thumb,
input[type="range"]::-webkit-slider-thumb {
width: 1.25rem;
height: 1.25rem;
background: rgba(228 228 231);
background: var(--color-gray-400);
filter: drop-shadow(0 var(--spacing) 0 var(--color-gray-500));
border: 0;
border-radius: 0.5rem;
cursor: pointer;
}
input[type="range"]::-moz-range-progress {
background: #525259;
input[type="range"]::-webkit-slider-thumb {
position: relative;
top: -9px;
}
input[type="range"]::-moz-range-track,
input[type="range"]::-webkit-slider-runnable-track {
background: var(--color-gray-800-muted);
height: 0.5rem;
filter: drop-shadow(
0 calc(0px - var(--spacing)) 0 var(--color-gray-900-muted)
);
border-radius: 0.25rem;
}
input[type="range"]::-moz-range-track {
background: rgba(39 39 42 / 50%);
height: 0.5rem;
border-radius: 0.25rem;
}
input[type="range"].input-range-reverse::-moz-range-progress {
background: #2d2d31;
height: 0.5rem;
border-radius: 0.25rem;
}
input[type="range"].input-range-reverse::-moz-range-track {
background: rgba(113 113 122 / 50%);
height: 0.5rem;
border-radius: 0.25rem;
}
input[type="range"]::-webkit-slider-thumb {
width: 1.25rem;
height: 1.25rem;
background: rgba(228 228 231);
border: 0;
border-radius: 0.5rem;
position: relative;
top: -0.4rem;
transform: translateY(3px);
}
input[type="range"]::-webkit-slider-runnable-track {
background: rgba(39 39 42 / 50%);
height: 0.5rem;
border-radius: 0.25rem;
}
input[type="range"].input-range-reverse::-webkit-slider-runnable-track {
background: rgba(39 39 42 / 50%);
height: 0.5rem;
border-radius: 0.25rem;
position: relative;
top: 3px;
}

View File

@@ -0,0 +1,7 @@
select {
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 640'%3E%3Cpath fill='%239ca3af' d='M300.3 440.8C312.9 451 331.4 450.3 343.1 438.6L471.1 310.6C480.3 301.4 483 287.7 478 275.7C473 263.7 461.4 256 448.5 256L192.5 256C179.6 256 167.9 263.8 162.9 275.8C157.9 287.8 160.7 301.5 169.9 310.6L297.9 438.6L300.3 440.8z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-size: 2rem;
background-position: right 0.5rem center;
}

View File

@@ -1,48 +0,0 @@
name-template: "Release v$RESOLVED_VERSION 🦀"
tag-template: "v$RESOLVED_VERSION"
categories:
- title: "🚀 Features"
label: "feature"
- title: "🐛 Bug Fixes"
label: "bug"
- title: "♻️ Refactor"
label: "refactor"
- title: "📝 Documentation"
label: "documentation"
- title: "🧰 Maintenance"
labels:
- "chore"
- "dependencies"
change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
version-resolver:
major:
labels:
- "major"
minor:
labels:
- "minor"
patch:
labels:
- "patch"
default: patch
template: |
## Changes
$CHANGES
autolabeler:
- label: feature
branch:
- "/^feat(ure)?[/-].+/"
- label: bug
branch:
- "/^fix[/-].+/"
- label: refactor
branch:
- "/(refactor|refactoring)[/-].+/"
- label: documentation
branch:
- "/doc(s|umentation)[/-].+/"
- label: chore
branch:
- "/^chore[/-].+/"

View File

@@ -1,19 +0,0 @@
name: Cargo Publish
on:
workflow_dispatch:
release:
types: [published]
env:
CARGO_TERM_COLOR: always
jobs:
publish:
name: Publish to crate.io
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: cargo publish
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

View File

@@ -1,19 +0,0 @@
name: Release Drafter
on:
push:
branches:
- main
pull_request:
types: [opened, reopened, synchronize]
jobs:
update_release_draft:
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,40 +0,0 @@
name: Test Runs
on:
push:
branches:
- main
pull_request:
types: [opened, reopened, synchronize]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Updates
run: |
sudo apt update
sudo apt install libwebkit2gtk-4.1-dev \
build-essential \
libxdo-dev \
libssl-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libglib2.0-dev
- name: Checkout
uses: actions/checkout@v4
- name: Lint
run: cargo clippy -- -D warnings
- name: Test
run: cargo test
- name: Compile
run: |
rustup target add wasm32-unknown-unknown
cargo build --target wasm32-unknown-unknown
cargo build --release

View File

@@ -1,2 +0,0 @@
Cargo.lock
/target

View File

@@ -1,117 +0,0 @@
# Changelog
## [0.4.3]
- [Issue #19](https://github.com/dioxus-community/dioxus-i18n/issues/19) Enable use of "message-id.attribute-id"
syntax in the `t!`, `te!`, `tid!` macros in order to extract attribute definition, e.g. `t!("mycomponent.placeholder")`
in:
```
mycomponent = Component Name
.placeholder = Some placeholder
.aria-text = Some aria text
```
- Added examples for all fluent grammar constructs and configuration variants.
## [0.4.2] 2025-02-08
### Fixed
- [Issue #15](https://github.com/dioxus-community/dioxus-i18n/issues/15) Recent change to t! macro unnecessarily breaks v0.3 code.
### Amended
- t! macro amended to use unwrap_or_else rather than panic!.
- Error messages made consistant across all macros.
## [0.4.1] 2025-02-02
### Added
- New methods (`I18nConfig::with_auto_locales`) to determine supported locales from deep search for translation files.
- New methods returning `Result<_, Error>` rather than `panic!`, such that:
| __`panic!` version__ | __`Result<_, Error>` vesion__ |
|-----------------------------------------|------------------------------------------|
| `LocaleResource::to_resource_string` | `LocaleResource::try_to_resource_string` |
| `I18n::translate` | `I18n::try_translate` |
| `I18n::translate_with_args` | `I18n::try_translate_with_args` |
| `I18n::set_fallback_language` | `I18n::try_set_fallback_language` |
| `I18n::set_language` | `I18n::try_set_language` |
| `use_init_i18n` | `try_use_init_i18n` |
| `I18nConfig::with_auto_locales` | `I18nConfig::try_with_auto_locales` |
- New `te!` macro which acts like `t!` but returns `Error`.
- New `tid!` macro which acts like `t!` but returns the message-id.
### Change
- t! macro amended to use `try_translate` and `try_translate_with_args`, but will perform `.expect("..")`
and therefore panic! on error. This retains backwards compatibility for this macro.
- Use of `set_fallback_language` / `try_set_fallback_language` without a corresponding locale
translation is treated as an error.
## [0.4.0] 2025-01-25
### Added
- Code:
- Doc comments
- Module tests for `cargo test`
- Amended `I18nConfig::with_locale` so that the `Locale` dynamic or static
constructors no longer have to be _explicitly_ given.
They can be determined implicitly from `(LanguageIdentifier, &str)` or
`(LanguageIdentifer, PathBuf)`.
- Enabled shared 'LocaleResource's, where two dialect can use the same translation file.
For example ["en", "en-GB"] share "en-GB.ftl".
### Changed
- The translations used are determined when `I18n::set_language` or
`I18n::set_fallback_language` is called, and not each time a message is translated.
- __Fallback handling has changed__. It no longer just uses _fallback_language_ when the message
id is missing from the current _locale_. It performs a graceful fallback from
_<language>-<region>_ to _<language>_ before using the actual _fallback_ (in fact it
falls back along the _<language>-<optionalScript>-<optionalRegion>-<optionalVariants>_
hiearchy).
__Note:__ this is a breaking change which may impact the selected translation.
- `LocaleResource::to_string` renamed to `LocaleResource::to_resource_string`
## [0.3.0] 2024-12-10
- [Dioxus 0.6](https://dioxuslabs.com/) support
## [0.2.4] 2024-09-11
- Hide new_dynamic in WASM
- New t!() macro
## [0.2.3] 2024-09-04
- Support dynamic loading of locales
## [0.2.2] 2024-09-02
- Enable macros instead of serde in unic-langid
## [0.2.1] 2024-09-02
- Export unic_langid and fluent
- Use absolute path to import fluent in the translate macro
- Updated freya example
## [0.2.0] 2024-09-01
- Now based in the [Fluent Project](https://github.com/projectfluent/fluent-rs)
## [0.1.0] 2024-08-31
- Initial release

View File

@@ -1,30 +0,0 @@
[package]
name = "dioxus-i18n"
version = "0.5.1"
edition = "2021"
authors = ["Marc Espín <mespinsanz@gmail.com>"]
description = "i18n integration for Dioxus apps based on Fluent Project."
license = "MIT"
repository = "https://github.com/dioxus-community/dioxus-i18n"
readme = "./README.md"
categories = ["accessibility", "gui", "localization", "internationalization"]
[dependencies]
dioxus = { git = "https://github.com/matous-volf/dioxus", rev = "627d5ca5b80aeed57c23e253024665f103117f5e", default-features = false, features = [
"hooks",
"macro",
"signals",
] }
fluent = "0.17"
thiserror = "2.0"
unic-langid = { version = "0.9", features = ["macros"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
walkdir = "2.5.0"
[dev-dependencies]
dioxus = { git = "https://github.com/matous-volf/dioxus", rev = "627d5ca5b80aeed57c23e253024665f103117f5e", features = ["desktop"] }
freya = "0.3"
futures = "0.3.31"
pretty_assertions = "1.4.1"
unic-langid = { version = "0.9.5", features = ["macros"] }

View File

@@ -1,20 +0,0 @@
MIT License
Copyright (c) Marc Espín Sanz
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,85 +0,0 @@
# dioxus-i18n 🌍
i18n integration for Dioxus apps based on the [Project Fluent](https://github.com/projectfluent/fluent-rs).
> This crate used to be in the [Dioxus SDK](https://github.com/DioxusLabs/sdk).
## Support
- **Dioxus v0.6** 🧬
- Renderers:
- [web](https://dioxuslabs.com/learn/0.6/guides/web/),
- [desktop](https://dioxuslabs.com/learn/0.6/guides/desktop/),
- [freya](https://github.com/marc2332/freya)
- Both WASM and native targets
## Example:
```ftl
# en-US.ftl
hello = Hello, {$name}!
```
```rs
// main.rs
fn app() -> Element {
let i18 = use_init_i18n(|| {
I18nConfig::new(langid!("en-US"))
// implicit [`Locale`]
.with_locale(( // Embed
langid!("en-US"),
include_str!("./en-US.ftl")
))
.with_locale(( // Load at launch
langid!("es-ES"),
PathBuf::from("./es-ES.ftl"),
))
.with_locale(( // Locales will share duplicated locale_resources
langid!("en"), // which is useful to assign a specific region for
include_str!("./en-US.ftl") // the primary language
))
// explicit [`Locale`]
.with_locale(Locale::new_static( // Embed
langid!("en-US"),
include_str!("./en-US.ftl"),
))
.with_locale(Locale::new_dynamic( // Load at launch
langid!("es-ES"),
PathBuf::from("./es-ES.ftl"),
))
});
rsx!(
label { { t!("hello", name: "World") } }
)
}
```
## Further examples
The examples folder contains a number of working examples:
* Desktop examples:
* [Dioxus](./examples/dioxus-desktop.rs)
* [Freya](./examples/freya.rs)
* Configuration variants:
* [Auto locales](./examples/config-auto-locales.rs)
* [Dynamic (PathBuf)](./examples/config-dynamic-pathbuf.rs)
* [Static (include_str!)](./examples/config-static-includestr.rs)
* Fluent grammer:
* [Application](./examples/fluent-grammar.rs)
* [FTL file](./examples/data/fluent/en.ftl)
## Development
```bash
# Checks clean compile against `#[cfg(not(target_arch = "wasm32"))]`
cargo build --target wasm32-unknown-unknown
# Runs all tests
cargo test
```
[MIT License](./LICENSE.md)

View File

@@ -1,47 +0,0 @@
//! This example demonstrates how to use an auto_locales derived I18nConfig.
//! This is useful when you have a lot of locales and you don't want to manually add them.
use dioxus::prelude::*;
use dioxus_i18n::{prelude::*, t};
use unic_langid::langid;
use std::path::PathBuf;
fn main() {
launch(app);
}
#[allow(non_snake_case)]
fn Body() -> Element {
let mut i18n = i18n();
let change_to_english = move |_| i18n.set_language(langid!("en-US"));
let change_to_spanish = move |_| i18n.set_language(langid!("es-ES"));
rsx!(
button {
onclick: change_to_english,
label {
"English"
}
}
button {
onclick: change_to_spanish,
label {
"Spanish"
}
}
p { { t!("hello_world") } }
p { { t!("hello", name: "Dioxus") } }
)
}
fn app() -> Element {
use_init_i18n(|| {
// This initialisation performs a deep search for all locales in the given path.
// It IS NOT supported in WASM targets.
I18nConfig::new(langid!("en-US")).with_auto_locales(PathBuf::from("./examples/data/i18n/"))
});
rsx!(Body {})
}

View File

@@ -1,62 +0,0 @@
//! This example demonstrates how to use pathbuf derived I18nConfig.
//! This is useful when the path to the translation files is not known at compile time.
use dioxus::prelude::*;
use dioxus_i18n::{prelude::*, t};
use unic_langid::langid;
use std::path::PathBuf;
fn main() {
launch(app);
}
#[allow(non_snake_case)]
fn Body() -> Element {
let mut i18n = i18n();
let change_to_english = move |_| i18n.set_language(langid!("en-US"));
let change_to_spanish = move |_| i18n.set_language(langid!("es-ES"));
rsx!(
button {
onclick: change_to_english,
label {
"English"
}
}
button {
onclick: change_to_spanish,
label {
"Spanish"
}
}
p { { t!("hello_world") } }
p { { t!("hello", name: "Dioxus") } }
)
}
fn app() -> Element {
use_init_i18n(|| {
// This initialisation allows individual translation files to be selected.
// The locales can be added with an implicitly derived locale (see config-static-includestr.rs for a comparison)
// or using an explicit Locale::new_dynamic call.
//
// The two examples are functionally equivalent.
//
// It IS NOT supported in WASM targets.
I18nConfig::new(langid!("en-US"))
// Implicit...
.with_locale((
langid!("es-ES"),
PathBuf::from("./examples/data/i18n/es-ES.ftl"),
))
// Explicit...
.with_locale(Locale::new_dynamic(
langid!("en-US"),
PathBuf::from("./examples/data/i18n/en-US.ftl"),
))
});
rsx!(Body {})
}

View File

@@ -1,57 +0,0 @@
//! This example demonstrates how to use pathbuf derived I18nConfig.
//! This is useful for WASM targets; the paths to the translation files must be known at compile time.
use dioxus::prelude::*;
use dioxus_i18n::{prelude::*, t};
use unic_langid::langid;
fn main() {
launch(app);
}
#[allow(non_snake_case)]
fn Body() -> Element {
let mut i18n = i18n();
let change_to_english = move |_| i18n.set_language(langid!("en-US"));
let change_to_spanish = move |_| i18n.set_language(langid!("es-ES"));
rsx!(
button {
onclick: change_to_english,
label {
"English"
}
}
button {
onclick: change_to_spanish,
label {
"Spanish"
}
}
p { { t!("hello_world") } }
p { { t!("hello", name: "Dioxus") } }
)
}
fn app() -> Element {
use_init_i18n(|| {
// This initialisation allows individual translation files to be selected.
// The locales can be added with an implicitly derived locale (see config-dynamic-pathbuf.rs for a comparison)
// or using an explicit Locale::new_static call.
//
// The two examples are functionally equivalent.
//
// It IS supported in WASM targets.
I18nConfig::new(langid!("en-US"))
// Implicit...
.with_locale((langid!("es-ES"), include_str!("./data/i18n/es-ES.ftl")))
// Explicit...
.with_locale(Locale::new_static(
langid!("en-US"),
include_str!("./data/i18n/en-US.ftl"),
))
});
rsx!(Body {})
}

View File

@@ -1,124 +0,0 @@
### Fluent grammar examples for dioxus-i18n.
## These examples demonstrate Fluent file grammar and how dioxus-i18n can be
## used to access these translations.
## Examples derived from: https://projectfluent.org/fluent/guide/index.html
# Simple message
simple-message = This is a simple message.
# $name (String) - The name you want to display.
message-with-variable = This is a message with a variable: { $name }.
# Reference to a term.
-a-term = This is a common term used by many messages.
message-referencing-a-term = This is a message with a reference: { -a-term }.
# Use of special characters.
message-with-special-character = This message contain opening curly brace {"{"} and a closing curly brace {"}"}.
# Message with blanks.
blank-is-removed = This message starts with no blanks.
blank-is-preserved = {" "}This message starts with 4 spaces (note HTML contracts them).
# Message with attributes.
message-with-attributes = Predefined value
.placeholder = email@example.com
.aria-label = Login input value
.title = Type your login email
# Message with quotes.
literal-quote-cryptic = Text in {"\""}double quotes{"\""}.
literal-quote-preferred = Text in "double quotes".
# Message with Unicode characters.
unicode-cryptic = {"\u2605"} {"\u2606"} {"\u2728"} {"\u262F"} {"\u263A"}
unicode-preferred = ★ ☆ ✨ ☯ ☺
# Message with a placeable.
single-line = Text can be written in a single line.
multi-line = Text can also span multiple lines
as long as each new line is indented
by at least one space.
block-line =
Sometimes it's more readable to format
multiline text as a "block", which means
starting it on a new line. All lines must
be indented by at least one space.
# Message using functions.
#
# Note: Builtin functions are currently unsupported: See Fluent issue https://github.com/projectfluent/fluent-rs/issues/181
# The Bundle::add_builtins() function is not published at the time of writing this example.
#
# Using a builtin currently results in an error.
#
# $duration (Number) - The duration in seconds.
time-elapsed-no-function = Time elapsed: { $duration }s.
time-elapsed-function = Currently unsupported: error raised: { NUMBER($duration) }.
# Message reference.
referenced-message = Referenced message
message-referencing-another-message = Message referencing another message: { referenced-message }.
# Message selection plurals.
message-selection-plurals =
{ $value ->
*[one] Value is one: { $value }.
[other] Value is more than one: { $value }.
}
# Message selection numeric.
# Argument must be numeric.
message-selection-numeric =
{ NUMERIC($value) ->
[0.0] Zero: { $value }.
*[0.5] A half: { $value }.
[other] Other ($value)
}
# Message selection number.
#
# Note: Builtin functions are currently unsupported: See Fluent issue https://github.com/projectfluent/fluent-rs/issues/181
# The Bundle::add_builtins() function is not published at the time of writing this example.
#
# Using the NUMBER builtin always results in a default behaviour.
#
message-selection-number = { NUMBER($pos, type: "ordinal") ->
[1] First!
[one] {$pos}st
[two] {$pos}nd
[few] {$pos}rd
*[other] {$pos}th
}
# Variables in references.
-term-using-variable = https://{ $host }
message-using-term-with-variable = For example: { -term-using-variable(host: "example.com") }.
-term-using-variable-2 =
{ $case ->
*[nominative] Firefox
[locative] Firefoksie
}
message-using-term-with-variable-2-1 = Informacje o { -term-using-variable-2(case: "locative") }.
message-using-term-with-variable-2-2 = About { -term-using-variable-2(case: "nominative") }.
message-using-term-with-variable-2-default = About { -term-using-variable-2(case: "") }.
message-using-term-with-variable-2-not-provided = About { -term-using-variable-2 }.
-brand-name =
{ $case ->
*[nominative] Firefox
[locative] Firefoksie
}
string-literal = { "string literal" }
number-literal-1 = { 1 }
number-literal-2 = { -123 }
number-literal-3 = { 3.14 }
inline-expression-placeable-1 = { { "string literal" } }
inline-expression-placeable-2 = { { 123 } }

View File

@@ -1,3 +0,0 @@
hello_world = Hello, World!
hello = Hello, {$name}!

View File

@@ -1,3 +0,0 @@
hello_world = Hola, Mundo!
hello = Hola, {$name}!

View File

@@ -1,47 +0,0 @@
use dioxus::prelude::*;
use dioxus_i18n::{prelude::*, t};
use unic_langid::langid;
use std::path::PathBuf;
fn main() {
launch(app);
}
#[allow(non_snake_case)]
fn Body() -> Element {
let mut i18n = i18n();
let change_to_english = move |_| i18n.set_language(langid!("en-US"));
let change_to_spanish = move |_| i18n.set_language(langid!("es-ES"));
rsx!(
button {
onclick: change_to_english,
label {
"English"
}
}
button {
onclick: change_to_spanish,
label {
"Spanish"
}
}
p { { t!("hello_world") } }
p { { t!("hello", name: "Dioxus") } }
)
}
fn app() -> Element {
use_init_i18n(|| {
I18nConfig::new(langid!("en-US"))
.with_locale((langid!("en-US"), include_str!("./data/i18n/en-US.ftl")))
.with_locale((
langid!("es-ES"),
PathBuf::from("./examples/data/i18n/es-ES.ftl"),
))
});
rsx!(Body {})
}

View File

@@ -1,233 +0,0 @@
//! This example demonstrates many of the Fluent grammar constructs, and how they are
//! used in dioxus-i18n.
//! This performs a lookup only, no additional translation files are provided
use dioxus::prelude::*;
use dioxus_i18n::{prelude::*, tid};
use unic_langid::langid;
use std::path::PathBuf;
fn main() {
launch(app);
}
#[allow(non_snake_case)]
#[component]
fn Body() -> Element {
rsx! {
table {
tbody {
tr {
td { "Simple message" }
td { {tid!("simple-message")} }
}
tr {
td { "Non-existing message: id provided by default when using tid! macro" }
td { {tid!("non-existing-message")} }
}
tr {
td { "Message with a variable" }
td { {tid!("message-with-variable", name: "Value 1")} }
}
tr {
td { }
td { {tid!("message-with-variable", name: "Value 2")} }
}
tr {
td { "Reference to a term" }
td { {tid!("message-referencing-a-term")} }
}
tr {
td { "Use of special characters." }
td { {tid!("message-with-special-character")} }
}
tr {
td { "Message with blanks." }
td { "'" {tid!("blank-is-removed")} "'" }
}
tr {
td { }
td { "'" {tid!("blank-is-preserved")} "'" }
}
tr {
td { "Message with attributes: root" }
td { {tid!("message-with-attributes")} }
}
tr {
td { "Message with attributes: attribute" }
td { {tid!("message-with-attributes.placeholder")} }
}
tr {
td { }
td { {tid!("message-with-attributes.aria-label")} }
}
tr {
td { }
td { {tid!("message-with-attributes.title")} }
}
tr {
td { "Message with attributes: not existing" }
td { {tid!("message-with-attributes.not-existing")} }
}
tr {
td { "Message with attributes: invalid" }
td { {tid!("message-with-attributes.placeholder.invalid")} }
}
tr {
td { "Message with quotes: cryptic" }
td { {tid!("literal-quote-cryptic")} }
}
tr {
td { "Message with quotes: preferred" }
td { {tid!("literal-quote-preferred")} }
}
tr {
td { "Message with Unicode characters: cryptic" }
td { {tid!("unicode-cryptic")} }
}
tr {
td { "Message with Unicode characters: preferred" }
td { {tid!("unicode-preferred")} }
}
tr {
td { "Message with a placeable: single-line" }
td { {tid!("line-single")} }
}
tr {
td { "Message with a placeable: single-line" }
td { {tid!("single-line")} }
}
tr {
td { "Message with a placeable: multi-line (1)" }
td { {tid!("multi-line")} }
}
tr {
td { "Message with a placeable: multi-line (2)" }
td { pre { {tid!("multi-line")} } }
}
tr {
td { "Message with a placeable: block-line (1)" }
td { {tid!("block-line")} }
}
tr {
td { "Message with a placeable: block-line (2)" }
td { pre { {tid!("block-line")} } }
}
tr {
td { "Message using functions: no function" }
td { pre { {tid!("time-elapsed-no-function", duration: 23.7114812589)} } }
}
tr {
td { "Message using functions: function" }
td { pre { {tid!("time-elapsed-function", duration: 23.7114812589)} } }
}
tr {
td { "Reference to a message" }
td { {tid!("message-referencing-another-message")} }
}
tr {
td { "Message selection: plurals" }
td { {tid!("message-selection-plurals", value: 1)} }
}
tr {
td { }
td { {tid!("message-selection-plurals", value: 2)} }
}
tr {
td { "Message selection: plurals (default: an 'empty' value must be provided...)" }
td { {tid!("message-selection-plurals", value: "")} }
}
tr {
td { "Message selection: plurals (default: ... otherwise an error is raised)" }
td { {tid!("message-selection-plurals")} }
}
tr {
td { "Message selection: numeric" }
td { {tid!("message-selection-numeric", value: 0.0)} }
}
tr {
td { }
td { {tid!("message-selection-numeric", value: 0.5)} }
}
tr {
td { }
td { {tid!("message-selection-numeric", value: 42.0)} }
}
tr {
td { "Message selection: numeric (default)" }
td { {tid!("message-selection-numeric", value: "")} }
}
tr {
td { "Message selection: number" }
td { {tid!("message-selection-number", pos: 1)} }
}
tr {
td { "" }
td { {tid!("message-selection-number", pos: 2)} }
}
tr {
td { "" }
td { {tid!("message-selection-number", pos: 3)} }
}
tr {
td { "" }
td { {tid!("message-selection-number", pos: 4)} }
}
tr {
td { "Variables in references (1)" }
td { {tid!("message-using-term-with-variable")} }
}
tr {
td { "Variables in references (2)" }
td { {tid!("message-using-term-with-variable-2-1")} }
}
tr {
td { }
td { {tid!("message-using-term-with-variable-2-2")} }
}
tr {
td { }
td { {tid!("message-using-term-with-variable-2-default")} }
}
tr {
td { }
td { {tid!("message-using-term-with-variable-2-not-provided")} }
}
tr {
td { "Literals: string" }
td { {tid!("string-literal")} }
}
tr {
td { }
td { {tid!("number-literal-1")} }
}
tr {
td { }
td { {tid!("number-literal-2")} }
}
tr {
td { }
td { {tid!("number-literal-3")} }
}
tr {
td { }
td { {tid!("inline-expression-placeable-1")} }
}
tr {
td { }
td { {tid!("inline-expression-placeable-2")} }
}
}
}
}
}
fn app() -> Element {
use_init_i18n(|| {
// Only one example in this path, which contains the complete Fluent grammar.
I18nConfig::new(langid!("en")).with_auto_locales(PathBuf::from("./examples/data/fluent/"))
});
rsx!(Body {})
}

View File

@@ -1,57 +0,0 @@
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
use dioxus_i18n::{prelude::*, t};
use freya::prelude::*;
use unic_langid::langid;
use std::path::PathBuf;
fn main() {
launch_with_props(app, "freya + i18n", (300.0, 200.0));
}
#[allow(non_snake_case)]
fn Body() -> Element {
let mut i18n = i18n();
let change_to_english = move |_| i18n.set_language(langid!("en-US"));
let change_to_spanish = move |_| i18n.set_language(langid!("es-ES"));
rsx!(
rect {
rect {
direction: "horizontal",
Button {
onclick: change_to_english,
label {
"English"
}
}
Button {
onclick: change_to_spanish,
label {
"Spanish"
}
}
}
label { { t!("hello_world") } }
label { { t!("hello", name: "Dioxus") } }
}
)
}
fn app() -> Element {
use_init_i18n(|| {
I18nConfig::new(langid!("en-US"))
.with_locale((langid!("en-US"), include_str!("./data/i18n/en-US.ftl")))
.with_locale((
langid!("es-ES"),
PathBuf::from("./examples/data/i18n/es-ES.ftl"),
))
});
rsx!(Body {})
}

View File

@@ -1,31 +0,0 @@
use thiserror::Error;
#[derive(Clone, Debug, Error)]
pub enum Error {
#[error("invalid message id: '{0}'")]
InvalidMessageId(String),
#[error("message id not found for key: '{0}'")]
MessageIdNotFound(String),
#[error("attribute id not found for key: '{0}'")]
AttributeIdNotFound(String),
#[error("message pattern not found for key: '{0}'")]
MessagePatternNotFound(String),
#[error("fluent errors during lookup:\n{0}")]
FluentErrorsDetected(String),
#[error("failed to read locale resource from path: {0}")]
LocaleResourcePathReadFailed(String),
#[error("fallback for \"{0}\" must have locale")]
FallbackMustHaveLocale(String),
#[error("language id cannot be determined - reason: {0}")]
InvalidLanguageId(String),
#[error("invalid path: {0}")]
InvalidPath(String),
}

View File

@@ -1,101 +0,0 @@
//! Key translation macros.
//!
//! Using file:
//!
//! ```ftl
//! # en-US.ftl
//! #
//! hello = Hello, {$name}!
//! ```
/// Translate message from key, returning [`crate::prelude::DioxusI18nError`] if id not found...
///
/// ```rust
/// # use dioxus::prelude::*;
/// # use dioxus_i18n::{te, prelude::*};
/// # use unic_langid::langid;
/// # #[component]
/// # fn Example() -> Element {
/// # let lang = langid!("en-US");
/// # let config = I18nConfig::new(lang.clone()).with_locale((lang.clone(), "hello = Hello, {$name}")).with_fallback(lang.clone());
/// # let mut i18n = use_init_i18n(|| config);
/// let name = "Avery Gigglesworth";
/// let hi = te!("hello", name: {name}).expect("message id 'name' should be present");
/// assert_eq!(hi, "Hello, Avery Gigglesworth");
/// # rsx! { "" }
/// # }
/// ```
///
#[macro_export]
macro_rules! te {
($id:expr, $( $name:ident : $value:expr ),* ) => {
{
let mut params_map = $crate::fluent::FluentArgs::new();
$(
params_map.set(stringify!($name), $value);
)*
$crate::prelude::i18n().try_translate_with_args($id, Some(&params_map))
}
};
($id:expr ) => {{
$crate::prelude::i18n().try_translate($id)
}};
}
/// Translate message from key, panic! if id not found...
///
/// ```rust
/// # use dioxus::prelude::*;
/// # use dioxus_i18n::{t, prelude::*};
/// # use unic_langid::langid;
/// # #[component]
/// # fn Example() -> Element {
/// # let lang = langid!("en-US");
/// # let config = I18nConfig::new(lang.clone()).with_locale((lang.clone(), "hello = Hello, {$name}")).with_fallback(lang.clone());
/// # let mut i18n = use_init_i18n(|| config);
/// let name = "Avery Gigglesworth";
/// let hi = t!("hello", name: {name});
/// assert_eq!(hi, "Hello, Avery Gigglesworth");
/// # rsx! { "" }
/// # }
/// ```
///
#[macro_export]
macro_rules! t {
($id:expr, $( $name:ident : $value:expr ),* ) => {
$crate::te!($id, $( $name : $value ),*).unwrap_or_else(|e| panic!("{}", e.to_string()))
};
($id:expr ) => {{
$crate::te!($id).unwrap_or_else(|e| panic!("{}", e.to_string()))
}};
}
/// Translate message from key, return id if no translation found...
///
/// ```rust
/// # use dioxus::prelude::*;
/// # use dioxus_i18n::{tid, prelude::*};
/// # use unic_langid::langid;
/// # #[component]
/// # fn Example() -> Element {
/// # let lang = langid!("en-US");
/// # let config = I18nConfig::new(lang.clone()).with_locale((lang.clone(), "hello = Hello, {$name}")).with_fallback(lang.clone());
/// # let mut i18n = use_init_i18n(|| config);
/// let message = tid!("no-key");
/// assert_eq!(message, "message-id: no-key should be translated");
/// # rsx! { "" }
/// # }
/// ```
///
#[macro_export]
macro_rules! tid {
($id:expr, $( $name:ident : $value:expr ),* ) => {
$crate::te!($id, $( $name : $value ),*).unwrap_or_else(|e| e.to_string())
};
($id:expr ) => {{
$crate::te!($id).unwrap_or_else(|e| e.to_string())
}};
}

View File

@@ -1,12 +0,0 @@
#![doc = include_str!("../README.md")]
mod error;
pub mod i18n_macro;
pub mod use_i18n;
pub use fluent;
pub use unic_langid;
pub mod prelude {
pub use crate::error::Error as DioxusI18nError;
pub use crate::use_i18n::*;
}

View File

@@ -1,703 +0,0 @@
use super::error::Error;
use dioxus::prelude::*;
use fluent::{FluentArgs, FluentBundle, FluentResource};
use unic_langid::LanguageIdentifier;
#[cfg(not(target_arch = "wasm32"))]
use walkdir::WalkDir;
use std::collections::HashMap;
#[cfg(not(target_arch = "wasm32"))]
use std::path::{Path, PathBuf};
/// `Locale` is a "place-holder" around what will eventually be a `fluent::FluentBundle`
#[cfg_attr(test, derive(Debug, PartialEq))]
pub struct Locale {
id: LanguageIdentifier,
resource: LocaleResource,
}
impl Locale {
pub fn new_static(id: LanguageIdentifier, str: &'static str) -> Self {
Self {
id,
resource: LocaleResource::Static(str),
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn new_dynamic(id: LanguageIdentifier, path: impl Into<PathBuf>) -> Self {
Self {
id,
resource: LocaleResource::Path(path.into()),
}
}
}
impl<T> From<(LanguageIdentifier, T)> for Locale
where
T: Into<LocaleResource>,
{
fn from((id, resource): (LanguageIdentifier, T)) -> Self {
let resource = resource.into();
Self { id, resource }
}
}
/// A `LocaleResource` can be static text, or a filesystem file (not supported in WASM).
#[derive(Debug, PartialEq)]
pub enum LocaleResource {
Static(&'static str),
#[cfg(not(target_arch = "wasm32"))]
Path(PathBuf),
}
impl LocaleResource {
pub fn try_to_resource_string(&self) -> Result<String, Error> {
match self {
Self::Static(str) => Ok(str.to_string()),
#[cfg(not(target_arch = "wasm32"))]
Self::Path(path) => std::fs::read_to_string(path)
.map_err(|e| Error::LocaleResourcePathReadFailed(e.to_string())),
}
}
pub fn to_resource_string(&self) -> String {
let result = self.try_to_resource_string();
match result {
Ok(string) => string,
Err(err) => panic!("failed to create resource string {:?}: {}", self, err),
}
}
}
impl From<&'static str> for LocaleResource {
fn from(value: &'static str) -> Self {
Self::Static(value)
}
}
#[cfg(not(target_arch = "wasm32"))]
impl From<PathBuf> for LocaleResource {
fn from(value: PathBuf) -> Self {
Self::Path(value)
}
}
/// The configuration for `I18n`.
#[cfg_attr(test, derive(Debug, PartialEq))]
pub struct I18nConfig {
/// The initial language, can be later changed with [`I18n::set_language`]
id: LanguageIdentifier,
/// The final fallback language if no other locales are found for `id`.
/// A `Locale` must exist in `locales' if `fallback` is defined.
fallback: Option<LanguageIdentifier>,
/// The locale_resources added to the configuration.
locale_resources: Vec<LocaleResource>,
/// The locales added to the configuration.
locales: HashMap<LanguageIdentifier, usize>,
}
impl I18nConfig {
/// Create an i18n config with the selected [LanguageIdentifier].
pub fn new(id: LanguageIdentifier) -> Self {
Self {
id,
fallback: None,
locale_resources: Vec::new(),
locales: HashMap::new(),
}
}
/// Set a fallback [LanguageIdentifier].
pub fn with_fallback(mut self, fallback: LanguageIdentifier) -> Self {
self.fallback = Some(fallback);
self
}
/// Add [Locale].
/// It is possible to share locales resources. If this locale's resource
/// matches a previously added one, then this locale will use the existing one.
/// This is primarily for the static locale_resources to avoid string duplication.
pub fn with_locale<T>(mut self, locale: T) -> Self
where
T: Into<Locale>,
{
let locale = locale.into();
let locale_resources_len = self.locale_resources.len();
let index = self
.locale_resources
.iter()
.position(|r| *r == locale.resource)
.unwrap_or(locale_resources_len);
if index == locale_resources_len {
self.locale_resources.push(locale.resource)
};
self.locales.insert(locale.id, index);
self
}
/// Add multiple locales from given folder, based on their filename.
///
/// If the path represents a folder, then the folder will be deep traversed for
/// all '*.ftl' files. If the filename represents a [LanguageIdentifier] then it
/// will be added to the config.
///
/// If the path represents a file, then the filename must represent a
/// unic_langid::LanguageIdentifier for it to be added to the config.
///
/// The method is not available for `wasm32` builds.
#[cfg(not(target_arch = "wasm32"))]
pub fn try_with_auto_locales(self, path: PathBuf) -> Result<Self, Error> {
if path.is_dir() {
let files = find_ftl_files(&path)?;
files
.into_iter()
.try_fold(self, |acc, file| acc.with_auto_pathbuf(file))
} else if is_ftl_file(&path) {
self.with_auto_pathbuf(path)
} else {
Err(Error::InvalidPath(path.to_string_lossy().to_string()))
}
}
#[cfg(not(target_arch = "wasm32"))]
fn with_auto_pathbuf(self, file: PathBuf) -> Result<Self, Error> {
assert!(is_ftl_file(&file));
let stem = file.file_stem().ok_or_else(|| {
Error::InvalidLanguageId(format!("No file stem: '{}'", file.display()))
})?;
let id_str = stem.to_str().ok_or_else(|| {
Error::InvalidLanguageId(format!("Cannot convert: {}", stem.to_string_lossy()))
})?;
let id = LanguageIdentifier::from_bytes(id_str.as_bytes())
.map_err(|e| Error::InvalidLanguageId(e.to_string()))?;
Ok(self.with_locale((id, file)))
}
/// Add multiple locales from given folder, based on their filename.
///
/// Will panic! on error.
///
/// The method is not available for `wasm32` builds.
#[cfg(not(target_arch = "wasm32"))]
pub fn with_auto_locales(self, path: PathBuf) -> Self {
let path_name = path.display().to_string();
let result = self.try_with_auto_locales(path);
match result {
Ok(result) => result,
Err(err) => panic!(
"with_auto_locales must have valid pathbuf {}: {}",
path_name, err
),
}
}
}
#[cfg(not(target_arch = "wasm32"))]
fn find_ftl_files(folder: &PathBuf) -> Result<Vec<PathBuf>, Error> {
let ftl_files: Vec<PathBuf> = WalkDir::new(folder)
.into_iter()
.filter_map(|entry| entry.ok())
.filter(|entry| is_ftl_file(entry.path()))
.map(|entry| entry.path().to_path_buf())
.collect();
Ok(ftl_files)
}
#[cfg(not(target_arch = "wasm32"))]
fn is_ftl_file(entry: &Path) -> bool {
entry.is_file() && entry.extension().map(|ext| ext == "ftl").unwrap_or(false)
}
/// Initialize an i18n provider.
pub fn try_use_init_i18n(init: impl FnOnce() -> I18nConfig) -> Result<I18n, Error> {
use_context_provider(move || {
// Coverage false -ve: See https://github.com/xd009642/tarpaulin/issues/1675
let I18nConfig {
id,
fallback,
locale_resources,
locales,
} = init();
I18n::try_new(id, fallback, locale_resources, locales)
})
}
/// Initialize an i18n provider.
pub fn use_init_i18n(init: impl FnOnce() -> I18nConfig) -> I18n {
use_context_provider(move || {
// Coverage false -ve: See https://github.com/xd009642/tarpaulin/issues/1675
let I18nConfig {
id,
fallback,
locale_resources,
locales,
} = init();
match I18n::try_new(id, fallback, locale_resources, locales) {
Ok(i18n) => i18n,
Err(e) => panic!("Failed to create I18n context: {}", e),
}
})
}
#[derive(Clone, Copy)]
pub struct I18n {
selected_language: Signal<LanguageIdentifier>,
fallback_language: Signal<Option<LanguageIdentifier>>,
locale_resources: Signal<Vec<LocaleResource>>,
locales: Signal<HashMap<LanguageIdentifier, usize>>,
active_bundle: Signal<FluentBundle<FluentResource>>,
}
impl I18n {
pub fn try_new(
selected_language: LanguageIdentifier,
fallback_language: Option<LanguageIdentifier>,
locale_resources: Vec<LocaleResource>,
locales: HashMap<LanguageIdentifier, usize>,
) -> Result<Self, Error> {
let bundle = try_create_bundle(
&selected_language,
&fallback_language,
&locale_resources,
&locales,
)?;
Ok(Self {
selected_language: Signal::new(selected_language),
fallback_language: Signal::new(fallback_language),
locale_resources: Signal::new(locale_resources),
locales: Signal::new(locales),
active_bundle: Signal::new(bundle),
})
}
pub fn new(
selected_language: LanguageIdentifier,
fallback_language: Option<LanguageIdentifier>,
locale_resources: Vec<LocaleResource>,
locales: HashMap<LanguageIdentifier, usize>,
) -> Self {
let result = Self::try_new(
selected_language,
fallback_language,
locale_resources,
locales,
);
match result {
Ok(i18n) => i18n,
Err(err) => panic!("I18n cannot be created: {}", err),
}
}
pub fn try_translate_with_args(
&self,
msg: &str,
args: Option<&FluentArgs>,
) -> Result<String, Error> {
let (message_id, attribute_name) = Self::decompose_identifier(msg)?;
let bundle = self.active_bundle.read();
let message = bundle
.get_message(message_id)
.ok_or_else(|| Error::MessageIdNotFound(message_id.into()))?;
let pattern = if let Some(attribute_name) = attribute_name {
let attribute = message
.get_attribute(attribute_name)
.ok_or_else(|| Error::AttributeIdNotFound(msg.to_string()))?;
attribute.value()
} else {
message
.value()
.ok_or_else(|| Error::MessagePatternNotFound(message_id.into()))?
};
let mut errors = vec![];
let translation = bundle
.format_pattern(pattern, args, &mut errors)
.to_string();
(errors.is_empty())
.then_some(translation)
.ok_or_else(|| Error::FluentErrorsDetected(format!("{:#?}", errors)))
}
pub fn decompose_identifier(msg: &str) -> Result<(&str, Option<&str>), Error> {
let parts: Vec<&str> = msg.split('.').collect();
match parts.as_slice() {
[message_id] => Ok((message_id, None)),
[message_id, attribute_name] => Ok((message_id, Some(attribute_name))),
_ => Err(Error::InvalidMessageId(msg.to_string())),
}
}
pub fn translate_with_args(&self, msg: &str, args: Option<&FluentArgs>) -> String {
let result = self.try_translate_with_args(msg, args);
match result {
Ok(translation) => translation,
Err(err) => panic!("Failed to translate {}: {}", msg, err),
}
}
#[inline]
pub fn try_translate(&self, msg: &str) -> Result<String, Error> {
self.try_translate_with_args(msg, None)
}
pub fn translate(&self, msg: &str) -> String {
let result = self.try_translate(msg);
match result {
Ok(translation) => translation,
Err(err) => panic!("Failed to translate {}: {}", msg, err),
}
}
/// Get the selected language.
#[inline]
pub fn language(&self) -> LanguageIdentifier {
self.selected_language.read().clone()
}
/// Get the fallback language.
pub fn fallback_language(&self) -> Option<LanguageIdentifier> {
self.fallback_language.read().clone()
}
/// Update the selected language.
pub fn try_set_language(&mut self, id: LanguageIdentifier) -> Result<(), Error> {
*self.selected_language.write() = id;
self.try_update_active_bundle()
}
/// Update the selected language.
pub fn set_language(&mut self, id: LanguageIdentifier) {
let id_name = id.to_string();
let result = self.try_set_language(id);
match result {
Ok(()) => (),
Err(err) => panic!("cannot set language {}: {}", id_name, err),
}
}
/// Update the fallback language.
pub fn try_set_fallback_language(&mut self, id: LanguageIdentifier) -> Result<(), Error> {
self.locales
.read()
.get(&id)
.ok_or_else(|| Error::FallbackMustHaveLocale(id.to_string()))?;
*self.fallback_language.write() = Some(id);
self.try_update_active_bundle()
}
/// Update the fallback language.
pub fn set_fallback_language(&mut self, id: LanguageIdentifier) {
let id_name = id.to_string();
let result = self.try_set_fallback_language(id);
match result {
Ok(()) => (),
Err(err) => panic!("cannot set fallback language {}: {}", id_name, err),
}
}
fn try_update_active_bundle(&mut self) -> Result<(), Error> {
let bundle = try_create_bundle(
&self.selected_language.peek(),
&self.fallback_language.peek(),
&self.locale_resources.peek(),
&self.locales.peek(),
)?;
self.active_bundle.set(bundle);
Ok(())
}
}
fn try_create_bundle(
selected_language: &LanguageIdentifier,
fallback_language: &Option<LanguageIdentifier>,
locale_resources: &[LocaleResource],
locales: &HashMap<LanguageIdentifier, usize>,
) -> Result<FluentBundle<FluentResource>, Error> {
let add_resource = move |bundle: &mut FluentBundle<FluentResource>,
langid: &LanguageIdentifier,
locale_resources: &[LocaleResource]| {
if let Some(&i) = locales.get(langid) {
let resource = &locale_resources[i];
let resource =
FluentResource::try_new(resource.try_to_resource_string()?).map_err(|e| {
Error::FluentErrorsDetected(format!("resource langid: {}\n{:#?}", langid, e))
})?;
bundle.add_resource_overriding(resource);
};
Ok(())
};
let mut bundle = FluentBundle::new(vec![selected_language.clone()]);
if let Some(fallback_language) = fallback_language {
add_resource(&mut bundle, fallback_language, locale_resources)?;
}
let (language, script, region, variants) = selected_language.clone().into_parts();
let variants_lang = LanguageIdentifier::from_parts(language, script, region, &variants);
let region_lang = LanguageIdentifier::from_parts(language, script, region, &[]);
let script_lang = LanguageIdentifier::from_parts(language, script, None, &[]);
let language_lang = LanguageIdentifier::from_parts(language, None, None, &[]);
add_resource(&mut bundle, &language_lang, locale_resources)?;
add_resource(&mut bundle, &script_lang, locale_resources)?;
add_resource(&mut bundle, &region_lang, locale_resources)?;
add_resource(&mut bundle, &variants_lang, locale_resources)?;
/* Add this code when the fluent crate includes FluentBundle::add_builtins.
* This will allow the use of built-in functions like `NUMBER` and `DATETIME`.
* See [Fluent issue](https://github.com/projectfluent/fluent-rs/issues/181) for more information.
bundle
.add_builtins()
.map_err(|e| Error::FluentErrorsDetected(e.to_string()))?;
*/
Ok(bundle)
}
pub fn i18n() -> I18n {
consume_context()
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
use unic_langid::langid;
#[test]
fn can_add_locale_to_config_explicit_locale() {
const LANG_A: LanguageIdentifier = langid!("la-LA");
const LANG_B: LanguageIdentifier = langid!("la-LB");
const LANG_C: LanguageIdentifier = langid!("la-LC");
let config = I18nConfig::new(LANG_A)
.with_locale(Locale::new_static(LANG_B, "lang = lang_b"))
.with_locale(Locale::new_dynamic(LANG_C, PathBuf::new()));
assert_eq!(
config,
I18nConfig {
id: LANG_A,
fallback: None,
locale_resources: vec![
LocaleResource::Static("lang = lang_b"),
LocaleResource::Path(PathBuf::new()),
],
locales: HashMap::from([(LANG_B, 0), (LANG_C, 1)]),
}
);
}
#[test]
fn can_add_locale_to_config_implicit_locale() {
const LANG_A: LanguageIdentifier = langid!("la-LA");
const LANG_B: LanguageIdentifier = langid!("la-LB");
const LANG_C: LanguageIdentifier = langid!("la-LC");
let config = I18nConfig::new(LANG_A)
.with_locale((LANG_B, "lang = lang_b"))
.with_locale((LANG_C, PathBuf::new()));
assert_eq!(
config,
I18nConfig {
id: LANG_A,
fallback: None,
locale_resources: vec![
LocaleResource::Static("lang = lang_b"),
LocaleResource::Path(PathBuf::new())
],
locales: HashMap::from([(LANG_B, 0), (LANG_C, 1)]),
}
);
}
#[test]
fn can_add_locale_string_to_config() {
const LANG_A: LanguageIdentifier = langid!("la-LA");
const LANG_B: LanguageIdentifier = langid!("la-LB");
let config = I18nConfig::new(LANG_A).with_locale((LANG_B, "lang = lang_b"));
assert_eq!(
config,
I18nConfig {
id: LANG_A,
fallback: None,
locale_resources: vec![LocaleResource::Static("lang = lang_b")],
locales: HashMap::from([(LANG_B, 0)]),
}
);
}
#[test]
fn can_add_shared_locale_string_to_config() {
const LANG_A: LanguageIdentifier = langid!("la-LA");
const LANG_B: LanguageIdentifier = langid!("la-LB");
const LANG_C: LanguageIdentifier = langid!("la-LC");
let shared_string = "lang = a language";
let config = I18nConfig::new(LANG_A)
.with_locale((LANG_B, shared_string))
.with_locale((LANG_C, shared_string));
assert_eq!(
config,
I18nConfig {
id: LANG_A,
fallback: None,
locale_resources: vec![LocaleResource::Static(shared_string)],
locales: HashMap::from([(LANG_B, 0), (LANG_C, 0)]),
}
);
}
#[test]
fn can_add_locale_pathbuf_to_config() {
const LANG_A: LanguageIdentifier = langid!("la-LA");
const LANG_C: LanguageIdentifier = langid!("la-LC");
let config = I18nConfig::new(LANG_A)
.with_locale((LANG_C, PathBuf::from("./test/data/fallback/la.ftl")));
assert_eq!(
config,
I18nConfig {
id: LANG_A,
fallback: None,
locale_resources: vec![LocaleResource::Path(PathBuf::from(
"./test/data/fallback/la.ftl"
))],
locales: HashMap::from([(LANG_C, 0)]),
}
);
}
#[test]
fn can_add_shared_locale_pathbuf_to_config() {
const LANG_A: LanguageIdentifier = langid!("la-LA");
const LANG_B: LanguageIdentifier = langid!("la-LB");
const LANG_C: LanguageIdentifier = langid!("la-LC");
let shared_pathbuf = PathBuf::from("./test/data/fallback/la.ftl");
let config = I18nConfig::new(LANG_A)
.with_locale((LANG_B, shared_pathbuf.clone()))
.with_locale((LANG_C, shared_pathbuf.clone()));
assert_eq!(
config,
I18nConfig {
id: LANG_A,
fallback: None,
locale_resources: vec![LocaleResource::Path(shared_pathbuf)],
locales: HashMap::from([(LANG_B, 0), (LANG_C, 0)]),
}
);
}
#[test]
fn can_auto_add_locales_folder_to_config() {
const LANG_A: LanguageIdentifier = langid!("la-LA");
let root_path_str = &format!("{}/tests/data/fallback/", env!("CARGO_MANIFEST_DIR"));
let pathbuf = PathBuf::from(root_path_str);
let config = I18nConfig::new(LANG_A)
.try_with_auto_locales(pathbuf)
.ok()
.unwrap();
let expected_locales = [
"fb-FB",
"la",
"la-Scpt",
"la-Scpt-LA",
"la-Scpt-LA-variants",
];
assert_eq!(config.locales.len(), expected_locales.len());
assert_eq!(config.locale_resources.len(), expected_locales.len());
expected_locales.into_iter().for_each(|l| {
let expected_filename = format!("{root_path_str}/{l}.ftl");
let id = LanguageIdentifier::from_bytes(l.as_bytes()).unwrap();
assert!(config.locales.get(&id).is_some());
assert!(config
.locale_resources
.contains(&LocaleResource::Path(PathBuf::from(expected_filename))));
});
}
#[test]
fn can_auto_add_locales_file_to_config() {
const LANG_A: LanguageIdentifier = langid!("la-LA");
let path_str = &format!(
"{}/tests/data/fallback/fb-FB.ftl",
env!("CARGO_MANIFEST_DIR")
);
let pathbuf = PathBuf::from(path_str);
let config = I18nConfig::new(LANG_A)
.try_with_auto_locales(pathbuf.clone())
.ok()
.unwrap();
assert_eq!(config.locales.len(), 1);
assert!(config.locales.get(&langid!("fb-FB")).is_some());
assert_eq!(config.locale_resources.len(), 1);
assert!(config
.locale_resources
.contains(&LocaleResource::Path(pathbuf)));
}
#[test]
fn will_fail_auto_locales_with_invalid_folder() {
const LANG_A: LanguageIdentifier = langid!("la-LA");
let root_path_str = &format!("{}/non_existing_path/", env!("CARGO_MANIFEST_DIR"));
let pathbuf = PathBuf::from(root_path_str);
let config = I18nConfig::new(LANG_A).try_with_auto_locales(pathbuf);
assert_eq!(config.is_err(), true);
}
#[test]
fn will_fail_auto_locales_with_invalid_file() {
const LANG_A: LanguageIdentifier = langid!("la-LA");
let path_str = &format!(
"{}/tests/data/fallback/invalid_language_id.ftl",
env!("CARGO_MANIFEST_DIR")
);
let pathbuf = PathBuf::from(path_str);
let config = I18nConfig::new(LANG_A).try_with_auto_locales(pathbuf);
assert_eq!(config.is_err(), true);
}
}

View File

@@ -1,13 +0,0 @@
# Note
//*****************************************************************************
//
// This set of tests takes a heavy handed approach to errors, whereby the
// process is exited. This is done because panic! and assert_eq! failures
// are trapped within `dioxus::runtime::RuntimeGuard`.
// Unfortunately panic! is still made silent.
//
// Errors will be shown with:
// cargo test -- --nocapture
//
//*****************************************************************************

View File

@@ -1,3 +0,0 @@
mod test_hook;
pub(crate) use test_hook::test_hook;

View File

@@ -1,85 +0,0 @@
// Lifted from: https://dioxuslabs.com/learn/0.6/cookbook/testing
//
// Much curtialed functionality and massaged to use in the local testing
// here. This hook isn't intended for reuse.
//
use dioxus::{dioxus_core::NoOpMutations, prelude::*};
use futures::FutureExt;
use std::{cell::RefCell, fmt::Debug, rc::Rc};
pub(crate) fn test_hook<V: 'static>(
initialize: impl FnMut() -> V + 'static,
check: impl FnMut(V, &mut Assertions) + 'static,
) {
#[derive(Props)]
struct MockAppComponent<I: 'static, C: 'static> {
hook: Rc<RefCell<I>>,
check: Rc<RefCell<C>>,
}
impl<I, C> PartialEq for MockAppComponent<I, C> {
fn eq(&self, _: &Self) -> bool {
true
}
}
impl<I, C> Clone for MockAppComponent<I, C> {
fn clone(&self) -> Self {
Self {
hook: self.hook.clone(),
check: self.check.clone(),
}
}
}
fn mock_app<I: FnMut() -> V, C: FnMut(V, &mut Assertions), V>(
props: MockAppComponent<I, C>,
) -> Element {
let value = props.hook.borrow_mut()();
let mut assertions = Assertions::new();
props.check.borrow_mut()(value, &mut assertions);
rsx! { div {} }
}
let mut vdom = VirtualDom::new_with_props(
mock_app,
MockAppComponent {
hook: Rc::new(RefCell::new(initialize)),
check: Rc::new(RefCell::new(check)),
},
);
vdom.rebuild_in_place();
while vdom.wait_for_work().now_or_never().is_some() {
vdom.render_immediate(&mut NoOpMutations);
}
}
#[derive(Debug)]
pub(crate) struct Assertions {}
impl Assertions {
pub fn new() -> Self {
Self {}
}
pub fn assert<T>(&mut self, actual: T, expected: T, id: &str)
where
T: PartialEq + Debug,
{
if actual != expected {
eprintln!(
"***** ERROR in {}: actual: '{:?}' != expected: '{:?}' *****\n",
id, actual, expected
);
std::process::exit(-1);
};
}
}

View File

@@ -1,5 +0,0 @@
fallback = fallback only
language = fallback language
script = fallback script
region = fallback region
variants = fallback variants

View File

@@ -1 +0,0 @@
variants = variants only

View File

@@ -1,2 +0,0 @@
region = region only
variants = region variants

View File

@@ -1,3 +0,0 @@
script = script only
region = script region
variants = script variants

View File

@@ -1,4 +0,0 @@
language = language only
script = language script
region = language region
variants = language variants

View File

@@ -1,5 +0,0 @@
hello = Hello, {$name}!
simple = Hello, Zaphod!
my_component = My Component
.placeholder = Component's placeholder
.hint = Component's hint with parameter {$name}

View File

@@ -1,44 +0,0 @@
mod common;
use common::*;
use dioxus_i18n::{
prelude::{use_init_i18n, I18n, I18nConfig},
t,
};
use unic_langid::{langid, LanguageIdentifier};
#[test]
fn issue_15_recent_change_to_t_macro_unnecessarily_breaks_v0_3_code_test_attr() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| {
let name = "World";
t!(&format!("hello"), name: name)
});
proxy.assert(panic.is_ok(), true, "translate_from_static_source");
proxy.assert(
panic.ok().unwrap(),
"Hello, \u{2068}World\u{2069}!".to_string(),
"translate_from_static_source",
);
});
}
#[test]
fn issue_15_recent_change_to_t_macro_unnecessarily_breaks_v0_3_code_test_no_attr() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| t!(&format!("simple")));
proxy.assert(panic.is_ok(), true, "translate_from_static_source");
proxy.assert(
panic.ok().unwrap(),
"Hello, Zaphod!".to_string(),
"translate_from_static_source",
);
});
}
const EN: LanguageIdentifier = langid!("en");
fn i18n_from_static() -> I18n {
let config = I18nConfig::new(EN).with_locale((EN, include_str!("./data/i18n/en.ftl")));
use_init_i18n(|| config)
}

View File

@@ -1,99 +0,0 @@
mod common;
use common::*;
use dioxus_i18n::prelude::{use_init_i18n, I18n, I18nConfig};
use unic_langid::{langid, LanguageIdentifier};
#[test]
fn exact_locale_match_will_use_translation() {
test_hook(i18n, |value, proxy| {
proxy.assert(
value
.try_translate("variants")
.expect("test message id must exist"),
"variants only".to_string(),
"exact_locale_match_will_use_translation",
);
});
}
#[test]
fn non_exact_locale_match_will_use_region() {
test_hook(i18n, |value, proxy| {
proxy.assert(
value
.try_translate("region")
.expect("test message id must exist"),
"region only".to_string(),
"non_exact_locale_match_will_use_region",
);
});
}
#[test]
fn non_exact_locale_match_will_use_script() {
test_hook(i18n, |value, proxy| {
proxy.assert(
value
.try_translate("script")
.expect("test message id must exist"),
"script only".to_string(),
"non_exact_locale_match_will_use_script",
);
});
}
#[test]
fn non_exact_locale_match_will_use_language() {
test_hook(i18n, |value, proxy| {
proxy.assert(
value
.try_translate("language")
.expect("test message id must exist"),
"language only".to_string(),
"non_exact_locale_match_will_use_language",
);
});
}
#[test]
fn no_locale_match_will_use_fallback() {
test_hook(i18n, |value, proxy| {
proxy.assert(
value
.try_translate("fallback")
.expect("test message id must exist"),
"fallback only".to_string(),
"no_locale_match_will_use_fallback",
);
});
}
fn i18n() -> I18n {
const FALLBACK_LANG: LanguageIdentifier = langid!("fb-FB");
const LANGUAGE_LANG: LanguageIdentifier = langid!("la");
const SCRIPT_LANG: LanguageIdentifier = langid!("la-Scpt");
const REGION_LANG: LanguageIdentifier = langid!("la-Scpt-LA");
let variants_lang: LanguageIdentifier = langid!("la-Scpt-LA-variants");
let config = I18nConfig::new(variants_lang.clone())
.with_locale((LANGUAGE_LANG, include_str!("../tests/data/fallback/la.ftl")))
.with_locale((
SCRIPT_LANG,
include_str!("../tests/data/fallback/la-Scpt.ftl"),
))
.with_locale((
REGION_LANG,
include_str!("../tests/data/fallback/la-Scpt-LA.ftl"),
))
.with_locale((
variants_lang.clone(),
include_str!("../tests/data/fallback/la-Scpt-LA-variants.ftl"),
))
.with_locale((
FALLBACK_LANG,
include_str!("../tests/data/fallback/fb-FB.ftl"),
))
.with_fallback(FALLBACK_LANG);
use_init_i18n(|| config)
}

View File

@@ -1,85 +0,0 @@
// Test that macros work correctly when re-exported from another module
// This verifies that $crate is used correctly instead of hard-coded dioxus_i18n
mod reexport_module {
// Re-export the macros as if they were from a different crate
pub use dioxus_i18n::{t, te, tid};
}
mod common;
use common::*;
use dioxus_i18n::prelude::{use_init_i18n, I18n, I18nConfig};
use unic_langid::{langid, LanguageIdentifier};
#[test]
fn reexported_t_macro_works() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| {
let name = "World";
reexport_module::t!("hello", name: name)
});
proxy.assert(panic.is_ok(), true, "reexported_t_macro_works");
proxy.assert(
panic.ok().unwrap(),
"Hello, \u{2068}World\u{2069}!".to_string(),
"reexported_t_macro_works",
);
});
}
#[test]
fn reexported_te_macro_works() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| {
let name = "World";
reexport_module::te!("hello", name: name)
});
proxy.assert(panic.is_ok(), true, "reexported_te_macro_works");
proxy.assert(
panic.ok().unwrap().ok().unwrap(),
"Hello, \u{2068}World\u{2069}!".to_string(),
"reexported_te_macro_works",
);
});
}
#[test]
fn reexported_tid_macro_works() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| {
let name = "World";
reexport_module::tid!("hello", name: name)
});
proxy.assert(panic.is_ok(), true, "reexported_tid_macro_works");
proxy.assert(
panic.ok().unwrap(),
"Hello, \u{2068}World\u{2069}!".to_string(),
"reexported_tid_macro_works",
);
});
}
#[test]
fn reexported_macro_with_invalid_key_as_error() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| reexport_module::te!("invalid"));
proxy.assert(
panic.is_ok(),
true,
"reexported_macro_with_invalid_key_as_error",
);
proxy.assert(
panic.ok().unwrap().err().unwrap().to_string(),
"message id not found for key: 'invalid'".to_string(),
"reexported_macro_with_invalid_key_as_error",
);
});
}
const EN: LanguageIdentifier = langid!("en");
fn i18n_from_static() -> I18n {
let config = I18nConfig::new(EN).with_locale((EN, include_str!("./data/i18n/en.ftl")));
use_init_i18n(|| config)
}

View File

@@ -1,343 +0,0 @@
mod common;
use common::*;
use dioxus_i18n::{
prelude::{use_init_i18n, I18n, I18nConfig},
t, te, tid,
};
use unic_langid::{langid, LanguageIdentifier};
use std::path::PathBuf;
#[test]
fn translate_from_static_source() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| {
let name = "World";
t!("hello", name: name)
});
proxy.assert(panic.is_ok(), true, "translate_from_static_source");
proxy.assert(
panic.ok().unwrap(),
"Hello, \u{2068}World\u{2069}!".to_string(),
"translate_from_static_source",
);
});
}
#[test]
fn failed_to_translate_with_invalid_key() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| {
let _ = &t!("invalid");
});
proxy.assert(panic.is_err(), true, "failed_to_translate_with_invalid_key");
});
}
#[test]
fn failed_to_translate_with_invalid_key_as_error() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| te!("invalid"));
proxy.assert(
panic.is_ok(),
true,
"failed_to_translate_with_invalid_key_as_error",
);
proxy.assert(
panic.ok().unwrap().err().unwrap().to_string(),
"message id not found for key: 'invalid'".to_string(),
"failed_to_translate_with_invalid_key_as_error",
);
});
}
#[test]
fn failed_to_translate_with_invalid_key_with_args_as_error() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| te!("invalid", name: "<don't care>"));
proxy.assert(
panic.is_ok(),
true,
"failed_to_translate_with_invalid_key_with_args_as_error",
);
proxy.assert(
panic.ok().unwrap().err().unwrap().to_string(),
"message id not found for key: 'invalid'".to_string(),
"failed_to_translate_with_invalid_key_with_args_as_error",
);
});
}
#[test]
fn failed_to_translate_with_invalid_key_as_id() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| tid!("invalid"));
proxy.assert(
panic.is_ok(),
true,
"failed_to_translate_with_invalid_key_as_id",
);
proxy.assert(
panic.ok().unwrap(),
"message id not found for key: 'invalid'".to_string(),
"failed_to_translate_with_invalid_key_as_id",
);
});
}
#[test]
fn failed_to_translate_with_invalid_key_with_args_as_id() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| tid!("invalid", name: "<don't care>"));
proxy.assert(
panic.is_ok(),
true,
"failed_to_translate_with_invalid_key_with_args_as_id",
);
proxy.assert(
panic.ok().unwrap(),
"message id not found for key: 'invalid'".to_string(),
"failed_to_translate_with_invalid_key_with_args_as_id",
);
});
}
#[test]
fn translate_root_message_in_attributed_definition() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| tid!("my_component"));
proxy.assert(
panic.is_ok(),
true,
"translate_root_message_in_attributed_definition",
);
proxy.assert(
panic.ok().unwrap(),
"My Component".to_string(),
"translate_root_message_in_attributed_definition",
);
});
}
#[test]
fn translate_attribute_with_no_args_in_attributed_definition() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| tid!("my_component.placeholder"));
proxy.assert(
panic.is_ok(),
true,
"translate_attribute_with_no_args_in_attributed_definition",
);
proxy.assert(
panic.ok().unwrap(),
"Component's placeholder".to_string(),
"translate_attribute_with_no_args_in_attributed_definition",
);
});
}
#[test]
fn translate_attribute_with_args_in_attributed_definition() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| tid!("my_component.hint", name: "Zaphod"));
proxy.assert(
panic.is_ok(),
true,
"translate_attribute_with_args_in_attributed_definition",
);
proxy.assert(
panic.ok().unwrap(),
"Component's hint with parameter \u{2068}Zaphod\u{2069}".to_string(),
"translate_attribute_with_args_in_attributed_definition",
);
});
}
#[test]
fn fail_translate_invalid_attribute_with_no_args_in_attributed_definition() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| tid!("my_component.not_a_placeholder"));
proxy.assert(
panic.is_ok(),
true,
"fail_translate_invalid_attribute_with_no_args_in_attributed_definition",
);
proxy.assert(
panic.ok().unwrap(),
"attribute id not found for key: 'my_component.not_a_placeholder'".to_string(),
"fail_translate_invalid_attribute_with_no_args_in_attributed_definition",
);
});
}
#[test]
fn fail_translate_invalid_attribute_with_args_in_attributed_definition() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| tid!("my_component.not_a_hint", name: "Zaphod"));
proxy.assert(
panic.is_ok(),
true,
"fail_translate_invalid_attribute_with_args_in_attributed_definition",
);
proxy.assert(
panic.ok().unwrap(),
"attribute id not found for key: 'my_component.not_a_hint'".to_string(),
"fail_translate_invalid_attribute_with_args_in_attributed_definition",
);
});
}
#[test]
fn fail_translate_with_invalid_attribute_key() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| tid!("my_component.placeholder.invalid"));
proxy.assert(
panic.is_ok(),
true,
"fail_translate_with_invalid_attribute_key",
);
proxy.assert(
panic.ok().unwrap(),
"invalid message id: 'my_component.placeholder.invalid'".to_string(),
"fail_translate_with_invalid_attribute_key",
);
});
}
#[test]
fn translate_from_dynamic_source() {
test_hook(i18n_from_dynamic, |_, proxy| {
let panic = std::panic::catch_unwind(|| {
let name = "World";
t!("hello", name: name)
});
proxy.assert(panic.is_ok(), true, "translate_from_dynamic_source");
proxy.assert(
panic.ok().unwrap(),
"Hello, \u{2068}World\u{2069}!".to_string(),
"translate_from_dynamic_source",
);
});
}
#[test]
#[should_panic]
#[ignore] // Panic hidden within test_hook.
fn fail_translate_from_dynamic_source_when_file_does_not_exist() {
test_hook(i18n_from_dynamic_none_existing, |_, _| unreachable!());
}
#[test]
fn initial_language_is_set() {
test_hook(i18n_from_static, |value, proxy| {
proxy.assert(value.language(), EN, "initial_language_is_set");
});
}
#[test]
fn language_can_be_set() {
test_hook(i18n_from_static, |mut value, proxy| {
value
.try_set_language(JP)
.expect("set_language must succeed");
proxy.assert(value.language(), JP, "language_can_be_set");
});
}
#[test]
fn no_default_fallback_language() {
test_hook(i18n_from_static, |value, proxy| {
proxy.assert(
format!("{:?}", value.fallback_language()),
"None".to_string(),
"no_default_fallback_language",
);
});
}
#[test]
fn some_default_fallback_language() {
test_hook(i18n_from_static_with_fallback, |value, proxy| {
proxy.assert(
format!("{:?}", value.fallback_language().map(|l| l.to_string())),
"Some(\"jp\")".to_string(),
"some_default_fallback_language",
);
});
}
#[test]
fn fallback_language_can_be_set() {
test_hook(i18n_from_static_with_fallback, |mut value, proxy| {
value
.try_set_fallback_language(EN)
.expect("try_set_fallback_language must succeed");
proxy.assert(
format!("{:?}", value.fallback_language().map(|l| l.to_string())),
"Some(\"en\")".to_string(),
"fallback_language_can_be_set",
);
});
}
#[test]
fn fallback_language_must_have_locale_translation() {
test_hook(i18n_from_static_with_fallback, |mut value, proxy| {
let result = value.try_set_fallback_language(IT);
proxy.assert(
result.is_err(),
true,
"fallback_language_must_have_locale_translation",
);
proxy.assert(
result.err().unwrap().to_string(),
"fallback for \"it\" must have locale".to_string(),
"fallback_language_must_have_locale_translation",
);
proxy.assert(
format!("{:?}", value.fallback_language().map(|l| l.to_string())),
"Some(\"jp\")".to_string(),
"fallback_language_must_have_locale_translation",
);
});
}
const EN: LanguageIdentifier = langid!("en");
const IT: LanguageIdentifier = langid!("it");
const JP: LanguageIdentifier = langid!("jp");
fn i18n_from_static() -> I18n {
let config = I18nConfig::new(EN).with_locale((EN, include_str!("./data/i18n/en.ftl")));
use_init_i18n(|| config)
}
fn i18n_from_static_with_fallback() -> I18n {
let config = I18nConfig::new(EN)
.with_locale((EN, include_str!("./data/i18n/en.ftl")))
.with_fallback(JP);
use_init_i18n(|| config)
}
fn i18n_from_dynamic() -> I18n {
let config = I18nConfig::new(EN).with_locale((
EN,
PathBuf::from(format!(
"{}/tests/data/i18n/en.ftl",
env!("CARGO_MANIFEST_DIR")
)),
));
use_init_i18n(|| config)
}
fn i18n_from_dynamic_none_existing() -> I18n {
let config = I18nConfig::new(EN).with_locale((
EN,
PathBuf::from(format!(
"{}/tests/data/i18n/non_existing.ftl",
env!("CARGO_MANIFEST_DIR")
)),
));
use_init_i18n(|| config)
}

View File

@@ -2,4 +2,5 @@
services:
app:
build:
dockerfile: docker/prod/app/Dockerfile
target: builder_android

View File

@@ -12,6 +12,7 @@ services:
- ./Cargo.toml:/srv/app/Cargo.toml
- ./diesel.toml:/srv/app/diesel.toml
- ./Dioxus.toml:/srv/app/Dioxus.toml
- ./tailwind.css:/srv/app/tailwind.css
restart: always
ports: ["8000:8000"]
depends_on: ["db"]

View File

@@ -6,6 +6,7 @@ services:
networks:
- default
- web-server-network
ports: ["8000:80"]
restart: always
depends_on: ["db"]

View File

@@ -5,7 +5,7 @@ RUN useradd -m -u 1000 -s /bin/bash app_user \
USER app_user
RUN cargo install --git https://github.com/diesel-rs/diesel --rev 2e85ba060d3d70ea605ea58a79b8a435749a7adc --locked diesel_cli \
RUN cargo install --git https://github.com/diesel-rs/diesel --rev 207604888d28a490061698f07a25090438be42fe --locked diesel_cli \
&& cargo install --git https://github.com/DioxusLabs/dioxus --rev 22b06badde44ba1af0fcf339c91b66483175b660 --locked dioxus-cli
COPY --chown=app_user . /srv/app

View File

@@ -1,6 +1,6 @@
FROM rust:1.92.0-bookworm@sha256:9676d0547a259997add8f5924eb6b959c589ed39055338e23b99aba7958d6d31 AS builder_base
RUN cargo install --git https://github.com/diesel-rs/diesel --rev 2e85ba060d3d70ea605ea58a79b8a435749a7adc --locked diesel_cli \
RUN cargo install --git https://github.com/diesel-rs/diesel --rev 207604888d28a490061698f07a25090438be42fe --locked diesel_cli \
&& cargo install --git https://github.com/DioxusLabs/dioxus --rev 22b06badde44ba1af0fcf339c91b66483175b660 --locked dioxus-cli --features disable-telemetry
COPY . /srv/app
@@ -39,7 +39,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& keytool -genkeypair -noprompt -keystore /tmp/android_keystore.jks -alias key -keyalg RSA -keysize 2048 -validity 3660 -dname "CN=" -storepass 123456 -keypass 123456 \
&& export ANDROID_HOME="$ANDROID_SDK_ROOT" \
&& export ANDROID_NDK_HOME="$ANDROID_SDK_ROOT/ndk/$ANDROID_NDK_VERSION" \
&& dx bundle --platform android --target aarch64-linux-android --release \
&& dx bundle --locked --platform android --target aarch64-linux-android --release \
&& java -jar /tmp/bundletool-all.jar build-apks --bundle=/srv/app/target/dx/todo_baggins/release/android/app/app/build/outputs/bundle/release/TodoBaggins-aarch64-linux-android.aab --output=/tmp/todo_baggins.apks --mode=universal --ks=/tmp/android_keystore.jks --ks-key-alias=key --ks-pass=pass:123456 \
&& mkdir -p /srv/app/bundle \
&& unzip -qp /tmp/todo_baggins.apks universal.apk > /srv/app/bundle/todo_baggins.apk
@@ -47,7 +47,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
FROM builder_base AS builder_web
RUN dx bundle --release
RUN dx bundle --locked --release
FROM debian:bookworm@sha256:b877a1a3fdf02469440f1768cf69c9771338a875b7add5e80c45b756c92ac20a AS runner_web

View File

@@ -7,15 +7,22 @@ use dioxus_i18n::prelude::*;
use dioxus_i18n::unic_langid::langid;
const FAVICON: Asset = asset!("/assets/favicon.ico");
const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css");
/* Once https://github.com/DioxusLabs/dioxus/issues/4490 is resolved, hopefully it will be
sufficient to just include the single icon.png. */
#[used]
static IMAGES_DIRECTORY: Asset = asset!(
"/assets/images",
AssetOptions::builder().with_hash_suffix(false)
);
#[used]
static FONTS_DIRECTORY: Asset = asset!(
"/assets/fonts",
AssetOptions::builder().with_hash_suffix(false)
);
const FONTS_CSS: Asset = asset!("/assets/styles/fonts.css");
const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css");
const INPUT_NUMBER_ARROWS_CSS: Asset = asset!("/assets/styles/input_number_arrows.css");
const INPUT_RANGE_CSS: Asset = asset!("/assets/styles/input_range.css");
const SELECT_ARROW_CSS: Asset = asset!("/assets/styles/select_arrow.css");
const MANIFEST: Asset = asset!("/assets/manifest.json");
#[component]
@@ -35,14 +42,13 @@ pub(crate) fn App() -> Element {
rsx! {
document::Link { rel: "icon", href: FAVICON }
document::Stylesheet { href: TAILWIND_CSS }
document::Stylesheet { href: FONTS_CSS }
document::Stylesheet { href: INPUT_NUMBER_ARROWS_CSS }
document::Stylesheet { href: INPUT_RANGE_CSS }
document::Stylesheet { href: SELECT_ARROW_CSS }
document::Link { rel: "manifest", href: MANIFEST, crossorigin: "use-credentials" }
document::Script { src: "https://kit.fontawesome.com/3c1b409f8f.js" }
div {
class: "min-h-screen pt-4 pb-36 flex flex-col text-zinc-200 bg-zinc-800",
class: "min-h-screen py-4 flex flex-col text-gray-300 bg-gray-900",
Router::<Route> {}
}
}

View File

@@ -1,78 +1,22 @@
use crate::components::error_boundary_message::ErrorBoundaryMessage;
use crate::components::navigation::Navigation;
use crate::components::project_form::ProjectForm;
use crate::components::task_form::TaskForm;
use crate::models::project::Project;
use crate::models::task::Task;
use crate::route::Route;
use dioxus::prelude::*;
#[component]
pub(crate) fn BottomPanel(display_form: Signal<bool>) -> Element {
// A signal for delaying the application of styles.
#[allow(clippy::redundant_closure)]
let mut expanded = use_signal(|| display_form());
pub(crate) fn BottomPanel() -> Element {
let navigation_expanded = use_signal(|| false);
let current_route = use_route();
let mut project_being_edited = use_context::<Signal<Option<Project>>>();
let mut task_being_edited = use_context::<Signal<Option<Task>>>();
use_effect(use_reactive(&display_form, move |display_form| {
if display_form() {
expanded.set(true);
} else {
spawn(async move {
// Necessary for a smooth not instant height transition.
#[cfg(not(feature = "server"))]
async_std::task::sleep(std::time::Duration::from_millis(500)).await;
/* The check is necessary for the situation when the user expands the panel while
it is being closed. */
if !display_form() {
expanded.set(false);
}
});
}
}));
rsx! {
div {
class: format!(
"flex flex-col pointer-events-auto bg-zinc-700/50 rounded-t-xl border-t-zinc-600 border-t backdrop-blur drop-shadow-[0_-5px_10px_rgba(0,0,0,0.2)] transition-[height] duration-[500ms] ease-[cubic-bezier(0.79,0.14,0.15,0.86)] overflow-y-scroll {}",
match (display_form(), current_route, navigation_expanded()) {
(false, _, false) => "h-[66px]",
(false, _, true) => "h-[130px]",
(true, Route::ProjectsPage, _) => "h-[130px]",
(true, _, _) => "h-[506px]",
"flex flex-col pointer-events-auto bg-gray-800 transition-[height] duration-[500ms] ease-[cubic-bezier(0.79,0.14,0.15,0.86)] overflow-y-scroll {}",
if navigation_expanded() {
"h-[130px]"
} else {
"h-[66px]"
}
),
if expanded() {
ErrorBoundaryMessage {
match current_route {
Route::ProjectsPage => rsx! {
ProjectForm {
project: project_being_edited(),
on_successful_submit: move |_| {
display_form.set(false);
project_being_edited.set(None);
}
}
},
_ => rsx! {
TaskForm {
task: task_being_edited(),
on_successful_submit: move |_| {
display_form.set(false);
task_being_edited.set(None);
}
}
}
}
}
} else {
Navigation {
expanded: navigation_expanded,
}
Navigation {
is_expanded: navigation_expanded,
}
}
}

View File

@@ -0,0 +1,29 @@
use dioxus::prelude::*;
#[component]
pub(crate) fn ButtonPrimary(
class: Option<String>,
children: Element,
#[props(extends = GlobalAttributes, extends = button)] attributes: Vec<Attribute>,
// TODO: Remove this once https://github.com/DioxusLabs/dioxus/issues/4019 gets resolved.
onclick: Option<Callback<Event<MouseData>>>,
) -> Element {
rsx! {
button {
class: format!(
"cursor-pointer pb-[6px] hover:pb-[7px] active:pb-[2px] mt-[1px] hover:mt-0 active:mt-[5px] hover:*:drop-shadow-[0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted)] active:*:drop-shadow-[0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted)] transition-all duration-150 {}",
class.unwrap_or("".to_owned())
),
onclick: move |event| {
if let Some(onclick) = onclick {
onclick.call(event);
}
},
..attributes,
div {
class: "py-3.5 px-4 flex flex-row justify-center items-center bg-amber-300-muted drop-shadow-[0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted)] text-amber-700-muted rounded-xl transition-all duration-150",
{children}
}
}
}
}

View File

@@ -0,0 +1,29 @@
use dioxus::prelude::*;
#[component]
pub(crate) fn ButtonSecondary(
class: Option<String>,
children: Element,
#[props(extends = GlobalAttributes, extends = button)] attributes: Vec<Attribute>,
// TODO: Remove this once https://github.com/DioxusLabs/dioxus/issues/4019 gets resolved.
onclick: Option<Callback<Event<MouseData>>>,
) -> Element {
rsx! {
button {
class: format!(
"cursor-pointer pb-[6px] hover:pb-[7px] active:pb-[2px] mt-[1px] hover:mt-0 active:mt-[5px] hover:*:drop-shadow-[0_7px_0_var(--color-gray-800)] active:*:drop-shadow-[0_2px_0_var(--color-gray-800)] transition-all duration-150 {}",
class.unwrap_or("".to_owned())
),
onclick: move |event| {
if let Some(onclick) = onclick {
onclick.call(event);
}
},
..attributes,
div {
class: "py-3.5 px-4 flex flex-row justify-center items-center bg-gray-600 drop-shadow-[0_6px_0_var(--color-gray-800)] rounded-xl transition-all duration-150",
{children}
}
}
}
}

View File

@@ -28,11 +28,12 @@ pub(crate) fn CategoryCalendarTaskList() -> Element {
div {
class: "flex flex-col gap-4",
div {
class: "px-8 flex flex-row items-center gap-2 font-bold",
class: "px-7 flex flex-row items-center gap-2 text-gray-500 font-bold",
div {
class: "pt-1",
{
date_current.format_localized(t!(
date_current.format_localized(
t!(
if date_current.year() == Local::now().year() {
"date-weekday-format"
} else {

View File

@@ -1,7 +1,12 @@
use crate::components::select_button::SelectButton;
use crate::models::category::Category;
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
use dioxus_free_icons::icons::fa_regular_icons::FaLightbulb;
use dioxus_free_icons::icons::fa_solid_icons::{
FaCalendarDays, FaHourglassHalf, FaInbox, FaSignsPost, FaWater,
};
#[component]
pub(crate) fn CategoryInput(
@@ -10,93 +15,51 @@ pub(crate) fn CategoryInput(
) -> Element {
rsx! {
div {
class: format!("flex flex-row gap-2 {}", class.unwrap_or("")),
button {
r#type: "button",
class: format!(
"py-2 rounded-lg grow basis-0 {} cursor-pointer",
if selected_category() == Category::SomedayMaybe { "bg-zinc-500/50" }
else { "bg-zinc-800/50" }
),
onclick: move |_| {
class: format!("grid grid-cols-3 gap-3 {}", class.unwrap_or("")),
SelectButton {
icon: FaLightbulb,
is_selected: matches!(selected_category(), Category::SomedayMaybe),
on_select: move |_| {
selected_category.set(Category::SomedayMaybe);
},
i {
class: "fa-solid fa-question"
}
},
button {
r#type: "button",
class: format!(
"py-2 rounded-lg grow basis-0 {} cursor-pointer",
if selected_category() == Category::LongTerm { "bg-zinc-500/50" }
else { "bg-zinc-800/50" }
),
onclick: move |_| {
}
SelectButton {
icon: FaWater,
is_selected: matches!(selected_category(), Category::LongTerm),
on_select: move |_| {
selected_category.set(Category::LongTerm);
},
i {
class: "fa-solid fa-water"
}
},
button {
r#type: "button",
class: format!(
"py-2 rounded-lg grow basis-0 {} cursor-pointer",
if let Category::WaitingFor(_) = selected_category() { "bg-zinc-500/50" }
else { "bg-zinc-800/50" }
),
onclick: move |_| {
}
SelectButton {
icon: FaHourglassHalf,
is_selected: matches!(selected_category(), Category::WaitingFor(_)),
on_select: move |_| {
selected_category.set(Category::WaitingFor(String::new()));
},
i {
class: "fa-solid fa-hourglass-half"
}
},
button {
r#type: "button",
class: format!(
"py-2 rounded-lg grow basis-0 {} cursor-pointer",
if selected_category() == Category::NextSteps { "bg-zinc-500/50" }
else { "bg-zinc-800/50" }
),
onclick: move |_| {
}
SelectButton {
icon: FaSignsPost,
is_selected: matches!(selected_category(), Category::NextSteps),
on_select: move |_| {
selected_category.set(Category::NextSteps);
},
i {
class: "fa-solid fa-forward"
}
},
button {
r#type: "button",
class: format!(
"py-2 rounded-lg grow basis-0 {} cursor-pointer",
if let Category::Calendar { .. } = selected_category() { "bg-zinc-500/50" }
else { "bg-zinc-800/50" }
),
onclick: move |_| {
}
SelectButton {
icon: FaCalendarDays,
is_selected: matches!(selected_category(), Category::Calendar { .. }),
on_select: move |_| {
selected_category.set(Category::Calendar {
date: chrono::Local::now().date_naive(),
reoccurrence: None,
time: None,
});
},
i {
class: "fa-solid fa-calendar-days"
}
},
button {
r#type: "button",
class: format!(
"py-2 rounded-lg grow basis-0 {} cursor-pointer",
if selected_category() == Category::Inbox { "bg-zinc-500/50" }
else { "bg-zinc-800/50" }
),
onclick: move |_| {
}
SelectButton {
icon: FaInbox,
is_selected: matches!(selected_category(), Category::Inbox),
on_select: move |_| {
selected_category.set(Category::Inbox);
},
i {
class: "fa-solid fa-inbox"
}
}
}

View File

@@ -1,11 +1,12 @@
use crate::components::task_list::TaskList;
use crate::components::task_list_item::TaskListItem;
use crate::hooks::use_tasks_with_subtasks_in_category;
use crate::internationalization::LocaleFromLanguageIdentifier;
use crate::models::category::Category;
use crate::models::task::TaskWithSubtasks;
use chrono::Local;
use dioxus::prelude::*;
use dioxus_free_icons::Icon;
use dioxus_free_icons::icons::fa_solid_icons::{FaCalendarCheck, FaCalendarXmark, FaWater};
use dioxus_i18n::t;
use dioxus_i18n::use_i18n::i18n;
use voca_rs::Voca;
@@ -45,34 +46,21 @@ pub(crate) fn CategoryTodayTaskList() -> Element {
rsx! {
div {
class: "pt-4 flex flex-col gap-8",
div {
class: "flex flex-col gap-4",
if !long_term_tasks.is_empty() {
div {
class: "px-8 flex flex-row items-center gap-2 font-bold",
i {
class: "fa-solid fa-water text-xl w-6 text-center"
}
class: "flex flex-col gap-4",
div {
class: "mt-1",
{t!("long-term")._upper_first()}
}
}
div {
for task in long_term_tasks {
div {
key: "{task.task.id}",
class: format!(
"px-8 pt-5 {} flex flex-row gap-4",
if task.task.deadline.is_some() {
"pb-0.5"
} else {
"pb-5"
}
),
TaskListItem {
task: task.clone()
}
class: "px-7 flex flex-row items-center gap-2 text-gray-500 font-bold",
Icon {
class: "mx-1.5",
icon: FaWater
}
div {
{t!("long-term")._upper_first()}
}
}
TaskList {
tasks: long_term_tasks
}
}
}
@@ -80,12 +68,14 @@ pub(crate) fn CategoryTodayTaskList() -> Element {
div {
class: "flex flex-col gap-4",
div {
class: "px-8 flex flex-row items-center gap-2 font-bold",
i {
class: "fa-solid fa-calendar-xmark text-xl w-6 text-center"
class: "px-7 flex flex-row items-center gap-2 text-gray-500 font-bold",
Icon {
class: "mx-1.25",
height: 22,
width: 22,
icon: FaCalendarXmark
}
div {
class: "mt-1",
{t!("overdue")._upper_first()}
}
}
@@ -98,12 +88,14 @@ pub(crate) fn CategoryTodayTaskList() -> Element {
div {
class: "flex flex-col gap-4",
div {
class: "px-8 flex flex-row items-center gap-2 font-bold",
i {
class: "fa-solid fa-calendar-check text-xl w-6 text-center"
class: "px-7 flex flex-row items-center gap-2 text-gray-500 font-bold",
Icon {
class: "mx-1.25",
height: 22,
width: 22,
icon: FaCalendarCheck
}
div {
class: "mt-1",
{
let format = t!("date-weekday-format");
let today_date = today_date.format_localized(

View File

@@ -0,0 +1,31 @@
use crate::components::project_form::PROJECT_BEING_EDITED;
use crate::components::{button_primary::ButtonPrimary, task_form::TASK_BEING_EDITED};
use crate::route::Route;
use dioxus::prelude::*;
use dioxus_free_icons::{Icon, icons::fa_solid_icons::FaGavel};
#[component]
pub(crate) fn CreateButton() -> Element {
let navigator = use_navigator();
let current_route = use_route();
rsx! {
ButtonPrimary {
class: "pointer-events-auto m-4 self-end *:rounded-full! *:p-4",
onclick: move |_| {
*TASK_BEING_EDITED.write() = None;
*PROJECT_BEING_EDITED.write() = None;
navigator.push(
match current_route {
Route::ProjectsPage => Route::ProjectFormPage,
_ => Route::TaskFormPage,
}
);
},
Icon {
icon: FaGavel,
height: 24,
width: 24
}
}
}
}

View File

@@ -1,6 +1,8 @@
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
use dioxus_free_icons::Icon;
use dioxus_free_icons::icons::fa_solid_icons::FaTriangleExclamation;
#[component]
pub(crate) fn ErrorBoundaryMessage(children: Element, class: Option<String>) -> Element {
@@ -11,8 +13,11 @@ pub(crate) fn ErrorBoundaryMessage(children: Element, class: Option<String>) ->
div {
class: "grow flex flex-col justify-center items-center",
div {
i {
class: "text-3xl fa-solid fa-triangle-exclamation"
Icon {
class: "text-gray-500",
icon: FaTriangleExclamation,
height: 32,
width: 32
}
}
}

View File

@@ -1,25 +0,0 @@
use crate::models::project::Project;
use crate::models::task::Task;
use dioxus::prelude::*;
#[component]
pub(crate) fn FormOpenButton(opened: Signal<bool>) -> Element {
let mut project_being_edited = use_context::<Signal<Option<Project>>>();
let mut task_being_edited = use_context::<Signal<Option<Task>>>();
rsx! {
button {
class: "pointer-events-auto m-4 py-3 px-5 self-end text-center bg-zinc-300/50 rounded-xl border-t-zinc-200 border-t backdrop-blur drop-shadow-[0_-5px_10px_rgba(0,0,0,0.2)] text-2xl text-zinc-200 cursor-pointer",
onclick: move |_| {
if opened() {
project_being_edited.set(None);
task_being_edited.set(None);
}
opened.set(!opened());
},
i {
class: format!("min-w-6 fa-solid {}", if opened() { "fa-xmark" } else { "fa-plus" }),
}
}
}
}

53
src/components/input.rs Normal file
View File

@@ -0,0 +1,53 @@
use dioxus::prelude::*;
#[component]
pub(crate) fn Input(
class: Option<String>,
name: String,
r#type: String,
id: Option<String>,
#[props(extends = GlobalAttributes, extends = input)] attributes: Vec<Attribute>,
// TODO: Remove this once https://github.com/DioxusLabs/dioxus/issues/5271 gets resolved.
autofocus: Option<bool>,
// TODO: Remove this once https://github.com/DioxusLabs/dioxus/issues/4019 gets resolved.
oninput: Option<Callback<Event<FormData>>>,
onchange: Option<Callback<Event<FormData>>>,
) -> Element {
rsx! {
input {
class: format!(
/* `w-full` is required for the Chromium renderer to allow the input to shrink
properly. */
"pt-3 pb-2.25 w-full {} bg-gray-800-muted enabled:hover:bg-gray-800 enabled:focus:bg-gray-800 drop-shadow-[0_calc(0px_-_var(--spacing))_0_var(--color-gray-900-muted)] rounded-xl outline-0 {} transition-all duration-150 {}",
match r#type.as_str() {
"date" => "ps-3.25 pe-3",
_ => "px-4"
},
match r#type.as_str() {
"text" | "number" => "",
_ => "enabled:cursor-pointer"
},
class.unwrap_or("".to_owned())
),
name: name.clone(),
r#type,
id: id.unwrap_or(format!("input_{}", name)),
oninput: move |event| {
if let Some(oninput) = oninput {
oninput.call(event);
}
},
onchange: move |event| {
if let Some(onchange) = oninput {
onchange.call(event);
}
},
onmounted: move |element| async move {
if let Some(true) = autofocus {
let _ = element.set_focus(true).await;
}
},
..attributes
}
}
}

View File

@@ -0,0 +1,21 @@
use dioxus::prelude::*;
use dioxus_free_icons::{Icon, IconShape};
#[component]
pub(crate) fn InputLabel<I: IconShape + Clone + PartialEq + 'static>(
icon: I,
r#for: Option<String>,
) -> Element {
rsx! {
label {
r#for,
class: "mt-0.5 min-w-7 flex flex-row justify-center items-center",
Icon {
class: "text-gray-600",
icon,
height: 16,
width: 16
}
}
}
}

View File

@@ -1,16 +1,21 @@
pub(crate) mod app;
pub(crate) mod bottom_panel;
pub(crate) mod button_primary;
pub(crate) mod button_secondary;
pub(crate) mod category_calendar_task_list;
pub(crate) mod category_input;
pub(crate) mod category_today_task_list;
pub(crate) mod create_button;
pub(crate) mod error_boundary_message;
pub(crate) mod form_open_button;
pub(crate) mod input;
pub(crate) mod input_label;
pub(crate) mod navigation;
pub(crate) mod navigation_item;
pub(crate) mod project_form;
pub(crate) mod project_list;
pub(crate) mod project_select;
pub(crate) mod reoccurrence_input;
pub(crate) mod reoccurrence_interval_input;
pub(crate) mod select_button;
pub(crate) mod sticky_bottom;
pub(crate) mod subtasks_form;
pub(crate) mod task_form;

View File

@@ -1,78 +1,72 @@
use crate::components::navigation_item::NavigationItem;
use crate::route::Route;
use dioxus::prelude::*;
use dioxus_free_icons::Icon;
use dioxus_free_icons::icons::fa_regular_icons::FaLightbulb;
use dioxus_free_icons::icons::fa_solid_icons::{
FaBars, FaCalendarDay, FaCalendarDays, FaHourglassHalf, FaInbox, FaList, FaSignsPost,
FaTrashCan, FaVolcano,
};
#[component]
pub(crate) fn Navigation(expanded: Signal<bool>) -> Element {
pub(crate) fn Navigation(is_expanded: Signal<bool>) -> Element {
rsx! {
div {
class: "grid grid-cols-5 justify-stretch",
button {
class: format!(
"py-4 text-center text-2xl {} cursor-pointer",
if expanded() { "text-zinc-200" }
else { "text-zinc-500" }
"py-2 flex flex-row justify-center items-center cursor-pointer",
),
onclick: move |_| expanded.set(!expanded()),
i {
class: "fa-solid fa-bars"
onclick: move |_| is_expanded.set(!is_expanded()),
div {
class: format!("pt-2.5 px-4 {} transition-all duration-150",
if is_expanded() { "pb-2 mt-1 bg-gray-900 text-gray-400 rounded-xl drop-shadow-[0_calc(0px_-_var(--spacing))_0_var(--color-gray-950)]" }
else { "pb-3 bg-gray-800 rounded-xl drop-shadow-[0_0_0_var(--color-gray-950)] text-gray-600" }
),
Icon {
icon: FaBars,
height: 24,
width: 24
}
}
},
NavigationItem {
route: Route::CategoryNextStepsPage,
i {
class: "fa-solid fa-forward"
}
icon: FaSignsPost
},
NavigationItem {
route: Route::CategoryCalendarPage,
i {
class: "fa-solid fa-calendar-days"
}
icon: FaCalendarDays
},
NavigationItem {
route: Route::CategoryTodayPage,
i {
class: "fa-solid fa-calendar-day"
}
icon: FaCalendarDay
},
NavigationItem {
route: Route::CategoryInboxPage,
i {
class: "fa-solid fa-inbox"
}
icon: FaInbox
},
{if expanded() {
{if is_expanded() {
rsx! {
NavigationItem {
route: Route::ProjectsPage,
i {
class: "fa-solid fa-list"
}
icon: FaList
},
NavigationItem {
route: Route::CategoryTrashPage,
i {
class: "fa-solid fa-trash-can"
}
icon: FaTrashCan
},
NavigationItem {
route: Route::CategoryDonePage,
i {
class: "fa-solid fa-check"
}
icon: FaVolcano
},
NavigationItem {
route: Route::CategoryLongTermPage,
i {
class: "fa-solid fa-water"
}
route: Route::CategorySomedayMaybePage,
icon: FaLightbulb
},
NavigationItem {
route: Route::CategoryWaitingForPage,
i {
class: "fa-solid fa-hourglass-half"
}
icon: FaHourglassHalf
}
}
} else { VNode::empty() }}

View File

@@ -1,19 +1,31 @@
use crate::route::Route;
use dioxus::prelude::*;
use dioxus_free_icons::{Icon, IconShape};
#[component]
pub(crate) fn NavigationItem(route: Route, children: Element) -> Element {
pub(crate) fn NavigationItem<I: IconShape + Clone + PartialEq + 'static>(
route: Route,
icon: I,
) -> Element {
let current_route = use_route::<Route>();
rsx! {
Link {
to: route.clone(),
class: format!(
"py-4 text-center text-2xl {}",
if current_route == route { "text-zinc-200" }
else { "text-zinc-500" }
"py-2.5 flex flex-row justify-center items-center hover:*:bg-gray-900 active:*:text-gray-400",
),
children
div {
class: format!("pt-2.5 px-4 {} transition-all duration-150",
if current_route == route { "pb-2 mt-1 bg-gray-900 text-gray-400 rounded-xl drop-shadow-[0_calc(0px_-_var(--spacing))_0_var(--color-gray-950)]" }
else { "pb-3 bg-gray-800 rounded-xl drop-shadow-[0_0_0_var(--color-gray-950)] text-gray-600" }
),
Icon {
icon,
height: 24,
width: 24
}
}
}
}
}

View File

@@ -1,73 +1,110 @@
use crate::components::button_primary::ButtonPrimary;
use crate::components::button_secondary::ButtonSecondary;
use crate::components::input::Input;
use crate::components::input_label::InputLabel;
use crate::models::project::Project;
use crate::server::projects::{create_project, delete_project, edit_project};
use dioxus::core_macro::{component, rsx};
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
use dioxus_free_icons::Icon;
use dioxus_free_icons::icons::fa_solid_icons::{FaFeatherPointed, FaStamp, FaTrashCan, FaXmark};
pub(crate) static PROJECT_BEING_EDITED: GlobalSignal<Option<Project>> = Signal::global(|| None);
#[component]
pub(crate) fn ProjectForm(
project: Option<Project>,
on_successful_submit: EventHandler<()>,
) -> Element {
pub(crate) fn ProjectForm() -> Element {
let navigator = use_navigator();
let project = PROJECT_BEING_EDITED();
let project_for_submit = project.clone();
rsx! {
form {
class: "px-4 flex flex-col gap-4",
onsubmit: move |event| {
event.prevent_default();
let project = project_for_submit.clone();
async move {
let new_project = event.parsed_values().unwrap();
if let Some(project) = project {
let _ = edit_project(project.id, new_project).await;
let result = if let Some(project) = project {
edit_project(project.id, new_project).await
} else {
let _ = create_project(new_project).await;
create_project(new_project).await
};
if result.is_ok() {
navigator.go_back();
}
on_successful_submit.call(());
}
},
class: "p-4 flex flex-col gap-4",
id: "form_project",
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "input_title",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-pen-clip text-zinc-400/50"
}
InputLabel {
icon: FaFeatherPointed,
r#for: "input_title"
}
input {
Input {
class: "grow",
name: "title",
required: true,
initial_value: project.as_ref().map(|project| project.title.to_owned()),
r#type: "text",
class: "py-2 px-3 grow bg-zinc-800/50 rounded-lg",
id: "input_title"
initial_value: project.as_ref().map(|project| project.title.to_owned()),
}
}
div {
class: "flex flex-row justify-between mt-auto",
button {
r#type: "button",
class: "py-2 px-4 bg-zinc-300/50 rounded-lg cursor-pointer",
onclick: move |_| {
}
div {
class: "px-4 grid grid-cols-3 gap-3 mt-auto",
ButtonSecondary {
r#type: "button",
class: "grow",
onclick: {
let project = project.clone();
move |_| {
let project = project.clone();
async move {
if let Some(project) = project {
let _ = delete_project(project.id).await;
let result = delete_project(project.id).await;
if result.is_ok() {
navigator.go_back();
}
} else {
navigator.go_back();
}
}
}
},
Icon {
icon: FaTrashCan,
height: 16,
width: 16
}
}
if project.is_some() {
div {
class: "grow flex flex-col items-stretch",
GoBackButton {
ButtonSecondary {
/* TODO: Replace w-full` with proper flexbox styling once
https://github.com/DioxusLabs/dioxus/issues/5269 is solved. */
class: "w-full",
r#type: "button",
Icon {
icon: FaXmark,
height: 16,
width: 16
}
on_successful_submit.call(());
}
},
i {
class: "fa-solid fa-trash-can"
}
}
button {
r#type: "submit",
class: "py-2 px-4 bg-zinc-300/50 rounded-lg cursor-pointer",
i {
class: "fa-solid fa-floppy-disk"
}
} else {
div {}
}
ButtonPrimary {
form: "form_project",
r#type: "submit",
Icon {
icon: FaStamp,
height: 16,
width: 16
}
}
}

View File

@@ -1,24 +1,22 @@
use crate::{hooks::use_projects, models::project::Project};
use crate::route::Route;
use crate::{components::project_form::PROJECT_BEING_EDITED, hooks::use_projects};
use dioxus::prelude::*;
#[component]
pub(crate) fn ProjectList() -> Element {
let navigator = use_navigator();
let projects = use_projects()?;
let mut project_being_edited = use_context::<Signal<Option<Project>>>();
rsx! {
div {
class: "flex flex-col",
for project in projects {
div {
class: "px-7 py-4 hover:bg-gray-800 font-medium text-pretty wrap-anywhere select-none transition-all duration-150 cursor-pointer",
key: "{project.id}",
class: format!(
"px-8 py-4 select-none {}",
if project_being_edited().is_some_and(|p| p.id == project.id) {
"bg-zinc-700"
} else { "" }
),
onclick: move |_| project_being_edited.set(Some(project.clone())),
onclick: move |_| {
*PROJECT_BEING_EDITED.write() = Some(project.clone());
navigator.push(Route::ProjectFormPage);
},
{project.title.clone()}
}
}

View File

@@ -10,8 +10,8 @@ pub(crate) fn ProjectSelect(initial_selected_id: Option<i32>) -> Element {
rsx! {
select {
name: "project_id",
class: "px-3.5 py-2.5 bg-zinc-800/50 rounded-lg grow cursor-pointer",
id: "input_project",
class: "px-4 pt-3 pb-2.25 bg-gray-800-muted enabled:hover:bg-gray-800 enabled:active:bg-gray-800 drop-shadow-[0_calc(0px_-_var(--spacing))_0_var(--color-gray-900-muted)] rounded-xl grow cursor-pointer transition-all duration-150",
id: "input_project_id",
option {
value: 0,
{t!("none")}

View File

@@ -1,76 +0,0 @@
use crate::models::category::ReoccurrenceInterval;
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
#[component]
pub(crate) fn ReoccurrenceIntervalInput(
reoccurrence_interval: Signal<Option<ReoccurrenceInterval>>,
class_buttons: Option<&'static str>,
) -> Element {
rsx! {
button {
r#type: "button",
class: format!(
"py-2 rounded-lg {} {} cursor-pointer",
class_buttons.unwrap_or(""),
if reoccurrence_interval().is_none() { "bg-zinc-500/50" }
else { "bg-zinc-800/50" }
),
onclick: move |_| {
reoccurrence_interval.set(None);
},
i {
class: "fa-solid fa-ban"
}
},
button {
r#type: "button",
class: format!(
"py-2 rounded-lg {} {} cursor-pointer",
class_buttons.unwrap_or(""),
if let Some(ReoccurrenceInterval::Day) = reoccurrence_interval()
{ "bg-zinc-500/50" }
else { "bg-zinc-800/50" }
),
onclick: move |_| {
reoccurrence_interval.set(Some(ReoccurrenceInterval::Day))
},
i {
class: "fa-solid fa-sun"
}
},
button {
r#type: "button",
class: format!(
"py-2 rounded-lg {} {} cursor-pointer",
class_buttons.unwrap_or(""),
if let Some(ReoccurrenceInterval::Month) = reoccurrence_interval()
{ "bg-zinc-500/50" }
else { "bg-zinc-800/50" }
),
onclick: move |_| {
reoccurrence_interval.set(Some(ReoccurrenceInterval::Month))
},
i {
class: "fa-solid fa-moon"
}
},
button {
r#type: "button",
class: format!(
"py-2 rounded-lg {} {} cursor-pointer",
class_buttons.unwrap_or(""),
if let Some(ReoccurrenceInterval::Year) = reoccurrence_interval()
{ "bg-zinc-500/50" }
else { "bg-zinc-800/50" }
),
onclick: move |_| {
reoccurrence_interval.set(Some(ReoccurrenceInterval::Year))
},
i {
class: "fa-solid fa-earth-europe"
}
}
}
}

View File

@@ -0,0 +1,43 @@
use crate::components::select_button::SelectButton;
use crate::models::category::ReoccurrenceInterval;
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
use dioxus_free_icons::icons::fa_solid_icons::{FaBan, FaEarthEurope, FaMoon, FaSun};
#[component]
pub(crate) fn ReoccurrenceIntervalInput(
reoccurrence_interval: Signal<Option<ReoccurrenceInterval>>,
class_buttons: Option<&'static str>,
) -> Element {
rsx! {
SelectButton {
icon: FaBan,
is_selected: reoccurrence_interval().is_none(),
on_select: move |_| {
reoccurrence_interval.set(None);
}
}
SelectButton {
icon: FaSun,
is_selected: matches!(reoccurrence_interval(), Some(ReoccurrenceInterval::Day)),
on_select: move |_| {
reoccurrence_interval.set(Some(ReoccurrenceInterval::Day))
}
}
SelectButton {
icon: FaMoon,
is_selected: matches!(reoccurrence_interval(), Some(ReoccurrenceInterval::Month)),
on_select: move |_| {
reoccurrence_interval.set(Some(ReoccurrenceInterval::Month));
}
}
SelectButton {
icon: FaEarthEurope,
is_selected: matches!(reoccurrence_interval(), Some(ReoccurrenceInterval::Year)),
on_select: move |_| {
reoccurrence_interval.set(Some(ReoccurrenceInterval::Year));
}
}
}
}

View File

@@ -0,0 +1,28 @@
use dioxus::prelude::*;
use dioxus_free_icons::{Icon, IconShape};
#[component]
pub(crate) fn SelectButton<I: IconShape + Clone + PartialEq + 'static>(
icon: I,
is_selected: bool,
on_select: Callback,
) -> Element {
rsx! {
button {
r#type: "button",
class: format!(
"pt-4.5 flex flex-row justify-center items-center {} rounded-xl transition-all duration-150",
if is_selected { "pb-3.75 bg-gray-900 drop-shadow-[0_0_0_var(--color-gray-900-muted)]" }
else { "pb-2.75 mt-1 bg-gray-800-muted hover:bg-gray-800 drop-shadow-[0_calc(0px_-_var(--spacing))_0_var(--color-gray-900-muted)] text-gray-400 cursor-pointer" }
),
onclick: move |_| {
on_select.call(());
},
Icon {
icon,
height: 16,
width: 16
}
},
}
}

View File

@@ -1,3 +1,6 @@
use crate::components::button_secondary::ButtonSecondary;
use crate::components::input::Input;
use crate::components::input_label::InputLabel;
use crate::hooks::use_subtasks_of_task;
use crate::models::subtask::NewSubtask;
use crate::models::task::Task;
@@ -5,131 +8,132 @@ use crate::server::subtasks::{create_subtask, delete_subtask, edit_subtask};
use dioxus::core_macro::{component, rsx};
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
use dioxus_free_icons::Icon;
use dioxus_free_icons::icons::fa_solid_icons::{FaGavel, FaListCheck, FaTrashCan};
#[component]
pub(crate) fn SubtasksForm(task: Task) -> Element {
let subtasks = use_subtasks_of_task(task.id)?;
let mut new_title = use_signal(String::new);
rsx! {
form {
class: "flex flex-row items-center gap-3",
onsubmit: move |event| {
event.prevent_default();
let task = task.clone();
async move {
let new_subtask = NewSubtask {
task_id: task.id,
title: event.get("title").first().cloned().and_then(|value| match value {
FormValue::Text(value) => Some(value),
FormValue::File(_) => None
}).unwrap(),
is_completed: false
};
let _ = create_subtask(new_subtask).await;
new_title.set(String::new());
}
},
label {
r#for: "input_new_title",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-list-check text-zinc-400/50"
}
}
div {
class: "grow grid grid-cols-6 gap-2",
input {
name: "title",
required: true,
value: new_title,
r#type: "text",
class: "grow py-2 px-3 col-span-5 bg-zinc-800/50 rounded-lg",
id: "input_new_title",
onchange: move |event| new_title.set(event.value())
}
button {
r#type: "submit",
class: "py-2 col-span-1 bg-zinc-800/50 rounded-lg",
i {
class: "fa-solid fa-plus"
}
}
}
}
for subtask in subtasks {
div {
key: "{subtask.id}",
div {
class: "flex flex-col gap-3",
form {
class: "flex flex-row items-center gap-3",
i {
class: format!(
"{} min-w-6 text-center text-2xl text-zinc-400/50",
if subtask.is_completed {
"fa solid fa-square-check"
} else {
"fa-regular fa-square"
}
),
onclick: {
let subtask = subtask.clone();
move |_| {
let subtask = subtask.clone();
async move {
let new_subtask = NewSubtask {
task_id: subtask.task_id,
title: subtask.title.clone(),
is_completed: !subtask.is_completed
};
let _ = edit_subtask(
subtask.id,
new_subtask
).await;
}
}
onsubmit: move |event| {
event.prevent_default();
let task = task.clone();
async move {
let new_subtask = NewSubtask {
task_id: task.id,
title: event.get("new_title").first().cloned().and_then(|value| match value {
FormValue::Text(value) => Some(value),
FormValue::File(_) => None
}).unwrap(),
is_completed: false
};
let _ = create_subtask(new_subtask).await;
new_title.set(String::new());
}
},
InputLabel {
icon: FaListCheck,
r#for: "input_new_title"
}
div {
class: "grow grid grid-cols-6 gap-2",
input {
class: "grow flex flex-row items-end gap-3",
Input {
class: "grow",
name: "new_title",
r#type: "text",
class: "grow py-2 px-3 col-span-5 bg-zinc-800/50 rounded-lg",
id: "input_title_{subtask.id}",
initial_value: subtask.title.clone(),
onchange: {
let subtask = subtask.clone();
move |event: Event<FormData>| {
let subtask = subtask.clone();
async move {
let new_subtask = NewSubtask {
task_id: subtask.task_id,
title: event.value(),
is_completed: subtask.is_completed
};
if new_subtask.title.is_empty() {
let _ = delete_subtask(subtask.id).await;
} else {
let _ = edit_subtask(
subtask.id,
new_subtask
).await;
}
}
}
required: true,
value: new_title,
onchange: move |event: Event<FormData>| new_title.set(event.value())
}
ButtonSecondary {
r#type: "submit",
Icon {
icon: FaGavel,
height: 16,
width: 16
}
}
}
}
for subtask in subtasks {
div {
key: "{subtask.id}",
class: "flex flex-row items-center gap-3",
button {
r#type: "button",
class: "py-2 col-span-1 bg-zinc-800/50 rounded-lg",
class: "mt-1.5 hover:mt-1 hover:pb-0.5 min-w-7 cursor-pointer transition-all duration-150",
onclick: {
let subtask = subtask.clone();
move |_| {
let subtask = subtask.clone();
async move {
let _ = delete_subtask(subtask.id).await;
let new_subtask = NewSubtask {
task_id: subtask.task_id,
title: subtask.title.clone(),
is_completed: !subtask.is_completed
};
let _ = edit_subtask(
subtask.id,
new_subtask
).await;
}
}
},
i {
class: "fa-solid fa-trash-can"
div {
class: format!("grow h-7 w-7 mb-[4px] drop-shadow-[0_1px_0_var(--color-gray-800),0_1px_0_var(--color-gray-800),0_1px_0_var(--color-gray-800),0_1px_0_var(--color-gray-800)] rounded-full {}",
if subtask.is_completed {"bg-gray-600"} else {"border-3 border-gray-600"}
)
}
}
div {
class: "grow flex flex-row items-end gap-3",
Input {
class: "grow",
name: "title_edit_{subtask.id}",
r#type: "text",
initial_value: subtask.title.clone(),
onchange: {
let subtask = subtask.clone();
move |event: Event<FormData>| {
let subtask = subtask.clone();
async move {
let new_subtask = NewSubtask {
task_id: subtask.task_id,
title: event.value(),
is_completed: subtask.is_completed
};
if new_subtask.title.is_empty() {
let _ = delete_subtask(subtask.id).await;
} else {
let _ = edit_subtask(
subtask.id,
new_subtask
).await;
}
}
}
}
}
ButtonSecondary {
r#type: "button",
onclick: {
let subtask = subtask.clone();
move |_| {
let subtask = subtask.clone();
async move {
let _ = delete_subtask(subtask.id).await;
}
}
},
Icon {
icon: FaTrashCan,
height: 16,
width: 16
}
}
}
}

View File

@@ -1,16 +1,24 @@
use crate::components::button_primary::ButtonPrimary;
use crate::components::button_secondary::ButtonSecondary;
use crate::components::category_input::CategoryInput;
use crate::components::input::Input;
use crate::components::input_label::InputLabel;
use crate::components::project_select::ProjectSelect;
use crate::components::reoccurrence_input::ReoccurrenceIntervalInput;
use crate::components::reoccurrence_interval_input::ReoccurrenceIntervalInput;
use crate::components::subtasks_form::SubtasksForm;
use crate::models::category::{CalendarTime, Category, Reoccurrence};
use crate::models::task::NewTask;
use crate::models::task::Task;
use crate::route::Route;
use crate::server::tasks::{create_task, delete_task, edit_task};
use chrono::Duration;
use dioxus::core_macro::{component, rsx};
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
use dioxus_free_icons::Icon;
use dioxus_free_icons::icons::fa_solid_icons::{
FaBell, FaBomb, FaClock, FaFeatherPointed, FaHourglassEnd, FaList, FaRepeat, FaScroll, FaStamp,
FaTrashCan, FaXmark,
};
use dioxus_i18n::t;
use serde::{Deserialize, Serialize};
@@ -46,25 +54,19 @@ struct InputData {
project_id: Option<String>,
}
pub(crate) static TASK_BEING_EDITED: GlobalSignal<Option<Task>> = Signal::global(|| None);
pub(crate) static LATEST_VISITED_CATEGORY: GlobalSignal<Category> =
Signal::global(|| Category::Inbox);
#[component]
pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()>) -> Element {
let route = use_route::<Route>();
pub(crate) fn TaskForm() -> Element {
let navigator = use_navigator();
let task = TASK_BEING_EDITED();
let selected_category = use_signal(|| {
if let Some(task) = &task {
task.category.clone()
} else {
match route {
Route::CategorySomedayMaybePage => Category::SomedayMaybe,
Route::CategoryWaitingForPage => Category::WaitingFor(String::new()),
Route::CategoryNextStepsPage => Category::NextSteps,
Route::CategoryCalendarPage | Route::CategoryTodayPage => Category::Calendar {
date: chrono::Local::now().date_naive(),
reoccurrence: None,
time: None,
},
Route::CategoryLongTermPage => Category::LongTerm,
_ => Category::Inbox,
}
LATEST_VISITED_CATEGORY()
}
});
let category_calendar_reoccurrence_interval = use_signal(|| {
@@ -104,9 +106,9 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()
rsx! {
div {
class: "p-4 flex flex-col gap-4",
class: "grow px-4 flex flex-col gap-6.5",
form {
class: "flex flex-col gap-4",
class: "flex flex-col gap-8",
id: "form_task",
onsubmit: move |event| {
event.prevent_default();
@@ -148,50 +150,45 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()
project_id: input_data.project_id
.and_then(|deadline| deadline.parse().ok()).filter(|&id| id > 0),
};
if let Some(task) = task {
let _ = edit_task(task.id, new_task).await;
let result = if let Some(task) = task {
edit_task(task.id, new_task).await
} else {
let _ = create_task(new_task).await;
create_task(new_task).await
};
if result.is_ok() {
navigator.go_back();
}
on_successful_submit.call(());
}
},
div {
class: "flex flex-row items-center gap-3",
label {
InputLabel {
r#for: "input_title",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-pen-clip text-zinc-400/50"
},
icon: FaFeatherPointed
},
input {
Input {
class: "grow",
name: "title",
required: true,
initial_value: task.as_ref().map(|task| task.title.clone()),
r#type: "text",
class: "py-2 px-3 grow bg-zinc-800/50 rounded-lg",
id: "input_title"
},
autofocus: task.is_none()
}
},
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "input_project",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-list text-zinc-400/50"
}
InputLabel {
r#for: "input_project_id",
icon: FaList
},
SuspenseBoundary {
fallback: |_| {
rsx ! {
select {
class: "px-3.5 py-2.5 bg-zinc-800/50 rounded-lg grow cursor-pointer",
class: "px-4 pt-3 pb-2.25 bg-gray-800-muted drop-shadow-[0_calc(0px_-_var(--spacing))_0_var(--color-gray-900-muted)] rounded-xl grow cursor-pointer",
option {
value: 0,
{t!("none")}
},
value: 0
}
}
}
},
@@ -202,86 +199,69 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()
},
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "input_deadline",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-bomb text-zinc-400/50"
}
InputLabel {
icon: FaBomb,
r#for: "input_deadline"
},
input {
Input {
name: "deadline",
initial_value: task.as_ref().and_then(|task| task.deadline)
.map(|deadline| deadline.format("%Y-%m-%d").to_string()),
r#type: "date",
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow basis-0 cursor-pointer",
id: "input_deadline"
class: "grow basis-0"
}
},
div {
class: "flex flex-row items-center gap-3",
label {
class: "min-w-6 text-center",
i {
class: "fa-solid fa-layer-group text-zinc-400/50"
}
InputLabel {
icon: FaScroll
},
CategoryInput {
selected_category: selected_category,
class: "grow"
class: "grow",
selected_category: selected_category
}
}
match selected_category() {
Category::WaitingFor(waiting_for) => rsx! {
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "input_deadline",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-hourglass-end text-zinc-400/50"
}
InputLabel {
icon: FaHourglassEnd,
r#for: "input_category_waiting_for",
},
input {
Input {
class: "grow",
name: "category_waiting_for",
required: true,
initial_value: waiting_for,
r#type: "text",
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow",
id: "input_category_waiting_for"
},
}
}
},
Category::Calendar { date, reoccurrence, time } => rsx! {
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "input_category_calendar_date",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-clock text-zinc-400/50"
}
InputLabel {
icon: FaClock,
r#for: "input_category_calendar_date"
},
div {
class: "grow flex flex-row gap-2",
input {
r#type: "date",
class: "grow grid grid-cols-7 gap-3",
Input {
class: "grow col-span-4",
name: "category_calendar_date",
r#type: "date",
required: true,
initial_value: date.format("%Y-%m-%d").to_string(),
class:
"py-2 px-3 bg-zinc-800/50 rounded-lg grow cursor-pointer",
id: "input_category_calendar_date"
},
input {
r#type: "time",
Input {
class: "grow col-span-3",
name: "category_calendar_time",
r#type: "time",
initial_value: time.map(|calendar_time|
calendar_time.time.format("%H:%M").to_string()
),
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow cursor-pointer",
id: "input_category_calendar_time",
oninput: move |event| {
oninput: move |event: Event<FormData>| {
category_calendar_has_time.set(!event.value().is_empty());
}
}
@@ -289,54 +269,46 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()
},
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "category_calendar_reoccurrence_length",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-repeat text-zinc-400/50"
}
InputLabel {
icon: FaRepeat,
r#for: "category_calendar_reoccurrence_length"
},
div {
class: "grow grid grid-cols-6 gap-2",
class: "grow grid grid-cols-5 items-end gap-3",
ReoccurrenceIntervalInput {
reoccurrence_interval: category_calendar_reoccurrence_interval
},
input {
Input {
class: "text-right",
r#type: "number",
inputmode: "numeric",
name: "category_calendar_reoccurrence_length",
disabled: category_calendar_reoccurrence_interval().is_none(),
required: true,
min: 1,
min: "1",
initial_value: category_calendar_reoccurrence_interval().map_or(
String::new(),
|_| reoccurrence.map_or(1, |reoccurrence|
reoccurrence.length).to_string()
),
class: "py-2 px-3 bg-zinc-800/50 rounded-lg col-span-2 text-right",
id: "category_calendar_reoccurrence_length"
)
}
}
},
if category_calendar_has_time() {
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "category_calendar_reminder_offset_index",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-bell text-zinc-400/50"
}
InputLabel {
r#for: "input_category_calendar_reminder_offset_index",
icon: FaBell
},
input {
r#type: "range",
class: "grow",
name: "category_calendar_reminder_offset_index",
r#type: "range",
min: 0,
max: REMINDER_OFFSETS.len() as i64 - 1,
initial_value: category_calendar_reminder_offset_index()
.to_string(),
class: "grow input-range-reverse cursor-pointer",
id: "category_calendar_has_reminder",
oninput: move |event| {
category_calendar_reminder_offset_index.set(
event.value().parse().unwrap()
@@ -344,8 +316,8 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()
}
},
label {
r#for: "category_calendar_reminder_offset_index",
class: "pr-3 min-w-16 text-right",
r#for: "category_calendar_reminder_offset_index",
{REMINDER_OFFSETS[category_calendar_reminder_offset_index()]
.map(
|offset| if offset.num_hours() < 1 {
@@ -371,17 +343,20 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()
}
}
}
div {
class: "flex flex-row justify-between mt-auto",
button {
r#type: "button",
class: "py-2 px-4 bg-zinc-300/50 rounded-lg cursor-pointer",
onclick: move |_| {
}
div {
class: "px-4 grid grid-cols-3 gap-3 mt-auto",
ButtonSecondary {
r#type: "button",
class: "grow",
onclick: {
let task = task.clone();
move |_| {
let task = task.clone();
async move {
if let Some(task) = task {
if let Category::Trash = task.category {
let _ = delete_task(task.id).await;
let result = if let Category::Trash = task.category {
delete_task(task.id).await
} else {
let new_task = NewTask {
title: task.title.to_owned(),
@@ -389,23 +364,50 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()
category: Category::Trash,
project_id: task.project_id
};
let _ = edit_task(task.id, new_task).await;
edit_task(task.id, new_task).await.map(|_| ())
};
if result.is_ok() {
navigator.go_back();
}
} else {
navigator.go_back();
}
}
}
},
Icon {
icon: FaTrashCan,
height: 16,
width: 16
}
}
if task.is_some() {
div {
class: "grow flex flex-col items-stretch",
GoBackButton {
ButtonSecondary {
/* TODO: Replace w-full` with proper flexbox styling once
https://github.com/DioxusLabs/dioxus/issues/5269 is solved. */
class: "w-full",
r#type: "button",
Icon {
icon: FaXmark,
height: 16,
width: 16
}
on_successful_submit.call(());
}
},
i {
class: "fa-solid fa-trash-can"
}
}
button {
form: "form_task",
r#type: "submit",
class: "py-2 px-4 bg-zinc-300/50 rounded-lg cursor-pointer",
i {
class: "fa-solid fa-floppy-disk"
}
} else {
div {}
}
ButtonPrimary {
form: "form_task",
r#type: "submit",
Icon {
icon: FaStamp,
height: 16,
width: 16
}
}
}

View File

@@ -1,6 +1,8 @@
use crate::components::task_form::TASK_BEING_EDITED;
use crate::components::task_list_item::TaskListItem;
use crate::models::category::Category;
use crate::models::task::{Task, TaskWithSubtasks};
use crate::models::task::TaskWithSubtasks;
use crate::route::Route;
use crate::server::tasks::complete_task;
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
@@ -8,7 +10,7 @@ use dioxus::prelude::*;
#[component]
pub(crate) fn TaskList(tasks: Vec<TaskWithSubtasks>, class: Option<&'static str>) -> Element {
let mut task_being_edited = use_context::<Signal<Option<Task>>>();
let navigator = use_navigator();
rsx! {
div {
class: format!("flex flex-col {}", class.unwrap_or("")),
@@ -16,34 +18,31 @@ pub(crate) fn TaskList(tasks: Vec<TaskWithSubtasks>, class: Option<&'static str>
div {
key: "{task.task.id}",
class: format!(
"px-8 pt-4 {} flex flex-row gap-4 select-none {}",
"px-7 pt-3.75 {} flex flex-row items-start gap-4 hover:bg-gray-800 cursor-pointer select-none transition-all duration-150",
if task.task.deadline.is_some() || !task.subtasks.is_empty() {
"pb-0.5"
"pb-0.25"
} else if let Category::Calendar { time, .. } = &task.task.category {
if time.is_some() {
"pb-0.5"
"pb-0.25"
} else {
"pb-4"
"pb-3.75"
}
} else {
"pb-4"
"pb-3.75"
},
if task_being_edited().is_some_and(|t| t.id == task.task.id) {
"bg-zinc-700"
} else { "" }
),
onclick: {
let task = task.clone();
move |_| task_being_edited.set(Some(task.task.clone()))
move |_| {
*TASK_BEING_EDITED.write() = Some(task.task.clone());
navigator.push(Route::TaskFormPage);
}
},
i {
button {
class: format!(
"{} text-3xl align-middle h-9 text-zinc-500",
if let Category::Done = task.task.category {
"fa solid fa-square-check"
} else {
"fa-regular fa-square cursor-pointer"
}
"mt-0.5 hover:mt-0 hover:pb-0.5 transition-all duration-150 cursor-pointer {}",
if let Category::Done = task.task.category { "pointer-events-none" }
else { "" }
),
onclick: {
move |event: Event<MouseData>| {
@@ -53,10 +52,22 @@ pub(crate) fn TaskList(tasks: Vec<TaskWithSubtasks>, class: Option<&'static str>
let _ = complete_task(task.task.id).await;
}
}
},
div {
class: format!("h-8 w-8 rounded-full {}",
if let Category::Done = task.task.category {
"mt-[3px] mb-[2px] bg-amber-300-muted"
} else {
"mb-[5px] border-3 border-amber-300-muted drop-shadow-[0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted)]"
}
)
}
},
TaskListItem {
task: task.clone()
div {
class: "mt-1.5",
TaskListItem {
task: task.clone()
}
}
}
}

View File

@@ -5,6 +5,8 @@ use chrono::{Datelike, Local};
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
use dioxus_free_icons::Icon;
use dioxus_free_icons::icons::fa_solid_icons::{FaBomb, FaClock, FaListCheck};
use dioxus_i18n::prelude::i18n;
use dioxus_i18n::t;
use voca_rs::Voca;
@@ -14,19 +16,21 @@ pub(crate) fn TaskListItem(task: TaskWithSubtasks) -> Element {
let today_date = Local::now().date_naive();
rsx! {
div {
class: "flex flex-col",
class: "pt-0.75 flex flex-col",
div {
class: "mt-1 grow font-medium",
class: "grow font-medium text-pretty wrap-anywhere",
{task.task.title}
},
div {
class: "flex flex-row gap-4",
if let Some(deadline) = task.task.deadline {
div {
class: "text-sm text-zinc-400",
i {
class: "fa-solid fa-bomb"
},
class: "flex flex-row items-center gap-1 text-sm text-gray-500",
Icon {
icon: FaBomb,
height: 14,
width: 14
}
{
format!(
" {}",
@@ -72,10 +76,12 @@ pub(crate) fn TaskListItem(task: TaskWithSubtasks) -> Element {
if let Category::Calendar { time, .. } = task.task.category {
if let Some(calendar_time) = time {
div {
class: "text-sm text-zinc-400",
i {
class: "fa-solid fa-clock"
},
class: "flex flex-row items-center gap-1 text-sm text-gray-500",
Icon {
icon: FaClock,
height: 14,
width: 14
}
{
let format = t!("time-format");
format!(" {}", calendar_time.time.format(format.as_str()))
@@ -85,10 +91,12 @@ pub(crate) fn TaskListItem(task: TaskWithSubtasks) -> Element {
}
if !task.subtasks.is_empty() {
div {
class: "text-sm text-zinc-400",
i {
class: "fa-solid fa-list-check"
},
class: "flex flex-row items-center gap-1 text-sm text-gray-500",
Icon {
icon: FaListCheck,
height: 14,
width: 14
}
{format!(
" {}/{}",
task.subtasks.iter()

View File

@@ -1,9 +1,6 @@
use std::fmt::Display;
use dioxus::{
CapturedError,
document::document,
fullstack::{Loader, Loading, WebSocketOptions, WebsocketState, use_websocket},
fullstack::{Loader, Loading, WebSocketOptions},
prelude::*,
};
use serde::{Serialize, de::DeserializeOwned};
@@ -16,7 +13,7 @@ use crate::{
},
};
fn use_on_document_visibility_change(mut callback: impl FnMut() + 'static) {
fn use_on_document_become_visible(mut callback: impl FnMut() + 'static) {
let callback = use_callback(move |_| callback());
use_effect(move || {
spawn(async move {
@@ -27,13 +24,6 @@ fn use_on_document_visibility_change(mut callback: impl FnMut() + 'static) {
dioxus.send(0);
}
});
// window.addEventListener("focus", () => resume());
// Keep this eval alive so dioxus.send keeps working.
// while (true) {
// await new Promise(r => setTimeout(r, 3600_000));
// }
"#,
);
loop {
@@ -41,7 +31,6 @@ fn use_on_document_visibility_change(mut callback: impl FnMut() + 'static) {
.await
.expect("The JS code returned a value not parsable to `u8`.");
callback.call(());
log("received resume");
}
});
});
@@ -58,19 +47,6 @@ fn sort_loader_result<T: Ord + Clone>(
})
}
pub static LOG_MESSAGES: GlobalSignal<Vec<String>> =
Signal::global(|| vec!["=== LOG ===".to_string()]);
fn log(message: impl Display) {
let mut old = LOG_MESSAGES();
old.push(format!(
"[{}] {}",
chrono::Local::now().format("%H:%M:%S"),
message.to_string()
));
*LOG_MESSAGES.write() = old;
}
#[allow(clippy::result_large_err)]
fn use_loader_with_update_subscription<F, T, E>(
mut future: impl FnMut() -> F + 'static,
@@ -91,29 +67,25 @@ where
use_effect(move || {
let initial_websocket_reset_tick = websocket_reset_tick();
spawn(async move {
log("spawning based on ws_gen");
let Ok(socket) =
subscribe_to_updates(WebSocketOptions::new().with_automatic_reconnect()).await
else {
log("failed to spawn");
return;
};
while socket.recv().await.is_ok() {
log("running the future recv");
if websocket_reset_tick() != initial_websocket_reset_tick {
log("new WS gen, breaking");
// A new WebSocket has been created (a new task spawned), cleaning this one up.
break;
}
refresh_tick += 1;
}
log("future ending");
});
});
/* So that when the device goes to sleep or suspends the app, the WebSocket gets recreated on
waking up. */
use_on_document_visibility_change(move || {
waking up. It is important to do this only on becoming visible (document.hidden == false),
because becoming hidden is the part when network may not work and thus cause errors. */
use_on_document_become_visible(move || {
websocket_reset_tick += 1;
refresh_tick += 1;
});

View File

@@ -1,47 +0,0 @@
use crate::components::bottom_panel::BottomPanel;
use crate::components::form_open_button::FormOpenButton;
use crate::components::sticky_bottom::StickyBottom;
use crate::models::project::Project;
use crate::models::task::Task;
use crate::route::Route;
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
#[component]
pub(crate) fn Main() -> Element {
let mut display_form = use_signal(|| false);
let project_being_edited =
use_context_provider::<Signal<Option<Project>>>(|| Signal::new(None));
let task_being_edited = use_context_provider::<Signal<Option<Task>>>(|| Signal::new(None));
use_effect(move || {
display_form.set(project_being_edited().is_some() || task_being_edited().is_some());
});
rsx! {
SuspenseBoundary {
fallback: |_| {
rsx! {
div {
class: "grow flex flex-col justify-center items-center",
div {
i {
class: "text-3xl fa-solid fa-cog fa-spin"
}
}
}
}
},
Outlet::<Route> {}
}
StickyBottom {
FormOpenButton {
opened: display_form,
}
BottomPanel {
display_form: display_form,
}
}
}
}

View File

@@ -1,2 +1,2 @@
mod main;
pub(crate) use main::Main;
pub(crate) mod navigation;
pub(crate) mod suspense;

38
src/layouts/navigation.rs Normal file
View File

@@ -0,0 +1,38 @@
use crate::components::bottom_panel::BottomPanel;
use crate::components::create_button::CreateButton;
use crate::components::sticky_bottom::StickyBottom;
use crate::components::task_form::LATEST_VISITED_CATEGORY;
use crate::models::category::Category;
use crate::route::Route;
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
#[component]
pub(crate) fn Navigation() -> Element {
let current_route = use_route();
use_effect(use_reactive(&current_route, move |current_route| {
*LATEST_VISITED_CATEGORY.write() = match current_route {
Route::CategorySomedayMaybePage => Category::SomedayMaybe,
Route::CategoryWaitingForPage => Category::WaitingFor(String::new()),
Route::CategoryNextStepsPage => Category::NextSteps,
Route::CategoryCalendarPage | Route::CategoryTodayPage => Category::Calendar {
date: chrono::Local::now().date_naive(),
reoccurrence: None,
time: None,
},
_ => Category::Inbox,
};
}));
rsx! {
div {
class: "grow flex flex-col pb-36",
Outlet::<Route> {}
}
StickyBottom {
CreateButton {},
BottomPanel {}
}
}
}

28
src/layouts/suspense.rs Normal file
View File

@@ -0,0 +1,28 @@
use crate::route::Route;
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
use dioxus_free_icons::Icon;
use dioxus_free_icons::icons::fa_solid_icons::FaCog;
#[component]
pub(crate) fn Suspense() -> Element {
rsx! {
SuspenseBoundary {
fallback: |_| {
rsx! {
div {
class: "grow flex flex-col justify-center items-center",
Icon {
class: "text-gray-500 animate-[spin_3000ms_linear_infinite]",
icon: FaCog,
height: 32,
width: 32
}
}
}
},
Outlet::<Route> {}
}
}
}

View File

@@ -2,14 +2,15 @@ use crate::layouts;
use crate::views::category_calendar_page::CategoryCalendarPage;
use crate::views::category_done_page::CategoryDonePage;
use crate::views::category_inbox_page::CategoryInboxPage;
use crate::views::category_long_term_page::CategoryLongTermPage;
use crate::views::category_next_steps_page::CategoryNextStepsPage;
use crate::views::category_someday_maybe_page::CategorySomedayMaybePage;
use crate::views::category_today_page::CategoryTodayPage;
use crate::views::category_trash_page::CategoryTrashPage;
use crate::views::category_waiting_for_page::CategoryWaitingForPage;
use crate::views::not_found_page::NotFoundPage;
use crate::views::project_form_page::ProjectFormPage;
use crate::views::projects_page::ProjectsPage;
use crate::views::task_form_page::TaskFormPage;
use dioxus::prelude::*;
// All variants have the same postfix because they have to match the component names.
@@ -17,28 +18,33 @@ use dioxus::prelude::*;
#[derive(Clone, Routable, Debug, PartialEq)]
#[rustfmt::skip]
pub(crate) enum Route {
#[layout(layouts::Main)]
#[redirect("/", || Route::CategoryTodayPage {})]
#[route("/today")]
CategoryTodayPage,
#[route("/inbox")]
CategoryInboxPage,
#[route("/someday-maybe")]
CategorySomedayMaybePage,
#[route("/waiting-for")]
CategoryWaitingForPage,
#[route("/next-steps")]
CategoryNextStepsPage,
#[route("/calendar")]
CategoryCalendarPage,
#[route("/long-term")]
CategoryLongTermPage,
#[route("/done")]
CategoryDonePage,
#[route("/trash")]
CategoryTrashPage,
#[route("/projects")]
ProjectsPage,
#[layout(layouts::navigation::Navigation)]
#[layout(layouts::suspense::Suspense)]
#[route("/today")]
CategoryTodayPage,
#[route("/inbox")]
CategoryInboxPage,
#[route("/someday-maybe")]
CategorySomedayMaybePage,
#[route("/waiting-for")]
CategoryWaitingForPage,
#[route("/next-steps")]
CategoryNextStepsPage,
#[route("/calendar")]
CategoryCalendarPage,
#[route("/done")]
CategoryDonePage,
#[route("/trash")]
CategoryTrashPage,
#[route("/projects")]
ProjectsPage,
#[end_layout]
#[end_layout]
#[layout(layouts::suspense::Suspense)]
#[route("/task")]
TaskFormPage,
#[route("/project")]
ProjectFormPage,
#[end_layout]
#[redirect("/", || Route::CategoryTodayPage)]
#[route("/:..route")]

View File

@@ -1,12 +0,0 @@
/** @type {import("tailwindcss").Config} */
module.exports = {
mode: "all",
content: ["./src/**/*.{rs,html,css}"],
theme: {
fontFamily: {
sans: ["Inter", "sans-serif"],
},
extend: {},
},
plugins: [],
};

View File

@@ -1,10 +0,0 @@
/* stylelint-disable */
/* noinspection CssInvalidAtRule */
@tailwind base;
/* noinspection CssInvalidAtRule */
@tailwind components;
/* noinspection CssInvalidAtRule */
@tailwind utilities;
/* stylelint-enable */

View File

@@ -1,17 +0,0 @@
use crate::components::error_boundary_message::ErrorBoundaryMessage;
use crate::models::category::Category;
use crate::views::category_page::CategoryPage;
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
#[component]
pub(crate) fn CategoryLongTermPage() -> Element {
rsx! {
ErrorBoundaryMessage {
CategoryPage {
category: Category::LongTerm,
}
}
}
}

View File

@@ -11,8 +11,7 @@ pub(crate) fn CategoryPage(category: Category) -> Element {
rsx! {
TaskList {
tasks: tasks.clone(),
class: "pb-36"
tasks: tasks.clone()
}
}
}

View File

@@ -1,7 +1,6 @@
pub(crate) mod category_calendar_page;
pub(crate) mod category_done_page;
pub(crate) mod category_inbox_page;
pub(crate) mod category_long_term_page;
pub(crate) mod category_next_steps_page;
pub(crate) mod category_page;
pub(crate) mod category_someday_maybe_page;
@@ -9,4 +8,6 @@ pub(crate) mod category_today_page;
pub(crate) mod category_trash_page;
pub(crate) mod category_waiting_for_page;
pub(crate) mod not_found_page;
pub(crate) mod project_form_page;
pub(crate) mod projects_page;
pub(crate) mod task_form_page;

View File

@@ -0,0 +1,12 @@
use crate::components::{error_boundary_message::ErrorBoundaryMessage, project_form::ProjectForm};
use dioxus::prelude::*;
#[component]
pub(crate) fn ProjectFormPage() -> Element {
rsx! {
ErrorBoundaryMessage {
class: "grow py-4 flex flex-col gap-12",
ProjectForm {}
}
}
}

View File

@@ -0,0 +1,12 @@
use crate::components::{error_boundary_message::ErrorBoundaryMessage, task_form::TaskForm};
use dioxus::prelude::*;
#[component]
pub(crate) fn TaskFormPage() -> Element {
rsx! {
ErrorBoundaryMessage {
class: "grow py-4 flex flex-col gap-12",
TaskForm {}
}
}
}

View File

@@ -1,2 +1,27 @@
/* stylelint-disable-next-line import-notation */
@import "tailwindcss";
@font-face {
font-family: Inter;
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url("/assets/fonts/inter_variable.woff2") format("woff2");
}
@font-face {
font-family: Inter;
font-style: italic;
font-weight: 100 900;
font-display: swap;
src: url("/assets/fonts/inter_variable_italic.woff2") format("woff2");
}
/* stylelint-disable-next-line */
@theme {
--font-sans: "Inter", "sans";
--color-amber-300-muted: #b89a2e;
--color-amber-700-muted: #80390b;
--color-gray-800-muted: #141d2d;
--color-gray-900-muted: #0b111f;
}