1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-22 10:22:31 +01:00

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
This commit is contained in:
Mikael Finstad 2018-05-15 18:36:33 +08:00
parent e9edf47b17
commit 17ee2798ec
5 changed files with 110 additions and 25 deletions

View File

@ -23,7 +23,7 @@ async function captureFrame(customOutDir, filePath, video, currentTime, captureF
const buf = getFrameFromVideo(video, captureFormat); const buf = getFrameFromVideo(video, captureFormat);
const ext = mime.extension(buf.mimetype); 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}`); const outPath = util.getOutPath(customOutDir, filePath, `${time}.${ext}`);
await fs.writeFileAsync(outPath, buf); await fs.writeFileAsync(outPath, buf);

View File

@ -59,7 +59,7 @@ async function cut({
customOutDir, filePath, format, cutFrom, cutTo, videoDuration, rotation, onProgress, customOutDir, filePath, format, cutFrom, cutTo, videoDuration, rotation, onProgress,
}) { }) {
const ext = path.extname(filePath) || `.${format}`; 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}`); const outPath = util.getOutPath(customOutDir, filePath, `${cutSpecification}${ext}`);

View File

@ -36,17 +36,21 @@ input, button, textarea, :focus {
.button { .button {
padding: .4em; 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; background: white;
border-radius: .3em; border-radius: .3em;
color: rgba(0, 0, 0, 0.7); color: rgba(0, 0, 0, 0.7);
font-size: 60%; font-size: 13px;
vertical-align: middle; vertical-align: middle;
padding: .2em .4em; padding: 0 .5em;
margin: 0 .5em; margin: 0 3px;
border: none; border: none;
height: 18px;
box-sizing: border-box;
font-family: inherit;
} }
.controls-wrapper button:active, .right-menu button:active, .left-menu button:active { .controls-wrapper button:active, .right-menu button:active, .left-menu button:active {

View File

@ -62,8 +62,8 @@ function renderHelpSheet(visible) {
<li><kbd>,</kbd> (comma) Tiny seek backward (1/60 sec)</li> <li><kbd>,</kbd> (comma) Tiny seek backward (1/60 sec)</li>
<li><kbd>I</kbd> Mark in / cut start point</li> <li><kbd>I</kbd> Mark in / cut start point</li>
<li><kbd>O</kbd> Mark out / cut end point</li> <li><kbd>O</kbd> Mark out / cut end point</li>
<li><kbd>E</kbd> Export selection (in the same dir as the video)</li> <li><kbd>E</kbd> Cut (export selection in the same directory)</li>
<li><kbd>C</kbd> Capture snapshot (in the same dir as the video)</li> <li><kbd>C</kbd> Capture snapshot (in the same directory)</li>
</ul> </ul>
</div>); </div>);
} }
@ -89,7 +89,9 @@ class App extends React.Component {
currentTime: undefined, currentTime: undefined,
duration: undefined, duration: undefined,
cutStartTime: 0, cutStartTime: 0,
cutStartTimeManual: undefined,
cutEndTime: undefined, cutEndTime: undefined,
cutEndTimeManual: undefined,
fileFormat: undefined, fileFormat: undefined,
captureFormat: 'jpeg', captureFormat: 'jpeg',
rotation: 360, rotation: 360,
@ -214,6 +216,14 @@ class App extends React.Component {
return this.state.rotation !== 360; 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() { increaseRotation() {
const rotation = (this.state.rotation + 90) % 450; const rotation = (this.state.rotation + 90) % 450;
this.setState({ rotation }); this.setState({ rotation });
@ -278,10 +288,10 @@ class App extends React.Component {
const filePath = this.state.filePath; const filePath = this.state.filePath;
const rotation = this.isRotationSet() ? this.getRotation() : undefined; const rotation = this.isRotationSet() ? this.getRotation() : undefined;
if (cutStartTime === undefined || cutEndTime === undefined) { if (!this.areCutTimesSet()) {
return alert('Please select both start and end time'); return alert('Please select both start and end time');
} }
if (cutStartTime >= cutEndTime) { if (!this.isCutRangeValid()) {
return alert('Start time must be before end time'); return alert('Start time must be before end time');
} }
@ -328,7 +338,44 @@ class App extends React.Component {
this.setState({ helpVisible: !this.state.helpVisible }); 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 (<input
style={Object.assign({}, cutTimeInputStyle, { color: isCutTimeManualSet() ? '#dc1d1d' : undefined })}
type="text"
onChange={e => handleCutTimeInput(e.target.value)}
value={isCutTimeManualSet()
? this.state[cutTimeManualKey]
: util.formatDuration(this.state[cutTimeKey])
}
/>);
}
render() { render() {
const jumpCutButtonStyle = { position: 'absolute', color: 'black', bottom: 0, top: 0, padding: '2px 8px' };
return (<div> return (<div>
{!this.state.filePath && <div id="drag-drop-field">DROP VIDEO</div>} {!this.state.filePath && <div id="drag-drop-field">DROP VIDEO</div>}
{this.state.working && ( {this.state.working && (
@ -374,12 +421,25 @@ class App extends React.Component {
</div> </div>
</Hammer> </Hammer>
<div> <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<i <i
className="button fa fa-step-backward" className="button fa fa-step-backward"
aria-hidden="true" aria-hidden="true"
title="Jump to start of video"
onClick={() => seekAbs(0)} onClick={() => seekAbs(0)}
/> />
<div style={{ position: 'relative' }}>
{this.renderCutTimeInput('start')}
<i
style={Object.assign({}, jumpCutButtonStyle, { left: 0 })}
className="fa fa-step-backward"
title="Jump to cut start"
aria-hidden="true"
onClick={withBlur(() => this.jumpCutStart())}
/>
</div>
<i <i
className="button fa fa-caret-left" className="button fa fa-caret-left"
aria-hidden="true" aria-hidden="true"
@ -395,44 +455,50 @@ class App extends React.Component {
aria-hidden="true" aria-hidden="true"
onClick={() => shortStep(1)} onClick={() => shortStep(1)}
/> />
<div style={{ position: 'relative' }}>
{this.renderCutTimeInput('end')}
<i
style={Object.assign({}, jumpCutButtonStyle, { right: 0 })}
className="fa fa-step-forward"
title="Jump to cut end"
aria-hidden="true"
onClick={withBlur(() => this.jumpCutEnd())}
/>
</div>
<i <i
className="button fa fa-step-forward" className="button fa fa-step-forward"
aria-hidden="true" aria-hidden="true"
title="Jump to end of video"
onClick={() => seekAbs(this.state.duration)} onClick={() => seekAbs(this.state.duration)}
/> />
</div> </div>
<div> <div>
<button
className="jump-cut-start" title="Cut start time (jump)"
onClick={withBlur(() => this.jumpCutStart())}
>{util.formatDuration(this.state.cutStartTime || 0)}</button>
<i <i
title="Set cut start" title="Set cut start to current position"
className="button fa fa-angle-left" className="button fa fa-angle-left"
aria-hidden="true" aria-hidden="true"
onClick={() => this.setCutStart()} onClick={() => this.setCutStart()}
/> />
<i <i
title="Export selection" title="Cut"
className="button fa fa-scissors" className="button fa fa-scissors"
aria-hidden="true" aria-hidden="true"
onClick={() => this.cutClick()} onClick={() => this.cutClick()}
/> />
<i <i
title="Set cut end" title="Set cut end to current position"
className="button fa fa-angle-right" className="button fa fa-angle-right"
aria-hidden="true" aria-hidden="true"
onClick={() => this.setCutEnd()} onClick={() => this.setCutEnd()}
/> />
<button
className="jump-cut-end" title="Cut end time (jump)"
onClick={withBlur(() => this.jumpCutEnd())}
>{util.formatDuration(this.state.cutEndTime || 0)}</button>
</div> </div>
</div> </div>
<div className="left-menu"> <div className="left-menu">
<button title="Format"> <button title="Format of current file">
{this.state.fileFormat || 'FMT'} {this.state.fileFormat || 'FMT'}
</button> </button>

View File

@ -2,7 +2,7 @@ const _ = require('lodash');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
function formatDuration(_seconds) { function formatDuration(_seconds, fileNameFriendly) {
const seconds = _seconds || 0; const seconds = _seconds || 0;
const minutes = seconds / 60; const minutes = seconds / 60;
const hours = minutes / 60; const hours = minutes / 60;
@ -13,7 +13,21 @@ function formatDuration(_seconds) {
const msPadded = _.padStart(Math.floor((seconds - Math.floor(seconds)) * 1000), 3, '0'); const msPadded = _.padStart(Math.floor((seconds - Math.floor(seconds)) * 1000), 3, '0');
// Be nice to filenames and use . // Be nice to filenames and use .
return `${hoursPadded}.${minutesPadded}.${secondsPadded}.${msPadded}`; const delim = fileNameFriendly ? '.' : ':';
return `${hoursPadded}${delim}${minutesPadded}${delim}${secondsPadded}.${msPadded}`;
}
function parseDuration(str) {
if (!str) return undefined;
const match = str.trim().match(/^(\d{2}):(\d{2}):(\d{2})\.(\d{3})$/);
if (!match) return undefined;
const hours = parseInt(match[1], 10);
const minutes = parseInt(match[2], 10);
const seconds = parseInt(match[3], 10);
const ms = parseInt(match[4], 10);
if (hours > 59 || minutes > 59 || seconds > 59) return undefined;
return ((((hours * 60) + minutes) * 60) + seconds) + (ms / 1000);
} }
function getOutPath(customOutDir, filePath, nameSuffix) { function getOutPath(customOutDir, filePath, nameSuffix) {
@ -45,6 +59,7 @@ async function transferTimestampsWithOffset(inPath, outPath, offset) {
module.exports = { module.exports = {
formatDuration, formatDuration,
parseDuration,
getOutPath, getOutPath,
transferTimestamps, transferTimestamps,
transferTimestampsWithOffset, transferTimestampsWithOffset,