From 117f5b499c8c3ae6cd279f37b1a4133054cccfc7 Mon Sep 17 00:00:00 2001 From: SirBlobby Date: Mon, 20 Jul 2026 18:37:28 -0400 Subject: [PATCH] Version checks, drop native drag plugin, cloud menus and delete, about links --- package.json | 1 - src-tauri/Cargo.lock | 200 ++----------------- src-tauri/Cargo.toml | 1 - src-tauri/capabilities/default.json | 3 +- src-tauri/src/lib.rs | 47 ++++- src-tauri/src/sync.rs | 108 ++++++++++ src-tauri/tauri.conf.json | 1 - src/lib/components/FileTree.svelte | 53 +---- src/lib/components/FileViewer.svelte | 249 ++++++++++++++++++++++-- src/lib/components/SettingsModal.svelte | 50 ++++- src/lib/ts/api.ts | 29 +++ src/lib/ts/menu-position.ts | 20 ++ src/routes/+page.svelte | 50 ++++- 13 files changed, 549 insertions(+), 263 deletions(-) create mode 100644 src/lib/ts/menu-position.ts diff --git a/package.json b/package.json index ed25ea5..d11967e 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "@codemirror/view": "^6.43.6", "@iconify/svelte": "^5.2.2", "@lezer/highlight": "^1.2.3", - "@crabnebula/tauri-plugin-drag": "^2.1.0", "@tauri-apps/api": "^2.11.1", "@tauri-apps/plugin-dialog": "^2.7.2", "@tauri-apps/plugin-opener": "^2.5.4", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index f805edf..848b568 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -722,19 +722,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "core-graphics" version = "0.25.0" @@ -1087,28 +1074,6 @@ dependencies = [ "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]] name = "dtoa" version = "1.0.11" @@ -3079,17 +3044,9 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.13.1", "block2", - "libc", "objc2", - "objc2-cloud-kit", - "objc2-core-data", "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-text", - "objc2-core-video", "objc2-foundation", - "objc2-quartz-core", ] [[package]] @@ -3109,7 +3066,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ - "bitflags 2.13.1", "objc2", "objc2-foundation", ] @@ -3170,19 +3126,6 @@ dependencies = [ "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]] name = "objc2-encode" version = "4.1.0" @@ -4840,7 +4783,7 @@ dependencies = [ "bitflags 2.13.1", "block2", "core-foundation", - "core-graphics 0.25.0", + "core-graphics", "crossbeam-channel", "dbus", "dispatch2", @@ -4865,7 +4808,7 @@ dependencies = [ "tao-macros", "unicode-segmentation", "url", - "windows 0.61.3", + "windows", "windows-core 0.61.2", "windows-version", "x11-dl", @@ -4947,7 +4890,7 @@ dependencies = [ "webkit2gtk", "webview2-com", "window-vibrancy", - "windows 0.61.3", + "windows", ] [[package]] @@ -5046,21 +4989,6 @@ dependencies = [ "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]] name = "tauri-plugin-fs" version = "2.5.1" @@ -5103,7 +5031,7 @@ dependencies = [ "tauri-plugin", "thiserror 2.0.18", "url", - "windows 0.61.3", + "windows", "zbus", ] @@ -5129,7 +5057,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows 0.61.3", + "windows", ] [[package]] @@ -5154,7 +5082,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows 0.61.3", + "windows", "wry", ] @@ -5688,7 +5616,6 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-dialog", - "tauri-plugin-drag", "tauri-plugin-opener", "typst", "typst-assets", @@ -6606,10 +6533,10 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows 0.61.3", + "windows", "windows-core 0.61.2", - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", ] [[package]] @@ -6630,7 +6557,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ "thiserror 2.0.18", - "windows 0.61.3", + "windows", "windows-core 0.61.2", ] @@ -6686,18 +6613,6 @@ dependencies = [ "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]] name = "windows" version = "0.61.3" @@ -6720,36 +6635,14 @@ dependencies = [ "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]] name = "windows-core" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", @@ -6761,8 +6654,8 @@ version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", "windows-link 0.2.1", "windows-result 0.4.1", "windows-strings 0.5.1", @@ -6779,28 +6672,6 @@ dependencies = [ "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]] name = "windows-implement" version = "0.60.2" @@ -6812,28 +6683,6 @@ dependencies = [ "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]] name = "windows-interface" version = "0.59.3" @@ -6867,15 +6716,6 @@ dependencies = [ "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]] name = "windows-result" version = "0.3.4" @@ -6894,16 +6734,6 @@ dependencies = [ "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]] name = "windows-strings" version = "0.4.2" @@ -7274,7 +7104,7 @@ dependencies = [ "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows 0.61.3", + "windows", "windows-core 0.61.2", "windows-version", "x11-dl", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index aa47b77..214202e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -22,7 +22,6 @@ tauri-build = { version = "2", features = [] } tauri = { version = "2", features = [] } tauri-plugin-opener = "2" tauri-plugin-dialog = "2" -tauri-plugin-drag = "2.1.1" serde = { version = "1", features = ["derive"] } serde_json = "1" rusqlite = { version = "0.32", features = ["bundled"] } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 462baca..95aa293 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -17,7 +17,6 @@ "opener:allow-reveal-item-in-dir", "dialog:default", "dialog:allow-open", - "dialog:allow-save", - "drag:default" + "dialog:allow-save" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a1b39d2..758fe7c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -675,6 +675,12 @@ fn lsp_running(state: State<'_, LspState>) -> bool { 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] fn cloud_login( app: AppHandle, @@ -781,6 +787,30 @@ fn cloud_download_file( 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] fn cloud_list_shared( 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()) } +#[tauri::command] +fn cloud_new_document(app: AppHandle, store: State<'_, Store>, title: String) -> Result { + 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] fn cloud_delete_project(app: AppHandle, store: State<'_, Store>, cloud_project_id: String) -> Result<(), String> { let (server_url, token) = cloud_credentials(&app, &store)?; @@ -1101,7 +1143,6 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) - .plugin(tauri_plugin_drag::init()) .setup(|app| { let store = Store::open(&app.handle())?; app.manage(store); @@ -1149,6 +1190,7 @@ pub fn run() { lsp_send, lsp_stop, lsp_running, + cloud_check_compatibility, cloud_login, cloud_logout, cloud_account, @@ -1158,7 +1200,9 @@ pub fn run() { cloud_list_shared, cloud_list_files, cloud_download_file, + cloud_delete_file, cloud_download_document, + cloud_delete_document, cloud_sync_document, cloud_resolve_document, cloud_document_link, @@ -1167,6 +1211,7 @@ pub fn run() { cloud_unlink_document, cloud_create_document, cloud_create_project, + cloud_new_document, cloud_delete_project, cloud_clone_project, cloud_link_project, diff --git a/src-tauri/src/sync.rs b/src-tauri/src/sync.rs index 5308556..fe7cf5b 100644 --- a/src-tauri/src/sync.rs +++ b/src-tauri/src/sync.rs @@ -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::().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, +} + +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::().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)] pub struct Account { pub user_id: String, @@ -55,12 +137,20 @@ pub fn login( password: &str, device_name: &str, ) -> Result { + 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() .post(&endpoint(server_url, "/auth/login")) .send_json(ureq::json!({ "email": email, "password": password, "device_name": device_name, + "client_version": env!("CARGO_PKG_VERSION"), })) .map_err(describe)? .into_json::() @@ -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)] pub struct CloudFile { pub id: String, @@ -974,3 +1073,12 @@ pub fn pull_account_file( .into_json::() .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(()) +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ab7f173..2026be3 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -42,7 +42,6 @@ "icons/icon.icns", "icons/icon.ico" ], - "resources": ["icons/32x32.png"], "linux": { "deb": { "depends": [ diff --git a/src/lib/components/FileTree.svelte b/src/lib/components/FileTree.svelte index 08cf7af..c9b5f31 100644 --- a/src/lib/components/FileTree.svelte +++ b/src/lib/components/FileTree.svelte @@ -1,14 +1,12 @@ { if (!(event.target as HTMLElement).closest("[data-tree-row]")) menu = null; }} - on:dragleave={handleWindowDragLeave} /> {#snippet branch(nodes: TreeNode[], depth: number)} @@ -287,17 +246,10 @@ onkeydown={(event) => handleKey(event, node)} ondragstart={(event) => { dragging = node.path; - dragPaths = - selected.has(node.path) && selected.size > 1 - ? [...selected] - : [node.path]; - nativeDragSent = false; event.dataTransfer?.setData("text/plain", node.path); }} ondragend={() => { dragging = null; - dragPaths = []; - nativeDragSent = false; }} ondragover={(event) => { if (!dragging || !node.isDir) return; @@ -432,6 +384,7 @@ selected.has(target.path) && selected.size > 1 ? [...selected] : [target.path]} {@const single = menuPaths.length === 1}
diff --git a/src/lib/components/FileViewer.svelte b/src/lib/components/FileViewer.svelte index 760f588..5444fe4 100644 --- a/src/lib/components/FileViewer.svelte +++ b/src/lib/components/FileViewer.svelte @@ -2,6 +2,7 @@ import Icon from "@iconify/svelte"; import * as api from "$lib/ts/api"; import type { BrowseEntry, EntryKind } from "$lib/ts/api"; + import { clampMenu } from "$lib/ts/menu-position"; import { app, breadcrumbs, @@ -12,6 +13,8 @@ openCloudFolder, openTarget, refreshCloud, + refreshEntries, + setError, } from "$lib/ts/state.svelte"; interface Props { @@ -27,9 +30,12 @@ ondownloaddocument: (documentId: string, title: string) => void; onremovedownload: (path: string) => void; ondownloadfile: (fileId: string, name: string) => void; + ondeletefile: (fileId: string) => void; oncloneproject: (cloudProjectId: string, name: string) => void; ondeleteproject: (cloudProjectId: string) => void; + ondeletedocument: (documentId: string) => void; onnewcloudproject: () => void; + onnewclouddocument: () => void; onsignin: () => void; } @@ -46,15 +52,35 @@ ondownloaddocument, onremovedownload, ondownloadfile, + ondeletefile, oncloneproject, ondeleteproject, + ondeletedocument, onnewcloudproject, + onnewclouddocument, onsignin, }: Props = $props(); let menuFor = $state(null); 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(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 cloudTrail = $derived(cloudBreadcrumbs()); @@ -169,6 +195,45 @@ .map((entry) => entry.path), ); + let dragging = $state(null); + let dropTarget = $state(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) { if (entry.kind === "folder") { browseTo(entry.path); @@ -192,7 +257,68 @@ } +{#snippet cloudMenu( + id: string, + onopen: (() => void) | null, + ondownload: () => void, + onremove: (() => void) | null, + ondelete: (() => void) | null, +)} + {#if cloudMenuFor === id} +
+ {#if onopen} + + {:else} + + {/if} + {#if onremove} + + {/if} + {#if ondelete} + + {/if} +
+ {/if} +{/snippet} + {#snippet cloudCard( + id: string, icon: string, title: string, meta: string, @@ -200,10 +326,13 @@ onopen: (() => void) | null, ondownload: () => void, onremove: (() => void) | null, + ondelete: (() => void) | null, )}
openCloudContextMenu(event, id)} > + {@render cloudMenu(id, onopen, ondownload, onremove, ondelete)}
@@ -278,11 +407,11 @@ {/if} - {#if onremove} + {#if onremove || ondelete} @@ -331,7 +460,8 @@ {#if menuFor === entry.path}
{#if entry.kind === "project" || entry.kind === "document"} @@ -404,10 +534,14 @@ if (!(event.target as HTMLElement).closest("[data-card-menu]")) { menuFor = null; } + cloudMenuFor = null; }} /> -
+
event.preventDefault()} +>
@@ -459,11 +593,18 @@ {:else if app.account} + {/if}
@@ -474,8 +615,14 @@ > @@ -527,7 +680,19 @@
{#each containers as entry (entry.path)}
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)} > +
+ + {@render cloudMenu( + `folder:${folder.id}`, + () => openCloudFolder(folder.id), + () => {}, + null, + null, + )} +
{/each}
{/if} @@ -755,6 +935,7 @@ {#each app.cloudDocuments as entry (entry.id)} {@const linked = linkedDocument(entry.id)} {@render cloudCard( + `document:${entry.id}`, "ph:file-text", entry.title, linked @@ -764,6 +945,7 @@ linked ? () => openTarget(linked.path) : null, () => ondownloaddocument(entry.id, entry.title), linked ? () => onremovedownload(linked.path) : null, + () => ondeletedocument(entry.id), )} {/each}
@@ -781,7 +963,16 @@ {#each app.cloudFiles as file (file.id)}
+ openCloudContextMenu(event, `file:${file.id}`)} > + {@render cloudMenu( + `file:${file.id}`, + null, + () => ondownloadfile(file.id, file.name), + null, + () => ondeletefile(file.id), + )}

Nothing here yet.

+ {#if app.cloudFolder !== "shared"} +
+ + +
+ {/if}
{/if} @@ -831,6 +1038,7 @@ {#each app.cloudProjects as project (project.id)} {@const linked = linkedProject(project.id)} {@render cloudCard( + `project:${project.id}`, "ph:folder-star", project.name, linked @@ -839,6 +1047,7 @@ linked, linked ? () => openTarget(linked.path) : null, () => oncloneproject(project.id, project.name), + null, project.role === "owner" ? () => ondeleteproject(project.id) : null, )} {/each} diff --git a/src/lib/components/SettingsModal.svelte b/src/lib/components/SettingsModal.svelte index 7f9db44..5b30de9 100644 --- a/src/lib/components/SettingsModal.svelte +++ b/src/lib/components/SettingsModal.svelte @@ -5,7 +5,7 @@ import { openUrl } from "@tauri-apps/plugin-opener"; import Modal from "./Modal.svelte"; import * as api from "$lib/ts/api"; - import type { AppInfo } from "$lib/ts/api"; + import type { AppInfo, CompatibilityStatus } from "$lib/ts/api"; import { app, applyTheme, @@ -62,6 +62,28 @@ let workspaceRoot = $state(untrack(() => app.settings?.workspace_root ?? "")); let serverUrl = $state(untrack(() => app.settings?.server_url ?? "")); + let compatibility = $state(null); + let checkingCompatibility = $state(false); + let compatibilityTimer: ReturnType | 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 syncMinutes = $state(untrack(() => app.settings?.sync_minutes ?? 0)); let saving = $state(false); @@ -101,6 +123,16 @@ url: "https://typst.app/universe/", 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() { @@ -366,6 +398,22 @@ TypstDrive server + {#if checkingCompatibility} + + + Checking server version... + + {:else if compatibility && !compatibility.compatible} + + + {compatibility.message} + + {:else if compatibility && compatibility.compatible} + + + Compatible (server v{compatibility.server_version}) + + {/if}
diff --git a/src/lib/ts/api.ts b/src/lib/ts/api.ts index 2855a4b..d4811a8 100644 --- a/src/lib/ts/api.ts +++ b/src/lib/ts/api.ts @@ -251,6 +251,18 @@ export const updateSettings = (changes: { syncMinutes?: number; }) => invoke("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("cloud_check_compatibility", { serverUrl }); + export const cloudLogin = ( serverUrl: string, email: string, @@ -314,12 +326,29 @@ export const cloudListFiles = (folderId?: string | null) => export const cloudDownloadFile = (fileId: string) => invoke("cloud_download_file", { fileId }); +export const cloudDeleteFile = (fileId: string) => + invoke("cloud_delete_file", { fileId }); + export const cloudDownloadDocument = (documentId: string, parent: string) => invoke("cloud_download_document", { documentId, parent }); +export const cloudDeleteDocument = (documentId: string) => + invoke("cloud_delete_document", { documentId }); + export const cloudCreateDocument = (path: string, title: string) => invoke("cloud_create_document", { path, title }); +export interface DocumentContent { + id: string; + title: string; + role: string; + hash: string; + content: string; +} + +export const cloudNewDocument = (title: string) => + invoke("cloud_new_document", { title }); + export const cloudSyncDocument = (path: string) => invoke("cloud_sync_document", { path }); diff --git a/src/lib/ts/menu-position.ts b/src/lib/ts/menu-position.ts new file mode 100644 index 0000000..ce7d907 --- /dev/null +++ b/src/lib/ts/menu-position.ts @@ -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`; +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index b202134..c453298 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -38,6 +38,7 @@ openFile, openTarget, refreshAccount, + refreshCloud, refreshCloudProjects, refreshEntries, refreshTarget, @@ -62,7 +63,10 @@ | { kind: "link-entry"; entry: BrowseEntry } | { kind: "save-document-to-cloud"; entry: BrowseEntry } | { kind: "new-cloud-project" } + | { kind: "new-cloud-document" } | { 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: "new-file"; parent: string } | { kind: "new-subfolder"; parent: string } @@ -171,12 +175,30 @@ await refreshCloudProjects(); }); + const createCloudDocument = (title: string) => + guard(async () => { + await api.cloudNewDocument(title); + await refreshCloud(); + }); + const deleteCloudProject = (id: string) => guard(async () => { await api.cloudDeleteProject(id); 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) => guard(async () => { await api.cloudCloneProject(id, name); @@ -618,10 +640,13 @@ downloadDocument(documentId, title)} onremovedownload={removeDownloadedDocument} ondownloadfile={downloadCloudFile} + ondeletefile={(id) => (dialog = { kind: "delete-cloud-file", id })} onnewcloudproject={() => (dialog = { kind: "new-cloud-project" })} + onnewclouddocument={() => (dialog = { kind: "new-cloud-document" })} oncloneproject={(id, name) => (dialog = { kind: "clone-cloud-project", id, name })} ondeleteproject={(id) => (dialog = { kind: "delete-cloud-project", id })} + ondeletedocument={(id) => (dialog = { kind: "delete-cloud-document", id })} onsignin={() => (dialog = { kind: "login" })} />
@@ -644,7 +669,6 @@ files={app.target?.files ?? []} activePath={app.activePath} entrypoint={app.target?.entrypoint ?? "main.typ"} - targetPath={app.target?.path ?? ""} selected={selectedEntries} dropTarget={treeDropTarget} onopen={openFile} @@ -834,6 +858,14 @@ onsubmit={createCloudProject} onclose={close} /> +{:else if dialog.kind === "new-cloud-document"} + {:else if dialog.kind === "delete-cloud-project"} {@const target = dialog} deleteCloudProject(target.id)} onclose={close} /> +{:else if dialog.kind === "delete-cloud-document"} + {@const target = dialog} + deleteCloudDocument(target.id)} + onclose={close} + /> +{:else if dialog.kind === "delete-cloud-file"} + {@const target = dialog} + deleteCloudFile(target.id)} + onclose={close} + /> {:else if dialog.kind === "clone-cloud-project"} {@const target = dialog}