1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-25 11:43:17 +01:00

upgrade lint

This commit is contained in:
Mikael Finstad 2024-02-20 22:48:40 +08:00
parent e7c85dd8b5
commit 4704d246b4
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26
48 changed files with 1745 additions and 767 deletions

View File

@ -1,47 +1,37 @@
module.exports = { module.exports = {
extends: [ extends: ['mifi'],
'airbnb', rules: {
'airbnb/hooks', 'no-console': 0,
'eslint:recommended', 'jsx-a11y/click-events-have-key-events': 0,
'plugin:import/recommended', 'jsx-a11y/interactive-supports-focus': 0,
'plugin:import/typescript', 'jsx-a11y/control-has-associated-label': 0,
'plugin:@typescript-eslint/recommended', 'unicorn/prefer-node-protocol': 0, // todo
'plugin:@typescript-eslint/stylistic', '@typescript-eslint/no-var-requires': 0, // todo
'plugin:react/recommended', 'react/display-name': 0, // todo
'plugin:react/jsx-runtime', },
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended', overrides: [
], {
files: ['./src/**/*.{js,cjs,mjs,jsx,ts,tsx,mts}'],
env: { env: {
node: true, node: false,
browser: true, browser: true,
}, },
rules: { rules: {
'max-len': 0,
'import/no-extraneous-dependencies': ['error', { 'import/no-extraneous-dependencies': ['error', {
devDependencies: true, devDependencies: true,
optionalDependencies: false, optionalDependencies: false,
}], }],
'no-console': 0,
'jsx-a11y/click-events-have-key-events': 0,
'jsx-a11y/interactive-supports-focus': 0,
'react/jsx-one-expression-per-line': 0,
'object-curly-newline': 0,
'arrow-parens': 0,
'jsx-a11y/control-has-associated-label': 0,
'react/prop-types': 0,
'no-multiple-empty-lines': ['error', { max: 2, maxBOF: 0, maxEOF: 0 }],
'no-promise-executor-return': 0,
'react/function-component-definition': 0,
'no-constant-binary-expression': 'error',
'@typescript-eslint/no-var-requires': 0, // todo
'react/display-name': 0, // todo
'@typescript-eslint/no-unused-vars': 0, // todo
'import/extensions': 0, // doesn't work with TS https://github.com/import-js/eslint-plugin-import/issues/2111
'react/jsx-filename-extension': ['error', { extensions: ['.jsx', '.tsx'] }],
'react/require-default-props': 0,
}, },
parserOptions: {
ecmaVersion: 2022,
}, },
{
files: ['./script/**/*.{js,cjs,mjs,jsx,ts,tsx,mts}', 'vite.config.js'],
rules: {
'import/no-extraneous-dependencies': ['error', {
devDependencies: true,
optionalDependencies: false,
}],
},
},
],
}; };

View File

