Version checks, drop native drag plugin, cloud menus and delete, about links
This commit is contained in:
@@ -24,7 +24,6 @@
|
|||||||
"@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",
|
||||||
|
|||||||
Generated
+15
-185
@@ -722,19 +722,6 @@ 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"
|
||||||
@@ -1087,28 +1074,6 @@ 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"
|
||||||
@@ -3079,17 +3044,9 @@ 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]]
|
||||||
@@ -3109,7 +3066,6 @@ 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",
|
||||||
]
|
]
|
||||||
@@ -3170,19 +3126,6 @@ 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"
|
||||||
@@ -4840,7 +4783,7 @@ dependencies = [
|
|||||||
"bitflags 2.13.1",
|
"bitflags 2.13.1",
|
||||||
"block2",
|
"block2",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
"core-graphics 0.25.0",
|
"core-graphics",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"dbus",
|
"dbus",
|
||||||
"dispatch2",
|
"dispatch2",
|
||||||
@@ -4865,7 +4808,7 @@ dependencies = [
|
|||||||
"tao-macros",
|
"tao-macros",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"url",
|
"url",
|
||||||
"windows 0.61.3",
|
"windows",
|
||||||
"windows-core 0.61.2",
|
"windows-core 0.61.2",
|
||||||
"windows-version",
|
"windows-version",
|
||||||
"x11-dl",
|
"x11-dl",
|
||||||
@@ -4947,7 +4890,7 @@ dependencies = [
|
|||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"window-vibrancy",
|
"window-vibrancy",
|
||||||
"windows 0.61.3",
|
"windows",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5046,21 +4989,6 @@ 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"
|
||||||
@@ -5103,7 +5031,7 @@ dependencies = [
|
|||||||
"tauri-plugin",
|
"tauri-plugin",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"url",
|
"url",
|
||||||
"windows 0.61.3",
|
"windows",
|
||||||
"zbus",
|
"zbus",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5129,7 +5057,7 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"windows 0.61.3",
|
"windows",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5154,7 +5082,7 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"windows 0.61.3",
|
"windows",
|
||||||
"wry",
|
"wry",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5688,7 +5616,6 @@ 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",
|
||||||
@@ -6606,10 +6533,10 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"webview2-com-macros",
|
"webview2-com-macros",
|
||||||
"webview2-com-sys",
|
"webview2-com-sys",
|
||||||
"windows 0.61.3",
|
"windows",
|
||||||
"windows-core 0.61.2",
|
"windows-core 0.61.2",
|
||||||
"windows-implement 0.60.2",
|
"windows-implement",
|
||||||
"windows-interface 0.59.3",
|
"windows-interface",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6630,7 +6557,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 0.61.3",
|
"windows",
|
||||||
"windows-core 0.61.2",
|
"windows-core 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -6686,18 +6613,6 @@ 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"
|
||||||
@@ -6720,36 +6635,14 @@ 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 0.60.2",
|
"windows-implement",
|
||||||
"windows-interface 0.59.3",
|
"windows-interface",
|
||||||
"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",
|
||||||
@@ -6761,8 +6654,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 0.60.2",
|
"windows-implement",
|
||||||
"windows-interface 0.59.3",
|
"windows-interface",
|
||||||
"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",
|
||||||
@@ -6779,28 +6672,6 @@ 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"
|
||||||
@@ -6812,28 +6683,6 @@ 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"
|
||||||
@@ -6867,15 +6716,6 @@ 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"
|
||||||
@@ -6894,16 +6734,6 @@ 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"
|
||||||
@@ -7274,7 +7104,7 @@ dependencies = [
|
|||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webkit2gtk-sys",
|
"webkit2gtk-sys",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"windows 0.61.3",
|
"windows",
|
||||||
"windows-core 0.61.2",
|
"windows-core 0.61.2",
|
||||||
"windows-version",
|
"windows-version",
|
||||||
"x11-dl",
|
"x11-dl",
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ 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,7 +17,6 @@
|
|||||||
"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"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-1
@@ -675,6 +675,12 @@ fn lsp_running(state: State<'_, LspState>) -> bool {
|
|||||||
state.is_running()
|
state.is_running()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn cloud_check_compatibility(server_url: String) -> sync::CompatibilityStatus {
|
||||||
|
let server_url = server_url.trim_end_matches('/').to_string();
|
||||||
|
sync::check_compatibility(&server_url)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn cloud_login(
|
fn cloud_login(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
@@ -781,6 +787,30 @@ fn cloud_download_file(
|
|||||||
Ok(file.name)
|
Ok(file.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn cloud_delete_file(app: AppHandle, store: State<'_, Store>, file_id: String) -> Result<(), String> {
|
||||||
|
let (server_url, token) = cloud_credentials(&app, &store)?;
|
||||||
|
sync::delete_account_file(&server_url, &token, &file_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn cloud_delete_document(
|
||||||
|
app: AppHandle,
|
||||||
|
store: State<'_, Store>,
|
||||||
|
document_id: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let (server_url, token) = cloud_credentials(&app, &store)?;
|
||||||
|
sync::delete_document(&server_url, &token, &document_id)?;
|
||||||
|
|
||||||
|
if let Ok(links) = store.all_document_links() {
|
||||||
|
if let Some((path, _, _)) = links.into_iter().find(|(_, id, _)| id == &document_id) {
|
||||||
|
let _ = store.forget_document_link(&path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn cloud_list_shared(
|
fn cloud_list_shared(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
@@ -970,6 +1000,18 @@ fn cloud_create_project(app: AppHandle, store: State<'_, Store>, name: String) -
|
|||||||
sync::create_cloud_project(&server_url, &token, name.trim())
|
sync::create_cloud_project(&server_url, &token, name.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn cloud_new_document(app: AppHandle, store: State<'_, Store>, title: String) -> Result<sync::DocumentContent, String> {
|
||||||
|
let title = title.trim();
|
||||||
|
if title.is_empty() {
|
||||||
|
return Err("Document name cannot be empty".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (server_url, token) = cloud_credentials(&app, &store)?;
|
||||||
|
let content = format!("= {}\n\nStart writing here.\n", title);
|
||||||
|
sync::create_document(&server_url, &token, title, &content)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn cloud_delete_project(app: AppHandle, store: State<'_, Store>, cloud_project_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)?;
|
||||||
@@ -1101,7 +1143,6 @@ 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);
|
||||||
@@ -1149,6 +1190,7 @@ pub fn run() {
|
|||||||
lsp_send,
|
lsp_send,
|
||||||
lsp_stop,
|
lsp_stop,
|
||||||
lsp_running,
|
lsp_running,
|
||||||
|
cloud_check_compatibility,
|
||||||
cloud_login,
|
cloud_login,
|
||||||
cloud_logout,
|
cloud_logout,
|
||||||
cloud_account,
|
cloud_account,
|
||||||
@@ -1158,7 +1200,9 @@ pub fn run() {
|
|||||||
cloud_list_shared,
|
cloud_list_shared,
|
||||||
cloud_list_files,
|
cloud_list_files,
|
||||||
cloud_download_file,
|
cloud_download_file,
|
||||||
|
cloud_delete_file,
|
||||||
cloud_download_document,
|
cloud_download_document,
|
||||||
|
cloud_delete_document,
|
||||||
cloud_sync_document,
|
cloud_sync_document,
|
||||||
cloud_resolve_document,
|
cloud_resolve_document,
|
||||||
cloud_document_link,
|
cloud_document_link,
|
||||||
@@ -1167,6 +1211,7 @@ pub fn run() {
|
|||||||
cloud_unlink_document,
|
cloud_unlink_document,
|
||||||
cloud_create_document,
|
cloud_create_document,
|
||||||
cloud_create_project,
|
cloud_create_project,
|
||||||
|
cloud_new_document,
|
||||||
cloud_delete_project,
|
cloud_delete_project,
|
||||||
cloud_clone_project,
|
cloud_clone_project,
|
||||||
cloud_link_project,
|
cloud_link_project,
|
||||||
|
|||||||
@@ -34,6 +34,88 @@ fn describe(error: ureq::Error) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const MIN_SERVER_VERSION: &str = "1.5.0";
|
||||||
|
|
||||||
|
fn parse_version(version: &str) -> (u32, u32, u32) {
|
||||||
|
let mut parts = version.trim().split('.').map(|part| part.parse::<u32>().unwrap_or(0));
|
||||||
|
(
|
||||||
|
parts.next().unwrap_or(0),
|
||||||
|
parts.next().unwrap_or(0),
|
||||||
|
parts.next().unwrap_or(0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn version_at_least(actual: &str, required: &str) -> bool {
|
||||||
|
parse_version(actual) >= parse_version(required)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ServerVersionInfo {
|
||||||
|
server_version: String,
|
||||||
|
min_desktop_version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
pub struct CompatibilityStatus {
|
||||||
|
pub compatible: bool,
|
||||||
|
pub server_version: String,
|
||||||
|
pub desktop_version: String,
|
||||||
|
pub min_server_version: String,
|
||||||
|
pub min_desktop_version: String,
|
||||||
|
pub message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_compatibility(server_url: &str) -> CompatibilityStatus {
|
||||||
|
let desktop_version = env!("CARGO_PKG_VERSION").to_string();
|
||||||
|
|
||||||
|
let info = agent()
|
||||||
|
.get(&endpoint(server_url, "/version"))
|
||||||
|
.call()
|
||||||
|
.map_err(describe)
|
||||||
|
.and_then(|response| response.into_json::<ServerVersionInfo>().map_err(|e| e.to_string()));
|
||||||
|
|
||||||
|
match info {
|
||||||
|
Ok(info) => {
|
||||||
|
let server_too_old = !version_at_least(&info.server_version, MIN_SERVER_VERSION);
|
||||||
|
let desktop_too_old = !version_at_least(&desktop_version, &info.min_desktop_version);
|
||||||
|
|
||||||
|
let message = if server_too_old {
|
||||||
|
Some(format!(
|
||||||
|
"This app requires a TypstDrive server v{} or newer (server is running v{}). Ask the administrator to update it.",
|
||||||
|
MIN_SERVER_VERSION, info.server_version
|
||||||
|
))
|
||||||
|
} else if desktop_too_old {
|
||||||
|
Some(format!(
|
||||||
|
"This TypstDrive server requires typst-desktop v{} or newer (you have v{}). Please update the app.",
|
||||||
|
info.min_desktop_version, desktop_version
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
CompatibilityStatus {
|
||||||
|
compatible: !server_too_old && !desktop_too_old,
|
||||||
|
server_version: info.server_version,
|
||||||
|
desktop_version,
|
||||||
|
min_server_version: MIN_SERVER_VERSION.to_string(),
|
||||||
|
min_desktop_version: info.min_desktop_version,
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => CompatibilityStatus {
|
||||||
|
compatible: false,
|
||||||
|
server_version: "unknown".to_string(),
|
||||||
|
desktop_version,
|
||||||
|
min_server_version: MIN_SERVER_VERSION.to_string(),
|
||||||
|
min_desktop_version: "unknown".to_string(),
|
||||||
|
message: Some(format!(
|
||||||
|
"Could not determine the server's version. It may be unreachable, or older than v{} which doesn't support version checks. Please update the server.",
|
||||||
|
MIN_SERVER_VERSION
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
pub struct Account {
|
pub struct Account {
|
||||||
pub user_id: String,
|
pub user_id: String,
|
||||||
@@ -55,12 +137,20 @@ pub fn login(
|
|||||||
password: &str,
|
password: &str,
|
||||||
device_name: &str,
|
device_name: &str,
|
||||||
) -> Result<LoginResponse, String> {
|
) -> Result<LoginResponse, String> {
|
||||||
|
let status = check_compatibility(server_url);
|
||||||
|
if !status.compatible {
|
||||||
|
return Err(status
|
||||||
|
.message
|
||||||
|
.unwrap_or_else(|| "This server is not compatible with this app.".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
agent()
|
agent()
|
||||||
.post(&endpoint(server_url, "/auth/login"))
|
.post(&endpoint(server_url, "/auth/login"))
|
||||||
.send_json(ureq::json!({
|
.send_json(ureq::json!({
|
||||||
"email": email,
|
"email": email,
|
||||||
"password": password,
|
"password": password,
|
||||||
"device_name": device_name,
|
"device_name": device_name,
|
||||||
|
"client_version": env!("CARGO_PKG_VERSION"),
|
||||||
}))
|
}))
|
||||||
.map_err(describe)?
|
.map_err(describe)?
|
||||||
.into_json::<LoginResponse>()
|
.into_json::<LoginResponse>()
|
||||||
@@ -926,6 +1016,15 @@ pub fn push_document(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn delete_document(server_url: &str, token: &str, document_id: &str) -> Result<(), String> {
|
||||||
|
agent()
|
||||||
|
.delete(&endpoint(server_url, &format!("/documents/{}", document_id)))
|
||||||
|
.set("Authorization", &format!("Bearer {}", token))
|
||||||
|
.call()
|
||||||
|
.map_err(describe)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
pub struct CloudFile {
|
pub struct CloudFile {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
@@ -974,3 +1073,12 @@ pub fn pull_account_file(
|
|||||||
.into_json::<CloudFileContent>()
|
.into_json::<CloudFileContent>()
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn delete_account_file(server_url: &str, token: &str, file_id: &str) -> Result<(), String> {
|
||||||
|
agent()
|
||||||
|
.delete(&endpoint(server_url, &format!("/files/{}", file_id)))
|
||||||
|
.set("Authorization", &format!("Bearer {}", token))
|
||||||
|
.call()
|
||||||
|
.map_err(describe)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,7 +42,6 @@
|
|||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
],
|
],
|
||||||
"resources": ["icons/32x32.png"],
|
|
||||||
"linux": {
|
"linux": {
|
||||||
"deb": {
|
"deb": {
|
||||||
"depends": [
|
"depends": [
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Icon from "@iconify/svelte";
|
import Icon from "@iconify/svelte";
|
||||||
import { resolveResource } from "@tauri-apps/api/path";
|
import type { FileEntry } from "$lib/ts/api";
|
||||||
import { startDrag } from "@crabnebula/tauri-plugin-drag";
|
import { clampMenu } from "$lib/ts/menu-position";
|
||||||
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;
|
||||||
targetPath: string;
|
|
||||||
selected: Set<string>;
|
selected: Set<string>;
|
||||||
dropTarget: string | null;
|
dropTarget: string | null;
|
||||||
onopen: (path: string) => void;
|
onopen: (path: string) => void;
|
||||||
@@ -28,7 +26,6 @@
|
|||||||
files,
|
files,
|
||||||
activePath,
|
activePath,
|
||||||
entrypoint,
|
entrypoint,
|
||||||
targetPath,
|
|
||||||
selected,
|
selected,
|
||||||
dropTarget,
|
dropTarget,
|
||||||
onopen,
|
onopen,
|
||||||
@@ -115,10 +112,7 @@
|
|||||||
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 anchorPath = $state<string | null>(null);
|
||||||
let nativeDragSent = false;
|
|
||||||
let iconPathPromise: Promise<string> | null = null;
|
|
||||||
|
|
||||||
function flattenVisible(nodes: TreeNode[]): TreeNode[] {
|
function flattenVisible(nodes: TreeNode[]): TreeNode[] {
|
||||||
const result: TreeNode[] = [];
|
const result: TreeNode[] = [];
|
||||||
@@ -217,40 +211,6 @@
|
|||||||
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
|
||||||
@@ -258,7 +218,6 @@
|
|||||||
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)}
|
||||||
@@ -287,17 +246,10 @@
|
|||||||
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={() => {
|
ondragend={() => {
|
||||||
dragging = null;
|
dragging = null;
|
||||||
dragPaths = [];
|
|
||||||
nativeDragSent = false;
|
|
||||||
}}
|
}}
|
||||||
ondragover={(event) => {
|
ondragover={(event) => {
|
||||||
if (!dragging || !node.isDir) return;
|
if (!dragging || !node.isDir) return;
|
||||||
@@ -432,6 +384,7 @@
|
|||||||
selected.has(target.path) && selected.size > 1 ? [...selected] : [target.path]}
|
selected.has(target.path) && selected.size > 1 ? [...selected] : [target.path]}
|
||||||
{@const single = menuPaths.length === 1}
|
{@const single = menuPaths.length === 1}
|
||||||
<div
|
<div
|
||||||
|
use:clampMenu
|
||||||
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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import Icon from "@iconify/svelte";
|
import Icon from "@iconify/svelte";
|
||||||
import * as api from "$lib/ts/api";
|
import * as api from "$lib/ts/api";
|
||||||
import type { BrowseEntry, EntryKind } from "$lib/ts/api";
|
import type { BrowseEntry, EntryKind } from "$lib/ts/api";
|
||||||
|
import { clampMenu } from "$lib/ts/menu-position";
|
||||||
import {
|
import {
|
||||||
app,
|
app,
|
||||||
breadcrumbs,
|
breadcrumbs,
|
||||||
@@ -12,6 +13,8 @@
|
|||||||
openCloudFolder,
|
openCloudFolder,
|
||||||
openTarget,
|
openTarget,
|
||||||
refreshCloud,
|
refreshCloud,
|
||||||
|
refreshEntries,
|
||||||
|
setError,
|
||||||
} from "$lib/ts/state.svelte";
|
} from "$lib/ts/state.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -27,9 +30,12 @@
|
|||||||
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;
|
||||||
|
ondeletefile: (fileId: string) => void;
|
||||||
oncloneproject: (cloudProjectId: string, name: string) => void;
|
oncloneproject: (cloudProjectId: string, name: string) => void;
|
||||||
ondeleteproject: (cloudProjectId: string) => void;
|
ondeleteproject: (cloudProjectId: string) => void;
|
||||||
|
ondeletedocument: (documentId: string) => void;
|
||||||
onnewcloudproject: () => void;
|
onnewcloudproject: () => void;
|
||||||
|
onnewclouddocument: () => void;
|
||||||
onsignin: () => void;
|
onsignin: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,15 +52,35 @@
|
|||||||
ondownloaddocument,
|
ondownloaddocument,
|
||||||
onremovedownload,
|
onremovedownload,
|
||||||
ondownloadfile,
|
ondownloadfile,
|
||||||
|
ondeletefile,
|
||||||
oncloneproject,
|
oncloneproject,
|
||||||
ondeleteproject,
|
ondeleteproject,
|
||||||
|
ondeletedocument,
|
||||||
onnewcloudproject,
|
onnewcloudproject,
|
||||||
|
onnewclouddocument,
|
||||||
onsignin,
|
onsignin,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let menuFor = $state<string | null>(null);
|
let menuFor = $state<string | null>(null);
|
||||||
let menuAt = $state({ x: 0, y: 0 });
|
let menuAt = $state({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
function openContextMenu(event: MouseEvent, path: string) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
menuAt = { x: event.clientX, y: event.clientY };
|
||||||
|
menuFor = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cloudMenuFor = $state<string | null>(null);
|
||||||
|
let cloudMenuAt = $state({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
function openCloudContextMenu(event: MouseEvent, id: string) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
cloudMenuAt = { x: event.clientX, y: event.clientY };
|
||||||
|
cloudMenuFor = id;
|
||||||
|
}
|
||||||
|
|
||||||
const trail = $derived(breadcrumbs());
|
const trail = $derived(breadcrumbs());
|
||||||
const cloudTrail = $derived(cloudBreadcrumbs());
|
const cloudTrail = $derived(cloudBreadcrumbs());
|
||||||
|
|
||||||
@@ -169,6 +195,45 @@
|
|||||||
.map((entry) => entry.path),
|
.map((entry) => entry.path),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let dragging = $state<string | null>(null);
|
||||||
|
let dropTarget = $state<string | null>(null);
|
||||||
|
|
||||||
|
async function moveTo(source: string, destination: string) {
|
||||||
|
if (source === destination) return;
|
||||||
|
const parent = source.includes("/") ? source.slice(0, source.lastIndexOf("/")) : "";
|
||||||
|
if (parent === destination) return;
|
||||||
|
try {
|
||||||
|
await api.moveEntry(source, destination);
|
||||||
|
await refreshEntries();
|
||||||
|
} catch (error) {
|
||||||
|
setError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDrag(event: DragEvent, path: string) {
|
||||||
|
dragging = path;
|
||||||
|
event.dataTransfer?.setData("text/plain", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
function endDrag() {
|
||||||
|
dragging = null;
|
||||||
|
dropTarget = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function allowDrop(event: DragEvent, path: string) {
|
||||||
|
if (!dragging) return;
|
||||||
|
event.preventDefault();
|
||||||
|
dropTarget = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDrop(event: DragEvent, destination: string) {
|
||||||
|
event.preventDefault();
|
||||||
|
const source = dragging ?? event.dataTransfer?.getData("text/plain");
|
||||||
|
dragging = null;
|
||||||
|
dropTarget = null;
|
||||||
|
if (source) moveTo(source, destination);
|
||||||
|
}
|
||||||
|
|
||||||
function activate(entry: BrowseEntry) {
|
function activate(entry: BrowseEntry) {
|
||||||
if (entry.kind === "folder") {
|
if (entry.kind === "folder") {
|
||||||
browseTo(entry.path);
|
browseTo(entry.path);
|
||||||
@@ -192,7 +257,68 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#snippet cloudMenu(
|
||||||
|
id: string,
|
||||||
|
onopen: (() => void) | null,
|
||||||
|
ondownload: () => void,
|
||||||
|
onremove: (() => void) | null,
|
||||||
|
ondelete: (() => void) | null,
|
||||||
|
)}
|
||||||
|
{#if cloudMenuFor === id}
|
||||||
|
<div
|
||||||
|
use:clampMenu
|
||||||
|
class="fixed z-50 flex w-44 flex-col rounded-md border border-[var(--color-line)] bg-[var(--color-surface)] py-1 text-xs shadow-lg"
|
||||||
|
style="left: {cloudMenuAt.x}px; top: {cloudMenuAt.y}px"
|
||||||
|
>
|
||||||
|
{#if onopen}
|
||||||
|
<button
|
||||||
|
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
||||||
|
onclick={() => {
|
||||||
|
onopen();
|
||||||
|
cloudMenuFor = null;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Open
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
||||||
|
onclick={() => {
|
||||||
|
ondownload();
|
||||||
|
cloudMenuFor = null;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#if onremove}
|
||||||
|
<button
|
||||||
|
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
||||||
|
onclick={() => {
|
||||||
|
onremove();
|
||||||
|
cloudMenuFor = null;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove from this device
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#if ondelete}
|
||||||
|
<button
|
||||||
|
class="px-3 py-1.5 text-left text-[var(--color-danger)] hover:bg-[var(--color-surface-sunken)]"
|
||||||
|
onclick={() => {
|
||||||
|
ondelete();
|
||||||
|
cloudMenuFor = null;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete from cloud
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
{#snippet cloudCard(
|
{#snippet cloudCard(
|
||||||
|
id: string,
|
||||||
icon: string,
|
icon: string,
|
||||||
title: string,
|
title: string,
|
||||||
meta: string,
|
meta: string,
|
||||||
@@ -200,10 +326,13 @@
|
|||||||
onopen: (() => void) | null,
|
onopen: (() => void) | null,
|
||||||
ondownload: () => void,
|
ondownload: () => void,
|
||||||
onremove: (() => void) | null,
|
onremove: (() => void) | null,
|
||||||
|
ondelete: (() => void) | null,
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
class="group flex flex-col overflow-hidden rounded-lg border border-[var(--color-line)] bg-[var(--color-surface)] transition hover:border-[var(--color-accent)] hover:shadow-sm"
|
class="group flex flex-col overflow-hidden rounded-lg border border-[var(--color-line)] bg-[var(--color-surface)] transition hover:border-[var(--color-accent)] hover:shadow-sm"
|
||||||
|
oncontextmenu={(event) => openCloudContextMenu(event, id)}
|
||||||
>
|
>
|
||||||
|
{@render cloudMenu(id, onopen, ondownload, onremove, ondelete)}
|
||||||
<div
|
<div
|
||||||
class="flex h-24 items-center justify-center overflow-hidden border-b border-[var(--color-line)] bg-[var(--color-surface-muted)]"
|
class="flex h-24 items-center justify-center overflow-hidden border-b border-[var(--color-line)] bg-[var(--color-surface-muted)]"
|
||||||
>
|
>
|
||||||
@@ -278,11 +407,11 @@
|
|||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if onremove}
|
{#if onremove || ondelete}
|
||||||
<button
|
<button
|
||||||
class="rounded-md border border-[var(--color-line)] px-2 py-1.5 text-[10px] text-[var(--color-ink-muted)] transition hover:border-[var(--color-danger)] hover:text-[var(--color-danger)]"
|
class="rounded-md border border-[var(--color-line)] px-2 py-1.5 text-[10px] text-[var(--color-ink-muted)] transition hover:border-[var(--color-danger)] hover:text-[var(--color-danger)]"
|
||||||
onclick={onremove}
|
onclick={onremove ?? ondelete}
|
||||||
aria-label="Remove"
|
aria-label={onremove ? "Remove" : "Delete"}
|
||||||
>
|
>
|
||||||
<Icon icon="ph:trash" />
|
<Icon icon="ph:trash" />
|
||||||
</button>
|
</button>
|
||||||
@@ -331,7 +460,8 @@
|
|||||||
|
|
||||||
{#if menuFor === entry.path}
|
{#if menuFor === entry.path}
|
||||||
<div
|
<div
|
||||||
class="fixed z-50 flex w-40 -translate-x-full flex-col rounded-md border border-[var(--color-line)] bg-[var(--color-surface)] py-1 text-xs shadow-lg"
|
use:clampMenu
|
||||||
|
class="fixed z-50 flex w-40 flex-col rounded-md border border-[var(--color-line)] bg-[var(--color-surface)] py-1 text-xs shadow-lg"
|
||||||
style="left: {menuAt.x}px; top: {menuAt.y}px"
|
style="left: {menuAt.x}px; top: {menuAt.y}px"
|
||||||
>
|
>
|
||||||
{#if entry.kind === "project" || entry.kind === "document"}
|
{#if entry.kind === "project" || entry.kind === "document"}
|
||||||
@@ -404,10 +534,14 @@
|
|||||||
if (!(event.target as HTMLElement).closest("[data-card-menu]")) {
|
if (!(event.target as HTMLElement).closest("[data-card-menu]")) {
|
||||||
menuFor = null;
|
menuFor = null;
|
||||||
}
|
}
|
||||||
|
cloudMenuFor = null;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex h-full flex-col bg-[var(--color-surface-muted)]">
|
<div
|
||||||
|
class="flex h-full flex-col bg-[var(--color-surface-muted)]"
|
||||||
|
oncontextmenu={(event) => event.preventDefault()}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-2 border-b border-[var(--color-line)] bg-[var(--color-surface)] px-4 py-2.5"
|
class="flex items-center gap-2 border-b border-[var(--color-line)] bg-[var(--color-surface)] px-4 py-2.5"
|
||||||
>
|
>
|
||||||
@@ -459,11 +593,18 @@
|
|||||||
</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 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={onnewcloudproject}
|
onclick={onnewcloudproject}
|
||||||
|
>
|
||||||
|
<Icon icon="ph:folder-star" />
|
||||||
|
Project
|
||||||
|
</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"
|
||||||
|
onclick={onnewclouddocument}
|
||||||
>
|
>
|
||||||
<Icon icon="ph:plus" />
|
<Icon icon="ph:plus" />
|
||||||
New cloud project
|
New document
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -474,8 +615,14 @@
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="flex items-center gap-1.5 rounded px-2 py-1 transition hover:bg-[var(--color-surface-muted)]
|
class="flex items-center gap-1.5 rounded px-2 py-1 transition hover:bg-[var(--color-surface-muted)]
|
||||||
{app.currentDir === '' ? 'font-medium' : 'text-[var(--color-ink-muted)]'}"
|
{app.currentDir === '' ? 'font-medium' : 'text-[var(--color-ink-muted)]'}
|
||||||
|
{dropTarget === '' ? 'bg-[var(--color-accent-soft)] text-[var(--color-accent)]' : ''}"
|
||||||
onclick={() => browseTo("")}
|
onclick={() => browseTo("")}
|
||||||
|
ondragover={(event) => allowDrop(event, "")}
|
||||||
|
ondragleave={() => {
|
||||||
|
if (dropTarget === "") dropTarget = null;
|
||||||
|
}}
|
||||||
|
ondrop={(event) => handleDrop(event, "")}
|
||||||
>
|
>
|
||||||
<Icon icon="ph:house" />
|
<Icon icon="ph:house" />
|
||||||
Workspace
|
Workspace
|
||||||
@@ -485,8 +632,14 @@
|
|||||||
<Icon icon="ph:caret-right" class="text-[10px] text-[var(--color-ink-muted)]" />
|
<Icon icon="ph:caret-right" class="text-[10px] text-[var(--color-ink-muted)]" />
|
||||||
<button
|
<button
|
||||||
class="rounded px-2 py-1 transition hover:bg-[var(--color-surface-muted)]
|
class="rounded px-2 py-1 transition hover:bg-[var(--color-surface-muted)]
|
||||||
{index === trail.length - 1 ? 'font-medium' : 'text-[var(--color-ink-muted)]'}"
|
{index === trail.length - 1 ? 'font-medium' : 'text-[var(--color-ink-muted)]'}
|
||||||
|
{dropTarget === crumb.path ? 'bg-[var(--color-accent-soft)] text-[var(--color-accent)]' : ''}"
|
||||||
onclick={() => browseTo(crumb.path)}
|
onclick={() => browseTo(crumb.path)}
|
||||||
|
ondragover={(event) => allowDrop(event, crumb.path)}
|
||||||
|
ondragleave={() => {
|
||||||
|
if (dropTarget === crumb.path) dropTarget = null;
|
||||||
|
}}
|
||||||
|
ondrop={(event) => handleDrop(event, crumb.path)}
|
||||||
>
|
>
|
||||||
{crumb.name}
|
{crumb.name}
|
||||||
</button>
|
</button>
|
||||||
@@ -527,7 +680,19 @@
|
|||||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(210px,1fr))] gap-2">
|
<div class="grid grid-cols-[repeat(auto-fill,minmax(210px,1fr))] gap-2">
|
||||||
{#each containers as entry (entry.path)}
|
{#each containers as entry (entry.path)}
|
||||||
<div
|
<div
|
||||||
class="group relative flex items-center gap-2.5 rounded-lg border border-[var(--color-line)] bg-[var(--color-surface-sunken)] px-3 py-2.5 transition hover:border-[var(--color-accent)] hover:bg-[var(--color-surface)]"
|
class="group relative flex items-center gap-2.5 rounded-lg border px-3 py-2.5 transition hover:border-[var(--color-accent)] hover:bg-[var(--color-surface)]
|
||||||
|
{dropTarget === entry.path
|
||||||
|
? 'border-[var(--color-accent)] bg-[var(--color-accent-soft)]'
|
||||||
|
: 'border-[var(--color-line)] bg-[var(--color-surface-sunken)]'}"
|
||||||
|
draggable="true"
|
||||||
|
ondragstart={(event) => startDrag(event, entry.path)}
|
||||||
|
ondragend={endDrag}
|
||||||
|
ondragover={(event) => allowDrop(event, entry.path)}
|
||||||
|
ondragleave={() => {
|
||||||
|
if (dropTarget === entry.path) dropTarget = null;
|
||||||
|
}}
|
||||||
|
ondrop={(event) => handleDrop(event, entry.path)}
|
||||||
|
oncontextmenu={(event) => openContextMenu(event, entry.path)}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="flex min-w-0 flex-1 items-center gap-2.5 text-left"
|
class="flex min-w-0 flex-1 items-center gap-2.5 text-left"
|
||||||
@@ -579,6 +744,10 @@
|
|||||||
{#each documents as entry (entry.path)}
|
{#each documents as entry (entry.path)}
|
||||||
<div
|
<div
|
||||||
class="group relative flex flex-col overflow-hidden rounded-lg border border-[var(--color-line)] bg-[var(--color-surface)] transition hover:border-[var(--color-accent)] hover:shadow-md"
|
class="group relative flex flex-col overflow-hidden rounded-lg border border-[var(--color-line)] bg-[var(--color-surface)] transition hover:border-[var(--color-accent)] hover:shadow-md"
|
||||||
|
draggable="true"
|
||||||
|
ondragstart={(event) => startDrag(event, entry.path)}
|
||||||
|
ondragend={endDrag}
|
||||||
|
oncontextmenu={(event) => openContextMenu(event, entry.path)}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="flex flex-col text-left"
|
class="flex flex-col text-left"
|
||||||
@@ -729,9 +898,12 @@
|
|||||||
class="mb-4 grid grid-cols-[repeat(auto-fill,minmax(210px,1fr))] gap-2"
|
class="mb-4 grid grid-cols-[repeat(auto-fill,minmax(210px,1fr))] gap-2"
|
||||||
>
|
>
|
||||||
{#each app.cloudFolders as folder (folder.id)}
|
{#each app.cloudFolders as folder (folder.id)}
|
||||||
|
<div class="relative">
|
||||||
<button
|
<button
|
||||||
class="flex items-center gap-2.5 rounded-lg border border-[var(--color-line)] bg-[var(--color-surface-sunken)] px-3 py-2.5 text-left transition hover:border-[var(--color-accent)] hover:bg-[var(--color-surface)]"
|
class="flex w-full items-center gap-2.5 rounded-lg border border-[var(--color-line)] bg-[var(--color-surface-sunken)] px-3 py-2.5 text-left transition hover:border-[var(--color-accent)] hover:bg-[var(--color-surface)]"
|
||||||
onclick={() => openCloudFolder(folder.id)}
|
onclick={() => openCloudFolder(folder.id)}
|
||||||
|
oncontextmenu={(event) =>
|
||||||
|
openCloudContextMenu(event, `folder:${folder.id}`)}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
icon="ph:folder-fill"
|
icon="ph:folder-fill"
|
||||||
@@ -739,6 +911,14 @@
|
|||||||
/>
|
/>
|
||||||
<span class="truncate text-xs font-medium">{folder.name}</span>
|
<span class="truncate text-xs font-medium">{folder.name}</span>
|
||||||
</button>
|
</button>
|
||||||
|
{@render cloudMenu(
|
||||||
|
`folder:${folder.id}`,
|
||||||
|
() => openCloudFolder(folder.id),
|
||||||
|
() => {},
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -755,6 +935,7 @@
|
|||||||
{#each app.cloudDocuments as entry (entry.id)}
|
{#each app.cloudDocuments as entry (entry.id)}
|
||||||
{@const linked = linkedDocument(entry.id)}
|
{@const linked = linkedDocument(entry.id)}
|
||||||
{@render cloudCard(
|
{@render cloudCard(
|
||||||
|
`document:${entry.id}`,
|
||||||
"ph:file-text",
|
"ph:file-text",
|
||||||
entry.title,
|
entry.title,
|
||||||
linked
|
linked
|
||||||
@@ -764,6 +945,7 @@
|
|||||||
linked ? () => openTarget(linked.path) : null,
|
linked ? () => openTarget(linked.path) : null,
|
||||||
() => ondownloaddocument(entry.id, entry.title),
|
() => ondownloaddocument(entry.id, entry.title),
|
||||||
linked ? () => onremovedownload(linked.path) : null,
|
linked ? () => onremovedownload(linked.path) : null,
|
||||||
|
() => ondeletedocument(entry.id),
|
||||||
)}
|
)}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -781,7 +963,16 @@
|
|||||||
{#each app.cloudFiles as file (file.id)}
|
{#each app.cloudFiles as file (file.id)}
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-2.5 rounded-lg border border-[var(--color-line)] bg-[var(--color-surface)] p-3 transition hover:border-[var(--color-accent)]"
|
class="flex items-center gap-2.5 rounded-lg border border-[var(--color-line)] bg-[var(--color-surface)] p-3 transition hover:border-[var(--color-accent)]"
|
||||||
|
oncontextmenu={(event) =>
|
||||||
|
openCloudContextMenu(event, `file:${file.id}`)}
|
||||||
>
|
>
|
||||||
|
{@render cloudMenu(
|
||||||
|
`file:${file.id}`,
|
||||||
|
null,
|
||||||
|
() => ondownloadfile(file.id, file.name),
|
||||||
|
null,
|
||||||
|
() => ondeletefile(file.id),
|
||||||
|
)}
|
||||||
<Icon
|
<Icon
|
||||||
icon={api.isImagePath(file.name)
|
icon={api.isImagePath(file.name)
|
||||||
? "ph:image"
|
? "ph:image"
|
||||||
@@ -817,6 +1008,22 @@
|
|||||||
>
|
>
|
||||||
<Icon icon="ph:cloud" class="text-5xl" />
|
<Icon icon="ph:cloud" class="text-5xl" />
|
||||||
<p class="text-sm">Nothing here yet.</p>
|
<p class="text-sm">Nothing here yet.</p>
|
||||||
|
{#if app.cloudFolder !== "shared"}
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
class="rounded-md bg-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-white hover:opacity-90"
|
||||||
|
onclick={onnewclouddocument}
|
||||||
|
>
|
||||||
|
New document
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="rounded-md border border-[var(--color-line)] px-3 py-1.5 text-xs hover:bg-[var(--color-surface)]"
|
||||||
|
onclick={onnewcloudproject}
|
||||||
|
>
|
||||||
|
New project
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -831,6 +1038,7 @@
|
|||||||
{#each app.cloudProjects as project (project.id)}
|
{#each app.cloudProjects as project (project.id)}
|
||||||
{@const linked = linkedProject(project.id)}
|
{@const linked = linkedProject(project.id)}
|
||||||
{@render cloudCard(
|
{@render cloudCard(
|
||||||
|
`project:${project.id}`,
|
||||||
"ph:folder-star",
|
"ph:folder-star",
|
||||||
project.name,
|
project.name,
|
||||||
linked
|
linked
|
||||||
@@ -839,6 +1047,7 @@
|
|||||||
linked,
|
linked,
|
||||||
linked ? () => openTarget(linked.path) : null,
|
linked ? () => openTarget(linked.path) : null,
|
||||||
() => oncloneproject(project.id, project.name),
|
() => oncloneproject(project.id, project.name),
|
||||||
|
null,
|
||||||
project.role === "owner" ? () => ondeleteproject(project.id) : null,
|
project.role === "owner" ? () => ondeleteproject(project.id) : null,
|
||||||
)}
|
)}
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||||
import Modal from "./Modal.svelte";
|
import Modal from "./Modal.svelte";
|
||||||
import * as api from "$lib/ts/api";
|
import * as api from "$lib/ts/api";
|
||||||
import type { AppInfo } from "$lib/ts/api";
|
import type { AppInfo, CompatibilityStatus } from "$lib/ts/api";
|
||||||
import {
|
import {
|
||||||
app,
|
app,
|
||||||
applyTheme,
|
applyTheme,
|
||||||
@@ -62,6 +62,28 @@
|
|||||||
|
|
||||||
let workspaceRoot = $state(untrack(() => app.settings?.workspace_root ?? ""));
|
let workspaceRoot = $state(untrack(() => app.settings?.workspace_root ?? ""));
|
||||||
let serverUrl = $state(untrack(() => app.settings?.server_url ?? ""));
|
let serverUrl = $state(untrack(() => app.settings?.server_url ?? ""));
|
||||||
|
let compatibility = $state<CompatibilityStatus | null>(null);
|
||||||
|
let checkingCompatibility = $state(false);
|
||||||
|
let compatibilityTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const url = serverUrl.trim();
|
||||||
|
clearTimeout(compatibilityTimer);
|
||||||
|
if (!url) {
|
||||||
|
compatibility = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
compatibilityTimer = setTimeout(async () => {
|
||||||
|
checkingCompatibility = true;
|
||||||
|
try {
|
||||||
|
compatibility = await api.cloudCheckCompatibility(url);
|
||||||
|
} catch {
|
||||||
|
compatibility = null;
|
||||||
|
} finally {
|
||||||
|
checkingCompatibility = false;
|
||||||
|
}
|
||||||
|
}, 600);
|
||||||
|
});
|
||||||
let autosaveSeconds = $state(untrack(() => app.settings?.autosave_seconds ?? 0));
|
let autosaveSeconds = $state(untrack(() => app.settings?.autosave_seconds ?? 0));
|
||||||
let syncMinutes = $state(untrack(() => app.settings?.sync_minutes ?? 0));
|
let syncMinutes = $state(untrack(() => app.settings?.sync_minutes ?? 0));
|
||||||
let saving = $state(false);
|
let saving = $state(false);
|
||||||
@@ -101,6 +123,16 @@
|
|||||||
url: "https://typst.app/universe/",
|
url: "https://typst.app/universe/",
|
||||||
icon: "ph:planet",
|
icon: "ph:planet",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "GitHub Repository",
|
||||||
|
url: "https://github.com/SirBlobby/typst-desktop",
|
||||||
|
icon: "ph:github-logo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Report an Issue",
|
||||||
|
url: "https://github.com/SirBlobby/typst-desktop/issues",
|
||||||
|
icon: "ph:bug",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
async function browse() {
|
async function browse() {
|
||||||
@@ -366,6 +398,22 @@
|
|||||||
TypstDrive server
|
TypstDrive server
|
||||||
</span>
|
</span>
|
||||||
<input class={fieldClass} bind:value={serverUrl} />
|
<input class={fieldClass} bind:value={serverUrl} />
|
||||||
|
{#if checkingCompatibility}
|
||||||
|
<span class="flex items-center gap-1 text-[var(--color-ink-muted)]">
|
||||||
|
<Icon icon="ph:circle-notch" class="animate-spin" />
|
||||||
|
Checking server version...
|
||||||
|
</span>
|
||||||
|
{:else if compatibility && !compatibility.compatible}
|
||||||
|
<span class="flex items-center gap-1 text-[var(--color-danger)]">
|
||||||
|
<Icon icon="ph:warning-circle" />
|
||||||
|
{compatibility.message}
|
||||||
|
</span>
|
||||||
|
{:else if compatibility && compatibility.compatible}
|
||||||
|
<span class="flex items-center gap-1 text-[var(--color-success)]">
|
||||||
|
<Icon icon="ph:check-circle" />
|
||||||
|
Compatible (server v{compatibility.server_version})
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1 text-xs">
|
<div class="flex flex-col gap-1 text-xs">
|
||||||
|
|||||||
@@ -251,6 +251,18 @@ export const updateSettings = (changes: {
|
|||||||
syncMinutes?: number;
|
syncMinutes?: number;
|
||||||
}) => invoke<Settings>("update_settings", changes);
|
}) => invoke<Settings>("update_settings", changes);
|
||||||
|
|
||||||
|
export interface CompatibilityStatus {
|
||||||
|
compatible: boolean;
|
||||||
|
server_version: string;
|
||||||
|
desktop_version: string;
|
||||||
|
min_server_version: string;
|
||||||
|
min_desktop_version: string;
|
||||||
|
message: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cloudCheckCompatibility = (serverUrl: string) =>
|
||||||
|
invoke<CompatibilityStatus>("cloud_check_compatibility", { serverUrl });
|
||||||
|
|
||||||
export const cloudLogin = (
|
export const cloudLogin = (
|
||||||
serverUrl: string,
|
serverUrl: string,
|
||||||
email: string,
|
email: string,
|
||||||
@@ -314,12 +326,29 @@ export const cloudListFiles = (folderId?: string | null) =>
|
|||||||
export const cloudDownloadFile = (fileId: string) =>
|
export const cloudDownloadFile = (fileId: string) =>
|
||||||
invoke<string>("cloud_download_file", { fileId });
|
invoke<string>("cloud_download_file", { fileId });
|
||||||
|
|
||||||
|
export const cloudDeleteFile = (fileId: string) =>
|
||||||
|
invoke<void>("cloud_delete_file", { fileId });
|
||||||
|
|
||||||
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 cloudDeleteDocument = (documentId: string) =>
|
||||||
|
invoke<void>("cloud_delete_document", { documentId });
|
||||||
|
|
||||||
export const cloudCreateDocument = (path: string, title: string) =>
|
export const cloudCreateDocument = (path: string, title: string) =>
|
||||||
invoke<string>("cloud_create_document", { path, title });
|
invoke<string>("cloud_create_document", { path, title });
|
||||||
|
|
||||||
|
export interface DocumentContent {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
role: string;
|
||||||
|
hash: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cloudNewDocument = (title: string) =>
|
||||||
|
invoke<DocumentContent>("cloud_new_document", { title });
|
||||||
|
|
||||||
export const cloudSyncDocument = (path: string) =>
|
export const cloudSyncDocument = (path: string) =>
|
||||||
invoke<SyncReport>("cloud_sync_document", { path });
|
invoke<SyncReport>("cloud_sync_document", { path });
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export function clampMenu(node: HTMLElement) {
|
||||||
|
const margin = 8;
|
||||||
|
const rect = node.getBoundingClientRect();
|
||||||
|
|
||||||
|
let left = rect.left;
|
||||||
|
let top = rect.top;
|
||||||
|
|
||||||
|
if (rect.right > window.innerWidth - margin) {
|
||||||
|
left = window.innerWidth - rect.width - margin;
|
||||||
|
}
|
||||||
|
if (rect.bottom > window.innerHeight - margin) {
|
||||||
|
top = window.innerHeight - rect.height - margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
left = Math.max(margin, left);
|
||||||
|
top = Math.max(margin, top);
|
||||||
|
|
||||||
|
node.style.left = `${left}px`;
|
||||||
|
node.style.top = `${top}px`;
|
||||||
|
}
|
||||||
+49
-1
@@ -38,6 +38,7 @@
|
|||||||
openFile,
|
openFile,
|
||||||
openTarget,
|
openTarget,
|
||||||
refreshAccount,
|
refreshAccount,
|
||||||
|
refreshCloud,
|
||||||
refreshCloudProjects,
|
refreshCloudProjects,
|
||||||
refreshEntries,
|
refreshEntries,
|
||||||
refreshTarget,
|
refreshTarget,
|
||||||
@@ -62,7 +63,10 @@
|
|||||||
| { kind: "link-entry"; entry: BrowseEntry }
|
| { kind: "link-entry"; entry: BrowseEntry }
|
||||||
| { kind: "save-document-to-cloud"; entry: BrowseEntry }
|
| { kind: "save-document-to-cloud"; entry: BrowseEntry }
|
||||||
| { kind: "new-cloud-project" }
|
| { kind: "new-cloud-project" }
|
||||||
|
| { kind: "new-cloud-document" }
|
||||||
| { kind: "delete-cloud-project"; id: string }
|
| { kind: "delete-cloud-project"; id: string }
|
||||||
|
| { kind: "delete-cloud-document"; id: string }
|
||||||
|
| { kind: "delete-cloud-file"; id: string }
|
||||||
| { kind: "clone-cloud-project"; id: string; name: 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 }
|
||||||
@@ -171,12 +175,30 @@
|
|||||||
await refreshCloudProjects();
|
await refreshCloudProjects();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createCloudDocument = (title: string) =>
|
||||||
|
guard(async () => {
|
||||||
|
await api.cloudNewDocument(title);
|
||||||
|
await refreshCloud();
|
||||||
|
});
|
||||||
|
|
||||||
const deleteCloudProject = (id: string) =>
|
const deleteCloudProject = (id: string) =>
|
||||||
guard(async () => {
|
guard(async () => {
|
||||||
await api.cloudDeleteProject(id);
|
await api.cloudDeleteProject(id);
|
||||||
await refreshCloudProjects();
|
await refreshCloudProjects();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const deleteCloudDocument = (id: string) =>
|
||||||
|
guard(async () => {
|
||||||
|
await api.cloudDeleteDocument(id);
|
||||||
|
await refreshCloud();
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteCloudFile = (id: string) =>
|
||||||
|
guard(async () => {
|
||||||
|
await api.cloudDeleteFile(id);
|
||||||
|
await refreshCloud();
|
||||||
|
});
|
||||||
|
|
||||||
const cloneCloudProject = (id: string, name: string) =>
|
const cloneCloudProject = (id: string, name: string) =>
|
||||||
guard(async () => {
|
guard(async () => {
|
||||||
await api.cloudCloneProject(id, name);
|
await api.cloudCloneProject(id, name);
|
||||||
@@ -618,10 +640,13 @@
|
|||||||
downloadDocument(documentId, title)}
|
downloadDocument(documentId, title)}
|
||||||
onremovedownload={removeDownloadedDocument}
|
onremovedownload={removeDownloadedDocument}
|
||||||
ondownloadfile={downloadCloudFile}
|
ondownloadfile={downloadCloudFile}
|
||||||
|
ondeletefile={(id) => (dialog = { kind: "delete-cloud-file", id })}
|
||||||
onnewcloudproject={() => (dialog = { kind: "new-cloud-project" })}
|
onnewcloudproject={() => (dialog = { kind: "new-cloud-project" })}
|
||||||
|
onnewclouddocument={() => (dialog = { kind: "new-cloud-document" })}
|
||||||
oncloneproject={(id, name) =>
|
oncloneproject={(id, name) =>
|
||||||
(dialog = { kind: "clone-cloud-project", id, name })}
|
(dialog = { kind: "clone-cloud-project", id, name })}
|
||||||
ondeleteproject={(id) => (dialog = { kind: "delete-cloud-project", id })}
|
ondeleteproject={(id) => (dialog = { kind: "delete-cloud-project", id })}
|
||||||
|
ondeletedocument={(id) => (dialog = { kind: "delete-cloud-document", id })}
|
||||||
onsignin={() => (dialog = { kind: "login" })}
|
onsignin={() => (dialog = { kind: "login" })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -644,7 +669,6 @@
|
|||||||
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"}
|
||||||
targetPath={app.target?.path ?? ""}
|
|
||||||
selected={selectedEntries}
|
selected={selectedEntries}
|
||||||
dropTarget={treeDropTarget}
|
dropTarget={treeDropTarget}
|
||||||
onopen={openFile}
|
onopen={openFile}
|
||||||
@@ -834,6 +858,14 @@
|
|||||||
onsubmit={createCloudProject}
|
onsubmit={createCloudProject}
|
||||||
onclose={close}
|
onclose={close}
|
||||||
/>
|
/>
|
||||||
|
{:else if dialog.kind === "new-cloud-document"}
|
||||||
|
<PromptModal
|
||||||
|
title="New cloud document"
|
||||||
|
label="Document title"
|
||||||
|
icon="ph:cloud-plus"
|
||||||
|
onsubmit={createCloudDocument}
|
||||||
|
onclose={close}
|
||||||
|
/>
|
||||||
{:else if dialog.kind === "delete-cloud-project"}
|
{:else if dialog.kind === "delete-cloud-project"}
|
||||||
{@const target = dialog}
|
{@const target = dialog}
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
@@ -842,6 +874,22 @@
|
|||||||
onconfirm={() => deleteCloudProject(target.id)}
|
onconfirm={() => deleteCloudProject(target.id)}
|
||||||
onclose={close}
|
onclose={close}
|
||||||
/>
|
/>
|
||||||
|
{:else if dialog.kind === "delete-cloud-document"}
|
||||||
|
{@const target = dialog}
|
||||||
|
<ConfirmModal
|
||||||
|
title="Delete cloud document"
|
||||||
|
message="This permanently deletes the document from TypstDrive. A local copy, if downloaded, is kept."
|
||||||
|
onconfirm={() => deleteCloudDocument(target.id)}
|
||||||
|
onclose={close}
|
||||||
|
/>
|
||||||
|
{:else if dialog.kind === "delete-cloud-file"}
|
||||||
|
{@const target = dialog}
|
||||||
|
<ConfirmModal
|
||||||
|
title="Delete cloud file"
|
||||||
|
message="This permanently deletes the file from TypstDrive."
|
||||||
|
onconfirm={() => deleteCloudFile(target.id)}
|
||||||
|
onclose={close}
|
||||||
|
/>
|
||||||
{:else if dialog.kind === "clone-cloud-project"}
|
{:else if dialog.kind === "clone-cloud-project"}
|
||||||
{@const target = dialog}
|
{@const target = dialog}
|
||||||
<PromptModal
|
<PromptModal
|
||||||
|
|||||||
Reference in New Issue
Block a user