Skip to content

Commit

Permalink
wip: pdf shape
Browse files Browse the repository at this point in the history
  • Loading branch information
sprocketc committed Jun 15, 2023
1 parent bc57766 commit fded58c
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 30 deletions.
10 changes: 6 additions & 4 deletions src/main/frontend/components/block.cljs
Expand Up @@ -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)))]

Expand Down
10 changes: 10 additions & 0 deletions src/main/frontend/extensions/tldraw.cljs
Expand Up @@ -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]
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -86,6 +94,7 @@
:Block block-cp
:Breadcrumb breadcrumb
:Tweet tweet
:Pdf pdf
:PageName page-name-link
:BacklinksCount references-count
:BlockReference block-reference
Expand All @@ -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}))
Expand Down
Expand Up @@ -76,6 +76,7 @@ export const shapeMapping: Record<ShapeType, ContextBarActionType[]> = {
html: ['ScaleLevel', 'AutoResizing', 'Links'],
image: ['Links'],
video: ['Links'],
pdf: ['Links'],
}

export const withFillShapes = Object.entries(shapeMapping)
Expand Down
57 changes: 35 additions & 22 deletions tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts
Expand Up @@ -17,6 +17,7 @@ import {
HTMLShape,
IFrameShape,
ImageShape,
PdfShape,
LogseqPortalShape,
VideoShape,
YouTubeShape,
Expand All @@ -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
Expand Down Expand Up @@ -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<VideoImageAsset> {
async function createAssetsFromURL(url: string, type: string): Promise<Asset> {
// 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
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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(),
Expand Down
6 changes: 5 additions & 1 deletion tldraw/apps/tldraw-logseq/src/lib/logseq-context.ts
Expand Up @@ -22,6 +22,9 @@ export interface LogseqContextValue {
Tweet: React.FC<{
tweetId: string
}>
Pdf: React.FC<{
src: string
}>
PageName: React.FC<{
pageName: string
}>
Expand Down Expand Up @@ -57,7 +60,8 @@ export interface LogseqContextValue {
isWhiteboardPage: (pageName: string) => boolean
isMobile: () => boolean
saveAsset: (file: File) => Promise<string>
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
Expand Down
93 changes: 93 additions & 0 deletions 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<PdfShapeProps> {
static id = 'pdf'
frameRef = React.createRef<HTMLElement>()

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<HTMLElement>(null)
const {
renderers: { Pdf },
handlers,
} = React.useContext(LogseqContext)
const app = useApp<Shape>()

const isMoving = useCameraMovingRef()

React.useEffect(() => {
if (asset && isEditing) {
// handlers.setCurrentPdf(handlers.makeAssetUrl(asset.src))
}
}, [isEditing])

return (
<HTMLContainer
style={{
overflow: 'hidden',
pointerEvents: 'all',
opacity: isErasing ? 0.2 : 1,
}}
{...events}
>
<div
className="relative tl-pdf-container"
onWheelCapture={stop}
onPointerDown={stop}
onPointerUp={stop}
style={{
width: '100%',
height: '100%',
pointerEvents: !isMoving && (isEditing || isSelected) ? 'all' : 'none',
}}
>
{asset ? (
<Pdf src={handlers ? handlers.makeAssetUrl(asset.src) : asset.src} />
) : null}
</div>
</HTMLContainer>
)
})

ReactIndicator = observer(() => {
const {
props: {
size: [w, h],
isLocked,
},
} = this
return (
<rect
width={w}
height={h}
fill="transparent"
rx={8}
ry={8}
strokeDasharray={isLocked ? '8 2' : 'undefined'}
/>
)
})
}
4 changes: 4 additions & 0 deletions tldraw/apps/tldraw-logseq/src/lib/shapes/index.ts
Expand Up @@ -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
Expand All @@ -30,6 +31,7 @@ export type Shape =
| YouTubeShape
| TweetShape
| IFrameShape
| PdfShape
| HTMLShape
| LogseqPortalShape
| GroupShape
Expand All @@ -49,6 +51,7 @@ export * from './TextShape'
export * from './VideoShape'
export * from './YouTubeShape'
export * from './TweetShape'
export * from './PdfShape'

export const shapes: TLReactShapeConstructor<Shape>[] = [
// DotShape,
Expand All @@ -65,6 +68,7 @@ export const shapes: TLReactShapeConstructor<Shape>[] = [
TweetShape,
IFrameShape,
HTMLShape,
PdfShape,
LogseqPortalShape,
GroupShape,
]
Expand Down
12 changes: 12 additions & 0 deletions tldraw/apps/tldraw-logseq/src/styles.css
Expand Up @@ -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;
}
}
8 changes: 5 additions & 3 deletions tldraw/packages/core/src/utils/DataUtils.ts
Expand Up @@ -76,9 +76,9 @@ export function fileToBase64(file: Blob): Promise<string | ArrayBuffer | null> {
})
}

export function getSizeFromSrc(dataURL: string, isVideo: boolean): Promise<number[]> {
export function getSizeFromSrc(dataURL: string, type: string): Promise<number[]> {
return new Promise((resolve, reject) => {
if (isVideo) {
if (type === 'video') {
const video = document.createElement('video')

// place a listener on it
Expand All @@ -96,11 +96,13 @@ export function getSizeFromSrc(dataURL: string, isVideo: boolean): Promise<numbe
)
// start download meta-datas
video.src = dataURL
} else {
} else if (type === 'image') {
const img = new Image()
img.onload = () => 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
}
})
}
Expand Down

0 comments on commit fded58c

Please sign in to comment.