feat(ui): add export section to SettingsPage with directory picker and format selection

Export Your Data section with three buttons: Export Transcriptions (MD/TXT),
Export Tasks (JSON/CSV), and Export to Obsidian. Each opens a directory picker
and shows status on completion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jake
2026-03-21 15:20:42 +00:00
parent eace0e88c3
commit 0685428c90

View File

@@ -10,7 +10,7 @@
import ZonePicker from "$lib/components/ZonePicker.svelte";
import AccessibilityControls from "$lib/components/AccessibilityControls.svelte";
import { getPreferences, updatePreferences } from "$lib/stores/preferences.svelte.js";
import { Check, ChevronRight } from "lucide-svelte";
import { Check, ChevronRight, Download } from "lucide-svelte";
import { open } from "@tauri-apps/plugin-dialog";
const prefs = getPreferences();
@@ -173,6 +173,37 @@
}
}
// --- Export state ---
let exportTranscriptFormat = $state("md");
let exportTaskFormat = $state("json");
let exportStatus = $state("");
let exporting = $state("");
async function doExport(type) {
try {
const folder = await open({ directory: true, title: "Select export folder" });
if (!folder) return;
exporting = type;
exportStatus = "";
if (type === "transcriptions") {
const count = await invoke("export_transcriptions", { directory: folder, format: exportTranscriptFormat });
exportStatus = count === 0 ? "No transcriptions yet" : `Exported ${count} transcription${count === 1 ? "" : "s"} to ${folder}`;
} else if (type === "tasks") {
const path = await invoke("export_tasks", { directory: folder, format: exportTaskFormat });
exportStatus = `Tasks exported to ${path}`;
} else if (type === "obsidian") {
const count = await invoke("export_to_obsidian", { directory: folder });
exportStatus = count === 0 ? "Nothing to export yet" : `Exported ${count} file${count === 1 ? "" : "s"} to ${folder}`;
}
} catch (err) {
exportStatus = typeof err === "string" ? err : "Export failed";
} finally {
exporting = "";
}
}
// --- Profile management ---
function createProfile() {
if (!newProfileName.trim()) return;
@@ -621,6 +652,86 @@
{/if}
</div>
<!-- Export -->
<div class="border-b border-border-subtle">
<button
class="flex items-center justify-between w-full py-4 px-5 text-left"
onclick={() => openSection = openSection === 'export' ? null : 'export'}
>
<h3 class="font-display text-[18px] italic text-text">Export Your Data</h3>
<span class="text-text-tertiary text-[16px] leading-none">{openSection === 'export' ? '' : '+'}</span>
</button>
{#if openSection === 'export'}
<div class="px-5 pb-5 animate-fade-in">
<p class="text-[11px] text-text-tertiary mb-4">All your data stays yours. Export everything as plain text files.</p>
<div class="space-y-3">
<!-- Export Transcriptions -->
<div class="flex items-center gap-3">
<button
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[12px] font-medium
bg-accent/10 text-accent hover:bg-accent/20 disabled:opacity-50"
disabled={exporting === "transcriptions"}
onclick={() => doExport("transcriptions")}
>
<Download class="w-3.5 h-3.5" />
{exporting === "transcriptions" ? "Exporting..." : "Export Transcriptions"}
</button>
<select
class="bg-bg-input border border-border-subtle rounded-lg px-2 py-1 text-[11px] text-text-secondary
focus:outline-none focus:border-accent appearance-none cursor-pointer"
bind:value={exportTranscriptFormat}
>
<option value="md">Markdown</option>
<option value="txt">Plain Text</option>
</select>
</div>
<!-- Export Tasks -->
<div class="flex items-center gap-3">
<button
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[12px] font-medium
bg-accent/10 text-accent hover:bg-accent/20 disabled:opacity-50"
disabled={exporting === "tasks"}
onclick={() => doExport("tasks")}
>
<Download class="w-3.5 h-3.5" />
{exporting === "tasks" ? "Exporting..." : "Export Tasks"}
</button>
<select
class="bg-bg-input border border-border-subtle rounded-lg px-2 py-1 text-[11px] text-text-secondary
focus:outline-none focus:border-accent appearance-none cursor-pointer"
bind:value={exportTaskFormat}
>
<option value="json">JSON</option>
<option value="csv">CSV</option>
</select>
</div>
<!-- Export to Obsidian -->
<div class="flex items-center gap-3">
<button
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[12px] font-medium
bg-accent/10 text-accent hover:bg-accent/20 disabled:opacity-50"
disabled={exporting === "obsidian"}
onclick={() => doExport("obsidian")}
>
<Download class="w-3.5 h-3.5" />
{exporting === "obsidian" ? "Exporting..." : "Export to Obsidian"}
</button>
<span class="text-[10px] text-text-tertiary">Markdown + wikilinks, both transcriptions and tasks</span>
</div>
</div>
{#if exportStatus}
<p class="text-[11px] text-text-secondary mt-3 bg-bg-input rounded-lg px-3 py-2 border border-border-subtle">
{exportStatus}
</p>
{/if}
</div>
{/if}
</div>
<!-- Hotkey -->
<div class="border-b border-border-subtle">
<button