Merge branch '8-export-drawing' into pre-release
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user