import { fileOpen, fileSave } from 'browser-fs-access' import type { FileSystemHandle } from 'browser-fs-access' import { get as getFromIdb, set as setToIdb } from 'idb-keyval' import { IMAGE_EXTENSIONS, VIDEO_EXTENSIONS } from '~constants' import type { TDDocument, TDFile } from '~types' const options = { mode: 'readwrite' as const } const checkPermissions = async (handle: FileSystemFileHandle) => { return ( (await (handle as unknown as FileSystemHandle).queryPermission(options)) === 'granted' || (await (handle as unknown as FileSystemHandle).requestPermission(options)) === 'granted' ) } export async function loadFileHandle() { if (typeof Window === 'undefined' || !('_location' in Window)) return const fileHandle = await getFromIdb(`Tldraw_file_handle_${window.location.origin}`) if (!fileHandle) return null return fileHandle } export async function saveFileHandle(fileHandle: FileSystemFileHandle | null) { return setToIdb(`Tldraw_file_handle_${window.location.origin}`, fileHandle) } export async function saveToFileSystem( document: TDDocument, fileHandle: FileSystemFileHandle | null ) { // Create the saved file data const file: TDFile = { name: document.name || 'New Document', fileHandle: fileHandle ?? null, document, assets: {}, } // Serialize to JSON const json = JSON.stringify(file, null, 2) // Create blob const blob = new Blob([json], { type: 'application/vnd.Tldraw+json', }) if (fileHandle) { const hasPermissions = await checkPermissions(fileHandle) if (!hasPermissions) return null } // Save to file system const newFileHandle = await fileSave( blob, { fileName: `${file.name}.tldr`, description: 'Tldraw File', extensions: [`.tldr`], }, fileHandle ) await saveFileHandle(newFileHandle) // Return true return newFileHandle } export async function openFromFileSystem(): Promise { // Get the blob const blob = await fileOpen({ description: 'Tldraw File', extensions: [`.tldr`], multiple: false, }) if (!blob) return null // Get JSON from blob const json: string = await new Promise((resolve) => { const reader = new FileReader() reader.onloadend = () => { if (reader.readyState === FileReader.DONE) { resolve(reader.result as string) } } reader.readAsText(blob, 'utf8') }) // Parse const file: TDFile = JSON.parse(json) const fileHandle = blob.handle ?? null await saveFileHandle(fileHandle) return { fileHandle, document: file.document, } } export async function openAssetsFromFileSystem() { return fileOpen({ description: 'Image or Video', extensions: [...IMAGE_EXTENSIONS, ...VIDEO_EXTENSIONS], multiple: true, }) } export function fileToBase64(file: Blob): Promise { return new Promise((resolve, reject) => { if (file) { const reader = new FileReader() reader.readAsDataURL(file) reader.onload = () => resolve(reader.result) reader.onerror = (error) => reject(error) reader.onabort = (error) => reject(error) } }) } export function fileToText(file: Blob): Promise { return new Promise((resolve, reject) => { if (file) { const reader = new FileReader() reader.readAsText(file) reader.onload = () => resolve(reader.result) reader.onerror = (error) => reject(error) reader.onabort = (error) => reject(error) } }) } export function getImageSizeFromSrc(src: string): Promise { return new Promise((resolve, reject) => { const img = new Image() img.onload = () => resolve([img.width, img.height]) img.onerror = () => reject(new Error('Could not get image size')) img.src = src }) } export function getVideoSizeFromSrc(src: string): Promise { return new Promise((resolve, reject) => { const video = document.createElement('video') video.onloadedmetadata = () => resolve([video.videoWidth, video.videoHeight]) video.onerror = () => reject(new Error('Could not get video size')) video.src = src }) }