import { Share } from '@capacitor/share' import { Filesystem, Directory } from '@capacitor/filesystem' import { SERVER_URL } from '../config' import { Editor } from 'tldraw' import { customAlert, customPrompt, showToast } from './uiUtils' import { jsPDF } from 'jspdf' import 'svg2pdf.js' // 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) }) } export const shareLink = async (roomId: string) => { 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) await customAlert('Link copied to clipboard!') } catch (clipboardError) { console.error('Failed to copy to clipboard:', clipboardError) await customPrompt('Copy this link:', url) } } } export const exportToImage = async (editor: Editor, roomId: string, format: 'png' | 'svg' | 'pdf' = 'png') => { try { // 1. Get all shape IDs from the current page const shapeIds = Array.from(editor.getCurrentPageShapeIds()) if (shapeIds.length === 0) { await customAlert('Board is empty') return } // Show quiet indicator showToast(`Preparing ${format.toUpperCase()} export...`, { duration: 3000 }) // 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 // --- SVG EXPORT --- if (format === 'svg') { try { // Use toImage with format: 'svg' which returns a Blob const result = await editor.toImage(shapeIds, { format: 'svg', background: true, scale: 1, padding: 32, }) if (!result || !result.blob) throw new Error('Failed to generate SVG blob') // Convert Blob to Base64 const base64Data = await blobToBase64(result.blob) // base64Data is like "data:image/svg+xml;base64,PHN2Zy..." const base64Svg = base64Data.split(',')[1] const fileName = `board-${roomId}-${Date.now()}.svg` const savedFile = await Filesystem.writeFile({ path: fileName, data: base64Svg, directory: Directory.Cache }) try { await Share.share({ title: 'Export Board as SVG', files: [savedFile.uri], }) } catch (shareError) { console.log('Share API not available for SVG, downloading directly') // For SVG blob, we can use the base64 data URI we already have const downloadUrl = base64Data const link = document.createElement('a') link.href = downloadUrl link.download = fileName document.body.appendChild(link) link.click() document.body.removeChild(link) } return } catch (e: any) { console.error('SVG Export failed', e) throw new Error('SVG Export failed: ' + e.message) } } // --- IMAGE GENERATION (PNG/PDF) --- // Limit max dimension to ~4000px (increased from 3000) for better quality, // but still safety capped for mobile memory const MAX_DIMENSION = 4000 let scale = 3 // Default high quality scale (increased from 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 improved scale let result try { // Force PNG for quality first result = await editor.toImage(shapeIds, { format: 'png', background: true, scale: scale, padding: 32, }) } catch (e) { throw new Error('Image generation failed: ' + (e as any).message) } if (!result || !result.blob) { throw new Error('Failed to generate image blob (empty result)') } // --- PDF EXPORT (Vector Optimized) --- if (format === 'pdf') { try { // Ask user for background preference const isDarkMode = editor.user.getIsDarkMode() let keepBackground = true if (isDarkMode) { keepBackground = window.confirm( "Export with Dark Background?\n\n" + "OK: Keep dark background (like screen)\n" + "Cancel: Use light background (better for printing)" ) } // 1. Generate SVG first (best quality, vector) const result = await editor.toImage(shapeIds, { format: 'svg', scale: 1, background: keepBackground, // Tldraw handles theme based on current user preference usually padding: 32, darkMode: keepBackground ? isDarkMode : false // Force light mode if user wants printing version }) if (!result || !result.blob) throw new Error('Failed to generate SVG for PDF') // 2. Read SVG string from Blob const svgText = await result.blob.text() // 3. Parse to DOM Element const parser = new DOMParser() const svgElement = parser.parseFromString(svgText, "image/svg+xml").documentElement as unknown as SVGElement // 4. Create PDF with correct dimensions // Dimensions are in pixels in the SVG, we can keep using pixels in PDF for simplicity const orientation = width > height ? 'l' : 'p' const pdf = new jsPDF({ orientation, unit: 'px', format: [width + 64, height + 64] // Add padding }) // Draw background directly on PDF canvas if (keepBackground) { const bgColor = isDarkMode ? "#212529" : "#ffffff" pdf.setFillColor(bgColor) pdf.rect(0, 0, width + 64, height + 64, 'F') } // 5. Render SVG to PDF using svg2pdf (injected into jsPDF) // Note: using 'await' as svg() returns a Promise await pdf.svg(svgElement, { x: 0, y: 0, width: width + 64, height: height + 64, }) // 6. Output as base64 string const pdfBase64 = pdf.output('datauristring').split(',')[1] const fileName = `board-${roomId}-${Date.now()}.pdf` const savedFile = await Filesystem.writeFile({ path: fileName, data: pdfBase64, directory: Directory.Cache }) try { await Share.share({ title: 'Export Board as PDF', files: [savedFile.uri], }) } catch (shareError) { console.log('Share API not available for PDF, downloading directly') const downloadUrl = `data:application/pdf;base64,${pdfBase64}` const link = document.createElement('a') link.href = downloadUrl link.download = fileName document.body.appendChild(link) link.click() document.body.removeChild(link) } return } catch (e: any) { console.error('PDF Export failed', e) throw new Error('PDF Export failed: ' + e.message) } } // --- PNG EXPORT (FALLBACK & DEFAULT) --- // 4. Check file size. If huge (>4MB), convert to JPEG to ensure shareability // Increased limit from 2MB to 4MB as devices handle larger files now if (result.blob.size > 4 * 1024 * 1024) { console.log('Image too large (>4MB), switching to JPEG for compression') try { result = await editor.toImage(shapeIds, { format: 'jpeg', quality: 0.9, // High quality JPEG background: true, scale: scale, padding: 32, }) } catch (e) { console.warn('JPEG fallback failed, using original PNG') } } // 5. Convert Blob to Base64 for Capacitor let pureBase64 try { const base64Data = await blobToBase64(result.blob!) pureBase64 = base64Data.split(',')[1] } catch (e) { throw new Error('Base64 conversion failed') } const isJpeg = result.blob!.type === 'image/jpeg' const ext = isJpeg ? 'jpg' : 'png' const mimeType = isJpeg ? 'image/jpeg' : 'image/png' const fileName = `board-${roomId}-${Date.now()}.${ext}` // 6. Save to Capacitor Filesystem let savedFile try { savedFile = await Filesystem.writeFile({ path: fileName, data: pureBase64, directory: Directory.Cache }) } catch (e) { throw new Error('File save failed: ' + (e as any).message) } // 7. Share try { await Share.share({ title: 'Export Board', files: [savedFile.uri], }) } catch (e) { console.log('Share API failed or not available, falling back to download', e) // Web/Desktop Fallback: Download via anchor tag const downloadUrl = `data:${mimeType};base64,${pureBase64}` const link = document.createElement('a') link.href = downloadUrl link.download = fileName document.body.appendChild(link) link.click() document.body.removeChild(link) } } catch (error) { console.error('Export failed:', error) await customAlert((error as any).message) } }