diff --git a/bun.lock b/bun.lock index 86ae8790a..8218e2019 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,8 @@ "dependencies": { "@tailwindcss/vite": "^4.2.1", "@tauri-apps/api": "^2", + "@tauri-apps/plugin-dialog": "^2.6.0", + "@tauri-apps/plugin-fs": "^2.4.5", "@tauri-apps/plugin-opener": "^2", "@vueuse/core": "^14.2.1", "canvaskit-wasm": "^0.40.0", @@ -318,6 +320,10 @@ "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.10.0", "", { "os": "win32", "cpu": "x64" }, "sha512-NTpyQxkpzGmU6ceWBTY2xRIEaS0ZLbVx1HE1zTA3TY/pV3+cPoPPOs+7YScr4IMzXMtOw7tLw5LEXo5oIG3qaQ=="], + "@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.6.0", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg=="], + + "@tauri-apps/plugin-fs": ["@tauri-apps/plugin-fs@2.4.5", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-dVxWWGE6VrOxC7/jlhyE+ON/Cc2REJlM35R3PJX3UvFw2XwYhLGQVAIyrehenDdKjotipjYEVc4YjOl3qq90fA=="], + "@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ=="], "@types/culori": ["@types/culori@4.0.1", "", {}, "sha512-43M51r/22CjhbOXyGT361GZ9vncSVQ39u62x5eJdBQFviI8zWp2X5jzqg7k4M6PVgDQAClpy2bUe2dtwEgEDVQ=="], diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index 5745a3d98..e3271210d 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -698,6 +698,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ "bitflags 2.11.0", + "block2", + "libc", "objc2", ] @@ -2329,6 +2331,8 @@ dependencies = [ "serde_json", "tauri", "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-fs", "tauri-plugin-opener", ] @@ -2939,6 +2943,30 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -3625,6 +3653,46 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-dialog" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9204b425d9be8d12aa60c2a83a289cf7d1caae40f57f336ed1155b3a5c0e359b" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", + "url", +] + [[package]] name = "tauri-plugin-opener" version = "2.5.3" diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index ec866a6eb..b0a21d2cb 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -22,4 +22,6 @@ tauri = { version = "2", features = ["devtools"] } tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" +tauri-plugin-dialog = "2.6.0" +tauri-plugin-fs = "2.4.5" diff --git a/desktop/capabilities/default.json b/desktop/capabilities/default.json index 4cdbf49a7..1d81037b4 100644 --- a/desktop/capabilities/default.json +++ b/desktop/capabilities/default.json @@ -5,6 +5,12 @@ "windows": ["main"], "permissions": [ "core:default", - "opener:default" + "opener:default", + "dialog:default", + "dialog:allow-save", + "dialog:allow-open", + "fs:default", + "fs:allow-read", + "fs:allow-write" ] } diff --git a/desktop/src/lib.rs b/desktop/src/lib.rs index 3b145e525..066dc360a 100644 --- a/desktop/src/lib.rs +++ b/desktop/src/lib.rs @@ -7,6 +7,8 @@ use tauri::{ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_fs::init()) .on_menu_event(|app, event| { #[cfg(debug_assertions)] if event.id().0.as_str() == "dev-tools" { diff --git a/package.json b/package.json index 7223508a9..e63f9c49e 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "dependencies": { "@tailwindcss/vite": "^4.2.1", "@tauri-apps/api": "^2", + "@tauri-apps/plugin-dialog": "^2.6.0", + "@tauri-apps/plugin-fs": "^2.4.5", "@tauri-apps/plugin-opener": "^2", "@vueuse/core": "^14.2.1", "canvaskit-wasm": "^0.40.0", diff --git a/src/composables/use-menu.ts b/src/composables/use-menu.ts index 7c21412b8..559383bf2 100644 --- a/src/composables/use-menu.ts +++ b/src/composables/use-menu.ts @@ -3,6 +3,20 @@ import { onUnmounted } from 'vue' import type { EditorStore } from '../stores/editor' export async function openFileDialog(store: EditorStore) { + if ('__TAURI_INTERNALS__' in window) { + const { open } = await import('@tauri-apps/plugin-dialog') + const { readFile } = await import('@tauri-apps/plugin-fs') + const path = await open({ + filters: [{ name: 'Figma file', extensions: ['fig'] }], + multiple: false + }) + if (!path) return + const bytes = await readFile(path as string) + const file = new File([bytes], (path as string).split('/').pop()!) + await store.openFigFile(file, undefined, path as string) + return + } + if ('showOpenFilePicker' in window) { try { const [handle] = await (window as any).showOpenFilePicker({ diff --git a/src/stores/editor.ts b/src/stores/editor.ts index cdd676f19..91267d167 100644 --- a/src/stores/editor.ts +++ b/src/stores/editor.ts @@ -71,6 +71,7 @@ export function createEditorStore() { const undo = new UndoManager() const pageViewports = new Map() let fileHandle: FileSystemFileHandle | null = null + let filePath: string | null = null prefetchFigmaSchema() @@ -422,7 +423,7 @@ export function createEditorStore() { requestRender() } - async function openFigFile(file: File, handle?: FileSystemFileHandle) { + async function openFigFile(file: File, handle?: FileSystemFileHandle, path?: string) { try { const imported = await readFigFile(file) graph = imported @@ -430,6 +431,7 @@ export function createEditorStore() { undo.clear() pageViewports.clear() fileHandle = handle ?? null + filePath = path ?? null state.selectedIds = new Set() const firstPage = graph.getPages()[0] state.currentPageId = firstPage?.id ?? graph.rootId @@ -444,11 +446,8 @@ export function createEditorStore() { } async function saveFigFile() { - if (fileHandle) { - const data = await exportFigFile(graph) - const writable = await fileHandle.createWritable() - await writable.write(data) - await writable.close() + if (filePath || fileHandle) { + await writeFile(await exportFigFile(graph)) } else { await saveFigFileAs() } @@ -457,6 +456,19 @@ export function createEditorStore() { async function saveFigFileAs() { const data = await exportFigFile(graph) + if ('__TAURI_INTERNALS__' in window) { + const { save } = await import('@tauri-apps/plugin-dialog') + const path = await save({ + defaultPath: 'Untitled.fig', + filters: [{ name: 'Figma file', extensions: ['fig'] }] + }) + if (!path) return + filePath = path + fileHandle = null + await writeFile(data) + return + } + if ('showSaveFilePicker' in window) { try { const handle = await (window as any).showSaveFilePicker({ @@ -467,9 +479,8 @@ export function createEditorStore() { }] }) fileHandle = handle - const writable = await handle.createWritable() - await writable.write(data) - await writable.close() + filePath = null + await writeFile(data) return } catch (e) { if ((e as Error).name === 'AbortError') return @@ -485,6 +496,19 @@ export function createEditorStore() { URL.revokeObjectURL(url) } + async function writeFile(data: Uint8Array) { + if (filePath && '__TAURI_INTERNALS__' in window) { + const { writeFile: tauriWrite } = await import('@tauri-apps/plugin-fs') + await tauriWrite(filePath, data) + return + } + if (fileHandle) { + const writable = await fileHandle.createWritable() + await writable.write(data) + await writable.close() + } + } + function runLayoutForNode(id: string) { const node = graph.getNode(id) if (!node) return