import { useState } from 'react' import { useEditor } from 'tldraw' import { Share } from '@capacitor/share' import { Filesystem, Directory } from '@capacitor/filesystem' import { colors } from '../constants/theme' import { LinkIcon, ImageIcon } from './Icons' import { SERVER_URL } from '../config' interface ExportMenuProps { roomId: string theme: any } export function ExportMenu({ roomId, theme }: ExportMenuProps) { const editor = useEditor() const [loading, setLoading] = useState(false) // Helper: Convert Blob to Base64 const blobToBase64 = (blob: Blob): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader() reader.onerror = reject reader.onload = () => resolve(reader.result as string) reader.readAsDataURL(blob) }) } const handleShareLink = async () => { const url = `${SERVER_URL}/#/${roomId}` try { // Try native share or Web Share API first await Share.share({ title: 'Join my Whiteboard', text: 'Collaborate with me on this board:', url: url, dialogTitle: 'Share Board Link', }) } catch (error) { // Fallback to clipboard if Share API fails (common on desktop) console.log('Share API not available, falling back to clipboard', error) try { await navigator.clipboard.writeText(url) alert('Link copied to clipboard!') } catch (clipboardError) { console.error('Failed to copy to clipboard:', clipboardError) prompt('Copy this link:', url) } } } const handleExportImage = async () => { setLoading(true) try { // 1. Get all shape IDs from the current page const shapeIds = Array.from(editor.getCurrentPageShapeIds()) if (shapeIds.length === 0) { alert('Board is empty') setLoading(false) return } // 2. Calculate Bounds to prevent OOM let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity shapeIds.forEach(id => { const bounds = editor.getShapePageBounds(id) if (bounds) { minX = Math.min(minX, bounds.x) minY = Math.min(minY, bounds.y) maxX = Math.max(maxX, bounds.x + bounds.w) maxY = Math.max(maxY, bounds.y + bounds.h) } }) const width = maxX - minX const height = maxY - minY // Limit max dimension to ~3000px to avoid Android texture/memory limits const MAX_DIMENSION = 3000 let scale = 2 if (width * scale > MAX_DIMENSION || height * scale > MAX_DIMENSION) { scale = Math.min(MAX_DIMENSION / width, MAX_DIMENSION / height) console.log(`Large board detected. Reducing scale to ${scale.toFixed(2)} to prevent crash.`) } // 3. Generate Image with safe scale let result = await editor.toImage(shapeIds, { format: 'png', background: true, scale: scale, padding: 32, }) if (!result || !result.blob) { throw new Error('Failed to generate image blob') } // 4. Check file size. If huge (>2MB), convert to JPEG to ensure shareability if (result.blob.size > 2 * 1024 * 1024) { console.log('Image too large (>2MB), switching to JPEG for compression') result = await editor.toImage(shapeIds, { format: 'jpeg', quality: 0.8, background: true, scale: scale, padding: 32, }) } // 5. Convert Blob to Base64 for Capacitor const base64Data = await blobToBase64(result.blob!) const pureBase64 = base64Data.split(',')[1] const ext = result.blob!.type === 'image/jpeg' ? 'jpg' : 'png' const fileName = `board-${roomId}-${Date.now()}.${ext}` // 6. Save to Capacitor Filesystem const savedFile = await Filesystem.writeFile({ path: fileName, data: pureBase64, directory: Directory.Cache }) // 7. Share await Share.share({ title: 'Export Board', files: [savedFile.uri], }) } catch (error) { console.error('Export failed:', error) alert('Failed to export image: ' + (error as any).message) } finally { setLoading(false) } } return (
Share & Export
) }