kopia lustrzana https://github.com/mifi/lossless-cut
upgrade to electron-vite
rodzic
8a7c1f8a17
commit
f677619039
|
@ -1,3 +1,3 @@
|
|||
/dist
|
||||
/vite-dist
|
||||
/out
|
||||
/ts-dist
|
|
@ -12,7 +12,7 @@ module.exports = {
|
|||
|
||||
overrides: [
|
||||
{
|
||||
files: ['./src/**/*.{js,cjs,mjs,jsx,ts,tsx,mts}'],
|
||||
files: ['./src/renderer/**/*.{js,cjs,mjs,jsx,ts,tsx,mts}'],
|
||||
env: {
|
||||
node: false,
|
||||
browser: true,
|
||||
|
@ -22,7 +22,13 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
{
|
||||
files: ['./script/**/*.{js,cjs,mjs,jsx,ts,tsx,mts}', 'vite.config.js'],
|
||||
files: ['./src/preload/**/*.{js,cjs,jsx,ts,tsx}'],
|
||||
env: {
|
||||
browser: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['./script/**/*.{js,cjs,mjs,jsx,ts,tsx,mts}', 'electron.vite.config.js'],
|
||||
rules: {
|
||||
'import/no-extraneous-dependencies': ['error', {
|
||||
devDependencies: true,
|
||||
|
|
|
@ -98,7 +98,7 @@ jobs:
|
|||
- name: (MacOS) Upload to Mac App Store
|
||||
if: startsWith(matrix.os, 'macos') && env.is_tag == 'true'
|
||||
run: |
|
||||
node script/xcrun-wrapper.mjs dist/mas-universal/LosslessCut-mac-universal.pkg ${{ secrets.api_key_id }} ${{ secrets.api_key_issuer_id }} 1505323402 no.mifi.losslesscut-mac
|
||||
npx tsx script/xcrun-wrapper.mts dist/mas-universal/LosslessCut-mac-universal.pkg ${{ secrets.api_key_id }} ${{ secrets.api_key_issuer_id }} 1505323402 no.mifi.losslesscut-mac
|
||||
|
||||
- name: (MacOS) Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
|
|
|
@ -11,7 +11,7 @@ node_modules
|
|||
!.yarn/versions
|
||||
|
||||
/dist
|
||||
/vite-dist
|
||||
/out
|
||||
/icon-build
|
||||
/build-resources
|
||||
/doc
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"search.exclude": {
|
||||
"/public/locales/**": true,
|
||||
"/src/main/locales/**": true,
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
<div align="center">
|
||||
<br>
|
||||
<p><a href="https://mifi.no/losslesscut/"><img src="src/icon.svg" width="120" alt="LosslessCut" /></a></p>
|
||||
<p><a href="https://mifi.no/losslesscut/"><img src="src/renderer/src/icon.svg" width="120" alt="LosslessCut" /></a></p>
|
||||
<p><b>LosslessCut</b></p>
|
||||
The swiss army knife of lossless video/audio editing
|
||||
<br>
|
||||
|
|
2
cli.md
2
cli.md
|
@ -26,7 +26,7 @@ LosslessCut file1.mp4 file2.mkv
|
|||
```
|
||||
|
||||
## Override settings (experimental)
|
||||
See [available settings](https://github.com/mifi/lossless-cut/blob/master/public/configStore.js). Note that this is subject to change in newer versions. ⚠️ If you specify incorrect values it could corrupt your configuration file. You may use JSON or JSON5. Example:
|
||||
See [available settings](https://github.com/mifi/lossless-cut/blob/master/src/main/configStore.js). Note that this is subject to change in newer versions. ⚠️ If you specify incorrect values it could corrupt your configuration file. You may use JSON or JSON5. Example:
|
||||
```bash
|
||||
LosslessCut --settings-json '{captureFormat:"jpeg", "keyframeCut":true}'
|
||||
```
|
||||
|
|
|
@ -5,10 +5,6 @@
|
|||
This app is built using Electron.
|
||||
Make sure you have at least Node v16. The app uses ffmpeg from PATH when developing.
|
||||
|
||||
```bash
|
||||
npm install -g yarn
|
||||
```
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mifi/lossless-cut.git
|
||||
cd lossless-cut
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
|
||||
export default defineConfig({
|
||||
main: {
|
||||
// https://electron-vite.org/guide/dev#dependencies-vs-devdependencies
|
||||
// For the main process and preload, the best practice is to externalize dependencies and only bundle our own code.
|
||||
// However, until we use ESM for electron main, we need to include ESM-only deps in the bundle: (exclude from externalize)
|
||||
plugins: [externalizeDepsPlugin({ exclude: ['p-map', 'execa', 'nanoid'] })],
|
||||
},
|
||||
preload: {
|
||||
// https://electron-vite.org/guide/dev#dependencies-vs-devdependencies
|
||||
plugins: [externalizeDepsPlugin({ exclude: [] })],
|
||||
},
|
||||
renderer: {
|
||||
plugins: [react()],
|
||||
build: {
|
||||
chunkSizeWarningLimit: 3e6,
|
||||
sourcemap: true,
|
||||
},
|
||||
server: {
|
||||
port: 3001,
|
||||
host: '127.0.0.1',
|
||||
https: false,
|
||||
},
|
||||
},
|
||||
});
|
|
@ -1,8 +1,8 @@
|
|||
// eslint-disable-line unicorn/filename-case
|
||||
export default {
|
||||
input: ['src/**/*.{js,jsx,ts,tsx}', 'public/*.{js,ts}'],
|
||||
input: ['src/renderer/**/*.{js,jsx,ts,tsx}', 'src/main/*.{js,ts}'],
|
||||
|
||||
output: 'public/locales/$LOCALE/$NAMESPACE.json',
|
||||
output: 'src/main/locales/$LOCALE/$NAMESPACE.json',
|
||||
indentation: 4,
|
||||
|
||||
sort: true,
|
||||
|
@ -16,7 +16,7 @@ export default {
|
|||
|
||||
defaultValue: (lng, ns, key) => key,
|
||||
|
||||
// Keep in sync between i18next-parser.config.js and i18n-common.js:
|
||||
// Keep in sync between i18next-parser.config.js and i18nCommon.js:
|
||||
keySeparator: false,
|
||||
namespaceSeparator: false,
|
||||
};
|
||||
|
|
66
package.json
66
package.json
|
@ -4,31 +4,27 @@
|
|||
"description": "The swiss army knife of lossless video/audio editing",
|
||||
"copyright": "Copyright © 2021 ${author}",
|
||||
"version": "3.60.0",
|
||||
"main": "public/electron.js",
|
||||
"main": "./out/main/index.js",
|
||||
"homepage": "./",
|
||||
"scripts": {
|
||||
"dev": "concurrently -k \"npm run dev:frontend\" \"npm run dev:electron\"",
|
||||
"dev:frontend": "cross-env vite --port 3001",
|
||||
"dev:electron": "wait-on tcp:3001 && electron .",
|
||||
"icon-gen": "mkdirp icon-build build-resources/appx && node script/icon-gen.mjs",
|
||||
"clean": "rimraf dist out ts-dist build-resources icon-build",
|
||||
"start": "electron-vite preview",
|
||||
"dev": "electron-vite dev -w",
|
||||
"icon-gen": "mkdirp icon-build build-resources/appx && tsx script/icon-gen.mts",
|
||||
"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-linux-x64": "mkdirp ffmpeg/linux-x64 && cd ffmpeg/linux-x64 && wget https://github.com/mifi/ffmpeg-builds/releases/download/6.0/ffmpeg-n6.0-12-ga6dc92968a-linux64-gpl-shared-6.0.tar.xz -O ffmpeg-ffprobe.xz && tar -xv -f ffmpeg-ffprobe.xz && mv ffmpeg-n6.0-12-ga6dc92968a-linux64-gpl-shared-6.0 extracted && mkdirp lib && mv extracted/bin/ffmpeg extracted/bin/ffprobe extracted/lib/lib*.so* lib",
|
||||
"download-ffmpeg-win32-x64": "mkdirp ffmpeg/win32-x64 && cd ffmpeg/win32-x64 && npx download-cli https://github.com/mifi/ffmpeg-builds/releases/download/6.0/ffmpeg-n6.0-12-ga6dc92968a-win64-gpl-shared-6.0.zip --out . --filename ffmpeg-ffprobe.zip && 7z x ffmpeg-ffprobe.zip && mkdirp lib && cd ffmpeg-n6.0-12-ga6dc92968a-win64-gpl-shared-6.0/bin && npx shx mv ffmpeg.exe ffprobe.exe *.dll ../../lib",
|
||||
"build": "yarn icon-gen && vite build --outDir vite-dist",
|
||||
"build": "yarn icon-gen && electron-vite build",
|
||||
"tsc": "tsc --build",
|
||||
"test": "vitest",
|
||||
"lint": "eslint --ext .js,.ts,.jsx,.tsx,.mjs .",
|
||||
"pack-mac": "electron-builder --mac -m dmg",
|
||||
"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)'",
|
||||
"prepack-mas-dev": "yarn build",
|
||||
"pack-win": "electron-builder --win",
|
||||
"prepack-win": "yarn build",
|
||||
"lint": "eslint --ext .js,.ts,.jsx,.tsx,.mjs,.mts .",
|
||||
"pack-mac": "yarn build && electron-builder --mac -m dmg",
|
||||
"pack-mas-dev": "yarn build && electron-builder --mac -m mas-dev -c.mas.provisioningProfile=LosslessCut_Dev.provisionprofile -c.mas.identity='Apple Development: Mikael Finstad (JH4PH8B3C8)'",
|
||||
"pack-win": "yarn build && electron-builder --win",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"version": "node script/postversion.mjs && git add no.mifi.losslesscut.appdata.xml",
|
||||
"pack-linux": "electron-builder --linux",
|
||||
"prepack-linux": "yarn build",
|
||||
"version": "tsx script/postversion.mts && git add no.mifi.losslesscut.appdata.xml",
|
||||
"pack-linux": "yarn build && electron-builder --linux",
|
||||
"scan-i18n": "i18next --config i18next-parser.config.mjs",
|
||||
"generate-licenses": "yarn licenses generate-disclaimer > licenses.txt && echo '\n\nffmpeg is licensed under GPL v2+:\n\nhttp://www.gnu.org/licenses/old-licenses/gpl-2.0.html' >> licenses.txt"
|
||||
},
|
||||
|
@ -44,29 +40,36 @@
|
|||
"license": "GPL-2.0-only",
|
||||
"devDependencies": {
|
||||
"@fontsource/open-sans": "^4.5.14",
|
||||
"@radix-ui/colors": "^0.1.8",
|
||||
"@radix-ui/react-switch": "^1.0.1",
|
||||
"@tsconfig/node18": "^18.2.2",
|
||||
"@tsconfig/strictest": "^2.0.2",
|
||||
"@tsconfig/vite-react": "^3.0.0",
|
||||
"@types/color": "^3.0.6",
|
||||
"@types/css-modules": "^1.0.5",
|
||||
"@types/eslint": "^8",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/morgan": "^1.9.9",
|
||||
"@types/node": "18",
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@types/sortablejs": "^1.15.0",
|
||||
"@types/yargs-parser": "^21.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
||||
"@typescript-eslint/parser": "^6.12.0",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"color": "^3.1.0",
|
||||
"concurrently": "^6.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"csv-parse": "^4.15.3",
|
||||
"csv-stringify": "^5.6.2",
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"electron": "^27.0.0",
|
||||
"electron-builder": "^24.6.4",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-vite": "^2.1.0",
|
||||
"eslint": "^8.2.0",
|
||||
"eslint-config-mifi": "^0.0.3",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
|
@ -78,7 +81,7 @@
|
|||
"fast-xml-parser": "^4.2.5",
|
||||
"framer-motion": "^9.0.3",
|
||||
"i18next-parser": "^7.6.0",
|
||||
"icon-gen": "^3.0.0",
|
||||
"icon-gen": "^4.0.0",
|
||||
"immer": "^10.0.2",
|
||||
"ky": "^0.33.1",
|
||||
"luxon": "^3.3.0",
|
||||
|
@ -97,6 +100,7 @@
|
|||
"react-sortablejs": "^6.1.4",
|
||||
"react-syntax-highlighter": "^15.4.3",
|
||||
"react-use": "^17.4.0",
|
||||
"rimraf": "^5.0.5",
|
||||
"screenfull": "^6.0.2",
|
||||
"scroll-into-view-if-needed": "^2.2.28",
|
||||
"sharp": "^0.32.6",
|
||||
|
@ -105,28 +109,25 @@
|
|||
"sweetalert2": "^11.0.0",
|
||||
"sweetalert2-react-content": "^5.0.7",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "~5.2.0",
|
||||
"use-debounce": "^5.1.0",
|
||||
"use-trace-update": "^1.3.0",
|
||||
"vite": "^4.5.2",
|
||||
"vitest": "^1.2.2",
|
||||
"wait-on": "^7.0.1"
|
||||
"vitest": "^1.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "^2.0.10",
|
||||
"@radix-ui/colors": "^0.1.8",
|
||||
"@octokit/core": "5",
|
||||
"cue-parser": "^0.3.0",
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"electron-is-dev": "^2.0.0",
|
||||
"electron-store": "5.1.1",
|
||||
"electron-unhandled": "^4.0.1",
|
||||
"execa": "5",
|
||||
"execa": "^8.0.1",
|
||||
"express": "^4.18.2",
|
||||
"express-async-handler": "^1.2.0",
|
||||
"file-type": "16",
|
||||
"file-url": "^3.0.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"github-api": "^3.2.2",
|
||||
"i18next": "^22.4.10",
|
||||
"i18next-fs-backend": "^2.1.1",
|
||||
"json5": "^2.2.2",
|
||||
|
@ -134,25 +135,28 @@
|
|||
"mime-types": "^2.1.14",
|
||||
"morgan": "^1.10.0",
|
||||
"semver": "^7.5.2",
|
||||
"string-to-stream": "^1.1.1",
|
||||
"string-to-stream": "^3.0.1",
|
||||
"winston": "^3.8.1",
|
||||
"yargs-parser": "^21.0.0"
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"build": {
|
||||
"directories": {
|
||||
"buildResources": "build-resources"
|
||||
},
|
||||
"extraMetadata": {
|
||||
"main": "vite-dist/electron.js"
|
||||
},
|
||||
"files": [
|
||||
"vite-dist/**/*"
|
||||
"out/**/*"
|
||||
],
|
||||
"asar": {
|
||||
"smartUnpack": false
|
||||
},
|
||||
"appId": "no.mifi.losslesscut",
|
||||
"artifactName": "${productName}-${os}-${arch}.${ext}",
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "locales",
|
||||
"to": "locales"
|
||||
}
|
||||
],
|
||||
"mac": {
|
||||
"hardenedRuntime": true,
|
||||
"appId": "no.mifi.losslesscut-mac",
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
const homepage = 'https://mifi.no/losslesscut/';
|
||||
const githubLink = 'https://github.com/mifi/lossless-cut/';
|
||||
const getReleaseUrl = (version) => `https://github.com/mifi/lossless-cut/releases/tag/v${version}`;
|
||||
const licensesPage = 'https://losslesscut.mifi.no/licenses.txt';
|
||||
|
||||
module.exports = {
|
||||
homepage,
|
||||
getReleaseUrl,
|
||||
githubLink,
|
||||
licensesPage,
|
||||
};
|
|
@ -1,20 +0,0 @@
|
|||
const os = require('os');
|
||||
|
||||
const frontendBuildDir = 'vite-dist';
|
||||
|
||||
const platform = os.platform();
|
||||
const arch = os.arch();
|
||||
|
||||
// todo dedupe between renderer and main
|
||||
const isWindows = platform === 'win32';
|
||||
const isMac = platform === 'darwin';
|
||||
const isLinux = platform === 'linux';
|
||||
|
||||
module.exports = {
|
||||
frontendBuildDir,
|
||||
isWindows,
|
||||
isMac,
|
||||
isLinux,
|
||||
platform,
|
||||
arch,
|
||||
};
|
|
@ -1,29 +0,0 @@
|
|||
// eslint-disable-line unicorn/filename-case
|
||||
import sharp from 'sharp';
|
||||
import icongen from 'icon-gen';
|
||||
|
||||
const svg2png = (from, to, width, height) => sharp(from)
|
||||
.png()
|
||||
.resize(width, height, {
|
||||
fit: sharp.fit.contain,
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
||||
})
|
||||
.toFile(to);
|
||||
|
||||
// Linux:
|
||||
await svg2png('src/icon.svg', './icon-build/app-512.png', 512, 512);
|
||||
|
||||
// Windows Store
|
||||
await svg2png('src/icon.svg', './build-resources/appx/StoreLogo.png', 50, 50);
|
||||
await svg2png('src/icon.svg', './build-resources/appx/Square150x150Logo.png', 300, 300);
|
||||
await svg2png('src/icon.svg', './build-resources/appx/Square44x44Logo.png', 44, 44);
|
||||
await svg2png('src/icon.svg', './build-resources/appx/Wide310x150Logo.png', 620, 300);
|
||||
|
||||
// MacOS:
|
||||
// https://github.com/mifi/lossless-cut/issues/1820
|
||||
await icongen('./src/icon-mac.svg', './icon-build', { icns: { sizes: [512, 1024] } });
|
||||
|
||||
// Windows ICO:
|
||||
// https://github.com/mifi/lossless-cut/issues/778
|
||||
// https://stackoverflow.com/questions/3236115/which-icon-sizes-should-my-windows-applications-icon-include
|
||||
await icongen('./src/icon.svg', './icon-build', { ico: { sizes: [16, 24, 32, 40, 48, 64, 96, 128, 256, 512] } });
|
|
@ -0,0 +1,32 @@
|
|||
// eslint-disable-line unicorn/filename-case
|
||||
import sharp from 'sharp';
|
||||
import icongenRaw from 'icon-gen';
|
||||
|
||||
const icongen = icongenRaw as unknown as typeof icongenRaw['default'];
|
||||
|
||||
const svg2png = (from: string, to: string, width: number, height: number) => sharp(from)
|
||||
.png()
|
||||
.resize(width, height, {
|
||||
fit: sharp.fit.contain,
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
||||
})
|
||||
.toFile(to);
|
||||
|
||||
const srcIcon = 'src/renderer/src/icon.svg';
|
||||
// Linux:
|
||||
await svg2png(srcIcon, './icon-build/app-512.png', 512, 512);
|
||||
|
||||
// Windows Store
|
||||
await svg2png(srcIcon, './build-resources/appx/StoreLogo.png', 50, 50);
|
||||
await svg2png(srcIcon, './build-resources/appx/Square150x150Logo.png', 300, 300);
|
||||
await svg2png(srcIcon, './build-resources/appx/Square44x44Logo.png', 44, 44);
|
||||
await svg2png(srcIcon, './build-resources/appx/Wide310x150Logo.png', 620, 300);
|
||||
|
||||
// MacOS:
|
||||
// https://github.com/mifi/lossless-cut/issues/1820
|
||||
await icongen('./src/renderer/src/icon-mac.svg', './icon-build', { icns: { sizes: [512, 1024] }, report: false });
|
||||
|
||||
// Windows ICO:
|
||||
// https://github.com/mifi/lossless-cut/issues/778
|
||||
// https://stackoverflow.com/questions/3236115/which-icon-sizes-should-my-windows-applications-icon-include
|
||||
await icongen(srcIcon, './icon-build', { ico: { sizes: [16, 24, 32, 40, 48, 64, 96, 128, 256, 512] }, report: false });
|
|
@ -1,12 +1,11 @@
|
|||
import { readFile, writeFile } from 'fs/promises';
|
||||
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
const xmlUrl = new URL('../no.mifi.losslesscut.appdata.xml', import.meta.url);
|
||||
const xmlData = await readFile(xmlUrl);
|
||||
|
||||
const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url)));
|
||||
const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url)) as unknown as string);
|
||||
|
||||
const parser = new XMLParser({ alwaysCreateTextNode: true, ignoreAttributes: false, ignoreDeclaration: false });
|
||||
const xml = parser.parse(xmlData);
|
|
@ -1,5 +1,5 @@
|
|||
// eslint-disable-line unicorn/filename-case
|
||||
import execa from 'execa';
|
||||
import { execa } from 'execa';
|
||||
import { readFile } from 'fs/promises';
|
||||
|
||||
// we need a wrapper script because altool tends to error out very often
|
||||
|
@ -17,7 +17,7 @@ const bundleId = args[4];
|
|||
// seems to be the same
|
||||
const ascPublicId = apiIssuer;
|
||||
|
||||
const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url)));
|
||||
const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url)) as unknown as string);
|
||||
|
||||
console.log('Using version', packageJson.version);
|
||||
|
||||
|
@ -74,8 +74,11 @@ async function runAttempt() {
|
|||
console.log('stdout', stdout);
|
||||
return false;
|
||||
} catch (err) {
|
||||
if (err.exitCode === 1 && err.stdout) {
|
||||
const errorJson = JSON.parse(err.stdout);
|
||||
if (err instanceof Error && 'exitCode' in err && err.exitCode === 1 && 'stdout' in err && err.stdout && typeof err.stdout === 'string') {
|
||||
const errorJson = JSON.parse(err.stdout) as unknown;
|
||||
if (!(errorJson != null && typeof errorJson === 'object' && 'product-errors' in errorJson && Array.isArray(errorJson['product-errors']))) {
|
||||
throw new TypeError('Invalid JSON');
|
||||
}
|
||||
const productErrors = errorJson['product-errors'];
|
||||
// Unable to authenticate
|
||||
if (productErrors.some((error) => error.code === -19209)) {
|
|
@ -1,3 +0,0 @@
|
|||
const { isDev }: { isDev: boolean } = window.require('@electron/remote').require('./electron');
|
||||
|
||||
export default isDev;
|
|
@ -1,8 +1,12 @@
|
|||
const logger = require('./logger');
|
||||
const { createMediaSourceProcess, readOneJpegFrame } = require('./ffmpeg');
|
||||
import assert from 'assert';
|
||||
|
||||
import logger from './logger.js';
|
||||
import { createMediaSourceProcess, readOneJpegFrame as readOneJpegFrameRaw } from './ffmpeg.js';
|
||||
|
||||
|
||||
function createMediaSourceStream({ path, videoStreamIndex, audioStreamIndex, seekTo, size, fps }) {
|
||||
export function createMediaSourceStream({ path, videoStreamIndex, audioStreamIndex, seekTo, size, fps }: {
|
||||
path: string, videoStreamIndex?: number | undefined, audioStreamIndex?: number | undefined, seekTo: number, size?: number | undefined, fps?: number | undefined,
|
||||
}) {
|
||||
const abortController = new AbortController();
|
||||
logger.info('Starting preview process', { videoStreamIndex, audioStreamIndex, seekTo });
|
||||
const process = createMediaSourceProcess({ path, videoStreamIndex, audioStreamIndex, seekTo, size, fps });
|
||||
|
@ -13,38 +17,39 @@ function createMediaSourceStream({ path, videoStreamIndex, audioStreamIndex, see
|
|||
process.kill('SIGKILL');
|
||||
};
|
||||
|
||||
process.stdout.pause();
|
||||
const { stdout } = process;
|
||||
assert(stdout != null);
|
||||
|
||||
async function readChunk() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let cleanup;
|
||||
stdout.pause();
|
||||
|
||||
const onClose = () => {
|
||||
cleanup();
|
||||
resolve(null);
|
||||
};
|
||||
const onData = (chunk) => {
|
||||
process.stdout.pause();
|
||||
cleanup();
|
||||
resolve(chunk);
|
||||
};
|
||||
const onError = (err) => {
|
||||
cleanup();
|
||||
reject(err);
|
||||
};
|
||||
cleanup = () => {
|
||||
process.stdout.off('data', onData);
|
||||
process.stdout.off('error', onError);
|
||||
process.stdout.off('close', onClose);
|
||||
};
|
||||
const readChunk = async () => new Promise((resolve, reject) => {
|
||||
let cleanup: () => void;
|
||||
|
||||
process.stdout.once('data', onData);
|
||||
process.stdout.once('error', onError);
|
||||
process.stdout.once('close', onClose);
|
||||
const onClose = () => {
|
||||
cleanup();
|
||||
resolve(null);
|
||||
};
|
||||
const onData = (chunk: Buffer) => {
|
||||
stdout.pause();
|
||||
cleanup();
|
||||
resolve(chunk);
|
||||
};
|
||||
const onError = (err: Error) => {
|
||||
cleanup();
|
||||
reject(err);
|
||||
};
|
||||
cleanup = () => {
|
||||
stdout.off('data', onData);
|
||||
stdout.off('error', onError);
|
||||
stdout.off('close', onClose);
|
||||
};
|
||||
|
||||
process.stdout.resume();
|
||||
});
|
||||
}
|
||||
stdout.once('data', onData);
|
||||
stdout.once('error', onError);
|
||||
stdout.once('close', onClose);
|
||||
|
||||
stdout.resume();
|
||||
});
|
||||
|
||||
function abort() {
|
||||
abortController.abort();
|
||||
|
@ -75,9 +80,9 @@ function createMediaSourceStream({ path, videoStreamIndex, audioStreamIndex, see
|
|||
return { abort, readChunk };
|
||||
}
|
||||
|
||||
function readOneJpegFrameWrapper({ path, seekTo, videoStreamIndex }) {
|
||||
export function readOneJpegFrame({ path, seekTo, videoStreamIndex }: { path: string, seekTo: number, videoStreamIndex: number }) {
|
||||
const abortController = new AbortController();
|
||||
const process = readOneJpegFrame({ path, seekTo, videoStreamIndex });
|
||||
const process = readOneJpegFrameRaw({ path, seekTo, videoStreamIndex });
|
||||
|
||||
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
||||
abortController.signal.onabort = () => process.kill('SIGKILL');
|
||||
|
@ -99,9 +104,3 @@ function readOneJpegFrameWrapper({ path, seekTo, videoStreamIndex }) {
|
|||
|
||||
return { promise, abort };
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
createMediaSourceStream,
|
||||
readOneJpegFrame: readOneJpegFrameWrapper,
|
||||
};
|
|
@ -1,17 +1,17 @@
|
|||
const Store = require('electron-store');
|
||||
import Store from 'electron-store';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const electron = require('electron');
|
||||
const os = require('os');
|
||||
const { join, dirname } = require('path');
|
||||
const { pathExists } = require('fs-extra');
|
||||
import electron from 'electron';
|
||||
import { join, dirname } from 'path';
|
||||
import { pathExists } from 'fs-extra';
|
||||
|
||||
const logger = require('./logger');
|
||||
import { KeyBinding, Config } from '../../types.js';
|
||||
import logger from './logger.js';
|
||||
import { isWindows } from './util.js';
|
||||
|
||||
const { app } = electron;
|
||||
|
||||
|
||||
/** @type {import('../types').KeyBinding[]} */
|
||||
const defaultKeyBindings = [
|
||||
const defaultKeyBindings: KeyBinding[] = [
|
||||
{ keys: 'plus', action: 'addSegment' },
|
||||
{ keys: 'space', action: 'togglePlayResetSpeed' },
|
||||
{ keys: 'k', action: 'togglePlayNoResetSpeed' },
|
||||
|
@ -83,8 +83,7 @@ const defaultKeyBindings = [
|
|||
{ keys: 'alt+down', action: 'decreaseVolume' },
|
||||
];
|
||||
|
||||
/** @type {import('../types').Config} */
|
||||
const defaults = {
|
||||
const defaults: Config = {
|
||||
captureFormat: 'jpeg',
|
||||
customOutDir: undefined,
|
||||
keyframeCut: true,
|
||||
|
@ -145,7 +144,6 @@ const defaults = {
|
|||
// For portable app: https://github.com/mifi/lossless-cut/issues/645
|
||||
async function getCustomStoragePath() {
|
||||
try {
|
||||
const isWindows = os.platform() === 'win32';
|
||||
if (!isWindows || process.windowsStore) return undefined;
|
||||
|
||||
// https://github.com/mifi/lossless-cut/issues/645#issuecomment-1001363314
|
||||
|
@ -161,28 +159,28 @@ async function getCustomStoragePath() {
|
|||
}
|
||||
}
|
||||
|
||||
let store;
|
||||
let store: Store;
|
||||
|
||||
/** @type {import('../types').StoreGetConfig} */
|
||||
function get(key) {
|
||||
export function get<T extends keyof Config>(key: T): Config[T] {
|
||||
return store.get(key);
|
||||
}
|
||||
|
||||
/** @type {import('../types').StoreSetConfig} */
|
||||
function set(key, val) {
|
||||
export function set<T extends keyof Config>(key: T, val: Config[T]) {
|
||||
if (val === undefined) store.delete(key);
|
||||
else store.set(key, val);
|
||||
}
|
||||
|
||||
/** @type {import('../types').StoreResetConfig} */
|
||||
function reset(key) {
|
||||
export function reset<T extends keyof Config>(key: T) {
|
||||
set(key, defaults[key]);
|
||||
}
|
||||
|
||||
async function tryCreateStore({ customStoragePath }) {
|
||||
async function tryCreateStore({ customStoragePath }: { customStoragePath: string | undefined }) {
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
try {
|
||||
store = new Store({ defaults, cwd: customStoragePath });
|
||||
store = new Store({
|
||||
defaults,
|
||||
...(customStoragePath != null ? { cwd: customStoragePath } : {}),
|
||||
});
|
||||
return;
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
|
@ -194,7 +192,7 @@ async function tryCreateStore({ customStoragePath }) {
|
|||
throw new Error('Timed out while creating config store');
|
||||
}
|
||||
|
||||
async function init() {
|
||||
export async function init() {
|
||||
const customStoragePath = await getCustomStoragePath();
|
||||
if (customStoragePath) logger.info('customStoragePath', customStoragePath);
|
||||
|
||||
|
@ -214,10 +212,3 @@ async function init() {
|
|||
set('cleanupChoices', { ...cleanupChoices, closeFile: true });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init,
|
||||
get,
|
||||
set,
|
||||
reset,
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
export const homepage = 'https://mifi.no/losslesscut/';
|
||||
export const githubLink = 'https://github.com/mifi/lossless-cut/';
|
||||
export const getReleaseUrl = (version: string) => `https://github.com/mifi/lossless-cut/releases/tag/v${version}`;
|
||||
export const licensesPage = 'https://losslesscut.mifi.no/licenses.txt';
|
|
@ -1,8 +1,8 @@
|
|||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const { Menu } = require('electron');
|
||||
import { BrowserWindow, Menu } from 'electron';
|
||||
|
||||
// https://github.com/electron/electron/issues/4068#issuecomment-274159726
|
||||
module.exports = (window) => {
|
||||
export default (window: BrowserWindow) => {
|
||||
const selectionMenu = Menu.buildFromTemplate([
|
||||
{ role: 'copy' },
|
||||
{ type: 'separator' },
|
||||
|
@ -23,9 +23,9 @@ module.exports = (window) => {
|
|||
window.webContents.on('context-menu', (_e, props) => {
|
||||
const { selectionText, isEditable } = props;
|
||||
if (isEditable) {
|
||||
inputMenu.popup(window);
|
||||
inputMenu.popup({ window });
|
||||
} else if (selectionText && selectionText.trim() !== '') {
|
||||
selectionMenu.popup(window);
|
||||
selectionMenu.popup({ window });
|
||||
}
|
||||
});
|
||||
};
|
|
@ -1,53 +1,60 @@
|
|||
const { join } = require('path');
|
||||
const isDev = require('electron-is-dev');
|
||||
const readline = require('readline');
|
||||
const stringToStream = require('string-to-stream');
|
||||
const execa = require('execa');
|
||||
import { join } from 'path';
|
||||
import readline from 'readline';
|
||||
import stringToStream from 'string-to-stream';
|
||||
import { BufferEncodingOption, execa, ExecaChildProcess, Options as ExecaOptions } from 'execa';
|
||||
import assert from 'assert';
|
||||
import { Readable } from 'stream';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { app } from 'electron';
|
||||
|
||||
const { platform, arch, isWindows, isMac, isLinux } = require('./util');
|
||||
import { platform, arch, isWindows, isMac, isLinux } from './util.js';
|
||||
import { CaptureFormat, Html5ifyMode, Waveform } from '../../types.js';
|
||||
import isDev from './isDev.js';
|
||||
|
||||
|
||||
const runningFfmpegs = new Set();
|
||||
const runningFfmpegs = new Set<ExecaChildProcess<Buffer>>();
|
||||
// setInterval(() => console.log(runningFfmpegs.size), 1000);
|
||||
|
||||
let customFfPath;
|
||||
let customFfPath: string | undefined;
|
||||
|
||||
// Note that this does not work on MAS because of sandbox restrictions
|
||||
function setCustomFfPath(path) {
|
||||
export function setCustomFfPath(path: string | undefined) {
|
||||
customFfPath = path;
|
||||
}
|
||||
|
||||
function getFfCommandLine(cmd, args) {
|
||||
export function getFfCommandLine(cmd: string, args: readonly string[]) {
|
||||
return `${cmd} ${args.map((arg) => (/[^\w-]/.test(arg) ? `'${arg}'` : arg)).join(' ')}`;
|
||||
}
|
||||
|
||||
function getFfPath(cmd) {
|
||||
function getFfPath(cmd: string) {
|
||||
const exeName = isWindows ? `${cmd}.exe` : cmd;
|
||||
|
||||
if (customFfPath) return join(customFfPath, exeName);
|
||||
|
||||
if (isDev) {
|
||||
const components = ['ffmpeg', `${platform}-${arch}`];
|
||||
if (isWindows || isLinux) components.push('lib');
|
||||
components.push(exeName);
|
||||
return join(...components);
|
||||
if (app.isPackaged) {
|
||||
return join(process.resourcesPath, exeName);
|
||||
}
|
||||
|
||||
return join(process.resourcesPath, exeName);
|
||||
// local dev
|
||||
const components = ['ffmpeg', `${platform}-${arch}`];
|
||||
if (isWindows || isLinux) components.push('lib');
|
||||
components.push(exeName);
|
||||
return join(...components);
|
||||
}
|
||||
|
||||
const getFfprobePath = () => getFfPath('ffprobe');
|
||||
const getFfmpegPath = () => getFfPath('ffmpeg');
|
||||
export const getFfmpegPath = () => getFfPath('ffmpeg');
|
||||
|
||||
function abortFfmpegs() {
|
||||
export function abortFfmpegs() {
|
||||
console.log('Aborting', runningFfmpegs.size, 'ffmpeg process(es)');
|
||||
runningFfmpegs.forEach((process) => {
|
||||
process.kill('SIGTERM', { forceKillAfterTimeout: 10000 });
|
||||
});
|
||||
}
|
||||
|
||||
function handleProgress(process, durationIn, onProgress, customMatcher = () => undefined) {
|
||||
function handleProgress(process: { stderr: Readable | null }, durationIn: number | undefined, onProgress: (a: number) => void, customMatcher: (a: string) => void = () => undefined) {
|
||||
if (!onProgress) return;
|
||||
if (process.stderr == null) return;
|
||||
onProgress(0);
|
||||
|
||||
const rl = readline.createInterface({ input: process.stderr });
|
||||
|
@ -61,20 +68,19 @@ function handleProgress(process, durationIn, onProgress, customMatcher = () => u
|
|||
// eslint-disable-next-line unicorn/better-regex
|
||||
if (!match) match = line.match(/(?:size|Lsize)=\s*[^\s]+\s+time=\s*([^\s]+)\s+/);
|
||||
if (!match) {
|
||||
// @ts-expect-error todo
|
||||
customMatcher(line);
|
||||
return;
|
||||
}
|
||||
|
||||
const timeStr = match[1];
|
||||
// console.log(timeStr);
|
||||
const match2 = timeStr.match(/^(\d+):(\d+):(\d+)\.(\d+)$/);
|
||||
const match2 = timeStr!.match(/^(\d+):(\d+):(\d+)\.(\d+)$/);
|
||||
if (!match2) throw new Error(`Invalid time from ffmpeg progress ${timeStr}`);
|
||||
|
||||
const h = parseInt(match2[1], 10);
|
||||
const m = parseInt(match2[2], 10);
|
||||
const s = parseInt(match2[3], 10);
|
||||
const cs = parseInt(match2[4], 10);
|
||||
const h = parseInt(match2[1]!, 10);
|
||||
const m = parseInt(match2[2]!, 10);
|
||||
const s = parseInt(match2[3]!, 10);
|
||||
const cs = parseInt(match2[4]!, 10);
|
||||
const time = (((h * 60) + m) * 60 + s) + cs / 100;
|
||||
// console.log(time);
|
||||
|
||||
|
@ -93,18 +99,22 @@ function handleProgress(process, durationIn, onProgress, customMatcher = () => u
|
|||
});
|
||||
}
|
||||
|
||||
// @ts-expect-error todo
|
||||
function getExecaOptions({ env, ...customExecaOptions } = {}) {
|
||||
const execaOptions = { ...customExecaOptions, env: { ...env } };
|
||||
function getExecaOptions({ env, ...customExecaOptions }: Omit<ExecaOptions<BufferEncodingOption>, 'buffer'> = {}) {
|
||||
const execaOptions: Omit<ExecaOptions<BufferEncodingOption>, 'buffer'> = { ...customExecaOptions, encoding: 'buffer' };
|
||||
// https://github.com/mifi/lossless-cut/issues/1143#issuecomment-1500883489
|
||||
if (isLinux && !isDev && !customFfPath) execaOptions.env.LD_LIBRARY_PATH = process.resourcesPath;
|
||||
if (isLinux && !isDev && !customFfPath) {
|
||||
return {
|
||||
...execaOptions,
|
||||
env: { ...env, LD_LIBRARY_PATH: process.resourcesPath },
|
||||
};
|
||||
}
|
||||
return execaOptions;
|
||||
}
|
||||
|
||||
// todo collect warnings from ffmpeg output and show them after export? example: https://github.com/mifi/lossless-cut/issues/1469
|
||||
function runFfmpegProcess(args, customExecaOptions, { logCli = true } = {}) {
|
||||
function runFfmpegProcess(args: readonly string[], customExecaOptions?: Omit<ExecaOptions<BufferEncodingOption>, 'encoding'>, additionalOptions?: { logCli?: boolean }) {
|
||||
const ffmpegPath = getFfmpegPath();
|
||||
if (logCli) console.log(getFfCommandLine('ffmpeg', args));
|
||||
if (additionalOptions?.logCli) console.log(getFfCommandLine('ffmpeg', args));
|
||||
|
||||
const process = execa(ffmpegPath, args, getExecaOptions(customExecaOptions));
|
||||
|
||||
|
@ -121,23 +131,29 @@ function runFfmpegProcess(args, customExecaOptions, { logCli = true } = {}) {
|
|||
return process;
|
||||
}
|
||||
|
||||
async function runFfmpegConcat({ ffmpegArgs, concatTxt, totalDuration, onProgress }) {
|
||||
export async function runFfmpegConcat({ ffmpegArgs, concatTxt, totalDuration, onProgress }: {
|
||||
ffmpegArgs: string[], concatTxt: string, totalDuration: number, onProgress: (a: number) => void
|
||||
}) {
|
||||
const process = runFfmpegProcess(ffmpegArgs);
|
||||
|
||||
handleProgress(process, totalDuration, onProgress);
|
||||
|
||||
assert(process.stdin != null);
|
||||
stringToStream(concatTxt).pipe(process.stdin);
|
||||
|
||||
return process;
|
||||
}
|
||||
|
||||
async function runFfmpegWithProgress({ ffmpegArgs, duration, onProgress }) {
|
||||
export async function runFfmpegWithProgress({ ffmpegArgs, duration, onProgress }: {
|
||||
ffmpegArgs: string[], duration: number | undefined, onProgress: (a: number) => void,
|
||||
}) {
|
||||
const process = runFfmpegProcess(ffmpegArgs);
|
||||
assert(process.stderr != null);
|
||||
handleProgress(process, duration, onProgress);
|
||||
return process;
|
||||
}
|
||||
|
||||
async function runFfprobe(args, { timeout = isDev ? 10000 : 30000 } = {}) {
|
||||
export async function runFfprobe(args: readonly string[], { timeout = isDev ? 10000 : 30000 } = {}) {
|
||||
const ffprobePath = getFfprobePath();
|
||||
console.log(getFfCommandLine('ffprobe', args));
|
||||
const ps = execa(ffprobePath, args, getExecaOptions());
|
||||
|
@ -152,13 +168,14 @@ async function runFfprobe(args, { timeout = isDev ? 10000 : 30000 } = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
/** @type {(a: any) => Promise<import('../types').Waveform>} */
|
||||
async function renderWaveformPng({ filePath, start, duration, color, streamIndex }) {
|
||||
export async function renderWaveformPng({ filePath, start, duration, color, streamIndex }: {
|
||||
filePath: string, start: number, duration: number, color: string, streamIndex: number,
|
||||
}): Promise<Waveform> {
|
||||
const args1 = [
|
||||
'-hide_banner',
|
||||
'-i', filePath,
|
||||
'-ss', start,
|
||||
'-t', duration,
|
||||
'-ss', String(start),
|
||||
'-t', String(duration),
|
||||
'-c', 'copy',
|
||||
'-vn',
|
||||
'-map', `0:${streamIndex}`,
|
||||
|
@ -179,16 +196,18 @@ async function renderWaveformPng({ filePath, start, duration, color, streamIndex
|
|||
console.log(getFfCommandLine('ffmpeg1', args1));
|
||||
console.log('|', getFfCommandLine('ffmpeg2', args2));
|
||||
|
||||
let ps1;
|
||||
let ps2;
|
||||
let ps1: ExecaChildProcess<Buffer> | undefined;
|
||||
let ps2: ExecaChildProcess<Buffer> | undefined;
|
||||
try {
|
||||
ps1 = runFfmpegProcess(args1, { encoding: null, buffer: false }, { logCli: false });
|
||||
ps2 = runFfmpegProcess(args2, { encoding: null }, { logCli: false });
|
||||
ps1 = runFfmpegProcess(args1, { buffer: false }, { logCli: false });
|
||||
ps2 = runFfmpegProcess(args2, undefined, { logCli: false });
|
||||
assert(ps1.stdout != null);
|
||||
assert(ps2.stdin != null);
|
||||
ps1.stdout.pipe(ps2.stdin);
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
ps1.kill();
|
||||
ps2.kill();
|
||||
ps1?.kill();
|
||||
ps2?.kill();
|
||||
console.warn('ffmpeg timed out');
|
||||
}, 10000);
|
||||
|
||||
|
@ -209,14 +228,14 @@ async function renderWaveformPng({ filePath, start, duration, color, streamIndex
|
|||
}
|
||||
}
|
||||
|
||||
const getInputSeekArgs = ({ filePath, from, to }) => [
|
||||
const getInputSeekArgs = ({ filePath, from, to }: { filePath: string, from?: number | undefined, to?: number | undefined }) => [
|
||||
...(from != null ? ['-ss', from.toFixed(5)] : []),
|
||||
'-i', filePath,
|
||||
...(to != null ? ['-t', (to - from).toFixed(5)] : []),
|
||||
...(from != null && to != null ? ['-t', (to - from).toFixed(5)] : []),
|
||||
];
|
||||
|
||||
function mapTimesToSegments(times) {
|
||||
const segments = [];
|
||||
export function mapTimesToSegments(times: number[]) {
|
||||
const segments: { start: number, end: number | undefined }[] = [];
|
||||
for (let i = 0; i < times.length; i += 1) {
|
||||
const start = times[i];
|
||||
const end = times[i + 1];
|
||||
|
@ -225,32 +244,35 @@ function mapTimesToSegments(times) {
|
|||
return segments;
|
||||
}
|
||||
|
||||
const getSegmentOffset = (from) => (from != null ? from : 0);
|
||||
const getSegmentOffset = (from?: number) => (from != null ? from : 0);
|
||||
|
||||
function adjustSegmentsWithOffset({ segments, from }) {
|
||||
function adjustSegmentsWithOffset({ segments, from }: { segments: { start: number, end: number | undefined }[], from?: number | undefined }) {
|
||||
const offset = getSegmentOffset(from);
|
||||
return segments.map(({ start, end }) => ({ start: start + offset, end: end != null ? end + offset : end }));
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/35675529/using-ffmpeg-how-to-do-a-scene-change-detection-with-timecode
|
||||
async function detectSceneChanges({ filePath, minChange, onProgress, from, to }) {
|
||||
export async function detectSceneChanges({ filePath, minChange, onProgress, from, to }: {
|
||||
filePath: string, minChange: number | string, onProgress: (p: number) => void, from: number, to: number,
|
||||
}) {
|
||||
const args = [
|
||||
'-hide_banner',
|
||||
...getInputSeekArgs({ filePath, from, to }),
|
||||
'-filter_complex', `select='gt(scene,${minChange})',metadata=print:file=-`,
|
||||
'-f', 'null', '-',
|
||||
];
|
||||
const process = runFfmpegProcess(args, { encoding: null, buffer: false });
|
||||
const process = runFfmpegProcess(args, { buffer: false });
|
||||
|
||||
const times = [0];
|
||||
|
||||
handleProgress(process, to - from, onProgress);
|
||||
assert(process.stdout != null);
|
||||
const rl = readline.createInterface({ input: process.stdout });
|
||||
rl.on('line', (line) => {
|
||||
// eslint-disable-next-line unicorn/better-regex
|
||||
const match = line.match(/^frame:\d+\s+pts:\d+\s+pts_time:([\d.]+)/);
|
||||
if (!match) return;
|
||||
const time = parseFloat(match[1]);
|
||||
const time = parseFloat(match[1]!);
|
||||
// @ts-expect-error todo
|
||||
if (Number.isNaN(time) || time <= times.at(-1)) return;
|
||||
times.push(time);
|
||||
|
@ -263,89 +285,134 @@ async function detectSceneChanges({ filePath, minChange, onProgress, from, to })
|
|||
return adjustSegmentsWithOffset({ segments, from });
|
||||
}
|
||||
|
||||
async function detectIntervals({ filePath, customArgs, onProgress, from, to, matchLineTokens }) {
|
||||
async function detectIntervals({ filePath, customArgs, onProgress, from, to, matchLineTokens }: {
|
||||
filePath: string, customArgs: string[], onProgress: (p: number) => void, from: number, to: number, matchLineTokens: (line: string) => { start?: string | number | undefined, end?: string | number | undefined },
|
||||
}) {
|
||||
const args = [
|
||||
'-hide_banner',
|
||||
...getInputSeekArgs({ filePath, from, to }),
|
||||
...customArgs,
|
||||
'-f', 'null', '-',
|
||||
];
|
||||
const process = runFfmpegProcess(args, { encoding: null, buffer: false });
|
||||
const process = runFfmpegProcess(args, { buffer: false });
|
||||
|
||||
const segments = [];
|
||||
const segments: { start: number, end: number }[] = [];
|
||||
|
||||
function customMatcher(line) {
|
||||
const { start: startStr, end: endStr } = matchLineTokens(line);
|
||||
const start = parseFloat(startStr);
|
||||
const end = parseFloat(endStr);
|
||||
if (start == null || end == null || Number.isNaN(start) || Number.isNaN(end)) return;
|
||||
segments.push({ start, end });
|
||||
function customMatcher(line: string) {
|
||||
const { start: startRaw, end: endRaw } = matchLineTokens(line);
|
||||
if (typeof startRaw === 'number' && typeof endRaw === 'number') {
|
||||
if (startRaw == null || endRaw == null) return;
|
||||
if (Number.isNaN(startRaw) || Number.isNaN(endRaw)) return;
|
||||
segments.push({ start: startRaw, end: endRaw });
|
||||
} else if (typeof startRaw === 'string' && typeof endRaw === 'string') {
|
||||
if (startRaw == null || endRaw == null) return;
|
||||
const start = parseFloat(startRaw);
|
||||
const end = parseFloat(endRaw);
|
||||
if (Number.isNaN(start) || Number.isNaN(end)) return;
|
||||
segments.push({ start, end });
|
||||
} else {
|
||||
throw new TypeError('Invalid line match');
|
||||
}
|
||||
}
|
||||
// @ts-expect-error todo
|
||||
handleProgress(process, to - from, onProgress, customMatcher);
|
||||
|
||||
await process;
|
||||
return adjustSegmentsWithOffset({ segments, from });
|
||||
}
|
||||
|
||||
const mapFilterOptions = (options) => Object.entries(options).map(([key, value]) => `${key}=${value}`).join(':');
|
||||
const mapFilterOptions = (options: Record<string, string>) => Object.entries(options).map(([key, value]) => `${key}=${value}`).join(':');
|
||||
|
||||
async function blackDetect({ filePath, filterOptions, onProgress, from, to }) {
|
||||
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\\.]+/);
|
||||
if (!match) return {};
|
||||
return {
|
||||
start: parseFloat(match[1]),
|
||||
end: parseFloat(match[2]),
|
||||
};
|
||||
}
|
||||
export async function blackDetect({ filePath, filterOptions, onProgress, from, to }: { filePath: string, filterOptions: Record<string, string>, onProgress: (p: number) => void, from: number, to: number }) {
|
||||
const customArgs = ['-vf', `blackdetect=${mapFilterOptions(filterOptions)}`, '-an'];
|
||||
return detectIntervals({ filePath, onProgress, from, to, matchLineTokens, customArgs });
|
||||
return detectIntervals({
|
||||
filePath,
|
||||
onProgress,
|
||||
from,
|
||||
to,
|
||||
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\\.]+/);
|
||||
if (!match) {
|
||||
return {
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
};
|
||||
}
|
||||
return {
|
||||
start: match[1],
|
||||
end: match[2],
|
||||
};
|
||||
},
|
||||
customArgs,
|
||||
});
|
||||
}
|
||||
|
||||
async function silenceDetect({ filePath, filterOptions, onProgress, from, to }) {
|
||||
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\\.]+)/);
|
||||
if (!match) return {};
|
||||
const end = parseFloat(match[1]);
|
||||
const silenceDuration = parseFloat(match[2]);
|
||||
if (Number.isNaN(end) || Number.isNaN(silenceDuration)) return {};
|
||||
const start = end - silenceDuration;
|
||||
if (start < 0 || end <= 0 || start >= end) return {};
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
};
|
||||
}
|
||||
const customArgs = ['-af', `silencedetect=${mapFilterOptions(filterOptions)}`, '-vn'];
|
||||
return detectIntervals({ filePath, onProgress, from, to, matchLineTokens, customArgs });
|
||||
export async function silenceDetect({ filePath, filterOptions, onProgress, from, to }: {
|
||||
filePath: string, filterOptions: Record<string, string>, onProgress: (p: number) => void, from: number, to: number,
|
||||
}) {
|
||||
return detectIntervals({
|
||||
filePath,
|
||||
onProgress,
|
||||
from,
|
||||
to,
|
||||
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\\.]+)/);
|
||||
if (!match) {
|
||||
return {
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
};
|
||||
}
|
||||
const end = parseFloat(match[1]!);
|
||||
const silenceDuration = parseFloat(match[2]!);
|
||||
if (Number.isNaN(end) || Number.isNaN(silenceDuration)) {
|
||||
return {
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
};
|
||||
}
|
||||
const start = end - silenceDuration;
|
||||
if (start < 0 || end <= 0 || start >= end) {
|
||||
return {
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
};
|
||||
}
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
};
|
||||
},
|
||||
customArgs: ['-af', `silencedetect=${mapFilterOptions(filterOptions)}`, '-vn'],
|
||||
});
|
||||
}
|
||||
|
||||
function getFffmpegJpegQuality(quality) {
|
||||
function getFffmpegJpegQuality(quality: number) {
|
||||
// Normal range for JPEG is 2-31 with 31 being the worst quality.
|
||||
const qMin = 2;
|
||||
const qMax = 31;
|
||||
return Math.min(Math.max(qMin, quality, Math.round((1 - quality) * (qMax - qMin) + qMin)), qMax);
|
||||
}
|
||||
|
||||
function getQualityOpts({ captureFormat, quality }) {
|
||||
if (captureFormat === 'jpeg') return ['-q:v', getFffmpegJpegQuality(quality)];
|
||||
if (captureFormat === 'webp') return ['-q:v', Math.max(0, Math.min(100, Math.round(quality * 100)))];
|
||||
function getQualityOpts({ captureFormat, quality }: { captureFormat: CaptureFormat, quality: number }) {
|
||||
if (captureFormat === 'jpeg') return ['-q:v', String(getFffmpegJpegQuality(quality))];
|
||||
if (captureFormat === 'webp') return ['-q:v', String(Math.max(0, Math.min(100, Math.round(quality * 100))))];
|
||||
return [];
|
||||
}
|
||||
|
||||
function getCodecOpts(captureFormat) {
|
||||
function getCodecOpts(captureFormat: CaptureFormat) {
|
||||
if (captureFormat === 'webp') return ['-c:v', 'libwebp']; // else we get only a single file for webp https://github.com/mifi/lossless-cut/issues/1693
|
||||
return [];
|
||||
}
|
||||
|
||||
async function captureFrames({ from, to, videoPath, outPathTemplate, quality, filter, framePts, onProgress, captureFormat }) {
|
||||
export async function captureFrames({ from, to, videoPath, outPathTemplate, quality, filter, framePts, onProgress, captureFormat }: {
|
||||
from: number, to: number, videoPath: string, outPathTemplate: string, quality: number, filter?: string | undefined, framePts?: boolean | undefined, onProgress: (p: number) => void, captureFormat: CaptureFormat,
|
||||
}) {
|
||||
const args = [
|
||||
'-ss', from,
|
||||
'-ss', String(from),
|
||||
'-i', videoPath,
|
||||
'-t', Math.max(0, to - from),
|
||||
'-t', String(Math.max(0, to - from)),
|
||||
...getQualityOpts({ captureFormat, quality }),
|
||||
...(filter != null ? ['-vf', filter] : []),
|
||||
// https://superuser.com/questions/1336285/use-ffmpeg-for-thumbnail-selections
|
||||
|
@ -356,39 +423,43 @@ async function captureFrames({ from, to, videoPath, outPathTemplate, quality, fi
|
|||
'-y', outPathTemplate,
|
||||
];
|
||||
|
||||
const process = runFfmpegProcess(args, { encoding: null, buffer: false });
|
||||
const process = runFfmpegProcess(args, { buffer: false });
|
||||
|
||||
handleProgress(process, to - from, onProgress);
|
||||
|
||||
await process;
|
||||
}
|
||||
|
||||
async function captureFrame({ timestamp, videoPath, outPath, quality }) {
|
||||
export async function captureFrame({ timestamp, videoPath, outPath, quality }: {
|
||||
timestamp: number, videoPath: string, outPath: string, quality: number,
|
||||
}) {
|
||||
const ffmpegQuality = getFffmpegJpegQuality(quality);
|
||||
await runFfmpegProcess([
|
||||
'-ss', timestamp,
|
||||
'-ss', String(timestamp),
|
||||
'-i', videoPath,
|
||||
'-vframes', '1',
|
||||
'-q:v', ffmpegQuality,
|
||||
'-q:v', String(ffmpegQuality),
|
||||
'-y', outPath,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
async function readFormatData(filePath) {
|
||||
async function readFormatData(filePath: string) {
|
||||
console.log('readFormatData', filePath);
|
||||
|
||||
const { stdout } = await runFfprobe([
|
||||
'-of', 'json', '-show_format', '-i', filePath, '-hide_banner',
|
||||
]);
|
||||
return JSON.parse(stdout).format;
|
||||
return JSON.parse(stdout as unknown as string).format;
|
||||
}
|
||||
|
||||
async function getDuration(filePath) {
|
||||
export async function getDuration(filePath: string) {
|
||||
return parseFloat((await readFormatData(filePath)).duration);
|
||||
}
|
||||
|
||||
async function html5ify({ outPath, filePath: filePathArg, speed, hasAudio, hasVideo, onProgress }) {
|
||||
export async function html5ify({ outPath, filePath: filePathArg, speed, hasAudio, hasVideo, onProgress }: {
|
||||
outPath: string, filePath: string, speed: Html5ifyMode, hasAudio: boolean, hasVideo: boolean, onProgress: (p: number) => void,
|
||||
}) {
|
||||
let audio;
|
||||
if (hasAudio) {
|
||||
if (speed === 'slowest') audio = 'hq';
|
||||
|
@ -492,14 +563,14 @@ async function html5ify({ outPath, filePath: filePathArg, speed, hasAudio, hasVi
|
|||
if (duration) handleProgress(process, duration, onProgress);
|
||||
|
||||
const { stdout } = await process;
|
||||
console.log(stdout);
|
||||
console.log(stdout.toString('utf8'));
|
||||
}
|
||||
|
||||
function readOneJpegFrame({ path, seekTo, videoStreamIndex }) {
|
||||
export function readOneJpegFrame({ path, seekTo, videoStreamIndex }: { path: string, seekTo: number, videoStreamIndex: number }) {
|
||||
const args = [
|
||||
'-hide_banner', '-loglevel', 'error',
|
||||
|
||||
'-ss', seekTo,
|
||||
'-ss', String(seekTo),
|
||||
|
||||
'-noautorotate',
|
||||
|
||||
|
@ -516,17 +587,19 @@ function readOneJpegFrame({ path, seekTo, videoStreamIndex }) {
|
|||
|
||||
// console.log(args);
|
||||
|
||||
return runFfmpegProcess(args, { encoding: 'buffer' }, { logCli: true });
|
||||
return runFfmpegProcess(args, undefined, { logCli: true });
|
||||
}
|
||||
|
||||
const enableLog = false;
|
||||
const encode = true;
|
||||
|
||||
function createMediaSourceProcess({ path, videoStreamIndex, audioStreamIndex, seekTo, size, fps }) {
|
||||
export function createMediaSourceProcess({ path, videoStreamIndex, audioStreamIndex, seekTo, size, fps }: {
|
||||
path: string, videoStreamIndex?: number | undefined, audioStreamIndex?: number | undefined, seekTo: number, size?: number | undefined, fps?: number | undefined,
|
||||
}) {
|
||||
function getVideoFilters() {
|
||||
if (videoStreamIndex == null) return [];
|
||||
|
||||
const filters = [];
|
||||
const filters: string[] = [];
|
||||
if (fps != null) filters.push(`fps=${fps}`);
|
||||
if (size != null) filters.push(`scale=${size}:${size}:flags=lanczos:force_original_aspect_ratio=decrease`);
|
||||
if (filters.length === 0) return [];
|
||||
|
@ -547,7 +620,7 @@ function createMediaSourceProcess({ path, videoStreamIndex, audioStreamIndex, se
|
|||
|
||||
'-vsync', 'passthrough',
|
||||
|
||||
'-ss', seekTo,
|
||||
'-ss', String(seekTo),
|
||||
|
||||
'-noautorotate',
|
||||
|
||||
|
@ -582,27 +655,5 @@ function createMediaSourceProcess({ path, videoStreamIndex, audioStreamIndex, se
|
|||
return execa(getFfmpegPath(), args, { encoding: null, buffer: false, stderr: enableLog ? 'inherit' : 'pipe' });
|
||||
}
|
||||
|
||||
// Don't pass complex objects over the bridge
|
||||
const runFfmpeg = async (...args) => runFfmpegProcess(...args);
|
||||
|
||||
module.exports = {
|
||||
setCustomFfPath,
|
||||
abortFfmpegs,
|
||||
getFfmpegPath,
|
||||
runFfprobe,
|
||||
runFfmpeg,
|
||||
runFfmpegConcat,
|
||||
runFfmpegWithProgress,
|
||||
renderWaveformPng,
|
||||
mapTimesToSegments,
|
||||
detectSceneChanges,
|
||||
captureFrames,
|
||||
captureFrame,
|
||||
getFfCommandLine,
|
||||
html5ify,
|
||||
getDuration,
|
||||
readOneJpegFrame,
|
||||
blackDetect,
|
||||
silenceDetect,
|
||||
createMediaSourceProcess,
|
||||
};
|
||||
// Don't pass complex objects over the bridge (process)
|
||||
export const runFfmpeg = async (...args: Parameters<typeof runFfmpegProcess>) => runFfmpegProcess(...args);
|
|
@ -1,14 +1,16 @@
|
|||
const express = require('express');
|
||||
const morgan = require('morgan');
|
||||
const http = require('http');
|
||||
const asyncHandler = require('express-async-handler');
|
||||
const { homepage } = require('./constants');
|
||||
import express from 'express';
|
||||
import morgan from 'morgan';
|
||||
import http from 'http';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import assert from 'assert';
|
||||
|
||||
import { homepage } from './constants.js';
|
||||
import logger from './logger.js';
|
||||
|
||||
|
||||
const logger = require('./logger');
|
||||
|
||||
|
||||
module.exports = ({ port, onKeyboardAction }) => {
|
||||
export default ({ port, onKeyboardAction }: {
|
||||
port: number, onKeyboardAction: (a: string) => Promise<void>,
|
||||
}) => {
|
||||
const app = express();
|
||||
|
||||
// https://expressjs.com/en/resources/middleware/morgan.html
|
||||
|
@ -25,7 +27,10 @@ module.exports = ({ port, onKeyboardAction }) => {
|
|||
app.use('/api', apiRouter);
|
||||
|
||||
apiRouter.post('/shortcuts/:action', express.json(), asyncHandler(async (req, res) => {
|
||||
await onKeyboardAction(req.params.action);
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
const action = req.params['action'];
|
||||
assert(action != null);
|
||||
await onKeyboardAction(action);
|
||||
res.end();
|
||||
}));
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
const i18n = require('i18next');
|
||||
const Backend = require('i18next-fs-backend');
|
||||
import i18n from 'i18next';
|
||||
import Backend from 'i18next-fs-backend';
|
||||
|
||||
const { commonI18nOptions, loadPath, addPath } = require('./i18n-common');
|
||||
import { commonI18nOptions, loadPath, addPath } from './i18nCommon.js';
|
||||
|
||||
// See also renderer
|
||||
|
||||
// https://github.com/i18next/i18next/issues/869
|
||||
i18n
|
||||
export default i18n
|
||||
.use(Backend)
|
||||
// detect user language
|
||||
// learn more: https://github.com/i18next/i18next-browser-languageDetector
|
||||
|
@ -23,5 +23,3 @@ i18n
|
|||
addPath,
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = i18n;
|
|
@ -1,28 +1,26 @@
|
|||
// eslint-disable-line unicorn/filename-case
|
||||
// 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 isDev = require('electron-is-dev');
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const { app } = require('electron');
|
||||
const { join } = require('path');
|
||||
import { app } from 'electron';
|
||||
import { join } from 'path';
|
||||
import { InitOptions } from 'i18next';
|
||||
|
||||
const { frontendBuildDir } = require('./util');
|
||||
|
||||
let customLocalesPath;
|
||||
function setCustomLocalesPath(p) {
|
||||
let customLocalesPath: string | undefined;
|
||||
export function setCustomLocalesPath(p: string) {
|
||||
customLocalesPath = p;
|
||||
}
|
||||
|
||||
function getLangPath(subPath) {
|
||||
function getLangPath(subPath: string) {
|
||||
if (customLocalesPath != null) return join(customLocalesPath, subPath);
|
||||
if (isDev) return join('public', subPath);
|
||||
return join(app.getAppPath(), frontendBuildDir, subPath);
|
||||
if (app.isPackaged) return join(process.resourcesPath, 'locales', subPath);
|
||||
return join('locales', subPath);
|
||||
}
|
||||
|
||||
// Weblate hardcodes different lang codes than electron
|
||||
// https://www.electronjs.org/docs/api/app#appgetlocale
|
||||
// https://source.chromium.org/chromium/chromium/src/+/master:ui/base/l10n/l10n_util.cc
|
||||
const mapLang = (lng) => ({
|
||||
const mapLang = (lng: string) => ({
|
||||
nb: 'nb_NO',
|
||||
no: 'nb_NO',
|
||||
zh: 'zh_Hans',
|
||||
|
@ -39,29 +37,21 @@ const mapLang = (lng) => ({
|
|||
'ru-RU': 'ru',
|
||||
}[lng] || lng);
|
||||
|
||||
const fallbackLng = 'en';
|
||||
export const fallbackLng = 'en';
|
||||
|
||||
const commonI18nOptions = {
|
||||
export const commonI18nOptions: InitOptions = {
|
||||
fallbackLng,
|
||||
// debug: isDev,
|
||||
// saveMissing: isDev,
|
||||
// updateMissing: isDev,
|
||||
// saveMissingTo: 'all',
|
||||
|
||||
// Keep in sync between i18next-parser.config.js and i18n-common.js:
|
||||
// Keep in sync between i18next-parser.config.js and i18nCommon.js:
|
||||
// TODO improve keys?
|
||||
// Maybe do something like this: https://stackoverflow.com/a/19405314/6519037
|
||||
keySeparator: false,
|
||||
nsSeparator: false,
|
||||
};
|
||||
|
||||
const loadPath = (lng, ns) => getLangPath(`locales/${mapLang(lng)}/${ns}.json`);
|
||||
const addPath = (lng, ns) => getLangPath(`locales/${mapLang(lng)}/${ns}.missing.json`);
|
||||
|
||||
module.exports = {
|
||||
fallbackLng,
|
||||
loadPath,
|
||||
addPath,
|
||||
commonI18nOptions,
|
||||
setCustomLocalesPath,
|
||||
};
|
||||
export const loadPath = (lng: string, ns: string) => getLangPath(`${mapLang(lng)}/${ns}.json`);
|
||||
export const addPath = (lng: string, ns: string) => getLangPath(`${mapLang(lng)}/${ns}.missing.json`);
|
|
@ -1,31 +1,55 @@
|
|||
/// <reference types="electron-vite/node" />
|
||||
process.traceDeprecation = true;
|
||||
process.traceProcessWarnings = true;
|
||||
|
||||
/* eslint-disable import/first */
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const electron = require('electron');
|
||||
const isDev = require('electron-is-dev');
|
||||
const unhandled = require('electron-unhandled');
|
||||
const i18n = require('i18next');
|
||||
const debounce = require('lodash/debounce');
|
||||
const yargsParser = require('yargs-parser');
|
||||
const JSON5 = require('json5');
|
||||
const remote = require('@electron/remote/main');
|
||||
const { stat } = require('fs/promises');
|
||||
import electron, { AboutPanelOptionsOptions, BrowserWindow, BrowserWindowConstructorOptions, nativeTheme, shell, app, ipcMain } from 'electron';
|
||||
import unhandled from 'electron-unhandled';
|
||||
import i18n from 'i18next';
|
||||
import debounce from 'lodash/debounce';
|
||||
import yargsParser from 'yargs-parser';
|
||||
import JSON5 from 'json5';
|
||||
import remote from '@electron/remote/main';
|
||||
import { stat } from 'fs/promises';
|
||||
import assert from 'assert';
|
||||
|
||||
const logger = require('./logger');
|
||||
const menu = require('./menu');
|
||||
const configStore = require('./configStore');
|
||||
const { frontendBuildDir, isLinux } = require('./util');
|
||||
const attachContextMenu = require('./contextMenu');
|
||||
const HttpServer = require('./httpServer');
|
||||
import logger from './logger.js';
|
||||
import menu from './menu.js';
|
||||
import * as configStore from './configStore.js';
|
||||
import { isLinux } from './util.js';
|
||||
import attachContextMenu from './contextMenu.js';
|
||||
import HttpServer from './httpServer.js';
|
||||
import isDev from './isDev.js';
|
||||
|
||||
const { checkNewVersion } = require('./update-checker');
|
||||
import { checkNewVersion } from './updateChecker.js';
|
||||
|
||||
const i18nCommon = require('./i18n-common');
|
||||
import * as i18nCommon from './i18nCommon.js';
|
||||
|
||||
require('./i18n');
|
||||
import './i18n.js';
|
||||
import { ApiKeyboardActionRequest } from '../../types.js';
|
||||
|
||||
const { app, ipcMain, shell, BrowserWindow, nativeTheme } = electron;
|
||||
export * as ffmpeg from './ffmpeg.js';
|
||||
|
||||
export * as i18n from './i18nCommon.js';
|
||||
|
||||
export * as compatPlayer from './compatPlayer.js';
|
||||
|
||||
export * as configStore from './configStore.js';
|
||||
|
||||
export { isLinux, isWindows, isMac, platform } from './util.js';
|
||||
|
||||
|
||||
// https://www.i18next.com/overview/typescript#argument-of-type-defaulttfuncreturn-is-not-assignable-to-parameter-of-type-xyz
|
||||
// todo This should not be necessary anymore since v23.0.0
|
||||
declare module 'i18next' {
|
||||
interface CustomTypeOptions {
|
||||
returnNull: false;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unicorn/prefer-export-from
|
||||
export { isDev };
|
||||
|
||||
// https://chromestatus.com/feature/5748496434987008
|
||||
// https://peter.sh/experiments/chromium-command-line-switches/
|
||||
|
@ -49,8 +73,7 @@ const isStoreBuild = process.windowsStore || process.mas;
|
|||
|
||||
const showVersion = !isStoreBuild;
|
||||
|
||||
/** @type import('electron').AboutPanelOptionsOptions */
|
||||
const aboutPanelOptions = {
|
||||
const aboutPanelOptions: AboutPanelOptionsOptions = {
|
||||
applicationName: appName,
|
||||
copyright: `Copyright © ${copyrightYear} Mikael Finstad ❤️ 🇳🇴`,
|
||||
version: '', // not very useful (MacOS only, and same as applicationVersion)
|
||||
|
@ -69,28 +92,28 @@ if (!showVersion) {
|
|||
// https://www.electronjs.org/docs/latest/api/app#appsetaboutpaneloptionsoptions
|
||||
app.setAboutPanelOptions(aboutPanelOptions);
|
||||
|
||||
let filesToOpen = [];
|
||||
let filesToOpen: string[] = [];
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
let mainWindow;
|
||||
let mainWindow: BrowserWindow | null;
|
||||
|
||||
let askBeforeClose = false;
|
||||
let rendererReady = false;
|
||||
let newVersion;
|
||||
let disableNetworking;
|
||||
let newVersion: string | undefined;
|
||||
let disableNetworking: boolean;
|
||||
|
||||
const openFiles = (paths) => mainWindow.webContents.send('openFiles', paths);
|
||||
const openFiles = (paths: string[]) => mainWindow!.webContents.send('openFiles', paths);
|
||||
|
||||
let apiKeyboardActionRequestsId = 0;
|
||||
const apiKeyboardActionRequests = new Map();
|
||||
const apiKeyboardActionRequests = new Map<number, () => void>();
|
||||
|
||||
async function sendApiKeyboardAction(action) {
|
||||
async function sendApiKeyboardAction(action: string) {
|
||||
try {
|
||||
const id = apiKeyboardActionRequestsId;
|
||||
apiKeyboardActionRequestsId += 1;
|
||||
mainWindow.webContents.send('apiKeyboardAction', { id, action });
|
||||
await new Promise((resolve) => {
|
||||
mainWindow!.webContents.send('apiKeyboardAction', { id, action } satisfies ApiKeyboardActionRequest);
|
||||
await new Promise<void>((resolve) => {
|
||||
apiKeyboardActionRequests.set(id, resolve);
|
||||
});
|
||||
} catch (err) {
|
||||
|
@ -101,7 +124,7 @@ async function sendApiKeyboardAction(action) {
|
|||
// https://github.com/electron/electron/issues/526#issuecomment-563010533
|
||||
function getSizeOptions() {
|
||||
const bounds = configStore.get('windowBounds');
|
||||
const options = {};
|
||||
const options: BrowserWindowConstructorOptions = {};
|
||||
if (bounds) {
|
||||
const area = electron.screen.getDisplayMatching(bounds).workArea;
|
||||
// If the saved position still valid (the window is entirely inside the display area), use it.
|
||||
|
@ -147,7 +170,7 @@ function createWindow() {
|
|||
|
||||
if (isDev) mainWindow.loadURL('http://localhost:3001');
|
||||
// Need to useloadFile for special characters https://github.com/mifi/lossless-cut/issues/40
|
||||
else mainWindow.loadFile(`${frontendBuildDir}/index.html`);
|
||||
else mainWindow.loadFile('out/renderer/index.html');
|
||||
|
||||
// Open the DevTools.
|
||||
// mainWindow.webContents.openDevTools()
|
||||
|
@ -164,6 +187,7 @@ function createWindow() {
|
|||
mainWindow.on('close', (e) => {
|
||||
if (!askBeforeClose) return;
|
||||
|
||||
assert(mainWindow);
|
||||
const choice = electron.dialog.showMessageBoxSync(mainWindow, {
|
||||
type: 'question',
|
||||
buttons: ['Yes', 'No'],
|
||||
|
@ -186,10 +210,11 @@ function createWindow() {
|
|||
}
|
||||
|
||||
function updateMenu() {
|
||||
assert(mainWindow);
|
||||
menu({ app, mainWindow, newVersion, isStoreBuild });
|
||||
}
|
||||
|
||||
function openFilesEventually(paths) {
|
||||
function openFilesEventually(paths: string[]) {
|
||||
if (rendererReady) openFiles(paths);
|
||||
else filesToOpen = paths;
|
||||
}
|
||||
|
@ -201,18 +226,18 @@ function openFilesEventually(paths) {
|
|||
function parseCliArgs(rawArgv = process.argv) {
|
||||
const ignoreFirstArgs = process.defaultApp ? 2 : 1;
|
||||
// production: First arg is the LosslessCut executable
|
||||
// dev: First 2 args are electron and the electron.js
|
||||
// dev: First 2 args are electron and the index.js
|
||||
const argsWithoutAppName = rawArgv.length > ignoreFirstArgs ? rawArgv.slice(ignoreFirstArgs) : [];
|
||||
|
||||
return yargsParser(argsWithoutAppName, { boolean: ['allow-multiple-instances', 'disable-networking'] });
|
||||
return yargsParser(argsWithoutAppName, { boolean: ['allow-multiple-instances', 'disable-networking'], string: ['settings-json'] });
|
||||
}
|
||||
|
||||
const argv = parseCliArgs();
|
||||
|
||||
if (argv.localesPath != null) i18nCommon.setCustomLocalesPath(argv.localesPath);
|
||||
if (argv['localesPath'] != null) i18nCommon.setCustomLocalesPath(argv['localesPath']);
|
||||
|
||||
|
||||
function safeRequestSingleInstanceLock(additionalData) {
|
||||
function safeRequestSingleInstanceLock(additionalData: Record<string, unknown>) {
|
||||
if (process.mas) return true; // todo remove when fixed https://github.com/electron/electron/issues/35540
|
||||
|
||||
// using additionalData because the built in "argv" passing is a bit broken:
|
||||
|
@ -232,16 +257,14 @@ function initApp() {
|
|||
mainWindow.focus();
|
||||
}
|
||||
|
||||
// @ts-expect-error todo
|
||||
if (!Array.isArray(additionalData?.argv)) return;
|
||||
if (!(additionalData != null && typeof additionalData === 'object' && 'argv' in additionalData) || !Array.isArray(additionalData.argv)) return;
|
||||
|
||||
// @ts-expect-error todo
|
||||
const argv2 = parseCliArgs(additionalData.argv);
|
||||
|
||||
logger.info('second-instance', argv2);
|
||||
|
||||
if (argv2._ && argv2._.length > 0) openFilesEventually(argv2._);
|
||||
else if (argv2.keyboardAction) sendApiKeyboardAction(argv2.keyboardAction);
|
||||
if (argv2._ && argv2._.length > 0) openFilesEventually(argv2._.map(String));
|
||||
else if (argv2['keyboardAction']) sendApiKeyboardAction(argv2['keyboardAction']);
|
||||
});
|
||||
|
||||
// Quit when all windows are closed.
|
||||
|
@ -322,7 +345,7 @@ const readyPromise = app.whenReady();
|
|||
|
||||
logger.info('CLI arguments', argv);
|
||||
// Only if no files to open already (open-file might have already added some files)
|
||||
if (filesToOpen.length === 0) filesToOpen = argv._;
|
||||
if (filesToOpen.length === 0) filesToOpen = argv._.map(String);
|
||||
const { settingsJson } = argv;
|
||||
|
||||
({ disableNetworking } = argv);
|
||||
|
@ -330,6 +353,7 @@ const readyPromise = app.whenReady();
|
|||
if (settingsJson != null) {
|
||||
logger.info('initializing settings', settingsJson);
|
||||
Object.entries(JSON5.parse(settingsJson)).forEach(([key, value]) => {
|
||||
// @ts-expect-error todo use zod?
|
||||
configStore.set(key, value);
|
||||
});
|
||||
}
|
||||
|
@ -340,6 +364,7 @@ const readyPromise = app.whenReady();
|
|||
const port = typeof httpApi === 'number' ? httpApi : 8080;
|
||||
const { startHttpServer } = HttpServer({ port, onKeyboardAction: sendApiKeyboardAction });
|
||||
await startHttpServer();
|
||||
logger.info('HTTP API listening on port', port);
|
||||
}
|
||||
|
||||
|
||||
|
@ -347,8 +372,8 @@ const readyPromise = app.whenReady();
|
|||
const { default: installExtension, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer'); // eslint-disable-line global-require,import/no-extraneous-dependencies
|
||||
|
||||
installExtension(REACT_DEVELOPER_TOOLS)
|
||||
.then((name) => logger.info('Added Extension', name))
|
||||
.catch((err) => logger.error('Failed to add extension', err));
|
||||
.then((name: string) => logger.info('Added Extension', name))
|
||||
.catch((err: unknown) => logger.error('Failed to add extension', err));
|
||||
}
|
||||
|
||||
createWindow();
|
||||
|
@ -366,7 +391,7 @@ const readyPromise = app.whenReady();
|
|||
}
|
||||
})();
|
||||
|
||||
function focusWindow() {
|
||||
export function focusWindow() {
|
||||
try {
|
||||
app.focus({ steal: true });
|
||||
} catch (err) {
|
||||
|
@ -374,10 +399,8 @@ function focusWindow() {
|
|||
}
|
||||
}
|
||||
|
||||
function quitApp() {
|
||||
export function quitApp() {
|
||||
electron.app.quit();
|
||||
}
|
||||
|
||||
const hasDisabledNetworking = () => !!disableNetworking;
|
||||
|
||||
module.exports = { focusWindow, isDev, hasDisabledNetworking, quitApp };
|
||||
export const hasDisabledNetworking = () => !!disableNetworking;
|
|
@ -0,0 +1,2 @@
|
|||
const isDev = import.meta.env.MODE === 'development';
|
||||
export default isDev;
|
|
@ -1,15 +1,18 @@
|
|||
const winston = require('winston');
|
||||
const util = require('util');
|
||||
const isDev = require('electron-is-dev');
|
||||
import winston from 'winston';
|
||||
import util from 'util';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const { app } = require('electron');
|
||||
const { join } = require('path');
|
||||
import { app } from 'electron';
|
||||
import { join } from 'path';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import type { TransformableInfo } from 'logform';
|
||||
|
||||
|
||||
// https://mifi.no/blog/winston-electron-logger/
|
||||
|
||||
// https://github.com/winstonjs/winston/issues/1427
|
||||
const combineMessageAndSplat = () => ({
|
||||
transform(info) {
|
||||
transform(info: TransformableInfo) {
|
||||
// @ts-expect-error todo
|
||||
const { [Symbol.for('splat')]: args = [], message } = info;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
info.message = util.format(message, ...args);
|
||||
|
@ -21,14 +24,14 @@ const createLogger = () => winston.createLogger({
|
|||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
combineMessageAndSplat(),
|
||||
winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`),
|
||||
winston.format.printf((info) => `${info['timestamp']} ${info.level}: ${info.message}`),
|
||||
),
|
||||
});
|
||||
|
||||
const logDirPath = isDev ? '.' : app.getPath('userData');
|
||||
const logDirPath = app.isPackaged ? app.getPath('userData') : '.';
|
||||
|
||||
const logger = createLogger();
|
||||
logger.add(new winston.transports.File({ level: 'debug', filename: join(logDirPath, 'app.log'), options: { flags: 'a' } }));
|
||||
if (isDev) logger.add(new winston.transports.Console());
|
||||
if (!app.isPackaged) logger.add(new winston.transports.Console());
|
||||
|
||||
module.exports = logger;
|
||||
export default logger;
|
|
@ -1,16 +1,19 @@
|
|||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const electron = require('electron');
|
||||
const { t } = require('i18next');
|
||||
import electron, { BrowserWindow } from 'electron';
|
||||
import { t } from 'i18next';
|
||||
|
||||
import { homepage, getReleaseUrl, licensesPage } from './constants.js';
|
||||
|
||||
|
||||
// menu-safe i18n.t:
|
||||
// https://github.com/mifi/lossless-cut/issues/1456
|
||||
const esc = (val) => val.replaceAll('&', '&&');
|
||||
const esc = (val: string) => val.replaceAll('&', '&&');
|
||||
|
||||
const { Menu } = electron;
|
||||
|
||||
const { homepage, getReleaseUrl, licensesPage } = require('./constants');
|
||||
|
||||
module.exports = ({ app, mainWindow, newVersion, isStoreBuild }) => {
|
||||
export default ({ app, mainWindow, newVersion, isStoreBuild }: {
|
||||
app: Electron.App, mainWindow: BrowserWindow, newVersion?: string | undefined, isStoreBuild: boolean,
|
||||
}) => {
|
||||
const menu = [
|
||||
...(process.platform === 'darwin' ? [{ role: 'appMenu' }] : []),
|
||||
|
|
@ -1,24 +1,33 @@
|
|||
// eslint-disable-line unicorn/filename-case
|
||||
const GitHub = require('github-api');
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const electron = require('electron');
|
||||
const semver = require('semver');
|
||||
import electron from 'electron';
|
||||
import semver from 'semver';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { Octokit } from '@octokit/core';
|
||||
|
||||
const logger = require('./logger');
|
||||
import logger from './logger.js';
|
||||
|
||||
|
||||
const { app } = electron;
|
||||
|
||||
const gh = new GitHub();
|
||||
const repo = gh.getRepo('mifi', 'lossless-cut');
|
||||
const octokit = new Octokit();
|
||||
|
||||
async function checkNewVersion() {
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export async function checkNewVersion() {
|
||||
try {
|
||||
// From API: https://developer.github.com/v3/repos/releases/#get-the-latest-release
|
||||
// View the latest published full release for the repository.
|
||||
// Draft releases and prereleases are not returned by this endpoint.
|
||||
const res = (await repo.getRelease('latest')).data;
|
||||
const newestVersion = res.tag_name.replace(/^v?/, '');
|
||||
|
||||
const { data } = await octokit.request('GET /repos/{owner}/{repo}/releases/latest', {
|
||||
owner: 'mifi',
|
||||
repo: 'lossless-cut',
|
||||
headers: {
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
},
|
||||
});
|
||||
|
||||
const newestVersion = data.tag_name.replace(/^v?/, '');
|
||||
|
||||
const currentVersion = app.getVersion();
|
||||
// const currentVersion = '3.17.2';
|
||||
|
@ -34,5 +43,3 @@ async function checkNewVersion() {
|
|||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { checkNewVersion };
|
|
@ -0,0 +1,8 @@
|
|||
import os from 'os';
|
||||
|
||||
export const platform = os.platform();
|
||||
export const arch = os.arch();
|
||||
|
||||
export const isWindows = platform === 'win32';
|
||||
export const isMac = platform === 'darwin';
|
||||
export const isLinux = platform === 'linux';
|
|
@ -0,0 +1,2 @@
|
|||
// todo
|
||||
console.log('preload');
|
|
@ -86,9 +86,9 @@ import { rightBarWidth, leftBarWidth, ffmpegExtractWindow, zoomMax } from './uti
|
|||
import BigWaveform from './components/BigWaveform';
|
||||
|
||||
import isDev from './isDev';
|
||||
import { ChromiumHTMLVideoElement, EdlFileType, FfmpegCommandLog, FormatTimecode, Html5ifyMode, PlaybackMode, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, Thumbnail, TunerType } from './types';
|
||||
import { CaptureFormat, KeyboardAction } from '../types';
|
||||
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../ffprobe';
|
||||
import { ChromiumHTMLVideoElement, EdlFileType, FfmpegCommandLog, FormatTimecode, PlaybackMode, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, Thumbnail, TunerType } from './types';
|
||||
import { CaptureFormat, KeyboardAction, Html5ifyMode } from '../../../types';
|
||||
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../ffprobe';
|
||||
|
||||
const electron = window.require('electron');
|
||||
const { exists } = window.require('fs-extra');
|
||||
|
@ -96,8 +96,7 @@ const { lstat } = window.require('fs/promises');
|
|||
const filePathToUrl = window.require('file-url');
|
||||
const { parse: parsePath, join: pathJoin, basename, dirname } = window.require('path');
|
||||
|
||||
const remote = window.require('@electron/remote');
|
||||
const { focusWindow, hasDisabledNetworking, quitApp } = remote.require('./electron');
|
||||
const { focusWindow, hasDisabledNetworking, quitApp } = window.require('@electron/remote').require('./index.js');
|
||||
|
||||
|
||||
const videoStyle: CSSProperties = { width: '100%', height: '100%', objectFit: 'contain' };
|
||||
|
@ -1338,8 +1337,10 @@ function App() {
|
|||
return;
|
||||
}
|
||||
|
||||
if ('stdout' in err) console.error('stdout:', err.stdout);
|
||||
if ('stderr' in err) console.error('stderr:', err.stderr);
|
||||
// @ts-expect-error todo
|
||||
if ('stdout' in err && err.stdout != null) console.error('stdout:', err.stdout.toString('utf8'));
|
||||
// @ts-expect-error todo
|
||||
if ('stderr' in err && err.stderr != null) console.error('stderr:', err.stderr.toString('utf8'));
|
||||
|
||||
if (isExecaFailure(err)) {
|
||||
if (isOutOfSpaceError(err)) {
|
|
@ -4,10 +4,9 @@ import { useDebounce } from 'use-debounce';
|
|||
|
||||
import isDev from './isDev';
|
||||
import { ChromiumHTMLVideoElement } from './types';
|
||||
import { FFprobeStream } from '../ffprobe';
|
||||
import { FFprobeStream } from '../../../ffprobe';
|
||||
|
||||
const remote = window.require('@electron/remote');
|
||||
const { createMediaSourceStream, readOneJpegFrame } = remote.require('./compatPlayer');
|
||||
const { compatPlayer: { createMediaSourceStream, readOneJpegFrame } } = window.require('@electron/remote').require('./index.js');
|
||||
|
||||
|
||||
async function startPlayback({ path, video, videoStreamIndex, audioStreamIndex, seekTo, signal, playSafe, onCanPlay, getTargetTime, size, fps }: {
|
||||
|
@ -231,7 +230,7 @@ async function startPlayback({ path, video, videoStreamIndex, audioStreamIndex,
|
|||
processChunk();
|
||||
}
|
||||
|
||||
function drawJpegFrame(canvas: HTMLCanvasElement, jpegImage: Buffer) {
|
||||
function drawJpegFrame(canvas: HTMLCanvasElement | null, jpegImage: Buffer) {
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
@ -248,7 +247,9 @@ function drawJpegFrame(canvas: HTMLCanvasElement, jpegImage: Buffer) {
|
|||
img.src = `data:image/jpeg;base64,${jpegImage.toString('base64')}`;
|
||||
}
|
||||
|
||||
async function createPauseImage({ path, seekTo, videoStreamIndex, canvas, signal }) {
|
||||
async function createPauseImage({ path, seekTo, videoStreamIndex, canvas, signal }: {
|
||||
path: string, seekTo: number, videoStreamIndex: number, canvas: HTMLCanvasElement | null, signal: AbortSignal,
|
||||
}) {
|
||||
const { promise, abort } = readOneJpegFrame({ path, seekTo, videoStreamIndex });
|
||||
signal.addEventListener('abort', () => abort());
|
||||
const jpegImage = await promise;
|
|
@ -14,7 +14,7 @@ import OutputFormatSelect from './OutputFormatSelect';
|
|||
import useUserSettings from '../hooks/useUserSettings';
|
||||
import { isMov } from '../util/streams';
|
||||
import { getOutFileExtension, getSuffixedFileName } from '../util';
|
||||
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../ffprobe';
|
||||
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../../ffprobe';
|
||||
|
||||
const { basename } = window.require('path');
|
||||
|
|
@ -26,8 +26,8 @@ import useUserSettings from '../hooks/useUserSettings';
|
|||
import styles from './ExportConfirm.module.css';
|
||||
import { InverseCutSegment, SegmentToExport } from '../types';
|
||||
import { GenerateOutSegFileNames } from '../util/outputNameTemplate';
|
||||
import { FFprobeStream } from '../../ffprobe';
|
||||
import { AvoidNegativeTs } from '../../types';
|
||||
import { FFprobeStream } from '../../../../ffprobe';
|
||||
import { AvoidNegativeTs } from '../../../../types';
|
||||
|
||||
|
||||
const boxStyle: CSSProperties = { margin: '15px 15px 50px 15px', borderRadius: 10, padding: '10px 20px', minHeight: 500, position: 'relative' };
|
|
@ -12,7 +12,7 @@ import Swal from '../swal';
|
|||
import SetCutpointButton from './SetCutpointButton';
|
||||
import SegmentCutpointButton from './SegmentCutpointButton';
|
||||
import { getModifier } from '../hooks/useTimelineScroll';
|
||||
import { KeyBinding, KeyboardAction } from '../../types';
|
||||
import { KeyBinding, KeyboardAction } from '../../../../types';
|
||||
import { StateSegment } from '../types';
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Ładowanie…
Reference in New Issue