Browse Source

fix: save image into asset folder instead of using base64

Peng Xiao 3 years ago
parent
commit
ce9a112ff0

+ 15 - 2
src/main/frontend/extensions/tldraw.cljs

@@ -3,6 +3,7 @@
             [frontend.components.block :as block]
             [frontend.components.page :as page]
             [frontend.db.model :as model]
+            [frontend.handler.editor :as editor-handler]
             [frontend.handler.search :as search]
             [frontend.handler.whiteboard :as whiteboard-handler]
             [frontend.rum :as r]
@@ -32,17 +33,27 @@
   [props]
   (block/page-cp {:preview? true} {:block/name (gobj/get props "pageName")}))
 
-(defn create-block-shape-by-id [e]
+(defn create-block-shape-by-id
+  [e]
   (when-let [block (block/get-dragging-block)]
     (let [uuid (:block/uuid block)
           client-x (gobj/get e "clientX")
           client-y (gobj/get e "clientY")]
       (whiteboard-handler/add-new-block-shape! uuid client-x client-y))))
 
-(defn search-handler [q]
+(defn search-handler
+  [q]
   (p/let [results (search/search q)]
     (clj->js results)))
 
+(defn save-asset-handler
+  [file]
+  (-> (editor-handler/save-assets! nil (state/get-current-repo) [(js->clj file)])
+      (p/then
+       (fn [res]
+         (when-let [[asset-file-name _ full-file-path] (and (seq res) (first res))]
+           (editor-handler/resolve-relative-path (or full-file-path asset-file-name)))))))
+
 (rum/defc tldraw-app
   [name block-id]
   (let [data (whiteboard-handler/page-name->tldr! name block-id)
@@ -71,6 +82,8 @@
                 :handlers (clj->js {:search search-handler
                                     :queryBlockByUUID #(clj->js (model/query-block-by-uuid (parse-uuid %)))
                                     :isWhiteboardPage model/whiteboard-page?
+                                    :saveAsset save-asset-handler
+                                    :makeAssetUrl editor-handler/make-asset-url
                                     :addNewBlock (fn [content]
                                                    (str (whiteboard-handler/add-new-block! name content)))})
                 :onMount (fn [app] (set-tln ^js app))

+ 2 - 0
src/main/logseq/api.cljs

@@ -857,6 +857,8 @@
   (p/let [_ (el/persist-dbs!)]
          true))
 
+(def ^:export make_asset_url editor-handler/make-asset-url)
+
 (defn ^:export __debug_state
   [path]
   (-> (if (string? path)

+ 160 - 145
tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts

@@ -1,6 +1,5 @@
 import {
   BoundsUtils,
-  fileToBase64,
   getSizeFromSrc,
   TLAsset,
   TLBinding,
@@ -23,11 +22,6 @@ const isValidURL = (url: string) => {
   }
 }
 
-const getYoutubeId = (url: string) => {
-  const match = url.match(/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#&?]*).*/)
-  return match && match[2].length === 11 ? match[2] : null
-}
-
 export function usePaste(context: LogseqContextValue) {
   const { handlers } = context
 
@@ -42,26 +36,30 @@ export function usePaste(context: LogseqContextValue) {
       const shapesToCreate: Shape['props'][] = []
       const bindingsToCreate: TLBinding[] = []
 
+      async function createAsset(file: File): Promise<string | null> {
+        return await handlers.saveAsset(file)
+      }
+
+      // TODO: handle PDF?
       async function handleFiles(files: File[]) {
         const IMAGE_EXTENSIONS = ['.png', '.svg', '.jpg', '.jpeg', '.gif']
 
-        const blobs = files.filter(file => {
+        for (const file of files) {
           // Get extension, verify that it's an image
           const extensionMatch = file.name.match(/\.[0-9a-z]+$/i)
-          if (!extensionMatch) throw Error('No extension.')
+          if (!extensionMatch) {
+            continue
+          }
           const extension = extensionMatch[0].toLowerCase()
-          return IMAGE_EXTENSIONS.includes(extension)
-        })
-
-        return handleBlobs(blobs)
-      }
-
-      async function handleBlobs(blobs: Blob[]) {
-        for (const blob of blobs) {
+          if (!IMAGE_EXTENSIONS.includes(extension)) {
+            continue
+          }
           try {
             // Turn the image into a base64 dataurl
-            const dataurl = await fileToBase64(blob)
-            if (typeof dataurl !== 'string') continue
+            const dataurl = await createAsset(file)
+            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) {
@@ -73,7 +71,7 @@ export function usePaste(context: LogseqContextValue) {
               id: assetId,
               type: 'image',
               src: dataurl,
-              size: await getSizeFromSrc(dataurl),
+              size: await getSizeFromSrc(handlers.makeAssetUrl(dataurl)),
             }
             assetsToCreate.push(asset)
           } catch (error) {
@@ -82,16 +80,6 @@ export function usePaste(context: LogseqContextValue) {
         }
       }
 
-      async function handleImage(item: ClipboardItem) {
-        const firstImageType = item.types.find(type => type.startsWith('image'))
-        if (firstImageType) {
-          const blob = await item.getType(firstImageType)
-          await handleBlobs([blob])
-          return true
-        }
-        return false
-      }
-
       async function handleHTML(item: ClipboardItem) {
         if (item.types.includes('text/html')) {
           const blob = await item.getType('text/html')
@@ -100,68 +88,75 @@ export function usePaste(context: LogseqContextValue) {
           shapesToCreate.push({
             ...HTMLShape.defaultProps,
             html: rawText,
-            parentId: app.currentPageId,
             point: [point[0], point[1]],
           })
           return true
         }
+        return false
+      }
 
+      async function handleTextPlain(item: ClipboardItem) {
         if (item.types.includes('text/plain')) {
           const blob = await item.getType('text/plain')
           const rawText = (await blob.text()).trim()
-          // if rawText is iframe text
-          if (rawText.startsWith('<iframe')) {
-            shapesToCreate.push({
-              ...HTMLShape.defaultProps,
-              html: rawText,
-              parentId: app.currentPageId,
-              point: [point[0], point[1]],
-            })
+
+          if (handleURL(rawText)) {
+            return true
+          }
+
+          if (handleIframe(rawText)) {
+            return true
+          }
+
+          if (handleTldrawShapes(rawText)) {
+            return true
+          }
+          if (await handleLogseqPortalShapes(rawText)) {
             return true
           }
         }
+
         return false
       }
 
-      async function handleLogseqShapes(item: ClipboardItem) {
-        if (item.types.includes('text/plain')) {
-          const blob = await item.getType('text/plain')
-          const rawText = (await blob.text()).trim()
-
-          try {
-            const data = JSON.parse(rawText)
-            if (data.type === 'logseq/whiteboard-shapes') {
-              const shapes = data.shapes as TLShapeModel[]
-              const commonBounds = BoundsUtils.getCommonBounds(
-                shapes.map(shape => ({
-                  minX: shape.point?.[0] ?? point[0],
-                  minY: shape.point?.[1] ?? point[1],
-                  width: shape.size?.[0] ?? 4,
-                  height: shape.size?.[1] ?? 4,
-                  maxX: (shape.point?.[0] ?? point[0]) + (shape.size?.[0] ?? 4),
-                  maxY: (shape.point?.[1] ?? point[1]) + (shape.size?.[1] ?? 4),
-                }))
-              )
-              const clonedShapes = shapes.map(shape => {
-                return {
-                  ...shape,
-                  parentId: app.currentPageId,
-                  point: [
-                    point[0] + shape.point![0] - commonBounds.minX,
-                    point[1] + shape.point![1] - commonBounds.minY,
-                  ],
-                }
-              })
-              // @ts-expect-error - This is a valid shape
-              shapesToCreate.push(...clonedShapes)
+      function handleTldrawShapes(rawText: string) {
+        try {
+          const data = JSON.parse(rawText)
+          if (data.type === 'logseq/whiteboard-shapes') {
+            debugger
+            const shapes = data.shapes as TLShapeModel[]
+            const commonBounds = BoundsUtils.getCommonBounds(
+              shapes.map(shape => ({
+                minX: shape.point?.[0] ?? point[0],
+                minY: shape.point?.[1] ?? point[1],
+                width: shape.size?.[0] ?? 4,
+                height: shape.size?.[1] ?? 4,
+                maxX: (shape.point?.[0] ?? point[0]) + (shape.size?.[0] ?? 4),
+                maxY: (shape.point?.[1] ?? point[1]) + (shape.size?.[1] ?? 4),
+              }))
+            )
+            const clonedShapes = shapes.map(shape => {
+              return {
+                ...shape,
+                point: [
+                  point[0] + shape.point![0] - commonBounds.minX,
+                  point[1] + shape.point![1] - commonBounds.minY,
+                ],
+              }
+            })
+            // @ts-expect-error - This is a valid shape
+            shapesToCreate.push(...clonedShapes)
 
-              // Try to rebinding the shapes to the new assets
-              shapesToCreate.forEach((s, idx) => {
-                if (s.handles) {
-                  Object.values(s.handles).forEach(h => {
-                    if (h.bindingId) {
-                      // try to bind the new shape
-                      const binding = app.currentPage.bindings[h.bindingId]
+            // Try to rebinding the shapes to the new assets
+            shapesToCreate.forEach((s, idx) => {
+              if (s.handles) {
+                Object.values(s.handles).forEach(h => {
+                  if (h.bindingId) {
+                    // try to bind the new shape
+                    const binding = app.currentPage.bindings[h.bindingId]
+                    // FIXME: if copy from a different whiteboard, the binding info
+                    // will not be available
+                    if (binding) {
                       // if the copied binding from/to is in the source
                       const oldFromIdx = shapes.findIndex(s => s.id === binding.fromId)
                       const oldToIdx = shapes.findIndex(s => s.id === binding.toId)
@@ -178,93 +173,112 @@ export function usePaste(context: LogseqContextValue) {
                         h.bindingId = undefined
                       }
                     }
-                  })
-                }
-              })
-              return true
-            }
-          } catch {
-            if (/^\(\(.*\)\)$/.test(rawText) && rawText.length === NIL_UUID.length + 4) {
-              const blockRef = rawText.slice(2, -2)
-              if (validUUID(blockRef)) {
-                shapesToCreate.push({
-                  ...LogseqPortalShape.defaultProps,
-                  parentId: app.currentPageId,
-                  point: [point[0], point[1]],
-                  size: [400, 0], // use 0 here to enable auto-resize
-                  pageId: blockRef,
-                  blockType: 'B',
+                  }
                 })
-                return true
               }
-            } else if (/^\[\[.*\]\]$/.test(rawText)) {
-              const pageName = rawText.slice(2, -2)
-              shapesToCreate.push({
-                ...LogseqPortalShape.defaultProps,
-                parentId: app.currentPageId,
-                point: [point[0], point[1]],
-                size: [400, 0], // use 0 here to enable auto-resize
-                pageId: pageName,
-                blockType: 'P',
-              })
-              return true
-            } else if (isValidURL(rawText)) {
-              const youtubeId = getYoutubeId(rawText)
-              if (youtubeId) {
-                shapesToCreate.push({
-                  ...YouTubeShape.defaultProps,
-                  embedId: youtubeId,
-                  parentId: app.currentPageId,
-                  point: [point[0], point[1]],
-                })
-                return true
-              }
-              // ??? deal with normal URLs?
-            }
-            const uuid = handlers?.addNewBlock(rawText)
-            if (uuid) {
-              // create text shape
-              shapesToCreate.push({
-                ...LogseqPortalShape.defaultProps,
-                id: uniqueId(),
-                parentId: app.currentPageId,
-                size: [400, 0], // use 0 here to enable auto-resize
-                point: [point[0], point[1]],
-                pageId: uuid,
-                blockType: 'B',
-                compact: true,
-              })
-              return true
-            }
+            })
+            return true
           }
+        } catch (err) {
+          console.error(err)
         }
         return false
       }
 
-      if (files && files.length > 0) {
-        await handleFiles(files)
-      } else {
-        for (const item of await navigator.clipboard.read()) {
-          try {
-            let handled = await handleImage(item)
+      function handleURL(rawText: string) {
+        if (isValidURL(rawText)) {
+          const getYoutubeId = (url: string) => {
+            const match = url.match(/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#&?]*).*/)
+            return match && match[2].length === 11 ? match[2] : null
+          }
+          const youtubeId = getYoutubeId(rawText)
+          if (youtubeId) {
+            shapesToCreate.push({
+              ...YouTubeShape.defaultProps,
+              embedId: youtubeId,
+              point: [point[0], point[1]],
+            })
+            return true
+          }
+          // ??? deal with normal URLs?
+        }
+        return false
+      }
 
-            if (!handled && !shiftKey) {
-              handled = await handleHTML(item)
-            }
+      function handleIframe(rawText: string) {
+        // if rawText is iframe text
+        if (rawText.startsWith('<iframe')) {
+          shapesToCreate.push({
+            ...HTMLShape.defaultProps,
+            html: rawText,
+            point: [point[0], point[1]],
+          })
+          return true
+        }
+        return false
+      }
+
+      async function handleLogseqPortalShapes(rawText: string) {
+        if (/^\(\(.*\)\)$/.test(rawText) && rawText.length === NIL_UUID.length + 4) {
+          const blockRef = rawText.slice(2, -2)
+          if (validUUID(blockRef)) {
+            shapesToCreate.push({
+              ...LogseqPortalShape.defaultProps,
+              point: [point[0], point[1]],
+              size: [400, 0], // use 0 here to enable auto-resize
+              pageId: blockRef,
+              blockType: 'B',
+            })
+            return true
+          }
+        } else if (/^\[\[.*\]\]$/.test(rawText)) {
+          const pageName = rawText.slice(2, -2)
+          shapesToCreate.push({
+            ...LogseqPortalShape.defaultProps,
+            point: [point[0], point[1]],
+            size: [400, 0], // use 0 here to enable auto-resize
+            pageId: pageName,
+            blockType: 'P',
+          })
+          return true
+        }
+
+        const uuid = handlers?.addNewBlock(rawText)
+        if (uuid) {
+          // create text shape
+          shapesToCreate.push({
+            ...LogseqPortalShape.defaultProps,
+            id: uniqueId(),
+            size: [400, 0], // use 0 here to enable auto-resize
+            point: [point[0], point[1]],
+            pageId: uuid,
+            blockType: 'B',
+            compact: true,
+          })
+          return true
+        }
+        return false
+      }
 
+      try {
+        if (files && files.length > 0) {
+          await handleFiles(files)
+        } else {
+          for (const item of await navigator.clipboard.read()) {
+            let handled = !shiftKey ? await handleHTML(item) : false
             if (!handled) {
-              await handleLogseqShapes(item)
+              await handleTextPlain(item)
             }
-          } catch (error) {
-            console.error(error)
           }
         }
+      } catch (error) {
+        console.error(error)
       }
 
       const allShapesToAdd: TLShapeModel[] = [
+        // assets to images
         ...assetsToCreate.map((asset, i) => ({
           type: 'image',
-          parentId: app.currentPageId,
           // TODO: Should be place near the last edited shape
           point: [point[0] - asset.size[0] / 2 + i * 16, point[1] - asset.size[1] / 2 + i * 16],
           size: asset.size,
@@ -275,6 +289,7 @@ export function usePaste(context: LogseqContextValue) {
       ].map(shape => {
         return {
           ...shape,
+          parentId: app.currentPageId,
           id: uniqueId(),
         }
       })

+ 2 - 0
tldraw/apps/tldraw-logseq/src/lib/logseq-context.ts

@@ -27,6 +27,8 @@ export interface LogseqContextValue {
     addNewBlock: (content: string) => string // returns the new block uuid
     queryBlockByUUID: (uuid: string) => any
     isWhiteboardPage: (pageName: string) => boolean
+    saveAsset: (file: File) => Promise<string>
+    makeAssetUrl: (relativeUrl: string) => string
   }
 }
 

+ 17 - 2
tldraw/apps/tldraw-logseq/src/lib/shapes/ImageShape.tsx

@@ -4,6 +4,7 @@ import { HTMLContainer, TLComponentProps } from '@tldraw/react'
 import { TLAsset, TLImageShape, TLImageShapeProps } from '@tldraw/core'
 import { observer } from 'mobx-react-lite'
 import type { CustomStyleProps } from './style-props'
+import { LogseqContext } from '~lib/logseq-context'
 
 export interface ImageShapeProps extends TLImageShapeProps, CustomStyleProps {
   type: 'image'
@@ -11,6 +12,16 @@ export interface ImageShapeProps extends TLImageShapeProps, CustomStyleProps {
   opacity: number
 }
 
+declare global {
+  interface Window {
+    logseq?: {
+      api?: {
+        make_asset_url?: (url: string) => string
+      }
+    }
+  }
+}
+
 export class ImageShape extends TLImageShape<ImageShapeProps> {
   static id = 'image'
 
@@ -44,12 +55,14 @@ export class ImageShape extends TLImageShape<ImageShapeProps> {
       ? clipping
       : [clipping, clipping, clipping, clipping]
 
+    const { handlers } = React.useContext(LogseqContext)
+
     return (
       <HTMLContainer {...events} opacity={isErasing ? 0.2 : opacity}>
         <div style={{ width: '100%', height: '100%', overflow: 'hidden' }}>
           {asset && (
             <img
-              src={asset.src}
+              src={handlers ? handlers.makeAssetUrl(asset.src) : asset.src}
               draggable={false}
               style={{
                 position: 'relative',
@@ -91,10 +104,12 @@ export class ImageShape extends TLImageShape<ImageShapeProps> {
         ? clipping
         : [clipping, clipping, clipping, clipping]
 
+      const make_asset_url = window.logseq?.api?.make_asset_url
+
       return (
         <foreignObject width={bounds.width} height={bounds.height}>
           <img
-            src={asset.src}
+            src={make_asset_url ? make_asset_url(asset.src) : asset.src}
             draggable={false}
             style={{
               position: 'relative',

+ 9 - 3
tldraw/demo/src/App.jsx

@@ -1,4 +1,4 @@
-import { uniqueId } from '@tldraw/core'
+import { uniqueId, fileToBase64 } from '@tldraw/core'
 import React from 'react'
 import ReactDOM from 'react-dom'
 import { App as TldrawApp } from 'tldraw-logseq'
@@ -136,6 +136,10 @@ const searchHandler = q => {
   })
 }
 
+const saveAssets = async files => {
+  return Promise.all(files.map(fileToBase64))
+}
+
 export default function App() {
   const [theme, setTheme] = React.useState('light')
 
@@ -152,8 +156,10 @@ export default function App() {
         handlers={{
           search: searchHandler,
           addNewBlock: () => uniqueId(),
-          queryBlockByUUID: (uuid) => ({uuid, content: 'some random content'}),
-          isWhiteboardPage: () => false
+          queryBlockByUUID: uuid => ({ uuid, content: 'some random content' }),
+          isWhiteboardPage: () => false,
+          saveAssets,
+          makeAssetUrl: a => a,
         }}
         model={documentModel}
         onPersist={onPersist}

+ 1 - 3
tldraw/packages/react/src/lib/TLReactApp.ts

@@ -2,6 +2,4 @@ import { TLApp } from '@tldraw/core'
 import type { TLReactShape } from './TLReactShape'
 import type { TLReactEventMap } from '~types'
 
-export class TLReactApp<S extends TLReactShape = TLReactShape> extends TLApp<S, TLReactEventMap> {
-  pubEvent?: any
-}
+export class TLReactApp<S extends TLReactShape = TLReactShape> extends TLApp<S, TLReactEventMap> {}