370 lines
12 KiB
TypeScript
370 lines
12 KiB
TypeScript
/* eslint-disable no-unused-vars */
|
|
/**
|
|
* @file The preload script
|
|
*
|
|
* The preload script runs in a renderer process before its web contents begin
|
|
* loading. During their execution they have access to a subset of Node.js APIs
|
|
* and imports. Its purpose is to expose the relevant imports and other
|
|
* functions as an object on the DOM, so that the renderer process can invoke
|
|
* functions that live in the main (Node.js) process if needed.
|
|
*
|
|
* Ref: https://www.electronjs.org/docs/latest/tutorial/tutorial-preload
|
|
*
|
|
* Note that this script cannot import other code from `src/` - conceptually it
|
|
* can be thought of as running in a separate, third, process different from
|
|
* both the main or a renderer process (technically, it runs in a BrowserWindow
|
|
* context that runs prior to the renderer process).
|
|
*
|
|
* > Since enabling the sandbox disables Node.js integration in your preload
|
|
* > scripts, you can no longer use require("../my-script"). In other words,
|
|
* > your preload script needs to be a single file.
|
|
* >
|
|
* > https://www.electronjs.org/blog/breach-to-barrier
|
|
*
|
|
* If we really wanted, we could setup a bundler to package this into a single
|
|
* file. However, since this is just boilerplate code providing a bridge between
|
|
* the main and renderer, we avoid introducing another moving part into the mix
|
|
* and just keep the entire preload setup in this single file.
|
|
*
|
|
* [Note: types.ts <-> preload.ts <-> ipc.ts]
|
|
*
|
|
* The following three files are boilerplatish linkage of the same functions,
|
|
* and when changing one of them, remember to see if the other two also need
|
|
* changing:
|
|
*
|
|
* - [renderer] web/packages/next/types/electron.ts contains docs
|
|
* - [preload] desktop/src/preload.ts ↕︎
|
|
* - [main] desktop/src/main/ipc.ts contains impl
|
|
*/
|
|
|
|
import { contextBridge, ipcRenderer, webUtils } from "electron/renderer";
|
|
|
|
// While we can't import other code, we can import types since they're just
|
|
// needed when compiling and will not be needed or looked around for at runtime.
|
|
import type {
|
|
AppUpdate,
|
|
CollectionMapping,
|
|
FolderWatch,
|
|
PendingUploads,
|
|
ZipItem,
|
|
} from "./types/ipc";
|
|
|
|
// - General
|
|
|
|
const appVersion = () => ipcRenderer.invoke("appVersion");
|
|
|
|
const logToDisk = (message: string): void =>
|
|
ipcRenderer.send("logToDisk", message);
|
|
|
|
const openDirectory = (dirPath: string) =>
|
|
ipcRenderer.invoke("openDirectory", dirPath);
|
|
|
|
const openLogDirectory = () => ipcRenderer.invoke("openLogDirectory");
|
|
|
|
const selectDirectory = () => ipcRenderer.invoke("selectDirectory");
|
|
|
|
const logout = () => {
|
|
watchRemoveListeners();
|
|
return ipcRenderer.invoke("logout");
|
|
};
|
|
|
|
const encryptionKey = () => ipcRenderer.invoke("encryptionKey");
|
|
|
|
const saveEncryptionKey = (encryptionKey: string) =>
|
|
ipcRenderer.invoke("saveEncryptionKey", encryptionKey);
|
|
|
|
const onMainWindowFocus = (cb?: () => void) => {
|
|
ipcRenderer.removeAllListeners("mainWindowFocus");
|
|
if (cb) ipcRenderer.on("mainWindowFocus", cb);
|
|
};
|
|
|
|
// - App update
|
|
|
|
const onAppUpdateAvailable = (
|
|
cb?: ((update: AppUpdate) => void) | undefined,
|
|
) => {
|
|
ipcRenderer.removeAllListeners("appUpdateAvailable");
|
|
if (cb) {
|
|
ipcRenderer.on("appUpdateAvailable", (_, update: AppUpdate) =>
|
|
cb(update),
|
|
);
|
|
}
|
|
};
|
|
|
|
const updateAndRestart = () => ipcRenderer.send("updateAndRestart");
|
|
|
|
const updateOnNextRestart = (version: string) =>
|
|
ipcRenderer.send("updateOnNextRestart", version);
|
|
|
|
const skipAppUpdate = (version: string) => {
|
|
ipcRenderer.send("skipAppUpdate", version);
|
|
};
|
|
|
|
// - FS
|
|
|
|
const fsExists = (path: string) => ipcRenderer.invoke("fsExists", path);
|
|
|
|
const fsMkdirIfNeeded = (dirPath: string) =>
|
|
ipcRenderer.invoke("fsMkdirIfNeeded", dirPath);
|
|
|
|
const fsRename = (oldPath: string, newPath: string) =>
|
|
ipcRenderer.invoke("fsRename", oldPath, newPath);
|
|
|
|
const fsRmdir = (path: string) => ipcRenderer.invoke("fsRmdir", path);
|
|
|
|
const fsRm = (path: string) => ipcRenderer.invoke("fsRm", path);
|
|
|
|
const fsReadTextFile = (path: string) =>
|
|
ipcRenderer.invoke("fsReadTextFile", path);
|
|
|
|
const fsWriteFile = (path: string, contents: string) =>
|
|
ipcRenderer.invoke("fsWriteFile", path, contents);
|
|
|
|
const fsIsDir = (dirPath: string) => ipcRenderer.invoke("fsIsDir", dirPath);
|
|
|
|
// - Conversion
|
|
|
|
const convertToJPEG = (imageData: Uint8Array) =>
|
|
ipcRenderer.invoke("convertToJPEG", imageData);
|
|
|
|
const generateImageThumbnail = (
|
|
dataOrPathOrZipItem: Uint8Array | string | ZipItem,
|
|
maxDimension: number,
|
|
maxSize: number,
|
|
) =>
|
|
ipcRenderer.invoke(
|
|
"generateImageThumbnail",
|
|
dataOrPathOrZipItem,
|
|
maxDimension,
|
|
maxSize,
|
|
);
|
|
|
|
const ffmpegExec = (
|
|
command: string[],
|
|
dataOrPathOrZipItem: Uint8Array | string | ZipItem,
|
|
outputFileExtension: string,
|
|
) =>
|
|
ipcRenderer.invoke(
|
|
"ffmpegExec",
|
|
command,
|
|
dataOrPathOrZipItem,
|
|
outputFileExtension,
|
|
);
|
|
|
|
// - ML
|
|
|
|
const computeCLIPImageEmbedding = (jpegImageData: Uint8Array) =>
|
|
ipcRenderer.invoke("computeCLIPImageEmbedding", jpegImageData);
|
|
|
|
const computeCLIPTextEmbeddingIfAvailable = (text: string) =>
|
|
ipcRenderer.invoke("computeCLIPTextEmbeddingIfAvailable", text);
|
|
|
|
const detectFaces = (input: Float32Array) =>
|
|
ipcRenderer.invoke("detectFaces", input);
|
|
|
|
const computeFaceEmbeddings = (input: Float32Array) =>
|
|
ipcRenderer.invoke("computeFaceEmbeddings", input);
|
|
|
|
// - Watch
|
|
|
|
const watchGet = () => ipcRenderer.invoke("watchGet");
|
|
|
|
const watchAdd = (folderPath: string, collectionMapping: CollectionMapping) =>
|
|
ipcRenderer.invoke("watchAdd", folderPath, collectionMapping);
|
|
|
|
const watchRemove = (folderPath: string) =>
|
|
ipcRenderer.invoke("watchRemove", folderPath);
|
|
|
|
const watchUpdateSyncedFiles = (
|
|
syncedFiles: FolderWatch["syncedFiles"],
|
|
folderPath: string,
|
|
) => ipcRenderer.invoke("watchUpdateSyncedFiles", syncedFiles, folderPath);
|
|
|
|
const watchUpdateIgnoredFiles = (
|
|
ignoredFiles: FolderWatch["ignoredFiles"],
|
|
folderPath: string,
|
|
) => ipcRenderer.invoke("watchUpdateIgnoredFiles", ignoredFiles, folderPath);
|
|
|
|
const watchOnAddFile = (f: (path: string, watch: FolderWatch) => void) => {
|
|
ipcRenderer.removeAllListeners("watchAddFile");
|
|
ipcRenderer.on("watchAddFile", (_, path: string, watch: FolderWatch) =>
|
|
f(path, watch),
|
|
);
|
|
};
|
|
|
|
const watchOnRemoveFile = (f: (path: string, watch: FolderWatch) => void) => {
|
|
ipcRenderer.removeAllListeners("watchRemoveFile");
|
|
ipcRenderer.on("watchRemoveFile", (_, path: string, watch: FolderWatch) =>
|
|
f(path, watch),
|
|
);
|
|
};
|
|
|
|
const watchOnRemoveDir = (f: (path: string, watch: FolderWatch) => void) => {
|
|
ipcRenderer.removeAllListeners("watchRemoveDir");
|
|
ipcRenderer.on("watchRemoveDir", (_, path: string, watch: FolderWatch) =>
|
|
f(path, watch),
|
|
);
|
|
};
|
|
|
|
const watchFindFiles = (folderPath: string) =>
|
|
ipcRenderer.invoke("watchFindFiles", folderPath);
|
|
|
|
const watchRemoveListeners = () => {
|
|
ipcRenderer.removeAllListeners("watchAddFile");
|
|
ipcRenderer.removeAllListeners("watchRemoveFile");
|
|
ipcRenderer.removeAllListeners("watchRemoveDir");
|
|
};
|
|
|
|
// - Upload
|
|
|
|
const pathForFile = (file: File) => {
|
|
const path = webUtils.getPathForFile(file);
|
|
// The path that we get back from `webUtils.getPathForFile` on Windows uses
|
|
// "/" as the path separator. Convert them to POSIX separators.
|
|
//
|
|
// Note that we do not have access to the path or the os module in the
|
|
// preload script, thus this hand rolled transformation.
|
|
|
|
// However that makes TypeScript fidgety since we it cannot find navigator,
|
|
// as we haven't included "lib": ["dom"] in our tsconfig to avoid making DOM
|
|
// APIs available to our main Node.js code. We could create a separate
|
|
// tsconfig just for the preload script, but for now let's go with a cast.
|
|
//
|
|
// @ts-expect-error navigator is not defined.
|
|
const platform = (navigator as { platform: string }).platform;
|
|
return platform.toLowerCase().includes("win")
|
|
? path.split("\\").join("/")
|
|
: path;
|
|
};
|
|
|
|
const listZipItems = (zipPath: string) =>
|
|
ipcRenderer.invoke("listZipItems", zipPath);
|
|
|
|
const pathOrZipItemSize = (pathOrZipItem: string | ZipItem) =>
|
|
ipcRenderer.invoke("pathOrZipItemSize", pathOrZipItem);
|
|
|
|
const pendingUploads = () => ipcRenderer.invoke("pendingUploads");
|
|
|
|
const setPendingUploads = (pendingUploads: PendingUploads) =>
|
|
ipcRenderer.invoke("setPendingUploads", pendingUploads);
|
|
|
|
const markUploadedFiles = (paths: PendingUploads["filePaths"]) =>
|
|
ipcRenderer.invoke("markUploadedFiles", paths);
|
|
|
|
const markUploadedZipItems = (items: PendingUploads["zipItems"]) =>
|
|
ipcRenderer.invoke("markUploadedZipItems", items);
|
|
|
|
const clearPendingUploads = () => ipcRenderer.invoke("clearPendingUploads");
|
|
|
|
/**
|
|
* These objects exposed here will become available to the JS code in our
|
|
* renderer (the web/ code) as `window.ElectronAPIs.*`
|
|
*
|
|
* There are a few related concepts at play here, and it might be worthwhile to
|
|
* read their (excellent) documentation to get an understanding;
|
|
*`
|
|
* - ContextIsolation:
|
|
* https://www.electronjs.org/docs/latest/tutorial/context-isolation
|
|
*
|
|
* - IPC https://www.electronjs.org/docs/latest/tutorial/ipc
|
|
*
|
|
* ---
|
|
*
|
|
* [Note: Transferring large amount of data over IPC]
|
|
*
|
|
* Electron's IPC implementation uses the HTML standard Structured Clone
|
|
* Algorithm to serialize objects passed between processes.
|
|
* https://www.electronjs.org/docs/latest/tutorial/ipc#object-serialization
|
|
*
|
|
* In particular, ArrayBuffer is eligible for structured cloning.
|
|
* https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
|
|
*
|
|
* Also, ArrayBuffer is "transferable", which means it is a zero-copy operation
|
|
* operation when it happens across threads.
|
|
* https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects
|
|
*
|
|
* In our case though, we're not dealing with threads but separate processes. So
|
|
* the ArrayBuffer will be copied:
|
|
*
|
|
* > "parameters, errors and return values are **copied** when they're sent over
|
|
* > the bridge".
|
|
* >
|
|
* > https://www.electronjs.org/docs/latest/api/context-bridge#methods
|
|
*
|
|
* The copy itself is relatively fast, but the problem with transfering large
|
|
* amounts of data is potentially running out of memory during the copy.
|
|
*
|
|
* For an alternative, see [Note: IPC streams].
|
|
*/
|
|
contextBridge.exposeInMainWorld("electron", {
|
|
// - General
|
|
|
|
appVersion,
|
|
logToDisk,
|
|
openDirectory,
|
|
openLogDirectory,
|
|
selectDirectory,
|
|
logout,
|
|
encryptionKey,
|
|
saveEncryptionKey,
|
|
onMainWindowFocus,
|
|
|
|
// - App update
|
|
|
|
onAppUpdateAvailable,
|
|
updateAndRestart,
|
|
updateOnNextRestart,
|
|
skipAppUpdate,
|
|
|
|
// - FS
|
|
|
|
fs: {
|
|
exists: fsExists,
|
|
rename: fsRename,
|
|
mkdirIfNeeded: fsMkdirIfNeeded,
|
|
rmdir: fsRmdir,
|
|
rm: fsRm,
|
|
readTextFile: fsReadTextFile,
|
|
writeFile: fsWriteFile,
|
|
isDir: fsIsDir,
|
|
},
|
|
|
|
// - Conversion
|
|
|
|
convertToJPEG,
|
|
generateImageThumbnail,
|
|
ffmpegExec,
|
|
|
|
// - ML
|
|
|
|
computeCLIPImageEmbedding,
|
|
computeCLIPTextEmbeddingIfAvailable,
|
|
detectFaces,
|
|
computeFaceEmbeddings,
|
|
|
|
// - Watch
|
|
|
|
watch: {
|
|
get: watchGet,
|
|
add: watchAdd,
|
|
remove: watchRemove,
|
|
updateSyncedFiles: watchUpdateSyncedFiles,
|
|
updateIgnoredFiles: watchUpdateIgnoredFiles,
|
|
onAddFile: watchOnAddFile,
|
|
onRemoveFile: watchOnRemoveFile,
|
|
onRemoveDir: watchOnRemoveDir,
|
|
findFiles: watchFindFiles,
|
|
},
|
|
|
|
// - Upload
|
|
|
|
pathForFile,
|
|
listZipItems,
|
|
pathOrZipItemSize,
|
|
pendingUploads,
|
|
setPendingUploads,
|
|
markUploadedFiles,
|
|
markUploadedZipItems,
|
|
clearPendingUploads,
|
|
});
|