Преглед изворни кода

feat: pasting image/video urls

Peng Xiao пре 3 година
родитељ
комит
9f58518318

+ 45 - 18
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)
     },
     []
   )

+ 2 - 0
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',

+ 2 - 1
tldraw/packages/core/src/utils/DataUtils.ts

@@ -77,7 +77,7 @@ export function fileToBase64(file: Blob): Promise<string | ArrayBuffer | null> {
 }
 
 export function getSizeFromSrc(dataURL: string, isVideo: boolean): Promise<number[]> {
-  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<numbe
       const img = new Image()
       img.onload = () => resolve([img.width, img.height])
       img.src = dataURL
+      img.onerror = err => reject(err)
     }
   })
 }

+ 2 - 0
tldraw/packages/react/src/hooks/useCursor.ts

@@ -24,6 +24,8 @@ const CURSORS: Record<TLCursor, (r: number, f?: boolean) => 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),