Spaces:
Sleeping
Sleeping
File size: 9,556 Bytes
2798672 8ab98be 9f072e4 88e8d49 2798672 8ab98be d81f838 2798672 d81f838 2798672 d81f838 2798672 d81f838 2798672 d81f838 2798672 d81f838 2798672 d81f838 2798672 d81f838 2798672 d81f838 9f072e4 88e8d49 | 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 | import { useState, useRef, useEffect, ChangeEvent } from 'react'
import { useEditor, TldrawUiMenuItem, uniqueId } from 'tldraw'
import { useAuth } from '../hooks/useAuth'
import { useBackups } from '../hooks/useBackups'
import { exportToImage } from '../utils/exportUtils'
import { triggerSvgImport } from '../utils/svgImport'
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem'
import { Share } from '@capacitor/share'
import { Capacitor } from '@capacitor/core'
export function BackupMenuItem({ roomId }: { roomId: string }) {
const editor = useEditor()
const { isAuthenticated } = useAuth()
const { createBackup } = useBackups()
const [isBackingUp, setIsBackingUp] = useState(false)
if (!isAuthenticated) return null
const handleBackup = async () => {
if (isBackingUp) return
const confirmBackup = window.confirm("Save a backup of this board to the cloud?")
if (!confirmBackup) return
setIsBackingUp(true)
try {
const snapshot = editor.getSnapshot()
// Get room name if available (from local storage or metadata?)
// We can try to get it from the store if we stored it there,
// otherwise just use the ID or ask the user.
// For now let's check localStorage for the name we saved in Lobby/RoomPage
// Try to find name in local storage first
let roomName = 'Untitled'
try {
const storedRooms = localStorage.getItem('tldraw_saved_rooms')
if (storedRooms) {
const parsed = JSON.parse(storedRooms)
const room = parsed.find((r: any) => r.id === roomId)
if (room) roomName = room.name
}
} catch (e) {}
await createBackup(snapshot, roomName, roomId, 'tldraw')
alert('Backup saved successfully!')
} catch (e: any) {
console.error(e)
alert('Failed to save backup: ' + e.message)
} finally {
setIsBackingUp(false)
}
}
return (
<TldrawUiMenuItem
id="cloud-backup"
label={isBackingUp ? "Backing up..." : "Save to Cloud"}
icon="cloud"
readonlyOk
onSelect={handleBackup}
disabled={isBackingUp}
/>
)
}
export function DownloadMenuItem({ roomId }: { roomId: string }) {
const editor = useEditor()
const handleDownload = async () => {
try {
const snapshot = editor.getSnapshot()
const jsonStr = JSON.stringify({
snapshot,
roomId,
timestamp: Date.now(),
source: 'tldraw-multiplayer'
}, null, 2)
const fileName = `tldraw-room-${roomId}-${new Date().toISOString().slice(0,10)}.json`
if (Capacitor.isNativePlatform()) {
// Native (Android/iOS): Use Filesystem + Share
try {
const savedFile = await Filesystem.writeFile({
path: fileName,
data: jsonStr,
directory: Directory.Cache,
encoding: Encoding.UTF8
})
await Share.share({
title: 'Backup Board JSON',
files: [savedFile.uri],
})
} catch (err: any) {
console.error('Native save failed', err)
alert('Failed to save file: ' + err.message)
}
} else {
// Web: Use anchor tag download
const blob = new Blob([jsonStr], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
} catch (e: any) {
console.error('Failed to download backup', e)
alert('Failed to download backup: ' + e.message)
}
}
return (
<TldrawUiMenuItem
id="download-json"
label="Download File"
icon="download"
readonlyOk
onSelect={handleDownload}
/>
)
}
export function RestoreMenuItem() {
const handleRestoreClick = () => {
window.dispatchEvent(new CustomEvent('tldraw-trigger-file-restore'))
}
return (
<TldrawUiMenuItem
id="restore-json"
label="Restore from File"
icon="external-link"
readonlyOk
onSelect={handleRestoreClick}
/>
)
}
export function RestoreFileHandler({ roomId: _roomId }: { roomId: string }) {
const editor = useEditor()
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
const handleTrigger = () => {
if (inputRef.current) {
inputRef.current.click()
}
}
window.addEventListener('tldraw-trigger-file-restore', handleTrigger)
return () => window.removeEventListener('tldraw-trigger-file-restore', handleTrigger)
}, [])
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
const reader = new FileReader()
reader.onload = (event) => {
try {
const jsonStr = event.target?.result as string
const data = JSON.parse(jsonStr)
// Handle both raw snapshot or our wrapped format
const snapshot = data.snapshot || data
const mode = window.confirm('Restore as a NEW board? (Click Cancel to OVERWRITE the current board)')
if (mode) {
// Restore as New
const newId = uniqueId()
const name = data.roomName || 'Restored Board'
// Store for the new room to pick up
localStorage.setItem(`restore_data_${newId}`, JSON.stringify({
snapshot,
roomName: name
}))
// Save to recents
const storedRooms = localStorage.getItem('tldraw_saved_rooms')
let recentRooms = []
try {
if (storedRooms) recentRooms = JSON.parse(storedRooms)
} catch (e) {}
recentRooms.push({
id: newId,
name: name,
lastVisited: Date.now()
})
localStorage.setItem('tldraw_saved_rooms', JSON.stringify(recentRooms))
// Open in new tab or navigate?
// Let's navigate to keep it simple, or open in new tab if requested.
// Implementation plan said "Creates new Room -> Loads snapshot -> Navigates to room"
window.location.assign(`/#/${newId}`)
} else {
// Overwrite Current
if (window.confirm('WARNING: This will permanently overwrite the current board content for everyone. Are you sure?')) {
editor.loadSnapshot(snapshot)
}
}
} catch (e: any) {
console.error('Failed to parse backup file', e)
alert('Failed to restore backup: Invalid file format (' + e.message + ')')
} finally {
// Reset input
if (inputRef.current) inputRef.current.value = ''
}
}
reader.onerror = (err) => {
alert('FileReader Error: ' + err)
}
reader.readAsText(file)
}
return (
<input
ref={inputRef}
type="file"
accept="application/json,.json"
style={{ position: 'fixed', top: '-10000px', left: '-10000px', opacity: 0, pointerEvents: 'none' }}
onChange={handleFileChange}
/>
)
}
export function PdfExportMenuItem({ roomId }: { roomId: string }) {
const editor = useEditor()
const [isExporting, setIsExporting] = useState(false)
const handleExport = async () => {
if (isExporting) return
setIsExporting(true)
try {
await exportToImage(editor, roomId, 'pdf')
} catch (e) {
console.error(e)
} finally {
setIsExporting(false)
}
}
return (
<TldrawUiMenuItem
id="export-as-pdf"
label={isExporting ? "Exporting PDF..." : "Export as PDF"}
icon="file" // Using generic file icon as 'pdf' might not be standard in Tldraw icon set yet, or we could check. 'file' is safe.
readonlyOk
onSelect={handleExport}
disabled={isExporting}
/>
)
}
export function ImportSvgMenuItem() {
const editor = useEditor()
const handleImport = () => {
triggerSvgImport(editor)
}
return (
<TldrawUiMenuItem
id="import-svg"
label="Import from SVG"
icon="image"
readonlyOk
onSelect={handleImport}
/>
)
}
|