kopia lustrzana https://github.com/mifi/lossless-cut
improvements
- implement drag drop sort #392 - scroll segment into view - refactorsnyk-fix-1c00065eb8541253ee2ceeb462d92a2c
rodzic
dd80971a61
commit
f6b824d04d
|
@ -75,8 +75,11 @@
|
|||
"react-icons": "^4.1.0",
|
||||
"react-scripts": "^3.4.0",
|
||||
"react-sortable-hoc": "^1.5.3",
|
||||
"react-sortablejs": "^6.0.0",
|
||||
"react-use": "^13.26.1",
|
||||
"scroll-into-view-if-needed": "^2.2.28",
|
||||
"smpte-timecode": "^1.2.3",
|
||||
"sortablejs": "^1.13.0",
|
||||
"strong-data-uri": "^1.0.5",
|
||||
"svg2png": "^4.1.1",
|
||||
"sweetalert2": "^9.10.10",
|
||||
|
|
29
src/App.jsx
29
src/App.jsx
|
@ -123,7 +123,7 @@ const App = memo(() => {
|
|||
const [thumbnails, setThumbnails] = useState([]);
|
||||
const [shortestFlag, setShortestFlag] = useState(false);
|
||||
const [zoomWindowStartTime, setZoomWindowStartTime] = useState(0);
|
||||
const [enabledSegmentUuids, setSegmentsExportEnabled] = useState();
|
||||
const [enabledSegmentIds, setSegmentIdsEnabled] = useState();
|
||||
|
||||
const [keyframesEnabled, setKeyframesEnabled] = useState(true);
|
||||
const [waveformEnabled, setWaveformEnabled] = useState(false);
|
||||
|
@ -324,7 +324,8 @@ const App = memo(() => {
|
|||
function invertSegmentsSafe() {
|
||||
if (haveInvalidSegs || !isDurationValid(duration)) return undefined;
|
||||
if (!isDurationValid(duration)) return undefined;
|
||||
return invertSegments(sortedCutSegments, duration);
|
||||
const inverted = invertSegments(sortedCutSegments, duration);
|
||||
return inverted.map((seg) => ({ ...seg, segId: `${seg.start}-${seg.end}` }));
|
||||
}
|
||||
return invertSegmentsSafe() || [];
|
||||
}, [duration, haveInvalidSegs, sortedCutSegments]);
|
||||
|
@ -363,6 +364,13 @@ const App = memo(() => {
|
|||
setCurrentSegIndex(newOrder);
|
||||
}, [cutSegments, setCutSegments]);
|
||||
|
||||
const updateSegOrders = useCallback((newOrders) => {
|
||||
const newSegments = sortBy(cutSegments, (seg) => newOrders.indexOf(seg.segId));
|
||||
const newCurrentSegIndex = newOrders.indexOf(currentCutSeg.segId);
|
||||
setCutSegments(newSegments);
|
||||
if (newCurrentSegIndex >= 0 && newCurrentSegIndex < newSegments.length) setCurrentSegIndex(newCurrentSegIndex);
|
||||
}, [cutSegments, setCutSegments, currentCutSeg]);
|
||||
|
||||
const reorderSegsByStartTime = useCallback(() => {
|
||||
setCutSegments(sortBy(cutSegments, getSegApparentStart));
|
||||
}, [cutSegments, setCutSegments]);
|
||||
|
@ -770,7 +778,7 @@ const App = memo(() => {
|
|||
setZoom(1);
|
||||
setShortestFlag(false);
|
||||
setZoomWindowStartTime(0);
|
||||
setSegmentsExportEnabled();
|
||||
setSegmentIdsEnabled();
|
||||
setHideCanvasPreview(false);
|
||||
|
||||
setExportConfirmVisible(false);
|
||||
|
@ -901,17 +909,17 @@ const App = memo(() => {
|
|||
const outSegments = useMemo(() => (invertCutSegments ? inverseCutSegments : apparentCutSegments),
|
||||
[invertCutSegments, inverseCutSegments, apparentCutSegments]);
|
||||
|
||||
// enabledSegmentUuids undefined means all are enabled
|
||||
const enabledSegmentUuidsEffective = enabledSegmentUuids || Object.fromEntries(cutSegments.map((s) => [s.uuid, true]));
|
||||
// enabledSegmentIds undefined means all are enabled
|
||||
const enabledSegmentIdsEffective = enabledSegmentIds || Object.fromEntries(cutSegments.map((s) => [s.segId, true]));
|
||||
// For invertCutSegments we do not support filtering
|
||||
const enabledOutSegmentsRaw = useMemo(() => (invertCutSegments ? outSegments : outSegments.filter((s) => enabledSegmentUuidsEffective[s.uuid])), [outSegments, invertCutSegments, enabledSegmentUuidsEffective]);
|
||||
const enabledOutSegmentsRaw = useMemo(() => (invertCutSegments ? outSegments : outSegments.filter((s) => enabledSegmentIdsEffective[s.segId])), [outSegments, invertCutSegments, enabledSegmentIdsEffective]);
|
||||
// If user has selected none to export, it makes no sense, so export all instead
|
||||
const enabledOutSegments = enabledOutSegmentsRaw.length > 0 ? enabledOutSegmentsRaw : outSegments;
|
||||
|
||||
const onExportSingleSegmentClick = useCallback((seg) => setSegmentsExportEnabled({ [seg.uuid]: true }), []);
|
||||
const onExportSegmentEnabledToggle = useCallback((seg) => setSegmentsExportEnabled({ ...enabledSegmentUuidsEffective, [seg.uuid]: !enabledSegmentUuidsEffective[seg.uuid] }), [enabledSegmentUuidsEffective]);
|
||||
const onExportSegmentDisableAll = useCallback(() => setSegmentsExportEnabled({}), []);
|
||||
const onExportSegmentEnableAll = useCallback(() => setSegmentsExportEnabled(), []);
|
||||
const onExportSingleSegmentClick = useCallback((seg) => setSegmentIdsEnabled({ [seg.segId]: true }), []);
|
||||
const onExportSegmentEnabledToggle = useCallback((seg) => setSegmentIdsEnabled({ ...enabledSegmentIdsEffective, [seg.segId]: !enabledSegmentIdsEffective[seg.segId] }), [enabledSegmentIdsEffective]);
|
||||
const onExportSegmentDisableAll = useCallback(() => setSegmentIdsEnabled({}), []);
|
||||
const onExportSegmentEnableAll = useCallback(() => setSegmentIdsEnabled(), []);
|
||||
|
||||
const generateOutSegFileNames = useCallback(({ segments = enabledOutSegments, template }) => (
|
||||
segments.map(({ start, end, name = '' }, i) => {
|
||||
|
@ -2191,6 +2199,7 @@ const App = memo(() => {
|
|||
invertCutSegments={invertCutSegments}
|
||||
onSegClick={setCurrentSegIndex}
|
||||
updateSegOrder={updateSegOrder}
|
||||
updateSegOrders={updateSegOrders}
|
||||
onLabelSegmentPress={onLabelSegmentPress}
|
||||
currentCutSeg={currentCutSeg}
|
||||
segmentAtCursor={segmentAtCursor}
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import React, { memo, useMemo, useRef } from 'react';
|
||||
import prettyMs from 'pretty-ms';
|
||||
import { FaSave, FaPlus, FaMinus, FaTag, FaSortNumericDown, FaAngleRight, FaArrowCircleUp, FaArrowCircleDown, FaCheck, FaTimes } from 'react-icons/fa';
|
||||
import { FaSave, FaPlus, FaMinus, FaTag, FaSortNumericDown, FaAngleRight, FaCheck, FaTimes } from 'react-icons/fa';
|
||||
import { AiOutlineSplitCells } from 'react-icons/ai';
|
||||
import { motion } from 'framer-motion';
|
||||
import Swal from 'sweetalert2';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useContextMenu from './hooks/useContextMenu';
|
||||
import { ReactSortable } from 'react-sortablejs';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import scrollIntoView from 'scroll-into-view-if-needed';
|
||||
|
||||
import useContextMenu from './hooks/useContextMenu';
|
||||
import { saveColor } from './colors';
|
||||
import { getSegColors } from './util/colors';
|
||||
|
||||
|
@ -51,6 +55,10 @@ const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCou
|
|||
|
||||
const isActive = !invertCutSegments && currentSegIndex === index;
|
||||
|
||||
useDebounce(() => {
|
||||
if (isActive && ref.current) scrollIntoView(ref.current, { behavior: 'smooth', scrollMode: 'if-needed' });
|
||||
}, 300, [isActive]);
|
||||
|
||||
function renderNumber() {
|
||||
if (invertCutSegments) return <FaSave style={{ color: saveColor, marginRight: 5, verticalAlign: 'middle' }} size={14} />;
|
||||
|
||||
|
@ -61,15 +69,6 @@ const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCou
|
|||
|
||||
const timeStr = useMemo(() => `${formatTimecode(seg.start)} - ${formatTimecode(seg.end)}`, [seg.start, seg.end, formatTimecode]);
|
||||
|
||||
function onSegOrderDecreasePress(e) {
|
||||
updateOrder(-1);
|
||||
e.stopPropagation();
|
||||
}
|
||||
function onSegOrderIncreasePress(e) {
|
||||
updateOrder(1);
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function onDoubleClick() {
|
||||
if (invertCutSegments) return;
|
||||
if (!enabled) {
|
||||
|
@ -103,12 +102,6 @@ const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCou
|
|||
({Math.floor(durationMs)} ms, {getFrameCount(duration)} frames)
|
||||
</div>
|
||||
|
||||
{isActive && (
|
||||
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }} style={{ position: 'absolute', right: 0, bottom: 0, display: 'flex', flexDirection: 'column' }}>
|
||||
<FaArrowCircleUp size={20} role="button" onClick={onSegOrderDecreasePress} />
|
||||
<FaArrowCircleDown size={20} role="button" onClick={onSegOrderIncreasePress} />
|
||||
</motion.div>
|
||||
)}
|
||||
{!enabled && !invertCutSegments && (
|
||||
<div style={{ position: 'absolute', pointerEvents: 'none', top: 0, right: 0, bottom: 0, left: 0, overflow: 'hidden', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<FaTimes style={{ fontSize: 100, color: 'rgba(255,0,0,0.8)' }} />
|
||||
|
@ -121,13 +114,20 @@ const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCou
|
|||
const SegmentList = memo(({
|
||||
formatTimecode, cutSegments, outSegments, getFrameCount, onSegClick,
|
||||
currentSegIndex, invertCutSegments,
|
||||
updateSegOrder, addCutSegment, removeCutSegment,
|
||||
updateSegOrder, updateSegOrders, addCutSegment, removeCutSegment,
|
||||
onLabelSegmentPress, currentCutSeg, segmentAtCursor, toggleSideBar, splitCurrentSegment,
|
||||
enabledOutSegments, enabledOutSegmentsRaw, onExportSingleSegmentClick, onExportSegmentEnabledToggle, onExportSegmentDisableAll, onExportSegmentEnableAll,
|
||||
jumpSegStart, jumpSegEnd, simpleMode,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const sortableList = outSegments.map((seg) => ({ id: seg.segId, seg }));
|
||||
|
||||
function setSortableList(newList) {
|
||||
if (isEqual(outSegments.map((s) => s.segId), newList.map((l) => l.id))) return; // No change
|
||||
updateSegOrders(newList.map((list) => list.id));
|
||||
}
|
||||
|
||||
let headerText = t('Segments to export:');
|
||||
if (outSegments.length === 0) {
|
||||
if (invertCutSegments) headerText = t('Make sure you have no overlapping segments.');
|
||||
|
@ -154,12 +154,12 @@ const SegmentList = memo(({
|
|||
}
|
||||
}
|
||||
|
||||
const renderFooter = () => {
|
||||
function renderFooter() {
|
||||
const { segActiveBgColor: currentSegActiveBgColor } = getSegColors(currentCutSeg);
|
||||
const { segActiveBgColor: segmentAtCursorActiveBgColor } = getSegColors(segmentAtCursor);
|
||||
|
||||
function renderExportEnabledCheckBox() {
|
||||
const segmentExportEnabled = currentCutSeg && enabledOutSegmentsRaw.some((s) => s.uuid === currentCutSeg.uuid);
|
||||
const segmentExportEnabled = currentCutSeg && enabledOutSegmentsRaw.some((s) => s.segId === currentCutSeg.segId);
|
||||
const Icon = segmentExportEnabled ? FaCheck : FaTimes;
|
||||
|
||||
return <Icon size={24} title={segmentExportEnabled ? t('Include this segment in export') : t('Exclude this segment from export')} style={{ ...buttonBaseStyle, backgroundColor: currentSegActiveBgColor }} role="button" onClick={() => onExportSegmentEnabledToggle(currentCutSeg)} />;
|
||||
|
@ -221,7 +221,7 @@ const SegmentList = memo(({
|
|||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -238,34 +238,35 @@ const SegmentList = memo(({
|
|||
{headerText}
|
||||
</div>
|
||||
|
||||
{outSegments.map((seg, index) => {
|
||||
const id = seg.uuid || `${seg.start}`;
|
||||
const enabled = !invertCutSegments && enabledOutSegmentsRaw.includes(seg);
|
||||
return (
|
||||
<Segment
|
||||
key={id}
|
||||
seg={seg}
|
||||
index={index}
|
||||
enabled={enabled}
|
||||
onClick={onSegClick}
|
||||
addCutSegment={addCutSegment}
|
||||
onRemovePress={() => removeCutSegment(index)}
|
||||
onReorderPress={() => onReorderSegsPress(index)}
|
||||
onLabelPress={() => onLabelSegmentPress(index)}
|
||||
jumpSegStart={() => jumpSegStart(index)}
|
||||
jumpSegEnd={() => jumpSegEnd(index)}
|
||||
updateOrder={(dir) => updateSegOrder(index, index + dir)}
|
||||
getFrameCount={getFrameCount}
|
||||
formatTimecode={formatTimecode}
|
||||
currentSegIndex={currentSegIndex}
|
||||
invertCutSegments={invertCutSegments}
|
||||
onExportSingleSegmentClick={onExportSingleSegmentClick}
|
||||
onExportSegmentEnabledToggle={onExportSegmentEnabledToggle}
|
||||
onExportSegmentDisableAll={onExportSegmentDisableAll}
|
||||
onExportSegmentEnableAll={onExportSegmentEnableAll}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<ReactSortable list={sortableList} setList={setSortableList} sort={!invertCutSegments}>
|
||||
{sortableList.map(({ id, seg }, index) => {
|
||||
const enabled = !invertCutSegments && enabledOutSegmentsRaw.includes(seg);
|
||||
return (
|
||||
<Segment
|
||||
key={id}
|
||||
seg={seg}
|
||||
index={index}
|
||||
enabled={enabled}
|
||||
onClick={onSegClick}
|
||||
addCutSegment={addCutSegment}
|
||||
onRemovePress={() => removeCutSegment(index)}
|
||||
onReorderPress={() => onReorderSegsPress(index)}
|
||||
onLabelPress={() => onLabelSegmentPress(index)}
|
||||
jumpSegStart={() => jumpSegStart(index)}
|
||||
jumpSegEnd={() => jumpSegEnd(index)}
|
||||
updateOrder={(dir) => updateSegOrder(index, index + dir)}
|
||||
getFrameCount={getFrameCount}
|
||||
formatTimecode={formatTimecode}
|
||||
currentSegIndex={currentSegIndex}
|
||||
invertCutSegments={invertCutSegments}
|
||||
onExportSingleSegmentClick={onExportSingleSegmentClick}
|
||||
onExportSegmentEnabledToggle={onExportSegmentEnabledToggle}
|
||||
onExportSegmentDisableAll={onExportSegmentDisableAll}
|
||||
onExportSegmentEnableAll={onExportSegmentEnableAll}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ReactSortable>
|
||||
</div>
|
||||
|
||||
{outSegments.length > 0 && renderFooter()}
|
||||
|
|
|
@ -233,7 +233,7 @@ const Timeline = memo(({
|
|||
|
||||
return (
|
||||
<TimelineSeg
|
||||
key={seg.uuid}
|
||||
key={seg.segId}
|
||||
segNum={i}
|
||||
segBgColor={segBgColor}
|
||||
segActiveBgColor={segActiveBgColor}
|
||||
|
|
|
@ -8,7 +8,7 @@ export const createSegment = ({ start, end, name } = {}) => ({
|
|||
end,
|
||||
name: name || '',
|
||||
color: generateColor(),
|
||||
uuid: uuid.v4(),
|
||||
segId: uuid.v4(),
|
||||
});
|
||||
|
||||
export const createInitialCutSegments = () => [createSegment()];
|
||||
|
|
30
yarn.lock
30
yarn.lock
|
@ -3580,6 +3580,11 @@ compression@^1.7.4:
|
|||
safe-buffer "5.1.2"
|
||||
vary "~1.1.2"
|
||||
|
||||
compute-scroll-into-view@^1.0.17:
|
||||
version "1.0.17"
|
||||
resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz#6a88f18acd9d42e9cf4baa6bec7e0522607ab7ab"
|
||||
integrity sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg==
|
||||
|
||||
compute-scroll-into-view@^1.0.9:
|
||||
version "1.0.13"
|
||||
resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.13.tgz#be1b1663b0e3f56cd5f7713082549f562a3477e2"
|
||||
|
@ -11082,6 +11087,14 @@ react-sortable-hoc@^1.5.3:
|
|||
invariant "^2.2.4"
|
||||
prop-types "^15.5.7"
|
||||
|
||||
react-sortablejs@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-sortablejs/-/react-sortablejs-6.0.0.tgz#ba75ded6dce3fa1b5b3b52c70d1928fcdee2003d"
|
||||
integrity sha512-vzi+TWOnofcYg+dYnC/Iz/ZZkBGG76uM6KaLwuAqBk0349JQxIy3PZizbK0TJdLlK6NnLt4CiEyyQXSSnVYvEw==
|
||||
dependencies:
|
||||
classnames "^2.2.6"
|
||||
tiny-invariant "^1.1.0"
|
||||
|
||||
react-syntax-highlighter@^13.0.0:
|
||||
version "13.4.0"
|
||||
resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-13.4.0.tgz#299996b15f27bde322079c429073fca0e8eb10c6"
|
||||
|
@ -11883,6 +11896,13 @@ screenfull@^5.0.0:
|
|||
resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.0.2.tgz#b9acdcf1ec676a948674df5cd0ff66b902b0bed7"
|
||||
integrity sha512-cCF2b+L/mnEiORLN5xSAz6H3t18i2oHh9BA8+CQlAh5DRw2+NFAGQJOSYbcGw8B2k04g/lVvFcfZ83b3ysH5UQ==
|
||||
|
||||
scroll-into-view-if-needed@^2.2.28:
|
||||
version "2.2.28"
|
||||
resolved "https://registry.yarnpkg.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.28.tgz#5a15b2f58a52642c88c8eca584644e01703d645a"
|
||||
integrity sha512-8LuxJSuFVc92+0AdNv4QOxRL4Abeo1DgLnGNkn1XlaujPH/3cCFz3QI60r2VNu4obJJROzgnIUw5TKQkZvZI1w==
|
||||
dependencies:
|
||||
compute-scroll-into-view "^1.0.17"
|
||||
|
||||
select-hose@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
|
||||
|
@ -12201,6 +12221,11 @@ sort-keys@^1.0.0:
|
|||
dependencies:
|
||||
is-plain-obj "^1.0.0"
|
||||
|
||||
sortablejs@^1.13.0:
|
||||
version "1.13.0"
|
||||
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.13.0.tgz#3ab2473f8c69ca63569e80b1cd1b5669b51269e9"
|
||||
integrity sha512-RBJirPY0spWCrU5yCmWM1eFs/XgX2J5c6b275/YyxFRgnzPhKl/TDeU2hNR8Dt7ITq66NRPM4UlOt+e5O4CFHg==
|
||||
|
||||
sortobject@^4.0.0:
|
||||
version "4.14.0"
|
||||
resolved "https://registry.yarnpkg.com/sortobject/-/sortobject-4.14.0.tgz#1c1b09862033c93731198a4f7d25eb5140328123"
|
||||
|
@ -12936,6 +12961,11 @@ tiny-emitter@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
|
||||
integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
|
||||
|
||||
tiny-invariant@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
|
||||
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
|
||||
|
||||
tinycolor2@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8"
|
||||
|
|
Ładowanie…
Reference in New Issue