@ -21,10 +21,10 @@ Note: `yarn` may take some time to complete.
Run one of the below commands: Run one of the below commands:
```bash ```bash
npm run download-ffmpeg-darwin-x64 yarn download-ffmpeg-darwin-x64
npm run download-ffmpeg-darwin-arm64 yarn download-ffmpeg-darwin-arm64
npm run download-ffmpeg-linux-x64 yarn download-ffmpeg-linux-x64
npm run download-ffmpeg-win32-x64 yarn download-ffmpeg-win32-x64
``` ```
For Windows, you may have to install [7z](https://www.7-zip.org/download.html), and then put the 7z folder in your `PATH`. For Windows, you may have to install [7z](https://www.7-zip.org/download.html), and then put the 7z folder in your `PATH`.
@ -32,7 +32,7 @@ For Windows, you may have to install [7z](https://www.7-zip.org/download.html),
### Running ### Running
```bash ```bash
npm start yarn dev
``` ```
## `mas-dev` (Mac App Store) local build ## `mas-dev` (Mac App Store) local build
@ -40,7 +40,7 @@ npm start
This will sign using the development provisioning profile: This will sign using the development provisioning profile:
``` ```
npm run pack-mas-dev yarn pack-mas-dev
``` ```
MAS builds have some restrictions, see `isMasBuild` variable in code. In particular, any file cannot be read without the user's consent. MAS builds have some restrictions, see `isMasBuild` variable in code. In particular, any file cannot be read without the user's consent.
@ -86,7 +86,7 @@ For per-platform build/signing setup, see [this article](https://mifi.no/blog/au
- If Mac App Store / Windows Store - If Mac App Store / Windows Store
- Merge `stores` into `master` - Merge `stores` into `master`
- Bump [snap version](https://snapcraft.io/losslesscut/listing) - Bump [snap version](https://snapcraft.io/losslesscut/listing)
- `npm run scan-i18n` to get the newest English strings and push so weblate gets them - `yarn scan-i18n` to get the newest English strings and push so weblate gets them
## Minimum OS version ## Minimum OS version
@ -100,7 +100,7 @@ Minimum supported OS versions for Electron. As of electron 22:
How to check the value: How to check the value:
```bash ```bash
npm run pack-mas-dev yarn pack-mas-dev
cat dist/mas-dev-arm64/LosslessCut.app/Contents/Info.plist cat dist/mas-dev-arm64/LosslessCut.app/Contents/Info.plist
``` ```
@ -126,7 +126,7 @@ Links:
- package.json - package.json
### i18n ### i18n
`npm run scan-i18n` `yarn scan-i18n`
### Licenses ### Licenses
@ -139,7 +139,7 @@ npx license-checker --summary
#### Regenerate licenses file #### Regenerate licenses file
``` ```
npm run generate-licenses yarn generate-licenses
#cp licenses.txt losslesscut.mifi.no/public/ #cp licenses.txt losslesscut.mifi.no/public/
``` ```
Then deploy. Then deploy.

View File

@ -1,3 +1,4 @@
// eslint-disable-line unicorn/filename-case
export default { export default {
input: ['src/**/*.{js,jsx,ts,tsx}', 'public/*.{js,ts}'], input: ['src/**/*.{js,jsx,ts,tsx}', 'public/*.{js,ts}'],

View File

@ -7,9 +7,9 @@
"main": "public/electron.js", "main": "public/electron.js",
"homepage": "./", "homepage": "./",
"scripts": { "scripts": {
"start": "concurrently -k \"npm run start:frontend\" \"npm run start:electron\"", "dev": "concurrently -k \"npm run dev:frontend\" \"npm run dev:electron\"",
"start:frontend": "cross-env vite --port 3001", "dev:frontend": "cross-env vite --port 3001",
"start:electron": "wait-on tcp:3001 && electron .", "dev:electron": "wait-on tcp:3001 && electron .",
"icon-gen": "mkdirp icon-build build-resources/appx && node script/icon-gen.mjs", "icon-gen": "mkdirp icon-build build-resources/appx && node script/icon-gen.mjs",
"download-ffmpeg-darwin-x64": "mkdirp ffmpeg/darwin-x64 && cd ffmpeg/darwin-x64 && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-1/ffmpeg-macos-X64 -O ffmpeg && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-1/ffprobe-macos-X64 -O ffprobe && chmod +x ffmpeg && chmod +x ffprobe", "download-ffmpeg-darwin-x64": "mkdirp ffmpeg/darwin-x64 && cd ffmpeg/darwin-x64 && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-1/ffmpeg-macos-X64 -O ffmpeg && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-1/ffprobe-macos-X64 -O ffprobe && chmod +x ffmpeg && chmod +x ffprobe",
"download-ffmpeg-darwin-arm64": "mkdirp ffmpeg/darwin-arm64 && cd ffmpeg/darwin-arm64 && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-1/ffmpeg-macos-ARM64 -O ffmpeg && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-1/ffprobe-macos-ARM64 -O ffprobe && chmod +x ffmpeg && chmod +x ffprobe", "download-ffmpeg-darwin-arm64": "mkdirp ffmpeg/darwin-arm64 && cd ffmpeg/darwin-arm64 && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-1/ffmpeg-macos-ARM64 -O ffmpeg && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-1/ffprobe-macos-ARM64 -O ffprobe && chmod +x ffmpeg && chmod +x ffprobe",
@ -18,7 +18,7 @@
"build": "yarn icon-gen && vite build --outDir vite-dist", "build": "yarn icon-gen && vite build --outDir vite-dist",
"tsc": "tsc --build", "tsc": "tsc --build",
"test": "vitest", "test": "vitest",
"lint": "eslint --ext .jsx --ext .js . --ext .mjs", "lint": "eslint --ext .js,.ts,.jsx,.tsx,.mjs .",
"pack-mac": "electron-builder --mac -m dmg", "pack-mac": "electron-builder --mac -m dmg",
"prepack-mac": "yarn build", "prepack-mac": "yarn build",
"pack-mas-dev": "electron-builder --mac -m mas-dev -c.mas.provisioningProfile=LosslessCut_Dev.provisionprofile -c.mas.identity='Apple Development: Mikael Finstad (JH4PH8B3C8)'", "pack-mas-dev": "electron-builder --mac -m mas-dev -c.mas.provisioningProfile=LosslessCut_Dev.provisionprofile -c.mas.identity='Apple Development: Mikael Finstad (JH4PH8B3C8)'",
@ -47,10 +47,11 @@
"@radix-ui/react-switch": "^1.0.1", "@radix-ui/react-switch": "^1.0.1",
"@tsconfig/strictest": "^2.0.2", "@tsconfig/strictest": "^2.0.2",
"@tsconfig/vite-react": "^3.0.0", "@tsconfig/vite-react": "^3.0.0",
"@types/eslint": "^8",
"@types/lodash": "^4.14.202", "@types/lodash": "^4.14.202",
"@types/sortablejs": "^1.15.0", "@types/sortablejs": "^1.15.0",
"@typescript-eslint/eslint-plugin": "^6.17.0", "@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.17.0", "@typescript-eslint/parser": "^6.12.0",
"@uidotdev/usehooks": "^2.4.1", "@uidotdev/usehooks": "^2.4.1",
"@vitejs/plugin-react": "^3.1.0", "@vitejs/plugin-react": "^3.1.0",
"color": "^3.1.0", "color": "^3.1.0",
@ -61,13 +62,13 @@
"electron": "^27.0.0", "electron": "^27.0.0",
"electron-builder": "^24.6.4", "electron-builder": "^24.6.4",
"electron-devtools-installer": "^3.2.0", "electron-devtools-installer": "^3.2.0",
"eslint": "^8.53.0", "eslint": "^8.2.0",
"eslint-config-airbnb": "^19.0.4", "eslint-config-mifi": "^0.0.3",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.25.3", "eslint-plugin-import": "^2.25.3",
"eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-react": "^7.28.0", "eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-unicorn": "^51.0.1",
"evergreen-ui": "^6.13.1", "evergreen-ui": "^6.13.1",
"fast-xml-parser": "^4.2.5", "fast-xml-parser": "^4.2.5",
"framer-motion": "^9.0.3", "framer-motion": "^9.0.3",
@ -97,7 +98,7 @@
"sortablejs": "^1.13.0", "sortablejs": "^1.13.0",
"sweetalert2": "^11.0.0", "sweetalert2": "^11.0.0",
"sweetalert2-react-content": "^5.0.7", "sweetalert2-react-content": "^5.0.7",
"typescript": "^5.3.3", "typescript": "~5.2.0",
"typescript-plugin-css-modules": "^5.1.0", "typescript-plugin-css-modules": "^5.1.0",
"use-debounce": "^5.1.0", "use-debounce": "^5.1.0",
"use-trace-update": "^1.3.0", "use-trace-update": "^1.3.0",
@ -132,9 +133,6 @@
"winston": "^3.8.1", "winston": "^3.8.1",
"yargs-parser": "^21.0.0" "yargs-parser": "^21.0.0"
}, },
"eslintConfig": {
"extends": "react-app"
},
"build": { "build": {
"directories": { "directories": {
"buildResources": "build-resources" "buildResources": "build-resources"

View File

@ -7,6 +7,7 @@ function createMediaSourceStream({ path, videoStreamIndex, audioStreamIndex, see
logger.info('Starting preview process', { videoStreamIndex, audioStreamIndex, seekTo }); logger.info('Starting preview process', { videoStreamIndex, audioStreamIndex, seekTo });
const process = createMediaSourceProcess({ path, videoStreamIndex, audioStreamIndex, seekTo, size, fps }); const process = createMediaSourceProcess({ path, videoStreamIndex, audioStreamIndex, seekTo, size, fps });
// eslint-disable-next-line unicorn/prefer-add-event-listener
abortController.signal.onabort = () => { abortController.signal.onabort = () => {
logger.info('Aborting preview process', { videoStreamIndex, audioStreamIndex, seekTo }); logger.info('Aborting preview process', { videoStreamIndex, audioStreamIndex, seekTo });
process.kill('SIGKILL'); process.kill('SIGKILL');
@ -64,7 +65,7 @@ function createMediaSourceStream({ path, videoStreamIndex, audioStreamIndex, see
if (!err.killed) { if (!err.killed) {
console.warn(err.message); console.warn(err.message);
console.warn(stderr.toString('utf-8')); console.warn(stderr.toString('utf8'));
} }
} }
})(); })();
@ -76,6 +77,7 @@ function readOneJpegFrameWrapper({ path, seekTo, videoStreamIndex }) {
const abortController = new AbortController(); const abortController = new AbortController();
const process = readOneJpegFrame({ path, seekTo, videoStreamIndex }); const process = readOneJpegFrame({ path, seekTo, videoStreamIndex });
// eslint-disable-next-line unicorn/prefer-add-event-listener
abortController.signal.onabort = () => process.kill('SIGKILL'); abortController.signal.onabort = () => process.kill('SIGKILL');
function abort() { function abort() {

View File

@ -1,4 +1,5 @@
const Store = require('electron-store'); const Store = require('electron-store');
// eslint-disable-next-line import/no-extraneous-dependencies
const electron = require('electron'); const electron = require('electron');
const os = require('os'); const os = require('os');
const { join, dirname } = require('path'); const { join, dirname } = require('path');
@ -175,7 +176,7 @@ async function tryCreateStore({ customStoragePath }) {
return; return;
} catch (err) { } catch (err) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await new Promise(r => setTimeout(r, 2000)); await new Promise((r) => setTimeout(r, 2000));
logger.error('Failed to create config store, retrying', err); logger.error('Failed to create config store, retrying', err);
} }
} }
@ -198,12 +199,10 @@ async function init() {
} }
const cleanupChoices = store.get('cleanupChoices'); // todo remove after a while const cleanupChoices = store.get('cleanupChoices'); // todo remove after a while
if (cleanupChoices != null) { if (cleanupChoices != null && cleanupChoices.closeFile == null) {
if (cleanupChoices.closeFile == null) {
logger.info('Migrating cleanupChoices.closeFile'); logger.info('Migrating cleanupChoices.closeFile');
set('cleanupChoices', { ...cleanupChoices, closeFile: true }); set('cleanupChoices', { ...cleanupChoices, closeFile: true });
} }
}
} }
module.exports = { module.exports = {

View File

@ -1,3 +1,4 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const { Menu } = require('electron'); const { Menu } = require('electron');
// https://github.com/electron/electron/issues/4068#issuecomment-274159726 // https://github.com/electron/electron/issues/4068#issuecomment-274159726

View File

@ -1,6 +1,7 @@
process.traceDeprecation = true; process.traceDeprecation = true;
process.traceProcessWarnings = true; process.traceProcessWarnings = true;
// eslint-disable-next-line import/no-extraneous-dependencies
const electron = require('electron'); const electron = require('electron');
const isDev = require('electron-is-dev'); const isDev = require('electron-is-dev');
const unhandled = require('electron-unhandled'); const unhandled = require('electron-unhandled');
@ -14,7 +15,7 @@ const { stat } = require('fs/promises');
const logger = require('./logger'); const logger = require('./logger');
const menu = require('./menu'); const menu = require('./menu');
const configStore = require('./configStore'); const configStore = require('./configStore');
const { frontendBuildDir, isLinux, isWindows } = require('./util'); const { frontendBuildDir, isLinux } = require('./util');
const attachContextMenu = require('./contextMenu'); const attachContextMenu = require('./contextMenu');
const HttpServer = require('./httpServer'); const HttpServer = require('./httpServer');
@ -298,6 +299,7 @@ function initApp() {
// Call this immediately, to make sure we don't miss it (race condition) // Call this immediately, to make sure we don't miss it (race condition)
const readyPromise = app.whenReady(); const readyPromise = app.whenReady();
// eslint-disable-next-line unicorn/prefer-top-level-await
(async () => { (async () => {
try { try {
logger.info('Initializing config store'); logger.info('Initializing config store');
@ -343,8 +345,8 @@ const readyPromise = app.whenReady();
const { default: installExtension, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer'); // eslint-disable-line global-require,import/no-extraneous-dependencies const { default: installExtension, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer'); // eslint-disable-line global-require,import/no-extraneous-dependencies
installExtension(REACT_DEVELOPER_TOOLS) installExtension(REACT_DEVELOPER_TOOLS)
.then(name => logger.info('Added Extension', name)) .then((name) => logger.info('Added Extension', name))
.catch(err => logger.error('Failed to add extension', err)); .catch((err) => logger.error('Failed to add extension', err));
} }
createWindow(); createWindow();

View File

@ -18,8 +18,7 @@ function setCustomFfPath(path) {
} }
function getFfCommandLine(cmd, args) { function getFfCommandLine(cmd, args) {
const mapArg = arg => (/[^0-9a-zA-Z-_]/.test(arg) ? `'${arg}'` : arg); return `${cmd} ${args.map((arg) => (/[^\w-]/.test(arg) ? `'${arg}'` : arg)).join(' ')}`;
return `${cmd} ${args.map(mapArg).join(' ')}`;
} }
function getFfPath(cmd) { function getFfPath(cmd) {
@ -56,8 +55,10 @@ function handleProgress(process, durationIn, onProgress, customMatcher = () => u
// console.log('progress', line); // console.log('progress', line);
try { try {
// eslint-disable-next-line unicorn/better-regex
let match = line.match(/frame=\s*[^\s]+\s+fps=\s*[^\s]+\s+q=\s*[^\s]+\s+(?:size|Lsize)=\s*[^\s]+\s+time=\s*([^\s]+)\s+/); let match = line.match(/frame=\s*[^\s]+\s+fps=\s*[^\s]+\s+q=\s*[^\s]+\s+(?:size|Lsize)=\s*[^\s]+\s+time=\s*([^\s]+)\s+/);
// Audio only looks like this: "line size= 233422kB time=01:45:50.68 bitrate= 301.1kbits/s speed= 353x " // Audio only looks like this: "line size= 233422kB time=01:45:50.68 bitrate= 301.1kbits/s speed= 353x "
// eslint-disable-next-line unicorn/better-regex
if (!match) match = line.match(/(?:size|Lsize)=\s*[^\s]+\s+time=\s*([^\s]+)\s+/); if (!match) match = line.match(/(?:size|Lsize)=\s*[^\s]+\s+time=\s*([^\s]+)\s+/);
if (!match) { if (!match) {
customMatcher(line); customMatcher(line);
@ -108,7 +109,7 @@ function runFfmpegProcess(args, customExecaOptions, { logCli = true } = {}) {
runningFfmpegs.add(process); runningFfmpegs.add(process);
try { try {
await process; await process;
} catch (err) { } catch {
// ignored here // ignored here
} finally { } finally {
runningFfmpegs.delete(process); runningFfmpegs.delete(process);
@ -246,10 +247,11 @@ async function detectSceneChanges({ filePath, minChange, onProgress, from, to })
handleProgress(process, to - from, onProgress); handleProgress(process, to - from, onProgress);
const rl = readline.createInterface({ input: process.stdout }); const rl = readline.createInterface({ input: process.stdout });
rl.on('line', (line) => { rl.on('line', (line) => {
// eslint-disable-next-line unicorn/better-regex
const match = line.match(/^frame:\d+\s+pts:\d+\s+pts_time:([\d.]+)/); const match = line.match(/^frame:\d+\s+pts:\d+\s+pts_time:([\d.]+)/);
if (!match) return; if (!match) return;
const time = parseFloat(match[1]); const time = parseFloat(match[1]);
if (Number.isNaN(time) || time <= times[times.length - 1]) return; if (Number.isNaN(time) || time <= times.at(-1)) return;
times.push(time); times.push(time);
}); });
@ -288,6 +290,7 @@ const mapFilterOptions = (options) => Object.entries(options).map(([key, value])
async function blackDetect({ filePath, filterOptions, onProgress, from, to }) { async function blackDetect({ filePath, filterOptions, onProgress, from, to }) {
function matchLineTokens(line) { function matchLineTokens(line) {
// eslint-disable-next-line unicorn/better-regex
const match = line.match(/^[blackdetect\s*@\s*0x[0-9a-f]+] black_start:([\d\\.]+) black_end:([\d\\.]+) black_duration:[\d\\.]+/); const match = line.match(/^[blackdetect\s*@\s*0x[0-9a-f]+] black_start:([\d\\.]+) black_end:([\d\\.]+) black_duration:[\d\\.]+/);
if (!match) return {}; if (!match) return {};
return { return {
@ -301,6 +304,7 @@ async function blackDetect({ filePath, filterOptions, onProgress, from, to }) {
async function silenceDetect({ filePath, filterOptions, onProgress, from, to }) { async function silenceDetect({ filePath, filterOptions, onProgress, from, to }) {
function matchLineTokens(line) { function matchLineTokens(line) {
// eslint-disable-next-line unicorn/better-regex
const match = line.match(/^[silencedetect\s*@\s*0x[0-9a-f]+] silence_end: ([\d\\.]+)[|\s]+silence_duration: ([\d\\.]+)/); const match = line.match(/^[silencedetect\s*@\s*0x[0-9a-f]+] silence_end: ([\d\\.]+)[|\s]+silence_duration: ([\d\\.]+)/);
if (!match) return {}; if (!match) return {};
const end = parseFloat(match[1]); const end = parseFloat(match[1]);
@ -409,6 +413,7 @@ async function html5ify({ outPath, filePath: filePathArg, speed, hasAudio, hasVi
switch (video) { switch (video) {
case 'hq': { case 'hq': {
// eslint-disable-next-line unicorn/prefer-ternary
if (isMac) { if (isMac) {
videoArgs = ['-vf', 'format=yuv420p', '-allow_sw', '1', '-vcodec', 'h264', '-b:v', '15M']; videoArgs = ['-vf', 'format=yuv420p', '-allow_sw', '1', '-vcodec', 'h264', '-b:v', '15M'];
} else { } else {
@ -423,6 +428,7 @@ async function html5ify({ outPath, filePath: filePathArg, speed, hasAudio, hasVi
break; break;
} }
case 'lq': { case 'lq': {
// eslint-disable-next-line unicorn/prefer-ternary
if (isMac) { if (isMac) {
videoArgs = ['-vf', `scale=-2:${targetHeight},format=yuv420p`, '-allow_sw', '1', '-sws_flags', 'lanczos', '-vcodec', 'h264', '-b:v', '1500k']; videoArgs = ['-vf', `scale=-2:${targetHeight},format=yuv420p`, '-allow_sw', '1', '-sws_flags', 'lanczos', '-vcodec', 'h264', '-b:v', '1500k'];
} else { } else {
@ -443,6 +449,7 @@ async function html5ify({ outPath, filePath: filePathArg, speed, hasAudio, hasVi
switch (audio) { switch (audio) {
case 'hq': { case 'hq': {
// eslint-disable-next-line unicorn/prefer-ternary
if (isMac) { if (isMac) {
audioArgs = ['-acodec', 'aac_at', '-b:a', '192k']; audioArgs = ['-acodec', 'aac_at', '-b:a', '192k'];
} else { } else {
@ -451,6 +458,7 @@ async function html5ify({ outPath, filePath: filePathArg, speed, hasAudio, hasVi
break; break;
} }
case 'lq': { case 'lq': {
// eslint-disable-next-line unicorn/prefer-ternary
if (isMac) { if (isMac) {
audioArgs = ['-acodec', 'aac_at', '-ar', '44100', '-ac', '2', '-b:a', '96k']; audioArgs = ['-acodec', 'aac_at', '-ar', '44100', '-ac', '2', '-b:a', '96k'];
} else { } else {

View File

@ -1,6 +1,8 @@
// eslint-disable-line unicorn/filename-case
// intentionally disabled because I don't know the quality of the languages, so better to default to english // intentionally disabled because I don't know the quality of the languages, so better to default to english
// const LanguageDetector = window.require('i18next-electron-language-detector'); // const LanguageDetector = window.require('i18next-electron-language-detector');
const isDev = require('electron-is-dev'); const isDev = require('electron-is-dev');
// eslint-disable-next-line import/no-extraneous-dependencies
const { app } = require('electron'); const { app } = require('electron');
const { join } = require('path'); const { join } = require('path');

View File

@ -1,6 +1,7 @@
const winston = require('winston'); const winston = require('winston');
const util = require('util'); const util = require('util');
const isDev = require('electron-is-dev'); const isDev = require('electron-is-dev');
// eslint-disable-next-line import/no-extraneous-dependencies
const { app } = require('electron'); const { app } = require('electron');
const { join } = require('path'); const { join } = require('path');

View File

@ -1,9 +1,10 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const electron = require('electron'); const electron = require('electron');
const { t } = require('i18next'); const { t } = require('i18next');
// menu-safe i18n.t: // menu-safe i18n.t:
// https://github.com/mifi/lossless-cut/issues/1456 // https://github.com/mifi/lossless-cut/issues/1456
const esc = (val) => val.replace(/&/g, '&&'); const esc = (val) => val.replaceAll('&', '&&');
const { Menu } = electron; const { Menu } = electron;

View File

@ -1,3 +1,4 @@
// eslint-disable-line unicorn/filename-case
const GitHub = require('github-api'); const GitHub = require('github-api');
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
const electron = require('electron'); const electron = require('electron');

View File

@ -1,3 +1,4 @@
// eslint-disable-line unicorn/filename-case
import sharp from 'sharp'; import sharp from 'sharp';
import icongen from 'icon-gen'; import icongen from 'icon-gen';

View File

@ -1,3 +1,4 @@
// eslint-disable-line unicorn/filename-case
import execa from 'execa'; import execa from 'execa';
import { readFile } from 'fs/promises'; import { readFile } from 'fs/promises';

View File

@ -101,6 +101,7 @@ const videoStyle: CSSProperties = { width: '100%', height: '100%', objectFit: 'c
const bottomStyle: CSSProperties = { background: controlsBackground, transition: darkModeTransition }; const bottomStyle: CSSProperties = { background: controlsBackground, transition: darkModeTransition };
const hevcPlaybackSupportedPromise = doesPlayerSupportHevcPlayback(); const hevcPlaybackSupportedPromise = doesPlayerSupportHevcPlayback();
// eslint-disable-next-line unicorn/prefer-top-level-await
hevcPlaybackSupportedPromise.catch((err) => console.error(err)); hevcPlaybackSupportedPromise.catch((err) => console.error(err));
@ -256,7 +257,7 @@ function App() {
}); });
}, []); }, []);
const toggleSegmentsList = useCallback(() => setShowRightBar(v => !v), []); const toggleSegmentsList = useCallback(() => setShowRightBar((v) => !v), []);
const toggleCopyStreamId = useCallback((path, index) => { const toggleCopyStreamId = useCallback((path, index) => {
setCopyStreamIdsForPath(path, (old) => ({ ...old, [index]: !old[index] })); setCopyStreamIdsForPath(path, (old) => ({ ...old, [index]: !old[index] }));
@ -333,7 +334,7 @@ function App() {
!!(copyStreamIdsByFile[path] || {})[streamId] !!(copyStreamIdsByFile[path] || {})[streamId]
), [copyStreamIdsByFile]); ), [copyStreamIdsByFile]);
const checkCopyingAnyTrackOfType = useCallback((filter) => mainStreams.some(stream => isCopyingStreamId(filePath, stream.index) && filter(stream)), [filePath, isCopyingStreamId, mainStreams]); const checkCopyingAnyTrackOfType = useCallback((filter) => mainStreams.some((stream) => isCopyingStreamId(filePath, stream.index) && filter(stream)), [filePath, isCopyingStreamId, mainStreams]);
const copyAnyAudioTrack = useMemo(() => checkCopyingAnyTrackOfType((stream) => stream.codec_type === 'audio'), [checkCopyingAnyTrackOfType]); const copyAnyAudioTrack = useMemo(() => checkCopyingAnyTrackOfType((stream) => stream.codec_type === 'audio'), [checkCopyingAnyTrackOfType]);
const subtitleStreams = useMemo(() => getSubtitleStreams(mainStreams), [mainStreams]); const subtitleStreams = useMemo(() => getSubtitleStreams(mainStreams), [mainStreams]);
@ -633,7 +634,7 @@ function App() {
}, []); }, []);
const mainCopiedStreams = useMemo(() => mainStreams.filter((stream) => isCopyingStreamId(filePath, stream.index)), [filePath, isCopyingStreamId, mainStreams]); const mainCopiedStreams = useMemo(() => mainStreams.filter((stream) => isCopyingStreamId(filePath, stream.index)), [filePath, isCopyingStreamId, mainStreams]);
const mainCopiedThumbnailStreams = useMemo(() => mainCopiedStreams.filter(isStreamThumbnail), [mainCopiedStreams]); const mainCopiedThumbnailStreams = useMemo(() => mainCopiedStreams.filter((stream) => isStreamThumbnail(stream)), [mainCopiedStreams]);
// Streams that are not copy enabled by default // Streams that are not copy enabled by default
const extraStreams = useMemo(() => mainStreams.filter((stream) => !shouldCopyStreamByDefault(stream)), [mainStreams]); const extraStreams = useMemo(() => mainStreams.filter((stream) => !shouldCopyStreamByDefault(stream)), [mainStreams]);
@ -678,7 +679,7 @@ function App() {
function addThumbnail(thumbnail) { function addThumbnail(thumbnail) {
// console.log('Rendered thumbnail', thumbnail.url); // console.log('Rendered thumbnail', thumbnail.url);
setThumbnails(v => [...v, thumbnail]); setThumbnails((v) => [...v, thumbnail]);
} }
const hasAudio = !!activeAudioStream; const hasAudio = !!activeAudioStream;
@ -710,7 +711,7 @@ function App() {
// Cleanup removed thumbnails // Cleanup removed thumbnails
useEffect(() => { useEffect(() => {
thumnailsRef.current.forEach((thumbnail) => { thumnailsRef.current.forEach((thumbnail) => {
if (!thumbnails.some(t => t.url === thumbnail.url)) URL.revokeObjectURL(thumbnail.url); if (!thumbnails.some((t) => t.url === thumbnail.url)) URL.revokeObjectURL(thumbnail.url);
}); });
thumnailsRef.current = thumbnails; thumnailsRef.current = thumbnails;
}, [thumbnails]); }, [thumbnails]);
@ -719,7 +720,7 @@ function App() {
const subtitlesByStreamIdRef = useRef({}); const subtitlesByStreamIdRef = useRef({});
useEffect(() => { useEffect(() => {
Object.values(thumnailsRef.current).forEach(({ url }) => { Object.values(thumnailsRef.current).forEach(({ url }) => {
if (!Object.values(subtitlesByStreamId).some(t => t.url === url)) URL.revokeObjectURL(url); if (!Object.values(subtitlesByStreamId).some((t) => t.url === url)) URL.revokeObjectURL(url);
}); });
subtitlesByStreamIdRef.current = subtitlesByStreamId; subtitlesByStreamIdRef.current = subtitlesByStreamId;
}, [subtitlesByStreamId]); }, [subtitlesByStreamId]);
@ -732,7 +733,7 @@ function App() {
const resetMergedOutFileName = useCallback(() => { const resetMergedOutFileName = useCallback(() => {
const ext = getOutFileExtension({ isCustomFormatSelected, outFormat: fileFormat, filePath }); const ext = getOutFileExtension({ isCustomFormatSelected, outFormat: fileFormat, filePath });
const outFileName = getSuffixedFileName(filePath, `cut-merged-${new Date().getTime()}${ext}`); const outFileName = getSuffixedFileName(filePath, `cut-merged-${Date.now()}${ext}`);
setMergedOutFileName(outFileName); setMergedOutFileName(outFileName);
}, [fileFormat, filePath, isCustomFormatSelected]); }, [fileFormat, filePath, isCustomFormatSelected]);
@ -833,7 +834,7 @@ function App() {
}, [html5ify, html5ifyDummy]); }, [html5ify, html5ifyDummy]);
const convertFormatBatch = useCallback(async () => { const convertFormatBatch = useCallback(async () => {
if (batchFiles.length < 1) return; if (batchFiles.length === 0) return;
const filePaths = batchFiles.map((f) => f.path); const filePaths = batchFiles.map((f) => f.path);
const failedFiles: string[] = []; const failedFiles: string[] = [];
@ -1008,7 +1009,7 @@ function App() {
externalFilesMeta, externalFilesMeta,
mainStreams, mainStreams,
copyStreamIdsByFile, copyStreamIdsByFile,
cutSegments: cutSegments.map(s => ({ start: s.start, end: s.end })), cutSegments: cutSegments.map((s) => ({ start: s.start, end: s.end })),
mainFileFormatData, mainFileFormatData,
rotation, rotation,
shortestFlag, shortestFlag,
@ -1176,7 +1177,7 @@ function App() {
return; return;
} }
if (segmentsToExport.length < 1) { if (segmentsToExport.length === 0) {
return; return;
} }
@ -1526,6 +1527,7 @@ function App() {
await html5ifyAndLoadWithPreferences(cod, fp, 'fastest', haveVideoStream, haveAudioStream); await html5ifyAndLoadWithPreferences(cod, fp, 'fastest', haveVideoStream, haveAudioStream);
} }
// eslint-disable-next-line unicorn/prefer-ternary
if (projectPath) { if (projectPath) {
await loadEdlFile({ path: projectPath, type: 'llc' }); await loadEdlFile({ path: projectPath, type: 'llc' });
} else { } else {
@ -1565,16 +1567,14 @@ function App() {
// https://github.com/mifi/lossless-cut/issues/515 // https://github.com/mifi/lossless-cut/issues/515
setFilePath(fp); setFilePath(fp);
} catch (err) { } catch (err) {
if (err) {
if (err instanceof DirectoryAccessDeclinedError) return; if (err instanceof DirectoryAccessDeclinedError) return;
}
resetState(); resetState();
throw err; throw err;
} }
}, [storeProjectInWorkingDir, setWorking, loadEdlFile, getEdlFilePath, getEdlFilePathOld, enableAskForImportChapters, ensureAccessToSourceDir, loadCutSegments, autoLoadTimecode, enableNativeHevc, ensureWritableOutDir, customOutDir, resetState, setCopyStreamIdsForPath, setFileFormat, outFormatLocked, setDetectedFileFormat, html5ifyAndLoadWithPreferences, showPreviewFileLoadedMessage, showUnsupportedFileMessage, hideAllNotifications]); }, [storeProjectInWorkingDir, setWorking, loadEdlFile, getEdlFilePath, getEdlFilePathOld, enableAskForImportChapters, ensureAccessToSourceDir, loadCutSegments, autoLoadTimecode, enableNativeHevc, ensureWritableOutDir, customOutDir, resetState, setCopyStreamIdsForPath, setFileFormat, outFormatLocked, setDetectedFileFormat, html5ifyAndLoadWithPreferences, showPreviewFileLoadedMessage, showUnsupportedFileMessage, hideAllNotifications]);
const toggleLastCommands = useCallback(() => setLastCommandsVisible(val => !val), []); const toggleLastCommands = useCallback(() => setLastCommandsVisible((val) => !val), []);
const toggleSettings = useCallback(() => setSettingsVisible(val => !val), []); const toggleSettings = useCallback(() => setSettingsVisible((val) => !val), []);
const seekClosestKeyframe = useCallback((direction) => { const seekClosestKeyframe = useCallback((direction) => {
const time = findNearestKeyFrameTime({ time: getRelevantTime(), direction }); const time = findNearestKeyFrameTime({ time: getRelevantTime(), direction });
@ -1822,7 +1822,7 @@ function App() {
const userOpenFiles = useCallback(async (filePathsIn) => { const userOpenFiles = useCallback(async (filePathsIn) => {
let filePaths = filePathsIn; let filePaths = filePathsIn;
if (!filePaths || filePaths.length < 1) return; if (!filePaths || filePaths.length === 0) return;
console.log('userOpenFiles'); console.log('userOpenFiles');
console.log(filePaths.join('\n')); console.log(filePaths.join('\n'));
@ -1866,7 +1866,7 @@ function App() {
const firstFilePath = filePaths[0]; const firstFilePath = filePaths[0];
// https://en.wikibooks.org/wiki/Inside_DVD-Video/Directory_Structure // https://en.wikibooks.org/wiki/Inside_DVD-Video/Directory_Structure
if (/^VIDEO_TS$/i.test(basename(firstFilePath))) { if (/^video_ts$/i.test(basename(firstFilePath))) {
if (mustDisallowVob()) return; if (mustDisallowVob()) return;
filePaths = await readVideoTs(firstFilePath); filePaths = await readVideoTs(firstFilePath);
} }
@ -1975,7 +1975,7 @@ function App() {
const showIncludeExternalStreamsDialog = useCallback(async () => { const showIncludeExternalStreamsDialog = useCallback(async () => {
try { try {
const { canceled, filePaths } = await showOpenDialog({ properties: ['openFile'] }); const { canceled, filePaths } = await showOpenDialog({ properties: ['openFile'] });
if (canceled || filePaths.length < 1) return; if (canceled || filePaths.length === 0) return;
await addStreamSourceFile(filePaths[0]); await addStreamSourceFile(filePaths[0]);
} catch (err) { } catch (err) {
handleError(err); handleError(err);
@ -2031,9 +2031,9 @@ function App() {
play: () => play(), play: () => play(),
pause, pause,
reducePlaybackRate: () => changePlaybackRate(-1), reducePlaybackRate: () => changePlaybackRate(-1),
reducePlaybackRateMore: () => changePlaybackRate(-1, 2.0), reducePlaybackRateMore: () => changePlaybackRate(-1, 2),
increasePlaybackRate: () => changePlaybackRate(1), increasePlaybackRate: () => changePlaybackRate(1),
increasePlaybackRateMore: () => changePlaybackRate(1, 2.0), increasePlaybackRateMore: () => changePlaybackRate(1, 2),
timelineToggleComfortZoom, timelineToggleComfortZoom,
captureSnapshot, captureSnapshot,
captureSnapshotAsCoverArt, captureSnapshotAsCoverArt,
@ -2191,7 +2191,9 @@ function App() {
useKeyboard({ keyBindings, onKeyPress }); useKeyboard({ keyBindings, onKeyPress });
useEffect(() => { useEffect(() => {
// eslint-disable-next-line unicorn/prefer-add-event-listener
document.ondragover = dragPreventer; document.ondragover = dragPreventer;
// eslint-disable-next-line unicorn/prefer-add-event-listener
document.ondragend = dragPreventer; document.ondragend = dragPreventer;
electron.ipcRenderer.send('renderer-ready'); electron.ipcRenderer.send('renderer-ready');
@ -2309,7 +2311,7 @@ function App() {
// todo // todo
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const actionsWithArgs: Record<string, (...args: any[]) => void> = { const actionsWithArgs: Record<string, (...args: any[]) => void> = {
openFiles: (filePaths: string[]) => { userOpenFiles(filePaths.map(resolvePathIfNeeded)); }, openFiles: (filePaths: string[]) => { userOpenFiles(filePaths.map((p) => resolvePathIfNeeded(p))); },
// todo separate actions per type and move them into mainActions? https://github.com/mifi/lossless-cut/issues/254#issuecomment-932649424 // todo separate actions per type and move them into mainActions? https://github.com/mifi/lossless-cut/issues/254#issuecomment-932649424
importEdlFile, importEdlFile,
exportEdlFile: tryExportEdlFile, exportEdlFile: tryExportEdlFile,
@ -2351,7 +2353,7 @@ function App() {
ev.preventDefault(); ev.preventDefault();
if (!ev.dataTransfer) return; if (!ev.dataTransfer) return;
const { files } = ev.dataTransfer; const { files } = ev.dataTransfer;
const filePaths = Array.from(files).map(f => f.path); const filePaths = [...files].map((f) => f.path);
focusWindow(); focusWindow();
@ -2382,7 +2384,7 @@ function App() {
useEffect(() => { useEffect(() => {
const keyScrollPreventer = (e) => { const keyScrollPreventer = (e) => {
// https://stackoverflow.com/questions/8916620/disable-arrow-key-scrolling-in-users-browser // https://stackoverflow.com/questions/8916620/disable-arrow-key-scrolling-in-users-browser
if (e.target === document.body && [32, 37, 38, 39, 40].indexOf(e.keyCode) > -1) { if (e.target === document.body && [32, 37, 38, 39, 40].includes(e.keyCode)) {
e.preventDefault(); e.preventDefault();
} }
}; };
@ -2393,7 +2395,7 @@ function App() {
const showLeftBar = batchFiles.length > 0; const showLeftBar = batchFiles.length > 0;
const thumbnailsSorted = useMemo(() => sortBy(thumbnails, thumbnail => thumbnail.time), [thumbnails]); const thumbnailsSorted = useMemo(() => sortBy(thumbnails, (thumbnail) => thumbnail.time), [thumbnails]);
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -1,4 +1,4 @@
import React, { memo } from 'react'; import { memo } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { FaTrashAlt, FaSave } from 'react-icons/fa'; import { FaTrashAlt, FaSave } from 'react-icons/fa';

View File

@ -27,7 +27,7 @@ import { askForPlaybackRate } from './dialogs';
const { clipboard } = window.require('electron'); const { clipboard } = window.require('electron');
const zoomOptions = Array(13).fill().map((unused, z) => 2 ** z); const zoomOptions = Array.from({ length: 13 }).fill().map((unused, z) => 2 ** z);
const leftRightWidth = 100; const leftRightWidth = 100;
@ -35,7 +35,7 @@ const InvertCutModeButton = memo(({ invertCutSegments, setInvertCutSegments }) =
const { t } = useTranslation(); const { t } = useTranslation();
const onYinYangClick = useCallback(() => { const onYinYangClick = useCallback(() => {
setInvertCutSegments(v => { setInvertCutSegments((v) => {
const newVal = !v; const newVal = !v;
if (newVal) toast.fire({ title: t('When you export, selected segments on the timeline will be REMOVED - the surrounding areas will be KEPT') }); if (newVal) toast.fire({ title: t('When you export, selected segments on the timeline will be REMOVED - the surrounding areas will be KEPT') });
else toast.fire({ title: t('When you export, selected segments on the timeline will be KEPT - the surrounding areas will be REMOVED.') }); else toast.fire({ title: t('When you export, selected segments on the timeline will be KEPT - the surrounding areas will be REMOVED.') });
@ -154,7 +154,7 @@ const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, see
style={{ ...cutTimeInputStyle, color: isCutTimeManualSet() ? 'var(--red11)' : 'var(--gray12)' }} style={{ ...cutTimeInputStyle, color: isCutTimeManualSet() ? 'var(--red11)' : 'var(--gray12)' }}
type="text" type="text"
title={isStart ? t('Manually input current segment\'s start time') : t('Manually input current segment\'s end time')} title={isStart ? t('Manually input current segment\'s start time') : t('Manually input current segment\'s end time')}
onChange={e => handleCutTimeInput(e.target.value)} onChange={(e) => handleCutTimeInput(e.target.value)}
onPaste={handleCutTimePaste} onPaste={handleCutTimePaste}
onBlur={() => setCutTimeManual()} onBlur={() => setCutTimeManual()}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
@ -392,9 +392,9 @@ const BottomBar = memo(({
<div role="button" style={{ marginRight: 5, marginLeft: 10 }} title={t('Zoom')} onClick={timelineToggleComfortZoom}>{Math.floor(zoom)}x</div> <div role="button" style={{ marginRight: 5, marginLeft: 10 }} title={t('Zoom')} onClick={timelineToggleComfortZoom}>{Math.floor(zoom)}x</div>
<Select style={{ height: 20, flexBasis: 85, flexGrow: 0 }} value={zoomOptions.includes(zoom) ? zoom.toString() : ''} title={t('Zoom')} onChange={withBlur(e => setZoom(parseInt(e.target.value, 10)))}> <Select style={{ height: 20, flexBasis: 85, flexGrow: 0 }} value={zoomOptions.includes(zoom) ? zoom.toString() : ''} title={t('Zoom')} onChange={withBlur((e) => setZoom(parseInt(e.target.value, 10)))}>
<option key="" value="" disabled>{t('Zoom')}</option> <option key="" value="" disabled>{t('Zoom')}</option>
{zoomOptions.map(val => ( {zoomOptions.map((val) => (
<option key={val} value={String(val)}>{t('Zoom')} {val}x</option> <option key={val} value={String(val)}>{t('Zoom')} {val}x</option>
))} ))}
</Select> </Select>

View File

@ -94,7 +94,7 @@ async function startPlayback({ path, video, videoStreamIndex, audioStreamIndex,
return undefined; return undefined;
} }
if (sourceBuffer.buffered.length < 1) { if (sourceBuffer.buffered.length === 0) {
return undefined; return undefined;
} }
@ -239,7 +239,9 @@ function drawJpegFrame(canvas: HTMLCanvasElement, jpegImage: Buffer) {
console.error('Canvas context is null'); console.error('Canvas context is null');
return; return;
} }
// eslint-disable-next-line unicorn/prefer-add-event-listener
img.onload = () => ctx.drawImage(img, 0, 0, canvas.width, canvas.height); img.onload = () => ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// eslint-disable-next-line unicorn/prefer-add-event-listener
img.onerror = (error) => console.error('Canvas JPEG image error', error); img.onerror = (error) => console.error('Canvas JPEG image error', error);
img.src = `data:image/jpeg;base64,${jpegImage.toString('base64')}`; img.src = `data:image/jpeg;base64,${jpegImage.toString('base64')}`;
} }

View File

@ -1,4 +1,4 @@
import React, { memo, useMemo, useRef, useCallback, useState } from 'react'; import { memo, useMemo, useRef, useCallback, useState } from 'react';
import { FaYinYang, FaSave, FaPlus, FaMinus, FaTag, FaSortNumericDown, FaAngleRight, FaRegCheckCircle, FaRegCircle } from 'react-icons/fa'; import { FaYinYang, FaSave, FaPlus, FaMinus, FaTag, FaSortNumericDown, FaAngleRight, FaRegCheckCircle, FaRegCircle } from 'react-icons/fa';
import { AiOutlineSplitCells } from 'react-icons/ai'; import { AiOutlineSplitCells } from 'react-icons/ai';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
@ -186,13 +186,9 @@ const SegmentList = memo(({
let header = t('Segments to export:'); let header = t('Segments to export:');
if (segments.length === 0) { if (segments.length === 0) {
if (invertCutSegments) { header = invertCutSegments ? (
header = (
<Trans>You have enabled the &quot;invert segments&quot; mode <FaYinYang style={{ verticalAlign: 'middle' }} /> which will cut away selected segments instead of keeping them. But there is no space between any segments, or at least two segments are overlapping. This would not produce any output. Either make room between segments or click the Yinyang <FaYinYang style={{ verticalAlign: 'middle', color: primaryTextColor }} /> symbol below to disable this mode. Alternatively you may combine overlapping segments from the menu.</Trans> <Trans>You have enabled the &quot;invert segments&quot; mode <FaYinYang style={{ verticalAlign: 'middle' }} /> which will cut away selected segments instead of keeping them. But there is no space between any segments, or at least two segments are overlapping. This would not produce any output. Either make room between segments or click the Yinyang <FaYinYang style={{ verticalAlign: 'middle', color: primaryTextColor }} /> symbol below to disable this mode. Alternatively you may combine overlapping segments from the menu.</Trans>
); ) : t('No segments to export.');
} else {
header = t('No segments to export.');
}
} }
const onReorderSegs = useCallback(async (index) => { const onReorderSegs = useCallback(async (index) => {
@ -285,6 +281,7 @@ const SegmentList = memo(({
[tag]: value, [tag]: value,
})), [setEditingSegmentTags]); })), [setEditingSegmentTags]);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const onTagReset = useCallback((tag) => setEditingSegmentTags(({ [tag]: deleted, ...rest }) => rest), [setEditingSegmentTags]); const onTagReset = useCallback((tag) => setEditingSegmentTags(({ [tag]: deleted, ...rest }) => rest), [setEditingSegmentTags]);
const onSegmentTagsCloseComplete = useCallback(() => { const onSegmentTagsCloseComplete = useCallback(() => {

View File

@ -1,4 +1,4 @@
import React, { memo, useState, useMemo, useCallback } from 'react'; import { memo, useState, useMemo, useCallback } from 'react';
import { FaImage, FaCheckCircle, FaPaperclip, FaVideo, FaVideoSlash, FaFileImport, FaVolumeUp, FaVolumeMute, FaBan, FaFileExport } from 'react-icons/fa'; import { FaImage, FaCheckCircle, FaPaperclip, FaVideo, FaVideoSlash, FaFileImport, FaVolumeUp, FaVolumeMute, FaBan, FaFileExport } from 'react-icons/fa';
import { GoFileBinary } from 'react-icons/go'; import { GoFileBinary } from 'react-icons/go';
@ -34,6 +34,7 @@ const EditFileDialog = memo(({ editingFile, allFilesMeta, customTagsByFile, setC
const onTagReset = useCallback((tag) => { const onTagReset = useCallback((tag) => {
setCustomTagsByFile((old) => { setCustomTagsByFile((old) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [tag]: deleted, ...rest } = old[editingFile] || {}; const { [tag]: deleted, ...rest } = old[editingFile] || {};
return { ...old, [editingFile]: rest }; return { ...old, [editingFile]: rest };
}); });
@ -101,7 +102,8 @@ const EditStreamDialog = memo(({ editingStream: { streamId: editingStreamId, pat
const onTagReset = useCallback((tag) => { const onTagReset = useCallback((tag) => {
updateStreamParams(editingFile, editingStreamId, (params) => { updateStreamParams(editingFile, editingStreamId, (params) => {
if (!params.has('customTags')) return; if (!params.has('customTags')) return;
// eslint-disable-next-line no-param-reassign // todo
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-dynamic-delete
delete params.get('customTags')[tag]; delete params.get('customTags')[tag];
}); });
}, [editingFile, editingStreamId, updateStreamParams]); }, [editingFile, editingStreamId, updateStreamParams]);
@ -140,6 +142,7 @@ const Stream = memo(({ filePath, stream, onToggle, batchSetCopyStreamIds, copySt
let Icon; let Icon;
let codecTypeHuman; let codecTypeHuman;
// eslint-disable-next-line unicorn/prefer-switch
if (stream.codec_type === 'audio') { if (stream.codec_type === 'audio') {
Icon = copyStream ? FaVolumeUp : FaVolumeMute; Icon = copyStream ? FaVolumeUp : FaVolumeMute;
codecTypeHuman = t('audio'); codecTypeHuman = t('audio');
@ -310,6 +313,7 @@ const StreamsSelector = memo(({
async function removeFile(path) { async function removeFile(path) {
setCopyStreamIdsForPath(path, () => ({})); setCopyStreamIdsForPath(path, () => ({}));
setExternalFilesMeta((old) => { setExternalFilesMeta((old) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [path]: val, ...rest } = old; const { [path]: val, ...rest } = old;
return rest; return rest;
}); });
@ -318,6 +322,7 @@ const StreamsSelector = memo(({
async function batchSetCopyStreamIdsForPath(path, streams, filter, enabled) { async function batchSetCopyStreamIdsForPath(path, streams, filter, enabled) {
setCopyStreamIdsForPath(path, (old) => { setCopyStreamIdsForPath(path, (old) => {
const ret = { ...old }; const ret = { ...old };
// eslint-disable-next-line unicorn/no-array-callback-reference
streams.filter(filter).forEach(({ index }) => { streams.filter(filter).forEach(({ index }) => {
ret[index] = enabled; ret[index] = enabled;
}); });

View File

@ -245,6 +245,7 @@ const Timeline = memo(({
style={{ position: 'relative', borderTop: '1px solid var(--gray7)', borderBottom: '1px solid var(--gray7)' }} style={{ position: 'relative', borderTop: '1px solid var(--gray7)', borderBottom: '1px solid var(--gray7)' }}
onMouseDown={onMouseDown} onMouseDown={onMouseDown}
onMouseMove={onMouseMove} onMouseMove={onMouseMove}
// eslint-disable-next-line jsx-a11y/mouse-events-have-key-events
onMouseOut={onMouseOut} onMouseOut={onMouseOut}
> >
{(waveformEnabled && !shouldShowWaveform) && ( {(waveformEnabled && !shouldShowWaveform) && (

View File

@ -1,7 +1,7 @@
import React, { memo, useCallback } from 'react'; import React, { memo, useCallback } from 'react';
import { IoIosSettings } from 'react-icons/io'; import { IoIosSettings } from 'react-icons/io';
import { FaLock, FaUnlock } from 'react-icons/fa'; import { FaLock, FaUnlock } from 'react-icons/fa';
import { IconButton, CrossIcon, ListIcon, VolumeUpIcon, VolumeOffIcon } from 'evergreen-ui'; import { CrossIcon, ListIcon, VolumeUpIcon, VolumeOffIcon } from 'evergreen-ui';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Button from './components/Button'; import Button from './components/Button';

View File

@ -52,14 +52,14 @@ const BigWaveform = memo(({ waveforms, relevantTime, playing, durationSafe, zoom
useEffect(() => { useEffect(() => {
const startTime = new Date().getTime(); const startTime = Date.now();
if (playing) { if (playing) {
let raf; let raf;
// eslint-disable-next-line no-inner-declarations // eslint-disable-next-line no-inner-declarations
function render() { function render() {
raf = window.requestAnimationFrame(() => { raf = window.requestAnimationFrame(() => {
setSmoothTime(relevantTime + (new Date().getTime() - startTime) / 1000); setSmoothTime(relevantTime + (Date.now() - startTime) / 1000);
render(); render();
}); });
} }

View File

@ -25,12 +25,16 @@ const rowStyle: CSSProperties = {
}; };
const ConcatDialog = memo(({ isShown, onHide, paths, onConcat, alwaysConcatMultipleFiles, setAlwaysConcatMultipleFiles }: { const ConcatDialog = memo(({ isShown, onHide, paths, onConcat, alwaysConcatMultipleFiles, setAlwaysConcatMultipleFiles }: {
// todo
// eslint-disable-next-line @typescript-eslint/no-explicit-any
isShown: boolean, onHide: () => void, paths: string[], onConcat: (a: { paths: string[], includeAllStreams: boolean, streams: any, outFileName: string, fileFormat: string, clearBatchFilesAfterConcat: boolean }) => Promise<void>, alwaysConcatMultipleFiles: boolean, setAlwaysConcatMultipleFiles: (a: boolean) => void, isShown: boolean, onHide: () => void, paths: string[], onConcat: (a: { paths: string[], includeAllStreams: boolean, streams: any, outFileName: string, fileFormat: string, clearBatchFilesAfterConcat: boolean }) => Promise<void>, alwaysConcatMultipleFiles: boolean, setAlwaysConcatMultipleFiles: (a: boolean) => void,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { preserveMovData, setPreserveMovData, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge } = useUserSettings(); const { preserveMovData, setPreserveMovData, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge } = useUserSettings();
const [includeAllStreams, setIncludeAllStreams] = useState(false); const [includeAllStreams, setIncludeAllStreams] = useState(false);
// todo
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [fileMeta, setFileMeta] = useState<{ format: any, streams: any, chapters: any }>(); const [fileMeta, setFileMeta] = useState<{ format: any, streams: any, chapters: any }>();
const [allFilesMetaCache, setAllFilesMetaCache] = useState({}); const [allFilesMetaCache, setAllFilesMetaCache] = useState({});
const [clearBatchFilesAfterConcat, setClearBatchFilesAfterConcat] = useState(false); const [clearBatchFilesAfterConcat, setClearBatchFilesAfterConcat] = useState(false);
@ -62,7 +66,7 @@ const ConcatDialog = memo(({ isShown, onHide, paths, onConcat, alwaysConcatMulti
setFileMeta(fileMetaNew); setFileMeta(fileMetaNew);
setFileFormat(fileFormatNew); setFileFormat(fileFormatNew);
setDetectedFileFormat(fileFormatNew); setDetectedFileFormat(fileFormatNew);
setUniqueSuffix(new Date().getTime()); setUniqueSuffix(Date.now());
})().catch(console.error); })().catch(console.error);
return () => { return () => {

View File

@ -24,7 +24,7 @@ const renderKeys = (keys) => keys.map((key, i) => (
// From https://craig.is/killing/mice // From https://craig.is/killing/mice
// For modifier keys you can use shift, ctrl, alt, or meta. // For modifier keys you can use shift, ctrl, alt, or meta.
// You can substitute option for alt and command for meta. // You can substitute option for alt and command for meta.
const allModifiers = ['shift', 'ctrl', 'alt', 'meta']; const allModifiers = new Set(['shift', 'ctrl', 'alt', 'meta']);
function fixKeys(keys) { function fixKeys(keys) {
const replaced = keys.map((key) => { const replaced = keys.map((key) => {
if (key === 'option') return 'alt'; if (key === 'option') return 'alt';
@ -32,10 +32,10 @@ function fixKeys(keys) {
return key; return key;
}); });
const uniqed = uniq(replaced); const uniqed = uniq(replaced);
const nonModifierKeys = keys.filter((key) => !allModifiers.includes(key)); const nonModifierKeys = keys.filter((key) => !allModifiers.has(key));
if (nonModifierKeys.length === 0) return []; // only modifiers is invalid if (nonModifierKeys.length === 0) return []; // only modifiers is invalid
if (nonModifierKeys.length > 1) return []; // can only have one non-modifier if (nonModifierKeys.length > 1) return []; // can only have one non-modifier
return orderBy(uniqed, [key => key !== 'shift', key => key !== 'ctrl', key => key !== 'alt', key => key !== 'meta', key => key]); return orderBy(uniqed, [(key) => key !== 'shift', (key) => key !== 'ctrl', (key) => key !== 'alt', (key) => key !== 'meta', (key) => key]);
} }
const CreateBinding = memo(({ const CreateBinding = memo(({

View File

@ -27,7 +27,7 @@ const OutputFormatSelect = memo(({ style, detectedFileFormat, fileFormat, onOutp
return ( return (
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
<Select style={style} value={fileFormat || ''} title={i18n.t('Output container format:')} onChange={withBlur(e => onOutputFormatUserChange(e.target.value))}> <Select style={style} value={fileFormat || ''} title={i18n.t('Output container format:')} onChange={withBlur((e) => onOutputFormatUserChange(e.target.value))}>
<option key="disabled1" value="" disabled>{i18n.t('Output container format:')}</option> <option key="disabled1" value="" disabled>{i18n.t('Output container format:')}</option>
{detectedFileFormat && ( {detectedFileFormat && (

View File

@ -222,7 +222,8 @@ async function askForSegmentDuration(fileDuration) {
// https://github.com/mifi/lossless-cut/issues/1153 // https://github.com/mifi/lossless-cut/issues/1153
async function askForSegmentsRandomDurationRange() { async function askForSegmentsRandomDurationRange() {
function parse(str) { function parse(str) {
const match = str.replace(/\s/g, '').match(/^duration([\d.]+)to([\d.]+),gap([-\d.]+)to([-\d.]+)$/i); // eslint-disable-next-line unicorn/better-regex
const match = str.replaceAll(/\s/g, '').match(/^duration([\d.]+)to([\d.]+),gap([-\d.]+)to([-\d.]+)$/i);
if (!match) return undefined; if (!match) return undefined;
const values = match.slice(1); const values = match.slice(1);
const parsed = values.map((val) => parseFloat(val)); const parsed = values.map((val) => parseFloat(val));
@ -275,7 +276,7 @@ export async function askForShiftSegments() {
let parseableValue = value; let parseableValue = value;
let sign = 1; let sign = 1;
if (parseableValue[0] === '-') { if (parseableValue[0] === '-') {
parseableValue = parseableValue.substring(1); parseableValue = parseableValue.slice(1);
sign = -1; sign = -1;
} }
const duration = parseDuration(parseableValue); const duration = parseDuration(parseableValue);

View File

@ -9,7 +9,7 @@ import { parseSrt, formatSrt, parseYouTube, formatYouTube, parseMplayerEdl, pars
// eslint-disable-next-line no-underscore-dangle // eslint-disable-next-line no-underscore-dangle
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
const readFixture = async (name: string, encoding: BufferEncoding = 'utf-8') => fs.readFile(join(__dirname, 'fixtures', name), encoding); const readFixture = async (name: string, encoding: BufferEncoding = 'utf8') => fs.readFile(join(__dirname, 'fixtures', name), encoding);
const readFixtureBinary = async (name: string) => fs.readFile(join(__dirname, 'fixtures', name), null); const readFixtureBinary = async (name: string) => fs.readFile(join(__dirname, 'fixtures', name), null);
const expectYouTube1 = [ const expectYouTube1 = [
@ -259,5 +259,5 @@ it('format srt', async () => {
// https://github.com/mifi/lossless-cut/issues/1664 // https://github.com/mifi/lossless-cut/issues/1664
it('parses DV Analyzer Summary.txt', async () => { it('parses DV Analyzer Summary.txt', async () => {
expect(parseDvAnalyzerSummaryTxt(await readFixture('DV Analyzer Summary.txt', 'utf-8'))).toMatchSnapshot(); expect(parseDvAnalyzerSummaryTxt(await readFixture('DV Analyzer Summary.txt', 'utf8'))).toMatchSnapshot();
}); });

View File

@ -21,10 +21,10 @@ export function getFrameCountRaw(detectedFps: number | undefined, sec: number) {
} }
function parseTime(str: string) { function parseTime(str: string) {
const timeMatch = str.match(/^[^0-9]*(?:(?:([0-9]{1,}):)?([0-9]{1,2}):)?([0-9]{1,})(?:\.([0-9]{1,3}))?:?/); const timeMatch = str.match(/^\D*(?:(?:(\d+):)?(\d{1,2}):)?(\d+)(?:\.(\d{1,3}))?:?/);
if (!timeMatch) return undefined; if (!timeMatch) return undefined;
const rest = str.substring(timeMatch[0].length); const rest = str.slice(timeMatch[0].length);
const [, hourStr, minStr, secStr, msStr] = timeMatch; const [, hourStr, minStr, secStr, msStr] = timeMatch;
const hour = hourStr != null ? parseInt(hourStr, 10) : 0; const hour = hourStr != null ? parseInt(hourStr, 10) : 0;
@ -49,7 +49,7 @@ export const getFrameValParser = (fps) => (str) => {
export async function parseCsv(csvStr, parseTimeFn) { export async function parseCsv(csvStr, parseTimeFn) {
const rows = await csvParseAsync(csvStr, {}); const rows = await csvParseAsync(csvStr, {});
if (rows.length === 0) throw new Error(i18n.t('No rows found')); if (rows.length === 0) throw new Error(i18n.t('No rows found'));
if (!rows.every(row => row.length === 3)) throw new Error(i18n.t('One or more rows does not have 3 columns')); if (!rows.every((row) => row.length === 3)) throw new Error(i18n.t('One or more rows does not have 3 columns'));
const mapped = rows const mapped = rows
.map(([start, end, name]) => ({ .map(([start, end, name]) => ({
@ -71,7 +71,7 @@ export async function parseCsv(csvStr, parseTimeFn) {
export async function parseMplayerEdl(text) { export async function parseMplayerEdl(text) {
const allRows = text.split('\n').map((line) => { const allRows = text.split('\n').map((line) => {
const match = line.match(/^\s*([^\s]+)\s+([^\s]+)\s+([0123])\s*$/); const match = line.match(/^\s*(\S+)\s+(\S+)\s+([0-3])\s*$/);
if (!match) return undefined; if (!match) return undefined;
const start = parseFloat(match[1]); const start = parseFloat(match[1]);
const end = parseFloat(match[2]); const end = parseFloat(match[2]);
@ -79,7 +79,7 @@ export async function parseMplayerEdl(text) {
if (Number.isNaN(start) || Number.isNaN(end)) return undefined; if (Number.isNaN(start) || Number.isNaN(end)) return undefined;
if (start < 0 || end < 0 || start >= end) return undefined; if (start < 0 || end < 0 || start >= end) return undefined;
return { start, end, type }; return { start, end, type };
}).filter((it) => it); }).filter(Boolean);
const cutAwaySegments = allRows.filter((row) => row.type === 0); const cutAwaySegments = allRows.filter((row) => row.type === 0);
const muteSegments = allRows.filter((row) => row.type === 1); const muteSegments = allRows.filter((row) => row.type === 1);
@ -128,10 +128,10 @@ export function parseCuesheet(cuesheet) {
export function parsePbf(buf: Buffer) { export function parsePbf(buf: Buffer) {
const text = buf.toString('utf16le'); const text = buf.toString('utf16le');
const bookmarks = text.split('\n').map((line) => { const bookmarks = text.split('\n').map((line) => {
const match = line.match(/^[0-9]+=([0-9]+)\*([^*]+)*([^*]+)?/); const match = line.match(/^\d+=(\d+)\*([^*]+)*([^*]+)?/);
if (match) return { time: parseInt(match[1]!, 10) / 1000, name: match[2] }; if (match) return { time: parseInt(match[1]!, 10) / 1000, name: match[2] };
return undefined; return undefined;
}).filter((it) => it); }).filter(Boolean);
const out: Segment[] = []; const out: Segment[] = [];
@ -156,7 +156,7 @@ export function parseXmeml(xmlStr) {
// TODO maybe support media.audio also? // TODO maybe support media.audio also?
const { xmeml } = xml; const { xmeml } = xml;
if (!xmeml) throw Error('Root element <xmeml> not found in file'); if (!xmeml) throw new Error('Root element <xmeml> not found in file');
let sequence; let sequence;
@ -181,10 +181,10 @@ export function parseFcpXml(xmlStr) {
const xml = new XMLParser({ ignoreAttributes: false }).parse(xmlStr); const xml = new XMLParser({ ignoreAttributes: false }).parse(xmlStr);
const { fcpxml } = xml; const { fcpxml } = xml;
if (!fcpxml) throw Error('Root element <fcpxml> not found in file'); if (!fcpxml) throw new Error('Root element <fcpxml> not found in file');
function getTime(str) { function getTime(str) {
const match = str.match(/([0-9]+)\/([0-9]+)s/); const match = str.match(/(\d+)\/(\d+)s/);
if (!match) throw new Error('Invalid attribute'); if (!match) throw new Error('Invalid attribute');
return parseInt(match[1], 10) / parseInt(match[2], 10); return parseInt(match[1], 10) / parseInt(match[2], 10);
} }
@ -212,7 +212,7 @@ export function parseYouTube(str) {
return { time, name }; return { time, name };
} }
const lines = str.split('\n').map(parseLine).filter((line) => line); const lines = str.split('\n').map((line) => parseLine(line)).filter(Boolean);
const linesSorted = sortBy(lines, (l) => l.time); const linesSorted = sortBy(lines, (l) => l.time);
@ -275,7 +275,7 @@ export function parseDvAnalyzerSummaryTxt(txt: string) {
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
for (const line of lines) { for (const line of lines) {
if (headerFound) { if (headerFound) {
const match = line.match(/^(\d{2}):(\d{2}):(\d{2}).(\d{3})\s+([^\s]+)\s+-\s+([^\s]+)\s+([^\s]+\s+[^\s]+)\s+-\s+([^\s]+\s+[^\s]+)/); const match = line.match(/^(\d{2}):(\d{2}):(\d{2}).(\d{3})\s+(\S+)\s+-\s+(\S+)\s+(\S+\s+\S+)\s+-\s+(\S+\s+\S+)/);
if (!match) break; if (!match) break;
const h = parseInt(match[1]!, 10); const h = parseInt(match[1]!, 10);
const m = parseInt(match[2]!, 10); const m = parseInt(match[2]!, 10);
@ -325,7 +325,7 @@ export function parseSrt(text: string) {
} else if (subtitleIndexAt != null && subtitleIndexAt > 0) { } else if (subtitleIndexAt != null && subtitleIndexAt > 0) {
const match = line.match(/^(\d+:\d+:\d+[,.]\d+\s+)-->(\s+\d+:\d+:\d+[,.]\d+)$/); const match = line.match(/^(\d+:\d+:\d+[,.]\d+\s+)-->(\s+\d+:\d+:\d+[,.]\d+)$/);
if (match) { if (match) {
const fixComma = (v) => v.replace(/,/g, '.'); const fixComma = (v) => v.replaceAll(',', '.');
start = parseTime(fixComma(match[1]))?.time; start = parseTime(fixComma(match[1]))?.time;
end = parseTime(fixComma(match[2]))?.time; end = parseTime(fixComma(match[2]))?.time;
} else if (start != null && end != null) { } else if (start != null && end != null) {
@ -345,5 +345,5 @@ export function parseSrt(text: string) {
} }
export function formatSrt(segments) { export function formatSrt(segments) {
return segments.reduce((acc, segment, index) => `${acc}${index > 0 ? '\r\n' : ''}${index + 1}\r\n${formatDuration({ seconds: segment.start }).replace(/\./g, ',')} --> ${formatDuration({ seconds: segment.end }).replace(/\./g, ',')}\r\n${segment.name || '-'}\r\n`, ''); return segments.reduce((acc, segment, index) => `${acc}${index > 0 ? '\r\n' : ''}${index + 1}\r\n${formatDuration({ seconds: segment.start }).replaceAll('.', ',')} --> ${formatDuration({ seconds: segment.end }).replaceAll('.', ',')}\r\n${segment.name || '-'}\r\n`, '');
} }

View File

@ -13,24 +13,24 @@ const { basename } = window.require('path');
const { dialog } = window.require('@electron/remote'); const { dialog } = window.require('@electron/remote');
export async function loadCsvSeconds(path) { export async function loadCsvSeconds(path) {
return parseCsv(await readFile(path, 'utf-8'), parseCsvTime); return parseCsv(await readFile(path, 'utf8'), parseCsvTime);
} }
export async function loadCsvFrames(path, fps) { export async function loadCsvFrames(path, fps) {
if (!fps) throw new Error('The loaded file has an unknown framerate'); if (!fps) throw new Error('The loaded file has an unknown framerate');
return parseCsv(await readFile(path, 'utf-8'), getFrameValParser(fps)); return parseCsv(await readFile(path, 'utf8'), getFrameValParser(fps));
} }
export async function loadXmeml(path) { export async function loadXmeml(path) {
return parseXmeml(await readFile(path, 'utf-8')); return parseXmeml(await readFile(path, 'utf8'));
} }
export async function loadFcpXml(path) { export async function loadFcpXml(path) {
return parseFcpXml(await readFile(path, 'utf-8')); return parseFcpXml(await readFile(path, 'utf8'));
} }
export async function loadDvAnalyzerSummaryTxt(path) { export async function loadDvAnalyzerSummaryTxt(path) {
return parseDvAnalyzerSummaryTxt(await readFile(path, 'utf-8')); return parseDvAnalyzerSummaryTxt(await readFile(path, 'utf8'));
} }
export async function loadPbf(path) { export async function loadPbf(path) {
@ -38,7 +38,7 @@ export async function loadPbf(path) {
} }
export async function loadMplayerEdl(path) { export async function loadMplayerEdl(path) {
return parseMplayerEdl(await readFile(path, 'utf-8')); return parseMplayerEdl(await readFile(path, 'utf8'));
} }
export async function loadCue(path) { export async function loadCue(path) {
@ -46,7 +46,7 @@ export async function loadCue(path) {
} }
export async function loadSrt(path) { export async function loadSrt(path) {
return parseSrt(await readFile(path, 'utf-8')); return parseSrt(await readFile(path, 'utf8'));
} }
export async function saveCsv(path, cutSegments) { export async function saveCsv(path, cutSegments) {
@ -103,6 +103,7 @@ export async function askForEdlImport({ type, fps }: { type: EdlImportType, fps?
if (type === 'youtube') return askForYouTubeInput(); if (type === 'youtube') return askForYouTubeInput();
let filters; let filters;
// eslint-disable-next-line unicorn/prefer-switch
if (type === 'csv' || type === 'csv-frames') filters = [{ name: i18n.t('CSV files'), extensions: ['csv'] }]; if (type === 'csv' || type === 'csv-frames') filters = [{ name: i18n.t('CSV files'), extensions: ['csv'] }];
else if (type === 'xmeml') filters = [{ name: i18n.t('XML files'), extensions: ['xml'] }]; else if (type === 'xmeml') filters = [{ name: i18n.t('XML files'), extensions: ['xml'] }];
else if (type === 'fcpxml') filters = [{ name: i18n.t('FCPXML files'), extensions: ['fcpxml'] }]; else if (type === 'fcpxml') filters = [{ name: i18n.t('FCPXML files'), extensions: ['fcpxml'] }];
@ -114,7 +115,7 @@ export async function askForEdlImport({ type, fps }: { type: EdlImportType, fps?
else if (type === 'llc') filters = [{ name: i18n.t('LosslessCut project'), extensions: ['llc'] }]; else if (type === 'llc') filters = [{ name: i18n.t('LosslessCut project'), extensions: ['llc'] }];
const { canceled, filePaths } = await showOpenDialog({ properties: ['openFile'], filters }); const { canceled, filePaths } = await showOpenDialog({ properties: ['openFile'], filters });
if (canceled || filePaths.length < 1) return []; if (canceled || filePaths.length === 0) return [];
return readEdlFile({ type, path: filePaths[0], fps }); return readEdlFile({ type, path: filePaths[0], fps });
} }
@ -123,6 +124,7 @@ export async function exportEdlFile({ type, cutSegments, customOutDir, filePath,
}) { }) {
let filters; let filters;
let ext; let ext;
// eslint-disable-next-line unicorn/prefer-switch
if (type === 'csv') { if (type === 'csv') {
ext = 'csv'; ext = 'csv';
filters = [{ name: i18n.t('CSV files'), extensions: [ext, 'txt'] }]; filters = [{ name: i18n.t('CSV files'), extensions: [ext, 'txt'] }];
@ -148,6 +150,7 @@ export async function exportEdlFile({ type, cutSegments, customOutDir, filePath,
const { canceled, filePath: savePath } = await dialog.showSaveDialog({ defaultPath, filters }); const { canceled, filePath: savePath } = await dialog.showSaveDialog({ defaultPath, filters });
if (canceled || !savePath) return; if (canceled || !savePath) return;
console.log('Saving', type, savePath); console.log('Saving', type, savePath);
// eslint-disable-next-line unicorn/prefer-switch
if (type === 'csv') await saveCsv(savePath, cutSegments); if (type === 'csv') await saveCsv(savePath, cutSegments);
else if (type === 'tsv-human') await saveTsv(savePath, cutSegments); else if (type === 'tsv-human') await saveTsv(savePath, cutSegments);
else if (type === 'csv-human') await saveCsvHuman(savePath, cutSegments); else if (type === 'csv-human') await saveCsvHuman(savePath, cutSegments);

View File

@ -1,3 +1,4 @@
// eslint-disable-line unicorn/filename-case
import i18n from 'i18next'; import i18n from 'i18next';
export const blackdetect = () => ({ export const blackdetect = () => ({

View File

@ -19,7 +19,12 @@ const { renderWaveformPng, mapTimesToSegments, detectSceneChanges, captureFrames
export { renderWaveformPng, mapTimesToSegments, detectSceneChanges, captureFrames, captureFrame, getFfCommandLine, runFfmpegConcat, runFfmpegWithProgress, html5ify, getDuration, abortFfmpegs, runFfprobe, getFfmpegPath, setCustomFfPath }; export { renderWaveformPng, mapTimesToSegments, detectSceneChanges, captureFrames, captureFrame, getFfCommandLine, runFfmpegConcat, runFfmpegWithProgress, html5ify, getDuration, abortFfmpegs, runFfprobe, getFfmpegPath, setCustomFfPath };
export class RefuseOverwriteError extends Error {} export class RefuseOverwriteError extends Error {
constructor() {
super();
this.name = 'RefuseOverwriteError';
}
}
export function logStdoutStderr({ stdout, stderr }) { export function logStdoutStderr({ stdout, stderr }) {
if (stdout.length > 0) { if (stdout.length > 0) {
@ -63,12 +68,12 @@ export async function readFrames({ filePath, from, to, streamIndex }) {
// todo types // todo types
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const packetsFiltered: Frame[] = (JSON.parse(stdout).packets as any[]) const packetsFiltered: Frame[] = (JSON.parse(stdout).packets as any[])
.map(p => ({ .map((p) => ({
keyframe: p.flags[0] === 'K', keyframe: p.flags[0] === 'K',
time: parseFloat(p.pts_time), time: parseFloat(p.pts_time),
createdAt: new Date(), createdAt: new Date(),
})) }))
.filter(p => !Number.isNaN(p.time)); .filter((p) => !Number.isNaN(p.time));
return sortBy(packetsFiltered, 'time'); return sortBy(packetsFiltered, 'time');
} }
@ -93,10 +98,18 @@ export type FindKeyframeMode = 'nearest' | 'before' | 'after';
function findKeyframe(keyframes: Keyframe[], time: number, mode: FindKeyframeMode) { function findKeyframe(keyframes: Keyframe[], time: number, mode: FindKeyframeMode) {
switch (mode) { switch (mode) {
case 'nearest': return findNearestKeyframe(keyframes, time); case 'nearest': {
case 'before': return findPreviousKeyframe(keyframes, time); return findNearestKeyframe(keyframes, time);
case 'after': return findNextKeyframe(keyframes, time); }
default: return undefined; case 'before': {
return findPreviousKeyframe(keyframes, time);
}
case 'after': {
return findNextKeyframe(keyframes, time);
}
default: {
return undefined;
}
} }
} }
@ -125,7 +138,7 @@ export function getSafeCutTime(frames, cutTime, nextMode) {
if (frames.length < 2) throw new Error(i18n.t('Less than 2 frames found')); if (frames.length < 2) throw new Error(i18n.t('Less than 2 frames found'));
if (nextMode) { if (nextMode) {
index = frames.findIndex(f => f.keyframe && f.time >= cutTime - sigma); index = frames.findIndex((f) => f.keyframe && f.time >= cutTime - sigma);
if (index === -1) throw new Error(i18n.t('Failed to find next keyframe')); if (index === -1) throw new Error(i18n.t('Failed to find next keyframe'));
if (index >= frames.length - 1) throw new Error(i18n.t('We are on the last frame')); if (index >= frames.length - 1) throw new Error(i18n.t('We are on the last frame'));
const { time } = frames[index]; const { time } = frames[index];
@ -136,12 +149,13 @@ export function getSafeCutTime(frames, cutTime, nextMode) {
} }
const findReverseIndex = (arr, cb) => { const findReverseIndex = (arr, cb) => {
// eslint-disable-next-line unicorn/no-array-callback-reference
const ret = [...arr].reverse().findIndex(cb); const ret = [...arr].reverse().findIndex(cb);
if (ret === -1) return -1; if (ret === -1) return -1;
return arr.length - 1 - ret; return arr.length - 1 - ret;
}; };
index = findReverseIndex(frames, f => f.time <= cutTime + sigma); index = findReverseIndex(frames, (f) => f.time <= cutTime + sigma);
if (index === -1) throw new Error(i18n.t('Failed to find any prev frame')); if (index === -1) throw new Error(i18n.t('Failed to find any prev frame'));
if (index === 0) throw new Error(i18n.t('We are on the first frame')); if (index === 0) throw new Error(i18n.t('We are on the first frame'));
@ -155,7 +169,7 @@ export function getSafeCutTime(frames, cutTime, nextMode) {
} }
// We are not on a frame before keyframe, look for preceding keyframe instead // We are not on a frame before keyframe, look for preceding keyframe instead
index = findReverseIndex(frames, f => f.keyframe && f.time <= cutTime + sigma); index = findReverseIndex(frames, (f) => f.keyframe && f.time <= cutTime + sigma);
if (index === -1) throw new Error(i18n.t('Failed to find any prev keyframe')); if (index === -1) throw new Error(i18n.t('Failed to find any prev keyframe'));
if (index === 0) throw new Error(i18n.t('We are on the first keyframe')); if (index === 0) throw new Error(i18n.t('We are on the first keyframe'));
@ -165,9 +179,9 @@ export function getSafeCutTime(frames, cutTime, nextMode) {
export function findNearestKeyFrameTime({ frames, time, direction, fps }) { export function findNearestKeyFrameTime({ frames, time, direction, fps }) {
const sigma = fps ? (1 / fps) : 0.1; const sigma = fps ? (1 / fps) : 0.1;
const keyframes = frames.filter(f => f.keyframe && (direction > 0 ? f.time > time + sigma : f.time < time - sigma)); const keyframes = frames.filter((f) => f.keyframe && (direction > 0 ? f.time > time + sigma : f.time < time - sigma));
if (keyframes.length === 0) return undefined; if (keyframes.length === 0) return undefined;
const nearestKeyFrame = sortBy(keyframes, keyframe => (direction > 0 ? keyframe.time - time : time - keyframe.time))[0]; const nearestKeyFrame = sortBy(keyframes, (keyframe) => (direction > 0 ? keyframe.time - time : time - keyframe.time))[0];
if (!nearestKeyFrame) return undefined; if (!nearestKeyFrame) return undefined;
return nearestKeyFrame.time; return nearestKeyFrame.time;
} }
@ -186,7 +200,7 @@ export async function tryMapChaptersToEdl(chapters) {
end, end,
name, name,
}; };
}).filter((it) => it); }).filter(Boolean);
} catch (err) { } catch (err) {
console.error('Failed to read chapters from file', err); console.error('Failed to read chapters from file', err);
return []; return [];
@ -218,6 +232,7 @@ export async function createChaptersFromSegments({ segmentPaths, chapterNames })
function mapDefaultFormat({ streams, requestedFormat }) { function mapDefaultFormat({ streams, requestedFormat }) {
if (requestedFormat === 'mp4') { if (requestedFormat === 'mp4') {
// Only MOV supports these codecs, so default to MOV instead https://github.com/mifi/lossless-cut/issues/948 // Only MOV supports these codecs, so default to MOV instead https://github.com/mifi/lossless-cut/issues/948
// eslint-disable-next-line unicorn/no-lonely-if
if (streams.some((stream) => pcmAudioCodecs.includes(stream.codec_name))) { if (streams.some((stream) => pcmAudioCodecs.includes(stream.codec_name))) {
return 'mov'; return 'mov';
} }
@ -230,7 +245,7 @@ function mapDefaultFormat({ streams, requestedFormat }) {
} }
async function determineOutputFormat(ffprobeFormatsStr, filePath) { async function determineOutputFormat(ffprobeFormatsStr, filePath) {
const ffprobeFormats = (ffprobeFormatsStr || '').split(',').map((str) => str.trim()).filter((str) => str); const ffprobeFormats = (ffprobeFormatsStr || '').split(',').map((str) => str.trim()).filter(Boolean);
if (ffprobeFormats.length === 0) { if (ffprobeFormats.length === 0) {
console.warn('ffprobe returned unknown formats', ffprobeFormatsStr); console.warn('ffprobe returned unknown formats', ffprobeFormatsStr);
return undefined; return undefined;
@ -256,19 +271,30 @@ async function determineOutputFormat(ffprobeFormatsStr, filePath) {
// https://www.ftyps.com/ // https://www.ftyps.com/
// https://exiftool.org/TagNames/QuickTime.html // https://exiftool.org/TagNames/QuickTime.html
switch (fileTypeResponse.mime) { switch (fileTypeResponse.mime) {
case 'video/x-matroska': return 'matroska'; case 'video/x-matroska': {
case 'video/webm': return 'webm'; return 'matroska';
case 'video/quicktime': return 'mov'; }
case 'video/3gpp2': return '3g2'; case 'video/webm': {
case 'video/3gpp': return '3gp'; return 'webm';
}
case 'video/quicktime': {
return 'mov';
}
case 'video/3gpp2': {
return '3g2';
}
case 'video/3gpp': {
return '3gp';
}
// These two cmds produce identical output, so we assume that encoding "ipod" means encoding m4a // These two cmds produce identical output, so we assume that encoding "ipod" means encoding m4a
// ffmpeg -i example.aac -c copy OutputFile2.m4a // ffmpeg -i example.aac -c copy OutputFile2.m4a
// ffmpeg -i example.aac -c copy -f ipod OutputFile.m4a // ffmpeg -i example.aac -c copy -f ipod OutputFile.m4a
// See also https://github.com/mifi/lossless-cut/issues/28 // See also https://github.com/mifi/lossless-cut/issues/28
case 'audio/x-m4a': case 'audio/x-m4a':
case 'audio/mp4': case 'audio/mp4': {
return 'ipod'; return 'ipod';
}
case 'image/avif': case 'image/avif':
case 'image/heif': case 'image/heif':
case 'image/heif-sequence': case 'image/heif-sequence':
@ -276,8 +302,9 @@ async function determineOutputFormat(ffprobeFormatsStr, filePath) {
case 'image/heic-sequence': case 'image/heic-sequence':
case 'video/x-m4v': case 'video/x-m4v':
case 'video/mp4': case 'video/mp4':
case 'image/x-canon-cr3': case 'image/x-canon-cr3': {
return 'mp4'; return 'mp4';
}
default: { default: {
console.warn('file-type returned unknown format', ffprobeFormats, fileTypeResponse.mime); console.warn('file-type returned unknown format', ffprobeFormats, fileTypeResponse.mime);
@ -303,7 +330,7 @@ export async function readFileMeta(filePath) {
try { try {
// https://github.com/mifi/lossless-cut/issues/1342 // https://github.com/mifi/lossless-cut/issues/1342
parsedJson = JSON.parse(stdout); parsedJson = JSON.parse(stdout);
} catch (err) { } catch {
console.log('ffprobe stdout', stdout); console.log('ffprobe stdout', stdout);
throw new Error('ffprobe returned malformed data'); throw new Error('ffprobe returned malformed data');
} }
@ -482,16 +509,16 @@ export async function extractSubtitleTrack(filePath, streamId) {
export async function renderThumbnails({ filePath, from, duration, onThumbnail }) { export async function renderThumbnails({ filePath, from, duration, onThumbnail }) {
// Time first render to determine how many to render // Time first render to determine how many to render
const startTime = new Date().getTime() / 1000; const startTime = Date.now() / 1000;
let url = await renderThumbnail(filePath, from); let url = await renderThumbnail(filePath, from);
const endTime = new Date().getTime() / 1000; const endTime = Date.now() / 1000;
onThumbnail({ time: from, url }); onThumbnail({ time: from, url });
// Aim for max 3 sec to render all // Aim for max 3 sec to render all
const numThumbs = Math.floor(Math.min(Math.max(3 / (endTime - startTime), 3), 10)); const numThumbs = Math.floor(Math.min(Math.max(3 / (endTime - startTime), 3), 10));
// console.log(numThumbs); // console.log(numThumbs);
const thumbTimes = Array(numThumbs - 1).fill(undefined).map((_unused, i) => (from + ((duration * (i + 1)) / (numThumbs)))); const thumbTimes = Array.from({ length: numThumbs - 1 }).fill(undefined).map((_unused, i) => (from + ((duration * (i + 1)) / (numThumbs))));
// console.log(thumbTimes); // console.log(thumbTimes);
await pMap(thumbTimes, async (time) => { await pMap(thumbTimes, async (time) => {
@ -504,7 +531,7 @@ export async function extractWaveform({ filePath, outPath }) {
const numSegs = 10; const numSegs = 10;
const duration = 60 * 60; const duration = 60 * 60;
const maxLen = 0.1; const maxLen = 0.1;
const segments = Array(numSegs).fill(undefined).map((_unused, i) => [i * (duration / numSegs), Math.min(duration / numSegs, maxLen)] as const); const segments = Array.from({ length: numSegs }).fill(undefined).map((_unused, i) => [i * (duration / numSegs), Math.min(duration / numSegs, maxLen)] as const);
// https://superuser.com/questions/681885/how-can-i-remove-multiple-segments-from-a-video-using-ffmpeg // https://superuser.com/questions/681885/how-can-i-remove-multiple-segments-from-a-video-using-ffmpeg
let filter = segments.map(([from, len], i) => `[0:a]atrim=start=${from}:end=${from + len},asetpts=PTS-STARTPTS[a${i}]`).join(';'); let filter = segments.map(([from, len], i) => `[0:a]atrim=start=${from}:end=${from + len},asetpts=PTS-STARTPTS[a${i}]`).join(';');
@ -541,7 +568,7 @@ export function isProblematicAvc1(outFormat, streams) {
} }
function parseFfprobeFps(stream) { function parseFfprobeFps(stream) {
const match = typeof stream.avg_frame_rate === 'string' && stream.avg_frame_rate.match(/^([0-9]+)\/([0-9]+)$/); const match = typeof stream.avg_frame_rate === 'string' && stream.avg_frame_rate.match(/^(\d+)\/(\d+)$/);
if (!match) return undefined; if (!match) return undefined;
const num = parseInt(match[1], 10); const num = parseInt(match[1], 10);
const den = parseInt(match[2], 10); const den = parseInt(match[2], 10);
@ -555,6 +582,7 @@ export function getStreamFps(stream) {
return fps; return fps;
} }
if (stream.codec_type === 'audio') { if (stream.codec_type === 'audio') {
// eslint-disable-next-line unicorn/no-lonely-if
if (typeof stream.sample_rate === 'string') { if (typeof stream.sample_rate === 'string') {
const sampleRate = parseInt(stream.sample_rate, 10); const sampleRate = parseInt(stream.sample_rate, 10);
if (!Number.isNaN(sampleRate) && sampleRate > 0) { if (!Number.isNaN(sampleRate) && sampleRate > 0) {
@ -595,7 +623,7 @@ export function getTimecodeFromStreams(streams) {
return true; return true;
} }
return undefined; return undefined;
} catch (err) { } catch {
// console.warn('Failed to parse timecode from file streams', err); // console.warn('Failed to parse timecode from file streams', err);
return undefined; return undefined;
} }

View File

@ -1,5 +1,4 @@
// Taken from: https://github.com/facebookarchive/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js // Taken from: https://github.com/facebookarchive/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js
/* eslint-disable */
/** /**
* Copyright (c) 2015, Facebook, Inc. * Copyright (c) 2015, Facebook, Inc.
@ -13,12 +12,10 @@
* @typechecks * @typechecks
*/ */
'use strict';
// Reasonable defaults // Reasonable defaults
var PIXEL_STEP = 10; const PIXEL_STEP = 10;
var LINE_HEIGHT = 40; const LINE_HEIGHT = 40;
var PAGE_HEIGHT = 800; const PAGE_HEIGHT = 800;
/** /**
* Mouse wheel (and 2-finger trackpad) support on the web sucks. It is * Mouse wheel (and 2-finger trackpad) support on the web sucks. It is
@ -120,9 +117,9 @@ var PAGE_HEIGHT = 800;
* Firefox v4/Win7 | undefined | 3 * Firefox v4/Win7 | undefined | 3
* *
*/ */
export default function normalizeWheel(/*object*/ event) /*object*/ { export default function normalizeWheel(/* object */ event) /* object */ {
var sX = 0, sY = 0, // spinX, spinY let sX = 0; let sY = 0; // spinX, spinY
pX = 0, pY = 0; // pixelX, pixelY let pX = 0; let pY = 0; // pixelX, pixelY
// Legacy // Legacy
if ('detail' in event) { sY = event.detail; } if ('detail' in event) { sY = event.detail; }
@ -131,7 +128,7 @@ export default function normalizeWheel(/*object*/ event) /*object*/ {
if ('wheelDeltaX' in event) { sX = -event.wheelDeltaX / 120; } if ('wheelDeltaX' in event) { sX = -event.wheelDeltaX / 120; }
// side scrolling on FF with DOMMouseScroll // side scrolling on FF with DOMMouseScroll
if ( 'axis' in event && event.axis === event.HORIZONTAL_AXIS ) { if ('axis' in event && event.axis === event.HORIZONTAL_AXIS) {
sX = sY; sX = sY;
sY = 0; sY = 0;
} }
@ -143,7 +140,7 @@ export default function normalizeWheel(/*object*/ event) /*object*/ {
if ('deltaX' in event) { pX = event.deltaX; } if ('deltaX' in event) { pX = event.deltaX; }
if ((pX || pY) && event.deltaMode) { if ((pX || pY) && event.deltaMode) {
if (event.deltaMode == 1) { // delta in LINE units if (event.deltaMode === 1) { // delta in LINE units
pX *= LINE_HEIGHT; pX *= LINE_HEIGHT;
pY *= LINE_HEIGHT; pY *= LINE_HEIGHT;
} else { // delta in PAGE units } else { // delta in PAGE units
@ -156,8 +153,10 @@ export default function normalizeWheel(/*object*/ event) /*object*/ {
if (pX && !sX) { sX = (pX < 1) ? -1 : 1; } if (pX && !sX) { sX = (pX < 1) ? -1 : 1; }
if (pY && !sY) { sY = (pY < 1) ? -1 : 1; } if (pY && !sY) { sY = (pY < 1) ? -1 : 1; }
return { spinX : sX, return {
spinY : sY, spinX: sX,
pixelX : pX, spinY: sY,
pixelY : pY }; pixelX: pX,
pixelY: pY,
};
} }

View File

@ -7,7 +7,12 @@ import { errorToast } from '../swal';
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
import isDev from '../isDev'; import isDev from '../isDev';
export class DirectoryAccessDeclinedError extends Error {} export class DirectoryAccessDeclinedError extends Error {
constructor() {
super();
this.name = 'DirectoryAccessDeclinedError';
}
}
// MacOS App Store sandbox doesn't allow reading/writing anywhere, // MacOS App Store sandbox doesn't allow reading/writing anywhere,
// except those exact file paths that have been explicitly drag-dropped into LosslessCut or opened using the opener dialog // except those exact file paths that have been explicitly drag-dropped into LosslessCut or opened using the opener dialog

View File

@ -16,7 +16,7 @@ const { writeFile, mkdir } = window.require('fs/promises');
async function writeChaptersFfmetadata(outDir, chapters) { async function writeChaptersFfmetadata(outDir, chapters) {
if (!chapters || chapters.length === 0) return undefined; if (!chapters || chapters.length === 0) return undefined;
const path = join(outDir, `ffmetadata-${new Date().getTime()}.txt`); const path = join(outDir, `ffmetadata-${Date.now()}.txt`);
const ffmetadata = chapters.map(({ start, end, name }) => ( const ffmetadata = chapters.map(({ start, end, name }) => (
`[CHAPTER]\nTIMEBASE=1/1000\nSTART=${Math.floor(start * 1000)}\nEND=${Math.floor(end * 1000)}\ntitle=${name || ''}` `[CHAPTER]\nTIMEBASE=1/1000\nSTART=${Math.floor(start * 1000)}\nEND=${Math.floor(end * 1000)}\ntitle=${name || ''}`
@ -37,7 +37,7 @@ function getMovFlags({ preserveMovData, movFastStart }) {
if (movFastStart) flags.push('+faststart'); if (movFastStart) flags.push('+faststart');
if (flags.length === 0) return []; if (flags.length === 0) return [];
return flatMap(flags, flag => ['-movflags', flag]); return flatMap(flags, (flag) => ['-movflags', flag]);
} }
function getMatroskaFlags() { function getMatroskaFlags() {
@ -153,7 +153,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
// https://superuser.com/questions/787064/filename-quoting-in-ffmpeg-concat // https://superuser.com/questions/787064/filename-quoting-in-ffmpeg-concat
// Must add "file:" or we get "Impossible to open 'pipe:xyz.mp4'" on newer ffmpeg versions // Must add "file:" or we get "Impossible to open 'pipe:xyz.mp4'" on newer ffmpeg versions
// https://superuser.com/questions/718027/ffmpeg-concat-doesnt-work-with-absolute-path // https://superuser.com/questions/718027/ffmpeg-concat-doesnt-work-with-absolute-path
const concatTxt = paths.map(file => `file 'file:${resolve(file).replace(/'/g, "'\\''")}'`).join('\n'); const concatTxt = paths.map((file) => `file 'file:${resolve(file).replaceAll('\'', "'\\''")}'`).join('\n');
const ffmpegCommandLine = getFfCommandLine('ffmpeg', ffmpegArgs); const ffmpegCommandLine = getFfCommandLine('ffmpeg', ffmpegArgs);

View File

@ -4,7 +4,7 @@ import { useEffect, useRef } from 'react';
// Also document.addEventListener needs custom handling of modifier keys or C will be triggered by CTRL+C, etc // Also document.addEventListener needs custom handling of modifier keys or C will be triggered by CTRL+C, etc
import Mousetrap from 'mousetrap'; import Mousetrap from 'mousetrap';
const keyupActions = ['seekBackwards', 'seekForwards']; const keyupActions = new Set(['seekBackwards', 'seekForwards']);
export default ({ keyBindings, onKeyPress: onKeyPressProp }) => { export default ({ keyBindings, onKeyPress: onKeyPressProp }) => {
const onKeyPressRef = useRef(); const onKeyPressRef = useRef();
@ -25,7 +25,7 @@ export default ({ keyBindings, onKeyPress: onKeyPressProp }) => {
keyBindings.forEach(({ action, keys }) => { keyBindings.forEach(({ action, keys }) => {
mousetrap.bind(keys, () => onKeyPress({ action })); mousetrap.bind(keys, () => onKeyPress({ action }));
if (keyupActions.includes(action)) { if (keyupActions.has(action)) {
mousetrap.bind(keys, () => onKeyPress({ action, keyup: true }), 'keyup'); mousetrap.bind(keys, () => onKeyPress({ action, keyup: true }), 'keyup');
} }
}); });

View File

@ -117,7 +117,7 @@ export default ({
const currentCutSeg = useMemo(() => cutSegments[currentSegIndexSafe], [currentSegIndexSafe, cutSegments]); const currentCutSeg = useMemo(() => cutSegments[currentSegIndexSafe], [currentSegIndexSafe, cutSegments]);
const currentApparentCutSeg = useMemo(() => apparentCutSegments[currentSegIndexSafe], [apparentCutSegments, currentSegIndexSafe]); const currentApparentCutSeg = useMemo(() => apparentCutSegments[currentSegIndexSafe], [apparentCutSegments, currentSegIndexSafe]);
const selectedSegmentsRaw = useMemo(() => apparentCutSegments.filter(isSegmentSelected), [apparentCutSegments, isSegmentSelected]); const selectedSegmentsRaw = useMemo(() => apparentCutSegments.filter((segment) => isSegmentSelected(segment)), [apparentCutSegments, isSegmentSelected]);
const detectBlackScenes = useCallback(async () => { const detectBlackScenes = useCallback(async () => {
const filterOptions = await showParametersDialog({ title: i18n.t('Enter parameters'), parameters: ffmpegParameters.blackdetect(), docUrl: 'https://ffmpeg.org/ffmpeg-filters.html#blackdetect' }); const filterOptions = await showParametersDialog({ title: i18n.t('Enter parameters'), parameters: ffmpegParameters.blackdetect(), docUrl: 'https://ffmpeg.org/ffmpeg-filters.html#blackdetect' });
@ -170,7 +170,7 @@ export default ({
}, [apparentCutSegments, duration, haveInvalidSegs]); }, [apparentCutSegments, duration, haveInvalidSegs]);
const invertAllSegments = useCallback(() => { const invertAllSegments = useCallback(() => {
if (inverseCutSegments.length < 1) { if (inverseCutSegments.length === 0) {
errorToast(i18n.t('Make sure you have no overlapping segments.')); errorToast(i18n.t('Make sure you have no overlapping segments.'));
return; return;
} }
@ -180,7 +180,7 @@ export default ({
}, [inverseCutSegments, setCutSegments]); }, [inverseCutSegments, setCutSegments]);
const fillSegmentsGaps = useCallback(() => { const fillSegmentsGaps = useCallback(() => {
if (inverseCutSegments.length < 1) { if (inverseCutSegments.length === 0) {
errorToast(i18n.t('Make sure you have no overlapping segments.')); errorToast(i18n.t('Make sure you have no overlapping segments.'));
return; return;
} }
@ -227,7 +227,7 @@ export default ({
return newSegment; return newSegment;
}, { concurrency }); }, { concurrency });
newSegments = newSegments.filter((segment) => segment.end > segment.start); newSegments = newSegments.filter((segment) => segment.end > segment.start);
if (newSegments.length < 1) setCutSegments(createInitialCutSegments()); if (newSegments.length === 0) setCutSegments(createInitialCutSegments());
else setCutSegments(newSegments); else setCutSegments(newSegments);
}, [apparentCutSegments, createInitialCutSegments, duration, isSegmentSelected, setCutSegments]); }, [apparentCutSegments, createInitialCutSegments, duration, isSegmentSelected, setCutSegments]);
@ -449,7 +449,7 @@ export default ({
}, [cutSegments, enableSegments]); }, [cutSegments, enableSegments]);
const onLabelSelectedSegments = useCallback(async () => { const onLabelSelectedSegments = useCallback(async () => {
if (selectedSegmentsRaw.length < 1) return; if (selectedSegmentsRaw.length === 0) return;
const { name } = selectedSegmentsRaw[0]; const { name } = selectedSegmentsRaw[0];
const value = await labelSegmentDialog({ currentName: name, maxLength: maxLabelLength }); const value = await labelSegmentDialog({ currentName: name, maxLength: maxLabelLength });
if (value == null) return; if (value == null) return;

View File

@ -42,4 +42,5 @@ i18n
}, },
}); });
// eslint-disable-next-line unicorn/prefer-export-from
export default i18n; export default i18n;

View File

@ -84,7 +84,7 @@ it('detects overlapping segments', () => {
]); ]);
expect(partitionIntoOverlappingRanges([ expect(partitionIntoOverlappingRanges([
{ start: 9, end: 10.50 }, { start: 9, end: 10.5 },
{ start: 11, end: 12 }, { start: 11, end: 12 },
{ start: 11.5, end: 12.5 }, { start: 11.5, end: 12.5 },
{ start: 11.5, end: 13 }, { start: 11.5, end: 13 },

View File

@ -99,11 +99,11 @@ export function combineOverlappingSegments(existingSegments, getSegApparentEnd2)
}; };
} }
return undefined; // then remove all other segments in this partition group return undefined; // then remove all other segments in this partition group
}).filter((segment) => segment); }).filter(Boolean);
} }
export function combineSelectedSegments<T extends SegmentBase>(existingSegments: T[], getSegApparentEnd2, isSegmentSelected) { export function combineSelectedSegments<T extends SegmentBase>(existingSegments: T[], getSegApparentEnd2, isSegmentSelected) {
const selectedSegments = existingSegments.filter(isSegmentSelected); const selectedSegments = existingSegments.filter((segment) => isSegmentSelected(segment));
const firstSegment = minBy(selectedSegments, (seg) => getSegApparentStart(seg)); const firstSegment = minBy(selectedSegments, (seg) => getSegApparentStart(seg));
const lastSegment = maxBy(selectedSegments, (seg) => getSegApparentEnd2(seg)); const lastSegment = maxBy(selectedSegments, (seg) => getSegApparentEnd2(seg));
@ -117,18 +117,18 @@ export function combineSelectedSegments<T extends SegmentBase>(existingSegments:
} }
if (isSegmentSelected(existingSegment)) return undefined; // remove other selected segments if (isSegmentSelected(existingSegment)) return undefined; // remove other selected segments
return existingSegment; return existingSegment;
}).filter((segment) => segment); }).filter(Boolean);
} }
export function hasAnySegmentOverlap(sortedSegments) { export function hasAnySegmentOverlap(sortedSegments) {
if (sortedSegments.length < 1) return false; if (sortedSegments.length === 0) return false;
const overlappingGroups = partitionIntoOverlappingRanges(sortedSegments); const overlappingGroups = partitionIntoOverlappingRanges(sortedSegments);
return overlappingGroups.length > 0; return overlappingGroups.length > 0;
} }
export function invertSegments(sortedCutSegments, includeFirstSegment: boolean, includeLastSegment: boolean, duration?: number) { export function invertSegments(sortedCutSegments, includeFirstSegment: boolean, includeLastSegment: boolean, duration?: number) {
if (sortedCutSegments.length < 1) return undefined; if (sortedCutSegments.length === 0) return undefined;
if (hasAnySegmentOverlap(sortedCutSegments)) return undefined; if (hasAnySegmentOverlap(sortedCutSegments)) return undefined;
@ -157,7 +157,7 @@ export function invertSegments(sortedCutSegments, includeFirstSegment: boolean,
}); });
if (includeLastSegment) { if (includeLastSegment) {
const lastSeg = sortedCutSegments[sortedCutSegments.length - 1]; const lastSeg = sortedCutSegments.at(-1);
if (duration == null || lastSeg.end < duration) { if (duration == null || lastSeg.end < duration) {
const inverted: InverseSegment = { const inverted: InverseSegment = {
start: lastSeg.end, start: lastSeg.end,
@ -175,7 +175,7 @@ export function invertSegments(sortedCutSegments, includeFirstSegment: boolean,
// because chapters need to be contiguous, we need to insert gaps in-between // because chapters need to be contiguous, we need to insert gaps in-between
export function convertSegmentsToChapters(sortedSegments) { export function convertSegmentsToChapters(sortedSegments) {
if (sortedSegments.length < 1) return []; if (sortedSegments.length === 0) return [];
if (hasAnySegmentOverlap(sortedSegments)) throw new Error('Segments cannot overlap'); if (hasAnySegmentOverlap(sortedSegments)) throw new Error('Segments cannot overlap');
sortedSegments.map((segment) => ({ start: segment.start, end: segment.end, name: segment.name })); sortedSegments.map((segment) => ({ start: segment.start, end: segment.end, name: segment.name }));

View File

@ -163,12 +163,12 @@ export function handleError(arg1: unknown, arg2?: unknown) {
toast.fire({ toast.fire({
icon: 'error', icon: 'error',
title: msg || i18n.t('An error has occurred.'), title: msg || i18n.t('An error has occurred.'),
text: errorMsg ? errorMsg.substring(0, 300) : undefined, text: errorMsg ? errorMsg.slice(0, 300) : undefined,
}); });
} }
export function filenamify(name) { export function filenamify(name) {
return name.replace(/[^0-9a-zA-Z_\-.]/g, '_'); return name.replaceAll(/[^\w.-]/g, '_');
} }
export function withBlur(cb) { export function withBlur(cb) {
@ -248,7 +248,7 @@ export async function findExistingHtml5FriendlyFile(fp, cod) {
matches = [...matches, ...nonMatches]; matches = [...matches, ...nonMatches];
// console.log(matches); // console.log(matches);
if (matches.length < 1) return undefined; if (matches.length === 0) return undefined;
const { suffix, entry } = matches[0]!; const { suffix, entry } = matches[0]!;
@ -318,7 +318,7 @@ export async function checkAppPath() {
// eslint-disable-next-line no-useless-concat, one-var, one-var-declaration-per-line // eslint-disable-next-line no-useless-concat, one-var, one-var-declaration-per-line
const mf = 'mi' + 'fi.no', llc = 'Los' + 'slessC' + 'ut'; const mf = 'mi' + 'fi.no', llc = 'Los' + 'slessC' + 'ut';
const appPath = isDev ? 'C:\\Program Files\\WindowsApps\\37672NoveltyStudio.MediaConverter_9.0.6.0_x64__vjhnv588cyf84' : remote.app.getAppPath(); const appPath = isDev ? 'C:\\Program Files\\WindowsApps\\37672NoveltyStudio.MediaConverter_9.0.6.0_x64__vjhnv588cyf84' : remote.app.getAppPath();
const pathMatch = appPath.replace(/\\/g, '/').match(/Windows ?Apps\/([^/]+)/); // find the first component after WindowsApps const pathMatch = appPath.replaceAll('\\', '/').match(/Windows ?Apps\/([^/]+)/); // find the first component after WindowsApps
// example pathMatch: 37672NoveltyStudio.MediaConverter_9.0.6.0_x64__vjhnv588cyf84 // example pathMatch: 37672NoveltyStudio.MediaConverter_9.0.6.0_x64__vjhnv588cyf84
if (!pathMatch) { if (!pathMatch) {
console.warn('Unknown path match', appPath); console.warn('Unknown path match', appPath);
@ -358,7 +358,8 @@ export function shuffleArray(arrayIn) {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
export function escapeRegExp(string) { export function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string // eslint-disable-next-line unicorn/better-regex
return string.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
} }
export const readFileSize = async (path) => (await stat(path)).size; export const readFileSize = async (path) => (await stat(path)).size;
@ -377,8 +378,7 @@ export function checkFileSizes(inputSize, outputSize) {
function setDocumentExtraTitle(extra) { function setDocumentExtraTitle(extra) {
const baseTitle = 'LosslessCut'; const baseTitle = 'LosslessCut';
if (extra != null) document.title = `${baseTitle} - ${extra}`; document.title = extra != null ? `${baseTitle} - ${extra}` : baseTitle;
else document.title = baseTitle;
} }
export function setDocumentTitle({ filePath, working, cutProgress }: { filePath: string, working?: string, cutProgress?: number }) { export function setDocumentTitle({ filePath, working, cutProgress }: { filePath: string, working?: string, cutProgress?: number }) {
@ -402,7 +402,7 @@ export function mustDisallowVob() {
export async function readVideoTs(videoTsPath) { export async function readVideoTs(videoTsPath) {
const files = await readdir(videoTsPath); const files = await readdir(videoTsPath);
const relevantFiles = files.filter((file) => /^VTS_\d+_\d+\.vob$/i.test(file) && !/^VTS_\d+_00\.vob$/i.test(file)); // skip menu const relevantFiles = files.filter((file) => /^vts_\d+_\d+\.vob$/i.test(file) && !/^vts_\d+_00\.vob$/i.test(file)); // skip menu
const ret = sortBy(relevantFiles).map((file) => join(videoTsPath, file)); const ret = sortBy(relevantFiles).map((file) => join(videoTsPath, file));
if (ret.length === 0) throw new Error('No VTS vob files found in folder'); if (ret.length === 0) throw new Error('No VTS vob files found in folder');
return ret; return ret;

View File

@ -43,7 +43,8 @@ export const isExactDurationMatch = (str) => /^-?\d{2}:\d{2}:\d{2}.\d{3}$/.test(
// See also parseYoutube // See also parseYoutube
export function parseDuration(str) { export function parseDuration(str) {
const match = str.replace(/\s/g, '').match(/^(-?)(?:(?:(\d{1,}):)?(\d{1,2}):)?(\d{1,2}(?:[.,]\d{1,3})?)$/); // eslint-disable-next-line unicorn/better-regex
const match = str.replaceAll(/\s/g, '').match(/^(-?)(?:(?:(\d{1,}):)?(\d{1,2}):)?(\d{1,2}(?:[.,]\d{1,3})?)$/);
if (!match) return undefined; if (!match) return undefined;

View File

@ -126,7 +126,7 @@ export function generateOutSegFileNames({ segments, template: desiredTemplate, f
// Fields that did not come from the source file's name must be sanitized, because they may contain characters that are not supported by the target operating/file system // Fields that did not come from the source file's name must be sanitized, because they may contain characters that are not supported by the target operating/file system
// however we disable this when the user has chosen to (safeOutputFileName === false) // however we disable this when the user has chosen to (safeOutputFileName === false)
const filenamifyOrNot = (fileName) => (safeOutputFileName || forceSafeOutputFileName ? filenamify(fileName) : fileName).substr(0, maxLabelLength); const filenamifyOrNot = (fileName) => (safeOutputFileName || forceSafeOutputFileName ? filenamify(fileName) : fileName).slice(0, Math.max(0, maxLabelLength));
function getSegSuffix() { function getSegSuffix() {
if (name) return `-${filenamifyOrNot(name)}`; if (name) return `-${filenamifyOrNot(name)}`;
@ -159,7 +159,7 @@ export function generateOutSegFileNames({ segments, template: desiredTemplate, f
return [ return [
...rest, ...rest,
// If sanitation is enabled, make sure filename (last seg of the path) is not too long // If sanitation is enabled, make sure filename (last seg of the path) is not too long
safeOutputFileName ? lastSeg!.substring(0, 200) : lastSeg, safeOutputFileName ? lastSeg!.slice(0, 200) : lastSeg,
].join(pathSep); ].join(pathSep);
}); });
} }

View File

@ -1,3 +1,4 @@
// eslint-disable-line unicorn/filename-case
import clamp from 'lodash/clamp'; import clamp from 'lodash/clamp';
/** /**
@ -24,12 +25,12 @@ export function adjustRate(playbackRate, direction, multiplier) {
// stop along the way at 1.0. This could happen if the current playbackRate was reached // stop along the way at 1.0. This could happen if the current playbackRate was reached
// using a different multiplier (e.g., holding the shift key). // using a different multiplier (e.g., holding the shift key).
// https://github.com/mifi/lossless-cut/issues/447#issuecomment-766339083 // https://github.com/mifi/lossless-cut/issues/447#issuecomment-766339083
if ((newRate > 1.0 && playbackRate < 1.0) || (newRate < 1.0 && playbackRate > 1.0)) { if ((newRate > 1 && playbackRate < 1) || (newRate < 1 && playbackRate > 1)) {
newRate = 1.0; newRate = 1;
} }
// And, clean up any rounding errors that get us to almost 1.0 (e.g., treat 1.00001 as 1) // And, clean up any rounding errors that get us to almost 1.0 (e.g., treat 1.00001 as 1)
if ((newRate > (m ** (-1 / 2))) && (newRate < (m ** (1 / 2)))) { if ((newRate > (m ** (-1 / 2))) && (newRate < (m ** (1 / 2)))) {
newRate = 1.0; newRate = 1;
} }
return clamp(newRate, 0.1, 16); return clamp(newRate, 0.1, 16);
} }

View File

@ -1,3 +1,4 @@
// eslint-disable-line unicorn/filename-case
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { adjustRate, DEFAULT_PLAYBACK_RATE } from './rate-calculator'; import { adjustRate, DEFAULT_PLAYBACK_RATE } from './rate-calculator';

View File

@ -1,14 +1,14 @@
// https://www.ffmpeg.org/doxygen/3.2/libavutil_2utils_8c_source.html#l00079 // https://www.ffmpeg.org/doxygen/3.2/libavutil_2utils_8c_source.html#l00079
const defaultProcessedCodecTypes = [ const defaultProcessedCodecTypes = new Set([
'video', 'video',
'audio', 'audio',
'subtitle', 'subtitle',
'attachment', 'attachment',
]; ]);
const unprocessableCodecs = [ const unprocessableCodecs = new Set([
'dvb_teletext', // ffmpeg doesn't seem to support this https://github.com/mifi/lossless-cut/issues/1343 'dvb_teletext', // ffmpeg doesn't seem to support this https://github.com/mifi/lossless-cut/issues/1343
]; ]);
// taken from `ffmpeg -codecs` // taken from `ffmpeg -codecs`
export const pcmAudioCodecs = [ export const pcmAudioCodecs = [
@ -122,6 +122,7 @@ function getPerStreamFlags({ stream, outputIndex, outFormat, manuallyCopyDisposi
addArgs(`-c:${outputIndex}`, codec); addArgs(`-c:${outputIndex}`, codec);
} }
// eslint-disable-next-line unicorn/prefer-switch
if (stream.codec_type === 'subtitle') { if (stream.codec_type === 'subtitle') {
// mp4/mov only supports mov_text, so convert it https://stackoverflow.com/a/17584272/6519037 // mp4/mov only supports mov_text, so convert it https://stackoverflow.com/a/17584272/6519037
// https://github.com/mifi/lossless-cut/issues/418 // https://github.com/mifi/lossless-cut/issues/418
@ -135,6 +136,7 @@ function getPerStreamFlags({ stream, outputIndex, outFormat, manuallyCopyDisposi
} else if (outFormat === 'webm' && stream.codec_name === 'mov_text') { } else if (outFormat === 'webm' && stream.codec_name === 'mov_text') {
// Only WebVTT subtitles are supported for WebM. // Only WebVTT subtitles are supported for WebM.
addCodecArgs('webvtt'); addCodecArgs('webvtt');
// eslint-disable-next-line unicorn/prefer-switch
} else if (outFormat === 'srt') { // not technically lossless but why not } else if (outFormat === 'srt') { // not technically lossless but why not
addCodecArgs('srt'); addCodecArgs('srt');
} else if (outFormat === 'ass') { // not technically lossless but why not } else if (outFormat === 'ass') { // not technically lossless but why not
@ -171,6 +173,7 @@ function getPerStreamFlags({ stream, outputIndex, outFormat, manuallyCopyDisposi
if (isMov(outFormat)) { if (isMov(outFormat)) {
// 0x31766568 see https://github.com/mifi/lossless-cut/issues/1444 // 0x31766568 see https://github.com/mifi/lossless-cut/issues/1444
// eslint-disable-next-line unicorn/prefer-switch, unicorn/no-lonely-if
if (['0x0000', '0x31637668', '0x31766568'].includes(stream.codec_tag) && stream.codec_name === 'hevc') { if (['0x0000', '0x31637668', '0x31766568'].includes(stream.codec_tag) && stream.codec_name === 'hevc') {
addArgs(`-tag:${outputIndex}`, 'hvc1'); addArgs(`-tag:${outputIndex}`, 'hvc1');
} }
@ -214,8 +217,8 @@ export function getMapStreamsArgs({ startIndex = 0, outFormat, allFilesMeta, cop
} }
export function shouldCopyStreamByDefault(stream) { export function shouldCopyStreamByDefault(stream) {
if (!defaultProcessedCodecTypes.includes(stream.codec_type)) return false; if (!defaultProcessedCodecTypes.has(stream.codec_type)) return false;
if (unprocessableCodecs.includes(stream.codec_name)) return false; if (unprocessableCodecs.has(stream.codec_name)) return false;
return true; return true;
} }
@ -225,9 +228,9 @@ export function isStreamThumbnail(stream) {
return stream && stream.codec_type === 'video' && stream.disposition?.[attachedPicDisposition] === 1; return stream && stream.codec_type === 'video' && stream.disposition?.[attachedPicDisposition] === 1;
} }
export const getAudioStreams = (streams) => streams.filter(stream => stream.codec_type === 'audio'); export const getAudioStreams = (streams) => streams.filter((stream) => stream.codec_type === 'audio');
export const getRealVideoStreams = (streams) => streams.filter(stream => stream.codec_type === 'video' && !isStreamThumbnail(stream)); export const getRealVideoStreams = (streams) => streams.filter((stream) => stream.codec_type === 'video' && !isStreamThumbnail(stream));
export const getSubtitleStreams = (streams) => streams.filter(stream => stream.codec_type === 'subtitle'); export const getSubtitleStreams = (streams) => streams.filter((stream) => stream.codec_type === 'subtitle');
// videoTracks/audioTracks seems to be 1-indexed, while ffmpeg is 0-indexes // videoTracks/audioTracks seems to be 1-indexed, while ffmpeg is 0-indexes
const getHtml5TrackId = (ffmpegTrackIndex: number) => String(ffmpegTrackIndex + 1); const getHtml5TrackId = (ffmpegTrackIndex: number) => String(ffmpegTrackIndex + 1);

1973
yarn.lock

File diff suppressed because it is too large Load Diff