From c900c8acd60af4f05ce476e5766e029c3af350b5 Mon Sep 17 00:00:00 2001 From: DigHuang <114602213+DigHuang@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:23:07 +0800 Subject: [PATCH 1/2] feat: Integrate Tauri dialog plugin for custom portfolio image save location and rename a sidebar SVG icon. --- frontend/bun.lock | 3 + frontend/package.json | 1 + frontend/src-tauri/Cargo.lock | 185 +++++++++++++++++- frontend/src-tauri/Cargo.toml | 1 + frontend/src-tauri/capabilities/default.json | 3 +- frontend/src-tauri/src/lib.rs | 1 + .../modals/share-portfolio-modal.tsx | 15 +- 7 files changed, 204 insertions(+), 5 deletions(-) diff --git a/frontend/bun.lock b/frontend/bun.lock index fdafb3960..cf4a96d8d 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -27,6 +27,7 @@ "@tanstack/react-query": "^5.90.5", "@tauri-apps/api": "^2.9.0", "@tauri-apps/plugin-deep-link": "^2.4.5", + "@tauri-apps/plugin-dialog": "~2", "@tauri-apps/plugin-opener": "^2.5.2", "@tauri-apps/plugin-os": "^2.3.2", "@tauri-apps/plugin-process": "^2.3.1", @@ -452,6 +453,8 @@ "@tauri-apps/plugin-deep-link": ["@tauri-apps/plugin-deep-link@2.4.5", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Zf2RTj1D9IQQ45/jqW8XTKvql24HqlPjcpv0mV/O2jHQkNe11HOTZBVj6IK37qs+MWV7xZzcmazx/QVZnhAwaQ=="], + "@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.4.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-lNIn5CZuw8WZOn8zHzmFmDSzg5zfohWoa3mdULP0YFh/VogVdMVWZPcWSHlydsiJhRQYaTNSYKN7RmZKE2lCYQ=="], + "@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew=="], "@tauri-apps/plugin-os": ["@tauri-apps/plugin-os@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A=="], diff --git a/frontend/package.json b/frontend/package.json index b2e781df1..e41807500 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -40,6 +40,7 @@ "@tanstack/react-query": "^5.90.5", "@tauri-apps/api": "^2.9.0", "@tauri-apps/plugin-deep-link": "^2.4.5", + "@tauri-apps/plugin-dialog": "~2", "@tauri-apps/plugin-opener": "^2.5.2", "@tauri-apps/plugin-os": "^2.3.2", "@tauri-apps/plugin-process": "^2.3.1", diff --git a/frontend/src-tauri/Cargo.lock b/frontend/src-tauri/Cargo.lock index 569cbb940..30961d682 100644 --- a/frontend/src-tauri/Cargo.lock +++ b/frontend/src-tauri/Cargo.lock @@ -90,6 +90,27 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "ashpd" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.2", + "raw-window-handle", + "serde", + "serde_repr", + "tokio", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -872,6 +893,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.10.0", + "block2 0.6.2", + "libc", "objc2 0.6.3", ] @@ -886,6 +909,15 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + [[package]] name = "dlopen2" version = "0.8.1" @@ -918,6 +950,12 @@ dependencies = [ "const-random", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dpi" version = "0.1.2" @@ -3102,7 +3140,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", "indexmap 2.12.1", - "quick-xml", + "quick-xml 0.38.4", "serde", "time", ] @@ -3252,6 +3290,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.38.4" @@ -3579,6 +3626,31 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "ashpd", + "block2 0.6.2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + [[package]] name = "ring" version = "0.17.14" @@ -3792,6 +3864,12 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -4548,6 +4626,46 @@ dependencies = [ "windows-result 0.3.4", ] +[[package]] +name = "tauri-plugin-dialog" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "313f8138692ddc4a2127c4c9607d616a46f5c042e77b3722450866da0aad2f19" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.17", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47df422695255ecbe7bac7012440eddaeefd026656171eac9559f5243d3230d9" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.17", + "toml 0.9.8", + "url", +] + [[package]] name = "tauri-plugin-log" version = "2.7.1" @@ -4947,8 +5065,10 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", + "tracing", "windows-sys 0.61.2", ] @@ -5350,6 +5470,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-deep-link", + "tauri-plugin-dialog", "tauri-plugin-log", "tauri-plugin-opener", "tauri-plugin-os", @@ -5510,6 +5631,66 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wayland-backend" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +dependencies = [ + "bitflags 2.10.0", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +dependencies = [ + "proc-macro2", + "quick-xml 0.37.5", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.83" @@ -6247,6 +6428,7 @@ dependencies = [ "ordered-stream", "serde", "serde_repr", + "tokio", "tracing", "uds_windows", "uuid", @@ -6385,6 +6567,7 @@ dependencies = [ "endi", "enumflags2", "serde", + "url", "winnow 0.7.14", "zvariant_derive", "zvariant_utils", diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index fe15cb483..4a1d113e8 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -31,6 +31,7 @@ tauri-plugin-deep-link = "2" reqwest = { version = "0.12", features = ["blocking", "json"] } tauri-plugin-os = "2" uuid = { version = "1", features = ["v7"] } +tauri-plugin-dialog = "2" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-process = "2" diff --git a/frontend/src-tauri/capabilities/default.json b/frontend/src-tauri/capabilities/default.json index dba79229c..b2e4949cc 100644 --- a/frontend/src-tauri/capabilities/default.json +++ b/frontend/src-tauri/capabilities/default.json @@ -10,6 +10,7 @@ "opener:default", "store:default", "deep-link:default", - "core:window:allow-set-focus" + "core:window:allow-set-focus", + "dialog:default" ] } \ No newline at end of file diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index 28bf0e0a8..e4758c8ef 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -8,6 +8,7 @@ use tauri::Manager; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() + .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { let _ = app diff --git a/frontend/src/app/agent/components/strategy-items/modals/share-portfolio-modal.tsx b/frontend/src/app/agent/components/strategy-items/modals/share-portfolio-modal.tsx index 04d186ad5..cf4b943f1 100644 --- a/frontend/src/app/agent/components/strategy-items/modals/share-portfolio-modal.tsx +++ b/frontend/src/app/agent/components/strategy-items/modals/share-portfolio-modal.tsx @@ -1,3 +1,4 @@ +import { save } from "@tauri-apps/plugin-dialog"; import { snapdom } from "@zumer/snapdom"; import { Download } from "lucide-react"; import { @@ -55,12 +56,20 @@ const SharePortfolioModal: FC<{ outerShadows: true, backgroundColor: "#ffffff", }); - - await capture.download({ - filename: `valuecell-${Date.now()}`, + const img = await capture.toPng({ type: "png", }); + const path = await save({ + canCreateDirectories: true, + filters: [ + { + name: `valucell-${TimeUtils.now()}`, + extensions: ["png"], + }, + ], + }); + console.log("🚀 ~ handleDownload ~ path:", path); setOpen(false); toast.success("Image downloaded in your Downloads folder"); } catch (err) { From 5e4cc499d10a06e3ebcd3f62ac8cf5717d4a28fe Mon Sep 17 00:00:00 2001 From: DigHuang <114602213+DigHuang@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:14:45 +0800 Subject: [PATCH 2/2] feat: enable direct image saving to file system with open file action --- frontend/bun.lock | 5 ++- frontend/package.json | 3 +- frontend/src-tauri/Cargo.lock | 1 + frontend/src-tauri/Cargo.toml | 1 + frontend/src-tauri/capabilities/default.json | 12 +++++- frontend/src-tauri/src/lib.rs | 1 + .../modals/share-portfolio-modal.tsx | 38 +++++++++++++++---- 7 files changed, 50 insertions(+), 11 deletions(-) diff --git a/frontend/bun.lock b/frontend/bun.lock index cf4a96d8d..12a8d32ee 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -27,7 +27,8 @@ "@tanstack/react-query": "^5.90.5", "@tauri-apps/api": "^2.9.0", "@tauri-apps/plugin-deep-link": "^2.4.5", - "@tauri-apps/plugin-dialog": "~2", + "@tauri-apps/plugin-dialog": "^2.4.2", + "@tauri-apps/plugin-fs": "^2.4.4", "@tauri-apps/plugin-opener": "^2.5.2", "@tauri-apps/plugin-os": "^2.3.2", "@tauri-apps/plugin-process": "^2.3.1", @@ -455,6 +456,8 @@ "@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.4.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-lNIn5CZuw8WZOn8zHzmFmDSzg5zfohWoa3mdULP0YFh/VogVdMVWZPcWSHlydsiJhRQYaTNSYKN7RmZKE2lCYQ=="], + "@tauri-apps/plugin-fs": ["@tauri-apps/plugin-fs@2.4.4", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-MTorXxIRmOnOPT1jZ3w96vjSuScER38ryXY88vl5F0uiKdnvTKKTtaEjTEo8uPbl4e3gnUtfsDVwC7h77GQLvQ=="], + "@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew=="], "@tauri-apps/plugin-os": ["@tauri-apps/plugin-os@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A=="], diff --git a/frontend/package.json b/frontend/package.json index e41807500..9d6c5515d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -40,7 +40,8 @@ "@tanstack/react-query": "^5.90.5", "@tauri-apps/api": "^2.9.0", "@tauri-apps/plugin-deep-link": "^2.4.5", - "@tauri-apps/plugin-dialog": "~2", + "@tauri-apps/plugin-dialog": "^2.4.2", + "@tauri-apps/plugin-fs": "^2.4.4", "@tauri-apps/plugin-opener": "^2.5.2", "@tauri-apps/plugin-os": "^2.3.2", "@tauri-apps/plugin-process": "^2.3.1", diff --git a/frontend/src-tauri/Cargo.lock b/frontend/src-tauri/Cargo.lock index 30961d682..8b6b24bb1 100644 --- a/frontend/src-tauri/Cargo.lock +++ b/frontend/src-tauri/Cargo.lock @@ -5471,6 +5471,7 @@ dependencies = [ "tauri-build", "tauri-plugin-deep-link", "tauri-plugin-dialog", + "tauri-plugin-fs", "tauri-plugin-log", "tauri-plugin-opener", "tauri-plugin-os", diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index 4a1d113e8..20342287e 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -32,6 +32,7 @@ reqwest = { version = "0.12", features = ["blocking", "json"] } tauri-plugin-os = "2" uuid = { version = "1", features = ["v7"] } tauri-plugin-dialog = "2" +tauri-plugin-fs = "2" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-process = "2" diff --git a/frontend/src-tauri/capabilities/default.json b/frontend/src-tauri/capabilities/default.json index b2e4949cc..1213c1982 100644 --- a/frontend/src-tauri/capabilities/default.json +++ b/frontend/src-tauri/capabilities/default.json @@ -11,6 +11,16 @@ "store:default", "deep-link:default", "core:window:allow-set-focus", - "dialog:default" + "dialog:default", + "fs:default", + "fs:allow-app-write", + { + "identifier": "opener:allow-open-path", + "allow": [ + { + "path": "$HOME/**" + } + ] + } ] } \ No newline at end of file diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index e4758c8ef..98b01ef8d 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -8,6 +8,7 @@ use tauri::Manager; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() + .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { diff --git a/frontend/src/app/agent/components/strategy-items/modals/share-portfolio-modal.tsx b/frontend/src/app/agent/components/strategy-items/modals/share-portfolio-modal.tsx index cf4b943f1..3b351266e 100644 --- a/frontend/src/app/agent/components/strategy-items/modals/share-portfolio-modal.tsx +++ b/frontend/src/app/agent/components/strategy-items/modals/share-portfolio-modal.tsx @@ -1,4 +1,6 @@ +import { downloadDir, join } from "@tauri-apps/api/path"; import { save } from "@tauri-apps/plugin-dialog"; +import { openPath } from "@tauri-apps/plugin-opener"; import { snapdom } from "@zumer/snapdom"; import { Download } from "lucide-react"; import { @@ -56,24 +58,44 @@ const SharePortfolioModal: FC<{ outerShadows: true, backgroundColor: "#ffffff", }); - const img = await capture.toPng({ - type: "png", - }); + const arrayBuffer = await ( + await capture.toBlob({ + type: "png", + }) + ).arrayBuffer(); + + const filename = `valuecell-${Date.now()}.png`; + const downloadPath = await downloadDir(); + const defaultPath = await join(downloadPath, filename); const path = await save({ - canCreateDirectories: true, + defaultPath, filters: [ { - name: `valucell-${TimeUtils.now()}`, + name: "Image", extensions: ["png"], }, ], }); - console.log("🚀 ~ handleDownload ~ path:", path); + + if (!path) return; + + const { writeFile } = await import("@tauri-apps/plugin-fs"); + await writeFile(path, new Uint8Array(arrayBuffer)); + setOpen(false); - toast.success("Image downloaded in your Downloads folder"); + toast.success("Image downloaded successfully", { + action: { + label: "open file", + onClick: async () => { + return await openPath(path); + }, + }, + }); } catch (err) { - toast.error(`Failed to download image: ${JSON.stringify(err)}`); + toast.error(`Failed to download image: ${JSON.stringify(err)}`, { + duration: 6 * 1000, + }); } finally { setIsDownloading(false); }