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 timers from 'node:timers/promises';
import { z } from 'zod';
import { pathToFileURL } from 'node:url';
import { fileURLToPath, pathToFileURL } from 'node:url';
import electronUnhandled from 'electron-unhandled';
import { fileTypeFromFile } from 'file-type/node';
import type { Asyncify } from 'type-fest';
import logger from './logger.js';
import menu from './menu.js';
@ -131,6 +132,7 @@ function createWindow() {
nodeIntegration: true,
// https://github.com/electron/electron/issues/5107
webSecurity: !isDev,
preload: fileURLToPath(new URL('../preload/index.cjs', import.meta.url)),
},
backgroundColor: darkMode ? '#333' : '#fff',
minWidth: 300,
@ -399,6 +401,23 @@ function sendOsNotification(options: NotificationConstructorOptions) {
}
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,
i18n: i18nCommon,
compatPlayer,
@ -408,29 +427,32 @@ const remoteApi = {
isMac,
platform,
arch,
pathExists,
pathToFileURL,
downloadMediaUrl,
fileTypeFromFile,
isDev,
lossyMode,
focusWindow,
quitApp,
pathToFileURL,
hasDisabledNetworking,
setProgressBar,
sendOsNotification,
};
export type RemoteApi = typeof remoteApi;
export type RemoteApiLegacy = typeof remoteApiLegacy;
// @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') {
// 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
// eslint-disable-next-line unicorn/prefer-top-level-await
init();

@ -1,2 +1,20 @@
// todo
console.log('preload');
import type { RemoteRpcApi } from '../main/index.ts';
// 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 useHtml5ify from './hooks/useHtml5ify';
import WhatsNew from './components/WhatsNew';
import mainApi from './mainApi.js';
const electron = window.require('electron');
const { lstat } = window.require('fs/promises');
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();
@ -200,7 +201,9 @@ function App() {
useEffect(() => setDocumentTitle({ filePath, working: working?.text, progress }), [progress, filePath, working?.text]);
useEffect(() => setProgressBar(progress ?? -1), [progress]);
useEffect(() => {
mainApi.setProgressBar(progress ?? -1);
}, [progress]);
useEffect(() => {
ffmpegSetCustomFfPath(customFfPath);
@ -248,7 +251,7 @@ function App() {
const showOsNotification = useCallback((text: string) => {
if (hideOsNotifications == null) {
sendOsNotification({ title: text });
mainApi.sendOsNotification({ title: text });
}
}, [hideOsNotifications]);
@ -1339,7 +1342,7 @@ function App() {
const loadMedia = useCallback(async ({ filePath: fp, projectPath }: { filePath: string, projectPath?: string | undefined }) => {
async function tryOpenProjectPath(path: string) {
if (!(await pathExists(path))) return false;
if (!(await mainApi.pathExists(path))) return false;
await loadEdlFile({ path, type: 'llc' });
return true;
}
@ -1355,7 +1358,7 @@ function App() {
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,
// 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
await ensureAccessToSourceDir(fp);
// 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);
// 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 }));
return;
}
@ -2063,7 +2066,7 @@ function App() {
toggleMuted,
copySegmentsToClipboard,
reloadFile: () => setCacheBuster((v) => v + 1),
quit: () => quitApp(),
quit: () => mainApi.quitApp(),
closeCurrentFile: () => { closeFileWithConfirm(); },
exportYouTube,
showStreamsSelector: handleShowStreamsSelectorClick,
@ -2314,7 +2317,7 @@ function App() {
if (!ev.dataTransfer) return;
await withErrorHandling(async () => {
const filePaths = [...ev.dataTransfer.files].map((f) => electron.webUtils.getPathForFile(f));
focusWindow();
await mainApi.focusWindow();
batchLoadPaths(filePaths, true);
});
}, [batchLoadPaths, withErrorHandling]);
@ -2325,7 +2328,7 @@ function App() {
await withErrorHandling(async () => {
const filePaths = [...ev.dataTransfer.files].map((f) => electron.webUtils.getPathForFile(f));
if (filePaths.length !== 1) return;
focusWindow();
await mainApi.focusWindow();
addStreamSourceFile(filePaths[0]!);
});
}, [addStreamSourceFile, withErrorHandling]);
@ -2335,7 +2338,7 @@ function App() {
ev.preventDefault();
if (!ev.dataTransfer) return;
const filePaths = [...ev.dataTransfer.files].map((f) => electron.webUtils.getPathForFile(f));
focusWindow();
await mainApi.focusWindow();
userOpenFiles(filePaths);
}
const element = videoContainerRef.current;

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

@ -12,8 +12,9 @@ import { isDurationValid } from './segments';
import type { FFprobeChapter, FFprobeFormat, FFprobeProbeResult, FFprobeStream } from '../../common/ffprobe';
import { parseSrt, parseSrtToSegments } from './edlFormats';
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;
@ -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
if (firstFfprobeFormat === 'mp3') {
// file-type detects it correctly
const fileTypeResponse = await fileTypeFromFile(filePath);
const fileTypeResponse = await mainApi.fileTypeFromFile(filePath);
if (fileTypeResponse?.mime === 'audio/mpeg') {
return 'mp2';
}
@ -287,7 +288,7 @@ async function determineSourceFileFormat(ffprobeFormatsStr: string | undefined,
return firstFfprobeFormat;
}
const fileTypeResponse = await fileTypeFromFile(filePath);
const fileTypeResponse = await mainApi.fileTypeFromFile(filePath);
if (fileTypeResponse == null) {
console.warn('file-type failed to detect format, defaulting to first FFprobe detected format', ffprobeFormats);
return firstFfprobeFormat;

@ -2,13 +2,17 @@ import { useCallback } from 'react';
import i18n from 'i18next';
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 { errorToast } from '../swal';
import { DirectoryAccessDeclinedError } from '../../errors';
import mainApi from '../mainApi';
// import isDev from '../isDev';
const { lstat } = window.require('fs/promises');
// 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
// 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) {
// 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) {
setCustomOutDir(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 { LossyMode } from '../../../main';
import { UserFacingError } from '../../errors';
import mainApi from '../mainApi';
const { join, resolve, dirname } = window.require('path');
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 {
@ -94,7 +94,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
appendFfmpegCommandLog: (args: string[]) => void,
}) {
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")
// this seems to sometimes happen on Windows, not sure why.
@ -917,7 +917,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
let streamArgs: string[] = [];
const outPaths = await pMap(outStreams, async ({ index, codec, type, format: { format, 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,
@ -953,7 +953,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
const ext = codec || 'bin';
const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `stream-${index}-${type}-${codec}.${ext}` });
invariant(outPath != null);
if (!enableOverwriteOutput && await pathExists(outPath)) throw new RefuseOverwriteError();
if (!enableOverwriteOutput && await mainApi.pathExists(outPath)) throw new RefuseOverwriteError();
streamArgs = [
...streamArgs,

@ -27,7 +27,7 @@ import App from './App';
import ErrorBoundary from './ErrorBoundary';
import './i18n';
import { RemoteApi } from '../../main';
import { RemoteApiLegacy, RemoteRpcApi } from '../../main';
import './main.css';
import './swal2.scss';
@ -35,7 +35,7 @@ import './swal2.scss';
type TypedRemote = Omit<typeof Remote, 'require'> & {
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
any
);
@ -55,6 +55,7 @@ declare global {
T extends 'cue-parser' ? typeof cueParser :
never
);
electron: RemoteRpcApi;
}
interface Navigator {
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 { ipcRenderer } = window.require('electron');
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 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 = false;

Loading…
Cancel
Save