Documents by default, cloud project rename, multi-select drag, new settings
This commit is contained in:
@@ -5,31 +5,32 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "typst-desktop",
|
"name": "typst-desktop",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.20.2",
|
"@codemirror/autocomplete": "^6.20.3",
|
||||||
"@codemirror/commands": "^6.10.3",
|
"@codemirror/commands": "^6.10.4",
|
||||||
"@codemirror/language": "^6.10.8",
|
"@codemirror/language": "^6.12.4",
|
||||||
"@codemirror/legacy-modes": "^6.5.1",
|
"@codemirror/legacy-modes": "^6.5.3",
|
||||||
"@codemirror/lint": "^6.9.6",
|
"@codemirror/lint": "^6.9.7",
|
||||||
"@codemirror/lsp-client": "^6.2.4",
|
"@codemirror/lsp-client": "^6.2.5",
|
||||||
"@codemirror/state": "^6.6.0",
|
"@codemirror/state": "^6.7.1",
|
||||||
"@codemirror/view": "^6.43.0",
|
"@codemirror/view": "^6.43.6",
|
||||||
"@iconify/svelte": "^5.2.1",
|
"@crabnebula/tauri-plugin-drag": "^2",
|
||||||
"@lezer/highlight": "^1.2.1",
|
"@iconify/svelte": "^5.2.2",
|
||||||
|
"@lezer/highlight": "^1.2.3",
|
||||||
"@tauri-apps/api": "^2.11.1",
|
"@tauri-apps/api": "^2.11.1",
|
||||||
"@tauri-apps/plugin-dialog": "^2.4.1",
|
"@tauri-apps/plugin-dialog": "^2.7.2",
|
||||||
"@tauri-apps/plugin-opener": "^2.5.4",
|
"@tauri-apps/plugin-opener": "^2.5.4",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
"codemirror-lang-typst": "^0.4.0",
|
"codemirror-lang-typst": "^0.4.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-static": "^3.0.10",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@sveltejs/kit": "^2.70.0",
|
"@sveltejs/kit": "^2.70.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.1.1",
|
"@sveltejs/vite-plugin-svelte": "^5.1.1",
|
||||||
"@tailwindcss/vite": "^4.3.0",
|
"@tailwindcss/vite": "^4.3.3",
|
||||||
"@tauri-apps/cli": "^2.11.4",
|
"@tauri-apps/cli": "^2.11.4",
|
||||||
"svelte": "^5.56.6",
|
"svelte": "^5.56.7",
|
||||||
"svelte-check": "^4.7.3",
|
"svelte-check": "^4.7.3",
|
||||||
"tailwindcss": "^4.3.0",
|
"tailwindcss": "^4.3.3",
|
||||||
"typescript": "~5.6.3",
|
"typescript": "~5.6.3",
|
||||||
"vite": "^6.4.3",
|
"vite": "^6.4.3",
|
||||||
"vite-plugin-top-level-await": "^1.6.0",
|
"vite-plugin-top-level-await": "^1.6.0",
|
||||||
@@ -56,6 +57,8 @@
|
|||||||
|
|
||||||
"@codemirror/view": ["@codemirror/[email protected]", "", { "dependencies": { "@codemirror/state": "^6.7.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA=="],
|
"@codemirror/view": ["@codemirror/[email protected]", "", { "dependencies": { "@codemirror/state": "^6.7.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA=="],
|
||||||
|
|
||||||
|
"@crabnebula/tauri-plugin-drag": ["@crabnebula/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-LnUXAZwQt1cdMoGDLJ6ogW9wFCYServCZXlGadS7CA+CZ9eXS7L+Q7QyQW6g/zGw9YI2MwKFqf1aSNBGyWw+OA=="],
|
||||||
|
|
||||||
"@esbuild/aix-ppc64": ["@esbuild/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
"@esbuild/aix-ppc64": ["@esbuild/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
||||||
|
|
||||||
"@esbuild/android-arm": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
|
"@esbuild/android-arm": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
|
||||||
@@ -400,7 +403,7 @@
|
|||||||
|
|
||||||
"style-mod": ["[email protected]", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
|
"style-mod": ["[email protected]", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
|
||||||
|
|
||||||
"svelte": ["[email protected].6", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-p4HDLDogGHKRKCrgckQHNs5PEfXkju6JI5jTywueaKJI5hAdjPohEhRtQ0M1SWC/+TA73SPln+r7srr+7e4nZA=="],
|
"svelte": ["[email protected].7", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-5qERUZX80oQj6XrDMUmD2Uhd/cIpCPDWWKBK3ZHmyRUC9apPyamWM8xMo31mbWsIQxwG2hVoSnOJ/EcnhVkkzQ=="],
|
||||||
|
|
||||||
"svelte-check": ["[email protected]", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "^0.2.0", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-DHdTCGX62R0fCxBEaT+USdASAnoaRBaaNczkRJl0K7o3WyoCeVUbVxo6fKqpOll/B+WMWCsiFK0eFrJSNBKZIg=="],
|
"svelte-check": ["[email protected]", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "^0.2.0", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-DHdTCGX62R0fCxBEaT+USdASAnoaRBaaNczkRJl0K7o3WyoCeVUbVxo6fKqpOll/B+WMWCsiFK0eFrJSNBKZIg=="],
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -24,6 +24,7 @@
|
|||||||
"@codemirror/view": "^6.43.6",
|
"@codemirror/view": "^6.43.6",
|
||||||
"@iconify/svelte": "^5.2.2",
|
"@iconify/svelte": "^5.2.2",
|
||||||
"@lezer/highlight": "^1.2.3",
|
"@lezer/highlight": "^1.2.3",
|
||||||
|
"@crabnebula/tauri-plugin-drag": "^2.1.0",
|
||||||
"@tauri-apps/api": "^2.11.1",
|
"@tauri-apps/api": "^2.11.1",
|
||||||
"@tauri-apps/plugin-dialog": "^2.7.2",
|
"@tauri-apps/plugin-dialog": "^2.7.2",
|
||||||
"@tauri-apps/plugin-opener": "^2.5.4",
|
"@tauri-apps/plugin-opener": "^2.5.4",
|
||||||
@@ -36,7 +37,7 @@
|
|||||||
"tailwindcss": "^4.3.3",
|
"tailwindcss": "^4.3.3",
|
||||||
"@sveltejs/kit": "^2.70.1",
|
"@sveltejs/kit": "^2.70.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.1.1",
|
"@sveltejs/vite-plugin-svelte": "^5.1.1",
|
||||||
"svelte": "^5.56.6",
|
"svelte": "^5.56.7",
|
||||||
"svelte-check": "^4.7.3",
|
"svelte-check": "^4.7.3",
|
||||||
"typescript": "~5.6.3",
|
"typescript": "~5.6.3",
|
||||||
"vite": "^6.4.3",
|
"vite": "^6.4.3",
|
||||||
|
|||||||
Generated
+185
-15
@@ -722,6 +722,19 @@ version = "0.8.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-graphics"
|
||||||
|
version = "0.24.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.1",
|
||||||
|
"core-foundation",
|
||||||
|
"core-graphics-types",
|
||||||
|
"foreign-types 0.5.0",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-graphics"
|
name = "core-graphics"
|
||||||
version = "0.25.0"
|
version = "0.25.0"
|
||||||
@@ -1074,6 +1087,28 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "drag"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e90b4a25ace5ce0561534b073943594cbcd21af936e64d09aec444568411f8c"
|
||||||
|
dependencies = [
|
||||||
|
"core-graphics 0.24.0",
|
||||||
|
"dunce",
|
||||||
|
"gdk",
|
||||||
|
"gdkx11",
|
||||||
|
"gtk",
|
||||||
|
"log",
|
||||||
|
"objc2",
|
||||||
|
"objc2-app-kit",
|
||||||
|
"objc2-foundation",
|
||||||
|
"raw-window-handle",
|
||||||
|
"serde",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"windows 0.52.0",
|
||||||
|
"windows-core 0.58.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dtoa"
|
name = "dtoa"
|
||||||
version = "1.0.11"
|
version = "1.0.11"
|
||||||
@@ -3044,9 +3079,17 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.1",
|
"bitflags 2.13.1",
|
||||||
"block2",
|
"block2",
|
||||||
|
"libc",
|
||||||
"objc2",
|
"objc2",
|
||||||
|
"objc2-cloud-kit",
|
||||||
|
"objc2-core-data",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
|
"objc2-core-graphics",
|
||||||
|
"objc2-core-image",
|
||||||
|
"objc2-core-text",
|
||||||
|
"objc2-core-video",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
|
"objc2-quartz-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3066,6 +3109,7 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
|
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bitflags 2.13.1",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
]
|
]
|
||||||
@@ -3126,6 +3170,19 @@ dependencies = [
|
|||||||
"objc2-core-graphics",
|
"objc2-core-graphics",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-core-video"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.1",
|
||||||
|
"objc2",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
"objc2-core-graphics",
|
||||||
|
"objc2-io-surface",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-encode"
|
name = "objc2-encode"
|
||||||
version = "4.1.0"
|
version = "4.1.0"
|
||||||
@@ -4783,7 +4840,7 @@ dependencies = [
|
|||||||
"bitflags 2.13.1",
|
"bitflags 2.13.1",
|
||||||
"block2",
|
"block2",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
"core-graphics",
|
"core-graphics 0.25.0",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"dbus",
|
"dbus",
|
||||||
"dispatch2",
|
"dispatch2",
|
||||||
@@ -4808,7 +4865,7 @@ dependencies = [
|
|||||||
"tao-macros",
|
"tao-macros",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"url",
|
"url",
|
||||||
"windows",
|
"windows 0.61.3",
|
||||||
"windows-core 0.61.2",
|
"windows-core 0.61.2",
|
||||||
"windows-version",
|
"windows-version",
|
||||||
"x11-dl",
|
"x11-dl",
|
||||||
@@ -4890,7 +4947,7 @@ dependencies = [
|
|||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"window-vibrancy",
|
"window-vibrancy",
|
||||||
"windows",
|
"windows 0.61.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4989,6 +5046,21 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-drag"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "729ca0ce4b1169869d3405216d3c09a524f41ea5e2eec89f917cd6623f8a70ca"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"drag",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-fs"
|
name = "tauri-plugin-fs"
|
||||||
version = "2.5.1"
|
version = "2.5.1"
|
||||||
@@ -5031,7 +5103,7 @@ dependencies = [
|
|||||||
"tauri-plugin",
|
"tauri-plugin",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"url",
|
"url",
|
||||||
"windows",
|
"windows 0.61.3",
|
||||||
"zbus",
|
"zbus",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5057,7 +5129,7 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"windows",
|
"windows 0.61.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5082,7 +5154,7 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"windows",
|
"windows 0.61.3",
|
||||||
"wry",
|
"wry",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5616,6 +5688,7 @@ dependencies = [
|
|||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
|
"tauri-plugin-drag",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
"typst",
|
"typst",
|
||||||
"typst-assets",
|
"typst-assets",
|
||||||
@@ -6533,10 +6606,10 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"webview2-com-macros",
|
"webview2-com-macros",
|
||||||
"webview2-com-sys",
|
"webview2-com-sys",
|
||||||
"windows",
|
"windows 0.61.3",
|
||||||
"windows-core 0.61.2",
|
"windows-core 0.61.2",
|
||||||
"windows-implement",
|
"windows-implement 0.60.2",
|
||||||
"windows-interface",
|
"windows-interface 0.59.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6557,7 +6630,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
|
checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"windows",
|
"windows 0.61.3",
|
||||||
"windows-core 0.61.2",
|
"windows-core 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -6613,6 +6686,18 @@ dependencies = [
|
|||||||
"windows-version",
|
"windows-version",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core 0.52.0",
|
||||||
|
"windows-implement 0.52.0",
|
||||||
|
"windows-interface 0.52.0",
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows"
|
name = "windows"
|
||||||
version = "0.61.3"
|
version = "0.61.3"
|
||||||
@@ -6635,14 +6720,36 @@ dependencies = [
|
|||||||
"windows-core 0.61.2",
|
"windows-core 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.58.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement 0.58.0",
|
||||||
|
"windows-interface 0.58.0",
|
||||||
|
"windows-result 0.2.0",
|
||||||
|
"windows-strings 0.1.0",
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.61.2"
|
version = "0.61.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-implement",
|
"windows-implement 0.60.2",
|
||||||
"windows-interface",
|
"windows-interface 0.59.3",
|
||||||
"windows-link 0.1.3",
|
"windows-link 0.1.3",
|
||||||
"windows-result 0.3.4",
|
"windows-result 0.3.4",
|
||||||
"windows-strings 0.4.2",
|
"windows-strings 0.4.2",
|
||||||
@@ -6654,8 +6761,8 @@ version = "0.62.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-implement",
|
"windows-implement 0.60.2",
|
||||||
"windows-interface",
|
"windows-interface 0.59.3",
|
||||||
"windows-link 0.2.1",
|
"windows-link 0.2.1",
|
||||||
"windows-result 0.4.1",
|
"windows-result 0.4.1",
|
||||||
"windows-strings 0.5.1",
|
"windows-strings 0.5.1",
|
||||||
@@ -6672,6 +6779,28 @@ dependencies = [
|
|||||||
"windows-threading",
|
"windows-threading",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.58.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-implement"
|
name = "windows-implement"
|
||||||
version = "0.60.2"
|
version = "0.60.2"
|
||||||
@@ -6683,6 +6812,28 @@ dependencies = [
|
|||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.58.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-interface"
|
name = "windows-interface"
|
||||||
version = "0.59.3"
|
version = "0.59.3"
|
||||||
@@ -6716,6 +6867,15 @@ dependencies = [
|
|||||||
"windows-link 0.1.3",
|
"windows-link 0.1.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-result"
|
name = "windows-result"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
@@ -6734,6 +6894,16 @@ dependencies = [
|
|||||||
"windows-link 0.2.1",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
||||||
|
dependencies = [
|
||||||
|
"windows-result 0.2.0",
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-strings"
|
name = "windows-strings"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
@@ -7104,7 +7274,7 @@ dependencies = [
|
|||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webkit2gtk-sys",
|
"webkit2gtk-sys",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"windows",
|
"windows 0.61.3",
|
||||||
"windows-core 0.61.2",
|
"windows-core 0.61.2",
|
||||||
"windows-version",
|
"windows-version",
|
||||||
"x11-dl",
|
"x11-dl",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ tauri-build = { version = "2", features = [] }
|
|||||||
tauri = { version = "2", features = [] }
|
tauri = { version = "2", features = [] }
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
|
tauri-plugin-drag = "2.1.1"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"opener:allow-reveal-item-in-dir",
|
"opener:allow-reveal-item-in-dir",
|
||||||
"dialog:default",
|
"dialog:default",
|
||||||
"dialog:allow-open",
|
"dialog:allow-open",
|
||||||
"dialog:allow-save"
|
"dialog:allow-save",
|
||||||
|
"drag:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-12
@@ -27,7 +27,7 @@ const SCHEMA: [&str; 5] = [
|
|||||||
"CREATE TABLE IF NOT EXISTS projects (
|
"CREATE TABLE IF NOT EXISTS projects (
|
||||||
path TEXT PRIMARY KEY,
|
path TEXT PRIMARY KEY,
|
||||||
entrypoint TEXT NOT NULL DEFAULT 'main.typ',
|
entrypoint TEXT NOT NULL DEFAULT 'main.typ',
|
||||||
space_id TEXT,
|
cloud_project_id TEXT,
|
||||||
last_synced_at TEXT
|
last_synced_at TEXT
|
||||||
)",
|
)",
|
||||||
"CREATE TABLE IF NOT EXISTS base_files (
|
"CREATE TABLE IF NOT EXISTS base_files (
|
||||||
@@ -53,8 +53,10 @@ const SCHEMA: [&str; 5] = [
|
|||||||
)",
|
)",
|
||||||
];
|
];
|
||||||
|
|
||||||
const MIGRATIONS: [&str; 1] =
|
const MIGRATIONS: [&str; 2] = [
|
||||||
["ALTER TABLE document_links ADD COLUMN synced_at TEXT"];
|
"ALTER TABLE document_links ADD COLUMN synced_at TEXT",
|
||||||
|
"ALTER TABLE projects RENAME COLUMN space_id TO cloud_project_id",
|
||||||
|
];
|
||||||
|
|
||||||
impl Store {
|
impl Store {
|
||||||
pub fn open(app: &AppHandle) -> Result<Self, String> {
|
pub fn open(app: &AppHandle) -> Result<Self, String> {
|
||||||
@@ -130,14 +132,14 @@ impl Store {
|
|||||||
let row: Option<(String, Option<String>, Option<String>)> = self.with(|connection| {
|
let row: Option<(String, Option<String>, Option<String>)> = self.with(|connection| {
|
||||||
connection
|
connection
|
||||||
.query_row(
|
.query_row(
|
||||||
"SELECT entrypoint, space_id, last_synced_at FROM projects WHERE path = ?1",
|
"SELECT entrypoint, cloud_project_id, last_synced_at FROM projects WHERE path = ?1",
|
||||||
params![project],
|
params![project],
|
||||||
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
|
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
|
||||||
)
|
)
|
||||||
.optional()
|
.optional()
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let Some((entrypoint, space_id, last_synced_at)) = row else {
|
let Some((entrypoint, cloud_project_id, last_synced_at)) = row else {
|
||||||
return Ok(ProjectMeta::default());
|
return Ok(ProjectMeta::default());
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -158,7 +160,7 @@ impl Store {
|
|||||||
|
|
||||||
Ok(ProjectMeta {
|
Ok(ProjectMeta {
|
||||||
entrypoint,
|
entrypoint,
|
||||||
space_id,
|
cloud_project_id,
|
||||||
last_synced_at,
|
last_synced_at,
|
||||||
base_hashes,
|
base_hashes,
|
||||||
})
|
})
|
||||||
@@ -180,16 +182,16 @@ impl Store {
|
|||||||
pub fn save_meta(&self, project: &str, meta: &ProjectMeta) -> Result<(), String> {
|
pub fn save_meta(&self, project: &str, meta: &ProjectMeta) -> Result<(), String> {
|
||||||
self.with(|connection| {
|
self.with(|connection| {
|
||||||
connection.execute(
|
connection.execute(
|
||||||
"INSERT INTO projects (path, entrypoint, space_id, last_synced_at)
|
"INSERT INTO projects (path, entrypoint, cloud_project_id, last_synced_at)
|
||||||
VALUES (?1, ?2, ?3, ?4)
|
VALUES (?1, ?2, ?3, ?4)
|
||||||
ON CONFLICT(path) DO UPDATE SET
|
ON CONFLICT(path) DO UPDATE SET
|
||||||
entrypoint = excluded.entrypoint,
|
entrypoint = excluded.entrypoint,
|
||||||
space_id = excluded.space_id,
|
cloud_project_id = excluded.cloud_project_id,
|
||||||
last_synced_at = excluded.last_synced_at",
|
last_synced_at = excluded.last_synced_at",
|
||||||
params![
|
params![
|
||||||
project,
|
project,
|
||||||
meta.entrypoint,
|
meta.entrypoint,
|
||||||
meta.space_id,
|
meta.cloud_project_id,
|
||||||
meta.last_synced_at
|
meta.last_synced_at
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
@@ -335,11 +337,11 @@ impl Store {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn all_space_links(&self) -> Result<Vec<(String, String, Option<String>)>, String> {
|
pub fn all_cloud_project_links(&self) -> Result<Vec<(String, String, Option<String>)>, String> {
|
||||||
self.with(|connection| {
|
self.with(|connection| {
|
||||||
let mut statement = connection.prepare(
|
let mut statement = connection.prepare(
|
||||||
"SELECT path, space_id, last_synced_at FROM projects
|
"SELECT path, cloud_project_id, last_synced_at FROM projects
|
||||||
WHERE space_id IS NOT NULL",
|
WHERE cloud_project_id IS NOT NULL",
|
||||||
)?;
|
)?;
|
||||||
let rows = statement.query_map([], |row| {
|
let rows = statement.query_map([], |row| {
|
||||||
Ok((
|
Ok((
|
||||||
|
|||||||
+64
-30
@@ -16,7 +16,7 @@ use assets::Asset;
|
|||||||
use db::Store;
|
use db::Store;
|
||||||
use compiler::{CompileResult, Diagnostic};
|
use compiler::{CompileResult, Diagnostic};
|
||||||
use lsp::{LspHandle, LspState};
|
use lsp::{LspHandle, LspState};
|
||||||
use sync::{Account, SpaceSummary, SyncReport};
|
use sync::{Account, ProjectSummary, SyncReport};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
browse, is_project_dir, is_text_file, list_files, load_settings, project_file_path,
|
browse, is_project_dir, is_text_file, list_files, load_settings, project_file_path,
|
||||||
read_target_files, resolve_target, save_settings, workspace_path, BrowseEntry,
|
read_target_files, resolve_target, save_settings, workspace_path, BrowseEntry,
|
||||||
@@ -351,7 +351,7 @@ pub struct TargetInfo {
|
|||||||
pub entrypoint: String,
|
pub entrypoint: String,
|
||||||
pub standalone: bool,
|
pub standalone: bool,
|
||||||
pub is_project: bool,
|
pub is_project: bool,
|
||||||
pub space_id: Option<String>,
|
pub cloud_project_id: Option<String>,
|
||||||
pub files: Vec<FileEntry>,
|
pub files: Vec<FileEntry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,7 +368,7 @@ fn target_info(app: AppHandle, store: State<'_, Store>, path: String) -> Result<
|
|||||||
entrypoint: target.entrypoint.clone(),
|
entrypoint: target.entrypoint.clone(),
|
||||||
standalone: true,
|
standalone: true,
|
||||||
is_project: false,
|
is_project: false,
|
||||||
space_id: None,
|
cloud_project_id: None,
|
||||||
files: vec![FileEntry {
|
files: vec![FileEntry {
|
||||||
path: target.entrypoint.clone(),
|
path: target.entrypoint.clone(),
|
||||||
name: target.entrypoint,
|
name: target.entrypoint,
|
||||||
@@ -385,7 +385,7 @@ fn target_info(app: AppHandle, store: State<'_, Store>, path: String) -> Result<
|
|||||||
entrypoint: target.entrypoint,
|
entrypoint: target.entrypoint,
|
||||||
standalone: false,
|
standalone: false,
|
||||||
is_project: is_project_dir(&target.root),
|
is_project: is_project_dir(&target.root),
|
||||||
space_id: meta.space_id,
|
cloud_project_id: meta.cloud_project_id,
|
||||||
files: list_files(&target.root)?,
|
files: list_files(&target.root)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -725,9 +725,9 @@ fn cloud_account(app: AppHandle, store: State<'_, Store>) -> Result<Option<Accou
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn cloud_list_spaces(app: AppHandle, store: State<'_, Store>) -> Result<Vec<SpaceSummary>, String> {
|
fn cloud_list_projects(app: AppHandle, store: State<'_, Store>) -> Result<Vec<ProjectSummary>, String> {
|
||||||
let (server_url, token) = cloud_credentials(&app, &store)?;
|
let (server_url, token) = cloud_credentials(&app, &store)?;
|
||||||
sync::list_spaces(&server_url, &token)
|
sync::list_cloud_projects(&server_url, &token)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -864,6 +864,30 @@ fn cloud_unlink_document(store: State<'_, Store>, path: String) -> Result<(), St
|
|||||||
store.forget_document_link(&path)
|
store.forget_document_link(&path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn cloud_create_document(
|
||||||
|
app: AppHandle,
|
||||||
|
store: State<'_, Store>,
|
||||||
|
path: String,
|
||||||
|
title: String,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let (server_url, token) = cloud_credentials(&app, &store)?;
|
||||||
|
let full = workspace_path(&app, &store, &path)?;
|
||||||
|
let content = std::fs::read_to_string(&full).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let document = sync::create_document(&server_url, &token, title.trim(), &content)?;
|
||||||
|
|
||||||
|
store.save_document_link(
|
||||||
|
&path,
|
||||||
|
&document.id,
|
||||||
|
&document.hash,
|
||||||
|
&document.role,
|
||||||
|
&document.content,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(document.id)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct LinkedDocument {
|
pub struct LinkedDocument {
|
||||||
pub path: String,
|
pub path: String,
|
||||||
@@ -899,21 +923,21 @@ fn cloud_linked_documents(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct LinkedSpace {
|
pub struct LinkedProject {
|
||||||
pub path: String,
|
pub path: String,
|
||||||
pub space_id: String,
|
pub cloud_project_id: String,
|
||||||
pub synced_at: Option<String>,
|
pub synced_at: Option<String>,
|
||||||
pub sync_state: Option<String>,
|
pub sync_state: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn cloud_linked_spaces(
|
fn cloud_linked_projects(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
store: State<'_, Store>,
|
store: State<'_, Store>,
|
||||||
) -> Result<Vec<LinkedSpace>, String> {
|
) -> Result<Vec<LinkedProject>, String> {
|
||||||
let mut linked = Vec::new();
|
let mut linked = Vec::new();
|
||||||
|
|
||||||
for (path, space_id, synced_at) in store.all_space_links()? {
|
for (path, cloud_project_id, synced_at) in store.all_cloud_project_links()? {
|
||||||
let Ok(full) = workspace_path(&app, &store, &path) else {
|
let Ok(full) = workspace_path(&app, &store, &path) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -921,10 +945,10 @@ fn cloud_linked_spaces(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
linked.push(LinkedSpace {
|
linked.push(LinkedProject {
|
||||||
sync_state: workspace::project_sync_state(&full, synced_at.as_deref()),
|
sync_state: workspace::project_sync_state(&full, synced_at.as_deref()),
|
||||||
path,
|
path,
|
||||||
space_id,
|
cloud_project_id,
|
||||||
synced_at,
|
synced_at,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -941,22 +965,22 @@ fn cloud_document_link(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn cloud_create_space(app: AppHandle, store: State<'_, Store>, name: String) -> Result<SpaceSummary, String> {
|
fn cloud_create_project(app: AppHandle, store: State<'_, Store>, name: String) -> Result<ProjectSummary, String> {
|
||||||
let (server_url, token) = cloud_credentials(&app, &store)?;
|
let (server_url, token) = cloud_credentials(&app, &store)?;
|
||||||
sync::create_space(&server_url, &token, name.trim())
|
sync::create_cloud_project(&server_url, &token, name.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn cloud_delete_space(app: AppHandle, store: State<'_, Store>, space_id: String) -> Result<(), String> {
|
fn cloud_delete_project(app: AppHandle, store: State<'_, Store>, cloud_project_id: String) -> Result<(), String> {
|
||||||
let (server_url, token) = cloud_credentials(&app, &store)?;
|
let (server_url, token) = cloud_credentials(&app, &store)?;
|
||||||
sync::delete_space(&server_url, &token, &space_id)
|
sync::delete_cloud_project(&server_url, &token, &cloud_project_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn cloud_clone_space(
|
fn cloud_clone_project(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
store: State<'_, Store>,
|
store: State<'_, Store>,
|
||||||
space_id: String,
|
cloud_project_id: String,
|
||||||
project_name: String,
|
project_name: String,
|
||||||
) -> Result<SyncReport, String> {
|
) -> Result<SyncReport, String> {
|
||||||
let (server_url, token) = cloud_credentials(&app, &store)?;
|
let (server_url, token) = cloud_credentials(&app, &store)?;
|
||||||
@@ -965,7 +989,15 @@ fn cloud_clone_space(
|
|||||||
if dir.exists() {
|
if dir.exists() {
|
||||||
return Err(format!("A project named '{}' already exists", project_name));
|
return Err(format!("A project named '{}' already exists", project_name));
|
||||||
}
|
}
|
||||||
sync::clone_space(&server_url, &token, &app, &store, &project, &dir, &space_id)
|
sync::clone_cloud_project(
|
||||||
|
&server_url,
|
||||||
|
&token,
|
||||||
|
&app,
|
||||||
|
&store,
|
||||||
|
&project,
|
||||||
|
&dir,
|
||||||
|
&cloud_project_id,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -973,17 +1005,17 @@ fn cloud_link_project(
|
|||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
store: State<'_, Store>,
|
store: State<'_, Store>,
|
||||||
project: String,
|
project: String,
|
||||||
space_id: Option<String>,
|
cloud_project_id: Option<String>,
|
||||||
) -> Result<SyncReport, String> {
|
) -> Result<SyncReport, String> {
|
||||||
let (server_url, token) = cloud_credentials(&app, &store)?;
|
let (server_url, token) = cloud_credentials(&app, &store)?;
|
||||||
let (dir, mut meta) = load_project(&app, &store, &project)?;
|
let (dir, mut meta) = load_project(&app, &store, &project)?;
|
||||||
|
|
||||||
let space_id = match space_id {
|
let cloud_project_id = match cloud_project_id {
|
||||||
Some(id) if !id.trim().is_empty() => id,
|
Some(id) if !id.trim().is_empty() => id,
|
||||||
_ => sync::create_space(&server_url, &token, &project)?.id,
|
_ => sync::create_cloud_project(&server_url, &token, &project)?.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
meta.space_id = Some(space_id);
|
meta.cloud_project_id = Some(cloud_project_id);
|
||||||
meta.base_hashes.clear();
|
meta.base_hashes.clear();
|
||||||
store.save_meta(&project, &meta)?;
|
store.save_meta(&project, &meta)?;
|
||||||
|
|
||||||
@@ -993,7 +1025,7 @@ fn cloud_link_project(
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn cloud_unlink_project(app: AppHandle, store: State<'_, Store>, project: String) -> Result<(), String> {
|
fn cloud_unlink_project(app: AppHandle, store: State<'_, Store>, project: String) -> Result<(), String> {
|
||||||
let (dir, mut meta) = load_project(&app, &store, &project)?;
|
let (dir, mut meta) = load_project(&app, &store, &project)?;
|
||||||
meta.space_id = None;
|
meta.cloud_project_id = None;
|
||||||
meta.base_hashes.clear();
|
meta.base_hashes.clear();
|
||||||
meta.last_synced_at = None;
|
meta.last_synced_at = None;
|
||||||
let _ = dir;
|
let _ = dir;
|
||||||
@@ -1069,6 +1101,7 @@ pub fn run() {
|
|||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
|
.plugin(tauri_plugin_drag::init())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let store = Store::open(&app.handle())?;
|
let store = Store::open(&app.handle())?;
|
||||||
app.manage(store);
|
app.manage(store);
|
||||||
@@ -1119,7 +1152,7 @@ pub fn run() {
|
|||||||
cloud_login,
|
cloud_login,
|
||||||
cloud_logout,
|
cloud_logout,
|
||||||
cloud_account,
|
cloud_account,
|
||||||
cloud_list_spaces,
|
cloud_list_projects,
|
||||||
cloud_list_folders,
|
cloud_list_folders,
|
||||||
cloud_list_documents,
|
cloud_list_documents,
|
||||||
cloud_list_shared,
|
cloud_list_shared,
|
||||||
@@ -1130,11 +1163,12 @@ pub fn run() {
|
|||||||
cloud_resolve_document,
|
cloud_resolve_document,
|
||||||
cloud_document_link,
|
cloud_document_link,
|
||||||
cloud_linked_documents,
|
cloud_linked_documents,
|
||||||
cloud_linked_spaces,
|
cloud_linked_projects,
|
||||||
cloud_unlink_document,
|
cloud_unlink_document,
|
||||||
cloud_create_space,
|
cloud_create_document,
|
||||||
cloud_delete_space,
|
cloud_create_project,
|
||||||
cloud_clone_space,
|
cloud_delete_project,
|
||||||
|
cloud_clone_project,
|
||||||
cloud_link_project,
|
cloud_link_project,
|
||||||
cloud_unlink_project,
|
cloud_unlink_project,
|
||||||
cloud_push,
|
cloud_push,
|
||||||
|
|||||||
+82
-37
@@ -87,7 +87,7 @@ pub fn me(server_url: &str, token: &str) -> Result<Account, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
pub struct SpaceSummary {
|
pub struct ProjectSummary {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub entrypoint: String,
|
pub entrypoint: String,
|
||||||
@@ -95,29 +95,37 @@ pub struct SpaceSummary {
|
|||||||
pub updated_at: String,
|
pub updated_at: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_spaces(server_url: &str, token: &str) -> Result<Vec<SpaceSummary>, String> {
|
pub fn list_cloud_projects(server_url: &str, token: &str) -> Result<Vec<ProjectSummary>, String> {
|
||||||
agent()
|
agent()
|
||||||
.get(&endpoint(server_url, "/spaces"))
|
.get(&endpoint(server_url, "/projects"))
|
||||||
.set("Authorization", &format!("Bearer {}", token))
|
.set("Authorization", &format!("Bearer {}", token))
|
||||||
.call()
|
.call()
|
||||||
.map_err(describe)?
|
.map_err(describe)?
|
||||||
.into_json::<Vec<SpaceSummary>>()
|
.into_json::<Vec<ProjectSummary>>()
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_space(server_url: &str, token: &str, name: &str) -> Result<SpaceSummary, String> {
|
pub fn create_cloud_project(
|
||||||
|
server_url: &str,
|
||||||
|
token: &str,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<ProjectSummary, String> {
|
||||||
agent()
|
agent()
|
||||||
.post(&endpoint(server_url, "/spaces"))
|
.post(&endpoint(server_url, "/projects"))
|
||||||
.set("Authorization", &format!("Bearer {}", token))
|
.set("Authorization", &format!("Bearer {}", token))
|
||||||
.send_json(ureq::json!({ "name": name }))
|
.send_json(ureq::json!({ "name": name }))
|
||||||
.map_err(describe)?
|
.map_err(describe)?
|
||||||
.into_json::<SpaceSummary>()
|
.into_json::<ProjectSummary>()
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_space(server_url: &str, token: &str, space_id: &str) -> Result<(), String> {
|
pub fn delete_cloud_project(
|
||||||
|
server_url: &str,
|
||||||
|
token: &str,
|
||||||
|
cloud_project_id: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
agent()
|
agent()
|
||||||
.delete(&endpoint(server_url, &format!("/spaces/{}", space_id)))
|
.delete(&endpoint(server_url, &format!("/projects/{}", cloud_project_id)))
|
||||||
.set("Authorization", &format!("Bearer {}", token))
|
.set("Authorization", &format!("Bearer {}", token))
|
||||||
.call()
|
.call()
|
||||||
.map_err(describe)?;
|
.map_err(describe)?;
|
||||||
@@ -131,7 +139,9 @@ pub struct ManifestEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct SpaceManifest {
|
pub struct ProjectManifest {
|
||||||
|
pub project_id: String,
|
||||||
|
pub name: String,
|
||||||
pub entrypoint: String,
|
pub entrypoint: String,
|
||||||
pub files: Vec<ManifestEntry>,
|
pub files: Vec<ManifestEntry>,
|
||||||
}
|
}
|
||||||
@@ -139,17 +149,17 @@ pub struct SpaceManifest {
|
|||||||
pub fn get_manifest(
|
pub fn get_manifest(
|
||||||
server_url: &str,
|
server_url: &str,
|
||||||
token: &str,
|
token: &str,
|
||||||
space_id: &str,
|
cloud_project_id: &str,
|
||||||
) -> Result<SpaceManifest, String> {
|
) -> Result<ProjectManifest, String> {
|
||||||
agent()
|
agent()
|
||||||
.get(&endpoint(
|
.get(&endpoint(
|
||||||
server_url,
|
server_url,
|
||||||
&format!("/spaces/{}/manifest", space_id),
|
&format!("/projects/{}/manifest", cloud_project_id),
|
||||||
))
|
))
|
||||||
.set("Authorization", &format!("Bearer {}", token))
|
.set("Authorization", &format!("Bearer {}", token))
|
||||||
.call()
|
.call()
|
||||||
.map_err(describe)?
|
.map_err(describe)?
|
||||||
.into_json::<SpaceManifest>()
|
.into_json::<ProjectManifest>()
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,11 +186,14 @@ impl FileContent {
|
|||||||
pub fn pull_file(
|
pub fn pull_file(
|
||||||
server_url: &str,
|
server_url: &str,
|
||||||
token: &str,
|
token: &str,
|
||||||
space_id: &str,
|
cloud_project_id: &str,
|
||||||
path: &str,
|
path: &str,
|
||||||
) -> Result<FileContent, String> {
|
) -> Result<FileContent, String> {
|
||||||
agent()
|
agent()
|
||||||
.get(&endpoint(server_url, &format!("/spaces/{}/file", space_id)))
|
.get(&endpoint(
|
||||||
|
server_url,
|
||||||
|
&format!("/projects/{}/file", cloud_project_id),
|
||||||
|
))
|
||||||
.query("path", path)
|
.query("path", path)
|
||||||
.set("Authorization", &format!("Bearer {}", token))
|
.set("Authorization", &format!("Bearer {}", token))
|
||||||
.call()
|
.call()
|
||||||
@@ -204,7 +217,7 @@ pub enum PushResult {
|
|||||||
pub fn push_file(
|
pub fn push_file(
|
||||||
server_url: &str,
|
server_url: &str,
|
||||||
token: &str,
|
token: &str,
|
||||||
space_id: &str,
|
cloud_project_id: &str,
|
||||||
path: &str,
|
path: &str,
|
||||||
bytes: &[u8],
|
bytes: &[u8],
|
||||||
base_hash: Option<&str>,
|
base_hash: Option<&str>,
|
||||||
@@ -216,7 +229,10 @@ pub fn push_file(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let response = agent()
|
let response = agent()
|
||||||
.put(&endpoint(server_url, &format!("/spaces/{}/file", space_id)))
|
.put(&endpoint(
|
||||||
|
server_url,
|
||||||
|
&format!("/projects/{}/file", cloud_project_id),
|
||||||
|
))
|
||||||
.set("Authorization", &format!("Bearer {}", token))
|
.set("Authorization", &format!("Bearer {}", token))
|
||||||
.send_json(ureq::json!({
|
.send_json(ureq::json!({
|
||||||
"path": path,
|
"path": path,
|
||||||
@@ -248,11 +264,14 @@ pub fn push_file(
|
|||||||
pub fn delete_remote_file(
|
pub fn delete_remote_file(
|
||||||
server_url: &str,
|
server_url: &str,
|
||||||
token: &str,
|
token: &str,
|
||||||
space_id: &str,
|
cloud_project_id: &str,
|
||||||
path: &str,
|
path: &str,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let response = agent()
|
let response = agent()
|
||||||
.delete(&endpoint(server_url, &format!("/spaces/{}/file", space_id)))
|
.delete(&endpoint(
|
||||||
|
server_url,
|
||||||
|
&format!("/projects/{}/file", cloud_project_id),
|
||||||
|
))
|
||||||
.query("path", path)
|
.query("path", path)
|
||||||
.set("Authorization", &format!("Bearer {}", token))
|
.set("Authorization", &format!("Bearer {}", token))
|
||||||
.call();
|
.call();
|
||||||
@@ -306,12 +325,12 @@ pub fn pull_project(
|
|||||||
project_dir: &Path,
|
project_dir: &Path,
|
||||||
meta: &mut ProjectMeta,
|
meta: &mut ProjectMeta,
|
||||||
) -> Result<SyncReport, String> {
|
) -> Result<SyncReport, String> {
|
||||||
let space_id = meta
|
let cloud_project_id = meta
|
||||||
.space_id
|
.cloud_project_id
|
||||||
.clone()
|
.clone()
|
||||||
.ok_or("Project is not linked to a cloud space")?;
|
.ok_or("Project is not linked to a cloud project")?;
|
||||||
|
|
||||||
let manifest = get_manifest(server_url, token, &space_id)?;
|
let manifest = get_manifest(server_url, token, &cloud_project_id)?;
|
||||||
let mut report = SyncReport::default();
|
let mut report = SyncReport::default();
|
||||||
|
|
||||||
let local_files: HashSet<String> = collect_files(project_dir)?.into_iter().collect();
|
let local_files: HashSet<String> = collect_files(project_dir)?.into_iter().collect();
|
||||||
@@ -327,7 +346,7 @@ pub fn pull_project(
|
|||||||
if base.is_some() {
|
if base.is_some() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let remote = pull_file(server_url, token, &space_id, &entry.path)?;
|
let remote = pull_file(server_url, token, &cloud_project_id, &entry.path)?;
|
||||||
write_local(project_dir, &entry.path, &remote.bytes()?)?;
|
write_local(project_dir, &entry.path, &remote.bytes()?)?;
|
||||||
meta.base_hashes.insert(entry.path.clone(), remote.hash);
|
meta.base_hashes.insert(entry.path.clone(), remote.hash);
|
||||||
report.pulled.push(entry.path.clone());
|
report.pulled.push(entry.path.clone());
|
||||||
@@ -346,7 +365,7 @@ pub fn pull_project(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let remote = pull_file(server_url, token, &space_id, &entry.path)?;
|
let remote = pull_file(server_url, token, &cloud_project_id, &entry.path)?;
|
||||||
let remote_bytes = remote.bytes()?;
|
let remote_bytes = remote.bytes()?;
|
||||||
|
|
||||||
if base.as_deref() == Some(local_hash.as_str()) {
|
if base.as_deref() == Some(local_hash.as_str()) {
|
||||||
@@ -428,10 +447,10 @@ pub fn push_project(
|
|||||||
project_dir: &Path,
|
project_dir: &Path,
|
||||||
meta: &mut ProjectMeta,
|
meta: &mut ProjectMeta,
|
||||||
) -> Result<SyncReport, String> {
|
) -> Result<SyncReport, String> {
|
||||||
let space_id = meta
|
let cloud_project_id = meta
|
||||||
.space_id
|
.cloud_project_id
|
||||||
.clone()
|
.clone()
|
||||||
.ok_or("Project is not linked to a cloud space")?;
|
.ok_or("Project is not linked to a cloud project")?;
|
||||||
|
|
||||||
let mut report = SyncReport::default();
|
let mut report = SyncReport::default();
|
||||||
let local_files = collect_files(project_dir)?;
|
let local_files = collect_files(project_dir)?;
|
||||||
@@ -446,7 +465,14 @@ pub fn push_project(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
match push_file(server_url, token, &space_id, path, &bytes, base.as_deref())? {
|
match push_file(
|
||||||
|
server_url,
|
||||||
|
token,
|
||||||
|
&cloud_project_id,
|
||||||
|
path,
|
||||||
|
&bytes,
|
||||||
|
base.as_deref(),
|
||||||
|
)? {
|
||||||
PushResult::Applied => {
|
PushResult::Applied => {
|
||||||
meta.base_hashes.insert(path.clone(), hash);
|
meta.base_hashes.insert(path.clone(), hash);
|
||||||
report.pushed.push(path.clone());
|
report.pushed.push(path.clone());
|
||||||
@@ -481,7 +507,7 @@ pub fn push_project(
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for path in removed {
|
for path in removed {
|
||||||
delete_remote_file(server_url, token, &space_id, &path)?;
|
delete_remote_file(server_url, token, &cloud_project_id, &path)?;
|
||||||
meta.base_hashes.remove(&path);
|
meta.base_hashes.remove(&path);
|
||||||
report.deleted_remote.push(path);
|
report.deleted_remote.push(path);
|
||||||
}
|
}
|
||||||
@@ -522,20 +548,20 @@ pub fn report_progress(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clone_space(
|
pub fn clone_cloud_project(
|
||||||
server_url: &str,
|
server_url: &str,
|
||||||
token: &str,
|
token: &str,
|
||||||
app: &tauri::AppHandle,
|
app: &tauri::AppHandle,
|
||||||
store: &Store,
|
store: &Store,
|
||||||
project: &str,
|
project: &str,
|
||||||
project_dir: &Path,
|
project_dir: &Path,
|
||||||
space_id: &str,
|
cloud_project_id: &str,
|
||||||
) -> Result<SyncReport, String> {
|
) -> Result<SyncReport, String> {
|
||||||
std::fs::create_dir_all(project_dir).map_err(|e| e.to_string())?;
|
std::fs::create_dir_all(project_dir).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let manifest = get_manifest(server_url, token, space_id)?;
|
let manifest = get_manifest(server_url, token, cloud_project_id)?;
|
||||||
let mut meta = store.meta(project)?;
|
let mut meta = store.meta(project)?;
|
||||||
meta.space_id = Some(space_id.to_string());
|
meta.cloud_project_id = Some(cloud_project_id.to_string());
|
||||||
meta.entrypoint = manifest.entrypoint.clone();
|
meta.entrypoint = manifest.entrypoint.clone();
|
||||||
|
|
||||||
let mut report = SyncReport::default();
|
let mut report = SyncReport::default();
|
||||||
@@ -544,7 +570,7 @@ pub fn clone_space(
|
|||||||
for (index, entry) in manifest.files.iter().enumerate() {
|
for (index, entry) in manifest.files.iter().enumerate() {
|
||||||
report_progress(app, project, index, total, false);
|
report_progress(app, project, index, total, false);
|
||||||
|
|
||||||
let remote = pull_file(server_url, token, space_id, &entry.path)?;
|
let remote = pull_file(server_url, token, cloud_project_id, &entry.path)?;
|
||||||
write_local(project_dir, &entry.path, &remote.bytes()?)?;
|
write_local(project_dir, &entry.path, &remote.bytes()?)?;
|
||||||
meta.base_hashes.insert(entry.path.clone(), remote.hash);
|
meta.base_hashes.insert(entry.path.clone(), remote.hash);
|
||||||
report.pulled.push(entry.path.clone());
|
report.pulled.push(entry.path.clone());
|
||||||
@@ -621,7 +647,7 @@ pub struct CloudDocument {
|
|||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
pub struct SharedItems {
|
pub struct SharedItems {
|
||||||
pub documents: Vec<CloudDocument>,
|
pub documents: Vec<CloudDocument>,
|
||||||
pub spaces: Vec<SpaceSummary>,
|
pub projects: Vec<ProjectSummary>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_folders(server_url: &str, token: &str) -> Result<Vec<CloudFolder>, String> {
|
pub fn list_folders(server_url: &str, token: &str) -> Result<Vec<CloudFolder>, String> {
|
||||||
@@ -687,6 +713,25 @@ pub fn pull_document(
|
|||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn create_document(
|
||||||
|
server_url: &str,
|
||||||
|
token: &str,
|
||||||
|
title: &str,
|
||||||
|
content: &str,
|
||||||
|
) -> Result<DocumentContent, String> {
|
||||||
|
agent()
|
||||||
|
.post(&endpoint(server_url, "/documents"))
|
||||||
|
.set("Authorization", &format!("Bearer {}", token))
|
||||||
|
.send_json(ureq::json!({
|
||||||
|
"title": title,
|
||||||
|
"content": content,
|
||||||
|
"folder_id": null,
|
||||||
|
}))
|
||||||
|
.map_err(describe)?
|
||||||
|
.into_json::<DocumentContent>()
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn sync_document(
|
pub fn sync_document(
|
||||||
server_url: &str,
|
server_url: &str,
|
||||||
token: &str,
|
token: &str,
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ pub fn workspace_root(app: &AppHandle, store: &Store) -> Result<PathBuf, String>
|
|||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct ProjectMeta {
|
pub struct ProjectMeta {
|
||||||
pub entrypoint: String,
|
pub entrypoint: String,
|
||||||
pub space_id: Option<String>,
|
pub cloud_project_id: Option<String>,
|
||||||
pub last_synced_at: Option<String>,
|
pub last_synced_at: Option<String>,
|
||||||
pub base_hashes: HashMap<String, String>,
|
pub base_hashes: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,7 @@ impl Default for ProjectMeta {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
ProjectMeta {
|
ProjectMeta {
|
||||||
entrypoint: "main.typ".to_string(),
|
entrypoint: "main.typ".to_string(),
|
||||||
space_id: None,
|
cloud_project_id: None,
|
||||||
last_synced_at: None,
|
last_synced_at: None,
|
||||||
base_hashes: HashMap::new(),
|
base_hashes: HashMap::new(),
|
||||||
}
|
}
|
||||||
@@ -154,7 +154,7 @@ pub struct BrowseEntry {
|
|||||||
pub kind: String,
|
pub kind: String,
|
||||||
pub size: u64,
|
pub size: u64,
|
||||||
pub modified: Option<String>,
|
pub modified: Option<String>,
|
||||||
pub space_id: Option<String>,
|
pub cloud_project_id: Option<String>,
|
||||||
pub last_synced_at: Option<String>,
|
pub last_synced_at: Option<String>,
|
||||||
pub child_count: usize,
|
pub child_count: usize,
|
||||||
pub cloud_linked: bool,
|
pub cloud_linked: bool,
|
||||||
@@ -259,9 +259,9 @@ pub fn browse(app: &AppHandle, store: &Store, relative: &str) -> Result<Vec<Brow
|
|||||||
})
|
})
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
let space_id = meta.as_ref().and_then(|m| m.space_id.clone());
|
let cloud_project_id = meta.as_ref().and_then(|m| m.cloud_project_id.clone());
|
||||||
let last_synced_at = meta.as_ref().and_then(|m| m.last_synced_at.clone());
|
let last_synced_at = meta.as_ref().and_then(|m| m.last_synced_at.clone());
|
||||||
let sync_state = if space_id.is_some() {
|
let sync_state = if cloud_project_id.is_some() {
|
||||||
sync_state_for(newest_change(&full), last_synced_at.as_deref())
|
sync_state_for(newest_change(&full), last_synced_at.as_deref())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -273,8 +273,8 @@ pub fn browse(app: &AppHandle, store: &Store, relative: &str) -> Result<Vec<Brow
|
|||||||
kind: if project { "project" } else { "folder" }.to_string(),
|
kind: if project { "project" } else { "folder" }.to_string(),
|
||||||
size: 0,
|
size: 0,
|
||||||
modified: modified_at(&full),
|
modified: modified_at(&full),
|
||||||
cloud_linked: space_id.is_some(),
|
cloud_linked: cloud_project_id.is_some(),
|
||||||
space_id,
|
cloud_project_id,
|
||||||
last_synced_at,
|
last_synced_at,
|
||||||
sync_state,
|
sync_state,
|
||||||
child_count,
|
child_count,
|
||||||
@@ -299,7 +299,7 @@ pub fn browse(app: &AppHandle, store: &Store, relative: &str) -> Result<Vec<Brow
|
|||||||
kind: kind.to_string(),
|
kind: kind.to_string(),
|
||||||
size: std::fs::metadata(&full).map(|m| m.len()).unwrap_or(0),
|
size: std::fs::metadata(&full).map(|m| m.len()).unwrap_or(0),
|
||||||
modified: modified_at(&full),
|
modified: modified_at(&full),
|
||||||
space_id: None,
|
cloud_project_id: None,
|
||||||
last_synced_at: link.as_ref().and_then(|link| link.synced_at.clone()),
|
last_synced_at: link.as_ref().and_then(|link| link.synced_at.clone()),
|
||||||
child_count: 0,
|
child_count: 0,
|
||||||
cloud_linked: link.is_some(),
|
cloud_linked: link.is_some(),
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
],
|
],
|
||||||
|
"resources": ["icons/32x32.png"],
|
||||||
"linux": {
|
"linux": {
|
||||||
"deb": {
|
"deb": {
|
||||||
"depends": [
|
"depends": [
|
||||||
|
|||||||
+24
@@ -31,6 +31,30 @@
|
|||||||
--color-success: #4cc47f;
|
--color-success: #4cc47f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root[data-contrast="high"] {
|
||||||
|
--color-line: #9aa0ab;
|
||||||
|
--color-ink-muted: #33363c;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"][data-contrast="high"] {
|
||||||
|
--color-line: #545b66;
|
||||||
|
--color-ink-muted: #c7cdd6;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-contrast="high"] :focus-visible {
|
||||||
|
outline: 2px solid var(--color-accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-reduce-motion="true"] *,
|
||||||
|
[data-reduce-motion="true"] *::before,
|
||||||
|
[data-reduce-motion="true"] *::after {
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Icon from "@iconify/svelte";
|
import Icon from "@iconify/svelte";
|
||||||
import type { FileEntry } from "$lib/ts/api";
|
import { resolveResource } from "@tauri-apps/api/path";
|
||||||
|
import { startDrag } from "@crabnebula/tauri-plugin-drag";
|
||||||
|
import { absolutePath, type FileEntry } from "$lib/ts/api";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
files: FileEntry[];
|
files: FileEntry[];
|
||||||
activePath: string | null;
|
activePath: string | null;
|
||||||
entrypoint: string;
|
entrypoint: string;
|
||||||
selected: string | null;
|
targetPath: string;
|
||||||
|
selected: Set<string>;
|
||||||
dropTarget: string | null;
|
dropTarget: string | null;
|
||||||
onopen: (path: string) => void;
|
onopen: (path: string) => void;
|
||||||
onselect: (path: string | null, isDir: boolean) => void;
|
onselect: (paths: string[], primary: string | null, isDir: boolean) => void;
|
||||||
onrename: (path: string) => void;
|
onrename: (path: string) => void;
|
||||||
ondelete: (path: string) => void;
|
ondelete: (paths: string[]) => void;
|
||||||
onduplicate: (path: string) => void;
|
onduplicate: (path: string) => void;
|
||||||
onreveal: (path: string) => void;
|
onreveal: (path: string) => void;
|
||||||
onsetentry: (path: string) => void;
|
onsetentry: (path: string) => void;
|
||||||
@@ -25,6 +28,7 @@
|
|||||||
files,
|
files,
|
||||||
activePath,
|
activePath,
|
||||||
entrypoint,
|
entrypoint,
|
||||||
|
targetPath,
|
||||||
selected,
|
selected,
|
||||||
dropTarget,
|
dropTarget,
|
||||||
onopen,
|
onopen,
|
||||||
@@ -111,9 +115,59 @@
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
let dragging = $state<string | null>(null);
|
let dragging = $state<string | null>(null);
|
||||||
|
let dragPaths = $state<string[]>([]);
|
||||||
|
let anchorPath = $state<string | null>(null);
|
||||||
|
let nativeDragSent = false;
|
||||||
|
let iconPathPromise: Promise<string> | null = null;
|
||||||
|
|
||||||
|
function flattenVisible(nodes: TreeNode[]): TreeNode[] {
|
||||||
|
const result: TreeNode[] = [];
|
||||||
|
for (const node of nodes) {
|
||||||
|
result.push(node);
|
||||||
|
if (node.isDir && node.children.length > 0 && !collapsed[node.path]) {
|
||||||
|
result.push(...flattenVisible(node.children));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectRange(from: string, to: TreeNode) {
|
||||||
|
const flat = flattenVisible(tree);
|
||||||
|
const fromIndex = flat.findIndex((node) => node.path === from);
|
||||||
|
const toIndex = flat.findIndex((node) => node.path === to.path);
|
||||||
|
|
||||||
|
if (fromIndex === -1 || toIndex === -1) {
|
||||||
|
onselect([to.path], to.path, to.isDir);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [start, end] =
|
||||||
|
fromIndex < toIndex ? [fromIndex, toIndex] : [toIndex, fromIndex];
|
||||||
|
const paths = flat.slice(start, end + 1).map((node) => node.path);
|
||||||
|
onselect(paths, to.path, to.isDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRowClick(event: MouseEvent, node: TreeNode) {
|
||||||
|
if (event.shiftKey && anchorPath) {
|
||||||
|
selectRange(anchorPath, node);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.ctrlKey || event.metaKey) {
|
||||||
|
const next = new Set(selected);
|
||||||
|
if (next.has(node.path)) {
|
||||||
|
next.delete(node.path);
|
||||||
|
} else {
|
||||||
|
next.add(node.path);
|
||||||
|
}
|
||||||
|
anchorPath = node.path;
|
||||||
|
onselect([...next], node.path, node.isDir);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
anchorPath = node.path;
|
||||||
|
onselect([node.path], node.path, node.isDir);
|
||||||
|
|
||||||
function activate(node: TreeNode) {
|
|
||||||
onselect(node.path, node.isDir);
|
|
||||||
if (node.isDir) {
|
if (node.isDir) {
|
||||||
collapsed[node.path] = !collapsed[node.path];
|
collapsed[node.path] = !collapsed[node.path];
|
||||||
} else {
|
} else {
|
||||||
@@ -123,7 +177,10 @@
|
|||||||
|
|
||||||
function openMenu(event: MouseEvent, node: TreeNode) {
|
function openMenu(event: MouseEvent, node: TreeNode) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onselect(node.path, node.isDir);
|
if (!selected.has(node.path)) {
|
||||||
|
anchorPath = node.path;
|
||||||
|
onselect([node.path], node.path, node.isDir);
|
||||||
|
}
|
||||||
menu = { path: node.path, isDir: node.isDir, x: event.clientX, y: event.clientY };
|
menu = { path: node.path, isDir: node.isDir, x: event.clientX, y: event.clientY };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +191,9 @@
|
|||||||
}
|
}
|
||||||
if (event.key === "Delete") {
|
if (event.key === "Delete") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
ondelete(node.path);
|
const paths =
|
||||||
|
selected.has(node.path) && selected.size > 1 ? [...selected] : [node.path];
|
||||||
|
ondelete(paths);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,6 +216,41 @@
|
|||||||
const index = path.lastIndexOf("/");
|
const index = path.lastIndexOf("/");
|
||||||
return index === -1 ? "" : path.slice(0, index);
|
return index === -1 ? "" : path.slice(0, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function joinTargetPath(path: string): string {
|
||||||
|
return targetPath ? `${targetPath}/${path}` : path;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAbsolutePaths(paths: string[]): Promise<string[]> {
|
||||||
|
return Promise.all(paths.map((path) => absolutePath(joinTargetPath(path))));
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragIcon(): Promise<string> {
|
||||||
|
if (!iconPathPromise) iconPathPromise = resolveResource("icons/32x32.png");
|
||||||
|
return iconPathPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportViaOsDrag(paths: string[]) {
|
||||||
|
if (nativeDragSent || paths.length === 0) return;
|
||||||
|
nativeDragSent = true;
|
||||||
|
try {
|
||||||
|
const [absolutePaths, icon] = await Promise.all([
|
||||||
|
resolveAbsolutePaths(paths),
|
||||||
|
dragIcon(),
|
||||||
|
]);
|
||||||
|
await startDrag({ item: absolutePaths, icon });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to start native drag", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWindowDragLeave(event: DragEvent) {
|
||||||
|
if (!dragging || nativeDragSent) return;
|
||||||
|
// relatedTarget is null only when the pointer leaves the whole window,
|
||||||
|
// as opposed to moving between elements inside it.
|
||||||
|
if (event.relatedTarget !== null) return;
|
||||||
|
exportViaOsDrag(dragPaths);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window
|
<svelte:window
|
||||||
@@ -164,6 +258,7 @@
|
|||||||
on:contextmenu={(event) => {
|
on:contextmenu={(event) => {
|
||||||
if (!(event.target as HTMLElement).closest("[data-tree-row]")) menu = null;
|
if (!(event.target as HTMLElement).closest("[data-tree-row]")) menu = null;
|
||||||
}}
|
}}
|
||||||
|
on:dragleave={handleWindowDragLeave}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#snippet branch(nodes: TreeNode[], depth: number)}
|
{#snippet branch(nodes: TreeNode[], depth: number)}
|
||||||
@@ -175,26 +270,35 @@
|
|||||||
data-tree-dir={node.isDir ? "true" : "false"}
|
data-tree-dir={node.isDir ? "true" : "false"}
|
||||||
role="treeitem"
|
role="treeitem"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
aria-selected={node.path === selected}
|
aria-selected={selected.has(node.path)}
|
||||||
draggable="true"
|
draggable="true"
|
||||||
class="group flex items-center gap-1.5 rounded px-2 py-1 text-xs transition
|
class="group flex items-center gap-1.5 rounded px-2 py-1 text-xs transition
|
||||||
{node.path === activePath
|
{node.path === activePath
|
||||||
? 'bg-[var(--color-accent-soft)] text-[var(--color-accent)]'
|
? 'bg-[var(--color-accent-soft)] text-[var(--color-accent)]'
|
||||||
: node.path === selected
|
: selected.has(node.path)
|
||||||
? 'bg-[var(--color-surface-sunken)]'
|
? 'bg-[var(--color-surface-sunken)]'
|
||||||
: 'text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)]'}
|
: 'text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)]'}
|
||||||
{dropTarget === node.path
|
{dropTarget === node.path
|
||||||
? 'ring-1 ring-inset ring-[var(--color-accent)]'
|
? 'ring-1 ring-inset ring-[var(--color-accent)]'
|
||||||
: ''}"
|
: ''}"
|
||||||
style="padding-left: {depth * 12 + 8}px"
|
style="padding-left: {depth * 12 + 8}px"
|
||||||
onclick={() => activate(node)}
|
onclick={(event) => handleRowClick(event, node)}
|
||||||
oncontextmenu={(event) => openMenu(event, node)}
|
oncontextmenu={(event) => openMenu(event, node)}
|
||||||
onkeydown={(event) => handleKey(event, node)}
|
onkeydown={(event) => handleKey(event, node)}
|
||||||
ondragstart={(event) => {
|
ondragstart={(event) => {
|
||||||
dragging = node.path;
|
dragging = node.path;
|
||||||
|
dragPaths =
|
||||||
|
selected.has(node.path) && selected.size > 1
|
||||||
|
? [...selected]
|
||||||
|
: [node.path];
|
||||||
|
nativeDragSent = false;
|
||||||
event.dataTransfer?.setData("text/plain", node.path);
|
event.dataTransfer?.setData("text/plain", node.path);
|
||||||
}}
|
}}
|
||||||
ondragend={() => (dragging = null)}
|
ondragend={() => {
|
||||||
|
dragging = null;
|
||||||
|
dragPaths = [];
|
||||||
|
nativeDragSent = false;
|
||||||
|
}}
|
||||||
ondragover={(event) => {
|
ondragover={(event) => {
|
||||||
if (!dragging || !node.isDir) return;
|
if (!dragging || !node.isDir) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -254,7 +358,7 @@
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="rounded p-1 text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)]"
|
class="rounded p-1 text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)]"
|
||||||
onclick={() => onnewfile(selected ?? "")}
|
onclick={() => onnewfile(anchorPath ?? "")}
|
||||||
title="New file"
|
title="New file"
|
||||||
aria-label="New file"
|
aria-label="New file"
|
||||||
>
|
>
|
||||||
@@ -262,7 +366,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="rounded p-1 text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)]"
|
class="rounded p-1 text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)]"
|
||||||
onclick={() => onnewfolder(selected ?? "")}
|
onclick={() => onnewfolder(anchorPath ?? "")}
|
||||||
title="New folder"
|
title="New folder"
|
||||||
aria-label="New folder"
|
aria-label="New folder"
|
||||||
>
|
>
|
||||||
@@ -270,7 +374,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="rounded p-1 text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)]"
|
class="rounded p-1 text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)]"
|
||||||
onclick={() => onimport(selected ?? "")}
|
onclick={() => onimport(anchorPath ?? "")}
|
||||||
title="Import files"
|
title="Import files"
|
||||||
aria-label="Import files"
|
aria-label="Import files"
|
||||||
>
|
>
|
||||||
@@ -293,10 +397,16 @@
|
|||||||
data-tree-path=""
|
data-tree-path=""
|
||||||
data-tree-dir="true"
|
data-tree-dir="true"
|
||||||
onclick={(event) => {
|
onclick={(event) => {
|
||||||
if (event.target === event.currentTarget) onselect(null, true);
|
if (event.target === event.currentTarget) {
|
||||||
|
anchorPath = null;
|
||||||
|
onselect([], null, true);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onkeydown={(event) => {
|
onkeydown={(event) => {
|
||||||
if (event.key === "Escape") onselect(null, true);
|
if (event.key === "Escape") {
|
||||||
|
anchorPath = null;
|
||||||
|
onselect([], null, true);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
ondragover={(event) => {
|
ondragover={(event) => {
|
||||||
if (dragging) event.preventDefault();
|
if (dragging) event.preventDefault();
|
||||||
@@ -318,11 +428,14 @@
|
|||||||
|
|
||||||
{#if menu}
|
{#if menu}
|
||||||
{@const target = menu}
|
{@const target = menu}
|
||||||
|
{@const menuPaths =
|
||||||
|
selected.has(target.path) && selected.size > 1 ? [...selected] : [target.path]}
|
||||||
|
{@const single = menuPaths.length === 1}
|
||||||
<div
|
<div
|
||||||
class="fixed z-50 flex w-48 flex-col rounded-md border border-[var(--color-line)] bg-[var(--color-surface)] py-1 text-xs shadow-lg"
|
class="fixed z-50 flex w-48 flex-col rounded-md border border-[var(--color-line)] bg-[var(--color-surface)] py-1 text-xs shadow-lg"
|
||||||
style="left: {target.x}px; top: {target.y}px"
|
style="left: {target.x}px; top: {target.y}px"
|
||||||
>
|
>
|
||||||
{#if target.isDir}
|
{#if single && target.isDir}
|
||||||
<button
|
<button
|
||||||
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
||||||
onclick={() => onnewfile(target.path)}
|
onclick={() => onnewfile(target.path)}
|
||||||
@@ -342,7 +455,7 @@
|
|||||||
Import files here
|
Import files here
|
||||||
</button>
|
</button>
|
||||||
<div class="my-1 h-px bg-[var(--color-line)]"></div>
|
<div class="my-1 h-px bg-[var(--color-line)]"></div>
|
||||||
{:else if target.path.endsWith(".typ")}
|
{:else if single && target.path.endsWith(".typ")}
|
||||||
<button
|
<button
|
||||||
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
||||||
onclick={() => onsetentry(target.path)}
|
onclick={() => onsetentry(target.path)}
|
||||||
@@ -352,6 +465,7 @@
|
|||||||
<div class="my-1 h-px bg-[var(--color-line)]"></div>
|
<div class="my-1 h-px bg-[var(--color-line)]"></div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if single}
|
||||||
<button
|
<button
|
||||||
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
||||||
onclick={() => onrename(target.path)}
|
onclick={() => onrename(target.path)}
|
||||||
@@ -377,11 +491,13 @@
|
|||||||
Reveal in file manager
|
Reveal in file manager
|
||||||
</button>
|
</button>
|
||||||
<div class="my-1 h-px bg-[var(--color-line)]"></div>
|
<div class="my-1 h-px bg-[var(--color-line)]"></div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="px-3 py-1.5 text-left text-[var(--color-danger)] hover:bg-[var(--color-surface-sunken)]"
|
class="px-3 py-1.5 text-left text-[var(--color-danger)] hover:bg-[var(--color-surface-sunken)]"
|
||||||
onclick={() => ondelete(target.path)}
|
onclick={() => ondelete(menuPaths)}
|
||||||
>
|
>
|
||||||
Delete
|
{single ? "Delete" : `Delete ${menuPaths.length} items`}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
browseTo,
|
browseTo,
|
||||||
cloudBreadcrumbs,
|
cloudBreadcrumbs,
|
||||||
linkedDocument,
|
linkedDocument,
|
||||||
linkedSpace,
|
linkedProject,
|
||||||
openCloudFolder,
|
openCloudFolder,
|
||||||
openTarget,
|
openTarget,
|
||||||
refreshCloud,
|
refreshCloud,
|
||||||
@@ -22,13 +22,14 @@
|
|||||||
onrename: (entry: BrowseEntry) => void;
|
onrename: (entry: BrowseEntry) => void;
|
||||||
ondelete: (entry: BrowseEntry) => void;
|
ondelete: (entry: BrowseEntry) => void;
|
||||||
onlink: (entry: BrowseEntry) => void;
|
onlink: (entry: BrowseEntry) => void;
|
||||||
|
onsavetocloud: (entry: BrowseEntry) => void;
|
||||||
onviewimage: (paths: string[], index: number) => void;
|
onviewimage: (paths: string[], index: number) => void;
|
||||||
ondownloaddocument: (documentId: string, title: string) => void;
|
ondownloaddocument: (documentId: string, title: string) => void;
|
||||||
onremovedownload: (path: string) => void;
|
onremovedownload: (path: string) => void;
|
||||||
ondownloadfile: (fileId: string, name: string) => void;
|
ondownloadfile: (fileId: string, name: string) => void;
|
||||||
onclonespace: (spaceId: string, name: string) => void;
|
oncloneproject: (cloudProjectId: string, name: string) => void;
|
||||||
ondeletespace: (spaceId: string) => void;
|
ondeleteproject: (cloudProjectId: string) => void;
|
||||||
onnewspace: () => void;
|
onnewcloudproject: () => void;
|
||||||
onsignin: () => void;
|
onsignin: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,13 +41,14 @@
|
|||||||
onrename,
|
onrename,
|
||||||
ondelete,
|
ondelete,
|
||||||
onlink,
|
onlink,
|
||||||
|
onsavetocloud,
|
||||||
onviewimage,
|
onviewimage,
|
||||||
ondownloaddocument,
|
ondownloaddocument,
|
||||||
onremovedownload,
|
onremovedownload,
|
||||||
ondownloadfile,
|
ondownloadfile,
|
||||||
onclonespace,
|
oncloneproject,
|
||||||
ondeletespace,
|
ondeleteproject,
|
||||||
onnewspace,
|
onnewcloudproject,
|
||||||
onsignin,
|
onsignin,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
@@ -82,7 +84,7 @@
|
|||||||
|
|
||||||
const pending = [
|
const pending = [
|
||||||
...app.linkedDocuments.map((linked) => linked.path),
|
...app.linkedDocuments.map((linked) => linked.path),
|
||||||
...app.linkedSpaces.map((linked) => linked.path),
|
...app.linkedProjects.map((linked) => linked.path),
|
||||||
];
|
];
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
@@ -353,7 +355,7 @@
|
|||||||
Open image
|
Open image
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if entry.kind === "project" && !entry.space_id && app.account}
|
{#if entry.kind === "project" && !entry.cloud_project_id && app.account}
|
||||||
<button
|
<button
|
||||||
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
@@ -364,6 +366,17 @@
|
|||||||
Upload to cloud
|
Upload to cloud
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if entry.kind === "document" && !entry.cloud_linked && app.account}
|
||||||
|
<button
|
||||||
|
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
||||||
|
onclick={() => {
|
||||||
|
onsavetocloud(entry);
|
||||||
|
menuFor = null;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save to cloud
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
<button
|
<button
|
||||||
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
@@ -432,25 +445,25 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs text-[var(--color-ink-muted)] transition hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-ink)]"
|
class="flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs text-[var(--color-ink-muted)] transition hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-ink)]"
|
||||||
onclick={onnewdocument}
|
onclick={onnewproject}
|
||||||
>
|
>
|
||||||
<Icon icon="ph:file-plus" />
|
<Icon icon="ph:folder-star" />
|
||||||
Document
|
Project
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="flex items-center gap-1.5 rounded-md bg-[var(--color-accent)] px-2.5 py-1.5 text-xs font-medium text-white transition hover:opacity-90"
|
class="flex items-center gap-1.5 rounded-md bg-[var(--color-accent)] px-2.5 py-1.5 text-xs font-medium text-white transition hover:opacity-90"
|
||||||
onclick={onnewproject}
|
onclick={onnewdocument}
|
||||||
>
|
>
|
||||||
<Icon icon="ph:plus" />
|
<Icon icon="ph:plus" />
|
||||||
New project
|
New document
|
||||||
</button>
|
</button>
|
||||||
{:else if app.account}
|
{:else if app.account}
|
||||||
<button
|
<button
|
||||||
class="flex items-center gap-1.5 rounded-md bg-[var(--color-accent)] px-2.5 py-1.5 text-xs font-medium text-white transition hover:opacity-90"
|
class="flex items-center gap-1.5 rounded-md bg-[var(--color-accent)] px-2.5 py-1.5 text-xs font-medium text-white transition hover:opacity-90"
|
||||||
onclick={onnewspace}
|
onclick={onnewcloudproject}
|
||||||
>
|
>
|
||||||
<Icon icon="ph:plus" />
|
<Icon icon="ph:plus" />
|
||||||
New space
|
New cloud project
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -490,16 +503,16 @@
|
|||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
class="rounded-md bg-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-white hover:opacity-90"
|
class="rounded-md bg-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-white hover:opacity-90"
|
||||||
onclick={onnewproject}
|
|
||||||
>
|
|
||||||
New project
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="rounded-md border border-[var(--color-line)] px-3 py-1.5 text-xs hover:bg-[var(--color-surface)]"
|
|
||||||
onclick={onnewdocument}
|
onclick={onnewdocument}
|
||||||
>
|
>
|
||||||
New document
|
New document
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="rounded-md border border-[var(--color-line)] px-3 py-1.5 text-xs hover:bg-[var(--color-surface)]"
|
||||||
|
onclick={onnewproject}
|
||||||
|
>
|
||||||
|
New project
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -798,7 +811,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if app.spaces.length === 0 && app.cloudDocuments.length === 0 && app.cloudFolders.length === 0 && app.cloudFiles.length === 0}
|
{#if app.cloudProjects.length === 0 && app.cloudDocuments.length === 0 && app.cloudFolders.length === 0 && app.cloudFiles.length === 0}
|
||||||
<div
|
<div
|
||||||
class="flex flex-col items-center justify-center gap-3 py-16 text-[var(--color-ink-muted)]"
|
class="flex flex-col items-center justify-center gap-3 py-16 text-[var(--color-ink-muted)]"
|
||||||
>
|
>
|
||||||
@@ -807,26 +820,26 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if app.spaces.length > 0}
|
{#if app.cloudProjects.length > 0}
|
||||||
<h2
|
<h2
|
||||||
class="mb-2 text-[10px] font-semibold uppercase tracking-wider text-[var(--color-ink-muted)]"
|
class="mb-2 text-[10px] font-semibold uppercase tracking-wider text-[var(--color-ink-muted)]"
|
||||||
>
|
>
|
||||||
Spaces
|
Cloud projects
|
||||||
</h2>
|
</h2>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(220px,1fr))] gap-3">
|
<div class="grid grid-cols-[repeat(auto-fill,minmax(220px,1fr))] gap-3">
|
||||||
{#each app.spaces as space (space.id)}
|
{#each app.cloudProjects as project (project.id)}
|
||||||
{@const linked = linkedSpace(space.id)}
|
{@const linked = linkedProject(project.id)}
|
||||||
{@render cloudCard(
|
{@render cloudCard(
|
||||||
"ph:folder-star",
|
"ph:folder-star",
|
||||||
space.name,
|
project.name,
|
||||||
linked
|
linked
|
||||||
? `Project · ${space.role}`
|
? `Project · ${project.role}`
|
||||||
: `${space.role} · ${formatDate(space.updated_at)}`,
|
: `${project.role} · ${formatDate(project.updated_at)}`,
|
||||||
linked,
|
linked,
|
||||||
linked ? () => openTarget(linked.path) : null,
|
linked ? () => openTarget(linked.path) : null,
|
||||||
() => onclonespace(space.id, space.name),
|
() => oncloneproject(project.id, project.name),
|
||||||
space.role === "owner" ? () => ondeletespace(space.id) : null,
|
project.role === "owner" ? () => ondeleteproject(project.id) : null,
|
||||||
)}
|
)}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,9 +9,15 @@
|
|||||||
import {
|
import {
|
||||||
app,
|
app,
|
||||||
applyTheme,
|
applyTheme,
|
||||||
|
applyAccent,
|
||||||
|
applyTextScale,
|
||||||
|
applyReduceMotion,
|
||||||
|
applyContrast,
|
||||||
refreshEntries,
|
refreshEntries,
|
||||||
restartAutoSync,
|
restartAutoSync,
|
||||||
setError,
|
setError,
|
||||||
|
type ThemePreference,
|
||||||
|
type TextScale,
|
||||||
} from "$lib/ts/state.svelte";
|
} from "$lib/ts/state.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -21,15 +27,37 @@
|
|||||||
|
|
||||||
let { onclose, onsignin }: Props = $props();
|
let { onclose, onsignin }: Props = $props();
|
||||||
|
|
||||||
type Section = "files" | "appearance" | "account" | "about";
|
type Section =
|
||||||
|
| "files"
|
||||||
|
| "appearance"
|
||||||
|
| "accessibility"
|
||||||
|
| "account"
|
||||||
|
| "about";
|
||||||
|
|
||||||
const sections: { id: Section; label: string; icon: string }[] = [
|
const sections: { id: Section; label: string; icon: string }[] = [
|
||||||
{ id: "files", label: "Files", icon: "ph:folder" },
|
{ id: "files", label: "Files", icon: "ph:folder" },
|
||||||
{ id: "appearance", label: "Appearance", icon: "ph:palette" },
|
{ id: "appearance", label: "Appearance", icon: "ph:palette" },
|
||||||
|
{ id: "accessibility", label: "Accessibility", icon: "ph:wheelchair" },
|
||||||
{ id: "account", label: "Account", icon: "ph:user-circle" },
|
{ id: "account", label: "Account", icon: "ph:user-circle" },
|
||||||
{ id: "about", label: "About", icon: "ph:info" },
|
{ id: "about", label: "About", icon: "ph:info" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const accentPresets = [
|
||||||
|
"#3b6cf6",
|
||||||
|
"#7c5cfc",
|
||||||
|
"#22b573",
|
||||||
|
"#f2994a",
|
||||||
|
"#ec4899",
|
||||||
|
"#14b8a6",
|
||||||
|
];
|
||||||
|
|
||||||
|
const textScaleOptions: { value: TextScale; label: string }[] = [
|
||||||
|
{ value: "small", label: "Small" },
|
||||||
|
{ value: "default", label: "Default" },
|
||||||
|
{ value: "large", label: "Large" },
|
||||||
|
{ value: "xlarge", label: "Extra large" },
|
||||||
|
];
|
||||||
|
|
||||||
let section = $state<Section>("files");
|
let section = $state<Section>("files");
|
||||||
|
|
||||||
let workspaceRoot = $state(untrack(() => app.settings?.workspace_root ?? ""));
|
let workspaceRoot = $state(untrack(() => app.settings?.workspace_root ?? ""));
|
||||||
@@ -105,7 +133,7 @@
|
|||||||
try {
|
try {
|
||||||
await api.cloudLogout();
|
await api.cloudLogout();
|
||||||
app.account = null;
|
app.account = null;
|
||||||
app.spaces = [];
|
app.cloudProjects = [];
|
||||||
app.settings = await api.getSettings();
|
app.settings = await api.getSettings();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError(error);
|
setError(error);
|
||||||
@@ -172,13 +200,13 @@
|
|||||||
<div class="flex flex-col gap-2 text-xs">
|
<div class="flex flex-col gap-2 text-xs">
|
||||||
<span class="font-medium text-[var(--color-ink-muted)]">Theme</span>
|
<span class="font-medium text-[var(--color-ink-muted)]">Theme</span>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
{#each [["light", "Light", "ph:sun"], ["dark", "Dark", "ph:moon"]] as [value, label, icon]}
|
{#each [["light", "Light", "ph:sun"], ["dark", "Dark", "ph:moon"], ["system", "System", "ph:desktop"]] as [value, label, icon]}
|
||||||
<button
|
<button
|
||||||
class="flex flex-1 items-center justify-center gap-1.5 rounded-md border px-3 py-2.5 transition
|
class="flex flex-1 items-center justify-center gap-1.5 rounded-md border px-3 py-2.5 transition
|
||||||
{app.theme === value
|
{app.themePreference === value
|
||||||
? 'border-[var(--color-accent)] bg-[var(--color-accent-soft)] text-[var(--color-accent)]'
|
? 'border-[var(--color-accent)] bg-[var(--color-accent-soft)] text-[var(--color-accent)]'
|
||||||
: 'border-[var(--color-line)] text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-muted)]'}"
|
: 'border-[var(--color-line)] text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-muted)]'}"
|
||||||
onclick={() => applyTheme(value as "light" | "dark")}
|
onclick={() => applyTheme(value as ThemePreference)}
|
||||||
>
|
>
|
||||||
<Icon {icon} class="text-base" />
|
<Icon {icon} class="text-base" />
|
||||||
{label}
|
{label}
|
||||||
@@ -186,7 +214,111 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<span class="text-[var(--color-ink-muted)]">
|
<span class="text-[var(--color-ink-muted)]">
|
||||||
Applies immediately to the editor and preview.
|
System follows your OS setting and updates live.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 text-xs">
|
||||||
|
<span class="font-medium text-[var(--color-ink-muted)]">
|
||||||
|
Accent color
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
class="flex h-7 w-7 items-center justify-center rounded-full border transition
|
||||||
|
{app.accent === null
|
||||||
|
? 'border-[var(--color-accent)] text-[var(--color-accent)]'
|
||||||
|
: 'border-[var(--color-line)] text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-muted)]'}"
|
||||||
|
title="Default"
|
||||||
|
onclick={() => applyAccent(null)}
|
||||||
|
>
|
||||||
|
<Icon icon="ph:arrow-counter-clockwise" class="text-sm" />
|
||||||
|
</button>
|
||||||
|
{#each accentPresets as preset}
|
||||||
|
<button
|
||||||
|
class="h-7 w-7 rounded-full border-2 transition
|
||||||
|
{app.accent === preset
|
||||||
|
? 'border-[var(--color-ink)]'
|
||||||
|
: 'border-transparent hover:opacity-80'}"
|
||||||
|
style="background-color: {preset}"
|
||||||
|
title={preset}
|
||||||
|
onclick={() => applyAccent(preset)}
|
||||||
|
></button>
|
||||||
|
{/each}
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
class="h-7 w-7 cursor-pointer rounded-full border border-[var(--color-line)] bg-transparent p-0"
|
||||||
|
value={app.accent ?? "#3b6cf6"}
|
||||||
|
title="Custom color"
|
||||||
|
oninput={(event) =>
|
||||||
|
applyAccent((event.target as HTMLInputElement).value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span class="text-[var(--color-ink-muted)]">
|
||||||
|
Overrides the accent color used across the app.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if section === "accessibility"}
|
||||||
|
<div class="flex flex-col gap-5">
|
||||||
|
<div class="flex flex-col gap-2 text-xs">
|
||||||
|
<span class="font-medium text-[var(--color-ink-muted)]">
|
||||||
|
UI text scale
|
||||||
|
</span>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
{#each textScaleOptions as option}
|
||||||
|
<button
|
||||||
|
class="flex flex-1 items-center justify-center rounded-md border px-3 py-2.5 transition
|
||||||
|
{app.textScale === option.value
|
||||||
|
? 'border-[var(--color-accent)] bg-[var(--color-accent-soft)] text-[var(--color-accent)]'
|
||||||
|
: 'border-[var(--color-line)] text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-muted)]'}"
|
||||||
|
onclick={() => applyTextScale(option.value)}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<span class="text-[var(--color-ink-muted)]">
|
||||||
|
Scales text and controls throughout the app.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 text-xs">
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={app.reduceMotion}
|
||||||
|
onchange={(event) =>
|
||||||
|
applyReduceMotion(
|
||||||
|
(event.target as HTMLInputElement).checked,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span class="font-medium text-[var(--color-ink-muted)]">
|
||||||
|
Reduce motion
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<span class="text-[var(--color-ink-muted)]">
|
||||||
|
Shortens transitions and animations across the app.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 text-xs">
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={app.contrast === "high"}
|
||||||
|
onchange={(event) =>
|
||||||
|
applyContrast(
|
||||||
|
(event.target as HTMLInputElement).checked
|
||||||
|
? "high"
|
||||||
|
: "normal",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span class="font-medium text-[var(--color-ink-muted)]">
|
||||||
|
High contrast
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<span class="text-[var(--color-ink-muted)]">
|
||||||
|
Increases contrast for borders, muted text, and focus outlines.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -305,13 +437,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#snippet footer()}
|
{#snippet footer()}
|
||||||
|
{@const draftless =
|
||||||
|
section === "about" ||
|
||||||
|
section === "appearance" ||
|
||||||
|
section === "accessibility"}
|
||||||
<button
|
<button
|
||||||
class="rounded-md px-3 py-1.5 text-xs text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)]"
|
class="rounded-md px-3 py-1.5 text-xs text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)]"
|
||||||
onclick={onclose}
|
onclick={onclose}
|
||||||
>
|
>
|
||||||
{section === "about" || section === "appearance" ? "Close" : "Cancel"}
|
{draftless ? "Close" : "Cancel"}
|
||||||
</button>
|
</button>
|
||||||
{#if section !== "about" && section !== "appearance"}
|
{#if !draftless}
|
||||||
<button
|
<button
|
||||||
class="rounded-md bg-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-white transition hover:opacity-90 disabled:opacity-50"
|
class="rounded-md bg-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-white transition hover:opacity-90 disabled:opacity-50"
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
|
|||||||
+24
-18
@@ -53,7 +53,7 @@ export interface Account {
|
|||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpaceSummary {
|
export interface ProjectSummary {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
entrypoint: string;
|
entrypoint: string;
|
||||||
@@ -94,7 +94,7 @@ export interface BrowseEntry {
|
|||||||
kind: EntryKind;
|
kind: EntryKind;
|
||||||
size: number;
|
size: number;
|
||||||
modified: string | null;
|
modified: string | null;
|
||||||
space_id: string | null;
|
cloud_project_id: string | null;
|
||||||
last_synced_at: string | null;
|
last_synced_at: string | null;
|
||||||
child_count: number;
|
child_count: number;
|
||||||
cloud_linked: boolean;
|
cloud_linked: boolean;
|
||||||
@@ -106,7 +106,7 @@ export interface TargetInfo {
|
|||||||
entrypoint: string;
|
entrypoint: string;
|
||||||
standalone: boolean;
|
standalone: boolean;
|
||||||
is_project: boolean;
|
is_project: boolean;
|
||||||
space_id: string | null;
|
cloud_project_id: string | null;
|
||||||
files: FileEntry[];
|
files: FileEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,8 +261,8 @@ export const cloudLogout = () => invoke<void>("cloud_logout");
|
|||||||
|
|
||||||
export const cloudAccount = () => invoke<Account | null>("cloud_account");
|
export const cloudAccount = () => invoke<Account | null>("cloud_account");
|
||||||
|
|
||||||
export const cloudListSpaces = () =>
|
export const cloudListProjects = () =>
|
||||||
invoke<SpaceSummary[]>("cloud_list_spaces");
|
invoke<ProjectSummary[]>("cloud_list_projects");
|
||||||
|
|
||||||
export interface CloudFolder {
|
export interface CloudFolder {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -280,7 +280,7 @@ export interface CloudDocument {
|
|||||||
|
|
||||||
export interface SharedItems {
|
export interface SharedItems {
|
||||||
documents: CloudDocument[];
|
documents: CloudDocument[];
|
||||||
spaces: SpaceSummary[];
|
projects: ProjectSummary[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentLink {
|
export interface DocumentLink {
|
||||||
@@ -317,6 +317,9 @@ export const cloudDownloadFile = (fileId: string) =>
|
|||||||
export const cloudDownloadDocument = (documentId: string, parent: string) =>
|
export const cloudDownloadDocument = (documentId: string, parent: string) =>
|
||||||
invoke<string>("cloud_download_document", { documentId, parent });
|
invoke<string>("cloud_download_document", { documentId, parent });
|
||||||
|
|
||||||
|
export const cloudCreateDocument = (path: string, title: string) =>
|
||||||
|
invoke<string>("cloud_create_document", { path, title });
|
||||||
|
|
||||||
export const cloudSyncDocument = (path: string) =>
|
export const cloudSyncDocument = (path: string) =>
|
||||||
invoke<SyncReport>("cloud_sync_document", { path });
|
invoke<SyncReport>("cloud_sync_document", { path });
|
||||||
|
|
||||||
@@ -333,9 +336,9 @@ export interface LinkedDocument {
|
|||||||
sync_state: "synced" | "pending" | null;
|
sync_state: "synced" | "pending" | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LinkedSpace {
|
export interface LinkedProject {
|
||||||
path: string;
|
path: string;
|
||||||
space_id: string;
|
cloud_project_id: string;
|
||||||
synced_at: string | null;
|
synced_at: string | null;
|
||||||
sync_state: "synced" | "pending" | null;
|
sync_state: "synced" | "pending" | null;
|
||||||
}
|
}
|
||||||
@@ -343,8 +346,8 @@ export interface LinkedSpace {
|
|||||||
export const cloudLinkedDocuments = () =>
|
export const cloudLinkedDocuments = () =>
|
||||||
invoke<LinkedDocument[]>("cloud_linked_documents");
|
invoke<LinkedDocument[]>("cloud_linked_documents");
|
||||||
|
|
||||||
export const cloudLinkedSpaces = () =>
|
export const cloudLinkedProjects = () =>
|
||||||
invoke<LinkedSpace[]>("cloud_linked_spaces");
|
invoke<LinkedProject[]>("cloud_linked_projects");
|
||||||
|
|
||||||
export const cloudDocumentLink = (path: string) =>
|
export const cloudDocumentLink = (path: string) =>
|
||||||
invoke<DocumentLink | null>("cloud_document_link", { path });
|
invoke<DocumentLink | null>("cloud_document_link", { path });
|
||||||
@@ -352,17 +355,20 @@ export const cloudDocumentLink = (path: string) =>
|
|||||||
export const cloudUnlinkDocument = (path: string) =>
|
export const cloudUnlinkDocument = (path: string) =>
|
||||||
invoke<void>("cloud_unlink_document", { path });
|
invoke<void>("cloud_unlink_document", { path });
|
||||||
|
|
||||||
export const cloudCreateSpace = (name: string) =>
|
export const cloudCreateProject = (name: string) =>
|
||||||
invoke<SpaceSummary>("cloud_create_space", { name });
|
invoke<ProjectSummary>("cloud_create_project", { name });
|
||||||
|
|
||||||
export const cloudDeleteSpace = (spaceId: string) =>
|
export const cloudDeleteProject = (cloudProjectId: string) =>
|
||||||
invoke<void>("cloud_delete_space", { spaceId });
|
invoke<void>("cloud_delete_project", { cloudProjectId });
|
||||||
|
|
||||||
export const cloudCloneSpace = (spaceId: string, projectName: string) =>
|
export const cloudCloneProject = (cloudProjectId: string, projectName: string) =>
|
||||||
invoke<SyncReport>("cloud_clone_space", { spaceId, projectName });
|
invoke<SyncReport>("cloud_clone_project", { cloudProjectId, projectName });
|
||||||
|
|
||||||
export const cloudLinkProject = (project: string, spaceId?: string) =>
|
export const cloudLinkProject = (project: string, cloudProjectId?: string) =>
|
||||||
invoke<SyncReport>("cloud_link_project", { project, spaceId: spaceId ?? null });
|
invoke<SyncReport>("cloud_link_project", {
|
||||||
|
project,
|
||||||
|
cloudProjectId: cloudProjectId ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
export const cloudUnlinkProject = (project: string) =>
|
export const cloudUnlinkProject = (project: string) =>
|
||||||
invoke<void>("cloud_unlink_project", { project });
|
invoke<void>("cloud_unlink_project", { project });
|
||||||
|
|||||||
+186
-24
@@ -10,15 +10,18 @@ import type {
|
|||||||
Diagnostic,
|
Diagnostic,
|
||||||
DocumentLink,
|
DocumentLink,
|
||||||
LinkedDocument,
|
LinkedDocument,
|
||||||
LinkedSpace,
|
LinkedProject,
|
||||||
|
ProjectSummary,
|
||||||
Settings,
|
Settings,
|
||||||
SpaceSummary,
|
|
||||||
TargetInfo,
|
TargetInfo,
|
||||||
} from "./api";
|
} from "./api";
|
||||||
|
|
||||||
export type Scope = "local" | "cloud";
|
export type Scope = "local" | "cloud";
|
||||||
export type View = "files" | "editor";
|
export type View = "files" | "editor";
|
||||||
export type LspStatus = "off" | "starting" | "on" | "unavailable";
|
export type LspStatus = "off" | "starting" | "on" | "unavailable";
|
||||||
|
export type ThemePreference = "light" | "dark" | "system";
|
||||||
|
export type TextScale = "small" | "default" | "large" | "xlarge";
|
||||||
|
export type ContrastLevel = "normal" | "high";
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
view: View;
|
view: View;
|
||||||
@@ -28,7 +31,7 @@ interface AppState {
|
|||||||
|
|
||||||
currentDir: string;
|
currentDir: string;
|
||||||
entries: BrowseEntry[];
|
entries: BrowseEntry[];
|
||||||
spaces: SpaceSummary[];
|
cloudProjects: ProjectSummary[];
|
||||||
cloudFolder: string | null | "shared";
|
cloudFolder: string | null | "shared";
|
||||||
cloudFolders: CloudFolder[];
|
cloudFolders: CloudFolder[];
|
||||||
cloudFolderTree: CloudFolder[];
|
cloudFolderTree: CloudFolder[];
|
||||||
@@ -36,7 +39,7 @@ interface AppState {
|
|||||||
cloudFiles: CloudFile[];
|
cloudFiles: CloudFile[];
|
||||||
cloudLoading: boolean;
|
cloudLoading: boolean;
|
||||||
linkedDocuments: LinkedDocument[];
|
linkedDocuments: LinkedDocument[];
|
||||||
linkedSpaces: LinkedSpace[];
|
linkedProjects: LinkedProject[];
|
||||||
documentLink: DocumentLink | null;
|
documentLink: DocumentLink | null;
|
||||||
|
|
||||||
target: TargetInfo | null;
|
target: TargetInfo | null;
|
||||||
@@ -54,6 +57,11 @@ interface AppState {
|
|||||||
status: string;
|
status: string;
|
||||||
error: string;
|
error: string;
|
||||||
theme: "light" | "dark";
|
theme: "light" | "dark";
|
||||||
|
themePreference: ThemePreference;
|
||||||
|
accent: string | null;
|
||||||
|
textScale: TextScale;
|
||||||
|
reduceMotion: boolean;
|
||||||
|
contrast: ContrastLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const app = $state<AppState>({
|
export const app = $state<AppState>({
|
||||||
@@ -64,7 +72,7 @@ export const app = $state<AppState>({
|
|||||||
|
|
||||||
currentDir: "",
|
currentDir: "",
|
||||||
entries: [],
|
entries: [],
|
||||||
spaces: [],
|
cloudProjects: [],
|
||||||
cloudFolder: null,
|
cloudFolder: null,
|
||||||
cloudFolders: [],
|
cloudFolders: [],
|
||||||
cloudFolderTree: [],
|
cloudFolderTree: [],
|
||||||
@@ -72,7 +80,7 @@ export const app = $state<AppState>({
|
|||||||
cloudFiles: [],
|
cloudFiles: [],
|
||||||
cloudLoading: false,
|
cloudLoading: false,
|
||||||
linkedDocuments: [],
|
linkedDocuments: [],
|
||||||
linkedSpaces: [],
|
linkedProjects: [],
|
||||||
documentLink: null,
|
documentLink: null,
|
||||||
|
|
||||||
target: null,
|
target: null,
|
||||||
@@ -90,6 +98,11 @@ export const app = $state<AppState>({
|
|||||||
status: "",
|
status: "",
|
||||||
error: "",
|
error: "",
|
||||||
theme: "light",
|
theme: "light",
|
||||||
|
themePreference: "light",
|
||||||
|
accent: null,
|
||||||
|
textScale: "default",
|
||||||
|
reduceMotion: false,
|
||||||
|
contrast: "normal",
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface DownloadProgress {
|
export interface DownloadProgress {
|
||||||
@@ -132,10 +145,138 @@ export function clearMessages() {
|
|||||||
app.error = "";
|
app.error = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyTheme(theme: "light" | "dark") {
|
const THEME_KEY = "typst-desktop-theme";
|
||||||
app.theme = theme;
|
const ACCENT_KEY = "typst-desktop-accent";
|
||||||
document.documentElement.dataset.theme = theme;
|
const TEXT_SCALE_KEY = "typst-desktop-text-scale";
|
||||||
localStorage.setItem("typst-desktop-theme", theme);
|
const REDUCE_MOTION_KEY = "typst-desktop-reduce-motion";
|
||||||
|
const CONTRAST_KEY = "typst-desktop-contrast";
|
||||||
|
|
||||||
|
const TEXT_SCALE_PX: Record<TextScale, number> = {
|
||||||
|
small: 14,
|
||||||
|
default: 16,
|
||||||
|
large: 18,
|
||||||
|
xlarge: 20,
|
||||||
|
};
|
||||||
|
|
||||||
|
let systemThemeQuery: MediaQueryList | null = null;
|
||||||
|
|
||||||
|
function resolveTheme(preference: ThemePreference): "light" | "dark" {
|
||||||
|
if (preference !== "system") return preference;
|
||||||
|
|
||||||
|
if (!systemThemeQuery) {
|
||||||
|
systemThemeQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
systemThemeQuery.addEventListener("change", () => {
|
||||||
|
if (app.themePreference === "system") applyTheme("system");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return systemThemeQuery.matches ? "dark" : "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyTheme(preference: ThemePreference) {
|
||||||
|
app.themePreference = preference;
|
||||||
|
app.theme = resolveTheme(preference);
|
||||||
|
document.documentElement.dataset.theme = app.theme;
|
||||||
|
localStorage.setItem(THEME_KEY, preference);
|
||||||
|
|
||||||
|
if (app.accent) applyAccent(app.accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hexToRgb(hex: string): [number, number, number] {
|
||||||
|
const value = parseInt(hex.replace("#", ""), 16);
|
||||||
|
return [(value >> 16) & 255, (value >> 8) & 255, value & 255];
|
||||||
|
}
|
||||||
|
|
||||||
|
function rgbToHsl(r: number, g: number, b: number): [number, number, number] {
|
||||||
|
r /= 255;
|
||||||
|
g /= 255;
|
||||||
|
b /= 255;
|
||||||
|
const max = Math.max(r, g, b);
|
||||||
|
const min = Math.min(r, g, b);
|
||||||
|
const l = (max + min) / 2;
|
||||||
|
let h = 0;
|
||||||
|
let s = 0;
|
||||||
|
|
||||||
|
if (max !== min) {
|
||||||
|
const d = max - min;
|
||||||
|
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||||
|
switch (max) {
|
||||||
|
case r:
|
||||||
|
h = (g - b) / d + (g < b ? 6 : 0);
|
||||||
|
break;
|
||||||
|
case g:
|
||||||
|
h = (b - r) / d + 2;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
h = (r - g) / d + 4;
|
||||||
|
}
|
||||||
|
h /= 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [h * 360, s * 100, l * 100];
|
||||||
|
}
|
||||||
|
|
||||||
|
function hslToHex(h: number, s: number, l: number): string {
|
||||||
|
s /= 100;
|
||||||
|
l /= 100;
|
||||||
|
const k = (n: number) => (n + h / 30) % 12;
|
||||||
|
const a = s * Math.min(l, 1 - l);
|
||||||
|
const f = (n: number) =>
|
||||||
|
l - a * Math.max(-1, Math.min(k(n) - 3, 9 - k(n), 1));
|
||||||
|
const toHex = (n: number) =>
|
||||||
|
Math.round(n * 255)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(2, "0");
|
||||||
|
return `#${toHex(f(0))}${toHex(f(8))}${toHex(f(4))}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function accentSoft(hex: string, dark: boolean): string {
|
||||||
|
const [r, g, b] = hexToRgb(hex);
|
||||||
|
const [h, s] = rgbToHsl(r, g, b);
|
||||||
|
return dark
|
||||||
|
? hslToHex(h, Math.min(s, 55), 18)
|
||||||
|
: hslToHex(h, Math.min(s, 70), 92);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyAccent(color: string | null) {
|
||||||
|
app.accent = color;
|
||||||
|
const root = document.documentElement.style;
|
||||||
|
|
||||||
|
if (color) {
|
||||||
|
root.setProperty("--color-accent", color);
|
||||||
|
root.setProperty("--color-accent-soft", accentSoft(color, app.theme === "dark"));
|
||||||
|
localStorage.setItem(ACCENT_KEY, color);
|
||||||
|
} else {
|
||||||
|
root.removeProperty("--color-accent");
|
||||||
|
root.removeProperty("--color-accent-soft");
|
||||||
|
localStorage.removeItem(ACCENT_KEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyTextScale(scale: TextScale) {
|
||||||
|
app.textScale = scale;
|
||||||
|
document.documentElement.style.fontSize = `${TEXT_SCALE_PX[scale]}px`;
|
||||||
|
localStorage.setItem(TEXT_SCALE_KEY, scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyReduceMotion(enabled: boolean) {
|
||||||
|
app.reduceMotion = enabled;
|
||||||
|
if (enabled) {
|
||||||
|
document.documentElement.dataset.reduceMotion = "true";
|
||||||
|
} else {
|
||||||
|
delete document.documentElement.dataset.reduceMotion;
|
||||||
|
}
|
||||||
|
localStorage.setItem(REDUCE_MOTION_KEY, String(enabled));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyContrast(level: ContrastLevel) {
|
||||||
|
app.contrast = level;
|
||||||
|
if (level === "high") {
|
||||||
|
document.documentElement.dataset.contrast = "high";
|
||||||
|
} else {
|
||||||
|
delete document.documentElement.dataset.contrast;
|
||||||
|
}
|
||||||
|
localStorage.setItem(CONTRAST_KEY, level);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function breadcrumbs(): { name: string; path: string }[] {
|
export function breadcrumbs(): { name: string; path: string }[] {
|
||||||
@@ -148,8 +289,27 @@ export function breadcrumbs(): { name: string; path: string }[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function bootstrap() {
|
export async function bootstrap() {
|
||||||
const stored = localStorage.getItem("typst-desktop-theme");
|
const storedTheme = localStorage.getItem(THEME_KEY);
|
||||||
applyTheme(stored === "dark" ? "dark" : "light");
|
applyTheme(
|
||||||
|
storedTheme === "dark" || storedTheme === "light" || storedTheme === "system"
|
||||||
|
? storedTheme
|
||||||
|
: "light",
|
||||||
|
);
|
||||||
|
|
||||||
|
const storedAccent = localStorage.getItem(ACCENT_KEY);
|
||||||
|
if (storedAccent) applyAccent(storedAccent);
|
||||||
|
|
||||||
|
const storedTextScale = localStorage.getItem(TEXT_SCALE_KEY);
|
||||||
|
applyTextScale(
|
||||||
|
storedTextScale === "small" ||
|
||||||
|
storedTextScale === "large" ||
|
||||||
|
storedTextScale === "xlarge"
|
||||||
|
? storedTextScale
|
||||||
|
: "default",
|
||||||
|
);
|
||||||
|
|
||||||
|
applyReduceMotion(localStorage.getItem(REDUCE_MOTION_KEY) === "true");
|
||||||
|
applyContrast(localStorage.getItem(CONTRAST_KEY) === "high" ? "high" : "normal");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
app.settings = await api.getSettings();
|
app.settings = await api.getSettings();
|
||||||
@@ -179,18 +339,18 @@ export async function refreshAccount() {
|
|||||||
try {
|
try {
|
||||||
app.account = await api.cloudAccount();
|
app.account = await api.cloudAccount();
|
||||||
if (app.account) {
|
if (app.account) {
|
||||||
await refreshSpaces();
|
await refreshCloudProjects();
|
||||||
} else {
|
} else {
|
||||||
app.spaces = [];
|
app.cloudProjects = [];
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
app.account = null;
|
app.account = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshSpaces() {
|
export async function refreshCloudProjects() {
|
||||||
try {
|
try {
|
||||||
app.spaces = await api.cloudListSpaces();
|
app.cloudProjects = await api.cloudListProjects();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError(error);
|
setError(error);
|
||||||
}
|
}
|
||||||
@@ -202,19 +362,19 @@ export async function refreshCloud() {
|
|||||||
app.cloudLoading = true;
|
app.cloudLoading = true;
|
||||||
try {
|
try {
|
||||||
app.linkedDocuments = await api.cloudLinkedDocuments().catch(() => []);
|
app.linkedDocuments = await api.cloudLinkedDocuments().catch(() => []);
|
||||||
app.linkedSpaces = await api.cloudLinkedSpaces().catch(() => []);
|
app.linkedProjects = await api.cloudLinkedProjects().catch(() => []);
|
||||||
|
|
||||||
if (app.cloudFolder === "shared") {
|
if (app.cloudFolder === "shared") {
|
||||||
const shared = await api.cloudListShared();
|
const shared = await api.cloudListShared();
|
||||||
app.cloudDocuments = shared.documents;
|
app.cloudDocuments = shared.documents;
|
||||||
app.spaces = shared.spaces;
|
app.cloudProjects = shared.projects;
|
||||||
app.cloudFolders = [];
|
app.cloudFolders = [];
|
||||||
app.cloudFiles = [];
|
app.cloudFiles = [];
|
||||||
} else {
|
} else {
|
||||||
const [folders, documents, spaces, files] = await Promise.all([
|
const [folders, documents, projects, files] = await Promise.all([
|
||||||
api.cloudListFolders(),
|
api.cloudListFolders(),
|
||||||
api.cloudListDocuments(app.cloudFolder),
|
api.cloudListDocuments(app.cloudFolder),
|
||||||
api.cloudListSpaces(),
|
api.cloudListProjects(),
|
||||||
api.cloudListFiles(app.cloudFolder),
|
api.cloudListFiles(app.cloudFolder),
|
||||||
]);
|
]);
|
||||||
app.cloudFolderTree = folders;
|
app.cloudFolderTree = folders;
|
||||||
@@ -222,7 +382,7 @@ export async function refreshCloud() {
|
|||||||
(folder) => (folder.parent_id ?? null) === app.cloudFolder,
|
(folder) => (folder.parent_id ?? null) === app.cloudFolder,
|
||||||
);
|
);
|
||||||
app.cloudDocuments = documents;
|
app.cloudDocuments = documents;
|
||||||
app.spaces = spaces;
|
app.cloudProjects = projects;
|
||||||
app.cloudFiles = files;
|
app.cloudFiles = files;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -279,8 +439,10 @@ export async function downloadCloudFile(fileId: string, name: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function linkedSpace(spaceId: string) {
|
export function linkedProject(cloudProjectId: string) {
|
||||||
return app.linkedSpaces.find((linked) => linked.space_id === spaceId);
|
return app.linkedProjects.find(
|
||||||
|
(linked) => linked.cloud_project_id === cloudProjectId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeDownloadedDocument(path: string) {
|
export async function removeDownloadedDocument(path: string) {
|
||||||
@@ -484,7 +646,7 @@ async function autoSync() {
|
|||||||
if (!app.account || app.syncing) return;
|
if (!app.account || app.syncing) return;
|
||||||
if (app.conflicts.length > 0) return;
|
if (app.conflicts.length > 0) return;
|
||||||
|
|
||||||
const linked = app.target?.space_id || app.documentLink;
|
const linked = app.target?.cloud_project_id || app.documentLink;
|
||||||
const project = linked ? app.target?.path : null;
|
const project = linked ? app.target?.path : null;
|
||||||
if (!project) return;
|
if (!project) return;
|
||||||
|
|
||||||
|
|||||||
+71
-40
@@ -38,8 +38,8 @@
|
|||||||
openFile,
|
openFile,
|
||||||
openTarget,
|
openTarget,
|
||||||
refreshAccount,
|
refreshAccount,
|
||||||
|
refreshCloudProjects,
|
||||||
refreshEntries,
|
refreshEntries,
|
||||||
refreshSpaces,
|
|
||||||
refreshTarget,
|
refreshTarget,
|
||||||
removeDownloadedDocument,
|
removeDownloadedDocument,
|
||||||
runSync,
|
runSync,
|
||||||
@@ -60,13 +60,14 @@
|
|||||||
| { kind: "rename-entry"; entry: BrowseEntry }
|
| { kind: "rename-entry"; entry: BrowseEntry }
|
||||||
| { kind: "delete-entry"; entry: BrowseEntry }
|
| { kind: "delete-entry"; entry: BrowseEntry }
|
||||||
| { kind: "link-entry"; entry: BrowseEntry }
|
| { kind: "link-entry"; entry: BrowseEntry }
|
||||||
| { kind: "new-space" }
|
| { kind: "save-document-to-cloud"; entry: BrowseEntry }
|
||||||
| { kind: "delete-space"; id: string }
|
| { kind: "new-cloud-project" }
|
||||||
| { kind: "clone-space"; id: string; name: string }
|
| { kind: "delete-cloud-project"; id: string }
|
||||||
|
| { kind: "clone-cloud-project"; id: string; name: string }
|
||||||
| { kind: "new-file"; parent: string }
|
| { kind: "new-file"; parent: string }
|
||||||
| { kind: "new-subfolder"; parent: string }
|
| { kind: "new-subfolder"; parent: string }
|
||||||
| { kind: "rename-file"; path: string }
|
| { kind: "rename-file"; path: string }
|
||||||
| { kind: "delete-file"; path: string }
|
| { kind: "delete-file"; paths: string[] }
|
||||||
| { kind: "login" }
|
| { kind: "login" }
|
||||||
| { kind: "settings" }
|
| { kind: "settings" }
|
||||||
| { kind: "assets" }
|
| { kind: "assets" }
|
||||||
@@ -76,6 +77,7 @@
|
|||||||
let dialog = $state<Dialog>({ kind: "none" });
|
let dialog = $state<Dialog>({ kind: "none" });
|
||||||
let editorView = $state<EditorView | null>(null);
|
let editorView = $state<EditorView | null>(null);
|
||||||
let imageViewer = $state<{ paths: string[]; index: number } | null>(null);
|
let imageViewer = $state<{ paths: string[]; index: number } | null>(null);
|
||||||
|
let selectedEntries = $state<Set<string>>(new Set());
|
||||||
let selectedEntry = $state<string | null>(null);
|
let selectedEntry = $state<string | null>(null);
|
||||||
let selectedIsDir = $state(false);
|
let selectedIsDir = $state(false);
|
||||||
let treeDropTarget = $state<string | null>(null);
|
let treeDropTarget = $state<string | null>(null);
|
||||||
@@ -152,25 +154,32 @@
|
|||||||
guard(async () => {
|
guard(async () => {
|
||||||
const report = await api.cloudLinkProject(entry.path);
|
const report = await api.cloudLinkProject(entry.path);
|
||||||
await refreshEntries();
|
await refreshEntries();
|
||||||
await refreshSpaces();
|
await refreshCloudProjects();
|
||||||
setStatus(`Uploaded ${report.pushed.length} files to a new cloud space`);
|
setStatus(`Uploaded ${report.pushed.length} files to a new cloud project`);
|
||||||
});
|
});
|
||||||
|
|
||||||
const createSpace = (name: string) =>
|
const saveDocumentToCloud = (entry: BrowseEntry, title: string) =>
|
||||||
guard(async () => {
|
guard(async () => {
|
||||||
await api.cloudCreateSpace(name);
|
await api.cloudCreateDocument(entry.path, title);
|
||||||
await refreshSpaces();
|
await refreshEntries();
|
||||||
|
setStatus(`Saved '${entry.name}' to the cloud`);
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteSpace = (id: string) =>
|
const createCloudProject = (name: string) =>
|
||||||
guard(async () => {
|
guard(async () => {
|
||||||
await api.cloudDeleteSpace(id);
|
await api.cloudCreateProject(name);
|
||||||
await refreshSpaces();
|
await refreshCloudProjects();
|
||||||
});
|
});
|
||||||
|
|
||||||
const cloneSpace = (id: string, name: string) =>
|
const deleteCloudProject = (id: string) =>
|
||||||
guard(async () => {
|
guard(async () => {
|
||||||
await api.cloudCloneSpace(id, name);
|
await api.cloudDeleteProject(id);
|
||||||
|
await refreshCloudProjects();
|
||||||
|
});
|
||||||
|
|
||||||
|
const cloneCloudProject = (id: string, name: string) =>
|
||||||
|
guard(async () => {
|
||||||
|
await api.cloudCloneProject(id, name);
|
||||||
app.scope = "local";
|
app.scope = "local";
|
||||||
await browseTo("");
|
await browseTo("");
|
||||||
setStatus(`Downloaded '${name}' to this device`);
|
setStatus(`Downloaded '${name}' to this device`);
|
||||||
@@ -250,13 +259,17 @@
|
|||||||
if (app.activePath === path) await openFile(next);
|
if (app.activePath === path) await openFile(next);
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteFile = (path: string) =>
|
const deleteFile = (paths: string[]) =>
|
||||||
guard(async () => {
|
guard(async () => {
|
||||||
|
for (const path of paths) {
|
||||||
await api.deleteEntry(`${app.target!.path}/${path}`);
|
await api.deleteEntry(`${app.target!.path}/${path}`);
|
||||||
if (app.activePath === path) {
|
if (app.activePath === path) {
|
||||||
app.activePath = null;
|
app.activePath = null;
|
||||||
app.editorContent = "";
|
app.editorContent = "";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
selectedEntries = new Set();
|
||||||
|
selectedEntry = null;
|
||||||
await refreshTarget();
|
await refreshTarget();
|
||||||
await compile();
|
await compile();
|
||||||
});
|
});
|
||||||
@@ -495,7 +508,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if app.target?.space_id || app.documentLink}
|
{#if app.target?.cloud_project_id || app.documentLink}
|
||||||
<button
|
<button
|
||||||
class="flex items-center gap-1.5 rounded-md bg-[var(--color-accent)] px-2.5 py-1.5 text-xs font-medium text-white transition hover:opacity-90 disabled:opacity-50"
|
class="flex items-center gap-1.5 rounded-md bg-[var(--color-accent)] px-2.5 py-1.5 text-xs font-medium text-white transition hover:opacity-90 disabled:opacity-50"
|
||||||
disabled={app.syncing}
|
disabled={app.syncing}
|
||||||
@@ -598,14 +611,17 @@
|
|||||||
onrename={(entry) => (dialog = { kind: "rename-entry", entry })}
|
onrename={(entry) => (dialog = { kind: "rename-entry", entry })}
|
||||||
ondelete={(entry) => (dialog = { kind: "delete-entry", entry })}
|
ondelete={(entry) => (dialog = { kind: "delete-entry", entry })}
|
||||||
onlink={(entry) => (dialog = { kind: "link-entry", entry })}
|
onlink={(entry) => (dialog = { kind: "link-entry", entry })}
|
||||||
|
onsavetocloud={(entry) =>
|
||||||
|
(dialog = { kind: "save-document-to-cloud", entry })}
|
||||||
onviewimage={(paths, index) => (imageViewer = { paths, index })}
|
onviewimage={(paths, index) => (imageViewer = { paths, index })}
|
||||||
ondownloaddocument={(documentId, title) =>
|
ondownloaddocument={(documentId, title) =>
|
||||||
downloadDocument(documentId, title)}
|
downloadDocument(documentId, title)}
|
||||||
onremovedownload={removeDownloadedDocument}
|
onremovedownload={removeDownloadedDocument}
|
||||||
ondownloadfile={downloadCloudFile}
|
ondownloadfile={downloadCloudFile}
|
||||||
onnewspace={() => (dialog = { kind: "new-space" })}
|
onnewcloudproject={() => (dialog = { kind: "new-cloud-project" })}
|
||||||
onclonespace={(id, name) => (dialog = { kind: "clone-space", id, name })}
|
oncloneproject={(id, name) =>
|
||||||
ondeletespace={(id) => (dialog = { kind: "delete-space", id })}
|
(dialog = { kind: "clone-cloud-project", id, name })}
|
||||||
|
ondeleteproject={(id) => (dialog = { kind: "delete-cloud-project", id })}
|
||||||
onsignin={() => (dialog = { kind: "login" })}
|
onsignin={() => (dialog = { kind: "login" })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -628,15 +644,17 @@
|
|||||||
files={app.target?.files ?? []}
|
files={app.target?.files ?? []}
|
||||||
activePath={app.activePath}
|
activePath={app.activePath}
|
||||||
entrypoint={app.target?.entrypoint ?? "main.typ"}
|
entrypoint={app.target?.entrypoint ?? "main.typ"}
|
||||||
selected={selectedEntry}
|
targetPath={app.target?.path ?? ""}
|
||||||
|
selected={selectedEntries}
|
||||||
dropTarget={treeDropTarget}
|
dropTarget={treeDropTarget}
|
||||||
onopen={openFile}
|
onopen={openFile}
|
||||||
onselect={(path, isDir) => {
|
onselect={(paths, primary, isDir) => {
|
||||||
selectedEntry = path;
|
selectedEntries = new Set(paths);
|
||||||
|
selectedEntry = primary;
|
||||||
selectedIsDir = isDir;
|
selectedIsDir = isDir;
|
||||||
}}
|
}}
|
||||||
onrename={(path) => (dialog = { kind: "rename-file", path })}
|
onrename={(path) => (dialog = { kind: "rename-file", path })}
|
||||||
ondelete={(path) => (dialog = { kind: "delete-file", path })}
|
ondelete={(paths) => (dialog = { kind: "delete-file", paths })}
|
||||||
onduplicate={duplicateInTarget}
|
onduplicate={duplicateInTarget}
|
||||||
onreveal={revealInTarget}
|
onreveal={revealInTarget}
|
||||||
onsetentry={setEntrypoint}
|
onsetentry={setEntrypoint}
|
||||||
@@ -792,36 +810,47 @@
|
|||||||
{@const target = dialog}
|
{@const target = dialog}
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
title="Upload to cloud"
|
title="Upload to cloud"
|
||||||
message="A new cloud space will be created for '{target.entry.name}' and its files uploaded."
|
message="A new cloud project will be created for '{target.entry.name}' and its files uploaded."
|
||||||
confirmLabel="Upload"
|
confirmLabel="Upload"
|
||||||
onconfirm={() => linkEntry(target.entry)}
|
onconfirm={() => linkEntry(target.entry)}
|
||||||
onclose={close}
|
onclose={close}
|
||||||
/>
|
/>
|
||||||
{:else if dialog.kind === "new-space"}
|
{:else if dialog.kind === "save-document-to-cloud"}
|
||||||
|
{@const target = dialog}
|
||||||
<PromptModal
|
<PromptModal
|
||||||
title="New cloud space"
|
title="Save to cloud"
|
||||||
label="Space name"
|
label="Document title"
|
||||||
icon="ph:cloud-plus"
|
icon="ph:cloud-arrow-up"
|
||||||
onsubmit={createSpace}
|
value={target.entry.name.replace(/\.typ$/i, "")}
|
||||||
|
confirmLabel="Save"
|
||||||
|
onsubmit={(title) => saveDocumentToCloud(target.entry, title)}
|
||||||
onclose={close}
|
onclose={close}
|
||||||
/>
|
/>
|
||||||
{:else if dialog.kind === "delete-space"}
|
{:else if dialog.kind === "new-cloud-project"}
|
||||||
|
<PromptModal
|
||||||
|
title="New cloud project"
|
||||||
|
label="Project name"
|
||||||
|
icon="ph:cloud-plus"
|
||||||
|
onsubmit={createCloudProject}
|
||||||
|
onclose={close}
|
||||||
|
/>
|
||||||
|
{:else if dialog.kind === "delete-cloud-project"}
|
||||||
{@const target = dialog}
|
{@const target = dialog}
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
title="Delete cloud space"
|
title="Delete cloud project"
|
||||||
message="This permanently deletes the space and its files from TypstDrive. Local copies are kept."
|
message="This permanently deletes the project and its files from TypstDrive. Local copies are kept."
|
||||||
onconfirm={() => deleteSpace(target.id)}
|
onconfirm={() => deleteCloudProject(target.id)}
|
||||||
onclose={close}
|
onclose={close}
|
||||||
/>
|
/>
|
||||||
{:else if dialog.kind === "clone-space"}
|
{:else if dialog.kind === "clone-cloud-project"}
|
||||||
{@const target = dialog}
|
{@const target = dialog}
|
||||||
<PromptModal
|
<PromptModal
|
||||||
title="Download space"
|
title="Download project"
|
||||||
label="Save as project"
|
label="Save as project"
|
||||||
icon="ph:download-simple"
|
icon="ph:download-simple"
|
||||||
value={target.name}
|
value={target.name}
|
||||||
confirmLabel="Download"
|
confirmLabel="Download"
|
||||||
onsubmit={(name) => cloneSpace(target.id, name)}
|
onsubmit={(name) => cloneCloudProject(target.id, name)}
|
||||||
onclose={close}
|
onclose={close}
|
||||||
/>
|
/>
|
||||||
{:else if dialog.kind === "new-file"}
|
{:else if dialog.kind === "new-file"}
|
||||||
@@ -857,9 +886,11 @@
|
|||||||
{:else if dialog.kind === "delete-file"}
|
{:else if dialog.kind === "delete-file"}
|
||||||
{@const target = dialog}
|
{@const target = dialog}
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
title="Delete file"
|
title={target.paths.length > 1 ? "Delete files" : "Delete file"}
|
||||||
message="'{target.path}' will be permanently deleted."
|
message={target.paths.length > 1
|
||||||
onconfirm={() => deleteFile(target.path)}
|
? `${target.paths.length} items will be permanently deleted.`
|
||||||
|
: `'${target.paths[0]}' will be permanently deleted.`}
|
||||||
|
onconfirm={() => deleteFile(target.paths)}
|
||||||
onclose={close}
|
onclose={close}
|
||||||
/>
|
/>
|
||||||
{:else if dialog.kind === "login"}
|
{:else if dialog.kind === "login"}
|
||||||
|
|||||||
Reference in New Issue
Block a user