diff --git a/.eslintrc b/.eslintrc
index e48dd403..012a46de 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -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"
diff --git a/README.md b/README.md
index bce9af0e..9a2a191e 100644
--- a/README.md
+++ b/README.md
@@ -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)
diff --git a/package.json b/package.json
index a27a0345..650ad4eb 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
}
diff --git a/src/HelpSheet.jsx b/src/HelpSheet.jsx
index 9d623fc9..108f7f5b 100644
--- a/src/HelpSheet.jsx
+++ b/src/HelpSheet.jsx
@@ -20,6 +20,8 @@ const HelpSheet = ({ visible }) => {
O Mark out / cut end point
E Cut (export selection in the same directory)
C Capture snapshot (in the same directory)
+ + Add cut segment
+ BACKSPACE Remove current cut segment
);
diff --git a/src/TimelineSeg.jsx b/src/TimelineSeg.jsx
new file mode 100644
index 00000000..5e6ed069
--- /dev/null
+++ b/src/TimelineSeg.jsx
@@ -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 (
+
+ {cutStartTime !== undefined && (
+
+ )}
+ {isCutRangeValid && (cutStartTime !== undefined || cutEndTime !== undefined) && (
+
+ )}
+ {cutEndTime !== undefined && (
+
+ )}
+
+ );
+};
+
+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;
diff --git a/src/ffmpeg.js b/src/ffmpeg.js
index 947c2817..80466812 100644
--- a/src/ffmpeg.js
+++ b/src/ffmpeg.js
@@ -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,
diff --git a/src/main.css b/src/main.css
index 47dd40c8..8ef52db8 100644
--- a/src/main.css
+++ b/src/main.css
@@ -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;
diff --git a/src/random-color.js b/src/random-color.js
new file mode 100644
index 00000000..66f64545
--- /dev/null
+++ b/src/random-color.js
@@ -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,
+ });
+};
diff --git a/src/renderer.jsx b/src/renderer.jsx
index 3078b40b..9da1a723 100644
--- a/src/renderer.jsx
+++ b/src/renderer.jsx
@@ -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 (
{!filePath && (
@@ -571,18 +625,21 @@ class App extends React.Component {
{currentTimePos !== undefined &&
}
- {cutStartTime !== undefined &&
}
- {isCutRangeValid && (cutStartTime !== undefined || cutEndTime !== undefined) && (
-
(
+
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 && }
+ ))}
{formatDuration(this.getOffsetCurrentTime())}
@@ -591,7 +648,8 @@ class App extends React.Component {
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)}
/>
shortStep(-1)}
/>
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)}
/>
seekAbs(duration)}
/>
@@ -646,27 +710,33 @@ class App extends React.Component {
1 ? 'Export all segments' : 'Export selection'}
className="button fa fa-scissors"
- aria-hidden="true"
+ role="button"
+ tabIndex="0"
onClick={this.cutClick}
/>
@@ -680,6 +750,25 @@ class App extends React.Component {
{round(playbackRate, 1) || 1}
+
+
+
+
@@ -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}
/>
diff --git a/src/util.js b/src/util.js
index d9c24350..42f2d646 100644
--- a/src/util.js
+++ b/src/util.js
@@ -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,
};
diff --git a/yarn.lock b/yarn.lock
index ce3e0c30..79cc4c3d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"