diff --git a/examples/core-example-advanced/index.html b/examples/core-example-advanced/index.html index 98aa82b74..9ddc4fa8a 100644 --- a/examples/core-example-advanced/index.html +++ b/examples/core-example-advanced/index.html @@ -2,7 +2,7 @@ - + tldraw - core example advanced diff --git a/examples/core-example-advanced/public/favicon.ico b/examples/core-example-advanced/public/favicon.ico new file mode 100644 index 000000000..ebed98e37 Binary files /dev/null and b/examples/core-example-advanced/public/favicon.ico differ diff --git a/examples/core-example-advanced/src/index.html b/examples/core-example-advanced/src/index.html deleted file mode 100644 index 362929e63..000000000 --- a/examples/core-example-advanced/src/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - tldraw - - -
- - - - diff --git a/examples/core-example/index.html b/examples/core-example/index.html index 3cc9cf6fb..22786ead5 100644 --- a/examples/core-example/index.html +++ b/examples/core-example/index.html @@ -2,7 +2,7 @@ - + tldraw - core example diff --git a/examples/core-example/public/favicon.ico b/examples/core-example/public/favicon.ico new file mode 100644 index 000000000..ebed98e37 Binary files /dev/null and b/examples/core-example/public/favicon.ico differ diff --git a/examples/core-example/src/index.html b/examples/core-example/src/index.html deleted file mode 100644 index 362929e63..000000000 --- a/examples/core-example/src/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - tldraw - - -
- - - - diff --git a/examples/tldraw-example/index.html b/examples/tldraw-example/index.html index 77350ef03..8100c26f9 100644 --- a/examples/tldraw-example/index.html +++ b/examples/tldraw-example/index.html @@ -2,7 +2,7 @@ - + tldraw - example diff --git a/examples/tldraw-example/public/favicon.ico b/examples/tldraw-example/public/favicon.ico new file mode 100644 index 000000000..ebed98e37 Binary files /dev/null and b/examples/tldraw-example/public/favicon.ico differ diff --git a/packages/tldraw/package.json b/packages/tldraw/package.json index 4e808979d..e8f6e58df 100644 --- a/packages/tldraw/package.json +++ b/packages/tldraw/package.json @@ -47,6 +47,7 @@ "@tldraw/core": "^1.15.0", "@tldraw/intersect": "^1.7.1", "@tldraw/vec": "^1.7.1", + "browser-fs-access": "^0.31.0", "idb-keyval": "^6.1.0", "lz-string": "^1.4.4", "perfect-freehand": "^1.1.0", diff --git a/packages/tldraw/src/state/TldrawApp.ts b/packages/tldraw/src/state/TldrawApp.ts index d04aa7fc5..769f4bedc 100644 --- a/packages/tldraw/src/state/TldrawApp.ts +++ b/packages/tldraw/src/state/TldrawApp.ts @@ -222,7 +222,7 @@ export class TldrawApp extends StateManager { editingStartTime = -1 - fileSystemHandle: FileSystemHandle | null = null + fileSystemHandle: FileSystemFileHandle | null = null viewport = Utils.getBoundsFromPoints([ [0, 0], diff --git a/packages/tldraw/src/state/data/browser-fs-access/directory-open.js b/packages/tldraw/src/state/data/browser-fs-access/directory-open.js deleted file mode 100644 index 7114ef396..000000000 --- a/packages/tldraw/src/state/data/browser-fs-access/directory-open.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. -import supported from './supported.js' - -const implementation = !supported - ? import('./legacy/directory-open.js') - : import('./fs-access/directory-open.js') - -/** - * For opening directories, dynamically either loads the File System Access API - * module or the legacy method. - */ -export async function directoryOpen(...args) { - return (await implementation).default(...args) -} diff --git a/packages/tldraw/src/state/data/browser-fs-access/file-open.js b/packages/tldraw/src/state/data/browser-fs-access/file-open.js deleted file mode 100644 index 81bc74450..000000000 --- a/packages/tldraw/src/state/data/browser-fs-access/file-open.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. -import supported from './supported.js' - -const implementation = !supported - ? import('./legacy/file-open.js') - : import('./fs-access/file-open.js') - -/** - * For opening files, dynamically either loads the File System Access API module - * or the legacy method. - */ -export async function fileOpen(...args) { - return (await implementation).default(...args) -} diff --git a/packages/tldraw/src/state/data/browser-fs-access/file-save.js b/packages/tldraw/src/state/data/browser-fs-access/file-save.js deleted file mode 100644 index 5a69443ba..000000000 --- a/packages/tldraw/src/state/data/browser-fs-access/file-save.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. -import supported from './supported.js' - -const implementation = !supported - ? import('./legacy/file-save.js') - : import('./fs-access/file-save.js') - -/** - * For saving files, dynamically either loads the File System Access API module - * or the legacy method. - */ -export async function fileSave(...args) { - return (await implementation).default(...args) -} diff --git a/packages/tldraw/src/state/data/browser-fs-access/fs-access/directory-open.js b/packages/tldraw/src/state/data/browser-fs-access/fs-access/directory-open.js deleted file mode 100644 index 9fa2505ac..000000000 --- a/packages/tldraw/src/state/data/browser-fs-access/fs-access/directory-open.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. - -const getFiles = async (dirHandle, recursive, path = dirHandle.name, skipDirectory) => { - const dirs = [] - const files = [] - for (const entry of dirHandle.values()) { - const nestedPath = `${path}/${entry.name}` - if (entry.kind === 'file') { - files.push( - await entry.getFile().then((file) => { - file.directoryHandle = dirHandle - return Object.defineProperty(file, 'webkitRelativePath', { - configurable: true, - enumerable: true, - get: () => nestedPath, - }) - }) - ) - } else if ( - entry.kind === 'directory' && - recursive && - (!skipDirectory || !skipDirectory(entry)) - ) { - dirs.push(await getFiles(entry, recursive, nestedPath, skipDirectory)) - } - } - return [...(await Promise.all(dirs)).flat(), ...(await Promise.all(files))] -} - -/** - * Opens a directory from disk using the File System Access API. - * @type { typeof import("../../index").directoryOpen } - */ -export default async (options = {}) => { - options.recursive = options.recursive || false - const handle = await window.showDirectoryPicker({ - id: options.id, - startIn: options.startIn, - }) - return getFiles(handle, options.recursive, undefined, options.skipDirectory) -} diff --git a/packages/tldraw/src/state/data/browser-fs-access/fs-access/file-open.js b/packages/tldraw/src/state/data/browser-fs-access/fs-access/file-open.js deleted file mode 100644 index 45d0a1328..000000000 --- a/packages/tldraw/src/state/data/browser-fs-access/fs-access/file-open.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. - -const getFileWithHandle = async (handle) => { - const file = await handle.getFile() - file.handle = handle - return file -} - -/** - * Opens a file from disk using the File System Access API. - * @type { typeof import("../../index").fileOpen } - */ -export default async (options = [{}]) => { - if (!Array.isArray(options)) { - options = [options] - } - const types = [] - options.forEach((option, i) => { - types[i] = { - description: option.description || '', - accept: {}, - } - if (option.mimeTypes) { - option.mimeTypes.map((mimeType) => { - types[i].accept[mimeType] = option.extensions || [] - }) - } else { - types[i].accept['*/*'] = option.extensions || [] - } - }) - const handleOrHandles = await window.showOpenFilePicker({ - id: options[0].id, - startIn: options[0].startIn, - types, - multiple: options[0].multiple || false, - excludeAcceptAllOption: options[0].excludeAcceptAllOption || false, - }) - const files = await Promise.all(handleOrHandles.map(getFileWithHandle)) - if (options[0].multiple) { - return files - } - return files[0] -} diff --git a/packages/tldraw/src/state/data/browser-fs-access/fs-access/file-save.js b/packages/tldraw/src/state/data/browser-fs-access/fs-access/file-save.js deleted file mode 100644 index 68fce57d6..000000000 --- a/packages/tldraw/src/state/data/browser-fs-access/fs-access/file-save.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. - -/** - * Saves a file to disk using the File System Access API. - * @type { typeof import("../../index").fileSave } - */ -export default async ( - blobOrResponse, - options = [{}], - existingHandle = null, - throwIfExistingHandleNotGood = false -) => { - if (!Array.isArray(options)) { - options = [options] - } - options[0].fileName = options[0].fileName || 'Untitled' - const types = [] - options.forEach((option, i) => { - types[i] = { - description: option.description || '', - accept: {}, - } - if (option.mimeTypes) { - if (i === 0) { - if (blobOrResponse.type) { - option.mimeTypes.push(blobOrResponse.type) - } else if (blobOrResponse.headers && blobOrResponse.headers.get('content-type')) { - option.mimeTypes.push(blobOrResponse.headers.get('content-type')) - } - } - option.mimeTypes.map((mimeType) => { - types[i].accept[mimeType] = option.extensions || [] - }) - } else if (blobOrResponse.type) { - types[i].accept[blobOrResponse.type] = option.extensions || [] - } - }) - if (existingHandle) { - try { - // Check if the file still exists. - await existingHandle.getFile() - } catch (err) { - existingHandle = null - if (throwIfExistingHandleNotGood) { - throw err - } - } - } - const handle = - existingHandle || - (await window.showSaveFilePicker({ - suggestedName: options[0].fileName, - id: options[0].id, - startIn: options[0].startIn, - types, - excludeAcceptAllOption: options[0].excludeAcceptAllOption || false, - })) - const writable = await handle.createWritable() - // Use streaming on the `Blob` if the browser supports it. - if ('stream' in blobOrResponse) { - const stream = blobOrResponse.stream() - await stream.pipeTo(writable) - return handle - // Handle passed `ReadableStream`. - } else if ('body' in blobOrResponse) { - await blobOrResponse.body.pipeTo(writable) - return handle - } - // Default case of `Blob` passed and `Blob.stream()` not supported. - await writable.write(blob) - await writable.close() - return handle -} diff --git a/packages/tldraw/src/state/data/browser-fs-access/index.d.ts b/packages/tldraw/src/state/data/browser-fs-access/index.d.ts deleted file mode 100644 index 838ce7856..000000000 --- a/packages/tldraw/src/state/data/browser-fs-access/index.d.ts +++ /dev/null @@ -1,225 +0,0 @@ -/** - * Properties shared by all `options` provided to file save and open operations - */ -export interface CoreFileOptions { - /** Acceptable file extensions. Defaults to [""]. */ - extensions?: string[] - /** Suggested file description. Defaults to "". */ - description?: string - /** Acceptable MIME types. [] */ - mimeTypes?: string[] -} - -/** - * Properties shared by the _first_ `options` object provided to file save and - * open operations (any additional options objects provided to those operations - * cannot have these properties) - */ -export interface FirstCoreFileOptions extends CoreFileOptions { - startIn?: WellKnownDirectory | FileSystemHandle - /** By specifying an ID, the user agent can remember different directories for different IDs. */ - id?: string - excludeAcceptAllOption?: boolean | false -} - -/** - * The first `options` object passed to file save operations can also specify - * a filename - */ -export interface FirstFileSaveOptions extends FirstCoreFileOptions { - /** Suggested file name. Defaults to "Untitled". */ - fileName?: string - /** - * Configurable cleanup and `Promise` rejector usable with legacy API for - * determining when (and reacting if) a user cancels the operation. The - * method will be passed a reference to the internal `rejectionHandler` that - * can, e.g., be attached to/removed from the window or called after a - * timeout. The method should return a function that will be called when - * either the user chooses to open a file or the `rejectionHandler` is - * called. In the latter case, the returned function will also be passed a - * reference to the `reject` callback for the `Promise` returned by - * `fileOpen`, so that developers may reject the `Promise` when desired at - * that time. - * Example rejector: - * - * const file = await fileOpen({ - * legacySetup: (rejectionHandler) => { - * const timeoutId = setTimeout(rejectionHandler, 10_000); - * return (reject) => { - * clearTimeout(timeoutId); - * if (reject) { - * reject('My error message here.'); - * } - * }; - * }, - * }); - * - * ToDo: Remove this workaround once - * https://github.com/whatwg/html/issues/6376 is specified and supported. - */ - legacySetup?: ( - resolve: (value: Blob) => void, - rejectionHandler: () => void, - anchor: HTMLAnchorElement - ) => (reject?: (reason?: any) => void) => void -} - -/** - * The first `options` object passed to file open operations can specify - * whether multiple files can be selected (the return type of the operation - * will be updated appropriately) and a way of handling cleanup and rejection - * for legacy open operations. - */ -export interface FirstFileOpenOptions extends FirstCoreFileOptions { - /** Allow multiple files to be selected. Defaults to false. */ - multiple?: M - /** - * Configurable cleanup and `Promise` rejector usable with legacy API for - * determining when (and reacting if) a user cancels the operation. The - * method will be passed a reference to the internal `rejectionHandler` that - * can, e.g., be attached to/removed from the window or called after a - * timeout. The method should return a function that will be called when - * either the user chooses to open a file or the `rejectionHandler` is - * called. In the latter case, the returned function will also be passed a - * reference to the `reject` callback for the `Promise` returned by - * `fileOpen`, so that developers may reject the `Promise` when desired at - * that time. - * Example rejector: - * - * const file = await fileOpen({ - * legacySetup: (rejectionHandler) => { - * const timeoutId = setTimeout(rejectionHandler, 10_000); - * return (reject) => { - * clearTimeout(timeoutId); - * if (reject) { - * reject('My error message here.'); - * } - * }; - * }, - * }); - * - * ToDo: Remove this workaround once - * https://github.com/whatwg/html/issues/6376 is specified and supported. - */ - legacySetup?: ( - resolve: (value: M extends false | undefined ? FileWithHandle : FileWithHandle[]) => void, - rejectionHandler: () => void, - input: HTMLInputElement - ) => (reject?: (reason?: any) => void) => void -} - -/** - * Opens file(s) from disk. - */ -export function fileOpen( - options?: [FirstFileOpenOptions, ...CoreFileOptions[]] | FirstFileOpenOptions -): M extends false | undefined ? Promise : Promise - -export type WellKnownDirectory = - | 'desktop' - | 'documents' - | 'downloads' - | 'music' - | 'pictures' - | 'videos' - -/** - * Saves a file to disk. - * @returns Optional file handle to save in place. - */ -export function fileSave( - /** To-be-saved `Blob` or `Response` */ - blobOrResponse: Blob | Response, - options?: [FirstFileSaveOptions, ...CoreFileOptions[]] | FirstFileSaveOptions, - /** - * A potentially existing file handle for a file to save to. Defaults to - * null. - */ - existingHandle?: FileSystemHandle | null, - /** - * Determines whether to throw (rather than open a new file save dialog) - * when existingHandle is no longer good. Defaults to false. - */ - throwIfExistingHandleNotGood?: boolean | false -): Promise - -/** - * Opens a directory from disk using the File System Access API. - * @returns Contained files. - */ -export function directoryOpen(options?: { - /** Whether to recursively get subdirectories. */ - recursive: boolean - /** Suggested directory in which the file picker opens. */ - startIn?: WellKnownDirectory | FileSystemHandle - /** By specifying an ID, the user agent can remember different directories for different IDs. */ - id?: string - /** Callback to determine whether a directory should be entered, return `true` to skip. */ - skipDirectory?: (fileSystemDirectoryEntry: FileSystemDirectoryEntry) => boolean - /** - * Configurable setup, cleanup and `Promise` rejector usable with legacy API - * for determining when (and reacting if) a user cancels the operation. The - * method will be passed a reference to the internal `rejectionHandler` that - * can, e.g., be attached to/removed from the window or called after a - * timeout. The method should return a function that will be called when - * either the user chooses to open a file or the `rejectionHandler` is - * called. In the latter case, the returned function will also be passed a - * reference to the `reject` callback for the `Promise` returned by - * `fileOpen`, so that developers may reject the `Promise` when desired at - * that time. - * Example rejector: - * - * const file = await directoryOpen({ - * legacySetup: (rejectionHandler) => { - * const timeoutId = setTimeout(rejectionHandler, 10_000); - * return (reject) => { - * clearTimeout(timeoutId); - * if (reject) { - * reject('My error message here.'); - * } - * }; - * }, - * }); - * - * ToDo: Remove this workaround once - * https://github.com/whatwg/html/issues/6376 is specified and supported. - */ - legacySetup?: ( - resolve: (value: FileWithDirectoryHandle) => void, - rejectionHandler: () => void, - input: HTMLInputElement - ) => (reject?: (reason?: any) => void) => void -}): Promise - -/** - * Whether the File System Access API is supported. - */ -export const supported: boolean - -export function imageToBlob(img: HTMLImageElement): Promise - -export interface FileWithHandle extends File { - handle?: FileSystemHandle -} - -export interface FileWithDirectoryHandle extends File { - directoryHandle?: FileSystemHandle -} - -// The following typings implement the relevant parts of the File System Access -// API. This can be removed once the specification reaches the Candidate phase -// and is implemented as part of microsoft/TSJS-lib-generator. - -export interface FileSystemHandlePermissionDescriptor { - mode?: 'read' | 'readwrite' -} - -export interface FileSystemHandle { - readonly kind: 'file' | 'directory' - readonly name: string - - isSameEntry: (other: FileSystemHandle) => Promise - - queryPermission: (descriptor?: FileSystemHandlePermissionDescriptor) => Promise - requestPermission: (descriptor?: FileSystemHandlePermissionDescriptor) => Promise -} diff --git a/packages/tldraw/src/state/data/browser-fs-access/index.js b/packages/tldraw/src/state/data/browser-fs-access/index.js deleted file mode 100644 index d7a94ab34..000000000 --- a/packages/tldraw/src/state/data/browser-fs-access/index.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. - -/** - * @module browser-fs-access - */ -export { fileOpen } from './file-open.js' -export { directoryOpen } from './directory-open.js' -export { fileSave } from './file-save.js' -export { default as supported } from './supported.js' diff --git a/packages/tldraw/src/state/data/browser-fs-access/legacy/directory-open.js b/packages/tldraw/src/state/data/browser-fs-access/legacy/directory-open.js deleted file mode 100644 index 9fc001e1a..000000000 --- a/packages/tldraw/src/state/data/browser-fs-access/legacy/directory-open.js +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. - -/** - * Opens a directory from disk using the legacy - * `` method. - * @type { typeof import("../../index").directoryOpen } - */ -export default async (options = [{}]) => { - if (!Array.isArray(options)) { - options = [options] - } - options[0].recursive = options[0].recursive || false - return new Promise((resolve, reject) => { - const input = document.createElement('input') - input.type = 'file' - input.webkitdirectory = true - - const _reject = () => cleanupListenersAndMaybeReject(reject) - const _resolve = (value) => { - if (typeof cleanupListenersAndMaybeReject === 'function') { - cleanupListenersAndMaybeReject() - } - resolve(value) - } - // ToDo: Remove this workaround once - // https://github.com/whatwg/html/issues/6376 is specified and supported. - const cleanupListenersAndMaybeReject = - options[0].legacySetup && options[0].legacySetup(_resolve, _reject, input) - - input.addEventListener('change', () => { - let files = Array.from(input.files) - if (!options[0].recursive) { - files = files.filter((file) => { - return file.webkitRelativePath.split('/').length === 2 - }) - } else if (options[0].recursive && options[0].skipDirectory) { - files = files.filter((file) => { - const directoriesName = file.webkitRelativePath.split('/') - return directoriesName.every( - (directoryName) => - !options[0].skipDirectory({ - name: directoryName, - kind: 'directory', - }) - ) - }) - } - - _resolve(files) - }) - - input.click() - }) -} diff --git a/packages/tldraw/src/state/data/browser-fs-access/legacy/file-open.js b/packages/tldraw/src/state/data/browser-fs-access/legacy/file-open.js deleted file mode 100644 index 4f8be2586..000000000 --- a/packages/tldraw/src/state/data/browser-fs-access/legacy/file-open.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. - -/** - * Opens a file from disk using the legacy `` method. - * @type { typeof import("../../index").fileOpen } - */ -export default async (options = [{}]) => { - if (!Array.isArray(options)) { - options = [options] - } - return new Promise((resolve, reject) => { - const input = document.createElement('input') - input.type = 'file' - const accept = [ - ...options.map((option) => option.mimeTypes || []).join(), - options.map((option) => option.extensions || []).join(), - ].join() - input.multiple = options[0].multiple || false - // Empty string allows everything. - input.accept = accept || '' - const _reject = () => cleanupListenersAndMaybeReject(reject) - const _resolve = (value) => { - if (typeof cleanupListenersAndMaybeReject === 'function') { - cleanupListenersAndMaybeReject() - } - resolve(value) - } - // ToDo: Remove this workaround once - // https://github.com/whatwg/html/issues/6376 is specified and supported. - const cleanupListenersAndMaybeReject = - options[0].legacySetup && options[0].legacySetup(_resolve, _reject, input) - input.addEventListener('change', () => { - _resolve(input.multiple ? Array.from(input.files) : input.files[0]) - }) - - input.click() - }) -} diff --git a/packages/tldraw/src/state/data/browser-fs-access/legacy/file-save.js b/packages/tldraw/src/state/data/browser-fs-access/legacy/file-save.js deleted file mode 100644 index f7f49a969..000000000 --- a/packages/tldraw/src/state/data/browser-fs-access/legacy/file-save.js +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. - -/** - * Saves a file to disk using the legacy `` method. - * @type { typeof import("../../index").fileSave } - */ -export default async (blobOrResponse, options = {}) => { - if (Array.isArray(options)) { - options = options[0] - } - const a = document.createElement('a') - let data = blobOrResponse - // Handle the case where input is a `ReadableStream`. - if ('body' in blobOrResponse) { - data = await streamToBlob(blobOrResponse.body, blobOrResponse.headers.get('content-type')) - } - a.download = options.fileName || 'Untitled' - a.href = URL.createObjectURL(data) - - const _reject = () => cleanupListenersAndMaybeReject(reject) - const _resolve = () => { - if (typeof cleanupListenersAndMaybeReject === 'function') { - cleanupListenersAndMaybeReject() - } - } - // ToDo: Remove this workaround once - // https://github.com/whatwg/html/issues/6376 is specified and supported. - const cleanupListenersAndMaybeReject = - options.legacySetup && options.legacySetup(_resolve, _reject, a) - - a.addEventListener('click', () => { - // `setTimeout()` due to - // https://github.com/LLK/scratch-gui/issues/1783#issuecomment-426286393 - setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000) - _resolve(null) - }) - a.click() - return null -} - -/** - * Converts a passed `ReadableStream` to a `Blob`. - * @param {ReadableStream} stream - * @param {string} type - * @returns {Promise} - */ -async function streamToBlob(stream, type) { - const reader = stream.getReader() - const pumpedStream = new ReadableStream({ - start(controller) { - return pump() - /** - * Recursively pumps data chunks out of the `ReadableStream`. - * @type { () => Promise } - */ - async function pump() { - return reader.read().then(({ done, value }) => { - if (done) { - controller.close() - return - } - controller.enqueue(value) - return pump() - }) - } - }, - }) - - const res = new Response(pumpedStream) - reader.releaseLock() - return new Blob([await res.blob()], { type }) -} diff --git a/packages/tldraw/src/state/data/browser-fs-access/supported.js b/packages/tldraw/src/state/data/browser-fs-access/supported.js deleted file mode 100644 index e50802bb3..000000000 --- a/packages/tldraw/src/state/data/browser-fs-access/supported.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// @license © 2020 Google LLC. Licensed under the Apache License, Version 2.0. - -/** - * Returns whether the File System Access API is supported and usable in the - * current context (for example cross-origin iframes). - * @returns {boolean} Returns `true` if the File System Access API is supported and usable, else returns `false`. - */ -const supported = (() => { - // When running in an SSR environment return `false`. - if (typeof self === 'undefined') { - return false - } - // ToDo: Remove this check once Permissions Policy integration - // has happened, tracked in - // https://github.com/WICG/file-system-access/issues/245. - if ('top' in self && self !== top) { - try { - // This will succeed on same-origin iframes, - // but fail on cross-origin iframes. - top.location + '' - } catch { - return false - } - } else if ('showOpenFilePicker' in self) { - return 'showOpenFilePicker' - } - return false -})() - -export default supported diff --git a/packages/tldraw/src/state/data/filesystem.ts b/packages/tldraw/src/state/data/filesystem.ts index 0aa54d9da..e36c14eee 100644 --- a/packages/tldraw/src/state/data/filesystem.ts +++ b/packages/tldraw/src/state/data/filesystem.ts @@ -1,14 +1,15 @@ +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' -import type { FileSystemHandle } from './browser-fs-access' const options = { mode: 'readwrite' as const } -const checkPermissions = async (handle: FileSystemHandle) => { +const checkPermissions = async (handle: FileSystemFileHandle) => { return ( - (await handle.queryPermission(options)) === 'granted' || - (await handle.requestPermission(options)) === 'granted' + (await (handle as unknown as FileSystemHandle).queryPermission(options)) === 'granted' || + (await (handle as unknown as FileSystemHandle).requestPermission(options)) === 'granted' ) } @@ -19,11 +20,14 @@ export async function loadFileHandle() { return fileHandle } -export async function saveFileHandle(fileHandle: FileSystemHandle | null) { +export async function saveFileHandle(fileHandle: FileSystemFileHandle | null) { return setToIdb(`Tldraw_file_handle_${window.location.origin}`, fileHandle) } -export async function saveToFileSystem(document: TDDocument, fileHandle: FileSystemHandle | null) { +export async function saveToFileSystem( + document: TDDocument, + fileHandle: FileSystemFileHandle | null +) { // Create the saved file data const file: TDFile = { name: document.name || 'New Document', @@ -46,9 +50,6 @@ export async function saveToFileSystem(document: TDDocument, fileHandle: FileSys } // Save to file system - // @ts-ignore - const browserFS = await import('./browser-fs-access') - const fileSave = browserFS.fileSave const newFileHandle = await fileSave( blob, { @@ -66,13 +67,10 @@ export async function saveToFileSystem(document: TDDocument, fileHandle: FileSys } export async function openFromFileSystem(): Promise { // Get the blob - // @ts-ignore - const browserFS = await import('./browser-fs-access') - const fileOpen = browserFS.fileOpen const blob = await fileOpen({ description: 'Tldraw File', extensions: [`.tldr`], @@ -106,9 +104,6 @@ export async function openFromFileSystem(): Promise // The shape of the files stored in JSON export interface TDFile { name: string - fileHandle: FileSystemHandle | null + fileHandle: FileSystemFileHandle | null document: TDDocument assets: Record } @@ -567,11 +567,11 @@ export interface Command { } export interface FileWithHandle extends File { - handle?: FileSystemHandle + handle?: FileSystemFileHandle } export interface FileWithDirectoryHandle extends File { - directoryHandle?: FileSystemHandle + directoryHandle?: FileSystemDirectoryHandle } // The following typings implement the relevant parts of the File System Access diff --git a/yarn.lock b/yarn.lock index f0cd9cd63..e48474770 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3784,6 +3784,11 @@ breakword@^1.0.5: dependencies: wcwidth "^1.0.1" +browser-fs-access@^0.31.0: + version "0.31.0" + resolved "https://registry.yarnpkg.com/browser-fs-access/-/browser-fs-access-0.31.0.tgz#b2ac17322b81f3c89eaddcfc871b5fdbac174734" + integrity sha512-OndIM5LkmHfx01D3YuVvPpW3fochBeXFS3yhDzcD/Yk7S3lx5RZGltnf7xwbr5V0B8XuN2becZmtceOQFZgQPw== + browser-process-hrtime@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"