init
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/ui": "^4.5.1",
|
"@nuxt/ui": "^4.5.1",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
|
"@tauri-apps/plugin-log": "~2",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"tailwindcss": "^4.2.2",
|
"tailwindcss": "^4.2.2",
|
||||||
|
|||||||
Generated
+289
@@ -45,6 +45,17 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ahash"
|
||||||
|
version = "0.7.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"once_cell",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
@@ -69,6 +80,23 @@ dependencies = [
|
|||||||
"alloc-no-stdlib",
|
"alloc-no-stdlib",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_log-sys"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_logger"
|
||||||
|
version = "0.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3"
|
||||||
|
dependencies = [
|
||||||
|
"android_log-sys",
|
||||||
|
"env_filter",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android_system_properties"
|
name = "android_system_properties"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -84,6 +112,12 @@ version = "1.0.102"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arrayvec"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-broadcast"
|
name = "async-broadcast"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
@@ -335,6 +369,18 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitvec"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
|
||||||
|
dependencies = [
|
||||||
|
"funty",
|
||||||
|
"radium",
|
||||||
|
"tap",
|
||||||
|
"wyz",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
@@ -376,6 +422,30 @@ dependencies = [
|
|||||||
"piper",
|
"piper",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "borsh"
|
||||||
|
version = "1.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a"
|
||||||
|
dependencies = [
|
||||||
|
"borsh-derive",
|
||||||
|
"bytes",
|
||||||
|
"cfg_aliases",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "borsh-derive"
|
||||||
|
version = "1.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"proc-macro-crate 3.5.0",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "brotli"
|
name = "brotli"
|
||||||
version = "8.0.2"
|
version = "8.0.2"
|
||||||
@@ -403,6 +473,40 @@ version = "3.20.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byte-unit"
|
||||||
|
version = "5.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d"
|
||||||
|
dependencies = [
|
||||||
|
"rust_decimal",
|
||||||
|
"schemars 1.2.1",
|
||||||
|
"serde",
|
||||||
|
"utf8-width",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytecheck"
|
||||||
|
version = "0.6.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2"
|
||||||
|
dependencies = [
|
||||||
|
"bytecheck_derive",
|
||||||
|
"ptr_meta",
|
||||||
|
"simdutf8",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytecheck_derive"
|
||||||
|
version = "0.6.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.25.0"
|
version = "1.25.0"
|
||||||
@@ -1228,6 +1332,16 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "env_filter"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -1291,6 +1405,15 @@ dependencies = [
|
|||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fern"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fiat-crypto"
|
name = "fiat-crypto"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@@ -1395,6 +1518,12 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "funty"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futf"
|
name = "futf"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -1840,6 +1969,9 @@ name = "hashbrown"
|
|||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
@@ -2451,6 +2583,9 @@ name = "log"
|
|||||||
version = "0.4.29"
|
version = "0.4.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
dependencies = [
|
||||||
|
"value-bag",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lru-slab"
|
name = "lru-slab"
|
||||||
@@ -2648,6 +2783,15 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_threads"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2"
|
name = "objc2"
|
||||||
version = "0.6.4"
|
version = "0.6.4"
|
||||||
@@ -2827,6 +2971,7 @@ dependencies = [
|
|||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"figment",
|
"figment",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"log",
|
||||||
"opusic-sys",
|
"opusic-sys",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"rand 0.10.0",
|
"rand 0.10.0",
|
||||||
@@ -2836,6 +2981,7 @@ dependencies = [
|
|||||||
"ssh-key",
|
"ssh-key",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"tauri-plugin-log",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -3369,6 +3515,26 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ptr_meta"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1"
|
||||||
|
dependencies = [
|
||||||
|
"ptr_meta_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ptr_meta_derive"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.38.4"
|
version = "0.38.4"
|
||||||
@@ -3455,6 +3621,12 @@ version = "6.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "radium"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
@@ -3657,6 +3829,15 @@ version = "0.8.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rend"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
|
||||||
|
dependencies = [
|
||||||
|
"bytecheck",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.13.2"
|
version = "0.13.2"
|
||||||
@@ -3724,6 +3905,35 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rkyv"
|
||||||
|
version = "0.7.46"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1"
|
||||||
|
dependencies = [
|
||||||
|
"bitvec",
|
||||||
|
"bytecheck",
|
||||||
|
"bytes",
|
||||||
|
"hashbrown 0.12.3",
|
||||||
|
"ptr_meta",
|
||||||
|
"rend",
|
||||||
|
"rkyv_derive",
|
||||||
|
"seahash",
|
||||||
|
"tinyvec",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rkyv_derive"
|
||||||
|
version = "0.7.46"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rsa"
|
name = "rsa"
|
||||||
version = "0.10.0-rc.17"
|
version = "0.10.0-rc.17"
|
||||||
@@ -3740,6 +3950,22 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust_decimal"
|
||||||
|
version = "1.40.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
|
"borsh",
|
||||||
|
"bytes",
|
||||||
|
"num-traits",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"rkyv",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
@@ -3945,6 +4171,12 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "seahash"
|
||||||
|
version = "4.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sec1"
|
name = "sec1"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@@ -4271,6 +4503,12 @@ version = "0.3.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simdutf8"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "siphasher"
|
name = "siphasher"
|
||||||
version = "0.3.11"
|
version = "0.3.11"
|
||||||
@@ -4607,6 +4845,12 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tap"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "target-lexicon"
|
name = "target-lexicon"
|
||||||
version = "0.12.16"
|
version = "0.12.16"
|
||||||
@@ -4744,6 +4988,28 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-log"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7545bd67f070a4500432c826e2e0682146a1d6712aee22a2786490156b574d93"
|
||||||
|
dependencies = [
|
||||||
|
"android_logger",
|
||||||
|
"byte-unit",
|
||||||
|
"fern",
|
||||||
|
"log",
|
||||||
|
"objc2",
|
||||||
|
"objc2-foundation",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_repr",
|
||||||
|
"swift-rs",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-opener"
|
name = "tauri-plugin-opener"
|
||||||
version = "2.5.3"
|
version = "2.5.3"
|
||||||
@@ -4948,7 +5214,9 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
"itoa",
|
"itoa",
|
||||||
|
"libc",
|
||||||
"num-conv",
|
"num-conv",
|
||||||
|
"num_threads",
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"time-core",
|
"time-core",
|
||||||
@@ -5443,6 +5711,12 @@ version = "0.7.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8-width"
|
||||||
|
version = "0.1.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8_iter"
|
name = "utf8_iter"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -5461,6 +5735,12 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "value-bag"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version-compare"
|
name = "version-compare"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -6389,6 +6669,15 @@ dependencies = [
|
|||||||
"x11-dl",
|
"x11-dl",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wyz"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
|
||||||
|
dependencies = [
|
||||||
|
"tap",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "x11"
|
name = "x11"
|
||||||
version = "2.21.0"
|
version = "2.21.0"
|
||||||
|
|||||||
@@ -36,3 +36,5 @@ toml = "1.0.7+spec-1.1.0"
|
|||||||
ssh-key = { version = "0.7.0-rc.9", features = ["default", "crypto"] }
|
ssh-key = { version = "0.7.0-rc.9", features = ["default", "crypto"] }
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
rand = "0.10"
|
rand = "0.10"
|
||||||
|
tauri-plugin-log = "2"
|
||||||
|
log = "0.4"
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
tauri_build::build()
|
tauri_build::build()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,12 @@
|
|||||||
"$schema": "../gen/schemas/desktop-schema.json",
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
"identifier": "default",
|
"identifier": "default",
|
||||||
"description": "Capability for the main window",
|
"description": "Capability for the main window",
|
||||||
"windows": ["main"],
|
"windows": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"opener:default"
|
"opener:default",
|
||||||
|
"log:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
import type {IApiClient} from './client';
|
|
||||||
import type {
|
|
||||||
CategoryResponse,
|
|
||||||
ChannelResponse,
|
|
||||||
ClaimAdminRequest,
|
|
||||||
CreateCategoryRequest,
|
|
||||||
CreateChannelRequest,
|
|
||||||
CreateMessageRequest,
|
|
||||||
CreateServerRequest,
|
|
||||||
LoginRequest,
|
|
||||||
LoginResponse,
|
|
||||||
MessageResponse,
|
|
||||||
ServerResponse,
|
|
||||||
SshChallengeResponse,
|
|
||||||
UserResponse,
|
|
||||||
} from './types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implémentation Web (standard) de l'interface API.
|
|
||||||
* Elle utilise fetch() pour communiquer directement avec le serveur.
|
|
||||||
*/
|
|
||||||
export class WebApiClient implements IApiClient {
|
|
||||||
private token: string | null = null;
|
|
||||||
|
|
||||||
constructor(public readonly baseUrl: string) {
|
|
||||||
this.token = localStorage.getItem(`token_${baseUrl}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async request<T>(method: string, path: string, body?: any): Promise<T> {
|
|
||||||
// Nettoyer l'URL
|
|
||||||
const cleanBaseUrl = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
|
|
||||||
const cleanPath = path.startsWith('/') ? path.slice(1) : path;
|
|
||||||
const url = `${cleanBaseUrl}/${cleanPath}`;
|
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.token) {
|
|
||||||
headers['Authorization'] = `Bearer ${this.token}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
body: body ? JSON.stringify(body) : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
let errorData;
|
|
||||||
try {
|
|
||||||
errorData = await response.json();
|
|
||||||
} catch (e) {
|
|
||||||
errorData = {message: await response.text()};
|
|
||||||
}
|
|
||||||
throw new Error(errorData.message || `Erreur HTTP ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status === 204) {
|
|
||||||
return {} as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
private setToken(token: string) {
|
|
||||||
this.token = token;
|
|
||||||
localStorage.setItem(`token_${this.baseUrl}`, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
// AUTH
|
|
||||||
async login(req: LoginRequest): Promise<LoginResponse> {
|
|
||||||
const res = await this.request<LoginResponse>('POST', 'api/auth/login', req);
|
|
||||||
this.setToken(res.token);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyToken(): Promise<UserResponse> {
|
|
||||||
return await this.request<UserResponse>('GET', 'api/auth/me');
|
|
||||||
}
|
|
||||||
|
|
||||||
async claimAdmin(req: ClaimAdminRequest): Promise<void> {
|
|
||||||
return await this.request<void>('POST', 'api/auth/claim-admin', req);
|
|
||||||
}
|
|
||||||
|
|
||||||
async sshChallenge(): Promise<SshChallengeResponse> {
|
|
||||||
return await this.request<SshChallengeResponse>('POST', 'api/auth/ssh-challenge');
|
|
||||||
}
|
|
||||||
|
|
||||||
// SERVER
|
|
||||||
async getServerList(): Promise<ServerResponse[]> {
|
|
||||||
return await this.request<ServerResponse[]>('GET', 'api/server');
|
|
||||||
}
|
|
||||||
|
|
||||||
async createServer(req: CreateServerRequest): Promise<ServerResponse> {
|
|
||||||
return await this.request<ServerResponse>('POST', 'api/server', req);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getServerTree(serverId: string): Promise<any> {
|
|
||||||
return await this.request<any>('GET', `api/server/servers/${serverId}/tree/`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// CATEGORY
|
|
||||||
async getCategoryList(serverId?: string): Promise<CategoryResponse[]> {
|
|
||||||
const path = serverId ? `api/category?server_id=${serverId}` : 'api/category';
|
|
||||||
return await this.request<CategoryResponse[]>('GET', path);
|
|
||||||
}
|
|
||||||
|
|
||||||
async createCategory(req: CreateCategoryRequest): Promise<CategoryResponse> {
|
|
||||||
return await this.request<CategoryResponse>('POST', 'api/category', req);
|
|
||||||
}
|
|
||||||
|
|
||||||
// CHANNEL
|
|
||||||
async getChannelList(): Promise<ChannelResponse[]> {
|
|
||||||
return await this.request<ChannelResponse[]>('GET', 'api/channel');
|
|
||||||
}
|
|
||||||
|
|
||||||
async createChannel(req: CreateChannelRequest): Promise<ChannelResponse> {
|
|
||||||
return await this.request<ChannelResponse>('POST', 'api/channel', req);
|
|
||||||
}
|
|
||||||
|
|
||||||
// MESSAGE
|
|
||||||
async getMessageList(): Promise<MessageResponse[]> {
|
|
||||||
return await this.request<MessageResponse[]>('GET', 'api/message');
|
|
||||||
}
|
|
||||||
|
|
||||||
async createMessage(req: CreateMessageRequest): Promise<MessageResponse> {
|
|
||||||
return await this.request<MessageResponse>('POST', 'api/message', req);
|
|
||||||
}
|
|
||||||
|
|
||||||
// USER
|
|
||||||
async getUserList(): Promise<UserResponse[]> {
|
|
||||||
return await this.request<UserResponse[]>('GET', 'api/user');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,16 @@
|
|||||||
use crate::config::ConfigManager;
|
use crate::config::ConfigManager;
|
||||||
use crate::network::ClientManager;
|
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
/// État global de l'application.
|
/// État global de l'application.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub config: OnceLock<ConfigManager>,
|
pub config: OnceLock<ConfigManager>,
|
||||||
pub client_manager: ClientManager,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
config: OnceLock::new(),
|
config: OnceLock::new(),
|
||||||
client_manager: ClientManager::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod commands;
|
||||||
|
pub mod input;
|
||||||
|
pub mod output;
|
||||||
|
|||||||
@@ -14,13 +14,78 @@ pub async fn config_update(
|
|||||||
new_config: ConfigTree,
|
new_config: ConfigTree,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let config = state.config.get().ok_or("Config non initialisée")?;
|
let config = state.config.get().ok_or("Config non initialisée")?;
|
||||||
println!("{:?}", new_config);
|
|
||||||
config
|
config
|
||||||
.update_config(new_config)
|
.update_config(new_config)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub async fn save_token(
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
base_url: String,
|
||||||
|
token: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let config_manager = state.config.get().ok_or("Config non initialisée")?;
|
||||||
|
let mut config = config_manager.get_config();
|
||||||
|
|
||||||
|
// Find server by address or add it
|
||||||
|
let found = config.servers.iter_mut().any(|s| {
|
||||||
|
if s.adresse == base_url {
|
||||||
|
s.token = Some(token.clone());
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
config.servers.push(crate::config::config::ConfigServer {
|
||||||
|
adresse: base_url,
|
||||||
|
identity: "default".to_string(),
|
||||||
|
token: Some(token),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
config_manager
|
||||||
|
.update_config(config)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub async fn load_token(
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
base_url: String,
|
||||||
|
) -> Result<Option<String>, String> {
|
||||||
|
let config_manager = state.config.get().ok_or("Config non initialisée")?;
|
||||||
|
let config = config_manager.get_config();
|
||||||
|
|
||||||
|
let token = config
|
||||||
|
.servers
|
||||||
|
.iter()
|
||||||
|
.find(|s| s.adresse == base_url)
|
||||||
|
.and_then(|s| s.token.clone());
|
||||||
|
|
||||||
|
Ok(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub async fn clear_token(state: State<'_, AppState>, base_url: String) -> Result<(), String> {
|
||||||
|
let config_manager = state.config.get().ok_or("Config non initialisée")?;
|
||||||
|
let mut config = config_manager.get_config();
|
||||||
|
|
||||||
|
if let Some(server) = config.servers.iter_mut().find(|s| s.adresse == base_url) {
|
||||||
|
server.token = None;
|
||||||
|
config_manager
|
||||||
|
.update_config(config)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub async fn generate_ssh_key() -> Result<String, String> {
|
pub async fn generate_ssh_key() -> Result<String, String> {
|
||||||
crate::config::config::generate_ssh_key_base64().map_err(|e| e.to_string())
|
crate::config::config::generate_ssh_key_base64().map_err(|e| e.to_string())
|
||||||
|
|||||||
@@ -1,131 +0,0 @@
|
|||||||
use crate::network::http::api::ApiClient;
|
|
||||||
use crate::network::http::ws::client::WsClient;
|
|
||||||
use parking_lot::RwLock;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tauri::AppHandle;
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Client {
|
|
||||||
pub base_url: String,
|
|
||||||
pub api: ApiClient,
|
|
||||||
pub ws_sender: Arc<RwLock<Option<mpsc::UnboundedSender<String>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Client {
|
|
||||||
pub fn new(base_url: String, token: Option<String>) -> Self {
|
|
||||||
let api = ApiClient::new(base_url.clone());
|
|
||||||
if let Some(t) = token {
|
|
||||||
api.set_token(t);
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
|
||||||
base_url,
|
|
||||||
api,
|
|
||||||
ws_sender: Arc::new(RwLock::new(None)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn connect_ws(&self, app_handle: AppHandle) -> Result<(), String> {
|
|
||||||
// 1. Vérifier le token via HTTP avant
|
|
||||||
if let Some(_token) = self.api.get_token() {
|
|
||||||
println!("Vérification du token pour {}", self.base_url);
|
|
||||||
match self.api.verify_token().await {
|
|
||||||
Ok(_) => {
|
|
||||||
println!("Token valide pour {}", self.base_url);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("Token invalide pour {}: {}", self.base_url, e);
|
|
||||||
// Ici on pourrait déclencher un événement pour demander une ré-authentification
|
|
||||||
// Pour le moment on s'arrête là ou on tente quand même la connexion WS
|
|
||||||
// selon le besoin. Mais l'utilisateur dit "relance une authentification http"
|
|
||||||
return Err("Token invalide, veuillez vous reconnecter".to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err("Aucun token disponible".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut ws_sender_lock = self.ws_sender.write();
|
|
||||||
|
|
||||||
// Fermer l'ancienne connexion si elle existe
|
|
||||||
if ws_sender_lock.is_some() {
|
|
||||||
println!("Déconnexion de l'ancien WebSocket pour {}", self.base_url);
|
|
||||||
*ws_sender_lock = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (tx, rx) = mpsc::unbounded_channel();
|
|
||||||
let token = self.api.get_token();
|
|
||||||
let client = WsClient::new(self.base_url.clone(), token, app_handle);
|
|
||||||
|
|
||||||
*ws_sender_lock = Some(tx);
|
|
||||||
|
|
||||||
// Lancer la tâche de connexion en arrière-plan
|
|
||||||
tokio::spawn(async move {
|
|
||||||
client.run(rx).await;
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send_ws(&self, message: String) -> Result<(), String> {
|
|
||||||
let ws_sender_lock = self.ws_sender.read();
|
|
||||||
if let Some(tx) = ws_sender_lock.as_ref() {
|
|
||||||
tx.send(message)
|
|
||||||
.map_err(|e| format!("Erreur d'envoi WebSocket: {}", e))
|
|
||||||
} else {
|
|
||||||
Err(format!(
|
|
||||||
"Pas de connexion WebSocket active pour {}",
|
|
||||||
self.base_url
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn disconnect_ws(&self) {
|
|
||||||
let mut ws_sender_lock = self.ws_sender.write();
|
|
||||||
*ws_sender_lock = None;
|
|
||||||
println!("WebSocket déconnecté pour {}", self.base_url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct ClientManager {
|
|
||||||
clients: RwLock<HashMap<String, Arc<Client>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClientManager {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
clients: RwLock::new(HashMap::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_or_create_client(&self, base_url: String) -> Arc<Client> {
|
|
||||||
let mut clients = self.clients.write();
|
|
||||||
if let Some(client) = clients.get(&base_url) {
|
|
||||||
client.clone()
|
|
||||||
} else {
|
|
||||||
let client = Arc::new(Client::new(base_url.clone(), None));
|
|
||||||
clients.insert(base_url, client.clone());
|
|
||||||
client
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_client(&self, base_url: String, token: Option<String>) -> Arc<Client> {
|
|
||||||
let mut clients = self.clients.write();
|
|
||||||
let client = Arc::new(Client::new(base_url.clone(), token));
|
|
||||||
clients.insert(base_url, client.clone());
|
|
||||||
client
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_client(&self, base_url: &str) {
|
|
||||||
let mut clients = self.clients.write();
|
|
||||||
clients.remove(base_url);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_client(&self, base_url: &str) -> Option<Arc<Client>> {
|
|
||||||
let clients = self.clients.read();
|
|
||||||
clients.get(base_url).cloned()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
use crate::network::http::api::models::*;
|
|
||||||
use parking_lot::RwLock;
|
|
||||||
use reqwest::{header, Client, Method, RequestBuilder, Response};
|
|
||||||
use serde::de::DeserializeOwned;
|
|
||||||
use serde::Serialize;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
/// Erreurs possibles lors de l'utilisation du client API.
|
|
||||||
#[derive(Debug, thiserror::Error, Serialize)]
|
|
||||||
#[serde(tag = "type", content = "data")]
|
|
||||||
pub enum ApiError {
|
|
||||||
#[error("Erreur de requête HTTP: {0}")]
|
|
||||||
Http(String),
|
|
||||||
#[error("Erreur de désérialisation: {0}")]
|
|
||||||
Json(String),
|
|
||||||
#[error("Réponse d'erreur du serveur: {status} - {body}")]
|
|
||||||
ServerError { status: u16, body: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<reqwest::Error> for ApiError {
|
|
||||||
fn from(err: reqwest::Error) -> Self {
|
|
||||||
ApiError::Http(err.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<serde_json::Error> for ApiError {
|
|
||||||
fn from(err: serde_json::Error) -> Self {
|
|
||||||
ApiError::Json(err.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type ApiResult<T> = Result<T, ApiError>;
|
|
||||||
|
|
||||||
/// Un client API générique capable de se connecter à plusieurs serveurs.
|
|
||||||
/// Il gère l'authentification par token JWT de manière thread-safe.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct ApiClient {
|
|
||||||
base_url: String,
|
|
||||||
inner: Client,
|
|
||||||
token: Arc<RwLock<Option<String>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ApiClient {
|
|
||||||
/// Initialise un nouveau client pour un serveur spécifique.
|
|
||||||
pub fn new(base_url: String) -> Self {
|
|
||||||
let mut headers = header::HeaderMap::new();
|
|
||||||
headers.insert(
|
|
||||||
header::CONTENT_TYPE,
|
|
||||||
header::HeaderValue::from_static("application/json"),
|
|
||||||
);
|
|
||||||
|
|
||||||
let inner = Client::builder()
|
|
||||||
.default_headers(headers)
|
|
||||||
.build()
|
|
||||||
.unwrap_or_else(|_| Client::new());
|
|
||||||
|
|
||||||
Self {
|
|
||||||
base_url: base_url.trim_end_matches('/').to_string(),
|
|
||||||
inner,
|
|
||||||
token: Arc::new(RwLock::new(None)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Définit le token JWT à utiliser pour les requêtes authentifiées.
|
|
||||||
pub fn set_token(&self, token: String) {
|
|
||||||
let mut lock = self.token.write();
|
|
||||||
*lock = Some(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Récupère le token JWT actuel s'il existe.
|
|
||||||
pub fn get_token(&self) -> Option<String> {
|
|
||||||
self.token.read().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Supprime le token JWT actuel.
|
|
||||||
pub fn clear_token(&self) {
|
|
||||||
let mut lock = self.token.write();
|
|
||||||
*lock = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Construit une requête HTTP avec l'authentification si disponible.
|
|
||||||
fn build_request(&self, method: Method, path: &str) -> RequestBuilder {
|
|
||||||
let url = if path.starts_with("http") {
|
|
||||||
path.to_string()
|
|
||||||
} else {
|
|
||||||
format!("{}/{}", self.base_url, path.trim_start_matches('/'))
|
|
||||||
};
|
|
||||||
let mut rb = self.inner.request(method, url);
|
|
||||||
|
|
||||||
if let Some(token) = self.token.read().as_ref() {
|
|
||||||
rb = rb.header(header::AUTHORIZATION, format!("Bearer {}", token));
|
|
||||||
}
|
|
||||||
|
|
||||||
rb
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_response<T: DeserializeOwned>(&self, response: Response) -> ApiResult<T> {
|
|
||||||
let status = response.status();
|
|
||||||
if status.is_success() {
|
|
||||||
let data = response.json::<T>().await?;
|
|
||||||
Ok(data)
|
|
||||||
} else {
|
|
||||||
let body = response.text().await.unwrap_or_default();
|
|
||||||
Err(ApiError::ServerError {
|
|
||||||
status: status.as_u16(),
|
|
||||||
body,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get<T: DeserializeOwned>(&self, path: &str) -> ApiResult<T> {
|
|
||||||
let response = self.build_request(Method::GET, path).send().await?;
|
|
||||||
self.handle_response(response).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn post<B: Serialize, T: DeserializeOwned>(
|
|
||||||
&self,
|
|
||||||
path: &str,
|
|
||||||
body: &B,
|
|
||||||
) -> ApiResult<T> {
|
|
||||||
let response = self
|
|
||||||
.build_request(Method::POST, path)
|
|
||||||
.json(body)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
self.handle_response(response).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn post_empty<T: DeserializeOwned>(&self, path: &str) -> ApiResult<T> {
|
|
||||||
let response = self.build_request(Method::POST, path).send().await?;
|
|
||||||
self.handle_response(response).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn put<B: Serialize, T: DeserializeOwned>(
|
|
||||||
&self,
|
|
||||||
path: &str,
|
|
||||||
body: &B,
|
|
||||||
) -> ApiResult<T> {
|
|
||||||
let response = self
|
|
||||||
.build_request(Method::PUT, path)
|
|
||||||
.json(body)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
self.handle_response(response).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete<T: DeserializeOwned>(&self, path: &str) -> ApiResult<T> {
|
|
||||||
let response = self.build_request(Method::DELETE, path).send().await?;
|
|
||||||
self.handle_response(response).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- ECOSYSTEMS ---
|
|
||||||
|
|
||||||
// Auth
|
|
||||||
pub async fn verify_token(&self) -> ApiResult<UserResponse> {
|
|
||||||
self.get("api/auth/me").await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn login(&self, req: &LoginRequest) -> ApiResult<LoginResponse> {
|
|
||||||
self.post("api/auth/login", req).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn claim_admin(&self, req: &ClaimAdminRequest) -> ApiResult<()> {
|
|
||||||
self.post("api/auth/claim-admin", req).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn ssh_challenge(&self) -> ApiResult<SshChallengeResponse> {
|
|
||||||
self.post_empty("api/auth/ssh-challenge").await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server
|
|
||||||
pub async fn server_list(&self) -> ApiResult<Vec<ServerResponse>> {
|
|
||||||
self.get("api/server").await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn server_create(&self, req: &CreateServerRequest) -> ApiResult<ServerResponse> {
|
|
||||||
self.post("api/server", req).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn server_tree(&self, server_id: &str) -> ApiResult<serde_json::Value> {
|
|
||||||
self.get(&format!("api/server/servers/{}/tree/", server_id))
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Category
|
|
||||||
pub async fn category_list(&self, server_id: Option<&str>) -> ApiResult<Vec<CategoryResponse>> {
|
|
||||||
let path = if let Some(id) = server_id {
|
|
||||||
format!("api/category?server_id={}", id)
|
|
||||||
} else {
|
|
||||||
"api/category".to_string()
|
|
||||||
};
|
|
||||||
self.get(&path).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn category_create(
|
|
||||||
&self,
|
|
||||||
req: &CreateCategoryRequest,
|
|
||||||
) -> ApiResult<CategoryResponse> {
|
|
||||||
self.post("api/category", req).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn category_detail(&self, id: &str) -> ApiResult<CategoryResponse> {
|
|
||||||
self.get(&format!("api/category/{}", id)).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn category_update(
|
|
||||||
&self,
|
|
||||||
id: &str,
|
|
||||||
req: &CreateCategoryRequest,
|
|
||||||
) -> ApiResult<CategoryResponse> {
|
|
||||||
self.put(&format!("api/category/{}", id), req).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn category_delete(&self, id: &str) -> ApiResult<()> {
|
|
||||||
self.delete(&format!("api/category/{}", id)).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Channel
|
|
||||||
pub async fn channel_list(&self) -> ApiResult<Vec<ChannelResponse>> {
|
|
||||||
self.get("api/channel").await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn channel_create(&self, req: &CreateChannelRequest) -> ApiResult<ChannelResponse> {
|
|
||||||
self.post("api/channel", req).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn channel_detail(&self, id: &str) -> ApiResult<ChannelResponse> {
|
|
||||||
self.get(&format!("api/channel/{}", id)).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn channel_update(
|
|
||||||
&self,
|
|
||||||
id: &str,
|
|
||||||
req: &CreateChannelRequest,
|
|
||||||
) -> ApiResult<ChannelResponse> {
|
|
||||||
self.put(&format!("api/channel/{}", id), req).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn channel_delete(&self, id: &str) -> ApiResult<()> {
|
|
||||||
self.delete(&format!("api/channel/{}", id)).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Message
|
|
||||||
pub async fn message_list(&self) -> ApiResult<Vec<MessageResponse>> {
|
|
||||||
self.get("api/message").await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn message_create(&self, req: &CreateMessageRequest) -> ApiResult<MessageResponse> {
|
|
||||||
self.post("api/message", req).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// User
|
|
||||||
pub async fn user_list(&self) -> ApiResult<Vec<UserResponse>> {
|
|
||||||
self.get("api/user").await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
use crate::app::state::AppState;
|
|
||||||
use crate::network::http::api::client::ApiResult;
|
|
||||||
use crate::network::http::api::models::*;
|
|
||||||
use tauri::{command, State};
|
|
||||||
|
|
||||||
// --- AUTH ---
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub async fn api_login(
|
|
||||||
state: State<'_, AppState>,
|
|
||||||
base_url: String,
|
|
||||||
req: LoginRequest,
|
|
||||||
) -> ApiResult<LoginResponse> {
|
|
||||||
let client = state.client_manager.get_or_create_client(base_url);
|
|
||||||
let res = client.api.login(&req).await?;
|
|
||||||
client.api.set_token(res.token.clone());
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub async fn api_verify_token(
|
|
||||||
state: State<'_, AppState>,
|
|
||||||
base_url: String,
|
|
||||||
) -> ApiResult<UserResponse> {
|
|
||||||
let client = state.client_manager.get_or_create_client(base_url);
|
|
||||||
client.api.verify_token().await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub async fn api_claim_admin(
|
|
||||||
state: State<'_, AppState>,
|
|
||||||
base_url: String,
|
|
||||||
req: ClaimAdminRequest,
|
|
||||||
) -> ApiResult<()> {
|
|
||||||
let client = state.client_manager.get_or_create_client(base_url);
|
|
||||||
client.api.claim_admin(&req).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub async fn api_ssh_challenge(
|
|
||||||
state: State<'_, AppState>,
|
|
||||||
base_url: String,
|
|
||||||
) -> ApiResult<SshChallengeResponse> {
|
|
||||||
let client = state.client_manager.get_or_create_client(base_url);
|
|
||||||
client.api.ssh_challenge().await
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- SERVER ---
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub async fn api_server_list(
|
|
||||||
state: State<'_, AppState>,
|
|
||||||
base_url: String,
|
|
||||||
) -> ApiResult<Vec<ServerResponse>> {
|
|
||||||
let client = state.client_manager.get_or_create_client(base_url);
|
|
||||||
client.api.server_list().await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub async fn api_server_create(
|
|
||||||
state: State<'_, AppState>,
|
|
||||||
base_url: String,
|
|
||||||
req: CreateServerRequest,
|
|
||||||
) -> ApiResult<ServerResponse> {
|
|
||||||
let client = state.client_manager.get_or_create_client(base_url);
|
|
||||||
client.api.server_create(&req).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub async fn api_server_tree(
|
|
||||||
state: State<'_, AppState>,
|
|
||||||
base_url: String,
|
|
||||||
server_id: String,
|
|
||||||
) -> ApiResult<serde_json::Value> {
|
|
||||||
let client = state.client_manager.get_or_create_client(base_url);
|
|
||||||
client.api.server_tree(&server_id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- CATEGORY ---
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub async fn api_category_list(
|
|
||||||
state: State<'_, AppState>,
|
|
||||||
base_url: String,
|
|
||||||
server_id: Option<String>,
|
|
||||||
) -> ApiResult<Vec<CategoryResponse>> {
|
|
||||||
let client = state.client_manager.get_or_create_client(base_url);
|
|
||||||
client.api.category_list(server_id.as_deref()).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub async fn api_category_create(
|
|
||||||
state: State<'_, AppState>,
|
|
||||||
base_url: String,
|
|
||||||
req: CreateCategoryRequest,
|
|
||||||
) -> ApiResult<CategoryResponse> {
|
|
||||||
let client = state.client_manager.get_or_create_client(base_url);
|
|
||||||
client.api.category_create(&req).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- CHANNEL ---
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub async fn api_channel_list(
|
|
||||||
state: State<'_, AppState>,
|
|
||||||
base_url: String,
|
|
||||||
) -> ApiResult<Vec<ChannelResponse>> {
|
|
||||||
let client = state.client_manager.get_or_create_client(base_url);
|
|
||||||
client.api.channel_list().await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub async fn api_channel_create(
|
|
||||||
state: State<'_, AppState>,
|
|
||||||
base_url: String,
|
|
||||||
req: CreateChannelRequest,
|
|
||||||
) -> ApiResult<ChannelResponse> {
|
|
||||||
let client = state.client_manager.get_or_create_client(base_url);
|
|
||||||
client.api.channel_create(&req).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- MESSAGE ---
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub async fn api_message_list(
|
|
||||||
state: State<'_, AppState>,
|
|
||||||
base_url: String,
|
|
||||||
) -> ApiResult<Vec<MessageResponse>> {
|
|
||||||
let client = state.client_manager.get_or_create_client(base_url);
|
|
||||||
client.api.message_list().await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub async fn api_message_create(
|
|
||||||
state: State<'_, AppState>,
|
|
||||||
base_url: String,
|
|
||||||
req: CreateMessageRequest,
|
|
||||||
) -> ApiResult<MessageResponse> {
|
|
||||||
let client = state.client_manager.get_or_create_client(base_url);
|
|
||||||
client.api.message_create(&req).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- USER ---
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub async fn api_user_list(
|
|
||||||
state: State<'_, AppState>,
|
|
||||||
base_url: String,
|
|
||||||
) -> ApiResult<Vec<UserResponse>> {
|
|
||||||
let client = state.client_manager.get_or_create_client(base_url);
|
|
||||||
client.api.user_list().await
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
pub mod client;
|
|
||||||
pub mod commands;
|
|
||||||
pub mod models;
|
|
||||||
|
|
||||||
pub use client::ApiClient;
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
// --- AUTH ---
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct LoginRequest {
|
|
||||||
pub username: String,
|
|
||||||
pub password: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct LoginResponse {
|
|
||||||
pub token: String,
|
|
||||||
pub username: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct ClaimAdminRequest {
|
|
||||||
pub token: String,
|
|
||||||
pub username: String,
|
|
||||||
pub password: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct SshChallengeResponse {
|
|
||||||
pub challenge: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- SERVER ---
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct CreateServerRequest {
|
|
||||||
pub name: String,
|
|
||||||
pub password: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
|
||||||
pub struct ServerResponse {
|
|
||||||
pub id: String,
|
|
||||||
pub name: String,
|
|
||||||
pub created_at: String,
|
|
||||||
pub updated_at: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- CATEGORY ---
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct CreateCategoryRequest {
|
|
||||||
pub server_id: String,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct CategoryResponse {
|
|
||||||
pub id: String,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- CHANNEL ---
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum ChannelType {
|
|
||||||
Text,
|
|
||||||
Voice,
|
|
||||||
DM,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct CreateChannelRequest {
|
|
||||||
pub channel_type: ChannelType,
|
|
||||||
pub category_id: Option<String>,
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub position: Option<i32>,
|
|
||||||
pub server_id: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct ChannelResponse {
|
|
||||||
pub id: String,
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub position: i32,
|
|
||||||
pub channel_type: ChannelType,
|
|
||||||
pub category_id: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- MESSAGE ---
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct CreateMessageRequest {
|
|
||||||
pub channel_id: String,
|
|
||||||
pub content: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct MessageResponse {
|
|
||||||
pub id: String,
|
|
||||||
pub channel_id: String,
|
|
||||||
pub author_id: String,
|
|
||||||
pub content: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- USER ---
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct CreateUserRequest {
|
|
||||||
pub username: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct UserResponse {
|
|
||||||
pub id: String,
|
|
||||||
pub username: String,
|
|
||||||
pub pub_key: String,
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
pub mod api;
|
|
||||||
pub mod ws;
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
use futures_util::{SinkExt, StreamExt};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::time::Duration;
|
|
||||||
use tauri::{AppHandle, Emitter};
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
use tokio::time::sleep;
|
|
||||||
use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct WsEvent {
|
|
||||||
pub server_url: String,
|
|
||||||
pub payload: serde_json::Value,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct WsClient {
|
|
||||||
server_url: String,
|
|
||||||
token: Option<String>,
|
|
||||||
app_handle: AppHandle,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WsClient {
|
|
||||||
pub fn new(server_url: String, token: Option<String>, app_handle: AppHandle) -> Self {
|
|
||||||
Self {
|
|
||||||
server_url,
|
|
||||||
token,
|
|
||||||
app_handle,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run(self, mut rx: mpsc::UnboundedReceiver<String>) {
|
|
||||||
let ws_url = self.prepare_url();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
println!("Tentative de connexion WebSocket à {}", ws_url);
|
|
||||||
|
|
||||||
match connect_async(&ws_url).await {
|
|
||||||
Ok((ws_stream, _)) => {
|
|
||||||
println!("Connecté au WebSocket: {}", ws_url);
|
|
||||||
let (mut write, mut read) = ws_stream.split();
|
|
||||||
|
|
||||||
// Envoyer le token si présent pour l'auth initiale si nécessaire par le protocole
|
|
||||||
// Ici on assume que le token est passé en query param ou via un message initial
|
|
||||||
// Pour le moment, on se contente de la connexion.
|
|
||||||
|
|
||||||
loop {
|
|
||||||
tokio::select! {
|
|
||||||
msg = read.next() => {
|
|
||||||
match msg {
|
|
||||||
Some(Ok(Message::Text(text))) => {
|
|
||||||
let payload = serde_json::from_str::<serde_json::Value>(&text)
|
|
||||||
.unwrap_or_else(|_| serde_json::Value::String(text.to_string()));
|
|
||||||
|
|
||||||
let _ = self.app_handle.emit("ws-message", WsEvent {
|
|
||||||
server_url: self.server_url.clone(),
|
|
||||||
payload,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Some(Ok(Message::Close(_))) | None => {
|
|
||||||
println!("WebSocket fermé par le serveur");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Some(Err(e)) => {
|
|
||||||
eprintln!("Erreur WebSocket: {}", e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(to_send) = rx.recv() => {
|
|
||||||
if let Err(e) = write.send(Message::Text(to_send.into())).await {
|
|
||||||
eprintln!("Erreur lors de l'envoi WebSocket: {}", e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Erreur de connexion WebSocket: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reconnexion après un délai
|
|
||||||
sleep(Duration::from_secs(5)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_url(&self) -> String {
|
|
||||||
let mut url = self
|
|
||||||
.server_url
|
|
||||||
.replace("http://", "ws://")
|
|
||||||
.replace("https://", "wss://");
|
|
||||||
if !url.ends_with("/ws") {
|
|
||||||
url = format!("{}/ws", url.trim_end_matches('/'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(token) = &self.token {
|
|
||||||
if url.contains('?') {
|
|
||||||
url = format!("{}&token={}", url, token);
|
|
||||||
} else {
|
|
||||||
url = format!("{}?token={}", url, token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
use crate::app::state::AppState;
|
|
||||||
use tauri::{command, AppHandle, State};
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub async fn ws_connect(
|
|
||||||
state: State<'_, AppState>,
|
|
||||||
app_handle: AppHandle,
|
|
||||||
server_url: String,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let client = state.client_manager.get_or_create_client(server_url);
|
|
||||||
client.connect_ws(app_handle).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub async fn ws_send(
|
|
||||||
state: State<'_, AppState>,
|
|
||||||
server_url: String,
|
|
||||||
message: String,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
if let Some(client) = state.client_manager.get_client(&server_url) {
|
|
||||||
client.send_ws(message)
|
|
||||||
} else {
|
|
||||||
Err(format!("Client non trouvé pour {}", server_url))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub async fn ws_disconnect(state: State<'_, AppState>, server_url: String) -> Result<(), String> {
|
|
||||||
if let Some(client) = state.client_manager.get_client(&server_url) {
|
|
||||||
client.disconnect_ws();
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
pub mod client;
|
|
||||||
pub mod commands;
|
|
||||||
@@ -1,5 +1 @@
|
|||||||
pub mod client;
|
|
||||||
pub mod http;
|
|
||||||
pub mod udp;
|
pub mod udp;
|
||||||
|
|
||||||
pub use client::ClientManager;
|
|
||||||
|
|||||||
+25
-19
@@ -2,10 +2,10 @@ use crate::app::ox_speak_app::OxSpeakApp;
|
|||||||
use crate::app::state::AppState;
|
use crate::app::state::AppState;
|
||||||
use crate::config::commands::*;
|
use crate::config::commands::*;
|
||||||
use crate::config::ConfigManager;
|
use crate::config::ConfigManager;
|
||||||
use crate::network::http::api::commands::*;
|
use log::info;
|
||||||
use crate::network::http::ws::commands::*;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tauri::{Manager, WindowEvent};
|
use tauri::{Manager, WindowEvent};
|
||||||
|
use tauri_plugin_log::{Target, TargetKind};
|
||||||
|
|
||||||
// Séparation du generate_context, sinon l'RustRover (l'ide) rame énormément dans la fonction run
|
// Séparation du generate_context, sinon l'RustRover (l'ide) rame énormément dans la fonction run
|
||||||
fn get_tauri_context() -> tauri::Context<tauri::Wry> {
|
fn get_tauri_context() -> tauri::Context<tauri::Wry> {
|
||||||
@@ -20,31 +20,37 @@ pub async fn run() {
|
|||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
|
.plugin(
|
||||||
|
tauri_plugin_log::Builder::new()
|
||||||
|
.targets([
|
||||||
|
Target::new(TargetKind::Stdout),
|
||||||
|
Target::new(TargetKind::LogDir { file_name: None }),
|
||||||
|
Target::new(TargetKind::Webview),
|
||||||
|
])
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
// .plugin(
|
||||||
|
// tauri_plugin_log::Builder::new()
|
||||||
|
// .targets([
|
||||||
|
// Target::new(TargetKind::Stdout),
|
||||||
|
// Target::new(TargetKind::LogDir { file_name: None }),
|
||||||
|
// Target::new(TargetKind::Webview),
|
||||||
|
// ])
|
||||||
|
// .level(log::LevelFilter::Trace)
|
||||||
|
// .build(),
|
||||||
|
// )
|
||||||
.manage(AppState::new())
|
.manage(AppState::new())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
greet,
|
greet,
|
||||||
api_login,
|
|
||||||
api_verify_token,
|
|
||||||
api_claim_admin,
|
|
||||||
api_ssh_challenge,
|
|
||||||
api_server_list,
|
|
||||||
api_server_create,
|
|
||||||
api_server_tree,
|
|
||||||
api_category_list,
|
|
||||||
api_category_create,
|
|
||||||
api_channel_list,
|
|
||||||
api_channel_create,
|
|
||||||
api_message_list,
|
|
||||||
api_message_create,
|
|
||||||
api_user_list,
|
|
||||||
config_get,
|
config_get,
|
||||||
config_update,
|
config_update,
|
||||||
generate_ssh_key,
|
generate_ssh_key,
|
||||||
ws_connect,
|
save_token,
|
||||||
ws_send,
|
load_token,
|
||||||
ws_disconnect,
|
clear_token,
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
|
info!("--- Tauri App Initialized (Rust side) ---");
|
||||||
let handle = app.handle().clone();
|
let handle = app.handle().clone();
|
||||||
|
|
||||||
// Démarrer le backend
|
// Démarrer le backend
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
|
"label": "main",
|
||||||
"title": "ox-speak",
|
"title": "ox-speak",
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
"height": 768
|
"height": 768
|
||||||
|
|||||||
@@ -2,11 +2,6 @@
|
|||||||
import {onMounted, onUnmounted, ref} from 'vue';
|
import {onMounted, onUnmounted, ref} from 'vue';
|
||||||
import {NavigationMenuItem} from "@nuxt/ui";
|
import {NavigationMenuItem} from "@nuxt/ui";
|
||||||
import {createApiClient} from "./api";
|
import {createApiClient} from "./api";
|
||||||
import {useConfigStore, useServerStore} from "./stores";
|
|
||||||
|
|
||||||
// stores
|
|
||||||
const configStore = useConfigStore();
|
|
||||||
const serverStore = useServerStore();
|
|
||||||
|
|
||||||
// vars
|
// vars
|
||||||
let unlisten: (() => void) | null = null;
|
let unlisten: (() => void) | null = null;
|
||||||
@@ -48,9 +43,6 @@ async function fetchServers() {
|
|||||||
// ])
|
// ])
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// config
|
|
||||||
await configStore.init();
|
|
||||||
|
|
||||||
// fetch servers
|
// fetch servers
|
||||||
await fetchServers()
|
await fetchServers()
|
||||||
})
|
})
|
||||||
|
|||||||
+5
-10
@@ -1,19 +1,14 @@
|
|||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './client';
|
export * from './client';
|
||||||
export * from './tauri-client';
|
export * from './api-client';
|
||||||
export * from './web-client';
|
|
||||||
|
|
||||||
import {TauriApiClient} from './tauri-client';
|
import {ApiClient} from './api-client';
|
||||||
import {WebApiClient} from './web-client';
|
|
||||||
import type {IApiClient} from './client';
|
import type {IApiClient} from './client';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Usine pour créer des clients API.
|
* Factory for creating API clients.
|
||||||
* Elle retourne une implémentation Web ou Tauri selon l'environnement.
|
* Always returns a unified ApiClient that works in both Web and Tauri.
|
||||||
*/
|
*/
|
||||||
export function createApiClient(baseUrl: string): IApiClient {
|
export function createApiClient(baseUrl: string): IApiClient {
|
||||||
if ((window as any).__TAURI_INTERNALS__) {
|
return new ApiClient(baseUrl);
|
||||||
return new TauriApiClient(baseUrl);
|
|
||||||
}
|
|
||||||
return new WebApiClient(baseUrl);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
import {invoke} from '@tauri-apps/api/core';
|
|
||||||
import type {IApiClient} from './client';
|
|
||||||
import type {
|
|
||||||
CategoryResponse,
|
|
||||||
ChannelResponse,
|
|
||||||
ClaimAdminRequest,
|
|
||||||
CreateCategoryRequest,
|
|
||||||
CreateChannelRequest,
|
|
||||||
CreateMessageRequest,
|
|
||||||
CreateServerRequest,
|
|
||||||
LoginRequest,
|
|
||||||
LoginResponse,
|
|
||||||
MessageResponse,
|
|
||||||
ServerResponse,
|
|
||||||
SshChallengeResponse,
|
|
||||||
UserResponse,
|
|
||||||
} from './types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implémentation Tauri de l'interface API.
|
|
||||||
* Cette version utilise 'invoke' pour communiquer avec le backend Rust.
|
|
||||||
*/
|
|
||||||
export class TauriApiClient implements IApiClient {
|
|
||||||
constructor(public readonly baseUrl: string) {
|
|
||||||
}
|
|
||||||
|
|
||||||
// AUTH
|
|
||||||
async login(req: LoginRequest): Promise<LoginResponse> {
|
|
||||||
return await invoke<LoginResponse>('api_login', {baseUrl: this.baseUrl, req});
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyToken(): Promise<UserResponse> {
|
|
||||||
return await invoke<UserResponse>('api_verify_token', {baseUrl: this.baseUrl});
|
|
||||||
}
|
|
||||||
|
|
||||||
async claimAdmin(req: ClaimAdminRequest): Promise<void> {
|
|
||||||
return await invoke<void>('api_claim_admin', {baseUrl: this.baseUrl, req});
|
|
||||||
}
|
|
||||||
|
|
||||||
async sshChallenge(): Promise<SshChallengeResponse> {
|
|
||||||
return await invoke<SshChallengeResponse>('api_ssh_challenge', {baseUrl: this.baseUrl});
|
|
||||||
}
|
|
||||||
|
|
||||||
// SERVER
|
|
||||||
async getServerList(): Promise<ServerResponse[]> {
|
|
||||||
return await invoke<ServerResponse[]>('api_server_list', {baseUrl: this.baseUrl});
|
|
||||||
}
|
|
||||||
|
|
||||||
async createServer(req: CreateServerRequest): Promise<ServerResponse> {
|
|
||||||
return await invoke<ServerResponse>('api_server_create', {baseUrl: this.baseUrl, req});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getServerTree(serverId: string): Promise<any> {
|
|
||||||
return await invoke<any>('api_server_tree', {baseUrl: this.baseUrl, serverId});
|
|
||||||
}
|
|
||||||
|
|
||||||
// CATEGORY
|
|
||||||
async getCategoryList(serverId?: string): Promise<CategoryResponse[]> {
|
|
||||||
return await invoke<CategoryResponse[]>('api_category_list', {
|
|
||||||
baseUrl: this.baseUrl,
|
|
||||||
serverId: serverId ?? null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async createCategory(req: CreateCategoryRequest): Promise<CategoryResponse> {
|
|
||||||
return await invoke<CategoryResponse>('api_category_create', {baseUrl: this.baseUrl, req});
|
|
||||||
}
|
|
||||||
|
|
||||||
// CHANNEL
|
|
||||||
async getChannelList(): Promise<ChannelResponse[]> {
|
|
||||||
return await invoke<ChannelResponse[]>('api_channel_list', {baseUrl: this.baseUrl});
|
|
||||||
}
|
|
||||||
|
|
||||||
async createChannel(req: CreateChannelRequest): Promise<ChannelResponse> {
|
|
||||||
return await invoke<ChannelResponse>('api_channel_create', {baseUrl: this.baseUrl, req});
|
|
||||||
}
|
|
||||||
|
|
||||||
// MESSAGE
|
|
||||||
async getMessageList(): Promise<MessageResponse[]> {
|
|
||||||
return await invoke<MessageResponse[]>('api_message_list', {baseUrl: this.baseUrl});
|
|
||||||
}
|
|
||||||
|
|
||||||
async createMessage(req: CreateMessageRequest): Promise<MessageResponse> {
|
|
||||||
return await invoke<MessageResponse>('api_message_create', {baseUrl: this.baseUrl, req});
|
|
||||||
}
|
|
||||||
|
|
||||||
// USER
|
|
||||||
async getUserList(): Promise<UserResponse[]> {
|
|
||||||
return await invoke<UserResponse[]>('api_user_list', {baseUrl: this.baseUrl});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
import type {IApiClient} from './client';
|
|
||||||
import type {
|
|
||||||
CategoryResponse,
|
|
||||||
ChannelResponse,
|
|
||||||
ClaimAdminRequest,
|
|
||||||
CreateCategoryRequest,
|
|
||||||
CreateChannelRequest,
|
|
||||||
CreateMessageRequest,
|
|
||||||
CreateServerRequest,
|
|
||||||
LoginRequest,
|
|
||||||
LoginResponse,
|
|
||||||
MessageResponse,
|
|
||||||
ServerResponse,
|
|
||||||
SshChallengeResponse,
|
|
||||||
UserResponse,
|
|
||||||
} from './types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implémentation Web (standard) de l'interface API.
|
|
||||||
* Elle utilise fetch() pour communiquer directement avec le serveur.
|
|
||||||
*/
|
|
||||||
export class WebApiClient implements IApiClient {
|
|
||||||
private token: string | null = null;
|
|
||||||
|
|
||||||
constructor(public readonly baseUrl: string) {
|
|
||||||
this.token = localStorage.getItem(`token_${baseUrl}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async request<T>(method: string, path: string, body?: any): Promise<T> {
|
|
||||||
// Nettoyer l'URL
|
|
||||||
const cleanBaseUrl = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
|
|
||||||
const cleanPath = path.startsWith('/') ? path.slice(1) : path;
|
|
||||||
const url = `${cleanBaseUrl}/${cleanPath}`;
|
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.token) {
|
|
||||||
headers['Authorization'] = `Bearer ${this.token}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
body: body ? JSON.stringify(body) : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
let errorData;
|
|
||||||
try {
|
|
||||||
errorData = await response.json();
|
|
||||||
} catch (e) {
|
|
||||||
errorData = {message: await response.text()};
|
|
||||||
}
|
|
||||||
throw new Error(errorData.message || `Erreur HTTP ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status === 204) {
|
|
||||||
return {} as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
private setToken(token: string) {
|
|
||||||
this.token = token;
|
|
||||||
localStorage.setItem(`token_${this.baseUrl}`, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
// AUTH
|
|
||||||
async login(req: LoginRequest): Promise<LoginResponse> {
|
|
||||||
const res = await this.request<LoginResponse>('POST', 'api/auth/login', req);
|
|
||||||
this.setToken(res.token);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyToken(): Promise<UserResponse> {
|
|
||||||
return await this.request<UserResponse>('GET', 'api/auth/me');
|
|
||||||
}
|
|
||||||
|
|
||||||
async claimAdmin(req: ClaimAdminRequest): Promise<void> {
|
|
||||||
return await this.request<void>('POST', 'api/auth/claim-admin', req);
|
|
||||||
}
|
|
||||||
|
|
||||||
async sshChallenge(): Promise<SshChallengeResponse> {
|
|
||||||
return await this.request<SshChallengeResponse>('POST', 'api/auth/ssh-challenge');
|
|
||||||
}
|
|
||||||
|
|
||||||
// SERVER
|
|
||||||
async getServerList(): Promise<ServerResponse[]> {
|
|
||||||
return await this.request<ServerResponse[]>('GET', 'api/server');
|
|
||||||
}
|
|
||||||
|
|
||||||
async createServer(req: CreateServerRequest): Promise<ServerResponse> {
|
|
||||||
return await this.request<ServerResponse>('POST', 'api/server', req);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getServerTree(serverId: string): Promise<any> {
|
|
||||||
return await this.request<any>('GET', `api/server/servers/${serverId}/tree/`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// CATEGORY
|
|
||||||
async getCategoryList(serverId?: string): Promise<CategoryResponse[]> {
|
|
||||||
const path = serverId ? `api/category?server_id=${serverId}` : 'api/category';
|
|
||||||
return await this.request<CategoryResponse[]>('GET', path);
|
|
||||||
}
|
|
||||||
|
|
||||||
async createCategory(req: CreateCategoryRequest): Promise<CategoryResponse> {
|
|
||||||
return await this.request<CategoryResponse>('POST', 'api/category', req);
|
|
||||||
}
|
|
||||||
|
|
||||||
// CHANNEL
|
|
||||||
async getChannelList(): Promise<ChannelResponse[]> {
|
|
||||||
return await this.request<ChannelResponse[]>('GET', 'api/channel');
|
|
||||||
}
|
|
||||||
|
|
||||||
async createChannel(req: CreateChannelRequest): Promise<ChannelResponse> {
|
|
||||||
return await this.request<ChannelResponse>('POST', 'api/channel', req);
|
|
||||||
}
|
|
||||||
|
|
||||||
// MESSAGE
|
|
||||||
async getMessageList(): Promise<MessageResponse[]> {
|
|
||||||
return await this.request<MessageResponse[]>('GET', 'api/message');
|
|
||||||
}
|
|
||||||
|
|
||||||
async createMessage(req: CreateMessageRequest): Promise<MessageResponse> {
|
|
||||||
return await this.request<MessageResponse>('POST', 'api/message', req);
|
|
||||||
}
|
|
||||||
|
|
||||||
// USER
|
|
||||||
async getUserList(): Promise<UserResponse[]> {
|
|
||||||
return await this.request<UserResponse[]>('GET', 'api/user');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+66
-22
@@ -1,34 +1,78 @@
|
|||||||
import './assets/main.css'
|
import './assets/main.css'
|
||||||
|
|
||||||
import {createApp} from 'vue'
|
import {createApp} from 'vue'
|
||||||
import {createRouter, createWebHistory} from 'vue-router'
|
import router from './router'
|
||||||
import ui from '@nuxt/ui/vue-plugin'
|
import ui from '@nuxt/ui/vue-plugin'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import {createPinia} from "pinia";
|
import {createPinia} from "pinia";
|
||||||
|
import {debug, error, info, trace, warn} from '@tauri-apps/plugin-log'
|
||||||
|
import {useGlobalStore} from "@/stores";
|
||||||
|
|
||||||
|
function getCallerLocation(): string {
|
||||||
|
const stack = new Error().stack ?? '';
|
||||||
|
const lines = stack.split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.includes('node_modules') || line.includes('main.ts')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chromium/Firefox : " at fetchServers (http://localhost:1420/src/App.vue:39:18)"
|
||||||
|
// ou sans nom : " at http://localhost:1420/src/App.vue:39:18"
|
||||||
|
const chromiumMatch = line.match(/^\s+at\s+(?:\S+\s+)?\(?https?:\/\/[^/]+([^?]+?)(?:\?[^:]*)?:(\d+):\d+\)?$/);
|
||||||
|
if (chromiumMatch) {
|
||||||
|
return `${chromiumMatch[1]}:${chromiumMatch[2]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebKit : "fetchServers@http://localhost:1420/src/App.vue:39:18"
|
||||||
|
const webkitMatch = line.match(/@https?:\/\/[^/]+([^?]+?)(?:\?[^:]*)?:(\d+):\d+$/);
|
||||||
|
if (webkitMatch) {
|
||||||
|
return `${webkitMatch[1]}:${webkitMatch[2]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
function forwardConsole(
|
||||||
|
fnName: 'log' | 'debug' | 'info' | 'warn' | 'error',
|
||||||
|
logger: (message: string) => Promise<void>
|
||||||
|
) {
|
||||||
|
const original = console[fnName].bind(console);
|
||||||
|
console[fnName] = (...args: unknown[]) => {
|
||||||
|
original(...args);
|
||||||
|
|
||||||
|
const location = getCallerLocation();
|
||||||
|
const message = `[${location}] ` + args
|
||||||
|
.map(a => {
|
||||||
|
if (a instanceof Error) return `${a.message}`;
|
||||||
|
if (typeof a === 'object' && a !== null) return JSON.stringify(a);
|
||||||
|
return String(a);
|
||||||
|
})
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
logger(message);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
forwardConsole('log', trace);
|
||||||
|
forwardConsole('debug', debug);
|
||||||
|
forwardConsole('info', info);
|
||||||
|
forwardConsole('warn', warn);
|
||||||
|
forwardConsole('error', error);
|
||||||
|
|
||||||
|
console.log('--- JS Logs Initialized (Forwarding enabled) ---');
|
||||||
|
info('--- Direct Plugin Info: JS side ready ---');
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
const router = createRouter({
|
|
||||||
routes: [
|
|
||||||
{path: '/', component: () => import('./pages/index.vue')},
|
|
||||||
{path: '/config', component: () => import('./pages/config.vue')},
|
|
||||||
{
|
|
||||||
path: '/server/:server_id',
|
|
||||||
component: () => import('./pages/server_detail.vue'),
|
|
||||||
props: true,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'channel/:channel_id',
|
|
||||||
component: () => import('./pages/channel_detail.vue'),
|
|
||||||
props: true
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
history: createWebHistory()
|
|
||||||
})
|
|
||||||
|
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(ui)
|
app.use(ui)
|
||||||
app.use(createPinia())
|
|
||||||
|
const pinia = createPinia()
|
||||||
|
app.use(pinia)
|
||||||
|
|
||||||
|
const globalStore = useGlobalStore()
|
||||||
|
await globalStore.init()
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|||||||
+26
-203
@@ -1,97 +1,38 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {onMounted, ref} from 'vue'
|
import {computed, onMounted} from 'vue'
|
||||||
import {createConfigClient} from '../config'
|
import {useRoute, useRouter} from 'vue-router'
|
||||||
|
import {useConfigStore} from '../stores/config'
|
||||||
|
|
||||||
// --- Types ---
|
const route = useRoute()
|
||||||
// Ils sont maintenant importés ou définis via l'interface du client
|
const router = useRouter()
|
||||||
|
const configStore = useConfigStore()
|
||||||
const configClient = createConfigClient()
|
|
||||||
const config = ref<any>({
|
|
||||||
servers: [],
|
|
||||||
identities: []
|
|
||||||
})
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const items = [{
|
const items = [
|
||||||
label: 'Serveurs',
|
{label: 'Serveurs', icon: 'i-lucide-server', value: 'servers'},
|
||||||
icon: 'i-lucide-server',
|
{label: 'Identités', icon: 'i-lucide-user', value: 'identities'},
|
||||||
slot: 'servers'
|
]
|
||||||
}, {
|
|
||||||
label: 'Identités',
|
|
||||||
icon: 'i-lucide-user',
|
|
||||||
slot: 'identities'
|
|
||||||
}]
|
|
||||||
|
|
||||||
// --- Methods ---
|
const activeTab = computed(() =>
|
||||||
|
route.path.endsWith('identities') ? 'identities' : 'servers'
|
||||||
|
)
|
||||||
|
|
||||||
async function fetchConfig() {
|
function onTabChange(value: string) {
|
||||||
loading.value = true
|
router.push(`/config/${value}`)
|
||||||
try {
|
|
||||||
const res = await configClient.get()
|
|
||||||
config.value = res
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erreur lors de la récupération de la config:', error)
|
|
||||||
toast.add({title: 'Erreur', description: 'Impossible de charger la configuration.', color: 'red'})
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveConfig() {
|
async function saveConfig() {
|
||||||
loading.value = true
|
|
||||||
try {
|
try {
|
||||||
await configClient.update(config.value)
|
await configStore.saveConfig()
|
||||||
// Re-fetch config to get any auto-generated keys
|
|
||||||
await fetchConfig()
|
|
||||||
toast.add({title: 'Succès', description: 'Configuration sauvegardée.', color: 'green'})
|
toast.add({title: 'Succès', description: 'Configuration sauvegardée.', color: 'green'})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur lors de la sauvegarde de la config:', error)
|
console.error('Erreur lors de la sauvegarde de la config:', error)
|
||||||
toast.add({title: 'Erreur', description: 'Impossible de sauvegarder la configuration.', color: 'red'})
|
toast.add({title: 'Erreur', description: 'Impossible de sauvegarder la configuration.', color: 'red'})
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// async function generateKey(index: number) {
|
|
||||||
// try {
|
|
||||||
// // Note: generate_ssh_key n'est pas encore dans IConfigClient, on peut l'ajouter si besoin
|
|
||||||
// // ou garder un invoke direct si c'est spécifique à Tauri, mais ici on veut de l'abstrait.
|
|
||||||
// // Pour le moment on utilise invoke car c'est un utilitaire.
|
|
||||||
// const {invoke} from '@tauri-apps/api/core'
|
|
||||||
// const key = await invoke<string>('generate_ssh_key')
|
|
||||||
// config.value.identities[index].private_key = key
|
|
||||||
// toast.add({title: 'Succès', description: 'Clé SSH générée.', color: 'green'})
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('Erreur lors de la génération de la clé:', error)
|
|
||||||
// toast.add({title: 'Erreur', description: 'Impossible de générer la clé.', color: 'red'})
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
function addServer() {
|
|
||||||
config.value.servers.push({adresse: '', identity: ''})
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeServer(index: number) {
|
|
||||||
config.value.servers.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function addIdentity() {
|
|
||||||
config.value.identities.push({
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
username: '',
|
|
||||||
private_key: '',
|
|
||||||
mode: 'private_key_path'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeIdentity(index: number) {
|
|
||||||
config.value.identities.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchConfig()
|
configStore.loadConfig()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -102,139 +43,21 @@ onMounted(() => {
|
|||||||
<UButton
|
<UButton
|
||||||
icon="i-lucide-save"
|
icon="i-lucide-save"
|
||||||
color="primary"
|
color="primary"
|
||||||
:loading="loading"
|
:loading="configStore.saving"
|
||||||
@click="saveConfig"
|
@click="saveConfig"
|
||||||
>
|
>
|
||||||
Sauvegarder
|
Sauvegarder
|
||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UTabs :items="items" class="w-full">
|
<UTabs
|
||||||
<template #servers>
|
:items="items"
|
||||||
<div class="space-y-4 py-4">
|
:content="false"
|
||||||
<div v-for="(server, index) in config.servers" :key="index"
|
:model-value="activeTab"
|
||||||
class="p-4 border border-gray-200 dark:border-gray-800 rounded-lg relative bg-white dark:bg-gray-900 shadow-sm">
|
class="w-full"
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
@update:model-value="onTabChange"
|
||||||
<UFormField label="Adresse du serveur (IP:Port)">
|
/>
|
||||||
<UInput v-model="server.adresse" placeholder="ex: 127.0.0.1:50051" class="w-full"/>
|
|
||||||
</UFormField>
|
|
||||||
|
|
||||||
<UFormField label="Identité à utiliser">
|
<RouterView/>
|
||||||
<USelect
|
|
||||||
v-model="server.identity"
|
|
||||||
:items="config.identities.map(i => ({ label: i.username || i.id, value: i.id }))"
|
|
||||||
placeholder="Sélectionner une identité"
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
</UFormField>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UButton
|
|
||||||
icon="i-lucide-trash"
|
|
||||||
color="red"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
class="absolute top-2 right-2"
|
|
||||||
@click="removeServer(index)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UButton
|
|
||||||
icon="i-lucide-plus"
|
|
||||||
variant="dashed"
|
|
||||||
block
|
|
||||||
@click="addServer"
|
|
||||||
>
|
|
||||||
Ajouter un serveur
|
|
||||||
</UButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #identities>
|
|
||||||
<div class="space-y-4 py-4">
|
|
||||||
<div v-for="(identity, index) in config.identities" :key="identity.id"
|
|
||||||
class="p-4 border border-gray-200 dark:border-gray-800 rounded-lg relative bg-white dark:bg-gray-900 shadow-sm">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<UFormField label="ID d'identité (unique)">
|
|
||||||
<UInput v-model="identity.id" placeholder="ex: mon-id" class="w-full"/>
|
|
||||||
</UFormField>
|
|
||||||
|
|
||||||
<UFormField label="Nom d'utilisateur">
|
|
||||||
<UInput v-model="identity.username" placeholder="ex: MonPseudo" class="w-full"/>
|
|
||||||
</UFormField>
|
|
||||||
|
|
||||||
<UFormField label="Mode d'authentification" class="md:col-span-2">
|
|
||||||
<USelect
|
|
||||||
v-model="identity.mode"
|
|
||||||
:items="[
|
|
||||||
{ label: 'Clé (Fichier)', value: 'private_key_path' },
|
|
||||||
{ label: 'Clé (Base64)', value: 'private_key_base64' },
|
|
||||||
{ label: 'Login uniquement', value: 'login' }
|
|
||||||
]"
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
</UFormField>
|
|
||||||
|
|
||||||
<template v-if="identity.mode === 'private_key_path' || identity.mode === 'private_key_base64'">
|
|
||||||
<UFormField :label="identity.mode === 'private_key_path' ? 'Chemin de la clé' : 'Clé privée (Base64)'"
|
|
||||||
class="md:col-span-2">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<UTextarea
|
|
||||||
v-model="identity.private_key"
|
|
||||||
:placeholder="identity.mode === 'private_key_path' ? 'ex: ~/.ssh/id_rsa' : 'Entrez la clé encodée en base64 (Si vide, une clé sera générée au besoin)'"
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
<!-- <UButton-->
|
|
||||||
<!-- v-if="identity.mode === 'private_key_base64'"-->
|
|
||||||
<!-- icon="i-lucide-key"-->
|
|
||||||
<!-- variant="subtle"-->
|
|
||||||
<!-- size="xs"-->
|
|
||||||
<!-- label="Générer une clé SSH"-->
|
|
||||||
<!-- @click="generateKey(index)"-->
|
|
||||||
<!-- />-->
|
|
||||||
</div>
|
|
||||||
</UFormField>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
|
|
||||||
<template v-if="identity.token">
|
|
||||||
<UFormField label="Jeton JWT (Persistant)" class="md:col-span-2">
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<UInput v-model="identity.token" readonly class="flex-1 font-mono text-xs"/>
|
|
||||||
<UButton
|
|
||||||
icon="i-lucide-copy"
|
|
||||||
variant="ghost"
|
|
||||||
color="neutral"
|
|
||||||
@click="() => {
|
|
||||||
navigator.clipboard.writeText(identity.token || '')
|
|
||||||
toast.add({ title: 'Copié', description: 'Jeton JWT copié dans le presse-papier', color: 'green' })
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</UFormField>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UButton
|
|
||||||
icon="i-lucide-trash"
|
|
||||||
color="red"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
class="absolute top-2 right-2"
|
|
||||||
@click="removeIdentity(index)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UButton
|
|
||||||
icon="i-lucide-plus"
|
|
||||||
variant="dashed"
|
|
||||||
block
|
|
||||||
@click="addIdentity"
|
|
||||||
>
|
|
||||||
Ajouter une identité
|
|
||||||
</UButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</UTabs>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+37
-5
@@ -1,11 +1,43 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import {onMounted, ref} from 'vue'
|
||||||
|
import {useConfigStore} from '../stores/config'
|
||||||
|
import JoinServerModal from '../components/JoinServerModal.vue'
|
||||||
|
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
const isModalOpen = ref(false)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await configStore.init()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<p>Hello world - index</p>
|
<div class="p-6 flex flex-col items-center justify-center h-full bg-gray-50 dark:bg-gray-950">
|
||||||
|
<div class="text-center mb-12">
|
||||||
|
<h1 class="text-4xl font-extrabold tracking-tight mb-2 text-gray-900 dark:text-white">Ox-Speak</h1>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">Votre plateforme de communication décentralisée.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-6 w-full max-w-sm">
|
||||||
|
<UButton
|
||||||
|
icon="i-lucide-plus"
|
||||||
|
size="xl"
|
||||||
|
block
|
||||||
|
label="Joindre un serveur"
|
||||||
|
@click="isModalOpen = true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
to="/config"
|
||||||
|
icon="i-lucide-settings"
|
||||||
|
size="xl"
|
||||||
|
variant="outline"
|
||||||
|
block
|
||||||
|
label="Configuration"
|
||||||
|
color="neutral"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<JoinServerModal v-model:open="isModalOpen"/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
+46
-1
@@ -1,11 +1,12 @@
|
|||||||
import {defineStore} from 'pinia';
|
import {defineStore} from 'pinia';
|
||||||
import {ref} from 'vue';
|
import {ref} from 'vue';
|
||||||
import {type ConfigTree, createConfigClient} from '../config';
|
import {type ConfigTree, createConfigClient} from '@/config';
|
||||||
|
|
||||||
export const useConfigStore = defineStore('config', () => {
|
export const useConfigStore = defineStore('config', () => {
|
||||||
const client = createConfigClient();
|
const client = createConfigClient();
|
||||||
const config = ref<ConfigTree | null>(null);
|
const config = ref<ConfigTree | null>(null);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
|
const saving = ref(false);
|
||||||
|
|
||||||
async function loadConfig() {
|
async function loadConfig() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
@@ -21,19 +22,63 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
config.value = newConfig;
|
config.value = newConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveConfig() {
|
||||||
|
if (!config.value) return;
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
await client.update(config.value);
|
||||||
|
// Re-fetch pour récupérer les clés auto-générées
|
||||||
|
await loadConfig();
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addServer() {
|
||||||
|
if (!config.value) return;
|
||||||
|
config.value.servers.push({adresse: '', identity: ''} as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeServer(index: number) {
|
||||||
|
if (!config.value) return;
|
||||||
|
config.value.servers.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addIdentity() {
|
||||||
|
if (!config.value) return;
|
||||||
|
config.value.identities.push({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
username: '',
|
||||||
|
private_key: '',
|
||||||
|
mode: 'private_key_path'
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeIdentity(index: number) {
|
||||||
|
if (!config.value) return;
|
||||||
|
config.value.identities.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
// Initialiser la configuration et écouter les changements
|
// Initialiser la configuration et écouter les changements
|
||||||
async function init() {
|
async function init() {
|
||||||
await loadConfig();
|
await loadConfig();
|
||||||
await client.onChanged((updatedConfig) => {
|
await client.onChanged((updatedConfig) => {
|
||||||
config.value = updatedConfig;
|
config.value = updatedConfig;
|
||||||
});
|
});
|
||||||
|
console.debug('Config store initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
config,
|
config,
|
||||||
loading,
|
loading,
|
||||||
|
saving,
|
||||||
loadConfig,
|
loadConfig,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
|
saveConfig,
|
||||||
|
addServer,
|
||||||
|
removeServer,
|
||||||
|
addIdentity,
|
||||||
|
removeIdentity,
|
||||||
init,
|
init,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,25 @@
|
|||||||
import {defineStore} from 'pinia';
|
import {defineStore} from 'pinia';
|
||||||
import {ref} from 'vue';
|
import {ref} from 'vue';
|
||||||
|
import {useConfigStore} from './config';
|
||||||
|
|
||||||
export const useGlobalStore = defineStore('global', () => {
|
export const useGlobalStore = defineStore('global', () => {
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const theme = ref('dark');
|
const theme = ref('dark');
|
||||||
|
const isInitialized = ref(false);
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
console.debug('Initializing global store...');
|
||||||
|
if (isInitialized.value) return;
|
||||||
|
|
||||||
|
// Initialisation des stores requis au boot
|
||||||
|
const configStore = useConfigStore();
|
||||||
|
await configStore.init();
|
||||||
|
|
||||||
|
// Autres initialisations peuvent être ajoutées ici plus tard
|
||||||
|
|
||||||
|
isInitialized.value = true;
|
||||||
|
console.debug('Global store initialized');
|
||||||
|
}
|
||||||
|
|
||||||
function setLoading(value: boolean) {
|
function setLoading(value: boolean) {
|
||||||
loading.value = value;
|
loading.value = value;
|
||||||
@@ -16,7 +32,9 @@ export const useGlobalStore = defineStore('global', () => {
|
|||||||
return {
|
return {
|
||||||
loading,
|
loading,
|
||||||
theme,
|
theme,
|
||||||
|
isInitialized,
|
||||||
setLoading,
|
setLoading,
|
||||||
toggleTheme,
|
toggleTheme,
|
||||||
|
init,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
+36
-65
@@ -1,54 +1,10 @@
|
|||||||
import {invoke} from '@tauri-apps/api/core';
|
import type {IWsClient} from './types';
|
||||||
import {listen} from '@tauri-apps/api/event';
|
|
||||||
import type {IWsClient, WsEvent} from './types';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implémentation Tauri du client WebSocket.
|
* Unified WebSocket Client for OxSpeak.
|
||||||
* Utilise le backend Rust pour maintenir la connexion.
|
* Implemented ONLY in TypeScript.
|
||||||
*/
|
*/
|
||||||
export class TauriWsClient implements IWsClient {
|
export class WsClient implements IWsClient {
|
||||||
private serverUrl: string | null = null;
|
|
||||||
|
|
||||||
async connect(serverUrl: string, token?: string): Promise<void> {
|
|
||||||
this.serverUrl = serverUrl;
|
|
||||||
await invoke('ws_connect', {serverUrl, token});
|
|
||||||
}
|
|
||||||
|
|
||||||
async send(message: any): Promise<void> {
|
|
||||||
if (!this.serverUrl) throw new Error('Non connecté');
|
|
||||||
const msgString = typeof message === 'string' ? message : JSON.stringify(message);
|
|
||||||
await invoke('ws_send', {serverUrl: this.serverUrl, message: msgString});
|
|
||||||
}
|
|
||||||
|
|
||||||
async disconnect(): Promise<void> {
|
|
||||||
if (this.serverUrl) {
|
|
||||||
await invoke('ws_disconnect', {serverUrl: this.serverUrl});
|
|
||||||
this.serverUrl = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMessage(callback: (payload: any) => void): () => void {
|
|
||||||
let unlisten: (() => void) | null = null;
|
|
||||||
|
|
||||||
listen<WsEvent>('ws-message', (event) => {
|
|
||||||
// Filtrer par serverUrl si nécessaire
|
|
||||||
if (this.serverUrl && event.payload.server_url === this.serverUrl) {
|
|
||||||
callback(event.payload.payload);
|
|
||||||
}
|
|
||||||
}).then((fn) => {
|
|
||||||
unlisten = fn;
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (unlisten) unlisten();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implémentation Web (standard) du client WebSocket.
|
|
||||||
*/
|
|
||||||
export class WebWsClient implements IWsClient {
|
|
||||||
private ws: WebSocket | null = null;
|
private ws: WebSocket | null = null;
|
||||||
private messageCallbacks: Set<(payload: any) => void> = new Set();
|
private messageCallbacks: Set<(payload: any) => void> = new Set();
|
||||||
private serverUrl: string | null = null;
|
private serverUrl: string | null = null;
|
||||||
@@ -65,19 +21,36 @@ export class WebWsClient implements IWsClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.ws = new WebSocket(wsUrl);
|
try {
|
||||||
|
this.ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
this.ws.onopen = () => resolve();
|
this.ws.onopen = () => {
|
||||||
this.ws.onerror = (err) => reject(err);
|
console.log('WebSocket connected to', wsUrl);
|
||||||
this.ws.onmessage = (event) => {
|
resolve();
|
||||||
try {
|
};
|
||||||
const payload = JSON.parse(event.data);
|
|
||||||
this.messageCallbacks.forEach((cb) => cb(payload));
|
this.ws.onerror = (err) => {
|
||||||
} catch (e) {
|
console.error('WebSocket error:', err);
|
||||||
// Si ce n'est pas du JSON, on renvoie la data brute
|
reject(err);
|
||||||
this.messageCallbacks.forEach((cb) => cb(event.data));
|
};
|
||||||
}
|
|
||||||
};
|
this.ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(event.data);
|
||||||
|
this.messageCallbacks.forEach((cb) => cb(payload));
|
||||||
|
} catch (e) {
|
||||||
|
// Si ce n'est pas du JSON, on renvoie la data brute
|
||||||
|
this.messageCallbacks.forEach((cb) => cb(event.data));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onclose = (event) => {
|
||||||
|
console.log('WebSocket closed:', event.code, event.reason);
|
||||||
|
this.ws = null;
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,11 +76,9 @@ export class WebWsClient implements IWsClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory pour créer le client approprié.
|
* Factory for creating the WebSocket client.
|
||||||
|
* Always returns the unified TS implementation.
|
||||||
*/
|
*/
|
||||||
export function createWsClient(): IWsClient {
|
export function createWsClient(): IWsClient {
|
||||||
if ((window as any).__TAURI_INTERNALS__) {
|
return new WsClient();
|
||||||
return new TauriWsClient();
|
|
||||||
}
|
|
||||||
return new WebWsClient();
|
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-6
@@ -3,9 +3,12 @@
|
|||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": [
|
||||||
|
"ES2020",
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
@@ -13,17 +16,26 @@
|
|||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
/* Paths */
|
/* Paths */
|
||||||
"paths": {
|
"paths": {
|
||||||
"#build/ui/*": ["./node_modules/.nuxt-ui/ui/*"]
|
"#build/ui/*": [
|
||||||
|
"./node_modules/.nuxt-ui/ui/*"
|
||||||
|
],
|
||||||
|
"@/*": [
|
||||||
|
"src/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "auto-imports.d.ts", "components.d.ts"]
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.tsx",
|
||||||
|
"src/**/*.vue",
|
||||||
|
"auto-imports.d.ts",
|
||||||
|
"components.d.ts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
+12
-6
@@ -1,27 +1,33 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"lib": ["ES2023"],
|
"lib": [
|
||||||
|
"ES2023"
|
||||||
|
],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
/* Paths */
|
/* Paths */
|
||||||
"paths": {
|
"paths": {
|
||||||
"#build/ui": ["./node_modules/.nuxt-ui/ui"]
|
"#build/ui": [
|
||||||
|
"./node_modules/.nuxt-ui/ui"
|
||||||
|
],
|
||||||
|
"@/*": [
|
||||||
|
"src/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["vite.config.ts"]
|
"include": [
|
||||||
|
"vite.config.ts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
+10
-6
@@ -1,4 +1,4 @@
|
|||||||
import { defineConfig } from "vite";
|
import {defineConfig} from "vite";
|
||||||
import vue from "@vitejs/plugin-vue";
|
import vue from "@vitejs/plugin-vue";
|
||||||
import ui from '@nuxt/ui/vite'
|
import ui from '@nuxt/ui/vite'
|
||||||
|
|
||||||
@@ -11,7 +11,11 @@ export default defineConfig(async () => ({
|
|||||||
vue(),
|
vue(),
|
||||||
ui()
|
ui()
|
||||||
],
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': '/src'
|
||||||
|
}
|
||||||
|
},
|
||||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||||
//
|
//
|
||||||
// 1. prevent vite from obscuring rust errors
|
// 1. prevent vite from obscuring rust errors
|
||||||
@@ -24,10 +28,10 @@ export default defineConfig(async () => ({
|
|||||||
cors: true,
|
cors: true,
|
||||||
hmr: host
|
hmr: host
|
||||||
? {
|
? {
|
||||||
protocol: "ws",
|
protocol: "ws",
|
||||||
host,
|
host,
|
||||||
port: 1421,
|
port: 1421,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
watch: {
|
watch: {
|
||||||
// 3. tell vite to ignore watching `src-tauri`
|
// 3. tell vite to ignore watching `src-tauri`
|
||||||
|
|||||||
@@ -1354,6 +1354,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@tauri-apps/plugin-log@npm:~2":
|
||||||
|
version: 2.8.0
|
||||||
|
resolution: "@tauri-apps/plugin-log@npm:2.8.0"
|
||||||
|
dependencies:
|
||||||
|
"@tauri-apps/api": "npm:^2.8.0"
|
||||||
|
checksum: 10c0/04c21bae0174de8a394760e40986ae5f28e07830b9e7388dd9ddd4796c4b4eac7f80807a90498d5896a6113134efb1ddab1038be9c6ba2095c31b75f1b85dc7f
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@tauri-apps/plugin-opener@npm:^2":
|
"@tauri-apps/plugin-opener@npm:^2":
|
||||||
version: 2.5.3
|
version: 2.5.3
|
||||||
resolution: "@tauri-apps/plugin-opener@npm:2.5.3"
|
resolution: "@tauri-apps/plugin-opener@npm:2.5.3"
|
||||||
@@ -3869,6 +3878,7 @@ __metadata:
|
|||||||
"@nuxt/ui": "npm:^4.5.1"
|
"@nuxt/ui": "npm:^4.5.1"
|
||||||
"@tauri-apps/api": "npm:^2"
|
"@tauri-apps/api": "npm:^2"
|
||||||
"@tauri-apps/cli": "npm:^2"
|
"@tauri-apps/cli": "npm:^2"
|
||||||
|
"@tauri-apps/plugin-log": "npm:~2"
|
||||||
"@tauri-apps/plugin-opener": "npm:^2"
|
"@tauri-apps/plugin-opener": "npm:^2"
|
||||||
"@vitejs/plugin-vue": "npm:^6.0.5"
|
"@vitejs/plugin-vue": "npm:^6.0.5"
|
||||||
pinia: "npm:^3.0.4"
|
pinia: "npm:^3.0.4"
|
||||||
|
|||||||
Reference in New Issue
Block a user