Skip to content

Commit d77ff8f

Browse files
committed
fix save issue advanced
1 parent 7b4f87c commit d77ff8f

3 files changed

Lines changed: 97 additions & 73 deletions

File tree

frontend/src/app/page.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,28 @@ export default function HomePage() {
5656
// the native dialog directly without the Tauri API. In Tauri mode this
5757
// would be replaced by window.__TAURI__.dialog.open().
5858
const fileInputRef = useRef<HTMLInputElement>(null);
59+
const isLikelyTauriRuntime =
60+
typeof window !== "undefined" &&
61+
("__TAURI_INTERNALS__" in window || "__TAURI__" in window || window.navigator.userAgent.includes("Tauri"));
5962

60-
const handleOpenFile = () => {
61-
fileInputRef.current?.click();
62-
};
63+
const handleOpenFile = useCallback(async () => {
64+
try {
65+
const { open } = await import("@tauri-apps/plugin-dialog");
66+
const selected = await open({
67+
multiple: false,
68+
filters: [{ name: "Markdown", extensions: ["md", "markdown", "txt"] }],
69+
});
70+
if (!selected) return;
71+
const filePath = Array.isArray(selected) ? selected[0] : selected;
72+
await editor.openFile(filePath);
73+
} catch {
74+
// In desktop runtime, avoid browser picker fallback to prevent permission dialogs.
75+
if (isLikelyTauriRuntime) return;
76+
77+
// Browser mode fallback.
78+
fileInputRef.current?.click();
79+
}
80+
}, [editor, isLikelyTauriRuntime]);
6381

6482
const handleFileInputChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
6583
const file = e.target.files?.[0];
@@ -126,7 +144,7 @@ export default function HomePage() {
126144
{/* Toolbar */}
127145
<div className="relative">
128146
<Toolbar
129-
onOpenFile={handleOpenFile}
147+
onOpenFile={() => { void handleOpenFile(); }}
130148
onSaveFile={() => { void handleSaveFile(); }}
131149
onExport={handleExport}
132150
onToggleDark={() => editor.setDarkMode((d) => !d)}

frontend/src/hooks/useEditor.ts

Lines changed: 73 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,19 @@ export type Tab = {
1515
id: string;
1616
label: string; // Display name (filename)
1717
filePath: string | null;
18+
browserHandle?: FileSystemFileHandle | null;
1819
content: string;
1920
dirty: boolean;
2021
};
2122

22-
function makeTab(id: string, label = "Untitled", content = "", filePath: string | null = null): Tab {
23-
return { id, label, content, filePath, dirty: false };
23+
function makeTab(
24+
id: string,
25+
label = "Untitled",
26+
content = "",
27+
filePath: string | null = null,
28+
browserHandle: FileSystemFileHandle | null = null
29+
): Tab {
30+
return { id, label, content, filePath, browserHandle, dirty: false };
2431
}
2532

2633
let _tabCounter = 0;
@@ -67,10 +74,11 @@ export function useEditor() {
6774
const handleContentChange = useCallback(
6875
(value: string | undefined) => {
6976
const v = value ?? "";
77+
if (v === activeTab.content) return;
7078
updateTab(activeTabId, { content: v, dirty: true });
7179
refreshPreview(v);
7280
},
73-
[activeTabId, updateTab, refreshPreview]
81+
[activeTab, activeTabId, updateTab, refreshPreview]
7482
);
7583

7684
// ── file ops ───────────────────────────────────────────────────────────────
@@ -87,7 +95,7 @@ export function useEditor() {
8795
const { content } = await Files.read(filePath);
8896
const label = filePath.split(/[/\\]/).pop() ?? filePath;
8997
const id = nextTabId();
90-
const newTab = makeTab(id, label, content, filePath);
98+
const newTab = makeTab(id, label, content, filePath, null);
9199
setTabs((prev) => [...prev, newTab]);
92100
setActiveTabId(id);
93101
refreshPreview(content, filePath);
@@ -103,10 +111,15 @@ export function useEditor() {
103111
);
104112

105113
const openTextAsTab = useCallback(
106-
(label: string, content: string, filePath: string | null = null) => {
114+
(
115+
label: string,
116+
content: string,
117+
filePath: string | null = null,
118+
browserHandle: FileSystemFileHandle | null = null
119+
) => {
107120
const id = nextTabId();
108121
const tabLabel = label.trim() || "Untitled";
109-
const newTab = makeTab(id, tabLabel, content, filePath);
122+
const newTab = makeTab(id, tabLabel, content, filePath, browserHandle);
110123
setTabs((prev) => [...prev, newTab]);
111124
setActiveTabId(id);
112125
refreshPreview(content, filePath ?? undefined);
@@ -116,83 +129,75 @@ export function useEditor() {
116129

117130
const saveFile = useCallback(
118131
async (filePath?: string) => {
119-
const path = filePath ?? activeTab.filePath;
132+
let path = filePath ?? activeTab.filePath;
133+
134+
// For tabs opened without absolute path, try to resolve an existing path
135+
// without opening any dialog.
136+
if (!path) {
137+
const byName = recentFiles.filter((p) => (p.split(/[/\\]/).pop() ?? "") === activeTab.label);
138+
if (byName.length === 1) {
139+
path = byName[0];
140+
} else {
141+
const candidates = [activeTab.label, `./${activeTab.label}`];
142+
for (const candidate of candidates) {
143+
try {
144+
await Files.read(candidate);
145+
path = candidate;
146+
break;
147+
} catch {
148+
// Keep trying other candidates.
149+
}
150+
}
151+
}
152+
}
153+
120154
if (path) {
121155
await Files.write(path, activeTab.content);
122-
updateTab(activeTabId, { dirty: false, filePath: path, label: path.split(/[/\\]/).pop() ?? path });
156+
updateTab(activeTabId, {
157+
dirty: false,
158+
filePath: path,
159+
label: path.split(/[/\\]/).pop() ?? path,
160+
});
161+
Files.addRecent(path)
162+
.then(({ entries }) => setRecentFiles(entries))
163+
.catch(console.error);
123164
return;
124165
}
125166

126167
// Save As flow: ask user for a destination when the tab has no path yet.
127168
const rawLabel = activeTab.label.trim() || "Untitled";
128169
const suggestedName = /\.[A-Za-z0-9]+$/.test(rawLabel) ? rawLabel : `${rawLabel}.md`;
129170

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+
// Try Tauri native save dialog first (desktop mode).
172+
try {
173+
const { save } = await import("@tauri-apps/plugin-dialog");
174+
const target = await save({
175+
defaultPath: suggestedName,
176+
filters: [{ name: "Markdown", extensions: ["md", "markdown", "txt"] }],
177+
});
171178

172-
if (!maybePicker) return;
179+
if (!target) return;
173180

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-
],
181+
const resolvedPath = Array.isArray(target) ? target[0] : target;
182+
await Files.write(resolvedPath, activeTab.content);
183+
updateTab(activeTabId, {
184+
dirty: false,
185+
filePath: resolvedPath,
186+
browserHandle: null,
187+
label: resolvedPath.split(/[/\\]/).pop() ?? resolvedPath,
186188
});
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 });
189+
Files.addRecent(resolvedPath)
190+
.then(({ entries }) => setRecentFiles(entries))
191+
.catch(console.error);
192+
return;
191193
} catch {
192-
// No picker capability (or picker cancelled): silently no-op.
194+
// Not running with Tauri dialog capability.
193195
}
196+
197+
// Outside Tauri dialog capability, do nothing silently.
198+
return;
194199
},
195-
[activeTab, activeTabId, updateTab]
200+
[activeTab, activeTabId, recentFiles, updateTab]
196201
);
197202

198203
const newTab = useCallback(() => {

frontend/src/lib/api.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
// Detect Tauri environment without importing the full @tauri-apps/api eagerly
1717
// (the module is only available inside Tauri's webview).
1818
const isTauri =
19-
typeof window !== "undefined" && "__TAURI__" in window;
19+
typeof window !== "undefined" &&
20+
("__TAURI__" in window || "__TAURI_INTERNALS__" in window);
2021

2122
let _resolvedBaseUrl: string | null = null;
2223

0 commit comments

Comments
 (0)