189 lines
5.1 KiB
TypeScript
189 lines
5.1 KiB
TypeScript
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<string, string> = 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<string, string>();
|
|
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',
|
|
};
|
|
}
|
|
}
|