Tauri native dialog for Save/Open with fs plugin

This commit is contained in:
Danila Poyarkov 2026-02-28 13:54:21 +03:00
parent 086fb7d652
commit 8d3d3444ad
8 changed files with 134 additions and 10 deletions

View file

@ -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=="],

68
desktop/Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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"
]
}

View file

@ -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" {

View file

@ -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",

View file

@ -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({

View file

@ -71,6 +71,7 @@ export function createEditorStore() {
const undo = new UndoManager()
const pageViewports = new Map<string, PageViewport>()
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