import JSZip from 'jszip'; import type { Manifest, AssetStore, Item } from './types'; import { state } from './state.svelte'; export async function exportBoard(): Promise<{ success: boolean; error?: string; }> { try { const zip = new JSZip(); const assetsFolder = zip.folder('assets'); if (!assetsFolder) throw new Error('Failed to create assets folder'); const exportManifest: Manifest = { version: 1, items: state.manifest.items.map((item) => ({ ...item })), sharedCss: state.manifest.sharedCss, appCss: state.manifest.appCss, flags: state.manifest.flags ?? {}, }; for (const item of exportManifest.items) { if (item.assetId) { const asset = state.assets[item.assetId]; if (asset) { const ext = asset.filename.split('.').pop() || 'bin'; const filename = `${item.assetId}.${ext}`; assetsFolder.file(filename, asset.blob); } } } zip.file('manifest.json', JSON.stringify(exportManifest)); const blob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE', compressionOptions: { level: 9 }, }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'board.bub'; a.click(); URL.revokeObjectURL(url); return { success: true }; } catch (e) { return { success: false, error: e instanceof Error ? e.message : 'Export failed', }; } } export async function importBoard( file: File, ): Promise<{ success: boolean; error?: string }> { try { const zip = await JSZip.loadAsync(file); const manifestFile = zip.file('manifest.json'); if (!manifestFile) throw new Error('Invalid .bub file: missing manifest.json'); const manifestJson = await manifestFile.async('string'); const raw = JSON.parse(manifestJson); if (raw.version !== 1) throw new Error(`Unsupported manifest version: ${raw.version}`); const manifest: Manifest = { version: 1, items: raw.items, sharedCss: raw.sharedCss ?? '', appCss: raw.appCss ?? '', flags: raw.flags ?? {}, }; const assets: AssetStore = {}; const urlReplacements: Map = new Map(); for (const item of manifest.items) { if (item.assetId) { const assetFiles = zip .folder('assets') ?.file(new RegExp(`^${item.assetId}\\.`)); if (assetFiles && assetFiles.length > 0) { const assetFile = assetFiles[0]; const blob = await assetFile.async('blob'); const url = URL.createObjectURL(blob); const filename = assetFile.name.split('/').pop() || 'asset'; assets[item.assetId] = { blob, url, filename }; urlReplacements.set(item.assetId, url); } } } for (const item of manifest.items) { if (item.assetId && urlReplacements.has(item.assetId)) { const newUrl = urlReplacements.get(item.assetId)!; item.html = item.html.replace(/src="[^"]*"/g, `src="${newUrl}"`); } } state.load(manifest, assets); return { success: true }; } catch (e) { return { success: false, error: e instanceof Error ? e.message : 'Import failed', }; } } export async function mergeBoard( file: File, ): Promise<{ success: boolean; error?: string; itemCount?: number }> { try { const zip = await JSZip.loadAsync(file); const manifestFile = zip.file('manifest.json'); if (!manifestFile) throw new Error('Invalid .bub file: missing manifest.json'); const manifestJson = await manifestFile.async('string'); const raw = JSON.parse(manifestJson); if (raw.version !== 1) throw new Error(`Unsupported manifest version: ${raw.version}`); const incomingItems: Item[] = raw.items; if (incomingItems.length === 0) { return { success: true, itemCount: 0 }; } const assetIdMap = new Map(); const newAssets: AssetStore = {}; for (const item of incomingItems) { if (item.assetId && !assetIdMap.has(item.assetId)) { const newAssetId = crypto.randomUUID(); assetIdMap.set(item.assetId, newAssetId); const assetFiles = zip .folder('assets') ?.file(new RegExp(`^${item.assetId}\\.`)); if (assetFiles && assetFiles.length > 0) { const assetFile = assetFiles[0]; const blob = await assetFile.async('blob'); const url = URL.createObjectURL(blob); const filename = assetFile.name.split('/').pop() || 'asset'; newAssets[newAssetId] = { blob, url, filename }; } } } const minZIndex = Math.min(...incomingItems.map((i) => i.zIndex)); const zIndexOffset = state.maxZIndex + 1 - minZIndex; const newItems: Item[] = incomingItems.map((item) => { const newId = crypto.randomUUID(); const newAssetId = item.assetId ? assetIdMap.get(item.assetId) : undefined; let html = item.html; if (newAssetId && newAssets[newAssetId]) { html = html.replace(/src="[^"]*"/g, `src="${newAssets[newAssetId].url}"`); } return { ...item, id: newId, assetId: newAssetId, zIndex: item.zIndex + zIndexOffset, html, }; }); state.addAssets(newAssets); state.addItems(newItems); return { success: true, itemCount: newItems.length }; } catch (e) { return { success: false, error: e instanceof Error ? e.message : 'Merge failed', }; } }