Skip to content

Commit

Permalink
improvements
Browse files Browse the repository at this point in the history
- implement drag drop sort #392
- scroll segment into view
- refactor
  • Loading branch information
mifi committed Mar 27, 2021
1 parent dd80971 commit f6b824d
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 61 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
29 changes: 19 additions & 10 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -770,7 +778,7 @@ const App = memo(() => {
setZoom(1);
setShortestFlag(false);
setZoomWindowStartTime(0);
setSegmentsExportEnabled();
setSegmentIdsEnabled();
setHideCanvasPreview(false);

setExportConfirmVisible(false);
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -2191,6 +2199,7 @@ const App = memo(() => {
invertCutSegments={invertCutSegments}
onSegClick={setCurrentSegIndex}
updateSegOrder={updateSegOrder}
updateSegOrders={updateSegOrders}
onLabelSegmentPress={onLabelSegmentPress}
currentCutSeg={currentCutSeg}
segmentAtCursor={segmentAtCursor}
Expand Down
99 changes: 50 additions & 49 deletions src/SegmentList.jsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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} />;

Expand All @@ -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) {
Expand Down Expand Up @@ -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)' }} />
Expand All @@ -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.');
Expand All @@ -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)} />;
Expand Down Expand Up @@ -221,7 +221,7 @@ const SegmentList = memo(({
</div>
</>
);
};
}

return (
<>
Expand All @@ -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()}
Expand Down
2 changes: 1 addition & 1 deletion src/Timeline.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ const Timeline = memo(({

return (
<TimelineSeg
key={seg.uuid}
key={seg.segId}
segNum={i}
segBgColor={segBgColor}
segActiveBgColor={segActiveBgColor}
Expand Down
2 changes: 1 addition & 1 deletion src/segments.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()];
Expand Down
30 changes: 30 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit f6b824d

Please sign in to comment.