lossless-cut/src/BottomBar.jsx

370 wiersze
14 KiB
React
Czysty Zwykły widok Historia

2022-02-13 09:21:51 +00:00
import React, { memo, useCallback, useEffect } from 'react';
import { Select } from 'evergreen-ui';
import { motion } from 'framer-motion';
import { MdRotate90DegreesCcw } from 'react-icons/md';
import { useTranslation } from 'react-i18next';
import { IoIosCamera, IoMdKey } from 'react-icons/io';
import { FaYinYang, FaTrashAlt, FaStepBackward, FaStepForward, FaCaretLeft, FaCaretRight, FaPause, FaPlay, FaImages, FaKey } from 'react-icons/fa';
import { GiSoundWaves } from 'react-icons/gi';
// import useTraceUpdate from 'use-trace-update';
import { primaryTextColor, primaryColor } from './colors';
import SegmentCutpointButton from './components/SegmentCutpointButton';
import SetCutpointButton from './components/SetCutpointButton';
import ExportButton from './components/ExportButton';
import ToggleExportConfirm from './components/ToggleExportConfirm';
import CaptureFormatButton from './components/CaptureFormatButton';
2022-02-13 09:21:51 +00:00
import SimpleModeButton from './components/SimpleModeButton';
2023-02-16 05:14:15 +00:00
import { withBlur, mirrorTransform, checkAppPath } from './util';
import { toast } from './swal';
2022-02-13 09:21:51 +00:00
import { getSegColor } from './util/colors';
import { formatDuration, parseDuration } from './util/duration';
import useUserSettings from './hooks/useUserSettings';
2022-02-13 09:21:51 +00:00
const zoomOptions = Array(13).fill().map((unused, z) => 2 ** z);
const leftRightWidth = 100;
const BottomBar = memo(({
zoom, setZoom, timelineToggleComfortZoom,
isRotationSet, rotation, areWeCutting, increaseRotation, cleanupFilesDialog,
2022-03-17 15:51:34 +00:00
captureSnapshot, onExportPress, segmentsToExport, hasVideo,
2022-02-13 09:21:51 +00:00
seekAbs, currentSegIndexSafe, cutSegments, currentCutSeg, setCutStart, setCutEnd,
setCurrentSegIndex, cutStartTimeManual, setCutStartTimeManual, cutEndTimeManual, setCutEndTimeManual,
jumpTimelineStart, jumpTimelineEnd, jumpCutEnd, jumpCutStart, startTimeOffset, setCutTime, currentApparentCutSeg,
2022-09-04 13:06:56 +00:00
playing, shortStep, togglePlay, toggleTimelineMode, hasAudio, timelineMode,
keyframesEnabled, toggleKeyframesEnabled, seekClosestKeyframe, detectedFps,
2022-02-13 09:21:51 +00:00
}) => {
const { t } = useTranslation();
const { invertCutSegments, setInvertCutSegments, simpleMode, toggleSimpleMode, exportConfirmEnabled } = useUserSettings();
2022-02-13 09:21:51 +00:00
const onYinYangClick = useCallback(() => {
setInvertCutSegments(v => {
const newVal = !v;
if (newVal) toast.fire({ title: t('When you export, selected segments on the timeline will be REMOVED - the surrounding areas will be KEPT') });
else toast.fire({ title: t('When you export, selected segments on the timeline will be KEPT - the surrounding areas will be REMOVED.') });
return newVal;
});
}, [setInvertCutSegments, t]);
const rotationStr = `${rotation}°`;
// Clear manual overrides if upstream cut time has changed
useEffect(() => {
setCutStartTimeManual();
setCutEndTimeManual();
}, [setCutStartTimeManual, setCutEndTimeManual, currentApparentCutSeg.start, currentApparentCutSeg.end]);
useEffect(() => {
checkAppPath();
}, []);
2022-02-13 09:21:51 +00:00
function renderJumpCutpointButton(direction) {
const newIndex = currentSegIndexSafe + direction;
const seg = cutSegments[newIndex];
const backgroundColor = seg && getSegColor(seg).alpha(0.5).string();
const opacity = seg ? undefined : 0.3;
2022-02-18 14:14:16 +00:00
const text = seg ? `${newIndex + 1}` : '-';
const wide = text.length > 1;
2022-02-13 09:21:51 +00:00
const segButtonStyle = {
2022-02-18 14:14:16 +00:00
backgroundColor, opacity, padding: `6px ${wide ? 4 : 6}px`, borderRadius: 10, color: 'white', fontSize: wide ? 12 : 14, width: 20, boxSizing: 'border-box', letterSpacing: -1, lineHeight: '10px', fontWeight: 'bold', margin: '0 6px',
2022-02-13 09:21:51 +00:00
};
return (
<div
style={segButtonStyle}
role="button"
title={`${direction > 0 ? t('Select next segment') : t('Select previous segment')} (${newIndex + 1})`}
onClick={() => seg && setCurrentSegIndex(newIndex)}
>
2022-02-18 14:14:16 +00:00
{text}
2022-02-13 09:21:51 +00:00
</div>
);
}
function renderCutTimeInput(type) {
const isStart = type === 'start';
const cutTimeManual = isStart ? cutStartTimeManual : cutEndTimeManual;
const cutTime = isStart ? currentApparentCutSeg.start : currentApparentCutSeg.end;
const setCutTimeManual = isStart ? setCutStartTimeManual : setCutEndTimeManual;
const isCutTimeManualSet = () => cutTimeManual !== undefined;
2022-02-18 14:14:16 +00:00
const border = `1px solid ${getSegColor(currentCutSeg).alpha(0.8).string()}`;
2022-02-13 09:21:51 +00:00
const cutTimeInputStyle = {
background: 'white', border, borderRadius: 5, color: 'rgba(0, 0, 0, 0.7)', fontSize: 13, textAlign: 'center', padding: '1px 5px', marginTop: 0, marginBottom: 0, marginLeft: isStart ? 0 : 5, marginRight: isStart ? 5 : 0, boxSizing: 'border-box', fontFamily: 'inherit', width: 90, outline: 'none',
};
function parseAndSetCutTime(text) {
setCutTimeManual(text);
// Don't proceed if not a valid time value
const timeWithOffset = parseDuration(text);
if (timeWithOffset === undefined) return;
const timeWithoutOffset = Math.max(timeWithOffset - startTimeOffset, 0);
try {
setCutTime(type, timeWithoutOffset);
seekAbs(timeWithoutOffset);
} catch (err) {
console.error('Cannot set cut time', err);
// If we get an error from setCutTime, remain in the editing state (cutTimeManual)
// https://github.com/mifi/lossless-cut/issues/988
}
}
function handleCutTimeInput(text) {
// Allow the user to erase to reset
if (text.length === 0) {
setCutTimeManual();
return;
}
parseAndSetCutTime(text);
}
async function handleCutTimePaste(e) {
e.preventDefault();
try {
const clipboardData = e.clipboardData.getData('Text');
parseAndSetCutTime(clipboardData);
} catch (err) {
console.error(err);
}
}
return (
<input
style={{ ...cutTimeInputStyle, color: isCutTimeManualSet() ? '#dc1d1d' : undefined }}
type="text"
title={isStart ? t('Manually input current segment\'s start time') : t('Manually input current segment\'s end time')}
2022-02-13 09:21:51 +00:00
onChange={e => handleCutTimeInput(e.target.value)}
onPaste={handleCutTimePaste}
value={isCutTimeManualSet()
? cutTimeManual
: formatDuration({ seconds: cutTime + startTimeOffset })}
/>
);
}
const PlayPause = playing ? FaPause : FaPlay;
return (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', alignItems: 'center', flexBasis: leftRightWidth }}>
{!simpleMode && (
2022-02-13 09:21:51 +00:00
<>
{hasAudio && (
<GiSoundWaves
size={24}
style={{ padding: '0 5px', color: timelineMode === 'waveform' ? primaryTextColor : undefined }}
role="button"
title={t('Show waveform')}
2022-09-04 13:06:56 +00:00
onClick={() => toggleTimelineMode('waveform')}
/>
)}
{hasVideo && (
<>
<FaImages
size={20}
style={{ padding: '0 5px', color: timelineMode === 'thumbnails' ? primaryTextColor : undefined }}
role="button"
title={t('Show thumbnails')}
2022-09-04 13:06:56 +00:00
onClick={() => toggleTimelineMode('thumbnails')}
/>
<FaKey
size={16}
style={{ padding: '0 5px', color: keyframesEnabled ? primaryTextColor : undefined }}
role="button"
title={t('Show keyframes')}
onClick={toggleKeyframesEnabled}
/>
</>
)}
2022-02-13 09:21:51 +00:00
</>
)}
</div>
<div style={{ flexGrow: 1 }} />
{!simpleMode && (
<>
<FaStepBackward
size={16}
title={t('Jump to start of video')}
role="button"
onClick={jumpTimelineStart}
/>
2022-02-13 09:21:51 +00:00
{renderJumpCutpointButton(-1)}
<SegmentCutpointButton currentCutSeg={currentCutSeg} side="start" Icon={FaStepBackward} onClick={jumpCutStart} title={t('Jump to current segment\'s start time')} style={{ marginRight: 5 }} />
</>
)}
2022-02-13 09:21:51 +00:00
<SetCutpointButton currentCutSeg={currentCutSeg} side="start" onClick={setCutStart} title={t('Start current segment at current time')} style={{ marginRight: 5 }} />
2022-02-13 09:21:51 +00:00
{!simpleMode && renderCutTimeInput('start')}
<IoMdKey
size={25}
role="button"
title={t('Seek previous keyframe')}
style={{ flexShrink: 0, marginRight: 2, transform: mirrorTransform }}
onClick={() => seekClosestKeyframe(-1)}
/>
{!simpleMode && (
<FaCaretLeft
style={{ flexShrink: 0, marginLeft: -6, marginRight: -4 }}
size={28}
role="button"
title={t('One frame back')}
onClick={() => shortStep(-1)}
/>
)}
<div role="button" onClick={() => togglePlay()} style={{ background: primaryColor, margin: '2px 5px 0 5px', display: 'flex', alignItems: 'center', justifyContent: 'center', width: 34, height: 34, borderRadius: 17 }}>
2022-02-13 09:21:51 +00:00
<PlayPause
style={{ marginLeft: playing ? 0 : 2 }}
size={16}
/>
</div>
{!simpleMode && (
<FaCaretRight
style={{ flexShrink: 0, marginRight: -6, marginLeft: -4 }}
size={28}
role="button"
title={t('One frame forward')}
onClick={() => shortStep(1)}
/>
)}
<IoMdKey
style={{ flexShrink: 0, marginLeft: 2 }}
size={25}
role="button"
title={t('Seek next keyframe')}
onClick={() => seekClosestKeyframe(1)}
/>
{!simpleMode && renderCutTimeInput('end')}
<SetCutpointButton currentCutSeg={currentCutSeg} side="end" onClick={setCutEnd} title={t('End current segment at current time')} style={{ marginLeft: 5 }} />
2022-02-13 09:21:51 +00:00
{!simpleMode && (
<>
<SegmentCutpointButton currentCutSeg={currentCutSeg} side="end" Icon={FaStepForward} onClick={jumpCutEnd} title={t('Jump to current segment\'s end time')} style={{ marginLeft: 5 }} />
{renderJumpCutpointButton(1)}
<FaStepForward
size={16}
title={t('Jump to end of video')}
role="button"
onClick={jumpTimelineEnd}
/>
</>
2022-02-13 09:21:51 +00:00
)}
<div style={{ flexGrow: 1 }} />
<div style={{ flexBasis: leftRightWidth }} />
</div>
<div
className="no-user-select"
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '3px 4px' }}
>
<SimpleModeButton style={{ flexShrink: 0 }} />
2022-02-13 09:21:51 +00:00
{simpleMode && <div role="button" onClick={toggleSimpleMode} style={{ marginLeft: 5, fontSize: '90%' }}>{t('Toggle advanced view')}</div>}
{!simpleMode && (
<>
<div style={{ marginLeft: 5 }}>
<motion.div
style={{ width: 24, height: 24 }}
animate={{ rotateX: invertCutSegments ? 0 : 180 }}
transition={{ duration: 0.3 }}
>
<FaYinYang
size={24}
role="button"
title={invertCutSegments ? t('Discard selected segments') : t('Keep selected segments')}
style={{ color: invertCutSegments ? primaryTextColor : undefined }}
onClick={onYinYangClick}
/>
</motion.div>
</div>
<div role="button" style={{ marginRight: 5, marginLeft: 10 }} title={t('Zoom')} onClick={timelineToggleComfortZoom}>{Math.floor(zoom)}x</div>
2022-02-13 09:21:51 +00:00
<Select height={20} style={{ flexBasis: 85, flexGrow: 0 }} value={zoomOptions.includes(zoom) ? zoom.toString() : ''} title={t('Zoom')} onChange={withBlur(e => setZoom(parseInt(e.target.value, 10)))}>
<option key="" value="" disabled>{t('Zoom')}</option>
{zoomOptions.map(val => (
<option key={val} value={String(val)}>{t('Zoom')} {val}x</option>
))}
</Select>
{detectedFps != null && <div title={t('Video FPS')} style={{ color: 'rgba(255,255,255,0.6)', fontSize: '.7em', marginLeft: 6 }}>{detectedFps.toFixed(3)}</div>}
2022-02-13 09:21:51 +00:00
</>
)}
<div style={{ flexGrow: 1 }} />
{hasVideo && (
<>
<span style={{ textAlign: 'right', display: 'inline-block' }}>{isRotationSet && rotationStr}</span>
<MdRotate90DegreesCcw
size={24}
style={{ margin: '0px 0px 0 2px', verticalAlign: 'middle', color: isRotationSet ? primaryTextColor : undefined }}
title={`${t('Set output rotation. Current: ')} ${isRotationSet ? rotationStr : t('Don\'t modify')}`}
onClick={increaseRotation}
role="button"
/>
</>
)}
{!simpleMode && (
<FaTrashAlt
title={t('Close file and clean up')}
style={{ padding: '5px 10px' }}
size={16}
onClick={cleanupFilesDialog}
2022-02-13 09:21:51 +00:00
role="button"
/>
)}
{hasVideo && (
<>
{!simpleMode && <CaptureFormatButton height={20} />}
2022-02-13 09:21:51 +00:00
<IoIosCamera
style={{ paddingLeft: 5, paddingRight: 15 }}
size={25}
title={t('Capture frame')}
onClick={captureSnapshot}
2022-02-13 09:21:51 +00:00
/>
</>
)}
{(!simpleMode || !exportConfirmEnabled) && <ToggleExportConfirm style={{ marginRight: 5 }} />}
2022-02-13 09:21:51 +00:00
2022-03-17 15:51:34 +00:00
<ExportButton size={1.3} segmentsToExport={segmentsToExport} areWeCutting={areWeCutting} onClick={onExportPress} />
2022-02-13 09:21:51 +00:00
</div>
</>
);
});
export default BottomBar;