feat(export): prefer native save dialog over browser blob fallback
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled

DictationPage + FilesPage handleExport() now use Tauri save() +
write_text_file_cmd when in the desktop runtime; browser blob path
remains as fallback when hasTauriRuntime() is false. saveMarkdown.ts
surfaces dialog errors via toasts. Adds txt to the extension map.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 08:26:19 +01:00
parent 1d71e8e361
commit 2ca01e7c9d
3 changed files with 81 additions and 18 deletions

View File

@@ -637,17 +637,38 @@
activeTemplate = ""; activeTemplate = "";
} }
function handleExport(format) { async function handleExport(format) {
if (!transcript) return; if (!transcript) return;
showExportMenu = false; showExportMenu = false;
const content = exportTranscript(transcript, segments, format); const content = exportTranscript(transcript, segments, format);
const extMap = { vtt: "vtt", srt: "srt", md: "md", csv: "csv", html: "html" }; const extMap = { txt: "txt", vtt: "vtt", srt: "srt", md: "md", csv: "csv", html: "html" };
const ext = extMap[format] || "txt"; const ext = extMap[format] || "txt";
const defaultPath = `magnotia-${new Date().toISOString().slice(0, 10)}.${ext}`;
if (hasTauriRuntime()) {
try {
const { save } = await import("@tauri-apps/plugin-dialog");
const path = await save({
title: `Export ${format.toUpperCase()} transcript`,
defaultPath,
filters: [{ name: format.toUpperCase(), extensions: [ext] }],
});
if (!path) return;
await invoke("write_text_file_cmd", { path, contents: content });
toasts.success(`Exported ${format.toUpperCase()} transcript`);
return;
} catch (err) {
console.error("dictation export failed", err);
error = `Export failed: ${typeof err === "string" ? err : err.message || err}`;
return;
}
}
const blob = new Blob([content], { type: "text/plain" }); const blob = new Blob([content], { type: "text/plain" });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement("a"); const a = document.createElement("a");
a.href = url; a.href = url;
a.download = `magnotia-${new Date().toISOString().slice(0, 10)}.${ext}`; a.download = defaultPath;
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }

View File

@@ -8,6 +8,7 @@
import Card from "$lib/components/Card.svelte"; import Card from "$lib/components/Card.svelte";
import EmptyState from "$lib/components/EmptyState.svelte"; import EmptyState from "$lib/components/EmptyState.svelte";
import { exportTranscript } from "$lib/utils/export.js"; import { exportTranscript } from "$lib/utils/export.js";
import { hasTauriRuntime } from "$lib/utils/runtime";
import { Upload } from 'lucide-svelte'; import { Upload } from 'lucide-svelte';
let fileTranscript = $state(""); let fileTranscript = $state("");
@@ -132,17 +133,37 @@
} }
} }
function handleExport(format) { async function handleExport(format) {
if (!fileTranscript) return; if (!fileTranscript) return;
showExportMenu = false; showExportMenu = false;
const content = exportTranscript(fileTranscript, segments, format); const content = exportTranscript(fileTranscript, segments, format);
const extMap = { vtt: "vtt", srt: "srt", md: "md", csv: "csv", html: "html" }; const extMap = { txt: "txt", vtt: "vtt", srt: "srt", md: "md", csv: "csv", html: "html" };
const ext = extMap[format] || "txt"; const ext = extMap[format] || "txt";
const defaultPath = `magnotia-file-${new Date().toISOString().slice(0, 10)}.${ext}`;
if (hasTauriRuntime()) {
try {
const { save } = await import("@tauri-apps/plugin-dialog");
const path = await save({
title: `Export ${format.toUpperCase()} transcript`,
defaultPath,
filters: [{ name: format.toUpperCase(), extensions: [ext] }],
});
if (!path) return;
await invoke("write_text_file_cmd", { path, contents: content });
return;
} catch (err) {
console.error("file transcript export failed", err);
error = `Export failed: ${typeof err === "string" ? err : err.message || err}`;
return;
}
}
const blob = new Blob([content], { type: "text/plain" }); const blob = new Blob([content], { type: "text/plain" });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement("a"); const a = document.createElement("a");
a.href = url; a.href = url;
a.download = `magnotia-file-${new Date().toISOString().slice(0, 10)}.${ext}`; a.download = defaultPath;
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }

View File

@@ -33,14 +33,24 @@ export function suggestedFilename(item: TranscriptEntry): string {
export async function saveTranscriptAsMarkdown( export async function saveTranscriptAsMarkdown(
item: TranscriptEntry, item: TranscriptEntry,
): Promise<string | null> { ): Promise<string | null> {
if (!hasTauriRuntime()) return null; if (!hasTauriRuntime()) {
toasts.error("Export unavailable", "Native save dialogs are only available in the desktop app.");
return null;
}
const md = buildMarkdown(item); const md = buildMarkdown(item);
const defaultPath = suggestedFilename(item); const defaultPath = suggestedFilename(item);
const path = await save({ let path: string | null = null;
try {
path = await save({
title: "Save transcript as Markdown", title: "Save transcript as Markdown",
defaultPath, defaultPath,
filters: [{ name: "Markdown", extensions: ["md"] }], filters: [{ name: "Markdown", extensions: ["md"] }],
}); });
} catch (err) {
console.error("saveTranscriptAsMarkdown dialog failed", err);
toasts.error("Couldn't open save dialog", String(err));
return null;
}
if (!path) return null; if (!path) return null;
try { try {
await invoke("write_text_file_cmd", { path, contents: md }); await invoke("write_text_file_cmd", { path, contents: md });
@@ -64,13 +74,24 @@ export async function saveTranscriptAsMarkdown(
export async function exportTranscriptsToDir( export async function exportTranscriptsToDir(
items: TranscriptEntry[], items: TranscriptEntry[],
): Promise<number> { ): Promise<number> {
if (!hasTauriRuntime() || items.length === 0) return 0; if (items.length === 0) return 0;
if (!hasTauriRuntime()) {
toasts.error("Export unavailable", "Native folder dialogs are only available in the desktop app.");
return 0;
}
const dir = await open({ let dir: string | string[] | null = null;
try {
dir = await open({
directory: true, directory: true,
multiple: false, multiple: false,
title: "Choose export folder", title: "Choose export folder",
}); });
} catch (err) {
console.error("exportTranscriptsToDir dialog failed", err);
toasts.error("Couldn't open export folder dialog", String(err));
return 0;
}
if (!dir || typeof dir !== "string") return 0; if (!dir || typeof dir !== "string") return 0;
const used = new Set<string>(); const used = new Set<string>();