mirror of
https://github.com/koodo-reader/koodo-reader.git
synced 2026-09-13 06:17:19 -04:00
284 lines
8.6 KiB
JavaScript
284 lines
8.6 KiB
JavaScript
const { contextBridge, ipcRenderer } = require("electron");
|
|
|
|
const INVOKE_CHANNELS = new Set([
|
|
"cancel-download-app",
|
|
"discord-rpc-update",
|
|
"discord-rpc-clear",
|
|
"update-win-app",
|
|
"open-book",
|
|
"generate-tts",
|
|
"get-tts-voices",
|
|
"cloud-upload",
|
|
"cloud-download",
|
|
"cloud-progress",
|
|
"picker-download",
|
|
"picker-progress",
|
|
"cloud-reset",
|
|
"cloud-stats",
|
|
"cloud-delete",
|
|
"cloud-list",
|
|
"picker-list",
|
|
"cloud-exist",
|
|
"cloud-close",
|
|
"clear-tts",
|
|
"select-path",
|
|
"select-file",
|
|
"select-import-file",
|
|
"encrypt-data",
|
|
"decrypt-data",
|
|
"check-cloud-url",
|
|
"get-proxy-config",
|
|
"set-proxy-config",
|
|
"test-proxy-connection",
|
|
"get-mac",
|
|
"get-device-name",
|
|
"get-biometric-capability",
|
|
"prompt-biometric-auth",
|
|
"reset-reader-position",
|
|
"reset-main-position",
|
|
"select-zip-file",
|
|
"select-book",
|
|
"custom-database-command",
|
|
"database-command",
|
|
"close-database",
|
|
"set-always-on-top",
|
|
"set-auto-maximize",
|
|
"toggle-auto-launch",
|
|
"toggle-minimize-to-tray",
|
|
"open-explorer-folder",
|
|
"get-debug-logs",
|
|
"hide-reader",
|
|
"open-console",
|
|
"reload-reader",
|
|
"reload-main",
|
|
"new-chat",
|
|
"clear-all-data",
|
|
"new-tab",
|
|
"reload-tab",
|
|
"adjust-tab-size",
|
|
"exit-tab",
|
|
"enter-tab-fullscreen",
|
|
"exit-tab-fullscreen",
|
|
"enter-fullscreen",
|
|
"exit-fullscreen",
|
|
"open-url",
|
|
"switch-moyu",
|
|
"set-native-theme-source",
|
|
"system-ocr",
|
|
"file-command",
|
|
"open-external",
|
|
"dict-lookup",
|
|
"partial-md5",
|
|
"crypto-file-md5",
|
|
"backup-path",
|
|
"restore-path",
|
|
"list-tar-file",
|
|
"untar-file",
|
|
"list-zip-file",
|
|
"unzip-file",
|
|
]);
|
|
const SEND_CHANNELS = new Set(["reader-close-ready", "tab-close-ready"]);
|
|
const SEND_SYNC_CHANNELS = new Set([
|
|
"storage-location",
|
|
"url-window-status",
|
|
"get-dirname",
|
|
"system-color",
|
|
"get-file-data",
|
|
"check-file-data",
|
|
"user-data",
|
|
"file-command-sync",
|
|
"clipboard-read-text-sync",
|
|
]);
|
|
const EVENT_CHANNELS = new Set([
|
|
"oauth-callback",
|
|
"before-reader-close",
|
|
"before-tab-close",
|
|
"reading-finished",
|
|
"chat-message",
|
|
"import-url-from-link",
|
|
"open-book-from-link",
|
|
"open-note-from-link",
|
|
"picker-finished",
|
|
"download-app-progress",
|
|
"backup-progress",
|
|
"restore-progress",
|
|
]);
|
|
|
|
const assertChannel = (set, channel) => {
|
|
if (!set.has(channel))
|
|
throw new Error(`IPC channel is not allowed: ${channel}`);
|
|
};
|
|
const invoke = (channel, ...args) => {
|
|
assertChannel(INVOKE_CHANNELS, channel);
|
|
return ipcRenderer.invoke(channel, ...args);
|
|
};
|
|
const send = (channel, ...args) => {
|
|
assertChannel(SEND_CHANNELS, channel);
|
|
return ipcRenderer.send(channel, ...args);
|
|
};
|
|
const sendSync = (channel, ...args) => {
|
|
assertChannel(SEND_SYNC_CHANNELS, channel);
|
|
return ipcRenderer.sendSync(channel, ...args);
|
|
};
|
|
|
|
const eventHandlers = new Map();
|
|
const listen = (channel, listener, once = false) => {
|
|
assertChannel(EVENT_CHANNELS, channel);
|
|
const handler = (_event, payload) => {
|
|
if (once) removeListener(channel, listener, handler);
|
|
listener(payload);
|
|
};
|
|
const handlers = eventHandlers.get(channel) || new Map();
|
|
const listeners = handlers.get(listener) || [];
|
|
listeners.push(handler);
|
|
handlers.set(listener, listeners);
|
|
eventHandlers.set(channel, handlers);
|
|
ipcRenderer[once ? "once" : "on"](channel, handler);
|
|
return () => removeListener(channel, listener, handler);
|
|
};
|
|
const removeListener = (channel, listener, handler) => {
|
|
assertChannel(EVENT_CHANNELS, channel);
|
|
const handlers = eventHandlers.get(channel);
|
|
const listeners = handlers && handlers.get(listener);
|
|
const target = handler || (listeners && listeners[listeners.length - 1]);
|
|
if (target) {
|
|
ipcRenderer.removeListener(channel, target);
|
|
if (listeners) {
|
|
const index = listeners.indexOf(target);
|
|
if (index >= 0) listeners.splice(index, 1);
|
|
if (listeners.length === 0) handlers.delete(listener);
|
|
}
|
|
}
|
|
};
|
|
|
|
const fileArgs = (args) => (args && typeof args === "object" ? args : {});
|
|
const fileSync = (operation, args) => {
|
|
const result = sendSync("file-command-sync", {
|
|
operation,
|
|
...fileArgs(args),
|
|
});
|
|
if (!result || result.ok !== true) {
|
|
const error = new Error(result?.error?.message || "File command failed");
|
|
if (result?.error?.code) error.code = result.error.code;
|
|
throw error;
|
|
}
|
|
return result.value;
|
|
};
|
|
const fileInvoke = (operation, args) =>
|
|
invoke("file-command", { operation, ...fileArgs(args) });
|
|
const bytes = (value) => {
|
|
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
if (ArrayBuffer.isView(value))
|
|
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
return value;
|
|
};
|
|
const callbackResult = (promise, callback) => {
|
|
promise
|
|
.then((value) => callback(null, bytes(value)))
|
|
.catch((error) => callback(error));
|
|
};
|
|
|
|
const file = {
|
|
existsSync: (filePath) => fileSync("exists", { path: filePath }),
|
|
mkdirSync: (filePath, options) =>
|
|
fileSync("mkdir", { path: filePath, options }),
|
|
writeFileSync: (filePath, data, options) =>
|
|
fileSync("write", { path: filePath, data: bytes(data), options }),
|
|
appendFileSync: (filePath, data, options) =>
|
|
fileSync("append", { path: filePath, data: bytes(data), options }),
|
|
readFileSync: (filePath, options) =>
|
|
bytes(fileSync("read", { path: filePath, options })),
|
|
readdirSync: (filePath, options) =>
|
|
fileSync("readdir", { path: filePath, options }),
|
|
statSync: (filePath) => fileSync("stat", { path: filePath }),
|
|
unlinkSync: (filePath) => fileSync("unlink", { path: filePath }),
|
|
copyFileSync: (source, destination) =>
|
|
fileSync("copyFile", { source, destination }),
|
|
renameSync: (source, destination) =>
|
|
fileSync("rename", { source, destination }),
|
|
rmSync: (filePath, options) => fileSync("rm", { path: filePath, options }),
|
|
emptyDirSync: (filePath) => fileSync("emptyDir", { path: filePath }),
|
|
copy: (source, destination) => fileInvoke("copy", { source, destination }),
|
|
rm: (filePath, options) => fileInvoke("rm", { path: filePath, options }),
|
|
readFile: (filePath, options, callback) => {
|
|
const cb = typeof options === "function" ? options : callback;
|
|
const readOptions = typeof options === "function" ? undefined : options;
|
|
const promise = fileInvoke("read", {
|
|
path: filePath,
|
|
options: readOptions,
|
|
});
|
|
if (cb) callbackResult(promise, cb);
|
|
return cb ? undefined : promise.then(bytes);
|
|
},
|
|
writeFile: (filePath, data, options, callback) => {
|
|
const cb = typeof options === "function" ? options : callback;
|
|
const writeOptions = typeof options === "function" ? undefined : options;
|
|
const promise = fileInvoke("write", {
|
|
path: filePath,
|
|
data: bytes(data),
|
|
options: writeOptions,
|
|
});
|
|
if (cb) promise.then(() => cb(null)).catch((error) => cb(error));
|
|
return cb ? undefined : promise;
|
|
},
|
|
promises: {
|
|
readFile: (filePath, options) =>
|
|
fileInvoke("read", { path: filePath, options }).then(bytes),
|
|
readdir: (filePath, options) =>
|
|
fileInvoke("readdir", { path: filePath, options }),
|
|
stat: (filePath) => fileInvoke("stat", { path: filePath }),
|
|
mkdir: (filePath, options) =>
|
|
fileInvoke("mkdir", { path: filePath, options }),
|
|
},
|
|
};
|
|
|
|
const nodeSync = (operation, args = {}) => {
|
|
const result = ipcRenderer.sendSync("node-command-sync", {
|
|
operation,
|
|
...args,
|
|
});
|
|
if (!result || result.ok !== true) {
|
|
const error = new Error(result?.error?.message || "Node command failed");
|
|
if (result?.error?.code) error.code = result.error.code;
|
|
throw error;
|
|
}
|
|
return result.value;
|
|
};
|
|
const pathApi = {
|
|
join: (...parts) => nodeSync("path-join", { values: parts }),
|
|
dirname: (value) => nodeSync("path-dirname", { value }),
|
|
basename: (value, suffix) => nodeSync("path-basename", { value, suffix }),
|
|
extname: (value) => nodeSync("path-extname", { value }),
|
|
resolve: (...parts) => nodeSync("path-resolve", { values: parts }),
|
|
posix: { join: (...parts) => nodeSync("path-posix-join", { values: parts }) },
|
|
};
|
|
const cryptoApi = {
|
|
md5: (data) => nodeSync("crypto-md5", { data: bytes(data) }),
|
|
partialMd5: (filePath) => invoke("partial-md5", filePath),
|
|
fileMd5: (filePath) => invoke("crypto-file-md5", filePath),
|
|
};
|
|
|
|
contextBridge.exposeInMainWorld("electronAPI", {
|
|
invoke,
|
|
send,
|
|
sendSync,
|
|
on: (channel, listener) => listen(channel, listener),
|
|
once: (channel, listener) => listen(channel, listener, true),
|
|
removeListener,
|
|
fs: file,
|
|
path: pathApi,
|
|
os: {
|
|
platform: () => process.platform,
|
|
homedir: () => nodeSync("os-homedir"),
|
|
},
|
|
runtime: {
|
|
platform: process.platform,
|
|
windowsStore: process.windowsStore === true,
|
|
},
|
|
crypto: cryptoApi,
|
|
shell: { openExternal: (url) => invoke("open-external", url) },
|
|
clipboard: {
|
|
readText: () => sendSync("clipboard-read-text-sync"),
|
|
},
|
|
});
|