feat(export): Obsidian-compatible Markdown vault as streamed ZIP

- GET /export/markdown streams the workspace as ZIP (no temp files):
  notes as .md with YAML front matter in the user's folder structure
  (shared notes and mounted folders included, empty folders kept)
- Clean Markdown: top-level headings/dividers/callouts/code as real blocks
  (their children move up a level), lists with tabs, todos as - [ ]
- Aufgaben/<list>.md in Obsidian Tasks format (due date, priority,
  subtasks, notes), Listen/<list>.md for shopping and other lists
- Safe, unique file names; download button in settings (mobile-safe)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-28 23:21:20 +01:00
1 parent d6c70d7a8e
commit e509f685c1
7 files changed
+531 -6

No files matched your search

@@ -362,17 +362,38 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
setTimeout(() => setCopiedLabel(null), 2000);
};
// Works on mobile browsers too: the link must be in the document and the URL alive until the click ran
const saveBlob = (blob: Blob, filename: string) => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
window.setTimeout(() => URL.revokeObjectURL(url), 10000);
};
const handleExportData = async () => {
const data = await api.exportAll();
const blob = new Blob([JSON.stringify(data, null, 2)], {
type: 'application/json;charset=utf-8',
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `mindsift-backup-${new Date().toISOString().slice(0, 10)}.json`;
a.click();
URL.revokeObjectURL(url);
saveBlob(blob, `mindsift-backup-${new Date().toISOString().slice(0, 10)}.json`);
};
const [exportingVault, setExportingVault] = useState(false);
const [vaultError, setVaultError] = useState<string | null>(null);
const handleExportVault = async () => {
setExportingVault(true);
setVaultError(null);
try {
saveBlob(await api.exportMarkdownVault(), `mindsift-vault-${new Date().toISOString().slice(0, 10)}.zip`);
} catch (err: any) {
setVaultError(err.message || 'Export fehlgeschlagen');
} finally {
setExportingVault(false);
}
};
const baseUrl = window.location.origin;
@@ -1230,6 +1251,28 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
<span>Backup herunterladen</span>
</button>
</div>
{/* Obsidian-compatible Markdown vault */}
<div className="pt-4 border-t border-border flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
<h3 className="font-bold text-main">Markdown-Archiv für Obsidian (.zip)</h3>
<p className="text-muted text-[11px]">
Alle Notizen als <code className="font-mono">.md</code>-Dateien in deiner Ordnerstruktur (mit Titel, Tags und
Datum im Kopf), Aufgaben je Liste unter <code className="font-mono">Aufgaben/</code> im Format des
Obsidian-Tasks-Plugins, Listen unter <code className="font-mono">Listen/</code>. Mit dir geteilte Notizen sind
dabei.
</p>
{vaultError && <p className="text-[11px] text-status-err mt-1">{vaultError}</p>}
</div>
<button
onClick={handleExportVault}
disabled={exportingVault}
className="btn-action h-11 sm:h-9 px-4 text-xs flex items-center justify-center gap-2 flex-shrink-0 disabled:opacity-50"
>
<Download className="w-3.5 h-3.5 text-accent" />
<span>{exportingVault ? 'Wird erstellt…' : 'Markdown-Archiv herunterladen'}</span>
</button>
</div>
</div>
)}
</div>
+11
View File
@@ -1074,6 +1074,17 @@ export const api = {
async exportAll(): Promise<any> {
return request<any>('/api/v1/export');
},
// Obsidian-compatible ZIP of the workspace; binary and possibly large, so no JSON helper and no timeout
async exportMarkdownVault(): Promise<Blob> {
const token = getToken();
const res = await fetch('/api/v1/export/markdown', {
headers: token ? { Authorization: `Bearer ${token}` } : {},
cache: 'no-store',
});
if (!res.ok) throw new Error(`Export fehlgeschlagen (HTTP ${res.status})`);
return res.blob();
},
};
export function getOfflineQueue(): Thought[] {