From 9f5851831848a830a2e9afafd3cee7b07994f516 Mon Sep 17 00:00:00 2001 From: Peng Xiao Date: Wed, 31 Aug 2022 13:25:38 +0800 Subject: [PATCH] feat: pasting image/video urls --- .../apps/tldraw-logseq/src/hooks/usePaste.ts | 63 +++++++++++++------ tldraw/packages/core/src/types/TLCursor.ts | 2 + tldraw/packages/core/src/utils/DataUtils.ts | 3 +- tldraw/packages/react/src/hooks/useCursor.ts | 2 + 4 files changed, 51 insertions(+), 19 deletions(-) diff --git a/tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts b/tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts index 2bcc3380ed..79e5c3a3eb 100644 --- a/tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts +++ b/tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts @@ -3,6 +3,7 @@ import { getSizeFromSrc, TLAsset, TLBinding, + TLCursor, TLShapeModel, uniqueId, validUUID, @@ -38,6 +39,10 @@ const safeParseJson = (json: string) => { } } +const IMAGE_EXTENSIONS = ['.png', '.svg', '.jpg', '.jpeg', '.gif'] +const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.ogg'] + +// FIXME: for assets, we should prompt the user a loading spinner export function usePaste(context: LogseqContextValue) { const { handlers } = context @@ -56,11 +61,32 @@ export function usePaste(context: LogseqContextValue) { return await handlers.saveAsset(file) } + async function handleAssetUrl(url: string, isVideo: boolean) { + // Do we already have an asset for this image? + const existingAsset = Object.values(app.assets).find(asset => asset.src === url) + if (existingAsset) { + imageAssetsToCreate.push(existingAsset as VideoImageAsset) + return true + } else { + try { + // Create a new asset for this image + const asset: VideoImageAsset = { + id: uniqueId(), + type: isVideo ? 'video' : 'image', + src: url, + size: await getSizeFromSrc(handlers.makeAssetUrl(url), isVideo), + } + imageAssetsToCreate.push(asset) + return true + } finally { + return false + } + } + } + // TODO: handle PDF? async function handleFiles(files: File[]) { - const IMAGE_EXTENSIONS = ['.png', '.svg', '.jpg', '.jpeg', '.gif'] - const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.ogg'] - + let added = false for (const file of files) { // Get extension, verify that it's an image const extensionMatch = file.name.match(/\.[0-9a-z]+$/i) @@ -78,24 +104,14 @@ export function usePaste(context: LogseqContextValue) { if (!dataurl) { continue } - // Do we already have an asset for this image? - const existingAsset = Object.values(app.assets).find(asset => asset.src === dataurl) - if (existingAsset) { - imageAssetsToCreate.push(existingAsset as VideoImageAsset) - continue + if (await handleAssetUrl(dataurl, isVideo)) { + added = true } - // Create a new asset for this image - const asset: VideoImageAsset = { - id: uniqueId(), - type: isVideo ? 'video' : 'image', - src: dataurl, - size: await getSizeFromSrc(handlers.makeAssetUrl(dataurl), isVideo), - } - imageAssetsToCreate.push(asset) } catch (error) { console.error(error) } } + return added } async function handleHTML(item: ClipboardItem) { @@ -118,7 +134,7 @@ export function usePaste(context: LogseqContextValue) { const blob = await item.getType('text/plain') const rawText = (await blob.text()).trim() - if (handleURL(rawText)) { + if (await handleURL(rawText)) { return true } @@ -204,7 +220,7 @@ export function usePaste(context: LogseqContextValue) { return false } - function handleURL(rawText: string) { + async function handleURL(rawText: string) { if (isValidURL(rawText)) { const isYoutubeUrl = (url: string) => { const youtubeRegex = @@ -219,6 +235,14 @@ export function usePaste(context: LogseqContextValue) { }) return true } + const extension = rawText.match(/\.[0-9a-z]+$/i)?.[0].toLowerCase() + if ( + extension && + [...IMAGE_EXTENSIONS, ...VIDEO_EXTENSIONS].includes(extension) && + (await handleAssetUrl(rawText, VIDEO_EXTENSIONS.includes(extension))) + ) { + return true + } // ??? deal with normal URLs? } return false @@ -279,6 +303,8 @@ export function usePaste(context: LogseqContextValue) { return false } + app.cursors.setCursor(TLCursor.Progress) + try { if (files && files.length > 0) { await handleFiles(files) @@ -324,6 +350,7 @@ export function usePaste(context: LogseqContextValue) { app.currentPage.updateBindings(Object.fromEntries(bindingsToCreate.map(b => [b.id, b]))) app.setSelectedShapes(allShapesToAdd.map(s => s.id)) }) + app.cursors.setCursor(TLCursor.Default) }, [] ) diff --git a/tldraw/packages/core/src/types/TLCursor.ts b/tldraw/packages/core/src/types/TLCursor.ts index c0f15d2304..752ca6f1c7 100644 --- a/tldraw/packages/core/src/types/TLCursor.ts +++ b/tldraw/packages/core/src/types/TLCursor.ts @@ -5,6 +5,8 @@ export enum TLCursor { Cross = 'crosshair', Grab = 'grab', Rotate = 'rotate', + Wait = 'wait', + Progress = 'progress', Grabbing = 'grabbing', ResizeEdge = 'resize-edge', ResizeCorner = 'resize-corner', diff --git a/tldraw/packages/core/src/utils/DataUtils.ts b/tldraw/packages/core/src/utils/DataUtils.ts index dfda35a35e..0c19bdadec 100644 --- a/tldraw/packages/core/src/utils/DataUtils.ts +++ b/tldraw/packages/core/src/utils/DataUtils.ts @@ -77,7 +77,7 @@ export function fileToBase64(file: Blob): Promise { } export function getSizeFromSrc(dataURL: string, isVideo: boolean): Promise { - return new Promise(resolve => { + return new Promise((resolve, reject) => { if (isVideo) { const video = document.createElement('video') @@ -100,6 +100,7 @@ export function getSizeFromSrc(dataURL: string, isVideo: boolean): Promise resolve([img.width, img.height]) img.src = dataURL + img.onerror = err => reject(err) } }) } diff --git a/tldraw/packages/react/src/hooks/useCursor.ts b/tldraw/packages/react/src/hooks/useCursor.ts index 3cbb36d11a..0bc792a0f2 100644 --- a/tldraw/packages/react/src/hooks/useCursor.ts +++ b/tldraw/packages/react/src/hooks/useCursor.ts @@ -24,6 +24,8 @@ const CURSORS: Record string> = { [TLCursor.Pointer]: (r, f) => 'pointer', [TLCursor.Cross]: (r, f) => 'crosshair', [TLCursor.Move]: (r, f) => 'move', + [TLCursor.Wait]: (r, f) => 'wait', + [TLCursor.Progress]: (r, f) => 'progress', [TLCursor.Grab]: (r, f) => getCursorCss(GRAB_SVG, r, f), [TLCursor.Grabbing]: (r, f) => getCursorCss(GRABBING_SVG, r, f), [TLCursor.Text]: (r, f) => getCursorCss(TEXT_SVG, r, f),