mirror of
https://github.com/logseq/logseq.git
synced 2026-05-24 12:44:22 +00:00
feat: pasting video
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
}}
|
||||
>
|
||||
|
||||
101
tldraw/apps/tldraw-logseq/src/lib/shapes/VideoShape.tsx
Normal file
101
tldraw/apps/tldraw-logseq/src/lib/shapes/VideoShape.tsx
Normal 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" />
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -105,7 +105,7 @@ export interface TLOffset {
|
||||
|
||||
export interface TLAsset {
|
||||
id: string
|
||||
type: any
|
||||
type: string
|
||||
src: string
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user