feat: pasting video

This commit is contained in:
Peng Xiao
2022-08-18 17:09:10 +08:00
parent 7ea559894f
commit e07f43e23e
8 changed files with 155 additions and 23 deletions

View File

@@ -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<TLReactCallbacks<Shape>['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,

View File

@@ -79,7 +79,7 @@ export class HTMLShape extends TLBoxShape<HTMLShapeProps> {
onPointerUp={stop}
className="tl-html-container"
style={{
pointerEvents: isEditing ? 'all' : 'none',
pointerEvents: !isMoving && (isEditing || isSelected) ? 'all' : 'none',
overflow: isEditing ? 'auto' : 'hidden',
}}
>

View File

@@ -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<VideoShapeProps> {
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<Shape>()
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 (
<HTMLContainer
style={{
overflow: 'hidden',
pointerEvents: 'all',
opacity: isErasing ? 0.2 : opacity,
}}
{...events}
>
<div
onWheelCapture={stop}
onPointerDown={stop}
onPointerUp={stop}
className="tl-video-container"
style={{
pointerEvents: !isMoving && (isEditing || isSelected) ? 'all' : 'none',
overflow: isEditing ? 'auto' : 'hidden',
}}
>
{asset && (
<video controls src={handlers ? handlers.makeAssetUrl(asset.src) : asset.src} />
)}
</div>
</HTMLContainer>
)
})
ReactIndicator = observer(() => {
const {
props: {
size: [w, h],
},
} = this
return <rect width={w} height={h} fill="transparent" />
})
}

View File

@@ -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<Shape>[] = [
BoxShape,
DotShape,
EllipseShape,
HighlighterShape,
ImageShape,
VideoShape,
LineShape,
PencilShape,
PolygonShape,

View File

@@ -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;

View File

@@ -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}

View File

@@ -105,7 +105,7 @@ export interface TLOffset {
export interface TLAsset {
id: string
type: any
type: string
src: string
}

View File

@@ -76,11 +76,31 @@ export function fileToBase64(file: Blob): Promise<string | ArrayBuffer | null> {
})
}
export function getSizeFromSrc(dataURL: string): Promise<number[]> {
export function getSizeFromSrc(dataURL: string, isVideo: boolean): Promise<number[]> {
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
}
})
}