Build the Synapsis Tauri onboarding and authentication shell with the official wordmark and a fluid stable-topology signal mesh
Hop-State: A_06FPFZF1AB46ARJ4HD8ZQH8 Hop-Proposal: R_06FPFZEBFD47ZTA2XKJ2Q0G Hop-Task: T_06FPFXR242APZTT77HV934R Hop-Attempt: AT_06FPFXR241F9H1V2J0RCGGG
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,37 @@
|
||||
# Synapsis Desktop
|
||||
|
||||
A Tauri desktop client for the Synapsis federated social network. The first-run experience follows the same node-first model as the native iOS app:
|
||||
|
||||
1. Choose and verify a Synapsis node.
|
||||
2. Sign in with an existing account or create a new account on that node.
|
||||
3. Enter the signed-in desktop shell.
|
||||
|
||||
Node and authentication requests run in Rust so independent nodes do not need to opt into browser CORS. Session cookies are held by the native HTTP client and are not exposed to the React webview.
|
||||
|
||||
## Develop
|
||||
|
||||
Requirements: Bun, Rust, and the platform prerequisites for Tauri 2.
|
||||
|
||||
```sh
|
||||
bun install
|
||||
bun run tauri dev
|
||||
```
|
||||
|
||||
The standalone frontend preview is available with `bun run dev`, but node connection is intentionally disabled outside Tauri because the native command layer owns networking.
|
||||
|
||||
## Validate
|
||||
|
||||
```sh
|
||||
bun run build
|
||||
cargo test --manifest-path src-tauri/Cargo.toml
|
||||
```
|
||||
|
||||
## Current scope
|
||||
|
||||
- Animated signal-field launch and onboarding visuals.
|
||||
- Node URL normalization and `/api/node` verification.
|
||||
- Existing-session detection while the app is running.
|
||||
- Cookie-based sign in, account creation, and sign out.
|
||||
- A desktop navigation shell ready for the feed and other iOS features.
|
||||
|
||||
The node address persists between launches. Authentication cookies currently live for the duration of the application process; durable, encrypted session restoration is a later security-focused addition.
|
||||
@@ -0,0 +1,285 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "synapsis-desktop",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.0.4",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
|
||||
|
||||
"@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="],
|
||||
|
||||
"@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
|
||||
|
||||
"@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
|
||||
|
||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
|
||||
|
||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
|
||||
|
||||
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.2", "", { "os": "android", "cpu": "arm" }, "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg=="],
|
||||
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.2", "", { "os": "none", "cpu": "arm64" }, "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA=="],
|
||||
|
||||
"@tauri-apps/api": ["@tauri-apps/api@2.11.1", "", {}, "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA=="],
|
||||
|
||||
"@tauri-apps/cli": ["@tauri-apps/cli@2.11.4", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.11.4", "@tauri-apps/cli-darwin-x64": "2.11.4", "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", "@tauri-apps/cli-linux-arm64-musl": "2.11.4", "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", "@tauri-apps/cli-linux-x64-gnu": "2.11.4", "@tauri-apps/cli-linux-x64-musl": "2.11.4", "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", "@tauri-apps/cli-win32-x64-msvc": "2.11.4" }, "bin": { "tauri": "tauri.js" } }, "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ=="],
|
||||
|
||||
"@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.11.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ=="],
|
||||
|
||||
"@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.11.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A=="],
|
||||
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.11.4", "", { "os": "linux", "cpu": "arm" }, "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ=="],
|
||||
|
||||
"@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.11.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA=="],
|
||||
|
||||
"@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.11.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw=="],
|
||||
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.11.4", "", { "os": "linux", "cpu": "none" }, "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ=="],
|
||||
|
||||
"@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.11.4", "", { "os": "linux", "cpu": "x64" }, "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ=="],
|
||||
|
||||
"@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.11.4", "", { "os": "linux", "cpu": "x64" }, "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A=="],
|
||||
|
||||
"@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.11.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ=="],
|
||||
|
||||
"@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.11.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA=="],
|
||||
|
||||
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.11.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw=="],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||
|
||||
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
|
||||
|
||||
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.43", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.6", "", { "dependencies": { "baseline-browser-mapping": "^2.10.42", "caniuse-lite": "^1.0.30001803", "electron-to-chromium": "^1.5.389", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001805", "", {}, "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.392", "", {}, "sha512-1yQq3VQCZRwsnYc67Oc+1fge6Lwtn0hzi6zmEVkB61Zx21kTbwJAW4dFLadl5Rc1tKhG/kSpYXnfiAhu0f0a1g=="],
|
||||
|
||||
"esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.51", "", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
|
||||
|
||||
"postcss": ["postcss@8.5.19", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ=="],
|
||||
|
||||
"react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="],
|
||||
|
||||
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
||||
|
||||
"rollup": ["rollup@4.62.2", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||
|
||||
"vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="],
|
||||
|
||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#070808" />
|
||||
<title>Synapsis</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "synapsis-desktop",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"@tauri-apps/api": "^2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.0.4",
|
||||
"@tauri-apps/cli": "^2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 4532 847" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g id="logo.png" transform="matrix(1,0,0,1,64.677826,46)">
|
||||
<path d="M304.019,263.551C305.652,271.447 303.736,283.853 305.372,289.534C309.02,302.195 300.103,295.787 299.567,295.402C295.892,292.761 179.642,227.694 173.743,224.084C172.18,223.128 154.443,213.343 150.751,211.07C140.744,204.906 140.277,205.902 130.411,199.662C126.035,196.894 124.425,196.538 118.768,193.053C118.171,192.685 75.592,168.79 67.72,164.118C50.371,153.821 40.078,151.747 48.297,148.155C51.477,146.766 58.668,141.301 72.373,134.267C73.58,133.647 82.469,127.438 96.37,120.261C98.188,119.322 104.017,114.775 120.378,106.272C121.461,105.709 121.004,105.086 133.418,98.339C146.666,91.138 146.145,90.391 163.368,81.274C165.176,80.317 164.94,79.941 185.4,68.304C206.053,56.556 205.664,55.998 226.403,44.323C242.236,35.409 253.536,28.4 255.86,26.958C258.106,25.565 258.573,26.419 284.583,10.654C294.55,4.612 295.136,4.631 295.44,5.542C296.789,9.575 294.774,9.868 296.624,15.486C297.168,17.14 295.72,46.107 297.419,50.524C298.475,53.27 296.609,76.802 298.486,81.504C299.397,83.786 297.688,108.545 299.404,113.529C300.21,115.871 298.88,136.601 300.425,141.522C302.579,148.383 298.499,148.681 301.636,170.486C302.136,173.958 300.811,195.308 302.477,199.507C303.468,202.002 301.591,224.981 303.43,229.522C304.452,232.045 302.979,232.185 304.019,263.551Z" style="fill:rgb(254,254,254);fill-opacity:0.99;"/>
|
||||
<path d="M431.157,290.099C430.505,290.683 417.455,299.199 416.24,300.073C410.454,304.238 410.117,301.928 410.1,301.911C409.223,300.98 409.627,300.735 409.292,247.501C409.273,244.423 408.806,244.548 408.333,241.515C407.885,238.638 409.246,142.669 407.611,138.467C406.701,136.128 408.338,54.956 406.598,50.47C405.316,47.163 407.533,31.627 406.398,25.509C405.516,20.757 406.222,5.206 406.302,3.442C406.392,1.46 408.021,2.026 428.641,14.229C437.763,19.627 541.782,80.104 545.29,81.907C547.301,82.94 547.09,83.235 569.633,96.254C576.025,99.946 647.235,141.072 649.401,142.638C651.816,144.384 656.029,145.526 650.383,149.314C646.012,152.246 645.797,151.776 595.12,185.025C594.19,185.635 594.165,185.471 593.258,186.112C590.743,187.889 573.995,198.033 572.47,199.464C571.467,200.405 509.123,240.164 503.602,243.685C497.799,247.386 432.243,289.194 431.157,290.099Z" style="fill:rgb(254,254,254);fill-opacity:0.99;"/>
|
||||
<path d="M33.196,269.863C35.094,271.32 53.244,280.215 58.604,284.36C59.591,285.123 60.061,284.284 72.29,291.86C72.708,292.12 78.887,295.201 86.291,299.853C89.193,301.676 94.674,304.083 102.284,308.859C110.072,313.747 110.497,312.868 118.277,317.868C119.996,318.974 120.342,318.299 140.627,330.255C145.794,333.3 189.693,358.096 191.285,358.909C195.409,361.014 215.67,373.434 230.262,380.952C231.907,381.799 248.026,390.104 250.133,392.776C252.561,395.854 247.834,394.016 208.151,418.039C206.78,418.869 206.719,418.711 190.606,427.709C174.878,436.491 170.409,438.814 168.63,439.738C167.644,440.251 167.798,440.488 156.591,446.686C61.751,499.128 62.031,499.508 53.615,503.733C52.269,504.409 33.347,515.25 22.665,520.796C19.384,522.499 6.684,530.214 5.305,531.051C-1.012,534.889 4.833,512.016 2.562,505.484C2.153,504.307 2.353,504.259 2.304,317.5C2.292,275.117 3.173,275.182 3.17,271.501C3.159,258.312 -0.553,250 7.247,254.933C10.234,256.821 32.436,269.163 33.196,269.863Z" style="fill:rgb(254,254,254);fill-opacity:0.99;"/>
|
||||
<path d="M615.834,474.047C613.965,472.666 613.886,472.955 611.739,472.048C608.124,470.522 608.854,469.422 605.317,467.875C600.769,465.886 600.806,465.953 596.573,463.364C591.476,460.246 557.039,441.084 556.4,440.685C544.299,433.139 532.485,426.863 530.364,425.736C511.75,415.847 474.229,393.735 472.224,392.965C465.615,390.425 471.159,388.345 478.594,383.673C480.359,382.564 500.048,370.192 500.746,369.958C502.006,369.536 657.092,274.386 662.366,271.253C679.367,261.152 700.878,246.866 703.462,246.854C704.487,246.849 704.766,247.506 704.745,298.5C704.669,486.271 704.865,486.247 704.454,487.489C702.634,492.996 704.799,514.494 704.661,521.532C704.599,524.695 700.566,522.09 696.409,519.669C677.735,508.793 660.007,499.345 615.834,474.047Z" style="fill:rgb(254,254,254);fill-opacity:0.99;"/>
|
||||
<path d="M304.007,677.487C303.299,704.156 305.311,704.237 302.808,715.521C301.909,719.575 304.513,755.179 300.568,755.816C299,756.069 223.586,711.063 217.58,708.352C215.199,707.277 202.655,699.526 201.372,698.733C190.449,691.984 176.482,684.739 172.388,681.657C170.844,680.495 170.424,681.416 151.379,669.722C141.349,663.562 70.832,623.138 70.589,622.456C70.344,621.765 69.857,620.391 77.43,616.361C87.379,611.065 112.843,595.213 122.372,590.262C123.516,589.668 123.193,589.134 136.343,582.226C137.425,581.658 137.047,581.069 149.385,574.278C152.149,572.756 151.869,572.356 183.378,554.281C185.591,553.011 206.973,540.745 210.741,537.896C211.596,537.25 211.701,537.437 222.393,531.297C224.61,530.023 240.546,520.87 249.874,514.976C258.239,509.689 277.147,499.627 307.685,480.9C311.561,478.524 309.72,487.101 309.682,493.501C309.614,505.156 310.12,505.167 309.175,510.411C308.706,513.019 309.341,513.039 307.903,543.51C307.181,558.821 308.884,573.611 306.792,582.517C306.468,583.899 307.061,615.423 306.469,617.491C305.179,621.995 305.598,659.749 304.007,677.487Z" style="fill:rgb(254,254,254);fill-opacity:0.99;"/>
|
||||
<path d="M412.978,728.547C413.291,713.672 413.478,712.999 412.374,707.511C412.343,707.358 412.064,697.943 412.575,696.522C414.366,691.542 412.539,539.146 413.605,536.529C414.689,533.87 414.367,524.717 414.35,488.489C414.347,482.676 427.213,492.857 433.657,496.203C455.256,507.42 454.781,508.223 456.662,509.188C464.859,513.39 464.477,513.967 472.648,518.216C484.746,524.505 619.364,601.245 628.276,605.916C634.395,609.122 639.867,612.68 644.238,614.942C649.789,617.815 635.639,622.259 632.602,624.641C630.054,626.639 444.729,734.101 441.668,735.806C427.184,743.876 427.558,744.422 426.277,745.072C414.002,751.297 412.422,754.809 412.303,751.568C412.262,750.443 411.909,740.751 412.413,737.493C413.104,733.03 412.057,733.002 412.978,728.547Z" style="fill:rgb(254,254,254);fill-opacity:0.99;"/>
|
||||
</g>
|
||||
<g transform="matrix(1.383006,0,0,1.383006,-551.567585,-462.336524)">
|
||||
<g transform="matrix(737.5,0,0,737.5,1085,854)">
|
||||
<path d="M0.185,0.007C0.145,0.007 0.113,-0.006 0.088,-0.033C0.062,-0.06 0.05,-0.094 0.05,-0.137L0.05,-0.152C0.05,-0.159 0.053,-0.162 0.06,-0.162L0.077,-0.162C0.084,-0.162 0.087,-0.159 0.087,-0.152L0.087,-0.141C0.087,-0.107 0.096,-0.08 0.115,-0.059C0.133,-0.037 0.156,-0.027 0.184,-0.027C0.213,-0.027 0.236,-0.037 0.255,-0.057C0.274,-0.077 0.283,-0.103 0.283,-0.136C0.283,-0.157 0.278,-0.175 0.269,-0.191C0.259,-0.207 0.247,-0.22 0.234,-0.231C0.221,-0.242 0.197,-0.259 0.164,-0.284C0.137,-0.304 0.115,-0.321 0.1,-0.335C0.085,-0.349 0.072,-0.365 0.063,-0.384C0.054,-0.402 0.049,-0.423 0.049,-0.448C0.049,-0.49 0.061,-0.523 0.086,-0.547C0.11,-0.571 0.141,-0.583 0.18,-0.583C0.221,-0.583 0.254,-0.57 0.279,-0.543C0.304,-0.516 0.316,-0.481 0.316,-0.438L0.316,-0.42C0.316,-0.413 0.313,-0.41 0.306,-0.41L0.287,-0.41C0.28,-0.41 0.277,-0.413 0.277,-0.42L0.277,-0.436C0.277,-0.469 0.268,-0.496 0.25,-0.517C0.232,-0.538 0.209,-0.549 0.18,-0.549C0.153,-0.549 0.131,-0.54 0.114,-0.522C0.097,-0.504 0.088,-0.479 0.088,-0.446C0.088,-0.419 0.095,-0.397 0.111,-0.379C0.126,-0.361 0.153,-0.338 0.193,-0.31C0.227,-0.286 0.252,-0.267 0.269,-0.252C0.286,-0.237 0.299,-0.221 0.308,-0.204C0.317,-0.186 0.322,-0.165 0.322,-0.14C0.322,-0.096 0.309,-0.06 0.284,-0.034C0.258,-0.007 0.225,0.007 0.185,0.007Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(737.5,0,0,737.5,1073.2,854)">
|
||||
</g>
|
||||
<g transform="matrix(737.5,0,0,737.5,1420.5625,854)">
|
||||
<path d="M0.167,-0C0.16,-0 0.157,-0.003 0.157,-0.01L0.157,-0.265C0.157,-0.268 0.157,-0.269 0.156,-0.27L0.038,-0.564L0.037,-0.568C0.037,-0.573 0.04,-0.576 0.046,-0.576L0.066,-0.576C0.071,-0.576 0.075,-0.573 0.077,-0.568L0.174,-0.316C0.175,-0.314 0.176,-0.313 0.177,-0.313C0.178,-0.313 0.179,-0.314 0.18,-0.316L0.276,-0.568C0.278,-0.573 0.282,-0.576 0.287,-0.576L0.307,-0.576C0.31,-0.576 0.313,-0.575 0.314,-0.573C0.315,-0.57 0.315,-0.567 0.314,-0.564L0.197,-0.27C0.196,-0.269 0.196,-0.268 0.196,-0.265L0.196,-0.01C0.196,-0.003 0.193,-0 0.186,-0L0.167,-0Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(737.5,0,0,737.5,1401.3875,854)">
|
||||
</g>
|
||||
<g transform="matrix(737.5,0,0,737.5,1740.6375,854)">
|
||||
<path d="M0.31,-0.565C0.31,-0.572 0.313,-0.575 0.32,-0.575L0.339,-0.575C0.346,-0.575 0.349,-0.572 0.349,-0.565L0.349,-0.01C0.349,-0.003 0.346,-0 0.339,-0L0.318,-0C0.313,-0 0.309,-0.003 0.307,-0.008L0.114,-0.474C0.113,-0.475 0.112,-0.476 0.111,-0.476C0.11,-0.476 0.109,-0.475 0.109,-0.473L0.109,-0.01C0.109,-0.003 0.106,-0 0.099,-0L0.08,-0C0.073,-0 0.07,-0.003 0.07,-0.01L0.07,-0.565C0.07,-0.572 0.073,-0.575 0.08,-0.575L0.101,-0.575C0.106,-0.575 0.11,-0.572 0.112,-0.567L0.306,-0.099C0.307,-0.098 0.308,-0.097 0.309,-0.097C0.31,-0.097 0.311,-0.098 0.311,-0.1L0.31,-0.565Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(737.5,0,0,737.5,1740.6375,854)">
|
||||
</g>
|
||||
<g transform="matrix(737.5,0,0,737.5,2129.3,854)">
|
||||
<path d="M0.316,-0C0.31,-0 0.306,-0.003 0.305,-0.009L0.281,-0.116C0.281,-0.117 0.281,-0.118 0.28,-0.119C0.279,-0.12 0.277,-0.12 0.276,-0.12L0.103,-0.12C0.102,-0.12 0.1,-0.12 0.1,-0.119C0.098,-0.118 0.098,-0.117 0.098,-0.116L0.074,-0.009C0.073,-0.003 0.07,-0 0.063,-0L0.044,-0C0.041,-0 0.038,-0.001 0.036,-0.003C0.035,-0.005 0.035,-0.008 0.036,-0.011L0.166,-0.567C0.167,-0.573 0.171,-0.576 0.177,-0.576L0.201,-0.576C0.207,-0.576 0.211,-0.573 0.212,-0.567L0.345,-0.011L0.346,-0.007C0.346,-0.002 0.343,-0 0.337,-0L0.316,-0ZM0.108,-0.157C0.107,-0.156 0.107,-0.155 0.108,-0.154C0.11,-0.153 0.111,-0.153 0.112,-0.153L0.268,-0.153C0.269,-0.153 0.271,-0.153 0.272,-0.154C0.273,-0.155 0.273,-0.156 0.272,-0.157L0.192,-0.508C0.191,-0.51 0.191,-0.511 0.19,-0.511C0.189,-0.511 0.189,-0.51 0.188,-0.508L0.108,-0.157Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(737.5,0,0,737.5,2129.3,854)">
|
||||
</g>
|
||||
<g transform="matrix(737.5,0,0,737.5,2489.2,854)">
|
||||
<path d="M0.204,-0.578C0.243,-0.578 0.275,-0.564 0.3,-0.535C0.325,-0.506 0.337,-0.469 0.337,-0.422C0.337,-0.377 0.325,-0.34 0.3,-0.312C0.276,-0.284 0.244,-0.27 0.205,-0.27L0.108,-0.27C0.105,-0.27 0.104,-0.269 0.104,-0.266L0.104,-0.01C0.104,-0.003 0.101,-0 0.094,-0L0.075,-0C0.068,-0 0.065,-0.003 0.065,-0.01L0.065,-0.568C0.065,-0.575 0.068,-0.578 0.075,-0.578L0.204,-0.578ZM0.2,-0.303C0.229,-0.303 0.253,-0.314 0.271,-0.336C0.289,-0.357 0.298,-0.386 0.298,-0.422C0.298,-0.459 0.289,-0.488 0.271,-0.51C0.253,-0.532 0.229,-0.543 0.2,-0.543L0.108,-0.543C0.105,-0.543 0.104,-0.542 0.104,-0.539L0.104,-0.307C0.104,-0.304 0.105,-0.303 0.108,-0.303L0.2,-0.303Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(737.5,0,0,737.5,2489.2,854)">
|
||||
</g>
|
||||
<g transform="matrix(737.5,0,0,737.5,2840.9875,854)">
|
||||
<path d="M0.185,0.007C0.145,0.007 0.113,-0.006 0.088,-0.033C0.062,-0.06 0.05,-0.094 0.05,-0.137L0.05,-0.152C0.05,-0.159 0.053,-0.162 0.06,-0.162L0.077,-0.162C0.084,-0.162 0.087,-0.159 0.087,-0.152L0.087,-0.141C0.087,-0.107 0.096,-0.08 0.115,-0.059C0.133,-0.037 0.156,-0.027 0.184,-0.027C0.213,-0.027 0.236,-0.037 0.255,-0.057C0.274,-0.077 0.283,-0.103 0.283,-0.136C0.283,-0.157 0.278,-0.175 0.269,-0.191C0.259,-0.207 0.247,-0.22 0.234,-0.231C0.221,-0.242 0.197,-0.259 0.164,-0.284C0.137,-0.304 0.115,-0.321 0.1,-0.335C0.085,-0.349 0.072,-0.365 0.063,-0.384C0.054,-0.402 0.049,-0.423 0.049,-0.448C0.049,-0.49 0.061,-0.523 0.086,-0.547C0.11,-0.571 0.141,-0.583 0.18,-0.583C0.221,-0.583 0.254,-0.57 0.279,-0.543C0.304,-0.516 0.316,-0.481 0.316,-0.438L0.316,-0.42C0.316,-0.413 0.313,-0.41 0.306,-0.41L0.287,-0.41C0.28,-0.41 0.277,-0.413 0.277,-0.42L0.277,-0.436C0.277,-0.469 0.268,-0.496 0.25,-0.517C0.232,-0.538 0.209,-0.549 0.18,-0.549C0.153,-0.549 0.131,-0.54 0.114,-0.522C0.097,-0.504 0.088,-0.479 0.088,-0.446C0.088,-0.419 0.095,-0.397 0.111,-0.379C0.126,-0.361 0.153,-0.338 0.193,-0.31C0.227,-0.286 0.252,-0.267 0.269,-0.252C0.286,-0.237 0.299,-0.221 0.308,-0.204C0.317,-0.186 0.322,-0.165 0.322,-0.14C0.322,-0.096 0.309,-0.06 0.284,-0.034C0.258,-0.007 0.225,0.007 0.185,0.007Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(737.5,0,0,737.5,2840.9875,854)">
|
||||
</g>
|
||||
<g transform="matrix(737.5,0,0,737.5,3188.35,854)">
|
||||
<path d="M0.08,-0C0.073,-0 0.07,-0.003 0.07,-0.01L0.07,-0.565C0.07,-0.572 0.073,-0.575 0.08,-0.575L0.099,-0.575C0.106,-0.575 0.109,-0.572 0.109,-0.565L0.109,-0.01C0.109,-0.003 0.106,-0 0.099,-0L0.08,-0Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(737.5,0,0,737.5,3188.35,854)">
|
||||
</g>
|
||||
<g transform="matrix(737.5,0,0,737.5,3400.0125,854)">
|
||||
<path d="M0.185,0.007C0.145,0.007 0.113,-0.006 0.088,-0.033C0.062,-0.06 0.05,-0.094 0.05,-0.137L0.05,-0.152C0.05,-0.159 0.053,-0.162 0.06,-0.162L0.077,-0.162C0.084,-0.162 0.087,-0.159 0.087,-0.152L0.087,-0.141C0.087,-0.107 0.096,-0.08 0.115,-0.059C0.133,-0.037 0.156,-0.027 0.184,-0.027C0.213,-0.027 0.236,-0.037 0.255,-0.057C0.274,-0.077 0.283,-0.103 0.283,-0.136C0.283,-0.157 0.278,-0.175 0.269,-0.191C0.259,-0.207 0.247,-0.22 0.234,-0.231C0.221,-0.242 0.197,-0.259 0.164,-0.284C0.137,-0.304 0.115,-0.321 0.1,-0.335C0.085,-0.349 0.072,-0.365 0.063,-0.384C0.054,-0.402 0.049,-0.423 0.049,-0.448C0.049,-0.49 0.061,-0.523 0.086,-0.547C0.11,-0.571 0.141,-0.583 0.18,-0.583C0.221,-0.583 0.254,-0.57 0.279,-0.543C0.304,-0.516 0.316,-0.481 0.316,-0.438L0.316,-0.42C0.316,-0.413 0.313,-0.41 0.306,-0.41L0.287,-0.41C0.28,-0.41 0.277,-0.413 0.277,-0.42L0.277,-0.436C0.277,-0.469 0.268,-0.496 0.25,-0.517C0.232,-0.538 0.209,-0.549 0.18,-0.549C0.153,-0.549 0.131,-0.54 0.114,-0.522C0.097,-0.504 0.088,-0.479 0.088,-0.446C0.088,-0.419 0.095,-0.397 0.111,-0.379C0.126,-0.361 0.153,-0.338 0.193,-0.31C0.227,-0.286 0.252,-0.267 0.269,-0.252C0.286,-0.237 0.299,-0.221 0.308,-0.204C0.317,-0.186 0.322,-0.165 0.322,-0.14C0.322,-0.096 0.309,-0.06 0.284,-0.034C0.258,-0.007 0.225,0.007 0.185,0.007Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(737.5,0,0,737.5,3400.0125,854)">
|
||||
</g>
|
||||
<g transform="matrix(737.5,0,0,737.5,3747.375,854)">
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,7 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Generated by Tauri
|
||||
# will have schema files for capabilities auto-completion
|
||||
/gen/schemas
|
||||
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "synapsis-desktop"
|
||||
version = "0.1.0"
|
||||
description = "Desktop client for the Synapsis federated social network"
|
||||
authors = ["Gnosys Labs"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
# The `_lib` suffix may seem redundant but it is necessary
|
||||
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||
name = "synapsis_desktop_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
reqwest = { version = "0.12", features = ["cookies", "json", "rustls-tls"] }
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default"
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 990 B |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#fff</color>
|
||||
</resources>
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<rect width="1024" height="1024" rx="224" fill="#080909"/>
|
||||
<g fill="none" stroke="#f7f7f3" stroke-width="28" stroke-linejoin="round">
|
||||
<path d="M512 166 318 278 512 390Z" fill="#f7f7f3"/>
|
||||
<path d="m540 166 194 112-194 112Z"/>
|
||||
<path d="m290 326 194 112-194 112Z"/>
|
||||
<path d="m762 326-194 112 194 112Z" fill="#f7f7f3"/>
|
||||
<path d="m512 486-194 112 194 112Z"/>
|
||||
<path d="m540 486 194 112-194 112Z" fill="#f7f7f3"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 514 B |
|
After Width: | Height: | Size: 675 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 946 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
@@ -0,0 +1,256 @@
|
||||
use reqwest::{Client, Method, StatusCode, Url};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use tauri::State;
|
||||
|
||||
struct AppState {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct NodeInfo {
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
accent_color: Option<String>,
|
||||
domain: Option<String>,
|
||||
logo_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct User {
|
||||
id: String,
|
||||
handle: String,
|
||||
display_name: Option<String>,
|
||||
avatar_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SessionPayload {
|
||||
user: Option<User>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ConnectionPayload {
|
||||
node_url: String,
|
||||
node: NodeInfo,
|
||||
user: Option<User>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AuthPayload {
|
||||
user: User,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ErrorPayload {
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RegistrationRequest<'a> {
|
||||
handle: &'a str,
|
||||
display_name: &'a str,
|
||||
email: &'a str,
|
||||
password: &'a str,
|
||||
}
|
||||
|
||||
fn normalize_node(input: &str) -> Result<Url, String> {
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err("Enter a valid Synapsis node URL.".into());
|
||||
}
|
||||
|
||||
let candidate = if trimmed.contains("://") {
|
||||
trimmed.to_owned()
|
||||
} else {
|
||||
format!("https://{trimmed}")
|
||||
};
|
||||
let mut url = Url::parse(&candidate).map_err(|_| "Enter a valid Synapsis node URL.")?;
|
||||
if url.scheme() != "http" && url.scheme() != "https" {
|
||||
return Err("Synapsis nodes must use an http or https URL.".into());
|
||||
}
|
||||
if url.host_str().is_none() {
|
||||
return Err("Enter a valid Synapsis node URL.".into());
|
||||
}
|
||||
if url.host_str() == Some("www.synapsis.social") {
|
||||
url.set_host(Some("synapsis.social"))
|
||||
.map_err(|_| "Enter a valid Synapsis node URL.")?;
|
||||
}
|
||||
url.set_username("").ok();
|
||||
url.set_password(None).ok();
|
||||
url.set_path("/");
|
||||
url.set_query(None);
|
||||
url.set_fragment(None);
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
fn endpoint(base: &Url, path: &str) -> Result<Url, String> {
|
||||
base.join(path)
|
||||
.map_err(|_| "The selected node address is invalid.".into())
|
||||
}
|
||||
|
||||
async fn decode_response<T: DeserializeOwned>(response: reqwest::Response) -> Result<T, String> {
|
||||
let status = response.status();
|
||||
let bytes = response.bytes().await.map_err(|error| error.to_string())?;
|
||||
if !status.is_success() {
|
||||
let message = serde_json::from_slice::<ErrorPayload>(&bytes)
|
||||
.ok()
|
||||
.and_then(|payload| payload.error)
|
||||
.unwrap_or_else(|| format!("Request failed ({status})."));
|
||||
return Err(message);
|
||||
}
|
||||
serde_json::from_slice(&bytes).map_err(|_| "The node returned an unexpected response.".into())
|
||||
}
|
||||
|
||||
async fn get<T: DeserializeOwned>(client: &Client, base: &Url, path: &str) -> Result<T, String> {
|
||||
let response = client
|
||||
.get(endpoint(base, path)?)
|
||||
.header("Accept", "application/json")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| format!("Couldn’t reach this node: {error}"))?;
|
||||
decode_response(response).await
|
||||
}
|
||||
|
||||
async fn post<T: DeserializeOwned, B: Serialize>(
|
||||
client: &Client,
|
||||
base: &Url,
|
||||
path: &str,
|
||||
body: &B,
|
||||
) -> Result<T, String> {
|
||||
let response = client
|
||||
.request(Method::POST, endpoint(base, path)?)
|
||||
.header("Accept", "application/json")
|
||||
.json(body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| format!("Couldn’t reach this node: {error}"))?;
|
||||
decode_response(response).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn connect(
|
||||
state: State<'_, AppState>,
|
||||
node_url: String,
|
||||
) -> Result<ConnectionPayload, String> {
|
||||
let base = normalize_node(&node_url)?;
|
||||
let node = get::<NodeInfo>(&state.client, &base, "/api/node").await?;
|
||||
let session = get::<SessionPayload>(&state.client, &base, "/api/auth/me")
|
||||
.await
|
||||
.unwrap_or(SessionPayload { user: None });
|
||||
Ok(ConnectionPayload {
|
||||
node_url: base.to_string(),
|
||||
node,
|
||||
user: session.user,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn login(
|
||||
state: State<'_, AppState>,
|
||||
node_url: String,
|
||||
email: String,
|
||||
password: String,
|
||||
) -> Result<AuthPayload, String> {
|
||||
let base = normalize_node(&node_url)?;
|
||||
let payload = post::<SessionPayload, _>(
|
||||
&state.client,
|
||||
&base,
|
||||
"/api/auth/login",
|
||||
&serde_json::json!({ "email": email.trim(), "password": password }),
|
||||
)
|
||||
.await?;
|
||||
payload
|
||||
.user
|
||||
.map(|user| AuthPayload { user })
|
||||
.ok_or_else(|| "The node returned an unexpected response.".into())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn register(
|
||||
state: State<'_, AppState>,
|
||||
node_url: String,
|
||||
handle: String,
|
||||
display_name: String,
|
||||
email: String,
|
||||
password: String,
|
||||
) -> Result<AuthPayload, String> {
|
||||
let base = normalize_node(&node_url)?;
|
||||
let body = RegistrationRequest {
|
||||
handle: handle.trim().trim_start_matches('@'),
|
||||
display_name: display_name.trim(),
|
||||
email: email.trim(),
|
||||
password: &password,
|
||||
};
|
||||
let payload =
|
||||
post::<SessionPayload, _>(&state.client, &base, "/api/auth/register", &body).await?;
|
||||
payload
|
||||
.user
|
||||
.map(|user| AuthPayload { user })
|
||||
.ok_or_else(|| "The node returned an unexpected response.".into())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn logout(state: State<'_, AppState>, node_url: String) -> Result<(), String> {
|
||||
let base = normalize_node(&node_url)?;
|
||||
let response = state
|
||||
.client
|
||||
.post(endpoint(&base, "/api/auth/logout")?)
|
||||
.json(&serde_json::json!({}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| format!("Couldn’t reach this node: {error}"))?;
|
||||
if response.status().is_success() || response.status() == StatusCode::UNAUTHORIZED {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Log out failed ({}).", response.status()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let client = Client::builder()
|
||||
.cookie_store(true)
|
||||
.timeout(Duration::from_secs(20))
|
||||
.user_agent("Synapsis Desktop/0.1")
|
||||
.build()
|
||||
.expect("failed to build Synapsis HTTP client");
|
||||
|
||||
tauri::Builder::default()
|
||||
.manage(AppState { client })
|
||||
.invoke_handler(tauri::generate_handler![connect, login, register, logout])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running Synapsis");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn normalizes_bare_node_names() {
|
||||
assert_eq!(
|
||||
normalize_node("synapsis.social").unwrap().as_str(),
|
||||
"https://synapsis.social/"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strips_paths_credentials_and_fragments() {
|
||||
assert_eq!(
|
||||
normalize_node("https://person:secret@example.com/somewhere?q=1#fragment")
|
||||
.unwrap()
|
||||
.as_str(),
|
||||
"https://example.com/"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_web_schemes() {
|
||||
assert!(normalize_node("file:///tmp/node").is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
synapsis_desktop_lib::run()
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Synapsis",
|
||||
"version": "0.1.0",
|
||||
"identifier": "xyz.gnosyslabs.synapsis",
|
||||
"build": {
|
||||
"beforeDevCommand": "bun run dev",
|
||||
"devUrl": "http://localhost:1430",
|
||||
"beforeBuildCommand": "bun run build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Synapsis",
|
||||
"width": 1080,
|
||||
"height": 720,
|
||||
"minWidth": 720,
|
||||
"minHeight": 560,
|
||||
"center": true,
|
||||
"hiddenTitle": true,
|
||||
"titleBarStyle": "Overlay"
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src 'self'; connect-src ipc: http://ipc.localhost; img-src 'self' asset: https: http: data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com"
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Manrope:wght@600;700;800&family=Roboto+Mono:wght@500;600&display=swap');
|
||||
|
||||
:root { font-family: "DM Sans", -apple-system, BlinkMacSystemFont, sans-serif; color: #f5f5f2; background: #070808; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; }
|
||||
* { box-sizing: border-box; }
|
||||
html, body, #root { width: 100%; min-width: 720px; height: 100%; min-height: 560px; margin: 0; overflow: hidden; }
|
||||
button, input { font: inherit; }
|
||||
button { color: inherit; }
|
||||
button:focus-visible, input:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }
|
||||
|
||||
.onboarding-shell { width: 100%; height: 100%; display: grid; grid-template-columns: minmax(300px, 45%) minmax(420px, 55%); background: #0b0c0c; }
|
||||
.signal-panel { position: relative; min-width: 0; overflow: hidden; border-right: 1px solid rgba(255,255,255,.1); }
|
||||
.signal-field { position: absolute; inset: 0; width: 100%; height: 100%; }
|
||||
.signal-shade { position: absolute; inset: 0; background: radial-gradient(circle at 55% 45%, transparent 0, rgba(0,0,0,.08) 38%, rgba(0,0,0,.74) 100%), linear-gradient(180deg, rgba(0,0,0,.12), rgba(0,0,0,.4)); pointer-events: none; }
|
||||
.panel-brand, .sidebar-brand, .launch-mark { position: absolute; top: 52px; left: 48px; z-index: 2; display: flex; align-items: center; }
|
||||
.wordmark { display: block; width: auto; object-fit: contain; }
|
||||
.panel-wordmark { width: 146px; height: 28px; }
|
||||
.signal-copy { position: absolute; z-index: 2; left: 48px; right: 36px; bottom: 104px; }
|
||||
.eyebrow { margin: 0 0 25px; color: rgba(255,255,255,.62); font: 600 10px/1 "Roboto Mono", monospace; letter-spacing: 2.1px; }
|
||||
.eyebrow i { display: inline-block; width: 5px; height: 5px; margin-right: 8px; border-radius: 50%; background: #fff; box-shadow: 0 0 9px #fff; vertical-align: 1px; }
|
||||
.signal-copy h2 { margin: 0; max-width: 480px; font-family: Manrope, sans-serif; font-size: clamp(37px, 4vw, 61px); line-height: .99; letter-spacing: -3.4px; }
|
||||
.panel-status { position: absolute; z-index: 2; bottom: 34px; left: 48px; right: 38px; display: flex; justify-content: space-between; color: rgba(255,255,255,.3); font: 500 9px "Roboto Mono", monospace; letter-spacing: 1.4px; }
|
||||
|
||||
.form-panel { position: relative; overflow-y: auto; background: radial-gradient(circle at 85% 10%, rgba(255,255,255,.035), transparent 31%), #0c0d0d; }
|
||||
.drag-strip { position: fixed; z-index: 1; top: 0; right: 0; width: 55%; height: 36px; }
|
||||
.back-button { position: absolute; z-index: 2; top: 46px; left: 46px; display: flex; align-items: center; gap: 7px; padding: 7px 0; border: 0; background: transparent; color: rgba(255,255,255,.55); font-size: 13px; cursor: pointer; }
|
||||
.back-button:hover { color: #fff; }
|
||||
.form-wrap { width: min(440px, calc(100% - 80px)); min-height: calc(100% - 66px); margin: 0 auto; display: flex; align-items: center; padding: 78px 0 68px; }
|
||||
.form-wrap form { width: 100%; }
|
||||
.step-label { display: flex; align-items: center; gap: 12px; margin-bottom: 35px; color: rgba(255,255,255,.43); font: 600 9px "Roboto Mono", monospace; letter-spacing: 1.6px; }
|
||||
.step-label span:first-child { color: #fff; }
|
||||
.step-label i { width: 27px; height: 1px; background: rgba(255,255,255,.22); }
|
||||
.form-wrap h1 { margin: 0 0 20px; font-family: Manrope, sans-serif; font-size: clamp(44px, 5vw, 64px); line-height: .95; letter-spacing: -4px; }
|
||||
.lede { max-width: 400px; margin: 0 0 42px; color: rgba(255,255,255,.5); font-size: 15px; line-height: 1.65; }
|
||||
.lede.compact { margin-bottom: 27px; }
|
||||
label { display: block; margin: 0 0 9px; color: rgba(255,255,255,.75); font-size: 12px; font-weight: 600; }
|
||||
.url-field, .handle-field { display: flex; align-items: center; height: 56px; border: 1px solid rgba(255,255,255,.16); border-radius: 8px; background: rgba(255,255,255,.045); transition: border-color .2s, background .2s; }
|
||||
.url-field:focus-within, .handle-field:focus-within { border-color: rgba(255,255,255,.72); background: rgba(255,255,255,.065); }
|
||||
.url-field span, .handle-field span { padding-left: 17px; color: rgba(255,255,255,.35); font: 500 14px "Roboto Mono", monospace; }
|
||||
.url-field input, .handle-field input { min-width: 0; flex: 1; height: 100%; padding: 0 17px 0 2px; border: 0; outline: 0; color: #fff; background: transparent; font: 500 14px "Roboto Mono", monospace; }
|
||||
.text-field { width: 100%; height: 52px; margin: 0 0 17px; padding: 0 15px; border: 1px solid rgba(255,255,255,.14); border-radius: 8px; outline: 0; color: #fff; background: rgba(255,255,255,.045); transition: border-color .2s, background .2s; }
|
||||
.text-field:focus { border-color: rgba(255,255,255,.68); background: rgba(255,255,255,.065); }
|
||||
.text-field::placeholder { color: rgba(255,255,255,.24); }
|
||||
.field-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 13px; }
|
||||
.handle-field { height: 52px; margin-bottom: 17px; }
|
||||
.field-note { margin: -8px 0 18px; color: rgba(255,255,255,.34); font-size: 11px; }
|
||||
.primary-button, .secondary-button { width: 100%; height: 54px; display: flex; align-items: center; justify-content: center; gap: 10px; border-radius: 8px; font-size: 13px; font-weight: 700; cursor: pointer; transition: transform .15s, background .15s, opacity .15s; }
|
||||
.primary-button { margin-top: 18px; border: 0; color: #080909; background: #f4f4f0; }
|
||||
.primary-button:hover:not(:disabled) { background: #fff; transform: translateY(-1px); }
|
||||
.primary-button:active:not(:disabled) { transform: translateY(0); }
|
||||
.primary-button:disabled { cursor: default; opacity: .48; }
|
||||
.secondary-button { border: 1px solid rgba(255,255,255,.17); background: transparent; }
|
||||
.secondary-button:hover { background: rgba(255,255,255,.06); border-color: rgba(255,255,255,.28); }
|
||||
.privacy-note { margin: 20px 0 0; text-align: center; color: rgba(255,255,255,.28); font-size: 10px; }
|
||||
.lock-dot { margin-right: 5px; font-size: 7px; }
|
||||
.connection-label { display: flex; align-items: center; gap: 8px; margin-bottom: 31px; color: rgba(255,255,255,.48); font: 600 9px "Roboto Mono", monospace; letter-spacing: 1.2px; }
|
||||
.connection-label > span { display: grid; place-items: center; width: 18px; height: 18px; border: 1px solid rgba(255,255,255,.4); border-radius: 50%; color: #fff; }
|
||||
.divider { display: flex; align-items: center; gap: 12px; margin: 25px 0 18px; color: rgba(255,255,255,.27); font: 600 9px "Roboto Mono", monospace; letter-spacing: 1.5px; }
|
||||
.divider i { flex: 1; height: 1px; background: rgba(255,255,255,.09); }
|
||||
.error-message { margin: 13px 0 0; padding: 10px 12px; border: 1px solid rgba(255,112,112,.22); border-radius: 7px; color: #ff9c9c; background: rgba(255,60,60,.07); font-size: 12px; line-height: 1.4; }
|
||||
.step-footer { position: absolute; left: 44px; right: 44px; bottom: 25px; display: flex; justify-content: space-between; color: rgba(255,255,255,.25); font: 500 8px "Roboto Mono", monospace; letter-spacing: 1.2px; }
|
||||
.step-dots { display: flex; gap: 5px; }
|
||||
.step-dots i { width: 17px; height: 2px; background: rgba(255,255,255,.12); }
|
||||
.step-dots i.active { background: rgba(255,255,255,.75); }
|
||||
.spinner { width: 17px; height: 17px; display: inline-block; border: 2px solid rgba(255,255,255,.2); border-top-color: #fff; border-radius: 50%; animation: spin .75s linear infinite; }
|
||||
.spinner.dark { width: 15px; height: 15px; border-color: rgba(0,0,0,.18); border-top-color: #111; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.launch-screen { position: relative; width: 100%; height: 100%; display: grid; place-items: center; background: #060707; }
|
||||
.launch-screen::after { content: ""; position: absolute; inset: 0; background: radial-gradient(circle, transparent, rgba(0,0,0,.6)); }
|
||||
.launch-mark { left: 50%; top: 50%; transform: translate(-50%, -50%); }
|
||||
.launch-wordmark { width: 176px; height: 33px; }
|
||||
.launch-screen > .spinner { position: absolute; z-index: 3; bottom: 12%; }
|
||||
|
||||
.app-shell { width: 100%; height: 100%; display: grid; grid-template-columns: 238px 1fr; background: #0a0b0b; }
|
||||
.sidebar { position: relative; display: flex; flex-direction: column; padding: 104px 18px 18px; border-right: 1px solid rgba(255,255,255,.09); background: #080909; }
|
||||
.sidebar-brand { top: 43px; left: 26px; }
|
||||
.sidebar-wordmark { width: 137px; height: 26px; }
|
||||
.sidebar nav { display: flex; flex-direction: column; gap: 4px; }
|
||||
.nav-item, .node-switcher, .profile-button { display: flex; align-items: center; width: 100%; border: 0; border-radius: 8px; background: transparent; color: rgba(255,255,255,.52); cursor: pointer; }
|
||||
.nav-item { gap: 13px; height: 44px; padding: 0 13px; font-size: 13px; }
|
||||
.nav-item:hover, .nav-item.active { color: #fff; background: rgba(255,255,255,.055); }
|
||||
.compose-button { height: 44px; display: flex; align-items: center; justify-content: center; gap: 9px; margin-top: 22px; border: 0; border-radius: 8px; color: #0a0b0b; background: #f4f4ef; font-size: 12px; font-weight: 700; cursor: pointer; }
|
||||
.sidebar-bottom { margin-top: auto; }
|
||||
.node-switcher { gap: 10px; padding: 11px 10px; }
|
||||
.node-switcher:hover, .profile-button:hover { background: rgba(255,255,255,.05); }
|
||||
.node-switcher > span, .profile-button > span:not(.avatar) { min-width: 0; flex: 1; display: flex; flex-direction: column; align-items: flex-start; }
|
||||
.node-switcher strong, .profile-button strong { max-width: 130px; overflow: hidden; color: rgba(255,255,255,.83); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.node-switcher small, .profile-button small { max-width: 130px; overflow: hidden; color: rgba(255,255,255,.3); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.profile-button { gap: 10px; margin-top: 7px; padding: 9px; border-top: 1px solid rgba(255,255,255,.06); border-radius: 0 0 8px 8px; text-align: left; }
|
||||
.avatar { width: 30px; height: 30px; display: grid; flex: 0 0 auto; place-items: center; border: 1px solid rgba(255,255,255,.22); border-radius: 50%; background: rgba(255,255,255,.08); font-size: 11px; font-weight: 700; }
|
||||
.home-content { min-width: 0; display: flex; flex-direction: column; }
|
||||
.home-header { height: 70px; display: flex; align-items: center; justify-content: space-between; padding: 18px 28px 0; border-bottom: 1px solid rgba(255,255,255,.08); }
|
||||
.home-header h1 { margin: 0; font: 700 17px Manrope, sans-serif; }
|
||||
.connected-pill { display: flex; align-items: center; gap: 7px; color: rgba(255,255,255,.4); font: 500 8px "Roboto Mono", monospace; letter-spacing: 1px; }
|
||||
.connected-pill i { width: 5px; height: 5px; border-radius: 50%; background: #78dc9b; box-shadow: 0 0 8px rgba(120,220,155,.7); }
|
||||
.home-empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 35px; text-align: center; }
|
||||
.home-empty .eyebrow { margin: 26px 0 17px; }
|
||||
.home-empty h2 { margin: 0; font: 700 clamp(32px, 4vw, 50px)/1.05 Manrope, sans-serif; letter-spacing: -2.8px; }
|
||||
.home-empty > p:last-child { max-width: 470px; margin: 20px 0 0; color: rgba(255,255,255,.4); font-size: 13px; line-height: 1.7; }
|
||||
.home-empty strong { color: rgba(255,255,255,.68); }
|
||||
.orbit-mark { position: relative; width: 76px; height: 76px; }
|
||||
.orbit-mark span { position: absolute; inset: 8px; border: 1px solid rgba(255,255,255,.16); border-radius: 50%; }
|
||||
.orbit-mark span:nth-child(2) { inset: 20px; border-color: rgba(255,255,255,.35); }
|
||||
.orbit-mark i { position: absolute; left: 50%; top: 0; width: 5px; height: 5px; border-radius: 50%; background: #fff; box-shadow: 0 0 8px #fff; animation: orbit 5s linear infinite; transform-origin: 0 38px; }
|
||||
@keyframes orbit { to { transform: rotate(360deg); } }
|
||||
|
||||
@media (max-width: 850px) { .onboarding-shell { grid-template-columns: 38% 62%; } .signal-copy h2 { font-size: 38px; letter-spacing: -2.4px; } .form-wrap { width: calc(100% - 54px); } .panel-brand, .signal-copy { left: 32px; } .panel-status { left: 32px; } }
|
||||
@media (max-height: 670px) { .form-wrap { align-items: flex-start; padding-top: 78px; } .form-wrap h1 { font-size: 42px; } .lede { margin-bottom: 24px; } .signal-copy { bottom: 74px; } .signal-copy h2 { font-size: 39px; } .field-grid { gap: 9px; } .text-field { height: 46px; margin-bottom: 11px; } .handle-field { height: 46px; margin-bottom: 11px; } .primary-button { height: 49px; margin-top: 12px; } }
|
||||
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; } }
|
||||
@@ -0,0 +1,449 @@
|
||||
import { FormEvent, useEffect, useRef, useState } from "react";
|
||||
import { invoke, isTauri } from "@tauri-apps/api/core";
|
||||
import "./App.css";
|
||||
|
||||
type Screen = "node" | "login" | "register" | "home";
|
||||
|
||||
type NodeInfo = {
|
||||
name?: string;
|
||||
description?: string;
|
||||
accentColor?: string;
|
||||
domain?: string;
|
||||
logoUrl?: string;
|
||||
};
|
||||
|
||||
type User = {
|
||||
id: string;
|
||||
handle: string;
|
||||
displayName?: string;
|
||||
avatarUrl?: string;
|
||||
};
|
||||
|
||||
type Connection = {
|
||||
nodeUrl: string;
|
||||
node: NodeInfo;
|
||||
user: User | null;
|
||||
};
|
||||
|
||||
type AuthResponse = { user: User };
|
||||
|
||||
const DEFAULT_NODE = "synapsis.social";
|
||||
const SAVED_NODE_KEY = "synapsis.nodeUrl";
|
||||
|
||||
function readableError(error: unknown) {
|
||||
if (typeof error === "string") return error;
|
||||
if (error instanceof Error) return error.message;
|
||||
return "Something went wrong. Please try again.";
|
||||
}
|
||||
|
||||
function displayHost(url: string) {
|
||||
try {
|
||||
return new URL(url).host;
|
||||
} catch {
|
||||
return url.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
||||
}
|
||||
}
|
||||
|
||||
function Icon({ name, size = 20 }: { name: string; size?: number }) {
|
||||
const paths: Record<string, React.ReactNode> = {
|
||||
arrow: <><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></>,
|
||||
back: <><path d="m15 18-6-6 6-6"/></>,
|
||||
check: <><path d="m5 12 4 4L19 6"/></>,
|
||||
home: <><path d="m3 11 9-8 9 8"/><path d="M5 10v10h14V10"/><path d="M9 20v-6h6v6"/></>,
|
||||
search: <><circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/></>,
|
||||
bell: <><path d="M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9"/><path d="M10 21h4"/></>,
|
||||
message: <><path d="M21 15a4 4 0 0 1-4 4H8l-5 3V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4z"/></>,
|
||||
plus: <><path d="M12 5v14M5 12h14"/></>,
|
||||
node: <><circle cx="12" cy="12" r="2"/><circle cx="5" cy="6" r="2"/><circle cx="19" cy="6" r="2"/><circle cx="5" cy="18" r="2"/><circle cx="19" cy="18" r="2"/><path d="m7 7 3.5 3M17 7l-3.5 3M7 17l3.5-3M17 17l-3.5-3"/></>,
|
||||
logout: <><path d="M10 17l5-5-5-5M15 12H3"/><path d="M14 3h5a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-5"/></>,
|
||||
};
|
||||
return <svg aria-hidden="true" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">{paths[name]}</svg>;
|
||||
}
|
||||
|
||||
function Wordmark({ className = "" }: { className?: string }) {
|
||||
return <img className={`wordmark ${className}`} src="/logotext.svg" alt="Synapsis" />;
|
||||
}
|
||||
|
||||
function SignalField() {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) return;
|
||||
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
let animation = 0;
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
let points: {
|
||||
x: number;
|
||||
y: number;
|
||||
driftX: number;
|
||||
driftY: number;
|
||||
phaseX: number;
|
||||
phaseY: number;
|
||||
speedX: number;
|
||||
speedY: number;
|
||||
radius: number;
|
||||
}[] = [];
|
||||
let triangles: [number, number, number][] = [];
|
||||
|
||||
function random(seed: number) {
|
||||
const value = Math.sin(seed * 999.17) * 43758.5453;
|
||||
return value - Math.floor(value);
|
||||
}
|
||||
|
||||
function resize() {
|
||||
const scale = window.devicePixelRatio || 1;
|
||||
width = canvas!.clientWidth;
|
||||
height = canvas!.clientHeight;
|
||||
canvas!.width = width * scale;
|
||||
canvas!.height = height * scale;
|
||||
context!.setTransform(scale, 0, 0, scale, 0, 0);
|
||||
const count = Math.min(64, Math.max(34, Math.round(width / 18)));
|
||||
points = Array.from({ length: count }, (_, index) => ({
|
||||
x: random(index + 11) * width,
|
||||
y: random(index + 97) * height,
|
||||
driftX: 9 + random(index + 207) * 16,
|
||||
driftY: 9 + random(index + 307) * 16,
|
||||
phaseX: random(index + 507) * Math.PI * 2,
|
||||
phaseY: random(index + 607) * Math.PI * 2,
|
||||
speedX: 0.055 + random(index + 707) * 0.055,
|
||||
speedY: 0.05 + random(index + 807) * 0.06,
|
||||
radius: 0.7 + random(index + 407) * 1.15,
|
||||
}));
|
||||
|
||||
// Keep the mesh topology stable. The previous version rebuilt the
|
||||
// nearest-neighbour fan every frame, which made edges visibly pop.
|
||||
triangles = [];
|
||||
for (let anchor = 0; anchor < points.length; anchor += 3) {
|
||||
const ring = points
|
||||
.map((point, index) => ({
|
||||
index,
|
||||
distance: Math.hypot(point.x - points[anchor].x, point.y - points[anchor].y),
|
||||
angle: Math.atan2(point.y - points[anchor].y, point.x - points[anchor].x),
|
||||
}))
|
||||
.filter((item) => item.index !== anchor && item.distance < 230)
|
||||
.sort((left, right) => left.distance - right.distance)
|
||||
.slice(0, 6)
|
||||
.sort((left, right) => left.angle - right.angle);
|
||||
for (let index = 0; index < ring.length - 1; index += 2) {
|
||||
if (Math.hypot(
|
||||
points[ring[index].index].x - points[ring[index + 1].index].x,
|
||||
points[ring[index].index].y - points[ring[index + 1].index].y,
|
||||
) < 220) {
|
||||
triangles.push([anchor, ring[index].index, ring[index + 1].index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function draw(milliseconds: number) {
|
||||
const time = reducedMotion ? 0 : milliseconds / 1000;
|
||||
context!.clearRect(0, 0, width, height);
|
||||
context!.fillStyle = "#060707";
|
||||
context!.fillRect(0, 0, width, height);
|
||||
const positions = points.map((point) => ({
|
||||
...point,
|
||||
px: point.x + Math.sin(time * point.speedX + point.phaseX) * point.driftX,
|
||||
py: point.y + Math.sin(time * point.speedY + point.phaseY) * point.driftY,
|
||||
}));
|
||||
|
||||
for (let left = 0; left < positions.length; left++) {
|
||||
for (let right = left + 1; right < positions.length; right++) {
|
||||
const dx = positions[left].px - positions[right].px;
|
||||
const dy = positions[left].py - positions[right].py;
|
||||
const distance = Math.hypot(dx, dy);
|
||||
if (distance < 112) {
|
||||
context!.strokeStyle = `rgba(255,255,255,${(1 - distance / 112) * 0.1})`;
|
||||
context!.lineWidth = 0.7;
|
||||
context!.beginPath();
|
||||
context!.moveTo(positions[left].px, positions[left].py);
|
||||
context!.lineTo(positions[right].px, positions[right].py);
|
||||
context!.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const signals = [0, 1].map((index) => ({
|
||||
x: width * (0.5 + 0.39 * Math.sin(time * (index ? 0.082 : 0.105) + index * 2.1)),
|
||||
y: height * (0.5 + 0.4 * Math.sin(time * (index ? 0.112 : 0.078) + index * 1.3)),
|
||||
intensity: 0.48 + 0.24 * Math.sin(time * 0.32 + index * Math.PI),
|
||||
}));
|
||||
|
||||
triangles.forEach(([firstIndex, secondIndex, thirdIndex], triangleIndex) => {
|
||||
const first = positions[firstIndex];
|
||||
const second = positions[secondIndex];
|
||||
const third = positions[thirdIndex];
|
||||
const centerX = (first.px + second.px + third.px) / 3;
|
||||
const centerY = (first.py + second.py + third.py) / 3;
|
||||
const signalStrength = Math.max(...signals.map((signal) => {
|
||||
const proximity = Math.max(0, 1 - Math.hypot(centerX - signal.x, centerY - signal.y) / 285);
|
||||
const easedProximity = proximity * proximity * (3 - 2 * proximity);
|
||||
return easedProximity * signal.intensity;
|
||||
}));
|
||||
const breath = 0.84 + 0.16 * Math.sin(time * 0.24 + triangleIndex * 0.41);
|
||||
const alpha = signalStrength * breath;
|
||||
if (alpha < 0.004) return;
|
||||
context!.beginPath();
|
||||
context!.moveTo(first.px, first.py);
|
||||
context!.lineTo(second.px, second.py);
|
||||
context!.lineTo(third.px, third.py);
|
||||
context!.closePath();
|
||||
context!.fillStyle = `rgba(255,255,255,${alpha * 0.03})`;
|
||||
context!.strokeStyle = `rgba(255,255,255,${alpha * 0.38})`;
|
||||
context!.lineWidth = 0.7;
|
||||
context!.fill();
|
||||
context!.stroke();
|
||||
});
|
||||
|
||||
positions.forEach((point) => {
|
||||
const glow = Math.max(...signals.map((signal) => signal.intensity * Math.max(0, 1 - Math.hypot(point.px - signal.x, point.py - signal.y) / 200)));
|
||||
context!.fillStyle = `rgba(255,255,255,${0.28 + glow * 0.58})`;
|
||||
context!.beginPath();
|
||||
context!.arc(point.px, point.py, point.radius + glow * 0.8, 0, Math.PI * 2);
|
||||
context!.fill();
|
||||
});
|
||||
if (!reducedMotion) animation = requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver(resize);
|
||||
observer.observe(canvas);
|
||||
resize();
|
||||
if (reducedMotion) draw(0);
|
||||
else animation = requestAnimationFrame(draw);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
cancelAnimationFrame(animation);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <canvas ref={canvasRef} className="signal-field" aria-hidden="true" />;
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [screen, setScreen] = useState<Screen>("node");
|
||||
const [nodeInput, setNodeInput] = useState(DEFAULT_NODE);
|
||||
const [nodeUrl, setNodeUrl] = useState("");
|
||||
const [node, setNode] = useState<NodeInfo>({ name: "Synapsis" });
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [working, setWorking] = useState(false);
|
||||
const [bootstrapping, setBootstrapping] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const savedNode = localStorage.getItem(SAVED_NODE_KEY);
|
||||
if (!savedNode || !isTauri()) {
|
||||
setBootstrapping(false);
|
||||
return;
|
||||
}
|
||||
invoke<Connection>("connect", { nodeUrl: savedNode })
|
||||
.then((connection) => {
|
||||
setNodeUrl(connection.nodeUrl);
|
||||
setNode(connection.node);
|
||||
setNodeInput(displayHost(connection.nodeUrl));
|
||||
setUser(connection.user);
|
||||
setScreen(connection.user ? "home" : "login");
|
||||
})
|
||||
.catch(() => setScreen("node"))
|
||||
.finally(() => setBootstrapping(false));
|
||||
}, []);
|
||||
|
||||
async function connect(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
if (!isTauri()) {
|
||||
setError("Open this screen with “bun run tauri dev” to connect to a node.");
|
||||
return;
|
||||
}
|
||||
setWorking(true);
|
||||
setError("");
|
||||
try {
|
||||
const connection = await invoke<Connection>("connect", { nodeUrl: nodeInput });
|
||||
localStorage.setItem(SAVED_NODE_KEY, connection.nodeUrl);
|
||||
setNodeUrl(connection.nodeUrl);
|
||||
setNode(connection.node);
|
||||
setUser(connection.user);
|
||||
setScreen(connection.user ? "home" : "login");
|
||||
} catch (caught) {
|
||||
setError(readableError(caught));
|
||||
} finally {
|
||||
setWorking(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function login(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const data = new FormData(event.currentTarget);
|
||||
setWorking(true);
|
||||
setError("");
|
||||
try {
|
||||
const response = await invoke<AuthResponse>("login", {
|
||||
nodeUrl,
|
||||
email: data.get("email"),
|
||||
password: data.get("password"),
|
||||
});
|
||||
setUser(response.user);
|
||||
setScreen("home");
|
||||
} catch (caught) {
|
||||
setError(readableError(caught));
|
||||
} finally {
|
||||
setWorking(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function register(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const data = new FormData(event.currentTarget);
|
||||
const password = String(data.get("password"));
|
||||
if (password !== String(data.get("confirmation"))) {
|
||||
setError("Passwords do not match.");
|
||||
return;
|
||||
}
|
||||
setWorking(true);
|
||||
setError("");
|
||||
try {
|
||||
const response = await invoke<AuthResponse>("register", {
|
||||
nodeUrl,
|
||||
displayName: data.get("displayName"),
|
||||
handle: data.get("handle"),
|
||||
email: data.get("email"),
|
||||
password,
|
||||
});
|
||||
setUser(response.user);
|
||||
setScreen("home");
|
||||
} catch (caught) {
|
||||
setError(readableError(caught));
|
||||
} finally {
|
||||
setWorking(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
setWorking(true);
|
||||
try {
|
||||
await invoke("logout", { nodeUrl });
|
||||
} finally {
|
||||
setUser(null);
|
||||
setScreen("login");
|
||||
setWorking(false);
|
||||
}
|
||||
}
|
||||
|
||||
function chooseAnotherNode() {
|
||||
setError("");
|
||||
setScreen("node");
|
||||
}
|
||||
|
||||
if (bootstrapping) {
|
||||
return <main className="launch-screen"><SignalField/><div className="launch-mark"><Wordmark className="launch-wordmark"/></div><span className="spinner" aria-label="Loading Synapsis"/></main>;
|
||||
}
|
||||
|
||||
if (screen === "home" && user) {
|
||||
const name = user.displayName?.trim() || user.handle.replace(/^@/, "").split("@")[0];
|
||||
return (
|
||||
<main className="app-shell">
|
||||
<aside className="sidebar" data-tauri-drag-region>
|
||||
<div className="sidebar-brand"><Wordmark className="sidebar-wordmark"/></div>
|
||||
<nav aria-label="Main navigation">
|
||||
<button className="nav-item active"><Icon name="home"/><span>Home</span></button>
|
||||
<button className="nav-item"><Icon name="search"/><span>Explore</span></button>
|
||||
<button className="nav-item"><Icon name="bell"/><span>Notifications</span></button>
|
||||
<button className="nav-item"><Icon name="message"/><span>Messages</span></button>
|
||||
</nav>
|
||||
<button className="compose-button"><Icon name="plus"/><span>New post</span></button>
|
||||
<div className="sidebar-bottom">
|
||||
<button className="node-switcher" onClick={chooseAnotherNode}><Icon name="node"/><span><strong>{node.name || "Synapsis"}</strong><small>{displayHost(nodeUrl)}</small></span></button>
|
||||
<button className="profile-button" onClick={logout} disabled={working} title="Log out">
|
||||
<span className="avatar">{name.slice(0, 1).toUpperCase()}</span><span><strong>{name}</strong><small>{user.handle.startsWith("@") ? user.handle : `@${user.handle}`}</small></span><Icon name="logout" size={17}/>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
<section className="home-content">
|
||||
<header className="home-header" data-tauri-drag-region><h1>Home</h1><span className="connected-pill"><i/>Connected</span></header>
|
||||
<div className="home-empty">
|
||||
<div className="orbit-mark"><span/><span/><i/></div>
|
||||
<p className="eyebrow">SESSION ESTABLISHED</p>
|
||||
<h2>Welcome to the network,<br/>{name}.</h2>
|
||||
<p>Your desktop session is connected to <strong>{displayHost(nodeUrl)}</strong>. The timeline is the next piece to bring over from iOS.</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="onboarding-shell">
|
||||
<section className="signal-panel" data-tauri-drag-region>
|
||||
<SignalField/>
|
||||
<div className="signal-shade"/>
|
||||
<div className="panel-brand"><Wordmark className="panel-wordmark"/></div>
|
||||
<div className="signal-copy">
|
||||
<p className="eyebrow"><i/> THE SIGNAL IS ALREADY MOVING</p>
|
||||
<h2>Your node.<br/>Your network.<br/>Your identity.</h2>
|
||||
</div>
|
||||
<div className="panel-status"><span>DECENTRALIZED SOCIAL</span><span>DESKTOP / 01</span></div>
|
||||
</section>
|
||||
|
||||
<section className="form-panel">
|
||||
<div className="drag-strip" data-tauri-drag-region/>
|
||||
{screen !== "node" && <button className="back-button" onClick={() => { setError(""); setScreen(screen === "register" ? "login" : "node"); }}><Icon name="back" size={18}/>Back</button>}
|
||||
<div className="form-wrap">
|
||||
{screen === "node" && (
|
||||
<form onSubmit={connect}>
|
||||
<div className="step-label"><span>01</span><i/><span>CONNECT</span></div>
|
||||
<h1>Choose your<br/>node.</h1>
|
||||
<p className="lede">Synapsis is a network of independent communities. Enter the node where your account lives.</p>
|
||||
<label htmlFor="node-url">Node URL</label>
|
||||
<div className="url-field"><span>https://</span><input id="node-url" value={nodeInput.replace(/^https?:\/\//, "")} onChange={(event) => setNodeInput(event.target.value)} autoCapitalize="none" autoCorrect="off" spellCheck={false} autoFocus/></div>
|
||||
{error && <p className="error-message" role="alert">{error}</p>}
|
||||
<button className="primary-button" disabled={!nodeInput.trim() || working}>{working ? <><span className="spinner dark"/>Connecting…</> : <>Continue<Icon name="arrow" size={19}/></>}</button>
|
||||
<p className="privacy-note"><span className="lock-dot">◆</span> Your credentials are sent only to the node you choose.</p>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{screen === "login" && (
|
||||
<form onSubmit={login}>
|
||||
<div className="connection-label"><span><Icon name="check" size={12}/></span>CONNECTED / {displayHost(nodeUrl).toUpperCase()}</div>
|
||||
<h1>Welcome<br/>back.</h1>
|
||||
<p className="lede">Sign in to continue on this node.</p>
|
||||
<label htmlFor="email">Email</label>
|
||||
<input className="text-field" id="email" name="email" type="email" placeholder="you@example.com" autoComplete="username" required autoFocus/>
|
||||
<label htmlFor="password">Password</label>
|
||||
<input className="text-field" id="password" name="password" type="password" placeholder="Enter your password" autoComplete="current-password" required/>
|
||||
<p className="field-note">Use the email associated with your username.</p>
|
||||
{error && <p className="error-message" role="alert">{error}</p>}
|
||||
<button className="primary-button" disabled={working}>{working ? <><span className="spinner dark"/>Signing in…</> : <>Sign in<Icon name="arrow" size={19}/></>}</button>
|
||||
<div className="divider"><i/><span>NEW HERE?</span><i/></div>
|
||||
<button type="button" className="secondary-button" onClick={() => { setError(""); setScreen("register"); }}>Create account</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{screen === "register" && (
|
||||
<form onSubmit={register}>
|
||||
<div className="connection-label"><span><Icon name="check" size={12}/></span>CONNECTED / {displayHost(nodeUrl).toUpperCase()}</div>
|
||||
<h1>Create your<br/>account.</h1>
|
||||
<p className="lede compact">Create your identity on this node.</p>
|
||||
<div className="field-grid">
|
||||
<div><label htmlFor="display-name">Display name</label><input className="text-field" id="display-name" name="displayName" autoComplete="name" required autoFocus/></div>
|
||||
<div><label htmlFor="handle">Username</label><div className="handle-field"><span>@</span><input id="handle" name="handle" autoCapitalize="none" autoComplete="username" required/></div></div>
|
||||
</div>
|
||||
<label htmlFor="register-email">Email</label>
|
||||
<input className="text-field" id="register-email" name="email" type="email" autoComplete="email" required/>
|
||||
<div className="field-grid">
|
||||
<div><label htmlFor="new-password">Password</label><input className="text-field" id="new-password" name="password" type="password" minLength={8} autoComplete="new-password" required/></div>
|
||||
<div><label htmlFor="confirmation">Confirm</label><input className="text-field" id="confirmation" name="confirmation" type="password" minLength={8} autoComplete="new-password" required/></div>
|
||||
</div>
|
||||
<p className="field-note">Use at least 8 characters.</p>
|
||||
{error && <p className="error-message" role="alert">{error}</p>}
|
||||
<button className="primary-button" disabled={working}>{working ? <><span className="spinner dark"/>Creating…</> : <>Create account<Icon name="arrow" size={19}/></>}</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
<div className="step-footer"><span>{screen === "node" ? "STEP 1 OF 2" : screen === "login" ? "STEP 2 OF 2" : "NEW ACCOUNT"}</span><span className="step-dots"><i className="active"/><i className={screen !== "node" ? "active" : ""}/></span></div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
// @ts-expect-error process is a nodejs global
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
plugins: [react()],
|
||||
|
||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
//
|
||||
// 1. prevent Vite from obscuring rust errors
|
||||
clearScreen: false,
|
||||
// 2. tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
port: 1430,
|
||||
strictPort: true,
|
||||
host: host || false,
|
||||
hmr: host
|
||||
? {
|
||||
protocol: "ws",
|
||||
host,
|
||||
port: 1431,
|
||||
}
|
||||
: undefined,
|
||||
watch: {
|
||||
// 3. tell Vite to ignore watching `src-tauri`
|
||||
ignored: ["**/src-tauri/**"],
|
||||
},
|
||||
},
|
||||
}));
|
||||