diff --git a/src/BottomBar.jsx b/src/BottomBar.jsx index e70bd89c..da7a1bc2 100644 --- a/src/BottomBar.jsx +++ b/src/BottomBar.jsx @@ -31,6 +31,38 @@ const zoomOptions = Array(13).fill().map((unused, z) => 2 ** z); const leftRightWidth = 100; +const InvertCutModeButton = memo(({ invertCutSegments, setInvertCutSegments }) => { + const { t } = useTranslation(); + + 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]); + + return ( +
+ + + +
+ ); +}); + + const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, seekAbs, currentCutSeg, currentApparentCutSeg, isStart }) => { const { t } = useTranslation(); const { getSegColor } = useSegColors(); @@ -179,15 +211,6 @@ const BottomBar = memo(({ const { invertCutSegments, setInvertCutSegments, simpleMode, toggleSimpleMode, exportConfirmEnabled } = useUserSettings(); - 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}°`; useEffect(() => { @@ -365,21 +388,7 @@ const BottomBar = memo(({ {!simpleMode && ( <> -
- - - -
+
{Math.floor(zoom)}x
diff --git a/src/SegmentList.jsx b/src/SegmentList.jsx index bb11d753..e640d00b 100644 --- a/src/SegmentList.jsx +++ b/src/SegmentList.jsx @@ -24,8 +24,7 @@ const buttonBaseStyle = { const neutralButtonColor = 'var(--gray8)'; - -const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, getFrameCount, updateOrder, invertCutSegments, onClick, onRemovePress, onRemoveSelected, onLabelSelectedSegments, onReorderPress, onLabelPress, selected, onSelectSingleSegment, onToggleSegmentSelected, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByTag, onSelectAllSegments, jumpSegStart, jumpSegEnd, addSegment, onEditSegmentTags, onExtractSegmentFramesAsImages, onInvertSelectedSegments, onDuplicateSegmentClick }) => { +const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, getFrameCount, updateSegOrder, invertCutSegments, onClick, onRemovePress, onRemoveSelected, onLabelSelectedSegments, onReorderPress, onLabelPress, selected, onSelectSingleSegment, onToggleSegmentSelected, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByTag, onSelectAllSegments, jumpSegStart, jumpSegEnd, addSegment, onEditSegmentTags, onExtractSegmentFramesAsImages, onInvertSelectedSegments, onDuplicateSegmentClick }) => { const { t } = useTranslation(); const { getSegColor } = useSegColors(); @@ -33,15 +32,18 @@ const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, g const contextMenuTemplate = useMemo(() => { if (invertCutSegments) return []; + + const updateOrder = (dir) => updateSegOrder(index, index + dir); + return [ - { label: t('Jump to start time'), click: jumpSegStart }, - { label: t('Jump to end time'), click: jumpSegEnd }, + { label: t('Jump to start time'), click: () => jumpSegStart(index) }, + { label: t('Jump to end time'), click: () => jumpSegEnd(index) }, { type: 'separator' }, { label: t('Add segment'), click: addSegment }, - { label: t('Label segment'), click: onLabelPress }, - { label: t('Remove segment'), click: onRemovePress }, + { label: t('Label segment'), click: () => onLabelPress(index) }, + { label: t('Remove segment'), click: () => onRemovePress(index) }, { label: t('Duplicate segment'), click: () => onDuplicateSegmentClick(seg) }, { type: 'separator' }, @@ -60,7 +62,7 @@ const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, g { type: 'separator' }, - { label: t('Change segment order'), click: onReorderPress }, + { label: t('Change segment order'), click: () => onReorderPress(index) }, { label: t('Increase segment order'), click: () => updateOrder(1) }, { label: t('Decrease segment order'), click: () => updateOrder(-1) }, @@ -69,7 +71,7 @@ const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, g { label: t('Segment tags'), click: () => onEditSegmentTags(index) }, { label: t('Extract frames as image files'), click: () => onExtractSegmentFramesAsImages([seg.segId]) }, ]; - }, [invertCutSegments, t, jumpSegStart, jumpSegEnd, addSegment, onLabelPress, onRemovePress, onLabelSelectedSegments, onRemoveSelected, onReorderPress, onDuplicateSegmentClick, seg, onSelectSingleSegment, onSelectAllSegments, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByTag, onInvertSelectedSegments, updateOrder, onEditSegmentTags, index, onExtractSegmentFramesAsImages]); + }, [invertCutSegments, t, addSegment, onLabelSelectedSegments, onRemoveSelected, updateSegOrder, index, jumpSegStart, jumpSegEnd, onLabelPress, onRemovePress, onDuplicateSegmentClick, seg, onSelectSingleSegment, onSelectAllSegments, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByTag, onInvertSelectedSegments, onReorderPress, onEditSegmentTags, onExtractSegmentFramesAsImages]); useContextMenu(ref, contextMenuTemplate); @@ -95,10 +97,10 @@ const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, g const timeStr = useMemo(() => `${formatTimecode({ seconds: seg.start })} - ${formatTimecode({ seconds: seg.end })}`, [seg.start, seg.end, formatTimecode]); - function onDoubleClick() { + const onDoubleClick = useCallback(() => { if (invertCutSegments) return; - jumpSegStart(); - } + jumpSegStart(index); + }, [index, invertCutSegments, jumpSegStart]); const durationMsFormatted = Math.floor(durationMs); const frameCount = getFrameCount(duration); @@ -114,14 +116,18 @@ const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, g const tags = useMemo(() => getSegmentTags(seg), [seg]); + const maybeOnClick = useCallback(() => !invertCutSegments && onClick(index), [index, invertCutSegments, onClick]); + + const motionStyle = useMemo(() => ({ originY: 0, margin: '5px 0', background: 'var(--gray2)', border: isActive ? '1px solid var(--gray10)' : '1px solid transparent', padding: 5, borderRadius: 5, position: 'relative' }), [isActive]); + return ( !invertCutSegments && onClick(index)} + onClick={maybeOnClick} onDoubleClick={onDoubleClick} layout - style={{ originY: 0, margin: '5px 0', background: 'var(--gray2)', border: isActive ? '1px solid var(--gray10)' : '1px solid transparent', padding: 5, borderRadius: 5, position: 'relative' }} + style={motionStyle} initial={{ scaleY: 0 }} animate={{ scaleY: 1, opacity: !selected && !invertCutSegments ? 0.5 : undefined }} exit={{ scaleY: 0 }} @@ -171,7 +177,7 @@ const SegmentList = memo(({ const segments = invertCutSegments ? inverseCutSegments : apparentCutSegments; - const sortableList = segments.map((seg) => ({ id: seg.segId, seg })); + const sortableList = useMemo(() => segments.map((seg) => ({ id: seg.segId, seg })), [segments]); const setSortableList = useCallback((newList) => { if (isEqual(segments.map((s) => s.segId), newList.map((l) => l.id))) return; // No change @@ -189,7 +195,7 @@ const SegmentList = memo(({ } } - async function onReorderSegs(index) { + const onReorderSegs = useCallback(async (index) => { if (apparentCutSegments.length < 2) return; const { value } = await Swal.fire({ title: `${t('Change order of segment')} ${index + 1}`, @@ -207,7 +213,7 @@ const SegmentList = memo(({ const newOrder = parseInt(value, 10); updateSegOrder(index, newOrder - 1); } - } + }, [apparentCutSegments.length, t, updateSegOrder]); function renderFooter() { const getButtonColor = (seg) => getSegColor(seg).desaturate(0.3).lightness(darkMode ? 45 : 55).string(); @@ -340,12 +346,12 @@ const SegmentList = memo(({ onClick={onSegClick} addSegment={addSegment} onRemoveSelected={onRemoveSelected} - onRemovePress={() => removeCutSegment(index)} - onReorderPress={() => onReorderSegs(index)} - onLabelPress={() => onLabelSegment(index)} - jumpSegStart={() => jumpSegStart(index)} - jumpSegEnd={() => jumpSegEnd(index)} - updateOrder={(dir) => updateSegOrder(index, index + dir)} + onRemovePress={removeCutSegment} + onReorderPress={onReorderSegs} + onLabelPress={onLabelSegment} + jumpSegStart={jumpSegStart} + jumpSegEnd={jumpSegEnd} + updateSegOrder={updateSegOrder} getFrameCount={getFrameCount} formatTimecode={formatTimecode} currentSegIndex={currentSegIndex} diff --git a/src/Timeline.jsx b/src/Timeline.jsx index a943bd4f..6effe3cd 100644 --- a/src/Timeline.jsx +++ b/src/Timeline.jsx @@ -52,6 +52,11 @@ const CommandedTime = memo(({ commandedTimePercent }) => { ); }); +const timelineHeight = 36; + +const timeWrapperStyle = { position: 'absolute', height: timelineHeight, left: 0, right: 0, bottom: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none' }; +const timeStyle = { background: 'rgba(0,0,0,0.4)', borderRadius: 3, padding: '2px 4px', color: 'rgba(255, 255, 255, 0.8)' }; + const Timeline = memo(({ durationSafe, startTimeOffset, playerTime, commandedTime, relevantTime, zoom, neighbouringKeyFrames, seekAbs, apparentCutSegments, @@ -62,8 +67,6 @@ const Timeline = memo(({ }) => { const { t } = useTranslation(); - const timelineHeight = 36; - const { invertCutSegments } = useUserSettings(); const timelineScrollerRef = useRef(); @@ -328,8 +331,8 @@ const Timeline = memo(({ -
-
+
+
{formatTimeAndFrames(displayTime)}{isZoomed ? ` ${displayTimePercent}` : ''}
diff --git a/src/TopMenu.jsx b/src/TopMenu.jsx index 29fdd954..22abe0e3 100644 --- a/src/TopMenu.jsx +++ b/src/TopMenu.jsx @@ -1,8 +1,9 @@ import React, { memo, useCallback } from 'react'; import { IoIosSettings } from 'react-icons/io'; import { FaLock, FaUnlock } from 'react-icons/fa'; -import { IconButton, Button, CrossIcon, ListIcon, VolumeUpIcon, VolumeOffIcon } from 'evergreen-ui'; +import { IconButton, CrossIcon, ListIcon, VolumeUpIcon, VolumeOffIcon } from 'evergreen-ui'; import { useTranslation } from 'react-i18next'; +import Button from './components/Button'; import ExportModeButton from './components/ExportModeButton'; @@ -11,6 +12,9 @@ import { primaryTextColor, controlsBackground, darkModeTransition } from './colo import useUserSettings from './hooks/useUserSettings'; +const outFmtStyle = { height: 20, maxWidth: 100 }; +const exportModeStyle = { flexGrow: 0, flexBasis: 140 }; + const TopMenu = memo(({ filePath, fileFormat, copyAnyAudioTrack, toggleStripAudio, renderOutFmt, numStreamsToCopy, numStreamsTotal, setStreamsSelectorShown, toggleSettings, @@ -35,17 +39,20 @@ const TopMenu = memo(({ > {filePath && ( <> - )} @@ -53,35 +60,34 @@ const TopMenu = memo(({
{showClearWorkingDirButton && ( - )} {filePath && ( <> - {renderOutFmt({ height: 20, maxWidth: 100 })} + {renderOutFmt(outFmtStyle)} {!simpleMode && (isCustomFormatSelected || outFormatLocked) && renderFormatLock()} - + )} - +
); }); diff --git a/src/components/Button.jsx b/src/components/Button.jsx new file mode 100644 index 00000000..65b4223d --- /dev/null +++ b/src/components/Button.jsx @@ -0,0 +1,10 @@ +import React, { memo } from 'react'; + +import styles from './Button.module.css'; + +const Button = memo(({ type = 'button', ...props }) => ( + // eslint-disable-next-line react/jsx-props-no-spreading, react/button-has-type +