diff --git a/package.json b/package.json
index fadd3bc7..980b3651 100644
--- a/package.json
+++ b/package.json
@@ -50,6 +50,7 @@
"eslint-plugin-react": "^7.14.3",
"eslint-plugin-react-hooks": "^1.7.0",
"evergreen-ui": "^4.23.0",
+ "fast-xml-parser": "^3.17.4",
"file-url": "^3.0.0",
"framer-motion": "^1.8.4",
"hammerjs": "^2.0.8",
@@ -62,6 +63,7 @@
"mousetrap": "^1.6.1",
"p-map": "^3.0.0",
"patch-package": "^6.2.1",
+ "pify": "^5.0.0",
"pretty-ms": "^6.0.0",
"react": "^16.12.0",
"react-dom": "^16.12.0",
@@ -81,11 +83,11 @@
"wait-on": "^4.0.1"
},
"dependencies": {
+ "cue-parser": "^0.3.0",
"electron-is-dev": "^0.1.2",
"electron-store": "^5.1.1",
"electron-unhandled": "^3.0.2",
"execa": "^4.0.0",
- "fast-xml-parser": "^3.17.4",
"ffmpeg-static": "^4.2.1",
"ffprobe-static": "^3.0.0",
"file-type": "^12.4.0",
diff --git a/public/configStore.js b/public/configStore.js
index f0ed4654..623a7164 100644
--- a/public/configStore.js
+++ b/public/configStore.js
@@ -10,6 +10,8 @@ const defaults = {
invertCutSegments: false,
autoExportExtraStreams: true,
askBeforeClose: false,
+ enableAskForImportChapters: true,
+ enableAskForFileOpenAction: true,
muted: false,
autoSaveProjectFile: true,
wheelSensitivity: 0.2,
diff --git a/public/menu.js b/public/menu.js
index 1132808c..af98be41 100644
--- a/public/menu.js
+++ b/public/menu.js
@@ -38,12 +38,24 @@ module.exports = (app, mainWindow, newVersion) => {
{
label: 'Import project',
submenu: [
+ {
+ label: 'Text chapters / YouTube',
+ click() {
+ mainWindow.webContents.send('importEdlFile', 'youtube');
+ },
+ },
{
label: 'DaVinci Resolve / Final Cut Pro XML',
click() {
mainWindow.webContents.send('importEdlFile', 'xmeml');
},
},
+ {
+ label: 'CUE sheet file',
+ click() {
+ mainWindow.webContents.send('importEdlFile', 'cue');
+ },
+ },
],
},
{
diff --git a/src/App.jsx b/src/App.jsx
index aecb0bb8..b81c7cd5 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -41,13 +41,14 @@ import {
defaultProcessedCodecTypes, getStreamFps, isCuttingStart, isCuttingEnd,
getDefaultOutFormat, getFormatData, mergeFiles as ffmpegMergeFiles, renderThumbnails as ffmpegRenderThumbnails,
readFrames, renderWaveformPng, html5ifyDummy, cutMultiple, extractStreams, autoMergeSegments, getAllStreams,
- findNearestKeyFrameTime, html5ify as ffmpegHtml5ify, isStreamThumbnail, isAudioSupported, isIphoneHevc,
+ findNearestKeyFrameTime, html5ify as ffmpegHtml5ify, isStreamThumbnail, isAudioSupported, isIphoneHevc, tryReadChaptersToEdl,
} from './ffmpeg';
-import { save as edlStoreSave, load as edlStoreLoad, loadXmeml } from './edlStore';
+import { saveCsv, loadCsv, loadXmeml, loadCue } from './edlStore';
import {
getOutPath, formatDuration, toast, errorToast, showFfmpegFail, setFileNameTitle,
promptTimeOffset, generateColor, getOutDir, withBlur, checkDirWriteAccess, dirExists, askForOutDir,
- openDirToast, askForHtml5ifySpeed, isMasBuild, isStoreBuild,
+ openDirToast, askForHtml5ifySpeed, askForYouTubeInput, isMasBuild, isStoreBuild, askForFileOpenAction,
+ askForImportChapters,
} from './util';
import { openSendReportDialog } from './reporting';
import { fallbackLng } from './i18n';
@@ -213,6 +214,10 @@ const App = memo(() => {
useEffect(() => safeSetConfig('autoExportExtraStreams', autoExportExtraStreams), [autoExportExtraStreams]);
const [askBeforeClose, setAskBeforeClose] = useState(configStore.get('askBeforeClose'));
useEffect(() => safeSetConfig('askBeforeClose', askBeforeClose), [askBeforeClose]);
+ const [enableAskForImportChapters, setEnableAskForImportChapters] = useState(configStore.get('enableAskForImportChapters'));
+ useEffect(() => safeSetConfig('enableAskForImportChapters', enableAskForImportChapters), [enableAskForImportChapters]);
+ const [enableAskForFileOpenAction, setEnableAskForFileOpenAction] = useState(configStore.get('enableAskForFileOpenAction'));
+ useEffect(() => safeSetConfig('enableAskForFileOpenAction', enableAskForFileOpenAction), [enableAskForFileOpenAction]);
const [muted, setMuted] = useState(configStore.get('muted'));
useEffect(() => safeSetConfig('muted', muted), [muted]);
const [autoSaveProjectFile, setAutoSaveProjectFile] = useState(configStore.get('autoSaveProjectFile'));
@@ -566,7 +571,7 @@ const App = memo(() => {
return;
} */
- await edlStoreSave(edlFilePath, debouncedCutSegments);
+ await saveCsv(edlFilePath, debouncedCutSegments);
lastSavedCutSegmentsRef.current = debouncedCutSegments;
} catch (err) {
errorToast(i18n.t('Unable to save project file'));
@@ -1106,29 +1111,26 @@ const App = memo(() => {
}, []);
const loadCutSegments = useCallback((edl) => {
- const allRowsValid = edl
- .every(row => row.start === undefined || row.end === undefined || row.start < row.end);
+ if (edl.length === 0) throw new Error();
+ const allRowsValid = edl.every(row => row.start === undefined || row.end === undefined || row.start < row.end);
- if (!allRowsValid) {
- throw new Error(i18n.t('Invalid start or end values for one or more segments'));
- }
+ if (!allRowsValid) throw new Error(i18n.t('Invalid start or end values for one or more segments'));
cutSegmentsHistory.go(0);
setCutSegments(edl.map(createSegment));
}, [cutSegmentsHistory, setCutSegments]);
- const loadEdlFile = useCallback(async (edlPath, type = 'csv') => {
+ const loadEdlFile = useCallback(async (path, type = 'csv') => {
try {
- let storedEdl;
- if (type === 'csv') storedEdl = await edlStoreLoad(edlPath);
- else if (type === 'xmeml') storedEdl = await loadXmeml(edlPath);
+ let edl;
+ if (type === 'csv') edl = await loadCsv(path);
+ else if (type === 'xmeml') edl = await loadXmeml(path);
+ else if (type === 'cue') edl = await loadCue(path);
- loadCutSegments(storedEdl);
+ loadCutSegments(edl);
} catch (err) {
- if (err.code !== 'ENOENT') {
- console.error('EDL load failed', err);
- errorToast(`${i18n.t('Failed to load project file')} (${err.message})`);
- }
+ console.error('EDL load failed', err);
+ errorToast(`${i18n.t('Failed to load project')} (${err.message})`);
}
}, [loadCutSegments]);
@@ -1216,7 +1218,17 @@ const App = memo(() => {
await createDummyVideo(cod, fp);
}
- await loadEdlFile(getEdlFilePath(fp));
+ const openedFileEdlPath = getEdlFilePath(fp);
+
+ if (await exists(openedFileEdlPath)) {
+ await loadEdlFile(openedFileEdlPath);
+ } else {
+ const edl = await tryReadChaptersToEdl(fp);
+ if (edl.length > 0 && enableAskForImportChapters && (await askForImportChapters())) {
+ console.log('Read chapters', edl);
+ loadCutSegments(edl);
+ }
+ }
} catch (err) {
if (err.exitCode === 1 || err.code === 'ENOENT') {
errorToast(i18n.t('Unsupported file'));
@@ -1227,7 +1239,7 @@ const App = memo(() => {
} finally {
setWorking();
}
- }, [resetState, working, createDummyVideo, loadEdlFile, getEdlFilePath, getHtml5ifiedPath]);
+ }, [resetState, working, createDummyVideo, loadEdlFile, getEdlFilePath, getHtml5ifiedPath, loadCutSegments, enableAskForImportChapters]);
const toggleHelp = useCallback(() => setHelpVisible(val => !val), []);
const toggleSettings = useCallback(() => setSettingsVisible(val => !val), []);
@@ -1363,27 +1375,15 @@ const App = memo(() => {
return;
}
- const { value } = await Swal.fire({
- title: i18n.t('You opened a new file. What do you want to do?'),
- icon: 'question',
- input: 'radio',
- inputValue: 'open',
- showCancelButton: true,
- customClass: { input: 'swal2-losslesscut-radio' },
- inputOptions: {
- open: i18n.t('Open the file instead of the current one'),
- add: i18n.t('Include all tracks from the new file'),
- },
- inputValidator: (v) => !v && i18n.t('You need to choose something!'),
- });
+ const openFileResponse = enableAskForFileOpenAction ? await askForFileOpenAction() : 'open';
- if (value === 'open') {
+ if (openFileResponse === 'open') {
load({ filePath: firstFile, customOutDir: newCustomOutDir });
- } else if (value === 'add') {
+ } else if (openFileResponse === 'add') {
addStreamSourceFile(firstFile);
setStreamsSelectorShown(true);
}
- }, [addStreamSourceFile, isFileOpened, load, mergeFiles, assureOutDirAccess]);
+ }, [addStreamSourceFile, isFileOpened, load, mergeFiles, assureOutDirAccess, enableAskForFileOpenAction]);
const onDrop = useCallback(async (ev) => {
ev.preventDefault();
@@ -1525,7 +1525,7 @@ const App = memo(() => {
errorToast(i18n.t('File exists, bailing'));
return;
}
- await edlStoreSave(fp, cutSegments);
+ await saveCsv(fp, cutSegments);
} catch (err) {
errorToast(i18n.t('Failed to export CSV'));
console.error('Failed to export CSV', err);
@@ -1538,9 +1538,16 @@ const App = memo(() => {
return;
}
+ if (type === 'youtube') {
+ const edl = await askForYouTubeInput();
+ if (edl.length > 0) loadCutSegments(edl);
+ return;
+ }
+
let filters;
if (type === 'csv') filters = [{ name: i18n.t('CSV files'), extensions: ['csv'] }];
else if (type === 'xmeml') filters = [{ name: i18n.t('XML files'), extensions: ['xml'] }];
+ else if (type === 'cue') filters = [{ name: i18n.t('CUE files'), extensions: ['cue'] }];
const { canceled, filePaths } = await dialog.showOpenDialog({ properties: ['openFile'], filters });
if (canceled || filePaths.length < 1) return;
@@ -1650,6 +1657,7 @@ const App = memo(() => {
mergeFiles, outputDir, filePath, isFileOpened, customOutDir, startTimeOffset, html5ifyCurrentFile,
createDummyVideo, resetState, extractAllStreams, userOpenFiles, cutSegmentsHistory, openSendReportDialogWithState,
loadEdlFile, cutSegments, edlFilePath, askBeforeClose, toggleHelp, toggleSettings, assureOutDirAccess, html5ifyAndLoad, html5ifyInternal,
+ loadCutSegments,
]);
async function showAddStreamSourceDialog() {
@@ -1735,6 +1743,10 @@ const App = memo(() => {
setTimecodeShowFrames={setTimecodeShowFrames}
askBeforeClose={askBeforeClose}
setAskBeforeClose={setAskBeforeClose}
+ enableAskForImportChapters={enableAskForImportChapters}
+ setEnableAskForImportChapters={setEnableAskForImportChapters}
+ enableAskForFileOpenAction={enableAskForFileOpenAction}
+ setEnableAskForFileOpenAction={setEnableAskForFileOpenAction}
ffmpegExperimental={ffmpegExperimental}
setFfmpegExperimental={setFfmpegExperimental}
invertTimelineScroll={invertTimelineScroll}
@@ -1747,7 +1759,7 @@ const App = memo(() => {
renderCaptureFormatButton={renderCaptureFormatButton}
onWheelTunerRequested={onWheelTunerRequested}
/>
- ), [AutoExportToggler, askBeforeClose, autoMerge, autoSaveProjectFile, customOutDir, invertCutSegments, keyframeCut, renderCaptureFormatButton, renderOutFmt, timecodeShowFrames, changeOutDir, onWheelTunerRequested, language, invertTimelineScroll, ffmpegExperimental, setFfmpegExperimental]);
+ ), [AutoExportToggler, askBeforeClose, autoMerge, autoSaveProjectFile, customOutDir, invertCutSegments, keyframeCut, renderCaptureFormatButton, renderOutFmt, timecodeShowFrames, changeOutDir, onWheelTunerRequested, language, invertTimelineScroll, ffmpegExperimental, setFfmpegExperimental, enableAskForImportChapters, setEnableAskForImportChapters, enableAskForFileOpenAction, setEnableAskForFileOpenAction]);
useEffect(() => {
if (!isStoreBuild) loadMifiLink().then(setMifiLink);
diff --git a/src/Settings.jsx b/src/Settings.jsx
index f1d3a117..819af2c7 100644
--- a/src/Settings.jsx
+++ b/src/Settings.jsx
@@ -8,6 +8,7 @@ const Settings = memo(({
autoSaveProjectFile, setAutoSaveProjectFile, timecodeShowFrames, setTimecodeShowFrames, askBeforeClose, setAskBeforeClose,
renderOutFmt, AutoExportToggler, renderCaptureFormatButton, onWheelTunerRequested, language, setLanguage,
invertTimelineScroll, setInvertTimelineScroll, ffmpegExperimental, setFfmpegExperimental,
+ enableAskForImportChapters, setEnableAskForImportChapters, enableAskForFileOpenAction, setEnableAskForFileOpenAction,
}) => {
const { t } = useTranslation();
@@ -187,6 +188,28 @@ const Settings = memo(({
/>
+
+
+ {t('Ask about importing chapters from opened file?')}
+
+ setEnableAskForImportChapters(e.target.checked)}
+ />
+
+
+
+
+ {t('Ask about what to do when opening a new file when another file is already already open?')}
+
+ setEnableAskForFileOpenAction(e.target.checked)}
+ />
+
+
);
});
diff --git a/src/edlFormats.js b/src/edlFormats.js
new file mode 100644
index 00000000..9ef396bc
--- /dev/null
+++ b/src/edlFormats.js
@@ -0,0 +1,96 @@
+import fastXmlParser from 'fast-xml-parser';
+import i18n from 'i18next';
+
+import csvParse from 'csv-parse';
+import pify from 'pify';
+import sortBy from 'lodash/sortBy';
+
+const csvParseAsync = pify(csvParse);
+
+export async function parseCsv(str) {
+ const rows = await csvParseAsync(str, {});
+ if (rows.length === 0) throw new Error(i18n.t('No rows found'));
+ if (!rows.every(row => row.length === 3)) throw new Error(i18n.t('One or more rows does not have 3 columns'));
+
+ const mapped = rows
+ .map(([start, end, name]) => ({
+ start: start === '' ? undefined : parseFloat(start, 10),
+ end: end === '' ? undefined : parseFloat(end, 10),
+ name,
+ }));
+
+ if (!mapped.every(({ start, end }) => (
+ (start === undefined || !Number.isNaN(start))
+ && (end === undefined || !Number.isNaN(end))
+ ))) {
+ console.log(mapped);
+ throw new Error(i18n.t('Invalid start or end value. Must contain a number of seconds'));
+ }
+
+ return mapped;
+}
+
+export function parseCuesheet(cuesheet) {
+ // There are 75 such frames per second of audio.
+ // https://en.wikipedia.org/wiki/Cue_sheet_(computing)
+ const fps = 75;
+
+ const { tracks } = cuesheet.files[0];
+
+ function parseTime(track) {
+ const index = track.indexes[0];
+ if (!index) return undefined;
+ const { time } = index;
+ if (!time) return undefined;
+
+ return (time.min * 60) + time.sec + time.frame / fps;
+ }
+
+ return tracks.map((track, i) => {
+ const nextTrack = tracks[i + 1];
+ const end = nextTrack && parseTime(nextTrack);
+
+ return { name: track.title, start: parseTime(track), end };
+ });
+}
+
+// https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/FinalCutPro_XML/VersionsoftheInterchangeFormat/VersionsoftheInterchangeFormat.html
+export function parseXmeml(xmlStr) {
+ const xml = fastXmlParser.parse(xmlStr);
+ // TODO maybe support media.audio also?
+ return xml.xmeml.project.children.sequence.media.video.track.clipitem.map((item) => ({ start: item.start / item.rate.timebase, end: item.end / item.rate.timebase }));
+}
+
+export function parseYouTube(str) {
+ const regex = /(?:([0-9]{2,}):)?([0-9]{2}):([0-9]{2})(?:\.([0-9]{3}))?[^\S\n]+([^\n]*)\n/g;
+
+ const lines = [];
+
+ function parseLine(match) {
+ if (!match) return undefined;
+ const [, hourStr, minStr, secStr, msStr, name] = match;
+ const hour = hourStr != null ? parseInt(hourStr, 10) : 0;
+ const min = parseInt(minStr, 10);
+ const sec = parseInt(secStr, 10);
+ const ms = msStr != null ? parseInt(msStr, 10) : 0;
+
+ const time = (((hour * 60) + min) * 60 + sec) + ms / 1000;
+
+ return { time, name };
+ }
+
+ let m;
+ // eslint-disable-next-line no-cond-assign
+ while ((m = regex.exec(`${str}\n`))) {
+ lines.push(parseLine(m));
+ }
+
+ const linesSorted = sortBy(lines, (l) => l.time);
+
+ const edl = linesSorted.map((line, i) => {
+ const nextLine = linesSorted[i + 1];
+ return { start: line.time, end: nextLine && nextLine.time, name: line.name };
+ });
+
+ return edl.filter((ed) => ed.start !== ed.end);
+}
diff --git a/src/edlFormats.test.js b/src/edlFormats.test.js
new file mode 100644
index 00000000..1812e49e
--- /dev/null
+++ b/src/edlFormats.test.js
@@ -0,0 +1,44 @@
+/* eslint-disable no-undef */
+import { parseYouTube } from './edlFormats';
+
+it('parseYoutube', () => {
+ const str = `
+Jump to chapters:
+00:00 Test 1
+00:01 Test 2
+00:02 00:57 double
+00:01:01 Test 3
+01:01:01.012 Test 4
+00:01:01.012 Test 5
+ 01:01.012 Test 6
+ :01:02.012 Test 7
+00:57:01.0123 Invalid 2
+00:57:01. Invalid 3
+01:15: Invalid 4
+0132 Invalid 5
+00:03
+00:04
+00:05
+`;
+ const edl = parseYouTube(str);
+ expect(edl).toEqual([
+ { start: 0, end: 1, name: 'Test 1' },
+ { start: 1, end: 2, name: 'Test 2' },
+ { start: 2, end: 4, name: '00:57 double' },
+ { start: 4, end: 5, name: '' },
+ { start: 5, end: 61, name: '' },
+ { start: 61, end: 61.012, name: 'Test 3' },
+ { start: 61.012, end: 62.012, name: 'Test 6' },
+ { start: 62.012, end: 3661.012, name: 'Test 7' },
+ { start: 3661.012, end: undefined, name: 'Test 4' },
+ ]);
+});
+
+it('parseYouTube eol', () => {
+ const str = ' 00:00 Test 1\n00:01 Test 2';
+ const edl = parseYouTube(str);
+ expect(edl).toEqual([
+ { start: 0, end: 1, name: 'Test 1' },
+ { start: 1, end: undefined, name: 'Test 2' },
+ ]);
+});
diff --git a/src/edlStore.js b/src/edlStore.js
index c2f2369b..e8b21eb9 100644
--- a/src/edlStore.js
+++ b/src/edlStore.js
@@ -1,48 +1,28 @@
-import parse from 'csv-parse';
-import stringify from 'csv-stringify';
-import i18n from 'i18next';
-import fastXmlParser from 'fast-xml-parser';
+import csvStringify from 'csv-stringify';
+import pify from 'pify';
-const fs = window.require('fs-extra');
-const { promisify } = window.require('util');
-
-const stringifyAsync = promisify(stringify);
-const parseAsync = promisify(parse);
-
-export async function load(path) {
- const str = await fs.readFile(path, 'utf-8');
- const rows = await parseAsync(str, {});
- if (rows.length === 0) throw new Error(i18n.t('No rows found'));
- if (!rows.every(row => row.length === 3)) throw new Error(i18n.t('One or more rows does not have 3 columns'));
+import { parseCuesheet, parseXmeml, parseCsv } from './edlFormats';
- const mapped = rows
- .map(([start, end, name]) => ({
- start: start === '' ? undefined : parseFloat(start, 10),
- end: end === '' ? undefined : parseFloat(end, 10),
- name,
- }));
+const fs = window.require('fs-extra');
+const cueParser = window.require('cue-parser');
- if (!mapped.every(({ start, end }) => (
- (start === undefined || !Number.isNaN(start))
- && (end === undefined || !Number.isNaN(end))
- ))) {
- console.log(mapped);
- throw new Error(i18n.t('Invalid start or end value. Must contain a number of seconds'));
- }
+const csvStringifyAsync = pify(csvStringify);
- return mapped;
+export async function loadCsv(path) {
+ return parseCsv(await fs.readFile(path, 'utf-8'));
}
-// https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/FinalCutPro_XML/VersionsoftheInterchangeFormat/VersionsoftheInterchangeFormat.html
export async function loadXmeml(path) {
- const xml = fastXmlParser.parse(await fs.readFile(path, 'utf-8'));
- // TODO maybe support media.audio also?
- return xml.xmeml.project.children.sequence.media.video.track.clipitem.map((item) => ({ start: item.start / item.rate.timebase, end: item.end / item.rate.timebase }));
+ return parseXmeml(await fs.readFile(path, 'utf-8'));
+}
+
+export async function loadCue(path) {
+ return parseCuesheet(cueParser.parse(path));
}
-export async function save(path, cutSegments) {
+export async function saveCsv(path, cutSegments) {
console.log('Saving', path);
const rows = cutSegments.map(({ start, end, name }) => [start, end, name]);
- const str = await stringifyAsync(rows);
+ const str = await csvStringifyAsync(rows);
await fs.writeFile(path, str);
}
diff --git a/src/ffmpeg.js b/src/ffmpeg.js
index 69054ee6..6c2dc5ad 100644
--- a/src/ffmpeg.js
+++ b/src/ffmpeg.js
@@ -344,6 +344,28 @@ export async function getDuration(filePath) {
return parseFloat(JSON.parse(stdout).format.duration);
}
+export async function tryReadChaptersToEdl(filePath) {
+ try {
+ const { stdout } = await runFfprobe(['-i', filePath, '-show_chapters', '-print_format', 'json']);
+ return JSON.parse(stdout).chapters.map((chapter) => {
+ const start = parseInt(chapter.start_time, 10);
+ const end = parseInt(chapter.end_time, 10);
+ if (Number.isNaN(start) || Number.isNaN(end)) return undefined;
+
+ const name = chapter.tags && typeof chapter.tags.title === 'string' ? chapter.tags.title : undefined;
+
+ return {
+ start,
+ end,
+ name,
+ };
+ }).filter((it) => it);
+ } catch (err) {
+ console.error('Failed to read chapters from file', err);
+ return [];
+ }
+}
+
export async function html5ify({ filePath, outPath, video, audio, onProgress }) {
console.log('Making HTML5 friendly version', { filePath, outPath, video, audio });
diff --git a/src/util.js b/src/util.js
index 38f1b2bf..b5c95d5d 100644
--- a/src/util.js
+++ b/src/util.js
@@ -4,6 +4,8 @@ import i18n from 'i18next';
import randomColor from './random-color';
+import { parseYouTube } from './edlFormats';
+
const path = window.require('path');
const fs = window.require('fs-extra');
const open = window.require('open');
@@ -197,6 +199,26 @@ export async function askForHtml5ifySpeed(allowedOptions) {
return value;
}
+export async function askForYouTubeInput() {
+ const example = i18n.t('YouTube video description\n00:00 Intro\n00:01 Chapter 2\n00:00:02.123 Chapter 3');
+ const { value } = await Swal.fire({
+ title: i18n.t('Import text chapters / YouTube'),
+ input: 'textarea',
+ inputPlaceholder: example,
+ text: i18n.t('Paste or type a YouTube chapters description or textual chapter description'),
+ showCancelButton: true,
+ inputValidator: (v) => {
+ if (v) {
+ const edl = parseYouTube(v);
+ if (edl.length > 0) return undefined;
+ }
+ return i18n.t('Please input a valid format.');
+ },
+ });
+
+ return parseYouTube(value);
+}
+
export const isMasBuild = window.process.mas;
export const isStoreBuild = isMasBuild || window.process.windowsStore;
@@ -210,3 +232,33 @@ export async function askForOutDir(defaultPath) {
});
return (filePaths && filePaths.length === 1) ? filePaths[0] : undefined;
}
+
+export async function askForFileOpenAction() {
+ const { value } = await Swal.fire({
+ title: i18n.t('You opened a new file. What do you want to do?'),
+ icon: 'question',
+ input: 'radio',
+ inputValue: 'open',
+ showCancelButton: true,
+ customClass: { input: 'swal2-losslesscut-radio' },
+ inputOptions: {
+ open: i18n.t('Open the file instead of the current one'),
+ add: i18n.t('Include all tracks from the new file'),
+ },
+ inputValidator: (v) => !v && i18n.t('You need to choose something!'),
+ });
+
+ return value;
+}
+
+export async function askForImportChapters() {
+ const { value } = await Swal.fire({
+ icon: 'question',
+ text: i18n.t('This file has embedded chapters. Do you want to import the chapters as cut-segments?'),
+ showCancelButton: true,
+ cancelButtonText: i18n.t('Ignore chapters'),
+ confirmButtonText: i18n.t('Import chapters'),
+ });
+
+ return value;
+}
diff --git a/yarn.lock b/yarn.lock
index 23eec6cb..98865e14 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3203,6 +3203,11 @@ chardet@^0.7.0:
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
+chardet@^1.0.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/chardet/-/chardet-1.3.0.tgz#a56ed2d9e4517a7128721340a0cb9a10a8fac238"
+ integrity sha512-cyTQGGptIjIT+CMGT5J/0l9c6Fb+565GCFjjeUTKxUO7w3oR+FcNCMEKTn5xtVKaLFmladN7QF68IiQsv5Fbdw==
+
chokidar@^2.0.2, chokidar@^2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@@ -4110,6 +4115,13 @@ csv-stringify@^5.3.6:
resolved "https://registry.yarnpkg.com/csv-stringify/-/csv-stringify-5.3.6.tgz#2655e2e1c01b97b3963bccbc9407b8fb876dc589"
integrity sha512-kPcRbMvo5NLLD71TAqW5K+g9kbM2HpIZJLAzm73Du8U+5TXmDp9YtXKCBLyxEh0q3Jbg8QhNFBz3b5VJzjZ/jw==
+cue-parser@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/cue-parser/-/cue-parser-0.3.0.tgz#813f4d47329b14bba998c6abdb7ee3f49147722f"
+ integrity sha512-dT9aE78B10akUWxt5SYhsyrGbIpoO67ELOk4DzIpwfhQNjauS9gxZ2SoOnwAfVUr2HAxFRH/8gH/8Umn4DYRow==
+ dependencies:
+ chardet "^1.0.0"
+
cyclist@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
@@ -9748,6 +9760,11 @@ pify@^4.0.1:
resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
+pify@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f"
+ integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==
+
pinkie-promise@^2.0.0, pinkie-promise@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"