diff --git a/.github/workflows/app-build-release-artifacts.yml b/.github/workflows/app-build-release-artifacts.yml index 41bdd21a..880f6a12 100644 --- a/.github/workflows/app-build-release-artifacts.yml +++ b/.github/workflows/app-build-release-artifacts.yml @@ -6,14 +6,14 @@ on: - staging paths: - 'apps/app/**' - - '.github/workflows/app-build-release-artifacts.yml' + - '.github/workflows/tauri-build-debug.yml' pull_request: branches: - dev - staging paths: - 'apps/app/**' - - '.github/workflows/app-build-release-artifacts.yml' + - '.github/workflows/tauri-build-debug.yml' workflow_dispatch: jobs: diff --git a/apps/app/src-tauri/Cargo.lock b/apps/app/src-tauri/Cargo.lock index 3f30b6a6..800f9775 100644 --- a/apps/app/src-tauri/Cargo.lock +++ b/apps/app/src-tauri/Cargo.lock @@ -289,6 +289,103 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "automerge" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fba3b76f952e13270300d8a1aca3476942bba013de5e64ef538ef5607b40df" +dependencies = [ + "cfg-if", + "flate2", + "fxhash", + "hex", + "im", + "itertools", + "leb128", + "serde", + "sha2", + "smol_str", + "thiserror 1.0.69", + "tinyvec", + "tracing", + "unicode-segmentation", + "uuid", +] + +[[package]] +name = "aws-lc-rs" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "base64" version = "0.21.7" @@ -316,6 +413,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + [[package]] name = "bitvec" version = "1.0.1" @@ -544,6 +650,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -590,10 +698,20 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" name = "cherit" version = "0.0.1" dependencies = [ + "automerge", + "axum", + "dirs", "futures", + "gethostname 0.4.3", "log", + "md5", + "mdns-sd", "natord", + "notify", + "notify-debouncer-full", + "rand 0.9.2", "rayon", + "reqwest", "serde", "serde_json", "tauri", @@ -607,6 +725,7 @@ dependencies = [ "tauri-plugin-os", "tauri-plugin-safe-area-insets-css", "tauri-plugin-store", + "tokio", "urlencoding", ] @@ -631,6 +750,15 @@ dependencies = [ "error-code", ] +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + [[package]] name = "combine" version = "4.6.7" @@ -666,6 +794,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -689,7 +827,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ "bitflags 2.11.0", - "core-foundation", + "core-foundation 0.10.1", "core-graphics-types", "foreign-types", "libc", @@ -702,7 +840,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.11.0", - "core-foundation", + "core-foundation 0.10.1", "libc", ] @@ -1029,6 +1167,15 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "endi" version = "1.1.1" @@ -1205,6 +1352,17 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1253,6 +1411,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -1497,6 +1661,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + [[package]] name = "gethostname" version = "1.1.0" @@ -1525,8 +1699,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1536,9 +1712,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1702,6 +1880,25 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.7.1" @@ -1812,6 +2009,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.8.1" @@ -1822,9 +2025,11 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -1833,6 +2038,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1851,9 +2072,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -2004,6 +2227,30 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "if-addrs" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0a05c691e1fae256cf7013d99dad472dc52d5543322761f83ec8d47eab40d2b" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "im" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" +dependencies = [ + "bitmaps", + "rand_core 0.6.4", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check", +] + [[package]] name = "image" version = "0.25.9" @@ -2105,6 +2352,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -2156,6 +2412,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.91" @@ -2237,6 +2503,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + [[package]] name = "leb128fmt" version = "0.1.0" @@ -2332,6 +2604,12 @@ dependencies = [ "value-bag", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "mac" version = "0.1.1" @@ -2369,6 +2647,33 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md5" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" + +[[package]] +name = "mdns-sd" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3efd5e35bde8fe2bf6f698a0158c198eade41805bb89788ee59853baeb877" +dependencies = [ + "fastrand", + "flume", + "if-addrs", + "log", + "mio", + "socket-pktinfo", + "socket2", +] + [[package]] name = "memchr" version = "2.8.0" @@ -2859,6 +3164,12 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "option-ext" version = "0.2.0" @@ -3355,6 +3666,62 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.44" @@ -3486,6 +3853,15 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -3598,21 +3974,30 @@ checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ "base64 0.22.1", "bytes", + "encoding_rs", "futures-core", "futures-util", + "h2", "http", "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", + "mime", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", "sync_wrapper", "tokio", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -3649,6 +4034,20 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rkyv" version = "0.7.45" @@ -3694,6 +4093,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -3716,12 +4121,93 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -3731,6 +4217,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.8.22" @@ -3800,6 +4295,29 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.24.0" @@ -3894,6 +4412,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -3923,6 +4452,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "3.17.0" @@ -4037,6 +4578,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + [[package]] name = "slab" version = "0.4.12" @@ -4049,6 +4600,26 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "socket-pktinfo" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927136cc2ae6a1b0e66ac6b1210902b75c3f726db004a73bc18686dcd0dcd22f" +dependencies = [ + "libc", + "socket2", + "windows-sys 0.60.2", +] + [[package]] name = "socket2" version = "0.6.2" @@ -4107,6 +4678,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -4150,6 +4730,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -4223,6 +4809,27 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -4244,7 +4851,7 @@ checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" dependencies = [ "bitflags 2.11.0", "block2", - "core-foundation", + "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", "dispatch", @@ -4556,7 +5163,7 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8f08346c8deb39e96f86973da0e2d76cbb933d7ac9b750f6dc4daf955a6f997" dependencies = [ - "gethostname", + "gethostname 1.1.0", "log", "os_info", "serde", @@ -4835,13 +5442,14 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", @@ -4861,6 +5469,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -4983,6 +5601,7 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -5021,6 +5640,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -5167,6 +5787,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -5504,6 +6130,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webkit2gtk" version = "2.0.2" @@ -5548,6 +6184,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.2" @@ -5739,6 +6384,17 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -5784,6 +6440,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -5826,6 +6491,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -5883,6 +6563,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -5901,6 +6587,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -5919,6 +6611,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -5949,6 +6647,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -5967,6 +6671,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -5985,6 +6695,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -6003,6 +6719,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -6236,7 +6958,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" dependencies = [ - "gethostname", + "gethostname 1.1.0", "rustix", "x11rb-protocol", ] @@ -6373,6 +7095,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.3" diff --git a/apps/app/src-tauri/Cargo.toml b/apps/app/src-tauri/Cargo.toml index 14ee775e..186f1245 100644 --- a/apps/app/src-tauri/Cargo.toml +++ b/apps/app/src-tauri/Cargo.toml @@ -18,6 +18,7 @@ crate-type = ["staticlib", "cdylib", "rlib"] tauri-build = { version = "2.5.5", features = [] } [dependencies] +gethostname = "0.4.3" tauri-plugin-android-fs = { version = "26", features = [ "legacy_storage_permission", ] } @@ -37,6 +38,16 @@ tauri-plugin-safe-area-insets-css = "0.2" tauri-plugin-clipboard-manager = "2" futures = "0.3" rayon = "1.10" +mdns-sd = "0.18.1" +automerge = "0.6.1" +axum = "0.8.4" +tokio = { version = "1.50.0", features = ["full"] } +reqwest = { version = "0.13.2", features = ["json"] } +rand = "0.9.2" +dirs = "6.0.0" +notify = "8.2.0" +notify-debouncer-full = "0.6.0" +md5 = "0.8.0" [profile.dev] incremental = true # Compile your binary in smaller steps. diff --git a/apps/app/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/apps/app/src-tauri/gen/android/app/src/main/AndroidManifest.xml index 3a189f18..8a83db71 100644 --- a/apps/app/src-tauri/gen/android/app/src/main/AndroidManifest.xml +++ b/apps/app/src-tauri/gen/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,12 @@ + + + + + + @@ -33,6 +39,11 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> + + diff --git a/apps/app/src-tauri/gen/android/app/src/main/java/io/github/keshav_writes_code/cherit/MainActivity.kt b/apps/app/src-tauri/gen/android/app/src/main/java/io/github/keshav_writes_code/cherit/MainActivity.kt index 79db2162..f71d661c 100644 --- a/apps/app/src-tauri/gen/android/app/src/main/java/io/github/keshav_writes_code/cherit/MainActivity.kt +++ b/apps/app/src-tauri/gen/android/app/src/main/java/io/github/keshav_writes_code/cherit/MainActivity.kt @@ -1,5 +1,7 @@ package io.github.keshav_writes_code.cherit +import android.content.Intent +import android.os.Build import android.os.Bundle import androidx.activity.enableEdgeToEdge @@ -8,4 +10,21 @@ class MainActivity : TauriActivity() { enableEdgeToEdge() super.onCreate(savedInstanceState) } + + // We are exposing a method that can be called from Rust/Tauri via JNI or plugin mechanism + // In a real plugin, this would be within a TauriPlugin class, but for simplicity here we + // expose it from the main activity. + fun startSyncForegroundService() { + val intent = Intent(this, SyncForegroundService::class.java) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(intent) + } else { + startService(intent) + } + } + + fun stopSyncForegroundService() { + val intent = Intent(this, SyncForegroundService::class.java) + stopService(intent) + } } diff --git a/apps/app/src-tauri/gen/android/app/src/main/java/io/github/keshav_writes_code/cherit/SyncForegroundService.kt b/apps/app/src-tauri/gen/android/app/src/main/java/io/github/keshav_writes_code/cherit/SyncForegroundService.kt new file mode 100644 index 00000000..f37fb946 --- /dev/null +++ b/apps/app/src-tauri/gen/android/app/src/main/java/io/github/keshav_writes_code/cherit/SyncForegroundService.kt @@ -0,0 +1,62 @@ +package io.github.keshav_writes_code.cherit + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationCompat + +class SyncForegroundService : Service() { + private val CHANNEL_ID = "SyncServiceChannel" + + override fun onCreate() { + super.onCreate() + createNotificationChannel() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val notificationIntent = Intent(this, MainActivity::class.java) + val pendingIntent = PendingIntent.getActivity( + this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE + ) + + val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("Cherit Sync Active") + .setContentText("Syncing notes on local network...") + .setSmallIcon(android.R.drawable.stat_notify_sync) + .setContentIntent(pendingIntent) + .build() + + startForeground(1, notification) + + // The actual sync logic (mDNS + Server) runs in Rust within the Tauri app process. + // This service exists purely to keep the app process alive and network allowed in the background. + + return START_NOT_STICKY + } + + override fun onDestroy() { + super.onDestroy() + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val serviceChannel = NotificationChannel( + CHANNEL_ID, + "Sync Service Channel", + NotificationManager.IMPORTANCE_DEFAULT + ) + val manager = getSystemService(NotificationManager::class.java) + manager?.createNotificationChannel(serviceChannel) + } + } +} diff --git a/apps/app/src-tauri/src/lib.rs b/apps/app/src-tauri/src/lib.rs index f6507290..143f2ca9 100644 --- a/apps/app/src-tauri/src/lib.rs +++ b/apps/app/src-tauri/src/lib.rs @@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize}; #[cfg(all(test, not(target_os = "android")))] mod desktop_test; +mod sync; + #[derive(Serialize, Deserialize, Clone)] pub struct FileNode { pub name: String, @@ -432,6 +434,8 @@ async fn move_file_android( } } +use tauri::Manager; + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() @@ -446,9 +450,20 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ build_file_tree, move_file_android, - move_directory_android + move_directory_android, + sync::commands::start_sync_service, + sync::commands::stop_sync_service, + sync::commands::generate_pairing_pin, + sync::commands::get_discovered_peers, + sync::commands::pair_with_peer, + sync::commands::sync_file, + sync::commands::remove_peer, + sync::commands::rename_peer ]) .setup(|app| { + app.manage(sync::commands::AppSyncState { + inner: tokio::sync::RwLock::new(None), + }); if cfg!(debug_assertions) { app.handle().plugin( tauri_plugin_log::Builder::default() diff --git a/apps/app/src-tauri/src/sync/commands.rs b/apps/app/src-tauri/src/sync/commands.rs new file mode 100644 index 00000000..2c36bf42 --- /dev/null +++ b/apps/app/src-tauri/src/sync/commands.rs @@ -0,0 +1,233 @@ +use crate::sync::discovery::SyncState; +use crate::sync::pairing::{PairRequest, PairResponse}; +use std::sync::Arc; +use tokio::sync::RwLock; +use tauri::{Manager, State}; +// use notify_debouncer_full::{new_debouncer, notify::*}; +// use std::time::Duration; + +pub struct AppSyncState { + pub inner: RwLock>>, +} + +#[tauri::command] +pub async fn start_sync_service(app_handle: tauri::AppHandle, state: State<'_, AppSyncState>, workspace_root: String) -> Result<(), String> { + let mut sync_state_lock = state.inner.write().await; + + if sync_state_lock.is_none() { + // Generate a random ID for the session to prevent collisions in MVP + let my_id = crate::sync::pairing::generate_pin().await; + let hostname = gethostname::gethostname().to_string_lossy().into_owned(); + let my_name = if hostname.is_empty() { format!("Device-{}", my_id) } else { hostname }; + + let port = 8080; // Should find an available port dynamically + + let config_dir = app_handle.path().app_config_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); + let _ = std::fs::create_dir_all(&config_dir); + + let sync_state = Arc::new(SyncState::new(my_id, my_name, port, workspace_root, config_dir).await?); + + // Start broadcasting our presence + sync_state.start_broadcasting()?; + + // Start discovering peers + SyncState::start_discovery(sync_state.clone()).await?; + + // Start the server + let state_clone = sync_state.clone(); + let app_handle_clone = app_handle.clone(); + tokio::spawn(async move { + if let Err(e) = crate::sync::server::start_server(state_clone, port, app_handle_clone).await { + eprintln!("Failed to start server: {}", e); + } + }); + + *sync_state_lock = Some(sync_state); + + #[cfg(target_os = "android")] + { + // The `run_mobile_plugin` API is typically used for actual Tauri plugins, + // not the main app code directly unless properly registered. + // For MVP, we can trigger the native Android method via JNI or skip it + // since the Rust code is running in a Tokio background task anyway. + // A fully correct implementation would use the `jni` crate to call + // `startSyncForegroundService` on `MainActivity`. + // For now to avoid compilation errors on Android: + println!("Android sync service started (ensure background constraints are met)."); + } + + } + + Ok(()) +} + +#[tauri::command] +pub async fn stop_sync_service(state: State<'_, AppSyncState>) -> Result<(), String> { + let mut sync_state_lock = state.inner.write().await; + if let Some(sync_state) = sync_state_lock.as_ref() { + sync_state.stop_broadcasting()?; + if let Some(tx) = &sync_state.server_shutdown_tx { + let _ = tx.send(()); + } + *sync_state_lock = None; + } + Ok(()) +} + +#[tauri::command] +pub async fn generate_pairing_pin(state: State<'_, AppSyncState>) -> Result { + let sync_state_lock = state.inner.read().await; + if let Some(sync_state) = sync_state_lock.as_ref() { + let pin = crate::sync::pairing::generate_pin().await; + let mut active_pin = sync_state.active_pin.write().await; + *active_pin = Some(pin.clone()); + Ok(pin) + } else { + Err("Sync service is not running".into()) + } +} + +#[tauri::command] +pub async fn get_discovered_peers(state: State<'_, AppSyncState>) -> Result, String> { + let sync_state_lock = state.inner.read().await; + if let Some(sync_state) = sync_state_lock.as_ref() { + let peers = sync_state.peers.read().await; + Ok(peers.values().cloned().collect()) + } else { + Err("Sync service is not running".into()) + } +} + +#[tauri::command] +pub async fn sync_file(state: State<'_, AppSyncState>, file_path: String) -> Result<(), String> { + let sync_state_lock = state.inner.read().await; + if let Some(sync_state) = sync_state_lock.as_ref() { + let peers = sync_state.peers.read().await; + + let client = reqwest::Client::new(); + + let path_obj = std::path::PathBuf::from(&file_path); + + // Use CRDT Manager to update our local state and get the automerge doc payload + let mut crdt_manager = sync_state.crdt_manager.write().await; + + // Strip the base_dir from the file path to get a relative path + let relative_path = match path_obj.strip_prefix(&crdt_manager.base_dir) { + Ok(p) => p.to_path_buf(), + Err(_) => { + // If it's not in the base dir, we just use the filename as a fallback MVP + std::path::PathBuf::from(path_obj.file_name().unwrap_or_default()) + } + }; + + // First ensure the file is loaded into the CRDT manager so we have an active tracking state + // This is necessary if it's the very first time the file is being synced. + if !crdt_manager.documents.contains_key(&relative_path) { + if let Err(e) = crdt_manager.load_or_create_doc(&relative_path).await { + eprintln!("Failed to initialize doc for sync: {}", e); + } + } + + if let Err(e) = crdt_manager.update_doc_from_file(&relative_path).await { + eprintln!("Failed to update doc for sync: {}", e); + } + + // Fetch the raw bytes of the automerge CRDT state to send + let payload_content = match crdt_manager.documents.get(&relative_path) { + Some(doc) => doc.automerge_doc.save(), + None => return Err("Failed to generate CRDT payload".to_string()), + }; + + // Convert the local relative path to a cross-platform friendly Unix format for the network + let relative_path_str = relative_path.to_string_lossy().replace('\\', "/"); + + for peer in peers.values() { + if peer.is_paired { + let url = format!("http://{}:{}/sync", peer.ip, peer.port); + + let request = crate::sync::server::SyncRequest { + peer_id: sync_state.my_id.clone(), + file_path: relative_path_str.clone(), + content: payload_content.clone(), + }; + + // Fire and forget sync request + let _ = client.post(&url).json(&request).send().await; + } + } + } + + Ok(()) +} + +#[tauri::command] +pub async fn remove_peer(state: State<'_, AppSyncState>, peer_id: String) -> Result<(), String> { + let sync_state_lock = state.inner.read().await; + if let Some(sync_state) = sync_state_lock.as_ref() { + let mut peers = sync_state.peers.write().await; + peers.remove(&peer_id); + drop(peers); + sync_state.save_peers().await; + Ok(()) + } else { + Err("Sync service is not running".into()) + } +} + +#[tauri::command] +pub async fn rename_peer(state: State<'_, AppSyncState>, peer_id: String, new_name: String) -> Result<(), String> { + let sync_state_lock = state.inner.read().await; + if let Some(sync_state) = sync_state_lock.as_ref() { + let mut peers = sync_state.peers.write().await; + if let Some(peer) = peers.get_mut(&peer_id) { + peer.name = new_name; + } + drop(peers); + sync_state.save_peers().await; + Ok(()) + } else { + Err("Sync service is not running".into()) + } +} + +#[tauri::command] +pub async fn pair_with_peer(state: State<'_, AppSyncState>, peer_id: String, pin: String) -> Result { + let sync_state_lock = state.inner.read().await; + if let Some(sync_state) = sync_state_lock.as_ref() { + let peers = sync_state.peers.read().await; + if let Some(peer) = peers.get(&peer_id) { + let client = reqwest::Client::new(); + let url = format!("http://{}:{}/pair", peer.ip, peer.port); + + let request = PairRequest { + peer_id: sync_state.my_id.clone(), + pin, + }; + + let res = client.post(&url) + .json(&request) + .send() + .await + .map_err(|e| e.to_string())?; + + let pair_response: PairResponse = res.json().await.map_err(|e| e.to_string())?; + + if pair_response.success { + // Drop read lock to acquire write lock safely + drop(peers); + let mut peers_write = sync_state.peers.write().await; + if let Some(p) = peers_write.get_mut(&peer_id) { + p.is_paired = true; + } + drop(peers_write); + sync_state.save_peers().await; + } + + Ok(pair_response) + } else { + Err("Peer not found".into()) + } + } else { + Err("Sync service is not running".into()) + } +} diff --git a/apps/app/src-tauri/src/sync/crdt.rs b/apps/app/src-tauri/src/sync/crdt.rs new file mode 100644 index 00000000..1218d9ba --- /dev/null +++ b/apps/app/src-tauri/src/sync/crdt.rs @@ -0,0 +1,184 @@ +use automerge::{Automerge, ObjType, ReadDoc, transaction::Transactable, ROOT}; +use std::collections::HashMap; +use std::path::PathBuf; +use tokio::fs; + +#[allow(dead_code)] +pub struct DocumentState { + pub automerge_doc: Automerge, + pub text_obj_id: automerge::ObjId, + pub path: PathBuf, +} + +#[allow(dead_code)] +pub struct CrdtManager { + pub base_dir: PathBuf, + pub sync_dir: PathBuf, + pub documents: HashMap, +} + +#[allow(dead_code)] +impl CrdtManager { + pub async fn new(base_dir: PathBuf, config_dir: PathBuf) -> Result { + // Instead of writing to base_dir which causes "Read-only file system" errors + // on Android SAF URIs, we store sync states in the app's native config directory. + // We hash the base_dir path so multiple workspaces don't collide their sync states. + + let workspace_hash = md5::compute(base_dir.to_string_lossy().as_bytes()); + let sync_dir = config_dir.join(format!("sync_states_{:x}", workspace_hash)); + + if !sync_dir.exists() { + fs::create_dir_all(&sync_dir) + .await + .map_err(|e| format!("Failed to create sync dir: {}", e))?; + } + + Ok(Self { + base_dir, + sync_dir, + documents: HashMap::new(), + }) + } + + pub async fn load_or_create_doc(&mut self, relative_path: &PathBuf) -> Result<(), String> { + let abs_file_path = self.base_dir.join(relative_path); + let sync_file_path = self.sync_dir.join(relative_path.with_extension("am")); + + let mut automerge_doc = Automerge::new(); + let mut text_obj_id = None; + + if sync_file_path.exists() { + let data = fs::read(&sync_file_path) + .await + .map_err(|e| format!("Failed to read sync file: {}", e))?; + automerge_doc = Automerge::load(&data) + .map_err(|e| format!("Failed to load automerge doc: {}", e))?; + + if let Ok(Some((automerge::Value::Object(ObjType::Text), id))) = automerge_doc.get(ROOT, "content") { + text_obj_id = Some(id); + } + } + + if text_obj_id.is_none() { + let mut tx = automerge_doc.transaction(); + let new_id = tx.put_object(ROOT, "content", ObjType::Text) + .map_err(|e| format!("Failed to create text object: {}", e))?; + + if abs_file_path.exists() { + if let Ok(content) = fs::read_to_string(&abs_file_path).await { + tx.splice_text(&new_id, 0, 0, &content) + .map_err(|e| format!("Failed to splice initial content: {}", e))?; + } + } else { + // Ensure the base directory exists if this is a newly synced file + if let Some(parent) = abs_file_path.parent() { + let _ = fs::create_dir_all(parent).await; + } + // Pre-create the empty markdown file + let _ = fs::write(&abs_file_path, "").await; + } + tx.commit(); + text_obj_id = Some(new_id); + + if let Some(parent) = sync_file_path.parent() { + fs::create_dir_all(parent) + .await + .map_err(|e| format!("Failed to create sync subdirs: {}", e))?; + } + fs::write(&sync_file_path, automerge_doc.save()) + .await + .map_err(|e| format!("Failed to write initial sync file: {}", e))?; + } + + self.documents.insert( + relative_path.clone(), + DocumentState { + automerge_doc, + text_obj_id: text_obj_id.unwrap(), + path: relative_path.clone(), + }, + ); + + Ok(()) + } + + pub async fn update_doc_from_file(&mut self, relative_path: &PathBuf) -> Result<(), String> { + let abs_file_path = self.base_dir.join(relative_path); + let sync_file_path = self.sync_dir.join(relative_path.with_extension("am")); + + if !self.documents.contains_key(relative_path) { + self.load_or_create_doc(relative_path).await?; + } + + if let Some(doc_state) = self.documents.get_mut(relative_path) { + let new_content = match fs::read_to_string(&abs_file_path).await { + Ok(c) => c, + Err(_) => { + // If the file cannot be read, assume it was deleted or missing. + // For now, we'll treat it as empty. + String::new() + } + }; + + // Calculate basic text change to minimize history bloat + let current_text = doc_state.automerge_doc.text(&doc_state.text_obj_id) + .unwrap_or_default(); + + // Only mutate if there's actually a change + if current_text != new_content { + let mut tx = doc_state.automerge_doc.transaction(); + let current_len = tx.length(&doc_state.text_obj_id); + + + // If new_content is empty (e.g. file just created or wiped), + // we still need to record the deletion. + if current_len > 0 { + tx.splice_text(&doc_state.text_obj_id, 0, current_len as isize, "") + .map_err(|e| format!("Failed to delete old content via splice: {}", e))?; + } + if !new_content.is_empty() { + tx.splice_text(&doc_state.text_obj_id, 0, 0, &new_content) + .map_err(|e| format!("Failed to insert new content via splice: {}", e))?; + } + tx.commit(); + } + + fs::write(&sync_file_path, doc_state.automerge_doc.save()) + .await + .map_err(|e| format!("Failed to save sync file: {}", e))?; + } + + Ok(()) + } + + pub async fn apply_sync_data(&mut self, relative_path: &PathBuf, remote_data: &[u8]) -> Result<(), String> { + let sync_file_path = self.sync_dir.join(relative_path.with_extension("am")); + let abs_file_path = self.base_dir.join(relative_path); + + if !self.documents.contains_key(relative_path) { + self.load_or_create_doc(relative_path).await?; + } + + if let Some(doc_state) = self.documents.get_mut(relative_path) { + let mut remote_doc = Automerge::load(remote_data) + .map_err(|e| format!("Failed to load remote automerge doc: {}", e))?; + + doc_state.automerge_doc.merge(&mut remote_doc) + .map_err(|e| format!("Failed to merge docs: {}", e))?; + + // Save updated sync state + fs::write(&sync_file_path, doc_state.automerge_doc.save()) + .await + .map_err(|e| format!("Failed to save sync file after merge: {}", e))?; + + // Project new state to plain markdown file + if let Ok(content_str) = doc_state.automerge_doc.text(&doc_state.text_obj_id) { + fs::write(&abs_file_path, content_str) + .await + .map_err(|e| format!("Failed to write merged content to markdown: {}", e))?; + } + } + + Ok(()) + } +} diff --git a/apps/app/src-tauri/src/sync/discovery.rs b/apps/app/src-tauri/src/sync/discovery.rs new file mode 100644 index 00000000..182aeed7 --- /dev/null +++ b/apps/app/src-tauri/src/sync/discovery.rs @@ -0,0 +1,189 @@ +use mdns_sd::{ServiceDaemon, ServiceEvent, ServiceInfo}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; +use crate::sync::crdt::CrdtManager; + +pub const SERVICE_TYPE: &str = "_cherit._tcp.local."; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct PeerInfo { + pub id: String, + pub name: String, + pub ip: String, + pub port: u16, + pub is_paired: bool, +} + +pub struct SyncState { + pub mdns: ServiceDaemon, + pub my_id: String, + pub my_name: String, + pub peers: RwLock>, + pub port: u16, + pub active_pin: RwLock>, + pub crdt_manager: RwLock, + pub server_shutdown_tx: Option>, + pub config_dir: std::path::PathBuf, + // pub fs_watcher: RwLock>>, +} + +impl SyncState { + pub async fn new(id: String, name: String, port: u16, workspace_root: String, config_dir: std::path::PathBuf) -> Result { + let mdns = ServiceDaemon::new().map_err(|e| e.to_string())?; + + // Clean URL-encoded file protocols if present + let mut path_str = workspace_root.clone(); + if path_str.starts_with("file://") { + path_str = path_str.trim_start_matches("file://").to_string(); + } + path_str = urlencoding::decode(&path_str).unwrap_or(std::borrow::Cow::Borrowed(&path_str)).to_string(); + + let crdt_manager = CrdtManager::new(std::path::PathBuf::from(path_str), config_dir.clone()).await?; + + // Channel for server shutdown + let (tx, _) = tokio::sync::broadcast::channel(1); + + let peers_file = config_dir.join("peers.json"); + let mut peers = HashMap::new(); + if peers_file.exists() { + if let Ok(data) = std::fs::read_to_string(&peers_file) { + if let Ok(saved_peers) = serde_json::from_str::>(&data) { + peers = saved_peers; + } + } + } + + Ok(Self { + mdns, + my_id: id, + my_name: name, + peers: RwLock::new(peers), + port, + active_pin: RwLock::new(None), + crdt_manager: RwLock::new(crdt_manager), + server_shutdown_tx: Some(tx), + config_dir, + }) + } + + pub async fn save_peers(&self) { + let peers = self.peers.read().await; + let peers_file = self.config_dir.join("peers.json"); + if let Ok(data) = serde_json::to_string(&*peers) { + let _ = std::fs::write(peers_file, data); + } + } + + pub fn start_broadcasting(&self) -> Result<(), String> { + let instance_name = format!("{}-{}", self.my_name, self.my_id); + + // Find the actual local IP address on the network instead of 0.0.0.0 + let my_ip = match std::net::UdpSocket::bind("0.0.0.0:0") { + Ok(s) => match s.connect("8.8.8.8:80") { + Ok(_) => s.local_addr().map(|a| a.ip().to_string()).unwrap_or_else(|_| "0.0.0.0".to_string()), + Err(_) => "0.0.0.0".to_string(), + }, + Err(_) => "0.0.0.0".to_string(), + }; + + let mut properties = HashMap::new(); + properties.insert("id".to_string(), self.my_id.clone()); + + let service_info = ServiceInfo::new( + SERVICE_TYPE, + &instance_name, + &format!("{}.local.", instance_name), + &my_ip, + self.port, + Some(properties), + ) + .map_err(|e| e.to_string())?; + + self.mdns + .register(service_info) + .map_err(|e| e.to_string())?; + + Ok(()) + } + + pub fn stop_broadcasting(&self) -> Result<(), String> { + let instance_name = format!("{}-{}", self.my_name, self.my_id); + self.mdns.unregister(&format!("{}.{}", instance_name, SERVICE_TYPE)).map_err(|e| e.to_string())?; + Ok(()) + } + + pub async fn start_discovery(state: Arc) -> Result<(), String> { + // Also proactively probe known peers to make connection fast instead of waiting for mDNS + let peers_clone = state.peers.read().await.clone(); + tokio::spawn(async move { + let client = reqwest::Client::builder().timeout(std::time::Duration::from_secs(2)).build().unwrap(); + for (_, peer) in peers_clone { + if peer.is_paired { + let url = format!("http://{}:{}/status", peer.ip, peer.port); + if client.get(&url).send().await.is_ok() { + // Found them! + } + } + } + }); + + let receiver = state.mdns.browse(SERVICE_TYPE).map_err(|e| e.to_string())?; + + tokio::spawn(async move { + while let Ok(event) = receiver.recv_async().await { + match event { + ServiceEvent::ServiceResolved(info) => { + let properties = info.get_properties(); + if let Some(peer_id) = properties.get_property_val_str("id") { + // Don't add ourselves + if peer_id == state.my_id { + continue; + } + + if let Some(ip) = info.get_addresses().iter().next() { + // Extract the clean name by removing the "-._cherit._tcp.local." suffix + let raw_fullname = info.get_fullname().to_string(); + let mut clean_name = raw_fullname.clone(); + if let Some(idx) = raw_fullname.find("._cherit") { + clean_name = raw_fullname[..idx].to_string(); + } + + let peer_info = PeerInfo { + id: peer_id.to_string(), + name: clean_name, + ip: ip.to_string(), + port: info.get_port(), + is_paired: false, + }; + let mut peers = state.peers.write().await; + + // Preserve pairing status if we already knew this peer + let is_paired = peers.get(&peer_id.to_string()).map(|p| p.is_paired).unwrap_or(false); + + let mut updated_info = peer_info; + updated_info.is_paired = is_paired; + + peers.insert(peer_id.to_string(), updated_info); + } + } + } + ServiceEvent::ServiceFound(_service_type, _fullname) => { + // mdns-sd automatically resolves found services on the next broadcast packet + } + ServiceEvent::ServiceRemoved(_, fullname) => { + // For now we need to iterate to find the peer to remove, + // or better yet, store fullnames or resolve ids. + // A simpler approach is to periodically clear dead peers. + let mut peers = state.peers.write().await; + peers.retain(|_, v| !fullname.contains(&v.name)); + } + _ => {} + } + } + }); + + Ok(()) + } +} diff --git a/apps/app/src-tauri/src/sync/mod.rs b/apps/app/src-tauri/src/sync/mod.rs new file mode 100644 index 00000000..74e4ac24 --- /dev/null +++ b/apps/app/src-tauri/src/sync/mod.rs @@ -0,0 +1,5 @@ +pub mod commands; +pub mod crdt; +pub mod discovery; +pub mod pairing; +pub mod server; diff --git a/apps/app/src-tauri/src/sync/pairing.rs b/apps/app/src-tauri/src/sync/pairing.rs new file mode 100644 index 00000000..eee9c29f --- /dev/null +++ b/apps/app/src-tauri/src/sync/pairing.rs @@ -0,0 +1,20 @@ +use rand::Rng; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PairRequest { + pub peer_id: String, + pub pin: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PairResponse { + pub success: bool, + pub message: String, +} + +pub async fn generate_pin() -> String { + let mut rng = rand::rng(); + let pin: u32 = rng.random_range(100_000..=999_999); + pin.to_string() +} diff --git a/apps/app/src-tauri/src/sync/server.rs b/apps/app/src-tauri/src/sync/server.rs new file mode 100644 index 00000000..223c7d0a --- /dev/null +++ b/apps/app/src-tauri/src/sync/server.rs @@ -0,0 +1,160 @@ +use axum::{ + extract::{State, Json}, + routing::{get, post}, + Router, +}; +use std::sync::Arc; +use crate::sync::discovery::SyncState; +use tauri::Emitter; +use crate::sync::pairing::{PairRequest, PairResponse}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct PeerStatus { + pub id: String, + pub name: String, +} + +#[derive(Serialize, Deserialize)] +pub struct SyncRequest { + pub peer_id: String, + pub file_path: String, + pub content: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct SyncResponse { + pub success: bool, +} + +pub async fn start_server(state: Arc, port: u16, app_handle: tauri::AppHandle) -> Result<(), String> { + let server_state = ServerState { + sync: state.clone(), + app: app_handle, + }; + + let app = Router::new() + .route("/status", get(status_handler)) + .route("/pair", post(pair_handler)) + .route("/sync", post(sync_handler)) + .with_state(server_state); + + let addr = format!("0.0.0.0:{}", port); + let listener = tokio::net::TcpListener::bind(&addr) + .await + .map_err(|e| e.to_string())?; + + println!("Sync server listening on {}", addr); + + let mut rx = state.server_shutdown_tx.as_ref().unwrap().subscribe(); + + tokio::spawn(async move { + let server = axum::serve(listener, app).with_graceful_shutdown(async move { + let _ = rx.recv().await; + println!("Sync server gracefully shutting down"); + }); + + if let Err(e) = server.await { + eprintln!("Server error: {}", e); + } + }); + + Ok(()) +} + +// Due to the wrapper struct, we need to adjust handlers signature to extract it. +// To avoid redefining struct visibility issues, we'll inline it at the module level. +#[derive(Clone)] +struct ServerState { + sync: Arc, + app: tauri::AppHandle, +} + +async fn status_handler(State(state): State) -> Json { + Json(PeerStatus { + id: state.sync.my_id.clone(), + name: state.sync.my_name.clone(), + }) +} + +async fn pair_handler( + State(state): State, + Json(payload): Json, +) -> Json { + let active_pin = state.sync.active_pin.read().await; + + if let Some(pin) = active_pin.as_ref() { + if pin == &payload.pin { + let mut peers = state.sync.peers.write().await; + if let Some(peer) = peers.get_mut(&payload.peer_id) { + peer.is_paired = true; + } else { + let stub_peer = crate::sync::discovery::PeerInfo { + id: payload.peer_id.clone(), + name: "Unknown Peer".to_string(), + ip: "0.0.0.0".to_string(), + port: 8080, + is_paired: true, + }; + peers.insert(payload.peer_id.clone(), stub_peer); + } + + // Drop lock and save asynchronously + drop(peers); + state.sync.save_peers().await; + + return Json(PairResponse { + success: true, + message: "Successfully paired".to_string(), + }); + } + } + + Json(PairResponse { + success: false, + message: "Invalid PIN or pairing not active".to_string(), + }) +} + +async fn sync_handler( + State(state): State, + Json(payload): Json, +) -> Json { + let peers = state.sync.peers.read().await; + + // Authenticate: Ensure the peer exists and is paired + if let Some(peer) = peers.get(&payload.peer_id) { + if peer.is_paired { + let relative_path_str = payload.file_path.clone(); + // Ensure any incoming Unix paths are correctly converted to local platform paths (e.g. Windows) + let relative_path = std::path::PathBuf::from(relative_path_str.replace('/', std::path::MAIN_SEPARATOR_STR)); + + let mut crdt_manager = state.sync.crdt_manager.write().await; + + // Wait for file creation / tracking initialization before applying merge + // otherwise the new file merge has no context. + if !crdt_manager.documents.contains_key(&relative_path) { + if let Err(e) = crdt_manager.load_or_create_doc(&relative_path).await { + eprintln!("Failed to init tracking for new sync file: {}", e); + } + } + + match crdt_manager.apply_sync_data(&relative_path, &payload.content).await { + Ok(_) => { + println!("Successfully merged sync data from {} for {:?}", peer.name, relative_path); + + // Emit event so the frontend knows to reload this file + let _ = state.app.emit("sync-file-updated", relative_path_str); + + return Json(SyncResponse { success: true }); + } + Err(e) => { + eprintln!("Failed to merge sync data: {}", e); + return Json(SyncResponse { success: false }); + } + } + } + } + + Json(SyncResponse { success: false }) +} diff --git a/apps/app/src-tauri/src/sync/test_crdt.rs b/apps/app/src-tauri/src/sync/test_crdt.rs new file mode 100644 index 00000000..758010be --- /dev/null +++ b/apps/app/src-tauri/src/sync/test_crdt.rs @@ -0,0 +1,24 @@ +use automerge::{Automerge, ObjType, ReadDoc, transaction::Transactable, ROOT}; + +fn main() { + let mut doc1 = Automerge::new(); + let mut tx1 = doc1.transaction(); + let text_id = tx1.put_object(ROOT, "content", ObjType::Text).unwrap(); + tx1.splice_text(&text_id, 0, 0, "Hello").unwrap(); + tx1.commit(); + + let mut doc2 = doc1.clone(); + + let mut tx1 = doc1.transaction(); + let len1 = tx1.length(&text_id); + tx1.splice_text(&text_id, 0, len1 as isize, "Hello World").unwrap(); + tx1.commit(); + + let mut tx2 = doc2.transaction(); + let len2 = tx2.length(&text_id); + tx2.splice_text(&text_id, 0, len2 as isize, "Hello there").unwrap(); + tx2.commit(); + + doc1.merge(&mut doc2).unwrap(); + println!("Merged: {}", doc1.text(&text_id).unwrap()); +} diff --git a/apps/app/src/components/general/root_folder_selector/index.svelte b/apps/app/src/components/general/root_folder_selector/index.svelte index 73dd2c51..d3b6b2e4 100644 --- a/apps/app/src/components/general/root_folder_selector/index.svelte +++ b/apps/app/src/components/general/root_folder_selector/index.svelte @@ -12,10 +12,7 @@ import { onMount } from 'svelte'; import { show_folder_picker } from '@/lib/operations/picker_dialog'; import { update_workspace } from '@/lib/operations/workspace'; - import { - current_platform, - current_platform_type, - } from '@/lib/states/'; + import { current_platform, current_platform_type } from '@/lib/states/'; import { RecentWorkspaces } from '@/lib/states'; onMount(async () => { diff --git a/apps/app/src/components/main_section/index.svelte b/apps/app/src/components/main_section/index.svelte index 42b69b8c..b9f37fc8 100644 --- a/apps/app/src/components/main_section/index.svelte +++ b/apps/app/src/components/main_section/index.svelte @@ -6,7 +6,7 @@ import BreadCrumb from '@/components/general/breadcrumb_path/index.svelte'; import PaneMenu from '@/components/main_section/pane_menu/index.svelte'; import { opened_filenode } from '@/lib/states'; - import { current_platform_type } from '@/lib/states/'; + import { current_platform_type } from '@/lib/states/domain_specific/os.svelte'; diff --git a/apps/app/src/components/main_section/text_editor/index.svelte b/apps/app/src/components/main_section/text_editor/index.svelte index 11bce8f9..c6d64c01 100644 --- a/apps/app/src/components/main_section/text_editor/index.svelte +++ b/apps/app/src/components/main_section/text_editor/index.svelte @@ -3,12 +3,14 @@ import { workspace_root_path } from '@/lib/states'; import type { Node } from '@/lib/types'; import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs'; + import { invoke } from '@tauri-apps/api/core'; import { toast } from 'svelte-sonner'; import Editor from './editor/index.svelte'; import MobileToolbar from './editor_toolbar_mobile/index.svelte'; import { editor_view } from './editor_state.svelte'; import { focused_subtree } from '@/components/sidebar_section/file_manager/states.svelte'; - import { current_platform_type } from '@/lib/states/'; + import { current_platform_type } from '@/lib/states/domain_specific/os.svelte'; + import { listen } from '@tauri-apps/api/event'; let { filenode = $bindable(), @@ -20,7 +22,7 @@ let is_file_named_changed: boolean = $state(false); let mobile_toolbar_visible: boolean = $state(false); - $effect(() => { + function load_file_content() { if (!filenode) return; readTextFile(filenode.path) .then((res) => { @@ -33,6 +35,46 @@ text_content = undefined; current_file_name = undefined; }); + } + + $effect(() => { + load_file_content(); + }); + + $effect(() => { + const unlisten = listen('sync-file-updated', (event) => { + const updated_relative_path = event.payload as string; + // The incoming event payload is a relative path. + // For a strict match, we'd compare base_dir + relative_path to filenode.path. + // As a simple MVP, check if the active file path ends with the updated path. + if (filenode && filenode.path.endsWith(updated_relative_path)) { + // Reload contents from disk. This updates text_content, + // but the codemirror editor needs to be informed. + // To force Svelte to recreate the Editor component or re-eval state, + // we briefly clear the content. + readTextFile(filenode.path) + .then((res) => { + // Update state if changed + if (text_content !== res) { + text_content = res; + if (editor_view.data) { + editor_view.data.dispatch({ + changes: { + from: 0, + to: editor_view.data.state.doc.length, + insert: res || '\n', + }, + }); + } + } + }) + .catch(console.error); + } + }); + + return () => { + unlisten.then((f) => f()); + }; }); @@ -83,6 +125,11 @@ write_to_file={(content) => { if (!filenode) return; writeTextFile(filenode?.path, content); + + // Trigger sync if enabled + invoke('sync_file', { filePath: filenode?.path }).catch((e) => { + console.warn('Failed to trigger sync: ', e); + }); }} on_focus_in={() => { mobile_toolbar_visible = true; diff --git a/apps/app/src/components/main_section/titlebar/index.svelte b/apps/app/src/components/main_section/titlebar/index.svelte index a74cac89..315be08b 100644 --- a/apps/app/src/components/main_section/titlebar/index.svelte +++ b/apps/app/src/components/main_section/titlebar/index.svelte @@ -2,7 +2,7 @@ import { getCurrentWindow } from '@tauri-apps/api/window'; import PaneMenu from '@/components/main_section/pane_menu/index.svelte'; import { opened_filenode } from '@/lib/states'; - import { current_platform_type } from '@/lib/states/'; + import { current_platform_type } from '@/lib/states/domain_specific/os.svelte'; const appWindow = getCurrentWindow(); diff --git a/apps/app/src/components/sidebar_section/bottom_sidebar.svelte b/apps/app/src/components/sidebar_section/bottom_sidebar.svelte index b604187e..306e48d6 100644 --- a/apps/app/src/components/sidebar_section/bottom_sidebar.svelte +++ b/apps/app/src/components/sidebar_section/bottom_sidebar.svelte @@ -20,5 +20,27 @@ .pop()} +
  • + +
  • + + + + diff --git a/apps/app/src/lib/components/sync/SyncSettings.svelte b/apps/app/src/lib/components/sync/SyncSettings.svelte new file mode 100644 index 00000000..03cab56c --- /dev/null +++ b/apps/app/src/lib/components/sync/SyncSettings.svelte @@ -0,0 +1,157 @@ + + +
    +

    Local Network Sync

    + +
    +
    +

    Enable Sync

    +

    Sync notes with devices on the same Wi-Fi network

    +
    + { + e.preventDefault(); + toggle_sync(); + }} /> +
    + + {#if is_sync_enabled} +
    +

    Pair a New Device

    +

    Generate a PIN to let another device connect to this one.

    + {#if pairing_pin} +
    + {pairing_pin} +
    + {:else} + + {/if} +
    + +
    +

    Discovered Devices

    + {#if discovered_peers.length === 0} +

    Scanning network for devices...

    + {:else} +
      + {#each discovered_peers as peer} +
    • +
      + {peer.name} + {#if peer.is_paired} +
      + Paired + +
      + {:else} +
      + + +
      + {/if} +
      +
      + { + const newName = e.currentTarget.value; + if (newName) { + await invoke('rename_peer', { peerId: peer.id, newName }); + refresh_peers(); + } + }} + /> +
      +
    • + {/each} +
    + {/if} +
    + {/if} +
    diff --git a/apps/app/src/lib/operations/file_tree/operations.ts b/apps/app/src/lib/operations/file_tree/operations.ts index 7b5f9bc6..10709f6b 100644 --- a/apps/app/src/lib/operations/file_tree/operations.ts +++ b/apps/app/src/lib/operations/file_tree/operations.ts @@ -168,6 +168,12 @@ export async function add_new_note( sort_nodes(subtree); const node = subtree.find((n) => n.path === new_file_path); if (!node) throw new Error('Failed to find the newly created note node.'); + + // Attempt to trigger initial sync for the newly created file so peers get it immediately. + invoke('sync_file', { filePath: node.path }).catch(e => { + console.warn("Failed to trigger sync for new file: ", e); + }); + return node; } catch (e) { console.error(e); diff --git a/apps/app/src/lib/operations/window_listeners/index.ts b/apps/app/src/lib/operations/window_listeners/index.ts index dbd2350b..714bb599 100644 --- a/apps/app/src/lib/operations/window_listeners/index.ts +++ b/apps/app/src/lib/operations/window_listeners/index.ts @@ -6,6 +6,7 @@ import { is_contents_changed, } from '@/components/main_section/text_editor/editor_state.svelte'; import { touch_recent_workspaces } from '../workspace'; +import { invoke } from '@tauri-apps/api/core'; export async function attach_window_listeners() { const unlistenFocus = await getCurrentWindow().onFocusChanged( @@ -31,5 +32,19 @@ async function on_window_blur() { opened_filenode.data.path, editor_view.data.state.doc.toString() ); + + // Also explicitly trigger sync so if offline edits happened, they get pushed. + try { + await invoke('sync_file', { filePath: opened_filenode.data.path }); + } catch (e) { + console.warn("Failed to trigger sync: ", e); + } } } + +// Ensure the sync engine is aware we just focused back to possibly push changes +getCurrentWindow().onFocusChanged(({ payload: focused }) => { + if (focused && opened_filenode.data) { + invoke('sync_file', { filePath: opened_filenode.data.path }).catch(console.warn); + } +});