diff --git a/src/main/frontend/components/block.cljs b/src/main/frontend/components/block.cljs index f0252539615..9178064f724 100644 --- a/src/main/frontend/components/block.cljs +++ b/src/main/frontend/components/block.cljs @@ -1044,10 +1044,12 @@ (cond (util/electron?) [:a.asset-ref.is-pdf - {:on-mouse-down (fn [event] - (when-let [current (pdf-assets/inflate-asset s)] - (state/set-current-pdf! current) - (util/stop event)))} + {:on-click (fn [event] + (when-let [current (pdf-assets/inflate-asset s)] + (state/set-current-pdf! current) + (util/stop event))) + :draggable true + :on-drag-start #(.setData (gobj/get % "dataTransfer") "text" s)} (or label-text (->elem :span (map-inline config label)))] diff --git a/src/main/frontend/extensions/tldraw.cljs b/src/main/frontend/extensions/tldraw.cljs index 409fafcf6d8..d68b48382b1 100644 --- a/src/main/frontend/extensions/tldraw.cljs +++ b/src/main/frontend/extensions/tldraw.cljs @@ -7,6 +7,8 @@ [frontend.config :as config] [frontend.context.i18n :refer [t]] [frontend.db.model :as model] + [frontend.extensions.pdf.core :as pdf] + [frontend.extensions.pdf.assets :as pdf-assets] [frontend.handler.editor :as editor-handler] [frontend.handler.route :as route-handler] [frontend.handler.whiteboard :as whiteboard-handler] @@ -46,6 +48,12 @@ [props] (ui/tweet-embed (gobj/get props "tweetId"))) +(rum/defc pdf + [props] + (let [pdf-current (state/sub :pdf/current) + pdf (pdf-assets/inflate-asset (gobj/get props "src"))] + (pdf/pdf-container (or pdf-current pdf)))) + (rum/defc block-reference [props] (block/block-reference {} (gobj/get props "blockId") nil)) @@ -86,6 +94,7 @@ :Block block-cp :Breadcrumb breadcrumb :Tweet tweet + :Pdf pdf :PageName page-name-link :BacklinksCount references-count :BlockReference block-reference @@ -105,6 +114,7 @@ :isMobile util/mobile? :saveAsset save-asset-handler :makeAssetUrl editor-handler/make-asset-url + :setCurrentPdf (fn [src] (state/set-current-pdf! (if src (pdf-assets/inflate-asset src) nil))) :copyToClipboard (fn [text, html] (util/copy-to-clipboard! text :html html)) :getRedirectPageName (fn [page-name-or-uuid] (model/get-redirect-page-name page-name-or-uuid)) :insertFirstPageBlock (fn [page-name] (editor-handler/insert-first-page-block-if-not-exists! page-name {:redirect? false})) diff --git a/tldraw/apps/tldraw-logseq/src/components/ContextBar/contextBarActionFactory.tsx b/tldraw/apps/tldraw-logseq/src/components/ContextBar/contextBarActionFactory.tsx index d560f53840c..7c03c69e6e1 100644 --- a/tldraw/apps/tldraw-logseq/src/components/ContextBar/contextBarActionFactory.tsx +++ b/tldraw/apps/tldraw-logseq/src/components/ContextBar/contextBarActionFactory.tsx @@ -76,6 +76,7 @@ export const shapeMapping: Record = { html: ['ScaleLevel', 'AutoResizing', 'Links'], image: ['Links'], video: ['Links'], + pdf: ['Links'], } export const withFillShapes = Object.entries(shapeMapping) diff --git a/tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts b/tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts index 1c7d2ac5705..cfcf4ca9fec 100644 --- a/tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts +++ b/tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts @@ -17,6 +17,7 @@ import { HTMLShape, IFrameShape, ImageShape, + PdfShape, LogseqPortalShape, VideoShape, YouTubeShape, @@ -36,27 +37,25 @@ const isValidURL = (url: string) => { } } -interface VideoImageAsset extends TLAsset { +interface Asset extends TLAsset { size?: number[] } -const IMAGE_EXTENSIONS = ['.png', '.svg', '.jpg', '.jpeg', '.gif'] -const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.ogg'] +const assetExtensions = { + 'image': ['.png', '.svg', '.jpg', '.jpeg', '.gif'], + 'video': ['.mp4', '.webm', '.ogg'], + 'pdf': ['.pdf'] +} function getFileType(filename: string) { // Get extension, verify that it's an image const extensionMatch = filename.match(/\.[0-9a-z]+$/i) - if (!extensionMatch) { - return 'unknown' - } + if (!extensionMatch) return 'unknown' const extension = extensionMatch[0].toLowerCase() - if (IMAGE_EXTENSIONS.includes(extension)) { - return 'image' - } - if (VIDEO_EXTENSIONS.includes(extension)) { - return 'video' - } - return 'unknown' + + const [type, _extensions] = Object.entries(assetExtensions).find(([_type, extensions]) => extensions.includes(extension)) ?? ['unknown', null] + + return type } type MaybeShapes = TLShapeModel[] | null | undefined @@ -96,23 +95,23 @@ const handleCreatingShapes = async ( { point, shiftKey, dataTransfer, fromDrop }: TLPasteEventInfo, handlers: LogseqContextValue['handlers'] ) => { - let imageAssetsToCreate: VideoImageAsset[] = [] + let imageAssetsToCreate: Asset[] = [] let assetsToClone: TLAsset[] = [] const bindingsToCreate: TLBinding[] = [] - async function createAssetsFromURL(url: string, isVideo: boolean): Promise { + async function createAssetsFromURL(url: string, type: string): Promise { // Do we already have an asset for this image? const existingAsset = Object.values(app.assets).find(asset => asset.src === url) if (existingAsset) { - return existingAsset as VideoImageAsset + return existingAsset as Asset } // Create a new asset for this image - const asset: VideoImageAsset = { + const asset: Asset = { id: uniqueId(), - type: isVideo ? 'video' : 'image', + type: type, src: url, - size: await getSizeFromSrc(handlers.makeAssetUrl(url), isVideo), + size: await getSizeFromSrc(handlers.makeAssetUrl(url), type), } return asset } @@ -123,7 +122,7 @@ const handleCreatingShapes = async ( .map(async file => { try { const dataurl = await handlers.saveAsset(file) - return await createAssetsFromURL(dataurl, getFileType(file.name) === 'video') + return await createAssetsFromURL(dataurl, getFileType(file.name)) } catch (err) { console.error(err) } @@ -175,8 +174,22 @@ const handleCreatingShapes = async ( imageAssetsToCreate = assets return assets.map((asset, i) => { - const defaultProps = - asset.type === 'video' ? VideoShape.defaultProps : ImageShape.defaultProps + let defaultProps = null + + switch (asset.type) { + case 'video': + defaultProps = VideoShape.defaultProps + break + case 'image': + defaultProps = ImageShape.defaultProps + break + case 'pdf': + defaultProps = PdfShape.defaultProps + break + default: + return null + } + const newShape = { ...defaultProps, id: uniqueId(), diff --git a/tldraw/apps/tldraw-logseq/src/lib/logseq-context.ts b/tldraw/apps/tldraw-logseq/src/lib/logseq-context.ts index 70c421b8a52..e47bcc44b54 100644 --- a/tldraw/apps/tldraw-logseq/src/lib/logseq-context.ts +++ b/tldraw/apps/tldraw-logseq/src/lib/logseq-context.ts @@ -22,6 +22,9 @@ export interface LogseqContextValue { Tweet: React.FC<{ tweetId: string }> + Pdf: React.FC<{ + src: string + }> PageName: React.FC<{ pageName: string }> @@ -57,7 +60,8 @@ export interface LogseqContextValue { isWhiteboardPage: (pageName: string) => boolean isMobile: () => boolean saveAsset: (file: File) => Promise - makeAssetUrl: (relativeUrl: string) => string + makeAssetUrl: (relativeUrl: string | null) => string + setCurrentPdf: (src: string | null) => void sidebarAddBlock: (uuid: string, type: 'block' | 'page') => void redirectToPage: (uuidOrPageName: string) => void copyToClipboard: (text: string, html: string) => void diff --git a/tldraw/apps/tldraw-logseq/src/lib/shapes/PdfShape.tsx b/tldraw/apps/tldraw-logseq/src/lib/shapes/PdfShape.tsx new file mode 100644 index 00000000000..a337b656eab --- /dev/null +++ b/tldraw/apps/tldraw-logseq/src/lib/shapes/PdfShape.tsx @@ -0,0 +1,93 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import * as React from 'react' +import { TLBoxShape, TLBoxShapeProps } from '@tldraw/core' +import { HTMLContainer, TLComponentProps, useApp } from '@tldraw/react' +import { observer } from 'mobx-react-lite' +import { LogseqContext } from '../logseq-context' +import { useCameraMovingRef } from '../../hooks/useCameraMoving' + +export interface PdfShapeProps extends TLBoxShapeProps { + type: 'pdf' + assetId: string +} + +export class PdfShape extends TLBoxShape { + static id = 'pdf' + frameRef = React.createRef() + + static defaultProps: PdfShapeProps = { + id: 'pdf', + type: 'pdf', + parentId: 'page', + point: [0, 0], + size: [853, 480], + assetId: '', + } + + canChangeAspectRatio = true + canFlip = true + canEdit = true + + ReactComponent = observer(({ events, asset, isErasing, isEditing, isSelected }: TLComponentProps) => { + const ref = React.useRef(null) + const { + renderers: { Pdf }, + handlers, + } = React.useContext(LogseqContext) + const app = useApp() + + const isMoving = useCameraMovingRef() + + React.useEffect(() => { + if (asset && isEditing) { + // handlers.setCurrentPdf(handlers.makeAssetUrl(asset.src)) + } + }, [isEditing]) + + return ( + +
+ {asset ? ( + + ) : null} +
+
+ ) + }) + + ReactIndicator = observer(() => { + const { + props: { + size: [w, h], + isLocked, + }, + } = this + return ( + + ) + }) +} diff --git a/tldraw/apps/tldraw-logseq/src/lib/shapes/index.ts b/tldraw/apps/tldraw-logseq/src/lib/shapes/index.ts index 362214ec363..f89a9d59337 100644 --- a/tldraw/apps/tldraw-logseq/src/lib/shapes/index.ts +++ b/tldraw/apps/tldraw-logseq/src/lib/shapes/index.ts @@ -14,6 +14,7 @@ import { TextShape } from './TextShape' import { VideoShape } from './VideoShape' import { YouTubeShape } from './YouTubeShape' import { TweetShape } from './TweetShape' +import { PdfShape } from './PdfShape' export type Shape = // | PenShape @@ -30,6 +31,7 @@ export type Shape = | YouTubeShape | TweetShape | IFrameShape + | PdfShape | HTMLShape | LogseqPortalShape | GroupShape @@ -49,6 +51,7 @@ export * from './TextShape' export * from './VideoShape' export * from './YouTubeShape' export * from './TweetShape' +export * from './PdfShape' export const shapes: TLReactShapeConstructor[] = [ // DotShape, @@ -65,6 +68,7 @@ export const shapes: TLReactShapeConstructor[] = [ TweetShape, IFrameShape, HTMLShape, + PdfShape, LogseqPortalShape, GroupShape, ] diff --git a/tldraw/apps/tldraw-logseq/src/styles.css b/tldraw/apps/tldraw-logseq/src/styles.css index 07b3db1c515..f877f2c1b1c 100644 --- a/tldraw/apps/tldraw-logseq/src/styles.css +++ b/tldraw/apps/tldraw-logseq/src/styles.css @@ -1183,3 +1183,15 @@ button.tl-shape-links-panel-item-remove-button { opacity: 1; } } + +.tl-pdf-container { + .extensions__pdf-container { + position: static !important; + width: 100% !important; + height: 100% !important; + } + + .extensions__pdf-header { + display: none; + } +} diff --git a/tldraw/packages/core/src/utils/DataUtils.ts b/tldraw/packages/core/src/utils/DataUtils.ts index 029dce090af..8ee384379ab 100644 --- a/tldraw/packages/core/src/utils/DataUtils.ts +++ b/tldraw/packages/core/src/utils/DataUtils.ts @@ -76,9 +76,9 @@ export function fileToBase64(file: Blob): Promise { }) } -export function getSizeFromSrc(dataURL: string, isVideo: boolean): Promise { +export function getSizeFromSrc(dataURL: string, type: string): Promise { return new Promise((resolve, reject) => { - if (isVideo) { + if (type === 'video') { const video = document.createElement('video') // place a listener on it @@ -96,11 +96,13 @@ export function getSizeFromSrc(dataURL: string, isVideo: boolean): Promise resolve([img.width, img.height]) img.src = dataURL img.onerror = err => reject(err) + } else if(type === 'pdf'){ + resolve([595, 842]) // A4 portrait dimensions at 72 ppi } }) }