From 17ee2798ec1db4b5d03dae14f410e1394d4d7912 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Tue, 15 May 2018 18:36:33 +0800 Subject: [PATCH] Implement manual input field for cutting range Format duration with : in GUI (still . in files) Minor improvements Move cut time indicators for more horizontal space --- src/capture-frame.js | 2 +- src/ffmpeg.js | 2 +- src/main.css | 12 ++++-- src/renderer.jsx | 100 +++++++++++++++++++++++++++++++++++-------- src/util.js | 19 +++++++- 5 files changed, 110 insertions(+), 25 deletions(-) diff --git a/src/capture-frame.js b/src/capture-frame.js index 69bd8c62..6aaaab5b 100644 --- a/src/capture-frame.js +++ b/src/capture-frame.js @@ -23,7 +23,7 @@ async function captureFrame(customOutDir, filePath, video, currentTime, captureF const buf = getFrameFromVideo(video, captureFormat); const ext = mime.extension(buf.mimetype); - const time = util.formatDuration(currentTime); + const time = util.formatDuration(currentTime, true); const outPath = util.getOutPath(customOutDir, filePath, `${time}.${ext}`); await fs.writeFileAsync(outPath, buf); diff --git a/src/ffmpeg.js b/src/ffmpeg.js index 71809c4c..f7ccfadf 100644 --- a/src/ffmpeg.js +++ b/src/ffmpeg.js @@ -59,7 +59,7 @@ async function cut({ customOutDir, filePath, format, cutFrom, cutTo, videoDuration, rotation, onProgress, }) { const ext = path.extname(filePath) || `.${format}`; - const cutSpecification = `${util.formatDuration(cutFrom)}-${util.formatDuration(cutTo)}`; + const cutSpecification = `${util.formatDuration(cutFrom, true)}-${util.formatDuration(cutTo, true)}`; const outPath = util.getOutPath(customOutDir, filePath, `${cutSpecification}${ext}`); diff --git a/src/main.css b/src/main.css index 8d8b725c..67300610 100644 --- a/src/main.css +++ b/src/main.css @@ -36,17 +36,21 @@ input, button, textarea, :focus { .button { padding: .4em; + vertical-align: middle; } -.controls-wrapper button, .right-menu button, .left-menu button { +.controls-wrapper button, .right-menu button, .left-menu button, .controls-wrapper input { background: white; border-radius: .3em; color: rgba(0, 0, 0, 0.7); - font-size: 60%; + font-size: 13px; vertical-align: middle; - padding: .2em .4em; - margin: 0 .5em; + padding: 0 .5em; + margin: 0 3px; border: none; + height: 18px; + box-sizing: border-box; + font-family: inherit; } .controls-wrapper button:active, .right-menu button:active, .left-menu button:active { diff --git a/src/renderer.jsx b/src/renderer.jsx index 4b7db354..6abddc85 100644 --- a/src/renderer.jsx +++ b/src/renderer.jsx @@ -62,8 +62,8 @@ function renderHelpSheet(visible) {
  • , (comma) Tiny seek backward (1/60 sec)
  • I Mark in / cut start point
  • O Mark out / cut end point
  • -
  • E Export selection (in the same dir as the video)
  • -
  • C Capture snapshot (in the same dir as the video)
  • +
  • E Cut (export selection in the same directory)
  • +
  • C Capture snapshot (in the same directory)
  • ); } @@ -89,7 +89,9 @@ class App extends React.Component { currentTime: undefined, duration: undefined, cutStartTime: 0, + cutStartTimeManual: undefined, cutEndTime: undefined, + cutEndTimeManual: undefined, fileFormat: undefined, captureFormat: 'jpeg', rotation: 360, @@ -214,6 +216,14 @@ class App extends React.Component { return this.state.rotation !== 360; } + areCutTimesSet() { + return (this.state.cutStartTime !== undefined || this.state.cutEndTime !== undefined); + } + + isCutRangeValid() { + return this.areCutTimesSet() && this.state.cutStartTime < this.state.cutEndTime; + } + increaseRotation() { const rotation = (this.state.rotation + 90) % 450; this.setState({ rotation }); @@ -278,10 +288,10 @@ class App extends React.Component { const filePath = this.state.filePath; const rotation = this.isRotationSet() ? this.getRotation() : undefined; - if (cutStartTime === undefined || cutEndTime === undefined) { + if (!this.areCutTimesSet()) { return alert('Please select both start and end time'); } - if (cutStartTime >= cutEndTime) { + if (!this.isCutRangeValid()) { return alert('Start time must be before end time'); } @@ -328,7 +338,44 @@ class App extends React.Component { this.setState({ helpVisible: !this.state.helpVisible }); } + renderCutTimeInput(type) { + const cutTimeKey = type === 'start' ? 'cutStartTime' : 'cutEndTime'; + const cutTimeManualKey = type === 'start' ? 'cutStartTimeManual' : 'cutEndTimeManual'; + const cutTimeInputStyle = Object.assign({}, { width: '8em', textAlign: type === 'start' ? 'right' : 'left' }); + + const isCutTimeManualSet = () => this.state[cutTimeManualKey] !== undefined; + + const handleCutTimeInput = (text) => { + // Allow the user to erase + if (text.length === 0) { + this.setState({ [cutTimeManualKey]: undefined }); + return; + } + + const time = util.parseDuration(text); + if (time === undefined) { + this.setState({ [cutTimeManualKey]: text }); + return; + } + + this.setState({ [cutTimeManualKey]: undefined, [cutTimeKey]: time }); + }; + + + return ( handleCutTimeInput(e.target.value)} + value={isCutTimeManualSet() + ? this.state[cutTimeManualKey] + : util.formatDuration(this.state[cutTimeKey]) + } + />); + } + render() { + const jumpCutButtonStyle = { position: 'absolute', color: 'black', bottom: 0, top: 0, padding: '2px 8px' }; + return (
    {!this.state.filePath &&
    DROP VIDEO
    } {this.state.working && ( @@ -374,12 +421,25 @@ class App extends React.Component {
    -
    +