Update duration display on selection change
This commit is contained in:
parent
f386e12f72
commit
a4e9ebca3b
|
@ -17,8 +17,9 @@ 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 toHHMMSS from './helpers/toHHMMSS';
|
||||||
import { ExternalLinkIcon } from '@heroicons/react/solid';
|
import framesToDuration from './helpers/framesToDuration';
|
||||||
|
import { ClockIcon, ExternalLinkIcon } from '@heroicons/react/solid';
|
||||||
|
|
||||||
// ported from backend, where should they live?
|
// ported from backend, where should they live?
|
||||||
const thumbnailWidth = 177; // height 100
|
const thumbnailWidth = 177; // height 100
|
||||||
|
@ -330,8 +331,6 @@ function App(): JSX.Element {
|
||||||
})();
|
})();
|
||||||
};
|
};
|
||||||
|
|
||||||
const zoomMultiplier = 1.777;
|
|
||||||
|
|
||||||
const handleZoomIn = () => {
|
const handleZoomIn = () => {
|
||||||
if (mediaSet == null) {
|
if (mediaSet == null) {
|
||||||
return;
|
return;
|
||||||
|
@ -392,6 +391,26 @@ function App(): JSX.Element {
|
||||||
[mediaSet]
|
[mediaSet]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const durationString = useCallback((): string => {
|
||||||
|
if (!mediaSet || !mediaSet.videoDuration) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalDur = toHHMMSS(mediaSet.videoDuration);
|
||||||
|
if (selection.start == selection.end) {
|
||||||
|
return totalDur;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clipDur = toHHMMSS(
|
||||||
|
framesToDuration(
|
||||||
|
selection.end - selection.start,
|
||||||
|
mediaSet.audioSampleRate
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return `Selected ${clipDur} of ${totalDur}`;
|
||||||
|
}, [mediaSet, selection]);
|
||||||
|
|
||||||
// render component
|
// render component
|
||||||
|
|
||||||
const offsetPixels = Math.floor(thumbnailWidth / 2);
|
const offsetPixels = Math.floor(thumbnailWidth / 2);
|
||||||
|
@ -408,7 +427,7 @@ function App(): JSX.Element {
|
||||||
<header className="bg-green-900 h-16 grow-0 flex items-center mb-12 px-[88px]">
|
<header className="bg-green-900 h-16 grow-0 flex items-center mb-12 px-[88px]">
|
||||||
<h1 className="text-3xl font-bold">Clipper</h1>
|
<h1 className="text-3xl font-bold">Clipper</h1>
|
||||||
</header>
|
</header>
|
||||||
<div className="flex flex-col grow-1 bg-gray-800 w-full h-full mx-auto">
|
<div className="flex flex-col grow bg-gray-800 w-full h-full mx-auto">
|
||||||
<div className={`flex flex-col grow ${marginClass}`}>
|
<div className={`flex flex-col grow ${marginClass}`}>
|
||||||
<div className="flex grow-0 h-8 pt-4 pb-2 items-center space-x-2 text-white">
|
<div className="flex grow-0 h-8 pt-4 pb-2 items-center space-x-2 text-white">
|
||||||
<span className="text-gray-300">{mediaSet.author}</span>
|
<span className="text-gray-300">{mediaSet.author}</span>
|
||||||
|
@ -422,6 +441,10 @@ function App(): JSX.Element {
|
||||||
>
|
>
|
||||||
<ExternalLinkIcon className="h-6 w-6 text-gray-500 hover:text-gray-200" />
|
<ExternalLinkIcon className="h-6 w-6 text-gray-500 hover:text-gray-200" />
|
||||||
</a>
|
</a>
|
||||||
|
<span className="flex grow justify-end text-gray-500">
|
||||||
|
<ClockIcon className="h-5 w-5 mr-1 mt-0.5" />
|
||||||
|
{durationString()}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ControlBar
|
<ControlBar
|
||||||
playState={playState}
|
playState={playState}
|
||||||
|
@ -429,8 +452,10 @@ function App(): JSX.Element {
|
||||||
onClip={handleClip}
|
onClip={handleClip}
|
||||||
onZoomIn={handleZoomIn}
|
onZoomIn={handleZoomIn}
|
||||||
onZoomOut={handleZoomOut}
|
onZoomOut={handleZoomOut}
|
||||||
|
downloadClipEnabled={selection.start != selection.end}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="w-full bg-gray-600 h-6"></div>
|
||||||
<Overview
|
<Overview
|
||||||
peaks={overviewPeaks}
|
peaks={overviewPeaks}
|
||||||
mediaSet={mediaSet}
|
mediaSet={mediaSet}
|
||||||
|
|
|
@ -16,16 +16,18 @@ interface Props {
|
||||||
onClip: () => void;
|
onClip: () => void;
|
||||||
onZoomIn: () => void;
|
onZoomIn: () => void;
|
||||||
onZoomOut: () => void;
|
onZoomOut: () => void;
|
||||||
|
downloadClipEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ControlBar: React.FC<Props> = React.memo((props: Props) => {
|
const ControlBar: React.FC<Props> = React.memo((props: Props) => {
|
||||||
const buttonStyle =
|
const buttonStyle =
|
||||||
'bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded';
|
'bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded';
|
||||||
|
|
||||||
const largeButtonStyle =
|
const downloadButtonStyle = props.downloadClipEnabled
|
||||||
'bg-green-700 hover:bg-green-600 text-white font-bold py-2 px-4 rounded absolute right-0';
|
? 'bg-green-700 hover:bg-green-600 text-white font-bold py-2 px-4 rounded absolute right-0'
|
||||||
|
: 'bg-gray-700 hover:cursor-not-allowed text-gray-500 font-bold py-2 px-4 rounded absolute right-0';
|
||||||
|
|
||||||
const iconStyle = 'inline h-6 w-6 text-white-500';
|
const iconStyle = 'inline h-7 w-7 text-white-500';
|
||||||
|
|
||||||
const playPauseComponent =
|
const playPauseComponent =
|
||||||
props.playState == PlayState.Playing ? (
|
props.playState == PlayState.Playing ? (
|
||||||
|
@ -34,6 +36,12 @@ const ControlBar: React.FC<Props> = React.memo((props: Props) => {
|
||||||
<PlayIcon className={iconStyle} />
|
<PlayIcon className={iconStyle} />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleClip = () => {
|
||||||
|
if (props.downloadClipEnabled) {
|
||||||
|
props.onClip();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Detect if the space bar has been used to trigger this event, and ignore
|
// Detect if the space bar has been used to trigger this event, and ignore
|
||||||
// it if so. This conflicts with the player interface.
|
// it if so. This conflicts with the player interface.
|
||||||
const filterMouseEvent = (evt: React.MouseEvent, cb: () => void) => {
|
const filterMouseEvent = (evt: React.MouseEvent, cb: () => void) => {
|
||||||
|
@ -71,11 +79,11 @@ const ControlBar: React.FC<Props> = React.memo((props: Props) => {
|
||||||
<ZoomOutIcon className={iconStyle} />
|
<ZoomOutIcon className={iconStyle} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={largeButtonStyle}
|
className={downloadButtonStyle}
|
||||||
onClick={(evt) => filterMouseEvent(evt, props.onClip)}
|
onClick={(evt) => filterMouseEvent(evt, handleClip)}
|
||||||
>
|
>
|
||||||
<CloudDownloadIcon className={`${iconStyle} mr-2`} />
|
<CloudDownloadIcon className={`${iconStyle} mr-2`} />
|
||||||
Download clip
|
Download clip as MP3
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -136,8 +136,8 @@ export const HudCanvas: React.FC<Props> = ({
|
||||||
ctx.strokeStyle = positionStrokeStyle;
|
ctx.strokeStyle = positionStrokeStyle;
|
||||||
ctx.lineWidth = positionLineWidth;
|
ctx.lineWidth = positionLineWidth;
|
||||||
ctx.moveTo(position, 0);
|
ctx.moveTo(position, 0);
|
||||||
ctx.lineWidth = 4;
|
ctx.lineWidth = position == 0 ? 8 : 4;
|
||||||
ctx.lineTo(position, canvas.height - 4);
|
ctx.lineTo(position, canvas.height);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
});
|
});
|
||||||
}, [selection, newSelection, position]);
|
}, [selection, newSelection, position]);
|
||||||
|
|
|
@ -77,7 +77,7 @@ export const Overview: React.FC<Props> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`relative grow-0 h-[80px]`}>
|
<div className={`relative grow-0 h-16`}>
|
||||||
<WaveformCanvas
|
<WaveformCanvas
|
||||||
peaks={peaks}
|
peaks={peaks}
|
||||||
channels={mediaSet.audioChannels}
|
channels={mediaSet.audioChannels}
|
||||||
|
|
|
@ -45,7 +45,7 @@ export const SeekBar: React.FC<Props> = ({
|
||||||
canvas.width = canvas.height * (canvas.clientWidth / canvas.clientHeight);
|
canvas.width = canvas.height * (canvas.clientWidth / canvas.clientHeight);
|
||||||
|
|
||||||
// background
|
// background
|
||||||
ctx.fillStyle = '#444444';
|
ctx.fillStyle = 'transparent';
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
// seek bar
|
// seek bar
|
||||||
|
@ -119,7 +119,7 @@ export const SeekBar: React.FC<Props> = ({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<canvas
|
<canvas
|
||||||
className={`w-full h-[30px] mx-0 my-auto ${cursor}`}
|
className={`w-full bg-gray-700 h-10 mx-0 my-auto ${cursor}`}
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
width={LogicalWidth}
|
width={LogicalWidth}
|
||||||
height={LogicalHeight}
|
height={LogicalHeight}
|
||||||
|
|
Loading…
Reference in New Issue