diff --git a/tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts b/tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts index a4f11f03dc..4556ac549c 100644 --- a/tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts +++ b/tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts @@ -10,7 +10,7 @@ import { import type { TLReactCallbacks } from '@tldraw/react' import * as React from 'react' import { NIL as NIL_UUID } from 'uuid' -import { HTMLShape, LogseqPortalShape, Shape, YouTubeShape } from '~lib' +import { HTMLShape, LogseqPortalShape, Shape, YouTubeShape, ImageShape, VideoShape } from '~lib' import type { LogseqContextValue } from '~lib/logseq-context' const isValidURL = (url: string) => { @@ -28,11 +28,11 @@ export function usePaste(context: LogseqContextValue) { return React.useCallback['onPaste']>( async (app, { point, shiftKey, files }) => { const assetId = uniqueId() - interface ImageAsset extends TLAsset { + interface VideoImageAsset extends TLAsset { size: number[] } - const assetsToCreate: ImageAsset[] = [] + const assetsToCreate: VideoImageAsset[] = [] const shapesToCreate: Shape['props'][] = [] const bindingsToCreate: TLBinding[] = [] @@ -43,6 +43,7 @@ export function usePaste(context: LogseqContextValue) { // TODO: handle PDF? async function handleFiles(files: File[]) { const IMAGE_EXTENSIONS = ['.png', '.svg', '.jpg', '.jpeg', '.gif'] + const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.ogg'] for (const file of files) { // Get extension, verify that it's an image @@ -51,9 +52,10 @@ export function usePaste(context: LogseqContextValue) { continue } const extension = extensionMatch[0].toLowerCase() - if (!IMAGE_EXTENSIONS.includes(extension)) { + if (![...IMAGE_EXTENSIONS, ...VIDEO_EXTENSIONS].includes(extension)) { continue } + const isVideo = VIDEO_EXTENSIONS.includes(extension) try { // Turn the image into a base64 dataurl const dataurl = await createAsset(file) @@ -63,16 +65,17 @@ export function usePaste(context: LogseqContextValue) { // Do we already have an asset for this image? const existingAsset = Object.values(app.assets).find(asset => asset.src === dataurl) if (existingAsset) { - assetsToCreate.push(existingAsset as ImageAsset) + assetsToCreate.push(existingAsset as VideoImageAsset) continue } // Create a new asset for this image - const asset: ImageAsset = { + const asset: VideoImageAsset = { id: assetId, - type: 'image', + type: isVideo ? 'video' : 'image', src: dataurl, - size: await getSizeFromSrc(handlers.makeAssetUrl(dataurl)), + size: await getSizeFromSrc(handlers.makeAssetUrl(dataurl), isVideo), } + console.log(asset) assetsToCreate.push(asset) } catch (error) { console.error(error) @@ -277,7 +280,7 @@ export function usePaste(context: LogseqContextValue) { const allShapesToAdd: TLShapeModel[] = [ // assets to images ...assetsToCreate.map((asset, i) => ({ - type: 'image', + ...(asset.type === 'video' ? VideoShape : ImageShape).defaultProps, // 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, diff --git a/tldraw/apps/tldraw-logseq/src/lib/shapes/HTMLShape.tsx b/tldraw/apps/tldraw-logseq/src/lib/shapes/HTMLShape.tsx index 183c65a903..3795bd7718 100644 --- a/tldraw/apps/tldraw-logseq/src/lib/shapes/HTMLShape.tsx +++ b/tldraw/apps/tldraw-logseq/src/lib/shapes/HTMLShape.tsx @@ -79,7 +79,7 @@ export class HTMLShape extends TLBoxShape { onPointerUp={stop} className="tl-html-container" style={{ - pointerEvents: isEditing ? 'all' : 'none', + pointerEvents: !isMoving && (isEditing || isSelected) ? 'all' : 'none', overflow: isEditing ? 'auto' : 'hidden', }} > diff --git a/tldraw/apps/tldraw-logseq/src/lib/shapes/VideoShape.tsx b/tldraw/apps/tldraw-logseq/src/lib/shapes/VideoShape.tsx new file mode 100644 index 0000000000..43b3c86eb9 --- /dev/null +++ b/tldraw/apps/tldraw-logseq/src/lib/shapes/VideoShape.tsx @@ -0,0 +1,101 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import * as React from 'react' +import { HTMLContainer, TLComponentProps, useApp } from '@tldraw/react' +import { TLAsset, TLBoxShape, TLBoxShapeProps, TLImageShape, TLImageShapeProps } from '@tldraw/core' +import { observer } from 'mobx-react-lite' +import type { CustomStyleProps } from './style-props' +import { useCameraMovingRef } from '~hooks/useCameraMoving' +import type { Shape } from '~lib' +import { LogseqContext } from '~lib/logseq-context' + +export interface VideoShapeProps extends TLBoxShapeProps, CustomStyleProps { + type: 'video' + assetId: string + opacity: number +} + +export class VideoShape extends TLBoxShape { + static id = 'video' + + static defaultProps: VideoShapeProps = { + id: 'video1', + parentId: 'page', + type: 'video', + point: [0, 0], + size: [100, 100], + stroke: '#000000', + fill: '#ffffff', + strokeWidth: 2, + opacity: 1, + assetId: '', + clipping: 0, + isAspectRatioLocked: true, + } + + canFlip = false + canEdit = true + canChangeAspectRatio = false + + ReactComponent = observer(({ events, isErasing, asset, isEditing }: TLComponentProps) => { + const { + props: { + opacity, + size: [w, h], + }, + } = this + + const isMoving = useCameraMovingRef() + const app = useApp() + + const isSelected = app.selectedIds.has(this.id) + + const tlEventsEnabled = + isMoving || (isSelected && !isEditing) || app.selectedTool.id !== 'select' + const stop = React.useCallback( + e => { + if (!tlEventsEnabled) { + // TODO: pinching inside Logseq Shape issue + e.stopPropagation() + } + }, + [tlEventsEnabled] + ) + + const { handlers } = React.useContext(LogseqContext) + + return ( + +
+ {asset && ( +
+
+ ) + }) + + ReactIndicator = observer(() => { + const { + props: { + size: [w, h], + }, + } = 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 56f4ec5fa5..b0c2ff4961 100644 --- a/tldraw/apps/tldraw-logseq/src/lib/shapes/index.ts +++ b/tldraw/apps/tldraw-logseq/src/lib/shapes/index.ts @@ -5,6 +5,7 @@ import { EllipseShape } from './EllipseShape' import { HighlighterShape } from './HighlighterShape' import { HTMLShape } from './HTMLShape' import { ImageShape } from './ImageShape' +import { VideoShape } from './VideoShape' import { LineShape } from './LineShape' import { LogseqPortalShape } from './LogseqPortalShape' import { PencilShape } from './PencilShape' @@ -18,7 +19,7 @@ export type Shape = | EllipseShape | HighlighterShape | ImageShape - | LineShape + | VideoShape | LineShape | PencilShape | PolygonShape @@ -33,6 +34,7 @@ export * from './EllipseShape' export * from './HighlighterShape' export * from './HTMLShape' export * from './ImageShape' +export * from './VideoShape' export * from './LineShape' export * from './LogseqPortalShape' export * from './PencilShape' @@ -40,12 +42,14 @@ export * from './PolygonShape' export * from './TextShape' export * from './YouTubeShape' + export const shapes: TLReactShapeConstructor[] = [ BoxShape, DotShape, EllipseShape, HighlighterShape, ImageShape, + VideoShape, LineShape, PencilShape, PolygonShape, diff --git a/tldraw/apps/tldraw-logseq/src/styles.css b/tldraw/apps/tldraw-logseq/src/styles.css index 4bfea0da77..18ef4b5faf 100644 --- a/tldraw/apps/tldraw-logseq/src/styles.css +++ b/tldraw/apps/tldraw-logseq/src/styles.css @@ -170,7 +170,7 @@ transform: translateX(3px); will-change: transform; } - + &[data-state='checked'] .switch-input-thumb { transform: translateX(17px); } @@ -581,7 +581,7 @@ } .tl-html-container { - @apply h-full w-full m-0 relative flex; + @apply h-full w-full m-0 relative; user-select: text; > iframe { @@ -590,6 +590,14 @@ } } +.tl-video-container { + @apply h-full w-full m-0 relative; + + > video { + @apply h-full w-full m-0 relative; + } +} + .tl-logseq-cp-container { @apply h-full w-full rounded-lg; diff --git a/tldraw/demo/src/App.jsx b/tldraw/demo/src/App.jsx index 9cf21bf319..157602f2a5 100644 --- a/tldraw/demo/src/App.jsx +++ b/tldraw/demo/src/App.jsx @@ -136,10 +136,6 @@ const searchHandler = q => { }) } -const saveAssets = async files => { - return Promise.all(files.map(fileToBase64)) -} - export default function App() { const [theme, setTheme] = React.useState('light') @@ -158,7 +154,7 @@ export default function App() { addNewBlock: () => uniqueId(), queryBlockByUUID: uuid => ({ uuid, content: 'some random content' }), isWhiteboardPage: () => false, - saveAssets, + saveAsset: fileToBase64, makeAssetUrl: a => a, }} model={documentModel} diff --git a/tldraw/packages/core/src/types/types.ts b/tldraw/packages/core/src/types/types.ts index a4bb5bbdd5..b2d5a7e24b 100644 --- a/tldraw/packages/core/src/types/types.ts +++ b/tldraw/packages/core/src/types/types.ts @@ -105,7 +105,7 @@ export interface TLOffset { export interface TLAsset { id: string - type: any + type: string src: string } diff --git a/tldraw/packages/core/src/utils/DataUtils.ts b/tldraw/packages/core/src/utils/DataUtils.ts index b39f00413b..dfda35a35e 100644 --- a/tldraw/packages/core/src/utils/DataUtils.ts +++ b/tldraw/packages/core/src/utils/DataUtils.ts @@ -76,11 +76,31 @@ export function fileToBase64(file: Blob): Promise { }) } -export function getSizeFromSrc(dataURL: string): Promise { +export function getSizeFromSrc(dataURL: string, isVideo: boolean): Promise { return new Promise(resolve => { - const img = new Image() - img.onload = () => resolve([img.width, img.height]) - img.src = dataURL + if (isVideo) { + const video = document.createElement('video') + + // place a listener on it + video.addEventListener( + 'loadedmetadata', + function () { + // retrieve dimensions + const height = this.videoHeight + const width = this.videoWidth + + // send back result + resolve([width, height]) + }, + false + ) + // start download meta-datas + video.src = dataURL + } else { + const img = new Image() + img.onload = () => resolve([img.width, img.height]) + img.src = dataURL + } }) }