1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-21 18:02:35 +01:00

implement multi cut feature 🎉

#12
This commit is contained in:
Mikael Finstad 2019-01-28 18:09:54 +01:00
parent 2a44d0b648
commit 0051e4d289
11 changed files with 379 additions and 95 deletions

View File

@ -9,6 +9,7 @@
"no-console": 0,
"react/destructuring-assignment": 0,
"react/forbid-prop-types": [1, { "forbid": ["any"] }],
"jsx-a11y/click-events-have-key-events": 0,
},
"plugins": [
"react"

View File

@ -8,6 +8,7 @@ Simple and ultra fast cross platform tool for lossless trimming/cutting of video
- Lossless cutting of common video and audio formats
- Lossless merge of files (identical codecs)
- Lossless extracting of all streams from a file (video, audio, subtitle, ++)
- Cut out multiple segments at the same time
- Take full-resolution snapshots from videos in JPEG/PNG format
- Manual input range of cutpoints
- Can include more than 2 streams or remove audio track (optional)

View File

@ -54,6 +54,7 @@
"dependencies": {
"bluebird": "^3.4.6",
"classnames": "^2.2.5",
"color": "^3.1.0",
"electron": "^2.0.9",
"electron-default-menu": "^1.0.0",
"electron-is-dev": "^0.1.2",
@ -77,6 +78,7 @@
"sweetalert2": "^8.0.1",
"sweetalert2-react-content": "^1.0.1",
"trash": "^4.3.0",
"uuid": "^3.3.2",
"which": "^1.2.11"
}
}

View File

@ -20,6 +20,8 @@ const HelpSheet = ({ visible }) => {
<li><kbd>O</kbd> Mark out / cut end point</li>
<li><kbd>E</kbd> Cut (export selection in the same directory)</li>
<li><kbd>C</kbd> Capture snapshot (in the same directory)</li>
<li><kbd>+</kbd> Add cut segment</li>
<li><kbd>BACKSPACE</kbd> Remove current cut segment</li>
</ul>
</div>
);

83
src/TimelineSeg.jsx Normal file
View File

@ -0,0 +1,83 @@
const React = require('react');
const PropTypes = require('prop-types');
const TimelineSeg = ({
isCutRangeValid, duration: durationRaw, cutStartTime, cutEndTime, apparentCutStart,
apparentCutEnd, isActive, segNum, onSegClick, color,
}) => {
const markerWidth = 4;
const duration = durationRaw || 1;
const cutSectionWidth = `calc(${((apparentCutEnd - apparentCutStart) / duration) * 100}% - ${markerWidth * 2}px)`;
const startTimePos = `${(apparentCutStart / duration) * 100}%`;
const endTimePos = `${(apparentCutEnd / duration) * 100}%`;
const markerBorder = isActive ? `2px solid ${color.string()}` : undefined;
const markerBorderRadius = 5;
const startMarkerStyle = {
background: color.alpha(0.5).string(),
width: markerWidth,
left: startTimePos,
borderLeft: markerBorder,
borderTopLeftRadius: markerBorderRadius,
borderBottomLeftRadius: markerBorderRadius,
};
const endMarkerStyle = {
background: color.alpha(0.5).string(),
width: markerWidth,
marginLeft: -markerWidth,
left: endTimePos,
borderRight: markerBorder,
borderTopRightRadius: markerBorderRadius,
borderBottomRightRadius: markerBorderRadius,
};
const cutSectionStyle = {
background: color.alpha(0.5).string(),
marginLeft: markerWidth,
left: startTimePos,
width: cutSectionWidth,
};
const onThisSegClick = () => onSegClick(segNum);
return (
<React.Fragment>
{cutStartTime !== undefined && (
<div style={startMarkerStyle} className="cut-time-marker" role="button" tabIndex="0" onClick={onThisSegClick} />
)}
{isCutRangeValid && (cutStartTime !== undefined || cutEndTime !== undefined) && (
<div
className="cut-section"
style={cutSectionStyle}
role="button"
tabIndex="0"
onClick={onThisSegClick}
/>
)}
{cutEndTime !== undefined && (
<div style={endMarkerStyle} className="cut-time-marker" role="button" tabIndex="0" onClick={onThisSegClick} />
)}
</React.Fragment>
);
};
TimelineSeg.propTypes = {
isCutRangeValid: PropTypes.bool.isRequired,
duration: PropTypes.number,
cutStartTime: PropTypes.number,
cutEndTime: PropTypes.number,
apparentCutStart: PropTypes.number.isRequired,
apparentCutEnd: PropTypes.number.isRequired,
isActive: PropTypes.bool.isRequired,
segNum: PropTypes.number.isRequired,
onSegClick: PropTypes.func.isRequired,
color: PropTypes.object.isRequired,
};
TimelineSeg.defaultProps = {
duration: undefined,
cutStartTime: undefined,
cutEndTime: undefined,
};
module.exports = TimelineSeg;

