improvements

- convert to esm
- upgrade electron-vite
- implement license check
- stricten ts
- remove fs-extra
pull/2715/head
Mikael Finstad 6 months ago
parent 26010721cf
commit 2601082c80
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -122,10 +122,10 @@ jobs:
APPLE_API_KEY_ID: ${{ secrets.api_key_id }}
APPLE_API_ISSUER: ${{ secrets.api_key_issuer_id }}
- run: npx tsx script/e2e.mts 'dist/mac-arm64/LosslessCut.app/Contents/MacOS/LosslessCut' 0:none screenshot.jpeg
- run: node script/e2e.mts 'dist/mac-arm64/LosslessCut.app/Contents/MacOS/LosslessCut' 0:none screenshot.jpeg
if: startsWith(matrix.os, 'macos')
- run: npx tsx script/e2e.mts 'dist\win-unpacked\LosslessCut.exe' desktop screenshot.jpeg
- run: node script/e2e.mts 'dist\win-unpacked\LosslessCut.exe' desktop screenshot.jpeg
if: startsWith(matrix.os, 'windows')
- run: |
@ -133,13 +133,13 @@ jobs:
sudo Xvfb -ac :0 -screen 0 1280x1024x24 > /dev/null 2>&1 &
sleep 5
chmod +x dist/linux-unpacked/losslesscut
npx tsx script/e2e.mts 'dist/linux-unpacked/losslesscut' ':0.0+0,0' screenshot.jpeg
node script/e2e.mts 'dist/linux-unpacked/losslesscut' ':0.0+0,0' screenshot.jpeg
if: startsWith(matrix.os, 'ubuntu')
- name: (MacOS) Upload to Mac App Store
if: startsWith(matrix.os, 'macos') && env.is_tag == 'true'
run: |
npx tsx script/xcrunWrapper.mts dist/mas-universal/LosslessCut-mac-universal.pkg ${{ secrets.api_key_id }} ${{ secrets.api_key_issuer_id }} 1505323402 no.mifi.losslesscut-mac
node script/xcrunWrapper.mts dist/mas-universal/LosslessCut-mac-universal.pkg ${{ secrets.api_key_id }} ${{ secrets.api_key_issuer_id }} 1505323402 no.mifi.losslesscut-mac
- name: (MacOS) Upload artifacts
uses: actions/upload-artifact@v4

@ -33,6 +33,7 @@ jobs:
yarn generate-docs
git diff --quiet HEAD
git reset --hard HEAD
- run: yarn check-licenses
- name: Generate licenses
run: |
yarn generate-licenses

