start to use ipc

pull/2715/head
Mikael Finstad 6 months ago
parent 26010830dc
commit 26010854e5
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -13,9 +13,10 @@ import { stat } from 'node:fs/promises';
import assert from 'node:assert'; import assert from 'node:assert';
import timers from 'node:timers/promises'; import timers from 'node:timers/promises';
import { z } from 'zod'; import { z } from 'zod';
import { pathToFileURL } from 'node:url'; import { fileURLToPath, pathToFileURL } from 'node:url';
import electronUnhandled from 'electron-unhandled'; import electronUnhandled from 'electron-unhandled';
import { fileTypeFromFile } from 'file-type/node'; import { fileTypeFromFile } from 'file-type/node';
import type { Asyncify } from 'type-fest';
import logger from './logger.js'; import logger from './logger.js';
import menu from './menu.js'; import menu from './menu.js';
@ -131,6 +132,7 @@ function createWindow() {
nodeIntegration: true, nodeIntegration: true,
// https://github.com/electron/electron/issues/5107 // https://github.com/electron/electron/issues/5107
webSecurity: !isDev, webSecurity: !isDev,
preload: fileURLToPath(new URL('../preload/index.cjs', import.meta.url)),
}, },
backgroundColor: darkMode ? '#333' : '#fff', backgroundColor: darkMode ? '#333' : '#fff',
minWidth: 300, minWidth: 300,
@ -399,6 +401,23 @@ function sendOsNotification(options: NotificationConstructorOptions) {
} }
const remoteApi = { const remoteApi = {
pathExists,
downloadMediaUrl,
fileTypeFromFile,
focusWindow,
quitApp,
setProgressBar,
sendOsNotification,
};
export type RemoteApi = typeof remoteApi;
export type RemoteRpcApi = {
[K in keyof RemoteApi]: Asyncify<RemoteApi[K]>;
};
// using @electron/remote
const remoteApiLegacy = {
ffmpeg, ffmpeg,
i18n: i18nCommon, i18n: i18nCommon,
compatPlayer, compatPlayer,
@ -408,29 +427,32 @@ const remoteApi = {
isMac, isMac,
platform, platform,
arch, arch,
pathExists,
pathToFileURL,
downloadMediaUrl,
fileTypeFromFile,
isDev, isDev,
lossyMode, lossyMode,
focusWindow, pathToFileURL,
quitApp,
hasDisabledNetworking, hasDisabledNetworking,
setProgressBar,
sendOsNotification,
}; };
export type RemoteApi = typeof remoteApi; export type RemoteApiLegacy = typeof remoteApiLegacy;
// @ts-expect-error don't know how to type // @ts-expect-error don't know how to type
app.addListener('remote-require', (event: { returnValue: RemoteApi }, _webContents: unknown, moduleName: string) => { app.addListener('remote-require', (event: { returnValue: RemoteApiLegacy }, _webContents: unknown, moduleName: string) => {
if (moduleName === './index.js') { if (moduleName === './index.js') {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
event.returnValue = remoteApi; event.returnValue = remoteApiLegacy;
} }
}); });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ipcMain.handle('__electron_rpc__', async (_event, method: keyof RemoteApi, args: any[]) => {
const fn = remoteApi[method];
assert(fn, `Unknown API method: ${method}`);
// @ts-expect-error don't know how to type
return fn(...args);
});
// cannot top level await because app.whenReady will hang forever // cannot top level await because app.whenReady will hang forever
// eslint-disable-next-line unicorn/prefer-top-level-await // eslint-disable-next-line unicorn/prefer-top-level-await
init(); init();

@ -1,2 +1,20 @@
// todo import type { RemoteRpcApi } from '../main/index.ts';
console.log('preload');
// eslint-disable-next-line import/no-extraneous-dependencies, @typescript-eslint/no-var-requires
const { ipcRenderer } = require('electron');
const apiProxy = new Proxy(
{} as RemoteRpcApi,
{
get(_target, method: keyof RemoteRpcApi) {
return (...args: unknown[]) => (
ipcRenderer.invoke('__electron_rpc__', method, args)
);
},
},
);
// todo use contextBridge instead once we get rid of @electron/remote
// @ts-expect-error untyped
window.electron = apiProxy;
// contextBridge.exposeInMainWorld('electron', apiProxy);

@ -111,12 +111,13 @@ import useErrorHandling from './hooks/useErrorHandling';
import GenericDialog, { useDialog } from './components/GenericDialog'; import GenericDialog, { useDialog } from './components/GenericDialog';
import useHtml5ify from './hooks/useHtml5ify'; import useHtml5ify from './hooks/useHtml5ify';
import WhatsNew from './components/WhatsNew'; import WhatsNew from './components/WhatsNew';
import mainApi from './mainApi.js';
const electron = window.require('electron'); const electron = window.require('electron');
const { lstat } = window.require('fs/promises'); const { lstat } = window.require('fs/promises');
const { parse: parsePath, join: pathJoin, basename, dirname } = window.require('path'); const { parse: parsePath, join: pathJoin, basename, dirname } = window.require('path');
const { focusWindow, hasDisabledNetworking, quitApp, pathToFileURL, setProgressBar, sendOsNotification, lossyMode, pathExists } = window.require('@electron/remote').require('./index.js'); const { hasDisabledNetworking, pathToFileURL, lossyMode } = window.require('@electron/remote').require('./index.js');
const hevcPlaybackSupportedPromise = doesPlayerSupportHevcPlayback(); const hevcPlaybackSupportedPromise = doesPlayerSupportHevcPlayback();
@ -200,7 +201,9 @@ function App() {
useEffect(() => setDocumentTitle({ filePath, working: working?.text, progress }), [progress, filePath, working?.text]); useEffect(() => setDocumentTitle({ filePath, working: working?.text, progress }), [progress, filePath, working?.text]);
useEffect(() => setProgressBar(progress ?? -1), [progress]); useEffect(() => {
mainApi.setProgressBar(progress ?? -1);
}, [progress]);
useEffect(() => { useEffect(() => {
ffmpegSetCustomFfPath(customFfPath); ffmpegSetCustomFfPath(customFfPath);
@ -248,7 +251,7 @@ function App() {
const showOsNotification = useCallback((text: string) => { const showOsNotification = useCallback((text: string) => {
if (hideOsNotifications == null) { if (hideOsNotifications == null) {
sendOsNotification({ title: text }); mainApi.sendOsNotification({ title: text });
} }
}, [hideOsNotifications]); }, [hideOsNotifications]);
@ -1339,7 +1342,7 @@ function App() {
const loadMedia = useCallback(async ({ filePath: fp, projectPath }: { filePath: string, projectPath?: string | undefined }) => { const loadMedia = useCallback(async ({ filePath: fp, projectPath }: { filePath: string, projectPath?: string | undefined }) => {
async function tryOpenProjectPath(path: string) { async function tryOpenProjectPath(path: string) {
if (!(await pathExists(path))) return false; if (!(await mainApi.pathExists(path))) return false;
await loadEdlFile({ path, type: 'llc' }); await loadEdlFile({ path, type: 'llc' });
return true; return true;
} }
@ -1355,7 +1358,7 @@ function App() {
const sameDirEdlFilePath = getEdlFilePath(fp); const sameDirEdlFilePath = getEdlFilePath(fp);
// MAS only allows fs.access (pathExists) if we don't have access to input dir yet, so check first if the file exists, // MAS only allows fs.access (pathExists) if we don't have access to input dir yet, so check first if the file exists,
// so we don't need to annoy the user by asking for permission if the project file doesn't exist // so we don't need to annoy the user by asking for permission if the project file doesn't exist
if (await pathExists(sameDirEdlFilePath)) { if (await mainApi.pathExists(sameDirEdlFilePath)) {
// Ok, the file exists. now we have to ask the user, because we need to read that file // Ok, the file exists. now we have to ask the user, because we need to read that file
await ensureAccessToSourceDir(fp); await ensureAccessToSourceDir(fp);
// Ok, we got access from the user (or already have access), now read the project file // Ok, we got access from the user (or already have access), now read the project file
@ -1523,7 +1526,7 @@ function App() {
const mediaFilePath = pathJoin(dirname(path), mediaFileName); const mediaFilePath = pathJoin(dirname(path), mediaFileName);
// Note: MAS only allows fs.stat (pathExists) if we don't have access to input dir yet // Note: MAS only allows fs.stat (pathExists) if we don't have access to input dir yet
if (!(await pathExists(mediaFilePath))) { if (!(await mainApi.pathExists(mediaFilePath))) {
errorToast(i18n.t('The media file referenced by the project file you tried to open does not exist in the same directory as the project file: {{mediaFileName}}', { mediaFileName })); errorToast(i18n.t('The media file referenced by the project file you tried to open does not exist in the same directory as the project file: {{mediaFileName}}', { mediaFileName }));
return; return;
} }
@ -2063,7 +2066,7 @@ function App() {
toggleMuted, toggleMuted,
copySegmentsToClipboard, copySegmentsToClipboard,
reloadFile: () => setCacheBuster((v) => v + 1), reloadFile: () => setCacheBuster((v) => v + 1),
quit: () => quitApp(), quit: () => mainApi.quitApp(),
closeCurrentFile: () => { closeFileWithConfirm(); }, closeCurrentFile: () => { closeFileWithConfirm(); },
exportYouTube, exportYouTube,
showStreamsSelector: handleShowStreamsSelectorClick, showStreamsSelector: handleShowStreamsSelectorClick,
@ -2314,7 +2317,7 @@ function App() {
if (!ev.dataTransfer) return; if (!ev.dataTransfer) return;
await withErrorHandling(async () => { await withErrorHandling(async () => {
const filePaths = [...ev.dataTransfer.files].map((f) => electron.webUtils.getPathForFile(f)); const filePaths = [...ev.dataTransfer.files].map((f) => electron.webUtils.getPathForFile(f));
focusWindow(); await mainApi.focusWindow();
batchLoadPaths(filePaths, true); batchLoadPaths(filePaths, true);
}); });
}, [batchLoadPaths, withErrorHandling]); }, [batchLoadPaths, withErrorHandling]);
@ -2325,7 +2328,7 @@ function App() {
await withErrorHandling(async () => { await withErrorHandling(async () => {
const filePaths = [...ev.dataTransfer.files].map((f) => electron.webUtils.getPathForFile(f)); const filePaths = [...ev.dataTransfer.files].map((f) => electron.webUtils.getPathForFile(f));
if (filePaths.length !== 1) return; if (filePaths.length !== 1) return;
focusWindow(); await mainApi.focusWindow();
addStreamSourceFile(filePaths[0]!); addStreamSourceFile(filePaths[0]!);
}); });
}, [addStreamSourceFile, withErrorHandling]); }, [addStreamSourceFile, withErrorHandling]);
@ -2335,7 +2338,7 @@ function App() {
ev.preventDefault(); ev.preventDefault();
if (!ev.dataTransfer) return; if (!ev.dataTransfer) return;
const filePaths = [...ev.dataTransfer.files].map((f) => electron.webUtils.getPathForFile(f)); const filePaths = [...ev.dataTransfer.files].map((f) => electron.webUtils.getPathForFile(f));
focusWindow(); await mainApi.focusWindow();
userOpenFiles(filePaths); userOpenFiles(filePaths);
} }
const element = videoContainerRef.current; const element = videoContainerRef.current;

@ -15,11 +15,11 @@ import type { FindKeyframeMode } from '../ffmpeg';
import { dangerColor, primaryColor, warningColor } from '../colors'; import { dangerColor, primaryColor, warningColor } from '../colors';
import getSwal from '../swal'; import getSwal from '../swal';
import isDev from '../isDev'; import isDev from '../isDev';
import mainApi from '../mainApi';
const remote = window.require('@electron/remote'); const remote = window.require('@electron/remote');
const { dialog } = remote; const { dialog } = remote;
const { downloadMediaUrl } = remote.require('./index.js');
// https://github.com/mifi/lossless-cut/issues/1495 // https://github.com/mifi/lossless-cut/issues/1495
export const showOpenDialog = async ({ export const showOpenDialog = async ({
@ -561,7 +561,7 @@ export async function promptDownloadMediaUrl(outPath: string) {
if (!value) return false; if (!value) return false;
await downloadMediaUrl(value, outPath); await mainApi.downloadMediaUrl(value, outPath);
return true; return true;
} }

@ -12,8 +12,9 @@ import { isDurationValid } from './segments';
import type { FFprobeChapter, FFprobeFormat, FFprobeProbeResult, FFprobeStream } from '../../common/ffprobe'; import type { FFprobeChapter, FFprobeFormat, FFprobeProbeResult, FFprobeStream } from '../../common/ffprobe';
import { parseSrt, parseSrtToSegments } from './edlFormats'; import { parseSrt, parseSrtToSegments } from './edlFormats';
import { UnsupportedFileError, UserFacingError } from '../errors'; import { UnsupportedFileError, UserFacingError } from '../errors';
import mainApi from './mainApi';
const { ffmpeg, fileTypeFromFile } = window.require('@electron/remote').require('./index.js'); const { ffmpeg } = window.require('@electron/remote').require('./index.js');
const { renderWaveformPng, mapTimesToSegments, detectSceneChanges, captureFrames, captureFrameToFile, captureFrameToClipboard, getFfCommandLine, runFfmpegConcat, runFfmpegWithProgress, getDuration, abortFfmpegs, runFfmpeg, runFfprobe, getFfmpegPath, setCustomFfPath } = ffmpeg; const { renderWaveformPng, mapTimesToSegments, detectSceneChanges, captureFrames, captureFrameToFile, captureFrameToClipboard, getFfCommandLine, runFfmpegConcat, runFfmpegWithProgress, getDuration, abortFfmpegs, runFfmpeg, runFfprobe, getFfmpegPath, setCustomFfPath } = ffmpeg;
@ -268,7 +269,7 @@ async function determineSourceFileFormat(ffprobeFormatsStr: string | undefined,
// We need to test mp3 first because ffprobe seems to report the wrong format sometimes https://github.com/mifi/lossless-cut/issues/2129 // We need to test mp3 first because ffprobe seems to report the wrong format sometimes https://github.com/mifi/lossless-cut/issues/2129
if (firstFfprobeFormat === 'mp3') { if (firstFfprobeFormat === 'mp3') {
// file-type detects it correctly // file-type detects it correctly
const fileTypeResponse = await fileTypeFromFile(filePath); const fileTypeResponse = await mainApi.fileTypeFromFile(filePath);
if (fileTypeResponse?.mime === 'audio/mpeg') { if (fileTypeResponse?.mime === 'audio/mpeg') {
return 'mp2'; return 'mp2';
} }
@ -287,7 +288,7 @@ async function determineSourceFileFormat(ffprobeFormatsStr: string | undefined,
return firstFfprobeFormat; return firstFfprobeFormat;
} }
const fileTypeResponse = await fileTypeFromFile(filePath); const fileTypeResponse = await mainApi.fileTypeFromFile(filePath);
if (fileTypeResponse == null) { if (fileTypeResponse == null) {
console.warn('file-type failed to detect format, defaulting to first FFprobe detected format', ffprobeFormats); console.warn('file-type failed to detect format, defaulting to first FFprobe detected format', ffprobeFormats);
return firstFfprobeFormat; return firstFfprobeFormat;

@ -2,13 +2,17 @@ import { useCallback } from 'react';
import i18n from 'i18next'; import i18n from 'i18next';
import invariant from 'tiny-invariant'; import invariant from 'tiny-invariant';
import { getOutDir, getFileDir, checkDirWriteAccess, dirExists, isMasBuild } from '../util'; import { getOutDir, getFileDir, checkDirWriteAccess, isMasBuild } from '../util';
import { askForOutDir, askForInputDir } from '../dialogs'; import { askForOutDir, askForInputDir } from '../dialogs';
import { errorToast } from '../swal'; import { errorToast } from '../swal';
import { DirectoryAccessDeclinedError } from '../../errors'; import { DirectoryAccessDeclinedError } from '../../errors';
import mainApi from '../mainApi';
// import isDev from '../isDev'; // import isDev from '../isDev';
const { lstat } = window.require('fs/promises');
// MacOS App Store sandbox doesn't allow reading/writing anywhere, // MacOS App Store sandbox doesn't allow reading/writing anywhere,
// except those exact file paths that have been explicitly drag-dropped into LosslessCut or opened using the opener dialog // except those exact file paths that have been explicitly drag-dropped into LosslessCut or opened using the opener dialog
// Therefore we set the flag com.apple.security.files.user-selected.read-write // Therefore we set the flag com.apple.security.files.user-selected.read-write
@ -60,7 +64,7 @@ export default function useDirectoryAccess({ setCustomOutDir }: { setCustomOutDi
if (newCustomOutDir) { if (newCustomOutDir) {
// Reset if working directory doesn't exist anymore // Reset if working directory doesn't exist anymore
const customOutDirExists = await dirExists(newCustomOutDir); const customOutDirExists = (await mainApi.pathExists(newCustomOutDir)) && (await lstat(newCustomOutDir)).isDirectory();
if (!customOutDirExists) { if (!customOutDirExists) {
setCustomOutDir(undefined); setCustomOutDir(undefined);
newCustomOutDir = undefined; newCustomOutDir = undefined;

@ -15,10 +15,10 @@ import type { AvoidNegativeTs, Html5ifyMode, PreserveMetadata } from '../../../c
import type { AllFilesMeta, Chapter, CopyfileStreams, CustomTagsByFile, LiteFFprobeStream, ParamsByStreamId, SegmentToExport } from '../types'; import type { AllFilesMeta, Chapter, CopyfileStreams, CustomTagsByFile, LiteFFprobeStream, ParamsByStreamId, SegmentToExport } from '../types';
import type { LossyMode } from '../../../main'; import type { LossyMode } from '../../../main';
import { UserFacingError } from '../../errors'; import { UserFacingError } from '../../errors';
import mainApi from '../mainApi';
const { join, resolve, dirname } = window.require('path'); const { join, resolve, dirname } = window.require('path');
const { writeFile, mkdir, access, constants: { W_OK } } = window.require('fs/promises'); const { writeFile, mkdir, access, constants: { W_OK } } = window.require('fs/promises');
const { pathExists } = window.require('@electron/remote').require('./index.js');
export class OutputNotWritableError extends Error { export class OutputNotWritableError extends Error {
@ -94,7 +94,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
appendFfmpegCommandLog: (args: string[]) => void, appendFfmpegCommandLog: (args: string[]) => void,
}) { }) {
const shouldSkipExistingFile = useCallback(async (path: string) => { const shouldSkipExistingFile = useCallback(async (path: string) => {
const fileExists = await pathExists(path); const fileExists = await mainApi.pathExists(path);
// If output file exists, check that it is writable, so we can inform user if it's not (or else ffmpeg will fail with "Permission denied") // If output file exists, check that it is writable, so we can inform user if it's not (or else ffmpeg will fail with "Permission denied")
// this seems to sometimes happen on Windows, not sure why. // this seems to sometimes happen on Windows, not sure why.
@ -917,7 +917,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
let streamArgs: string[] = []; let streamArgs: string[] = [];
const outPaths = await pMap(outStreams, async ({ index, codec, type, format: { format, ext } }) => { const outPaths = await pMap(outStreams, async ({ index, codec, type, format: { format, ext } }) => {
const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `stream-${index}-${type}-${codec}.${ext}` }); const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `stream-${index}-${type}-${codec}.${ext}` });
if (!enableOverwriteOutput && await pathExists(outPath)) throw new RefuseOverwriteError(); if (!enableOverwriteOutput && await mainApi.pathExists(outPath)) throw new RefuseOverwriteError();
streamArgs = [ streamArgs = [
...streamArgs, ...streamArgs,
@ -953,7 +953,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
const ext = codec || 'bin'; const ext = codec || 'bin';
const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `stream-${index}-${type}-${codec}.${ext}` }); const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `stream-${index}-${type}-${codec}.${ext}` });
invariant(outPath != null); invariant(outPath != null);
if (!enableOverwriteOutput && await pathExists(outPath)) throw new RefuseOverwriteError(); if (!enableOverwriteOutput && await mainApi.pathExists(outPath)) throw new RefuseOverwriteError();
streamArgs = [ streamArgs = [
...streamArgs, ...streamArgs,

@ -27,7 +27,7 @@ import App from './App';
import ErrorBoundary from './ErrorBoundary'; import ErrorBoundary from './ErrorBoundary';
import './i18n'; import './i18n';
import { RemoteApi } from '../../main'; import { RemoteApiLegacy, RemoteRpcApi } from '../../main';
import './main.css'; import './main.css';
import './swal2.scss'; import './swal2.scss';
@ -35,7 +35,7 @@ import './swal2.scss';
type TypedRemote = Omit<typeof Remote, 'require'> & { type TypedRemote = Omit<typeof Remote, 'require'> & {
require: <T extends string>(module: T) => ( require: <T extends string>(module: T) => (
T extends './index.js' ? RemoteApi : T extends './index.js' ? RemoteApiLegacy :
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
any any
); );
@ -55,6 +55,7 @@ declare global {
T extends 'cue-parser' ? typeof cueParser : T extends 'cue-parser' ? typeof cueParser :
never never
); );
electron: RemoteRpcApi;
} }
interface Navigator { interface Navigator {
keyboard: { keyboard: {

@ -0,0 +1,3 @@
const mainApi = window.electron;
export default mainApi;

@ -18,7 +18,7 @@ const { dirname, parse: parsePath, join, extname, isAbsolute, resolve, basename
const { stat, lstat, readdir, utimes, unlink, open, access, constants: { R_OK, W_OK } } = window.require('fs/promises'); const { stat, lstat, readdir, utimes, unlink, open, access, constants: { R_OK, W_OK } } = window.require('fs/promises');
const { ipcRenderer } = window.require('electron'); const { ipcRenderer } = window.require('electron');
const remote = window.require('@electron/remote'); const remote = window.require('@electron/remote');
const { isWindows, isMac, pathExists } = remote.require('./index.js'); const { isWindows, isMac } = remote.require('./index.js');
const appVersion = remote.app.getVersion(); const appVersion = remote.app.getVersion();
const appPath = remote.app.getAppPath(); const appPath = remote.app.getAppPath();
@ -104,10 +104,6 @@ export async function getPathReadAccessError(pathIn: string) {
} }
} }
export async function dirExists(dirPath: string) {
return (await pathExists(dirPath)) && (await lstat(dirPath)).isDirectory();
}
// export const testFailFsOperation = isDev; // export const testFailFsOperation = isDev;
export const testFailFsOperation = false; export const testFailFsOperation = false;

Loading…
Cancel
Save