Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa73708d97 |
@@ -0,0 +1,7 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Library" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY DEFAULT 'default',
|
||||||
|
"items" TEXT NOT NULL DEFAULT '[]',
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
Binary file not shown.
Binary file not shown.
@@ -33,3 +33,10 @@ model Drawing {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Library {
|
||||||
|
id String @id @default("default") // Singleton pattern - use "default" ID
|
||||||
|
items String @default("[]") // Stored as JSON string array of library items
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -139,6 +139,13 @@ exports.Prisma.DrawingScalarFieldEnum = {
|
|||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.Prisma.LibraryScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
items: 'items',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
};
|
||||||
|
|
||||||
exports.Prisma.SortOrder = {
|
exports.Prisma.SortOrder = {
|
||||||
asc: 'asc',
|
asc: 'asc',
|
||||||
desc: 'desc'
|
desc: 'desc'
|
||||||
@@ -152,7 +159,8 @@ exports.Prisma.NullsOrder = {
|
|||||||
|
|
||||||
exports.Prisma.ModelName = {
|
exports.Prisma.ModelName = {
|
||||||
Collection: 'Collection',
|
Collection: 'Collection',
|
||||||
Drawing: 'Drawing'
|
Drawing: 'Drawing',
|
||||||
|
Library: 'Library'
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+1083
-2
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "prisma-client-6afe3d9baa793154c8d01c79f8418d91423e5ccaec794547bf848a451459cf53",
|
"name": "prisma-client-8eed3ee5004eaec649fc60571177778f25acb4a3cdc2c238bbb8e70dd820d0ff",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"types": "index.d.ts",
|
"types": "index.d.ts",
|
||||||
"browser": "index-browser.js",
|
"browser": "index-browser.js",
|
||||||
|
|||||||
@@ -33,3 +33,10 @@ model Drawing {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Library {
|
||||||
|
id String @id @default("default") // Singleton pattern - use "default" ID
|
||||||
|
items String @default("[]") // Stored as JSON string array of library items
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|||||||
@@ -139,6 +139,13 @@ exports.Prisma.DrawingScalarFieldEnum = {
|
|||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.Prisma.LibraryScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
items: 'items',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
};
|
||||||
|
|
||||||
exports.Prisma.SortOrder = {
|
exports.Prisma.SortOrder = {
|
||||||
asc: 'asc',
|
asc: 'asc',
|
||||||
desc: 'desc'
|
desc: 'desc'
|
||||||
@@ -152,7 +159,8 @@ exports.Prisma.NullsOrder = {
|
|||||||
|
|
||||||
exports.Prisma.ModelName = {
|
exports.Prisma.ModelName = {
|
||||||
Collection: 'Collection',
|
Collection: 'Collection',
|
||||||
Drawing: 'Drawing'
|
Drawing: 'Drawing',
|
||||||
|
Library: 'Library'
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -801,6 +801,58 @@ app.delete("/collections/:id", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Library ---
|
||||||
|
|
||||||
|
// GET /library - Fetch stored library items
|
||||||
|
app.get("/library", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const library = await prisma.library.findUnique({
|
||||||
|
where: { id: "default" },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!library) {
|
||||||
|
// Return empty array if no library exists yet
|
||||||
|
return res.json({ items: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
items: JSON.parse(library.items),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch library:", error);
|
||||||
|
res.status(500).json({ error: "Failed to fetch library" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PUT /library - Update/create library items
|
||||||
|
app.put("/library", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { items } = req.body;
|
||||||
|
|
||||||
|
if (!Array.isArray(items)) {
|
||||||
|
return res.status(400).json({ error: "Items must be an array" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const library = await prisma.library.upsert({
|
||||||
|
where: { id: "default" },
|
||||||
|
update: {
|
||||||
|
items: JSON.stringify(items),
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: "default",
|
||||||
|
items: JSON.stringify(items),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
items: JSON.parse(library.items),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update library:", error);
|
||||||
|
res.status(500).json({ error: "Failed to update library" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// --- Export/Import Endpoints ---
|
// --- Export/Import Endpoints ---
|
||||||
|
|
||||||
// GET /export - Export SQLite database (supports .sqlite and .db extensions)
|
// GET /export - Export SQLite database (supports .sqlite and .db extensions)
|
||||||
|
|||||||
@@ -84,3 +84,15 @@ export const deleteCollection = async (id: string) => {
|
|||||||
const response = await api.delete<{ success: true }>(`/collections/${id}`);
|
const response = await api.delete<{ success: true }>(`/collections/${id}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Library ---
|
||||||
|
|
||||||
|
export const getLibrary = async () => {
|
||||||
|
const response = await api.get<{ items: any[] }>("/library");
|
||||||
|
return response.data.items;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateLibrary = async (items: any[]) => {
|
||||||
|
const response = await api.put<{ items: any[] }>("/library", { items });
|
||||||
|
return response.data.items;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { PenTool, Trash2, FolderInput, ArrowRight, Check, Clock, Copy, Download } from 'lucide-react';
|
import { PenTool, Trash2, FolderInput, ArrowRight, Check, Clock, Copy } from 'lucide-react';
|
||||||
import type { Drawing, Collection } from '../types';
|
import type { Drawing, Collection } from '../types';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { exportToSvg } from "@excalidraw/excalidraw";
|
import { exportToSvg } from "@excalidraw/excalidraw";
|
||||||
import { exportDrawingToFile } from '../utils/exportUtils';
|
|
||||||
|
|
||||||
import * as api from '../api';
|
import * as api from '../api';
|
||||||
|
|
||||||
@@ -326,16 +325,6 @@ export const DrawingCard: React.FC<DrawingCardProps> = ({
|
|||||||
<Copy size={14} /> Duplicate
|
<Copy size={14} /> Duplicate
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
exportDrawingToFile(drawing);
|
|
||||||
setContextMenu(null);
|
|
||||||
}}
|
|
||||||
className="w-full px-3 py-2 text-sm text-left text-slate-600 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800 hover:text-neutral-900 dark:hover:text-white flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<Download size={14} /> Export
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="border-t border-slate-50 dark:border-slate-700 my-1"></div>
|
<div className="border-t border-slate-50 dark:border-slate-700 my-1"></div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useEffect, useState, useRef } from 'react';
|
import React, { useCallback, useEffect, useState, useRef } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { ArrowLeft, Download } from 'lucide-react';
|
import { ArrowLeft } from 'lucide-react';
|
||||||
import { Excalidraw, exportToSvg } from '@excalidraw/excalidraw';
|
import { Excalidraw, exportToSvg } from '@excalidraw/excalidraw';
|
||||||
import '@excalidraw/excalidraw/index.css';
|
import '@excalidraw/excalidraw/index.css';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
@@ -9,7 +9,6 @@ import { Toaster, toast } from 'sonner';
|
|||||||
import { io, Socket } from 'socket.io-client';
|
import { io, Socket } from 'socket.io-client';
|
||||||
import { getUserIdentity } from '../utils/identity';
|
import { getUserIdentity } from '../utils/identity';
|
||||||
import { reconcileElements } from '../utils/sync';
|
import { reconcileElements } from '../utils/sync';
|
||||||
import { exportFromEditor } from '../utils/exportUtils';
|
|
||||||
import type { UserIdentity } from '../utils/identity';
|
import type { UserIdentity } from '../utils/identity';
|
||||||
import * as api from '../api';
|
import * as api from '../api';
|
||||||
import { useTheme } from '../context/ThemeContext';
|
import { useTheme } from '../context/ThemeContext';
|
||||||
@@ -242,6 +241,59 @@ export const Editor: React.FC = () => {
|
|||||||
setIsReady(true);
|
setIsReady(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Handle #addLibrary URL hash parameter for importing libraries from links
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isReady || !excalidrawAPI.current) return;
|
||||||
|
|
||||||
|
const hash = window.location.hash;
|
||||||
|
if (!hash.includes('addLibrary=')) return;
|
||||||
|
|
||||||
|
const params = new URLSearchParams(hash.slice(1)); // Remove the leading #
|
||||||
|
const libraryUrl = params.get('addLibrary');
|
||||||
|
|
||||||
|
if (!libraryUrl) return;
|
||||||
|
|
||||||
|
const importLibraryFromUrl = async () => {
|
||||||
|
try {
|
||||||
|
console.log('[Editor] Importing library from URL:', libraryUrl);
|
||||||
|
toast.loading('Importing library...', { id: 'library-import' });
|
||||||
|
|
||||||
|
const response = await fetch(libraryUrl);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch library: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
|
||||||
|
// Use Excalidraw's updateLibrary API with proper settings:
|
||||||
|
// - defaultStatus: "published" puts items in "Excalidraw library" section
|
||||||
|
// - merge: true preserves existing library items
|
||||||
|
// - openLibraryMenu: true shows the library sidebar after import
|
||||||
|
await excalidrawAPI.current.updateLibrary({
|
||||||
|
libraryItems: blob,
|
||||||
|
merge: true,
|
||||||
|
defaultStatus: "published",
|
||||||
|
openLibraryMenu: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the updated library items and persist to server
|
||||||
|
const updatedItems = excalidrawAPI.current.getAppState().libraryItems || [];
|
||||||
|
await api.updateLibrary([...updatedItems]);
|
||||||
|
|
||||||
|
toast.success('Library imported successfully', { id: 'library-import' });
|
||||||
|
console.log('[Editor] Library import complete');
|
||||||
|
|
||||||
|
// Clear the hash to prevent re-importing on refresh
|
||||||
|
window.history.replaceState(null, '', window.location.pathname + window.location.search);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Editor] Failed to import library:', err);
|
||||||
|
toast.error('Failed to import library', { id: 'library-import' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
importLibraryFromUrl();
|
||||||
|
}, [isReady]);
|
||||||
|
|
||||||
const buildEmptyScene = useCallback(() => ({
|
const buildEmptyScene = useCallback(() => ({
|
||||||
elements: [],
|
elements: [],
|
||||||
appState: {
|
appState: {
|
||||||
@@ -260,6 +312,7 @@ export const Editor: React.FC = () => {
|
|||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
const saveDataRef = useRef<((elements: readonly any[], appState: any) => Promise<void>) | null>(null);
|
const saveDataRef = useRef<((elements: readonly any[], appState: any) => Promise<void>) | null>(null);
|
||||||
const savePreviewRef = useRef<((elements: readonly any[], appState: any, files: any) => Promise<void>) | null>(null);
|
const savePreviewRef = useRef<((elements: readonly any[], appState: any, files: any) => Promise<void>) | null>(null);
|
||||||
|
const saveLibraryRef = useRef<((items: any[]) => Promise<void>) | null>(null);
|
||||||
|
|
||||||
// Update the ref on every render to ensure it has access to the latest props/state
|
// Update the ref on every render to ensure it has access to the latest props/state
|
||||||
saveDataRef.current = async (elements: readonly any[], appState: any) => {
|
saveDataRef.current = async (elements: readonly any[], appState: any) => {
|
||||||
@@ -327,6 +380,17 @@ export const Editor: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
saveLibraryRef.current = async (items: any[]) => {
|
||||||
|
try {
|
||||||
|
console.log("[Editor] Saving library", { itemCount: items.length });
|
||||||
|
await api.updateLibrary(items);
|
||||||
|
console.log("[Editor] Library save complete");
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to save library', err);
|
||||||
|
toast.error("Failed to save library");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Create the debounced function ONLY ONCE.
|
// Create the debounced function ONLY ONCE.
|
||||||
// It simply calls whatever is currently in saveDataRef.current
|
// It simply calls whatever is currently in saveDataRef.current
|
||||||
const debouncedSave = useCallback(
|
const debouncedSave = useCallback(
|
||||||
@@ -347,6 +411,15 @@ export const Editor: React.FC = () => {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const debouncedSaveLibrary = useCallback(
|
||||||
|
debounce((items: any[]) => {
|
||||||
|
if (saveLibraryRef.current) {
|
||||||
|
saveLibraryRef.current(items);
|
||||||
|
}
|
||||||
|
}, 1000),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const broadcastChanges = useCallback(
|
const broadcastChanges = useCallback(
|
||||||
throttle((elements: readonly any[]) => {
|
throttle((elements: readonly any[]) => {
|
||||||
if (!socketRef.current || !id) return;
|
if (!socketRef.current || !id) return;
|
||||||
@@ -392,7 +465,14 @@ export const Editor: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const data = await api.getDrawing(id);
|
// Fetch drawing and library in parallel
|
||||||
|
const [data, libraryItems] = await Promise.all([
|
||||||
|
api.getDrawing(id),
|
||||||
|
api.getLibrary().catch((err) => {
|
||||||
|
console.warn('Failed to load library, using empty:', err);
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
]);
|
||||||
setDrawingName(data.name);
|
setDrawingName(data.name);
|
||||||
|
|
||||||
// Use elements directly without converting - they're already normalized during import
|
// Use elements directly without converting - they're already normalized during import
|
||||||
@@ -418,6 +498,7 @@ export const Editor: React.FC = () => {
|
|||||||
appState: hydratedAppState,
|
appState: hydratedAppState,
|
||||||
files,
|
files,
|
||||||
scrollToContent: true,
|
scrollToContent: true,
|
||||||
|
libraryItems,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load drawing', err);
|
console.error('Failed to load drawing', err);
|
||||||
@@ -538,6 +619,12 @@ export const Editor: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle library changes and persist to server
|
||||||
|
const handleLibraryChange = useCallback((items: readonly any[]) => {
|
||||||
|
console.log("[Editor] Library changed", { itemCount: items.length });
|
||||||
|
debouncedSaveLibrary([...items]);
|
||||||
|
}, [debouncedSaveLibrary]);
|
||||||
|
|
||||||
// Disable native Excalidraw save dialogs
|
// Disable native Excalidraw save dialogs
|
||||||
// UIOptions is now defined outside the component
|
// UIOptions is now defined outside the component
|
||||||
|
|
||||||
@@ -595,25 +682,6 @@ export const Editor: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{/* Download Button */}
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (excalidrawAPI.current) {
|
|
||||||
const elements = excalidrawAPI.current.getSceneElementsIncludingDeleted();
|
|
||||||
const appState = excalidrawAPI.current.getAppState();
|
|
||||||
const files = excalidrawAPI.current.getFiles() || {};
|
|
||||||
exportFromEditor(drawingName, elements, appState, files);
|
|
||||||
toast.success('Drawing exported');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-neutral-800 rounded-lg text-gray-600 dark:text-gray-300 transition-colors"
|
|
||||||
title="Export drawing"
|
|
||||||
>
|
|
||||||
<Download size={20} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="h-6 w-px bg-gray-300 dark:bg-gray-700" />
|
|
||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<div
|
<div
|
||||||
@@ -659,6 +727,7 @@ export const Editor: React.FC = () => {
|
|||||||
initialData={initialData}
|
initialData={initialData}
|
||||||
onChange={handleCanvasChange}
|
onChange={handleCanvasChange}
|
||||||
onPointerUpdate={onPointerUpdate}
|
onPointerUpdate={onPointerUpdate}
|
||||||
|
onLibraryChange={handleLibraryChange}
|
||||||
excalidrawAPI={setExcalidrawAPI}
|
excalidrawAPI={setExcalidrawAPI}
|
||||||
UIOptions={UIOptions}
|
UIOptions={UIOptions}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
import type { Drawing } from "../types";
|
|
||||||
|
|
||||||
export interface ExportData {
|
|
||||||
type: "excalidraw";
|
|
||||||
version: 2;
|
|
||||||
source: string;
|
|
||||||
elements: any[];
|
|
||||||
appState: any;
|
|
||||||
files: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export a drawing to a .excalidraw file and trigger download
|
|
||||||
*/
|
|
||||||
export const exportDrawingToFile = (
|
|
||||||
drawing: Drawing,
|
|
||||||
filename?: string
|
|
||||||
): void => {
|
|
||||||
const exportData: ExportData = {
|
|
||||||
type: "excalidraw",
|
|
||||||
version: 2,
|
|
||||||
source: window.location.origin,
|
|
||||||
elements: drawing.elements || [],
|
|
||||||
appState: {
|
|
||||||
gridSize: drawing.appState?.gridSize ?? null,
|
|
||||||
viewBackgroundColor: drawing.appState?.viewBackgroundColor ?? "#ffffff",
|
|
||||||
},
|
|
||||||
files: drawing.files || {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], {
|
|
||||||
type: "application/json",
|
|
||||||
});
|
|
||||||
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const link = document.createElement("a");
|
|
||||||
link.href = url;
|
|
||||||
link.download = filename || `${drawing.name}.excalidraw`;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export drawing from Editor with current state
|
|
||||||
*/
|
|
||||||
export const exportFromEditor = (
|
|
||||||
name: string,
|
|
||||||
elements: readonly any[],
|
|
||||||
appState: any,
|
|
||||||
files: Record<string, any>
|
|
||||||
): void => {
|
|
||||||
const exportData: ExportData = {
|
|
||||||
type: "excalidraw",
|
|
||||||
version: 2,
|
|
||||||
source: window.location.origin,
|
|
||||||
elements: Array.from(elements),
|
|
||||||
appState: {
|
|
||||||
gridSize: appState?.gridSize ?? null,
|
|
||||||
viewBackgroundColor: appState?.viewBackgroundColor ?? "#ffffff",
|
|
||||||
},
|
|
||||||
files: files || {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], {
|
|
||||||
type: "application/json",
|
|
||||||
});
|
|
||||||
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const link = document.createElement("a");
|
|
||||||
link.href = url;
|
|
||||||
link.download = `${name}.excalidraw`;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user