Spaces:
Sleeping
Sleeping
File size: 11,803 Bytes
2ec5eb1 2798672 9f072e4 2ec5eb1 9f072e4 2ec5eb1 2798672 2ec5eb1 9f072e4 deefa8e 9f072e4 2ec5eb1 9f072e4 2ec5eb1 9f072e4 2ec5eb1 9f072e4 2ec5eb1 9f072e4 2ec5eb1 9f072e4 2ec5eb1 9f072e4 2ec5eb1 9f072e4 2ec5eb1 9f072e4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 | 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<string> => {
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)
}
}
|