View File

@ -5,6 +5,7 @@ const path = require('path');
const fileType = require('file-type');
const readChunk = require('read-chunk');
const flatMap = require('lodash/flatMap');
const sum = require('lodash/sum');
const readline = require('readline');
const moment = require('moment');
const stringToStream = require('string-to-stream');
@ -110,6 +111,39 @@ async function cut({
await transferTimestamps(filePath, outPath);
}
async function cutMultiple({
customOutDir, filePath, format, segments, videoDuration, rotation,
includeAllStreams, onProgress, stripAudio, keyframeCut,
}) {
const singleProgresses = {};
function onSingleProgress(id, singleProgress) {
singleProgresses[id] = singleProgress;
return onProgress((sum(Object.values(singleProgresses)) / segments.length));
}
let i = 0;
// eslint-disable-next-line no-restricted-syntax
for (const { cutFrom, cutTo, cutToApparent } of segments) {
// eslint-disable-next-line no-await-in-loop
await cut({
customOutDir,
filePath,
format,
videoDuration,
rotation,
includeAllStreams,
stripAudio,
keyframeCut,
cutFrom,
cutTo,
cutToApparent,
// eslint-disable-next-line no-loop-func
onProgress: progress => onSingleProgress(i, progress),
});
i += 1;
}
}
async function html5ify(filePath, outPath, encodeVideo) {
console.log('Making HTML5 friendly version', { filePath, outPath, encodeVideo });
@ -274,7 +308,7 @@ async function extractAllStreams(filePath) {
}
module.exports = {
cut,
cutMultiple,
getFormat,
html5ify,
mergeFiles,

View File

@ -35,6 +35,7 @@ input, button, textarea, :focus {
}
.button {
border-radius: 3px;
padding: .4em;
vertical-align: middle;
}
@ -102,12 +103,10 @@ input, button, textarea, :focus {
.timeline-wrapper .cut-section {
z-index: 1;
background-color: rgba(0, 255, 149, 0.5);
}
.timeline-wrapper .cut-time-marker {
z-index: 2;
box-sizing: border-box;
background: rgba(0, 255, 149, 0.5);
}
.timeline-wrapper .current-time {
z-index: 3;

26
src/random-color.js Normal file
View File

@ -0,0 +1,26 @@
// https://github.com/mock-end/random-color/blob/master/index.js
/* eslint-disable */
const color = require('color');
var ratio = 0.618033988749895;
var hue = 0.65;
module.exports = function (saturation, value) {
hue += ratio;
hue %= 1;
if (typeof saturation !== 'number') {
saturation = 0.5;
}
if (typeof value !== 'number') {
value = 0.95;
}
return color({
h: hue * 360,
s: saturation * 100,
v: value * 100,
});
};

View File

@ -3,16 +3,19 @@ const $ = require('jquery');
const Mousetrap = require('mousetrap');
const round = require('lodash/round');
const clamp = require('lodash/clamp');
const clone = require('lodash/clone');
const throttle = require('lodash/throttle');
const Hammer = require('react-hammerjs').default;
const path = require('path');
const trash = require('trash');
const uuid = require('uuid');
const React = require('react');
const ReactDOM = require('react-dom');
const classnames = require('classnames');
const HelpSheet = require('./HelpSheet');
const TimelineSeg = require('./TimelineSeg');
const { showMergeDialog } = require('./merge/merge');
const captureFrame = require('./capture-frame');
@ -21,7 +24,7 @@ const ffmpeg = require('./ffmpeg');
const {
getOutPath, parseDuration, formatDuration, toast, errorToast, showFfmpegFail, setFileNameTitle,
promptTimeOffset,
promptTimeOffset, generateColor,
} = require('./util');
const { dialog } = electron.remote;
@ -60,23 +63,31 @@ function withBlur(cb) {
};
}
function createSegment({ start, end } = {}) {
return {
start,
end,
color: generateColor(),
uuid: uuid.v4(),
};
}
const localState = {
const getInitialLocalState = () => ({
working: false,
filePath: '', // Setting video src="" prevents memory leak in chromium
html5FriendlyPath: undefined,
playing: false,
currentTime: undefined,
duration: undefined,
cutStartTime: undefined,
cutSegments: [createSegment()],
currentSeg: 0,
cutStartTimeManual: undefined,
cutEndTime: undefined,
cutEndTimeManual: undefined,
fileFormat: undefined,
rotation: 360,
cutProgress: undefined,
startTimeOffset: 0,
};
});
const globalState = {
stripAudio: false,
@ -91,7 +102,7 @@ class App extends React.Component {
super(props);
this.state = {
...localState,
...getInitialLocalState(),
...globalState,
};
@ -220,6 +231,8 @@ class App extends React.Component {
Mousetrap.bind('i', () => this.setCutStart());
Mousetrap.bind('o', () => this.setCutEnd());
Mousetrap.bind('h', () => this.toggleHelp());
Mousetrap.bind('+', () => this.addCutSegment());
Mousetrap.bind('backspace', () => this.removeCutSegment());
electron.ipcRenderer.send('renderer-ready');
}
@ -241,11 +254,13 @@ class App extends React.Component {
}
setCutStart = () => {
this.setState(({ currentTime }) => ({ cutStartTime: currentTime }));
const { currentTime } = this.state;
this.setCutTime('start', currentTime);
}
setCutEnd = () => {
this.setState(({ currentTime }) => ({ cutEndTime: currentTime }));
const { currentTime } = this.state;
this.setCutTime('end', currentTime);
}
setOutputDir = () => {
@ -274,13 +289,35 @@ class App extends React.Component {
return `${this.getRotation()}°`;
}
getApparentCutStartTime() {
if (this.state.cutStartTime !== undefined) return this.state.cutStartTime;
getCutSeg(i) {
const { currentSeg, cutSegments } = this.state;
return cutSegments[i !== undefined ? i : currentSeg];
}
getCutStartTime(i) {
return this.getCutSeg(i).start;
}
getCutEndTime(i) {
return this.getCutSeg(i).end;
}
setCutTime(type, time) {
const { currentSeg, cutSegments } = this.state;
const cloned = clone(cutSegments);
cloned[currentSeg][type] = time;
this.setState({ cutSegments: cloned });
}
getApparentCutStartTime(i) {
const cutStartTime = this.getCutStartTime(i);
if (cutStartTime !== undefined) return cutStartTime;
return 0;
}
getApparentCutEndTime() {
if (this.state.cutEndTime !== undefined) return this.state.cutEndTime;
getApparentCutEndTime(i) {
const cutEndTime = this.getCutEndTime(i);
if (cutEndTime !== undefined) return cutEndTime;
if (this.state.duration !== undefined) return this.state.duration;
return 0; // Haven't gotten duration yet
}
@ -306,6 +343,41 @@ class App extends React.Component {
toggleKeyframeCut = () => this.setState(({ keyframeCut }) => ({ keyframeCut: !keyframeCut }));
addCutSegment = () => {
const { cutSegments, currentTime, duration } = this.state;
const cutStartTime = this.getCutStartTime();
const cutEndTime = this.getCutEndTime();
if (cutStartTime === undefined && cutEndTime === undefined) return;
const suggestedStart = currentTime;
const suggestedEnd = suggestedStart + 10;
const cutSegmentsNew = [
...cutSegments,
createSegment({
start: currentTime,
end: suggestedEnd <= duration ? suggestedEnd : undefined,
}),
];
const currentSegNew = cutSegmentsNew.length - 1;
this.setState({ currentSeg: currentSegNew, cutSegments: cutSegmentsNew });
}
removeCutSegment = () => {
const { currentSeg, cutSegments } = this.state;
if (cutSegments.length < 2) return;
const cutSegmentsNew = [...cutSegments];
cutSegmentsNew.splice(currentSeg, 1);
const currentSegNew = Math.min(currentSeg, cutSegmentsNew.length - 1);
this.setState({ currentSeg: currentSegNew, cutSegments: cutSegmentsNew });
}
jumpCutStart = () => {
seekAbs(this.getApparentCutStartTime());
}
@ -350,37 +422,45 @@ class App extends React.Component {
}
cutClick = async () => {
if (this.state.working) {
const {
filePath, customOutDir, fileFormat, duration, includeAllStreams,
stripAudio, keyframeCut, working, cutSegments,
} = this.state;
if (working) {
errorToast('I\'m busy');
return;
}
const {
cutEndTime, cutStartTime, filePath, customOutDir, fileFormat, duration, includeAllStreams,
stripAudio, keyframeCut,
} = this.state;
const rotation = this.isRotationSet() ? this.getRotation() : undefined;
const cutStartTime = this.getCutStartTime();
const cutEndTime = this.getCutEndTime();
if (!(this.isCutRangeValid() || cutEndTime === undefined || cutStartTime === undefined)) {
errorToast('Start time must be before end time');
return;
}
this.setState({ working: true });
try {
await ffmpeg.cut({
this.setState({ working: true });
const segments = cutSegments.map((seg, i) => ({
cutFrom: this.getApparentCutStartTime(i),
cutTo: this.getCutEndTime(i),
cutToApparent: this.getApparentCutEndTime(i),
}));
await ffmpeg.cutMultiple({
customOutDir,
filePath,
format: fileFormat,
cutFrom: this.getApparentCutStartTime(),
cutTo: cutEndTime,
cutToApparent: this.getApparentCutEndTime(),
videoDuration: duration,
rotation,
includeAllStreams,
stripAudio,
keyframeCut,
segments,
onProgress: this.onCutProgress,
});
} catch (err) {
@ -425,7 +505,7 @@ class App extends React.Component {
const video = getVideo();
video.currentTime = 0;
video.playbackRate = 1;
this.setState(localState);
this.setState(getInitialLocalState());
setFileNameTitle();
}
@ -434,8 +514,8 @@ class App extends React.Component {
return this.state.rotation !== 360;
}
isCutRangeValid() {
return this.getApparentCutStartTime() < this.getApparentCutEndTime();
isCutRangeValid(i) {
return this.getApparentCutStartTime(i) < this.getApparentCutEndTime(i);
}
toggleHelp() {
@ -461,11 +541,9 @@ class App extends React.Component {
return;
}
const cutTimeKey = type === 'start' ? 'cutStartTime' : 'cutEndTime';
this.setState(state => ({
[cutTimeManualKey]: undefined,
[cutTimeKey]: time - state.startTimeOffset,
}));
this.setState({ [cutTimeManualKey]: undefined });
this.setCutTime(type, time - this.state.startTimeOffset);
};
const cutTime = type === 'start' ? this.getApparentCutStartTime() : this.getApparentCutEndTime();
@ -484,6 +562,18 @@ class App extends React.Component {
}
render() {
const {
working, filePath, duration: durationRaw, cutProgress, currentTime, playing,
fileFormat, playbackRate, keyframeCut, includeAllStreams, stripAudio, captureFormat,
helpVisible, currentSeg, cutSegments,
} = this.state;
const duration = durationRaw || 1;
const currentTimePos = currentTime !== undefined && `${(currentTime / duration) * 100}%`;
const segColor = this.getCutSeg().color;
const segBgColor = segColor.alpha(0.5).string();
const jumpCutButtonStyle = {
position: 'absolute', color: 'black', bottom: 0, top: 0, padding: '2px 8px',
};
@ -491,42 +581,6 @@ class App extends React.Component {
background: 'rgba(255, 255, 255, 0.4)', padding: '.1em .4em', margin: '0 3px', fontSize: 13, borderRadius: '.3em',
};
const {
working, filePath, duration: durationRaw, cutProgress, currentTime, playing,
fileFormat, playbackRate, keyframeCut, includeAllStreams, stripAudio, captureFormat,
helpVisible, cutStartTime, cutEndTime,
} = this.state;
const markerWidth = 4;
const apparentCutStart = this.getApparentCutStartTime();
const apprentCutEnd = this.getApparentCutEndTime();
const duration = durationRaw || 1;
const currentTimePos = currentTime !== undefined && `${(currentTime / duration) * 100}%`;
const cutSectionWidth = `calc(${((apprentCutEnd - apparentCutStart) / duration) * 100}% - ${markerWidth * 2}px)`;
const isCutRangeValid = this.isCutRangeValid();
const startTimePos = `${(apparentCutStart / duration) * 100}%`;
const endTimePos = `${(apprentCutEnd / duration) * 100}%`;
const markerBorder = '2px solid rgb(0, 255, 149)';
const markerBorderRadius = 5;
const startMarkerStyle = {
width: markerWidth,
left: startTimePos,
borderLeft: markerBorder,
borderTopLeftRadius: markerBorderRadius,
borderBottomLeftRadius: markerBorderRadius,
};
const endMarkerStyle = {
width: markerWidth,
marginLeft: -markerWidth,
left: endTimePos,
borderRight: markerBorder,
borderTopRightRadius: markerBorderRadius,
borderBottomRightRadius: markerBorderRadius,
};
return (
<div>
{!filePath && (
@ -571,18 +625,21 @@ class App extends React.Component {
<div className="timeline-wrapper">
{currentTimePos !== undefined && <div className="current-time" style={{ left: currentTimePos }} />}
{cutStartTime !== undefined && <div style={startMarkerStyle} className="cut-time-marker" />}
{isCutRangeValid && (cutStartTime !== undefined || cutEndTime !== undefined) && (
<div
className="cut-section"
style={{
marginLeft: markerWidth,
left: startTimePos,
width: cutSectionWidth,
}}
{cutSegments.map((seg, i) => (
<TimelineSeg
key={seg.uuid}
segNum={i}
color={seg.color}
onSegClick={currentSegNew => this.setState({ currentSeg: currentSegNew })}
isActive={i === currentSeg}
isCutRangeValid={this.isCutRangeValid(i)}
duration={duration}
cutStartTime={this.getCutStartTime(i)}
cutEndTime={this.getCutEndTime(i)}
apparentCutStart={this.getApparentCutStartTime(i)}
apparentCutEnd={this.getApparentCutEndTime(i)}
/>
)}
{cutEndTime !== undefined && <div style={endMarkerStyle} className="cut-time-marker" />}
))}
<div id="current-time-display">{formatDuration(this.getOffsetCurrentTime())}</div>
</div>
@ -591,7 +648,8 @@ class App extends React.Component {
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<i
className="button fa fa-step-backward"
aria-hidden="true"
role="button"
tabIndex="0"
title="Jump to start of video"
onClick={() => seekAbs(0)}
/>
@ -602,26 +660,30 @@ class App extends React.Component {
style={{ ...jumpCutButtonStyle, left: 0 }}
className="fa fa-step-backward"
title="Jump to cut start"
aria-hidden="true"
role="button"
tabIndex="0"
onClick={withBlur(this.jumpCutStart)}
/>
</div>
<i
className="button fa fa-caret-left"
aria-hidden="true"
role="button"
tabIndex="0"
onClick={() => shortStep(-1)}
/>
<i
className={classnames({
button: true, fa: true, 'fa-pause': playing, 'fa-play': !playing,
})}
aria-hidden="true"
role="button"
tabIndex="0"
onClick={this.playCommand}
/>
<i
className="button fa fa-caret-right"
aria-hidden="true"
role="button"
tabIndex="0"
onClick={() => shortStep(1)}
/>
@ -631,14 +693,16 @@ class App extends React.Component {
style={{ ...jumpCutButtonStyle, right: 0 }}
className="fa fa-step-forward"
title="Jump to cut end"
aria-hidden="true"
role="button"
tabIndex="0"
onClick={withBlur(this.jumpCutEnd)}
/>
</div>
<i
className="button fa fa-step-forward"
aria-hidden="true"
role="button"
tabIndex="0"
title="Jump to end of video"
onClick={() => seekAbs(duration)}
/>
@ -646,27 +710,33 @@ class App extends React.Component {
<div>
<i
style={{ background: segBgColor }}
title="Set cut start to current position"
className="button fa fa-angle-left"
aria-hidden="true"
role="button"
tabIndex="0"
onClick={this.setCutStart}
/>
<i
title="Cut"
title={cutSegments.length > 1 ? 'Export all segments' : 'Export selection'}
className="button fa fa-scissors"
aria-hidden="true"
role="button"
tabIndex="0"
onClick={this.cutClick}
/>
<i
title="Delete source file"
className="button fa fa-trash"
aria-hidden="true"
role="button"
tabIndex="0"
onClick={this.deleteSourceClick}
/>
<i
style={{ background: segBgColor }}
title="Set cut end to current position"
className="button fa fa-angle-right"
aria-hidden="true"
role="button"
tabIndex="0"
onClick={this.setCutEnd}
/>
</div>
@ -680,6 +750,25 @@ class App extends React.Component {
<span style={infoSpanStyle} title="Playback rate">
{round(playbackRate, 1) || 1}
</span>
<button
style={{ ...infoSpanStyle, background: segBgColor, color: 'white' }}
disabled={cutSegments.length < 2}
type="button"
title={`Delete selected segment ${currentSeg + 1}`}
onClick={withBlur(() => this.removeCutSegment())}
>
d
{currentSeg + 1}
</button>
<button
type="button"
title={`Add cut segment ${currentSeg + 1}`}
onClick={withBlur(() => this.addCutSegment())}
>
c+
</button>
</div>
<div className="right-menu">
@ -727,7 +816,8 @@ class App extends React.Component {
title="Capture frame"
style={{ margin: '-.4em -.2em' }}
className="button fa fa-camera"
aria-hidden="true"
role="button"
tabIndex="0"
onClick={this.capture}
/>

View File

@ -3,6 +3,9 @@ const path = require('path');
const fs = require('fs');
const swal = require('sweetalert2');
const randomColor = require('./random-color');
function formatDuration(_seconds, fileNameFriendly) {
const seconds = _seconds || 0;
const minutes = seconds / 60;
@ -101,6 +104,10 @@ async function promptTimeOffset(inputValue) {
return duration;
}
function generateColor() {
return randomColor(1, 0.95);
}
module.exports = {
formatDuration,
parseDuration,
@ -112,4 +119,5 @@ module.exports = {
showFfmpegFail,
setFileNameTitle,
promptTimeOffset,
generateColor,
};

View File

@ -1287,7 +1287,7 @@ code-point-at@^1.0.0:
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
color-convert@^1.9.0:
color-convert@^1.9.0, color-convert@^1.9.1:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
@ -1299,6 +1299,27 @@ color-name@1.1.3:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
color-name@^1.0.0:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
color-string@^1.5.2:
version "1.5.3"
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc"
integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==
dependencies:
color-name "^1.0.0"
simple-swizzle "^0.2.2"
color@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/color/-/color-3.1.0.tgz#d8e9fb096732875774c84bf922815df0308d0ffc"
integrity sha512-CwyopLkuRYO5ei2EpzpIh6LqJMt6Mt+jZhO5VI5f/wJLZriXQE32/SSqzmrh+QB+AZT81Cj8yv+7zwToW8ahZg==
dependencies:
color-convert "^1.9.1"
color-string "^1.5.2"
combined-stream@1.0.6:
version "1.0.6"
resolved "http://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818"
@ -2713,6 +2734,11 @@ is-arrayish@^0.2.1:
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
is-arrayish@^0.3.1:
version "0.3.2"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
is-binary-path@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
@ -4350,6 +4376,13 @@ signal-exit@^3.0.0, signal-exit@^3.0.2:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
simple-swizzle@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=
dependencies:
is-arrayish "^0.3.1"
single-line-log@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/single-line-log/-/single-line-log-1.1.2.tgz#c2f83f273a3e1a16edb0995661da0ed5ef033364"
@ -4857,6 +4890,11 @@ uuid@^3.1.0:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
integrity sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==
uuid@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
v8flags@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4"