Update frontend with Tailwind.
- Replace inline CSS with Tailwind classes - Improve page layout and scaling - Add icons to ControlBar - Small refactor of play/pause logic - Add basic (not by any means final) colours
This commit is contained in:
parent
ec3ac8996d
commit
a33057651d
|
@ -3,6 +3,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@heroicons/react": "^1.0.5",
|
||||||
"@improbable-eng/grpc-web": "^0.14.1",
|
"@improbable-eng/grpc-web": "^0.14.1",
|
||||||
"@testing-library/jest-dom": "^5.11.4",
|
"@testing-library/jest-dom": "^5.11.4",
|
||||||
"@testing-library/react": "^11.1.0",
|
"@testing-library/react": "^11.1.0",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"short_name": "React App",
|
"short_name": "Clipper",
|
||||||
"name": "Create React App Sample",
|
"name": "Clipper",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"src": "favicon.ico",
|
||||||
|
@ -20,6 +20,7 @@
|
||||||
],
|
],
|
||||||
"start_url": ".",
|
"start_url": ".",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
|
"orientation": "landscape",
|
||||||
"theme_color": "#000000",
|
"theme_color": "#000000",
|
||||||
"background_color": "#ffffff"
|
"background_color": "#ffffff"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
body {
|
|
||||||
background-color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.App {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
|
@ -13,15 +13,15 @@ import { Overview, CanvasLogicalWidth } from './Overview';
|
||||||
import { Waveform } from './Waveform';
|
import { Waveform } from './Waveform';
|
||||||
import { ControlBar } from './ControlBar';
|
import { ControlBar } from './ControlBar';
|
||||||
import { SeekBar } from './SeekBar';
|
import { SeekBar } from './SeekBar';
|
||||||
import './App.css';
|
|
||||||
import { firstValueFrom, from, Observable } from 'rxjs';
|
import { firstValueFrom, from, Observable } from 'rxjs';
|
||||||
import { first, map } from 'rxjs/operators';
|
import { first, map } from 'rxjs/operators';
|
||||||
import millisFromDuration from './helpers/millisFromDuration';
|
import millisFromDuration from './helpers/millisFromDuration';
|
||||||
import { zoomViewportIn, zoomViewportOut } from './helpers/zoom';
|
import { zoomViewportIn, zoomViewportOut } from './helpers/zoom';
|
||||||
|
|
||||||
|
import { ExternalLinkIcon } from '@heroicons/react/solid';
|
||||||
|
|
||||||
// ported from backend, where should they live?
|
// ported from backend, where should they live?
|
||||||
const thumbnailWidth = 177;
|
const thumbnailWidth = 177; // height 100
|
||||||
const thumbnailHeight = 100;
|
|
||||||
|
|
||||||
const initialViewportCanvasPixels = 100;
|
const initialViewportCanvasPixels = 100;
|
||||||
|
|
||||||
|
@ -40,6 +40,11 @@ export interface VideoPosition {
|
||||||
percent: number;
|
percent: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum PlayState {
|
||||||
|
Paused,
|
||||||
|
Playing,
|
||||||
|
}
|
||||||
|
|
||||||
const video = document.createElement('video');
|
const video = document.createElement('video');
|
||||||
const audio = document.createElement('audio');
|
const audio = document.createElement('audio');
|
||||||
|
|
||||||
|
@ -50,6 +55,7 @@ function App(): JSX.Element {
|
||||||
const [overviewPeaks, setOverviewPeaks] = useState<Observable<number[]>>(
|
const [overviewPeaks, setOverviewPeaks] = useState<Observable<number[]>>(
|
||||||
from([])
|
from([])
|
||||||
);
|
);
|
||||||
|
const [playState, setPlayState] = useState(PlayState.Paused);
|
||||||
|
|
||||||
// position stores the current playback position. positionRef makes it
|
// position stores the current playback position. positionRef makes it
|
||||||
// available inside a setInterval callback.
|
// available inside a setInterval callback.
|
||||||
|
@ -98,7 +104,7 @@ function App(): JSX.Element {
|
||||||
currentTimeToFrame(position.currentTime) < selection.end &&
|
currentTimeToFrame(position.currentTime) < selection.end &&
|
||||||
currentTimeToFrame(currTime) >= selection.end
|
currentTimeToFrame(currTime) >= selection.end
|
||||||
) {
|
) {
|
||||||
handlePause();
|
pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the current position
|
// update the current position
|
||||||
|
@ -114,7 +120,7 @@ function App(): JSX.Element {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.addEventListener('keypress', handleKeyPress);
|
document.addEventListener('keypress', handleKeyPress);
|
||||||
return () => document.removeEventListener('keypress', handleKeyPress);
|
return () => document.removeEventListener('keypress', handleKeyPress);
|
||||||
}, [selection]);
|
}, [playState]);
|
||||||
|
|
||||||
// load audio when MediaSet is loaded:
|
// load audio when MediaSet is loaded:
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -189,11 +195,7 @@ function App(): JSX.Element {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audio.paused) {
|
handleTogglePlay();
|
||||||
handlePlay();
|
|
||||||
} else {
|
|
||||||
handlePause();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// handler called when the selection in the overview (zoom setting) is changed.
|
// handler called when the selection in the overview (zoom setting) is changed.
|
||||||
|
@ -227,18 +229,30 @@ function App(): JSX.Element {
|
||||||
video.currentTime = currentTime;
|
video.currentTime = currentTime;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePlay = () => {
|
const handleTogglePlay = () => {
|
||||||
audio.play();
|
if (playState == PlayState.Paused) {
|
||||||
video.play();
|
play();
|
||||||
|
} else {
|
||||||
|
pause();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePause = () => {
|
const play = () => {
|
||||||
|
audio.play();
|
||||||
|
video.play();
|
||||||
|
|
||||||
|
setPlayState(PlayState.Playing);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pause = () => {
|
||||||
video.pause();
|
video.pause();
|
||||||
audio.pause();
|
audio.pause();
|
||||||
|
|
||||||
if (selection.start != selection.end) {
|
if (selection.start != selection.end) {
|
||||||
setPositionFromFrame(selection.start);
|
setPositionFromFrame(selection.start);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPlayState(PlayState.Paused);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClip = () => {
|
const handleClip = () => {
|
||||||
|
@ -370,17 +384,8 @@ function App(): JSX.Element {
|
||||||
|
|
||||||
// render component
|
// render component
|
||||||
|
|
||||||
const containerStyles = {
|
|
||||||
border: '1px solid black',
|
|
||||||
width: '90%',
|
|
||||||
margin: '1em auto',
|
|
||||||
minHeight: '500px',
|
|
||||||
height: '700px',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
} as React.CSSProperties;
|
|
||||||
|
|
||||||
const offsetPixels = Math.floor(thumbnailWidth / 2);
|
const offsetPixels = Math.floor(thumbnailWidth / 2);
|
||||||
|
const marginClass = 'mx-[88px]'; // offsetPixels
|
||||||
|
|
||||||
if (mediaSet == null) {
|
if (mediaSet == null) {
|
||||||
// TODO: improve
|
// TODO: improve
|
||||||
|
@ -389,33 +394,48 @@ function App(): JSX.Element {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="App">
|
<div className="App bg-gray-800 h-screen flex flex-col">
|
||||||
<div style={containerStyles}>
|
<header className="bg-green-900 h-16 grow-0 flex items-center mb-12 px-[88px]">
|
||||||
<ControlBar
|
<h1 className="text-3xl font-bold">Clipper</h1>
|
||||||
onPlay={handlePlay}
|
</header>
|
||||||
onPause={handlePause}
|
<div className="flex flex-col grow-1 bg-gray-800 w-full h-full mx-auto">
|
||||||
onClip={handleClip}
|
<div className={`flex flex-col grow ${marginClass}`}>
|
||||||
onZoomIn={handleZoomIn}
|
<div className="flex grow-0 h-8 pt-4 pb-2 items-center space-x-2 text-white">
|
||||||
onZoomOut={handleZoomOut}
|
<span className="text-gray-300">{mediaSet.author}</span>
|
||||||
/>
|
<span>/</span>
|
||||||
|
<span>{mediaSet.title}</span>
|
||||||
|
<a
|
||||||
|
href={`https://www.youtube.com/watch?v=${mediaSet.youtubeId}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
title="Open in YouTube"
|
||||||
|
>
|
||||||
|
<ExternalLinkIcon className="h-6 w-6 text-gray-500 hover:text-gray-200" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<ControlBar
|
||||||
|
playState={playState}
|
||||||
|
onTogglePlay={handleTogglePlay}
|
||||||
|
onClip={handleClip}
|
||||||
|
onZoomIn={handleZoomIn}
|
||||||
|
onZoomOut={handleZoomOut}
|
||||||
|
/>
|
||||||
|
|
||||||
<Overview
|
<Overview
|
||||||
peaks={overviewPeaks}
|
peaks={overviewPeaks}
|
||||||
mediaSet={mediaSet}
|
mediaSet={mediaSet}
|
||||||
offsetPixels={offsetPixels}
|
viewport={viewport}
|
||||||
height={80}
|
position={position}
|
||||||
viewport={viewport}
|
onSelectionChange={handleOverviewSelectionChange}
|
||||||
position={position}
|
/>
|
||||||
onSelectionChange={handleOverviewSelectionChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Waveform
|
<Waveform
|
||||||
mediaSet={mediaSet}
|
mediaSet={mediaSet}
|
||||||
position={position}
|
position={position}
|
||||||
viewport={viewport}
|
viewport={viewport}
|
||||||
offsetPixels={offsetPixels}
|
onSelectionChange={handleWaveformSelectionChange}
|
||||||
onSelectionChange={handleWaveformSelectionChange}
|
/>
|
||||||
/>
|
</div>
|
||||||
|
|
||||||
<SeekBar
|
<SeekBar
|
||||||
position={video.currentTime}
|
position={video.currentTime}
|
||||||
|
@ -432,10 +452,9 @@ function App(): JSX.Element {
|
||||||
video={video}
|
video={video}
|
||||||
position={position}
|
position={position}
|
||||||
duration={millisFromDuration(mediaSet.videoDuration)}
|
duration={millisFromDuration(mediaSet.videoDuration)}
|
||||||
height={thumbnailHeight}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ul style={{ listStyleType: 'none' } as React.CSSProperties}>
|
<ul className="hidden">
|
||||||
<li>Frames: {mediaSet.audioFrames}</li>
|
<li>Frames: {mediaSet.audioFrames}</li>
|
||||||
<li>
|
<li>
|
||||||
Viewport (frames): {viewport.start} to {viewport.end}
|
Viewport (frames): {viewport.start} to {viewport.end}
|
||||||
|
|
|
@ -1,42 +1,60 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { PlayState } from './App';
|
||||||
|
import {
|
||||||
|
CloudDownloadIcon,
|
||||||
|
FastForwardIcon,
|
||||||
|
PauseIcon,
|
||||||
|
PlayIcon,
|
||||||
|
RewindIcon,
|
||||||
|
ZoomInIcon,
|
||||||
|
ZoomOutIcon,
|
||||||
|
} from '@heroicons/react/solid';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onPlay: () => void;
|
playState: PlayState;
|
||||||
onPause: () => void;
|
onTogglePlay: () => void;
|
||||||
onClip: () => void;
|
onClip: () => void;
|
||||||
onZoomIn: () => void;
|
onZoomIn: () => void;
|
||||||
onZoomOut: () => void;
|
onZoomOut: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ControlBar: React.FC<Props> = React.memo((props: Props) => {
|
const ControlBar: React.FC<Props> = React.memo((props: Props) => {
|
||||||
const styles = { width: '100%', flexGrow: 0 };
|
const buttonStyle =
|
||||||
const buttonStyles = {
|
'bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded';
|
||||||
cursor: 'pointer',
|
|
||||||
background: 'black',
|
const largeButtonStyle =
|
||||||
outline: 'none',
|
'bg-green-700 hover:bg-green-600 text-white font-bold py-2 px-4 rounded absolute right-0';
|
||||||
border: 'none',
|
|
||||||
color: 'green',
|
const iconStyle = 'inline h-6 w-6 text-white-500';
|
||||||
display: 'inline-block',
|
|
||||||
margin: '0 2px',
|
const playPauseComponent =
|
||||||
};
|
props.playState == PlayState.Playing ? (
|
||||||
|
<PauseIcon className={iconStyle} />
|
||||||
|
) : (
|
||||||
|
<PlayIcon className={iconStyle} />
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={styles}>
|
<div className="relative grow-0 w-full py-2 space-x-2">
|
||||||
<button style={buttonStyles} onClick={props.onPlay}>
|
<button className={buttonStyle}>
|
||||||
Play
|
<RewindIcon className={iconStyle} />
|
||||||
</button>
|
</button>
|
||||||
<button style={buttonStyles} onClick={props.onPause}>
|
<button className={buttonStyle} onClick={props.onTogglePlay}>
|
||||||
Pause
|
{playPauseComponent}
|
||||||
</button>
|
</button>
|
||||||
<button style={buttonStyles} onClick={props.onClip}>
|
<button className={buttonStyle}>
|
||||||
Clip
|
<FastForwardIcon className={iconStyle} />
|
||||||
</button>
|
</button>
|
||||||
<button style={buttonStyles} onClick={props.onZoomIn}>
|
<button className={buttonStyle} onClick={props.onZoomIn}>
|
||||||
Zoom In
|
<ZoomInIcon className={iconStyle} />
|
||||||
</button>
|
</button>
|
||||||
<button style={buttonStyles} onClick={props.onZoomOut}>
|
<button className={buttonStyle} onClick={props.onZoomOut}>
|
||||||
Zoom Out
|
<ZoomOutIcon className={iconStyle} />
|
||||||
|
</button>
|
||||||
|
<button className={largeButtonStyle} onClick={props.onClip}>
|
||||||
|
<CloudDownloadIcon className={`${iconStyle} mr-2`} />
|
||||||
|
Download clip
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -10,7 +10,6 @@ interface Styles {
|
||||||
interface Props {
|
interface Props {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
zIndex: number;
|
|
||||||
emptySelectionAction: EmptySelectionAction;
|
emptySelectionAction: EmptySelectionAction;
|
||||||
styles: Styles;
|
styles: Styles;
|
||||||
position: number | null;
|
position: number | null;
|
||||||
|
@ -48,7 +47,6 @@ const emptySelection: Selection = { start: 0, end: 0 };
|
||||||
export const HudCanvas: React.FC<Props> = ({
|
export const HudCanvas: React.FC<Props> = ({
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
zIndex,
|
|
||||||
emptySelectionAction,
|
emptySelectionAction,
|
||||||
styles: {
|
styles: {
|
||||||
borderLineWidth,
|
borderLineWidth,
|
||||||
|
@ -66,7 +64,7 @@ export const HudCanvas: React.FC<Props> = ({
|
||||||
});
|
});
|
||||||
const [mode, setMode] = useState(Mode.Normal);
|
const [mode, setMode] = useState(Mode.Normal);
|
||||||
const [hoverState, setHoverState] = useState(HoverState.Normal);
|
const [hoverState, setHoverState] = useState(HoverState.Normal);
|
||||||
const [cursor, setCursor] = useState('auto');
|
const [cursor, setCursor] = useState('cursor-auto');
|
||||||
|
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const moveOffsetX = useRef(0);
|
const moveOffsetX = useRef(0);
|
||||||
|
@ -189,11 +187,11 @@ export const HudCanvas: React.FC<Props> = ({
|
||||||
moveOffsetX.current = x;
|
moveOffsetX.current = x;
|
||||||
} else if (isHoveringSelection(x)) {
|
} else if (isHoveringSelection(x)) {
|
||||||
setMode(Mode.Dragging);
|
setMode(Mode.Dragging);
|
||||||
setCursor('pointer');
|
setCursor('cursor-pointer');
|
||||||
moveOffsetX.current = x;
|
moveOffsetX.current = x;
|
||||||
} else {
|
} else {
|
||||||
setMode(Mode.Selecting);
|
setMode(Mode.Selecting);
|
||||||
setCursor('col-resize');
|
setCursor('cursor-col-resize');
|
||||||
moveOffsetX.current = x;
|
moveOffsetX.current = x;
|
||||||
setNewSelection({ start: x, end: x });
|
setNewSelection({ start: x, end: x });
|
||||||
}
|
}
|
||||||
|
@ -206,15 +204,15 @@ export const HudCanvas: React.FC<Props> = ({
|
||||||
case Mode.Normal: {
|
case Mode.Normal: {
|
||||||
if (isHoveringSelectionStart(x)) {
|
if (isHoveringSelectionStart(x)) {
|
||||||
setHoverState(HoverState.OverSelectionStart);
|
setHoverState(HoverState.OverSelectionStart);
|
||||||
setCursor('col-resize');
|
setCursor('cursor-col-resize');
|
||||||
} else if (isHoveringSelectionEnd(x)) {
|
} else if (isHoveringSelectionEnd(x)) {
|
||||||
setHoverState(HoverState.OverSelectionEnd);
|
setHoverState(HoverState.OverSelectionEnd);
|
||||||
setCursor('col-resize');
|
setCursor('cursor-col-resize');
|
||||||
} else if (isHoveringSelection(x)) {
|
} else if (isHoveringSelection(x)) {
|
||||||
setHoverState(HoverState.OverSelection);
|
setHoverState(HoverState.OverSelection);
|
||||||
setCursor('pointer');
|
setCursor('cursor-pointer');
|
||||||
} else {
|
} else {
|
||||||
setCursor('auto');
|
setCursor('cursor-auto');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -275,7 +273,7 @@ export const HudCanvas: React.FC<Props> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
setMode(Mode.Normal);
|
setMode(Mode.Normal);
|
||||||
setCursor('auto');
|
setCursor('cursor-auto');
|
||||||
|
|
||||||
if (newSelection.start == newSelection.end) {
|
if (newSelection.start == newSelection.end) {
|
||||||
handleEmptySelectionAction();
|
handleEmptySelectionAction();
|
||||||
|
@ -300,22 +298,13 @@ export const HudCanvas: React.FC<Props> = ({
|
||||||
setHoverState(HoverState.Normal);
|
setHoverState(HoverState.Normal);
|
||||||
};
|
};
|
||||||
|
|
||||||
const canvasStyles = {
|
|
||||||
display: 'block',
|
|
||||||
position: 'absolute',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
zIndex: zIndex,
|
|
||||||
cursor: cursor,
|
|
||||||
} as React.CSSProperties;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<canvas
|
<canvas
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
|
className={`block absolute w-full h-full ${cursor} z-20`}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
style={canvasStyles}
|
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
|
|
|
@ -13,8 +13,6 @@ export interface Selection {
|
||||||
interface Props {
|
interface Props {
|
||||||
peaks: Observable<number[]>;
|
peaks: Observable<number[]>;
|
||||||
mediaSet: MediaSet;
|
mediaSet: MediaSet;
|
||||||
height: number;
|
|
||||||
offsetPixels: number;
|
|
||||||
position: VideoPosition;
|
position: VideoPosition;
|
||||||
viewport: Frames;
|
viewport: Frames;
|
||||||
onSelectionChange: (selection: Selection) => void;
|
onSelectionChange: (selection: Selection) => void;
|
||||||
|
@ -26,8 +24,6 @@ export const CanvasLogicalHeight = 500;
|
||||||
export const Overview: React.FC<Props> = ({
|
export const Overview: React.FC<Props> = ({
|
||||||
peaks,
|
peaks,
|
||||||
mediaSet,
|
mediaSet,
|
||||||
height,
|
|
||||||
offsetPixels,
|
|
||||||
position,
|
position,
|
||||||
viewport,
|
viewport,
|
||||||
onSelectionChange,
|
onSelectionChange,
|
||||||
|
@ -69,13 +65,6 @@ export const Overview: React.FC<Props> = ({
|
||||||
|
|
||||||
// render component
|
// render component
|
||||||
|
|
||||||
const containerStyles = {
|
|
||||||
flexGrow: 0,
|
|
||||||
position: 'relative',
|
|
||||||
margin: `0 ${offsetPixels}px`,
|
|
||||||
height: `${height}px`,
|
|
||||||
} as React.CSSProperties;
|
|
||||||
|
|
||||||
const hudStyles = {
|
const hudStyles = {
|
||||||
borderLineWidth: 4,
|
borderLineWidth: 4,
|
||||||
borderStrokeStyle: 'red',
|
borderStrokeStyle: 'red',
|
||||||
|
@ -85,7 +74,7 @@ export const Overview: React.FC<Props> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={containerStyles}>
|
<div className={`relative grow-0 h-[80px]`}>
|
||||||
<WaveformCanvas
|
<WaveformCanvas
|
||||||
peaks={peaks}
|
peaks={peaks}
|
||||||
channels={mediaSet.audioChannels}
|
channels={mediaSet.audioChannels}
|
||||||
|
@ -93,13 +82,11 @@ export const Overview: React.FC<Props> = ({
|
||||||
height={CanvasLogicalHeight}
|
height={CanvasLogicalHeight}
|
||||||
strokeStyle="black"
|
strokeStyle="black"
|
||||||
fillStyle="#003300"
|
fillStyle="#003300"
|
||||||
zIndex={1}
|
|
||||||
alpha={1}
|
alpha={1}
|
||||||
></WaveformCanvas>
|
></WaveformCanvas>
|
||||||
<HudCanvas
|
<HudCanvas
|
||||||
width={CanvasLogicalWidth}
|
width={CanvasLogicalWidth}
|
||||||
height={CanvasLogicalHeight}
|
height={CanvasLogicalHeight}
|
||||||
zIndex={1}
|
|
||||||
emptySelectionAction={EmptySelectionAction.SelectPrevious}
|
emptySelectionAction={EmptySelectionAction.SelectPrevious}
|
||||||
styles={hudStyles}
|
styles={hudStyles}
|
||||||
position={positionPixels}
|
position={positionPixels}
|
||||||
|
|
|
@ -24,7 +24,7 @@ export const SeekBar: React.FC<Props> = ({
|
||||||
onPositionChanged,
|
onPositionChanged,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [mode, setMode] = useState(Mode.Normal);
|
const [mode, setMode] = useState(Mode.Normal);
|
||||||
const [cursor, setCursor] = useState('auto');
|
const [cursor, setCursor] = useState('cursor-auto');
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
// render canvas
|
// render canvas
|
||||||
|
@ -98,9 +98,9 @@ export const SeekBar: React.FC<Props> = ({
|
||||||
|
|
||||||
// TODO: improve mouse detection around knob.
|
// TODO: improve mouse detection around knob.
|
||||||
if (y > InnerMargin && y < LogicalHeight - InnerMargin) {
|
if (y > InnerMargin && y < LogicalHeight - InnerMargin) {
|
||||||
setCursor('pointer');
|
setCursor('cursor-pointer');
|
||||||
} else {
|
} else {
|
||||||
setCursor('auto');
|
setCursor('cursor-auto');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode == Mode.Normal) return;
|
if (mode == Mode.Normal) return;
|
||||||
|
@ -116,17 +116,10 @@ export const SeekBar: React.FC<Props> = ({
|
||||||
|
|
||||||
// render component
|
// render component
|
||||||
|
|
||||||
const styles = {
|
|
||||||
width: '100%',
|
|
||||||
height: '30px',
|
|
||||||
margin: '0 auto',
|
|
||||||
cursor: cursor,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<canvas
|
<canvas
|
||||||
style={styles}
|
className={`w-full h-[30px] mx-0 my-auto ${cursor}`}
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
width={LogicalWidth}
|
width={LogicalWidth}
|
||||||
height={LogicalHeight}
|
height={LogicalHeight}
|
||||||
|
|
|
@ -6,7 +6,6 @@ interface Props {
|
||||||
mediaSet: MediaSet;
|
mediaSet: MediaSet;
|
||||||
position: VideoPosition;
|
position: VideoPosition;
|
||||||
duration: number;
|
duration: number;
|
||||||
height: number;
|
|
||||||
video: HTMLVideoElement;
|
video: HTMLVideoElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +13,6 @@ export const VideoPreview: React.FC<Props> = ({
|
||||||
mediaSet,
|
mediaSet,
|
||||||
position,
|
position,
|
||||||
duration,
|
duration,
|
||||||
height,
|
|
||||||
video,
|
video,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const videoCanvasRef = useRef<HTMLCanvasElement>(null);
|
const videoCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
@ -76,30 +74,15 @@ export const VideoPreview: React.FC<Props> = ({
|
||||||
|
|
||||||
// render component
|
// render component
|
||||||
|
|
||||||
const containerStyles = {
|
|
||||||
height: height + 'px',
|
|
||||||
position: 'relative',
|
|
||||||
flexGrow: 0,
|
|
||||||
} as React.CSSProperties;
|
|
||||||
|
|
||||||
const canvasStyles = {
|
|
||||||
position: 'absolute',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
display: 'block',
|
|
||||||
zIndex: 1,
|
|
||||||
} as React.CSSProperties;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={containerStyles}>
|
<div className={`relative grow-0 h-[100px]`}>
|
||||||
<canvas
|
<canvas
|
||||||
|
className="absolute block w-full h-full"
|
||||||
width="500"
|
width="500"
|
||||||
height="100"
|
height="100"
|
||||||
ref={videoCanvasRef}
|
ref={videoCanvasRef}
|
||||||
style={canvasStyles}
|
|
||||||
></canvas>
|
></canvas>
|
||||||
<canvas style={canvasStyles}></canvas>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,7 +10,6 @@ interface Props {
|
||||||
mediaSet: MediaSet;
|
mediaSet: MediaSet;
|
||||||
position: VideoPosition;
|
position: VideoPosition;
|
||||||
viewport: Frames;
|
viewport: Frames;
|
||||||
offsetPixels: number;
|
|
||||||
onSelectionChange: (selection: Selection) => void;
|
onSelectionChange: (selection: Selection) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +20,6 @@ export const Waveform: React.FC<Props> = ({
|
||||||
mediaSet,
|
mediaSet,
|
||||||
position,
|
position,
|
||||||
viewport,
|
viewport,
|
||||||
offsetPixels,
|
|
||||||
onSelectionChange,
|
onSelectionChange,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [peaks, setPeaks] = useState<Observable<number[]>>(from([]));
|
const [peaks, setPeaks] = useState<Observable<number[]>>(from([]));
|
||||||
|
@ -115,13 +113,6 @@ export const Waveform: React.FC<Props> = ({
|
||||||
|
|
||||||
// render component
|
// render component
|
||||||
|
|
||||||
const containerStyles = {
|
|
||||||
background: 'black',
|
|
||||||
margin: '0 ' + offsetPixels + 'px',
|
|
||||||
flexGrow: 1,
|
|
||||||
position: 'relative',
|
|
||||||
} as React.CSSProperties;
|
|
||||||
|
|
||||||
const hudStyles = {
|
const hudStyles = {
|
||||||
borderLineWidth: 0,
|
borderLineWidth: 0,
|
||||||
borderStrokeStyle: 'transparent',
|
borderStrokeStyle: 'transparent',
|
||||||
|
@ -131,7 +122,7 @@ export const Waveform: React.FC<Props> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={containerStyles}>
|
<div className={`relative grow`}>
|
||||||
<WaveformCanvas
|
<WaveformCanvas
|
||||||
peaks={peaks}
|
peaks={peaks}
|
||||||
channels={mediaSet.audioChannels}
|
channels={mediaSet.audioChannels}
|
||||||
|
@ -139,13 +130,11 @@ export const Waveform: React.FC<Props> = ({
|
||||||
height={CanvasLogicalHeight}
|
height={CanvasLogicalHeight}
|
||||||
strokeStyle="green"
|
strokeStyle="green"
|
||||||
fillStyle="black"
|
fillStyle="black"
|
||||||
zIndex={0}
|
|
||||||
alpha={1}
|
alpha={1}
|
||||||
></WaveformCanvas>
|
></WaveformCanvas>
|
||||||
<HudCanvas
|
<HudCanvas
|
||||||
width={CanvasLogicalWidth}
|
width={CanvasLogicalWidth}
|
||||||
height={CanvasLogicalHeight}
|
height={CanvasLogicalHeight}
|
||||||
zIndex={1}
|
|
||||||
emptySelectionAction={EmptySelectionAction.SelectNothing}
|
emptySelectionAction={EmptySelectionAction.SelectNothing}
|
||||||
styles={hudStyles}
|
styles={hudStyles}
|
||||||
position={positionPixels}
|
position={positionPixels}
|
||||||
|
|
|
@ -10,18 +10,10 @@ interface Props {
|
||||||
channels: number;
|
channels: number;
|
||||||
strokeStyle: string;
|
strokeStyle: string;
|
||||||
fillStyle: string;
|
fillStyle: string;
|
||||||
zIndex: number;
|
|
||||||
alpha: number;
|
alpha: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Canvas is a generic component that renders a waveform to a canvas.
|
// Canvas is a generic component that renders a waveform to a canvas.
|
||||||
//
|
|
||||||
// Properties:
|
|
||||||
//
|
|
||||||
// peaks: a 2d array of uint16s representing the peak values. Each inner array length should match logicalWidth
|
|
||||||
// strokeStyle: waveform style
|
|
||||||
// fillStyle: background style
|
|
||||||
// style: React.CSSProperties applied to canvas element
|
|
||||||
const WaveformCanvas: React.FC<Props> = React.memo((props: Props) => {
|
const WaveformCanvas: React.FC<Props> = React.memo((props: Props) => {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
@ -71,21 +63,13 @@ const WaveformCanvas: React.FC<Props> = React.memo((props: Props) => {
|
||||||
})();
|
})();
|
||||||
}, [props.peaks]);
|
}, [props.peaks]);
|
||||||
|
|
||||||
const canvasStyles = {
|
|
||||||
display: 'block',
|
|
||||||
position: 'absolute',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
zIndex: props.zIndex,
|
|
||||||
} as React.CSSProperties;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<canvas
|
<canvas
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
|
className={`block absolute w-full h-full z-10`}
|
||||||
width={props.width}
|
width={props.width}
|
||||||
height={props.height}
|
height={props.height}
|
||||||
style={canvasStyles}
|
|
||||||
></canvas>
|
></canvas>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1102,6 +1102,11 @@
|
||||||
minimatch "^3.0.4"
|
minimatch "^3.0.4"
|
||||||
strip-json-comments "^3.1.1"
|
strip-json-comments "^3.1.1"
|
||||||
|
|
||||||
|
"@heroicons/react@^1.0.5":
|
||||||
|
version "1.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.5.tgz#2fe4df9d33eb6ce6d5178a0f862e97b61c01e27d"
|
||||||
|
integrity sha512-UDMyLM2KavIu2vlWfMspapw9yii7aoLwzI2Hudx4fyoPwfKfxU8r3cL8dEBXOjcLG0/oOONZzbT14M1HoNtEcg==
|
||||||
|
|
||||||
"@humanwhocodes/config-array@^0.5.0":
|
"@humanwhocodes/config-array@^0.5.0":
|
||||||
version "0.5.0"
|
version "0.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9"
|
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9"
|
||||||
|
|
Loading…
Reference in New Issue