Merge branch '8-export-drawing' into pre-release

This commit is contained in:
Zimeng Xiong
2025-11-24 14:43:58 -08:00
3 changed files with 111 additions and 2 deletions
+12 -1
View File
@@ -1,11 +1,12 @@
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 } from 'lucide-react'; import { PenTool, Trash2, FolderInput, ArrowRight, Check, Clock, Copy, Download } 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';
@@ -325,6 +326,16 @@ 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
+21 -1
View File
@@ -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 } from 'lucide-react'; import { ArrowLeft, Download } 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,6 +9,7 @@ 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';
@@ -682,6 +683,25 @@ 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
+78
View File
@@ -0,0 +1,78 @@
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);
};