@ -72,7 +72,7 @@ Before releasing, consider [Maintainence chores](#maintainence-chores) first.
- `git merge stores` (in case there's an old unmerged stores hotfix)
- **Prepare release notes** from commit history
- Create a new file `versions/x.y.z.md` and write the most important highlights from the release notes, but **remove github issue #references**
- `tsx script/generateVersions.mts && git add versions/*.md src/renderer/src/versions.json && git commit -m 'Update change log'`
- `node script/generateVersions.mts && git add versions/*.md src/renderer/src/versions.json && git commit -m 'Update change log'`
- *If Store-only hotfix release*
- `git checkout stores`
- `npm version patch`
@ -157,12 +157,6 @@ Links:
yarn scan-i18n
```
### Generate license summary
```bash
npx license-checker --summary
```
### Regenerate licenses file
```bash

@ -1,24 +1,27 @@
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
import { defineConfig } from 'electron-vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
main: {
// https://electron-vite.org/guide/dev#dependencies-vs-devdependencies
// For the main process and preload, the best practice is to externalize dependencies and only bundle our own code.
// However, until we use ESM for electron main, we need to include ESM-only deps in the bundle: (exclude from externalize)
plugins: [externalizeDepsPlugin({ exclude: ['p-map', 'execa', 'nanoid', 'file-type'] })],
build: {
// https://electron-vite.org/guide/dev#dependencies-vs-devdependencies
// For the main process and preload, the best practice is to externalize dependencies and only bundle our own code.
externalizeDeps: true,
target: 'node22.18',
sourcemap: true,
},
},
preload: {
// https://electron-vite.org/guide/dev#dependencies-vs-devdependencies
plugins: [externalizeDepsPlugin({ exclude: [] })],
build: {
externalizeDeps: true,
target: 'node22.18',
sourcemap: true,
rollupOptions: {
output: {
format: 'cjs',
},
},
},
},
renderer: {

@ -1,7 +1,8 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { defineConfig } from 'i18next-cli';
import { langNames, mapLang, SupportedLanguage } from './src/common/i18n.js';
import type { SupportedLanguage } from './src/common/i18n.js';
import { langNames, mapLang } from './src/common/i18n.js';
import configBase from './i18next.config.base.js';
export default defineConfig({

@ -6,6 +6,7 @@
"version": "3.67.2",
"main": "./out/main/index.js",
"homepage": "./",
"type": "module",
"scripts": {
"clean": "rimraf dist out common-ts-dist main-ts-dist build-resources icon-build",
"start": "electron-vite preview",
@ -22,12 +23,13 @@
"pack-mas-dev": "yarn build && electron-builder --mac mas-dev -c.mas.provisioningProfile=LosslessCut_Dev.provisionprofile -c.mas.identity='Mikael Finstad (JH4PH8B3C8)'",
"pack-win": "yarn build && electron-builder --win zip --x64",
"postinstall": "electron-builder install-app-deps",
"version": "tsx script/postversion.mts && git add no.mifi.losslesscut.appdata.xml",
"version": "node script/postversion.mts && git add no.mifi.losslesscut.appdata.xml",
"pack-linux": "yarn build && electron-builder --linux",
"scan-i18n": "i18next-cli -c i18next.config.scan.ts extract",
"generate-icon": "mkdirp icon-build build-resources/appx && tsx script/generateIcon.mts",
"generate-licenses": "yarn licenses generate-disclaimer > licenses.txt && echo '\n\nffmpeg is licensed under GPL v2+:\n\nhttp://www.gnu.org/licenses/old-licenses/gpl-2.0.html' >> licenses.txt",
"generate-docs": "tsx script/generateDocs.mts"
"generate-icon": "mkdirp icon-build build-resources/appx && node script/generateIcon.mts",
"generate-licenses": "yarn licenses generate-disclaimer -R > licenses.txt && echo '\n\nffmpeg is licensed under GPL v2+:\n\nhttp://www.gnu.org/licenses/old-licenses/gpl-2.0.html' >> licenses.txt",
"generate-docs": "node script/generateDocs.mts",
"check-licenses": "node script/checkLicenses.mts"
},
"author": {
"name": "Mikael Finstad",
@ -87,7 +89,7 @@
"electron": "^38.3.0",
"electron-builder": "26.0.12",
"electron-devtools-installer": "^4.0.0",
"electron-vite": "^2.3.0",
"electron-vite": "^5.0.0",
"eslint": "^8.2.0",
"eslint-config-mifi": "^0.0.3",
"eslint-plugin-import": "^2.25.3",
@ -131,13 +133,12 @@
"sweetalert2-react-content": "^5.0.7",
"tiny-invariant": "^1.3.3",
"ts-morph": "^27.0.2",
"tsx": "^4.7.1",
"type-fest": "^4.23.0",
"typescript": "^5.9.3",
"use-debounce": "^5.1.0",
"use-trace-update": "^1.3.0",
"vite": "^6.4.1",
"vitest": "^3.0.8"
"vite": "^7.3.0",
"vitest": "^4.0.16"
},
"dependencies": {
"@electron/remote": "^2.1.3",
@ -149,7 +150,6 @@
"express": "^4.20.0",
"express-async-handler": "^1.2.0",
"file-type": "patch:file-type@npm%3A19.4.1#~/.yarn/patches/file-type-npm-19.4.1-d18086444c.patch",
"fs-extra": "^8.1.0",
"i18next": "^25.6.1",
"i18next-fs-backend": "^2.3.2",
"json5": "^2.2.2",

@ -0,0 +1,91 @@
import { execa } from 'execa';
const { stdout } = await execa('yarn', ['licenses', 'list', '-R', '--json'], { encoding: 'utf8' });
const licensesJson: { value: string; children: Record<string, unknown> }[] = JSON.parse(`[${stdout.split('\n').join(',')}]`);
const safeLicenses: Record<string, true | string[]> = {
// These aim to remove almost all restrictions
'CC0-1.0': true,
Unlicense: true,
WTFPL: true,
'0BSD': true,
// Permissive licenses: These impose minimal requirements (mostly attribution + license notice). You can generally use them interchangeably in commercial and proprietary software.
MIT: true,
'BSD-2-Clause': true,
'BSD-3-Clause': true,
ISC: true,
Zlib: true,
'Python-2.0': true,
'BlueOak-1.0.0': true,
// Permissive licenses with some conditions
'Apache-2.0': true,
// Copyleft licenses: These require that derivative works be distributed under the same license terms. They ensure that modifications remain open source.
// which is OK because LosslessCut is also GPL-2.0-only
'GPL-2.0-only': ['lossless-cut'],
// Weak copyleft licenses: These allow linking with proprietary software under certain conditions, making them more flexible than strong copyleft licenses.
'LGPL-3.0-only': true,
'MPL-2.0': true,
// Special purpose licenses
'Hippocratic-2.1': ['@react-leaflet/core', 'react-leaflet'],
// not software licenses
'CC-BY-3.0': ['spdx-exceptions'], // eslint-plugin-unicorn
'CC-BY-4.0': ['caniuse-lite'],
UNKNOWN: ['fast-shallow-equal', 'react-universal-interface'],
};
const unsafeLicenses = licensesJson.flatMap((l) => {
const checkLicense = (license: string) => {
const safeLicense = safeLicenses[license];
if (safeLicense === true) {
return true;
}
if (safeLicense == null) {
return false;
}
return Object.keys(l.children).every((pkg) => (
safeLicense.some((safeLicensePackage) => pkg.startsWith(safeLicensePackage))
));
};
// e.g. "(BSD-2-Clause OR MIT OR Apache-2.0)"
// e.g. "(LGPL-3.0-only AND MIT)"
const trimmed = l.value.replace(/^\(/, '').replace(/\)$/, '');
const isOr = l.value.includes(' OR ');
const isAnd = l.value.includes(' AND ');
if (isOr || isAnd) {
const licenses = trimmed.split(isOr ? ' OR ' : ' AND ').map((s) => s.trim());
if (isOr) {
return licenses.some((license) => checkLicense(license)) ? [] : [l];
}
if (isAnd) {
return licenses.every((license) => checkLicense(license)) ? [] : [l];
}
}
return checkLicense(trimmed) ? [] : [l];
});
if (unsafeLicenses.length > 0) {
console.error('Found unsafe licenses:');
console.error();
for (const l of unsafeLicenses) {
console.error(l.value);
console.error(`${Object.keys(l.children).map((v) => `- ${v}`).join('\n')}`);
console.error();
}
process.exitCode = 1;
} else {
console.log('All licenses are safe.');
}

@ -1,4 +1,4 @@
import { SupportedLanguage } from './i18n';
import type { SupportedLanguage } from './i18n.ts';
export type KeyboardAction = 'addSegment' | 'togglePlayResetSpeed' | 'togglePlayNoResetSpeed' | 'reducePlaybackRate' | 'reducePlaybackRateMore' | 'increasePlaybackRate' | 'increasePlaybackRateMore' | 'timelineToggleComfortZoom' | 'seekPreviousFrame' | 'seekNextFrame' | 'captureSnapshot' | 'captureSnapshotToClipboard' | 'setCutStart' | 'setCutEnd' | 'removeCurrentSegment' | 'removeCurrentCutpoint' | 'cleanupFilesDialog' | 'splitCurrentSegment' | 'focusSegmentAtCursor' | 'selectSegmentsAtCursor' | 'increaseRotation' | 'goToTimecode' | 'seekBackwards' | 'seekBackwards2' | 'seekBackwards3' | 'seekBackwardsPercent' | 'seekBackwardsPercent' | 'seekBackwardsKeyframe' | 'jumpCutStart' | 'seekForwards' | 'seekForwards2' | 'seekForwards3' | 'seekForwardsPercent' | 'seekForwardsPercent' | 'seekForwardsKeyframe' | 'jumpCutEnd' | 'jumpTimelineStart' | 'jumpTimelineEnd' | 'jumpFirstSegment' | 'jumpPrevSegment' | 'jumpSeekFirstSegment' | 'jumpSeekPrevSegment' | 'timelineZoomIn' | 'timelineZoomIn' | 'batchPreviousFile' | 'jumpLastSegment' | 'jumpNextSegment' | 'jumpSeekLastSegment' | 'jumpSeekNextSegment' | 'timelineZoomOut' | 'timelineZoomOut' | 'batchNextFile' | 'batchOpenSelectedFile' | 'batchOpenPreviousFile' | 'batchOpenNextFile' | 'undo' | 'undo' | 'redo' | 'redo' | 'copySegmentsToClipboard' | 'copySegmentsToClipboard' | 'toggleFullscreenVideo' | 'labelCurrentSegment' | 'export' | 'toggleKeyboardShortcuts' | 'increaseVolume' | 'decreaseVolume' | 'toggleMuted' | 'detectBlackScenes' | 'detectSilentScenes' | 'detectSceneChanges' | 'toggleLastCommands' | 'play' | 'pause' | 'reloadFile' | 'html5ify' | 'makeCursorTimeZero' | 'togglePlayOnlyCurrentSegment' | 'toggleLoopOnlyCurrentSegment' | 'toggleLoopStartEndOnlyCurrentSegment' | 'togglePlaySelectedSegments' | 'toggleLoopSelectedSegments' | 'editCurrentSegmentTags' | 'duplicateCurrentSegment' | 'reorderSegsByStartTime' | 'invertAllSegments' | 'fillSegmentsGaps' | 'shiftAllSegmentTimes' | 'alignSegmentTimesToKeyframes' | 'readAllKeyframes' | 'createSegmentsFromKeyframes' | 'createFixedDurationSegments' | 'createNumSegments' | 'createFixedByteSizedSegments' | 'createRandomSegments' | 'shuffleSegments' | 'combineOverlappingSegments' | 'combineSelectedSegments' | 'clearSegments' | 'toggleSegmentsList' | 'selectOnlyCurrentSegment' | 'deselectAllSegments' | 'selectAllSegments' | 'toggleCurrentSegmentSelected' | 'invertSelectedSegments' | 'removeSelectedSegments' | 'toggleStreamsSelector' | 'extractAllStreams' | 'showStreamsSelector' | 'showIncludeExternalStreamsDialog' | 'captureSnapshotAsCoverArt' | 'extractCurrentSegmentFramesAsImages' | 'extractSelectedSegmentsFramesAsImages' | 'convertFormatBatch' | 'convertFormatCurrentFile' | 'fixInvalidDuration' | 'closeBatch' | 'concatBatch' | 'toggleKeyframeCutMode' | 'toggleCaptureFormat' | 'toggleStripAudio' | 'toggleStripVideo' | 'toggleStripSubtitle' | 'toggleStripThumbnail' | 'toggleStripCurrentFilter' | 'toggleStripAll' | 'toggleDarkMode' | 'setStartTimeOffset' | 'toggleWaveformMode' | 'toggleShowThumbnails' | 'toggleShowKeyframes' | 'toggleSettings' | 'openSendReportDialog' | 'openFilesDialog' | 'openDirDialog' | 'exportYouTube' | 'closeCurrentFile' | 'quit' | 'selectAllMarkers' | 'generateOverviewWaveform';

@ -1,5 +1,6 @@
import type { AboutPanelOptionsOptions } from 'electron';
// eslint-disable-next-line import/no-extraneous-dependencies
import { AboutPanelOptionsOptions, app } from 'electron';
import { app } from 'electron';
import { appName, copyrightYear } from './common.js';
import { isLinux } from './util.js';

@ -2,13 +2,12 @@ import Store from 'electron-store';
// eslint-disable-next-line import/no-extraneous-dependencies
import electron from 'electron';
import { join, dirname } from 'node:path';
import { pathExists } from 'fs-extra';
import assert from 'node:assert';
import { copyFile } from 'node:fs/promises';
import { KeyBinding, Config } from '../common/types.js';
import type { KeyBinding, Config } from '../common/types.js';
import logger from './logger.js';
import { isWindows } from './util.js';
import { isWindows, pathExists } from './util.js';
import { fallbackLng } from './i18nCommon.js';
const { app } = electron;

@ -1,5 +1,6 @@
import type { BrowserWindow } from 'electron';
// eslint-disable-next-line import/no-extraneous-dependencies
import { BrowserWindow, Menu } from 'electron';
import { Menu } from 'electron';
// https://github.com/electron/electron/issues/4068#issuecomment-274159726
export default (window: BrowserWindow) => {

@ -1,14 +1,15 @@
import { join } from 'node:path';
import readline from 'node:readline';
import stringToStream from 'string-to-stream';
import { execa, Options as ExecaOptions, ResultPromise } from 'execa';
import type { Options as ExecaOptions, ResultPromise } from 'execa';
import { execa } from 'execa';
import assert from 'node:assert';
import { Readable } from 'node:stream';
import type { Readable } from 'node:stream';
// eslint-disable-next-line import/no-extraneous-dependencies
import { app, clipboard, nativeImage } from 'electron';
import { platform, arch, isWindows, isLinux } from './util.js';
import { CaptureFormat, Waveform } from '../common/types.js';
import type { CaptureFormat, Waveform } from '../common/types.js';
import isDev from './isDev.js';
import logger from './logger.js';
import { parseFfmpegProgressLine } from './progress.js';

@ -3,9 +3,10 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { app } from 'electron';
import { join } from 'node:path';
import { InitOptions } from 'i18next';
import type { InitOptions } from 'i18next';
import { mapLang, SupportedLanguage } from '../common/i18n';
import type { SupportedLanguage } from '../common/i18n';
import { mapLang } from '../common/i18n';
let customLocalesPath: string | undefined;

@ -3,64 +3,40 @@ process.traceProcessWarnings = true;
/* eslint-disable import/first */
// eslint-disable-next-line import/no-extraneous-dependencies
import electron, { BrowserWindow, BrowserWindowConstructorOptions, nativeTheme, shell, app, ipcMain, Notification, NotificationConstructorOptions } from 'electron';
import electron, { BrowserWindow, type BrowserWindowConstructorOptions, nativeTheme, shell, app, ipcMain, Notification, type NotificationConstructorOptions } from 'electron';
import i18n from 'i18next';
import debounce from 'lodash.debounce/index.js';
import yargsParser from 'yargs-parser';
import JSON5 from 'json5';
import remote from '@electron/remote/main';
import remote from '@electron/remote/main/index.js';
import { stat } from 'node:fs/promises';
import assert from 'node:assert';
import timers from 'node:timers/promises';
import { z } from 'zod';
import { pathToFileURL } from 'node:url';
import electronUnhandled from 'electron-unhandled';
import { fileTypeFromFile } from 'file-type/node';
import logger from './logger.js';
import menu from './menu.js';
import * as configStore from './configStore.js';
import { isWindows } from './util.js';
import { isLinux, isWindows, isMac, platform, arch, pathExists } from './util.js';
import { appName } from './common.js';
import attachContextMenu from './contextMenu.js';
import HttpServer from './httpServer.js';
import isDev from './isDev.js';
import isStoreBuild from './isStoreBuild.js';
import { getAboutPanelOptions } from './aboutPanel.js';
import { checkNewVersion } from './updateChecker.js';
import * as i18nCommon from './i18nCommon.js';
import './i18n.js';
import { ApiActionRequest } from '../common/types.js';
export * as ffmpeg from './ffmpeg.js';
export * as i18n from './i18nCommon.js';
export * as compatPlayer from './compatPlayer.js';
export * as configStore from './configStore.js';
export { isLinux, isWindows, isMac, platform, arch } from './util.js';
export { pathToFileURL } from 'node:url';
export { downloadMediaUrl } from './ffmpeg.js';
import type { ApiActionRequest } from '../common/types.js';
import * as ffmpeg from './ffmpeg.js';
import * as compatPlayer from './compatPlayer.js';
import { downloadMediaUrl } from './ffmpeg.js';
const electronUnhandled = import('electron-unhandled');
export const fileTypePromise = import('file-type/node');
// eslint-disable-next-line unicorn/prefer-top-level-await
(async () => {
try {
(await electronUnhandled).default({ showDialog: true, logger: (err) => logger.error('electron-unhandled', err) });
} catch (err) {
logger.error(err);
}
})();
// eslint-disable-next-line unicorn/prefer-export-from
export { isDev };
electronUnhandled({ showDialog: true, logger: (err) => logger.error('electron-unhandled', err) });
// https://chromestatus.com/feature/5748496434987008
// https://peter.sh/experiments/chromium-command-line-switches/
@ -243,7 +219,6 @@ const argv = parseCliArgs();
const lossyModeSchema = z.object({ videoEncoder: z.union([z.literal('libx264'), z.literal('libx265'), z.literal('libsvtav1')]) });
// eslint-disable-next-line prefer-destructuring
const lossyMode = argv['lossyMode'] ? lossyModeSchema.parse(JSON5.parse(argv['lossyMode'])) : undefined;
export { lossyMode };
export type LossyMode = z.infer<typeof lossyModeSchema>;
@ -258,86 +233,13 @@ function safeRequestSingleInstanceLock(additionalData: Record<string, unknown>)
return app.requestSingleInstanceLock(additionalData);
}
function initApp() {
// On macOS, the system enforces single instance automatically when users try to open a second instance of your app in Finder, and the open-file and open-url events will be emitted for that.
// However when users start your app in command line, the system's single instance mechanism will be bypassed, and you have to use this method to ensure single instance.
// This can be tested with one terminal: npx electron .
// and another terminal: npx electron . path/to/file.mp4
app.on('second-instance', (_event, _commandLine, _workingDirectory, additionalData) => {
// Someone tried to run a second instance, we should focus our window.
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
if (!(additionalData != null && typeof additionalData === 'object' && 'argv' in additionalData) || !Array.isArray(additionalData.argv)) return;
const argv2 = parseCliArgs(additionalData.argv);
logger.info('second-instance', argv2);
if (argv2._ && argv2._.length > 0) openFilesEventually(argv2._.map(String));
else if (argv2['keyboardAction']) sendApiAction(argv2['keyboardAction']);
});
// Quit when all windows are closed.
app.on('window-all-closed', () => {
app.quit();
});
app.on('activate', () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {
createWindow();
}
});
ipcMain.on('renderer-ready', () => {
rendererReady = true;
if (filesToOpen.length > 0) openFiles(filesToOpen);
});
// Mac OS open with LosslessCut
// Emitted when the user wants to open a file with the application. The open-file event is usually emitted when the application is already open and the OS wants to reuse the application to open the file.
app.on('open-file', (event, path) => {
openFilesEventually([path]);
event.preventDefault(); // recommended in docs https://www.electronjs.org/docs/latest/api/app#event-open-file-macos
});
ipcMain.on('setAskBeforeClose', (_e, val) => {
askBeforeClose = val;
});
ipcMain.on('setLanguage', (_e, language) => {
i18n.changeLanguage(language).then(() => updateMenu()).catch((err) => logger.error('Failed to set language', err));
});
ipcMain.handle('tryTrashItem', async (_e, path) => {
try {
await stat(path);
} catch (err) {
if (err instanceof Error && 'code' in err && err.code === 'ENOENT') return;
}
await shell.trashItem(path);
});
ipcMain.handle('showItemInFolder', (_e, path) => shell.showItemInFolder(path));
ipcMain.on('apiActionResponse', (_e, { id }) => {
apiActionRequests.get(id)?.();
});
}
// This promise will be fulfilled when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
// Call this immediately, to make sure we don't miss it (race condition)
const readyPromise = app.whenReady();
// eslint-disable-next-line unicorn/prefer-top-level-await
(async () => {
async function init() {
try {
logger.info('LosslessCut version', app.getVersion(), { isDev });
await configStore.init({ customConfigDir: argv['configDir'] });
@ -351,7 +253,74 @@ const readyPromise = app.whenReady();
return;
}
initApp();
// On macOS, the system enforces single instance automatically when users try to open a second instance of your app in Finder, and the open-file and open-url events will be emitted for that.
// However when users start your app in command line, the system's single instance mechanism will be bypassed, and you have to use this method to ensure single instance.
// This can be tested with one terminal: npx electron .
// and another terminal: npx electron . path/to/file.mp4
app.on('second-instance', (_event, _commandLine, _workingDirectory, additionalData) => {
// Someone tried to run a second instance, we should focus our window.
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
if (!(additionalData != null && typeof additionalData === 'object' && 'argv' in additionalData) || !Array.isArray(additionalData.argv)) return;
const argv2 = parseCliArgs(additionalData.argv);
logger.info('second-instance', argv2);
if (argv2._ && argv2._.length > 0) openFilesEventually(argv2._.map(String));
else if (argv2['keyboardAction']) sendApiAction(argv2['keyboardAction']);
});
// Quit when all windows are closed.
app.on('window-all-closed', () => {
app.quit();
});
app.on('activate', () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {
createWindow();
}
});
ipcMain.on('renderer-ready', () => {
rendererReady = true;
if (filesToOpen.length > 0) openFiles(filesToOpen);
});
// Mac OS open with LosslessCut
// Emitted when the user wants to open a file with the application. The open-file event is usually emitted when the application is already open and the OS wants to reuse the application to open the file.
app.on('open-file', (event, path) => {
openFilesEventually([path]);
event.preventDefault(); // recommended in docs https://www.electronjs.org/docs/latest/api/app#event-open-file-macos
});
ipcMain.on('setAskBeforeClose', (_e, val) => {
askBeforeClose = val;
});
ipcMain.on('setLanguage', (_e, language) => {
i18n.changeLanguage(language).then(() => updateMenu()).catch((err) => logger.error('Failed to set language', err));
});
ipcMain.handle('tryTrashItem', async (_e, path) => {
try {
await stat(path);
} catch (err) {
if (err instanceof Error && 'code' in err && err.code === 'ENOENT') return;
}
await shell.trashItem(path);
});
ipcMain.handle('showItemInFolder', (_e, path) => shell.showItemInFolder(path));
ipcMain.on('apiActionResponse', (_e, { id }) => {
apiActionRequests.get(id)?.();
});
logger.info('Waiting for app to become ready');
await readyPromise;
@ -403,9 +372,9 @@ const readyPromise = app.whenReady();
} catch (err) {
logger.error('Failed to initialize', err);
}
})();
}
export function focusWindow() {
function focusWindow() {
try {
app.focus({ steal: true });
} catch (err) {
@ -413,18 +382,55 @@ export function focusWindow() {
}
}
export function quitApp() {
function quitApp() {
// allow HTTP API to respond etc.
timers.setTimeout(1000).then(() => electron.app.quit());
}
export const hasDisabledNetworking = () => !!disableNetworking;
const hasDisabledNetworking = () => !!disableNetworking;
export const setProgressBar = (v: number) => mainWindow?.setProgressBar(v);
const setProgressBar = (v: number) => mainWindow?.setProgressBar(v);
export function sendOsNotification(options: NotificationConstructorOptions) {
function sendOsNotification(options: NotificationConstructorOptions) {
if (!Notification.isSupported()) return;
const notification = new Notification(options);
notification.on('failed', (_e, error) => logger.warn('Notification failed', error));
notification.show();
}
const remoteApi = {
ffmpeg,
i18n: i18nCommon,
compatPlayer,
configStore,
isLinux,
isWindows,
isMac,
platform,
arch,
pathExists,
pathToFileURL,
downloadMediaUrl,
fileTypeFromFile,
isDev,
lossyMode,
focusWindow,
quitApp,
hasDisabledNetworking,
setProgressBar,
sendOsNotification,
};
export type RemoteApi = typeof remoteApi;
// @ts-expect-error don't know how to type
app.addListener('remote-require', (event: { returnValue: RemoteApi }, _webContents: unknown, moduleName: string) => {
if (moduleName === './index.js') {
// eslint-disable-next-line no-param-reassign
event.returnValue = remoteApi;
}
});
// cannot top level await because app.whenReady will hang forever
// eslint-disable-next-line unicorn/prefer-top-level-await
init();

@ -1,5 +1,6 @@
import type { BrowserWindow, MenuItem, MenuItemConstructorOptions } from 'electron';
// eslint-disable-next-line import/no-extraneous-dependencies
import electron, { BrowserWindow, MenuItem, MenuItemConstructorOptions } from 'electron';
import electron from 'electron';
import { t } from 'i18next';
import { homepageUrl, getReleaseUrl, licensesUrl, thanksUrl, usageUrl, faqUrl, troubleshootingUrl, featureRequestUrl } from '../common/constants.js';

@ -1,3 +1,4 @@
import { access, constants } from 'node:fs/promises';
import os from 'node:os';
export const platform = os.platform();
@ -6,3 +7,12 @@ export const arch = os.arch();
export const isWindows = platform === 'win32';
export const isMac = platform === 'darwin';
export const isLinux = platform === 'linux';
export async function pathExists(path: string) {
try {
await access(path, constants.F_OK);
return true;
} catch {
return false;
}
}

@ -1,4 +1,5 @@
import { memo, useEffect, useState, useCallback, useRef, useMemo, CSSProperties, ReactEventHandler, FocusEventHandler, DragEventHandler } from 'react';
import type { CSSProperties, ReactEventHandler, FocusEventHandler, DragEventHandler } from 'react';
import { memo, useEffect, useState, useCallback, useRef, useMemo } from 'react';
import { FaAngleLeft, FaRegTimesCircle } from 'react-icons/fa';
import { MdRotate90DegreesCcw } from 'react-icons/md';
import { AnimatePresence, MotionConfig } from 'framer-motion';
@ -6,12 +7,12 @@ import i18n from 'i18next';
import { useTranslation } from 'react-i18next';
import { produce } from 'immer';
import screenfull from 'screenfull';
import { IpcRendererEvent } from 'electron';
import type { IpcRendererEvent } from 'electron';
import fromPairs from 'lodash/fromPairs';
import sum from 'lodash/sum';
import invariant from 'tiny-invariant';
import { SweetAlertOptions } from 'sweetalert2';
import type { SweetAlertOptions } from 'sweetalert2';
import useTimelineScroll from './hooks/useTimelineScroll';
import useUserSettingsRoot from './hooks/useUserSettingsRoot';
@ -24,7 +25,8 @@ import useFrameCapture from './hooks/useFrameCapture';
import useSegments from './hooks/useSegments';
import useDirectoryAccess from './hooks/useDirectoryAccess';
import { UserSettingsContext, SegColorsContext, UserSettingsContextType, AppContext, AppContextType, SegColorsContextType } from './contexts';
import type { UserSettingsContextType, AppContextType, SegColorsContextType } from './contexts';
import { UserSettingsContext, SegColorsContext, AppContext } from './contexts';
import NoFileLoaded from './NoFileLoaded';
import MediaSourcePlayer from './MediaSourcePlayer';
@ -49,6 +51,8 @@ import * as Dialog from './components/Dialog';
import { loadMifiLink, runStartupCheck } from './mifi';
import { darkModeTransition } from './colors';
import { getSegColor } from './util/colors';
import type {
FileFfprobeMeta } from './ffmpeg';
import {
getStreamFps, isCuttingStart, isCuttingEnd,
readFileFfprobeMeta, getDefaultOutFormat,
@ -58,7 +62,6 @@ import {
RefuseOverwriteError, extractSubtitleTrackToSegments,
mapRecommendedDefaultFormat,
getFfCommandLine,
FileFfprobeMeta,
} from './ffmpeg';
import { shouldCopyStreamByDefault, getAudioStreams, getRealVideoStreams, isAudioDefinitelyNotSupported, willPlayerProperlyHandleVideo, doesPlayerSupportHevcPlayback, getSubtitleStreams, enableVideoTrack, enableAudioTrack, canHtml5PlayerPlayStreams, isMatroska } from './util/streams';
import { exportEdlFile, readEdlFile, loadLlcProject, askForEdlImport } from './edlStore';
@ -79,16 +82,19 @@ import {
import getSwal, { errorToast, showPlaybackFailedMessage } from './swal';
import { adjustRate } from './util/rate-calculator';
import { askExtractFramesAsImages } from './dialogs/extractFrames';
import { askForOutDir, askForImportChapters, askForFileOpenAction, showDiskFull, showExportFailedDialog, showConcatFailedDialog, openYouTubeChaptersDialog, showRefuseToOverwrite, showOpenDialog, showMuxNotSupported, promptDownloadMediaUrl, CleanupChoicesType, showOutputNotWritable, deleteFiles, mustDisallowVob, toastError } from './dialogs';
import type { CleanupChoicesType } from './dialogs';
import { askForOutDir, askForImportChapters, askForFileOpenAction, showDiskFull, showExportFailedDialog, showConcatFailedDialog, openYouTubeChaptersDialog, showRefuseToOverwrite, showOpenDialog, showMuxNotSupported, promptDownloadMediaUrl, showOutputNotWritable, deleteFiles, mustDisallowVob, toastError } from './dialogs';
import { openSendReportDialog } from './reporting';
import { sortSegments, convertSegmentsToChaptersWithGaps, hasAnySegmentOverlap, isDurationValid, getPlaybackAction, getSegmentTags, filterNonMarkers, isInitialSegment } from './segments';
import { generateCutFileNames as generateCutFileNamesRaw, generateCutMergedFileNames as generateCutMergedFileNamesRaw, generateMergedFileNames as generateMergedFileNamesRaw, defaultCutFileTemplate, defaultCutMergedFileTemplate, defaultMergedFileTemplate, GenerateMergedOutFileNamesParams, GeneratedOutFileNames } from './util/outputNameTemplate';
import type { GenerateMergedOutFileNamesParams, GeneratedOutFileNames } from './util/outputNameTemplate';
import { generateCutFileNames as generateCutFileNamesRaw, generateCutMergedFileNames as generateCutMergedFileNamesRaw, generateMergedFileNames as generateMergedFileNamesRaw, defaultCutFileTemplate, defaultCutMergedFileTemplate, defaultMergedFileTemplate } from './util/outputNameTemplate';
import { rightBarWidth, leftBarWidth, ffmpegExtractWindow, zoomMax } from './util/constants';
import BigWaveform from './components/BigWaveform';
import { BatchFile, Chapter, CustomTagsByFile, EdlExportType, EdlFileType, EdlImportType, FfmpegCommandLog, FilesMeta, FileStats, goToTimecodeDirectArgsSchema, openFilesActionArgsSchema, ParamsByStreamId, PlaybackMode, SegmentBase, SegmentColorIndex, SegmentTags, StateSegment, TunerType } from './types';
import { CaptureFormat, KeyboardAction, ApiActionRequest } from '../../common/types.js';
import { FFprobeChapter, FFprobeStream } from '../../common/ffprobe.js';
import type { BatchFile, Chapter, CustomTagsByFile, EdlExportType, EdlFileType, EdlImportType, FfmpegCommandLog, FilesMeta, FileStats, ParamsByStreamId, PlaybackMode, SegmentBase, SegmentColorIndex, SegmentTags, StateSegment, TunerType } from './types';
import { goToTimecodeDirectArgsSchema, openFilesActionArgsSchema } from './types';
import type { CaptureFormat, KeyboardAction, ApiActionRequest } from '../../common/types.js';
import type { FFprobeChapter, FFprobeStream } from '../../common/ffprobe.js';
import useLoading from './hooks/useLoading';
import useVideo from './hooks/useVideo';
import useTimecode from './hooks/useTimecode';
@ -107,11 +113,10 @@ import useHtml5ify from './hooks/useHtml5ify';
import WhatsNew from './components/WhatsNew';
const electron = window.require('electron');
const { exists } = window.require('fs-extra');
const { lstat } = window.require('fs/promises');
const { parse: parsePath, join: pathJoin, basename, dirname } = window.require('path');
const { focusWindow, hasDisabledNetworking, quitApp, pathToFileURL, setProgressBar, sendOsNotification, lossyMode } = window.require('@electron/remote').require('./index.js');
const { focusWindow, hasDisabledNetworking, quitApp, pathToFileURL, setProgressBar, sendOsNotification, lossyMode, pathExists } = window.require('@electron/remote').require('./index.js');
const hevcPlaybackSupportedPromise = doesPlayerSupportHevcPlayback();
@ -1334,7 +1339,7 @@ function App() {
const loadMedia = useCallback(async ({ filePath: fp, projectPath }: { filePath: string, projectPath?: string | undefined }) => {
async function tryOpenProjectPath(path: string) {
if (!(await exists(path))) return false;
if (!(await pathExists(path))) return false;
await loadEdlFile({ path, type: 'llc' });
return true;
}
@ -1348,9 +1353,9 @@ function App() {
// then try to open project from source file dir
const sameDirEdlFilePath = getEdlFilePath(fp);
// MAS only allows fs.access (fs-extra.exists) if we don't have access to input dir yet, so check first if the file exists,
// MAS only allows fs.access (pathExists) if we don't have access to input dir yet, so check first if the file exists,
// so we don't need to annoy the user by asking for permission if the project file doesn't exist
if (await exists(sameDirEdlFilePath)) {
if (await pathExists(sameDirEdlFilePath)) {
// Ok, the file exists. now we have to ask the user, because we need to read that file
await ensureAccessToSourceDir(fp);
// Ok, we got access from the user (or already have access), now read the project file
@ -1517,8 +1522,8 @@ function App() {
const mediaFilePath = pathJoin(dirname(path), mediaFileName);
// Note: MAS only allows fs.stat (fs-extra.exists) if we don't have access to input dir yet
if (!(await exists(mediaFilePath))) {
// Note: MAS only allows fs.stat (pathExists) if we don't have access to input dir yet
if (!(await pathExists(mediaFilePath))) {
errorToast(i18n.t('The media file referenced by the project file you tried to open does not exist in the same directory as the project file: {{mediaFileName}}', { mediaFileName }));
return;
}

@ -1,4 +1,5 @@
import { CSSProperties, ClipboardEvent, Dispatch, FormEvent, SetStateAction, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { CSSProperties, ClipboardEvent, Dispatch, FormEvent, SetStateAction } from 'react';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { motion } from 'framer-motion';
import { MdRotate90DegreesCcw } from 'react-icons/md';
import { useTranslation } from 'react-i18next';
@ -24,9 +25,9 @@ import { useSegColors } from './contexts';
import { isExactDurationMatch } from './util/duration';
import useUserSettings from './hooks/useUserSettings';
import { askForPlaybackRate, checkAppPath } from './dialogs';
import { FormatTimecode, ParseTimecode, PlaybackMode, SegmentColorIndex, SegmentToExport, StateSegment } from './types';
import { WaveformMode } from '../../common/types';
import { Frame } from './ffmpeg';
import type { FormatTimecode, ParseTimecode, PlaybackMode, SegmentColorIndex, SegmentToExport, StateSegment } from './types';
import type { WaveformMode } from '../../common/types';
import type { Frame } from './ffmpeg';
const { clipboard } = window.require('electron');

@ -1,4 +1,5 @@
import { Component, ErrorInfo, ReactNode } from 'react';
import type { ErrorInfo, ReactNode } from 'react';
import { Component } from 'react';
import { Trans } from 'react-i18next';
import { openSendReportDialog } from './reporting';

@ -1,10 +1,11 @@
import { Dispatch, memo, SetStateAction } from 'react';
import type { Dispatch, SetStateAction } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { DateTime } from 'luxon';
import sortBy from 'lodash/sortBy.js';
import CopyClipboardButton from './components/CopyClipboardButton';
import { FfmpegCommandLog } from './types';
import type { FfmpegCommandLog } from './types';
import Button from './components/Button';
import * as Dialog from './components/Dialog';

@ -1,11 +1,12 @@
import { useEffect, useRef, useState, useCallback, useMemo, memo, CSSProperties, RefObject, ReactEventHandler, FocusEventHandler } from 'react';
import type { CSSProperties, RefObject, ReactEventHandler, FocusEventHandler } from 'react';
import { useEffect, useRef, useState, useCallback, useMemo, memo } from 'react';
import invariant from 'tiny-invariant';
import debounce from 'lodash/debounce.js';
import { FaVideo } from 'react-icons/fa';
import isDev from './isDev';
import { ChromiumHTMLVideoElement } from './types';
import { FFprobeStream } from '../../common/ffprobe';
import type { ChromiumHTMLVideoElement } from './types';
import type { FFprobeStream } from '../../common/ffprobe';
import { getFrameDuration } from './util';
const { compatPlayer: { createMediaSourceStream } } = window.require('@electron/remote').require('./index.js');

@ -1,13 +1,14 @@
import { Fragment, memo, useMemo, useState } from 'react';
import { motion, MotionStyle } from 'framer-motion';
import type { MotionStyle } from 'framer-motion';
import { motion } from 'framer-motion';
import { FaMouse } from 'react-icons/fa';
import { useTranslation, Trans } from 'react-i18next';
import SetCutpointButton from './components/SetCutpointButton';
import SimpleModeButton from './components/SimpleModeButton';
import useUserSettings from './hooks/useUserSettings';
import { StateSegment } from './types';
import { KeyBinding } from '../../common/types';
import type { StateSegment } from './types';
import type { KeyBinding } from '../../common/types';
import { splitKeyboardKeys } from './util';
import { getModifier } from './hooks/useTimelineScroll';
import Kbd from './components/Kbd';

@ -1,9 +1,11 @@
import { memo, useMemo, useRef, useCallback, useState, SetStateAction, Dispatch, MouseEventHandler, CSSProperties, useEffect } from 'react';
import type { SetStateAction, Dispatch, MouseEventHandler, CSSProperties } from 'react';
import { memo, useMemo, useRef, useCallback, useState, useEffect } from 'react';
import { FaYinYang, FaSave, FaPlus, FaMinus, FaTag, FaSortNumericDown, FaRegCheckCircle, FaRegCircle, FaTimes } from 'react-icons/fa';
import { AiOutlineSplitCells } from 'react-icons/ai';
import { motion } from 'framer-motion';
import { useTranslation, Trans } from 'react-i18next';
import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, DragEndEvent, DragStartEvent, DragOverlay, UniqueIdentifier } from '@dnd-kit/core';
import type { DragEndEvent, DragStartEvent, UniqueIdentifier } from '@dnd-kit/core';
import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, DragOverlay } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy, arrayMove, useSortable } from '@dnd-kit/sortable';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import { useVirtualizer } from '@tanstack/react-virtual';
@ -17,8 +19,8 @@ import { saveColor, controlsBackground, primaryTextColor, darkModeTransition } f
import { useSegColors } from './contexts';
import { getSegmentTags } from './segments';
import TagEditor from './components/TagEditor';
import { ContextMenuTemplate, DefiniteSegmentBase, FormatTimecode, GetFrameCount, InverseCutSegment, SegmentBase, SegmentColorIndex, SegmentTags, StateSegment } from './types';
import { UseSegments } from './hooks/useSegments';
import type { ContextMenuTemplate, DefiniteSegmentBase, FormatTimecode, GetFrameCount, InverseCutSegment, SegmentBase, SegmentColorIndex, SegmentTags, StateSegment } from './types';
import type { UseSegments } from './hooks/useSegments';
import * as Dialog from './components/Dialog';
import { DialogButton } from './components/Button';
import getSwal from './swal';

@ -1,4 +1,5 @@
import { memo, useState, useMemo, useCallback, Dispatch, SetStateAction, CSSProperties, ReactNode, ChangeEventHandler, DragEventHandler } from 'react';
import type { Dispatch, SetStateAction, CSSProperties, ReactNode, ChangeEventHandler, DragEventHandler } from 'react';
import { memo, useState, useMemo, useCallback } from 'react';
import { FaImage, FaPaperclip, FaVideo, FaVideoSlash, FaFileImport, FaVolumeUp, FaVolumeMute, FaBan, FaFileExport, FaBook, FaInfoCircle, FaFilter, FaEye, FaEdit, FaTrash, FaSortNumericDown, FaSortNumericUp, FaHamburger, FaMap } from 'react-icons/fa';
import { GoFileBinary } from 'react-icons/go';
@ -10,12 +11,13 @@ import * as DropdownMenu from './components/DropdownMenu';
import * as Dialog from './components/Dialog';
import AutoExportToggler from './components/AutoExportToggler';
import Select from './components/Select';
import { FileStream, getStreamFps } from './ffmpeg';
import type { FileStream } from './ffmpeg';
import { getStreamFps } from './ffmpeg';
import { deleteDispositionValue } from './util';
import { getActiveDisposition, attachedPicDisposition, isGpsStream } from './util/streams';
import TagEditor from './components/TagEditor';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../common/ffprobe';
import { CustomTagsByFile, FilesMeta, FormatTimecode, ParamsByStreamId, StreamParams } from './types';
import type { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../common/ffprobe';
import type { CustomTagsByFile, FilesMeta, FormatTimecode, ParamsByStreamId, StreamParams } from './types';
import Button, { DialogButton } from './components/Button';
import Checkbox from './components/Checkbox';
import styles from './StreamsSelector.module.css';

@ -1,4 +1,5 @@
import { memo, useRef, useMemo, useCallback, useEffect, useState, MutableRefObject, CSSProperties, WheelEventHandler, MouseEventHandler } from 'react';
import type { MutableRefObject, CSSProperties, WheelEventHandler, MouseEventHandler } from 'react';
import { memo, useRef, useMemo, useCallback, useEffect, useState } from 'react';
import { motion, useMotionValue, useSpring } from 'framer-motion';
import debounce from 'lodash/debounce';
import { useTranslation } from 'react-i18next';
@ -14,10 +15,10 @@ import styles from './Timeline.module.css';
import { timelineBackground, darkModeTransition } from './colors';
import { Frame } from './ffmpeg';
import { FormatTimecode, InverseCutSegment, OverviewWaveform, RenderableWaveform, WaveformSlice, StateSegment, Thumbnail } from './types';
import type { Frame } from './ffmpeg';
import type { FormatTimecode, InverseCutSegment, OverviewWaveform, RenderableWaveform, WaveformSlice, StateSegment, Thumbnail } from './types';
import Button from './components/Button';
import { UseSegments } from './hooks/useSegments';
import type { UseSegments } from './hooks/useSegments';
import { keyMap } from './hooks/useTimelineScroll';

@ -1,11 +1,12 @@
import { memo, useCallback, useMemo } from 'react';
import { motion, AnimatePresence, MotionStyle } from 'framer-motion';
import type { MotionStyle } from 'framer-motion';
import { motion, AnimatePresence } from 'framer-motion';
import { FaSave, FaTrashAlt } from 'react-icons/fa';
import Color from 'color';
import type Color from 'color';
import useUserSettings from './hooks/useUserSettings';
import { useSegColors } from './contexts';
import { FormatTimecode, StateSegment } from './types';
import type { FormatTimecode, StateSegment } from './types';
const markerButtonStyle: React.CSSProperties = { fontSize: 10, minWidth: 0, letterSpacing: '-.1em', color: 'white' };

@ -1,4 +1,5 @@
import { CSSProperties, ReactNode, memo, useCallback, useEffect, useRef } from 'react';
import type { CSSProperties, ReactNode } from 'react';
import { memo, useCallback, useEffect, useRef } from 'react';
import { IoIosSettings } from 'react-icons/io';
import { FaFilter, FaList, FaLock, FaMoon, FaSun, FaTimes, FaUnlock } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';

@ -1,5 +1,5 @@
import { FaBolt } from 'react-icons/fa';
import { KeyboardAction } from '../../../common/types';
import type { KeyboardAction } from '../../../common/types';
export default function Action({ name }: { name: KeyboardAction }) {

@ -1,4 +1,5 @@
import { motion, HTMLMotionProps } from 'framer-motion';
import type { HTMLMotionProps } from 'framer-motion';
import { motion } from 'framer-motion';
export default function AnimatedTr(props: HTMLMotionProps<'tr'>) {
return (

@ -1,4 +1,5 @@
import { memo, useRef, useMemo, useCallback, CSSProperties, MouseEventHandler } from 'react';
import type { CSSProperties, MouseEventHandler } from 'react';
import { memo, useRef, useMemo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { FaFile, FaTimes } from 'react-icons/fa';
import { useSortable } from '@dnd-kit/sortable';

@ -1,15 +1,17 @@
import { DragEventHandler, memo, useCallback, useMemo, useState } from 'react';
import type { DragEventHandler } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { motion } from 'framer-motion';
import { FaTimes, FaHatWizard, FaSortAlphaDown, FaSortAlphaUp } from 'react-icons/fa';
import { AiOutlineMergeCells } from 'react-icons/ai';
import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, DragEndEvent, DragStartEvent, DragOverlay, UniqueIdentifier } from '@dnd-kit/core';
import type { DragEndEvent, DragStartEvent, UniqueIdentifier } from '@dnd-kit/core';
import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, DragOverlay } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy, arrayMove } from '@dnd-kit/sortable';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import BatchFile from './BatchFile';
import { controlsBackground, darkModeTransition, primaryColor } from '../colors';
import { BatchFile as BatchFileType } from '../types';
import type { BatchFile as BatchFileType } from '../types';
import useUserSettings from '../hooks/useUserSettings';

@ -1,7 +1,8 @@
import { memo, useEffect, useState, useCallback, useRef, CSSProperties, MouseEventHandler, WheelEventHandler, useMemo } from 'react';
import type { CSSProperties, MouseEventHandler, WheelEventHandler } from 'react';
import { memo, useEffect, useState, useCallback, useRef, useMemo } from 'react';
import { ffmpegExtractWindow } from '../util/constants';
import { WaveformSlice } from '../types';
import type { WaveformSlice } from '../types';
import Spinner from './Spinner';

@ -1,4 +1,5 @@
import { ButtonHTMLAttributes, DetailedHTMLProps, forwardRef } from 'react';
import type { ButtonHTMLAttributes, DetailedHTMLProps } from 'react';
import { forwardRef } from 'react';
import styles from './Button.module.css';
import { primaryColor, primaryTextColor } from '../colors';

@ -1,5 +1,6 @@
import { useId } from 'react';
import { Root, Indicator, CheckboxProps } from '@radix-ui/react-checkbox';
import type { CheckboxProps } from '@radix-ui/react-checkbox';
import { Root, Indicator } from '@radix-ui/react-checkbox';
import { FaCheck } from 'react-icons/fa';
import classes from './Checkbox.module.css';

@ -1,5 +1,5 @@
import { FaTimes } from 'react-icons/fa';
import { DetailedHTMLProps, ButtonHTMLAttributes } from 'react';
import type { DetailedHTMLProps, ButtonHTMLAttributes } from 'react';
import styles from './CloseButton.module.css';
import i18n from '../i18n';

@ -1,4 +1,5 @@
import { memo, useState, useCallback, useEffect, useMemo, CSSProperties, Dispatch, SetStateAction } from 'react';
import type { CSSProperties, Dispatch, SetStateAction } from 'react';
import { memo, useState, useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { AiOutlineMergeCells } from 'react-icons/ai';
import { FaQuestionCircle, FaExclamationTriangle, FaCog, FaCheck, FaNotEqual } from 'react-icons/fa';
@ -7,19 +8,21 @@ import invariant from 'tiny-invariant';
import pMap from 'p-map';
import Checkbox from './Checkbox';
import { readFileFfprobeMeta, getDefaultOutFormat, mapRecommendedDefaultFormat, FileFfprobeMeta } from '../ffmpeg';
import type { FileFfprobeMeta } from '../ffmpeg';
import { readFileFfprobeMeta, getDefaultOutFormat, mapRecommendedDefaultFormat } from '../ffmpeg';
import OutputFormatSelect from './OutputFormatSelect';
import useUserSettings from '../hooks/useUserSettings';
import { isMov } from '../util/streams';
import { getOutDir, readFileStats } from '../util';
import { FFprobeStream } from '../../../common/ffprobe';
import type { FFprobeStream } from '../../../common/ffprobe';
import Button, { DialogButton } from './Button';
import { defaultMergedFileTemplate, GeneratedOutFileNames, GenerateMergedOutFileNames } from '../util/outputNameTemplate';
import type { GeneratedOutFileNames, GenerateMergedOutFileNames } from '../util/outputNameTemplate';
import { defaultMergedFileTemplate } from '../util/outputNameTemplate';
import { dangerColor, saveColor, warningColor } from '../colors';
import * as Dialog from './Dialog';
import FileNameTemplateEditor from './FileNameTemplateEditor';
import HighlightedText from './HighlightedText';
import { FileStats } from '../types';
import type { FileStats } from '../types';
const { basename } = window.require('path');

@ -1,6 +1,8 @@
import { memo, ReactNode, useCallback } from 'react';
import type { ReactNode } from 'react';
import { memo, useCallback } from 'react';
import { FaClipboard } from 'react-icons/fa';
import { MotionStyle, motion, useAnimation } from 'framer-motion';
import type { MotionStyle } from 'framer-motion';
import { motion, useAnimation } from 'framer-motion';
import i18n from '../i18n';
const electron = window.require('electron');

@ -1,5 +1,5 @@
import * as Dialog from '@radix-ui/react-dialog';
import { ReactNode } from 'react';
import type { ReactNode } from 'react';
import styles from './Dialog.module.css';
import { withClass } from './util';

@ -1,11 +1,12 @@
import { CSSProperties, forwardRef, MouseEventHandler } from 'react';
import type { CSSProperties, MouseEventHandler } from 'react';
import { forwardRef } from 'react';
import { FiScissors } from 'react-icons/fi';
import { FaFileExport } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';
import { primaryColor } from '../colors';
import useUserSettings from '../hooks/useUserSettings';
import { SegmentToExport } from '../types';
import type { SegmentToExport } from '../types';
import styles from './ExportButton.module.css';

@ -1,4 +1,5 @@
import { CSSProperties, Dispatch, ReactNode, SetStateAction, memo, useCallback, useMemo, useState } from 'react';
import type { CSSProperties, Dispatch, ReactNode, SetStateAction } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { FaExclamationTriangle, FaInfoCircle, FaRegCheckCircle } from 'react-icons/fa';
import i18n from 'i18next';
import { useTranslation, Trans } from 'react-i18next';
@ -18,15 +19,16 @@ import getSwal from '../swal';
import { isMov as ffmpegIsMov } from '../util/streams';
import useUserSettings from '../hooks/useUserSettings';
import styles from './ExportConfirm.module.css';
import { SegmentToExport } from '../types';
import { defaultCutFileTemplate, defaultCutMergedFileTemplate, GenerateOutFileNames } from '../util/outputNameTemplate';
import { FFprobeStream } from '../../../common/ffprobe';
import { AvoidNegativeTs, PreserveMetadata } from '../../../common/types';
import type { SegmentToExport } from '../types';
import type { GenerateOutFileNames } from '../util/outputNameTemplate';
import { defaultCutFileTemplate, defaultCutMergedFileTemplate } from '../util/outputNameTemplate';
import type { FFprobeStream } from '../../../common/ffprobe';
import type { AvoidNegativeTs, PreserveMetadata } from '../../../common/types';
import TextInput from './TextInput';
import { UseSegments } from '../hooks/useSegments';
import type { UseSegments } from '../hooks/useSegments';
import ExportSheet from './ExportSheet';
import ToggleExportConfirm from './ToggleExportConfirm';
import { LossyMode } from '../../../main';
import type { LossyMode } from '../../../main';
import AnimatedTr from './AnimatedTr';

@ -1,10 +1,11 @@
import { CSSProperties, memo, useMemo } from 'react';
import type { CSSProperties } from 'react';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { withBlur } from '../util';
import useUserSettings from '../hooks/useUserSettings';
import Select from './Select';
import { ExportMode } from '../types';
import type { ExportMode } from '../types';
function ExportModeButton({ selectedSegments, style }: { selectedSegments: unknown[], style?: CSSProperties }) {

@ -1,4 +1,4 @@
import { CSSProperties, ReactNode } from 'react';
import type { CSSProperties, ReactNode } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import styles from './ExportSheet.module.css';

@ -1,5 +1,6 @@
import { useTranslation } from 'react-i18next';
import { FormEventHandler, forwardRef, ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import type { FormEventHandler, ReactNode } from 'react';
import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
import * as AlertDialog from './AlertDialog';
import { DialogButton } from './Button';

@ -1,4 +1,5 @@
import { memo, useState, useEffect, useCallback, useRef, useMemo, ChangeEventHandler } from 'react';
import type { ChangeEventHandler } from 'react';
import { memo, useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { useDebounce } from 'use-debounce';
import i18n from 'i18next';
import { useTranslation } from 'react-i18next';
@ -7,7 +8,8 @@ import { motion, AnimatePresence } from 'framer-motion';
import { FaCaretUp, FaEdit, FaExclamationTriangle, FaEye, FaFile, FaUndo } from 'react-icons/fa';
import HighlightedText from './HighlightedText';
import { segNumVariable, segSuffixVariable, GenerateOutFileNames, extVariable, segTagsVariable, segNumIntVariable, selectedSegNumVariable, selectedSegNumIntVariable, GeneratedOutFileNames } from '../util/outputNameTemplate';
import type { GenerateOutFileNames, GeneratedOutFileNames } from '../util/outputNameTemplate';
import { segNumVariable, segSuffixVariable, extVariable, segTagsVariable, segNumIntVariable, selectedSegNumVariable, selectedSegNumIntVariable } from '../util/outputNameTemplate';
import useUserSettings from '../hooks/useUserSettings';
import Switch from './Switch';
import Select from './Select';

@ -1,4 +1,5 @@
import React, { MouseEventHandler, ReactNode, useCallback, useContext, useMemo, useRef, useState } from 'react';
import type { MouseEventHandler, ReactNode } from 'react';
import React, { useCallback, useContext, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import invariant from 'tiny-invariant';
import { FaCheckCircle, FaInfoCircle } from 'react-icons/fa';
@ -7,7 +8,8 @@ import * as Dialog from './Dialog';
import * as AlertDialog from './AlertDialog';
import { DialogButton } from './Button';
import { showItemInFolder } from '../util';
import { CleanupChoice, CleanupChoicesType, ListItem, Notices, OutputIncorrectSeeHelpMenu, UnorderedList, Warnings } from '../dialogs';
import type { CleanupChoice, CleanupChoicesType } from '../dialogs';
import { ListItem, Notices, OutputIncorrectSeeHelpMenu, UnorderedList, Warnings } from '../dialogs';
import Checkbox from './Checkbox';
import { saveColor, warningColor } from '../colors';

@ -1,4 +1,5 @@
import { CSSProperties, HTMLAttributes, memo } from 'react';
import type { CSSProperties, HTMLAttributes } from 'react';
import { memo } from 'react';
import { primaryTextColor } from '../colors';
import styles from './HighlightedText.module.css';

@ -1,4 +1,5 @@
import { HTMLAttributes, useMemo } from 'react';
import type { HTMLAttributes } from 'react';
import { useMemo } from 'react';
import { useAppContext } from '../contexts';
import { getMetaKeyName } from '../util';

@ -1,4 +1,5 @@
import { memo, Fragment, useEffect, useMemo, useCallback, useState, ReactNode, SetStateAction, Dispatch, useRef } from 'react';
import type { ReactNode, SetStateAction, Dispatch } from 'react';
import { memo, Fragment, useEffect, useMemo, useCallback, useState, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { FaMouse, FaPlus, FaStepForward, FaStepBackward, FaUndo, FaTrash, FaSave, FaHammer } from 'react-icons/fa';
import groupBy from 'lodash/groupBy';
@ -10,8 +11,8 @@ import useUserSettings from '../hooks/useUserSettings';
import SetCutpointButton from './SetCutpointButton';
import SegmentCutpointButton from './SegmentCutpointButton';
import { getModifier } from '../hooks/useTimelineScroll';
import { KeyBinding, KeyboardAction, ModifierKey } from '../../../common/types';
import { StateSegment } from '../types';
import type { KeyBinding, KeyboardAction, ModifierKey } from '../../../common/types';
import type { StateSegment } from '../types';
import { allModifiers, altModifiers, controlModifiers, metaModifiers, shiftModifiers, splitKeyboardKeys } from '../util';
import * as Dialog from './Dialog';
import Warning from './Warning';

@ -1,7 +1,9 @@
import { CSSProperties, memo, useMemo } from 'react';
import type { CSSProperties } from 'react';
import { memo, useMemo } from 'react';
import i18n from 'i18next';
import allOutFormats, { FfmpegFormat } from '../outFormats';
import type { FfmpegFormat } from '../outFormats';
import allOutFormats from '../outFormats';
import { withBlur } from '../util';
import Select from './Select';

@ -1,4 +1,5 @@
import { memo, useState, useCallback, useRef, useEffect, ChangeEventHandler, ChangeEvent } from 'react';
import type { ChangeEventHandler, ChangeEvent } from 'react';
import { memo, useState, useCallback, useRef, useEffect } from 'react';
import { MdSubtitles } from 'react-icons/md';
import { useTranslation } from 'react-i18next';
import { motion } from 'framer-motion';
@ -6,7 +7,7 @@ import { motion } from 'framer-motion';
import Select from './Select';
import Switch from './Switch';
import styles from './PlaybackStreamSelector.module.css';
import { FFprobeStream } from '../../../common/ffprobe';
import type { FFprobeStream } from '../../../common/ffprobe';
function PlaybackStreamSelector({

@ -1,9 +1,10 @@
import { CSSProperties, useMemo } from 'react';
import { FaStepForward } from 'react-icons/fa';
import type { CSSProperties } from 'react';
import { useMemo } from 'react';
import type { FaStepForward } from 'react-icons/fa';
import { useSegColors } from '../contexts';
import useUserSettings from '../hooks/useUserSettings';
import { SegmentColorIndex } from '../types';
import type { SegmentColorIndex } from '../types';
const SegmentCutpointButton = ({ currentCutSeg, side, Icon, onClick, title, style }: {
currentCutSeg: SegmentColorIndex | undefined,

@ -1,4 +1,5 @@
import { SelectHTMLAttributes, memo } from 'react';
import type { SelectHTMLAttributes } from 'react';
import { memo } from 'react';
import styles from './Select.module.css';

@ -1,9 +1,9 @@
import { CSSProperties } from 'react';
import type { CSSProperties } from 'react';
import { FaHandPointUp } from 'react-icons/fa';
import SegmentCutpointButton from './SegmentCutpointButton';
import { mirrorTransform } from '../util';
import { SegmentColorIndex } from '../types';
import type { SegmentColorIndex } from '../types';
// constant side because we are mirroring
const SetCutpointButton = ({ currentCutSeg, side, title, onClick, style }: {

@ -1,4 +1,5 @@
import { CSSProperties, ChangeEventHandler, TdHTMLAttributes, memo, useCallback, useMemo } from 'react';
import type { CSSProperties, ChangeEventHandler, TdHTMLAttributes } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { FaYinYang, FaKeyboard, FaGlobe, FaBroom, FaCogs, FaHashtag, FaClock, FaFolder, FaFile, FaTimes } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';
import invariant from 'tiny-invariant';
@ -10,13 +11,16 @@ import Switch from './Switch';
import useUserSettings from '../hooks/useUserSettings';
import { askForFfPath } from '../dialogs';
import { isMasBuild, isStoreBuild } from '../util';
import { SupportedLanguage, langNames } from '../../../common/i18n';
import { Config, ModifierKey, TimecodeFormat } from '../../../common/types.js';
import type { SupportedLanguage } from '../../../common/i18n';
import { langNames } from '../../../common/i18n';
import type { Config, ModifierKey, TimecodeFormat } from '../../../common/types.js';
import styles from './Settings.module.css';
import SelectRaw, { SelectProps } from './Select';
import ButtonRaw, { ButtonProps } from './Button';
import type { SelectProps } from './Select';
import SelectRaw from './Select';
import type { ButtonProps } from './Button';
import ButtonRaw from './Button';
import { getModifierKeyNames } from '../hooks/useTimelineScroll';
import { TunerType } from '../types';
import type { TunerType } from '../types';
import Truncated from './Truncated';
import { dangerColor } from '../colors';

@ -1,4 +1,5 @@
import { CSSProperties, memo } from 'react';
import type { CSSProperties } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { FaBaby } from 'react-icons/fa';

@ -1,4 +1,4 @@
import { HTMLAttributes } from 'react';
import type { HTMLAttributes } from 'react';
import { swalContainerWrapperId } from '../swal';
export default function SwalContainer({ darkMode, ...props }: HTMLAttributes<HTMLDivElement> & { darkMode: boolean }) {

@ -1,4 +1,4 @@
import { RefAttributes } from 'react';
import type { RefAttributes } from 'react';
import * as RadixSwitch from '@radix-ui/react-switch';
import classes from './Switch.module.css';

@ -1,10 +1,12 @@
import { memo, useRef, useState, useMemo, useCallback, useEffect, FormEventHandler } from 'react';
import type { FormEventHandler } from 'react';
import { memo, useRef, useState, useMemo, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import invariant from 'tiny-invariant';
import { motion } from 'framer-motion';
import { FaCheck, FaEdit, FaPlus, FaTrash, FaUndo } from 'react-icons/fa';
import { SegmentTags, segmentTagsSchema } from '../types';
import type { SegmentTags } from '../types';
import { segmentTagsSchema } from '../types';
import CopyClipboardButton from './CopyClipboardButton';
import { errorToast } from '../swal';
import TextInput from './TextInput';

@ -1,4 +1,5 @@
import { CSSProperties, forwardRef } from 'react';
import type { CSSProperties } from 'react';
import { forwardRef } from 'react';
const inputStyle: CSSProperties = { borderRadius: '.4em', flexGrow: 1, fontFamily: 'inherit', fontSize: '.8em', backgroundColor: 'var(--gray-3)', color: 'var(--gray-12)', border: '1px solid var(--gray-7)', appearance: 'none' };

@ -1,4 +1,5 @@
import { CSSProperties, memo } from 'react';
import type { CSSProperties } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { MdEventNote } from 'react-icons/md';

@ -1,4 +1,4 @@
import { CSSProperties, DetailedHTMLProps, HTMLAttributes } from 'react';
import type { CSSProperties, DetailedHTMLProps, HTMLAttributes } from 'react';
export default function Truncated({ maxWidth, style, ...props }: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & { maxWidth: CSSProperties['maxWidth'] }) {
return (

@ -1,4 +1,5 @@
import { memo, useState, useCallback, ChangeEventHandler } from 'react';
import type { ChangeEventHandler } from 'react';
import { memo, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import styles from './ValueTuner.module.css';

@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import ValueTuner from './ValueTuner';
import useUserSettings from '../hooks/useUserSettings';
import { TunerType } from '../types';
import type { TunerType } from '../types';
function ValueTuners({ type, onFinished }: { type: TunerType, onFinished: () => void }) {

@ -1,4 +1,5 @@
import { memo, useState, useCallback, useRef, useEffect, ChangeEventHandler } from 'react';
import type { ChangeEventHandler } from 'react';
import { memo, useState, useCallback, useRef, useEffect } from 'react';
import { FaVolumeMute, FaVolumeUp } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';

@ -1,4 +1,5 @@
import { DetailedHTMLProps, HTMLAttributes, useMemo } from 'react';
import type { DetailedHTMLProps, HTMLAttributes } from 'react';
import { useMemo } from 'react';
import { warningColor } from '../colors';

@ -1,12 +1,12 @@
import React, { useContext } from 'react';
import Color from 'color';
import type Color from 'color';
import invariant from 'tiny-invariant';
import { UserSettingsRoot } from './hooks/useUserSettingsRoot';
import { ExportMode, KeyboardLayoutMap, SegmentColorIndex } from './types';
import type { UserSettingsRoot } from './hooks/useUserSettingsRoot';
import type { ExportMode, KeyboardLayoutMap, SegmentColorIndex } from './types';
import type useLoading from './hooks/useLoading';
import { GenericError } from './components/ErrorDialog';
import { ConfirmDialog, ShowGenericDialog } from './components/GenericDialog';
import type { GenericError } from './components/ErrorDialog';
import type { ConfirmDialog, ShowGenericDialog } from './components/GenericDialog';
export type UserSettingsContextType = Omit<UserSettingsRoot, 'settings'> & UserSettingsRoot['settings'] & {

@ -1,4 +1,4 @@
import { CSSProperties, ReactNode } from 'react';
import type { CSSProperties, ReactNode } from 'react';
import i18n from 'i18next';
import { Trans } from 'react-i18next';
import invariant from 'tiny-invariant';
@ -10,8 +10,8 @@ import { formatDuration } from '../util/duration';
import { parseYouTube } from '../edlFormats';
import CopyClipboardButton from '../components/CopyClipboardButton';
import { appPath, isMac, isMasBuild, isWindows, isWindowsStoreBuild, testFailFsOperation, trashFile, unlinkWithRetry } from '../util';
import { ParseTimecode } from '../types';
import { FindKeyframeMode } from '../ffmpeg';
import type { ParseTimecode } from '../types';
import type { FindKeyframeMode } from '../ffmpeg';
import { dangerColor, primaryColor, warningColor } from '../colors';
import getSwal from '../swal';
import isDev from '../isDev';

@ -1,6 +1,7 @@
import { it, describe, expect, test } from 'vitest';
import { parseSrtToSegments, formatSrt, parseYouTube, formatYouTube, parseMplayerEdl, parseXmeml, parseFcpXml, parseCsv, parseCsvTime, getFrameValParser, formatCsvFrames, getFrameCountRaw, parsePbf, parseDvAnalyzerSummaryTxt, parseCutlist, parseDjiGps1, Otio, parseOtio, parseDjiGps2 } from './edlFormats';
import type { Otio } from './edlFormats';
import { parseSrtToSegments, formatSrt, parseYouTube, formatYouTube, parseMplayerEdl, parseXmeml, parseFcpXml, parseCsv, parseCsvTime, getFrameValParser, formatCsvFrames, getFrameCountRaw, parsePbf, parseDvAnalyzerSummaryTxt, parseCutlist, parseDjiGps1, parseOtio, parseDjiGps2 } from './edlFormats';
import { readFixture, readFixtureBinary } from './test/util';
import otioFixture from './test/fixtures/otio';

@ -11,7 +11,7 @@ import { z } from 'zod';
import { formatDuration } from './util/duration';
import { invertSegments, sortSegments } from './segments';
import { GetFrameCount, SegmentBase, SegmentTags } from './types';
import type { GetFrameCount, SegmentBase, SegmentTags } from './types';
import parseCmx3600 from './cmx3600';
import { UserFacingError } from '../errors';

@ -6,7 +6,8 @@ import { ZodError } from 'zod';
import { parseSrtToSegments, formatSrt, parseCuesheet, parseXmeml, parseFcpXml, parseCsv, parseCutlist, parsePbf, parseEdl, formatCsvHuman, formatTsvHuman, formatCsvFrames, formatCsvSeconds, parseCsvTime, getFrameValParser, parseDvAnalyzerSummaryTxt, parseOtio } from './edlFormats';
import { askForYouTubeInput, showOpenDialog } from './dialogs';
import { getOutPath } from './util';
import { EdlExportType, EdlFileType, EdlImportType, GetFrameCount, LlcProject, llcProjectV1Schema, llcProjectV2Schema, SegmentBase, StateSegment } from './types';
import type { EdlExportType, EdlFileType, EdlImportType, GetFrameCount, LlcProject, SegmentBase, StateSegment } from './types';
import { llcProjectV1Schema, llcProjectV2Schema } from './types';
import { mapSaveableSegments } from './segments';
import isDev from './isDev';

@ -1,18 +1,19 @@
import pMap from 'p-map';
import sortBy from 'lodash/sortBy';
import i18n from 'i18next';
import Timecode, { FRAMERATE } from 'smpte-timecode';
import type { FRAMERATE } from 'smpte-timecode';
import Timecode from 'smpte-timecode';
import minBy from 'lodash/minBy';
import invariant from 'tiny-invariant';
import { pcmAudioCodecs, isMov } from './util/streams';
import { isExecaError } from './util';
import { isDurationValid } from './segments';
import { FFprobeChapter, FFprobeFormat, FFprobeProbeResult, FFprobeStream } from '../../common/ffprobe';
import type { FFprobeChapter, FFprobeFormat, FFprobeProbeResult, FFprobeStream } from '../../common/ffprobe';
import { parseSrt, parseSrtToSegments } from './edlFormats';
import { UnsupportedFileError, UserFacingError } from '../errors';
const { ffmpeg, fileTypePromise } = window.require('@electron/remote').require('./index.js');
const { ffmpeg, fileTypeFromFile } = window.require('@electron/remote').require('./index.js');
const { renderWaveformPng, mapTimesToSegments, detectSceneChanges, captureFrames, captureFrameToFile, captureFrameToClipboard, getFfCommandLine, runFfmpegConcat, runFfmpegWithProgress, getDuration, abortFfmpegs, runFfmpeg, runFfprobe, getFfmpegPath, setCustomFfPath } = ffmpeg;
@ -267,7 +268,7 @@ async function determineSourceFileFormat(ffprobeFormatsStr: string | undefined,
// We need to test mp3 first because ffprobe seems to report the wrong format sometimes https://github.com/mifi/lossless-cut/issues/2129
if (firstFfprobeFormat === 'mp3') {
// file-type detects it correctly
const fileTypeResponse = await (await fileTypePromise).fileTypeFromFile(filePath);
const fileTypeResponse = await fileTypeFromFile(filePath);
if (fileTypeResponse?.mime === 'audio/mpeg') {
return 'mp2';
}
@ -286,7 +287,7 @@ async function determineSourceFileFormat(ffprobeFormatsStr: string | undefined,
return firstFfprobeFormat;
}
const fileTypeResponse = await (await fileTypePromise).fileTypeFromFile(filePath);
const fileTypeResponse = await fileTypeFromFile(filePath);
if (fileTypeResponse == null) {
console.warn('file-type failed to detect format, defaulting to first FFprobe detected format', ffprobeFormats);
return firstFfprobeFormat;

@ -1,6 +1,6 @@
// This code is for future use (e.g. creating black video to fill in using same codec parameters)
import { FFprobeStream } from '../../common/ffprobe';
import type { FFprobeStream } from '../../common/ffprobe';
export function parseLevel(videoStream: FFprobeStream) {
const { level: levelNumeric, codec_name: videoCodec } = videoStream;

@ -1,6 +1,6 @@
// Taken from: https://github.com/facebookarchive/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js
import { WheelEvent } from 'react';
import type { WheelEvent } from 'react';
/**
* Copyright (c) 2015, Facebook, Inc.

@ -1,7 +1,8 @@
import { RefObject, useEffect } from 'react';
import type { RefObject } from 'react';
import { useEffect } from 'react';
import useNativeMenu from './useNativeMenu';
import { ContextMenuTemplate } from '../types';
import type { ContextMenuTemplate } from '../types';
// https://github.com/transflow/use-electron-context-menu

@ -3,7 +3,7 @@ import { useCallback, useState } from 'react';
import { DirectoryAccessDeclinedError, UnsupportedFileError } from '../../errors';
import { isAbortedError } from '../util';
import { GenericError } from '../components/ErrorDialog';
import type { GenericError } from '../components/ErrorDialog';
export default function useErrorHandling() {

@ -10,14 +10,15 @@ import { isCuttingStart, isCuttingEnd, runFfmpegWithProgress, getFfCommandLine,
import { getMapStreamsArgs, getStreamIdsToCopy } from '../util/streams';
import { needsSmartCut, getCodecParams } from '../smartcut';
import { getGuaranteedSegments, isDurationValid } from '../segments';
import { FFprobeStream } from '../../../common/ffprobe';
import { AvoidNegativeTs, Html5ifyMode, PreserveMetadata } from '../../../common/types';
import { AllFilesMeta, Chapter, CopyfileStreams, CustomTagsByFile, LiteFFprobeStream, ParamsByStreamId, SegmentToExport } from '../types';
import { LossyMode } from '../../../main';
import type { FFprobeStream } from '../../../common/ffprobe';
import type { AvoidNegativeTs, Html5ifyMode, PreserveMetadata } from '../../../common/types';
import type { AllFilesMeta, Chapter, CopyfileStreams, CustomTagsByFile, LiteFFprobeStream, ParamsByStreamId, SegmentToExport } from '../types';
import type { LossyMode } from '../../../main';
import { UserFacingError } from '../../errors';
const { join, resolve, dirname } = window.require('path');
const { writeFile, mkdir, access, constants: { F_OK, W_OK } } = window.require('fs/promises');
const { writeFile, mkdir, access, constants: { W_OK } } = window.require('fs/promises');
const { pathExists } = window.require('@electron/remote').require('./index.js');
export class OutputNotWritableError extends Error {
@ -70,15 +71,6 @@ async function tryDeleteFiles(paths: string[]) {
return pMap(paths, (path) => unlinkWithRetry(path).catch((err) => console.error('Failed to delete', path, err)), { concurrency: 5 });
}
async function pathExists(path: string) {
try {
await access(path, F_OK);
return true;
} catch {
return false;
}
}
export async function maybeMkDeepOutDir({ outputDir, fileOutPath }: { outputDir: string, fileOutPath: string }) {
// cutFileNames might contain slashes and therefore might have a subdir(tree) that we need to mkdir
// https://github.com/mifi/lossless-cut/issues/1532

@ -6,8 +6,8 @@ import { getSuffixedOutPath, getOutDir, transferTimestamps, getSuffixedFileName,
import { getNumDigits, isDurationValid } from '../segments';
import * as ffmpeg from '../ffmpeg';
import { FormatTimecode } from '../types';
import { CaptureFormat } from '../../../common/types';
import type { FormatTimecode } from '../types';
import type { CaptureFormat } from '../../../common/types';
const mime = window.require('mime-types');
const { rename, readdir, writeFile } = window.require('fs/promises');

@ -1,16 +1,18 @@
import { useState, useCallback, ChangeEventHandler } from 'react';
import type { ChangeEventHandler } from 'react';
import { useState, useCallback } from 'react';
import i18n from 'i18next';
import { useTranslation } from 'react-i18next';
import { Html5ifyMode } from '../../../common/types';
import type { Html5ifyMode } from '../../../common/types';
import { DirectoryAccessDeclinedError } from '../../errors';
import getSwal from '../swal';
import Checkbox from '../components/Checkbox';
import { getSuffixedOutPath, html5dummySuffix, html5ifiedPrefix } from '../util';
import { SetWorking } from './useLoading';
import { WithErrorHandling } from './useErrorHandling';
import { FfmpegOperations } from './useFfmpegOperations';
import { ShowGenericDialog, useGenericDialogContext } from '../components/GenericDialog';
import type { SetWorking } from './useLoading';
import type { WithErrorHandling } from './useErrorHandling';
import type { FfmpegOperations } from './useFfmpegOperations';
import type { ShowGenericDialog } from '../components/GenericDialog';
import { useGenericDialogContext } from '../components/GenericDialog';
import * as Dialog from '../components/Dialog';
import { ButtonRow } from '../components/Dialog';
import { DialogButton } from '../components/Button';

@ -1,8 +1,8 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { KeyBinding, KeyboardAction } from '../../../common/types';
import type { KeyBinding, KeyboardAction } from '../../../common/types';
import { allModifiers, altModifiers, controlModifiers, metaModifiers, shiftModifiers } from '../util';
import { KeyboardLayoutMap } from '../types';
import type { KeyboardLayoutMap } from '../types';
import isDev from '../isDev';

@ -3,10 +3,11 @@ import sortBy from 'lodash/sortBy';
import useDebounceOld from 'react-use/lib/useDebounce'; // Want to phase out this
import { useTranslation } from 'react-i18next';
import { readFramesAroundTime, findNearestKeyFrameTime as ffmpegFindNearestKeyFrameTime, Frame, readFrames } from '../ffmpeg';
import { FFprobeStream } from '../../../common/ffprobe';
import type { Frame } from '../ffmpeg';
import { readFramesAroundTime, findNearestKeyFrameTime as ffmpegFindNearestKeyFrameTime, readFrames } from '../ffmpeg';
import type { FFprobeStream } from '../../../common/ffprobe';
import { getFrameCountRaw } from '../edlFormats';
import { HandleError } from '../contexts';
import type { HandleError } from '../contexts';
const toObj = (map: Frame[]) => Object.fromEntries(map.map((frame) => [frame.time, frame]));

@ -1,4 +1,5 @@
import { useCallback, useRef, useMemo, useState, MutableRefObject, FormEvent, useEffect } from 'react';
import type { MutableRefObject, FormEvent } from 'react';
import { useCallback, useRef, useMemo, useState, useEffect } from 'react';
import { useStateWithHistory } from 'react-use/lib/useStateWithHistory';
import i18n from 'i18next';
import pMap from 'p-map';
@ -13,20 +14,23 @@ import { getFileSize, shuffleArray } from '../util';
import { errorToast } from '../swal';
import { createNumSegments as createNumSegmentsDialog, createFixedByteSixedSegments as createFixedByteSixedSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, labelSegmentDialog, askForShiftSegments, askForAlignSegments, selectSegmentsByLabelDialog, askForSegmentDuration, toastError } from '../dialogs';
import { createSegment, sortSegments, invertSegments, combineOverlappingSegments as combineOverlappingSegments2, combineSelectedSegments as combineSelectedSegments2, isDurationValid, addSegmentColorIndex, filterNonMarkers, makeDurationSegments, isInitialSegment } from '../segments';
import { parameters as allFfmpegParameters, FfmpegDialog, getHint, getLabel } from '../ffmpegParameters';
import type { FfmpegDialog } from '../ffmpegParameters';
import { parameters as allFfmpegParameters, getHint, getLabel } from '../ffmpegParameters';
import { maxSegmentsAllowed } from '../util/constants';
import { DefiniteSegmentBase, ParseTimecode, SegmentBase, segmentTagsSchema, SegmentToExport, StateSegment, UpdateSegAtIndex } from '../types';
import type { DefiniteSegmentBase, ParseTimecode, SegmentBase, SegmentToExport, StateSegment, UpdateSegAtIndex } from '../types';
import { segmentTagsSchema } from '../types';
import safeishEval from '../worker/eval';
import { FFprobeFormat, FFprobeStream } from '../../../common/ffprobe';
import { HandleError } from '../contexts';
import { ShowGenericDialog, useGenericDialogContext } from '../components/GenericDialog';
import type { FFprobeFormat, FFprobeStream } from '../../../common/ffprobe';
import type { HandleError } from '../contexts';
import type { ShowGenericDialog } from '../components/GenericDialog';
import { useGenericDialogContext } from '../components/GenericDialog';
import ExpressionDialog from '../components/ExpressionDialog';
import Button, { DialogButton } from '../components/Button';
import { ButtonRow } from '../components/Dialog';
import * as Dialog from '../components/Dialog';
import { UserFacingError } from '../../errors';
import { editSegmentByExpressionHelpUrl, selectSegmentByExpressionHelpUrl } from '../../../common/constants';
import { Segment as ScopeSegment } from '../../../common/userTypes';
import type { Segment as ScopeSegment } from '../../../common/userTypes';
const remote = window.require('@electron/remote');
const { shell } = remote;

@ -6,7 +6,7 @@ import isDev from '../isDev';
import { saveLlcProject } from '../edlStore';
import { mapSaveableSegments } from '../segments';
import { getSuffixedOutPath } from '../util';
import { StateSegment } from '../types';
import type { StateSegment } from '../types';
import { errorToast } from '../swal';
import i18n from '../i18n';

@ -4,14 +4,14 @@ import invariant from 'tiny-invariant';
import { Trans, useTranslation } from 'react-i18next';
import { isStreamThumbnail, shouldCopyStreamByDefault } from '../util/streams';
import StreamsSelector from '../StreamsSelector';
import { FFprobeStream } from '../../../common/ffprobe';
import { FilesMeta } from '../types';
import type StreamsSelector from '../StreamsSelector';
import type { FFprobeStream } from '../../../common/ffprobe';
import type { FilesMeta } from '../types';
import safeishEval from '../worker/eval';
import i18n from '../i18n';
import Action from '../components/Action';
import ExpressionDialog from '../components/ExpressionDialog';
import { ShowGenericDialog } from '../components/GenericDialog';
import type { ShowGenericDialog } from '../components/GenericDialog';
export default function useStreamsMeta({ mainStreams, externalFilesMeta, filePath, autoExportExtraStreams, showGenericDialog }: {

@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { extractSubtitleTrackVtt } from '../ffmpeg';
import { FFprobeStream } from '../../../common/ffprobe';
import type { FFprobeStream } from '../../../common/ffprobe';
export default () => {

@ -4,7 +4,7 @@ import invariant from 'tiny-invariant';
import sortBy from 'lodash/sortBy';
import { renderThumbnails as ffmpegRenderThumbnails } from '../ffmpeg';
import { Thumbnail } from '../types';
import type { Thumbnail } from '../types';
import { isDurationValid } from '../segments';
import { isExecaError } from '../util';

@ -1,12 +1,14 @@
import { FormEventHandler, useCallback, useMemo, useState } from 'react';
import type { FormEventHandler } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FormatTimecode, ParseTimecode } from '../types';
import type { FormatTimecode, ParseTimecode } from '../types';
import { getFrameCountRaw } from '../edlFormats';
import { getFrameDuration } from '../util';
import { TimecodeFormat } from '../../../common/types';
import type { TimecodeFormat } from '../../../common/types';
import { formatDuration, parseDuration } from '../util/duration';
import { ShowGenericDialog, useGenericDialogContext } from '../components/GenericDialog';
import type { ShowGenericDialog } from '../components/GenericDialog';
import { useGenericDialogContext } from '../components/GenericDialog';
import * as Dialog from '../components/Dialog';
import TextInput from '../components/TextInput';
import { ButtonRow } from '../components/Dialog';

@ -1,8 +1,9 @@
import { WheelEventHandler, useCallback } from 'react';
import type { WheelEventHandler } from 'react';
import { useCallback } from 'react';
import { t } from 'i18next';
import normalizeWheel from './normalizeWheel';
import { ModifierKey } from '../../../common/types';
import type { ModifierKey } from '../../../common/types';
import { getMetaKeyName } from '../util';
export const keyMap = {

@ -1,8 +1,8 @@
import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
import i18n from 'i18next';
import { Transition } from 'framer-motion';
import type { Transition } from 'framer-motion';
import { Config } from '../../../common/types.js';
import type { Config } from '../../../common/types.js';
import { errorToast } from '../swal';
import isDev from '../isDev';

@ -1,5 +1,6 @@
import { ReactEventHandler, useCallback, useMemo, useRef, useState } from 'react';
import { ChromiumHTMLVideoElement, PlaybackMode } from '../types';
import type { ReactEventHandler } from 'react';
import { useCallback, useMemo, useRef, useState } from 'react';
import type { ChromiumHTMLVideoElement, PlaybackMode } from '../types';
import { showPlaybackFailedMessage } from '../swal';
export default ({ filePath }: { filePath: string | undefined }) => {

@ -3,8 +3,8 @@ import sortBy from 'lodash/sortBy';
import invariant from 'tiny-invariant';
import { renderWaveformPng, safeCreateBlob } from '../ffmpeg';
import { OverviewWaveform, WaveformSlice } from '../types';
import { FFprobeStream } from '../../../common/ffprobe';
import type { OverviewWaveform, WaveformSlice } from '../types';
import type { FFprobeStream } from '../../../common/ffprobe';
const maxWaveforms = 100;

@ -1,11 +1,10 @@
import { Suspense, StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { enableMapSet } from 'immer';
import * as Electron from 'electron';
import Remote from '@electron/remote';
import type * as Electron from 'electron';
import type Remote from '@electron/remote';
import type path from 'node:path';
import type fsPromises from 'node:fs/promises';
import type fsExtraRaw from 'fs-extra';
import type mimeTypes from 'mime-types';
import type i18nextFsBackend from 'i18next-fs-backend';
import type cueParser from 'cue-parser';
@ -23,23 +22,20 @@ import '@fontsource/open-sans/700-italic.css';
import '@fontsource/open-sans/800.css';
import '@fontsource/open-sans/800-italic.css';
import type * as main from '../../main/index';
import type { KeyboardLayoutMap } from './types';
import App from './App';
import ErrorBoundary from './ErrorBoundary';
import './i18n';
import { RemoteApi } from '../../main';
import './main.css';
import './swal2.scss';
import { KeyboardLayoutMap } from './types';
// something wrong with the tyep
type FsExtra = typeof fsExtraRaw & { exists: (p: string) => Promise<boolean> };
type TypedRemote = Omit<typeof Remote, 'require'> & {
require: <T extends string>(module: T) => (
T extends './index.js' ? typeof main :
T extends './index.js' ? RemoteApi :
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any
);
@ -54,7 +50,6 @@ declare global {
T extends 'node:path' ? typeof path :
T extends 'fs/promises' ? typeof fsPromises :
T extends 'node:fs/promises' ? typeof fsPromises :
T extends 'fs-extra' ? FsExtra :
T extends 'mime-types' ? typeof mimeTypes :
T extends 'i18next-fs-backend' ? typeof i18nextFsBackend :
T extends 'cue-parser' ? typeof cueParser :

@ -4,7 +4,7 @@ import minBy from 'lodash/minBy';
import maxBy from 'lodash/maxBy';
import invariant from 'tiny-invariant';
import { DefiniteSegmentBase, PlaybackMode, SegmentBase, SegmentTags, SegmentToExport, StateSegment } from './types';
import type { DefiniteSegmentBase, PlaybackMode, SegmentBase, SegmentTags, SegmentToExport, StateSegment } from './types';
export const isDurationValid = (duration?: number): duration is number => duration != null && Number.isFinite(duration) && duration > 0;

@ -3,7 +3,7 @@ import i18n from 'i18next';
import { getRealVideoStreams, getVideoTimebase } from './util/streams';
import { readKeyframesAroundTime, findNextKeyframe, findKeyframeAtExactTime } from './ffmpeg';
import { FFprobeStream } from '../../common/ffprobe';
import type { FFprobeStream } from '../../common/ffprobe';
import { UserFacingError } from '../errors';
import { readFileSize } from './util';

@ -1,4 +1,4 @@
import { CSSProperties } from 'react';
import type { CSSProperties } from 'react';
import { controlsBackground, darkModeTransition } from './colors';

@ -1,6 +1,7 @@
import SwalRaw from 'sweetalert2/dist/sweetalert2.js';
import type { SweetAlertOptions } from 'sweetalert2';
import withReactContent, { ReactSweetAlert, SweetAlert2 } from 'sweetalert2-react-content';
import type { ReactSweetAlert, SweetAlert2 } from 'sweetalert2-react-content';
import withReactContent from 'sweetalert2-react-content';
import i18n from './i18n';
import { emitter as animationsEmitter } from './animations';

@ -1,6 +1,6 @@
import type { MenuItem, MenuItemConstructorOptions } from 'electron';
import { z } from 'zod';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../common/ffprobe';
import type { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../common/ffprobe';
import type { FileStream } from './ffmpeg';

@ -2,23 +2,23 @@ import i18n from 'i18next';
import pMap from 'p-map';
import prettyBytes from 'pretty-bytes';
import sortBy from 'lodash/sortBy';
import pRetry, { Options } from 'p-retry';
import { ExecaError } from 'execa';
import type { Options } from 'p-retry';
import pRetry from 'p-retry';
import type { ExecaError } from 'execa';
import confetti from 'canvas-confetti';
import invariant from 'tiny-invariant';
import { ffmpegExtractWindow } from './util/constants';
import { appName } from '../../main/common';
import { Html5ifyMode } from '../../common/types';
import type { Html5ifyMode } from '../../common/types';
import { UserFacingError } from '../errors';
import { FFprobeFormat } from '../../common/ffprobe';
import type { FFprobeFormat } from '../../common/ffprobe';
const { dirname, parse: parsePath, join, extname, isAbsolute, resolve, basename } = window.require('path');
const fsExtra = window.require('fs-extra');
const { stat, lstat, readdir, utimes, unlink } = window.require('fs/promises');
const { stat, lstat, readdir, utimes, unlink, open, access, constants: { R_OK, W_OK } } = window.require('fs/promises');
const { ipcRenderer } = window.require('electron');
const remote = window.require('@electron/remote');
const { isWindows, isMac } = remote.require('./index.js');
const { isWindows, isMac, pathExists } = remote.require('./index.js');
const appVersion = remote.app.getVersion();
const appPath = remote.app.getAppPath();
@ -69,9 +69,9 @@ export function getSuffixedOutPath({ customOutDir, filePath, nameSuffix }: { cus
export async function havePermissionToReadFile(filePath: string) {
try {
const fd = await fsExtra.open(filePath, 'r');
const fd = await open(filePath, 'r');
try {
await fsExtra.close(fd);
await fd.close();
} catch (err) {
console.error('Failed to close fd', err);
}
@ -84,7 +84,7 @@ export async function havePermissionToReadFile(filePath: string) {
export async function checkDirWriteAccess(dirPath: string) {
try {
await fsExtra.access(dirPath, fsExtra.constants.W_OK);
await access(dirPath, W_OK);
} catch (err) {
if (err instanceof Error && 'code' in err) {
if (err.code === 'EPERM') return false; // Thrown on Mac (MAS build) when user has not yet allowed access
@ -95,13 +95,9 @@ export async function checkDirWriteAccess(dirPath: string) {
return true;
}
export async function pathExists(pathIn: string) {
return fsExtra.pathExists(pathIn);
}
export async function getPathReadAccessError(pathIn: string) {
try {
await fsExtra.access(pathIn, fsExtra.constants.R_OK);
await access(pathIn, R_OK);
return undefined;
} catch (err) {
return err instanceof Error && 'code' in err && typeof err.code === 'string' ? err.code : undefined;

@ -1,7 +1,7 @@
import color from 'color';
import invariant from 'tiny-invariant';
import { SegmentColorIndex } from '../types';
import type { SegmentColorIndex } from '../types';
// http://phrogz.net/css/distinct-colors.html
const colorStrings = '#ff5100, #ffc569, #ddffd1, #00ccff, #e9d1ff, #ff0084, #ff6975, #ffe6d1, #ffff69, #69ff96, #008cff, #ae00ff, #ff002b, #ff8c00, #8cff00, #69ffff, #0044ff, #ff00d4, #ffd1d9'.split(',').map((str) => str.trim());

@ -1,5 +1,5 @@
import i18n from 'i18next';
import { PlatformPath } from 'node:path';
import type { PlatformPath } from 'node:path';
import pMap from 'p-map';
import max from 'lodash/max';
import invariant from 'tiny-invariant';
@ -9,10 +9,10 @@ import type { FileNameTemplateContext } from '../../../common/userTypes.ts';
import { isMac, isWindows, hasDuplicates, filenamify, getOutFileExtension } from '../util';
import isDev from '../isDev';
import { getSegmentTags, formatSegNum, getGuaranteedSegments } from '../segments';
import { FileStats, FormatTimecode, SegmentToExport } from '../types';
import type { FileStats, FormatTimecode, SegmentToExport } from '../types';
import safeishEval from '../worker/eval';
import { UserFacingError } from '../../errors';
import { FileFfprobeMeta } from '../ffmpeg';
import type { FileFfprobeMeta } from '../ffmpeg';
export const segNumVariable = 'SEG_NUM';

@ -1,8 +1,8 @@
import { test, expect } from 'vitest';
import { getMapStreamsArgs, getStreamIdsToCopy } from './streams';
import { FFprobeStreamDisposition } from '../../../common/ffprobe';
import { LiteFFprobeStream } from '../types';
import type { FFprobeStreamDisposition } from '../../../common/ffprobe';
import type { LiteFFprobeStream } from '../types';
const makeDisposition = (override?: Partial<FFprobeStreamDisposition>): FFprobeStreamDisposition => ({

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save