Jaimodiji's picture
Deploying multiplayer tldraw app
2ec5eb1 verified
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<string> => {
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 (
<div style={{ padding: 16 }}>
<div style={{
fontSize: 11,
fontWeight: 600,
color: colors.textMuted,
textTransform: 'uppercase',
marginBottom: 12,
}}>
Share & Export
</div>
<button
onClick={handleShareLink}
style={{
width: '100%',
padding: '12px',
marginBottom: 12,
background: theme.hover,
color: theme.text,
border: `1px solid ${theme.border}`,
borderRadius: 8,
display: 'flex',
alignItems: 'center',
gap: 10,
cursor: 'pointer',
fontSize: 14,
}}
>
<LinkIcon />
Share Link
</button>
<button
onClick={handleExportImage}
disabled={loading}
style={{
width: '100%',
padding: '12px',
background: theme.hover,
color: theme.text,
border: `1px solid ${theme.border}`,
borderRadius: 8,
display: 'flex',
alignItems: 'center',
gap: 10,
cursor: 'pointer',
fontSize: 14,
opacity: loading ? 0.7 : 1
}}
>
<ImageIcon />
{loading ? 'Generating...' : 'Export Image'}
</button>
</div>
)
}