diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..8105f84
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,45 @@
+# Git
+.git
+.gitignore
+
+# Docker (but keep essential files)
+.dockerignore
+
+# Rust target directory
+target/
+**/.cargo/
+
+# IDE files
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# OS files
+.DS_Store
+Thumbs.db
+
+# Logs
+*.log
+logs/
+
+# Documentation
+*.md
+!README.md
+
+# Test files
+test_data.sql
+
+# Environment files
+.env*
+
+# Temporary files
+*.tmp
+*.temp
+
+# Build artifacts
+*.o
+*.so
+*.dll
+*.exe
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 161c121..61bd1c9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -13,9 +13,9 @@ dependencies = [
[[package]]
name = "adler2"
-version = "2.0.0"
+version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aho-corasick"
@@ -26,11 +26,47 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "anstream"
-version = "0.6.18"
+version = "0.6.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
+checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
dependencies = [
"anstyle",
"anstyle-parse",
@@ -43,33 +79,33 @@ dependencies = [
[[package]]
name = "anstyle"
-version = "1.0.10"
+version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
+checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
[[package]]
name = "anstyle-parse"
-version = "0.2.6"
+version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
-version = "1.1.2"
+version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "anstyle-wincon"
-version = "3.0.8"
+version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa"
+checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
dependencies = [
"anstyle",
"once_cell_polyfill",
@@ -92,718 +128,6649 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[package]]
-name = "autocfg"
-version = "1.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
-
-[[package]]
-name = "backtrace"
-version = "0.3.75"
+name = "async-broadcast"
+version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
+checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
dependencies = [
- "addr2line",
- "cfg-if",
- "libc",
- "miniz_oxide",
- "object",
- "rustc-demangle",
- "windows-targets",
+ "event-listener",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
]
[[package]]
-name = "bitflags"
-version = "2.9.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
-
-[[package]]
-name = "bytes"
-version = "1.10.1"
+name = "async-channel"
+version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
[[package]]
-name = "cfg-if"
-version = "1.0.0"
+name = "async-executor"
+version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "pin-project-lite",
+ "slab",
+]
[[package]]
-name = "colorchoice"
-version = "1.0.3"
+name = "async-io"
+version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
+checksum = "1237c0ae75a0f3765f58910ff9cdd0a12eeb39ab2f4c7de23262f337f0aacbb3"
+dependencies = [
+ "async-lock",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix",
+ "slab",
+ "tracing",
+ "windows-sys 0.59.0",
+]
[[package]]
-name = "crc"
-version = "3.3.0"
+name = "async-lock"
+version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
+checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18"
dependencies = [
- "crc-catalog",
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
]
[[package]]
-name = "crc-catalog"
-version = "2.4.0"
+name = "async-process"
+version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
+checksum = "cde3f4e40e6021d7acffc90095cbd6dc54cb593903d1de5832f435eb274b85dc"
+dependencies = [
+ "async-channel",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix",
+ "tracing",
+]
[[package]]
-name = "crossbeam-channel"
-version = "0.5.15"
+name = "async-recursion"
+version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
- "crossbeam-utils",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
]
[[package]]
-name = "crossbeam-utils"
-version = "0.8.21"
+name = "async-signal"
+version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+checksum = "d7605a4e50d4b06df3898d5a70bf5fde51ed9059b0434b73105193bc27acce0d"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.59.0",
+]
[[package]]
-name = "defer"
-version = "0.2.1"
+name = "async-task"
+version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "930c7171c8df9fb1782bdf9b918ed9ed2d33d1d22300abb754f9085bc48bf8e8"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
-name = "dtoa"
-version = "1.0.10"
+name = "async-trait"
+version = "0.1.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04"
+checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
[[package]]
-name = "env_filter"
-version = "0.1.3"
+name = "async-tungstenite"
+version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
+checksum = "ef0f7efedeac57d9b26170f72965ecfd31473ca52ca7a64e925b0b6f5f079886"
dependencies = [
+ "atomic-waker",
+ "futures-core",
+ "futures-io",
+ "futures-task",
+ "futures-util",
"log",
- "regex",
+ "pin-project-lite",
+ "tokio",
+ "tungstenite",
]
[[package]]
-name = "env_logger"
-version = "0.11.8"
+name = "atk"
+version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
+checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b"
dependencies = [
- "anstream",
- "anstyle",
- "env_filter",
- "jiff",
- "log",
+ "atk-sys",
+ "glib",
+ "libc",
]
[[package]]
-name = "filename"
-version = "0.1.1"
+name = "atk-sys"
+version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63e4df03effebdf9cfa31a663f569fd96cc7e206184b0d4dcd388dc490f7ebe8"
+checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086"
dependencies = [
+ "glib-sys",
+ "gobject-sys",
"libc",
- "winapi",
+ "system-deps",
]
[[package]]
-name = "getrandom"
-version = "0.3.3"
+name = "atoi"
+version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
+checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
dependencies = [
- "cfg-if",
- "libc",
- "r-efi",
- "wasi 0.14.2+wasi-0.2.4",
+ "num-traits",
]
[[package]]
-name = "gimli"
-version = "0.31.1"
+name = "atomic-waker"
+version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
-name = "is_terminal_polyfill"
-version = "1.70.1"
+name = "autocfg"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
-name = "itoa"
-version = "1.0.15"
+name = "axum"
+version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5"
+dependencies = [
+ "axum-core",
+ "axum-macros",
+ "base64 0.22.1",
+ "bytes",
+ "form_urlencoded",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "itoa 1.0.15",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
+ "serde",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sha1",
+ "sync_wrapper",
+ "tokio",
+ "tokio-tungstenite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
[[package]]
-name = "jiff"
-version = "0.2.14"
+name = "axum-core"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93"
+checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6"
dependencies = [
- "jiff-static",
- "log",
- "portable-atomic",
- "portable-atomic-util",
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "rustversion",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-extra"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d"
+dependencies = [
+ "axum",
+ "axum-core",
+ "bytes",
+ "futures-util",
+ "headers",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "rustversion",
"serde",
+ "tower",
+ "tower-layer",
+ "tower-service",
]
[[package]]
-name = "jiff-static"
-version = "0.2.14"
+name = "axum-macros"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442"
+checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.103",
]
[[package]]
-name = "lazy_static"
-version = "1.5.0"
+name = "backtrace"
+version = "0.3.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-targets 0.52.6",
+]
[[package]]
-name = "libc"
-version = "0.2.172"
+name = "base64"
+version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
-name = "lock_api"
-version = "0.4.13"
+name = "base64"
+version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
-dependencies = [
- "autocfg",
- "scopeguard",
-]
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
-name = "log"
-version = "0.4.27"
+name = "base64ct"
+version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
[[package]]
-name = "memchr"
-version = "2.7.4"
+name = "bitflags"
+version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
-name = "memmap2"
-version = "0.9.5"
+name = "bitflags"
+version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f"
+checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
dependencies = [
- "libc",
+ "serde",
]
[[package]]
-name = "miniz_oxide"
-version = "0.8.8"
+name = "block-buffer"
+version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
- "adler2",
+ "generic-array",
]
[[package]]
-name = "mio"
-version = "1.0.4"
+name = "block2"
+version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
+checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
dependencies = [
- "libc",
- "wasi 0.11.0+wasi-snapshot-preview1",
- "windows-sys 0.59.0",
+ "objc2 0.5.2",
]
[[package]]
-name = "object"
-version = "0.36.7"
+name = "block2"
+version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2"
dependencies = [
- "memchr",
+ "objc2 0.6.1",
]
[[package]]
-name = "once_cell_polyfill"
-version = "1.70.1"
+name = "blocking"
+version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
+checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
+dependencies = [
+ "async-channel",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
[[package]]
-name = "parking_lot"
-version = "0.12.4"
+name = "brotli"
+version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
+checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd"
dependencies = [
- "lock_api",
- "parking_lot_core",
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
]
[[package]]
-name = "parking_lot_core"
-version = "0.9.11"
+name = "brotli-decompressor"
+version = "4.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
+checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd"
dependencies = [
- "cfg-if",
- "libc",
- "redox_syscall",
- "smallvec",
- "windows-targets",
+ "alloc-no-stdlib",
+ "alloc-stdlib",
]
[[package]]
-name = "pin-project-lite"
-version = "0.2.16"
+name = "bumpalo"
+version = "3.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee"
[[package]]
-name = "portable-atomic"
-version = "1.11.0"
+name = "bytemuck"
+version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
+checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
[[package]]
-name = "portable-atomic-util"
-version = "0.2.4"
+name = "byteorder"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
-dependencies = [
- "portable-atomic",
-]
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
-name = "ppv-lite86"
-version = "0.2.21"
+name = "bytes"
+version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
dependencies = [
- "zerocopy",
+ "serde",
]
[[package]]
-name = "proc-macro2"
-version = "1.0.95"
+name = "cairo-rs"
+version = "0.18.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
dependencies = [
- "unicode-ident",
+ "bitflags 2.9.1",
+ "cairo-sys-rs",
+ "glib",
+ "libc",
+ "once_cell",
+ "thiserror 1.0.69",
]
[[package]]
-name = "prometheus-client"
-version = "0.23.1"
+name = "cairo-sys-rs"
+version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf41c1a7c32ed72abe5082fb19505b969095c12da9f5732a4bc9878757fd087c"
+checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51"
dependencies = [
- "dtoa",
- "itoa",
- "parking_lot",
- "prometheus-client-derive-encode",
+ "glib-sys",
+ "libc",
+ "system-deps",
]
[[package]]
-name = "prometheus-client-derive-encode"
-version = "0.4.2"
+name = "camino"
+version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8"
+checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "serde",
]
[[package]]
-name = "quote"
-version = "1.0.40"
+name = "cargo-platform"
+version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea"
dependencies = [
- "proc-macro2",
+ "serde",
]
[[package]]
-name = "r-efi"
-version = "5.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
-
-[[package]]
-name = "rand"
-version = "0.9.1"
+name = "cargo_metadata"
+version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
+checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba"
dependencies = [
- "rand_chacha",
- "rand_core",
+ "camino",
+ "cargo-platform",
+ "semver",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.12",
]
[[package]]
-name = "rand_chacha"
-version = "0.9.0"
+name = "cargo_toml"
+version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+checksum = "02260d489095346e5cafd04dea8e8cb54d1d74fcd759022a9b72986ebe9a1257"
dependencies = [
- "ppv-lite86",
- "rand_core",
+ "serde",
+ "toml",
]
[[package]]
-name = "rand_core"
-version = "0.9.3"
+name = "cc"
+version = "1.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
+checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc"
dependencies = [
- "getrandom",
+ "shlex",
]
[[package]]
-name = "redox_syscall"
-version = "0.5.12"
+name = "cesu8"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
-dependencies = [
- "bitflags",
-]
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
-name = "regex"
-version = "1.11.1"
+name = "cfb"
+version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
dependencies = [
- "aho-corasick",
- "memchr",
- "regex-automata",
- "regex-syntax",
+ "byteorder",
+ "fnv",
+ "uuid",
]
[[package]]
-name = "regex-automata"
-version = "0.4.9"
+name = "cfg-expr"
+version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
dependencies = [
- "aho-corasick",
- "memchr",
- "regex-syntax",
+ "smallvec",
+ "target-lexicon",
]
[[package]]
-name = "regex-syntax"
-version = "0.8.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
-
-[[package]]
-name = "rustc-demangle"
-version = "0.1.24"
+name = "cfg-if"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
-name = "scopeguard"
-version = "1.2.0"
+name = "cfg_aliases"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
-name = "serde"
-version = "1.0.219"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+name = "cherry"
+version = "0.1.0"
dependencies = [
- "serde_derive",
+ "anyhow",
+ "async-tungstenite",
+ "cherrycore",
+ "chrono",
+ "dotenvy",
+ "futures-util",
+ "log",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "sqlx",
+ "tauri",
+ "tauri-build",
+ "tauri-plugin-opener",
+ "tokio",
+ "uuid",
]
[[package]]
-name = "serde_derive"
-version = "1.0.219"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+name = "cherrycore"
+version = "0.1.0"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "anyhow",
+ "axum",
+ "axum-extra",
+ "chrono",
+ "jsonwebtoken",
+ "serde",
+ "serde_json",
+ "serde_with",
+ "serde_yaml",
+ "sqlx",
+ "tokio",
+ "uuid",
]
[[package]]
-name = "signal-hook-registry"
-version = "1.4.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
+name = "cherryserver"
+version = "0.1.0"
dependencies = [
- "libc",
+ "anyhow",
+ "axum",
+ "cherrycore",
+ "chrono",
+ "clap",
+ "jsonwebtoken",
+ "serde",
+ "serde_json",
+ "serde_yaml",
+ "sqlx",
+ "tokio",
+ "use",
+ "uuid",
]
[[package]]
-name = "smallvec"
-version = "1.15.1"
+name = "chrono"
+version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "windows-link",
+]
[[package]]
-name = "socket2"
-version = "0.5.10"
+name = "clap"
+version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
+checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
dependencies = [
- "libc",
- "windows-sys 0.52.0",
+ "clap_builder",
+ "clap_derive",
]
[[package]]
-name = "streamstore-rs"
-version = "0.1.0"
+name = "clap_builder"
+version = "4.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
dependencies = [
- "anyhow",
- "arc-swap",
- "backtrace",
- "crc",
- "crossbeam-channel",
- "defer",
- "env_logger",
- "filename",
- "lazy_static",
- "log",
- "memmap2",
- "prometheus-client",
- "rand",
- "thiserror",
- "tokio",
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
]
[[package]]
-name = "syn"
-version = "2.0.101"
+name = "clap_derive"
+version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
+checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
dependencies = [
+ "heck 0.5.0",
"proc-macro2",
"quote",
- "unicode-ident",
+ "syn 2.0.103",
]
[[package]]
-name = "thiserror"
-version = "2.0.12"
+name = "clap_lex"
+version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
-dependencies = [
- "thiserror-impl",
-]
+checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
[[package]]
-name = "thiserror-impl"
-version = "2.0.12"
+name = "colorchoice"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
-name = "tokio"
-version = "1.45.1"
+name = "combine"
+version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
dependencies = [
- "backtrace",
"bytes",
- "libc",
- "mio",
- "parking_lot",
- "pin-project-lite",
- "signal-hook-registry",
- "socket2",
- "tokio-macros",
- "windows-sys 0.52.0",
+ "memchr",
]
[[package]]
-name = "tokio-macros"
+name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "crossbeam-utils",
]
[[package]]
-name = "unicode-ident"
-version = "1.0.18"
+name = "const-oid"
+version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
-name = "utf8parse"
-version = "0.2.2"
+name = "convert_case"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
-name = "wasi"
-version = "0.11.0+wasi-snapshot-preview1"
+name = "cookie"
+version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
+dependencies = [
+ "time",
+ "version_check",
+]
[[package]]
-name = "wasi"
-version = "0.14.2+wasi-0.2.4"
+name = "core-foundation"
+version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
- "wit-bindgen-rt",
+ "core-foundation-sys",
+ "libc",
]
[[package]]
-name = "winapi"
-version = "0.3.9"
+name = "core-foundation"
+version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
- "winapi-i686-pc-windows-gnu",
- "winapi-x86_64-pc-windows-gnu",
+ "core-foundation-sys",
+ "libc",
]
[[package]]
-name = "winapi-i686-pc-windows-gnu"
-version = "0.4.0"
+name = "core-foundation-sys"
+version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
-name = "winapi-x86_64-pc-windows-gnu"
-version = "0.4.0"
+name = "core-graphics"
+version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
+dependencies = [
+ "bitflags 2.9.1",
+ "core-foundation 0.10.1",
+ "core-graphics-types",
+ "foreign-types 0.5.0",
+ "libc",
+]
[[package]]
-name = "windows-sys"
-version = "0.52.0"
+name = "core-graphics-types"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [
- "windows-targets",
+ "bitflags 2.9.1",
+ "core-foundation 0.10.1",
+ "libc",
]
[[package]]
-name = "windows-sys"
-version = "0.59.0"
+name = "cpufeatures"
+version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
- "windows-targets",
+ "libc",
]
[[package]]
-name = "windows-targets"
-version = "0.52.6"
+name = "crc"
+version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_gnullvm",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "crc-catalog",
]
[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.52.6"
+name = "crc-catalog"
+version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
-name = "windows_aarch64_msvc"
-version = "0.52.6"
+name = "crc32fast"
+version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
+dependencies = [
+ "cfg-if",
+]
[[package]]
-name = "windows_i686_gnu"
-version = "0.52.6"
+name = "crossbeam-channel"
+version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+dependencies = [
+ "crossbeam-utils",
+]
[[package]]
-name = "windows_i686_gnullvm"
-version = "0.52.6"
+name = "crossbeam-queue"
+version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
+dependencies = [
+ "crossbeam-utils",
+]
[[package]]
-name = "windows_i686_msvc"
-version = "0.52.6"
+name = "crossbeam-utils"
+version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
-name = "windows_x86_64_gnu"
-version = "0.52.6"
+name = "crypto-common"
+version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.52.6"
+name = "cssparser"
+version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a"
+dependencies = [
+ "cssparser-macros",
+ "dtoa-short",
+ "itoa 0.4.8",
+ "matches",
+ "phf 0.8.0",
+ "proc-macro2",
+ "quote",
+ "smallvec",
+ "syn 1.0.109",
+]
[[package]]
-name = "windows_x86_64_msvc"
-version = "0.52.6"
+name = "cssparser-macros"
+version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
+dependencies = [
+ "quote",
+ "syn 2.0.103",
+]
[[package]]
-name = "wit-bindgen-rt"
-version = "0.39.0"
+name = "ctor"
+version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
+checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
dependencies = [
- "bitflags",
+ "quote",
+ "syn 2.0.103",
]
[[package]]
-name = "zerocopy"
-version = "0.8.25"
+name = "darling"
+version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
+checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
- "zerocopy-derive",
+ "darling_core",
+ "darling_macro",
]
[[package]]
-name = "zerocopy-derive"
-version = "0.8.25"
+name = "darling_core"
+version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
+checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [
+ "fnv",
+ "ident_case",
"proc-macro2",
"quote",
- "syn",
+ "strsim",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
+
+[[package]]
+name = "defer"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "930c7171c8df9fb1782bdf9b918ed9ed2d33d1d22300abb754f9085bc48bf8e8"
+
+[[package]]
+name = "der"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
+dependencies = [
+ "const-oid",
+ "pem-rfc7468",
+ "zeroize",
+]
+
+[[package]]
+name = "deranged"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
+dependencies = [
+ "powerfmt",
+ "serde",
+]
+
+[[package]]
+name = "derive_more"
+version = "0.99.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "const-oid",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "dispatch"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
+
+[[package]]
+name = "dispatch2"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
+dependencies = [
+ "bitflags 2.9.1",
+ "objc2 0.6.1",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "dlopen2"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1297103d2bbaea85724fcee6294c2d50b1081f9ad47d0f6f6f61eda65315a6"
+dependencies = [
+ "dlopen2_derive",
+ "libc",
+ "once_cell",
+ "winapi",
+]
+
+[[package]]
+name = "dlopen2_derive"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "dotenvy"
+version = "0.15.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+
+[[package]]
+name = "dpi"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "dtoa"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04"
+
+[[package]]
+name = "dtoa-short"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
+dependencies = [
+ "dtoa",
+]
+
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "embed-resource"
+version = "3.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0963f530273dc3022ab2bdc3fcd6d488e850256f2284a82b7413cb9481ee85dd"
+dependencies = [
+ "cc",
+ "memchr",
+ "rustc_version",
+ "toml",
+ "vswhom",
+ "winreg",
+]
+
+[[package]]
+name = "embed_plist"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "endi"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
+
+[[package]]
+name = "enumflags2"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
+dependencies = [
+ "enumflags2_derive",
+ "serde",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "env_filter"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
+dependencies = [
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.11.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "env_filter",
+ "jiff",
+ "log",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "erased-serde"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7"
+dependencies = [
+ "serde",
+ "typeid",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "etcetera"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
+dependencies = [
+ "cfg-if",
+ "home",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "event-listener"
+version = "5.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "fdeflate"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "field-offset"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
+dependencies = [
+ "memoffset",
+ "rustc_version",
+]
+
+[[package]]
+name = "flate2"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "flume"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "spin",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared 0.1.1",
+]
+
+[[package]]
+name = "foreign-types"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
+dependencies = [
+ "foreign-types-macros",
+ "foreign-types-shared 0.3.1",
+]
+
+[[package]]
+name = "foreign-types-macros"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
+dependencies = [
+ "mac",
+ "new_debug_unreachable",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-intrusive"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
+dependencies = [
+ "futures-core",
+ "lock_api",
+ "parking_lot",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
+name = "futures-lite"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "gdk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691"
+dependencies = [
+ "cairo-rs",
+ "gdk-pixbuf",
+ "gdk-sys",
+ "gio",
+ "glib",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "gdk-pixbuf"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec"
+dependencies = [
+ "gdk-pixbuf-sys",
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+]
+
+[[package]]
+name = "gdk-pixbuf-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gdk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7"
+dependencies = [
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gdkwayland-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69"
+dependencies = [
+ "gdk-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gdkx11"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe"
+dependencies = [
+ "gdk",
+ "gdkx11-sys",
+ "gio",
+ "glib",
+ "libc",
+ "x11",
+]
+
+[[package]]
+name = "gdkx11-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d"
+dependencies = [
+ "gdk-sys",
+ "glib-sys",
+ "libc",
+ "system-deps",
+ "x11",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasi 0.14.2+wasi-0.2.4",
+]
+
+[[package]]
+name = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+
+[[package]]
+name = "gio"
+version = "0.18.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "gio-sys",
+ "glib",
+ "libc",
+ "once_cell",
+ "pin-project-lite",
+ "smallvec",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gio-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+ "winapi",
+]
+
+[[package]]
+name = "glib"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5"
+dependencies = [
+ "bitflags 2.9.1",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-task",
+ "futures-util",
+ "gio-sys",
+ "glib-macros",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "memchr",
+ "once_cell",
+ "smallvec",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "glib-macros"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro-crate 2.0.0",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "glib-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898"
+dependencies = [
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
+
+[[package]]
+name = "gobject-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a"
+dependencies = [
+ "atk",
+ "cairo-rs",
+ "field-offset",
+ "futures-channel",
+ "gdk",
+ "gdk-pixbuf",
+ "gio",
+ "glib",
+ "gtk-sys",
+ "gtk3-macros",
+ "libc",
+ "pango",
+ "pkg-config",
+]
+
+[[package]]
+name = "gtk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414"
+dependencies = [
+ "atk-sys",
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gdk-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk3-macros"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d"
+dependencies = [
+ "proc-macro-crate 1.3.1",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "h2"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap 2.9.0",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hashbrown"
+version = "0.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash",
+]
+
+[[package]]
+name = "hashlink"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
+dependencies = [
+ "hashbrown 0.15.4",
+]
+
+[[package]]
+name = "headers"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "headers-core",
+ "http",
+ "httpdate",
+ "mime",
+ "sha1",
+]
+
+[[package]]
+name = "headers-core"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
+dependencies = [
+ "http",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hkdf"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+dependencies = [
+ "hmac",
+]
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "home"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "html5ever"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7"
+dependencies = [
+ "log",
+ "mac",
+ "markup5ever",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "http"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa 1.0.15",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa 1.0.15",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+dependencies = [
+ "bytes",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "system-configuration",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "windows-registry",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.63"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "ico"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98"
+dependencies = [
+ "byteorder",
+ "png",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
+
+[[package]]
+name = "icu_properties"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "potential_utf",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
+
+[[package]]
+name = "icu_provider"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "stable_deref_trait",
+ "tinystr",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.15.4",
+ "serde",
+]
+
+[[package]]
+name = "infer"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7"
+dependencies = [
+ "cfb",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
+
+[[package]]
+name = "iri-string"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "is-docker"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "is-wsl"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
+dependencies = [
+ "is-docker",
+ "once_cell",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
+[[package]]
+name = "itoa"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "javascriptcore-rs"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc"
+dependencies = [
+ "bitflags 1.3.2",
+ "glib",
+ "javascriptcore-rs-sys",
+]
+
+[[package]]
+name = "javascriptcore-rs-sys"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "jiff"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
+dependencies = [
+ "jiff-static",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "jni"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+dependencies = [
+ "cesu8",
+ "cfg-if",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror 1.0.69",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
+
+[[package]]
+name = "js-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "json-patch"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08"
+dependencies = [
+ "jsonptr",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "jsonptr"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "jsonwebtoken"
+version = "9.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
+dependencies = [
+ "base64 0.22.1",
+ "js-sys",
+ "pem",
+ "ring",
+ "serde",
+ "serde_json",
+ "simple_asn1",
+]
+
+[[package]]
+name = "keyboard-types"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a"
+dependencies = [
+ "bitflags 2.9.1",
+ "serde",
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "kuchikiki"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8"
+dependencies = [
+ "cssparser",
+ "html5ever",
+ "indexmap 1.9.3",
+ "matches",
+ "selectors",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+dependencies = [
+ "spin",
+]
+
+[[package]]
+name = "libappindicator"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a"
+dependencies = [
+ "glib",
+ "gtk",
+ "gtk-sys",
+ "libappindicator-sys",
+ "log",
+]
+
+[[package]]
+name = "libappindicator-sys"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf"
+dependencies = [
+ "gtk-sys",
+ "libloading",
+ "once_cell",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.174"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
+
+[[package]]
+name = "libloading"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
+dependencies = [
+ "cfg-if",
+ "winapi",
+]
+
+[[package]]
+name = "libm"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
+
+[[package]]
+name = "libredox"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
+dependencies = [
+ "bitflags 2.9.1",
+ "libc",
+]
+
+[[package]]
+name = "libsqlite3-sys"
+version = "0.30.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
+dependencies = [
+ "cc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
+
+[[package]]
+name = "litemap"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
+
+[[package]]
+name = "lock_api"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "mac"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
+
+[[package]]
+name = "markup5ever"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016"
+dependencies = [
+ "log",
+ "phf 0.10.1",
+ "phf_codegen 0.10.0",
+ "string_cache",
+ "string_cache_codegen",
+ "tendril",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
+
+[[package]]
+name = "matchit"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
+
+[[package]]
+name = "md-5"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
+dependencies = [
+ "cfg-if",
+ "digest",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
+
+[[package]]
+name = "memmap2"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
+dependencies = [
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "muda"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4de14a9b5d569ca68d7c891d613b390cf5ab4f851c77aaa2f9e435555d3d9492"
+dependencies = [
+ "crossbeam-channel",
+ "dpi",
+ "gtk",
+ "keyboard-types",
+ "objc2 0.6.1",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.1",
+ "once_cell",
+ "png",
+ "serde",
+ "thiserror 2.0.12",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "ndk"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
+dependencies = [
+ "bitflags 2.9.1",
+ "jni-sys",
+ "log",
+ "ndk-sys",
+ "num_enum",
+ "raw-window-handle",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "ndk-context"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
+
+[[package]]
+name = "ndk-sys"
+version = "0.6.0+11769913"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
+dependencies = [
+ "jni-sys",
+]
+
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+
+[[package]]
+name = "nix"
+version = "0.30.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
+dependencies = [
+ "bitflags 2.9.1",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+ "memoffset",
+]
+
+[[package]]
+name = "nodrop"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint-dig"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
+dependencies = [
+ "byteorder",
+ "lazy_static",
+ "libm",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "rand 0.8.5",
+ "smallvec",
+ "zeroize",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+ "libm",
+]
+
+[[package]]
+name = "num_enum"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179"
+dependencies = [
+ "num_enum_derive",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
+dependencies = [
+ "proc-macro-crate 3.3.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "objc-sys"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310"
+
+[[package]]
+name = "objc2"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
+dependencies = [
+ "objc-sys",
+ "objc2-encode",
+]
+
+[[package]]
+name = "objc2"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551"
+dependencies = [
+ "objc2-encode",
+ "objc2-exception-helper",
+]
+
+[[package]]
+name = "objc2-app-kit"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc"
+dependencies = [
+ "bitflags 2.9.1",
+ "block2 0.6.1",
+ "libc",
+ "objc2 0.6.1",
+ "objc2-cloud-kit",
+ "objc2-core-data",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-core-image",
+ "objc2-foundation 0.3.1",
+ "objc2-quartz-core 0.3.1",
+]
+
+[[package]]
+name = "objc2-cloud-kit"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d"
+dependencies = [
+ "bitflags 2.9.1",
+ "objc2 0.6.1",
+ "objc2-foundation 0.3.1",
+]
+
+[[package]]
+name = "objc2-core-data"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291fbbf7d29287518e8686417cf7239c74700fd4b607623140a7d4a3c834329d"
+dependencies = [
+ "bitflags 2.9.1",
+ "objc2 0.6.1",
+ "objc2-foundation 0.3.1",
+]
+
+[[package]]
+name = "objc2-core-foundation"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
+dependencies = [
+ "bitflags 2.9.1",
+ "dispatch2",
+ "objc2 0.6.1",
+]
+
+[[package]]
+name = "objc2-core-graphics"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4"
+dependencies = [
+ "bitflags 2.9.1",
+ "dispatch2",
+ "objc2 0.6.1",
+ "objc2-core-foundation",
+ "objc2-io-surface",
+]
+
+[[package]]
+name = "objc2-core-image"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79b3dc0cc4386b6ccf21c157591b34a7f44c8e75b064f85502901ab2188c007e"
+dependencies = [
+ "objc2 0.6.1",
+ "objc2-foundation 0.3.1",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "objc2-exception-helper"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "objc2-foundation"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
+dependencies = [
+ "bitflags 2.9.1",
+ "block2 0.5.1",
+ "libc",
+ "objc2 0.5.2",
+]
+
+[[package]]
+name = "objc2-foundation"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c"
+dependencies = [
+ "bitflags 2.9.1",
+ "block2 0.6.1",
+ "libc",
+ "objc2 0.6.1",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-io-surface"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c"
+dependencies = [
+ "bitflags 2.9.1",
+ "objc2 0.6.1",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-metal"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
+dependencies = [
+ "bitflags 2.9.1",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
+dependencies = [
+ "bitflags 2.9.1",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+ "objc2-metal",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5"
+dependencies = [
+ "bitflags 2.9.1",
+ "objc2 0.6.1",
+ "objc2-foundation 0.3.1",
+]
+
+[[package]]
+name = "objc2-ui-kit"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed"
+dependencies = [
+ "bitflags 2.9.1",
+ "objc2 0.6.1",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.1",
+]
+
+[[package]]
+name = "objc2-web-kit"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91672909de8b1ce1c2252e95bbee8c1649c9ad9d14b9248b3d7b4c47903c47ad"
+dependencies = [
+ "bitflags 2.9.1",
+ "block2 0.6.1",
+ "objc2 0.6.1",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.1",
+]
+
+[[package]]
+name = "object"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
+
+[[package]]
+name = "open"
+version = "5.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95"
+dependencies = [
+ "dunce",
+ "is-wsl",
+ "libc",
+ "pathdiff",
+]
+
+[[package]]
+name = "openssl"
+version = "0.10.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
+dependencies = [
+ "bitflags 2.9.1",
+ "cfg-if",
+ "foreign-types 0.3.2",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "ordered-stream"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "pango"
+version = "0.18.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4"
+dependencies = [
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+ "pango-sys",
+]
+
+[[package]]
+name = "pango-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "pathdiff"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
+
+[[package]]
+name = "pem"
+version = "3.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3"
+dependencies = [
+ "base64 0.22.1",
+ "serde",
+]
+
+[[package]]
+name = "pem-rfc7468"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
+dependencies = [
+ "base64ct",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "phf"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
+dependencies = [
+ "phf_macros 0.8.0",
+ "phf_shared 0.8.0",
+ "proc-macro-hack",
+]
+
+[[package]]
+name = "phf"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
+dependencies = [
+ "phf_shared 0.10.0",
+]
+
+[[package]]
+name = "phf"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
+dependencies = [
+ "phf_macros 0.11.3",
+ "phf_shared 0.11.3",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
+dependencies = [
+ "phf_generator 0.8.0",
+ "phf_shared 0.8.0",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
+dependencies = [
+ "phf_generator 0.10.0",
+ "phf_shared 0.10.0",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
+dependencies = [
+ "phf_shared 0.8.0",
+ "rand 0.7.3",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
+dependencies = [
+ "phf_shared 0.10.0",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
+dependencies = [
+ "phf_shared 0.11.3",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c"
+dependencies = [
+ "phf_generator 0.8.0",
+ "phf_shared 0.8.0",
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
+dependencies = [
+ "phf_generator 0.11.3",
+ "phf_shared 0.11.3",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
+dependencies = [
+ "siphasher 0.3.11",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
+dependencies = [
+ "siphasher 0.3.11",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
+dependencies = [
+ "siphasher 1.0.1",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "piper"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
+[[package]]
+name = "pkcs1"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
+dependencies = [
+ "der",
+ "pkcs8",
+ "spki",
+]
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+[[package]]
+name = "plist"
+version = "1.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d77244ce2d584cd84f6a15f86195b8c9b2a0dfbfd817c09e0464244091a58ed"
+dependencies = [
+ "base64 0.22.1",
+ "indexmap 2.9.0",
+ "quick-xml",
+ "serde",
+ "time",
+]
+
+[[package]]
+name = "png"
+version = "0.17.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
+dependencies = [
+ "bitflags 1.3.2",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "polling"
+version = "3.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix",
+ "tracing",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "portable-atomic"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
+name = "potential_utf"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+dependencies = [
+ "once_cell",
+ "toml_edit 0.19.15",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8"
+dependencies = [
+ "toml_edit 0.20.7",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
+dependencies = [
+ "toml_edit 0.22.27",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-hack"
+version = "0.5.20+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "prometheus-client"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf41c1a7c32ed72abe5082fb19505b969095c12da9f5732a4bc9878757fd087c"
+dependencies = [
+ "dtoa",
+ "itoa 1.0.15",
+ "parking_lot",
+ "prometheus-client-derive-encode",
+]
+
+[[package]]
+name = "prometheus-client-derive-encode"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.37.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom 0.1.16",
+ "libc",
+ "rand_chacha 0.2.2",
+ "rand_core 0.5.1",
+ "rand_hc",
+ "rand_pcg",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.3",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.3",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
+dependencies = [
+ "getrandom 0.3.3",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_pcg"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "raw-window-handle"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
+dependencies = [
+ "bitflags 2.9.1",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
+dependencies = [
+ "getrandom 0.2.16",
+ "libredox",
+ "thiserror 2.0.12",
+]
+
+[[package]]
+name = "ref-cast"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf"
+dependencies = [
+ "ref-cast-impl",
+]
+
+[[package]]
+name = "ref-cast-impl"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "refinery"
+version = "0.8.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ba5d693abf62492c37268512ff35b77655d2e957ca53dab85bf993fe9172d15"
+dependencies = [
+ "refinery-core",
+ "refinery-macros",
+]
+
+[[package]]
+name = "refinery-core"
+version = "0.8.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a83581f18c1a4c3a6ebd7a174bdc665f17f618d79f7edccb6a0ac67e660b319"
+dependencies = [
+ "async-trait",
+ "cfg-if",
+ "log",
+ "regex",
+ "serde",
+ "siphasher 1.0.1",
+ "thiserror 1.0.69",
+ "time",
+ "toml",
+ "url",
+ "walkdir",
+]
+
+[[package]]
+name = "refinery-macros"
+version = "0.8.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72c225407d8e52ef8cf094393781ecda9a99d6544ec28d90a6915751de259264"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "refinery-core",
+ "regex",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
+[[package]]
+name = "reqwest"
+version = "0.12.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-tls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-native-tls",
+ "tokio-util",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.16",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rsa"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
+dependencies = [
+ "const-oid",
+ "digest",
+ "num-bigint-dig",
+ "num-integer",
+ "num-traits",
+ "pkcs1",
+ "pkcs8",
+ "rand_core 0.6.4",
+ "signature",
+ "spki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
+dependencies = [
+ "bitflags 2.9.1",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643"
+dependencies = [
+ "once_cell",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "schemars"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
+dependencies = [
+ "dyn-clone",
+ "indexmap 1.9.3",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "schemars"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "security-framework"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+dependencies = [
+ "bitflags 2.9.1",
+ "core-foundation 0.9.4",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "selectors"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe"
+dependencies = [
+ "bitflags 1.3.2",
+ "cssparser",
+ "derive_more",
+ "fxhash",
+ "log",
+ "matches",
+ "phf 0.8.0",
+ "phf_codegen 0.8.0",
+ "precomputed-hash",
+ "servo_arc",
+ "smallvec",
+ "thin-slice",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-untagged"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "299d9c19d7d466db4ab10addd5703e4c615dec2a5a16dbbafe191045e87ee66e"
+dependencies = [
+ "erased-serde",
+ "serde",
+ "typeid",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "serde_derive_internals"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.140"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
+dependencies = [
+ "itoa 1.0.15",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a"
+dependencies = [
+ "itoa 1.0.15",
+ "serde",
+]
+
+[[package]]
+name = "serde_repr"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa 1.0.15",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_with"
+version = "3.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf65a400f8f66fb7b0552869ad70157166676db75ed8181f8104ea91cf9d0b42"
+dependencies = [
+ "base64 0.22.1",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.9.0",
+ "schemars 0.9.0",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "serde_with_macros",
+ "time",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "3.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81679d9ed988d5e9a5e6531dc3f2c28efbd639cbd1dfb628df08edea6004da77"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "serde_yaml"
+version = "0.9.34+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
+dependencies = [
+ "indexmap 2.9.0",
+ "itoa 1.0.15",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
+[[package]]
+name = "serialize-to-javascript"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb"
+dependencies = [
+ "serde",
+ "serde_json",
+ "serialize-to-javascript-impl",
+]
+
+[[package]]
+name = "serialize-to-javascript-impl"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "servo_arc"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432"
+dependencies = [
+ "nodrop",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "digest",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
+
+[[package]]
+name = "simple_asn1"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
+dependencies = [
+ "num-bigint",
+ "num-traits",
+ "thiserror 2.0.12",
+ "time",
+]
+
+[[package]]
+name = "siphasher"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
+
+[[package]]
+name = "siphasher"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
+
+[[package]]
+name = "slab"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "socket2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "softbuffer"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08"
+dependencies = [
+ "bytemuck",
+ "cfg_aliases",
+ "core-graphics",
+ "foreign-types 0.5.0",
+ "js-sys",
+ "log",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+ "objc2-quartz-core 0.2.2",
+ "raw-window-handle",
+ "redox_syscall",
+ "wasm-bindgen",
+ "web-sys",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "soup3"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f"
+dependencies = [
+ "futures-channel",
+ "gio",
+ "glib",
+ "libc",
+ "soup3-sys",
+]
+
+[[package]]
+name = "soup3-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "sqlx"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
+dependencies = [
+ "sqlx-core",
+ "sqlx-macros",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+]
+
+[[package]]
+name = "sqlx-core"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "chrono",
+ "crc",
+ "crossbeam-queue",
+ "either",
+ "event-listener",
+ "futures-core",
+ "futures-intrusive",
+ "futures-io",
+ "futures-util",
+ "hashbrown 0.15.4",
+ "hashlink",
+ "indexmap 2.9.0",
+ "log",
+ "memchr",
+ "native-tls",
+ "once_cell",
+ "percent-encoding",
+ "serde",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "thiserror 2.0.12",
+ "tokio",
+ "tokio-stream",
+ "tracing",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "sqlx-macros"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "sqlx-core",
+ "sqlx-macros-core",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "sqlx-macros-core"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
+dependencies = [
+ "dotenvy",
+ "either",
+ "heck 0.5.0",
+ "hex",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "sha2",
+ "sqlx-core",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+ "syn 2.0.103",
+ "tokio",
+ "url",
+]
+
+[[package]]
+name = "sqlx-mysql"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
+dependencies = [
+ "atoi",
+ "base64 0.22.1",
+ "bitflags 2.9.1",
+ "byteorder",
+ "bytes",
+ "chrono",
+ "crc",
+ "digest",
+ "dotenvy",
+ "either",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "generic-array",
+ "hex",
+ "hkdf",
+ "hmac",
+ "itoa 1.0.15",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "percent-encoding",
+ "rand 0.8.5",
+ "rsa",
+ "serde",
+ "sha1",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror 2.0.12",
+ "tracing",
+ "uuid",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-postgres"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
+dependencies = [
+ "atoi",
+ "base64 0.22.1",
+ "bitflags 2.9.1",
+ "byteorder",
+ "chrono",
+ "crc",
+ "dotenvy",
+ "etcetera",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "hex",
+ "hkdf",
+ "hmac",
+ "home",
+ "itoa 1.0.15",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "rand 0.8.5",
+ "serde",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror 2.0.12",
+ "tracing",
+ "uuid",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-sqlite"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
+dependencies = [
+ "atoi",
+ "chrono",
+ "flume",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-intrusive",
+ "futures-util",
+ "libsqlite3-sys",
+ "log",
+ "percent-encoding",
+ "serde",
+ "serde_urlencoded",
+ "sqlx-core",
+ "thiserror 2.0.12",
+ "tracing",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "streamserver"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "axum",
+ "cherrycore",
+ "clap",
+ "futures-util",
+ "log",
+ "serde",
+ "serde_json",
+ "serde_with",
+ "serde_yaml",
+ "streamstore",
+ "tokio",
+ "tokio-util",
+ "uuid",
+]
+
+[[package]]
+name = "streamstore"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "arc-swap",
+ "backtrace",
+ "crc",
+ "crossbeam-channel",
+ "defer",
+ "env_logger",
+ "lazy_static",
+ "log",
+ "memmap2",
+ "prometheus-client",
+ "rand 0.9.1",
+ "refinery",
+ "thiserror 2.0.12",
+ "tokio",
+]
+
+[[package]]
+name = "string_cache"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f"
+dependencies = [
+ "new_debug_unreachable",
+ "parking_lot",
+ "phf_shared 0.11.3",
+ "precomputed-hash",
+ "serde",
+]
+
+[[package]]
+name = "string_cache_codegen"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0"
+dependencies = [
+ "phf_generator 0.11.3",
+ "phf_shared 0.11.3",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "stringprep"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+ "unicode-properties",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "swift-rs"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7"
+dependencies = [
+ "base64 0.21.7",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "system-configuration"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
+dependencies = [
+ "bitflags 2.9.1",
+ "core-foundation 0.9.4",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "system-deps"
+version = "6.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
+dependencies = [
+ "cfg-expr",
+ "heck 0.5.0",
+ "pkg-config",
+ "toml",
+ "version-compare",
+]
+
+[[package]]
+name = "tao"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82"
+dependencies = [
+ "bitflags 2.9.1",
+ "core-foundation 0.10.1",
+ "core-graphics",
+ "crossbeam-channel",
+ "dispatch",
+ "dlopen2",
+ "dpi",
+ "gdkwayland-sys",
+ "gdkx11-sys",
+ "gtk",
+ "jni",
+ "lazy_static",
+ "libc",
+ "log",
+ "ndk",
+ "ndk-context",
+ "ndk-sys",
+ "objc2 0.6.1",
+ "objc2-app-kit",
+ "objc2-foundation 0.3.1",
+ "once_cell",
+ "parking_lot",
+ "raw-window-handle",
+ "scopeguard",
+ "tao-macros",
+ "unicode-segmentation",
+ "url",
+ "windows",
+ "windows-core",
+ "windows-version",
+ "x11-dl",
+]
+
+[[package]]
+name = "tao-macros"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
+
+[[package]]
+name = "tauri"
+version = "2.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7b0bc1aec81bda6bc455ea98fcaed26b3c98c1648c627ad6ff1c704e8bf8cbc"
+dependencies = [
+ "anyhow",
+ "bytes",
+ "dirs",
+ "dunce",
+ "embed_plist",
+ "futures-util",
+ "getrandom 0.2.16",
+ "glob",
+ "gtk",
+ "heck 0.5.0",
+ "http",
+ "jni",
+ "libc",
+ "log",
+ "mime",
+ "muda",
+ "objc2 0.6.1",
+ "objc2-app-kit",
+ "objc2-foundation 0.3.1",
+ "objc2-ui-kit",
+ "percent-encoding",
+ "plist",
+ "raw-window-handle",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "serde_repr",
+ "serialize-to-javascript",
+ "swift-rs",
+ "tauri-build",
+ "tauri-macros",
+ "tauri-runtime",
+ "tauri-runtime-wry",
+ "tauri-utils",
+ "thiserror 2.0.12",
+ "tokio",
+ "tray-icon",
+ "url",
+ "urlpattern",
+ "webkit2gtk",
+ "webview2-com",
+ "window-vibrancy",
+ "windows",
+]
+
+[[package]]
+name = "tauri-build"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7a0350f0df1db385ca5c02888a83e0e66655c245b7443db8b78a70da7d7f8fc"
+dependencies = [
+ "anyhow",
+ "cargo_toml",
+ "dirs",
+ "glob",
+ "heck 0.5.0",
+ "json-patch",
+ "schemars 0.8.22",
+ "semver",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "tauri-winres",
+ "toml",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-codegen"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f93f035551bf7b11b3f51ad9bc231ebbe5e085565527991c16cf326aa38cdf47"
+dependencies = [
+ "base64 0.22.1",
+ "brotli",
+ "ico",
+ "json-patch",
+ "plist",
+ "png",
+ "proc-macro2",
+ "quote",
+ "semver",
+ "serde",
+ "serde_json",
+ "sha2",
+ "syn 2.0.103",
+ "tauri-utils",
+ "thiserror 2.0.12",
+ "time",
+ "url",
+ "uuid",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-macros"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8db4df25e2d9d45de0c4c910da61cd5500190da14ae4830749fee3466dddd112"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+ "tauri-codegen",
+ "tauri-utils",
+]
+
+[[package]]
+name = "tauri-plugin"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37a5ebe6a610d1b78a94650896e6f7c9796323f408800cef436e0fa0539de601"
+dependencies = [
+ "anyhow",
+ "glob",
+ "plist",
+ "schemars 0.8.22",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "toml",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-plugin-opener"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c8983f50326d34437142a6d560b5c3426e91324297519b6eeb32ed0a1d1e0f2"
+dependencies = [
+ "dunce",
+ "glob",
+ "objc2-app-kit",
+ "objc2-foundation 0.3.1",
+ "open",
+ "schemars 0.8.22",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-plugin",
+ "thiserror 2.0.12",
+ "url",
+ "windows",
+ "zbus",
+]
+
+[[package]]
+name = "tauri-runtime"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00f004905d549854069e6774533d742b03cacfd6f03deb08940a8677586cbe39"
+dependencies = [
+ "cookie",
+ "dpi",
+ "gtk",
+ "http",
+ "jni",
+ "objc2 0.6.1",
+ "objc2-ui-kit",
+ "raw-window-handle",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "thiserror 2.0.12",
+ "url",
+ "windows",
+]
+
+[[package]]
+name = "tauri-runtime-wry"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f85d056f4d4b014fe874814034f3416d57114b617a493a4fe552580851a3f3a2"
+dependencies = [
+ "gtk",
+ "http",
+ "jni",
+ "log",
+ "objc2 0.6.1",
+ "objc2-app-kit",
+ "objc2-foundation 0.3.1",
+ "once_cell",
+ "percent-encoding",
+ "raw-window-handle",
+ "softbuffer",
+ "tao",
+ "tauri-runtime",
+ "tauri-utils",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "windows",
+ "wry",
+]
+
+[[package]]
+name = "tauri-utils"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2900399c239a471bcff7f15c4399eb1a8c4fe511ba2853e07c996d771a5e0a4"
+dependencies = [
+ "anyhow",
+ "brotli",
+ "cargo_metadata",
+ "ctor",
+ "dunce",
+ "glob",
+ "html5ever",
+ "http",
+ "infer",
+ "json-patch",
+ "kuchikiki",
+ "log",
+ "memchr",
+ "phf 0.11.3",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "schemars 0.8.22",
+ "semver",
+ "serde",
+ "serde-untagged",
+ "serde_json",
+ "serde_with",
+ "swift-rs",
+ "thiserror 2.0.12",
+ "toml",
+ "url",
+ "urlpattern",
+ "uuid",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-winres"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8d321dbc6f998d825ab3f0d62673e810c861aac2d0de2cc2c395328f1d113b4"
+dependencies = [
+ "embed-resource",
+ "indexmap 2.9.0",
+ "toml",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
+dependencies = [
+ "fastrand",
+ "getrandom 0.3.3",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "tendril"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
+dependencies = [
+ "futf",
+ "mac",
+ "utf-8",
+]
+
+[[package]]
+name = "thin-slice"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+dependencies = [
+ "thiserror-impl 2.0.12",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "time"
+version = "0.3.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
+dependencies = [
+ "deranged",
+ "itoa 1.0.15",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
+
+[[package]]
+name = "time-macros"
+version = "0.2.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.45.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-tungstenite"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
+dependencies = [
+ "futures-util",
+ "log",
+ "tokio",
+ "tungstenite",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit 0.22.27",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.19.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
+dependencies = [
+ "indexmap 2.9.0",
+ "toml_datetime",
+ "winnow 0.5.40",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.20.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81"
+dependencies = [
+ "indexmap 2.9.0",
+ "toml_datetime",
+ "winnow 0.5.40",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap 2.9.0",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_write",
+ "winnow 0.7.11",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
+[[package]]
+name = "tower"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
+dependencies = [
+ "bitflags 2.9.1",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "iri-string",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "tray-icon"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7eee98ec5c90daf179d55c20a49d8c0d043054ce7c26336c09a24d31f14fa0"
+dependencies = [
+ "crossbeam-channel",
+ "dirs",
+ "libappindicator",
+ "muda",
+ "objc2 0.6.1",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation 0.3.1",
+ "once_cell",
+ "png",
+ "serde",
+ "thiserror 2.0.12",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "tungstenite"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
+dependencies = [
+ "bytes",
+ "data-encoding",
+ "http",
+ "httparse",
+ "log",
+ "rand 0.9.1",
+ "sha1",
+ "thiserror 2.0.12",
+ "utf-8",
+]
+
+[[package]]
+name = "typeid"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
+
+[[package]]
+name = "typenum"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
+
+[[package]]
+name = "uds_windows"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
+dependencies = [
+ "memoffset",
+ "tempfile",
+ "winapi",
+]
+
+[[package]]
+name = "unic-char-property"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
+dependencies = [
+ "unic-char-range",
+]
+
+[[package]]
+name = "unic-char-range"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
+
+[[package]]
+name = "unic-common"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
+
+[[package]]
+name = "unic-ucd-ident"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987"
+dependencies = [
+ "unic-char-property",
+ "unic-char-range",
+ "unic-ucd-version",
+]
+
+[[package]]
+name = "unic-ucd-version"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
+dependencies = [
+ "unic-common",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-properties"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "urlpattern"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d"
+dependencies = [
+ "regex",
+ "serde",
+ "unic-ucd-ident",
+ "url",
+]
+
+[[package]]
+name = "use"
+version = "0.0.1-pre.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f916b8b6102de89f9999988ddc8e9bd0f119a8344e06bb19b0b03fb655769035"
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
+dependencies = [
+ "getrandom 0.3.3",
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version-compare"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "vswhom"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b"
+dependencies = [
+ "libc",
+ "vswhom-sys",
+]
+
+[[package]]
+name = "vswhom-sys"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasi"
+version = "0.14.2+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+dependencies = [
+ "wit-bindgen-rt",
+]
+
+[[package]]
+name = "wasite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "once_cell",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-streams"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webkit2gtk"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a"
+dependencies = [
+ "bitflags 1.3.2",
+ "cairo-rs",
+ "gdk",
+ "gdk-sys",
+ "gio",
+ "gio-sys",
+ "glib",
+ "glib-sys",
+ "gobject-sys",
+ "gtk",
+ "gtk-sys",
+ "javascriptcore-rs",
+ "libc",
+ "once_cell",
+ "soup3",
+ "webkit2gtk-sys",
+]
+
+[[package]]
+name = "webkit2gtk-sys"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c"
+dependencies = [
+ "bitflags 1.3.2",
+ "cairo-sys-rs",
+ "gdk-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "gtk-sys",
+ "javascriptcore-rs-sys",
+ "libc",
+ "pkg-config",
+ "soup3-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "webview2-com"
+version = "0.37.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b542b5cfbd9618c46c2784e4d41ba218c336ac70d44c55e47b251033e7d85601"
+dependencies = [
+ "webview2-com-macros",
+ "webview2-com-sys",
+ "windows",
+ "windows-core",
+ "windows-implement",
+ "windows-interface",
+]
+
+[[package]]
+name = "webview2-com-macros"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "webview2-com-sys"
+version = "0.37.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae2d11c4a686e4409659d7891791254cf9286d3cfe0eef54df1523533d22295"
+dependencies = [
+ "thiserror 2.0.12",
+ "windows",
+ "windows-core",
+]
+
+[[package]]
+name = "whoami"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7"
+dependencies = [
+ "redox_syscall",
+ "wasite",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "window-vibrancy"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c"
+dependencies = [
+ "objc2 0.6.1",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.1",
+ "raw-window-handle",
+ "windows-sys 0.59.0",
+ "windows-version",
+]
+
+[[package]]
+name = "windows"
+version = "0.61.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
+dependencies = [
+ "windows-collections",
+ "windows-core",
+ "windows-future",
+ "windows-link",
+ "windows-numerics",
+]
+
+[[package]]
+name = "windows-collections"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
+dependencies = [
+ "windows-core",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-future"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
+dependencies = [
+ "windows-core",
+ "windows-link",
+ "windows-threading",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
+[[package]]
+name = "windows-numerics"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
+dependencies = [
+ "windows-core",
+ "windows-link",
+]
+
+[[package]]
+name = "windows-registry"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820"
+dependencies = [
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
+dependencies = [
+ "windows_aarch64_gnullvm 0.53.0",
+ "windows_aarch64_msvc 0.53.0",
+ "windows_i686_gnu 0.53.0",
+ "windows_i686_gnullvm 0.53.0",
+ "windows_i686_msvc 0.53.0",
+ "windows_x86_64_gnu 0.53.0",
+ "windows_x86_64_gnullvm 0.53.0",
+ "windows_x86_64_msvc 0.53.0",
+]
+
+[[package]]
+name = "windows-threading"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-version"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e04a5c6627e310a23ad2358483286c7df260c964eb2d003d8efd6d0f4e79265c"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+
+[[package]]
+name = "winnow"
+version = "0.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winnow"
+version = "0.7.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winreg"
+version = "0.55.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
+dependencies = [
+ "bitflags 2.9.1",
+]
+
+[[package]]
+name = "writeable"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
+
+[[package]]
+name = "wry"
+version = "0.51.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c886a0a9d2a94fd90cfa1d929629b79cfefb1546e2c7430c63a47f0664c0e4e2"
+dependencies = [
+ "base64 0.22.1",
+ "block2 0.6.1",
+ "cookie",
+ "crossbeam-channel",
+ "dpi",
+ "dunce",
+ "gdkx11",
+ "gtk",
+ "html5ever",
+ "http",
+ "javascriptcore-rs",
+ "jni",
+ "kuchikiki",
+ "libc",
+ "ndk",
+ "objc2 0.6.1",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.1",
+ "objc2-ui-kit",
+ "objc2-web-kit",
+ "once_cell",
+ "percent-encoding",
+ "raw-window-handle",
+ "sha2",
+ "soup3",
+ "tao-macros",
+ "thiserror 2.0.12",
+ "url",
+ "webkit2gtk",
+ "webkit2gtk-sys",
+ "webview2-com",
+ "windows",
+ "windows-core",
+ "windows-version",
+ "x11-dl",
+]
+
+[[package]]
+name = "x11"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "x11-dl"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
+dependencies = [
+ "libc",
+ "once_cell",
+ "pkg-config",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+ "synstructure",
+]
+
+[[package]]
+name = "zbus"
+version = "5.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3a7c7cee313d044fca3f48fa782cb750c79e4ca76ba7bc7718cd4024cdf6f68"
+dependencies = [
+ "async-broadcast",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "async-recursion",
+ "async-task",
+ "async-trait",
+ "blocking",
+ "enumflags2",
+ "event-listener",
+ "futures-core",
+ "futures-lite",
+ "hex",
+ "nix",
+ "ordered-stream",
+ "serde",
+ "serde_repr",
+ "tracing",
+ "uds_windows",
+ "windows-sys 0.59.0",
+ "winnow 0.7.11",
+ "zbus_macros",
+ "zbus_names",
+ "zvariant",
+]
+
+[[package]]
+name = "zbus_macros"
+version = "5.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a17e7e5eec1550f747e71a058df81a9a83813ba0f6a95f39c4e218bdc7ba366a"
+dependencies = [
+ "proc-macro-crate 3.3.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+ "zbus_names",
+ "zvariant",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zbus_names"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97"
+dependencies = [
+ "serde",
+ "static_assertions",
+ "winnow 0.7.11",
+ "zvariant",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+]
+
+[[package]]
+name = "zvariant"
+version = "5.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d30786f75e393ee63a21de4f9074d4c038d52c5b1bb4471f955db249f9dffb1"
+dependencies = [
+ "endi",
+ "enumflags2",
+ "serde",
+ "winnow 0.7.11",
+ "zvariant_derive",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_derive"
+version = "5.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75fda702cd42d735ccd48117b1630432219c0e9616bf6cb0f8350844ee4d9580"
+dependencies = [
+ "proc-macro-crate 3.3.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.103",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_utils"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde",
+ "static_assertions",
+ "syn 2.0.103",
+ "winnow 0.7.11",
]
diff --git a/Cargo.toml b/Cargo.toml
index 4e43f81..7058729 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,29 +1,2 @@
-[package]
-name = "streamstore-rs"
-version = "0.1.0"
-edition = "2024"
-authors = ["fu.niukey@gmail.com"]
-rust-version = "1.86"
-license = "MIT"
-keywords = ["streamstore", "stream"]
-description = "lib for storage stream programing"
-repository = "https://github.com/akzj/streamstore-rs"
-homepage = "https://github.com/akzj/streamstore-rs"
-
-[dependencies]
-anyhow = { version = "1.0.98", features = ["backtrace"] }
-arc-swap = "1.7.1"
-backtrace = "0.3.75"
-crc = "3.3.0"
-crossbeam-channel = "0.5.15"
-defer = "0.2.1"
-env_logger = "0.11.8"
-filename = "0.1.1"
-lazy_static = "1.5.0"
-log = "0.4.27"
-memmap2 = "0.9.5"
-prometheus-client = "0.23.1"
-rand = "0.9.1"
-thiserror = "2.0.12"
-tokio = { version = "1.45.1", features = ["full"] }
-
+[workspace]
+members = [ "crates/cherryserver","crates/streamstore", "crates/cherry/src-tauri", "crates/cherrycore", "crates/streamserver"]
diff --git a/README.md b/README.md
index e1f1194..8a60a58 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,61 @@
-# streamstore-rs
+# Cherry
-streamstore-rs is a Rust library for building stream-sourced applications using the StreamStore protocol. It provides a simple and efficient way to manage streams, making it easier to build scalable and maintainable systems.
+Cherry is a modern instant messaging application built with Tauri, React, and Rust. It provides a fast, secure, and feature-rich messaging experience.
-## Features
-- **Stream Management**: Efficiently manage streams with support for seek, read, and append operations.
-- **High Performance**: Built with performance in mind, leveraging Rust's capabilities for low-level memory management.
-- **Concurrency**: Designed to handle concurrent operations safely and efficiently.
\ No newline at end of file
+## Project Structure
+
+This repository is organized as a Rust workspace with multiple crates:
+
+- [Cherry](./crates/cherry): The main application using Tauri, React, and TypeScript
+- [CherryCore](./crates/cherrycore): Core functionality for the Cherry application
+- [CherryServer](./crates/cherryserver): Server-side implementation for Cherry
+- [StreamStore](./crates/streamstore): A Rust library for building stream-sourced applications
+- [StreamServer](./crates/streamserver): Server implementation for StreamStore
+
+## Getting Started
+
+### Prerequisites
+
+- Rust (latest stable version)
+- Node.js (v14 or later)
+- npm or yarn
+
+### Development Setup
+
+1. Clone the repository:
+ ```bash
+ git clone https://github.com/akzj/cherry.git
+ cd cherry
+ ```
+
+2. Install dependencies:
+ ```bash
+ # Install Rust dependencies
+ cargo build
+
+ # Install frontend dependencies
+ cd crates/cherry
+ npm install
+ ```
+
+3. Run the development server:
+ ```bash
+ # In the crates/cherry directory
+ npm run tauri dev
+ ```
+
+## Docker Development Environment
+
+A Docker development environment is available for easier setup:
+
+```bash
+docker-compose -f docker-compose.dev.yml up
+```
+
+## Contributing
+
+We welcome contributions to Cherry! Please see our [contributing guidelines](./CONTRIBUTING.md) for more information on how to get involved.
+
+## License
+
+This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.
\ No newline at end of file
diff --git a/TESTING.md b/TESTING.md
new file mode 100644
index 0000000..7074039
--- /dev/null
+++ b/TESTING.md
@@ -0,0 +1,179 @@
+# Testing Documentation for Cherry Project
+
+This document describes the testing infrastructure and coverage improvements made to the Cherry project.
+
+## Overview
+
+The Cherry project now has comprehensive unit tests covering the core Rust libraries. The test suite has been significantly expanded from the original 4 tests to **70 tests** across multiple crates.
+
+## Test Coverage Summary
+
+### StreamStore Crate (42 tests)
+The `streamstore` crate is the core stream-sourced application library with comprehensive test coverage:
+
+#### Error Handling (`errors.rs`) - 3 tests
+- โ
Error display formatting
+- โ
Error constructor functions
+- โ
Debug trait implementation
+
+#### Entry Encoding/Decoding (`entry.rs`) - 9 tests
+- โ
Entry creation and default values
+- โ
Entry encoding to binary format
+- โ
Entry decoding from binary format
+- โ
Multiple entry encoding/decoding
+- โ
Empty file handling
+- โ
Early termination during decoding
+- โ
Large data handling (1MB+)
+- โ
Invalid version handling
+- โ
Debug trait implementation
+
+#### Memory Tables (`mem_table.rs`) - 13 tests
+- โ
MemTable creation and initialization
+- โ
Single and multiple entry appending
+- โ
Stream range queries
+- โ
Stream data reading
+- โ
Custom stream offset handling
+- โ
Error conditions (zero stream ID, empty data, invalid entry IDs)
+- โ
Concurrent access patterns
+- โ
Thread safety verification
+
+#### Stream Tables (`table.rs`) - 16 tests
+- โ
StreamData creation and capacity management
+- โ
Data filling with overflow handling
+- โ
StreamTable creation and management
+- โ
Single and multiple data appending
+- โ
Large data handling across multiple buffers
+- โ
CRC64 checksum calculation
+- โ
Stream reading with various offsets and sizes
+- โ
Cross-buffer boundary reading
+- โ
Empty stream handling
+
+#### Segments (`segments.rs`) - 1 test
+- โ
Segment header size validation
+
+### CherryCore Crate (28 tests)
+The `cherrycore` crate contains shared types and JWT authentication with full test coverage:
+
+#### JWT Authentication (`jwt.rs`) - 11 tests
+- โ
JWT configuration
+- โ
JWT claims creation and validation
+- โ
Token generation and parsing
+- โ
Invalid token handling
+- โ
Authentication error handling
+- โ
Error response conversion
+- โ
Keys creation and usage
+- โ
Serialization/deserialization
+- โ
Debug trait implementations
+
+#### Type Definitions (`types.rs`) - 17 tests
+- โ
Login request/response serialization
+- โ
OAuth login handling
+- โ
Stream operation request/response types
+- โ
Response error handling and HTTP status codes
+- โ
Stream and conversation data structures
+- โ
Base64 encoding for binary data
+- โ
JSON serialization/deserialization
+- โ
Debug trait implementations
+- โ
Error conversion from anyhow
+
+## Running Tests
+
+### Quick Test Run
+Use the provided test runner script:
+```bash
+./run_tests.sh
+```
+
+### Manual Test Execution
+Run tests for individual crates:
+
+```bash
+# StreamStore tests
+cd crates/streamstore
+cargo test
+
+# CherryCore tests (requires JWT_SECRET environment variable)
+cd crates/cherrycore
+JWT_SECRET=test_secret_key_for_testing cargo test
+
+# Both crates together
+JWT_SECRET=test_secret_key_for_testing cargo test -p streamstore -p cherrycore
+```
+
+### Test Environment Setup
+Some tests require environment variables:
+- `JWT_SECRET`: Required for JWT-related tests in cherrycore
+
+## Test Categories
+
+### Unit Tests
+- **Functionality Tests**: Verify core business logic
+- **Error Handling Tests**: Ensure proper error conditions and messages
+- **Serialization Tests**: Validate JSON/binary serialization
+- **Concurrency Tests**: Test thread safety and concurrent access
+- **Edge Case Tests**: Handle boundary conditions and invalid inputs
+
+### Integration Points
+- **Cross-Module Integration**: Tests that verify interaction between modules
+- **Data Format Compatibility**: Ensure encoding/decoding consistency
+- **Error Propagation**: Verify error handling across module boundaries
+
+## Test Quality Features
+
+### Comprehensive Coverage
+- **Happy Path Testing**: Normal operation scenarios
+- **Error Path Testing**: Exception and error conditions
+- **Edge Case Testing**: Boundary conditions and limits
+- **Concurrency Testing**: Multi-threaded scenarios
+
+### Test Reliability
+- **Deterministic Tests**: Consistent results across runs
+- **Isolated Tests**: No dependencies between test cases
+- **Clean Up**: Proper resource cleanup (temporary files, etc.)
+- **Environment Independence**: Tests work in different environments
+
+### Maintainability
+- **Clear Test Names**: Descriptive test function names
+- **Good Documentation**: Comments explaining complex test scenarios
+- **Modular Structure**: Tests organized by functionality
+- **Easy to Extend**: Simple to add new tests
+
+## Known Limitations
+
+### GTK Dependencies
+The main `cherry` crate (Tauri application) requires GTK system dependencies that are not available in all environments. These tests are excluded from the automated test suite but can be run manually in environments with proper GTK setup.
+
+### Integration Tests
+The current test suite focuses on unit tests. Integration tests that require database connections, network access, or complex service interactions are not included but could be added in the future.
+
+### Performance Tests
+While the tests include some large data scenarios, dedicated performance and benchmark tests are not included in the current suite.
+
+## Future Improvements
+
+### Potential Enhancements
+1. **Integration Tests**: Add tests for database operations and API endpoints
+2. **Property-Based Testing**: Use libraries like `proptest` for more comprehensive testing
+3. **Benchmark Tests**: Add performance regression testing
+4. **Mock Testing**: Add mocking for external dependencies
+5. **Coverage Reporting**: Integrate with coverage tools like `tarpaulin`
+
+### Test Infrastructure
+1. **CI/CD Integration**: Automated testing in continuous integration
+2. **Test Data Management**: Structured test data and fixtures
+3. **Parallel Test Execution**: Optimize test runtime
+4. **Test Reporting**: Enhanced test result reporting and analysis
+
+## Contributing to Tests
+
+When adding new functionality:
+1. **Write Tests First**: Consider test-driven development
+2. **Test All Paths**: Include both success and error scenarios
+3. **Use Descriptive Names**: Make test purposes clear
+4. **Keep Tests Simple**: One concept per test
+5. **Clean Up Resources**: Ensure proper cleanup in tests
+6. **Document Complex Tests**: Add comments for non-obvious test logic
+
+## Conclusion
+
+The Cherry project now has a robust testing foundation with 70 comprehensive unit tests covering the core functionality. This provides confidence in code quality, helps prevent regressions, and makes the codebase more maintainable for future development.
\ No newline at end of file
diff --git a/TEST_COVERAGE_SUMMARY.md b/TEST_COVERAGE_SUMMARY.md
new file mode 100644
index 0000000..e626854
--- /dev/null
+++ b/TEST_COVERAGE_SUMMARY.md
@@ -0,0 +1,96 @@
+# Test Coverage Improvement Summary
+
+## Before
+- **Total Tests**: 4 tests (only in streamstore crate)
+- **Coverage**: Minimal, basic functionality only
+- **Crates Tested**: 1 out of 5 crates
+
+## After
+- **Total Tests**: 70 tests across 2 core crates
+- **Coverage**: Comprehensive unit testing
+- **Crates Tested**: 2 out of 5 crates (core libraries)
+
+## Detailed Breakdown
+
+### StreamStore Crate: 4 โ 42 tests (+38 tests)
+- **errors.rs**: 0 โ 3 tests (error handling, display, debug)
+- **entry.rs**: 0 โ 9 tests (encoding/decoding, edge cases, large data)
+- **mem_table.rs**: 1 โ 13 tests (CRUD operations, concurrency, validation)
+- **table.rs**: 1 โ 16 tests (data management, CRC, multi-buffer handling)
+- **segments.rs**: 1 โ 1 test (existing test preserved)
+
+### CherryCore Crate: 0 โ 28 tests (+28 tests)
+- **jwt.rs**: 0 โ 11 tests (authentication, token handling, errors)
+- **types.rs**: 0 โ 17 tests (serialization, HTTP responses, data structures)
+
+## Test Quality Features Added
+
+### Comprehensive Coverage
+- โ
Happy path testing
+- โ
Error condition testing
+- โ
Edge case handling
+- โ
Concurrency testing
+- โ
Large data scenarios
+- โ
Serialization/deserialization
+- โ
Debug trait implementations
+
+### Test Infrastructure
+- โ
Test runner script (`run_tests.sh`)
+- โ
Environment variable setup
+- โ
Documentation (`TESTING.md`)
+- โ
Clear test organization
+- โ
Descriptive test names
+
+### Code Quality Improvements
+- โ
Fixed bugs discovered during testing
+- โ
Improved error handling
+- โ
Better validation logic
+- โ
Enhanced type safety
+
+## Impact
+
+### Development Benefits
+- **Regression Prevention**: Catch breaking changes early
+- **Refactoring Confidence**: Safe code modifications
+- **Documentation**: Tests serve as usage examples
+- **Debugging**: Easier to isolate issues
+
+### Code Quality
+- **Reliability**: Higher confidence in core functionality
+- **Maintainability**: Easier to modify and extend
+- **Robustness**: Better handling of edge cases and errors
+
+### Future Development
+- **Foundation**: Solid base for adding more tests
+- **Standards**: Established testing patterns and practices
+- **CI/CD Ready**: Tests can be integrated into automation
+
+## Next Steps (Recommendations)
+
+1. **Integration Tests**: Add tests for database operations and API endpoints
+2. **Performance Tests**: Add benchmark tests for critical paths
+3. **Property-Based Testing**: Use `proptest` for more comprehensive testing
+4. **Coverage Reporting**: Integrate with `tarpaulin` or similar tools
+5. **CI/CD Integration**: Automate testing in continuous integration
+
+## Files Modified/Created
+
+### Test Files Added
+- `crates/streamstore/src/errors.rs` (added tests)
+- `crates/streamstore/src/entry.rs` (added tests)
+- `crates/streamstore/src/mem_table.rs` (expanded tests)
+- `crates/streamstore/src/table.rs` (expanded tests)
+- `crates/cherrycore/src/jwt.rs` (added tests)
+- `crates/cherrycore/src/types.rs` (added tests)
+
+### Infrastructure Files
+- `run_tests.sh` (test runner script)
+- `TESTING.md` (comprehensive testing documentation)
+- `TEST_COVERAGE_SUMMARY.md` (this summary)
+
+### Configuration Updates
+- `crates/cherrycore/Cargo.toml` (added test dependencies)
+
+## Conclusion
+
+The test coverage has been dramatically improved from 4 to 70 tests, providing comprehensive coverage of the core Rust libraries. This establishes a solid foundation for reliable development and future enhancements to the Cherry messaging application.
\ No newline at end of file
diff --git a/config.yaml b/config.yaml
new file mode 100644
index 0000000..1f5255a
--- /dev/null
+++ b/config.yaml
@@ -0,0 +1,5 @@
+server_url: "http://localhost"
+server_port: 3000
+jwt_secret: "test_secret_key"
+jwt_expire_time: 86400
+stream_storage_path: "/tmp/stream_storage"
\ No newline at end of file
diff --git a/crates/cherry/.gitignore b/crates/cherry/.gitignore
new file mode 100644
index 0000000..cb9f744
--- /dev/null
+++ b/crates/cherry/.gitignore
@@ -0,0 +1,27 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# Database
+*.db
\ No newline at end of file
diff --git a/crates/cherry/.vscode/extensions.json b/crates/cherry/.vscode/extensions.json
new file mode 100644
index 0000000..24d7cc6
--- /dev/null
+++ b/crates/cherry/.vscode/extensions.json
@@ -0,0 +1,3 @@
+{
+ "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
+}
diff --git a/crates/cherry/README.md b/crates/cherry/README.md
new file mode 100644
index 0000000..eb1d19d
--- /dev/null
+++ b/crates/cherry/README.md
@@ -0,0 +1,177 @@
+# Cherry - Tauri + React + TypeScript IM Application
+
+This project is built with Tauri, React, and TypeScript to create a modern instant messaging desktop application.
+
+## Recommended IDE Setup
+
+- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
+
+## ๅ็ซฏๆถๆ่ฎพ่ฎก (Frontend Architecture Design)
+
+ๅฅฝ็๏ผไฝฟ็จ Tauri + React ๅผๅ IM ๆก้ขๅบ็จ๏ผๅ็ซฏๆถๆ่ฎพ่ฎก้่ฆ่่ไปฅไธๅ ไธชๅ
ณ้ฎๆน้ข๏ผ
+
+๐งฑ **ๆ ธๅฟ็ฎๆ ๏ผ**
+
+* **้ซๆๆธฒๆ๏ผ** ๅฟซ้ๅๅบ็จๆทๆไฝๅๅฎๆถๆฐๆฎๆดๆฐใ
+* **ๅคๆ็ถๆ็ฎก็๏ผ** ๅค็็จๆทใไผ่ฏใๆถๆฏใ่็ณปไบบใ็พค็ปใๅจ็บฟ็ถๆ็ญๅคๆ็ถๆใ
+* **ๅฎๆถ้ไฟก๏ผ** ๆต็
ๅฐๆฅๆถๅๅฑ็คบๅฎๆถๆถๆฏใ้็ฅใ็ถๆๅๅใ
+* **่ฏๅฅฝ็็จๆทไฝ้ช๏ผ** ็้ขๆต็
ใๅๅบ่ฟ
้ใไบคไบ่ช็ถใ
+* **ๅฏ็ปดๆคๆงไธๆฉๅฑๆง๏ผ** ไปฃ็ ็ปๆๆธ
ๆฐ๏ผๆไบ็่งฃๅๆฉๅฑๅ่ฝใ
+* **ไธ Tauri ๅ็ซฏ้ๆ๏ผ** ๆ ็ผ่ฐ็จ Tauri ๆไพ็ Rust APIใ
+
+## ๐ ๆถๆ่ฎพ่ฎกๅปบ่ฎฎ
+
+### 1๏ธโฃ ๆๆฏๆ ้ๆฉ
+
+* **ๅ็ซฏๆกๆถ:** React (ไฝฟ็จ Hooks๏ผๆจ่ไฝฟ็จ React 18+)
+* **็ถๆ็ฎก็:**
+ * **ไธป่ฆ็ถๆ็ฎก็:** Pinia (Vue ็็ถๆ็ฎก็ๅบ๏ผไฝ React ็คพๅบไนๆ็ฑปไผผๆนๆกๅฆ `useReducer` + Redux Toolkit, Zustand)
+ * **ๅพฎ็ถๆ/ๅฏไฝ็จ็ฎก็:** Zustand (่ฝป้ใ็ฎๅใๆง่ฝๅฅฝ๏ผ้ๅ็ฎก็็ปไปถ้ดๅ
ฑไบซ็โๅพฎโ็ถๆๆๅฏไฝ็จ้ป่พ)
+ * **ๅฏ้:** Redux + Redux Toolkit (ๅ่ฝๅผบๅคง๏ผไฝ็ธๅฏน้๏ผ้ๅๅคงๅ้กน็ฎ)
+* **UI ็ปไปถๅบ:** Material-UI (MUI), Ant Design, ๆ่ชๅฎไน็ปไปถๅบ (ๆจ่ MUI๏ผๆๆกฃๅฎๅ๏ผ็ปไปถไธฐๅฏ)
+* **่ทฏ็ฑ็ฎก็:** React Router DOM (ๆ ๅ้ๆฉ)
+* **ๆๅปบๅทฅๅ
ท:** Vite (ๆจ่๏ผ้ๅบฆๅฟซ๏ผๆฏๆ JSX/TSX)
+* **็ฑปๅๅฎไน:** TypeScript (ๅผบ็ๆจ่๏ผๆ้ซไปฃ็ ่ดจ้ๅๅฏ็ปดๆคๆง)
+* **Tauri ๅ็ซฏ้จๅ:** ไฝฟ็จ `@tauri-apps/api` ่ฟ่ก่ฐ็จ
+
+### 2๏ธโฃ ๆ ธๅฟๆถๆๆจกๅผ
+
+* **็ปไปถๅ:** ๅฐ UI ๆๅไธบๅฏๅค็จใๅฏๆต่ฏ็ๅฐ้จไปถใ
+ * **ๅๅญ็ปไปถ:** ๆๅฐ็ UI ๅๅ
๏ผๆ้ฎใๅพๆ ็ญ๏ผใ
+ * **ๅๅญ็ปไปถ:** ็ปๅๅๅญ็ปไปถๅฝขๆ็นๅฎๅ่ฝ็ UI๏ผๆถๆฏๆฐๆณกใ่็ณปไบบๆก็ฎ็ญ๏ผใ
+ * **็ป็ป็ปไปถ:** ๅคๆ็ๅธๅฑ๏ผ็ปๅๅๅญ/ๅๅญ็ปไปถ๏ผ่ๅคฉๅ่กจใไผ่ฏ็ชๅฃใ่ฎพ็ฝฎ้ขๆฟ็ญ๏ผใ
+* **็ถๆ็ฎก็ๆถๆ:**
+ * **ๅ
จๅฑ็ถๆ (Pinia/Zustand):** ๅญๅจ้่ฆ่ทจ็ปไปถๅ
ฑไบซใๆไน
ๅ็็ถๆ๏ผ็จๆทไฟกๆฏใไผ่ฏๅ่กจใๅฝๅ้ไธญไผ่ฏใๅ
จๅฑ้็ฅใๅจ็บฟ็ถๆใ็ฆป็บฟๆถๆฏ็ญ๏ผใ
+ * **ๅฑ้จ็ถๆ (React Hooks):** ๅค็็ปไปถๅ
้จ็ถๆๅ้ป่พ๏ผๅฆๅไธช่ๅคฉ็ชๅฃ็ๆถๆฏ่พๅ
ฅใๆปๅจไฝ็ฝฎ็ญ๏ผใ
+* **ๆฐๆฎๆต:**
+ * **่ช้กถๅไธ:** ๅ
จๅฑ็ถๆๅๅ้ๅธธ็ฑไบไปถ่งฆๅ๏ผ้่ฟ `commit`/`dispatch` ๆ็ดๆฅ mutation ๆดๆฐ็ถๆใ
+ * **่ชๅบๅไธ:** ็ปไปถ่งฆๅไบไปถ๏ผๅฆ็นๅปๅ้ๆถๆฏ๏ผ๏ผ้็ฅ็ถ็ปไปถๆ็ถๆ็ฎก็ๅบ่ฟ่ก็ถๆๆดๆฐใ
+ * **ไธ Tauri ๅ็ซฏ:** React ็ปไปถ้่ฟ่ฐ็จ Pinia Store ไธญ็ๆนๆณ๏ผๆ็ดๆฅไฝฟ็จ `@tauri-apps/api`๏ผๆฅไธ Tauri ๅ็ซฏ่ฟ่ก้ไฟก๏ผๅ่ตท WebSocket ่ฟๆฅใๅ้ๆถๆฏใๆฅๆถๆถๆฏๅ่ฐ็ญ๏ผใ
+
+### 3๏ธโฃ ๆ ธๅฟๆจกๅ่ฎพ่ฎก
+
+#### โ
A. ็ถๆ็ฎก็ (Pinia/Zustand)
+
+* **`store/index.ts`:** ๅฎไนไธป่ฆ็ Store ๆจกๅใ
+ * **`user.ts`:** ็จๆทไฟกๆฏใ็ปๅฝ็ถๆใๆ้ใ
+ * **`conversations.ts`:** ไผ่ฏๅ่กจ๏ผๅ
ๅซไผ่ฏ็ๅบๆฌไฟกๆฏ๏ผๅฏนๆน็จๆท/็พค็ปใๆๅไธๆกๆถๆฏใๅจ็บฟ็ถๆ็ญ๏ผใ
+ * **`messages.ts`:** ๆถๆฏๅ
ๅฎนใๆถๆฏ็ถๆ๏ผๅทฒๅ้ใๅทฒ้่พพใๅทฒ่ฏปใๆคๅ๏ผใๆถๆฏๅทฒ่ฏปๅๆงใ
+ * **`contacts.ts`:** ่็ณปไบบๅ่กจใๅฅฝๅ็ณ่ฏทใ็พค็ป็ณ่ฏทใ
+ * **`settings.ts`:** ๅบ็จ่ฎพ็ฝฎใ็้ขไธป้ขใ้็ฅ่ฎพ็ฝฎใ
+ * **`notification.ts`:** ๅ
จๅฑ้็ฅๆถๆฏใ
+ * **`onlineStatus.ts`:** ็จๆทๅจ็บฟ/็ฆป็บฟ็ถๆใ
+ * **`syncManager.ts`:** ็ฎก็็ฆป็บฟๆถๆฏๅๆญฅ้ป่พใ
+* **`store/utils.ts`:** ๅฐ่ฃ
ไธไบ้็จ็ Pinia ๅ่ฝ๏ผๅฆๆไน
ๅๅญๅจใๅ ่ฝฝ๏ผใ
+* **`store/actions.ts`:** ๅฏ้๏ผ็จไบๅฐ่ฃ
ไธไบๅคๆ็ๆไฝๆต็จ๏ผๅฆๅ้ๆถๆฏ็ๅฎๆดๆต็จ๏ผใ
+
+#### โ
B. ๅฎๆถ้ไฟกไธๆถๆฏๅค็
+
+* **WebSocket ่ฟๆฅ:**
+ * ๅจ Tauri ็ Rust ๅ็ซฏๅปบ็ซ WebSocket ๆๅกๅจๆไฝฟ็จ็ฐๆ็ๅบ๏ผๅฆ `tokio-websocket`๏ผใ
+ * React ๅ็ซฏ้่ฟ [`@tauri-apps/api`](https://tauri.app/v1/api/js/) ่ฐ็จ Rust ๅฝๆฐๆฅๅปบ็ซๅ็ปดๆค WebSocket ่ฟๆฅใ
+ * **ๅปบ่ฎฎ:** ๅฐ WebSocket ่ฟๆฅๅฎไพๅไธป่ฆ็ๆฅๆถๆถๆฏ้ป่พๆพๅจ [`messages`](#-a-็ถๆ็ฎก็-piniazustand) Store ๆไธไธชไธ้จ็ `WebSocketManager` ็ฑปไธญ็ฎก็ใ
+* **ๆถๆฏๅค็:**
+ * **ๆฅๆถๆถๆฏ:** WebSocket ๆฅๆถๅฐๆถๆฏๅ๏ผ้่ฟ `@tauri-apps/api` ๅ่ฐๆไบไปถๆป็บฟ้็ฅ React ๅ็ซฏ๏ผๆดๆฐ `messages` Storeใ
+ * **ๅ้ๆถๆฏ:** ็จๆท็นๅปๅ้๏ผReact ็ปไปถ่ฐ็จ `messages` Store ็ๆนๆณ๏ผๆ็ดๆฅ `@tauri-apps/api`๏ผๅ้ๆถๆฏๅฐ Tauri ๅ็ซฏ๏ผTauri ๅ็ซฏ้่ฟ WebSocket ๆจ้ๅบๅปใ
+ * **ๆถๆฏ็ถๆๆดๆฐ:** ๅฎๆถๆดๆฐๆถๆฏ็โๅทฒๅ้ใๅทฒ้่พพใๅทฒ่ฏปโ็ถๆ๏ผ่ฟไบ็ถๆ้ๅธธ็ฑๆๅกๅจๆจ้ๆ้่ฟ้ฟ่ฝฎ่ฏข/็ญ่ฝฎ่ฏข่กฅๅ
ใ
+* **้ๆ:** ไฝฟ็จ [`@tauri-apps/api`](https://tauri.app/v1/api/js/) ๆไพ็ [`invoke`](https://tauri.app/v1/api/js/invoke) ๆนๆณๅ [`listen`](https://tauri.app/v1/api/js/event#listen) ๆนๆณๆฅไธ Rust ๅ็ซฏ่ฟ่กๅๅ้ไฟกใ
+
+#### โ
C. UI ็ปไปถ
+
+* **`components/atoms`:** ๅบ็ก UI ๅ่ฏญใ
+* **`components/molecules`:** ็ปๅๅๅญ็ปไปถๅฝขๆ็นๅฎๅ่ฝใ
+ * `MessageBubble.tsx`: ๆ นๆฎๆถๆฏ็ฑปๅ๏ผๆๆฌใๅพ็ใ่ฏญ้ณ็ญ๏ผๅๅ้่
ๆธฒๆๆถๆฏๆฐๆณกใ
+ * `ConversationListItem.tsx`: ไผ่ฏๅ่กจไธญ็ๅๆกไผ่ฏ้กนใ
+ * `ContactListItem.tsx`: ่็ณปไบบๅ่กจไธญ็ๅๆก่็ณปไบบ้กนใ
+ * `InputArea.tsx`: ๆถๆฏ่พๅ
ฅๆกๅๅ้ๆ้ฎใ
+ * `OnlineIndicator.tsx`: ๆพ็คบ็จๆทๅจ็บฟ็ถๆ็ๅฐ็ปไปถใ
+* **`components/organisms`:** ๅคๅ็ปไปถใ
+ * `ChatWindow.tsx`: ๆดไธช่ๅคฉ็ชๅฃ๏ผๅ
ๅซไผ่ฏๅคด้จใๆถๆฏๅ่กจใ่พๅ
ฅๅบๅใ
+ * `ContactsList.tsx`: ่็ณปไบบๅ่กจ้กต้ขใ
+ * `ConversationsList.tsx`: ไผ่ฏๅ่กจ้กต้ขใ
+ * `SettingsPage.tsx`: ่ฎพ็ฝฎ้กต้ขใ
+* **`components/templates`:** ้กต้ขๅธๅฑๆจกๆฟใ
+ * `Layout.tsx`: ๆดไฝๅบ็จๅธๅฑ๏ผไพง่พนๆ + ไธปๅ
ๅฎนๅบ๏ผใ
+
+#### โ
D. ่ทฏ็ฑ
+
+* **`src/routes`:**
+ * `Conversation.tsx`: ไผ่ฏ้กต้ขใ
+ * `Contacts.tsx`: ่็ณปไบบ้กต้ขใ
+ * `Conversations.tsx`: ไผ่ฏๅ่กจ้กต้ขใ
+ * `Settings.tsx`: ่ฎพ็ฝฎ้กต้ขใ
+ * `NotFound.tsx`: 404 ้กต้ขใ
+* ไฝฟ็จ `React Router DOM` ่ฟ่ก่ทฏ็ฑ้
็ฝฎใ
+
+#### โ
E. ไธ Tauri ็้ๆ
+
+* **`src/tauri`:**
+ * `invoke.ts`: ๅฐ่ฃ
`@tauri-apps/api` ็ `invoke` ๆนๆณ๏ผๆไพ็ฑปๅๅฎๅ
จ็่ฐ็จๆนๅผใ
+ * `listen.ts`: ๅฐ่ฃ
ไบไปถ็ๅฌใ
+ * `commands.ts`: ๅฎไน Tauri ๅ็ซฏ้่ฆ่ฐ็จ็ Rust ๅฝไปคๆฅๅฃใ
+* **้ๆๆนๅผ:**
+ 1. React ็ปไปถ้่ฆๆง่กๆไฝ๏ผๅฆๅ้ๆถๆฏ๏ผๆถ๏ผ่ฐ็จ Pinia Store ไธญ็ๆนๆณใ
+ 2. Pinia Store ็ๆนๆณๅ
้จ๏ผ้่ฟ `invoke` ่ฐ็จ Tauri ๅฝไปคใ
+ 3. Tauri Rust ๅ็ซฏๅค็ๅฝไปค๏ผๆง่กไธๅก้ป่พ๏ผๅฆ้่ฟ WebSocket ๅ้ๆถๆฏ๏ผใ
+ 4. Tauri Rust ๅ็ซฏ้่ฟไบไปถ็ณป็ป๏ผๅฆ `tokio::sync::mpsc` ๆ Tauri ็ `Event` ๆบๅถ๏ผ้็ฅๅ็ซฏ WebSocket ๆถๆฏๅทฒๅ้ใ
+ 5. ๅ็ซฏ้่ฟ `listen` ๆฅๆถ Tauri ๅๆฅ็ไบไปถ๏ผๆดๆฐ็ถๆใ
+
+#### โ
F. ๆฐๆฎๅค็ไธๅๆญฅ
+
+* **ๆถๆฏๅญๅจ:** Tauri ๅ็ซฏ๏ผRust + SQLite ๆๅ
ถไปๆฐๆฎๅบ๏ผ่ด่ดฃๅญๅจๆถๆฏใ็จๆทใไผ่ฏ็ญๆฐๆฎใ
+* **ๅ็ซฏ็ผๅญ:** ไฝฟ็จ `localStorage` ๆ IndexedDB ็ผๅญ้จๅๆฐๆฎ๏ผๅฆ็ฆป็บฟๆถๆฏใไผ่ฏๅ่กจ๏ผ๏ผๅจๅบ็จๅฏๅจๆถๅ ่ฝฝ๏ผๆๅ็ฆป็บฟไฝ้ชใ
+* **ๅๆญฅ้ป่พ:** ๅจ `syncManager` Store ไธญๅฎ็ฐ๏ผ่ด่ดฃๅฐๆฌๅฐ็ผๅญ็ๆฐๆฎๅๆญฅๅฐ Tauri ๅ็ซฏ๏ผๆๅฐๅ็ซฏ็ๆฐๆฎๅๆญฅๅฐๆฌๅฐ็ผๅญใ
+
+### 4๏ธโฃ ้ๅ่ฝๆง่่
+
+* **ๆง่ฝไผๅ:**
+ * ไฝฟ็จ `React.memo`, `useMemo`, `useCallback` ่ฟ่ก็ปไปถๅ่ฎก็ฎ็ไผๅใ
+ * ้ฟๆถๆฏๅ่กจไฝฟ็จ `react-window` ๆ `@tanstack/react-virtualized` ๅฎ็ฐ่ๆๆปๅจใ
+ * ๆๅ ่ฝฝ็ปไปถ (`React.lazy` + `Suspense`)ใ
+* **้่ฏฏๅค็:** ๅฏน WebSocket ่ฟๆฅใAPI ่ฐ็จใ็ถๆๆดๆฐ็ญ่ฟ่กๅฅๅฃฎ็้่ฏฏๅค็ๅ็จๆทๆ็คบใ
+* **ๆต่ฏ:**
+ * ๅๅ
ๆต่ฏ๏ผไฝฟ็จ Jest + React Testing Library ๆต่ฏ็ปไปถๅ Hookใ
+ * ๅญๅจๆต่ฏ๏ผไฝฟ็จ Vitest + Testing Library + `@testing-library/user-event`ใ
+ * ้ๆๆต่ฏ๏ผๆจกๆ Tauri API ่ฐ็จ๏ผๆต่ฏ็ถๆ็ฎก็ๅ UI ่กไธบใ
+* **ๅฏ่ฎฟ้ฎๆง (a11y):** ็กฎไฟ UI ็ฌฆๅ WCAG ๆ ๅใ
+* **ๅฝ้
ๅ:** ไฝฟ็จ `i18next` ็ญๅบๅฎ็ฐๅค่ฏญ่จๆฏๆใ
+* **ๆฅๅฟ:** ไฝฟ็จ `console` ๆ `logseq` (ๅ็ซฏๆฅๅฟๅบ) ่ฟ่ก่ฐ่ฏ๏ผ็ไบง็ฏๅขๅฏ้ๆๆดๅผบๅคง็ๆฅๅฟๆนๆกใ
+
+### 5๏ธโฃ ๆป็ป
+
+ไธไธชๅ
ธๅ็ Tauri + React IM ๅบ็จๅ็ซฏๆถๆ่ฎพ่ฎกๅ
ๅซไปฅไธๅฑๆฌก๏ผ
+
+```plaintext
+src/
+โโโ /components # UI ็ปไปถ (ๅๅญใๅๅญใ็ป็ปใๆจกๆฟ)
+โโโ /hooks # ่ชๅฎไน React Hooks
+โโโ /layouts # ้กต้ขๅธๅฑๆจกๆฟ
+โโโ /pages # ้กต้ข็ปไปถ (ๅฏนๅบ่ทฏ็ฑ)
+โโโ /routes # ่ทฏ็ฑ้
็ฝฎ
+โโโ /services # ไธ Tauri ๅ็ซฏ้ไฟก็ๆๅกๅฐ่ฃ
(invoke, listen)
+โโโ /stores # Pinia/Zustand ็ถๆ็ฎก็ Store
+โโโ /utils # ๅทฅๅ
ทๅฝๆฐใ็ฑปๅๅฎไน็ญ
+โโโ /assets # ้ๆ่ตๆบ
+โโโ index.tsx # ๅ
ฅๅฃๆไปถ
+โโโ router.tsx # ่ทฏ็ฑ้
็ฝฎๆไปถ
+โโโ stores/index.ts # ็ถๆ็ฎก็ๅ
ฅๅฃ
+```
+
+## ๐ ๅ
ณ้ฎ็นๅผบ่ฐ
+
+* **Tauri ๆฏๆกฅๆข:** React ๅ็ซฏๅ Rust ๅ็ซฏ้่ฟ Tauri ็ `invoke` ๅ `listen` ๆบๅถ่ฟ่กไบคไบ๏ผRust ๅค็ๆ ธๅฟ้ป่พๅ I/Oใ
+* **็ถๆ็ฎก็ๆฏๆ ธๅฟ:** IM ๅบ็จ็ถๆๅคๆ๏ผ้่ฆ้ๆฉๅ้็็ถๆ็ฎก็ๆนๆกๆฅ็ป็ปๅ็ปดๆค่ฟไบ็ถๆใ
+* **ๅฎๆถๆงๆฏๅ
ณ้ฎ:** WebSocket ๆฏ IM ๅบ็จ็ๆ ้
๏ผ้่ฆๅฆฅๅ็ฎก็่ฟๆฅๅๆถๆฏๆตใ
+* **็ปไปถๅๆๅๅฏ็ปดๆคๆง:** ๅฐ UI ๆๅๆๅฐ่ไธๆณจ็็ปไปถๆฏ่ฏๅฅฝๆถๆ็ๅบ็กใ
+* **ๆง่ฝไธๅฏๅฟฝ่ง:** ๅฐคๅ
ถๆฏๆถๆฏๅ่กจ่ฟ็งๅคงๆฐๆฎ้ๅบๆฏ๏ผ่ๆๆปๅจๆฏๅฟ
้กป่่็ใ
+
+## ๐ ๆไฝณๅฎ่ทตๅปบ่ฎฎ
+
+1. **ๅ
ๆญๅปบๅบ็กๆกๆถ:** ไฝฟ็จ Vite + React + TypeScript + Tailwind CSS (ๆ MUI) ๆญๅปบๅบ็ก้กน็ฎใ
+2. **่ฎพ่ฎก็ถๆๆจกๅ:** ๅจๅผๅง่ฏฆ็ป็ผ็ ๅ๏ผๅ
่ฎพ่ฎกๅฅฝ้่ฆๅญๅจ็ๆ ธๅฟ็ถๆๅๅ
ถ็ปๆใ
+3. **ๅฎ็ฐ Tauri ้ๆ:** ๅจๅฎๆๅบๆฌ UI ๅ๏ผๅฟซ้้ๆ Tauri๏ผๅฎ็ฐๅบๆฌ็ๅๅ็ซฏ้ไฟกใ
+4. **้ๆญฅๅฎ็ฐๅ่ฝ:** ๅ
ๅฎ็ฐๆ ธๅฟๅ่ฝ๏ผ็ปๅฝใไผ่ฏๅ่กจใๆถๆฏๆถๅ๏ผ๏ผๅ้ๆญฅๆทปๅ ๅ
ถไปๅ่ฝใ
+5. **ๆณจ้็ถๆๅๆญฅ:** ็กฎไฟๅ็ซฏ็ถๆไธ Tauri ๅ็ซฏ็ถๆ็ๅๆญฅ้ป่พๆธ
ๆฐๅฏ้ ใ
+6. **ไปฃ็ ่ง่:** ๅผบๅถๆง่ก ESLint + Prettier ่ง่ใ
+
+่ฟไธชๆถๆ่ฎพ่ฎกๆฏไธไธช่ตท็น๏ผๅ
ทไฝๅฎ็ฐ็ป่ไผๆ นๆฎ้กน็ฎ่งๆจกใๅข้ๅๅฅฝๅๅฎ้
้ๆฑ่ๅๅใ็ฅไฝ ๅผๅ้กบๅฉ๏ผ๐
\ No newline at end of file
diff --git a/crates/cherry/index.html b/crates/cherry/index.html
new file mode 100644
index 0000000..ff93803
--- /dev/null
+++ b/crates/cherry/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ Tauri + React + Typescript
+
+
+
+
+
+
+
diff --git a/crates/cherry/package-lock.json b/crates/cherry/package-lock.json
new file mode 100644
index 0000000..0c34b68
--- /dev/null
+++ b/crates/cherry/package-lock.json
@@ -0,0 +1,2929 @@
+{
+ "name": "cherry",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "cherry",
+ "version": "0.1.0",
+ "dependencies": {
+ "@heroicons/react": "^2.2.0",
+ "@reduxjs/toolkit": "^2.8.2",
+ "@tailwindcss/vite": "^4.1.10",
+ "@tauri-apps/api": "^2",
+ "@tauri-apps/plugin-opener": "^2",
+ "@types/styled-components": "^5.1.34",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-icons": "^5.5.0",
+ "react-redux": "^9.2.0",
+ "styled-components": "^6.1.19"
+ },
+ "devDependencies": {
+ "@tauri-apps/cli": "^2",
+ "@types/react": "^18.3.1",
+ "@types/react-dom": "^18.3.1",
+ "@vitejs/plugin-react": "^4.3.4",
+ "autoprefixer": "^10.4.21",
+ "postcss": "^8.5.6",
+ "tailwindcss": "^4.1.10",
+ "typescript": "~5.6.2",
+ "vite": "^6.0.3"
+ }
+ },
+ "node_modules/@ampproject/remapping": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
+ "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.27.5",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz",
+ "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.27.4",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz",
+ "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.2.0",
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.27.3",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.27.3",
+ "@babel/helpers": "^7.27.4",
+ "@babel/parser": "^7.27.4",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.27.4",
+ "@babel/types": "^7.27.3",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.27.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz",
+ "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.27.5",
+ "@babel/types": "^7.27.3",
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.27.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz",
+ "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.27.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.27.6",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz",
+ "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.27.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.27.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz",
+ "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.3"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.27.4",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz",
+ "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.27.3",
+ "@babel/parser": "^7.27.4",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.27.3",
+ "debug": "^4.3.1",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.27.6",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz",
+ "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@emotion/is-prop-valid": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz",
+ "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/memoize": "^0.8.1"
+ }
+ },
+ "node_modules/@emotion/memoize": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
+ "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/unitless": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
+ "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==",
+ "license": "MIT"
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
+ "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
+ "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
+ "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
+ "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
+ "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
+ "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
+ "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
+ "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
+ "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
+ "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
+ "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
+ "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
+ "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
+ "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
+ "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
+ "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
+ "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
+ "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
+ "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
+ "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
+ "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
+ "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
+ "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
+ "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
+ "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@heroicons/react": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
+ "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">= 16 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@isaacs/fs-minipass": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
+ "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.4"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
+ "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/set-array": "^1.2.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.25",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+ "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.8.2",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz",
+ "integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@standard-schema/utils": "^0.3.0",
+ "immer": "^10.0.3",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.1.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz",
+ "integrity": "sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.43.0.tgz",
+ "integrity": "sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.43.0.tgz",
+ "integrity": "sha512-ss4YJwRt5I63454Rpj+mXCXicakdFmKnUNxr1dLK+5rv5FJgAxnN7s31a5VchRYxCFWdmnDWKd0wbAdTr0J5EA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.43.0.tgz",
+ "integrity": "sha512-eKoL8ykZ7zz8MjgBenEF2OoTNFAPFz1/lyJ5UmmFSz5jW+7XbH1+MAgCVHy72aG59rbuQLcJeiMrP8qP5d/N0A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.43.0.tgz",
+ "integrity": "sha512-SYwXJgaBYW33Wi/q4ubN+ldWC4DzQY62S4Ll2dgfr/dbPoF50dlQwEaEHSKrQdSjC6oIe1WgzosoaNoHCdNuMg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.43.0.tgz",
+ "integrity": "sha512-SV+U5sSo0yujrjzBF7/YidieK2iF6E7MdF6EbYxNz94lA+R0wKl3SiixGyG/9Klab6uNBIqsN7j4Y/Fya7wAjQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.43.0.tgz",
+ "integrity": "sha512-J7uCsiV13L/VOeHJBo5SjasKiGxJ0g+nQTrBkAsmQBIdil3KhPnSE9GnRon4ejX1XDdsmK/l30IYLiAaQEO0Cg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.43.0.tgz",
+ "integrity": "sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.43.0.tgz",
+ "integrity": "sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.43.0.tgz",
+ "integrity": "sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.43.0.tgz",
+ "integrity": "sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.43.0.tgz",
+ "integrity": "sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.43.0.tgz",
+ "integrity": "sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.43.0.tgz",
+ "integrity": "sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.43.0.tgz",
+ "integrity": "sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.43.0.tgz",
+ "integrity": "sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.43.0.tgz",
+ "integrity": "sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.43.0.tgz",
+ "integrity": "sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.43.0.tgz",
+ "integrity": "sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.43.0.tgz",
+ "integrity": "sha512-fYCTEyzf8d+7diCw8b+asvWDCLMjsCEA8alvtAutqJOJp/wL5hs1rWSqJ1vkjgW0L2NB4bsYJrpKkiIPRR9dvw==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.43.0.tgz",
+ "integrity": "sha512-SnGhLiE5rlK0ofq8kzuDkM0g7FN1s5VYY+YSMTibP7CqShxCQvqtNxTARS4xX4PFJfHjG0ZQYX9iGzI3FQh5Aw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
+ "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/utils": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
+ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
+ "license": "MIT"
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.10.tgz",
+ "integrity": "sha512-2ACf1znY5fpRBwRhMgj9ZXvb2XZW8qs+oTfotJ2C5xR0/WNL7UHZ7zXl6s+rUqedL1mNi+0O+WQr5awGowS3PQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.3.0",
+ "enhanced-resolve": "^5.18.1",
+ "jiti": "^2.4.2",
+ "lightningcss": "1.30.1",
+ "magic-string": "^0.30.17",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.1.10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.10.tgz",
+ "integrity": "sha512-v0C43s7Pjw+B9w21htrQwuFObSkio2aV/qPx/mhrRldbqxbWJK6KizM+q7BF1/1CmuLqZqX3CeYF7s7P9fbA8Q==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^2.0.4",
+ "tar": "^7.4.3"
+ },
+ "engines": {
+ "node": ">= 10"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.1.10",
+ "@tailwindcss/oxide-darwin-arm64": "4.1.10",
+ "@tailwindcss/oxide-darwin-x64": "4.1.10",
+ "@tailwindcss/oxide-freebsd-x64": "4.1.10",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.10",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.1.10",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.1.10",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.1.10",
+ "@tailwindcss/oxide-linux-x64-musl": "4.1.10",
+ "@tailwindcss/oxide-wasm32-wasi": "4.1.10",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.1.10",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.1.10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.10.tgz",
+ "integrity": "sha512-VGLazCoRQ7rtsCzThaI1UyDu/XRYVyH4/EWiaSX6tFglE+xZB5cvtC5Omt0OQ+FfiIVP98su16jDVHDEIuH4iQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.10.tgz",
+ "integrity": "sha512-ZIFqvR1irX2yNjWJzKCqTCcHZbgkSkSkZKbRM3BPzhDL/18idA8uWCoopYA2CSDdSGFlDAxYdU2yBHwAwx8euQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.10.tgz",
+ "integrity": "sha512-eCA4zbIhWUFDXoamNztmS0MjXHSEJYlvATzWnRiTqJkcUteSjO94PoRHJy1Xbwp9bptjeIxxBHh+zBWFhttbrQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.10.tgz",
+ "integrity": "sha512-8/392Xu12R0cc93DpiJvNpJ4wYVSiciUlkiOHOSOQNH3adq9Gi/dtySK7dVQjXIOzlpSHjeCL89RUUI8/GTI6g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.10.tgz",
+ "integrity": "sha512-t9rhmLT6EqeuPT+MXhWhlRYIMSfh5LZ6kBrC4FS6/+M1yXwfCtp24UumgCWOAJVyjQwG+lYva6wWZxrfvB+NhQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.10.tgz",
+ "integrity": "sha512-3oWrlNlxLRxXejQ8zImzrVLuZ/9Z2SeKoLhtCu0hpo38hTO2iL86eFOu4sVR8cZc6n3z7eRXXqtHJECa6mFOvA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.10.tgz",
+ "integrity": "sha512-saScU0cmWvg/Ez4gUmQWr9pvY9Kssxt+Xenfx1LG7LmqjcrvBnw4r9VjkFcqmbBb7GCBwYNcZi9X3/oMda9sqQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.10.tgz",
+ "integrity": "sha512-/G3ao/ybV9YEEgAXeEg28dyH6gs1QG8tvdN9c2MNZdUXYBaIY/Gx0N6RlJzfLy/7Nkdok4kaxKPHKJUlAaoTdA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.10.tgz",
+ "integrity": "sha512-LNr7X8fTiKGRtQGOerSayc2pWJp/9ptRYAa4G+U+cjw9kJZvkopav1AQc5HHD+U364f71tZv6XamaHKgrIoVzA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.10.tgz",
+ "integrity": "sha512-d6ekQpopFQJAcIK2i7ZzWOYGZ+A6NzzvQ3ozBvWFdeyqfOZdYHU66g5yr+/HC4ipP1ZgWsqa80+ISNILk+ae/Q==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.4.3",
+ "@emnapi/runtime": "^1.4.3",
+ "@emnapi/wasi-threads": "^1.0.2",
+ "@napi-rs/wasm-runtime": "^0.2.10",
+ "@tybys/wasm-util": "^0.9.0",
+ "tslib": "^2.8.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.10.tgz",
+ "integrity": "sha512-i1Iwg9gRbwNVOCYmnigWCCgow8nDWSFmeTUU5nbNx3rqbe4p0kRbEqLwLJbYZKmSSp23g4N6rCDmm7OuPBXhDA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.10.tgz",
+ "integrity": "sha512-sGiJTjcBSfGq2DVRtaSljq5ZgZS2SDHSIfhOylkBvHVjwOsodBhnb3HdmiKkVuUGKD0I7G63abMOVaskj1KpOA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/vite": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.10.tgz",
+ "integrity": "sha512-QWnD5HDY2IADv+vYR82lOhqOlS1jSCUUAmfem52cXAhRTKxpDh3ARX8TTXJTCCO7Rv7cD2Nlekabv02bwP3a2A==",
+ "license": "MIT",
+ "dependencies": {
+ "@tailwindcss/node": "4.1.10",
+ "@tailwindcss/oxide": "4.1.10",
+ "tailwindcss": "4.1.10"
+ },
+ "peerDependencies": {
+ "vite": "^5.2.0 || ^6"
+ }
+ },
+ "node_modules/@tauri-apps/api": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.5.0.tgz",
+ "integrity": "sha512-Ldux4ip+HGAcPUmuLT8EIkk6yafl5vK0P0c0byzAKzxJh7vxelVtdPONjfgTm96PbN24yjZNESY8CKo8qniluA==",
+ "license": "Apache-2.0 OR MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/tauri"
+ }
+ },
+ "node_modules/@tauri-apps/cli": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.5.0.tgz",
+ "integrity": "sha512-rAtHqG0Gh/IWLjN2zTf3nZqYqbo81oMbqop56rGTjrlWk9pTTAjkqOjSL9XQLIMZ3RbeVjveCqqCA0s8RnLdMg==",
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "bin": {
+ "tauri": "tauri.js"
+ },
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/tauri"
+ },
+ "optionalDependencies": {
+ "@tauri-apps/cli-darwin-arm64": "2.5.0",
+ "@tauri-apps/cli-darwin-x64": "2.5.0",
+ "@tauri-apps/cli-linux-arm-gnueabihf": "2.5.0",
+ "@tauri-apps/cli-linux-arm64-gnu": "2.5.0",
+ "@tauri-apps/cli-linux-arm64-musl": "2.5.0",
+ "@tauri-apps/cli-linux-riscv64-gnu": "2.5.0",
+ "@tauri-apps/cli-linux-x64-gnu": "2.5.0",
+ "@tauri-apps/cli-linux-x64-musl": "2.5.0",
+ "@tauri-apps/cli-win32-arm64-msvc": "2.5.0",
+ "@tauri-apps/cli-win32-ia32-msvc": "2.5.0",
+ "@tauri-apps/cli-win32-x64-msvc": "2.5.0"
+ }
+ },
+ "node_modules/@tauri-apps/cli-darwin-arm64": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.5.0.tgz",
+ "integrity": "sha512-VuVAeTFq86dfpoBDNYAdtQVLbP0+2EKCHIIhkaxjeoPARR0sLpFHz2zs0PcFU76e+KAaxtEtAJAXGNUc8E1PzQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-darwin-x64": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.5.0.tgz",
+ "integrity": "sha512-hUF01sC06cZVa8+I0/VtsHOk9BbO75rd+YdtHJ48xTdcYaQ5QIwL4yZz9OR1AKBTaUYhBam8UX9Pvd5V2/4Dpw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.5.0.tgz",
+ "integrity": "sha512-LQKqttsK252LlqYyX8R02MinUsfFcy3+NZiJwHFgi5Y3+ZUIAED9cSxJkyNtuY5KMnR4RlpgWyLv4P6akN1xhg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-linux-arm64-gnu": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.5.0.tgz",
+ "integrity": "sha512-mTQufsPcpdHg5RW0zypazMo4L55EfeE5snTzrPqbLX4yCK2qalN7+rnP8O8GT06xhp6ElSP/Ku1M2MR297SByQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-linux-arm64-musl": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.0.tgz",
+ "integrity": "sha512-rQO1HhRUQqyEaal5dUVOQruTRda/TD36s9kv1hTxZiFuSq3558lsTjAcUEnMAtBcBkps20sbyTJNMT0AwYIk8Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.5.0.tgz",
+ "integrity": "sha512-7oS18FN46yDxyw1zX/AxhLAd7T3GrLj3Ai6s8hZKd9qFVzrAn36ESL7d3G05s8wEtsJf26qjXnVF4qleS3dYsA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-linux-x64-gnu": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.5.0.tgz",
+ "integrity": "sha512-SG5sFNL7VMmDBdIg3nO3EzNRT306HsiEQ0N90ILe3ZABYAVoPDO/ttpCO37ApLInTzrq/DLN+gOlC/mgZvLw1w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-linux-x64-musl": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.0.tgz",
+ "integrity": "sha512-QXDM8zp/6v05PNWju5ELsVwF0VH1n6b5pk2E6W/jFbbiwz80Vs1lACl9pv5kEHkrxBj+aWU/03JzGuIj2g3SkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-win32-arm64-msvc": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.5.0.tgz",
+ "integrity": "sha512-pFSHFK6b+o9y4Un8w0gGLwVyFTZaC3P0kQ7umRt/BLDkzD5RnQ4vBM7CF8BCU5nkwmEBUCZd7Wt3TWZxe41o6Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-win32-ia32-msvc": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.5.0.tgz",
+ "integrity": "sha512-EArv1IaRlogdLAQyGlKmEqZqm5RfHCUMhJoedWu7GtdbOMUfSAz6FMX2boE1PtEmNO4An+g188flLeVErrxEKg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-win32-x64-msvc": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.5.0.tgz",
+ "integrity": "sha512-lj43EFYbnAta8pd9JnUq87o+xRUR0odz+4rixBtTUwUgdRdwQ2V9CzFtsMu6FQKpFQ6mujRK6P1IEwhL6ADRsQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/plugin-opener": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.3.0.tgz",
+ "integrity": "sha512-yAbauwp8BCHIhhA48NN8rEf6OtfZBPCgTOCa10gmtoVCpmic5Bq+1Ba7C+NZOjogedkSiV7hAotjYnnbUVmYrw==",
+ "license": "MIT OR Apache-2.0",
+ "dependencies": {
+ "@tauri-apps/api": "^2.0.0"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.20.7",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz",
+ "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.20.7"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
+ "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/hoist-non-react-statics": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz",
+ "integrity": "sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*",
+ "hoist-non-react-statics": "^3.3.0"
+ }
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.23",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz",
+ "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@types/styled-components": {
+ "version": "5.1.34",
+ "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.34.tgz",
+ "integrity": "sha512-mmiVvwpYklFIv9E8qfxuPyIt/OuyIrn6gMOAMOFUO3WJfSrSE+sGUoa4PiZj77Ut7bKZpaa6o1fBKS/4TOEvnA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hoist-non-react-statics": "*",
+ "@types/react": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/stylis": {
+ "version": "4.2.5",
+ "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz",
+ "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.5.2",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.2.tgz",
+ "integrity": "sha512-QNVT3/Lxx99nMQWJWF7K4N6apUEuT0KlZA3mx/mVaoGj3smm/8rc8ezz15J1pcbcjDK0V15rpHetVfya08r76Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.27.4",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.11",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0"
+ }
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.21",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
+ "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.24.4",
+ "caniuse-lite": "^1.0.30001702",
+ "fraction.js": "^4.3.7",
+ "normalize-range": "^0.1.2",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.25.0",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz",
+ "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001718",
+ "electron-to-chromium": "^1.5.160",
+ "node-releases": "^2.0.19",
+ "update-browserslist-db": "^1.1.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/camelize": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
+ "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001723",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001723.tgz",
+ "integrity": "sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chownr": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
+ "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/css-color-keywords": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
+ "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/css-to-react-native": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
+ "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "camelize": "^1.0.0",
+ "css-color-keywords": "^1.0.0",
+ "postcss-value-parser": "^4.0.2"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
+ "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.168",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.168.tgz",
+ "integrity": "sha512-RUNQmFLNIWVW6+z32EJQ5+qx8ci6RGvdtDC0Ls+F89wz6I2AthpXF0w0DIrn2jpLX0/PU9ZCo+Qp7bg/EckJmA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.18.1",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
+ "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
+ "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.5",
+ "@esbuild/android-arm": "0.25.5",
+ "@esbuild/android-arm64": "0.25.5",
+ "@esbuild/android-x64": "0.25.5",
+ "@esbuild/darwin-arm64": "0.25.5",
+ "@esbuild/darwin-x64": "0.25.5",
+ "@esbuild/freebsd-arm64": "0.25.5",
+ "@esbuild/freebsd-x64": "0.25.5",
+ "@esbuild/linux-arm": "0.25.5",
+ "@esbuild/linux-arm64": "0.25.5",
+ "@esbuild/linux-ia32": "0.25.5",
+ "@esbuild/linux-loong64": "0.25.5",
+ "@esbuild/linux-mips64el": "0.25.5",
+ "@esbuild/linux-ppc64": "0.25.5",
+ "@esbuild/linux-riscv64": "0.25.5",
+ "@esbuild/linux-s390x": "0.25.5",
+ "@esbuild/linux-x64": "0.25.5",
+ "@esbuild/netbsd-arm64": "0.25.5",
+ "@esbuild/netbsd-x64": "0.25.5",
+ "@esbuild/openbsd-arm64": "0.25.5",
+ "@esbuild/openbsd-x64": "0.25.5",
+ "@esbuild/sunos-x64": "0.25.5",
+ "@esbuild/win32-arm64": "0.25.5",
+ "@esbuild/win32-ia32": "0.25.5",
+ "@esbuild/win32-x64": "0.25.5"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.4.6",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
+ "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
+ "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "patreon",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "license": "ISC"
+ },
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
+ "node_modules/immer": {
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
+ "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
+ "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
+ "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-darwin-arm64": "1.30.1",
+ "lightningcss-darwin-x64": "1.30.1",
+ "lightningcss-freebsd-x64": "1.30.1",
+ "lightningcss-linux-arm-gnueabihf": "1.30.1",
+ "lightningcss-linux-arm64-gnu": "1.30.1",
+ "lightningcss-linux-arm64-musl": "1.30.1",
+ "lightningcss-linux-x64-gnu": "1.30.1",
+ "lightningcss-linux-x64-musl": "1.30.1",
+ "lightningcss-win32-arm64-msvc": "1.30.1",
+ "lightningcss-win32-x64-msvc": "1.30.1"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz",
+ "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz",
+ "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz",
+ "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz",
+ "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz",
+ "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz",
+ "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz",
+ "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz",
+ "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz",
+ "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz",
+ "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.17",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+ "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/minizlib": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
+ "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
+ "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
+ "license": "MIT",
+ "bin": {
+ "mkdirp": "dist/cjs/src/bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
+ "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-range": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "license": "MIT"
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-icons": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
+ "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
+ "node_modules/react-redux": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
+ "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25 || ^19",
+ "react": "^18.0 || ^19",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+ "license": "MIT"
+ },
+ "node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
+ "node_modules/reselect": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
+ "license": "MIT"
+ },
+ "node_modules/rollup": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.43.0.tgz",
+ "integrity": "sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.7"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.43.0",
+ "@rollup/rollup-android-arm64": "4.43.0",
+ "@rollup/rollup-darwin-arm64": "4.43.0",
+ "@rollup/rollup-darwin-x64": "4.43.0",
+ "@rollup/rollup-freebsd-arm64": "4.43.0",
+ "@rollup/rollup-freebsd-x64": "4.43.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.43.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.43.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.43.0",
+ "@rollup/rollup-linux-arm64-musl": "4.43.0",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.43.0",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.43.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.43.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.43.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.43.0",
+ "@rollup/rollup-linux-x64-gnu": "4.43.0",
+ "@rollup/rollup-linux-x64-musl": "4.43.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.43.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.43.0",
+ "@rollup/rollup-win32-x64-msvc": "4.43.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/shallowequal": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
+ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==",
+ "license": "MIT"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/styled-components": {
+ "version": "6.1.19",
+ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.19.tgz",
+ "integrity": "sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/is-prop-valid": "1.2.2",
+ "@emotion/unitless": "0.8.1",
+ "@types/stylis": "4.2.5",
+ "css-to-react-native": "3.2.0",
+ "csstype": "3.1.3",
+ "postcss": "8.4.49",
+ "shallowequal": "1.1.0",
+ "stylis": "4.3.2",
+ "tslib": "2.6.2"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/styled-components"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0",
+ "react-dom": ">= 16.8.0"
+ }
+ },
+ "node_modules/styled-components/node_modules/postcss": {
+ "version": "8.4.49",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
+ "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.7",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/stylis": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz",
+ "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==",
+ "license": "MIT"
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.10.tgz",
+ "integrity": "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==",
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
+ "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tar": {
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
+ "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
+ "license": "ISC",
+ "dependencies": {
+ "@isaacs/fs-minipass": "^4.0.0",
+ "chownr": "^3.0.0",
+ "minipass": "^7.1.2",
+ "minizlib": "^3.0.1",
+ "mkdirp": "^3.0.1",
+ "yallist": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tar/node_modules/yallist": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
+ "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.14",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
+ "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
+ "license": "0BSD"
+ },
+ "node_modules/typescript": {
+ "version": "5.6.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
+ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
+ "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
+ "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.3.5",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
+ "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ }
+ }
+}
diff --git a/crates/cherry/package.json b/crates/cherry/package.json
new file mode 100644
index 0000000..0e61a6a
--- /dev/null
+++ b/crates/cherry/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "cherry",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "tauri": "tauri"
+ },
+ "dependencies": {
+ "@heroicons/react": "^2.2.0",
+ "@reduxjs/toolkit": "^2.8.2",
+ "@tailwindcss/vite": "^4.1.10",
+ "@tauri-apps/api": "^2",
+ "@tauri-apps/plugin-opener": "^2",
+ "@types/styled-components": "^5.1.34",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-icons": "^5.5.0",
+ "react-redux": "^9.2.0",
+ "styled-components": "^6.1.19"
+ },
+ "devDependencies": {
+ "@tauri-apps/cli": "^2",
+ "@types/react": "^18.3.1",
+ "@types/react-dom": "^18.3.1",
+ "@vitejs/plugin-react": "^4.3.4",
+ "autoprefixer": "^10.4.21",
+ "postcss": "^8.5.6",
+ "tailwindcss": "^4.1.10",
+ "typescript": "~5.6.2",
+ "vite": "^6.0.3"
+ }
+}
diff --git a/crates/cherry/public/tauri.svg b/crates/cherry/public/tauri.svg
new file mode 100644
index 0000000..31b62c9
--- /dev/null
+++ b/crates/cherry/public/tauri.svg
@@ -0,0 +1,6 @@
+
diff --git a/crates/cherry/public/vite.svg b/crates/cherry/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/crates/cherry/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/crates/cherry/src-tauri/.env b/crates/cherry/src-tauri/.env
new file mode 100644
index 0000000..47b1b66
--- /dev/null
+++ b/crates/cherry/src-tauri/.env
@@ -0,0 +1 @@
+DATABASE_URL=sqlite:C:\\Users\\wangqin.fu\\workspace\\streamstore-rs\\crates\\cherry\\src-tauri\\sqlite.db
\ No newline at end of file
diff --git a/crates/cherry/src-tauri/.gitignore b/crates/cherry/src-tauri/.gitignore
new file mode 100644
index 0000000..3ef5717
--- /dev/null
+++ b/crates/cherry/src-tauri/.gitignore
@@ -0,0 +1,8 @@
+# Generated by Cargo
+# will have compiled files and executables
+/target/
+
+# Generated by Tauri
+# will have schema files for capabilities auto-completion
+/gen/schemas
+.env
\ No newline at end of file
diff --git a/crates/cherry/src-tauri/.sqlx/query-407a8e754b2fc29b2cf3b1396e5457bab0322121aeff3ef3493657377827765d.json b/crates/cherry/src-tauri/.sqlx/query-407a8e754b2fc29b2cf3b1396e5457bab0322121aeff3ef3493657377827765d.json
new file mode 100644
index 0000000..ab3bbf0
--- /dev/null
+++ b/crates/cherry/src-tauri/.sqlx/query-407a8e754b2fc29b2cf3b1396e5457bab0322121aeff3ef3493657377827765d.json
@@ -0,0 +1,44 @@
+{
+ "db_name": "SQLite",
+ "query": "SELECT id, username, display_name, avatar_path, status FROM users WHERE id = ?",
+ "describe": {
+ "columns": [
+ {
+ "name": "id",
+ "ordinal": 0,
+ "type_info": "Integer"
+ },
+ {
+ "name": "username",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "display_name",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "avatar_path",
+ "ordinal": 3,
+ "type_info": "Text"
+ },
+ {
+ "name": "status",
+ "ordinal": 4,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ true,
+ false
+ ]
+ },
+ "hash": "407a8e754b2fc29b2cf3b1396e5457bab0322121aeff3ef3493657377827765d"
+}
diff --git a/crates/cherry/src-tauri/.sqlx/query-fa6380c0c9d937e30be8c9368e95992726ae930b17f39cdd4765d5d183e7f6a0.json b/crates/cherry/src-tauri/.sqlx/query-fa6380c0c9d937e30be8c9368e95992726ae930b17f39cdd4765d5d183e7f6a0.json
new file mode 100644
index 0000000..8e5dd2b
--- /dev/null
+++ b/crates/cherry/src-tauri/.sqlx/query-fa6380c0c9d937e30be8c9368e95992726ae930b17f39cdd4765d5d183e7f6a0.json
@@ -0,0 +1,80 @@
+{
+ "db_name": "SQLite",
+ "query": "SELECT id, user_id, contact_id, relationship_type, nickname, status, last_seen, notes, is_verified, is_blocked, created_at FROM contacts",
+ "describe": {
+ "columns": [
+ {
+ "name": "id",
+ "ordinal": 0,
+ "type_info": "Integer"
+ },
+ {
+ "name": "user_id",
+ "ordinal": 1,
+ "type_info": "Integer"
+ },
+ {
+ "name": "contact_id",
+ "ordinal": 2,
+ "type_info": "Integer"
+ },
+ {
+ "name": "relationship_type",
+ "ordinal": 3,
+ "type_info": "Text"
+ },
+ {
+ "name": "nickname",
+ "ordinal": 4,
+ "type_info": "Text"
+ },
+ {
+ "name": "status",
+ "ordinal": 5,
+ "type_info": "Text"
+ },
+ {
+ "name": "last_seen",
+ "ordinal": 6,
+ "type_info": "Datetime"
+ },
+ {
+ "name": "notes",
+ "ordinal": 7,
+ "type_info": "Text"
+ },
+ {
+ "name": "is_verified",
+ "ordinal": 8,
+ "type_info": "Bool"
+ },
+ {
+ "name": "is_blocked",
+ "ordinal": 9,
+ "type_info": "Bool"
+ },
+ {
+ "name": "created_at",
+ "ordinal": 10,
+ "type_info": "Datetime"
+ }
+ ],
+ "parameters": {
+ "Right": 0
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "hash": "fa6380c0c9d937e30be8c9368e95992726ae930b17f39cdd4765d5d183e7f6a0"
+}
diff --git a/crates/cherry/src-tauri/Cargo.toml b/crates/cherry/src-tauri/Cargo.toml
new file mode 100644
index 0000000..2926e39
--- /dev/null
+++ b/crates/cherry/src-tauri/Cargo.toml
@@ -0,0 +1,40 @@
+[package]
+name = "cherry"
+version = "0.1.0"
+description = "A Tauri App"
+authors = ["you"]
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[lib]
+# The `_lib` suffix may seem redundant but it is necessary
+# to make the lib name unique and wouldn't conflict with the bin name.
+# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
+name = "cherry_lib"
+crate-type = ["staticlib", "cdylib", "rlib"]
+
+[build-dependencies]
+tauri-build = { version = "2", features = [] }
+
+[dependencies]
+cherrycore = { path = "../../cherrycore" }
+reqwest = { version = "0.12", features = ["json"] }
+tokio = { version = "1", features = ["full"] }
+tauri = { version = "2", features = [] }
+tauri-plugin-opener = "2"
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+dotenvy = "0.15.7"
+chrono = { version = "0.4.41", features = ["serde"] }
+anyhow = "1.0.98"
+sqlx = { version = "0.8.6", features = [
+ "sqlite",
+ "runtime-tokio",
+ "tls-native-tls",
+ "chrono",
+] }
+async-tungstenite = { version = "0.29.1", features = ["tokio-runtime"] }
+futures-util = "0.3"
+log = "0.4.27"
+uuid = "1.17.0"
diff --git a/crates/cherry/src-tauri/build.rs b/crates/cherry/src-tauri/build.rs
new file mode 100644
index 0000000..d860e1e
--- /dev/null
+++ b/crates/cherry/src-tauri/build.rs
@@ -0,0 +1,3 @@
+fn main() {
+ tauri_build::build()
+}
diff --git a/crates/cherry/src-tauri/capabilities/default.json b/crates/cherry/src-tauri/capabilities/default.json
new file mode 100644
index 0000000..4cdbf49
--- /dev/null
+++ b/crates/cherry/src-tauri/capabilities/default.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "../gen/schemas/desktop-schema.json",
+ "identifier": "default",
+ "description": "Capability for the main window",
+ "windows": ["main"],
+ "permissions": [
+ "core:default",
+ "opener:default"
+ ]
+}
diff --git a/crates/cherry/src-tauri/diesel.toml b/crates/cherry/src-tauri/diesel.toml
new file mode 100644
index 0000000..bb1d1f7
--- /dev/null
+++ b/crates/cherry/src-tauri/diesel.toml
@@ -0,0 +1,9 @@
+# For documentation on how to configure this file,
+# see https://diesel.rs/guides/configuring-diesel-cli
+
+[print_schema]
+file = "src/db/schema.rs"
+custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]
+
+[migrations_directory]
+dir = "migrations"
diff --git a/crates/cherry/src-tauri/icons/128x128.png b/crates/cherry/src-tauri/icons/128x128.png
new file mode 100644
index 0000000..6be5e50
Binary files /dev/null and b/crates/cherry/src-tauri/icons/128x128.png differ
diff --git a/crates/cherry/src-tauri/icons/128x128@2x.png b/crates/cherry/src-tauri/icons/128x128@2x.png
new file mode 100644
index 0000000..e81bece
Binary files /dev/null and b/crates/cherry/src-tauri/icons/128x128@2x.png differ
diff --git a/crates/cherry/src-tauri/icons/32x32.png b/crates/cherry/src-tauri/icons/32x32.png
new file mode 100644
index 0000000..a437dd5
Binary files /dev/null and b/crates/cherry/src-tauri/icons/32x32.png differ
diff --git a/crates/cherry/src-tauri/icons/Square107x107Logo.png b/crates/cherry/src-tauri/icons/Square107x107Logo.png
new file mode 100644
index 0000000..0ca4f27
Binary files /dev/null and b/crates/cherry/src-tauri/icons/Square107x107Logo.png differ
diff --git a/crates/cherry/src-tauri/icons/Square142x142Logo.png b/crates/cherry/src-tauri/icons/Square142x142Logo.png
new file mode 100644
index 0000000..b81f820
Binary files /dev/null and b/crates/cherry/src-tauri/icons/Square142x142Logo.png differ
diff --git a/crates/cherry/src-tauri/icons/Square150x150Logo.png b/crates/cherry/src-tauri/icons/Square150x150Logo.png
new file mode 100644
index 0000000..624c7bf
Binary files /dev/null and b/crates/cherry/src-tauri/icons/Square150x150Logo.png differ
diff --git a/crates/cherry/src-tauri/icons/Square284x284Logo.png b/crates/cherry/src-tauri/icons/Square284x284Logo.png
new file mode 100644
index 0000000..c021d2b
Binary files /dev/null and b/crates/cherry/src-tauri/icons/Square284x284Logo.png differ
diff --git a/crates/cherry/src-tauri/icons/Square30x30Logo.png b/crates/cherry/src-tauri/icons/Square30x30Logo.png
new file mode 100644
index 0000000..6219700
Binary files /dev/null and b/crates/cherry/src-tauri/icons/Square30x30Logo.png differ
diff --git a/crates/cherry/src-tauri/icons/Square310x310Logo.png b/crates/cherry/src-tauri/icons/Square310x310Logo.png
new file mode 100644
index 0000000..f9bc048
Binary files /dev/null and b/crates/cherry/src-tauri/icons/Square310x310Logo.png differ
diff --git a/crates/cherry/src-tauri/icons/Square44x44Logo.png b/crates/cherry/src-tauri/icons/Square44x44Logo.png
new file mode 100644
index 0000000..d5fbfb2
Binary files /dev/null and b/crates/cherry/src-tauri/icons/Square44x44Logo.png differ
diff --git a/crates/cherry/src-tauri/icons/Square71x71Logo.png b/crates/cherry/src-tauri/icons/Square71x71Logo.png
new file mode 100644
index 0000000..63440d7
Binary files /dev/null and b/crates/cherry/src-tauri/icons/Square71x71Logo.png differ
diff --git a/crates/cherry/src-tauri/icons/Square89x89Logo.png b/crates/cherry/src-tauri/icons/Square89x89Logo.png
new file mode 100644
index 0000000..f3f705a
Binary files /dev/null and b/crates/cherry/src-tauri/icons/Square89x89Logo.png differ
diff --git a/crates/cherry/src-tauri/icons/StoreLogo.png b/crates/cherry/src-tauri/icons/StoreLogo.png
new file mode 100644
index 0000000..4556388
Binary files /dev/null and b/crates/cherry/src-tauri/icons/StoreLogo.png differ
diff --git a/crates/cherry/src-tauri/icons/icon.icns b/crates/cherry/src-tauri/icons/icon.icns
new file mode 100644
index 0000000..12a5bce
Binary files /dev/null and b/crates/cherry/src-tauri/icons/icon.icns differ
diff --git a/crates/cherry/src-tauri/icons/icon.ico b/crates/cherry/src-tauri/icons/icon.ico
new file mode 100644
index 0000000..b3636e4
Binary files /dev/null and b/crates/cherry/src-tauri/icons/icon.ico differ
diff --git a/crates/cherry/src-tauri/icons/icon.png b/crates/cherry/src-tauri/icons/icon.png
new file mode 100644
index 0000000..e1cd261
Binary files /dev/null and b/crates/cherry/src-tauri/icons/icon.png differ
diff --git a/crates/cherry/src-tauri/migrations/20250619095126_initial.down.sql b/crates/cherry/src-tauri/migrations/20250619095126_initial.down.sql
new file mode 100644
index 0000000..e69de29
diff --git a/crates/cherry/src-tauri/migrations/20250619095126_initial.up.sql b/crates/cherry/src-tauri/migrations/20250619095126_initial.up.sql
new file mode 100644
index 0000000..00807f1
--- /dev/null
+++ b/crates/cherry/src-tauri/migrations/20250619095126_initial.up.sql
@@ -0,0 +1,161 @@
+-- Add migration script here
+CREATE TABLE
+ messages (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ conversation_id INTEGER NOT NULL,
+ sender_id INTEGER NOT NULL REFERENCES users (id),
+ content TEXT NOT NULL,
+ type TEXT NOT NULL, --'text','image','voice','video','file','location','contact','system','encrypted_text'
+ status TEXT NOT NULL CHECK (status IN ('sent', 'delivered', 'read')),
+ timestamp TEXT NOT NULL,
+ reaction TEXT,
+ reply_to INTEGER,
+ media_path TEXT
+ );
+
+CREATE TABLE
+ offline_messages (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ conversation_id INTEGER NOT NULL,
+ sender_id INTEGER NOT NULL,
+ content TEXT NOT NULL,
+ timestamp TEXT NOT NULL,
+ is_sent BOOLEAN DEFAULT FALSE
+ );
+
+CREATE TABLE
+ contacts (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ user_id INTEGER NOT NULL,
+ contact_id INTEGER NOT NULL,
+ relationship_type TEXT DEFAULT 'friend', -- friend, family, colleague
+ nickname TEXT,
+ status TEXT, -- online, offline, etc.
+ last_seen TEXT,
+ notes TEXT,
+ is_verified BOOLEAN DEFAULT 0,
+ is_blocked BOOLEAN DEFAULT 0,
+ created_at TEXT NOT NULL
+ );
+
+CREATE TABLE
+ friend_requests (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ from_user_id INTEGER NOT NULL, -- ๅ่ตท็ณ่ฏท็็จๆทID
+ to_user_id INTEGER NOT NULL, -- ๆฅๅ็ณ่ฏท็็จๆทID
+ content TEXT, -- ็ณ่ฏทๅ
ๅฎน๏ผๅฏ้
+ status TEXT NOT NULL DEFAULT 'pending', -- ็ถๆ๏ผpending, accepted, rejected
+ created_at TEXT NOT NULL
+ );
+
+CREATE TABLE
+ group_requests (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ group_id INTEGER NOT NULL, -- ๅค้ฎ๏ผๆๅGroup.id
+ user_id INTEGER NOT NULL, -- ็ณ่ฏท็จๆทID
+ content TEXT, -- ็ณ่ฏทๅ
ๅฎน๏ผๅฏ้
+ status TEXT NOT NULL DEFAULT 'pending', -- ็ถๆ๏ผpending, accepted, rejected
+ created_at TEXT NOT NULL
+ );
+
+CREATE TABLE
+ users (
+ id INTEGER PRIMARY KEY NOT NULL,
+ username TEXT NOT NULL UNIQUE,
+ display_name TEXT NOT NULL,
+ avatar_path TEXT,
+ last_login TEXT,
+ registration_date TEXT NOT NULL,
+ status TEXT NOT NULL DEFAULT 'online', -- online, offline, busy, away
+ last_active TEXT
+ );
+
+CREATE TABLE
+ conversations (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ type TEXT NOT NULL CHECK (type IN ('private', 'group')),
+ user_id INTEGER NOT NULL,
+ other_user_id INTEGER,
+ group_id INTEGER,
+ stream_id INTEGER NOT NULL,
+ last_message_id INTEGER,
+ unread_count INTEGER DEFAULT 0,
+ is_pinned BOOLEAN DEFAULT 0,
+ created_at TEXT NOT NULL
+ );
+
+CREATE TABLE
+ groups (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ name TEXT NOT NULL,
+ description TEXT,
+ creator_id INTEGER NOT NULL,
+ avatar_path TEXT,
+ created_at TEXT NOT NULL,
+ is_encrypted BOOLEAN DEFAULT 0,
+ encryption_key TEXT,
+ visibility TEXT DEFAULT 'public', -- public, private, secret
+ member_count INTEGER DEFAULT 0,
+ last_active TEXT
+ );
+
+CREATE TABLE
+ group_members (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ group_id INTEGER NOT NULL,
+ user_id INTEGER NOT NULL,
+ role TEXT DEFAULT 'member', -- member, admin, owner
+ joined_at TEXT NOT NULL,
+ is_muted BOOLEAN DEFAULT 0,
+ mute_until TEXT,
+ is_banned BOOLEAN DEFAULT 0
+ );
+
+CREATE TABLE
+ files (
+ id TEXT PRIMARY KEY NOT NULL,
+ sender_id INTEGER NOT NULL,
+ conversation_id INTEGER,
+ type TEXT NOT NULL CHECK (
+ type IN ('image', 'document', 'audio', 'video', 'other')
+ ),
+ name TEXT NOT NULL,
+ size INTEGER NOT NULL,
+ path TEXT NOT NULL,
+ uploaded_at TEXT NOT NULL
+ );
+
+CREATE TABLE
+ settings (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ key TEXT NOT NULL UNIQUE,
+ value TEXT NOT NULL,
+ category TEXT DEFAULT 'general'
+ );
+
+CREATE TABLE
+ notifications (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ sender_id INTEGER,
+ conversation_id INTEGER,
+ user_id INTEGER NOT NULL,
+ type TEXT NOT NULL CHECK (type IN ('message', 'system', 'event')),
+ content TEXT NOT NULL,
+ is_read BOOLEAN DEFAULT FALSE,
+ timestamp TEXT NOT NULL,
+ expires_at TEXT
+ );
+
+CREATE INDEX idx_conversation_last_message ON conversations (last_message_id);
+
+CREATE INDEX idx_message_timestamp ON messages (timestamp);
+
+CREATE INDEX idx_user_last_login ON users (last_login);
+
+CREATE INDEX idx_contact_status ON contacts (status);
+
+CREATE INDEX idx_group_member ON group_members (group_id, role);
+
+CREATE INDEX idx_offline_message ON offline_messages (conversation_id, is_sent);
+
+CREATE INDEX idx_notification ON notifications (user_id, is_read, timestamp);
\ No newline at end of file
diff --git a/crates/cherry/src-tauri/src/client/cherry.rs b/crates/cherry/src-tauri/src/client/cherry.rs
new file mode 100644
index 0000000..d7b88f6
--- /dev/null
+++ b/crates/cherry/src-tauri/src/client/cherry.rs
@@ -0,0 +1,111 @@
+use std::time::Duration;
+
+use crate::db::models::{Contact, User};
+use anyhow::Result;
+use cherrycore::types::{LoginRequest, LoginResponse, ListStreamRequest, ListStreamResponse};
+// Use the Conversation type from cherrycore
+use cherrycore::types::Conversation;
+use reqwest::header::HeaderMap;
+use reqwest::header::HeaderValue;
+use serde::{Deserialize, Serialize};
+use uuid::Uuid;
+
+struct CherryClientImpl {
+ options: CherryClientOptions,
+ cherry_client: reqwest::Client,
+ base_headers: HeaderMap,
+}
+
+trait CherryClient {
+ async fn new(options: CherryClientOptions) -> Self;
+ async fn contact_list_all(&self) -> Result>;
+ async fn user_get_by_id(&self, id: u64) -> Result;
+ async fn conversation_list_all(&self) -> Result>;
+ async fn login_request(server_url: String, req: LoginRequest) -> Result;
+ async fn stream_list_all(&self, uuid: Uuid) -> Result;
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct CherryClientOptions {
+ cherry_server: String,
+ user_id: u64,
+ jwt_token: String,
+}
+
+impl CherryClient for CherryClientImpl {
+ async fn new(options: CherryClientOptions) -> Self {
+ let mut headers = HeaderMap::new();
+ headers.insert(
+ "Authorization",
+ HeaderValue::from_str(&format!("Bearer {}", options.jwt_token)).unwrap(),
+ );
+ Self {
+ options,
+ cherry_client: reqwest::Client::builder()
+ .pool_idle_timeout(Duration::from_secs(10))
+ .pool_max_idle_per_host(3)
+ .connect_timeout(Duration::from_secs(10))
+ .connection_verbose(true)
+ .build()
+ .unwrap(),
+ base_headers: headers,
+ }
+ }
+
+ async fn contact_list_all(&self) -> Result> {
+ let url = format!("{}/api/v1/contacts", self.options.cherry_server);
+
+ let resp = self
+ .cherry_client
+ .get(url)
+ .headers(self.base_headers.clone())
+ .send()
+ .await?;
+ let body = resp.json::>().await?;
+ Ok(body)
+ }
+
+ async fn user_get_by_id(&self, id: u64) -> Result {
+ let url = format!("{}/api/v1/users/{}", self.options.cherry_server, id);
+ let resp = self
+ .cherry_client
+ .get(url)
+ .headers(self.base_headers.clone())
+ .send()
+ .await?;
+ let body = resp.json::().await?;
+ Ok(body)
+ }
+
+ async fn conversation_list_all(&self) -> Result> {
+ let url = format!("{}/api/v1/conversations", self.options.cherry_server);
+ let resp = self
+ .cherry_client
+ .get(url)
+ .headers(self.base_headers.clone())
+ .send()
+ .await?;
+ let body = resp.json::>().await?;
+ Ok(body)
+ }
+
+ async fn login_request(server_url: String, req: LoginRequest) -> Result {
+ let url = format!("{}/api/v1/login", server_url);
+ let resp = reqwest::Client::new().post(url).json(&req).send().await?;
+ let body = resp.json::().await?;
+ Ok(body)
+ }
+
+ async fn stream_list_all(&self, uuid: Uuid) -> Result {
+ let url = format!("{}/api/v1/streams", self.options.cherry_server);
+ let resp = self
+ .cherry_client
+ .get(url)
+ .headers(self.base_headers.clone())
+ .query(&ListStreamRequest { user_id: uuid })
+ .send()
+ .await?;
+ let body = resp.json::().await?;
+ Ok(body)
+ }
+}
diff --git a/crates/cherry/src-tauri/src/client/mod.rs b/crates/cherry/src-tauri/src/client/mod.rs
new file mode 100644
index 0000000..c8184be
--- /dev/null
+++ b/crates/cherry/src-tauri/src/client/mod.rs
@@ -0,0 +1,2 @@
+pub mod cherry;
+pub mod stream;
\ No newline at end of file
diff --git a/crates/cherry/src-tauri/src/client/stream.rs b/crates/cherry/src-tauri/src/client/stream.rs
new file mode 100644
index 0000000..c67123b
--- /dev/null
+++ b/crates/cherry/src-tauri/src/client/stream.rs
@@ -0,0 +1,104 @@
+use std::{sync::Arc, time::Duration};
+
+use anyhow::Result;
+use async_tungstenite::{tokio::ConnectStream, tungstenite::Message, WebSocketStream};
+use cherrycore::types::{
+ StreamAppendRequest, StreamAppendResponse, StreamReadRequest, StreamReadResponse,
+};
+use futures_util::{SinkExt, StreamExt};
+use tokio::select;
+
+pub struct StreamClient {
+ stream_server_url: String,
+ client: reqwest::Client,
+}
+
+impl StreamClient {
+ pub fn new(stream_server_url: String) -> Self {
+ Self {
+ stream_server_url,
+ client: reqwest::Client::builder()
+ .pool_idle_timeout(Duration::from_secs(10))
+ .pool_max_idle_per_host(3)
+ .connect_timeout(Duration::from_secs(10))
+ .connection_verbose(true)
+ .build()
+ .unwrap(),
+ }
+ }
+
+ pub async fn append_stream(
+ &self,
+ stream_id: u64,
+ data: Vec,
+ ) -> Result {
+ let url = format!("{}/api/v1/stream/append", self.stream_server_url);
+ let request = StreamAppendRequest {
+ stream_id,
+ data: Some(data),
+ };
+
+ let resp = self.client.post(url).json(&request).send().await?;
+ let response = resp.json::().await?;
+ Ok(response)
+ }
+}
+
+pub async fn open_stream(
+ stream_server_ws_url: String,
+) -> Result<(
+ tokio::sync::mpsc::Sender,
+ tokio::sync::mpsc::Receiver,
+)> {
+ let url = format!("{}/api/v1/stream/read", stream_server_ws_url);
+ let (mut ws_stream, _) = async_tungstenite::tokio::connect_async(url).await?;
+
+ let (tx, msg_rx) = tokio::sync::mpsc::channel(100);
+ let (req_tx, mut req_rx) = tokio::sync::mpsc::channel::(100);
+ tokio::spawn(async move {
+ loop {
+ select! {
+ Some(msg) = ws_stream.next() => {
+ match msg {
+ Ok(Message::Text(text)) => {
+ let msg: StreamReadResponse = serde_json::from_str(&text).unwrap();
+ if let Err(e) = tx.send(msg).await {
+ log::error!("send stream read response error: {:?}", e);
+ break;
+ }
+ }
+ Ok(Message::Ping(ping)) => {
+ log::info!("ping: {:?}", ping);
+ }
+ Ok(Message::Pong(pong)) => {
+ log::info!("pong: {:?}", pong);
+ }
+ Ok(Message::Close(close)) => {
+ log::info!("close: {:?}", close);
+ }
+ Ok(Message::Binary(binary)) => {
+ log::info!("binary: {:?}", binary);
+ }
+ Ok(Message::Frame(frame)) => {
+ log::info!("frame: {:?}", frame);
+ }
+ Err(e) => {
+ log::error!("error: {:?}", e);
+ break;
+ }
+ }
+ }
+
+ Some(msg) = req_rx.recv() => {
+ let msg = Message::Text(serde_json::to_string(&msg).unwrap().into());
+ if let Err(e) = ws_stream.send(msg).await {
+ log::error!("send stream read request error: {:?}", e);
+ break;
+ }
+ }
+ }
+ }
+ });
+
+ Ok((req_tx, msg_rx))
+}
diff --git a/crates/cherry/src-tauri/src/db/api.rs b/crates/cherry/src-tauri/src/db/api.rs
new file mode 100644
index 0000000..2cc3a00
--- /dev/null
+++ b/crates/cherry/src-tauri/src/db/api.rs
@@ -0,0 +1,7 @@
+use std::env;
+
+
+use dotenvy::dotenv;
+
+use crate::db::{models::*};
+
diff --git a/crates/cherry/src-tauri/src/db/mod.rs b/crates/cherry/src-tauri/src/db/mod.rs
new file mode 100644
index 0000000..a46e213
--- /dev/null
+++ b/crates/cherry/src-tauri/src/db/mod.rs
@@ -0,0 +1,2 @@
+pub mod repo;
+pub mod models;
\ No newline at end of file
diff --git a/crates/cherry/src-tauri/src/db/models.rs b/crates/cherry/src-tauri/src/db/models.rs
new file mode 100644
index 0000000..e4993ad
--- /dev/null
+++ b/crates/cherry/src-tauri/src/db/models.rs
@@ -0,0 +1,301 @@
+use chrono::{DateTime, NaiveDateTime, Utc};
+use serde::{Deserialize, Serialize};
+
+// CREATE TABLE messages (
+// id INTEGER PRIMARY KEY AUTOINCREMENT,
+// conversation_id INTEGER NOT NULL,
+// sender_id INTEGER NOT NULL,
+// content TEXT NOT NULL,
+// type TEXT NOT NULL CHECK(type IN ('text', 'image', 'voice', 'video', 'file', 'location', 'contact', 'system', 'encrypted_text')),
+// status TEXT NOT NULL CHECK(status IN ('sent', 'delivered', 'read')),
+// timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+// reaction TEXT,
+// reply_to INTEGER,
+// media_path TEXT,
+// FOREIGN KEY (conversation_id) REFERENCES conversations(id),
+// FOREIGN KEY (sender_id) REFERENCES users(id),
+// FOREIGN KEY (reply_to) REFERENCES messages(id)
+// );
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Message {
+ pub id: i32,
+ pub conversation_id: i32,
+ pub sender_id: i32,
+ pub content: String,
+ pub type_: String,
+ pub status: String,
+ pub timestamp: chrono::NaiveDateTime,
+ pub reaction: Option,
+ pub reply_to: Option,
+ pub media_path: Option,
+}
+
+// CREATE TABLE offline_messages (
+// id INTEGER PRIMARY KEY AUTOINCREMENT,
+// conversation_id INTEGER NOT NULL,
+// sender_id INTEGER NOT NULL,
+// content TEXT NOT NULL,
+// timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+// is_sent BOOLEAN DEFAULT FALSE,
+// FOREIGN KEY (conversation_id) REFERENCES conversations(id),
+// FOREIGN KEY (sender_id) REFERENCES users(id)
+// );
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct OfflineMessage {
+ pub id: i32,
+ pub conversation_id: i32,
+ pub sender_id: i32,
+ pub content: String,
+ pub timestamp: chrono::NaiveDateTime,
+ pub is_sent: bool,
+}
+
+// CREATE TABLE
+// contacts (
+// id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+// user_id INTEGER NOT NULL,
+// contact_id INTEGER NOT NULL,
+// relationship_type TEXT DEFAULT 'friend', -- friend, family, colleague
+// nickname TEXT,
+// status TEXT, -- online, offline, etc.
+// last_seen TIMESTAMP WITH TIME ZONE,
+// notes TEXT,
+// is_verified BOOLEAN DEFAULT 0,
+// is_blocked BOOLEAN DEFAULT 0,
+// created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
+// );
+
+
+#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
+pub struct Contact {
+ pub id: i64,
+ pub user_id: i64,
+ pub contact_id: i64,
+ pub relationship_type: Option,
+ pub nickname: Option,
+ pub status: Option,
+ pub last_seen: Option,
+ pub notes: Option,
+ pub is_verified: Option,
+ pub is_blocked: Option,
+ pub created_at: String,
+}
+
+// CREATE TABLE friend_requests (
+// id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+// from_user_id INTEGER NOT NULL, -- ๅ่ตท็ณ่ฏท็็จๆทID
+// to_user_id INTEGER NOT NULL, -- ๆฅๅ็ณ่ฏท็็จๆทID
+// content TEXT, -- ็ณ่ฏทๅ
ๅฎน๏ผๅฏ้
+// status TEXT NOT NULL DEFAULT 'pending', -- ็ถๆ๏ผpending, accepted, rejected
+// created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+// );
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct FriendRequest {
+ pub id: i32,
+ pub from_user_id: i32,
+ pub to_user_id: i32,
+ pub content: Option,
+ pub status: String,
+ pub created_at: chrono::NaiveDateTime,
+}
+
+// CREATE TABLE group_requests (
+// id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+// group_id INTEGER NOT NULL, -- ๅค้ฎ๏ผๆๅGroup.id
+// user_id INTEGER NOT NULL, -- ็ณ่ฏท็จๆทID
+// content TEXT, -- ็ณ่ฏทๅ
ๅฎน๏ผๅฏ้
+// status TEXT NOT NULL DEFAULT 'pending', -- ็ถๆ๏ผpending, accepted, rejected
+// created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+// );
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct GroupRequest {
+ pub id: i32,
+ pub group_id: i32,
+ pub user_id: i32,
+ pub content: Option,
+ pub status: String,
+ pub created_at: chrono::NaiveDateTime,
+}
+
+// CREATE TABLE users (
+// id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+// username TEXT NOT NULL UNIQUE,
+// password_hash TEXT NOT NULL,
+// display_name TEXT NOT NULL,
+// avatar_path TEXT,
+// last_login TIMESTAMP,
+// registration_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+// status TEXT DEFAULT 'online', -- online, offline, busy, away
+// last_active TIMESTAMP
+// );
+
+#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
+
+pub struct User {
+ pub id: i32,
+ pub username: String,
+ pub display_name: String,
+ pub avatar_path: Option,
+ pub status: String, // online, offline, busy, away
+}
+
+// CREATE TABLE conversations (
+// id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+// type TEXT NOT NULL CHECK(type IN ('private', 'group')),
+// user_id INTEGER NOT NULL,
+// other_user_id INTEGER,
+// group_id INTEGER,
+// last_message_id INTEGER,
+// unread_count INTEGER DEFAULT 0,
+// is_pinned BOOLEAN DEFAULT 0,
+// created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+// FOREIGN KEY (user_id) REFERENCES users(id),
+// FOREIGN KEY (other_user_id) REFERENCES users(id),
+// FOREIGN KEY (group_id) REFERENCES groups(id)
+// );
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Conversation {
+ pub id: i32,
+ pub type_: String,
+ pub user_id: i32,
+ pub other_user_id: Option,
+ pub group_id: Option,
+ pub last_message_id: Option,
+ pub unread_count: i32,
+ pub is_pinned: bool,
+ pub created_at: chrono::NaiveDateTime,
+}
+
+// CREATE TABLE groups (
+// id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+// name TEXT NOT NULL,
+// description TEXT,
+// creator_id INTEGER NOT NULL,
+// avatar_path TEXT,
+// created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+// is_encrypted BOOLEAN DEFAULT 0,
+// encryption_key TEXT,
+// visibility TEXT DEFAULT 'public', -- public, private, secret
+// member_count INTEGER DEFAULT 0,
+// last_active TIMESTAMP,
+// FOREIGN KEY (creator_id) REFERENCES users(id)
+// );
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Group {
+ pub id: i32,
+ pub name: String,
+ pub description: Option,
+ pub creator_id: i32,
+ pub avatar_path: Option,
+ pub created_at: chrono::NaiveDateTime,
+ pub is_encrypted: bool,
+ pub encryption_key: Option,
+ pub visibility: String, // public, private, secret
+ pub member_count: i32,
+ pub last_active: Option,
+}
+
+// CREATE TABLE group_members (
+// id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+// group_id INTEGER NOT NULL,
+// user_id INTEGER NOT NULL,
+// role TEXT DEFAULT 'member', -- member, admin, owner
+// joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+// is_online BOOLEAN DEFAULT 0,
+// last_seen TIMESTAMP,
+// is_muted BOOLEAN DEFAULT 0,
+// mute_until TIMESTAMP,
+// is_banned BOOLEAN DEFAULT 0,
+// FOREIGN KEY (group_id) REFERENCES groups(id),
+// FOREIGN KEY (user_id) REFERENCES users(id)
+// );
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct GroupMember {
+ pub id: i32,
+ pub group_id: i32,
+ pub user_id: i32,
+ pub role: String,
+ pub joined_at: chrono::NaiveDateTime,
+ pub is_online: bool,
+ pub last_seen: Option,
+ pub is_muted: bool,
+ pub mute_until: Option,
+ pub is_banned: bool,
+}
+
+// CREATE TABLE files (
+// id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+// sender_id INTEGER NOT NULL,
+// conversation_id INTEGER,
+// type TEXT NOT NULL CHECK(type IN ('image', 'document', 'audio', 'video', 'other')),
+// name TEXT NOT NULL,
+// size INTEGER NOT NULL,
+// path TEXT NOT NULL,
+// uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+// FOREIGN KEY (sender_id) REFERENCES users(id),
+// FOREIGN KEY (conversation_id) REFERENCES conversations(id)
+// );
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct File {
+ pub id: i32,
+ pub sender_id: i32,
+ pub conversation_id: Option,
+ pub type_: String,
+ pub name: String,
+ pub size: i32,
+ pub path: String,
+ pub uploaded_at: chrono::NaiveDateTime,
+}
+
+// CREATE TABLE settings (
+// id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+// user_id INTEGER NOT NULL UNIQUE,
+// key TEXT NOT NULL,
+// value TEXT NOT NULL,
+// category TEXT DEFAULT 'general',
+// FOREIGN KEY (user_id) REFERENCES users(id)
+// );
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Setting {
+ pub id: i32,
+ pub key: String,
+ pub value: String,
+ pub category: String,
+}
+
+// CREATE TABLE notifications (
+// id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+// sender_id INTEGER,
+// conversation_id INTEGER,
+// user_id INTEGER NOT NULL,
+// type TEXT NOT NULL CHECK(type IN ('message', 'system', 'event')),
+// content TEXT NOT NULL,
+// is_read BOOLEAN DEFAULT FALSE,
+// timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+// expires_at TIMESTAMP,
+// FOREIGN KEY (sender_id) REFERENCES users(id),
+// FOREIGN KEY (conversation_id) REFERENCES conversations(id),
+// FOREIGN KEY (user_id) REFERENCES users(id)
+// );
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Notification {
+ pub id: i32,
+ pub sender_id: Option,
+ pub conversation_id: Option,
+ pub user_id: i32,
+ pub type_: String,
+ pub content: String,
+ pub is_read: bool,
+ pub timestamp: chrono::NaiveDateTime,
+ pub expires_at: Option,
+}
diff --git a/crates/cherry/src-tauri/src/db/repo.rs b/crates/cherry/src-tauri/src/db/repo.rs
new file mode 100644
index 0000000..5ab92e0
--- /dev/null
+++ b/crates/cherry/src-tauri/src/db/repo.rs
@@ -0,0 +1,56 @@
+use super::models::{Contact, User};
+use sqlx::{
+ query_as,
+ sqlite::{SqlitePool, SqlitePoolOptions},
+};
+
+pub struct Repo {
+ sqlx_pool: Option,
+}
+
+impl Repo {
+ pub async fn new(db_url: &str) -> Self {
+ let pool = SqlitePoolOptions::new()
+ .max_connections(5)
+ .connect(db_url)
+ .await
+ .ok();
+ Self { sqlx_pool: pool }
+ }
+
+ pub async fn user_get_by_id(&self, id: i32) -> Result {
+ if let Some(pool) = &self.sqlx_pool {
+ let user = query_as::<_, User>(
+ "SELECT id, username, display_name, avatar_path, status FROM users WHERE id = ?",
+ )
+ .bind(id)
+ .fetch_one(pool)
+ .await?;
+
+ Ok(user)
+ } else {
+ // Return a mock user for development when database is not available
+ Ok(User {
+ id,
+ username: format!("user_{}", id),
+ display_name: format!("User {}", id),
+ avatar_path: None,
+ status: "offline".to_string(),
+ })
+ }
+ }
+
+ pub async fn contact_list_all(&self) -> Result, sqlx::Error> {
+ if let Some(pool) = &self.sqlx_pool {
+ // Use query_as with explicit type instead of the macro to avoid compile-time checks
+ let contacts = sqlx::query_as::<_, Contact>("SELECT * FROM contacts")
+ .fetch_all(pool)
+ .await?;
+
+ Ok(contacts)
+ } else {
+ // Return empty contacts list when database is not available
+ Ok(Vec::new())
+ }
+ }
+}
diff --git a/crates/cherry/src-tauri/src/lib.rs b/crates/cherry/src-tauri/src/lib.rs
new file mode 100644
index 0000000..ef64b76
--- /dev/null
+++ b/crates/cherry/src-tauri/src/lib.rs
@@ -0,0 +1,91 @@
+// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
+
+mod client;
+mod db;
+use anyhow::Result;
+
+use serde::Serialize;
+use tauri::State;
+
+use crate::client::cherry::CherryClientOptions;
+use crate::db::{models::{Contact, User}, repo::Repo};
+
+
+
+#[derive(Debug, Serialize)]
+struct CommandError {
+ message: String,
+}
+
+impl From for CommandError {
+ fn from(err: anyhow::Error) -> Self {
+ CommandError {
+ message: err.to_string(),
+ }
+ }
+}
+
+impl From for CommandError {
+ fn from(err: sqlx::Error) -> Self {
+ CommandError {
+ message: err.to_string(),
+ }
+ }
+}
+
+#[derive(Debug, Serialize)]
+pub struct Options {
+ // stream data pull/push server
+ stream_server: String,
+ // chat server
+ cherry_server: String,
+
+ user_id: u64,
+}
+
+struct AppState {
+ repo: Repo,
+}
+
+#[tauri::command]
+async fn cmd_contact_list_all(state: State<'_, AppState>) -> Result, CommandError> {
+ let contacts = state
+ .repo
+ .contact_list_all()
+ .await
+ .map_err(CommandError::from)?;
+ Ok(contacts)
+}
+
+#[tauri::command]
+async fn cmd_user_get_by_id(id: i32, state: State<'_, AppState>) -> Result {
+ let user = state
+ .repo
+ .user_get_by_id(id)
+ .await
+ .map_err(CommandError::from)?;
+ Ok(user)
+}
+
+#[tauri::command]
+fn greet(name: &str) -> String {
+ format!("Hello, {}! You've been greeted from Rust!", name)
+}
+
+#[cfg_attr(mobile, tauri::mobile_entry_point)]
+pub async fn run() {
+ let db_path = std::env::current_dir().unwrap().join("sqlite.db");
+ println!("db_path: {}", db_path.to_str().unwrap());
+ tauri::Builder::default()
+ .plugin(tauri_plugin_opener::init())
+ .manage(AppState {
+ repo: Repo::new(db_path.to_str().unwrap()).await,
+ })
+ .invoke_handler(tauri::generate_handler![
+ greet,
+ cmd_user_get_by_id,
+ cmd_contact_list_all
+ ])
+ .run(tauri::generate_context!())
+ .expect("error while running tauri application");
+}
diff --git a/crates/cherry/src-tauri/src/main.rs b/crates/cherry/src-tauri/src/main.rs
new file mode 100644
index 0000000..a9eaa10
--- /dev/null
+++ b/crates/cherry/src-tauri/src/main.rs
@@ -0,0 +1,7 @@
+// Prevents additional console window on Windows in release, DO NOT REMOVE!!
+#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+
+#[tokio::main]
+async fn main() {
+ cherry_lib::run().await;
+}
diff --git a/crates/cherry/src-tauri/tauri.conf.json b/crates/cherry/src-tauri/tauri.conf.json
new file mode 100644
index 0000000..ee0477d
--- /dev/null
+++ b/crates/cherry/src-tauri/tauri.conf.json
@@ -0,0 +1,38 @@
+{
+ "$schema": "https://schema.tauri.app/config/2",
+ "productName": "cherry",
+ "version": "0.1.0",
+ "identifier": "cherry.chat",
+ "build": {
+ "beforeDevCommand": "npm run dev",
+ "devUrl": "http://localhost:1420",
+ "beforeBuildCommand": "npm run build",
+ "frontendDist": "../dist"
+ },
+ "app": {
+ "windows": [
+ {
+ "title": "cherry",
+ "width": 1000,
+ "height": 800,
+ "decorations": false,
+ "transparent": false,
+ "titleBarStyle": "Overlay"
+ }
+ ],
+ "security": {
+ "csp": null
+ }
+ },
+ "bundle": {
+ "active": true,
+ "targets": "all",
+ "icon": [
+ "icons/32x32.png",
+ "icons/128x128.png",
+ "icons/128x128@2x.png",
+ "icons/icon.icns",
+ "icons/icon.ico"
+ ]
+ }
+}
diff --git a/crates/cherry/src/App.css b/crates/cherry/src/App.css
new file mode 100644
index 0000000..4a70154
--- /dev/null
+++ b/crates/cherry/src/App.css
@@ -0,0 +1,38 @@
+@import "tailwindcss";
+
+/* ็งป้คๆๆๅ
็ด ็้ป่ฎค็ฆ็นๆ ทๅผ */
+*:focus {
+ outline: none !important;
+}
+
+/* ็งป้ค็นๅฎๅ
็ด ็็ฆ็นๆ ทๅผ */
+button:focus,
+input:focus,
+select:focus,
+textarea:focus,
+div:focus,
+span:focus {
+ outline: none !important;
+ border-color: rgba(134, 239, 172, 0.3) !important;
+}
+
+/* ็งป้ค้พๆฅ็็ฆ็นๆ ทๅผ */
+a:focus {
+ outline: none !important;
+}
+
+/* ็กฎไฟ Tauri ็ชๅฃๆฒกๆ้ป่ฎค่พนๆก */
+body {
+ -webkit-user-select: none;
+ -webkit-app-region: no-drag;
+}
+
+/* ็งป้คๆ้ฎ็้ป่ฎคๆ ทๅผ */
+button {
+ -webkit-app-region: no-drag;
+}
+
+/* ็งป้ค่พๅ
ฅๆก็้ป่ฎคๆ ทๅผ */
+input, textarea, select {
+ -webkit-app-region: no-drag;
+}
\ No newline at end of file
diff --git a/crates/cherry/src/App.tsx b/crates/cherry/src/App.tsx
new file mode 100644
index 0000000..0e9180f
--- /dev/null
+++ b/crates/cherry/src/App.tsx
@@ -0,0 +1,678 @@
+// src/App.tsx
+import React, { useState } from 'react';
+import styled from 'styled-components';
+import Sidebar from './components/Sidebar';
+import ChatHeader from './components/ChatHeader';
+import MessageList from './components/MessageList';
+import MessageInput from './components/MessageInput';
+import WindowControls from './components/WindowControls';
+import SettingsPage from './components/settings/SettingsPage';
+import ContactPage from './components/ContactPage';
+import { Conversation, Message, User } from './types/types';
+import { useWindowSize } from './hooks/useWindowsSize.ts';
+
+// ==================== Styled Components ====================
+const AppContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ background: linear-gradient(135deg,rgba(134, 239, 172, 0.1) 0%,rgba(147, 197, 253, 0.05) 100%);
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background:
+ radial-gradient(circle at 20% 80%, rgba(134, 239, 172, 0.2) 0%, transparent 50%),
+ radial-gradient(circle at 80% 20%, rgba(147, 197, 253, 0.15) 0%, transparent 50%),
+ radial-gradient(circle at 40% 40%, rgba(167, 243, 208, 0.1) 0%, transparent 50%);
+ pointer-events: none;
+ }
+
+ /* ๅ
จๅฑๆปๅจๆกๆ ทๅผ */
+ * {
+ scrollbar-width: none; /* Firefox */
+ -ms-overflow-style: none; /* IE and Edge */
+ }
+
+ *::-webkit-scrollbar {
+ display: none; /* Chrome, Safari, Opera */
+ }
+
+ /* ็งป้ค้ป่ฎค็ฆ็นๆ ทๅผ */
+ *:focus {
+ outline: none !important;
+ }
+
+ /* ็งป้คๆ้ฎๅ่พๅ
ฅๆก็้ป่ฎค่พนๆก */
+ button:focus,
+ input:focus,
+ select:focus,
+ textarea:focus {
+ outline: none !important;
+ border-color: rgba(134, 239, 172, 0.3) !important;
+ }
+
+ /* ็งป้ค้พๆฅ็้ป่ฎค็ฆ็นๆ ทๅผ */
+ a:focus {
+ outline: none !important;
+ }
+`;
+
+const TitleBar = styled.div`
+ height: 64px;
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(249, 250, 251, 0.95));
+ backdrop-filter: blur(20px);
+ border-bottom: 1px solid rgba(134, 239, 172, 0.2);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 16px;
+ -webkit-app-region: drag;
+ user-select: none;
+ position: relative;
+ z-index: 1000;
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+
+ /* ็กฎไฟๆ้ฎไธๅฏๆๆฝ */
+ button, input, select, textarea {
+ -webkit-app-region: no-drag;
+ }
+`;
+
+const LeftSection = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ -webkit-app-region: no-drag;
+`;
+
+const AvatarContainer = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ background: rgba(255, 255, 255, 0.8);
+ padding: 0.5rem 0.75rem;
+ border-radius: 12px;
+ border: 1px solid rgba(229, 231, 235, 0.5);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+ }
+`;
+
+const AvatarWrapper = styled.div`
+ position: relative;
+ width: 36px;
+ height: 36px;
+`;
+
+const Avatar = styled.img`
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ object-fit: cover;
+ border: 2px solid rgba(255, 255, 255, 0.9);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: scale(1.05);
+ border-color: rgba(134, 239, 172, 0.3);
+ }
+`;
+
+const StatusIndicator = styled.div<{ status: string }>`
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ border: 2px solid rgba(255, 255, 255, 0.9);
+ background: ${({ status }) => {
+ const colors: Record = {
+ online: 'linear-gradient(135deg, #10b981, #059669)',
+ offline: 'linear-gradient(135deg, #6b7280, #4b5563)',
+ away: 'linear-gradient(135deg, #f59e0b, #d97706)',
+ dnd: 'linear-gradient(135deg, #ef4444, #dc2626)',
+ busy: 'linear-gradient(135deg, #ef4444, #dc2626)',
+ };
+ return colors[status] || colors.offline;
+ }};
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
+ z-index: 10;
+`;
+
+const UserInfo = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 0.125rem;
+`;
+
+const UserName = styled.p`
+ font-weight: 600;
+ font-size: 0.875rem;
+ margin: 0;
+ letter-spacing: 0.025em;
+ color: #1f2937;
+ background: linear-gradient(135deg, #6366f1, #8b5cf6);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+`;
+
+const UserStatus = styled.p<{ status: string }>`
+ font-size: 0.75rem;
+ text-transform: capitalize;
+ margin: 0;
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+ color: #6b7280;
+ font-weight: 500;
+
+ &::before {
+ content: '';
+ display: inline-block;
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: ${({ status }) => {
+ const colors: Record = {
+ online: 'linear-gradient(135deg, #10b981, #059669)',
+ offline: 'linear-gradient(135deg, #6b7280, #4b5563)',
+ away: 'linear-gradient(135deg, #f59e0b, #d97706)',
+ dnd: 'linear-gradient(135deg, #ef4444, #dc2626)',
+ busy: 'linear-gradient(135deg, #ef4444, #dc2626)',
+ };
+ return colors[status] || colors.offline;
+ }};
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+ }
+`;
+
+const CenterSection = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex: 1;
+ -webkit-app-region: drag;
+`;
+
+const TitleText = styled.div`
+ color: rgba(34, 197, 94, 0.8);
+ font-size: 16px;
+ font-weight: 600;
+ text-align: center;
+`;
+
+const RightSection = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ -webkit-app-region: no-drag;
+`;
+
+const ActionContainer = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ background: rgba(255, 255, 255, 0.8);
+ padding: 0.5rem 0.75rem;
+ border-radius: 12px;
+ border: 1px solid rgba(229, 231, 235, 0.5);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+ margin-right: 0.5rem;
+`;
+
+const ActionButton = styled.button`
+ background: linear-gradient(135deg, rgba(134, 239, 172, 0.1), rgba(147, 197, 253, 0.1));
+ border: 1px solid rgba(134, 239, 172, 0.2);
+ width: 32px;
+ height: 32px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ position: relative;
+
+ &:hover {
+ background: linear-gradient(135deg, rgba(134, 239, 172, 0.2), rgba(147, 197, 253, 0.2));
+ transform: translateY(-1px) scale(1.05);
+ box-shadow: 0 4px 12px rgba(134, 239, 172, 0.3);
+
+ svg {
+ transform: scale(1.1);
+ fill: #22c55e;
+ }
+ }
+
+ &:active {
+ transform: scale(0.95);
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: -4px;
+ right: -4px;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #ef4444, #dc2626);
+ opacity: 0;
+ transition: all 0.3s ease;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
+ }
+
+ &.has-notification::after {
+ opacity: 1;
+ }
+`;
+
+const ActionIcon = styled.svg`
+ width: 16px;
+ height: 16px;
+ fill: #6b7280;
+ transition: all 0.3s ease;
+`;
+
+const MainContent = styled.div`
+ flex: 1;
+ display: flex;
+ overflow: hidden;
+ position: relative;
+ z-index: 1;
+`;
+
+const ChatArea = styled.div`
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(20px);
+ border-radius: 20px;
+ margin: 16px;
+ margin-left: 8px;
+ box-shadow:
+ 0 8px 32px rgba(0, 0, 0, 0.1),
+ 0 4px 16px rgba(0, 0, 0, 0.05);
+ border: 1px solid rgba(134, 239, 172, 0.2);
+ overflow: hidden;
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow:
+ 0 12px 40px rgba(0, 0, 0, 0.15),
+ 0 6px 20px rgba(0, 0, 0, 0.1);
+ border-color: rgba(134, 239, 172, 0.3);
+ }
+`;
+
+// ๆจกๆ็ชๅฃๆ ทๅผ
+const ModalOverlay = styled.div`
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.3);
+ backdrop-filter: blur(4px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 2000;
+ animation: fadeIn 0.3s ease-out;
+
+ @keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+ }
+`;
+
+const SettingsModalContainer = styled.div`
+ width: 90vw;
+ height: 90vh;
+ max-width: 800px;
+ max-height: 600px;
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(20px);
+ border-radius: 24px;
+ border: 1px solid rgba(134, 239, 172, 0.2);
+ box-shadow:
+ 0 20px 60px rgba(0, 0, 0, 0.3),
+ 0 8px 32px rgba(0, 0, 0, 0.2);
+ overflow: hidden;
+ position: relative;
+ animation: slideIn 0.3s ease-out;
+
+ @keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: scale(0.9) translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+ }
+`;
+
+const ContactModalContainer = styled.div`
+ width: 90vw;
+ height: 90vh;
+ max-width: 1000px;
+ max-height: 1000px;
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(20px);
+ border-radius: 24px;
+ border: 1px solid rgba(134, 239, 172, 0.2);
+ box-shadow:
+ 0 20px 60px rgba(0, 0, 0, 0.3),
+ 0 8px 32px rgba(0, 0, 0, 0.2);
+ overflow: hidden;
+ position: relative;
+ animation: slideIn 0.3s ease-out;
+
+ @keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: scale(0.9) translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+ }
+`;
+
+const App: React.FC = () => {
+ const { width } = useWindowSize();
+ const isMobile = width < 768;
+ const [selectedConversation, setSelectedConversation] = useState(null);
+ const [conversations] = useState(mockConversations);
+ const [messages, setMessages] = useState(mockMessages);
+
+ // ๆจกๆ็ชๅฃ็ถๆ
+ const [isSettingsOpen, setIsSettingsOpen] = useState(false);
+ const [isContactModalOpen, setIsContactModalOpen] = useState(false);
+
+ const currentUser: User = {
+ id: 'user1',
+ name: 'John Doe',
+ avatar: 'https://randomuser.me/api/portraits/men/1.jpg',
+ status: 'online'
+ };
+
+ const handleSelectConversation = (id: string) => {
+ setSelectedConversation(id);
+ };
+
+ const handleSendMessage = (content: string) => {
+ const newMessage: Message = {
+ id: `msg${messages.length + 1}`,
+ userId: currentUser.id,
+ content,
+ timestamp: new Date().toISOString(),
+ isOwn: true,
+ status: 'sent'
+ };
+
+ setMessages([...messages, newMessage]);
+
+ // Simulate message delivery
+ setTimeout(() => {
+ setMessages(prev => prev.map(msg =>
+ msg.id === newMessage.id ? { ...msg, status: 'delivered' } : msg
+ ));
+ }, 1000);
+
+ // Simulate message read
+ setTimeout(() => {
+ setMessages(prev => prev.map(msg =>
+ msg.id === newMessage.id ? { ...msg, status: 'read' } : msg
+ ));
+ }, 3000);
+ };
+
+ const selectedConvo = conversations.find(c => c.id === selectedConversation) || conversations[0];
+
+ return (
+
+
+
+
+
+
+
+
+
+ {currentUser.name}
+ {currentUser.status}
+
+
+
+
+
+ Cherry Chat
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {(isMobile && !selectedConversation) || !isMobile ? (
+ setIsSettingsOpen(true)}
+ onOpenContacts={() => setIsContactModalOpen(true)}
+ />
+ ) : null}
+
+ {(selectedConversation || !isMobile) && (
+
+
+
+
+
+ )}
+
+
+ {/* Settings Modal */}
+ {isSettingsOpen && (
+ setIsSettingsOpen(false)}>
+ e.stopPropagation()}>
+
+
+
+ )}
+
+ {/* Contact Modal */}
+ {isContactModalOpen && (
+ setIsContactModalOpen(false)}>
+ e.stopPropagation()}>
+
+
+
+ )}
+
+ );
+};
+
+// Mock data
+const mockUsers: User[] = [
+ {
+ id: 'user2',
+ name: 'Jane Smith',
+ avatar: 'https://randomuser.me/api/portraits/women/2.jpg',
+ status: 'online'
+ },
+ {
+ id: 'user3',
+ name: 'Alex Johnson',
+ avatar: 'https://cdn3.iconfinder.com/data/icons/diversity-avatars/64/doctor-man-asian-128.png',
+ status: 'away'
+ },
+ {
+ id: 'user4',
+ name: 'Sarah Williams',
+ avatar: 'https://randomuser.me/api/portraits/women/4.jpg',
+ status: 'offline'
+ }
+];
+
+
+const mockMessages: Message[] = [
+ {
+ id: 'msg1',
+ userId: 'user2',
+ content: 'Hey, how are you doing?',
+ timestamp: '2023-05-15T10:30:00Z'
+ },
+ {
+ id: 'msg2',
+ userId: 'user1',
+ content: "I'm good, thanks! How about you?",
+ timestamp: '2023-05-15T10:32:00Z',
+ isOwn: true,
+ status: 'read'
+ },
+ {
+ id: 'msg3',
+ userId: 'user2',
+ content: "I'm doing great! Just finished that project we were talking about.",
+ timestamp: '2023-05-15T10:33:00Z'
+ },
+ {
+ id: 'msg4',
+ userId: 'user1',
+ content: "That's awesome! Can you share some screenshots?",
+ timestamp: '2023-05-15T10:35:00Z',
+ isOwn: true,
+ status: 'read'
+ },
+ {
+ id: 'msg5',
+ userId: 'user2',
+ content: "Sure, I'll send them over shortly.",
+ timestamp: '2023-05-15T10:36:00Z'
+ },
+ {
+ id: 'msg6',
+ userId: 'user1',
+ content: "I'll send them over shortly.",
+ timestamp: '2023-05-15T10:37:00Z',
+ isOwn: true,
+ status: 'read'
+ },
+ {
+ id: 'msg7',
+ userId: 'user2',
+ content: "I'll send them over shortly.",
+ timestamp: '2023-05-15T10:37:00Z',
+ },
+ {
+ id: 'msg8',
+ userId: 'user1',
+ content: "I'll send them over shortly.",
+ timestamp: '2023-05-15T10:37:00Z',
+ isOwn: true,
+ status: 'read'
+ }
+];
+
+
+const mockConversations: Conversation[] = [
+ {
+ id: 'convo1',
+ name: 'Jane Smith',
+ avatar: 'https://randomuser.me/api/portraits/women/2.jpg',
+ participants: [mockUsers[0]],
+ mentions: 0,
+ type: 'direct',
+ messages: mockMessages,
+ lastMessage: {
+ id: 'msg1',
+ userId: 'user2',
+ content: 'Hey, how are you doing?',
+ timestamp: '2023-05-15T10:30:00Z'
+ },
+ unreadCount: 0
+ },
+ {
+ id: 'convo2',
+ name: 'Group Chat',
+ avatar: 'https://cdn.dribbble.com/users/7179533/avatars/normal/f422e09d77e62217dc67c457f3cf1807.jpg',
+ mentions: 1,
+ type: 'group',
+ messages: mockMessages,
+ participants: mockUsers,
+ lastMessage: {
+ id: 'msg2',
+ userId: 'user3',
+ content: 'Meeting at 3pm tomorrow',
+ timestamp: '2023-05-15T09:15:00Z'
+ },
+ unreadCount: 2
+ },
+ {
+ id: 'convo3',
+ name: 'Alex Johnson',
+ avatar: 'https://randomuser.me/api/portraits/men/3.jpg',
+ participants: [mockUsers[1]],
+ mentions: 0,
+ type: 'direct',
+ messages: mockMessages,
+ lastMessage: {
+ id: 'msg3',
+ userId: 'user1',
+ content: 'Thanks for the help!',
+ timestamp: '2023-05-14T16:45:00Z'
+ },
+ unreadCount: 0
+ }
+];
+
+export default App;
diff --git a/crates/cherry/src/assets/react.svg b/crates/cherry/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/crates/cherry/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/crates/cherry/src/components/ChatHeader.tsx b/crates/cherry/src/components/ChatHeader.tsx
new file mode 100644
index 0000000..0123667
--- /dev/null
+++ b/crates/cherry/src/components/ChatHeader.tsx
@@ -0,0 +1,203 @@
+// src/components/ChatHeader.tsx
+import React from 'react';
+import styled from 'styled-components';
+import { Conversation } from '../types/types';
+
+interface ChatHeaderProps {
+ conversation: Conversation;
+}
+
+// ==================== Styled Components ====================
+const HeaderContainer = styled.div`
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(249, 250, 251, 0.95));
+ backdrop-filter: blur(20px);
+ padding: 0.5rem 1.5rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-bottom: 1px solid rgba(229, 231, 235, 0.5);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+`;
+
+const UserInfo = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 1.5rem;
+ background: rgba(255, 255, 255, 0.8);
+ padding: 0.15rem 1rem;
+ border-radius: 16px;
+ border: 1px solid rgba(229, 231, 235, 0.5);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
+ }
+`;
+
+const AvatarContainer = styled.div`
+ position: relative;
+`;
+
+const Avatar = styled.img`
+ width: 3rem;
+ height: 3rem;
+ border-radius: 50%;
+ object-fit: cover;
+ border: 3px solid rgba(255, 255, 255, 0.9);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: scale(1.05);
+ border-color: rgba(99, 102, 241, 0.3);
+ }
+`;
+
+const OnlineIndicator = styled.div`
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ width: 1rem;
+ height: 1rem;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #10b981, #059669);
+ border: 3px solid rgba(255, 255, 255, 0.9);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ animation: pulse 2s infinite;
+
+ @keyframes pulse {
+ 0% { transform: scale(1); opacity: 1; }
+ 50% { transform: scale(1.1); opacity: 0.7; }
+ 100% { transform: scale(1); opacity: 1; }
+ }
+`;
+
+const UserDetails = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+`;
+
+const UserName = styled.h2`
+ font-weight: 700;
+ font-size: 1.125rem;
+ margin: 0;
+ color: #1f2937;
+ background: linear-gradient(135deg, #6366f1, #8b5cf6);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ letter-spacing: 0.025em;
+`;
+
+const OnlineStatus = styled.p`
+ font-size: 0.875rem;
+ margin: 0;
+ color: #6b7280;
+ font-weight: 500;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+
+ &::before {
+ content: '';
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #10b981, #059669);
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+ }
+`;
+
+const ActionButtons = styled.div`
+ display: flex;
+ gap: 0.75rem;
+ background: rgba(255, 255, 255, 0.8);
+ padding: 0.75rem 1rem;
+ border-radius: 16px;
+ border: 1px solid rgba(229, 231, 235, 0.5);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+`;
+
+const IconButton = styled.button`
+ color: #6b7280;
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.1), rgba(139, 92, 246, 0.1));
+ border: 1px solid rgba(99, 102, 241, 0.2);
+ cursor: pointer;
+ padding: 0.5rem;
+ border-radius: 10px;
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &:hover {
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(139, 92, 246, 0.2));
+ transform: translateY(-2px) scale(1.05);
+ box-shadow: 0 8px 20px rgba(99, 102, 241, 0.3);
+ color: #6366f1;
+
+ svg {
+ transform: scale(1.1);
+ }
+ }
+
+ &:active {
+ transform: scale(0.95);
+ }
+
+ svg {
+ width: 1.25rem;
+ height: 1.25rem;
+ transition: all 0.3s ease;
+ }
+`;
+
+// ==================== Component Implementation ====================
+const ChatHeader: React.FC = ({ conversation }) => {
+ const onlineCount = conversation.participants.filter(p => p.status === 'online').length;
+
+ return (
+
+
+
+
+ {onlineCount > 0 && }
+
+
+
+ {conversation.name}
+ {onlineCount} online
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ChatHeader;
diff --git a/crates/cherry/src/components/ContactList.tsx b/crates/cherry/src/components/ContactList.tsx
new file mode 100644
index 0000000..698f0a4
--- /dev/null
+++ b/crates/cherry/src/components/ContactList.tsx
@@ -0,0 +1,221 @@
+// src/components/ContactList.tsx
+import React from 'react';
+import styled from 'styled-components';
+import { Conversation } from '../types/types';
+
+interface ContactListProps {
+ conversations: Conversation[];
+ onSelectConversation: (id: string) => void;
+}
+
+// ==================== Styled Components ====================
+const ContactListContainer = styled.div`
+ padding: 0.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+`;
+
+const ContactItem = styled.div`
+ padding: 1rem;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ gap: 0.65rem;
+ border-radius: 10px;
+ background: rgba(102, 162, 172, 0.15);
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.1);
+ transform: translateY(-2px);
+ box-shadow:
+ 0 4px 20px rgba(0, 0, 0, 0.1),
+ 0 2px 10px rgba(0, 0, 0, 0.05);
+ border-color: rgba(255, 255, 255, 0.2);
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+`;
+
+const AvatarContainer = styled.div`
+ position: relative;
+ flex-shrink: 0;
+`;
+
+const Avatar = styled.img`
+ width: 2.5rem;
+ height: 2.5rem;
+ border-radius: 10px;
+ object-fit: cover;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ transition: all 0.3s ease;
+
+ ${ContactItem}:hover & {
+ border-color: rgba(255, 255, 255, 0.3);
+ transform: scale(1.05);
+ }
+`;
+
+const OnlineIndicator = styled.div`
+ position: absolute;
+ bottom: -2px;
+ right: -2px;
+ width: 0.875rem;
+ height: 0.875rem;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+ border: 2px solid rgba(255, 255, 255, 0.9);
+ box-shadow: 0 2px 8px rgba(16, 185, 129, 0.4);
+ animation: pulse 2s infinite;
+
+ @keyframes pulse {
+ 0%, 100% {
+ transform: scale(1);
+ opacity: 1;
+ }
+ 50% {
+ transform: scale(1.1);
+ opacity: 0.8;
+ }
+ }
+`;
+
+const ContactInfo = styled.div`
+ flex: 1;
+ min-width: 0;
+`;
+
+const ContactHeader = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ margin-bottom: 0rem;
+`;
+
+const ContactName = styled.h3`
+ font-weight: 700;
+ font-size: 0.75rem;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ color: rgba(17, 9, 75, 0.9);
+ margin: 0;
+`;
+
+const Timestamp = styled.span`
+ font-size: 0.75rem;
+ color: rgba(63, 1, 46, 0.6);
+ white-space: nowrap;
+ margin-left: 0.5rem;
+ font-weight: 500;
+`;
+
+const MessagePreviewContainer = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ gap: 0.2rem;
+`;
+
+const LastMessage = styled.p<{ $unread: boolean }>`
+ font-size: 0.875rem;
+ color: ${({ $unread }) => $unread ? 'rgba(159, 120, 120, 0.8)' : 'rgba(84, 158, 122, 0.6)'};
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-weight: ${({ $unread }) => $unread ? '600' : '400'};
+ margin: 0;
+ flex: 1;
+`;
+
+const UnreadBadge = styled.span`
+ background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
+ color: white;
+ font-size: 0.75rem;
+ border-radius: 12px;
+ height: 1.25rem;
+ min-width: 1.25rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 0.375rem;
+ flex-shrink: 0;
+ font-weight: 600;
+ box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
+ animation: bounce 1s infinite;
+
+ @keyframes bounce {
+ 0%, 100% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.1);
+ }
+ }
+`;
+
+// ==================== Component Implementation ====================
+const ContactList: React.FC = ({
+ conversations,
+ onSelectConversation
+}) => {
+ return (
+
+ {conversations.map(conversation => (
+ onSelectConversation(conversation.id)}
+ >
+
+
+ {conversation.participants.some(p => p.status === 'online') && (
+
+ )}
+
+
+
+
+ {conversation.name}
+ {conversation.lastMessage && (
+
+ {new Date(conversation.lastMessage.timestamp).toLocaleTimeString([], {
+ hour: '2-digit',
+ minute: '2-digit'
+ })}
+
+ )}
+
+
+
+ {conversation.lastMessage ? (
+ 0}>
+ {conversation.lastMessage.content}
+
+ ) : (
+
+ Start a conversation
+
+ )}
+
+ {conversation.unreadCount > 0 && (
+
+ {conversation.unreadCount}
+
+ )}
+
+
+
+ ))}
+
+ );
+};
+
+export default ContactList;
diff --git a/crates/cherry/src/components/ContactPage/ContactGroup.tsx b/crates/cherry/src/components/ContactPage/ContactGroup.tsx
new file mode 100644
index 0000000..7e3bfe4
--- /dev/null
+++ b/crates/cherry/src/components/ContactPage/ContactGroup.tsx
@@ -0,0 +1,133 @@
+import React, { useState } from 'react';
+import styled, { css } from 'styled-components';
+import ContactItem from './ContactItem';
+import type { ContactGroup } from '../../types/contact';
+import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
+
+interface ContactGroupProps {
+ group: ContactGroup;
+}
+
+// ==================== Styled Components ====================
+const GroupContainer = styled.div`
+ margin-bottom: 1.5rem;
+`;
+
+const GroupHeader = styled.div`
+ display: flex;
+ align-items: center;
+ padding: 1rem 1.25rem;
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(15px);
+ border-radius: 16px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ box-shadow:
+ 0 4px 20px rgba(0, 0, 0, 0.1),
+ 0 2px 10px rgba(0, 0, 0, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.15);
+ transform: translateY(-2px);
+ box-shadow:
+ 0 6px 25px rgba(0, 0, 0, 0.15),
+ 0 3px 15px rgba(0, 0, 0, 0.1);
+ border-color: rgba(255, 255, 255, 0.3);
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+`;
+
+const IconContainer = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 0.75rem;
+ transition: transform 0.3s ease;
+ color: rgba(23, 150, 104, 0.49);
+
+ svg {
+ width: 1.25rem;
+ height: 1.25rem;
+ }
+`;
+
+const GroupTitle = styled.h3`
+ font-weight: 600;
+ color: rgba(92, 41, 179, 0.43);
+ margin: 0;
+ flex-grow: 1;
+ font-size: 1rem;
+`;
+
+const ContactCount = styled.span`
+ background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
+ color: white;
+ font-size: 0.75rem;
+ font-weight: 600;
+ border-radius: 12px;
+ padding: 0.375rem 0.75rem;
+ margin-left: 0.5rem;
+ box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
+`;
+
+const ContactListContainer = styled.div<{ $expanded: boolean }>`
+ border-radius: 0 0 16px 16px;
+ overflow: hidden;
+ transition: all 0.3s ease;
+ margin-top: 0.5rem;
+ background: rgba(255, 255, 255, 0.05);
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+
+ ${({ $expanded }) => !$expanded && css`
+ max-height: 0;
+ opacity: 0;
+ transform: translateY(-10px);
+ margin-top: 0;
+ `}
+
+ ${({ $expanded }) => $expanded && css`
+ max-height: 1000px;
+ opacity: 1;
+ transform: translateY(0);
+ `}
+`;
+
+// ==================== Component Implementation ====================
+const ContactGroup: React.FC = ({ group }) => {
+ const [isExpanded, setIsExpanded] = useState(true);
+
+ const toggleExpand = () => setIsExpanded(!isExpanded);
+
+ return (
+
+
+
+ {isExpanded ? : }
+
+
+ {group.name}
+
+
+ {group.contacts.length} ไฝ่็ณปไบบ
+
+
+
+
+ {group.contacts.map(contact => (
+ console.log('Contact clicked', contact.id)}
+ />
+ ))}
+
+
+ );
+};
+
+export default ContactGroup;
diff --git a/crates/cherry/src/components/ContactPage/ContactItem.tsx b/crates/cherry/src/components/ContactPage/ContactItem.tsx
new file mode 100644
index 0000000..2f5b4a1
--- /dev/null
+++ b/crates/cherry/src/components/ContactPage/ContactItem.tsx
@@ -0,0 +1,72 @@
+import React from 'react';
+import styled from 'styled-components';
+import Avatar from '../UI/Avatar';
+import { Contact } from '../../types/contact';
+
+interface ContactItemProps {
+ contact: Contact;
+ onClick: () => void;
+}
+
+// ==================== Styled Components ====================
+const ContactItemContainer = styled.div`
+ display: flex;
+ align-items: center;
+ padding: 1rem 1.25rem;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ border-radius: 12px;
+ margin: 0.25rem 0.5rem;
+ background: rgba(255, 255, 255, 0.05);
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.1);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
+ border-color: rgba(255, 255, 255, 0.2);
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+`;
+
+const ContactInfo = styled.div`
+ margin-left: 1rem;
+ flex: 1;
+`;
+
+const ContactName = styled.div`
+ font-weight: 600;
+ color: rgba(11, 18, 86, 0.51);
+ font-size: 0.875rem;
+ margin-bottom: 0.25rem;
+`;
+
+const ContactStatus = styled.div`
+ font-size: 0.75rem;
+ color: rgba(38, 91, 0, 0.66);
+ text-transform: capitalize;
+ font-weight: 500;
+`;
+
+const ContactItem: React.FC = ({ contact, onClick }) => {
+ return (
+
+
+
+ {contact.name}
+ {contact.status}
+
+
+ );
+};
+
+export default ContactItem;
diff --git a/crates/cherry/src/components/ContactPage/GroupSection.tsx b/crates/cherry/src/components/ContactPage/GroupSection.tsx
new file mode 100644
index 0000000..b41cae8
--- /dev/null
+++ b/crates/cherry/src/components/ContactPage/GroupSection.tsx
@@ -0,0 +1,252 @@
+import React, { useState } from 'react';
+import styled from 'styled-components';
+import { Group } from '../../types/contact';
+import Avatar from '../UI/Avatar';
+import { FaCrown, FaChevronDown, FaChevronRight } from 'react-icons/fa';
+
+interface GroupSectionProps {
+ title: string;
+ groups: Group[];
+}
+
+// ==================== Styled Components ====================
+const SectionContainer = styled.div`
+ margin-bottom: 1.75rem;
+`;
+
+const SectionHeader = styled.div`
+ display: flex;
+ align-items: center;
+ padding: 0.75rem 1rem;
+ margin-bottom: 1rem;
+ cursor: pointer;
+ color: rgba(41, 56, 59, 0.7);
+ user-select: none;
+ transition: all 0.3s ease;
+ border-radius: 16px;
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(15px);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.15);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
+ border-color: rgba(255, 255, 255, 0.3);
+
+ .section-title {
+ color: rgba(33, 65, 54, 0.9);
+ }
+
+ .collapse-icon {
+ transform: scale(1.1);
+ }
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+`;
+
+const CollapseIcon = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 0.75rem;
+ transition: all 0.3s ease;
+ color: rgba(255, 255, 255, 0.8);
+
+ svg {
+ width: 0.85rem;
+ height: 0.85rem;
+ }
+`;
+
+const SectionTitle = styled.h3`
+ font-size: 0.875rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ text-color: rgba(9, 33, 98, 0.66);
+ letter-spacing: 0.05em;
+ color: rgba(51, 89, 97, 0.7);
+ position: relative;
+ flex-grow: 1;
+ transition: color 0.3s ease;
+ margin: 0;
+
+ &::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ height: 1px;
+ width: 100%;
+ background: linear-gradient(to right, rgba(255, 255, 255, 0.2), transparent);
+ z-index: 0;
+ }
+
+ span {
+ position: relative;
+ z-index: 1;
+ background: rgba(255, 255, 255, 0.05);
+ backdrop-filter: blur(10px);
+ padding: 0 0.5rem 0 0;
+ }
+`;
+
+const GroupCount = styled.span`
+ background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
+ color: white;
+ font-size: 0.75rem;
+ font-weight: 600;
+ border-radius: 12px;
+ padding: 0.375rem 0.75rem;
+ margin-left: 0.5rem;
+ z-index: 1;
+ box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
+`;
+
+const GroupCard = styled.div<{ $expanded: boolean }>`
+ background: rgba(255, 255, 255, 0.05);
+ backdrop-filter: blur(15px);
+ border-radius: 16px;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ overflow: hidden;
+ transition: all 0.3s ease;
+ max-height: ${({ $expanded }) => $expanded ? '1000px' : '0'};
+ opacity: ${({ $expanded }) => $expanded ? '1' : '0'};
+ transform: translateY(${({ $expanded }) => $expanded ? '0' : '-10px'});
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
+`;
+
+const GroupItem = styled.div`
+ display: flex;
+ align-items: center;
+ padding: 1rem 1.25rem;
+ transition: all 0.3s ease;
+ cursor: pointer;
+ position: relative;
+
+ &:not(:last-child)::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 1.25rem;
+ right: 1.25rem;
+ height: 1px;
+ background: rgba(255, 255, 255, 0.1);
+ }
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.1);
+ transform: translateY(-2px);
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
+ z-index: 1;
+ border-radius: 12px;
+
+ &::after {
+ opacity: 0;
+ }
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+`;
+
+const GroupInfo = styled.div`
+ margin-left: 1rem;
+ flex: 1;
+`;
+
+const GroupName = styled.div`
+ font-weight: 600;
+ color: rgba(10, 10, 10, 0.7);
+ margin-bottom: 0.25rem;
+ display: flex;
+ align-items: center;
+ font-size: 0.875rem;
+`;
+
+const GroupMeta = styled.div`
+ display: flex;
+ align-items: center;
+ font-size: 0.75rem;
+ color: rgba(10, 10, 10, 0.6);
+`;
+
+const MemberCount = styled.span`
+ margin-right: 0.75rem;
+ font-weight: 500;
+`;
+
+const OwnerBadge = styled.span`
+ display: inline-flex;
+ align-items: center;
+ background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
+ color: white;
+ font-size: 0.75rem;
+ font-weight: 600;
+ padding: 0.25rem 0.75rem 0.25rem 0.5rem;
+ border-radius: 12px;
+ box-shadow: 0 2px 8px rgba(251, 191, 36, 0.3);
+
+ svg {
+ margin-right: 0.25rem;
+ font-size: 0.7rem;
+ }
+`;
+
+// ==================== Component Implementation ====================
+const GroupSection: React.FC = ({ title, groups }) => {
+ const [isExpanded, setIsExpanded] = useState(true);
+
+ const toggleExpand = () => {
+ setIsExpanded(!isExpanded);
+ };
+
+ return (
+
+
+
+ {isExpanded ? : }
+
+
+
+ {title}
+
+
+ {groups.length}
+
+
+
+ {groups.map(group => (
+ console.log('Group clicked', group.id)}
+ >
+
+
+
+ {group.name}
+
+
+ {group.memberCount} ๆๅ
+
+ {group.isOwner && (
+
+
+ ๅๅปบ่
+
+ )}
+
+
+
+ ))}
+
+
+ );
+};
+
+export default GroupSection;
diff --git a/crates/cherry/src/components/ContactPage/index.tsx b/crates/cherry/src/components/ContactPage/index.tsx
new file mode 100644
index 0000000..575f8a3
--- /dev/null
+++ b/crates/cherry/src/components/ContactPage/index.tsx
@@ -0,0 +1,338 @@
+import { useState } from 'react';
+import styled, { css } from 'styled-components';
+import ContactGroup from './ContactGroup';
+import GroupSection from './GroupSection';
+import { mockContactGroups, mockOwnedGroups, mockJoinedGroups } from '../../data/mockContacts';
+import { FaUserFriends, FaUsers, FaPlus, FaSearch } from 'react-icons/fa';
+
+interface SidebarButtonProps {
+ $active?: boolean;
+}
+
+const Container = styled.div`
+ display: flex;
+ height: 100vh;
+ background: linear-gradient(135deg,rgb(255, 255, 255) 0%,rgb(175, 222, 227) 100%);
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background:
+ radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
+ radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
+ radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.2) 0%, transparent 50%);
+ pointer-events: none;
+ }
+`;
+
+const Sidebar = styled.div`
+ width: 200px;
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(20px);
+ border-right: 1px solid rgba(255, 255, 255, 0.2);
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ z-index: 1;
+ box-shadow: 4px 0 20px rgba(0, 0, 0, 0.1);
+`;
+
+const SidebarHeader = styled.div`
+ padding: 2rem 1.5rem 1.5rem;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.2);
+ font-size: 1.2rem;
+ font-weight: 700;
+ color: rgba(68, 38, 38, 0.9);
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+
+ svg {
+ color: #6366f1;
+ font-size: 1.5rem;
+ }
+`;
+
+const SidebarNav = styled.nav`
+ margin-top: 1rem;
+ padding: 0 1rem;
+`;
+
+const SidebarButton = styled.button`
+ display: flex;
+ align-items: center;
+ width: 100%;
+ padding: 1rem 1.25rem;
+ font-size: 1rem;
+ color: ${props => props.$active ? 'rgba(0, 0, 0, 0.8)' : 'rgba(9, 29, 34, 0.7)'};
+ background: ${props => props.$active
+ ? 'rgba(162, 184, 195, 0.35)'
+ : 'rgba(255, 255, 255, 0.05)'
+ };
+ backdrop-filter: blur(10px);
+ border-radius: 16px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ gap: 0.75rem;
+ border: 1px solid ${props => props.$active
+ ? 'rgba(255, 255, 255, 0.3)'
+ : 'rgba(255, 255, 255, 0.1)'
+ };
+ text-align: left;
+ margin-bottom: 0.5rem;
+
+ ${props => props.$active && css`
+ font-weight: 600;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
+ `}
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.15);
+ transform: translateY(-2px);
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
+ border-color: rgba(255, 255, 255, 0.3);
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+
+ svg {
+ font-size: 1.125rem;
+ flex-shrink: 0;
+ }
+`;
+
+const MainContent = styled.div`
+ flex: 1;
+ padding: 2rem;
+ overflow-y: auto;
+ position: relative;
+ z-index: 1;
+
+ /* ้่ๆปๅจๆก */
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+`;
+
+const HeaderContainer = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 2rem;
+`;
+
+const Header = styled.h1`
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: rgba(81, 17, 17, 0.35);
+ margin: 0;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+`;
+
+const SearchBar = styled.div`
+ display: flex;
+ margin-bottom: 2rem;
+ border-radius: 20px;
+ overflow: hidden;
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
+ transition: all 0.3s ease;
+
+ &:focus-within {
+ background: rgba(255, 255, 255, 0.15);
+ border-color: rgba(99, 102, 241, 0.5);
+ box-shadow:
+ 0 0 0 3px rgba(99, 102, 241, 0.1),
+ 0 6px 25px rgba(0, 0, 0, 0.15);
+ transform: translateY(-1px);
+ }
+`;
+
+const SearchInput = styled.input`
+ flex: 1;
+ padding: 1rem 1.25rem;
+ border: none;
+ font-size: 1rem;
+ background: rgba(224, 186, 186, 0.1);
+ color: rgba(10, 10, 10, 0.8);
+ font-weight: 400;
+
+ &::placeholder {
+ color: rgba(16, 60, 30, 0.6);
+ }
+
+ &:focus {
+ outline: none;
+ }
+`;
+
+const SearchButton = styled.button`
+ padding: 1rem 1.25rem;
+ background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
+ color: white;
+ border: none;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 4px 15px rgba(99, 102, 241, 0.4);
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+
+ svg {
+ font-size: 1rem;
+ }
+`;
+
+const NewGroupButton = styled.button`
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ background: linear-gradient(135deg,rgb(75, 152, 165) 0%,rgb(144, 37, 150) 100%);
+ color: white;
+ border: none;
+ padding: 0.875rem 1.25rem;
+ border-radius: 16px;
+ font-size: 0.875rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ box-shadow: 0 4px 20px rgba(99, 102, 241, 0.3);
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 25px rgba(99, 102, 241, 0.4);
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+
+ svg {
+ font-size: 0.875rem;
+ }
+`;
+
+const ContentSection = styled.div`
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(20px);
+ border-radius: 20px;
+ padding: 2rem;
+ box-shadow:
+ 0 8px 32px rgba(0, 0, 0, 0.1),
+ 0 4px 16px rgba(0, 0, 0, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ margin-bottom: 2rem;
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow:
+ 0 12px 40px rgba(0, 0, 0, 0.15),
+ 0 6px 20px rgba(0, 0, 0, 0.1);
+ }
+`;
+
+const ContactPage = () => {
+ const [activeTab, setActiveTab] = useState('contacts');
+ const [searchQuery, setSearchQuery] = useState('');
+
+ const contactGroups = mockContactGroups;
+ const ownedGroups = mockOwnedGroups;
+ const joinedGroups = mockJoinedGroups;
+
+ return (
+
+
+
+
+ ้่ฎฏๅฝ
+
+
+ setActiveTab('contacts')}
+ >
+
+ ่็ณปไบบ
+
+ setActiveTab('groups')}
+ >
+
+ ็พค็ป
+
+
+
+
+
+ {activeTab === 'contacts' ? (
+ <>
+
+ setSearchQuery(e.target.value)}
+ />
+
+
+
+
+
+
+ {contactGroups.map(group => (
+
+ ))}
+
+ >
+ ) : (
+ <>
+
+ setSearchQuery(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
+ );
+};
+
+export default ContactPage;
diff --git a/crates/cherry/src/components/MessageInput.tsx b/crates/cherry/src/components/MessageInput.tsx
new file mode 100644
index 0000000..856cb7e
--- /dev/null
+++ b/crates/cherry/src/components/MessageInput.tsx
@@ -0,0 +1,183 @@
+// src/components/MessageInput.tsx
+import React, { useState } from 'react';
+import styled from 'styled-components';
+
+interface MessageInputProps {
+ onSend: (message: string) => void;
+}
+
+// ==================== Styled Components ====================
+const Container = styled.div`
+ padding: 1.25rem 1.5rem;
+ background: rgba(38, 116, 22, 0.1);
+ backdrop-filter: blur(15px);
+ border-top: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 0 0 20px 20px;
+`;
+
+const Form = styled.form`
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+`;
+
+const IconButton = styled.button`
+ padding: 0.75rem;
+ color: rgba(255, 255, 255, 0.7);
+ transition: all 0.3s ease;
+ border-radius: 16px;
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+
+ &:hover {
+ color: rgba(255, 255, 255, 0.9);
+ background: rgba(255, 255, 255, 0.15);
+ transform: translateY(-2px);
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+`;
+
+const InputContainer = styled.div`
+ flex: 1;
+ position: relative;
+`;
+
+const InputField = styled.input`
+ width: 100%;
+ padding: 1rem 1.25rem 1rem 3.5rem;
+ border-radius: 20px;
+ background: rgba(129, 250, 95, 0);
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ transition: all 0.3s ease;
+ font-size: 1rem;
+ color: rgb(38, 72, 66);
+ font-weight: 400;
+
+ &::placeholder {
+ color: rgba(255, 255, 255, 0.6);
+ }
+
+ &:focus {
+ outline: none;
+ background: rgba(255, 255, 255, 0.2);
+ border-color: rgba(99, 102, 241, 0.5);
+ box-shadow:
+ 0 0 0 3px rgba(99, 102, 241, 0.1),
+ 0 4px 20px rgba(0, 0, 0, 0.1);
+ transform: translateY(-1px);
+ }
+`;
+
+const EmojiButton = styled(IconButton)`
+ position: absolute;
+ right: 0.75rem;
+ top: 50%;
+ transform: translateY(-50%);
+ padding: 0.5rem;
+ border-radius: 12px;
+
+ &:hover {
+ transform: translateY(-50%) scale(1.1);
+ }
+`;
+
+const SendButton = styled.button<{ $disabled: boolean }>`
+ padding: 1rem 1.25rem;
+ border-radius: 20px;
+ background: ${({ $disabled }) =>
+ $disabled
+ ? 'rgba(255, 255, 255, 0.1)'
+ : 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)'
+ };
+ color: white;
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: none;
+ cursor: ${({ $disabled }) => $disabled ? 'not-allowed' : 'pointer'};
+ font-weight: 600;
+ box-shadow: ${({ $disabled }) =>
+ $disabled
+ ? 'none'
+ : '0 4px 20px rgba(99, 102, 241, 0.3), 0 2px 10px rgba(139, 92, 246, 0.2)'
+ };
+
+ &:hover {
+ ${({ $disabled }) => !$disabled && `
+ transform: translateY(-2px);
+ box-shadow:
+ 0 6px 25px rgba(99, 102, 241, 0.4),
+ 0 3px 15px rgba(139, 92, 246, 0.3);
+ `}
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+
+ ${({ $disabled }) => $disabled && `
+ opacity: 0.5;
+ cursor: not-allowed;
+ `}
+`;
+
+// ==================== Component Implementation ====================
+const MessageInput: React.FC = ({ onSend }) => {
+ const [message, setMessage] = useState('');
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (message.trim()) {
+ onSend(message);
+ setMessage('');
+ }
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default MessageInput;
diff --git a/crates/cherry/src/components/MessageList.tsx b/crates/cherry/src/components/MessageList.tsx
new file mode 100644
index 0000000..66255ba
--- /dev/null
+++ b/crates/cherry/src/components/MessageList.tsx
@@ -0,0 +1,170 @@
+// src/components/MessageList.tsx
+import React from 'react';
+import styled from 'styled-components';
+import { Message, User } from '../types/types';
+
+interface MessageListProps {
+ messages: Message[];
+ currentUser: User;
+}
+
+// ==================== Styled Components ====================
+const MessageContainer = styled.div<{ $isOwn: boolean }>`
+ flex: 1;
+ overflow-y: auto;
+ padding: 1.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ background: rgba(255, 255, 255, 0.05);
+ backdrop-filter: blur(10px);
+
+ /* ้่ๆปๅจๆก */
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+`;
+
+const MessageBubbleWrapper = styled.div<{ $isOwn: boolean }>`
+ display: flex;
+ justify-content: ${({ $isOwn }) => ($isOwn ? 'flex-end' : 'flex-start')};
+ animation: slideIn 0.3s ease-out;
+
+ @keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ }
+`;
+
+const MessageBubble = styled.div<{ $isOwn: boolean }>`
+ max-width: 18rem;
+
+ @media (min-width: 768px) {
+ max-width: 24rem;
+ }
+
+ @media (min-width: 1024px) {
+ max-width: 32rem;
+ }
+
+ padding: 0.875rem 1.25rem;
+ border-radius: 20px;
+ position: relative;
+ transition: all 0.3s ease;
+
+ ${({ $isOwn }) => $isOwn
+ ? `
+ background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
+ color: white;
+ border-top-right-radius: 8px;
+ box-shadow:
+ 0 4px 20px rgba(99, 102, 241, 0.3),
+ 0 2px 10px rgba(139, 92, 246, 0.2);
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow:
+ 0 6px 25px rgba(99, 102, 241, 0.4),
+ 0 3px 15px rgba(139, 92, 246, 0.3);
+ }
+ `
+ : `
+ background: rgba(255, 255, 255, 0.15);
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-top-left-radius: 8px;
+ box-shadow:
+ 0 4px 20px rgba(0, 0, 0, 0.1),
+ 0 2px 10px rgba(0, 0, 0, 0.05);
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow:
+ 0 6px 25px rgba(0, 0, 0, 0.15),
+ 0 3px 15px rgba(0, 0, 0, 0.1);
+ }
+ `
+ }
+`;
+
+const MessageContent = styled.p`
+ font-size: 0.875rem;
+ line-height: 1.4;
+ word-wrap: break-word;
+ margin: 0;
+ font-weight: 400;
+`;
+
+const TimestampContainer = styled.div<{ $isOwn: boolean }>`
+ font-size: 0.75rem;
+ margin-top: 0.5rem;
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 0.25rem;
+ color: ${({ $isOwn }) => $isOwn ? 'rgba(255, 255, 255, 0.8)' : 'rgba(255, 255, 255, 0.6)'};
+ font-weight: 500;
+`;
+
+const StatusIndicator = styled.span`
+ display: flex;
+ align-items: center;
+ font-size: 0.875rem;
+
+ &.sent {
+ color: rgba(255, 255, 255, 0.7);
+ }
+
+ &.delivered {
+ color: rgba(255, 255, 255, 0.8);
+ }
+
+ &.read {
+ color: #10b981;
+ }
+`;
+
+// ==================== Component Implementation ====================
+const MessageList: React.FC = ({ messages, currentUser }) => {
+ return (
+
+ {messages.map(message => {
+ const isOwn = message.userId === currentUser.id;
+
+ return (
+
+
+ {message.content}
+
+
+
+ {new Date(message.timestamp).toLocaleTimeString([], {
+ hour: '2-digit',
+ minute: '2-digit'
+ })}
+
+ {isOwn && message.status && (
+
+ {message.status === 'sent' ? 'โ' :
+ message.status === 'delivered' ? 'โโ' : 'โโโ'}
+
+ )}
+
+
+
+ );
+ })}
+
+ );
+};
+
+export default MessageList;
diff --git a/crates/cherry/src/components/Sidebar.tsx b/crates/cherry/src/components/Sidebar.tsx
new file mode 100644
index 0000000..5f6d3d5
--- /dev/null
+++ b/crates/cherry/src/components/Sidebar.tsx
@@ -0,0 +1,520 @@
+// src/components/Sidebar.tsx
+import React, { useState } from 'react';
+import styled from 'styled-components';
+import { Conversation, User } from '../types/types';
+import ContactList from './ContactList.tsx';
+import { FaUserFriends } from 'react-icons/fa';
+
+interface SidebarProps {
+ conversations: Conversation[];
+ currentUser: User;
+ onSelectConversation: (id: string) => void;
+ onOpenSettings: () => void;
+ onOpenContacts: () => void;
+}
+
+type TabType = 'all' | 'unread' | 'mentions' | 'direct' | 'group';
+
+// ==================== Styled Components ====================
+const SidebarContainer = styled.div`
+ width: 420px;
+ background-color: rgba(255, 255, 255, 0.95);
+ backdrop-filter: blur(20px);
+ color: #1f2937;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ border-right: 1px solid rgba(229, 231, 235, 0.5);
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+`;
+
+const Header = styled.div`
+ padding: 1.5rem;
+ border-bottom: 1px solid rgba(229, 231, 235, 0.5);
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.1), rgba(168, 85, 247, 0.1));
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+`;
+
+const HeaderActions = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ -webkit-app-region: no-drag;
+ flex-shrink: 0;
+`;
+
+const IconButton = styled.button`
+ padding: 0.75rem;
+ border-radius: 12px;
+ transition: all 0.2s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: rgba(255, 255, 255, 0.8);
+ border: 1px solid rgba(229, 231, 235, 0.5);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+
+ &:hover {
+ background-color: rgba(99, 102, 241, 0.1);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ }
+
+ svg {
+ width: 20px;
+ height: 20px;
+ color: #6b7280;
+ }
+`;
+
+const SearchContainer = styled.div`
+ position: relative;
+ flex: 1;
+`;
+
+const SearchInput = styled.input`
+ width: 100%;
+ padding: 0.75rem 0.75rem 0.75rem 3rem;
+ border-radius: 12px;
+ background-color: rgba(255, 255, 255, 0.8);
+ transition: all 0.2s ease;
+ color: #1f2937;
+ border: 1px solid rgba(229, 231, 235, 0.5);
+ font-size: 14px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+
+ &:focus {
+ background-color: rgba(255, 255, 255, 0.95);
+ outline: none;
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1), 0 4px 6px rgba(0, 0, 0, 0.1);
+ border-color: rgba(99, 102, 241, 0.3);
+ }
+
+ &::placeholder {
+ color: #9ca3af;
+ }
+`;
+
+const SearchIcon = styled.div`
+ position: absolute;
+ left: 1rem;
+ top: 50%;
+ transform: translateY(-50%);
+ color: #9ca3af;
+
+ svg {
+ width: 18px;
+ height: 18px;
+ }
+`;
+
+const ContentContainer = styled.div`
+ flex: 1;
+ display: flex;
+ overflow: hidden;
+ padding: 1rem;
+`;
+
+const VerticalNav = styled.div`
+ width: 80px;
+ background-color: rgba(249, 250, 251, 0.8);
+ display: flex;
+ flex-direction: column;
+ border-radius: 16px;
+ border: 1px solid rgba(229, 231, 235, 0.5);
+ padding: 1rem 0.5rem;
+ margin-right: 1rem;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+`;
+
+const NavButton = styled.button<{ $active?: boolean }>`
+ padding: 1rem 0.5rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ transition: all 0.2s ease;
+ background-color: ${props => props.$active ? 'rgba(99, 102, 241, 0.1)' : 'transparent'};
+ border-radius: 12px;
+ margin-bottom: 0.5rem;
+ border: 1px solid ${props => props.$active ? 'rgba(99, 102, 241, 0.2)' : 'transparent'};
+
+ &:hover {
+ background-color: ${props => props.$active ? 'rgba(99, 102, 241, 0.15)' : 'rgba(99, 102, 241, 0.05)'};
+ transform: translateY(-1px);
+ }
+`;
+
+const NavIconWrapper = styled.div`
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+`;
+
+const Badge = styled.span<{ $color?: string; $bgColor?: string }>`
+ position: absolute;
+ top: -0.5rem;
+ right: -0.5rem;
+ background-color: ${props => props.$bgColor || props.$color || '#ef4444'};
+ color: white;
+ font-size: 0.65rem;
+ border-radius: 9999px;
+ height: 1.25rem;
+ width: 1.25rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 600;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ border: 2px solid white;
+`;
+
+const NavLabel = styled.span<{ $active?: boolean }>`
+ font-size: 0.75rem;
+ margin-top: 0.5rem;
+ color: ${props => props.$active ? '#6366f1' : '#6b7280'};
+ font-weight: ${props => props.$active ? '600' : '500'};
+`;
+
+const NavSpacer = styled.div`
+ flex: 1;
+ border-top: 1px solid rgba(229, 231, 235, 0.5);
+ padding-top: 1rem;
+ margin-top: 0.5rem;
+`;
+
+const MainContent = styled.div`
+ flex: 1;
+ overflow-y: auto;
+ background-color: rgba(255, 255, 255, 0.8);
+ border-radius: 16px;
+ border: 1px solid rgba(229, 231, 235, 0.5);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+`;
+
+const ContentHeader = styled.div`
+ padding: 1.5rem 1.5rem 1rem;
+ border-bottom: 1px solid rgba(229, 231, 235, 0.5);
+ background: linear-gradient(135deg, rgba(249, 250, 251, 0.8), rgba(243, 244, 246, 0.8));
+ border-radius: 16px 16px 0 0;
+`;
+
+const Title = styled.h2`
+ font-size: 1.05rem;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ color:rgb(63, 149, 73);
+ margin: 0;
+`;
+
+const StatusBadge = styled.span<{ $color?: string, $bgColor?: string }>`
+ margin-left: 0.75rem;
+ background-color: ${props => props.$bgColor || '#6366f1'};
+ color: ${props => props.$color || 'white'};
+ font-size: 0.75rem;
+ border-radius: 9999px;
+ padding: 0.25rem 0.75rem;
+ font-weight: 600;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+`;
+
+const EmptyState = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: calc(100% - 80px);
+ padding: 2rem;
+ text-align: center;
+ background: linear-gradient(135deg, rgba(249, 250, 251, 0.5), rgba(243, 244, 246, 0.5));
+`;
+
+const EmptyIcon = styled.div`
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.1), rgba(168, 85, 247, 0.1));
+ border-radius: 50%;
+ padding: 2rem;
+ margin-bottom: 1.5rem;
+ color: #6366f1;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+
+ svg {
+ width: 48px;
+ height: 48px;
+ }
+`;
+
+const EmptyText = styled.p`
+ font-size: 1.125rem;
+ margin-bottom: 0.5rem;
+ font-weight: 600;
+ color: #1f2937;
+`;
+
+const EmptySubtext = styled.p`
+ font-size: 0.875rem;
+ margin-bottom: 1.5rem;
+ color: #6b7280;
+ line-height: 1.5;
+ max-width: 320px;
+`;
+
+const ActionButton = styled.button`
+ margin-top: 0.5rem;
+ padding: 0.75rem 1.5rem;
+ background: linear-gradient(135deg, #6366f1, #8b5cf6);
+ color: white;
+ border-radius: 12px;
+ font-size: 0.875rem;
+ font-weight: 600;
+ transition: all 0.2s ease;
+ border: none;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ box-shadow: 0 4px 6px rgba(99, 102, 241, 0.2);
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 15px rgba(99, 102, 241, 0.3);
+ }
+
+ &:active {
+ transform: translateY(0);
+ box-shadow: 0 4px 6px rgba(99, 102, 241, 0.2);
+ }
+
+ svg {
+ width: 16px;
+ height: 16px;
+ }
+`;
+
+// ==================== Component Implementation ====================
+const Sidebar: React.FC = ({
+ conversations,
+ currentUser,
+ onSelectConversation,
+ onOpenSettings,
+ onOpenContacts
+}) => {
+ const [activeTab, setActiveTab] = useState('all');
+ const [searchQuery, setSearchQuery] = useState('');
+
+ // ่ฎก็ฎๅ็ฑปไผ่ฏๆฐ้
+ const unreadCount = conversations.filter(c => c.unreadCount > 0).length;
+ const mentionCount = conversations.filter(c => c.mentions > 0).length;
+ const directCount = conversations.filter(c => c.type === 'direct').length;
+ const groupCount = conversations.filter(c => c.type === 'group').length;
+
+ // ๆ นๆฎๅฝๅๆ ็ญพ่ฟๆปคไผ่ฏ
+ const filteredConversations = conversations.filter(conversation => {
+ const matchesSearch = conversation.name.toLowerCase().includes(searchQuery.toLowerCase());
+
+ switch (activeTab) {
+ case 'unread':
+ return matchesSearch && conversation.unreadCount > 0;
+ case 'mentions':
+ return matchesSearch && conversation.mentions > 0;
+ case 'direct':
+ return matchesSearch && conversation.type === 'direct';
+ case 'group':
+ return matchesSearch && conversation.type === 'group';
+ default:
+ return matchesSearch;
+ }
+ });
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ />
+
+
+
+ {/* ๅ็ดๅ็ฑปๅฏผ่ชๆ */}
+
+
+
+ setActiveTab('mentions')}
+ >
+
+
+ {mentionCount > 0 && (
+ {mentionCount}
+ )}
+ @ๆ
+
+
+
+
+ setActiveTab('unread')}
+ >
+
+
+ {unreadCount > 0 && (
+ {unreadCount}
+ )}
+ ๆช่ฏป
+
+
+
+
+ setActiveTab('all')}
+ >
+
+
+ {conversations.length > 0 && (
+ {conversations.length}
+ )}
+ ไผ่ฏ
+
+
+
+
+
+
+ setActiveTab('direct')}
+ >
+
+
+ ๅ่
+
+
+
+ setActiveTab('group')}
+ >
+
+
+ ็พค่
+
+
+
+
+
+
+
+ ๆทปๅ
+
+
+
+
+
+
+
+
+ {getTabLabel(activeTab)}
+ {activeTab === 'unread' && unreadCount > 0 && (
+ {unreadCount} ๆช่ฏป
+ )}
+ {activeTab === 'mentions' && mentionCount > 0 && (
+ {mentionCount} ๆๅ
+ )}
+
+
+
+ {filteredConversations.length > 0 ? (
+
+ ) : (
+
+
+
+
+
+ {activeTab === 'all' && 'ๆฒกๆไผ่ฏ'}
+ {activeTab === 'unread' && 'ๆฒกๆๆช่ฏปๆถๆฏ'}
+ {activeTab === 'mentions' && 'ๆฒกๆๆๅฐๆจ็ๆถๆฏ'}
+ {activeTab === 'direct' && 'ๆฒกๆๅ่ไผ่ฏ'}
+ {activeTab === 'group' && 'ๆฒกๆ็พค่ไผ่ฏ'}
+
+
+ {activeTab === 'all' && 'ๅผๅงๆฐ็ๅฏน่ฏๆๆทปๅ ่็ณปไบบ'}
+ {activeTab === 'unread' && 'ๆๆๆถๆฏ้ฝๅทฒ้
่ฏป'}
+ {activeTab === 'mentions' && 'ๅฝๆไบบๆๅฐๆจๆถ๏ผๆถๆฏๅฐๆพ็คบๅจ่ฟ้'}
+ {activeTab === 'direct' && 'ๆทปๅ ่็ณปไบบๅผๅงไธๅฏนไธ่ๅคฉ'}
+ {activeTab === 'group' && 'ๅๅปบ็พค็ปๆๅ ๅ
ฅ็ฐๆ็พค็ป'}
+
+
+ {activeTab === 'all' && 'ๅผๅงๆฐๅฏน่ฏ'}
+ {activeTab === 'unread' && 'ๆฅ็ๆๆๆถๆฏ'}
+ {activeTab === 'mentions' && 'ๆฅ็้็ฅ่ฎพ็ฝฎ'}
+ {activeTab === 'direct' && 'ๆทปๅ ่็ณปไบบ'}
+ {activeTab === 'group' && 'ๅๅปบ็พค็ป'}
+
+
+ )}
+
+
+
+ >
+ );
+};
+
+// ่ทๅๆ ็ญพๅ็งฐ
+const getTabLabel = (tab: TabType): string => {
+ switch (tab) {
+ case 'all': return 'ๆๆไผ่ฏ';
+ case 'unread': return 'ๆช่ฏปๆถๆฏ';
+ case 'mentions': return '@ ๆๅฐๆ';
+ case 'direct': return 'ๅ่ไผ่ฏ';
+ case 'group': return '็พค่ไผ่ฏ';
+ default: return '';
+ }
+};
+
+export default Sidebar;
diff --git a/crates/cherry/src/components/StatusBar.tsx b/crates/cherry/src/components/StatusBar.tsx
new file mode 100644
index 0000000..aa62382
--- /dev/null
+++ b/crates/cherry/src/components/StatusBar.tsx
@@ -0,0 +1,257 @@
+import React from 'react';
+import { User } from '../types/types';
+import styled, { keyframes } from 'styled-components';
+
+interface StatusBarProps {
+ currentUser: User;
+}
+
+// ==================== Animations ====================
+const pulse = keyframes`
+ 0% { transform: scale(1); opacity: 1; }
+ 50% { transform: scale(1.1); opacity: 0.7; }
+ 100% { transform: scale(1); opacity: 1; }
+`;
+
+const float = keyframes`
+ 0% { transform: translateY(0); }
+ 50% { transform: translateY(-3px); }
+ 100% { transform: translateY(0); }
+`;
+
+// ==================== Styled Components ====================
+const StatusBarContainer = styled.div`
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(249, 250, 251, 0.95));
+ backdrop-filter: blur(20px);
+ color: #1f2937;
+ padding: 1rem 1.5rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+ border-bottom: 1px solid rgba(229, 231, 235, 0.5);
+ position: relative;
+ z-index: 100;
+`;
+
+const AvatarContainer = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ background: rgba(255, 255, 255, 0.8);
+ padding: 0.75rem 1rem;
+ border-radius: 16px;
+ border: 1px solid rgba(229, 231, 235, 0.5);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
+ }
+`;
+
+const AvatarWrapper = styled.div`
+ position: relative;
+ width: 48px;
+ height: 48px;
+`;
+
+const Avatar = styled.img`
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ object-fit: cover;
+ border: 3px solid rgba(255, 255, 255, 0.9);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: scale(1.05);
+ border-color: rgba(99, 102, 241, 0.3);
+ }
+`;
+
+const StatusIndicator = styled.div<{ status: string }>`
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ border: 3px solid rgba(255, 255, 255, 0.9);
+ background: ${({ status }) => {
+ const colors: Record = {
+ online: 'linear-gradient(135deg, #10b981, #059669)',
+ offline: 'linear-gradient(135deg, #6b7280, #4b5563)',
+ away: 'linear-gradient(135deg, #f59e0b, #d97706)',
+ dnd: 'linear-gradient(135deg, #ef4444, #dc2626)',
+ busy: 'linear-gradient(135deg, #ef4444, #dc2626)',
+ };
+ return colors[status] || colors.offline;
+ }};
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ animation: ${pulse} 2s infinite;
+ z-index: 10;
+`;
+
+const UserInfo = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+`;
+
+const UserName = styled.p`
+ font-weight: 700;
+ font-size: 1rem;
+ margin: 0;
+ letter-spacing: 0.025em;
+ color: #1f2937;
+ background: linear-gradient(135deg, #6366f1, #8b5cf6);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+`;
+
+const UserStatus = styled.p<{ status: string }>`
+ font-size: 0.875rem;
+ text-transform: capitalize;
+ margin: 0;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ color: #6b7280;
+ font-weight: 500;
+
+ &::before {
+ content: '';
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: ${({ status }) => {
+ const colors: Record = {
+ online: 'linear-gradient(135deg, #10b981, #059669)',
+ offline: 'linear-gradient(135deg, #6b7280, #4b5563)',
+ away: 'linear-gradient(135deg, #f59e0b, #d97706)',
+ dnd: 'linear-gradient(135deg, #ef4444, #dc2626)',
+ busy: 'linear-gradient(135deg, #ef4444, #dc2626)',
+ };
+ return colors[status] || colors.offline;
+ }};
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+ }
+`;
+
+const ActionContainer = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ background: rgba(255, 255, 255, 0.8);
+ padding: 0.75rem 1rem;
+ border-radius: 16px;
+ border: 1px solid rgba(229, 231, 235, 0.5);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+`;
+
+const ActionButton = styled.button`
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.1), rgba(139, 92, 246, 0.1));
+ border: 1px solid rgba(99, 102, 241, 0.2);
+ width: 44px;
+ height: 44px;
+ border-radius: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ position: relative;
+
+ &:hover {
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(139, 92, 246, 0.2));
+ transform: translateY(-3px) scale(1.05);
+ box-shadow: 0 8px 20px rgba(99, 102, 241, 0.3);
+ animation: ${float} 1.5s ease-in-out infinite;
+
+ svg {
+ transform: scale(1.1);
+ fill: #6366f1;
+ }
+ }
+
+ &:active {
+ transform: scale(0.95);
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: -6px;
+ right: -6px;
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #ef4444, #dc2626);
+ opacity: 0;
+ transition: all 0.3s ease;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ }
+
+ &.has-notification::after {
+ opacity: 1;
+ animation: ${pulse} 1.5s infinite;
+ }
+`;
+
+const ActionIcon = styled.svg`
+ width: 22px;
+ height: 22px;
+ fill: #6b7280;
+ transition: all 0.3s ease;
+`;
+
+// ==================== Component Implementation ====================
+const StatusBar: React.FC = ({ currentUser }) => {
+ // ๆจกๆ้็ฅ็ถๆ
+ const hasNotifications = true;
+ const hasMessages = false;
+
+ return (
+
+
+
+
+
+
+
+ {currentUser.name}
+ {currentUser.status}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default StatusBar;
diff --git a/crates/cherry/src/components/UI/Avatar.tsx b/crates/cherry/src/components/UI/Avatar.tsx
new file mode 100644
index 0000000..2b0c08c
--- /dev/null
+++ b/crates/cherry/src/components/UI/Avatar.tsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import type { Contact } from '../../types/contact';
+
+interface AvatarProps {
+ src: string;
+ alt: string;
+ size?: 'sm' | 'md' | 'lg';
+ status?: Contact['status'];
+}
+
+const statusColors = {
+ online: 'bg-green-500',
+ offline: 'bg-gray-400',
+ busy: 'bg-red-500',
+ away: 'bg-yellow-500',
+};
+
+const Avatar: React.FC = ({
+ src,
+ alt,
+ size = 'md',
+ status
+}) => {
+ const sizeClasses = {
+ sm: 'w-8 h-8',
+ md: 'w-10 h-10',
+ lg: 'w-12 h-12',
+ };
+
+ return (
+
+

+ {status && (
+
+ )}
+
+ );
+};
+
+export default Avatar;
diff --git a/crates/cherry/src/components/WindowControls.tsx b/crates/cherry/src/components/WindowControls.tsx
new file mode 100644
index 0000000..e4387ca
--- /dev/null
+++ b/crates/cherry/src/components/WindowControls.tsx
@@ -0,0 +1,113 @@
+import React from 'react';
+import styled from 'styled-components';
+import { Window } from '@tauri-apps/api/window';
+import {
+ IoRemoveOutline,
+ IoExpandOutline,
+ IoCloseOutline
+} from 'react-icons/io5';
+
+const WindowControlsContainer = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ margin-left: auto;
+`;
+
+const WindowControlButton = styled.button<{ $variant: 'minimize' | 'maximize' | 'close' }>`
+ width: 32px;
+ height: 32px;
+ border-radius: 8px;
+ border: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(134, 239, 172, 0.2);
+ color: rgba(34, 197, 94, 0.8);
+
+ &:hover {
+ transform: scale(1.05);
+ background: ${props => {
+ switch (props.$variant) {
+ case 'minimize':
+ return 'rgba(59, 130, 246, 0.2)';
+ case 'maximize':
+ return 'rgba(16, 185, 129, 0.2)';
+ case 'close':
+ return 'rgba(239, 68, 68, 0.2)';
+ default:
+ return 'rgba(134, 239, 172, 0.2)';
+ }
+ }};
+ color: ${props => {
+ switch (props.$variant) {
+ case 'minimize':
+ return 'rgb(59, 130, 246)';
+ case 'maximize':
+ return 'rgb(16, 185, 129)';
+ case 'close':
+ return 'rgb(239, 68, 68)';
+ default:
+ return 'rgb(34, 197, 94)';
+ }
+ }};
+ }
+
+ &:active {
+ transform: scale(0.95);
+ }
+
+ svg {
+ width: 16px;
+ height: 16px;
+ }
+`;
+
+const WindowControls: React.FC = () => {
+ const handleMinimize = async () => {
+ try {
+ const window = Window.getCurrent();
+ await window.minimize();
+ } catch (error) {
+ console.error('Failed to minimize window:', error);
+ }
+ };
+
+ const handleMaximize = async () => {
+ try {
+ const window = Window.getCurrent();
+ await window.toggleMaximize();
+ } catch (error) {
+ console.error('Failed to maximize window:', error);
+ }
+ };
+
+ const handleClose = async () => {
+ try {
+ const window = Window.getCurrent();
+ await window.close();
+ } catch (error) {
+ console.error('Failed to close window:', error);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default WindowControls;
\ No newline at end of file
diff --git a/crates/cherry/src/components/settings/AppearanceSettings.tsx b/crates/cherry/src/components/settings/AppearanceSettings.tsx
new file mode 100644
index 0000000..dd6f066
--- /dev/null
+++ b/crates/cherry/src/components/settings/AppearanceSettings.tsx
@@ -0,0 +1,289 @@
+import React, { useState } from 'react';
+import styled from 'styled-components';
+import type { ThemePreference } from '../../types/settings';
+
+interface AppearanceSettingsProps {
+ darkMode: boolean;
+ setDarkMode: React.Dispatch>;
+}
+
+// ==================== Styled Components ====================
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+`;
+
+const Section = styled.div`
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(15px);
+ border-radius: 20px;
+ padding: 1.5rem;
+ border: 1px solid rgba(134, 239, 172, 0.2);
+ box-shadow:
+ 0 8px 32px rgba(0, 0, 0, 0.1),
+ 0 4px 16px rgba(0, 0, 0, 0.05);
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow:
+ 0 12px 40px rgba(0, 0, 0, 0.15),
+ 0 6px 20px rgba(0, 0, 0, 0.1);
+ border-color: rgba(134, 239, 172, 0.3);
+ }
+`;
+
+const SectionTitle = styled.h3`
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: rgba(22, 57, 35, 0.9);
+ margin: 0 0 1rem 0;
+`;
+
+const ThemeGrid = styled.div`
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 1rem;
+`;
+
+const ThemeButton = styled.button<{ $active: boolean }>`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 1.25rem 1rem;
+ border-radius: 16px;
+ border: 2px solid ${props => props.$active ? 'rgba(134, 239, 172, 0.4)' : 'rgba(134, 239, 172, 0.1)'};
+ background: ${props => props.$active ? 'rgba(134, 239, 172, 0.15)' : 'rgba(255, 255, 255, 0.05)'};
+ cursor: pointer;
+ transition: all 0.3s ease;
+ backdrop-filter: blur(10px);
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(134, 239, 172, 0.2);
+ border-color: rgba(134, 239, 172, 0.3);
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+`;
+
+const ThemeIcon = styled.div<{ $theme: string }>`
+ width: 3rem;
+ height: 3rem;
+ border-radius: 50%;
+ margin-bottom: 0.75rem;
+ background: ${props => {
+ switch (props.$theme) {
+ case 'light': return 'linear-gradient(135deg, #fbbf24, #f59e0b)';
+ case 'dark': return 'linear-gradient(135deg, #374151, #1f2937)';
+ case 'system': return 'linear-gradient(135deg, #e5e7eb, #6b7280)';
+ default: return 'linear-gradient(135deg, #e5e7eb, #6b7280)';
+ }
+ }};
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ transition: all 0.3s ease;
+
+ ${ThemeButton}:hover & {
+ transform: scale(1.1);
+ }
+`;
+
+const ThemeLabel = styled.span<{ $active: boolean }>`
+ font-size: 0.875rem;
+ font-weight: ${props => props.$active ? '600' : '500'};
+ color: ${props => props.$active ? 'rgba(22, 57, 35, 0.9)' : 'rgba(22, 57, 35, 0.7)'};
+`;
+
+const SliderContainer = styled.div`
+ margin-top: 1rem;
+`;
+
+const SliderLabel = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 0.75rem;
+`;
+
+const SliderValue = styled.span`
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: rgba(22, 57, 35, 0.8);
+ background: rgba(134, 239, 172, 0.1);
+ padding: 0.25rem 0.75rem;
+ border-radius: 12px;
+ border: 1px solid rgba(134, 239, 172, 0.2);
+`;
+
+const Slider = styled.input`
+ width: 100%;
+ height: 6px;
+ border-radius: 3px;
+ background: rgba(134, 239, 172, 0.2);
+ outline: none;
+ appearance: none;
+ cursor: pointer;
+
+ &::-webkit-slider-thumb {
+ appearance: none;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #86efac, #22c55e);
+ cursor: pointer;
+ box-shadow: 0 2px 8px rgba(134, 239, 172, 0.3);
+ transition: all 0.3s ease;
+ }
+
+ &::-webkit-slider-thumb:hover {
+ transform: scale(1.2);
+ box-shadow: 0 4px 12px rgba(134, 239, 172, 0.4);
+ }
+
+ &::-moz-range-thumb {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #86efac, #22c55e);
+ cursor: pointer;
+ border: none;
+ box-shadow: 0 2px 8px rgba(134, 239, 172, 0.3);
+ }
+`;
+
+const SliderLabels = styled.div`
+ display: flex;
+ justify-content: space-between;
+ margin-top: 0.5rem;
+`;
+
+const SliderLabelText = styled.span`
+ font-size: 0.75rem;
+ color: rgba(22, 57, 35, 0.6);
+`;
+
+const DensityGrid = styled.div`
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 0.75rem;
+`;
+
+const DensityButton = styled.button<{ $active: boolean }>`
+ padding: 0.875rem 1rem;
+ border-radius: 12px;
+ border: 1px solid ${props => props.$active ? 'rgba(134, 239, 172, 0.4)' : 'rgba(134, 239, 172, 0.1)'};
+ background: ${props => props.$active ? 'rgba(134, 239, 172, 0.15)' : 'rgba(255, 255, 255, 0.05)'};
+ color: ${props => props.$active ? 'rgba(22, 57, 35, 0.9)' : 'rgba(22, 57, 35, 0.7)'};
+ font-weight: ${props => props.$active ? '600' : '500'};
+ font-size: 0.875rem;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ backdrop-filter: blur(10px);
+
+ &:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(134, 239, 172, 0.2);
+ border-color: rgba(134, 239, 172, 0.3);
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+`;
+
+const AppearanceSettings: React.FC = ({ darkMode, setDarkMode }) => {
+ const [settings, setSettings] = useState({
+ theme: 'system' as ThemePreference,
+ fontSize: 16,
+ density: 'normal' as 'compact' | 'normal' | 'spacious',
+ });
+
+ const handleThemeChange = (theme: ThemePreference) => {
+ setSettings(prev => ({ ...prev, theme }));
+ if (theme === 'dark') {
+ setDarkMode(true);
+ } else if (theme === 'light') {
+ setDarkMode(false);
+ }
+ };
+
+ const handleFontSizeChange = (e: React.ChangeEvent) => {
+ const fontSize = parseInt(e.target.value);
+ setSettings(prev => ({ ...prev, fontSize }));
+ };
+
+ const handleDensityChange = (density: 'compact' | 'normal' | 'spacious') => {
+ setSettings(prev => ({ ...prev, density }));
+ };
+
+ return (
+
+ {/* ไธป้ข้ๆฉ */}
+
+ ไธป้ข
+
+ {(['light', 'dark', 'system'] as ThemePreference[]).map((theme) => (
+ handleThemeChange(theme)}
+ >
+
+
+ {theme === 'light' && 'ๆต
่ฒ'}
+ {theme === 'dark' && 'ๆทฑ่ฒ'}
+ {theme === 'system' && '่ท้็ณป็ป'}
+
+
+ ))}
+
+
+
+ {/* ๅญไฝๅคงๅฐ */}
+
+ ๅญไฝๅคงๅฐ
+
+
+ ่ฐๆดๅญไฝๅคงๅฐ
+ {settings.fontSize}px
+
+
+
+ ๅฐ
+ ไธญ
+ ๅคง
+
+
+
+
+ {/* ็้ขๅฏๅบฆ */}
+
+ ็้ขๅฏๅบฆ
+
+ {(['compact', 'normal', 'spacious'] as const).map((density) => (
+ handleDensityChange(density)}
+ >
+ {density === 'compact' && '็ดงๅ'}
+ {density === 'normal' && 'ๆ ๅ'}
+ {density === 'spacious' && 'ๅฎฝๆพ'}
+
+ ))}
+
+
+
+ );
+};
+
+export default AppearanceSettings;
diff --git a/crates/cherry/src/components/settings/GeneralSettings.tsx b/crates/cherry/src/components/settings/GeneralSettings.tsx
new file mode 100644
index 0000000..b4a4f13
--- /dev/null
+++ b/crates/cherry/src/components/settings/GeneralSettings.tsx
@@ -0,0 +1,319 @@
+import React, { useState } from 'react';
+import styled from 'styled-components';
+
+// ==================== Styled Components ====================
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+`;
+
+const Section = styled.div`
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(15px);
+ border-radius: 20px;
+ padding: 1.5rem;
+ border: 1px solid rgba(134, 239, 172, 0.2);
+ box-shadow:
+ 0 8px 32px rgba(0, 0, 0, 0.1),
+ 0 4px 16px rgba(0, 0, 0, 0.05);
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow:
+ 0 12px 40px rgba(0, 0, 0, 0.15),
+ 0 6px 20px rgba(0, 0, 0, 0.1);
+ border-color: rgba(134, 239, 172, 0.3);
+ }
+`;
+
+const SectionTitle = styled.h3`
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: rgba(22, 57, 35, 0.9);
+ margin: 0 0 1rem 0;
+`;
+
+const SettingItem = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem 0;
+ border-bottom: 1px solid rgba(134, 239, 172, 0.1);
+
+ &:last-child {
+ border-bottom: none;
+ padding-bottom: 0;
+ }
+`;
+
+const SettingInfo = styled.div`
+ flex: 1;
+`;
+
+const SettingLabel = styled.label`
+ font-size: 0.95rem;
+ font-weight: 500;
+ color: rgba(22, 57, 35, 0.8);
+ cursor: pointer;
+ display: block;
+ margin-bottom: 0.25rem;
+`;
+
+const SettingDescription = styled.p`
+ font-size: 0.8rem;
+ color: rgba(22, 57, 35, 0.6);
+ margin: 0;
+ line-height: 1.4;
+`;
+
+const ToggleSwitch = styled.label`
+ position: relative;
+ display: inline-block;
+ width: 3rem;
+ height: 1.5rem;
+ cursor: pointer;
+`;
+
+const ToggleInput = styled.input`
+ opacity: 0;
+ width: 0;
+ height: 0;
+
+ &:checked + span {
+ background: linear-gradient(135deg, #86efac, #22c55e);
+ }
+
+ &:checked + span:before {
+ transform: translateX(1.5rem);
+ background: white;
+ }
+`;
+
+const ToggleSlider = styled.span`
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(134, 239, 172, 0.2);
+ transition: all 0.3s ease;
+ border-radius: 1rem;
+ border: 1px solid rgba(134, 239, 172, 0.3);
+
+ &:before {
+ position: absolute;
+ content: "";
+ height: 1.125rem;
+ width: 1.125rem;
+ left: 0.125rem;
+ bottom: 0.125rem;
+ background: rgba(134, 239, 172, 0.8);
+ transition: all 0.3s ease;
+ border-radius: 50%;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ }
+
+ ${ToggleInput}:checked + & {
+ box-shadow: 0 0 0 3px rgba(134, 239, 172, 0.2);
+ }
+`;
+
+const Select = styled.select`
+ padding: 0.75rem 1rem;
+ border-radius: 12px;
+ border: 1px solid rgba(134, 239, 172, 0.2);
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(10px);
+ color: rgba(22, 57, 35, 0.8);
+ font-size: 0.9rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ min-width: 200px;
+
+ &:focus {
+ outline: none;
+ border-color: rgba(134, 239, 172, 0.4);
+ box-shadow: 0 0 0 3px rgba(134, 239, 172, 0.1);
+ }
+
+ &:hover {
+ border-color: rgba(134, 239, 172, 0.3);
+ background: rgba(255, 255, 255, 0.15);
+ }
+
+ option {
+ background: rgba(255, 255, 255, 0.95);
+ color: rgba(22, 57, 35, 0.8);
+ padding: 0.5rem;
+ }
+`;
+
+const Input = styled.input`
+ padding: 0.75rem 1rem;
+ border-radius: 12px;
+ border: 1px solid rgba(134, 239, 172, 0.2);
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(10px);
+ color: rgba(22, 57, 35, 0.8);
+ font-size: 0.9rem;
+ font-weight: 500;
+ transition: all 0.3s ease;
+ min-width: 200px;
+
+ &:focus {
+ outline: none;
+ border-color: rgba(134, 239, 172, 0.4);
+ box-shadow: 0 0 0 3px rgba(134, 239, 172, 0.1);
+ }
+
+ &:hover {
+ border-color: rgba(134, 239, 172, 0.3);
+ background: rgba(255, 255, 255, 0.15);
+ }
+
+ &::placeholder {
+ color: rgba(22, 57, 35, 0.5);
+ }
+`;
+
+const GeneralSettings: React.FC = () => {
+ const [settings, setSettings] = useState({
+ autoSave: true,
+ notifications: true,
+ sound: false,
+ language: 'zh-CN',
+ username: 'John Doe',
+ email: 'john.doe@example.com'
+ });
+
+ const handleChange = (e: React.ChangeEvent) => {
+ const { name, value, type } = e.target;
+ setSettings(prev => ({
+ ...prev,
+ [name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : value
+ }));
+ };
+
+ return (
+
+ {/* ๅบๆฌ่ฎพ็ฝฎ */}
+
+
+ {/* ้็ฅ่ฎพ็ฝฎ */}
+
+ ้็ฅ่ฎพ็ฝฎ
+
+
+
+ ๅฏ็จ้็ฅ
+ ๆฅๆถๆฐๆถๆฏๅ็ณป็ป้็ฅ
+
+
+
+
+
+
+
+
+
+ ๅฃฐ้ณๆ้
+ ๆถๅฐๆถๆฏๆถๆญๆพๆ็คบ้ณ
+
+
+
+
+
+
+
+
+ {/* ๆฐๆฎ่ฎพ็ฝฎ */}
+
+ ๆฐๆฎ่ฎพ็ฝฎ
+
+
+
+ ่ชๅจไฟๅญ
+ ่ชๅจไฟๅญ่็จฟๅ่ฎพ็ฝฎ
+
+
+
+
+
+
+
+
+ );
+};
+
+export default GeneralSettings;
diff --git a/crates/cherry/src/components/settings/NotificationSettings.tsx b/crates/cherry/src/components/settings/NotificationSettings.tsx
new file mode 100644
index 0000000..59dfca4
--- /dev/null
+++ b/crates/cherry/src/components/settings/NotificationSettings.tsx
@@ -0,0 +1,382 @@
+import React, { useState } from 'react';
+import styled from 'styled-components';
+
+// ==================== Styled Components ====================
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+`;
+
+const Section = styled.div`
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(15px);
+ border-radius: 20px;
+ padding: 1.5rem;
+ border: 1px solid rgba(134, 239, 172, 0.2);
+ box-shadow:
+ 0 8px 32px rgba(0, 0, 0, 0.1),
+ 0 4px 16px rgba(0, 0, 0, 0.05);
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow:
+ 0 12px 40px rgba(0, 0, 0, 0.15),
+ 0 6px 20px rgba(0, 0, 0, 0.1);
+ border-color: rgba(134, 239, 172, 0.3);
+ }
+`;
+
+const SectionTitle = styled.h3`
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: rgba(22, 57, 35, 0.9);
+ margin: 0 0 1rem 0;
+`;
+
+const SettingItem = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem 0;
+ border-bottom: 1px solid rgba(134, 239, 172, 0.1);
+
+ &:last-child {
+ border-bottom: none;
+ padding-bottom: 0;
+ }
+`;
+
+const SettingInfo = styled.div`
+ flex: 1;
+`;
+
+const SettingLabel = styled.label`
+ font-size: 0.95rem;
+ font-weight: 500;
+ color: rgba(22, 57, 35, 0.8);
+ cursor: pointer;
+ display: block;
+ margin-bottom: 0.25rem;
+`;
+
+const SettingDescription = styled.p`
+ font-size: 0.8rem;
+ color: rgba(22, 57, 35, 0.6);
+ margin: 0;
+ line-height: 1.4;
+`;
+
+const ToggleSwitch = styled.label`
+ position: relative;
+ display: inline-block;
+ width: 3rem;
+ height: 1.5rem;
+ cursor: pointer;
+`;
+
+const ToggleInput = styled.input`
+ opacity: 0;
+ width: 0;
+ height: 0;
+
+ &:checked + span {
+ background: linear-gradient(135deg, #86efac, #22c55e);
+ }
+
+ &:checked + span:before {
+ transform: translateX(1.5rem);
+ background: white;
+ }
+`;
+
+const ToggleSlider = styled.span`
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(134, 239, 172, 0.2);
+ transition: all 0.3s ease;
+ border-radius: 1rem;
+ border: 1px solid rgba(134, 239, 172, 0.3);
+
+ &:before {
+ position: absolute;
+ content: "";
+ height: 1.125rem;
+ width: 1.125rem;
+ left: 0.125rem;
+ bottom: 0.125rem;
+ background: rgba(134, 239, 172, 0.8);
+ transition: all 0.3s ease;
+ border-radius: 50%;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ }
+
+ ${ToggleInput}:checked + & {
+ box-shadow: 0 0 0 3px rgba(134, 239, 172, 0.2);
+ }
+`;
+
+const Select = styled.select`
+ padding: 0.75rem 1rem;
+ border-radius: 12px;
+ border: 1px solid rgba(134, 239, 172, 0.2);
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(10px);
+ color: rgba(22, 57, 35, 0.8);
+ font-size: 0.9rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ min-width: 200px;
+
+ &:focus {
+ outline: none;
+ border-color: rgba(134, 239, 172, 0.4);
+ box-shadow: 0 0 0 3px rgba(134, 239, 172, 0.1);
+ }
+
+ &:hover {
+ border-color: rgba(134, 239, 172, 0.3);
+ background: rgba(255, 255, 255, 0.15);
+ }
+
+ option {
+ background: rgba(255, 255, 255, 0.95);
+ color: rgba(22, 57, 35, 0.8);
+ padding: 0.5rem;
+ }
+`;
+
+const TimeSelect = styled.select`
+ padding: 0.5rem 0.75rem;
+ border-radius: 8px;
+ border: 1px solid rgba(134, 239, 172, 0.2);
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(10px);
+ color: rgba(22, 57, 35, 0.8);
+ font-size: 0.8rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ min-width: 120px;
+
+ &:focus {
+ outline: none;
+ border-color: rgba(134, 239, 172, 0.4);
+ box-shadow: 0 0 0 2px rgba(134, 239, 172, 0.1);
+ }
+
+ &:hover {
+ border-color: rgba(134, 239, 172, 0.3);
+ background: rgba(255, 255, 255, 0.15);
+ }
+`;
+
+const NotificationSettings: React.FC = () => {
+ const [settings, setSettings] = useState({
+ newMessages: true,
+ sound: true,
+ vibration: true,
+ previewContent: true,
+ groupMessages: true,
+ mentions: true,
+ quietHours: false,
+ quietStart: '22:00',
+ quietEnd: '08:00'
+ });
+
+ const handleChange = (e: React.ChangeEvent) => {
+ const { name, value, type } = e.target;
+ setSettings(prev => ({
+ ...prev,
+ [name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : value
+ }));
+ };
+
+ return (
+
+ {/* ๅบๆฌ้็ฅ่ฎพ็ฝฎ */}
+
+ ๅบๆฌ้็ฅ
+
+
+
+ ๆฐๆถๆฏ้็ฅ
+ ๆฅๆถๆฐๆถๆฏ็ๆจ้้็ฅ
+
+
+
+
+
+
+
+
+
+ ๆ็คบ้ณๆ
+ ๆถๅฐๆถๆฏๆถๆญๆพๆ็คบ้ณ
+
+
+
+
+
+
+
+
+
+ ๆฏๅจๆ้
+ ๆถๅฐๆถๆฏๆถ่ฎพๅคๆฏๅจ
+
+
+
+
+
+
+
+
+
+ ๆพ็คบๆถๆฏ้ข่ง
+ ๅจ้็ฅไธญๆพ็คบๆถๆฏๅ
ๅฎน
+
+
+
+
+
+
+
+
+ {/* ้ซ็บง้็ฅ่ฎพ็ฝฎ */}
+
+ ้ซ็บง้็ฅ
+
+
+
+ ็พค็ปๆถๆฏ
+ ๆฅๆถ็พค็ปๆถๆฏ็้็ฅ
+
+
+
+
+
+
+
+
+
+ @ๆๅ้็ฅ
+ ๅฝๆไบบ@ๆจๆถๅ้็นๆฎ้็ฅ
+
+
+
+
+
+
+
+
+ {/* ๅ
ๆๆฐ่ฎพ็ฝฎ */}
+
+ ๅ
ๆๆฐๆถ้ด
+
+
+
+ ๅฏ็จๅ
ๆๆฐ
+ ๅจๆๅฎๆถ้ดๆฎตๅ
้้ณ้็ฅ
+
+
+
+
+
+
+
+ {settings.quietHours && (
+ <>
+
+
+ ๅผๅงๆถ้ด
+ ๅ
ๆๆฐๆจกๅผๅผๅงๆถ้ด
+
+
+
+
+
+
+
+
+
+
+
+
+ ็ปๆๆถ้ด
+ ๅ
ๆๆฐๆจกๅผ็ปๆๆถ้ด
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
+ );
+};
+
+export default NotificationSettings;
diff --git a/crates/cherry/src/components/settings/PrivacySettings.tsx b/crates/cherry/src/components/settings/PrivacySettings.tsx
new file mode 100644
index 0000000..5db3d37
--- /dev/null
+++ b/crates/cherry/src/components/settings/PrivacySettings.tsx
@@ -0,0 +1,295 @@
+import React, { useState } from 'react';
+import styled from 'styled-components';
+
+// ==================== Styled Components ====================
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+`;
+
+const Section = styled.div`
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(15px);
+ border-radius: 20px;
+ padding: 1.5rem;
+ border: 1px solid rgba(134, 239, 172, 0.2);
+ box-shadow:
+ 0 8px 32px rgba(0, 0, 0, 0.1),
+ 0 4px 16px rgba(0, 0, 0, 0.05);
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow:
+ 0 12px 40px rgba(0, 0, 0, 0.15),
+ 0 6px 20px rgba(0, 0, 0, 0.1);
+ border-color: rgba(134, 239, 172, 0.3);
+ }
+`;
+
+const SectionTitle = styled.h3`
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: rgba(22, 57, 35, 0.9);
+ margin: 0 0 1rem 0;
+`;
+
+const SettingItem = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem 0;
+ border-bottom: 1px solid rgba(134, 239, 172, 0.1);
+
+ &:last-child {
+ border-bottom: none;
+ padding-bottom: 0;
+ }
+`;
+
+const SettingInfo = styled.div`
+ flex: 1;
+`;
+
+const SettingLabel = styled.label`
+ font-size: 0.95rem;
+ font-weight: 500;
+ color: rgba(22, 57, 35, 0.8);
+ cursor: pointer;
+ display: block;
+ margin-bottom: 0.25rem;
+`;
+
+const SettingDescription = styled.p`
+ font-size: 0.8rem;
+ color: rgba(22, 57, 35, 0.6);
+ margin: 0;
+ line-height: 1.4;
+`;
+
+const ToggleSwitch = styled.label`
+ position: relative;
+ display: inline-block;
+ width: 3rem;
+ height: 1.5rem;
+ cursor: pointer;
+`;
+
+const ToggleInput = styled.input`
+ opacity: 0;
+ width: 0;
+ height: 0;
+
+ &:checked + span {
+ background: linear-gradient(135deg, #86efac, #22c55e);
+ }
+
+ &:checked + span:before {
+ transform: translateX(1.5rem);
+ background: white;
+ }
+`;
+
+const ToggleSlider = styled.span`
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(134, 239, 172, 0.2);
+ transition: all 0.3s ease;
+ border-radius: 1rem;
+ border: 1px solid rgba(134, 239, 172, 0.3);
+
+ &:before {
+ position: absolute;
+ content: "";
+ height: 1.125rem;
+ width: 1.125rem;
+ left: 0.125rem;
+ bottom: 0.125rem;
+ background: rgba(134, 239, 172, 0.8);
+ transition: all 0.3s ease;
+ border-radius: 50%;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ }
+
+ ${ToggleInput}:checked + & {
+ box-shadow: 0 0 0 3px rgba(134, 239, 172, 0.2);
+ }
+`;
+
+const Select = styled.select`
+ padding: 0.75rem 1rem;
+ border-radius: 12px;
+ border: 1px solid rgba(134, 239, 172, 0.2);
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(10px);
+ color: rgba(22, 57, 35, 0.8);
+ font-size: 0.9rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ min-width: 200px;
+
+ &:focus {
+ outline: none;
+ border-color: rgba(134, 239, 172, 0.4);
+ box-shadow: 0 0 0 3px rgba(134, 239, 172, 0.1);
+ }
+
+ &:hover {
+ border-color: rgba(134, 239, 172, 0.3);
+ background: rgba(255, 255, 255, 0.15);
+ }
+
+ option {
+ background: rgba(255, 255, 255, 0.95);
+ color: rgba(22, 57, 35, 0.8);
+ padding: 0.5rem;
+ }
+`;
+
+const PrivacySettings: React.FC = () => {
+ const [settings, setSettings] = useState({
+ readReceipts: true,
+ lastSeen: 'contacts',
+ profileVisibility: 'contacts',
+ messagePrivacy: 'contacts',
+ dataSharing: false,
+ analytics: false
+ });
+
+ const handleChange = (e: React.ChangeEvent) => {
+ const { name, value, type } = e.target;
+ setSettings(prev => ({
+ ...prev,
+ [name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : value
+ }));
+ };
+
+ return (
+
+ {/* ๆถๆฏ้็ง */}
+
+ ๆถๆฏ้็ง
+
+
+
+ ๅทฒ่ฏปๅๆง
+ ่ฎฉ่็ณปไบบ็ฅ้ๆจไฝๆถ้
่ฏปไบไปไปฌ็ๆถๆฏ
+
+
+
+
+
+
+
+
+
+ ๆถๆฏๅฏ่งๆง
+ ๆงๅถ่ฐๅฏไปฅ็ๅฐๆจ็ๆถๆฏ็ถๆ
+
+
+
+
+
+ {/* ไธชไบบ่ตๆ้็ง */}
+
+ ไธชไบบ่ตๆ้็ง
+
+
+
+ ๆๅๅจ็บฟๆถ้ด
+ ๆงๅถ่ฐๅฏไปฅ็ๅฐๆจ็ๆๅๅจ็บฟๆถ้ด
+
+
+
+
+
+
+ ไธชไบบ่ตๆๅฏ่งๆง
+ ๆงๅถ่ฐๅฏไปฅ็ๅฐๆจ็ไธชไบบ่ตๆไฟกๆฏ
+
+
+
+
+
+ {/* ๆฐๆฎ้็ง */}
+
+ ๆฐๆฎ้็ง
+
+
+
+ ๆฐๆฎๅ
ฑไบซ
+ ๅ
่ฎธไธ็ฌฌไธๆนๅ
ฑไบซๅฟๅไฝฟ็จๆฐๆฎไปฅๆน่ฟๆๅก
+
+
+
+
+
+
+
+
+
+ ไฝฟ็จๅๆ
+ ๆถ้ไฝฟ็จๆฐๆฎไปฅๆน่ฟๅบ็จๆง่ฝๅ็จๆทไฝ้ช
+
+
+
+
+
+
+
+
+ );
+};
+
+export default PrivacySettings;
diff --git a/crates/cherry/src/components/settings/SettingsPage.tsx b/crates/cherry/src/components/settings/SettingsPage.tsx
new file mode 100644
index 0000000..4e6d092
--- /dev/null
+++ b/crates/cherry/src/components/settings/SettingsPage.tsx
@@ -0,0 +1,258 @@
+import React, { useState } from 'react';
+import styled from 'styled-components';
+import GeneralSettings from './GeneralSettings';
+import PrivacySettings from './PrivacySettings';
+import NotificationSettings from './NotificationSettings';
+import AppearanceSettings from './AppearanceSettings';
+
+type SettingCategory = 'general' | 'privacy' | 'notifications' | 'appearance';
+
+// ==================== Styled Components ====================
+const SettingsContainer = styled.div`
+ display: flex;
+ height: 100%;
+ background: linear-gradient(135deg, rgb(190, 216, 199) 0%, rgba(240, 221, 242, 0.83) 100%);
+`;
+
+const Sidebar = styled.div`
+ width: 280px;
+ // background: rgba(255, 255, 255, 0.45);
+ backdrop-filter: blur(20px);
+ border-right: 1px solid rgba(134, 239, 172, 0.2);
+ display: flex;
+ flex-direction: column;
+ box-shadow: 0 4px 20px rgba(89, 220, 209, 0.1);
+`;
+
+const SidebarHeader = styled.div`
+ padding: 2rem 1.5rem 1.5rem;
+ border-bottom: 1px solid rgba(134, 239, 172, 0.2);
+ // background: linear-gradient(135deg, rgba(134, 239, 172, 0.1), rgba(147, 197, 253, 0.05));
+`;
+
+const SidebarTitle = styled.h1`
+ font-size: 1.75rem;
+ font-weight: 700;
+ color: rgba(22, 57, 35, 0.8);
+ margin: 0;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+`;
+
+const SidebarSubtitle = styled.p`
+ font-size: 0.875rem;
+ color: rgb(11, 36, 8);
+ margin: 0.5rem 0 0;
+`;
+
+const NavigationContainer = styled.nav`
+ flex: 1;
+ padding: 1.5rem 1rem;
+ overflow-y: auto;
+
+ /* ้่ๆปๅจๆก */
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+`;
+
+const NavButton = styled.button<{ $active: boolean }>`
+ width: 100%;
+ display: flex;
+ align-items: center;
+ padding: 1rem 1.25rem;
+ margin-bottom: 0.5rem;
+ border-radius: 16px;
+ border: 1px solid ${props => props.$active ? 'rgba(134, 239, 172, 0.3)' : 'transparent'};
+ background: ${props => props.$active
+ ? 'rgba(96, 222, 140, 0.15)'
+ : 'rgba(255, 255, 255, 0.05)'};
+ color: ${props => props.$active
+ ? 'rgba(10, 40, 21, 0.9)'
+ : 'rgba(92, 193, 155, 0.7)'};
+ font-weight: ${props => props.$active ? '600' : '500'};
+ font-size: 0.95rem;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ text-align: left;
+ backdrop-filter: blur(10px);
+
+ &:hover {
+ background: ${props => props.$active
+ ? 'rgba(134, 239, 172, 0.2)'
+ : 'rgba(134, 239, 172, 0.1)'};
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(134, 239, 172, 0.2);
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+
+ &:focus {
+ outline: none !important;
+ border-color: rgba(134, 239, 172, 0.4) !important;
+ box-shadow: 0 0 0 2px rgba(134, 239, 172, 0.2) !important;
+ }
+`;
+
+const NavIcon = styled.div`
+ width: 1.5rem;
+ height: 1.5rem;
+ margin-right: 0.75rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ svg {
+ width: 1.25rem;
+ height: 1.25rem;
+ }
+`;
+
+const MainContent = styled.div`
+ flex: 1;
+ padding: 2rem;
+ overflow-y: auto;
+ // background: rgba(255, 255, 255, 0.05);
+ backdrop-filter: blur(10px);
+
+ /* ้่ๆปๅจๆก */
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+`;
+
+const ContentContainer = styled.div`
+ max-width: 800px;
+ margin: 0 auto;
+`;
+
+const ContentHeader = styled.div`
+ margin-bottom: 2rem;
+ padding-bottom: 1.5rem;
+ border-bottom: 1px solid rgba(5, 15, 9, 0.2);
+`;
+
+const ContentTitle = styled.h2`
+ font-size: 1.5rem;
+ font-weight: 600;
+ color: rgba(5, 28, 13, 0.61);
+ margin: 0 0 0.5rem 0;
+`;
+
+const ContentDescription = styled.p`
+ font-size: 0.95rem;
+ color: rgba(14, 40, 23, 0.7);
+ margin: 0;
+ line-height: 1.5;
+`;
+
+const SettingsPage: React.FC = () => {
+ const [activeCategory, setActiveCategory] = useState('general');
+ const [darkMode, setDarkMode] = useState(false);
+
+ const getCategoryInfo = (category: SettingCategory) => {
+ switch (category) {
+ case 'general':
+ return { title: '้็จ่ฎพ็ฝฎ', description: '็ฎก็ๅบ็จ็จๅบ็ๅบๆฌ่ฎพ็ฝฎๅๅๅฅฝ' };
+ case 'privacy':
+ return { title: '้็ง่ฎพ็ฝฎ', description: 'ๆงๅถๆจ็้็งๅๆฐๆฎๅฎๅ
จ้้กน' };
+ case 'notifications':
+ return { title: '้็ฅ่ฎพ็ฝฎ', description: '่ชๅฎไนๆถๆฏๅ็ณป็ป้็ฅ็ๆพ็คบๆนๅผ' };
+ case 'appearance':
+ return { title: 'ๅค่ง่ฎพ็ฝฎ', description: 'ไธชๆงๅ็้ขไธป้ขๅๆพ็คบ้้กน' };
+ default:
+ return { title: '', description: '' };
+ }
+ };
+
+ const renderSettingsContent = () => {
+ switch (activeCategory) {
+ case 'general':
+ return ;
+ case 'privacy':
+ return ;
+ case 'notifications':
+ return ;
+ case 'appearance':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const categoryInfo = getCategoryInfo(activeCategory);
+
+ return (
+
+ {/* ไพง่พนๅฏผ่ช */}
+
+
+ ่ฎพ็ฝฎ
+ ไธชๆงๅๆจ็่ๅคฉไฝ้ช
+
+
+
+ {(['general', 'privacy', 'notifications', 'appearance'] as SettingCategory[]).map((category) => (
+ setActiveCategory(category)}
+ >
+
+ {category === 'general' && (
+
+ )}
+ {category === 'privacy' && (
+
+ )}
+ {category === 'notifications' && (
+
+ )}
+ {category === 'appearance' && (
+
+ )}
+
+
+ {category === 'general' && '้็จ'}
+ {category === 'privacy' && '้็ง'}
+ {category === 'notifications' && '้็ฅ'}
+ {category === 'appearance' && 'ๅค่ง'}
+
+
+ ))}
+
+
+
+ {/* ไธปๅ
ๅฎนๅบ */}
+
+
+
+ {categoryInfo.title}
+ {categoryInfo.description}
+
+
+ {renderSettingsContent()}
+
+
+
+ );
+};
+
+export default SettingsPage;
diff --git a/crates/cherry/src/data/mockContacts.ts b/crates/cherry/src/data/mockContacts.ts
new file mode 100644
index 0000000..7775a87
--- /dev/null
+++ b/crates/cherry/src/data/mockContacts.ts
@@ -0,0 +1,123 @@
+import { Contact, ContactGroup, Group } from '../types/contact';
+
+// ๆจกๆ่็ณปไบบๆฐๆฎ
+export const mockContacts: Contact[] = [
+ {
+ id: '1',
+ name: 'ๅผ ไธ',
+ avatar: 'https://cdn3.iconfinder.com/data/icons/diversity-avatars/64/doctor-man-asian-128.png',
+ status: 'online',
+ },
+ {
+ id: '2',
+ name: 'ๆๅ',
+ avatar: 'https://cdn3.iconfinder.com/data/icons/diversity-avatars/64/doctor-man-asian-128.png',
+ status: 'busy',
+ },
+ {
+ id: '3',
+ name: '็ไบ',
+ avatar: 'https://cdn3.iconfinder.com/data/icons/diversity-avatars/64/doctor-man-asian-128.png',
+ status: 'offline',
+ },
+ {
+ id: '4',
+ name: '่ตตๅ
ญ',
+ avatar: 'https://cdn0.iconfinder.com/data/icons/avatars-158/512/man_9-256.png',
+ status: 'away',
+ },
+ {
+ id: '5',
+ name: 'ๅญไธ',
+ avatar: 'https://cdn2.iconfinder.com/data/icons/avatar-and-emotion-2/64/07-Wink-256.png',
+ status: 'offline',
+ },
+ {
+ id: '6',
+ name: 'ๅจๅ
ซ',
+ avatar: 'https://cdn3.iconfinder.com/data/icons/diversity-avatars/64/doctor-man-asian-128.png',
+ status: 'away',
+ },
+ {
+ id: '7',
+ name: 'ๅดไน',
+ avatar: 'https://cdn3.iconfinder.com/data/icons/diversity-avatars/64/doctor-man-asian-128.png',
+ status: 'offline',
+ },
+ {
+ id: '8',
+ name: '้ๅ',
+ avatar: 'https://cdn3.iconfinder.com/data/icons/diversity-avatars/64/doctor-man-asian-128.png',
+ status: 'away',
+ },
+ {
+ id: '9',
+ name: '้ๅไธ',
+ avatar: 'https://cdn3.iconfinder.com/data/icons/diversity-avatars/64/doctor-man-asian-128.png',
+ status: 'offline',
+ },
+ {
+ id: '10',
+ name: 'ๅๅไบ',
+ avatar: 'https://cdn3.iconfinder.com/data/icons/diversity-avatars/64/doctor-man-asian-128.png',
+ status: 'away',
+ }
+];
+
+// ๆจกๆ่็ณปไบบๅ็ป
+export const mockContactGroups: ContactGroup[] = [
+ {
+ id: 'g1',
+ name: '้กน็ฎA่ฎพ่ฎก',
+ contacts: mockContacts.slice(0, 3),
+ },
+ {
+ id: 'g2',
+ name: '้กน็ฎA็ ๅ',
+ contacts: mockContacts.slice(3, 10),
+ },
+];
+
+// ๆจกๆๅๅปบ็็พค็ป
+export const mockOwnedGroups: Group[] = [
+ {
+ id: 'gr1',
+ name: 'ไบงๅ่ฎพ่ฎก่ฎจ่ฎบ็ป',
+ avatar: 'https://cdn3.iconfinder.com/data/icons/diversity-avatars/64/doctor-man-asian-128.png',
+ memberCount: 15,
+ isOwner: true,
+ },
+ // ๆดๅคๅๅปบ็็พค...
+];
+
+// ๆจกๆๅ ๅ
ฅ็็พค็ป
+export const mockJoinedGroups: Group[] = [
+ {
+ id: 'gr2',
+ name: 'ๅ็ซฏๆๆฏไบคๆต',
+ avatar: 'https://cdn4.iconfinder.com/data/icons/avatars-xmas-giveaway/128/boy_male_avatar_portrait-128.png',
+ memberCount: 42,
+ isOwner: false,
+ },
+ {
+ id: 'gr3',
+ name: 'ๅ็ซฏๆๆฏไบคๆต',
+ avatar: 'https://cdn4.iconfinder.com/data/icons/avatars-xmas-giveaway/128/boy_male_avatar_portrait-128.png',
+ memberCount: 42,
+ isOwner: false,
+ },
+ {
+ id: 'gr4',
+ name: 'ๅ็ซฏๆๆฏไบคๆต',
+ avatar: 'https://cdn1.iconfinder.com/data/icons/male-2/64/man_male_boy_cute_person_avatar_people-05-256.png',
+ memberCount: 42,
+ isOwner: false,
+ },
+ {
+ id: 'gr5',
+ name: 'ๅ็ซฏๆๆฏไบคๆต',
+ avatar: 'https://cdn3.iconfinder.com/data/icons/diversity-avatars/64/doctor-man-asian-128.png',
+ memberCount: 42,
+ isOwner: false,
+ }
+];
diff --git a/crates/cherry/src/hooks/useWindowsSize.ts b/crates/cherry/src/hooks/useWindowsSize.ts
new file mode 100644
index 0000000..aba1e86
--- /dev/null
+++ b/crates/cherry/src/hooks/useWindowsSize.ts
@@ -0,0 +1,28 @@
+// src/hooks/useWindowSize.ts
+import { useState, useEffect } from 'react';
+
+interface WindowSize {
+ width: number;
+ height: number;
+}
+
+export function useWindowSize(): WindowSize {
+ const [windowSize, setWindowSize] = useState({
+ width: window.innerWidth,
+ height: window.innerHeight,
+ });
+
+ useEffect(() => {
+ function handleResize() {
+ setWindowSize({
+ width: window.innerWidth,
+ height: window.innerHeight,
+ });
+ }
+
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
+
+ return windowSize;
+}
diff --git a/crates/cherry/src/main.tsx b/crates/cherry/src/main.tsx
new file mode 100644
index 0000000..49d3b7a
--- /dev/null
+++ b/crates/cherry/src/main.tsx
@@ -0,0 +1,10 @@
+import React from "react";
+import ReactDOM from "react-dom/client";
+import App from "./App";
+import "./App.css";
+
+ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
+
+
+ ,
+);
diff --git a/crates/cherry/src/pages/login.tsx b/crates/cherry/src/pages/login.tsx
new file mode 100644
index 0000000..0595e11
--- /dev/null
+++ b/crates/cherry/src/pages/login.tsx
@@ -0,0 +1,474 @@
+import React, { useState, ChangeEvent, FormEvent } from 'react';
+import styled from 'styled-components';
+import "../App.css";
+
+interface FormErrors {
+ [key: string]: string | undefined;
+ email?: string;
+ password?: string;
+}
+
+interface FormData {
+ email: string;
+ password: string;
+ rememberMe: boolean;
+}
+
+// ==================== Styled Components ====================
+const LoginContainer = styled.div`
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ position: relative;
+ overflow: hidden;
+ padding: 1rem;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background:
+ radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
+ radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
+ radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.2) 0%, transparent 50%);
+ pointer-events: none;
+ }
+`;
+
+const LoginCard = styled.div`
+ width: 100%;
+ max-width: 28rem;
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(20px);
+ border-radius: 24px;
+ box-shadow:
+ 0 8px 32px rgba(0, 0, 0, 0.1),
+ 0 4px 16px rgba(0, 0, 0, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ overflow: hidden;
+ position: relative;
+ z-index: 1;
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: translateY(-4px);
+ box-shadow:
+ 0 12px 40px rgba(0, 0, 0, 0.15),
+ 0 6px 20px rgba(0, 0, 0, 0.1);
+ }
+`;
+
+const HeaderSection = styled.div`
+ background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
+ padding: 2rem;
+ text-align: center;
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background:
+ radial-gradient(circle at 30% 70%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
+ radial-gradient(circle at 70% 30%, rgba(255, 255, 255, 0.1) 0%, transparent 50%);
+ }
+`;
+
+const HeaderTitle = styled.h2`
+ font-size: 2rem;
+ font-weight: 700;
+ color: white;
+ margin: 0;
+ position: relative;
+ z-index: 1;
+`;
+
+const HeaderSubtitle = styled.p`
+ color: rgba(255, 255, 255, 0.8);
+ margin: 0.5rem 0 0 0;
+ font-size: 1rem;
+ position: relative;
+ z-index: 1;
+`;
+
+const FormContainer = styled.form`
+ padding: 2rem;
+`;
+
+const FormGroup = styled.div`
+ margin-bottom: 1.5rem;
+`;
+
+const Label = styled.label`
+ display: block;
+ color: rgba(255, 255, 255, 0.9);
+ font-size: 0.875rem;
+ font-weight: 600;
+ margin-bottom: 0.5rem;
+`;
+
+const InputWrapper = styled.div`
+ position: relative;
+`;
+
+const InputIcon = styled.div`
+ position: absolute;
+ left: 1rem;
+ top: 50%;
+ transform: translateY(-50%);
+ color: rgba(255, 255, 255, 0.6);
+ z-index: 1;
+`;
+
+const Input = styled.input<{ $hasError: boolean }>`
+ width: 100%;
+ padding: 1rem 1rem 1rem 3rem;
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(10px);
+ border: 1px solid ${({ $hasError }) => $hasError ? 'rgba(239, 68, 68, 0.5)' : 'rgba(255, 255, 255, 0.2)'};
+ border-radius: 16px;
+ color: white;
+ font-size: 1rem;
+ transition: all 0.3s ease;
+
+ &::placeholder {
+ color: rgba(255, 255, 255, 0.5);
+ }
+
+ &:focus {
+ outline: none;
+ background: rgba(255, 255, 255, 0.15);
+ border-color: rgba(99, 102, 241, 0.5);
+ box-shadow:
+ 0 0 0 3px rgba(99, 102, 241, 0.1),
+ 0 4px 20px rgba(0, 0, 0, 0.1);
+ transform: translateY(-1px);
+ }
+`;
+
+const ErrorMessage = styled.p`
+ margin: 0.5rem 0 0 0;
+ font-size: 0.875rem;
+ color: #fca5a5;
+ font-weight: 500;
+`;
+
+const CheckboxRow = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 1.5rem;
+`;
+
+const CheckboxGroup = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+`;
+
+const Checkbox = styled.input`
+ width: 1rem;
+ height: 1rem;
+ accent-color: #6366f1;
+ border-radius: 4px;
+`;
+
+const CheckboxLabel = styled.label`
+ color: rgba(255, 255, 255, 0.8);
+ font-size: 0.875rem;
+ cursor: pointer;
+`;
+
+const ForgotLink = styled.a`
+ color: rgba(255, 255, 255, 0.8);
+ font-size: 0.875rem;
+ text-decoration: none;
+ font-weight: 500;
+ transition: color 0.3s ease;
+
+ &:hover {
+ color: white;
+ }
+`;
+
+const SubmitButton = styled.button<{ $isSubmitting: boolean }>`
+ width: 100%;
+ padding: 1rem;
+ background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
+ color: white;
+ border: none;
+ border-radius: 16px;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: ${({ $isSubmitting }) => $isSubmitting ? 'not-allowed' : 'pointer'};
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ box-shadow: 0 4px 20px rgba(99, 102, 241, 0.3);
+
+ &:hover {
+ ${({ $isSubmitting }) => !$isSubmitting && `
+ transform: translateY(-2px);
+ box-shadow: 0 6px 25px rgba(99, 102, 241, 0.4);
+ `}
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+
+ ${({ $isSubmitting }) => $isSubmitting && `
+ opacity: 0.7;
+ `}
+`;
+
+const FooterSection = styled.div`
+ background: rgba(255, 255, 255, 0.05);
+ padding: 1.5rem 2rem;
+ text-align: center;
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
+`;
+
+const FooterText = styled.p`
+ color: rgba(255, 255, 255, 0.7);
+ font-size: 0.875rem;
+ margin: 0;
+`;
+
+const SignUpLink = styled.a`
+ color: rgba(255, 255, 255, 0.9);
+ font-weight: 600;
+ text-decoration: none;
+ transition: color 0.3s ease;
+
+ &:hover {
+ color: white;
+ }
+`;
+
+const SocialLoginSection = styled.div`
+ margin-top: 1.5rem;
+ text-align: center;
+`;
+
+const SocialText = styled.p`
+ color: rgba(255, 255, 255, 0.7);
+ font-size: 0.875rem;
+ margin-bottom: 1rem;
+`;
+
+const SocialButtons = styled.div`
+ display: flex;
+ justify-content: center;
+ gap: 1rem;
+`;
+
+const SocialButton = styled.button`
+ padding: 0.75rem;
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 16px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.15);
+ transform: translateY(-2px);
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
+ }
+
+ svg {
+ width: 1.25rem;
+ height: 1.25rem;
+ color: rgba(255, 255, 255, 0.8);
+ }
+`;
+
+const LoginForm = () => {
+ const [formData, setFormData] = useState({
+ email: '',
+ password: '',
+ rememberMe: false
+ });
+
+ const [errors, setErrors] = useState({});
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const handleChange = (e: ChangeEvent) => {
+ const { name, value, type, checked } = e.target;
+ setFormData(prev => ({
+ ...prev,
+ [name]: type === 'checkbox' ? checked : value
+ }));
+
+ // ๆธ
้คๅฏนๅบๅญๆฎต็้่ฏฏ
+ if (errors[name]) {
+ setErrors(prev => {
+ const newErrors = { ...prev };
+ delete newErrors[name];
+ return newErrors;
+ });
+ }
+ };
+
+ const validate = () => {
+ const newErrors: FormErrors = {};
+
+ if (!formData.email) {
+ newErrors.email = 'Email is required';
+ } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
+ newErrors.email = 'Email address is invalid';
+ }
+
+ if (!formData.password) {
+ newErrors.password = 'Password is required';
+ } else if (formData.password.length < 6) {
+ newErrors.password = 'Password must be at least 6 characters';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSubmit = (e: FormEvent) => {
+ e.preventDefault();
+
+ if (validate()) {
+ setIsSubmitting(true);
+
+ // ๆจกๆAPI่ฏทๆฑ
+ setTimeout(() => {
+ console.log('Login data:', formData);
+ setIsSubmitting(false);
+ alert('Login successful!');
+ }, 1500);
+ }
+ };
+
+ return (
+
+
+
+ Welcome Back
+ Sign in to your account
+
+
+
+
+
+
+
+
+
+
+
+ {errors.email && {errors.email}}
+
+
+
+
+
+
+
+
+
+
+ {errors.password && {errors.password}}
+
+
+
+
+
+ Remember me
+
+ Forgot password?
+
+
+
+ {isSubmitting ? (
+ <>
+
+ Signing in...
+ >
+ ) : 'Sign in'}
+
+
+
+
+
+ Don't have an account?{' '}
+ Sign up
+
+
+
+
+
+ Or continue with
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default LoginForm;
diff --git a/crates/cherry/src/store/message.ts b/crates/cherry/src/store/message.ts
new file mode 100644
index 0000000..4099468
--- /dev/null
+++ b/crates/cherry/src/store/message.ts
@@ -0,0 +1,35 @@
+import { createSlice } from '@reduxjs/toolkit'
+import type { PayloadAction } from '@reduxjs/toolkit'
+
+export interface CounterState {
+ value: number
+}
+
+const initialState: CounterState = {
+ value: 0,
+}
+
+export const counterSlice = createSlice({
+ name: 'counter',
+ initialState,
+ reducers: {
+ increment: (state) => {
+ // Redux Toolkit allows us to write "mutating" logic in reducers. It
+ // doesn't actually mutate the state because it uses the Immer library,
+ // which detects changes to a "draft state" and produces a brand new
+ // immutable state based off those changes
+ state.value += 1
+ },
+ decrement: (state) => {
+ state.value -= 1
+ },
+ incrementByAmount: (state, action: PayloadAction) => {
+ state.value += action.payload
+ },
+ },
+})
+
+// Action creators are generated for each case reducer function
+export const { increment, decrement, incrementByAmount } = counterSlice.actions
+
+export default counterSlice.reducer
\ No newline at end of file
diff --git a/crates/cherry/src/types/contact.d.ts b/crates/cherry/src/types/contact.d.ts
new file mode 100644
index 0000000..edc9dd7
--- /dev/null
+++ b/crates/cherry/src/types/contact.d.ts
@@ -0,0 +1,25 @@
+// ๅบ็ก่็ณปไบบ็ฑปๅ
+export interface Contact {
+ id: string;
+ name: string;
+ avatar: string;
+ status: 'online' | 'offline' | 'busy' | 'away';
+ lastActive?: Date;
+ }
+
+ // ็พค็ป็ฑปๅ
+ export interface Group {
+ id: string;
+ name: string;
+ avatar: string;
+ memberCount: number;
+ isOwner: boolean; // ๅบๅๅๅปบ่
/ๆๅ
+ }
+
+ // ่็ณปไบบๅ็ป็ฑปๅ
+ export interface ContactGroup {
+ id: string;
+ name: string;
+ contacts: Contact[];
+ }
+
\ No newline at end of file
diff --git a/crates/cherry/src/types/settings.d.ts b/crates/cherry/src/types/settings.d.ts
new file mode 100644
index 0000000..b5a3806
--- /dev/null
+++ b/crates/cherry/src/types/settings.d.ts
@@ -0,0 +1,34 @@
+// ่ฎพ็ฝฎ้กน็ฑปๅ
+export type SettingCategory = 'general' | 'privacy' | 'notifications' | 'appearance';
+
+// ไธป้ข็ฑปๅ
+export type ThemePreference = 'light' | 'dark' | 'system';
+
+// ้็จ่ฎพ็ฝฎ
+export interface GeneralSettings {
+ startup: boolean;
+ language: string;
+ sendWithEnter: boolean;
+}
+
+// ้็ง่ฎพ็ฝฎ
+export interface PrivacySettings {
+ readReceipts: boolean;
+ onlineStatus: 'all' | 'contacts' | 'none';
+ messageHistory: 'forever' | '30days' | '7days';
+}
+
+// ้็ฅ่ฎพ็ฝฎ
+export interface NotificationSettings {
+ messageAlerts: boolean;
+ sound: boolean;
+ vibration: boolean;
+ previewContent: boolean;
+}
+
+// ๅค่ง่ฎพ็ฝฎ
+export interface AppearanceSettings {
+ theme: ThemePreference;
+ fontSize: number;
+ density: 'compact' | 'normal' | 'spacious';
+}
diff --git a/crates/cherry/src/types/types.ts b/crates/cherry/src/types/types.ts
new file mode 100644
index 0000000..942a810
--- /dev/null
+++ b/crates/cherry/src/types/types.ts
@@ -0,0 +1,30 @@
+// src/types/types.ts
+export interface User {
+ id: string;
+ name: string;
+ avatar: string;
+ status: 'online' | 'offline' | 'away';
+ lastSeen?: string;
+ }
+
+ export interface Message {
+ id: string;
+ userId: string;
+ content: string;
+ timestamp: string;
+ isOwn?: boolean;
+ status?: 'sent' | 'delivered' | 'read';
+ }
+
+ export interface Conversation {
+ id: string;
+ name: string;
+ avatar: string;
+ type: 'direct' | 'group';
+ mentions: number;
+ participants: User[];
+ lastMessage?: Message;
+ messages: Message[];
+ unreadCount: number;
+ }
+
\ No newline at end of file
diff --git a/crates/cherry/src/vite-env.d.ts b/crates/cherry/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/crates/cherry/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/crates/cherry/tsconfig.json b/crates/cherry/tsconfig.json
new file mode 100644
index 0000000..a7fc6fb
--- /dev/null
+++ b/crates/cherry/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/crates/cherry/tsconfig.node.json b/crates/cherry/tsconfig.node.json
new file mode 100644
index 0000000..42872c5
--- /dev/null
+++ b/crates/cherry/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/crates/cherry/vite.config.ts b/crates/cherry/vite.config.ts
new file mode 100644
index 0000000..10ea71f
--- /dev/null
+++ b/crates/cherry/vite.config.ts
@@ -0,0 +1,33 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import tailwindcss from '@tailwindcss/vite'
+
+// @ts-expect-error process is a nodejs global
+const host = process.env.TAURI_DEV_HOST;
+
+// https://vitejs.dev/config/
+export default defineConfig(async () => ({
+ plugins: [react(), tailwindcss()],
+
+ // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
+ //
+ // 1. prevent vite from obscuring rust errors
+ clearScreen: false,
+ // 2. tauri expects a fixed port, fail if that port is not available
+ server: {
+ port: 1420,
+ strictPort: true,
+ host: host || false,
+ hmr: host
+ ? {
+ protocol: "ws",
+ host,
+ port: 1421,
+ }
+ : undefined,
+ watch: {
+ // 3. tell vite to ignore watching `src-tauri`
+ ignored: ["**/src-tauri/**"],
+ },
+ },
+}));
diff --git a/crates/cherrycore/Cargo.toml b/crates/cherrycore/Cargo.toml
new file mode 100644
index 0000000..00eb613
--- /dev/null
+++ b/crates/cherrycore/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "cherrycore"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+anyhow = "1.0.98"
+axum = "0.8.4"
+axum-extra = { version = "0.10.1", features = ["typed-header"] }
+chrono = { version = "0.4.41", features = ["serde"] }
+jsonwebtoken = "9.3.1"
+serde = { version = "1.0.219", features = ["derive", "rc", "serde_derive"] }
+serde_json = "1.0.140"
+serde_with = { version = "3.13.0", features = ["base64"] }
+serde_yaml = "0.9.34"
+sqlx = "0.8.6"
+uuid = { version = "1.17.0", features = ["serde", "v4"] }
+tokio = { version = "1.45.1", features = ["macros", "rt-multi-thread"] }
+# diesel = "2.2.10"
+# diesel-async = { version = "0.3.1", features = ["postgres"] }
diff --git a/crates/cherrycore/src/jwt.rs b/crates/cherrycore/src/jwt.rs
new file mode 100644
index 0000000..80ee92a
--- /dev/null
+++ b/crates/cherrycore/src/jwt.rs
@@ -0,0 +1,286 @@
+use anyhow::Result;
+use axum::{
+ RequestPartsExt,
+ extract::FromRequestParts,
+ http::{StatusCode, request::Parts},
+ response::{IntoResponse, Response},
+};
+use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
+use serde::{Deserialize, Serialize};
+use std::sync::LazyLock;
+use uuid::Uuid;
+
+use axum_extra::{
+ TypedHeader,
+ headers::{Authorization, authorization::Bearer},
+};
+
+use crate::types::ResponseError;
+
+#[derive(Clone)]
+pub struct JwtConfig {
+ pub secret: String,
+ pub expire_time: u64,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct JwtClaims {
+ pub user_id: Uuid, // ็จๆทID
+ pub exp: u64, // ่ฟๆๆถ้ด
+ pub iat: u64, // ๅๅปบๆถ้ด
+}
+
+struct Keys {
+ encoding: EncodingKey,
+ decoding: DecodingKey,
+}
+
+impl Keys {
+ fn new(secret: &[u8]) -> Self {
+ Self {
+ encoding: EncodingKey::from_secret(secret),
+ decoding: DecodingKey::from_secret(secret),
+ }
+ }
+}
+
+static KEYS: LazyLock = LazyLock::new(|| {
+ let secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
+ Keys::new(secret.as_bytes())
+});
+
+#[derive(Debug)]
+pub enum AuthError {
+ WrongCredentials,
+ MissingCredentials,
+ TokenCreation,
+ InvalidToken,
+}
+
+impl std::fmt::Display for AuthError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ AuthError::WrongCredentials => write!(f, "Wrong credentials"),
+ AuthError::MissingCredentials => write!(f, "Missing credentials"),
+ AuthError::TokenCreation => write!(f, "Token creation error"),
+ AuthError::InvalidToken => write!(f, "Invalid token"),
+ }
+ }
+}
+
+impl IntoResponse for AuthError {
+ fn into_response(self) -> Response {
+ let status = match self {
+ AuthError::WrongCredentials => StatusCode::UNAUTHORIZED,
+ AuthError::MissingCredentials => StatusCode::BAD_REQUEST,
+ AuthError::TokenCreation => StatusCode::INTERNAL_SERVER_ERROR,
+ AuthError::InvalidToken => StatusCode::BAD_REQUEST,
+ };
+ (status, self.to_string()).into_response()
+ }
+}
+
+impl FromRequestParts for JwtClaims
+where
+ S: Send + Sync,
+{
+ type Rejection = AuthError;
+
+ async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result {
+ // Extract the token from the authorization header
+ let TypedHeader(Authorization(bearer)) = parts
+ .extract::>>()
+ .await
+ .map_err(|_| AuthError::InvalidToken)?;
+ // Decode the user data
+ let token_data =
+ decode::(bearer.token(), &KEYS.decoding, &Validation::default())
+ .map_err(|_| AuthError::InvalidToken)?;
+
+ Ok(token_data.claims)
+ }
+}
+
+impl JwtClaims {
+ pub fn new(user_id: Uuid, expire_time: u64) -> Self {
+ Self {
+ user_id,
+ exp: chrono::Utc::now().timestamp() as u64 + expire_time,
+ iat: chrono::Utc::now().timestamp() as u64,
+ }
+ }
+
+ pub fn from_token(token: &str) -> Result {
+ let token_data = decode::(token, &KEYS.decoding, &Validation::default())
+ .map_err(|_| AuthError::InvalidToken)?;
+ Ok(token_data.claims)
+ }
+
+ pub fn to_token(&self) -> Result {
+ let token = encode::(&Header::default(), self, &KEYS.encoding)
+ .map_err(|_| AuthError::TokenCreation)?;
+ Ok(token)
+ }
+}
+
+impl From for ResponseError {
+ fn from(error: AuthError) -> Self {
+ Self::AuthError(error)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::env;
+ use axum::http::{HeaderMap, HeaderValue};
+ use axum_extra::headers::authorization::Bearer;
+
+ fn setup_test_env() {
+ unsafe {
+ env::set_var("JWT_SECRET", "test_secret_key_for_testing");
+ }
+ }
+
+ #[test]
+ fn test_jwt_config() {
+ let config = JwtConfig {
+ secret: "test_secret".to_string(),
+ expire_time: 3600,
+ };
+ assert_eq!(config.secret, "test_secret");
+ assert_eq!(config.expire_time, 3600);
+ }
+
+ #[test]
+ fn test_jwt_claims_new() {
+ let user_id = Uuid::new_v4();
+ let expire_time = 3600;
+ let claims = JwtClaims::new(user_id, expire_time);
+
+ assert_eq!(claims.user_id, user_id);
+ assert!(claims.exp > claims.iat);
+ assert_eq!(claims.exp - claims.iat, expire_time);
+ }
+
+ #[test]
+ fn test_jwt_claims_to_token_and_from_token() {
+ setup_test_env();
+
+ let user_id = Uuid::new_v4();
+ let claims = JwtClaims::new(user_id, 3600);
+
+ // Test token creation
+ let token = claims.to_token().unwrap();
+ assert!(!token.is_empty());
+
+ // Test token parsing
+ let parsed_claims = JwtClaims::from_token(&token).unwrap();
+ assert_eq!(parsed_claims.user_id, user_id);
+ assert_eq!(parsed_claims.exp, claims.exp);
+ assert_eq!(parsed_claims.iat, claims.iat);
+ }
+
+ #[test]
+ fn test_jwt_claims_from_invalid_token() {
+ setup_test_env();
+
+ let result = JwtClaims::from_token("invalid_token");
+ assert!(result.is_err());
+ assert!(matches!(result.unwrap_err(), AuthError::InvalidToken));
+ }
+
+ #[test]
+ fn test_auth_error_display() {
+ assert_eq!(AuthError::WrongCredentials.to_string(), "Wrong credentials");
+ assert_eq!(AuthError::MissingCredentials.to_string(), "Missing credentials");
+ assert_eq!(AuthError::TokenCreation.to_string(), "Token creation error");
+ assert_eq!(AuthError::InvalidToken.to_string(), "Invalid token");
+ }
+
+ #[test]
+ fn test_auth_error_into_response() {
+ use axum::response::IntoResponse;
+
+ let response = AuthError::WrongCredentials.into_response();
+ assert_eq!(response.status(), axum::http::StatusCode::UNAUTHORIZED);
+
+ let response = AuthError::MissingCredentials.into_response();
+ assert_eq!(response.status(), axum::http::StatusCode::BAD_REQUEST);
+
+ let response = AuthError::TokenCreation.into_response();
+ assert_eq!(response.status(), axum::http::StatusCode::INTERNAL_SERVER_ERROR);
+
+ let response = AuthError::InvalidToken.into_response();
+ assert_eq!(response.status(), axum::http::StatusCode::BAD_REQUEST);
+ }
+
+ #[test]
+ fn test_auth_error_debug() {
+ let error = AuthError::InvalidToken;
+ let debug_str = format!("{:?}", error);
+ assert!(debug_str.contains("InvalidToken"));
+ }
+
+ #[test]
+ fn test_keys_new() {
+ let secret = b"test_secret";
+ let keys = Keys::new(secret);
+ // We can't directly test the keys, but we can test that they work together
+ let test_claims = JwtClaims {
+ user_id: Uuid::new_v4(),
+ exp: chrono::Utc::now().timestamp() as u64 + 3600,
+ iat: chrono::Utc::now().timestamp() as u64,
+ };
+
+ let token = encode(&Header::default(), &test_claims, &keys.encoding).unwrap();
+ let decoded = decode::(&token, &keys.decoding, &Validation::default()).unwrap();
+ assert_eq!(decoded.claims.user_id, test_claims.user_id);
+ }
+
+ #[test]
+ fn test_jwt_claims_serialization() {
+ let user_id = Uuid::new_v4();
+ let claims = JwtClaims {
+ user_id,
+ exp: 1234567890,
+ iat: 1234567800,
+ };
+
+ let json = serde_json::to_string(&claims).unwrap();
+ let deserialized: JwtClaims = serde_json::from_str(&json).unwrap();
+
+ assert_eq!(deserialized.user_id, user_id);
+ assert_eq!(deserialized.exp, 1234567890);
+ assert_eq!(deserialized.iat, 1234567800);
+ }
+
+ #[test]
+ fn test_jwt_claims_debug() {
+ let user_id = Uuid::new_v4();
+ let claims = JwtClaims {
+ user_id,
+ exp: 1234567890,
+ iat: 1234567800,
+ };
+
+ let debug_str = format!("{:?}", claims);
+ assert!(debug_str.contains("user_id"));
+ assert!(debug_str.contains("exp"));
+ assert!(debug_str.contains("iat"));
+ }
+
+ // Note: Testing FromRequestParts requires complex setup with axum internals
+ // These tests would be better suited for integration tests
+
+ #[test]
+ fn test_auth_error_from_to_response_error() {
+ let auth_error = AuthError::InvalidToken;
+ let response_error: ResponseError = auth_error.into();
+
+ match response_error {
+ ResponseError::AuthError(AuthError::InvalidToken) => {},
+ _ => panic!("Expected AuthError::InvalidToken"),
+ }
+ }
+}
diff --git a/crates/cherrycore/src/lib.rs b/crates/cherrycore/src/lib.rs
new file mode 100644
index 0000000..c658cb3
--- /dev/null
+++ b/crates/cherrycore/src/lib.rs
@@ -0,0 +1,2 @@
+pub mod jwt;
+pub mod types;
diff --git a/crates/cherrycore/src/types.rs b/crates/cherrycore/src/types.rs
new file mode 100644
index 0000000..8940db1
--- /dev/null
+++ b/crates/cherrycore/src/types.rs
@@ -0,0 +1,468 @@
+
+
+use axum::{
+ http::StatusCode,
+ response::{IntoResponse, Response},
+};
+
+use chrono::DateTime;
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+use serde_with::{base64::Base64, serde_as};
+use uuid::Uuid;
+
+use crate::jwt::AuthError;
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct LoginRequest {
+ #[serde(rename = "type")]
+ pub type_: String, // username_password, github_oauth
+ pub username: Option,
+ pub password: Option,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct LoginResponse {
+ pub user_id: Uuid,
+ pub username: String,
+ pub email: String,
+ pub avatar_url: Option,
+ pub status: String,
+ pub jwt_token: String,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+
+pub struct StreamAppendRequest {
+ pub stream_id: u64,
+ pub data: Option>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct StreamReadRequest {
+ pub stream_id: u64,
+ pub offset: u64,
+}
+
+#[serde_as]
+#[derive(Debug, Serialize, Deserialize)]
+pub struct StreamReadResponse {
+ pub stream_id: u64,
+ pub offset: u64,
+ #[serde_as(as = "Base64")]
+ pub data: Vec,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct StreamAppendResponse {
+ pub stream_id: u64,
+ pub offset: u64, // ๅ็งป้
+}
+
+pub enum ResponseError {
+ InternalError(anyhow::Error),
+ AuthError(AuthError),
+ DataEmpty,
+ DataTooLarge,
+ DataInvalid,
+ StreamNotFound,
+ Forbidden,
+}
+
+impl IntoResponse for ResponseError {
+ fn into_response(self) -> Response {
+ match self {
+ Self::InternalError(error) => {
+ (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response()
+ }
+ Self::AuthError(error) => (StatusCode::UNAUTHORIZED, error.to_string()).into_response(),
+ Self::DataEmpty => (StatusCode::BAD_REQUEST, "data is empty").into_response(),
+ Self::DataTooLarge => (StatusCode::BAD_REQUEST, "data is too large").into_response(),
+ Self::DataInvalid => (StatusCode::BAD_REQUEST, "data is invalid").into_response(),
+ Self::StreamNotFound => (StatusCode::NOT_FOUND, "stream not found").into_response(),
+ Self::Forbidden => (StatusCode::FORBIDDEN, "forbidden").into_response(),
+ }
+ }
+}
+
+impl From for ResponseError {
+ fn from(error: anyhow::Error) -> Self {
+ Self::InternalError(error)
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct ListStreamRequest {
+ pub user_id: Uuid,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Stream {
+ pub stream_id: i64,
+ pub owner_id: Uuid,
+ pub stream_type: String,
+ pub status: String,
+ pub offset: i64,
+ pub stream_meta: Value,
+ pub created_at: DateTime,
+ pub updated_at: DateTime,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct ListStreamResponse {
+ pub streams: Vec,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Conversation {
+ pub conversation_id: Uuid,
+ pub conversation_type: String,
+ pub members: Value,
+ pub meta: Value,
+ pub stream_id: i64,
+ pub created_at: DateTime,
+ pub updated_at: DateTime,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct ListConversationsResponse {
+ pub conversations: Vec,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct StreamErrorResponse {
+ pub error: String,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use axum::response::IntoResponse;
+ use serde_json::json;
+
+ #[test]
+ fn test_login_request_serialization() {
+ let request = LoginRequest {
+ type_: "username_password".to_string(),
+ username: Some("testuser".to_string()),
+ password: Some("testpass".to_string()),
+ };
+
+ let json = serde_json::to_string(&request).unwrap();
+ let deserialized: LoginRequest = serde_json::from_str(&json).unwrap();
+
+ assert_eq!(deserialized.type_, "username_password");
+ assert_eq!(deserialized.username, Some("testuser".to_string()));
+ assert_eq!(deserialized.password, Some("testpass".to_string()));
+ }
+
+ #[test]
+ fn test_login_request_oauth() {
+ let request = LoginRequest {
+ type_: "github_oauth".to_string(),
+ username: None,
+ password: None,
+ };
+
+ let json = serde_json::to_string(&request).unwrap();
+ let deserialized: LoginRequest = serde_json::from_str(&json).unwrap();
+
+ assert_eq!(deserialized.type_, "github_oauth");
+ assert_eq!(deserialized.username, None);
+ assert_eq!(deserialized.password, None);
+ }
+
+ #[test]
+ fn test_login_response_serialization() {
+ let user_id = Uuid::new_v4();
+ let response = LoginResponse {
+ user_id,
+ username: "testuser".to_string(),
+ email: "test@example.com".to_string(),
+ avatar_url: Some("https://example.com/avatar.jpg".to_string()),
+ status: "active".to_string(),
+ jwt_token: "test_token".to_string(),
+ };
+
+ let json = serde_json::to_string(&response).unwrap();
+ let deserialized: LoginResponse = serde_json::from_str(&json).unwrap();
+
+ assert_eq!(deserialized.user_id, user_id);
+ assert_eq!(deserialized.username, "testuser");
+ assert_eq!(deserialized.email, "test@example.com");
+ assert_eq!(deserialized.avatar_url, Some("https://example.com/avatar.jpg".to_string()));
+ assert_eq!(deserialized.status, "active");
+ assert_eq!(deserialized.jwt_token, "test_token");
+ }
+
+ #[test]
+ fn test_stream_append_request_serialization() {
+ let request = StreamAppendRequest {
+ stream_id: 123,
+ data: Some(vec![1, 2, 3, 4]),
+ };
+
+ let json = serde_json::to_string(&request).unwrap();
+ let deserialized: StreamAppendRequest = serde_json::from_str(&json).unwrap();
+
+ assert_eq!(deserialized.stream_id, 123);
+ assert_eq!(deserialized.data, Some(vec![1, 2, 3, 4]));
+ }
+
+ #[test]
+ fn test_stream_read_request_serialization() {
+ let request = StreamReadRequest {
+ stream_id: 456,
+ offset: 789,
+ };
+
+ let json = serde_json::to_string(&request).unwrap();
+ let deserialized: StreamReadRequest = serde_json::from_str(&json).unwrap();
+
+ assert_eq!(deserialized.stream_id, 456);
+ assert_eq!(deserialized.offset, 789);
+ }
+
+ #[test]
+ fn test_stream_read_response_serialization() {
+ let response = StreamReadResponse {
+ stream_id: 123,
+ offset: 456,
+ data: vec![1, 2, 3, 4, 5],
+ };
+
+ let json = serde_json::to_string(&response).unwrap();
+ let deserialized: StreamReadResponse = serde_json::from_str(&json).unwrap();
+
+ assert_eq!(deserialized.stream_id, 123);
+ assert_eq!(deserialized.offset, 456);
+ assert_eq!(deserialized.data, vec![1, 2, 3, 4, 5]);
+ }
+
+ #[test]
+ fn test_stream_append_response_serialization() {
+ let response = StreamAppendResponse {
+ stream_id: 789,
+ offset: 1000,
+ };
+
+ let json = serde_json::to_string(&response).unwrap();
+ let deserialized: StreamAppendResponse = serde_json::from_str(&json).unwrap();
+
+ assert_eq!(deserialized.stream_id, 789);
+ assert_eq!(deserialized.offset, 1000);
+ }
+
+ #[test]
+ fn test_response_error_into_response() {
+ let error = ResponseError::InternalError(anyhow::anyhow!("Test error"));
+ let response = error.into_response();
+ assert_eq!(response.status(), axum::http::StatusCode::INTERNAL_SERVER_ERROR);
+
+ let error = ResponseError::AuthError(AuthError::InvalidToken);
+ let response = error.into_response();
+ assert_eq!(response.status(), axum::http::StatusCode::UNAUTHORIZED);
+
+ let error = ResponseError::DataEmpty;
+ let response = error.into_response();
+ assert_eq!(response.status(), axum::http::StatusCode::BAD_REQUEST);
+
+ let error = ResponseError::DataTooLarge;
+ let response = error.into_response();
+ assert_eq!(response.status(), axum::http::StatusCode::BAD_REQUEST);
+
+ let error = ResponseError::DataInvalid;
+ let response = error.into_response();
+ assert_eq!(response.status(), axum::http::StatusCode::BAD_REQUEST);
+
+ let error = ResponseError::StreamNotFound;
+ let response = error.into_response();
+ assert_eq!(response.status(), axum::http::StatusCode::NOT_FOUND);
+
+ let error = ResponseError::Forbidden;
+ let response = error.into_response();
+ assert_eq!(response.status(), axum::http::StatusCode::FORBIDDEN);
+ }
+
+ #[test]
+ fn test_response_error_from_anyhow() {
+ let anyhow_error = anyhow::anyhow!("Test error");
+ let response_error: ResponseError = anyhow_error.into();
+
+ match response_error {
+ ResponseError::InternalError(_) => {},
+ _ => panic!("Expected InternalError"),
+ }
+ }
+
+ #[test]
+ fn test_list_stream_request_serialization() {
+ let user_id = Uuid::new_v4();
+ let request = ListStreamRequest { user_id };
+
+ let json = serde_json::to_string(&request).unwrap();
+ let deserialized: ListStreamRequest = serde_json::from_str(&json).unwrap();
+
+ assert_eq!(deserialized.user_id, user_id);
+ }
+
+ #[test]
+ fn test_stream_serialization() {
+ let owner_id = Uuid::new_v4();
+ let now = chrono::Utc::now();
+ let stream = Stream {
+ stream_id: 123,
+ owner_id,
+ stream_type: "chat".to_string(),
+ status: "active".to_string(),
+ offset: 456,
+ stream_meta: json!({"key": "value"}),
+ created_at: now,
+ updated_at: now,
+ };
+
+ let json = serde_json::to_string(&stream).unwrap();
+ let deserialized: Stream = serde_json::from_str(&json).unwrap();
+
+ assert_eq!(deserialized.stream_id, 123);
+ assert_eq!(deserialized.owner_id, owner_id);
+ assert_eq!(deserialized.stream_type, "chat");
+ assert_eq!(deserialized.status, "active");
+ assert_eq!(deserialized.offset, 456);
+ assert_eq!(deserialized.stream_meta, json!({"key": "value"}));
+ }
+
+ #[test]
+ fn test_list_stream_response_serialization() {
+ let owner_id = Uuid::new_v4();
+ let now = chrono::Utc::now();
+ let stream = Stream {
+ stream_id: 123,
+ owner_id,
+ stream_type: "chat".to_string(),
+ status: "active".to_string(),
+ offset: 456,
+ stream_meta: json!({"key": "value"}),
+ created_at: now,
+ updated_at: now,
+ };
+
+ let response = ListStreamResponse {
+ streams: vec![stream],
+ };
+
+ let json = serde_json::to_string(&response).unwrap();
+ let deserialized: ListStreamResponse = serde_json::from_str(&json).unwrap();
+
+ assert_eq!(deserialized.streams.len(), 1);
+ assert_eq!(deserialized.streams[0].stream_id, 123);
+ }
+
+ #[test]
+ fn test_conversation_serialization() {
+ let conversation_id = Uuid::new_v4();
+ let now = chrono::Utc::now();
+ let conversation = Conversation {
+ conversation_id,
+ conversation_type: "group".to_string(),
+ members: json!(["user1", "user2"]),
+ meta: json!({"name": "Test Group"}),
+ stream_id: 789,
+ created_at: now,
+ updated_at: now,
+ };
+
+ let json = serde_json::to_string(&conversation).unwrap();
+ let deserialized: Conversation = serde_json::from_str(&json).unwrap();
+
+ assert_eq!(deserialized.conversation_id, conversation_id);
+ assert_eq!(deserialized.conversation_type, "group");
+ assert_eq!(deserialized.members, json!(["user1", "user2"]));
+ assert_eq!(deserialized.meta, json!({"name": "Test Group"}));
+ assert_eq!(deserialized.stream_id, 789);
+ }
+
+ #[test]
+ fn test_list_conversations_response_serialization() {
+ let conversation_id = Uuid::new_v4();
+ let now = chrono::Utc::now();
+ let conversation = Conversation {
+ conversation_id,
+ conversation_type: "group".to_string(),
+ members: json!(["user1", "user2"]),
+ meta: json!({"name": "Test Group"}),
+ stream_id: 789,
+ created_at: now,
+ updated_at: now,
+ };
+
+ let response = ListConversationsResponse {
+ conversations: vec![conversation],
+ };
+
+ let json = serde_json::to_string(&response).unwrap();
+ let deserialized: ListConversationsResponse = serde_json::from_str(&json).unwrap();
+
+ assert_eq!(deserialized.conversations.len(), 1);
+ assert_eq!(deserialized.conversations[0].conversation_id, conversation_id);
+ }
+
+ #[test]
+ fn test_stream_error_response_serialization() {
+ let response = StreamErrorResponse {
+ error: "Something went wrong".to_string(),
+ };
+
+ let json = serde_json::to_string(&response).unwrap();
+ let deserialized: StreamErrorResponse = serde_json::from_str(&json).unwrap();
+
+ assert_eq!(deserialized.error, "Something went wrong");
+ }
+
+ #[test]
+ fn test_debug_implementations() {
+ let user_id = Uuid::new_v4();
+
+ let login_request = LoginRequest {
+ type_: "test".to_string(),
+ username: Some("user".to_string()),
+ password: Some("pass".to_string()),
+ };
+ let debug_str = format!("{:?}", login_request);
+ assert!(debug_str.contains("LoginRequest"));
+
+ let login_response = LoginResponse {
+ user_id,
+ username: "test".to_string(),
+ email: "test@example.com".to_string(),
+ avatar_url: None,
+ status: "active".to_string(),
+ jwt_token: "token".to_string(),
+ };
+ let debug_str = format!("{:?}", login_response);
+ assert!(debug_str.contains("LoginResponse"));
+
+ let stream_append_request = StreamAppendRequest {
+ stream_id: 1,
+ data: Some(vec![1, 2, 3]),
+ };
+ let debug_str = format!("{:?}", stream_append_request);
+ assert!(debug_str.contains("StreamAppendRequest"));
+ }
+
+ #[test]
+ fn test_base64_encoding_in_stream_read_response() {
+ let response = StreamReadResponse {
+ stream_id: 123,
+ offset: 456,
+ data: vec![72, 101, 108, 108, 111], // "Hello" in bytes
+ };
+
+ let json = serde_json::to_string(&response).unwrap();
+ // The data should be base64 encoded in JSON
+ assert!(json.contains("SGVsbG8=")); // "Hello" in base64
+
+ let deserialized: StreamReadResponse = serde_json::from_str(&json).unwrap();
+ assert_eq!(deserialized.data, vec![72, 101, 108, 108, 111]);
+ }
+}
diff --git a/crates/cherryserver/.env b/crates/cherryserver/.env
new file mode 100644
index 0000000..7dc42bf
--- /dev/null
+++ b/crates/cherryserver/.env
@@ -0,0 +1 @@
+DATABASE_URL=postgres://postgres:postgres123@localhost:5432/cherryserver
\ No newline at end of file
diff --git a/crates/cherryserver/Cargo.toml b/crates/cherryserver/Cargo.toml
new file mode 100644
index 0000000..862ba61
--- /dev/null
+++ b/crates/cherryserver/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "cherryserver"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+anyhow = { version = "1.0.98", features = ["backtrace"] }
+axum = { version = "0.8.4", features = ["macros", "ws"] }
+chrono = { version = "0.4.41", features = ["serde"] }
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0.140"
+serde_yaml = "0.9"
+sqlx = { version = "0.8", features = [
+ "runtime-tokio",
+ "postgres",
+ "chrono",
+ "uuid",
+] }
+tokio = { version = "1.45.1", features = ["macros", "rt-multi-thread"] }
+use = "0.0.1-pre.0"
+uuid = "1.17.0"
+cherrycore = { path = "../cherrycore" }
+jsonwebtoken = "9.3.1"
+clap = { version = "4.5.40", features = ["derive"] }
diff --git a/crates/cherryserver/diesel.toml b/crates/cherryserver/diesel.toml
new file mode 100644
index 0000000..bb1d1f7
--- /dev/null
+++ b/crates/cherryserver/diesel.toml
@@ -0,0 +1,9 @@
+# For documentation on how to configure this file,
+# see https://diesel.rs/guides/configuring-diesel-cli
+
+[print_schema]
+file = "src/db/schema.rs"
+custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]
+
+[migrations_directory]
+dir = "migrations"
diff --git a/crates/cherryserver/migrations/20250619134000_initial.down.sql b/crates/cherryserver/migrations/20250619134000_initial.down.sql
new file mode 100644
index 0000000..b51e64f
--- /dev/null
+++ b/crates/cherryserver/migrations/20250619134000_initial.down.sql
@@ -0,0 +1,5 @@
+drop table if exists streams cascade;
+drop table if exists contacts cascade;
+drop table if exists contact_details cascade;
+drop table if exists users cascade;
+drop table if exists conversations cascade;
\ No newline at end of file
diff --git a/crates/cherryserver/migrations/20250619134000_initial.sql b/crates/cherryserver/migrations/20250619134000_initial.sql
new file mode 100644
index 0000000..7bc52e0
--- /dev/null
+++ b/crates/cherryserver/migrations/20250619134000_initial.sql
@@ -0,0 +1,79 @@
+-- Add migration script here
+-- Your SQL goes here
+-- ๅฏ็จUUIDๆฉๅฑ
+CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+
+-- ็จๆท่กจ
+CREATE TABLE IF NOT EXISTS users (
+ user_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ username VARCHAR(50) NOT NULL UNIQUE,
+ avatar_url TEXT,
+ email VARCHAR(100) NOT NULL UNIQUE,
+ password_hash TEXT NOT NULL,
+ profile JSONB NOT NULL DEFAULT '{}'::JSONB, -- ๅญๅจๅจๆ็จๆทๅฑๆง
+ app_config JSONB NOT NULL DEFAULT '{}'::JSONB, -- ๅญๅจๅบ็จ้
็ฝฎ
+ stream_meta JSONB NOT NULL DEFAULT '{}'::JSONB, -- ๅญๅจๆตๅ
ๆฐๆฎ
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ last_active TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+-- ไผ่ฏ่กจ๏ผๆททๅๅ่/็พค่๏ผ
+CREATE TABLE IF NOT EXISTS conversations (
+ conversation_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ conversation_type VARCHAR(10) NOT NULL, -- ไผ่ฏ็ฑปๅ
+ members JSONB NOT NULL DEFAULT '[]'::JSONB, -- ๆๅIDๆฐ็ป
+ meta JSONB NOT NULL DEFAULT '{}'::JSONB, -- ๅจๆไผ่ฏๅฑๆง
+ -- ๆถๆฏๆตID
+ stream_id BIGINT NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+-- ่็ณปไบบ่กจ๏ผๆฏๆๅๅๅ
ณ็ณปไธๅค็ง่็ณปไบบ็ฑปๅ๏ผ
+CREATE TABLE IF NOT EXISTS contacts (
+ contact_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ owner_id UUID NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
+ target_id UUID NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
+ relation_type VARCHAR(20) NOT NULL CHECK (relation_type IN ('friend', 'blocked', 'pending_outgoing', 'pending_incoming')),
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+
+ -- ่็ณปไบบไธๅฑไฟกๆฏ
+ remark_name VARCHAR(100), -- ๅคๆณจๅ
+ tags JSONB DEFAULT '[]'::JSONB, -- ๆ ็ญพๅ็ฑป ["ๅไบ", "ๅฎถไบบ"]
+ is_favorite BOOLEAN NOT NULL DEFAULT false,
+ mute_settings JSONB DEFAULT '{}'::JSONB, -- {"muted": true, "expire_at": "2023-12-31"}
+
+ -- ๅฏไธ็บฆๆ็กฎไฟไธไผ้ๅคๆทปๅ
+ UNIQUE (owner_id, target_id)
+);
+
+-- ่็ณปไบบๆฉๅฑไฟกๆฏ่กจ๏ผๅญๅจ้ๅฏน็งฐๆฐๆฎ๏ผ
+CREATE TABLE IF NOT EXISTS contact_details (
+ contact_id UUID NOT NULL REFERENCES contacts(contact_id) ON DELETE CASCADE,
+ user_id UUID NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
+
+ -- ็จๆท่ชๅฎไน็ไธๅฑไฟกๆฏ
+ custom_fields JSONB DEFAULT '{}'::JSONB, -- {"department": "ๆๆฏ้จ", "notes": "ๅคงๅญฆๅๅญฆ"}
+ last_interaction TIMESTAMPTZ, -- ๆๅไบๅจๆถ้ด
+
+ PRIMARY KEY (contact_id, user_id)
+);
+
+-- ๆต่กจ
+CREATE TABLE IF NOT EXISTS streams (
+ stream_id BIGSERIAL PRIMARY KEY,
+ owner_id UUID NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
+ stream_type VARCHAR(20) NOT NULL,
+ status VARCHAR(20) NOT NULL,
+ "offset" BIGINT NOT NULL DEFAULT 0,
+ stream_meta JSONB NOT NULL DEFAULT '{}'::JSONB, -- ๅญๅจๆตๅ
ๆฐๆฎ
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+-- ็ดขๅผไผๅ
+CREATE INDEX IF NOT EXISTS idx_streams_owner ON streams(owner_id);
+CREATE INDEX IF NOT EXISTS idx_streams_stream_id ON streams(stream_id);
+CREATE INDEX IF NOT EXISTS idx_contacts_owner ON contacts(owner_id);
+CREATE INDEX IF NOT EXISTS idx_contacts_relation ON contacts(owner_id, relation_type);
diff --git a/crates/cherryserver/migrations/20250619134216_update_user.down.sql b/crates/cherryserver/migrations/20250619134216_update_user.down.sql
new file mode 100644
index 0000000..20b92b4
--- /dev/null
+++ b/crates/cherryserver/migrations/20250619134216_update_user.down.sql
@@ -0,0 +1,3 @@
+-- Add down migration script here
+alter table users drop column IF EXISTS avatar_url;
+alter table users drop column IF EXISTS status;
\ No newline at end of file
diff --git a/crates/cherryserver/migrations/20250619134216_update_user.up.sql b/crates/cherryserver/migrations/20250619134216_update_user.up.sql
new file mode 100644
index 0000000..4281f64
--- /dev/null
+++ b/crates/cherryserver/migrations/20250619134216_update_user.up.sql
@@ -0,0 +1,5 @@
+-- Add up migration script here
+alter table users
+add column IF NOT EXISTS avatar_url TEXT;
+
+alter table users add column IF NOT EXISTS status TEXT NOT NULL DEFAULT 'online';
\ No newline at end of file
diff --git a/crates/cherryserver/src/db/mod.rs b/crates/cherryserver/src/db/mod.rs
new file mode 100644
index 0000000..10c1399
--- /dev/null
+++ b/crates/cherryserver/src/db/mod.rs
@@ -0,0 +1,2 @@
+pub mod models;
+pub mod repo;
\ No newline at end of file
diff --git a/crates/cherryserver/src/db/models.rs b/crates/cherryserver/src/db/models.rs
new file mode 100644
index 0000000..0e93b84
--- /dev/null
+++ b/crates/cherryserver/src/db/models.rs
@@ -0,0 +1,112 @@
+// -- ็จๆท่กจ
+// CREATE TABLE IF NOT EXISTS users (
+// user_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+// username VARCHAR(50) NOT NULL UNIQUE,
+// avatar_url TEXT,
+// email VARCHAR(100) NOT NULL UNIQUE,
+// password_hash TEXT NOT NULL,
+// profile JSONB NOT NULL DEFAULT '{}'::JSONB, -- ๅญๅจๅจๆ็จๆทๅฑๆง
+// app_config JSONB NOT NULL DEFAULT '{}'::JSONB, -- ๅญๅจๅบ็จ้
็ฝฎ
+// stream_meta JSONB NOT NULL DEFAULT '{}'::JSONB, -- ๅญๅจๆตๅ
ๆฐๆฎ
+// created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+// last_active TIMESTAMPTZ NOT NULL DEFAULT NOW()
+// );
+
+use chrono::DateTime;
+
+use serde::{Deserialize, Serialize};
+use serde_json::Value as JsonValue;
+use uuid::Uuid;
+
+#[derive(Debug, sqlx::FromRow, Serialize, Deserialize)]
+pub struct User {
+ pub user_id: Uuid,
+ pub username: String,
+ pub avatar_url: Option,
+ pub status: String,
+ pub email: String,
+ pub password_hash: String,
+ pub profile: JsonValue,
+ pub app_config: JsonValue,
+ pub stream_meta: JsonValue,
+ pub created_at: DateTime,
+ pub last_active: DateTime,
+}
+
+// CREATE TABLE IF NOT EXISTS contacts (
+// contact_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+// owner_id UUID NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
+// target_id UUID NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
+// relation_type VARCHAR(20) NOT NULL CHECK (relation_type IN ('friend', 'blocked', 'pending_outgoing', 'pending_incoming')),
+// created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+// updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+
+// -- ่็ณปไบบไธๅฑไฟกๆฏ
+// remark_name VARCHAR(100), -- ๅคๆณจๅ
+// tags JSONB DEFAULT '[]'::JSONB, -- ๆ ็ญพๅ็ฑป ["ๅไบ", "ๅฎถไบบ"]
+// is_favorite BOOLEAN NOT NULL DEFAULT false,
+// mute_settings JSONB DEFAULT '{}'::JSONB, -- {"muted": true, "expire_at": "2023-12-31"}
+
+// -- ๅฏไธ็บฆๆ็กฎไฟไธไผ้ๅคๆทปๅ
+// UNIQUE (owner_id, target_id)
+// );
+
+#[derive(Debug, sqlx::FromRow, Serialize, Deserialize)]
+pub struct Contact {
+ pub contact_id: Uuid,
+ pub owner_id: Uuid,
+ pub target_id: Uuid,
+ pub relation_type: String,
+ pub created_at: DateTime,
+ pub updated_at: DateTime,
+ pub remark_name: Option,
+ pub tags: JsonValue,
+ pub is_favorite: bool,
+ pub mute_settings: JsonValue,
+}
+
+// CREATE TABLE IF NOT EXISTS streams (
+// stream_id BIGSERIAL PRIMARY KEY,
+// owner_id UUID NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
+// type VARCHAR(20) NOT NULL CHECK (type IN ('message', 'system', 'event', 'notification', 'file')),
+// name VARCHAR(100) NOT NULL,
+// status VARCHAR(20) NOT NULL CHECK (status IN ('active', 'inactive', 'archived')),
+// offset BIGINT NOT NULL DEFAULT 0,
+// stream_meta JSONB NOT NULL DEFAULT '{}'::JSONB, -- ๅญๅจๆตๅ
ๆฐๆฎ
+// created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+// updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+// );
+#[derive(Debug, sqlx::FromRow, Serialize, Deserialize)]
+pub struct Stream {
+ pub stream_id: i64,
+ pub owner_id: Uuid,
+ pub stream_type: String,
+ pub status: String,
+ pub offset: i64,
+ pub stream_meta: JsonValue,
+ pub created_at: DateTime,
+ pub updated_at: DateTime,
+}
+
+
+// CREATE TABLE IF NOT EXISTS conversations (
+// conversation_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+// type VARCHAR(10) NOT NULL CHECK (type IN ('direct', 'group')), -- ไผ่ฏ็ฑปๅ
+// members JSONB NOT NULL DEFAULT '[]'::JSONB, -- ๆๅIDๆฐ็ป
+// meta JSONB NOT NULL DEFAULT '{}'::JSONB, -- ๅจๆไผ่ฏๅฑๆง
+// -- ๆถๆฏๆตID
+// message_stream_id BIGINT NOT NULL,
+// created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+// updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+// );
+
+#[derive(Debug, sqlx::FromRow, Serialize, Deserialize)]
+pub struct Conversation {
+ pub conversation_id: Uuid,
+ pub conversation_type: String,
+ pub members: JsonValue,
+ pub meta: JsonValue,
+ pub stream_id: i64,
+ pub created_at: DateTime,
+ pub updated_at: DateTime,
+}
\ No newline at end of file
diff --git a/crates/cherryserver/src/db/repo.rs b/crates/cherryserver/src/db/repo.rs
new file mode 100644
index 0000000..f04100a
--- /dev/null
+++ b/crates/cherryserver/src/db/repo.rs
@@ -0,0 +1,83 @@
+use std::time::Duration;
+
+use anyhow::Result;
+use serde_json::json;
+use sqlx::{
+ Pool,
+ postgres::{PgPool, PgPoolOptions},
+ query, query_as,
+ types::Uuid,
+};
+
+use crate::db::models::*;
+
+#[derive(Clone)]
+pub struct Repo {
+ sqlx_pool: PgPool,
+}
+
+impl Repo {
+ pub async fn new(db_url: &str) -> Self {
+ let pool = PgPoolOptions::new()
+ .max_connections(500)
+ .acquire_timeout(Duration::from_secs(10))
+ .connect(db_url)
+ .await
+ .unwrap();
+ Self { sqlx_pool: pool }
+ }
+
+ pub async fn user_get_by_username(&self, username: &str) -> Result {
+ let user = query_as!(User, "SELECT * FROM users WHERE username = $1", username)
+ .fetch_one(&self.sqlx_pool)
+ .await?;
+ Ok(user)
+ }
+
+ pub async fn check_password(&self, username: &str, password: &str) -> Result {
+ let user = query_as!(User, "SELECT * FROM users WHERE username = $1", username)
+ .fetch_one(&self.sqlx_pool)
+ .await?;
+ Ok(user.password_hash == password)
+ }
+
+ pub async fn list_contacts(&self, user_id: Uuid) -> Result> {
+ let contacts = query_as!(
+ Contact,
+ "SELECT * FROM contacts WHERE owner_id = $1",
+ user_id
+ )
+ .fetch_all(&self.sqlx_pool)
+ .await?;
+ Ok(contacts)
+ }
+
+ pub async fn list_streams(&self, user_id: Uuid) -> Result> {
+ let streams = query_as!(Stream, "SELECT * FROM streams WHERE owner_id = $1", user_id)
+ .fetch_all(&self.sqlx_pool)
+ .await?;
+ Ok(streams)
+ }
+
+ pub async fn list_conversations(&self, user_id: Uuid) -> Result> {
+ let conversations = query_as!(
+ Conversation,
+ "SELECT * FROM conversations WHERE members @> $1::jsonb",
+ json!(user_id.to_string())
+ )
+ .fetch_all(&self.sqlx_pool)
+ .await?;
+ Ok(conversations)
+ }
+
+ pub async fn update_stream_offset(&self, stream_id: i64, offset: i64) -> Result<()> {
+ let _ = query!(
+ "UPDATE streams SET \"offset\" = $1 WHERE stream_id = $2",
+ offset,
+ stream_id
+ )
+ .execute(&self.sqlx_pool)
+ .await?;
+ Ok(())
+ }
+}
diff --git a/crates/cherryserver/src/main.rs b/crates/cherryserver/src/main.rs
new file mode 100644
index 0000000..9c54a54
--- /dev/null
+++ b/crates/cherryserver/src/main.rs
@@ -0,0 +1,23 @@
+mod db;
+mod server;
+
+use clap::Parser;
+use std::path::PathBuf;
+
+use crate::server::ServerConfig;
+
+#[derive(Parser, Debug)]
+struct Cli {
+ #[clap(short, long, default_value = "config.yaml")]
+ config: PathBuf,
+}
+
+#[tokio::main(flavor = "multi_thread", worker_threads = 10)]
+async fn main() {
+ let cli = Cli::parse();
+ let config_file = cli.config;
+
+ let config = ServerConfig::load(config_file).await.unwrap();
+ let server = server::CherryServer::new(config).await;
+ server::start(server).await;
+}
diff --git a/crates/cherryserver/src/server.rs b/crates/cherryserver/src/server.rs
new file mode 100644
index 0000000..49679cb
--- /dev/null
+++ b/crates/cherryserver/src/server.rs
@@ -0,0 +1,152 @@
+use std::path::PathBuf;
+
+use anyhow::Result;
+use axum::{
+ Json, Router,
+ extract::{Query, State},
+ routing::{get, post},
+};
+use cherrycore::{
+ jwt::{AuthError, JwtClaims},
+ types::*,
+};
+use serde::Deserialize;
+use sqlx::query;
+use tokio::net::TcpListener;
+use uuid::Uuid;
+
+use crate::db::{models::Contact, repo::Repo};
+
+#[derive(Clone, Deserialize)]
+pub(crate) struct ServerConfig {
+ pub(crate) db_url: String,
+ pub(crate) expire_time: u64,
+}
+
+impl ServerConfig {
+ pub(crate) async fn load(filename: PathBuf) -> Result {
+ let content = tokio::fs::read_to_string(filename).await?;
+ let config = serde_yaml::from_str(&content)
+ .map_err(|e| anyhow::anyhow!("Failed to load config: {}", e))?;
+ Ok(config)
+ }
+}
+#[derive(Clone)]
+pub(crate) struct CherryServer {
+ config: ServerConfig,
+ db: Repo,
+}
+
+#[axum::debug_handler]
+async fn list_contacts(
+ server: State,
+ claims: JwtClaims,
+) -> Result>, ResponseError> {
+ let user_id = claims.user_id;
+ let contacts = server.db.list_contacts(user_id).await?;
+ Ok(Json(contacts))
+}
+
+#[axum::debug_handler]
+async fn list_streams(
+ server: State,
+ request: Query,
+) -> Result, ResponseError> {
+ let user_id = request.user_id;
+ let streams = server.db.list_streams(user_id).await?;
+ Ok(Json(ListStreamResponse {
+ streams: streams
+ .into_iter()
+ .map(|s| Stream {
+ stream_id: s.stream_id,
+ owner_id: s.owner_id,
+ stream_type: s.stream_type,
+ status: s.status,
+ offset: s.offset,
+ stream_meta: s.stream_meta.clone(),
+ created_at: s.created_at,
+ updated_at: s.updated_at,
+ })
+ .collect(),
+ }))
+}
+
+#[axum::debug_handler]
+async fn list_conversations(
+ server: State,
+ claims: JwtClaims,
+) -> Result, ResponseError> {
+ let user_id = claims.user_id;
+ let conversations = server.db.list_conversations(user_id).await?;
+ Ok(Json(ListConversationsResponse {
+ conversations: conversations
+ .into_iter()
+ .map(|c| cherrycore::types::Conversation {
+ conversation_id: c.conversation_id,
+ conversation_type: c.conversation_type,
+ members: c.members.clone(),
+ meta: c.meta.clone(),
+ stream_id: c.stream_id,
+ created_at: c.created_at,
+ updated_at: c.updated_at,
+ })
+ .collect(),
+ }))
+}
+
+#[axum::debug_handler]
+async fn login(
+ server: State,
+ body: Json,
+) -> Result, ResponseError> {
+ let user = server
+ .db
+ .check_password(
+ body.username.as_ref().unwrap(),
+ body.password.as_ref().unwrap(),
+ )
+ .await?;
+
+ if !user {
+ return Err(AuthError::WrongCredentials.into());
+ }
+
+ let user = server
+ .db
+ .user_get_by_username(body.username.as_ref().unwrap())
+ .await?;
+
+ let jwt_token = JwtClaims::new(user.user_id, server.config.expire_time).to_token()?;
+
+ Ok(Json(LoginResponse {
+ jwt_token,
+ user_id: user.user_id,
+ username: user.username,
+ email: user.email,
+ avatar_url: user.avatar_url,
+ status: user.status,
+ }))
+}
+
+impl CherryServer {
+ pub(crate) async fn new(config: ServerConfig) -> Self {
+ let db = Repo::new(&config.db_url).await;
+ Self {
+ db,
+ config: config.clone(),
+ }
+ }
+}
+
+pub(crate) async fn start(server: CherryServer) {
+ let app = Router::new()
+ .route("/", get(|| async { "Hello, World!" }))
+ .route("/api/v1/auth/login", post(login))
+ .route("/api/v1/contract/list", get(list_contacts))
+ .route("/api/v1/streams/list", get(list_streams))
+ .route("/api/v1/conversations/list", get(list_conversations))
+ .with_state(server.clone());
+
+ let listener = TcpListener::bind("0.0.0.0:8080").await.unwrap();
+ axum::serve(listener, app).await.unwrap();
+}
diff --git a/crates/streamserver/Cargo.toml b/crates/streamserver/Cargo.toml
new file mode 100644
index 0000000..8a8e323
--- /dev/null
+++ b/crates/streamserver/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "streamserver"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+anyhow = "1.0.98"
+axum = { version = "0.8.4", features = ["macros", "ws"] }
+clap = { version = "4.5.40", features = ["derive", "env", "string"] }
+serde = { version = "1.0.219", features = ["serde_derive"] }
+serde_yaml = "0.9.34"
+tokio = { version = "1.45.1", features = ["fs", "macros", "net", "rt-multi-thread"] }
+cherrycore = { path = "../cherrycore" }
+streamstore = { path = "../streamstore" }
+serde_json = "1.0.140"
+log = { version = "0.4.27", features = ["serde"] }
+serde_with = { version = "3.13.0", features = ["base64"] }
+uuid = "1.17.0"
+tokio-util = "0.7.15"
+futures-util = "0.3.31"
diff --git a/crates/streamserver/src/main.rs b/crates/streamserver/src/main.rs
new file mode 100644
index 0000000..529bb8e
--- /dev/null
+++ b/crates/streamserver/src/main.rs
@@ -0,0 +1,74 @@
+use std::{
+ collections::HashMap,
+ net::SocketAddr,
+ path::PathBuf,
+ sync::{Arc, Mutex},
+};
+
+use anyhow::Result;
+use axum::{
+ Router,
+ routing::{get, post},
+};
+use clap::Parser;
+use serde::Deserialize;
+use tokio::{net::TcpListener, sync::watch};
+
+use streamstore::store::Store;
+
+mod stream;
+
+#[derive(Clone, Deserialize)]
+struct StreamServerConfig {
+ pub server_url: String,
+ pub server_port: u16,
+ pub jwt_secret: String,
+ pub jwt_expire_time: u64,
+ pub stream_storage_path: String,
+}
+
+impl StreamServerConfig {
+ pub async fn load(filename: PathBuf) -> Result {
+ let content = tokio::fs::read_to_string(filename).await?;
+ let config = serde_yaml::from_str(&content)?;
+ Ok(config)
+ }
+}
+
+#[derive(Clone)]
+struct StreamServer {
+ config: StreamServerConfig,
+ store: streamstore::store::Store,
+ watchers: Arc, watch::Receiver)>>>,
+}
+
+#[derive(Parser, Debug)]
+struct Cli {
+ #[clap(short, long, default_value = "config.yaml")]
+ config: PathBuf,
+}
+
+#[tokio::main(flavor = "multi_thread", worker_threads = 10)]
+async fn main() {
+ let cli = Cli::parse();
+ let config = StreamServerConfig::load(cli.config).await.unwrap();
+
+ let store = streamstore::options::Options::default()
+ .wal_path(&config.stream_storage_path)
+ .open_store()
+ .unwrap();
+
+ let server = StreamServer {
+ config,
+ store,
+ watchers: Arc::new(Mutex::new(HashMap::new())),
+ };
+
+ let app = Router::new()
+ .merge(stream::init_routes())
+ .with_state(server);
+ let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap();
+ let addr = listener.local_addr().unwrap();
+ println!("Listening on {}", addr);
+ axum::serve(listener, app).await.unwrap();
+}
diff --git a/crates/streamserver/src/stream.rs b/crates/streamserver/src/stream.rs
new file mode 100644
index 0000000..3e1ec00
--- /dev/null
+++ b/crates/streamserver/src/stream.rs
@@ -0,0 +1,330 @@
+use anyhow::Result;
+use axum::{
+ Json, Router,
+ extract::{
+ State, WebSocketUpgrade,
+ ws::{Message, WebSocket},
+ },
+ response::IntoResponse,
+ routing::{get, post},
+};
+use futures_util::{SinkExt, StreamExt};
+use std::{io::Read, sync::Arc, time};
+use tokio::{select, sync::Semaphore};
+
+use cherrycore::{jwt::JwtClaims, types::*};
+use tokio::sync::{mpsc, watch};
+
+use crate::StreamServer;
+
+#[axum::debug_handler]
+async fn append_stream(
+ claims: JwtClaims,
+ server: State,
+ mut request: Json,
+) -> Result, ResponseError> {
+ let mut acl_checker = AclChecker::new(claims.user_id, request.stream_id, &server);
+ if !acl_checker.check_acl().await.unwrap_or(false) {
+ return Err(ResponseError::Forbidden);
+ }
+
+ if request.data.is_none() || request.data.as_ref().unwrap().is_empty() {
+ return Err(ResponseError::DataEmpty);
+ }
+
+ let stream_id = request.stream_id;
+ let data = request.data.take().unwrap();
+ let offset = server.store.append_async(stream_id, data).await?;
+
+ // notify watchers to read the stream data
+ let watchers = server.watchers.lock().unwrap();
+ if let Some((tx, _rx)) = watchers.get(&stream_id) {
+ let _ = tx.send_replace(offset);
+ }
+
+ Ok(Json(StreamAppendResponse { stream_id, offset }))
+}
+
+struct AclChecker<'a> {
+ user_id: uuid::Uuid,
+ stream_id: u64,
+ server: &'a StreamServer,
+ check_ts: time::Instant,
+}
+
+impl<'a> AclChecker<'a> {
+ fn new(user_id: uuid::Uuid, stream_id: u64, server: &'a StreamServer) -> Self {
+ Self {
+ user_id,
+ stream_id,
+ server,
+ check_ts: time::Instant::now()
+ .checked_sub(time::Duration::from_secs(5))
+ .unwrap(),
+ }
+ }
+
+ async fn check_acl(&mut self) -> Result {
+ if self.check_ts < time::Instant::now() {
+ // TODO: check acl
+ self.check_ts = time::Instant::now() + time::Duration::from_secs(5);
+ Ok(true)
+ } else {
+ Ok(true)
+ }
+ }
+}
+
+async fn read_one_stream_handler(
+ user_id: uuid::Uuid,
+ token: tokio_util::sync::CancellationToken,
+ request: StreamReadRequest,
+ semaphore: Arc,
+ sender: mpsc::Sender,
+ server: State,
+) -> Result<()> {
+ let stream_id = request.stream_id;
+ let mut offset = request.offset;
+ let mut acl_checker = AclChecker::new(user_id, stream_id, &server);
+ loop {
+ // check acl every 5 seconds
+ if !acl_checker.check_acl().await.unwrap_or(false) {
+ log::error!("acl check failed, stream_id: {}", stream_id);
+ return Err(anyhow::anyhow!("acl check failed"));
+ }
+
+ if let Ok((begin, end)) = server.store.get_stream_range(stream_id) {
+ if offset < begin || offset > end {
+ log::error!("offset or length is out of range");
+ return Err(anyhow::anyhow!("offset or length is out of range"));
+ }
+
+ let _permit = select! {
+ permit = semaphore.acquire() => {
+ permit
+ }
+ _ = token.cancelled() => {
+ return Ok(());
+ }
+ };
+
+ let reader = server.store.new_stream_reader(stream_id).unwrap();
+ reader.set_offset(offset);
+ loop {
+ // check acl every 5 seconds
+ if !acl_checker.check_acl().await.unwrap_or(false) {
+ log::error!("acl check failed, stream_id: {}", stream_id);
+ return Err(anyhow::anyhow!("acl check failed"));
+ }
+
+ let mut reader = reader.clone();
+ let data = select! {
+ data = tokio::task::spawn_blocking(move || {
+ let mut data = Vec::with_capacity(128 * 1024);
+ match reader.read(&mut data) {
+ Ok(read_bytes) => {
+ data.truncate(read_bytes);
+ Ok(data)
+ }
+ Err(e) => {
+ Err(e)
+ }
+ }
+ })=> {
+ match data {
+ Ok(data) => {
+ data
+ }
+ Err(e) => {
+ log::error!("spawn_blocking read stream error, stream_id: {}, error: {}", stream_id, e);
+ break;
+ }
+ }
+ }
+ _ = token.cancelled() => {
+ return Ok(());
+ }
+ };
+
+ if let Ok(data) = data {
+ if data.is_empty() {
+ log::info!("read stream end, stream_id: {}", stream_id);
+ break;
+ }
+
+ offset += data.len() as u64;
+ let response = StreamReadResponse {
+ stream_id,
+ offset,
+ data,
+ };
+ select! {
+ _ = sender.send(response) => {
+ continue;
+ }
+ _ = token.cancelled() => {
+ return Ok(());
+ }
+ }
+ } else {
+ log::error!("read stream error, stream_id: {}", stream_id);
+ break;
+ }
+ }
+ } else {
+ log::info!("stream not found, stream_id: {}", stream_id);
+ }
+
+ let mut rx = server
+ .watchers
+ .lock()
+ .unwrap()
+ .entry(stream_id)
+ .or_insert_with(|| {
+ let (tx, rx) = watch::channel(offset);
+ (tx, rx)
+ })
+ .1
+ .clone();
+
+ select! {
+ _ = rx.wait_for(move |new_offset| *new_offset > offset) => {
+ continue;
+ }
+ _ = token.cancelled() => {
+ return Ok(());
+ }
+ }
+ }
+}
+
+async fn read_stream_handler(
+ user_id: uuid::Uuid,
+ socket: WebSocket,
+ server: State,
+) -> Result<()> {
+ let (socket_sender, mut socket_receiver) = socket.split();
+ let socket_sender = Arc::new(tokio::sync::Mutex::new(socket_sender));
+ let socket_sender_clone = socket_sender.clone();
+ let token = tokio_util::sync::CancellationToken::new();
+ let token_clone = token.clone();
+ let (tx, mut rx) = mpsc::channel(32);
+ let semaphore = Arc::new(Semaphore::new(8));
+
+ // Spawn a task to handle incoming messages from the WebSocket
+ let receiver_task = tokio::spawn(async move {
+ while let Some(msg) = socket_receiver.next().await {
+ match msg {
+ Ok(Message::Text(text)) => {
+ let text_str = text.to_string();
+ match serde_json::from_str::(&text_str) {
+ Ok(request) => {
+ let semaphore = semaphore.clone();
+ let tx = tx.clone();
+ let token = token_clone.clone();
+ let server = server.clone();
+ tokio::spawn(async move {
+ if let Err(e) = read_one_stream_handler(
+ user_id,
+ token,
+ request,
+ semaphore,
+ tx,
+ server.clone(),
+ )
+ .await
+ {
+ log::error!("read stream error: {}", e);
+ }
+ });
+ },
+ Err(e) => {
+ log::error!("Failed to parse stream read request: {}", e);
+ // Send error message back to client
+ if let Ok(error_msg) = serde_json::to_string(&StreamErrorResponse {
+ error: format!("Invalid request format: {}", e),
+ }) {
+ let mut sender = socket_sender.lock().await;
+ if let Err(e) = sender.send(Message::Text(error_msg.into())).await {
+ log::error!("Failed to send error message: {}", e);
+ }
+ }
+ }
+ }
+ },
+ Ok(Message::Binary(_data)) => {
+ log::warn!("Received binary message, which is not supported");
+ },
+ Ok(Message::Ping(data)) => {
+ let mut sender = socket_sender.lock().await;
+ if let Err(e) = sender.send(Message::Pong(data)).await {
+ log::error!("Failed to send pong: {}", e);
+ }
+ },
+ Ok(Message::Pong(_)) => {
+ // Ignore pong messages
+ },
+ Ok(Message::Close(_)) => {
+ log::info!("WebSocket connection closed by client");
+ break;
+ },
+ Err(e) => {
+ log::error!("WebSocket error: {}", e);
+ break;
+ }
+ }
+ }
+ token_clone.cancel();
+ });
+
+ // Spawn a task to handle outgoing messages to the WebSocket
+ let sender_task = tokio::spawn(async move {
+ while let Some(response) = rx.recv().await {
+ match serde_json::to_string(&response) {
+ Ok(json) => {
+ let mut sender = socket_sender_clone.lock().await;
+ if let Err(e) = sender.send(Message::Text(json.into())).await {
+ log::error!("Failed to send stream data: {}", e);
+ break;
+ }
+ },
+ Err(e) => {
+ log::error!("Failed to serialize stream response: {}", e);
+ }
+ }
+ }
+ });
+
+ // Wait for either task to complete
+ tokio::select! {
+ _ = receiver_task => {
+ log::info!("Receiver task completed");
+ }
+ _ = sender_task => {
+ log::info!("Sender task completed");
+ token.cancel();
+ }
+ }
+
+ Ok(())
+}
+
+#[axum::debug_handler]
+async fn read_stream(
+ ws: WebSocketUpgrade,
+ claims: JwtClaims,
+ server: State,
+) -> impl IntoResponse {
+ let user_id = claims.user_id;
+ ws.on_upgrade(move |socket| async move {
+ if let Err(e) = read_stream_handler(user_id, socket, server).await {
+ log::error!("read stream error: {}", e);
+ }
+ })
+}
+
+pub(crate) fn init_routes() -> Router {
+ Router::new()
+ .route("/api/v1/stream/append", post(append_stream))
+ .route("/api/v1/stream/read", get(read_stream))
+}
diff --git a/crates/streamstore/Cargo.lock b/crates/streamstore/Cargo.lock
new file mode 100644
index 0000000..72eaa5c
--- /dev/null
+++ b/crates/streamstore/Cargo.lock
@@ -0,0 +1,776 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "addr2line"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.98"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
+dependencies = [
+ "backtrace",
+]
+
+[[package]]
+name = "arc-swap"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
+
+[[package]]
+name = "autocfg"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+
+[[package]]
+name = "backtrace"
+version = "0.3.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-targets",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+
+[[package]]
+name = "bytes"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+
+[[package]]
+name = "crc"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "defer"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "930c7171c8df9fb1782bdf9b918ed9ed2d33d1d22300abb754f9085bc48bf8e8"
+
+[[package]]
+name = "dtoa"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04"
+
+[[package]]
+name = "env_filter"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
+dependencies = [
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.11.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "env_filter",
+ "jiff",
+ "log",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasi 0.14.2+wasi-0.2.4",
+]
+
+[[package]]
+name = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "jiff"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
+dependencies = [
+ "jiff-static",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.173"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb"
+
+[[package]]
+name = "lock_api"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+
+[[package]]
+name = "memchr"
+version = "2.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
+
+[[package]]
+name = "memmap2"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
+dependencies = [
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "object"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "portable-atomic"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "prometheus-client"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf41c1a7c32ed72abe5082fb19505b969095c12da9f5732a4bc9878757fd087c"
+dependencies = [
+ "dtoa",
+ "itoa",
+ "parking_lot",
+ "prometheus-client-derive-encode",
+]
+
+[[package]]
+name = "prometheus-client-derive-encode"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
+
+[[package]]
+name = "rand"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
+dependencies = [
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "socket2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "streamstore-rs"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "arc-swap",
+ "backtrace",
+ "crc",
+ "crossbeam-channel",
+ "defer",
+ "env_logger",
+ "lazy_static",
+ "log",
+ "memmap2",
+ "prometheus-client",
+ "rand",
+ "thiserror",
+ "tokio",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio"
+version = "1.45.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasi"
+version = "0.14.2+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+dependencies = [
+ "wit-bindgen-rt",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/crates/streamstore/Cargo.toml b/crates/streamstore/Cargo.toml
new file mode 100644
index 0000000..a79e329
--- /dev/null
+++ b/crates/streamstore/Cargo.toml
@@ -0,0 +1,29 @@
+[package]
+name = "streamstore"
+version = "0.1.0"
+edition = "2024"
+authors = ["fu.niukey@gmail.com"]
+rust-version = "1.86"
+license = "MIT"
+keywords = ["streamstore", "stream"]
+description = "lib for storage stream programing"
+repository = "https://github.com/akzj/cherry/tree/main/crates/streamstore"
+homepage = "https://github.com/akzj/cherry/tree/main/crates/streamstore"
+
+[dependencies]
+anyhow = { version = "1.0.98", features = ["backtrace"] }
+arc-swap = "1.7.1"
+backtrace = "0.3.75"
+crc = "3.3.0"
+crossbeam-channel = "0.5.15"
+defer = "0.2.1"
+env_logger = "0.11.8"
+lazy_static = "1.5.0"
+log = "0.4.27"
+memmap2 = "0.9.5"
+prometheus-client = "0.23.1"
+rand = "0.9.1"
+refinery = "0.8.16"
+thiserror = "2.0.12"
+tokio = { version = "1.45.1", features = ["full"] }
+
diff --git a/LICENSE b/crates/streamstore/LICENSE
similarity index 100%
rename from LICENSE
rename to crates/streamstore/LICENSE
diff --git a/crates/streamstore/README.md b/crates/streamstore/README.md
new file mode 100644
index 0000000..421f5e2
--- /dev/null
+++ b/crates/streamstore/README.md
@@ -0,0 +1,23 @@
+# StreamStore-rs
+
+StreamStore-rs is a Rust library for building stream-sourced applications using the StreamStore protocol. It provides a simple and efficient way to manage streams, making it easier to build scalable and maintainable systems.
+
+## Features
+
+- **Stream Management**: Efficiently manage streams with support for seek, read, and append operations.
+- **High Performance**: Built with performance in mind, leveraging Rust's capabilities for low-level memory management.
+- **Concurrency**: Designed to handle concurrent operations safely and efficiently.
+
+## Usage
+
+To use StreamStore in your project, add it to your `Cargo.toml` dependencies:
+
+```toml
+[dependencies]
+streamstore = { path = "../streamstore" }
+```
+
+## Related Components
+
+- [StreamServer](/workspace/cherry/crates/streamserver): Server implementation for StreamStore
+- [CherryCore](/workspace/cherry/crates/cherrycore): Core functionality for the Cherry application
\ No newline at end of file
diff --git a/examples/append.rs b/crates/streamstore/examples/append.rs
similarity index 98%
rename from examples/append.rs
rename to crates/streamstore/examples/append.rs
index dfc6fa1..82b575d 100644
--- a/examples/append.rs
+++ b/crates/streamstore/examples/append.rs
@@ -4,7 +4,7 @@ use std::env;
use std::io::{Read, Seek, Write};
use std::sync::{Arc, Condvar, Mutex};
use std::thread::sleep;
-use streamstore_rs::entry::AppendEntryResultFn;
+use streamstore::entry::AppendEntryResultFn;
fn main() {
// set rust_log to use the environment variable RUST_LOG
let log_level = "RUST_LOG";
@@ -27,7 +27,7 @@ fn main() {
.init();
log::info!("Starting streamstore example");
- let mut options = streamstore_rs::options::Options::default();
+ let mut options = streamstore::options::Options::default();
options.max_wal_size(320);
options.max_table_size(640);
diff --git a/examples/async_append.rs b/crates/streamstore/examples/async_append.rs
similarity index 96%
rename from examples/async_append.rs
rename to crates/streamstore/examples/async_append.rs
index f2563b8..b174a61 100644
--- a/examples/async_append.rs
+++ b/crates/streamstore/examples/async_append.rs
@@ -23,7 +23,7 @@ async fn main() {
.init();
log::info!("Starting streamstore example");
- let mut options = streamstore_rs::options::Options::new_with_data_path("data/async");
+ let mut options = streamstore::options::Options::new_with_data_path("data/async");
options.max_wal_size(32 * 1024);
options.max_table_size(64 * 1024);
diff --git a/crates/streamstore/src/entry.rs b/crates/streamstore/src/entry.rs
new file mode 100644
index 0000000..a43ba78
--- /dev/null
+++ b/crates/streamstore/src/entry.rs
@@ -0,0 +1,378 @@
+use std::{
+ fs::File,
+ io::Read,
+};
+
+use anyhow::{Error, anyhow};
+
+use crate::errors;
+use anyhow::{Context, Result};
+
+pub type AppendEntryResultFn = Box) -> () + Send + Sync>;
+pub type DataType = Vec;
+
+pub struct Entry {
+ // auto increment id
+ pub version: u8,
+ pub id: u64,
+ pub stream_id: u64,
+ pub data: DataType,
+ pub callback: Option,
+}
+
+pub trait Encoder {
+ fn encode(&self) -> Vec;
+}
+
+impl Encoder for Entry {
+ fn encode(&self) -> Vec {
+ // Encode the item into bytes
+ let mut data = Vec::new();
+ data.extend_from_slice(&self.version.to_le_bytes());
+
+ if self.version == 1 {
+ data.extend_from_slice(&self.id.to_le_bytes());
+ data.extend_from_slice(&self.stream_id.to_le_bytes());
+ data.extend_from_slice(&(self.data.len() as u32).to_le_bytes());
+ data.extend_from_slice(&self.data);
+ } else {
+ panic!("Unsupported version");
+ }
+ data
+ }
+}
+
+pub trait Decoder<'a> {
+ fn decode(&mut self, closure: Box Result + 'a>) -> Result<()>;
+}
+
+impl<'a> Decoder<'a> for File {
+ fn decode(
+ &mut self,
+ mut closure: Box Result + 'a>,
+ ) -> Result<()> {
+ // Decode the item from bytes
+ loop {
+ let mut entry = Entry::default();
+
+ let mut version = [0u8; 1];
+ match self.read_exact(&mut version) {
+ Ok(()) => {
+ entry.version = u8::from_le_bytes(version);
+ }
+ Err(ref e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break, // End of file
+ Err(e) => return Err(anyhow!(e)),
+ }
+ if entry.version == 1 {
+ let mut id_buf = [0u8; 8];
+ self.read_exact(&mut id_buf).context("")?;
+ entry.id = u64::from_le_bytes(id_buf);
+
+ let mut stream_id_buf = [0u8; 8];
+ self.read_exact(&mut stream_id_buf)
+ .context("Failed to read stream_id")?;
+ entry.stream_id = u64::from_le_bytes(stream_id_buf);
+
+ let mut data_size_buf = [0u8; 4];
+ self.read_exact(&mut data_size_buf)
+ .context("Failed to read data size")?;
+
+ let data_size = u32::from_le_bytes(data_size_buf);
+
+ entry.data.resize(data_size as usize, 0);
+ self.read_exact(&mut entry.data)
+ .map_err(errors::new_io_error)?;
+ } else {
+ log::error!("Unsupported version: {}", entry.version);
+ return Err(anyhow!(errors::new_invalid_data()));
+ }
+ // Call the closure with the decoded entry
+ if !closure(entry)? {
+ break;
+ }
+ }
+ Ok(())
+ }
+}
+
+impl Entry {
+ pub fn default() -> Self {
+ Entry {
+ version: 0,
+ id: 0,
+ stream_id: 0,
+ data: Vec::new(),
+ callback: None,
+ }
+ }
+}
+
+impl std::fmt::Debug for Entry {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Entry")
+ .field("version", &self.version)
+ .field("id", &self.id)
+ .field("stream_id", &self.stream_id)
+ .field("data", &self.data)
+ .finish()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::io::Write;
+ use std::fs;
+
+ #[test]
+ fn test_entry_encode() {
+ let entry = Entry {
+ version: 1,
+ id: 1,
+ stream_id: 1,
+ data: "hello world".as_bytes().to_vec(),
+ callback: None,
+ };
+
+ let encoded = entry.encode();
+
+ // write the encoded data to a file
+ let mut file = File::create("test_entry.bin").expect("Failed to create file");
+ file.write_all(&encoded).expect("Failed to write to file");
+
+ file.sync_all().expect("Failed to flush file");
+ drop(file);
+
+ // Read the file back and decode
+ let mut file = File::open("test_entry.bin").expect("Failed to open file");
+
+ let meta = file.metadata().expect("Failed to read metadata");
+ assert!(meta.len() > 0, "File should not be empty");
+
+ file.decode(Box::new(|decoded_entry| {
+ assert_eq!(decoded_entry.version, 1);
+ assert_eq!(decoded_entry.id, 1);
+ assert_eq!(decoded_entry.stream_id, 1);
+ assert_eq!(decoded_entry.data, b"hello world");
+ Ok(true)
+ }))
+ .expect("Failed to decode entry");
+
+ // Clean up
+ let _ = fs::remove_file("test_entry.bin");
+ }
+
+ #[test]
+ fn test_entry_default() {
+ let entry = Entry::default();
+ assert_eq!(entry.version, 0);
+ assert_eq!(entry.id, 0);
+ assert_eq!(entry.stream_id, 0);
+ assert!(entry.data.is_empty());
+ assert!(entry.callback.is_none());
+ }
+
+ #[test]
+ fn test_entry_debug() {
+ let entry = Entry {
+ version: 1,
+ id: 42,
+ stream_id: 123,
+ data: vec![1, 2, 3],
+ callback: None,
+ };
+ let debug_str = format!("{:?}", entry);
+ assert!(debug_str.contains("version: 1"));
+ assert!(debug_str.contains("id: 42"));
+ assert!(debug_str.contains("stream_id: 123"));
+ assert!(debug_str.contains("data: [1, 2, 3]"));
+ }
+
+ #[test]
+ fn test_entry_encode_different_versions() {
+ let entry = Entry {
+ version: 1,
+ id: 100,
+ stream_id: 200,
+ data: vec![0x41, 0x42, 0x43], // "ABC"
+ callback: None,
+ };
+
+ let encoded = entry.encode();
+
+ // Check the encoded format
+ assert_eq!(encoded[0], 1); // version
+ assert_eq!(u64::from_le_bytes([encoded[1], encoded[2], encoded[3], encoded[4], encoded[5], encoded[6], encoded[7], encoded[8]]), 100); // id
+ assert_eq!(u64::from_le_bytes([encoded[9], encoded[10], encoded[11], encoded[12], encoded[13], encoded[14], encoded[15], encoded[16]]), 200); // stream_id
+ assert_eq!(u32::from_le_bytes([encoded[17], encoded[18], encoded[19], encoded[20]]), 3); // data length
+ assert_eq!(&encoded[21..24], &[0x41, 0x42, 0x43]); // data
+ }
+
+ #[test]
+ #[should_panic(expected = "Unsupported version")]
+ fn test_entry_encode_unsupported_version() {
+ let entry = Entry {
+ version: 2, // Unsupported version
+ id: 1,
+ stream_id: 1,
+ data: vec![1, 2, 3],
+ callback: None,
+ };
+ entry.encode();
+ }
+
+ #[test]
+ fn test_entry_decode_multiple_entries() {
+ let entries = vec![
+ Entry {
+ version: 1,
+ id: 1,
+ stream_id: 10,
+ data: "first".as_bytes().to_vec(),
+ callback: None,
+ },
+ Entry {
+ version: 1,
+ id: 2,
+ stream_id: 20,
+ data: "second".as_bytes().to_vec(),
+ callback: None,
+ },
+ Entry {
+ version: 1,
+ id: 3,
+ stream_id: 30,
+ data: "third".as_bytes().to_vec(),
+ callback: None,
+ },
+ ];
+
+ // Encode all entries to a file
+ let mut file = File::create("test_multiple_entries.bin").expect("Failed to create file");
+ for entry in &entries {
+ file.write_all(&entry.encode()).expect("Failed to write entry");
+ }
+ file.sync_all().expect("Failed to flush file");
+ drop(file);
+
+ // Decode all entries
+ let mut file = File::open("test_multiple_entries.bin").expect("Failed to open file");
+ let mut decoded_entries = Vec::new();
+
+ file.decode(Box::new(|entry| {
+ decoded_entries.push(entry);
+ Ok(true)
+ })).expect("Failed to decode entries");
+
+ assert_eq!(decoded_entries.len(), 3);
+
+ for (i, decoded) in decoded_entries.iter().enumerate() {
+ assert_eq!(decoded.version, entries[i].version);
+ assert_eq!(decoded.id, entries[i].id);
+ assert_eq!(decoded.stream_id, entries[i].stream_id);
+ assert_eq!(decoded.data, entries[i].data);
+ }
+
+ // Clean up
+ let _ = fs::remove_file("test_multiple_entries.bin");
+ }
+
+ #[test]
+ fn test_entry_decode_empty_file() {
+ // Create an empty file
+ let file = File::create("test_empty.bin").expect("Failed to create file");
+ drop(file);
+
+ let mut file = File::open("test_empty.bin").expect("Failed to open file");
+ let mut count = 0;
+
+ file.decode(Box::new(|_entry| {
+ count += 1;
+ Ok(true)
+ })).expect("Failed to decode empty file");
+
+ assert_eq!(count, 0);
+
+ // Clean up
+ let _ = fs::remove_file("test_empty.bin");
+ }
+
+ #[test]
+ fn test_entry_decode_early_termination() {
+ let entries = vec![
+ Entry {
+ version: 1,
+ id: 1,
+ stream_id: 10,
+ data: "first".as_bytes().to_vec(),
+ callback: None,
+ },
+ Entry {
+ version: 1,
+ id: 2,
+ stream_id: 20,
+ data: "second".as_bytes().to_vec(),
+ callback: None,
+ },
+ ];
+
+ // Encode entries to a file
+ let mut file = File::create("test_early_term.bin").expect("Failed to create file");
+ for entry in &entries {
+ file.write_all(&entry.encode()).expect("Failed to write entry");
+ }
+ file.sync_all().expect("Failed to flush file");
+ drop(file);
+
+ // Decode with early termination
+ let mut file = File::open("test_early_term.bin").expect("Failed to open file");
+ let mut count = 0;
+
+ file.decode(Box::new(|_entry| {
+ count += 1;
+ if count == 1 {
+ Ok(false) // Stop after first entry
+ } else {
+ Ok(true)
+ }
+ })).expect("Failed to decode with early termination");
+
+ assert_eq!(count, 1);
+
+ // Clean up
+ let _ = fs::remove_file("test_early_term.bin");
+ }
+
+ #[test]
+ fn test_entry_encode_large_data() {
+ let large_data = vec![0x42; 1024 * 1024]; // 1MB of data
+ let entry = Entry {
+ version: 1,
+ id: 999,
+ stream_id: 888,
+ data: large_data.clone(),
+ callback: None,
+ };
+
+ let encoded = entry.encode();
+
+ // Write and read back
+ let mut file = File::create("test_large_entry.bin").expect("Failed to create file");
+ file.write_all(&encoded).expect("Failed to write to file");
+ file.sync_all().expect("Failed to flush file");
+ drop(file);
+
+ let mut file = File::open("test_large_entry.bin").expect("Failed to open file");
+ file.decode(Box::new(|decoded_entry| {
+ assert_eq!(decoded_entry.version, 1);
+ assert_eq!(decoded_entry.id, 999);
+ assert_eq!(decoded_entry.stream_id, 888);
+ assert_eq!(decoded_entry.data.len(), 1024 * 1024);
+ assert_eq!(decoded_entry.data, large_data);
+ Ok(true)
+ })).expect("Failed to decode large entry");
+
+ // Clean up
+ let _ = fs::remove_file("test_large_entry.bin");
+ }
+}
diff --git a/crates/streamstore/src/errors.rs b/crates/streamstore/src/errors.rs
new file mode 100644
index 0000000..dcdb568
--- /dev/null
+++ b/crates/streamstore/src/errors.rs
@@ -0,0 +1,129 @@
+use thiserror::Error;
+
+#[derive(Debug, Error)]
+pub enum Error {
+ #[error("Stream already exists")]
+ AlreadyExists,
+
+ #[error("path {path} is invalid")]
+ InValidPath { path: std::path::PathBuf },
+
+ #[error("invalid data")]
+ InvalidData,
+
+ #[error("internal error")]
+ InternalError,
+
+ #[error("is closed")]
+ CloseError,
+
+ #[error("store is read-only")]
+ StoreIsReadOnly,
+
+ #[error("channel is closed")]
+ WalChannelSendError,
+
+ #[error("IO error")]
+ IoError(std::io::Error),
+
+ #[error("Stream {stream_id} offset {offset} is invalid")]
+ StreamOffsetInvalid { stream_id: u64, offset: u64 },
+
+ #[error("Stream {stream_id} Not Found")]
+ StreamNotFound { stream_id: u64 },
+}
+
+pub fn new_stream_offset_invalid(stream_id: u64, offset: u64) -> anyhow::Error {
+ anyhow::anyhow!(Error::StreamOffsetInvalid { stream_id, offset })
+}
+
+pub fn new_stream_not_found(stream_id: u64) -> anyhow::Error {
+ anyhow::anyhow!(Error::StreamNotFound { stream_id })
+}
+
+pub fn new_io_error(e: std::io::Error) -> anyhow::Error {
+ anyhow::anyhow!(Error::IoError(e))
+}
+
+pub fn new_invalid_path(path: std::path::PathBuf) -> anyhow::Error {
+ anyhow::anyhow!(Error::InValidPath { path })
+}
+
+pub fn new_invalid_data() -> anyhow::Error {
+ anyhow::anyhow!(Error::InvalidData)
+}
+
+pub fn new_store_is_read_only() -> anyhow::Error {
+ anyhow::anyhow!(Error::StoreIsReadOnly)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::path::PathBuf;
+
+ #[test]
+ fn test_error_display() {
+ let error = Error::AlreadyExists;
+ assert_eq!(error.to_string(), "Stream already exists");
+
+ let path = PathBuf::from("/invalid/path");
+ let error = Error::InValidPath { path: path.clone() };
+ assert_eq!(error.to_string(), format!("path {} is invalid", path.display()));
+
+ let error = Error::InvalidData;
+ assert_eq!(error.to_string(), "invalid data");
+
+ let error = Error::InternalError;
+ assert_eq!(error.to_string(), "internal error");
+
+ let error = Error::CloseError;
+ assert_eq!(error.to_string(), "is closed");
+
+ let error = Error::StoreIsReadOnly;
+ assert_eq!(error.to_string(), "store is read-only");
+
+ let error = Error::WalChannelSendError;
+ assert_eq!(error.to_string(), "channel is closed");
+
+ let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
+ let error = Error::IoError(io_error);
+ assert_eq!(error.to_string(), "IO error");
+
+ let error = Error::StreamOffsetInvalid { stream_id: 123, offset: 456 };
+ assert_eq!(error.to_string(), "Stream 123 offset 456 is invalid");
+
+ let error = Error::StreamNotFound { stream_id: 789 };
+ assert_eq!(error.to_string(), "Stream 789 Not Found");
+ }
+
+ #[test]
+ fn test_error_constructors() {
+ let err = new_stream_offset_invalid(123, 456);
+ assert!(err.to_string().contains("Stream 123 offset 456 is invalid"));
+
+ let err = new_stream_not_found(789);
+ assert!(err.to_string().contains("Stream 789 Not Found"));
+
+ let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "test error");
+ let err = new_io_error(io_error);
+ assert!(err.to_string().contains("IO error"));
+
+ let path = PathBuf::from("/test/path");
+ let err = new_invalid_path(path.clone());
+ assert!(err.to_string().contains(&format!("path {} is invalid", path.display())));
+
+ let err = new_invalid_data();
+ assert!(err.to_string().contains("invalid data"));
+
+ let err = new_store_is_read_only();
+ assert!(err.to_string().contains("store is read-only"));
+ }
+
+ #[test]
+ fn test_error_debug() {
+ let error = Error::AlreadyExists;
+ let debug_str = format!("{:?}", error);
+ assert!(debug_str.contains("AlreadyExists"));
+ }
+}
\ No newline at end of file
diff --git a/src/futures.rs b/crates/streamstore/src/futures.rs
similarity index 100%
rename from src/futures.rs
rename to crates/streamstore/src/futures.rs
diff --git a/src/lib.rs b/crates/streamstore/src/lib.rs
similarity index 100%
rename from src/lib.rs
rename to crates/streamstore/src/lib.rs
diff --git a/crates/streamstore/src/mem_table.rs b/crates/streamstore/src/mem_table.rs
new file mode 100644
index 0000000..52a4704
--- /dev/null
+++ b/crates/streamstore/src/mem_table.rs
@@ -0,0 +1,456 @@
+use crate::{entry::Entry, table::StreamTable};
+use anyhow::Result;
+use std::{
+ collections::HashMap,
+ io,
+ sync::{atomic::AtomicU64, Arc, Mutex, Weak},
+};
+
+pub type MemTableArc = Arc;
+pub type MemTableWeak = Weak;
+pub(crate) type GetStreamOffset = Box Result + Send + Sync>;
+pub struct MemTable {
+ stream_tables: Mutex>,
+ first_entry: AtomicU64,
+ last_entry: AtomicU64,
+ size: AtomicU64,
+ get_stream_offset: Mutex,
+}
+
+impl MemTable {
+ pub fn new(get_stream_offset: GetStreamOffset) -> Self {
+ MemTable {
+ stream_tables: Mutex::new(HashMap::new()),
+ first_entry: AtomicU64::new(0),
+ last_entry: AtomicU64::new(0),
+ size: AtomicU64::new(0),
+ get_stream_offset: Mutex::new(get_stream_offset),
+ }
+ }
+
+ pub fn get_first_entry(&self) -> u64 {
+ self.first_entry.load(std::sync::atomic::Ordering::SeqCst)
+ }
+
+ pub fn get_last_entry(&self) -> u64 {
+ self.last_entry.load(std::sync::atomic::Ordering::SeqCst)
+ }
+
+ pub fn get_size(&self) -> u64 {
+ self.size.load(std::sync::atomic::Ordering::SeqCst)
+ }
+
+ pub fn get_stream_ids(&self) -> Vec {
+ let guard = self.stream_tables.lock().unwrap();
+ guard.keys().cloned().collect()
+ }
+
+ pub fn get_stream_tables(&self) -> std::sync::MutexGuard> {
+ self.stream_tables.lock().unwrap()
+ }
+
+ pub fn get_stream_range(&self, stream_id: u64) -> Option<(u64, u64)> {
+ let guard = self.stream_tables.lock().unwrap();
+ if let Some(stream_table) = guard.get(&stream_id) {
+ return stream_table.get_stream_range();
+ }
+ None
+ }
+
+ pub fn read_stream(&self, stream_id: u64, offset: u64, buf: &mut [u8]) -> io::Result {
+ let guard = self.stream_tables.lock().unwrap();
+ if let Some(stream_table) = guard.get(&stream_id) {
+ return stream_table.read_stream(offset, buf);
+ }
+ Err(io::Error::new(
+ io::ErrorKind::NotFound,
+ format!("Stream ID {} not found", stream_id),
+ ))
+ }
+
+ // return the stream offset
+ pub fn append(&self, entry: &Entry) -> Result {
+ assert!(entry.stream_id != 0, "Stream ID cannot be zero");
+ assert!(entry.data.len() > 0, "Entry data cannot be empty");
+ assert!(entry.id > 0, "Entry ID must be greater than zero");
+ assert!(
+ entry.id > self.last_entry.load(std::sync::atomic::Ordering::SeqCst),
+ "Entry ID must be greater than the last entry ID"
+ );
+
+ let data_len = entry.data.len() as u64;
+
+ let mut guard = self.stream_tables.lock().unwrap();
+
+ let res = match guard.get_mut(&entry.stream_id) {
+ Some(stream_table) => stream_table,
+ None => {
+ let offset = match self.get_stream_offset.lock().unwrap()(entry.stream_id) {
+ Ok(offset) => offset,
+ Err(e) => return Err(e),
+ };
+ guard.insert(entry.stream_id, StreamTable::new(entry.stream_id, offset));
+ guard.get_mut(&entry.stream_id).unwrap()
+ }
+ };
+
+ // Append the data to the stream table
+ let offset = res.append(&entry.data)?;
+
+ // Update the stream table
+ self.size
+ .fetch_add(data_len, std::sync::atomic::Ordering::SeqCst);
+
+ self.last_entry
+ .store(entry.id, std::sync::atomic::Ordering::SeqCst);
+
+ if self.first_entry.load(std::sync::atomic::Ordering::SeqCst) == 0 {
+ self.first_entry
+ .store(entry.id, std::sync::atomic::Ordering::SeqCst);
+ }
+ Ok(offset)
+ }
+}
+
+/// Asserts that the type `T` is `Send` and `Sync`.
+/// This is useful for ensuring that types used in concurrent contexts are safe to share across threads.
+#[allow(unused)]
+fn assert_send_sync() {}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::entry::Entry;
+
+ #[test]
+ fn test_stream_data() {
+ assert_send_sync::();
+ }
+
+ #[test]
+ fn test_mem_table_new() {
+ let get_stream_offset = Box::new(|_stream_id| Ok(0));
+ let mem_table = MemTable::new(get_stream_offset);
+
+ assert_eq!(mem_table.get_first_entry(), 0);
+ assert_eq!(mem_table.get_last_entry(), 0);
+ assert_eq!(mem_table.get_size(), 0);
+ assert!(mem_table.get_stream_ids().is_empty());
+ }
+
+ #[test]
+ fn test_mem_table_append_single_entry() {
+ let get_stream_offset = Box::new(|_stream_id| Ok(0));
+ let mem_table = MemTable::new(get_stream_offset);
+
+ let entry = Entry {
+ version: 1,
+ id: 1,
+ stream_id: 100,
+ data: b"test data".to_vec(),
+ callback: None,
+ };
+
+ let offset = mem_table.append(&entry).unwrap();
+ assert_eq!(offset, 9); // Length of "test data"
+
+ assert_eq!(mem_table.get_first_entry(), 1);
+ assert_eq!(mem_table.get_last_entry(), 1);
+ assert_eq!(mem_table.get_size(), 9);
+ assert_eq!(mem_table.get_stream_ids(), vec![100]);
+ }
+
+ #[test]
+ fn test_mem_table_append_multiple_entries() {
+ let get_stream_offset = Box::new(|_stream_id| Ok(0));
+ let mem_table = MemTable::new(get_stream_offset);
+
+ let entries = vec![
+ Entry {
+ version: 1,
+ id: 1,
+ stream_id: 100,
+ data: b"first".to_vec(),
+ callback: None,
+ },
+ Entry {
+ version: 1,
+ id: 2,
+ stream_id: 100,
+ data: b"second".to_vec(),
+ callback: None,
+ },
+ Entry {
+ version: 1,
+ id: 3,
+ stream_id: 200,
+ data: b"third".to_vec(),
+ callback: None,
+ },
+ ];
+
+ // For stream 100: first entry at offset 0, gets offset 5
+ let offset1 = mem_table.append(&entries[0]).unwrap();
+ assert_eq!(offset1, 5); // 0 + 5
+
+ // For stream 100: second entry continues from offset 5, gets offset 11
+ let offset2 = mem_table.append(&entries[1]).unwrap();
+ assert_eq!(offset2, 11); // 5 + 6
+
+ // For stream 200: first entry at offset 0, gets offset 5
+ let offset3 = mem_table.append(&entries[2]).unwrap();
+ assert_eq!(offset3, 5); // 0 + 5 (new stream starts at 0)
+
+ assert_eq!(mem_table.get_first_entry(), 1);
+ assert_eq!(mem_table.get_last_entry(), 3);
+ assert_eq!(mem_table.get_size(), 16); // 5 + 6 + 5
+
+ let mut stream_ids = mem_table.get_stream_ids();
+ stream_ids.sort();
+ assert_eq!(stream_ids, vec![100, 200]);
+ }
+
+ #[test]
+ fn test_mem_table_get_stream_range() {
+ let get_stream_offset = Box::new(|_stream_id| Ok(0));
+ let mem_table = MemTable::new(get_stream_offset);
+
+ // Test with non-existent stream
+ assert_eq!(mem_table.get_stream_range(999), None);
+
+ let entry = Entry {
+ version: 1,
+ id: 1,
+ stream_id: 100,
+ data: b"test data".to_vec(),
+ callback: None,
+ };
+
+ mem_table.append(&entry).unwrap();
+
+ let range = mem_table.get_stream_range(100);
+ assert_eq!(range, Some((0, 9)));
+ }
+
+ #[test]
+ fn test_mem_table_read_stream() {
+ let get_stream_offset = Box::new(|_stream_id| Ok(0));
+ let mem_table = MemTable::new(get_stream_offset);
+
+ let entry = Entry {
+ version: 1,
+ id: 1,
+ stream_id: 100,
+ data: b"hello world".to_vec(),
+ callback: None,
+ };
+
+ mem_table.append(&entry).unwrap();
+
+ // Test reading the entire data
+ let mut buf = vec![0u8; 11];
+ let bytes_read = mem_table.read_stream(100, 0, &mut buf).unwrap();
+ assert_eq!(bytes_read, 11);
+ assert_eq!(&buf, b"hello world");
+
+ // Test reading partial data
+ let mut buf = vec![0u8; 5];
+ let bytes_read = mem_table.read_stream(100, 0, &mut buf).unwrap();
+ assert_eq!(bytes_read, 5);
+ assert_eq!(&buf, b"hello");
+
+ // Test reading from offset
+ let mut buf = vec![0u8; 5];
+ let bytes_read = mem_table.read_stream(100, 6, &mut buf).unwrap();
+ assert_eq!(bytes_read, 5);
+ assert_eq!(&buf, b"world");
+
+ // Test reading non-existent stream
+ let mut buf = vec![0u8; 5];
+ let result = mem_table.read_stream(999, 0, &mut buf);
+ assert!(result.is_err());
+ assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound);
+ }
+
+ #[test]
+ fn test_mem_table_with_custom_stream_offset() {
+ let get_stream_offset = Box::new(|stream_id| {
+ match stream_id {
+ 100 => Ok(1000),
+ 200 => Ok(2000),
+ _ => Ok(0),
+ }
+ });
+ let mem_table = MemTable::new(get_stream_offset);
+
+ let entry1 = Entry {
+ version: 1,
+ id: 1,
+ stream_id: 100,
+ data: b"data1".to_vec(),
+ callback: None,
+ };
+
+ let entry2 = Entry {
+ version: 1,
+ id: 2,
+ stream_id: 200,
+ data: b"data2".to_vec(),
+ callback: None,
+ };
+
+ let offset1 = mem_table.append(&entry1).unwrap();
+ let offset2 = mem_table.append(&entry2).unwrap();
+
+ assert_eq!(offset1, 1005); // 1000 + 5
+ assert_eq!(offset2, 2005); // 2000 + 5
+
+ assert_eq!(mem_table.get_stream_range(100), Some((1000, 1005)));
+ assert_eq!(mem_table.get_stream_range(200), Some((2000, 2005)));
+ }
+
+ #[test]
+ #[should_panic(expected = "Stream ID cannot be zero")]
+ fn test_mem_table_append_zero_stream_id() {
+ let get_stream_offset = Box::new(|_stream_id| Ok(0));
+ let mem_table = MemTable::new(get_stream_offset);
+
+ let entry = Entry {
+ version: 1,
+ id: 1,
+ stream_id: 0, // Invalid stream ID
+ data: b"test".to_vec(),
+ callback: None,
+ };
+
+ mem_table.append(&entry).unwrap();
+ }
+
+ #[test]
+ #[should_panic(expected = "Entry data cannot be empty")]
+ fn test_mem_table_append_empty_data() {
+ let get_stream_offset = Box::new(|_stream_id| Ok(0));
+ let mem_table = MemTable::new(get_stream_offset);
+
+ let entry = Entry {
+ version: 1,
+ id: 1,
+ stream_id: 100,
+ data: Vec::new(), // Empty data
+ callback: None,
+ };
+
+ mem_table.append(&entry).unwrap();
+ }
+
+ #[test]
+ #[should_panic(expected = "Entry ID must be greater than zero")]
+ fn test_mem_table_append_zero_entry_id() {
+ let get_stream_offset = Box::new(|_stream_id| Ok(0));
+ let mem_table = MemTable::new(get_stream_offset);
+
+ let entry = Entry {
+ version: 1,
+ id: 0, // Invalid entry ID
+ stream_id: 100,
+ data: b"test".to_vec(),
+ callback: None,
+ };
+
+ mem_table.append(&entry).unwrap();
+ }
+
+ #[test]
+ #[should_panic(expected = "Entry ID must be greater than the last entry ID")]
+ fn test_mem_table_append_non_increasing_entry_id() {
+ let get_stream_offset = Box::new(|_stream_id| Ok(0));
+ let mem_table = MemTable::new(get_stream_offset);
+
+ let entry1 = Entry {
+ version: 1,
+ id: 2,
+ stream_id: 100,
+ data: b"first".to_vec(),
+ callback: None,
+ };
+
+ let entry2 = Entry {
+ version: 1,
+ id: 1, // Lower than previous entry ID
+ stream_id: 100,
+ data: b"second".to_vec(),
+ callback: None,
+ };
+
+ mem_table.append(&entry1).unwrap();
+ mem_table.append(&entry2).unwrap(); // Should panic
+ }
+
+ #[test]
+ fn test_mem_table_get_stream_offset_error() {
+ let get_stream_offset = Box::new(|stream_id| {
+ if stream_id == 999 {
+ Err(anyhow::anyhow!("Stream offset error"))
+ } else {
+ Ok(0)
+ }
+ });
+ let mem_table = MemTable::new(get_stream_offset);
+
+ let entry = Entry {
+ version: 1,
+ id: 1,
+ stream_id: 999,
+ data: b"test".to_vec(),
+ callback: None,
+ };
+
+ let result = mem_table.append(&entry);
+ assert!(result.is_err());
+ assert!(result.unwrap_err().to_string().contains("Stream offset error"));
+ }
+
+ #[test]
+ fn test_mem_table_concurrent_access() {
+ use std::sync::{Arc, atomic::{AtomicU64, Ordering}};
+ use std::thread;
+
+ let get_stream_offset = Box::new(|_stream_id| Ok(0));
+ let mem_table = Arc::new(MemTable::new(get_stream_offset));
+ let entry_id_counter = Arc::new(AtomicU64::new(0));
+
+ let mut handles = vec![];
+
+ // Spawn multiple threads to append entries
+ for i in 1..=10 {
+ let mem_table_clone = Arc::clone(&mem_table);
+ let counter_clone = Arc::clone(&entry_id_counter);
+ let handle = thread::spawn(move || {
+ let entry_id = counter_clone.fetch_add(1, Ordering::SeqCst) + 1;
+ let entry = Entry {
+ version: 1,
+ id: entry_id,
+ stream_id: 100 + (i % 3), // Use different streams to reduce contention
+ data: format!("data{}", i).into_bytes(),
+ callback: None,
+ };
+ mem_table_clone.append(&entry)
+ });
+ handles.push(handle);
+ }
+
+ // Wait for all threads to complete
+ let mut results = vec![];
+ for handle in handles {
+ results.push(handle.join().unwrap());
+ }
+
+ // Check that all operations succeeded with proper ID ordering
+ let successful_count = results.iter().filter(|r| r.is_ok()).count();
+ assert_eq!(successful_count, 10);
+
+ // Verify final state
+ assert_eq!(mem_table.get_last_entry(), 10);
+ assert_eq!(mem_table.get_first_entry(), 1);
+ }
+}
diff --git a/src/metrics.rs b/crates/streamstore/src/metrics.rs
similarity index 100%
rename from src/metrics.rs
rename to crates/streamstore/src/metrics.rs
diff --git a/src/options.rs b/crates/streamstore/src/options.rs
similarity index 100%
rename from src/options.rs
rename to crates/streamstore/src/options.rs
diff --git a/src/reader.rs b/crates/streamstore/src/reader.rs
similarity index 91%
rename from src/reader.rs
rename to crates/streamstore/src/reader.rs
index 8a2d2c6..ed23289 100644
--- a/src/reader.rs
+++ b/crates/streamstore/src/reader.rs
@@ -1,7 +1,7 @@
use std::{io, sync::Arc};
use crate::{
- mem_table::MemTableArc,
+ mem_table::MemTableWeak,
metrics,
store::{SegmentWeak, StreamStoreInner},
};
@@ -14,13 +14,15 @@ pub enum StreamReadState {
MemTable,
}
+#[derive(Clone)]
+
pub struct StreamReader {
stream_id: u64,
inner: Arc,
- offset: std::sync::atomic::AtomicU64,
- read_memtable: Option,
+ offset: Arc,
+ read_mem_table: Option,
read_segment: Option,
- read_state: std::sync::Mutex,
+ read_state: Arc>,
}
impl StreamReader {
@@ -28,10 +30,10 @@ impl StreamReader {
Self {
inner,
stream_id,
- read_memtable: None,
+ read_mem_table: None,
read_segment: None,
- read_state: std::sync::Mutex::new(StreamReadState::None),
- offset: std::sync::atomic::AtomicU64::new(0),
+ read_state: Arc::new(std::sync::Mutex::new(StreamReadState::None)),
+ offset: Arc::new(std::sync::atomic::AtomicU64::new(0)),
}
}
@@ -54,7 +56,7 @@ impl StreamReader {
}
fn reset_read_state(&mut self) {
- self.read_memtable = None;
+ self.read_mem_table = None;
self.read_segment = None;
*self.read_state.lock().unwrap() = StreamReadState::None;
}
@@ -118,13 +120,15 @@ impl StreamReader {
fn read_from_tables(&mut self, buf: &mut [u8]) -> io::Result {
let mut read_bytes_all = 0;
- if let Some(memtable) = &self.read_memtable {
- let bytes_read =
- memtable.read_stream(self.stream_id, self.offset(), &mut buf[read_bytes_all..])?;
- self.offset_inc(bytes_read);
- read_bytes_all += bytes_read;
- if read_bytes_all >= buf.len() {
- return Ok(read_bytes_all); // Stop if we filled the buffer
+ if let Some(mem_table) = &self.read_mem_table {
+ if let Some(table) = mem_table.upgrade() {
+ let bytes_read =
+ table.read_stream(self.stream_id, self.offset(), &mut buf[read_bytes_all..])?;
+ self.offset_inc(bytes_read);
+ read_bytes_all += bytes_read;
+ if read_bytes_all >= buf.len() {
+ return Ok(read_bytes_all); // Stop if we filled the buffer
+ }
}
}
@@ -146,7 +150,7 @@ impl StreamReader {
read_bytes_all += bytes_read;
if read_bytes_all >= buf.len() {
- self.read_memtable = Some(memtable.clone());
+ self.read_mem_table = Some(Arc::downgrade(&memtable));
return Ok(read_bytes_all); // Stop if we filled the buffer
}
}
@@ -203,8 +207,8 @@ impl io::Read for StreamReader {
false
}
}) {
- Some(memtable) => {
- let (begin, end) = memtable.get_stream_range(self.stream_id).unwrap();
+ Some(mem_table) => {
+ let (begin, end) = mem_table.get_stream_range(self.stream_id).unwrap();
log::info!(
"Stream ID {} offset {} found in MemTable begin {} end {}",
self.stream_id,
@@ -212,7 +216,7 @@ impl io::Read for StreamReader {
begin,
end
);
- self.read_memtable = Some(memtable.clone());
+ self.read_mem_table = Some(Arc::downgrade(&mem_table));
*self.read_state.lock().unwrap() = StreamReadState::MemTables;
continue;
}
diff --git a/src/reload.rs b/crates/streamstore/src/reload.rs
similarity index 98%
rename from src/reload.rs
rename to crates/streamstore/src/reload.rs
index 4de50c6..be6b27d 100644
--- a/src/reload.rs
+++ b/crates/streamstore/src/reload.rs
@@ -124,7 +124,7 @@ pub fn reload_wals(
last_segment_entry_index: u64,
max_table_size: u64,
offset_map: &mut hash_map::HashMap,
-) -> Result<(VecDeque>, HashMap, File)> {
+) -> Result<(VecDeque>, HashMap, (File, PathBuf))> {
// Check if the WAL path exists
if !std::path::Path::new(wal_path).exists() {
// create the wal path if it does not exist
@@ -224,12 +224,12 @@ pub fn reload_wals(
let mut file = OpenOptions::new()
.write(true)
.create(true)
- .open(file_name)
+ .open(file_name.clone())
.map_err(errors::new_io_error)?;
// seek to the end of the file
file.seek(std::io::SeekFrom::End(0))
.map_err(errors::new_io_error)?;
- Ok((tables, files, file))
+ Ok((tables, files, (file, file_name)))
}
diff --git a/src/segments.rs b/crates/streamstore/src/segments.rs
similarity index 98%
rename from src/segments.rs
rename to crates/streamstore/src/segments.rs
index 46c51fc..f4d866f 100644
--- a/src/segments.rs
+++ b/crates/streamstore/src/segments.rs
@@ -16,7 +16,7 @@ const SEGMENT_HEADER_SIZE: u64 = std::mem::size_of::() as u64;
const SEGMENT_STREAM_HEADER_VERSION_V1: u64 = 1;
const SEGMENT_HEADER_VERSION_V1: u32 = 1;
-#[derive(Default, Debug, Clone)]
+#[derive(Debug, Clone)]
#[repr(C)]
pub struct SegmentStreamHeader {
pub(crate) version: u64,
@@ -32,6 +32,19 @@ pub struct SegmentStreamHeader {
pub(crate) crc64: u64,
}
+impl Default for SegmentStreamHeader {
+ fn default() -> Self {
+ SegmentStreamHeader {
+ version: SEGMENT_STREAM_HEADER_VERSION_V1,
+ stream_id: 0,
+ offset: 0,
+ file_offset: 0,
+ size: 0,
+ crc64: 0,
+ }
+ }
+}
+
#[derive(Clone, Debug)]
#[repr(C)]
pub struct SegmentHeader {
@@ -587,7 +600,7 @@ fn test_segment_header_size() {
"header.size {}",
header.size
); // 1000 entries * 10 bytes each
- assert!(header.crc64 == 0); // checksum is not calculated in this test
+ assert!(header.crc64 > 0); // checksum is not calculated in this test
file_offset += header.size;
}
diff --git a/src/store.rs b/crates/streamstore/src/store.rs
similarity index 99%
rename from src/store.rs
rename to crates/streamstore/src/store.rs
index 7b3f591..a7fd867 100644
--- a/src/store.rs
+++ b/crates/streamstore/src/store.rs
@@ -559,7 +559,7 @@ impl Store {
}
}
- let (mut mem_tables, files, file) = reload::reload_wals(
+ let (mut mem_tables, files, (file, file_name)) = reload::reload_wals(
&options.wal_path,
last_segment_entry_index,
options.max_table_size,
@@ -602,7 +602,7 @@ impl Store {
let last_log_entry = mem_table.get_last_entry();
let wal = Wal::new(
- file,
+ (file, file_name),
options.wal_path.clone(),
options.max_wal_size,
last_log_entry,
diff --git a/crates/streamstore/src/table.rs b/crates/streamstore/src/table.rs
new file mode 100644
index 0000000..5d26774
--- /dev/null
+++ b/crates/streamstore/src/table.rs
@@ -0,0 +1,470 @@
+use std::{io, slice::Iter};
+
+use anyhow::Result;
+
+const STREAM_DATA_BUFFER_CAP: u64 = 128 << 10; // 128KB
+
+pub struct StreamData {
+ stream_id: u64,
+ offset: u64,
+ data: Vec,
+}
+
+impl StreamData {
+ pub fn new(stream_id: u64, offset: u64, buffer_cap: u64) -> Self {
+ StreamData {
+ stream_id,
+ offset,
+ data: Vec::with_capacity(buffer_cap as usize),
+ }
+ }
+
+ // Fill the buffer with data
+ // If the buffer is full, return the remaining data
+ // If the buffer is not full, return None
+ pub fn fill<'a>(&mut self, data: &'a [u8]) -> Result<(usize, Option<&'a [u8]>)> {
+ let available = self.cap_remaining().min(data.len());
+ self.data.extend_from_slice(&data[..available as usize]);
+
+ let remaining_data = if available < data.len() {
+ Some(&data[available as usize..])
+ } else {
+ None
+ };
+
+ Ok((available, remaining_data))
+ }
+
+ pub fn get_stream_range(&self) -> Option<(u64, u64)> {
+ if self.data.is_empty() {
+ return None;
+ }
+ let start = self.offset;
+ let end = self.offset + self.data.len() as u64;
+ Some((start, end))
+ }
+
+ pub fn size(&self) -> u64 {
+ self.data.len() as u64
+ }
+
+ pub fn data(&self) -> &[u8] {
+ &self.data
+ }
+
+ pub fn cap_remaining(&self) -> usize {
+ self.data.capacity() - self.data.len()
+ }
+}
+
+pub struct StreamTable {
+ stream_id: u64,
+ offset: u64,
+ size: u64,
+ stream_datas: Vec,
+}
+
+impl StreamTable {
+ pub fn new(stream_id: u64, offset: u64) -> Self {
+ StreamTable {
+ stream_id,
+ offset: offset,
+ size: 0,
+ stream_datas: Vec::new(),
+ }
+ }
+
+ pub fn stream_id(&self) -> u64 {
+ self.stream_id
+ }
+ pub fn offset(&self) -> u64 {
+ self.offset
+ }
+ pub fn size(&self) -> u64 {
+ self.size
+ }
+ pub fn stream_datas(&self) -> Iter {
+ self.stream_datas.iter()
+ }
+
+ pub fn append(&mut self, data: &[u8]) -> Result {
+ if self.stream_datas.is_empty() || self.stream_datas.last().unwrap().cap_remaining() == 0 {
+ if !self.stream_datas.is_empty() {
+ assert_eq!(
+ self.stream_datas.last().unwrap().size(),
+ STREAM_DATA_BUFFER_CAP
+ );
+ }
+
+ self.stream_datas.push(StreamData::new(
+ self.stream_id,
+ self.offset + self.size,
+ STREAM_DATA_BUFFER_CAP,
+ ));
+ }
+
+ let stream_data = self.stream_datas.last_mut().unwrap();
+ let (size, remain_buffer) = stream_data.fill(data)?;
+ self.size += size as u64;
+
+ // If the buffer is full, we need to create a new buffer
+ if let Some(buffer) = remain_buffer {
+ return self.append(buffer);
+ }
+
+ Ok(self.offset + self.size)
+ }
+
+ pub fn get_stream_range(&self) -> Option<(u64, u64)> {
+ if self.stream_datas.is_empty() {
+ return None;
+ }
+ return Some((self.offset, self.offset + self.size));
+ }
+
+ pub fn print_stream_meta(&self) {
+ for (i, stream_data) in self.stream_datas.iter().enumerate() {
+ assert!(stream_data.stream_id == self.stream_id);
+ if i != self.stream_datas.len() - 1 {
+ assert_eq!(stream_data.size(), STREAM_DATA_BUFFER_CAP);
+ }
+ }
+ }
+
+ pub fn crc64(&self) -> u64 {
+ let crc64 = crc::Crc::::new(&crc::CRC_64_REDIS);
+ let mut digest = crc64.digest();
+ for stream_data in &self.stream_datas {
+ digest.update(&stream_data.data);
+ }
+ digest.finalize()
+ }
+
+ pub fn read_stream(&self, offset: u64, buf: &mut [u8]) -> io::Result {
+ self.print_stream_meta();
+
+ let mut offset = offset;
+ let mut size = buf.len() as u64;
+ let mut copied_size = 0;
+
+ // find the first stream data that offset <= offset by quick search
+ let res = self.stream_datas.binary_search_by(|stream_data| {
+ let (_begin, end) = stream_data.get_stream_range().unwrap();
+ end.cmp(&offset)
+ });
+ let mut index = match res {
+ Ok(index) => index + 1, // we want the first stream data that starts after the offset
+ Err(index) => index,
+ };
+
+ if index >= self.stream_datas.len() {
+ log::debug!(
+ "Offset {} find index {} is beyond the last stream data [{},{}), returning 0 bytes read",
+ offset,
+ index,
+ self.stream_datas.last().unwrap().offset,
+ self.stream_datas.last().unwrap().offset + self.stream_datas.last().unwrap().size()
+ );
+ return Ok(0);
+ }
+ // read the data from the stream data
+ while index < self.stream_datas.len() && size > 0 {
+ let stream_data = &self.stream_datas[index];
+ let stream_data_offset = stream_data.offset;
+ let stream_data_size = stream_data.size();
+
+ assert!(
+ stream_data_offset <= offset && offset <= stream_data_offset + stream_data_size
+ );
+ // we can read the data from this stream data
+
+ let start = (offset - stream_data_offset) as usize;
+ let end = (start + size as usize).min(stream_data_size as usize);
+
+ // copy the data to the buffer
+ let data_to_copy = &stream_data.data[start..end];
+ let bytes_to_copy = data_to_copy.len();
+ buf[copied_size as usize..(copied_size as u64 + bytes_to_copy as u64) as usize]
+ .copy_from_slice(data_to_copy);
+
+ copied_size += bytes_to_copy;
+ size -= (bytes_to_copy) as u64;
+ offset += bytes_to_copy as u64;
+ index += 1;
+ }
+
+ Ok(copied_size)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_stream_data_new() {
+ let buffer_cap = 1024u64;
+ let stream_data = StreamData::new(123, 1000, buffer_cap);
+ assert_eq!(stream_data.stream_id, 123);
+ assert_eq!(stream_data.offset, 1000);
+ assert_eq!(stream_data.data.capacity(), buffer_cap as usize);
+ assert_eq!(stream_data.size(), 0);
+ assert_eq!(stream_data.cap_remaining(), buffer_cap as usize);
+ assert_eq!(stream_data.get_stream_range(), None);
+ }
+
+ #[test]
+ fn test_stream_data_fill() {
+ let buffer_cap = 100u64;
+ let mut stream_data = StreamData::new(1, 0, buffer_cap);
+
+ // Fill with data that fits
+ let data1 = b"hello world";
+ let (filled, remaining) = stream_data.fill(data1).unwrap();
+ assert_eq!(filled, 11);
+ assert!(remaining.is_none());
+ assert_eq!(stream_data.size(), 11);
+ assert_eq!(stream_data.cap_remaining(), (buffer_cap as usize) - 11);
+ assert_eq!(stream_data.get_stream_range(), Some((0, 11)));
+
+ // Fill with more data
+ let data2 = b" how are you?";
+ let (filled, remaining) = stream_data.fill(data2).unwrap();
+ assert_eq!(filled, 13);
+ assert!(remaining.is_none());
+ assert_eq!(stream_data.size(), 24);
+ assert_eq!(stream_data.cap_remaining(), (buffer_cap as usize) - 24);
+ assert_eq!(stream_data.get_stream_range(), Some((0, 24)));
+ }
+
+ #[test]
+ fn test_stream_data_fill_overflow() {
+ let buffer_cap = 10u64;
+ let mut stream_data = StreamData::new(1, 0, buffer_cap);
+
+ // Fill with data that exceeds capacity
+ let data = b"this is a very long string that exceeds capacity";
+ let (filled, remaining) = stream_data.fill(data).unwrap();
+ assert_eq!(filled, buffer_cap as usize);
+ assert!(remaining.is_some());
+ assert_eq!(remaining.unwrap(), &data[buffer_cap as usize..]);
+ assert_eq!(stream_data.size(), buffer_cap);
+ assert_eq!(stream_data.cap_remaining(), 0);
+ assert_eq!(stream_data.get_stream_range(), Some((0, buffer_cap)));
+ }
+
+ #[test]
+ fn test_stream_data_data() {
+ let mut stream_data = StreamData::new(1, 0, 100);
+ let data = b"test data";
+ stream_data.fill(data).unwrap();
+ assert_eq!(stream_data.data(), data);
+ }
+
+ #[test]
+ fn test_stream_table_new() {
+ let table = StreamTable::new(123, 1000);
+ assert_eq!(table.stream_id(), 123);
+ assert_eq!(table.offset(), 1000);
+ assert_eq!(table.size(), 0);
+ assert_eq!(table.stream_datas().count(), 0);
+ assert_eq!(table.get_stream_range(), None);
+ }
+
+ #[test]
+ fn test_stream_table_append_single() {
+ let mut table = StreamTable::new(1, 0);
+ let data = b"hello world";
+ let offset = table.append(data).unwrap();
+ assert_eq!(offset, 11);
+ assert_eq!(table.size(), 11);
+ assert_eq!(table.stream_datas().count(), 1);
+ assert_eq!(table.get_stream_range(), Some((0, 11)));
+ }
+
+ #[test]
+ fn test_stream_table_append_multiple() {
+ let mut table = StreamTable::new(1, 100);
+
+ let data1 = b"first";
+ let offset1 = table.append(data1).unwrap();
+ assert_eq!(offset1, 105); // 100 + 5
+
+ let data2 = b"second";
+ let offset2 = table.append(data2).unwrap();
+ assert_eq!(offset2, 111); // 100 + 5 + 6
+
+ assert_eq!(table.size(), 11);
+ assert_eq!(table.stream_datas().count(), 1);
+ assert_eq!(table.get_stream_range(), Some((100, 111)));
+ }
+
+ #[test]
+ fn test_stream_table_append_large_data() {
+ let mut table = StreamTable::new(1, 0);
+
+ // Create data larger than STREAM_DATA_BUFFER_CAP
+ let large_data = vec![0x42; (STREAM_DATA_BUFFER_CAP + 1000) as usize];
+ let offset = table.append(&large_data).unwrap();
+ assert_eq!(offset, large_data.len() as u64);
+ assert_eq!(table.size(), large_data.len() as u64);
+ assert!(table.stream_datas().count() > 1); // Should create multiple buffers
+ assert_eq!(table.get_stream_range(), Some((0, large_data.len() as u64)));
+ }
+
+ #[test]
+ fn test_stream_table_crc64() {
+ let mut table = StreamTable::new(1, 0);
+ let data = b"test data for crc";
+ table.append(data).unwrap();
+
+ let crc = table.crc64();
+
+ // Verify CRC is calculated correctly
+ let crc64 = crc::Crc::::new(&crc::CRC_64_REDIS);
+ let expected_crc = crc64.checksum(data);
+ assert_eq!(crc, expected_crc);
+ }
+
+ #[test]
+ fn test_stream_table_read_stream() {
+ let mut table = StreamTable::new(1, 0);
+ let data = b"hello world test data";
+ table.append(data).unwrap();
+
+ // Read entire data
+ let mut buf = vec![0u8; data.len()];
+ let bytes_read = table.read_stream(0, &mut buf).unwrap();
+ assert_eq!(bytes_read, data.len());
+ assert_eq!(&buf, data);
+
+ // Read partial data from start
+ let mut buf = vec![0u8; 5];
+ let bytes_read = table.read_stream(0, &mut buf).unwrap();
+ assert_eq!(bytes_read, 5);
+ assert_eq!(&buf, b"hello");
+
+ // Read partial data from middle
+ let mut buf = vec![0u8; 5];
+ let bytes_read = table.read_stream(6, &mut buf).unwrap();
+ assert_eq!(bytes_read, 5);
+ assert_eq!(&buf, b"world");
+
+ // Read from end
+ let mut buf = vec![0u8; 4];
+ let bytes_read = table.read_stream(17, &mut buf).unwrap();
+ assert_eq!(bytes_read, 4);
+ assert_eq!(&buf, b"data");
+
+ // Read beyond end
+ let mut buf = vec![0u8; 10];
+ let bytes_read = table.read_stream(100, &mut buf).unwrap();
+ assert_eq!(bytes_read, 0);
+ }
+
+ #[test]
+ fn test_stream_table_read_stream_multiple_buffers() {
+ let mut table = StreamTable::new(1, 0);
+
+ // Add data that will span multiple buffers
+ let data_size = (STREAM_DATA_BUFFER_CAP + 1000) as usize;
+ let large_data = (0..data_size).map(|i| (i % 256) as u8).collect::>();
+ table.append(&large_data).unwrap();
+
+ // Read entire data
+ let mut buf = vec![0u8; data_size];
+ let bytes_read = table.read_stream(0, &mut buf).unwrap();
+ assert_eq!(bytes_read, data_size);
+ assert_eq!(buf, large_data);
+
+ // Read across buffer boundaries
+ let start_offset = STREAM_DATA_BUFFER_CAP - 100;
+ let read_size = 200;
+ let mut buf = vec![0u8; read_size as usize];
+ let bytes_read = table.read_stream(start_offset, &mut buf).unwrap();
+ assert_eq!(bytes_read, read_size as usize);
+ assert_eq!(buf, large_data[start_offset as usize..(start_offset + read_size) as usize]);
+ }
+
+ #[test]
+ fn test_stream_data_fill_original() {
+ // Original test from the codebase
+ let mut table = StreamTable::new(1, 0);
+ let count = 1000;
+ let mut next_offset = 0;
+ let crc64 = crc::Crc::::new(&crc::CRC_64_ECMA_182);
+ let mut digest = crc64.digest();
+ for i in 0..count {
+ let data = format!("hello world {}\n", i);
+ digest.update(data.as_bytes());
+ next_offset += data.len() as u64;
+ let offset = table.append(data.as_bytes()).unwrap();
+ assert_eq!(offset, next_offset);
+ }
+
+ let checksum = digest.finalize();
+
+ let mut buf = vec![0u8; next_offset as usize];
+ let read_size = table.read_stream(0, &mut buf).unwrap();
+ assert_eq!(read_size, next_offset as usize);
+ let read_checksum = crc64.checksum(&buf);
+ assert_eq!(read_checksum, checksum);
+
+ let mut read_bytes = Vec::new();
+ loop {
+ let mut buf = vec![0u8; (rand::random::() % 64 + 1) as usize];
+ let read_size = table
+ .read_stream(read_bytes.len() as u64, &mut buf)
+ .unwrap();
+ if read_size == 0 {
+ break;
+ }
+ buf.truncate(read_size);
+ read_bytes.extend_from_slice(buf.as_slice());
+ if read_bytes.len() as u64 >= next_offset {
+ break;
+ }
+
+ assert_eq!(read_size, buf.len() as usize);
+ }
+
+ let read_checksum = crc64.checksum(&read_bytes);
+ assert_eq!(read_bytes.len() as u64, next_offset);
+ assert_eq!(read_checksum, checksum);
+ }
+
+ #[test]
+ fn test_stream_table_print_stream_meta() {
+ let mut table = StreamTable::new(1, 0);
+
+ // Add enough data to create multiple buffers
+ let data_size = (STREAM_DATA_BUFFER_CAP * 2 + 100) as usize;
+ let large_data = vec![0x42; data_size];
+ table.append(&large_data).unwrap();
+
+ // This should not panic
+ table.print_stream_meta();
+ }
+
+ #[test]
+ fn test_stream_data_empty_range() {
+ let stream_data = StreamData::new(1, 100, 1024);
+ assert_eq!(stream_data.get_stream_range(), None);
+ }
+
+ #[test]
+ fn test_stream_data_with_offset() {
+ let mut stream_data = StreamData::new(1, 500, 1024);
+ let data = b"test data";
+ stream_data.fill(data).unwrap();
+ assert_eq!(stream_data.get_stream_range(), Some((500, 509)));
+ }
+
+ #[test]
+ fn test_stream_table_empty_read() {
+ let table = StreamTable::new(1, 0);
+ let mut buf = vec![0u8; 10];
+ let bytes_read = table.read_stream(0, &mut buf).unwrap();
+ assert_eq!(bytes_read, 0);
+ }
+}
diff --git a/src/wal.rs b/crates/streamstore/src/wal.rs
similarity index 91%
rename from src/wal.rs
rename to crates/streamstore/src/wal.rs
index 30b2505..737cc4c 100644
--- a/src/wal.rs
+++ b/crates/streamstore/src/wal.rs
@@ -19,7 +19,7 @@ use std::{
};
pub struct WalInner {
- file: Mutex,
+ file: Mutex<(File, PathBuf)>,
dir: String,
max_size: u64,
file_size: atomic::AtomicU64,
@@ -49,7 +49,7 @@ impl WalInner {
}
let mut file_guard = self.file.lock().unwrap();
- match file_guard.write_all(&buffer) {
+ match file_guard.0.write_all(&buffer) {
Ok(_) => {
// Update the file size
let _ = self
@@ -61,7 +61,7 @@ impl WalInner {
}
}
// Flush the file to ensure all data is written
- file_guard.flush().map_err(errors::new_io_error)?;
+ file_guard.0.flush().map_err(errors::new_io_error)?;
let elapsed = begin_ts.elapsed();
metrics::wal_write_file_seconds.observe(elapsed.as_secs_f64());
@@ -78,9 +78,9 @@ impl WalInner {
let mut file_guard = self.file.lock().unwrap();
- file_guard.sync_all().map_err(errors::new_io_error)?;
+ file_guard.0.sync_all().map_err(errors::new_io_error)?;
- let filename = filename::file_name(&*file_guard).context("get filename error")?;
+ let filename = file_guard.1.clone();
self.wal_files
.lock()
@@ -93,11 +93,14 @@ impl WalInner {
));
// Close the current file
- *file_guard = File::create(&filename).map_err(errors::new_io_error)?;
+ *file_guard = (
+ File::create(&filename).map_err(errors::new_io_error)?,
+ filename.clone(),
+ );
self.file_size.store(0, atomic::Ordering::Relaxed);
- println!("WAL file rotated to {}", filename.to_str().unwrap());
+ println!("WAL file rotated to {}", filename.display());
Ok(())
}
@@ -195,7 +198,7 @@ unsafe impl Send for WalInner {}
#[derive(Clone)]
pub struct Wal {
inner: Arc,
- sender: SyncSender,
+ sender: Arc>,
}
impl std::ops::Deref for Wal {
@@ -208,7 +211,7 @@ impl std::ops::Deref for Wal {
impl Wal {
pub fn new(
- file: File,
+ file: (File, PathBuf),
dir: String,
max_size: u64,
last_entry: u64,
@@ -217,7 +220,11 @@ impl Wal {
err_handler: Box,
) -> Self {
let (sender, receiver) = std::sync::mpsc::sync_channel(1024);
- let file_size = file.metadata().expect("Failed to get file metadata").len();
+ let file_size = file
+ .0
+ .metadata()
+ .expect("Failed to get file metadata")
+ .len();
Wal {
inner: Arc::new(WalInner {
dir,
@@ -230,7 +237,7 @@ impl Wal {
file_size: atomic::AtomicU64::new(file_size),
err_handler: std::sync::Mutex::new(err_handler),
}),
- sender: sender,
+ sender: Arc::new(sender),
}
}
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
new file mode 100644
index 0000000..7407248
--- /dev/null
+++ b/docker-compose.dev.yml
@@ -0,0 +1,27 @@
+
+services:
+ # PostgreSQL Database
+ postgres:
+ image: postgres:15-alpine
+ container_name: cherryserver-postgres
+ restart: unless-stopped
+ environment:
+ POSTGRES_DB: cherryserver
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres123
+ ports:
+ - "5432:5432"
+
+ # Optional: PostgreSQL Admin (pgAdmin)
+ pgadmin:
+ image: dpage/pgadmin4:latest
+ container_name: cherryserver-pgadmin
+ restart: unless-stopped
+ environment:
+ PGADMIN_DEFAULT_EMAIL: admin@cherryserver.com
+ PGADMIN_DEFAULT_PASSWORD: admin123
+ PGADMIN_LISTEN_PORT: 8099
+ ports:
+ - "8099:80"
+ depends_on:
+ - postgres
\ No newline at end of file
diff --git a/run_tests.sh b/run_tests.sh
new file mode 100755
index 0000000..90135e6
--- /dev/null
+++ b/run_tests.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+# Test runner script for Cherry project
+# This script runs tests for the core Rust libraries that don't depend on GTK
+
+set -e
+
+echo "๐งช Running tests for Cherry project..."
+echo "======================================"
+
+# Set environment variables for tests
+export JWT_SECRET="test_secret_key_for_testing"
+
+# Source Rust environment
+source $HOME/.cargo/env
+
+echo ""
+echo "๐ฆ Testing streamstore crate..."
+echo "-------------------------------"
+cd crates/streamstore
+cargo test --lib
+echo "โ
streamstore tests passed!"
+
+echo ""
+echo "๐ฆ Testing cherrycore crate..."
+echo "-------------------------------"
+cd ../cherrycore
+cargo test --lib
+echo "โ
cherrycore tests passed!"
+
+echo ""
+echo "๐ All tests passed successfully!"
+echo ""
+echo "๐ Test Summary:"
+echo " - streamstore: 42 tests"
+echo " - cherrycore: 28 tests"
+echo " - Total: 70 tests"
+echo ""
+echo "Note: The Tauri application (cherry crate) requires GTK dependencies"
+echo " and is not included in this test run."
\ No newline at end of file
diff --git a/src/entry.rs b/src/entry.rs
deleted file mode 100644
index 8e854ff..0000000
--- a/src/entry.rs
+++ /dev/null
@@ -1,155 +0,0 @@
-use std::{
- fs::File,
- io::Read,
-};
-
-use anyhow::{Error, anyhow};
-
-use crate::errors;
-use anyhow::{Context, Result};
-
-pub type AppendEntryResultFn = Box) -> () + Send + Sync>;
-pub type DataType = Vec;
-
-pub struct Entry {
- // auto increment id
- pub version: u8,
- pub id: u64,
- pub stream_id: u64,
- pub data: DataType,
- pub callback: Option,
-}
-
-pub trait Encoder {
- fn encode(&self) -> Vec;
-}
-
-impl Encoder for Entry {
- fn encode(&self) -> Vec {
- // Encode the item into bytes
- let mut data = Vec::new();
- data.extend_from_slice(&self.version.to_le_bytes());
-
- if self.version == 1 {
- data.extend_from_slice(&self.id.to_le_bytes());
- data.extend_from_slice(&self.stream_id.to_le_bytes());
- data.extend_from_slice(&(self.data.len() as u32).to_le_bytes());
- data.extend_from_slice(&self.data);
- } else {
- panic!("Unsupported version");
- }
- data
- }
-}
-
-pub trait Decoder<'a> {
- fn decode(&mut self, closure: Box Result + 'a>) -> Result<()>;
-}
-
-impl<'a> Decoder<'a> for File {
- fn decode(
- &mut self,
- mut closure: Box Result + 'a>,
- ) -> Result<()> {
- // Decode the item from bytes
- loop {
- let mut entry = Entry::default();
-
- let mut version = [0u8; 1];
- match self.read_exact(&mut version) {
- Ok(()) => {
- entry.version = u8::from_le_bytes(version);
- }
- Err(ref e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break, // End of file
- Err(e) => return Err(anyhow!(e)),
- }
- if entry.version == 1 {
- let mut id_buf = [0u8; 8];
- self.read_exact(&mut id_buf).context("")?;
- entry.id = u64::from_le_bytes(id_buf);
-
- let mut stream_id_buf = [0u8; 8];
- self.read_exact(&mut stream_id_buf)
- .context("Failed to read stream_id")?;
- entry.stream_id = u64::from_le_bytes(stream_id_buf);
-
- let mut data_size_buf = [0u8; 4];
- self.read_exact(&mut data_size_buf)
- .context("Failed to read data size")?;
-
- let data_size = u32::from_le_bytes(data_size_buf);
-
- entry.data.resize(data_size as usize, 0);
- self.read_exact(&mut entry.data)
- .map_err(errors::new_io_error)?;
- } else {
- log::error!("Unsupported version: {}", entry.version);
- return Err(anyhow!(errors::new_invalid_data()));
- }
- // Call the closure with the decoded entry
- if !closure(entry)? {
- break;
- }
- }
- Ok(())
- }
-}
-
-impl Entry {
- pub fn default() -> Self {
- Entry {
- version: 0,
- id: 0,
- stream_id: 0,
- data: Vec::new(),
- callback: None,
- }
- }
-}
-
-impl std::fmt::Debug for Entry {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.debug_struct("Entry")
- .field("version", &self.version)
- .field("id", &self.id)
- .field("stream_id", &self.stream_id)
- .field("data", &self.data)
- .finish()
- }
-}
-
-#[test]
-fn test_entry_encode() {
- let entry = Entry {
- version: 1,
- id: 1,
- stream_id: 1,
- data: "hello world".as_bytes().to_vec(),
- callback: None,
- };
-
- let encoded = entry.encode();
-
- use std::io::Write;
- // write the encoded data to a file
- let mut file = File::create("test_entry.bin").expect("Failed to create file");
- file.write_all(&encoded).expect("Failed to write to file");
-
- file.sync_all().expect("Failed to flush file");
- drop(file);
-
- // Read the file back and decode
- let mut file = File::open("test_entry.bin").expect("Failed to open file");
-
- let meta = file.metadata().expect("Failed to read metadata");
- assert!(meta.len() > 0, "File should not be empty");
-
- file.decode(Box::new(|decoded_entry| {
- assert_eq!(decoded_entry.version, 1);
- assert_eq!(decoded_entry.id, 1);
- assert_eq!(decoded_entry.stream_id, 1);
- assert_eq!(decoded_entry.data, b"hello world");
- Ok(true)
- }))
- .expect("Failed to decode entry");
-}
diff --git a/src/errors.rs b/src/errors.rs
deleted file mode 100644
index 2e53359..0000000
--- a/src/errors.rs
+++ /dev/null
@@ -1,60 +0,0 @@
-use thiserror::Error;
-
-#[derive(Debug, Error)]
-pub enum Error {
- #[error("Stream already exists")]
- AlreadyExists,
-
- #[error("path {path} is invalid")]
- InValidPath { path: std::path::PathBuf },
-
- #[error("invalid data")]
- InvalidData,
-
- #[error("internal error")]
- InternalError,
-
- #[error("is closed")]
- CloseError,
-
- #[error("store is read-only")]
- StoreIsReadOnly,
-
- #[error("channel is closed")]
- WalChannelSendError,
-
- #[error("IO error")]
- IoError(std::io::Error),
-
- #[error("Stream {stream_id} offset {offset} is invalid")]
- StreamOffsetInvalid { stream_id: u64, offset: u64 },
-
- #[error("Stream {stream_id} Not Found")]
- StreamNotFound { stream_id: u64 },
-}
-
-pub fn new_stream_offset_invalid(stream_id: u64, offset: u64) -> anyhow::Error {
- anyhow::anyhow!(Error::StreamOffsetInvalid { stream_id, offset })
-}
-
-pub fn new_stream_not_found(stream_id: u64) -> anyhow::Error {
- anyhow::anyhow!(Error::StreamNotFound { stream_id })
-}
-
-pub fn new_io_error(e: std::io::Error) -> anyhow::Error {
- anyhow::anyhow!(Error::IoError(e))
-}
-
-pub fn new_invalid_path(path: std::path::PathBuf) -> anyhow::Error {
- anyhow::anyhow!(Error::InValidPath { path })
-}
-
-pub fn new_invalid_data() -> anyhow::Error {
- anyhow::anyhow!(Error::InvalidData)
-}
-
-pub fn new_store_is_read_only() -> anyhow::Error {
- anyhow::anyhow!(Error::StoreIsReadOnly)
-}
-
-
diff --git a/src/mem_table.rs b/src/mem_table.rs
deleted file mode 100644
index 3074e6a..0000000
--- a/src/mem_table.rs
+++ /dev/null
@@ -1,127 +0,0 @@
-use crate::{entry::Entry, table::StreamTable};
-use anyhow::Result;
-use std::{
- collections::HashMap,
- io,
- sync::{Arc, Mutex, atomic::AtomicU64},
-};
-
-pub type MemTableArc = Arc;
-pub(crate) type GetStreamOffset = Box Result + Send + Sync>;
-pub struct MemTable {
- stream_tables: Mutex>,
- first_entry: AtomicU64,
- last_entry: AtomicU64,
- size: AtomicU64,
- get_stream_offset: Mutex,
-}
-
-impl MemTable {
- pub fn new(get_stream_offset: GetStreamOffset) -> Self {
- MemTable {
- stream_tables: Mutex::new(HashMap::new()),
- first_entry: AtomicU64::new(0),
- last_entry: AtomicU64::new(0),
- size: AtomicU64::new(0),
- get_stream_offset: Mutex::new(get_stream_offset),
- }
- }
-
- pub fn get_first_entry(&self) -> u64 {
- self.first_entry.load(std::sync::atomic::Ordering::SeqCst)
- }
-
- pub fn get_last_entry(&self) -> u64 {
- self.last_entry.load(std::sync::atomic::Ordering::SeqCst)
- }
-
- pub fn get_size(&self) -> u64 {
- self.size.load(std::sync::atomic::Ordering::SeqCst)
- }
-
- pub fn get_stream_ids(&self) -> Vec {
- let guard = self.stream_tables.lock().unwrap();
- guard.keys().cloned().collect()
- }
-
- pub fn get_stream_tables(&self) -> std::sync::MutexGuard> {
- self.stream_tables.lock().unwrap()
- }
-
- pub fn get_stream_range(&self, stream_id: u64) -> Option<(u64, u64)> {
- let guard = self.stream_tables.lock().unwrap();
- if let Some(stream_table) = guard.get(&stream_id) {
- return stream_table.get_stream_range();
- }
- None
- }
-
- pub fn read_stream(&self, stream_id: u64, offset: u64, buf: &mut [u8]) -> io::Result {
- let guard = self.stream_tables.lock().unwrap();
- if let Some(stream_table) = guard.get(&stream_id) {
- return stream_table.read_stream(offset, buf);
- }
- Err(io::Error::new(
- io::ErrorKind::NotFound,
- format!("Stream ID {} not found", stream_id),
- ))
- }
-
- // return the stream offset
- pub fn append(&self, entry: &Entry) -> Result {
- assert!(entry.stream_id != 0, "Stream ID cannot be zero");
- assert!(entry.data.len() > 0, "Entry data cannot be empty");
- assert!(entry.id > 0, "Entry ID must be greater than zero");
- assert!(
- entry.id > self.last_entry.load(std::sync::atomic::Ordering::SeqCst),
- "Entry ID must be greater than the last entry ID"
- );
-
- let data_len = entry.data.len() as u64;
-
- let mut guard = self.stream_tables.lock().unwrap();
-
- let res = match guard.get_mut(&entry.stream_id) {
- Some(stream_table) => stream_table,
- None => {
- let offset = match self.get_stream_offset.lock().unwrap()(entry.stream_id) {
- Ok(offset) => offset,
- Err(e) => return Err(e),
- };
- guard.insert(entry.stream_id, StreamTable::new(entry.stream_id, offset));
- guard.get_mut(&entry.stream_id).unwrap()
- }
- };
-
- // Append the data to the stream table
- let offset = res.append(&entry.data)?;
-
- // Update the stream table
- self.size
- .fetch_add(data_len, std::sync::atomic::Ordering::SeqCst);
-
- self.last_entry
- .store(entry.id, std::sync::atomic::Ordering::SeqCst);
-
- if self.first_entry.load(std::sync::atomic::Ordering::SeqCst) == 0 {
- self.first_entry
- .store(entry.id, std::sync::atomic::Ordering::SeqCst);
- }
- Ok(offset)
- }
-}
-
-/// Asserts that the type `T` is `Send` and `Sync`.
-/// This is useful for ensuring that types used in concurrent contexts are safe to share across threads.
-#[allow(unused)]
-fn assert_send_sync() {}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_stream_data() {
- assert_send_sync::();
- }
-}
diff --git a/src/table.rs b/src/table.rs
deleted file mode 100644
index f2505dd..0000000
--- a/src/table.rs
+++ /dev/null
@@ -1,249 +0,0 @@
-use std::{io, slice::Iter};
-
-use anyhow::Result;
-
-const STREAM_DATA_BUFFER_CAP: u64 = 128 << 10; // 128KB
-
-pub struct StreamData {
- stream_id: u64,
- offset: u64,
- data: Vec,
-}
-
-impl StreamData {
- pub fn new(stream_id: u64, offset: u64, buffer_cap: u64) -> Self {
- StreamData {
- stream_id,
- offset,
- data: Vec::with_capacity(buffer_cap as usize),
- }
- }
-
- // Fill the buffer with data
- // If the buffer is full, return the remaining data
- // If the buffer is not full, return None
- pub fn fill<'a>(&mut self, data: &'a [u8]) -> Result<(usize, Option<&'a [u8]>)> {
- let available = self.cap_remaining().min(data.len());
- self.data.extend_from_slice(&data[..available as usize]);
-
- let remaining_data = if available < data.len() {
- Some(&data[available as usize..])
- } else {
- None
- };
-
- Ok((available, remaining_data))
- }
-
- pub fn get_stream_range(&self) -> Option<(u64, u64)> {
- if self.data.is_empty() {
- return None;
- }
- let start = self.offset;
- let end = self.offset + self.data.len() as u64;
- Some((start, end))
- }
-
- pub fn size(&self) -> u64 {
- self.data.len() as u64
- }
-
- pub fn data(&self) -> &[u8] {
- &self.data
- }
-
- pub fn cap_remaining(&self) -> usize {
- STREAM_DATA_BUFFER_CAP as usize - self.data.len()
- }
-}
-
-pub struct StreamTable {
- stream_id: u64,
- offset: u64,
- size: u64,
- stream_datas: Vec,
-}
-
-impl StreamTable {
- pub fn new(stream_id: u64, offset: u64) -> Self {
- StreamTable {
- stream_id,
- offset: offset,
- size: 0,
- stream_datas: Vec::new(),
- }
- }
-
- pub fn stream_id(&self) -> u64 {
- self.stream_id
- }
- pub fn offset(&self) -> u64 {
- self.offset
- }
- pub fn size(&self) -> u64 {
- self.size
- }
- pub fn stream_datas(&self) -> Iter {
- self.stream_datas.iter()
- }
-
- pub fn append(&mut self, data: &[u8]) -> Result {
- if self.stream_datas.is_empty() || self.stream_datas.last().unwrap().cap_remaining() == 0 {
- if !self.stream_datas.is_empty() {
- assert_eq!(
- self.stream_datas.last().unwrap().size(),
- STREAM_DATA_BUFFER_CAP
- );
- }
-
- self.stream_datas.push(StreamData::new(
- self.stream_id,
- self.offset + self.size,
- STREAM_DATA_BUFFER_CAP,
- ));
- }
-
- let stream_data = self.stream_datas.last_mut().unwrap();
- let (size, remain_buffer) = stream_data.fill(data)?;
- self.size += size as u64;
-
- // If the buffer is full, we need to create a new buffer
- if let Some(buffer) = remain_buffer {
- return self.append(buffer);
- }
-
- Ok(self.offset + self.size)
- }
-
- pub fn get_stream_range(&self) -> Option<(u64, u64)> {
- if self.stream_datas.is_empty() {
- return None;
- }
- return Some((self.offset, self.offset + self.size));
- }
-
- pub fn print_stream_meta(&self) {
- for (i, stream_data) in self.stream_datas.iter().enumerate() {
- assert!(stream_data.stream_id == self.stream_id);
- if i != self.stream_datas.len() - 1 {
- assert_eq!(stream_data.size(), STREAM_DATA_BUFFER_CAP);
- }
- }
- }
-
- pub fn crc64(&self) -> u64 {
- let crc64 = crc::Crc::::new(&crc::CRC_64_REDIS);
- let mut digest = crc64.digest();
- for stream_data in &self.stream_datas {
- digest.update(&stream_data.data);
- }
- digest.finalize()
- }
-
- pub fn read_stream(&self, offset: u64, buf: &mut [u8]) -> io::Result {
- self.print_stream_meta();
-
- let mut offset = offset;
- let mut size = buf.len() as u64;
- let mut copied_size = 0;
-
- // find the first stream data that offset <= offset by quick search
- let res = self.stream_datas.binary_search_by(|stream_data| {
- let (_begin, end) = stream_data.get_stream_range().unwrap();
- end.cmp(&offset)
- });
- let mut index = match res {
- Ok(index) => index + 1, // we want the first stream data that starts after the offset
- Err(index) => index,
- };
-
- if index >= self.stream_datas.len() {
- log::debug!(
- "Offset {} find index {} is beyond the last stream data [{},{}), returning 0 bytes read",
- offset,
- index,
- self.stream_datas.last().unwrap().offset,
- self.stream_datas.last().unwrap().offset + self.stream_datas.last().unwrap().size()
- );
- return Ok(0);
- }
- // read the data from the stream data
- while index < self.stream_datas.len() && size > 0 {
- let stream_data = &self.stream_datas[index];
- let stream_data_offset = stream_data.offset;
- let stream_data_size = stream_data.size();
-
- assert!(
- stream_data_offset <= offset && offset <= stream_data_offset + stream_data_size
- );
- // we can read the data from this stream data
-
- let start = (offset - stream_data_offset) as usize;
- let end = (start + size as usize).min(stream_data_size as usize);
-
- // copy the data to the buffer
- let data_to_copy = &stream_data.data[start..end];
- let bytes_to_copy = data_to_copy.len();
- buf[copied_size as usize..(copied_size as u64 + bytes_to_copy as u64) as usize]
- .copy_from_slice(data_to_copy);
-
- copied_size += bytes_to_copy;
- size -= (bytes_to_copy) as u64;
- offset += bytes_to_copy as u64;
- index += 1;
- }
-
- Ok(copied_size)
- }
-}
-
-#[test]
-fn test_stream_data_fill() {
- // set rust_log to use the environment variable RUST_LOG
-
- let mut table = StreamTable::new(1, 0);
- let count = 1000;
- let mut next_offset = 0;
- let crc64 = crc::Crc::::new(&crc::CRC_64_ECMA_182);
- let mut digest = crc64.digest();
- for i in 0..count {
- let data = format!("hello world {}\n", i);
- digest.update(data.as_bytes());
- next_offset += data.len() as u64;
- let offset = table.append(data.as_bytes()).unwrap();
- assert_eq!(offset, next_offset);
- }
-
- let checksum = digest.finalize();
- log::debug!("Checksum: {}", checksum);
-
- let mut buf = vec![0u8; next_offset as usize];
- let read_size = table.read_stream(0, &mut buf).unwrap();
- assert_eq!(read_size, next_offset as usize);
- let read_checksum = crc64.checksum(&buf);
- log::debug!("Read Checksum: {}", read_checksum);
- assert_eq!(read_checksum, checksum);
-
- let mut read_bytes = Vec::new();
- loop {
- let mut buf = vec![0u8; rand::random::() as usize % 64 + 1];
- let read_size = table
- .read_stream(read_bytes.len() as u64, &mut buf)
- .unwrap();
- if read_size == 0 {
- break;
- }
- buf.truncate(read_size);
- read_bytes.extend_from_slice(buf.as_slice());
- if read_bytes.len() as u64 >= next_offset {
- break;
- }
-
- assert_eq!(read_size, buf.len() as usize);
- }
-
- let read_checksum = crc64.checksum(&read_bytes);
- log::debug!("Multi Read Checksum: {}", read_checksum);
- assert_eq!(read_bytes.len() as u64, next_offset);
- assert_eq!(read_checksum, checksum);
-}
diff --git a/start-with-env.ps1 b/start-with-env.ps1
new file mode 100644
index 0000000..5abdf8c
--- /dev/null
+++ b/start-with-env.ps1
@@ -0,0 +1,20 @@
+# PowerShell script to start CherryServer with environment variables
+# This demonstrates how to override configuration using environment variables
+
+# Set environment variables
+$env:CHERRYSERVER_SERVER__HOST = "127.0.0.1"
+$env:CHERRYSERVER_SERVER__PORT = "8080"
+$env:CHERRYSERVER_DATABASE__URL = "postgresql://user:pass@localhost:5432/cherryserver"
+$env:CHERRYSERVER_DATABASE__MAX_CONNECTIONS = "20"
+$env:CHERRYSERVER_JWT__SECRET = "env-override-secret"
+$env:CHERRYSERVER_JWT__EXPIRATION_HOURS = "72"
+$env:CHERRYSERVER_LOGGING__LEVEL = "debug"
+
+Write-Host "Starting CherryServer with environment variable overrides..."
+Write-Host "Server will run on $env:CHERRYSERVER_SERVER__HOST`:$env:CHERRYSERVER_SERVER__PORT"
+Write-Host "Database URL: $env:CHERRYSERVER_DATABASE__URL"
+Write-Host "JWT expiration: $env:CHERRYSERVER_JWT__EXPIRATION_HOURS hours"
+
+# Start the server
+cd crates/cherryserver
+cargo run
\ No newline at end of file