diff --git a/.eslintrc b/.eslintrc index bdcaded3..e48dd403 100644 --- a/.eslintrc +++ b/.eslintrc @@ -6,9 +6,9 @@ "browser": true, }, "rules": { - "no-alert": 0, "no-console": 0, "react/destructuring-assignment": 0, + "react/forbid-prop-types": [1, { "forbid": ["any"] }], }, "plugins": [ "react" diff --git a/src/ffmpeg.js b/src/ffmpeg.js index 9eb4cc2f..51cfbaf2 100644 --- a/src/ffmpeg.js +++ b/src/ffmpeg.js @@ -2,7 +2,6 @@ const execa = require('execa'); const bluebird = require('bluebird'); const which = bluebird.promisify(require('which')); const path = require('path'); -const fs = require('fs'); const fileType = require('file-type'); const readChunk = require('read-chunk'); const _ = require('lodash'); @@ -11,14 +10,6 @@ const moment = require('moment'); const util = require('./util'); -bluebird.promisifyAll(fs); - - -function showFfmpegFail(err) { - alert(`Failed to run ffmpeg:\n${err.stack}`); - console.error(err.stack); -} - function getWithExt(name) { return process.platform === 'win32' ? `${name}.exe` : name; } @@ -187,6 +178,5 @@ function getFormat(filePath) { module.exports = { cut, getFormat, - showFfmpegFail, html5ify, }; diff --git a/src/renderer.jsx b/src/renderer.jsx index 22e3e11a..98377f62 100644 --- a/src/renderer.jsx +++ b/src/renderer.jsx @@ -15,7 +15,10 @@ const classnames = require('classnames'); const captureFrame = require('./capture-frame'); const ffmpeg = require('./ffmpeg'); -const util = require('./util'); + +const { + getOutPath, parseDuration, formatDuration, toast, errorToast, showFfmpegFail, +} = require('./util'); const { dialog } = electron.remote; @@ -122,30 +125,36 @@ class App extends React.Component { ...globalState, }; - const load = (filePath, html5FriendlyPath) => { + const load = async (filePath, html5FriendlyPath) => { const { working } = this.state; console.log('Load', { filePath, html5FriendlyPath }); - if (working) return alert('I\'m busy'); + if (working) { + errorToast('I\'m busy'); + return; + } this.resetState(); this.setState({ working: true }); - return ffmpeg.getFormat(filePath) - .then((fileFormat) => { - if (!fileFormat) return alert('Unsupported file'); + try { + const fileFormat = await ffmpeg.getFormat(filePath); + if (!fileFormat) { + errorToast('Unsupported file'); + return; + } setFileNameTitle(filePath); - return this.setState({ filePath, html5FriendlyPath, fileFormat }); - }) - .catch((err) => { + this.setState({ filePath, html5FriendlyPath, fileFormat }); + } catch (err) { if (err.code === 1 || err.code === 'ENOENT') { - alert('Unsupported file'); + errorToast('Unsupported file'); return; } - ffmpeg.showFfmpegFail(err); - }) - .finally(() => this.setState({ working: false })); + showFfmpegFail(err); + } finally { + this.setState({ working: false }); + } }; electron.ipcRenderer.on('file-opened', (event, filePaths) => { @@ -159,12 +168,12 @@ class App extends React.Component { try { this.setState({ working: true }); - const html5ifiedPath = util.getOutPath(customOutDir, filePath, 'html5ified.mp4'); + const html5ifiedPath = getOutPath(customOutDir, filePath, 'html5ified.mp4'); await ffmpeg.html5ify(filePath, html5ifiedPath, encodeVideo); this.setState({ working: false }); load(filePath, html5ifiedPath); } catch (err) { - alert('Failed to html5ify file'); + errorToast('Failed to html5ify file'); console.error('Failed to html5ify file', err); this.setState({ working: false }); } @@ -184,7 +193,7 @@ class App extends React.Component { return undefined; } - const duration = util.parseDuration(value); + const duration = parseDuration(value); // Invalid, try again if (duration === undefined) return promptTimeOffset(value); @@ -194,7 +203,7 @@ class App extends React.Component { electron.ipcRenderer.on('set-start-offset', async () => { const { startTimeOffset: startTimeOffsetOld } = this.state; const startTimeOffset = await promptTimeOffset( - startTimeOffsetOld !== undefined ? util.formatDuration(startTimeOffsetOld) : undefined, + startTimeOffsetOld !== undefined ? formatDuration(startTimeOffsetOld) : undefined, ); if (startTimeOffset === undefined) { @@ -210,7 +219,10 @@ class App extends React.Component { document.body.ondrop = (ev) => { ev.preventDefault(); - if (ev.dataTransfer.files.length !== 1) return; + if (ev.dataTransfer.files.length !== 1) { + errorToast('Please drop only one file'); + return; + } load(ev.dataTransfer.files[0].path); }; @@ -337,12 +349,13 @@ class App extends React.Component { return video.play().catch((err) => { console.log(err); if (err.name === 'NotSupportedError') { - alert('This video format or codec is not supported. Try to convert it to a friendly format/codec in the player from the "File" menu. Note that this will only create a temporary, low quality encoded file used for previewing your cuts, and will not affect the final cut. The final cut will still be lossless. Audio is also removed to make it faster, but only in the preview.'); + toast({ type: 'error', title: 'This format/codec is not supported. Try to convert it to a friendly format/codec in the player from the "File" menu. Note that this will only create a temporary, low quality encoded file used for previewing your cuts, and will not affect the final cut. The final cut will still be lossless. Audio is also removed to make it faster, but only in the preview.', timer: 10000 }); } }); } deleteSourceClick = async () => { + // eslint-disable-next-line no-alert if (this.state.working || !window.confirm('Are you sure you want to move the source file to trash?')) return; const { filePath } = this.state; @@ -352,7 +365,10 @@ class App extends React.Component { } cutClick = async () => { - if (this.state.working) return alert('I\'m busy'); + if (this.state.working) { + errorToast('I\'m busy'); + return; + } const { cutStartTime, cutEndTime, filePath, customOutDir, fileFormat, duration, includeAllStreams, @@ -362,12 +378,13 @@ class App extends React.Component { const rotation = this.isRotationSet() ? this.getRotation() : undefined; if (!this.isCutRangeValid()) { - return alert('Start time must be before end time'); + errorToast('Start time must be before end time'); + return; } this.setState({ working: true }); try { - return await ffmpeg.cut({ + await ffmpeg.cut({ customOutDir, filePath, format: fileFormat, @@ -386,21 +403,26 @@ class App extends React.Component { console.error('stderr:', err.stderr); if (err.code === 1 || err.code === 'ENOENT') { - return alert('Whoops! ffmpeg was unable to cut this video. It may be of an unknown format or codec combination'); + errorToast('Whoops! ffmpeg was unable to cut this video. It may be of an unknown format or codec combination'); + return; } - return ffmpeg.showFfmpegFail(err); + showFfmpegFail(err); } finally { this.setState({ working: false }); } } - capture = () => { + capture = async () => { const { filePath, customOutDir: outputDir, currentTime, captureFormat, } = this.state; if (!filePath) return; - captureFrame(outputDir, filePath, getVideo(), currentTime, captureFormat) - .catch(err => alert(err)); + try { + await captureFrame(outputDir, filePath, getVideo(), currentTime, captureFormat); + } catch (err) { + console.error(err); + errorToast('Failed to capture frame'); + } } changePlaybackRate(dir) { @@ -448,7 +470,7 @@ class App extends React.Component { return; } - const time = util.parseDuration(text); + const time = parseDuration(text); if (time === undefined) { this.setState({ [cutTimeManualKey]: text }); return; @@ -470,7 +492,7 @@ class App extends React.Component { onChange={e => handleCutTimeInput(e.target.value)} value={isCutTimeManualSet() ? this.state[cutTimeManualKey] - : util.formatDuration(cutTime + this.state.startTimeOffset) + : formatDuration(cutTime + this.state.startTimeOffset) } /> ); @@ -539,7 +561,7 @@ class App extends React.Component { ) } -
{util.formatDuration(this.getOffsetCurrentTime())}
+
{formatDuration(this.getOffsetCurrentTime())}
diff --git a/src/util.js b/src/util.js index bfeb2185..649acde5 100644 --- a/src/util.js +++ b/src/util.js @@ -1,6 +1,7 @@ const _ = require('lodash'); const path = require('path'); const fs = require('fs'); +const swal = require('sweetalert2'); function formatDuration(_seconds, fileNameFriendly) { const seconds = _seconds || 0; @@ -57,10 +58,31 @@ async function transferTimestampsWithOffset(inPath, outPath, offset) { } } +const toast = swal.mixin({ + toast: true, + position: 'top', + showConfirmButton: false, + timer: 3000, +}); + +const errorToast = title => toast({ + type: 'error', + title, +}); + +async function showFfmpegFail(err) { + console.error(err); + return errorToast(`Failed to run ffmpeg: ${err.stack}`); +} + + module.exports = { formatDuration, parseDuration, getOutPath, transferTimestamps, transferTimestampsWithOffset, + toast, + errorToast, + showFfmpegFail, }; diff --git a/yarn.lock b/yarn.lock index 1d68c7cd..670122bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -57,6 +57,13 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.1.2.tgz#85c5c47af6d244fab77bce6b9bd830e38c978409" integrity sha512-x5HFsW+E/nQalGMw7hu+fvPqnBeBaIr0lWJ2SG0PPL2j+Pm9lYvCrsZJGIgauPIENx0v10INIyFjmSNUD/gSqQ== +"@babel/runtime@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.2.0.tgz#b03e42eeddf5898e00646e4c840fa07ba8dcad7f" + integrity sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg== + dependencies: + regenerator-runtime "^0.12.0" + "@babel/template@^7.1.0": version "7.1.2" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.1.2.tgz#090484a574fef5a2d2d7726a674eceda5c5b5644" @@ -2733,6 +2740,13 @@ invariant@^2.2.2: dependencies: loose-envify "^1.0.0" +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + invert-kv@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" @@ -3847,7 +3861,7 @@ prop-types@^15.5.10: loose-envify "^1.3.1" object-assign "^4.1.1" -prop-types@^15.6.2: +prop-types@^15.5.7, prop-types@^15.6.2: version "15.6.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" integrity sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ== @@ -3935,6 +3949,15 @@ react-hammerjs@^0.5.0: dependencies: hammerjs "^2.0.8" +react-sortable-hoc@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/react-sortable-hoc/-/react-sortable-hoc-1.4.0.tgz#b477ce700ba755754200a1dabd36e588e2f5608d" + integrity sha512-4++hdwMTrzpOHcqndi2M2gEsqgoGMGmmYzs3wp/xZdap/d8oT2yUR3m6STNi1d1trRyl9Ud0C54agbYH7XdQAQ== + dependencies: + "@babel/runtime" "^7.2.0" + invariant "^2.2.4" + prop-types "^15.5.7" + react@^15.3.2: version "15.6.2" resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72" @@ -4041,7 +4064,7 @@ readable-stream@^2.0.6, readable-stream@^2.1.4: string_decoder "~1.0.0" util-deprecate "~1.0.1" -readable-stream@^2.2.2: +readable-stream@^2.1.0, readable-stream@^2.2.2: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== @@ -4107,6 +4130,11 @@ regenerator-runtime@^0.11.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== +regenerator-runtime@^0.12.0: + version "0.12.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" + integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== + regenerator-transform@^0.10.0: version "0.10.1" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd" @@ -4489,6 +4517,14 @@ sshpk@^1.7.0: jsbn "~0.1.0" tweetnacl "~0.14.0" +string-to-stream@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string-to-stream/-/string-to-stream-1.1.1.tgz#aba78f73e70661b130ee3e1c0192be4fef6cb599" + integrity sha512-QySF2+3Rwq0SdO3s7BAp4x+c3qsClpPQ6abAmb0DGViiSBAkT5kL6JT2iyzEVP+T1SmzHrQD1TwlP9QAHCc+Sw== + dependencies: + inherits "^2.0.1" + readable-stream "^2.1.0" + string-width@^1.0.1, string-width@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"