Skip to content

Commit 7b4f87c

Browse files
committed
fix save issue
1 parent 7142418 commit 7b4f87c

8 files changed

Lines changed: 168 additions & 7 deletions

File tree

frontend/package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"dependencies": {
1212
"@monaco-editor/react": "^4.6.0",
1313
"@tauri-apps/api": "^2.10.1",
14+
"@tauri-apps/plugin-dialog": "^2.4.2",
1415
"monaco-editor": "^0.48.0",
1516
"next": "^14.2.0",
1617
"react": "^18.3.0",

frontend/src-tauri/Cargo.lock

Lines changed: 68 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/src-tauri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ log = "0.4"
2424
tauri = { version = "2.10.3", features = [] }
2525
tauri-plugin-log = "2"
2626
tauri-plugin-shell = "2"
27+
tauri-plugin-dialog = "2"

frontend/src-tauri/capabilities/default.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
],
88
"permissions": [
99
"core:default",
10-
"shell:default"
10+
"shell:default",
11+
"dialog:default"
1112
]
1213
}

frontend/src-tauri/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub fn run() {
1818
let port_state = Arc::new(Mutex::new(None::<u16>));
1919

2020
tauri::Builder::default()
21+
.plugin(tauri_plugin_dialog::init())
2122
.plugin(tauri_plugin_shell::init())
2223
.manage(BackendPort(port_state))
2324
.invoke_handler(tauri::generate_handler![get_backend_port])

frontend/src/app/page.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ export default function HomePage() {
2323
const [showAIPanel, setShowAIPanel] = useState(false);
2424
const monacoRef = useRef<MonacoEditor.IStandaloneCodeEditor | null>(null);
2525

26+
const handleSaveFile = useCallback(async () => {
27+
try {
28+
await editor.saveFile();
29+
} catch (err) {
30+
alert(`Save failed: ${err instanceof Error ? err.message : String(err)}`);
31+
}
32+
}, [editor]);
33+
2634
// Load recent files on mount and initialise preview
2735
useEffect(() => {
2836
editor.loadRecentFiles();
@@ -37,12 +45,12 @@ export default function HomePage() {
3745
const onKeyDown = (e: KeyboardEvent) => {
3846
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
3947
e.preventDefault();
40-
editor.saveFile();
48+
void handleSaveFile();
4149
}
4250
};
4351
window.addEventListener("keydown", onKeyDown);
4452
return () => window.removeEventListener("keydown", onKeyDown);
45-
}, [editor]);
53+
}, [handleSaveFile]);
4654

4755
// "Open file" — uses a hidden file-input because Tauri/browser can't call
4856
// the native dialog directly without the Tauri API. In Tauri mode this
@@ -119,7 +127,7 @@ export default function HomePage() {
119127
<div className="relative">
120128
<Toolbar
121129
onOpenFile={handleOpenFile}
122-
onSaveFile={() => editor.saveFile()}
130+
onSaveFile={() => { void handleSaveFile(); }}
123131
onExport={handleExport}
124132
onToggleDark={() => editor.setDarkMode((d) => !d)}
125133
onToggleAIPanel={() => setShowAIPanel((v) => !v)}

frontend/src/hooks/useEditor.ts

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,80 @@ export function useEditor() {
117117
const saveFile = useCallback(
118118
async (filePath?: string) => {
119119
const path = filePath ?? activeTab.filePath;
120-
if (!path) return;
121-
await Files.write(path, activeTab.content);
122-
updateTab(activeTabId, { dirty: false, filePath: path, label: path.split(/[/\\]/).pop() ?? path });
120+
if (path) {
121+
await Files.write(path, activeTab.content);
122+
updateTab(activeTabId, { dirty: false, filePath: path, label: path.split(/[/\\]/).pop() ?? path });
123+
return;
124+
}
125+
126+
// Save As flow: ask user for a destination when the tab has no path yet.
127+
const rawLabel = activeTab.label.trim() || "Untitled";
128+
const suggestedName = /\.[A-Za-z0-9]+$/.test(rawLabel) ? rawLabel : `${rawLabel}.md`;
129+
130+
// In Tauri desktop mode, use native save dialog and persist via backend API.
131+
const isTauri = typeof window !== "undefined" && "__TAURI__" in window;
132+
if (isTauri) {
133+
try {
134+
const { save } = await import("@tauri-apps/plugin-dialog");
135+
const target = await save({
136+
defaultPath: suggestedName,
137+
filters: [{ name: "Markdown", extensions: ["md", "markdown", "txt"] }],
138+
});
139+
140+
if (!target) return;
141+
142+
const resolvedPath = Array.isArray(target) ? target[0] : target;
143+
await Files.write(resolvedPath, activeTab.content);
144+
updateTab(activeTabId, {
145+
dirty: false,
146+
filePath: resolvedPath,
147+
label: resolvedPath.split(/[/\\]/).pop() ?? resolvedPath,
148+
});
149+
Files.addRecent(resolvedPath)
150+
.then(({ entries }) => setRecentFiles(entries))
151+
.catch(console.error);
152+
} catch {
153+
// No dialog capability in runtime: silently no-op.
154+
}
155+
return;
156+
}
157+
158+
// In browser mode, use the native file save picker when available.
159+
const maybePicker = (window as Window & {
160+
showSaveFilePicker?: (options?: {
161+
suggestedName?: string;
162+
types?: Array<{ description?: string; accept: Record<string, string[]> }>;
163+
}) => Promise<{
164+
createWritable: () => Promise<{
165+
write: (data: string) => Promise<void>;
166+
close: () => Promise<void>;
167+
}>;
168+
name?: string;
169+
}>;
170+
}).showSaveFilePicker;
171+
172+
if (!maybePicker) return;
173+
174+
try {
175+
const handle = await maybePicker({
176+
suggestedName,
177+
types: [
178+
{
179+
description: "Markdown files",
180+
accept: {
181+
"text/markdown": [".md", ".markdown"],
182+
"text/plain": [".txt"],
183+
},
184+
},
185+
],
186+
});
187+
const writable = await handle.createWritable();
188+
await writable.write(activeTab.content);
189+
await writable.close();
190+
updateTab(activeTabId, { dirty: false, label: handle.name || suggestedName });
191+
} catch {
192+
// No picker capability (or picker cancelled): silently no-op.
193+
}
123194
},
124195
[activeTab, activeTabId, updateTab]
125196
);

0 commit comments

Comments
 (0)