From e7d3de3a258170ec81e2b002f670962e65e1d21c Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Thu, 20 Feb 2020 18:41:01 +0800 Subject: [PATCH] implement undo/redo #176 --- package.json | 2 +- src/menu.js | 18 ++++++++++++++ src/renderer.jsx | 65 ++++++++++++++++++++++++++++++------------------ yarn.lock | 25 ++++++++----------- 4 files changed, 70 insertions(+), 40 deletions(-) diff --git a/package.json b/package.json index 9a325f84..dff61446 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "react-icons": "^3.9.0", "react-lottie": "^1.2.3", "react-sortable-hoc": "^1.5.3", - "react-use": "^13.24.0", + "react-use": "^13.26.1", "read-chunk": "^2.0.0", "string-to-stream": "^1.1.1", "strong-data-uri": "^1.0.5", diff --git a/src/menu.js b/src/menu.js index 8a6b5f3b..83cef4c5 100644 --- a/src/menu.js +++ b/src/menu.js @@ -74,6 +74,24 @@ module.exports = (app, mainWindow, newVersion) => { ], }; + const editSubMenu = menu.find(item => item.label === 'Edit').submenu; + editSubMenu.splice(editSubMenu.findIndex(item => item.label === 'Undo'), 1, { + label: 'Undo', + accelerator: 'CmdOrCtrl+Z', + click() { + mainWindow.webContents.send('undo'); + }, + }); + + editSubMenu.splice(editSubMenu.findIndex(item => item.label === 'Redo'), 1, { + label: 'Redo', + accelerator: 'Shift+CmdOrCtrl+Z', + click() { + mainWindow.webContents.send('redo'); + }, + }); + + menu.splice((process.platform === 'darwin' ? 1 : 0), 0, fileMenu); const helpIndex = menu.findIndex(item => item.role === 'help'); diff --git a/src/renderer.jsx b/src/renderer.jsx index f9e4f346..d5a22e60 100644 --- a/src/renderer.jsx +++ b/src/renderer.jsx @@ -7,10 +7,11 @@ import { AnimatePresence, motion } from 'framer-motion'; import Swal from 'sweetalert2'; import Lottie from 'react-lottie'; import { SideSheet, Button, Position } from 'evergreen-ui'; +import { useStateWithHistory } from 'react-use/lib/useStateWithHistory'; import fromPairs from 'lodash/fromPairs'; import clamp from 'lodash/clamp'; -import clone from 'lodash/clone'; +import cloneDeep from 'lodash/cloneDeep'; import sortBy from 'lodash/sortBy'; import flatMap from 'lodash/flatMap'; @@ -89,10 +90,6 @@ const App = memo(() => { const [playing, setPlaying] = useState(false); const [playerTime, setPlayerTime] = useState(); const [duration, setDuration] = useState(); - const [cutSegments, setCutSegments] = useState([createSegment()]); - const [currentSegIndex, setCurrentSegIndex] = useState(0); - const [cutStartTimeManual, setCutStartTimeManual] = useState(); - const [cutEndTimeManual, setCutEndTimeManual] = useState(); const [fileFormat, setFileFormat] = useState(); const [detectedFileFormat, setDetectedFileFormat] = useState(); const [rotation, setRotation] = useState(360); @@ -109,6 +106,16 @@ const App = memo(() => { const [commandedTime, setCommandedTime] = useState(0); const [ffmpegCommandLog, setFfmpegCommandLog] = useState([]); + // Segment related state + const [currentSegIndex, setCurrentSegIndex] = useState(0); + const [cutStartTimeManual, setCutStartTimeManual] = useState(); + const [cutEndTimeManual, setCutEndTimeManual] = useState(); + const [cutSegments, setCutSegments, cutSegmentsHistory] = useStateWithHistory( + [createSegment()], + 100, + ); + + // Preferences const [captureFormat, setCaptureFormat] = useState(configStore.get('captureFormat')); useEffect(() => configStore.set('captureFormat', captureFormat), [captureFormat]); @@ -194,8 +201,8 @@ const App = memo(() => { setWorking(false); setPlaying(false); setDuration(); - setCurrentSegIndex(0); - setCutSegments([createSegment()]); + cutSegmentsHistory.go(0); + setCutSegments([createSegment()]); // TODO this will cause two history items setCutStartTimeManual(); setCutEndTimeManual(); setFileFormat(); @@ -211,7 +218,7 @@ const App = memo(() => { setCopyStreamIdsByFile({}); setStreamsSelectorShown(false); setZoom(1); - }, []); + }, [cutSegmentsHistory, setCutSegments]); useEffect(() => () => { if (dummyVideoPath) unlink(dummyVideoPath).catch(console.error); @@ -245,8 +252,9 @@ const App = memo(() => { const haveInvalidSegs = invalidSegUuids.length > 0; - const currentCutSeg = cutSegments[currentSegIndex]; - const currentApparentCutSeg = apparentCutSegments[currentSegIndex]; + const currentSegIndexSafe = Math.min(currentSegIndex, cutSegments.length - 1); + const currentCutSeg = cutSegments[currentSegIndexSafe]; + const currentApparentCutSeg = apparentCutSegments[currentSegIndexSafe]; const areWeCutting = apparentCutSegments.length > 1 || isCuttingStart(currentApparentCutSeg.start) || isCuttingEnd(currentApparentCutSeg.end, duration); @@ -294,17 +302,17 @@ const App = memo(() => { })(); const setCutTime = useCallback((type, time) => { - const cloned = clone(cutSegments); - const currentSeg = cloned[currentSegIndex]; + const cloned = cloneDeep(cutSegments); + const currentSeg = currentCutSeg; if (type === 'start' && time >= getSegApparentEnd(currentSeg)) { throw new Error('Start time must precede end time'); } if (type === 'end' && time <= getSegApparentStart(currentSeg)) { throw new Error('Start time must precede end time'); } - cloned[currentSegIndex][type] = time; + cloned[currentSegIndexSafe][type] = time; setCutSegments(cloned); - }, [currentSegIndex, getSegApparentEnd, cutSegments]); + }, [currentSegIndexSafe, getSegApparentEnd, cutSegments, currentCutSeg, setCutSegments]); function formatTimecode(sec) { return formatDuration({ seconds: sec, fps: timecodeShowFrames ? detectedFps : undefined }); @@ -330,11 +338,10 @@ const App = memo(() => { }), ]; - const currentSegIndexNew = cutSegmentsNew.length - 1; setCutSegments(cutSegmentsNew); - setCurrentSegIndex(currentSegIndexNew); + setCurrentSegIndex(cutSegmentsNew.length - 1); }, [ - currentCutSeg, cutSegments, getCurrentTime, duration, + currentCutSeg, cutSegments, getCurrentTime, duration, setCutSegments, ]); const setCutStart = useCallback(() => { @@ -494,12 +501,10 @@ const App = memo(() => { if (cutSegments.length < 2) return; const cutSegmentsNew = [...cutSegments]; - cutSegmentsNew.splice(currentSegIndex, 1); + cutSegmentsNew.splice(currentSegIndexSafe, 1); - const currentSegIndexNew = Math.min(currentSegIndex, cutSegmentsNew.length - 1); - setCurrentSegIndex(currentSegIndexNew); setCutSegments(cutSegmentsNew); - }, [currentSegIndex, cutSegments]); + }, [currentSegIndexSafe, cutSegments, setCutSegments]); const jumpCutStart = () => seekAbs(currentApparentCutSeg.start); const jumpCutEnd = () => seekAbs(currentApparentCutSeg.end); @@ -961,12 +966,22 @@ const App = memo(() => { setStartTimeOffset(newStartTimeOffset); } + function undo() { + cutSegmentsHistory.back(); + } + + function redo() { + cutSegmentsHistory.forward(); + } + electron.ipcRenderer.on('file-opened', fileOpened); electron.ipcRenderer.on('close-file', closeFile); electron.ipcRenderer.on('html5ify', html5ify); electron.ipcRenderer.on('show-merge-dialog', showOpenAndMergeDialog2); electron.ipcRenderer.on('set-start-offset', setStartOffset); electron.ipcRenderer.on('extract-all-streams', extractAllStreams); + electron.ipcRenderer.on('undo', undo); + electron.ipcRenderer.on('redo', redo); return () => { electron.ipcRenderer.removeListener('file-opened', fileOpened); @@ -975,10 +990,12 @@ const App = memo(() => { electron.ipcRenderer.removeListener('show-merge-dialog', showOpenAndMergeDialog2); electron.ipcRenderer.removeListener('set-start-offset', setStartOffset); electron.ipcRenderer.removeListener('extract-all-streams', extractAllStreams); + electron.ipcRenderer.removeListener('undo', undo); + electron.ipcRenderer.removeListener('redo', redo); }; }, [ load, mergeFiles, outputDir, filePath, customOutDir, startTimeOffset, getHtml5ifiedPath, - createDummyVideo, resetState, extractAllStreams, userOpenFiles, + createDummyVideo, resetState, extractAllStreams, userOpenFiles, cutSegmentsHistory, ]); async function showAddStreamSourceDialog() { @@ -1455,7 +1472,7 @@ const App = memo(() => { segNum={i} color={seg.color} onSegClick={currentSegIndexNew => setCurrentSegIndex(currentSegIndexNew)} - isActive={i === currentSegIndex} + isActive={i === currentSegIndexSafe} duration={durationSafe} cutStart={seg.start} cutEnd={seg.end} @@ -1575,7 +1592,7 @@ const App = memo(() => { size={30} style={{ margin: '0 5px', background: cutSegments.length < 2 ? undefined : segBgColor, borderRadius: 3, color: 'white' }} role="button" - title={`Delete current segment ${currentSegIndex + 1}`} + title={`Delete current segment ${currentSegIndexSafe + 1}`} onClick={removeCutSegment} /> diff --git a/yarn.lock b/yarn.lock index b5cef9e2..3374c26d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -260,10 +260,10 @@ "@types/prop-types" "*" csstype "^2.2.0" -"@xobotyi/scrollbar-width@1.8.2": - version "1.8.2" - resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.8.2.tgz#056946ac41ade4885c576619c8d70c46c77e9683" - integrity sha512-RV6+4hR29oMaPCvSYFUvzOvlsrg2s2k5NE9tNERs+4nFIC9dRXxs+lL2CcaRTbl3yQxKwAZ8Cd+qMI8aUu9TFw== +"@xobotyi/scrollbar-width@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.0.tgz#2a5d02f15c7f5624339e5d690aba432bfd9e79f0" + integrity sha512-W8oNXd3HkW9eQHxk+47iRx4aqd0yIV9NoeykUTd0uE0sYx3LOAQE7rfHOd8xtMP7IADfLIdG0o0H1sXvHUF7dw== abbrev@1: version "1.1.1" @@ -4846,11 +4846,6 @@ react-event-listener@^0.5.1: prop-types "^15.6.0" warning "^3.0.0" -react-fast-compare@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" - integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== - react-hammerjs@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/react-hammerjs/-/react-hammerjs-1.0.1.tgz#bc1ed9e9ef7da057163fb169ce12917b6d6ca7d8" @@ -4919,18 +4914,18 @@ react-transition-group@^2.5.0: prop-types "^15.6.2" react-lifecycles-compat "^3.0.4" -react-use@^13.24.0: - version "13.24.0" - resolved "https://registry.yarnpkg.com/react-use/-/react-use-13.24.0.tgz#f4574e26cfaaad65e3f04c0d5ff80c1836546236" - integrity sha512-p8GsZuMdz8OeIGzuYLm6pzJysKOhNyQjCUG6SHrQGk6o6ghy/RVGSqnmxVacNbN9166S0+9FsM1N1yH9GzWlgg== +react-use@^13.26.1: + version "13.26.1" + resolved "https://registry.yarnpkg.com/react-use/-/react-use-13.26.1.tgz#a26e51b26ebe1a3a00cadfe4d7f15c25bb19780b" + integrity sha512-hDc4s8w4WI8G7c1BX+IsrdQFcZPfCHE/6oLpGPtcIPoxVhwj4QvVmNE8RnsnddBJ57HN8Xvkc3jp/8Z/4OB53w== dependencies: "@types/js-cookie" "2.2.4" - "@xobotyi/scrollbar-width" "1.8.2" + "@xobotyi/scrollbar-width" "1.9.0" copy-to-clipboard "^3.2.0" + fast-deep-equal "^3.1.1" fast-shallow-equal "^1.0.0" js-cookie "^2.2.1" nano-css "^5.2.1" - react-fast-compare "^2.0.4" resize-observer-polyfill "^1.5.1" screenfull "^5.0.0" set-harmonic-interval "^1.0.1"