feat(export): prefer native save dialog over browser blob fallback
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:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
title: "Save transcript as Markdown",
|
try {
|
||||||
defaultPath,
|
path = await save({
|
||||||
filters: [{ name: "Markdown", extensions: ["md"] }],
|
title: "Save transcript as Markdown",
|
||||||
});
|
defaultPath,
|
||||||
|
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;
|
||||||
directory: true,
|
try {
|
||||||
multiple: false,
|
dir = await open({
|
||||||
title: "Choose export folder",
|
directory: true,
|
||||||
});
|
multiple: false,
|
||||||
|
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>();
|
||||||
|
|||||||
Reference in New Issue
Block a user