upgrade to electron-vite

pull/1939/head
Mikael Finstad 2024-03-21 23:28:25 +08:00
rodzic 8a7c1f8a17
commit f677619039
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 25AB36E3E81CBC26
195 zmienionych plików z 1399 dodań i 996 usunięć

Wyświetl plik

@ -1,3 +1,3 @@
/dist /dist
/vite-dist /out
/ts-dist /ts-dist

Wyświetl plik

@ -12,7 +12,7 @@ module.exports = {
overrides: [ overrides: [
{ {
files: ['./src/**/*.{js,cjs,mjs,jsx,ts,tsx,mts}'], files: ['./src/renderer/**/*.{js,cjs,mjs,jsx,ts,tsx,mts}'],
env: { env: {
node: false, node: false,
browser: true, 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: { rules: {
'import/no-extraneous-dependencies': ['error', { 'import/no-extraneous-dependencies': ['error', {
devDependencies: true, devDependencies: true,

Wyświetl plik

@ -98,7 +98,7 @@ jobs:
- name: (MacOS) Upload to Mac App Store - name: (MacOS) Upload to Mac App Store
if: startsWith(matrix.os, 'macos') && env.is_tag == 'true' if: startsWith(matrix.os, 'macos') && env.is_tag == 'true'
run: | 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 - name: (MacOS) Upload artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3

2
.gitignore vendored
Wyświetl plik

@ -11,7 +11,7 @@ node_modules
!.yarn/versions !.yarn/versions
/dist /dist
/vite-dist /out
/icon-build /icon-build
/build-resources /build-resources
/doc /doc

Wyświetl plik

@ -1,5 +1,5 @@
{ {
"search.exclude": { "search.exclude": {
"/public/locales/**": true, "/src/main/locales/**": true,
} }
} }

Wyświetl plik

@ -1,6 +1,6 @@
<div align="center"> <div align="center">
<br> <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> <p><b>LosslessCut</b></p>
The swiss army knife of lossless video/audio editing The swiss army knife of lossless video/audio editing
<br> <br>

2
cli.md
Wyświetl plik

@ -26,7 +26,7 @@ LosslessCut file1.mp4 file2.mkv
``` ```
## Override settings (experimental) ## 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 ```bash
LosslessCut --settings-json '{captureFormat:"jpeg", "keyframeCut":true}' LosslessCut --settings-json '{captureFormat:"jpeg", "keyframeCut":true}'
``` ```

Wyświetl plik

@ -5,10 +5,6 @@
This app is built using Electron. This app is built using Electron.
Make sure you have at least Node v16. The app uses ffmpeg from PATH when developing. Make sure you have at least Node v16. The app uses ffmpeg from PATH when developing.
```bash
npm install -g yarn
```
```bash ```bash
git clone https://github.com/mifi/lossless-cut.git git clone https://github.com/mifi/lossless-cut.git
cd lossless-cut cd lossless-cut

Wyświetl plik

@ -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,
},
},
});

Wyświetl plik

@ -1,8 +1,8 @@
// eslint-disable-line unicorn/filename-case // eslint-disable-line unicorn/filename-case
export default { 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, indentation: 4,
sort: true, sort: true,
@ -16,7 +16,7 @@ export default {
defaultValue: (lng, ns, key) => key, 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, keySeparator: false,
namespaceSeparator: false, namespaceSeparator: false,
}; };

Wyświetl plik

@ -4,31 +4,27 @@
"description": "The swiss army knife of lossless video/audio editing", "description": "The swiss army knife of lossless video/audio editing",
"copyright": "Copyright © 2021 ${author}", "copyright": "Copyright © 2021 ${author}",
"version": "3.60.0", "version": "3.60.0",
"main": "public/electron.js", "main": "./out/main/index.js",
"homepage": "./", "homepage": "./",
"scripts": { "scripts": {
"dev": "concurrently -k \"npm run dev:frontend\" \"npm run dev:electron\"", "clean": "rimraf dist out ts-dist build-resources icon-build",
"dev:frontend": "cross-env vite --port 3001", "start": "electron-vite preview",
"dev:electron": "wait-on tcp:3001 && electron .", "dev": "electron-vite dev -w",
"icon-gen": "mkdirp icon-build build-resources/appx && node script/icon-gen.mjs", "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-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",
"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-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", "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", "tsc": "tsc --build",
"test": "vitest", "test": "vitest",
"lint": "eslint --ext .js,.ts,.jsx,.tsx,.mjs .", "lint": "eslint --ext .js,.ts,.jsx,.tsx,.mjs,.mts .",
"pack-mac": "electron-builder --mac -m dmg", "pack-mac": "yarn build && electron-builder --mac -m dmg",
"prepack-mac": "yarn build", "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-mas-dev": "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",
"prepack-mas-dev": "yarn build",
"pack-win": "electron-builder --win",
"prepack-win": "yarn build",
"postinstall": "electron-builder install-app-deps", "postinstall": "electron-builder install-app-deps",
"version": "node script/postversion.mjs && git add no.mifi.losslesscut.appdata.xml", "version": "tsx script/postversion.mts && git add no.mifi.losslesscut.appdata.xml",
"pack-linux": "electron-builder --linux", "pack-linux": "yarn build && electron-builder --linux",
"prepack-linux": "yarn build",
"scan-i18n": "i18next --config i18next-parser.config.mjs", "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" "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", "license": "GPL-2.0-only",
"devDependencies": { "devDependencies": {
"@fontsource/open-sans": "^4.5.14", "@fontsource/open-sans": "^4.5.14",
"@radix-ui/colors": "^0.1.8",
"@radix-ui/react-switch": "^1.0.1", "@radix-ui/react-switch": "^1.0.1",
"@tsconfig/node18": "^18.2.2",
"@tsconfig/strictest": "^2.0.2", "@tsconfig/strictest": "^2.0.2",
"@tsconfig/vite-react": "^3.0.0", "@tsconfig/vite-react": "^3.0.0",
"@types/color": "^3.0.6", "@types/color": "^3.0.6",
"@types/css-modules": "^1.0.5", "@types/css-modules": "^1.0.5",
"@types/eslint": "^8", "@types/eslint": "^8",
"@types/express": "^4.17.21",
"@types/lodash": "^4.14.202", "@types/lodash": "^4.14.202",
"@types/luxon": "^3.4.2",
"@types/morgan": "^1.9.9",
"@types/node": "18", "@types/node": "18",
"@types/react": "^18.2.66", "@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22", "@types/react-dom": "^18.2.22",
"@types/sortablejs": "^1.15.0", "@types/sortablejs": "^1.15.0",
"@types/yargs-parser": "^21.0.3",
"@typescript-eslint/eslint-plugin": "^6.12.0", "@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.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",
"concurrently": "^6.0.0", "concurrently": "^6.0.0",
"cross-env": "^7.0.3",
"csv-parse": "^4.15.3", "csv-parse": "^4.15.3",
"csv-stringify": "^5.6.2", "csv-stringify": "^5.6.2",
"data-uri-to-buffer": "^4.0.0",
"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",
"electron-vite": "^2.1.0",
"eslint": "^8.2.0", "eslint": "^8.2.0",
"eslint-config-mifi": "^0.0.3", "eslint-config-mifi": "^0.0.3",
"eslint-plugin-import": "^2.25.3", "eslint-plugin-import": "^2.25.3",
@ -78,7 +81,7 @@
"fast-xml-parser": "^4.2.5", "fast-xml-parser": "^4.2.5",
"framer-motion": "^9.0.3", "framer-motion": "^9.0.3",
"i18next-parser": "^7.6.0", "i18next-parser": "^7.6.0",
"icon-gen": "^3.0.0", "icon-gen": "^4.0.0",
"immer": "^10.0.2", "immer": "^10.0.2",
"ky": "^0.33.1", "ky": "^0.33.1",
"luxon": "^3.3.0", "luxon": "^3.3.0",
@ -97,6 +100,7 @@
"react-sortablejs": "^6.1.4", "react-sortablejs": "^6.1.4",
"react-syntax-highlighter": "^15.4.3", "react-syntax-highlighter": "^15.4.3",
"react-use": "^17.4.0", "react-use": "^17.4.0",
"rimraf": "^5.0.5",
"screenfull": "^6.0.2", "screenfull": "^6.0.2",
"scroll-into-view-if-needed": "^2.2.28", "scroll-into-view-if-needed": "^2.2.28",
"sharp": "^0.32.6", "sharp": "^0.32.6",
@ -105,28 +109,25 @@
"sweetalert2": "^11.0.0", "sweetalert2": "^11.0.0",
"sweetalert2-react-content": "^5.0.7", "sweetalert2-react-content": "^5.0.7",
"tiny-invariant": "^1.3.3", "tiny-invariant": "^1.3.3",
"tsx": "^4.7.1",
"typescript": "~5.2.0", "typescript": "~5.2.0",
"use-debounce": "^5.1.0", "use-debounce": "^5.1.0",
"use-trace-update": "^1.3.0", "use-trace-update": "^1.3.0",
"vite": "^4.5.2", "vite": "^4.5.2",
"vitest": "^1.2.2", "vitest": "^1.2.2"
"wait-on": "^7.0.1"
}, },
"dependencies": { "dependencies": {
"@electron/remote": "^2.0.10", "@electron/remote": "^2.0.10",
"@radix-ui/colors": "^0.1.8", "@octokit/core": "5",
"cue-parser": "^0.3.0", "cue-parser": "^0.3.0",
"data-uri-to-buffer": "^4.0.0",
"electron-is-dev": "^2.0.0",
"electron-store": "5.1.1", "electron-store": "5.1.1",
"electron-unhandled": "^4.0.1", "electron-unhandled": "^4.0.1",
"execa": "5", "execa": "^8.0.1",
"express": "^4.18.2", "express": "^4.18.2",
"express-async-handler": "^1.2.0", "express-async-handler": "^1.2.0",
"file-type": "16", "file-type": "16",
"file-url": "^3.0.0", "file-url": "^3.0.0",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"github-api": "^3.2.2",
"i18next": "^22.4.10", "i18next": "^22.4.10",
"i18next-fs-backend": "^2.1.1", "i18next-fs-backend": "^2.1.1",
"json5": "^2.2.2", "json5": "^2.2.2",
@ -134,25 +135,28 @@
"mime-types": "^2.1.14", "mime-types": "^2.1.14",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"semver": "^7.5.2", "semver": "^7.5.2",
"string-to-stream": "^1.1.1", "string-to-stream": "^3.0.1",
"winston": "^3.8.1", "winston": "^3.8.1",
"yargs-parser": "^21.0.0" "yargs-parser": "^21.1.1"
}, },
"build": { "build": {
"directories": { "directories": {
"buildResources": "build-resources" "buildResources": "build-resources"
}, },
"extraMetadata": {
"main": "vite-dist/electron.js"
},
"files": [ "files": [
"vite-dist/**/*" "out/**/*"
], ],
"asar": { "asar": {
"smartUnpack": false "smartUnpack": false
}, },
"appId": "no.mifi.losslesscut", "appId": "no.mifi.losslesscut",
"artifactName": "${productName}-${os}-${arch}.${ext}", "artifactName": "${productName}-${os}-${arch}.${ext}",
"extraResources": [
{
"from": "locales",
"to": "locales"
}
],
"mac": { "mac": {
"hardenedRuntime": true, "hardenedRuntime": true,
"appId": "no.mifi.losslesscut-mac", "appId": "no.mifi.losslesscut-mac",

Wyświetl plik

@ -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,
};

Wyświetl plik

@ -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,
};

Wyświetl plik

@ -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] } });

Wyświetl plik

@ -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 });

Wyświetl plik

@ -1,12 +1,11 @@
import { readFile, writeFile } from 'fs/promises'; import { readFile, writeFile } from 'fs/promises';
import { XMLParser, XMLBuilder } from 'fast-xml-parser'; import { XMLParser, XMLBuilder } from 'fast-xml-parser';
// eslint-disable-next-line import/no-unresolved
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
const xmlUrl = new URL('../no.mifi.losslesscut.appdata.xml', import.meta.url); const xmlUrl = new URL('../no.mifi.losslesscut.appdata.xml', import.meta.url);
const xmlData = await readFile(xmlUrl); 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 parser = new XMLParser({ alwaysCreateTextNode: true, ignoreAttributes: false, ignoreDeclaration: false });
const xml = parser.parse(xmlData); const xml = parser.parse(xmlData);

Wyświetl plik

@ -1,5 +1,5 @@
// eslint-disable-line unicorn/filename-case // eslint-disable-line unicorn/filename-case
import execa from 'execa'; import { execa } from 'execa';
import { readFile } from 'fs/promises'; import { readFile } from 'fs/promises';
// we need a wrapper script because altool tends to error out very often // 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 // seems to be the same
const ascPublicId = apiIssuer; 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); console.log('Using version', packageJson.version);
@ -74,8 +74,11 @@ async function runAttempt() {
console.log('stdout', stdout); console.log('stdout', stdout);
return false; return false;
} catch (err) { } catch (err) {
if (err.exitCode === 1 && 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); 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']; const productErrors = errorJson['product-errors'];
// Unable to authenticate // Unable to authenticate
if (productErrors.some((error) => error.code === -19209)) { if (productErrors.some((error) => error.code === -19209)) {

Wyświetl plik

@ -1,3 +0,0 @@
const { isDev }: { isDev: boolean } = window.require('@electron/remote').require('./electron');
export default isDev;

Wyświetl plik

@ -1,8 +1,12 @@
const logger = require('./logger'); import assert from 'assert';
const { createMediaSourceProcess, readOneJpegFrame } = require('./ffmpeg');
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(); const abortController = new AbortController();
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 });
@ -13,38 +17,39 @@ function createMediaSourceStream({ path, videoStreamIndex, audioStreamIndex, see
process.kill('SIGKILL'); process.kill('SIGKILL');
}; };
process.stdout.pause(); const { stdout } = process;
assert(stdout != null);
async function readChunk() { stdout.pause();
return new Promise((resolve, reject) => {
let cleanup; const readChunk = async () => new Promise((resolve, reject) => {
let cleanup: () => void;
const onClose = () => { const onClose = () => {
cleanup(); cleanup();
resolve(null); resolve(null);
}; };
const onData = (chunk) => { const onData = (chunk: Buffer) => {
process.stdout.pause(); stdout.pause();
cleanup(); cleanup();
resolve(chunk); resolve(chunk);
}; };
const onError = (err) => { const onError = (err: Error) => {
cleanup(); cleanup();
reject(err); reject(err);
}; };
cleanup = () => { cleanup = () => {
process.stdout.off('data', onData); stdout.off('data', onData);
process.stdout.off('error', onError); stdout.off('error', onError);
process.stdout.off('close', onClose); stdout.off('close', onClose);
}; };
process.stdout.once('data', onData); stdout.once('data', onData);
process.stdout.once('error', onError); stdout.once('error', onError);
process.stdout.once('close', onClose); stdout.once('close', onClose);
process.stdout.resume(); stdout.resume();
}); });
}
function abort() { function abort() {
abortController.abort(); abortController.abort();
@ -75,9 +80,9 @@ function createMediaSourceStream({ path, videoStreamIndex, audioStreamIndex, see
return { abort, readChunk }; 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 abortController = new AbortController();
const process = readOneJpegFrame({ path, seekTo, videoStreamIndex }); const process = readOneJpegFrameRaw({ path, seekTo, videoStreamIndex });
// eslint-disable-next-line unicorn/prefer-add-event-listener // eslint-disable-next-line unicorn/prefer-add-event-listener
abortController.signal.onabort = () => process.kill('SIGKILL'); abortController.signal.onabort = () => process.kill('SIGKILL');
@ -99,9 +104,3 @@ function readOneJpegFrameWrapper({ path, seekTo, videoStreamIndex }) {
return { promise, abort }; return { promise, abort };
} }
module.exports = {
createMediaSourceStream,
readOneJpegFrame: readOneJpegFrameWrapper,
};

Wyświetl plik

@ -1,17 +1,17 @@
const Store = require('electron-store'); import Store from 'electron-store';
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
const electron = require('electron'); import electron from 'electron';
const os = require('os'); import { join, dirname } from 'path';
const { join, dirname } = require('path'); import { pathExists } from 'fs-extra';
const { pathExists } = require('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; const { app } = electron;
/** @type {import('../types').KeyBinding[]} */ const defaultKeyBindings: KeyBinding[] = [
const defaultKeyBindings = [
{ keys: 'plus', action: 'addSegment' }, { keys: 'plus', action: 'addSegment' },
{ keys: 'space', action: 'togglePlayResetSpeed' }, { keys: 'space', action: 'togglePlayResetSpeed' },
{ keys: 'k', action: 'togglePlayNoResetSpeed' }, { keys: 'k', action: 'togglePlayNoResetSpeed' },
@ -83,8 +83,7 @@ const defaultKeyBindings = [
{ keys: 'alt+down', action: 'decreaseVolume' }, { keys: 'alt+down', action: 'decreaseVolume' },
]; ];
/** @type {import('../types').Config} */ const defaults: Config = {
const defaults = {
captureFormat: 'jpeg', captureFormat: 'jpeg',
customOutDir: undefined, customOutDir: undefined,
keyframeCut: true, keyframeCut: true,
@ -145,7 +144,6 @@ const defaults = {
// For portable app: https://github.com/mifi/lossless-cut/issues/645 // For portable app: https://github.com/mifi/lossless-cut/issues/645
async function getCustomStoragePath() { async function getCustomStoragePath() {
try { try {
const isWindows = os.platform() === 'win32';
if (!isWindows || process.windowsStore) return undefined; if (!isWindows || process.windowsStore) return undefined;
// https://github.com/mifi/lossless-cut/issues/645#issuecomment-1001363314 // 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} */ export function get<T extends keyof Config>(key: T): Config[T] {
function get(key) {
return store.get(key); return store.get(key);
} }
/** @type {import('../types').StoreSetConfig} */ export function set<T extends keyof Config>(key: T, val: Config[T]) {
function set(key, val) {
if (val === undefined) store.delete(key); if (val === undefined) store.delete(key);
else store.set(key, val); else store.set(key, val);
} }
/** @type {import('../types').StoreResetConfig} */ export function reset<T extends keyof Config>(key: T) {
function reset(key) {
set(key, defaults[key]); set(key, defaults[key]);
} }
async function tryCreateStore({ customStoragePath }) { async function tryCreateStore({ customStoragePath }: { customStoragePath: string | undefined }) {
for (let i = 0; i < 5; i += 1) { for (let i = 0; i < 5; i += 1) {
try { try {
store = new Store({ defaults, cwd: customStoragePath }); store = new Store({
defaults,
...(customStoragePath != null ? { cwd: customStoragePath } : {}),
});
return; return;
} catch (err) { } catch (err) {
// eslint-disable-next-line no-await-in-loop // 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'); throw new Error('Timed out while creating config store');
} }
async function init() { export async function init() {
const customStoragePath = await getCustomStoragePath(); const customStoragePath = await getCustomStoragePath();
if (customStoragePath) logger.info('customStoragePath', customStoragePath); if (customStoragePath) logger.info('customStoragePath', customStoragePath);
@ -214,10 +212,3 @@ async function init() {
set('cleanupChoices', { ...cleanupChoices, closeFile: true }); set('cleanupChoices', { ...cleanupChoices, closeFile: true });
} }
} }
module.exports = {
init,
get,
set,
reset,
};

Wyświetl plik

@ -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';

Wyświetl plik

@ -1,8 +1,8 @@
// eslint-disable-next-line import/no-extraneous-dependencies // 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 // https://github.com/electron/electron/issues/4068#issuecomment-274159726
module.exports = (window) => { export default (window: BrowserWindow) => {
const selectionMenu = Menu.buildFromTemplate([ const selectionMenu = Menu.buildFromTemplate([
{ role: 'copy' }, { role: 'copy' },
{ type: 'separator' }, { type: 'separator' },
@ -23,9 +23,9 @@ module.exports = (window) => {
window.webContents.on('context-menu', (_e, props) => { window.webContents.on('context-menu', (_e, props) => {
const { selectionText, isEditable } = props; const { selectionText, isEditable } = props;
if (isEditable) { if (isEditable) {
inputMenu.popup(window); inputMenu.popup({ window });
} else if (selectionText && selectionText.trim() !== '') { } else if (selectionText && selectionText.trim() !== '') {
selectionMenu.popup(window); selectionMenu.popup({ window });
} }
}); });
}; };

Wyświetl plik

@ -1,53 +1,60 @@
const { join } = require('path'); import { join } from 'path';
const isDev = require('electron-is-dev'); import readline from 'readline';
const readline = require('readline'); import stringToStream from 'string-to-stream';
const stringToStream = require('string-to-stream'); import { BufferEncodingOption, execa, ExecaChildProcess, Options as ExecaOptions } from 'execa';
const execa = require('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); // setInterval(() => console.log(runningFfmpegs.size), 1000);
let customFfPath; let customFfPath: string | undefined;
// Note that this does not work on MAS because of sandbox restrictions // Note that this does not work on MAS because of sandbox restrictions
function setCustomFfPath(path) { export function setCustomFfPath(path: string | undefined) {
customFfPath = path; 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(' ')}`; return `${cmd} ${args.map((arg) => (/[^\w-]/.test(arg) ? `'${arg}'` : arg)).join(' ')}`;
} }
function getFfPath(cmd) { function getFfPath(cmd: string) {
const exeName = isWindows ? `${cmd}.exe` : cmd; const exeName = isWindows ? `${cmd}.exe` : cmd;
if (customFfPath) return join(customFfPath, exeName); if (customFfPath) return join(customFfPath, exeName);
if (isDev) { if (app.isPackaged) {
return join(process.resourcesPath, exeName);
}
// local dev
const components = ['ffmpeg', `${platform}-${arch}`]; const components = ['ffmpeg', `${platform}-${arch}`];
if (isWindows || isLinux) components.push('lib'); if (isWindows || isLinux) components.push('lib');
components.push(exeName); components.push(exeName);
return join(...components); return join(...components);
}
return join(process.resourcesPath, exeName);
} }
const getFfprobePath = () => getFfPath('ffprobe'); 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)'); console.log('Aborting', runningFfmpegs.size, 'ffmpeg process(es)');
runningFfmpegs.forEach((process) => { runningFfmpegs.forEach((process) => {
process.kill('SIGTERM', { forceKillAfterTimeout: 10000 }); 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 (!onProgress) return;
if (process.stderr == null) return;
onProgress(0); onProgress(0);
const rl = readline.createInterface({ input: process.stderr }); 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 // 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) {
// @ts-expect-error todo
customMatcher(line); customMatcher(line);
return; return;
} }
const timeStr = match[1]; const timeStr = match[1];
// console.log(timeStr); // 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}`); if (!match2) throw new Error(`Invalid time from ffmpeg progress ${timeStr}`);
const h = parseInt(match2[1], 10); const h = parseInt(match2[1]!, 10);
const m = parseInt(match2[2], 10); const m = parseInt(match2[2]!, 10);
const s = parseInt(match2[3], 10); const s = parseInt(match2[3]!, 10);
const cs = parseInt(match2[4], 10); const cs = parseInt(match2[4]!, 10);
const time = (((h * 60) + m) * 60 + s) + cs / 100; const time = (((h * 60) + m) * 60 + s) + cs / 100;
// console.log(time); // console.log(time);
@ -93,18 +99,22 @@ function handleProgress(process, durationIn, onProgress, customMatcher = () => u
}); });
} }
// @ts-expect-error todo function getExecaOptions({ env, ...customExecaOptions }: Omit<ExecaOptions<BufferEncodingOption>, 'buffer'> = {}) {
function getExecaOptions({ env, ...customExecaOptions } = {}) { const execaOptions: Omit<ExecaOptions<BufferEncodingOption>, 'buffer'> = { ...customExecaOptions, encoding: 'buffer' };
const execaOptions = { ...customExecaOptions, env: { ...env } };
// https://github.com/mifi/lossless-cut/issues/1143#issuecomment-1500883489 // 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; return execaOptions;
} }
// todo collect warnings from ffmpeg output and show them after export? example: https://github.com/mifi/lossless-cut/issues/1469 // 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(); 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)); const process = execa(ffmpegPath, args, getExecaOptions(customExecaOptions));
@ -121,23 +131,29 @@ function runFfmpegProcess(args, customExecaOptions, { logCli = true } = {}) {
return process; 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); const process = runFfmpegProcess(ffmpegArgs);
handleProgress(process, totalDuration, onProgress); handleProgress(process, totalDuration, onProgress);
assert(process.stdin != null);
stringToStream(concatTxt).pipe(process.stdin); stringToStream(concatTxt).pipe(process.stdin);
return process; 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); const process = runFfmpegProcess(ffmpegArgs);
assert(process.stderr != null);
handleProgress(process, duration, onProgress); handleProgress(process, duration, onProgress);
return process; return process;
} }
async function runFfprobe(args, { timeout = isDev ? 10000 : 30000 } = {}) { export async function runFfprobe(args: readonly string[], { timeout = isDev ? 10000 : 30000 } = {}) {
const ffprobePath = getFfprobePath(); const ffprobePath = getFfprobePath();
console.log(getFfCommandLine('ffprobe', args)); console.log(getFfCommandLine('ffprobe', args));
const ps = execa(ffprobePath, args, getExecaOptions()); 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>} */ export async function renderWaveformPng({ filePath, start, duration, color, streamIndex }: {
async function renderWaveformPng({ filePath, start, duration, color, streamIndex }) { filePath: string, start: number, duration: number, color: string, streamIndex: number,
}): Promise<Waveform> {
const args1 = [ const args1 = [
'-hide_banner', '-hide_banner',
'-i', filePath, '-i', filePath,
'-ss', start, '-ss', String(start),
'-t', duration, '-t', String(duration),
'-c', 'copy', '-c', 'copy',
'-vn', '-vn',
'-map', `0:${streamIndex}`, '-map', `0:${streamIndex}`,
@ -179,16 +196,18 @@ async function renderWaveformPng({ filePath, start, duration, color, streamIndex
console.log(getFfCommandLine('ffmpeg1', args1)); console.log(getFfCommandLine('ffmpeg1', args1));
console.log('|', getFfCommandLine('ffmpeg2', args2)); console.log('|', getFfCommandLine('ffmpeg2', args2));
let ps1; let ps1: ExecaChildProcess<Buffer> | undefined;
let ps2; let ps2: ExecaChildProcess<Buffer> | undefined;
try { try {
ps1 = runFfmpegProcess(args1, { encoding: null, buffer: false }, { logCli: false }); ps1 = runFfmpegProcess(args1, { buffer: false }, { logCli: false });
ps2 = runFfmpegProcess(args2, { encoding: null }, { logCli: false }); ps2 = runFfmpegProcess(args2, undefined, { logCli: false });
assert(ps1.stdout != null);
assert(ps2.stdin != null);
ps1.stdout.pipe(ps2.stdin); ps1.stdout.pipe(ps2.stdin);
const timer = setTimeout(() => { const timer = setTimeout(() => {
ps1.kill(); ps1?.kill();
ps2.kill(); ps2?.kill();
console.warn('ffmpeg timed out'); console.warn('ffmpeg timed out');
}, 10000); }, 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)] : []), ...(from != null ? ['-ss', from.toFixed(5)] : []),
'-i', filePath, '-i', filePath,
...(to != null ? ['-t', (to - from).toFixed(5)] : []), ...(from != null && to != null ? ['-t', (to - from).toFixed(5)] : []),
]; ];
function mapTimesToSegments(times) { export function mapTimesToSegments(times: number[]) {
const segments = []; const segments: { start: number, end: number | undefined }[] = [];
for (let i = 0; i < times.length; i += 1) { for (let i = 0; i < times.length; i += 1) {
const start = times[i]; const start = times[i];
const end = times[i + 1]; const end = times[i + 1];
@ -225,32 +244,35 @@ function mapTimesToSegments(times) {
return segments; 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); const offset = getSegmentOffset(from);
return segments.map(({ start, end }) => ({ start: start + offset, end: end != null ? end + offset : end })); 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 // 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 = [ const args = [
'-hide_banner', '-hide_banner',
...getInputSeekArgs({ filePath, from, to }), ...getInputSeekArgs({ filePath, from, to }),
'-filter_complex', `select='gt(scene,${minChange})',metadata=print:file=-`, '-filter_complex', `select='gt(scene,${minChange})',metadata=print:file=-`,
'-f', 'null', '-', '-f', 'null', '-',
]; ];
const process = runFfmpegProcess(args, { encoding: null, buffer: false }); const process = runFfmpegProcess(args, { buffer: false });
const times = [0]; const times = [0];
handleProgress(process, to - from, onProgress); handleProgress(process, to - from, onProgress);
assert(process.stdout != null);
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 // 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]!);
// @ts-expect-error todo // @ts-expect-error todo
if (Number.isNaN(time) || time <= times.at(-1)) return; if (Number.isNaN(time) || time <= times.at(-1)) return;
times.push(time); times.push(time);
@ -263,89 +285,134 @@ async function detectSceneChanges({ filePath, minChange, onProgress, from, to })
return adjustSegmentsWithOffset({ segments, from }); 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 = [ const args = [
'-hide_banner', '-hide_banner',
...getInputSeekArgs({ filePath, from, to }), ...getInputSeekArgs({ filePath, from, to }),
...customArgs, ...customArgs,
'-f', 'null', '-', '-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) { function customMatcher(line: string) {
const { start: startStr, end: endStr } = matchLineTokens(line); const { start: startRaw, end: endRaw } = matchLineTokens(line);
const start = parseFloat(startStr); if (typeof startRaw === 'number' && typeof endRaw === 'number') {
const end = parseFloat(endStr); if (startRaw == null || endRaw == null) return;
if (start == null || end == null || Number.isNaN(start) || Number.isNaN(end)) 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 }); segments.push({ start, end });
} else {
throw new TypeError('Invalid line match');
}
} }
// @ts-expect-error todo
handleProgress(process, to - from, onProgress, customMatcher); handleProgress(process, to - from, onProgress, customMatcher);
await process; await process;
return adjustSegmentsWithOffset({ segments, from }); 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 }) { export async function blackDetect({ filePath, filterOptions, onProgress, from, to }: { filePath: string, filterOptions: Record<string, string>, onProgress: (p: number) => void, from: number, to: number }) {
function matchLineTokens(line) { const customArgs = ['-vf', `blackdetect=${mapFilterOptions(filterOptions)}`, '-an'];
return detectIntervals({
filePath,
onProgress,
from,
to,
matchLineTokens: (line) => {
// eslint-disable-next-line unicorn/better-regex // 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 {
start: parseFloat(match[1]), start: undefined,
end: parseFloat(match[2]), end: undefined,
}; };
} }
const customArgs = ['-vf', `blackdetect=${mapFilterOptions(filterOptions)}`, '-an']; return {
return detectIntervals({ filePath, onProgress, from, to, matchLineTokens, customArgs }); start: match[1],
end: match[2],
};
},
customArgs,
});
} }
async function silenceDetect({ filePath, filterOptions, onProgress, from, to }) { export async function silenceDetect({ filePath, filterOptions, onProgress, from, to }: {
function matchLineTokens(line) { 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 // 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) {
const end = parseFloat(match[1]); return {
const silenceDuration = parseFloat(match[2]); start: undefined,
if (Number.isNaN(end) || Number.isNaN(silenceDuration)) return {}; 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; const start = end - silenceDuration;
if (start < 0 || end <= 0 || start >= end) return {}; if (start < 0 || end <= 0 || start >= end) {
return {
start: undefined,
end: undefined,
};
}
return { return {
start, start,
end, end,
}; };
} },
const customArgs = ['-af', `silencedetect=${mapFilterOptions(filterOptions)}`, '-vn']; customArgs: ['-af', `silencedetect=${mapFilterOptions(filterOptions)}`, '-vn'],
return detectIntervals({ filePath, onProgress, from, to, matchLineTokens, customArgs }); });
} }
function getFffmpegJpegQuality(quality) { function getFffmpegJpegQuality(quality: number) {
// Normal range for JPEG is 2-31 with 31 being the worst quality. // Normal range for JPEG is 2-31 with 31 being the worst quality.
const qMin = 2; const qMin = 2;
const qMax = 31; const qMax = 31;
return Math.min(Math.max(qMin, quality, Math.round((1 - quality) * (qMax - qMin) + qMin)), qMax); return Math.min(Math.max(qMin, quality, Math.round((1 - quality) * (qMax - qMin) + qMin)), qMax);
} }
function getQualityOpts({ captureFormat, quality }) { function getQualityOpts({ captureFormat, quality }: { captureFormat: CaptureFormat, quality: number }) {
if (captureFormat === 'jpeg') return ['-q:v', getFffmpegJpegQuality(quality)]; if (captureFormat === 'jpeg') return ['-q:v', String(getFffmpegJpegQuality(quality))];
if (captureFormat === 'webp') return ['-q:v', Math.max(0, Math.min(100, Math.round(quality * 100)))]; if (captureFormat === 'webp') return ['-q:v', String(Math.max(0, Math.min(100, Math.round(quality * 100))))];
return []; 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 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 []; 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 = [ const args = [
'-ss', from, '-ss', String(from),
'-i', videoPath, '-i', videoPath,
'-t', Math.max(0, to - from), '-t', String(Math.max(0, to - from)),
...getQualityOpts({ captureFormat, quality }), ...getQualityOpts({ captureFormat, quality }),
...(filter != null ? ['-vf', filter] : []), ...(filter != null ? ['-vf', filter] : []),
// https://superuser.com/questions/1336285/use-ffmpeg-for-thumbnail-selections // 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, '-y', outPathTemplate,
]; ];
const process = runFfmpegProcess(args, { encoding: null, buffer: false }); const process = runFfmpegProcess(args, { buffer: false });
handleProgress(process, to - from, onProgress); handleProgress(process, to - from, onProgress);
await process; 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); const ffmpegQuality = getFffmpegJpegQuality(quality);
await runFfmpegProcess([ await runFfmpegProcess([
'-ss', timestamp, '-ss', String(timestamp),
'-i', videoPath, '-i', videoPath,
'-vframes', '1', '-vframes', '1',
'-q:v', ffmpegQuality, '-q:v', String(ffmpegQuality),
'-y', outPath, '-y', outPath,
]); ]);
} }
async function readFormatData(filePath) { async function readFormatData(filePath: string) {
console.log('readFormatData', filePath); console.log('readFormatData', filePath);
const { stdout } = await runFfprobe([ const { stdout } = await runFfprobe([
'-of', 'json', '-show_format', '-i', filePath, '-hide_banner', '-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); 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; let audio;
if (hasAudio) { if (hasAudio) {
if (speed === 'slowest') audio = 'hq'; if (speed === 'slowest') audio = 'hq';
@ -492,14 +563,14 @@ async function html5ify({ outPath, filePath: filePathArg, speed, hasAudio, hasVi
if (duration) handleProgress(process, duration, onProgress); if (duration) handleProgress(process, duration, onProgress);
const { stdout } = await process; 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 = [ const args = [
'-hide_banner', '-loglevel', 'error', '-hide_banner', '-loglevel', 'error',
'-ss', seekTo, '-ss', String(seekTo),
'-noautorotate', '-noautorotate',
@ -516,17 +587,19 @@ function readOneJpegFrame({ path, seekTo, videoStreamIndex }) {
// console.log(args); // console.log(args);
return runFfmpegProcess(args, { encoding: 'buffer' }, { logCli: true }); return runFfmpegProcess(args, undefined, { logCli: true });
} }
const enableLog = false; const enableLog = false;
const encode = true; 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() { function getVideoFilters() {
if (videoStreamIndex == null) return []; if (videoStreamIndex == null) return [];
const filters = []; const filters: string[] = [];
if (fps != null) filters.push(`fps=${fps}`); if (fps != null) filters.push(`fps=${fps}`);
if (size != null) filters.push(`scale=${size}:${size}:flags=lanczos:force_original_aspect_ratio=decrease`); if (size != null) filters.push(`scale=${size}:${size}:flags=lanczos:force_original_aspect_ratio=decrease`);
if (filters.length === 0) return []; if (filters.length === 0) return [];
@ -547,7 +620,7 @@ function createMediaSourceProcess({ path, videoStreamIndex, audioStreamIndex, se
'-vsync', 'passthrough', '-vsync', 'passthrough',
'-ss', seekTo, '-ss', String(seekTo),
'-noautorotate', '-noautorotate',
@ -582,27 +655,5 @@ function createMediaSourceProcess({ path, videoStreamIndex, audioStreamIndex, se
return execa(getFfmpegPath(), args, { encoding: null, buffer: false, stderr: enableLog ? 'inherit' : 'pipe' }); return execa(getFfmpegPath(), args, { encoding: null, buffer: false, stderr: enableLog ? 'inherit' : 'pipe' });
} }
// Don't pass complex objects over the bridge // Don't pass complex objects over the bridge (process)
const runFfmpeg = async (...args) => runFfmpegProcess(...args); export const runFfmpeg = async (...args: Parameters<typeof runFfmpegProcess>) => runFfmpegProcess(...args);
module.exports = {
setCustomFfPath,
abortFfmpegs,
getFfmpegPath,
runFfprobe,
runFfmpeg,
runFfmpegConcat,
runFfmpegWithProgress,
renderWaveformPng,
mapTimesToSegments,
detectSceneChanges,
captureFrames,
captureFrame,
getFfCommandLine,
html5ify,
getDuration,
readOneJpegFrame,
blackDetect,
silenceDetect,
createMediaSourceProcess,
};

Wyświetl plik

@ -1,14 +1,16 @@
const express = require('express'); import express from 'express';
const morgan = require('morgan'); import morgan from 'morgan';
const http = require('http'); import http from 'http';
const asyncHandler = require('express-async-handler'); import asyncHandler from 'express-async-handler';
const { homepage } = require('./constants'); import assert from 'assert';
import { homepage } from './constants.js';
import logger from './logger.js';
const logger = require('./logger'); export default ({ port, onKeyboardAction }: {
port: number, onKeyboardAction: (a: string) => Promise<void>,
}) => {
module.exports = ({ port, onKeyboardAction }) => {
const app = express(); const app = express();
// https://expressjs.com/en/resources/middleware/morgan.html // https://expressjs.com/en/resources/middleware/morgan.html
@ -25,7 +27,10 @@ module.exports = ({ port, onKeyboardAction }) => {
app.use('/api', apiRouter); app.use('/api', apiRouter);
apiRouter.post('/shortcuts/:action', express.json(), asyncHandler(async (req, res) => { 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(); res.end();
})); }));

Wyświetl plik

@ -1,12 +1,12 @@
const i18n = require('i18next'); import i18n from 'i18next';
const Backend = require('i18next-fs-backend'); import Backend from 'i18next-fs-backend';
const { commonI18nOptions, loadPath, addPath } = require('./i18n-common'); import { commonI18nOptions, loadPath, addPath } from './i18nCommon.js';
// See also renderer // See also renderer
// https://github.com/i18next/i18next/issues/869 // https://github.com/i18next/i18next/issues/869
i18n export default i18n
.use(Backend) .use(Backend)
// detect user language // detect user language
// learn more: https://github.com/i18next/i18next-browser-languageDetector // learn more: https://github.com/i18next/i18next-browser-languageDetector
@ -23,5 +23,3 @@ i18n
addPath, addPath,
}, },
}); });
module.exports = i18n;

Wyświetl plik

@ -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 // 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');
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
const { app } = require('electron'); import { app } from 'electron';
const { join } = require('path'); import { join } from 'path';
import { InitOptions } from 'i18next';
const { frontendBuildDir } = require('./util');
let customLocalesPath; let customLocalesPath: string | undefined;
function setCustomLocalesPath(p) { export function setCustomLocalesPath(p: string) {
customLocalesPath = p; customLocalesPath = p;
} }
function getLangPath(subPath) { function getLangPath(subPath: string) {
if (customLocalesPath != null) return join(customLocalesPath, subPath); if (customLocalesPath != null) return join(customLocalesPath, subPath);
if (isDev) return join('public', subPath); if (app.isPackaged) return join(process.resourcesPath, 'locales', subPath);
return join(app.getAppPath(), frontendBuildDir, subPath); return join('locales', subPath);
} }
// Weblate hardcodes different lang codes than electron // Weblate hardcodes different lang codes than electron
// https://www.electronjs.org/docs/api/app#appgetlocale // https://www.electronjs.org/docs/api/app#appgetlocale
// https://source.chromium.org/chromium/chromium/src/+/master:ui/base/l10n/l10n_util.cc // https://source.chromium.org/chromium/chromium/src/+/master:ui/base/l10n/l10n_util.cc
const mapLang = (lng) => ({ const mapLang = (lng: string) => ({
nb: 'nb_NO', nb: 'nb_NO',
no: 'nb_NO', no: 'nb_NO',
zh: 'zh_Hans', zh: 'zh_Hans',
@ -39,29 +37,21 @@ const mapLang = (lng) => ({
'ru-RU': 'ru', 'ru-RU': 'ru',
}[lng] || lng); }[lng] || lng);
const fallbackLng = 'en'; export const fallbackLng = 'en';
const commonI18nOptions = { export const commonI18nOptions: InitOptions = {
fallbackLng, fallbackLng,
// debug: isDev, // debug: isDev,
// saveMissing: isDev, // saveMissing: isDev,
// updateMissing: isDev, // updateMissing: isDev,
// saveMissingTo: 'all', // 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? // TODO improve keys?
// Maybe do something like this: https://stackoverflow.com/a/19405314/6519037 // Maybe do something like this: https://stackoverflow.com/a/19405314/6519037
keySeparator: false, keySeparator: false,
nsSeparator: false, nsSeparator: false,
}; };
const loadPath = (lng, ns) => getLangPath(`locales/${mapLang(lng)}/${ns}.json`); export const loadPath = (lng: string, ns: string) => getLangPath(`${mapLang(lng)}/${ns}.json`);
const addPath = (lng, ns) => getLangPath(`locales/${mapLang(lng)}/${ns}.missing.json`); export const addPath = (lng: string, ns: string) => getLangPath(`${mapLang(lng)}/${ns}.missing.json`);
module.exports = {
fallbackLng,
loadPath,
addPath,
commonI18nOptions,
setCustomLocalesPath,
};

Wyświetl plik

@ -1,31 +1,55 @@
/// <reference types="electron-vite/node" />
process.traceDeprecation = true; process.traceDeprecation = true;
process.traceProcessWarnings = true; process.traceProcessWarnings = true;
/* eslint-disable import/first */
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
const electron = require('electron'); import electron, { AboutPanelOptionsOptions, BrowserWindow, BrowserWindowConstructorOptions, nativeTheme, shell, app, ipcMain } from 'electron';
const isDev = require('electron-is-dev'); import unhandled from 'electron-unhandled';
const unhandled = require('electron-unhandled'); import i18n from 'i18next';
const i18n = require('i18next'); import debounce from 'lodash/debounce';
const debounce = require('lodash/debounce'); import yargsParser from 'yargs-parser';
const yargsParser = require('yargs-parser'); import JSON5 from 'json5';
const JSON5 = require('json5'); import remote from '@electron/remote/main';
const remote = require('@electron/remote/main'); import { stat } from 'fs/promises';
const { stat } = require('fs/promises'); import assert from 'assert';
const logger = require('./logger'); import logger from './logger.js';
const menu = require('./menu'); import menu from './menu.js';
const configStore = require('./configStore'); import * as configStore from './configStore.js';
const { frontendBuildDir, isLinux } = require('./util'); import { isLinux } from './util.js';
const attachContextMenu = require('./contextMenu'); import attachContextMenu from './contextMenu.js';
const HttpServer = require('./httpServer'); 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://chromestatus.com/feature/5748496434987008
// https://peter.sh/experiments/chromium-command-line-switches/ // https://peter.sh/experiments/chromium-command-line-switches/
@ -49,8 +73,7 @@ const isStoreBuild = process.windowsStore || process.mas;
const showVersion = !isStoreBuild; const showVersion = !isStoreBuild;
/** @type import('electron').AboutPanelOptionsOptions */ const aboutPanelOptions: AboutPanelOptionsOptions = {
const aboutPanelOptions = {
applicationName: appName, applicationName: appName,
copyright: `Copyright © ${copyrightYear} Mikael Finstad ❤️ 🇳🇴`, copyright: `Copyright © ${copyrightYear} Mikael Finstad ❤️ 🇳🇴`,
version: '', // not very useful (MacOS only, and same as applicationVersion) 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 // https://www.electronjs.org/docs/latest/api/app#appsetaboutpaneloptionsoptions
app.setAboutPanelOptions(aboutPanelOptions); app.setAboutPanelOptions(aboutPanelOptions);
let filesToOpen = []; let filesToOpen: string[] = [];
// Keep a global reference of the window object, if you don't, the window will // 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. // be closed automatically when the JavaScript object is garbage collected.
let mainWindow; let mainWindow: BrowserWindow | null;
let askBeforeClose = false; let askBeforeClose = false;
let rendererReady = false; let rendererReady = false;
let newVersion; let newVersion: string | undefined;
let disableNetworking; let disableNetworking: boolean;
const openFiles = (paths) => mainWindow.webContents.send('openFiles', paths); const openFiles = (paths: string[]) => mainWindow!.webContents.send('openFiles', paths);
let apiKeyboardActionRequestsId = 0; let apiKeyboardActionRequestsId = 0;
const apiKeyboardActionRequests = new Map(); const apiKeyboardActionRequests = new Map<number, () => void>();
async function sendApiKeyboardAction(action) { async function sendApiKeyboardAction(action: string) {
try { try {
const id = apiKeyboardActionRequestsId; const id = apiKeyboardActionRequestsId;
apiKeyboardActionRequestsId += 1; apiKeyboardActionRequestsId += 1;
mainWindow.webContents.send('apiKeyboardAction', { id, action }); mainWindow!.webContents.send('apiKeyboardAction', { id, action } satisfies ApiKeyboardActionRequest);
await new Promise((resolve) => { await new Promise<void>((resolve) => {
apiKeyboardActionRequests.set(id, resolve); apiKeyboardActionRequests.set(id, resolve);
}); });
} catch (err) { } catch (err) {
@ -101,7 +124,7 @@ async function sendApiKeyboardAction(action) {
// https://github.com/electron/electron/issues/526#issuecomment-563010533 // https://github.com/electron/electron/issues/526#issuecomment-563010533
function getSizeOptions() { function getSizeOptions() {
const bounds = configStore.get('windowBounds'); const bounds = configStore.get('windowBounds');
const options = {}; const options: BrowserWindowConstructorOptions = {};
if (bounds) { if (bounds) {
const area = electron.screen.getDisplayMatching(bounds).workArea; const area = electron.screen.getDisplayMatching(bounds).workArea;
// If the saved position still valid (the window is entirely inside the display area), use it. // 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'); if (isDev) mainWindow.loadURL('http://localhost:3001');
// Need to useloadFile for special characters https://github.com/mifi/lossless-cut/issues/40 // 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. // Open the DevTools.
// mainWindow.webContents.openDevTools() // mainWindow.webContents.openDevTools()
@ -164,6 +187,7 @@ function createWindow() {
mainWindow.on('close', (e) => { mainWindow.on('close', (e) => {
if (!askBeforeClose) return; if (!askBeforeClose) return;
assert(mainWindow);
const choice = electron.dialog.showMessageBoxSync(mainWindow, { const choice = electron.dialog.showMessageBoxSync(mainWindow, {
type: 'question', type: 'question',
buttons: ['Yes', 'No'], buttons: ['Yes', 'No'],
@ -186,10 +210,11 @@ function createWindow() {
} }
function updateMenu() { function updateMenu() {
assert(mainWindow);
menu({ app, mainWindow, newVersion, isStoreBuild }); menu({ app, mainWindow, newVersion, isStoreBuild });
} }
function openFilesEventually(paths) { function openFilesEventually(paths: string[]) {
if (rendererReady) openFiles(paths); if (rendererReady) openFiles(paths);
else filesToOpen = paths; else filesToOpen = paths;
} }
@ -201,18 +226,18 @@ function openFilesEventually(paths) {
function parseCliArgs(rawArgv = process.argv) { function parseCliArgs(rawArgv = process.argv) {
const ignoreFirstArgs = process.defaultApp ? 2 : 1; const ignoreFirstArgs = process.defaultApp ? 2 : 1;
// production: First arg is the LosslessCut executable // 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) : []; 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(); 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 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: // using additionalData because the built in "argv" passing is a bit broken:
@ -232,16 +257,14 @@ function initApp() {
mainWindow.focus(); mainWindow.focus();
} }
// @ts-expect-error todo if (!(additionalData != null && typeof additionalData === 'object' && 'argv' in additionalData) || !Array.isArray(additionalData.argv)) return;
if (!Array.isArray(additionalData?.argv)) return;
// @ts-expect-error todo
const argv2 = parseCliArgs(additionalData.argv); const argv2 = parseCliArgs(additionalData.argv);
logger.info('second-instance', argv2); logger.info('second-instance', argv2);
if (argv2._ && argv2._.length > 0) openFilesEventually(argv2._); if (argv2._ && argv2._.length > 0) openFilesEventually(argv2._.map(String));
else if (argv2.keyboardAction) sendApiKeyboardAction(argv2.keyboardAction); else if (argv2['keyboardAction']) sendApiKeyboardAction(argv2['keyboardAction']);
}); });
// Quit when all windows are closed. // Quit when all windows are closed.
@ -322,7 +345,7 @@ const readyPromise = app.whenReady();
logger.info('CLI arguments', argv); logger.info('CLI arguments', argv);
// Only if no files to open already (open-file might have already added some files) // 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; const { settingsJson } = argv;
({ disableNetworking } = argv); ({ disableNetworking } = argv);
@ -330,6 +353,7 @@ const readyPromise = app.whenReady();
if (settingsJson != null) { if (settingsJson != null) {
logger.info('initializing settings', settingsJson); logger.info('initializing settings', settingsJson);
Object.entries(JSON5.parse(settingsJson)).forEach(([key, value]) => { Object.entries(JSON5.parse(settingsJson)).forEach(([key, value]) => {
// @ts-expect-error todo use zod?
configStore.set(key, value); configStore.set(key, value);
}); });
} }
@ -340,6 +364,7 @@ const readyPromise = app.whenReady();
const port = typeof httpApi === 'number' ? httpApi : 8080; const port = typeof httpApi === 'number' ? httpApi : 8080;
const { startHttpServer } = HttpServer({ port, onKeyboardAction: sendApiKeyboardAction }); const { startHttpServer } = HttpServer({ port, onKeyboardAction: sendApiKeyboardAction });
await startHttpServer(); 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 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: string) => logger.info('Added Extension', name))
.catch((err) => logger.error('Failed to add extension', err)); .catch((err: unknown) => logger.error('Failed to add extension', err));
} }
createWindow(); createWindow();
@ -366,7 +391,7 @@ const readyPromise = app.whenReady();
} }
})(); })();
function focusWindow() { export function focusWindow() {
try { try {
app.focus({ steal: true }); app.focus({ steal: true });
} catch (err) { } catch (err) {
@ -374,10 +399,8 @@ function focusWindow() {
} }
} }
function quitApp() { export function quitApp() {
electron.app.quit(); electron.app.quit();
} }
const hasDisabledNetworking = () => !!disableNetworking; export const hasDisabledNetworking = () => !!disableNetworking;
module.exports = { focusWindow, isDev, hasDisabledNetworking, quitApp };

Wyświetl plik

@ -0,0 +1,2 @@
const isDev = import.meta.env.MODE === 'development';
export default isDev;

Wyświetl plik

@ -1,15 +1,18 @@
const winston = require('winston'); import winston from 'winston';
const util = require('util'); import util from 'util';
const isDev = require('electron-is-dev');
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
const { app } = require('electron'); import { app } from 'electron';
const { join } = require('path'); 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://mifi.no/blog/winston-electron-logger/
// https://github.com/winstonjs/winston/issues/1427 // https://github.com/winstonjs/winston/issues/1427
const combineMessageAndSplat = () => ({ const combineMessageAndSplat = () => ({
transform(info) { transform(info: TransformableInfo) {
// @ts-expect-error todo
const { [Symbol.for('splat')]: args = [], message } = info; const { [Symbol.for('splat')]: args = [], message } = info;
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
info.message = util.format(message, ...args); info.message = util.format(message, ...args);
@ -21,14 +24,14 @@ const createLogger = () => winston.createLogger({
format: winston.format.combine( format: winston.format.combine(
winston.format.timestamp(), winston.format.timestamp(),
combineMessageAndSplat(), 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(); const logger = createLogger();
logger.add(new winston.transports.File({ level: 'debug', filename: join(logDirPath, 'app.log'), options: { flags: 'a' } })); 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;

Wyświetl plik

@ -1,16 +1,19 @@
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
const electron = require('electron'); import electron, { BrowserWindow } from 'electron';
const { t } = require('i18next'); import { t } from 'i18next';
import { homepage, getReleaseUrl, licensesPage } from './constants.js';
// 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.replaceAll('&', '&&'); const esc = (val: string) => val.replaceAll('&', '&&');
const { Menu } = electron; const { Menu } = electron;
const { homepage, getReleaseUrl, licensesPage } = require('./constants'); export default ({ app, mainWindow, newVersion, isStoreBuild }: {
app: Electron.App, mainWindow: BrowserWindow, newVersion?: string | undefined, isStoreBuild: boolean,
module.exports = ({ app, mainWindow, newVersion, isStoreBuild }) => { }) => {
const menu = [ const menu = [
...(process.platform === 'darwin' ? [{ role: 'appMenu' }] : []), ...(process.platform === 'darwin' ? [{ role: 'appMenu' }] : []),

Wyświetl plik

@ -1,24 +1,33 @@
// eslint-disable-line unicorn/filename-case
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'); import electron from 'electron';
const semver = require('semver'); 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 { app } = electron;
const gh = new GitHub(); const octokit = new Octokit();
const repo = gh.getRepo('mifi', 'lossless-cut');
async function checkNewVersion() {
// eslint-disable-next-line import/prefer-default-export
export async function checkNewVersion() {
try { try {
// From API: https://developer.github.com/v3/repos/releases/#get-the-latest-release // From API: https://developer.github.com/v3/repos/releases/#get-the-latest-release
// View the latest published full release for the repository. // View the latest published full release for the repository.
// Draft releases and prereleases are not returned by this endpoint. // 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 = app.getVersion();
// const currentVersion = '3.17.2'; // const currentVersion = '3.17.2';
@ -34,5 +43,3 @@ async function checkNewVersion() {
return undefined; return undefined;
} }
} }
module.exports = { checkNewVersion };

8
src/main/util.ts 100644
Wyświetl plik

@ -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';

Wyświetl plik

@ -0,0 +1,2 @@
// todo
console.log('preload');

Wyświetl plik

@ -86,9 +86,9 @@ import { rightBarWidth, leftBarWidth, ffmpegExtractWindow, zoomMax } from './uti
import BigWaveform from './components/BigWaveform'; import BigWaveform from './components/BigWaveform';
import isDev from './isDev'; import isDev from './isDev';
import { ChromiumHTMLVideoElement, EdlFileType, FfmpegCommandLog, FormatTimecode, Html5ifyMode, PlaybackMode, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, Thumbnail, TunerType } from './types'; import { ChromiumHTMLVideoElement, EdlFileType, FfmpegCommandLog, FormatTimecode, PlaybackMode, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, Thumbnail, TunerType } from './types';
import { CaptureFormat, KeyboardAction } from '../types'; import { CaptureFormat, KeyboardAction, Html5ifyMode } from '../../../types';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../ffprobe'; import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../ffprobe';
const electron = window.require('electron'); const electron = window.require('electron');
const { exists } = window.require('fs-extra'); const { exists } = window.require('fs-extra');
@ -96,8 +96,7 @@ const { lstat } = window.require('fs/promises');
const filePathToUrl = window.require('file-url'); const filePathToUrl = window.require('file-url');
const { parse: parsePath, join: pathJoin, basename, dirname } = window.require('path'); const { parse: parsePath, join: pathJoin, basename, dirname } = window.require('path');
const remote = window.require('@electron/remote'); const { focusWindow, hasDisabledNetworking, quitApp } = window.require('@electron/remote').require('./index.js');
const { focusWindow, hasDisabledNetworking, quitApp } = remote.require('./electron');
const videoStyle: CSSProperties = { width: '100%', height: '100%', objectFit: 'contain' }; const videoStyle: CSSProperties = { width: '100%', height: '100%', objectFit: 'contain' };
@ -1338,8 +1337,10 @@ function App() {
return; return;
} }
if ('stdout' in err) console.error('stdout:', err.stdout); // @ts-expect-error todo
if ('stderr' in err) console.error('stderr:', err.stderr); 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 (isExecaFailure(err)) {
if (isOutOfSpaceError(err)) { if (isOutOfSpaceError(err)) {

Wyświetl plik

@ -4,10 +4,9 @@ import { useDebounce } from 'use-debounce';
import isDev from './isDev'; import isDev from './isDev';
import { ChromiumHTMLVideoElement } from './types'; import { ChromiumHTMLVideoElement } from './types';
import { FFprobeStream } from '../ffprobe'; import { FFprobeStream } from '../../../ffprobe';
const remote = window.require('@electron/remote'); const { compatPlayer: { createMediaSourceStream, readOneJpegFrame } } = window.require('@electron/remote').require('./index.js');
const { createMediaSourceStream, readOneJpegFrame } = remote.require('./compatPlayer');
async function startPlayback({ path, video, videoStreamIndex, audioStreamIndex, seekTo, signal, playSafe, onCanPlay, getTargetTime, size, fps }: { 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(); processChunk();
} }
function drawJpegFrame(canvas: HTMLCanvasElement, jpegImage: Buffer) { function drawJpegFrame(canvas: HTMLCanvasElement | null, jpegImage: Buffer) {
if (!canvas) return; if (!canvas) return;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
@ -248,7 +247,9 @@ function drawJpegFrame(canvas: HTMLCanvasElement, jpegImage: Buffer) {
img.src = `data:image/jpeg;base64,${jpegImage.toString('base64')}`; 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 }); const { promise, abort } = readOneJpegFrame({ path, seekTo, videoStreamIndex });
signal.addEventListener('abort', () => abort()); signal.addEventListener('abort', () => abort());
const jpegImage = await promise; const jpegImage = await promise;

Wyświetl plik

@ -14,7 +14,7 @@ import OutputFormatSelect from './OutputFormatSelect';
import useUserSettings from '../hooks/useUserSettings'; import useUserSettings from '../hooks/useUserSettings';
import { isMov } from '../util/streams'; import { isMov } from '../util/streams';
import { getOutFileExtension, getSuffixedFileName } from '../util'; import { getOutFileExtension, getSuffixedFileName } from '../util';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../ffprobe'; import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../../ffprobe';
const { basename } = window.require('path'); const { basename } = window.require('path');

Wyświetl plik

@ -26,8 +26,8 @@ import useUserSettings from '../hooks/useUserSettings';
import styles from './ExportConfirm.module.css'; import styles from './ExportConfirm.module.css';
import { InverseCutSegment, SegmentToExport } from '../types'; import { InverseCutSegment, SegmentToExport } from '../types';
import { GenerateOutSegFileNames } from '../util/outputNameTemplate'; import { GenerateOutSegFileNames } from '../util/outputNameTemplate';
import { FFprobeStream } from '../../ffprobe'; import { FFprobeStream } from '../../../../ffprobe';
import { AvoidNegativeTs } from '../../types'; import { AvoidNegativeTs } from '../../../../types';
const boxStyle: CSSProperties = { margin: '15px 15px 50px 15px', borderRadius: 10, padding: '10px 20px', minHeight: 500, position: 'relative' }; const boxStyle: CSSProperties = { margin: '15px 15px 50px 15px', borderRadius: 10, padding: '10px 20px', minHeight: 500, position: 'relative' };

Wyświetl plik

@ -12,7 +12,7 @@ import Swal from '../swal';
import SetCutpointButton from './SetCutpointButton'; import SetCutpointButton from './SetCutpointButton';
import SegmentCutpointButton from './SegmentCutpointButton'; import SegmentCutpointButton from './SegmentCutpointButton';
import { getModifier } from '../hooks/useTimelineScroll'; import { getModifier } from '../hooks/useTimelineScroll';
import { KeyBinding, KeyboardAction } from '../../types'; import { KeyBinding, KeyboardAction } from '../../../../types';
import { StateSegment } from '../types'; import { StateSegment } from '../types';

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