mirror of
https://github.com/koodo-reader/koodo-reader.git
synced 2025-12-23 15:07:55 -05:00
- Updated PopupBox to send menuMode type when checking URL window status. - Modified PopupDict and PopupTrans to include a type parameter when opening external URLs. - Refactored Header component to ensure 'isFinshReading' is set correctly and added logging for sync operations. - Improved AccountSetting component to streamline login and binding processes, ensuring proper error handling and user feedback. - Enhanced openExternalUrl utility to accept a type parameter for better URL handling in Electron. - Added console logging for configuration string during backup to assist in debugging.
1011 lines
33 KiB
JavaScript
1011 lines
33 KiB
JavaScript
const {
|
||
app,
|
||
BrowserWindow,
|
||
WebContentsView,
|
||
Menu,
|
||
ipcMain,
|
||
dialog,
|
||
powerSaveBlocker,
|
||
nativeTheme,
|
||
protocol,
|
||
screen
|
||
} = require("electron");
|
||
const path = require("path");
|
||
const isDev = require("electron-is-dev");
|
||
const Store = require("electron-store");
|
||
const log = require('electron-log/main');
|
||
const os = require("os");
|
||
const store = new Store();
|
||
const fs = require("fs");
|
||
const configDir = app.getPath("userData");
|
||
const dirPath = path.join(configDir, "uploads");
|
||
const packageJson = require("./package.json");
|
||
let mainWin;
|
||
let readerWindow;
|
||
let readerWindowList = []
|
||
let dictWindow;
|
||
let transWindow;
|
||
let linkWindow;
|
||
let mainView;
|
||
//multi tab
|
||
// let mainViewList = []
|
||
let chatWindow;
|
||
let googlePickerView;
|
||
let dbConnection = {};
|
||
let syncUtilCache = {};
|
||
let pickerUtilCache = {};
|
||
let downloadRequest = null;
|
||
const singleInstance = app.requestSingleInstanceLock();
|
||
var filePath = null;
|
||
if (process.platform != "darwin" && process.argv.length >= 2) {
|
||
filePath = process.argv[1];
|
||
}
|
||
log.transports.file.fileName = "debug.log";
|
||
log.transports.file.maxSize = 1024 * 1024; // 1MB
|
||
log.initialize();
|
||
store.set(
|
||
"appVersion", packageJson.version,
|
||
);
|
||
store.set(
|
||
"appPlatform", os.platform() + " " + os.release(),
|
||
);
|
||
const mainWinDisplayScale = store.get("mainWinDisplayScale") || 1
|
||
let options = {
|
||
width: parseInt(store.get("mainWinWidth") || 1050) / mainWinDisplayScale,
|
||
height: parseInt(store.get("mainWinHeight") || 660) / mainWinDisplayScale,
|
||
x: parseInt(store.get("mainWinX")),
|
||
y: parseInt(store.get("mainWinY")),
|
||
backgroundColor: '#fff',
|
||
webPreferences: {
|
||
webSecurity: false,
|
||
nodeIntegration: true,
|
||
contextIsolation: false,
|
||
nativeWindowOpen: true,
|
||
nodeIntegrationInSubFrames: false,
|
||
allowRunningInsecureContent: false,
|
||
enableRemoteModule: true,
|
||
sandbox: false,
|
||
},
|
||
};
|
||
const Database = require("better-sqlite3");
|
||
if (os.platform() === 'linux') {
|
||
options = Object.assign({}, options, {
|
||
icon: path.join(__dirname, "./build/assets/icon.png"),
|
||
});
|
||
}
|
||
// Single Instance Lock
|
||
if (!singleInstance) {
|
||
app.quit();
|
||
} else {
|
||
app.on("second-instance", (event, argv, workingDir) => {
|
||
if (mainWin) {
|
||
if (!mainWin.isVisible()) mainWin.show();
|
||
mainWin.focus();
|
||
}
|
||
});
|
||
}
|
||
if (filePath) {
|
||
// Make sure the directory exists
|
||
if (!fs.existsSync(dirPath)) {
|
||
fs.mkdirSync(dirPath, { recursive: true });
|
||
}
|
||
fs.writeFileSync(
|
||
path.join(dirPath, "log.json"),
|
||
JSON.stringify({ filePath }),
|
||
"utf-8"
|
||
);
|
||
}
|
||
const getDBConnection = (dbName, storagePath, sqlStatement) => {
|
||
if (!dbConnection[dbName]) {
|
||
if (!fs.existsSync(path.join(storagePath, "config"))) {
|
||
fs.mkdirSync(path.join(storagePath, "config"), { recursive: true });
|
||
}
|
||
dbConnection[dbName] = new Database(path.join(storagePath, "config", `${dbName}.db`), {});
|
||
dbConnection[dbName].pragma('journal_mode = WAL');
|
||
dbConnection[dbName].exec(sqlStatement["createTableStatement"][dbName]);
|
||
}
|
||
return dbConnection[dbName];
|
||
}
|
||
const getSyncUtil = async (config, isUseCache = true) => {
|
||
if (!isUseCache || !syncUtilCache[config.service]) {
|
||
const { SyncUtil, TokenService, ConfigService, ThirdpartyRequest } = await import('./src/assets/lib/kookit-extra.min.mjs');
|
||
let thirdpartyRequest = new ThirdpartyRequest(TokenService, ConfigService);
|
||
|
||
syncUtilCache[config.service] = new SyncUtil(config.service, config, config.storagePath, thirdpartyRequest);
|
||
}
|
||
return syncUtilCache[config.service];
|
||
}
|
||
const removeSyncUtil = (config) => {
|
||
delete syncUtilCache[config.service];
|
||
}
|
||
const getPickerUtil = async (config, isUseCache = true) => {
|
||
if (!isUseCache || !pickerUtilCache[config.service]) {
|
||
const { SyncUtil, TokenService, ThirdpartyRequest, ConfigService } = await import('./src/assets/lib/kookit-extra.min.mjs');
|
||
let thirdpartyRequest = new ThirdpartyRequest(TokenService, ConfigService);
|
||
|
||
pickerUtilCache[config.service] = new SyncUtil(config.service, config, config.storagePath, thirdpartyRequest);
|
||
}
|
||
return pickerUtilCache[config.service];
|
||
}
|
||
const removePickerUtil = (config) => {
|
||
if (pickerUtilCache[config.service]) {
|
||
pickerUtilCache[config.service] = null;
|
||
}
|
||
}
|
||
// Simple encryption function
|
||
const encrypt = (text, key) => {
|
||
let result = "";
|
||
for (let i = 0; i < text.length; i++) {
|
||
const charCode = text.charCodeAt(i) ^ key.charCodeAt(i % key.length);
|
||
result += String.fromCharCode(charCode);
|
||
}
|
||
return Buffer.from(result).toString("base64");
|
||
}
|
||
|
||
// Simple decryption function
|
||
const decrypt = (encryptedText, key) => {
|
||
const buff = Buffer.from(encryptedText, "base64").toString();
|
||
let result = "";
|
||
for (let i = 0; i < buff.length; i++) {
|
||
const charCode = buff.charCodeAt(i) ^ key.charCodeAt(i % key.length);
|
||
result += String.fromCharCode(charCode);
|
||
}
|
||
return result;
|
||
}
|
||
// Helper to check if two rectangles intersect (for partial visibility)
|
||
const rectanglesIntersect = (rect1, rect2) => {
|
||
return !(
|
||
rect1.x + rect1.width <= rect2.x ||
|
||
rect1.y + rect1.height <= rect2.y ||
|
||
rect1.x >= rect2.x + rect2.width ||
|
||
rect1.y >= rect2.y + rect2.height
|
||
);
|
||
}
|
||
|
||
// Check if the window is at least partially visible on any display
|
||
const isWindowPartiallyVisible = (bounds) => {
|
||
const displays = screen.getAllDisplays();
|
||
for (const display of displays) {
|
||
if (rectanglesIntersect(bounds, display.workArea)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
const createMainWin = () => {
|
||
const isMainWindVisible = isWindowPartiallyVisible({
|
||
width: parseInt(store.get("mainWinWidth") || 1050) / mainWinDisplayScale,
|
||
height: parseInt(store.get("mainWinHeight") || 660) / mainWinDisplayScale,
|
||
x: parseInt(store.get("mainWinX")),
|
||
y: parseInt(store.get("mainWinY")),
|
||
});
|
||
if (!isMainWindVisible) {
|
||
delete options.x
|
||
delete options.y
|
||
}
|
||
mainWin = new BrowserWindow(options);
|
||
if (store.get("isAlwaysOnTop") === "yes") {
|
||
mainWin.setAlwaysOnTop(true);
|
||
}
|
||
if (store.get("isAutoMaximizeWin") === "yes") {
|
||
mainWin.maximize();
|
||
}
|
||
|
||
if (!isDev) {
|
||
Menu.setApplicationMenu(null);
|
||
}
|
||
|
||
const urlLocation = isDev
|
||
? "http://localhost:3000"
|
||
: `file://${path.join(__dirname, "./build/index.html")}`;
|
||
mainWin.loadURL(urlLocation);
|
||
mainWin.webContents.on('console-message', (event, level, message, line, sourceId) => {
|
||
console.log(`[Renderer Console] Message: ${message}`);
|
||
// 这里你可以进一步处理消息,例如写入文件或发送到其他地方
|
||
});
|
||
|
||
mainWin.on("close", () => {
|
||
if (mainWin && !mainWin.isDestroyed()) {
|
||
let bounds = mainWin.getBounds();
|
||
const currentDisplay = screen.getDisplayMatching(bounds);
|
||
const primaryDisplay = screen.getPrimaryDisplay();
|
||
if (bounds.width > 0 && bounds.height > 0) {
|
||
store.set({
|
||
mainWinWidth: bounds.width,
|
||
mainWinHeight: bounds.height,
|
||
mainWinX: mainWin.isMaximized() ? 0 : bounds.x,
|
||
mainWinY: mainWin.isMaximized() ? 0 : bounds.y,
|
||
mainWinDisplayScale: currentDisplay.scaleFactor / primaryDisplay.scaleFactor,
|
||
});
|
||
}
|
||
}
|
||
mainWin = null;
|
||
});
|
||
mainWin.on("resize", () => {
|
||
if (mainView) {
|
||
if (!mainWin) return
|
||
let { width, height } = mainWin.getContentBounds()
|
||
mainView.setBounds({ x: 0, y: 0, width: width, height: height })
|
||
}
|
||
});
|
||
mainWin.on("maximize", () => {
|
||
if (mainView) {
|
||
let { width, height } = mainWin.getContentBounds()
|
||
mainView.setBounds({ x: 0, y: 0, width: width, height: height })
|
||
}
|
||
});
|
||
mainWin.on("unmaximize", () => {
|
||
if (mainView) {
|
||
let { width, height } = mainWin.getContentBounds()
|
||
mainView.setBounds({ x: 0, y: 0, width: width, height: height })
|
||
}
|
||
});
|
||
//cancel-download-app
|
||
ipcMain.handle('cancel-download-app', (event, arg) => {
|
||
// Implement cancellation logic here
|
||
// Note: In this example, we are not keeping a reference to the request,
|
||
// so we cannot actually abort it. This is a placeholder for demonstration.
|
||
if (downloadRequest) {
|
||
downloadRequest.abort();
|
||
downloadRequest = null;
|
||
}
|
||
event.returnValue = 'cancelled';
|
||
});
|
||
ipcMain.handle('update-win-app', (event, config) => {
|
||
let fileName = `koodo-reader-installer.exe`;
|
||
let supportedArchs = ['x64', 'ia32', 'arm64'];
|
||
//get system arch
|
||
let arch = os.arch();
|
||
if (!supportedArchs.includes(arch)) {
|
||
return;
|
||
}
|
||
|
||
let url = `https://dl.koodoreader.com/v${config.version}/Koodo-Reader-${config.version}-${arch}.exe`;
|
||
const https = require("https");
|
||
const { spawn } = require("child_process");
|
||
const file = fs.createWriteStream(path.join(app.getPath('temp'), fileName));
|
||
downloadRequest = https.get(url, (res) => {
|
||
const totalSize = parseInt(res.headers['content-length'], 10);
|
||
let downloadedSize = 0;
|
||
res.on('data', (chunk) => {
|
||
downloadedSize += chunk.length;
|
||
const progress = ((downloadedSize / totalSize) * 100).toFixed(2);
|
||
const downloadedMB = (downloadedSize / 1024 / 1024).toFixed(2);
|
||
const totalMB = (totalSize / 1024 / 1024).toFixed(2);
|
||
mainWin.webContents.send('download-app-progress', { progress, downloadedMB, totalMB });
|
||
});
|
||
|
||
res.pipe(file);
|
||
file.on('finish', () => {
|
||
console.log('\n下载完成!');
|
||
file.close();
|
||
|
||
|
||
let updateExePath = path.join(app.getPath('temp'), fileName);
|
||
if (!fs.existsSync(updateExePath)) {
|
||
console.error('更新包不存在:', updateExePath);
|
||
return;
|
||
}
|
||
// 验证文件可执行性
|
||
try {
|
||
fs.accessSync(updateExePath, fs.constants.X_OK);
|
||
console.info('更新包可执行性验证通过');
|
||
} catch (err) {
|
||
console.error('更新包不可执行:', err.message);
|
||
return;
|
||
}
|
||
try {
|
||
// 使用 spawn 执行非静默安装
|
||
const child = spawn(updateExePath, [], {
|
||
stdio: ['ignore', 'pipe', 'pipe'], // 捕获 stdout 和 stderr 以便调试
|
||
detached: true, // 独立进程
|
||
shell: true, // 确保 UAC 提示
|
||
windowsHide: false // 确保窗口可见
|
||
});
|
||
|
||
setTimeout(() => {
|
||
app.quit();
|
||
}, 1000);
|
||
child.unref();
|
||
} catch (err) {
|
||
console.error(`spawn 执行异常: ${err.message}`);
|
||
}
|
||
});
|
||
});
|
||
});
|
||
ipcMain.handle("open-book", (event, config) => {
|
||
let { url, isMergeWord, isAutoFullscreen, isPreventSleep } = config;
|
||
options.webPreferences.nodeIntegrationInSubFrames = true;
|
||
if (isMergeWord) {
|
||
delete options.backgroundColor
|
||
}
|
||
store.set({
|
||
url,
|
||
isMergeWord: isMergeWord || "no",
|
||
isAutoFullscreen: isAutoFullscreen || "no",
|
||
isPreventSleep: isPreventSleep || "no",
|
||
});
|
||
let id;
|
||
if (isPreventSleep === "yes") {
|
||
id = powerSaveBlocker.start("prevent-display-sleep");
|
||
console.log(powerSaveBlocker.isStarted(id));
|
||
}
|
||
|
||
|
||
if (isAutoFullscreen === "yes") {
|
||
if (readerWindow) {
|
||
readerWindowList.push(readerWindow)
|
||
}
|
||
readerWindow = new BrowserWindow(options);
|
||
readerWindow.webContents.on('console-message', (event, level, message, line, sourceId) => {
|
||
console.log(`[Renderer Console] Message: ${message}`);
|
||
});
|
||
if (store.get("isAlwaysOnTop") === "yes") {
|
||
readerWindow.setAlwaysOnTop(true);
|
||
}
|
||
readerWindow.loadURL(url);
|
||
readerWindow.maximize();
|
||
} else {
|
||
if (readerWindow) {
|
||
readerWindowList.push(readerWindow)
|
||
}
|
||
const scaleRatio = store.get("windowDisplayScale") || 1
|
||
const isWindowVisible = isWindowPartiallyVisible({
|
||
x: parseInt(store.get("windowX")),
|
||
y: parseInt(store.get("windowY")),
|
||
width: parseInt(store.get("windowWidth") || 1050) / scaleRatio,
|
||
height: parseInt(store.get("windowHeight") || 660) / scaleRatio,
|
||
});
|
||
readerWindow = new BrowserWindow({
|
||
...options,
|
||
width: parseInt(store.get("windowWidth") || 1050) / scaleRatio,
|
||
height: parseInt(store.get("windowHeight") || 660) / scaleRatio,
|
||
x: isWindowVisible ? parseInt(store.get("windowX")) : undefined,
|
||
y: isWindowVisible ? parseInt(store.get("windowY")) : undefined,
|
||
frame: isMergeWord === "yes" ? false : true,
|
||
hasShadow: isMergeWord === "yes" ? false : true,
|
||
transparent: isMergeWord === "yes" ? true : false,
|
||
});
|
||
readerWindow.webContents.on('console-message', (event, level, message, line, sourceId) => {
|
||
console.log(`[Renderer Console] Message: ${message}`);
|
||
});
|
||
readerWindow.loadURL(url);
|
||
// readerWindow.webContents.openDevTools();
|
||
}
|
||
if (store.get("isAlwaysOnTop") === "yes") {
|
||
readerWindow.setAlwaysOnTop(true);
|
||
}
|
||
readerWindow.on("close", (event) => {
|
||
if (readerWindow && !readerWindow.isDestroyed()) {
|
||
let bounds = readerWindow.getBounds();
|
||
const currentDisplay = screen.getDisplayMatching(bounds);
|
||
const primaryDisplay = screen.getPrimaryDisplay();
|
||
if (bounds.width > 0 && bounds.height > 0) {
|
||
store.set({
|
||
windowWidth: bounds.width,
|
||
windowHeight: bounds.height,
|
||
windowX: readerWindow.isMaximized() && currentDisplay.id === primaryDisplay.id ? 0 : bounds.x,
|
||
windowY: readerWindow.isMaximized() && currentDisplay.id === primaryDisplay.id ? 0 : (bounds.y < 0 ? 0 : bounds.y),
|
||
windowDisplayScale: currentDisplay.scaleFactor / primaryDisplay.scaleFactor,
|
||
});
|
||
}
|
||
}
|
||
if (isPreventSleep && !readerWindow.isDestroyed()) {
|
||
id && powerSaveBlocker.stop(id);
|
||
}
|
||
if (mainWin && !mainWin.isDestroyed()) {
|
||
mainWin.webContents.send('reading-finished', {});
|
||
}
|
||
|
||
});
|
||
|
||
|
||
event.returnValue = "success";
|
||
});
|
||
ipcMain.handle("generate-tts", async (event, voiceConfig) => {
|
||
let { text, speed, plugin, config } = voiceConfig;
|
||
let voiceFunc = plugin.script
|
||
// eslint-disable-next-line no-eval
|
||
eval(voiceFunc);
|
||
return global.getAudioPath(text, speed, dirPath, config);
|
||
|
||
});
|
||
ipcMain.handle("cloud-upload", async (event, config) => {
|
||
let syncUtil = await getSyncUtil(config, config.isUseCache);
|
||
let result = await syncUtil.uploadFile(config.fileName, config.fileName, config.type);
|
||
return result;
|
||
});
|
||
|
||
ipcMain.handle("cloud-download", async (event, config) => {
|
||
let syncUtil = await getSyncUtil(config);
|
||
let result = await syncUtil.downloadFile(config.fileName, (config.isTemp ? "temp-" : "") + config.fileName, config.type);
|
||
return result;
|
||
});
|
||
ipcMain.handle("cloud-progress", async (event, config) => {
|
||
let syncUtil = await getSyncUtil(config);
|
||
let result = syncUtil.getDownloadedSize();
|
||
return result;
|
||
});
|
||
ipcMain.handle("picker-download", async (event, config) => {
|
||
let pickerUtil = await getPickerUtil(config);
|
||
let result = await pickerUtil.remote.downloadFile(config.sourcePath, config.destPath);
|
||
return result;
|
||
});
|
||
ipcMain.handle("picker-progress", async (event, config) => {
|
||
let pickerUtil = await getPickerUtil(config);
|
||
let result = await pickerUtil.getDownloadedSize();
|
||
return result;
|
||
});
|
||
ipcMain.handle("cloud-reset", async (event, config) => {
|
||
let syncUtil = await getSyncUtil(config);
|
||
let result = syncUtil.resetCounters();
|
||
return result;
|
||
});
|
||
ipcMain.handle("cloud-stats", async (event, config) => {
|
||
let syncUtil = await getSyncUtil(config);
|
||
let result = syncUtil.getStats();
|
||
return result;
|
||
});
|
||
ipcMain.handle("cloud-delete", async (event, config) => {
|
||
try {
|
||
let syncUtil = await getSyncUtil(config, config.isUseCache);
|
||
let result = await syncUtil.deleteFile(config.fileName, config.type);
|
||
return result;
|
||
} catch (error) {
|
||
console.error("Error deleting file:", error);
|
||
}
|
||
return false;
|
||
|
||
});
|
||
|
||
ipcMain.handle("cloud-list", async (event, config) => {
|
||
let syncUtil = await getSyncUtil(config);
|
||
let result = await syncUtil.listFiles(config.type);
|
||
return result;
|
||
});
|
||
ipcMain.handle("picker-list", async (event, config) => {
|
||
let pickerUtil = await getPickerUtil(config);
|
||
let result = await pickerUtil.listFileInfos(config.currentPath);
|
||
return result;
|
||
});
|
||
ipcMain.handle("cloud-exist", async (event, config) => {
|
||
let syncUtil = await getSyncUtil(config);
|
||
let result = await syncUtil.isExist(config.fileName, config.type);
|
||
return result;
|
||
});
|
||
ipcMain.handle("cloud-close", async (event, config) => {
|
||
removeSyncUtil(config);
|
||
return "pong";
|
||
});
|
||
|
||
ipcMain.handle("clear-tts", async (event, config) => {
|
||
if (!fs.existsSync(path.join(dirPath, "tts"))) {
|
||
return "pong";
|
||
} else {
|
||
const fsExtra = require("fs-extra");
|
||
try {
|
||
await fsExtra.remove(path.join(dirPath, "tts"));
|
||
await fsExtra.mkdir(path.join(dirPath, "tts"));
|
||
return "pong";
|
||
} catch (err) {
|
||
console.error(err);
|
||
return "pong";
|
||
}
|
||
}
|
||
});
|
||
ipcMain.handle("select-path", async (event) => {
|
||
var path = await dialog.showOpenDialog({
|
||
properties: ["openDirectory"],
|
||
});
|
||
return path.filePaths[0];
|
||
});
|
||
ipcMain.handle("encrypt-data", async (event, config) => {
|
||
const { TokenService } = await import('./src/assets/lib/kookit-extra.min.mjs');
|
||
let fingerprint = await TokenService.getFingerprint();
|
||
let encrypted = encrypt(config.token, fingerprint);
|
||
store.set("encryptedToken", encrypted);
|
||
return "pong";
|
||
});
|
||
ipcMain.handle("decrypt-data", async (event) => {
|
||
let encrypted = store.get("encryptedToken");
|
||
if (!encrypted) return "";
|
||
const { TokenService } = await import('./src/assets/lib/kookit-extra.min.mjs');
|
||
let fingerprint = await TokenService.getFingerprint();
|
||
let decrypted = decrypt(encrypted, fingerprint);
|
||
if (decrypted.startsWith("{") && decrypted.endsWith("}")) {
|
||
return decrypted
|
||
} else {
|
||
const { safeStorage } = require("electron")
|
||
decrypted = safeStorage.decryptString(Buffer.from(encrypted, "base64"));
|
||
let newEncrypted = encrypt(decrypted, fingerprint);
|
||
store.set("encryptedToken", newEncrypted);
|
||
return decrypted;
|
||
}
|
||
|
||
});
|
||
ipcMain.handle("get-mac", async (event, config) => {
|
||
const { machineIdSync } = require('node-machine-id');
|
||
return machineIdSync();
|
||
});
|
||
ipcMain.handle("get-store-value", async (event, config) => {
|
||
return store.get(config.key);
|
||
});
|
||
|
||
ipcMain.handle("reset-reader-position", async (event) => {
|
||
store.delete("windowX");
|
||
store.delete("windowY");
|
||
return "success"
|
||
|
||
});
|
||
ipcMain.handle("reset-main-position", async (event) => {
|
||
store.delete("mainWinX");
|
||
store.delete("mainWinY");
|
||
app.relaunch()
|
||
app.exit()
|
||
return "success"
|
||
|
||
});
|
||
|
||
ipcMain.handle("select-file", async (event, config) => {
|
||
const result = await dialog.showOpenDialog({
|
||
properties: ['openFile'],
|
||
filters: [{ name: 'Zip Files', extensions: ['zip'] }]
|
||
});
|
||
|
||
if (result.canceled) {
|
||
return "";
|
||
} else {
|
||
const filePath = result.filePaths[0];
|
||
return filePath;
|
||
}
|
||
});
|
||
|
||
ipcMain.handle("select-book", async (event, config) => {
|
||
const result = await dialog.showOpenDialog({
|
||
properties: ['openFile', 'multiSelections'],
|
||
filters: [{
|
||
name: 'Books', extensions: ["epub", "pdf", "txt", "mobi", "azw3", "azw", "htm", "html", "xml", "xhtml", "mhtml", "docx", "md", "fb2", "cbz", "cbt", "cbr", "cb7",]
|
||
}]
|
||
});
|
||
|
||
if (result.canceled) {
|
||
console.log('User canceled the file selection');
|
||
return [];
|
||
} else {
|
||
const filePaths = result.filePaths;
|
||
console.log('Selected file path:', filePaths);
|
||
return filePaths;
|
||
}
|
||
});
|
||
ipcMain.handle("database-command", async (event, config) => {
|
||
const { SqlStatement } = await import('./src/assets/lib/kookit-extra.min.mjs');
|
||
let { statement, statementType, executeType, dbName, data, storagePath } = config;
|
||
let db = getDBConnection(dbName, storagePath, SqlStatement.sqlStatement);
|
||
let sql = ""
|
||
if (statementType === "string") {
|
||
sql = SqlStatement.sqlStatement[statement][dbName];
|
||
} else if (statementType === "function") {
|
||
sql = SqlStatement.sqlStatement[statement][dbName](data);
|
||
}
|
||
const row = db.prepare(sql);
|
||
let result;
|
||
if (data) {
|
||
if (statement.startsWith("save") || statement.startsWith("update")) {
|
||
data = SqlStatement.jsonToSqlite[dbName](data)
|
||
}
|
||
result = row[executeType](data);
|
||
} else {
|
||
result = row[executeType]();
|
||
}
|
||
if (executeType === 'all') {
|
||
return result.map(item => SqlStatement.sqliteToJson[dbName](item));
|
||
} else if (executeType === 'get') {
|
||
return SqlStatement.sqliteToJson[dbName](result);
|
||
} else {
|
||
return result;
|
||
}
|
||
});
|
||
ipcMain.handle("close-database", async (event, config) => {
|
||
const { SqlStatement } = await import('./src/assets/lib/kookit-extra.min.mjs');
|
||
let { dbName, storagePath } = config;
|
||
let db = getDBConnection(dbName, storagePath, SqlStatement.sqlStatement);
|
||
delete dbConnection[dbName];
|
||
db.close();
|
||
});
|
||
ipcMain.handle("set-always-on-top", async (event, config) => {
|
||
store.set("isAlwaysOnTop", config.isAlwaysOnTop);
|
||
if (mainWin && !mainWin.isDestroyed()) {
|
||
if (config.isAlwaysOnTop === "yes") {
|
||
mainWin.setAlwaysOnTop(true);
|
||
} else {
|
||
mainWin.setAlwaysOnTop(false);
|
||
}
|
||
|
||
}
|
||
if (readerWindow && !readerWindow.isDestroyed()) {
|
||
if (config.isAlwaysOnTop === "yes") {
|
||
readerWindow.setAlwaysOnTop(true);
|
||
} else {
|
||
readerWindow.setAlwaysOnTop(false);
|
||
}
|
||
}
|
||
return "pong";
|
||
})
|
||
ipcMain.handle("set-auto-maximize", async (event, config) => {
|
||
store.set("isAutoMaximizeWin", config.isAutoMaximizeWin);
|
||
if (mainWin && !mainWin.isDestroyed()) {
|
||
if (config.isAutoMaximizeWin === "yes") {
|
||
mainWin.maximize();
|
||
} else {
|
||
mainWin.unmaximize();
|
||
}
|
||
|
||
}
|
||
if (readerWindow && !readerWindow.isDestroyed()) {
|
||
if (config.isAlwaysOnTop === "yes") {
|
||
readerWindow.setAlwaysOnTop(true);
|
||
} else {
|
||
readerWindow.setAlwaysOnTop(false);
|
||
}
|
||
}
|
||
return "pong";
|
||
})
|
||
ipcMain.handle("toggle-auto-launch", async (event, config) => {
|
||
app.setLoginItemSettings({
|
||
openAtLogin: config.isAutoLaunch === "yes"
|
||
})
|
||
return "pong";
|
||
})
|
||
ipcMain.handle("open-explorer-folder", async (event, config) => {
|
||
const { shell } = require("electron");
|
||
if (config.isFolder) {
|
||
shell.openPath(config.path);
|
||
} else {
|
||
shell.showItemInFolder(config.path);
|
||
}
|
||
|
||
return "pong";
|
||
})
|
||
ipcMain.handle("get-debug-logs", async (event, config) => {
|
||
const { shell } = require("electron");
|
||
const file = log.transports.file.getFile();
|
||
shell.showItemInFolder(file.path);
|
||
return "pong";
|
||
})
|
||
|
||
ipcMain.on("user-data", (event, arg) => {
|
||
event.returnValue = dirPath;
|
||
});
|
||
ipcMain.handle("hide-reader", (event, arg) => {
|
||
if (!readerWindow.isDestroyed() && readerWindow && readerWindow.isFocused()) {
|
||
readerWindow.minimize();
|
||
event.returnvalue = true;
|
||
} else if (mainWin && mainWin.isFocused()) {
|
||
mainWin.minimize();
|
||
event.returnvalue = true;
|
||
} else {
|
||
event.returnvalue = false;
|
||
}
|
||
});
|
||
ipcMain.handle("open-console", (event, arg) => {
|
||
mainWin.webContents.openDevTools();
|
||
event.returnvalue = true;
|
||
});
|
||
ipcMain.handle("reload-reader", (event, arg) => {
|
||
if (readerWindowList.length > 0) {
|
||
readerWindowList.forEach(win => {
|
||
if (win && !win.isDestroyed() && win.webContents.getURL().indexOf(arg.bookKey) > -1) {
|
||
win.reload();
|
||
}
|
||
})
|
||
}
|
||
if (readerWindow && !readerWindow.isDestroyed() && readerWindow.webContents.getURL().indexOf(arg.bookKey) > -1) {
|
||
readerWindow.reload();
|
||
}
|
||
});
|
||
ipcMain.handle("reload-main", (event, arg) => {
|
||
if (mainWin) {
|
||
mainWin.reload();
|
||
}
|
||
});
|
||
|
||
ipcMain.handle("new-chat", (event, config) => {
|
||
if (!chatWindow && mainWin) {
|
||
let bounds = mainWin.getBounds();
|
||
chatWindow = new BrowserWindow({
|
||
...options,
|
||
width: 450,
|
||
height: bounds.height,
|
||
x: bounds.x + (bounds.width - 450),
|
||
y: bounds.y,
|
||
frame: true,
|
||
hasShadow: true,
|
||
transparent: false,
|
||
});
|
||
chatWindow.loadURL(config.url);
|
||
chatWindow.on("close", (event) => {
|
||
chatWindow && chatWindow.destroy();
|
||
chatWindow = null;
|
||
});
|
||
} else if (chatWindow && !chatWindow.isDestroyed()) {
|
||
chatWindow.show();
|
||
chatWindow.focus();
|
||
}
|
||
});
|
||
|
||
ipcMain.handle("google-picker", (event, config) => {
|
||
if (!googlePickerView && mainWin) {
|
||
googlePickerView = new WebContentsView({ ...options, transparent: true })
|
||
mainWin.contentView.addChildView(googlePickerView)
|
||
let { width, height } = mainWin.getContentBounds()
|
||
googlePickerView.setBounds({ x: 0, y: 0, width, height })
|
||
googlePickerView.setBackgroundColor("#00000000");
|
||
googlePickerView.webContents.loadURL(config.url)
|
||
}
|
||
});
|
||
ipcMain.on('picker-action', (event, config) => {
|
||
//将数据传递给主窗口
|
||
|
||
if (mainWin && !mainWin.isDestroyed() && config.action === 'picked') {
|
||
mainWin.webContents.send('picker-finished', config);
|
||
}
|
||
if (googlePickerView && (config.action === 'cancel' || config.action === 'picked')) {
|
||
mainWin.contentView.removeChildView(googlePickerView);
|
||
googlePickerView = null;
|
||
}
|
||
});
|
||
ipcMain.handle('clear-all-data', (event, config) => {
|
||
store.clear();
|
||
});
|
||
ipcMain.handle("new-tab", (event, config) => {
|
||
if (mainWin) {
|
||
mainView = new WebContentsView(options)
|
||
mainWin.contentView.addChildView(mainView)
|
||
let { width, height } = mainWin.getContentBounds()
|
||
mainView.setBounds({ x: 0, y: 0, width: width, height: height })
|
||
mainView.webContents.loadURL(config.url)
|
||
}
|
||
});
|
||
ipcMain.handle("reload-tab", (event, config) => {
|
||
if (mainWin && mainView) {
|
||
mainView.webContents.reload()
|
||
}
|
||
});
|
||
ipcMain.handle("adjust-tab-size", (event, config) => {
|
||
if (mainWin && mainView) {
|
||
let { width, height } = mainWin.getContentBounds()
|
||
mainView.setBounds({ x: 0, y: 0, width: width, height: height })
|
||
}
|
||
});
|
||
ipcMain.handle("exit-tab", (event, message) => {
|
||
if (mainWin && mainView) {
|
||
mainWin.contentView.removeChildView(mainView)
|
||
}
|
||
});
|
||
ipcMain.handle("enter-tab-fullscreen", () => {
|
||
if (mainWin && mainView) {
|
||
mainWin.setFullScreen(true);
|
||
console.log("enter full");
|
||
}
|
||
});
|
||
ipcMain.handle("exit-tab-fullscreen", () => {
|
||
if (mainWin && mainView) {
|
||
mainWin.setFullScreen(false);
|
||
console.log("exit full");
|
||
}
|
||
});
|
||
ipcMain.handle("enter-fullscreen", () => {
|
||
if (readerWindow) {
|
||
readerWindow.setFullScreen(true);
|
||
console.log("enter full");
|
||
}
|
||
});
|
||
ipcMain.handle("exit-fullscreen", () => {
|
||
if (readerWindow) {
|
||
readerWindow.setFullScreen(false);
|
||
console.log("exit full");
|
||
}
|
||
});
|
||
ipcMain.handle("open-url", (event, config) => {
|
||
if (config.type === "dict") {
|
||
if (!dictWindow || dictWindow.isDestroyed()) {
|
||
dictWindow = new BrowserWindow();
|
||
}
|
||
dictWindow.loadURL(config.url);
|
||
dictWindow.focus();
|
||
} else if (config.type === "trans") {
|
||
if (!transWindow || transWindow.isDestroyed()) {
|
||
transWindow = new BrowserWindow();
|
||
}
|
||
transWindow.loadURL(config.url);
|
||
transWindow.focus();
|
||
} else {
|
||
if (!linkWindow || linkWindow.isDestroyed()) {
|
||
linkWindow = new BrowserWindow();
|
||
}
|
||
linkWindow.loadURL(config.url);
|
||
linkWindow.focus();
|
||
}
|
||
|
||
event.returnvalue = true;
|
||
});
|
||
ipcMain.handle("switch-moyu", (event, arg) => {
|
||
let id;
|
||
if (store.get("isPreventSleep") === "yes") {
|
||
id = powerSaveBlocker.start("prevent-display-sleep");
|
||
console.log(powerSaveBlocker.isStarted(id));
|
||
}
|
||
if (readerWindow) {
|
||
readerWindow.close();
|
||
if (store.get("isMergeWord") === "yes") {
|
||
delete options.backgroundColor
|
||
}
|
||
const scaleRatio = store.get("windowDisplayScale") || 1
|
||
Object.assign(options, {
|
||
width: parseInt(store.get("windowWidth") || 1050) / scaleRatio,
|
||
height: parseInt(store.get("windowHeight") || 660) / scaleRatio,
|
||
x: parseInt(store.get("windowX")),
|
||
y: parseInt(store.get("windowY")),
|
||
frame: store.get("isMergeWord") !== "yes" ? false : true,
|
||
hasShadow: store.get("isMergeWord") !== "yes" ? false : true,
|
||
transparent: store.get("isMergeWord") !== "yes" ? true : false,
|
||
});
|
||
options.webPreferences.nodeIntegrationInSubFrames = true;
|
||
|
||
store.set(
|
||
"isMergeWord",
|
||
store.get("isMergeWord") !== "yes" ? "yes" : "no"
|
||
);
|
||
if (readerWindow) {
|
||
readerWindowList.push(readerWindow)
|
||
}
|
||
readerWindow = new BrowserWindow(options);
|
||
readerWindow.webContents.on('console-message', (event, level, message, line, sourceId) => {
|
||
console.log(`[Renderer Console] Message: ${message}`);
|
||
});
|
||
if (store.get("isAlwaysOnTop") === "yes") {
|
||
readerWindow.setAlwaysOnTop(true);
|
||
}
|
||
|
||
readerWindow.loadURL(store.get("url"));
|
||
readerWindow.on("close", (event) => {
|
||
if (!readerWindow.isDestroyed()) {
|
||
let bounds = readerWindow.getBounds();
|
||
const currentDisplay = screen.getDisplayMatching(bounds);
|
||
const primaryDisplay = screen.getPrimaryDisplay();
|
||
if (bounds.width > 0 && bounds.height > 0) {
|
||
store.set({
|
||
windowWidth: bounds.width,
|
||
windowHeight: bounds.height,
|
||
windowX: readerWindow.isMaximized() && currentDisplay.id === primaryDisplay.id ? 0 : bounds.x,
|
||
windowY: readerWindow.isMaximized() && currentDisplay.id === primaryDisplay.id ? 0 : (bounds.y < 0 ? 0 : bounds.y),
|
||
});
|
||
}
|
||
}
|
||
if (store.get("isPreventSleep") && !readerWindow.isDestroyed()) {
|
||
id && powerSaveBlocker.stop(id);
|
||
}
|
||
if (mainWin && !mainWin.isDestroyed()) {
|
||
mainWin.webContents.send('reading-finished', {});
|
||
}
|
||
});
|
||
}
|
||
event.returnvalue = false;
|
||
});
|
||
ipcMain.on("storage-location", (event, config) => {
|
||
event.returnValue = path.join(dirPath, "data");
|
||
});
|
||
ipcMain.on("url-window-status", (event, config) => {
|
||
console.log(config, 'url status')
|
||
if (config.type === "dict") {
|
||
event.returnValue = dictWindow && !dictWindow.isDestroyed() ? true : false;
|
||
} else if (config.type === "trans") {
|
||
event.returnValue = transWindow && !transWindow.isDestroyed() ? true : false;
|
||
} else {
|
||
event.returnValue = linkWindow && !linkWindow.isDestroyed() ? true : false;
|
||
}
|
||
|
||
});
|
||
ipcMain.on("get-dirname", (event, arg) => {
|
||
event.returnValue = __dirname;
|
||
});
|
||
ipcMain.on("system-color", (event, arg) => {
|
||
event.returnValue = nativeTheme.shouldUseDarkColors || false;
|
||
});
|
||
ipcMain.on("check-main-open", (event, arg) => {
|
||
event.returnValue = mainWin ? true : false;
|
||
});
|
||
ipcMain.on("get-file-data", function (event) {
|
||
if (fs.existsSync(path.join(dirPath, "log.json"))) {
|
||
const _data = JSON.parse(
|
||
fs.readFileSync(path.join(dirPath, "log.json"), "utf-8") || "{}"
|
||
);
|
||
if (_data && _data.filePath) {
|
||
filePath = _data.filePath;
|
||
fs.writeFileSync(path.join(dirPath, "log.json"), "", "utf-8");
|
||
}
|
||
}
|
||
|
||
event.returnValue = filePath;
|
||
filePath = null;
|
||
});
|
||
ipcMain.on("check-file-data", function (event) {
|
||
if (fs.existsSync(path.join(dirPath, "log.json"))) {
|
||
const _data = JSON.parse(
|
||
fs.readFileSync(path.join(dirPath, "log.json"), "utf-8") || "{}"
|
||
);
|
||
if (_data && _data.filePath) {
|
||
filePath = _data.filePath;
|
||
}
|
||
}
|
||
|
||
event.returnValue = filePath;
|
||
filePath = null;
|
||
});
|
||
};
|
||
|
||
|
||
app.on("ready", () => {
|
||
createMainWin();
|
||
});
|
||
app.on("window-all-closed", () => {
|
||
app.quit();
|
||
});
|
||
app.on("open-file", (e, pathToFile) => {
|
||
filePath = pathToFile;
|
||
});
|
||
// Register protocol handler
|
||
app.setAsDefaultProtocolClient('koodo-reader');
|
||
// Handle deep linking
|
||
app.on('second-instance', (event, commandLine) => {
|
||
const url = commandLine.pop();
|
||
if (url) {
|
||
handleCallback(url);
|
||
}
|
||
});
|
||
const originalConsoleLog = console.log;
|
||
console.log = function (...args) {
|
||
originalConsoleLog(...args); // 保留原日志
|
||
log.info(args.join(' ')); // 写入日志文件
|
||
};
|
||
const originalConsoleError = console.error;
|
||
console.error = function (...args) {
|
||
originalConsoleError(...args); // 保留原错误日志
|
||
log.error(args.join(' ')); // 写入错误日志文件
|
||
};
|
||
const originalConsoleWarn = console.warn;
|
||
console.warn = function (...args) {
|
||
originalConsoleWarn(...args); // 保留原警告日志
|
||
log.warn(args.join(' ')); // 写入警告日志文件
|
||
};
|
||
const originalConsoleInfo = console.info;
|
||
console.info = function (...args) {
|
||
originalConsoleInfo(...args); // 保留原信息日志
|
||
log.info(args.join(' ')); // 写入信息日志文件
|
||
};
|
||
// Handle MacOS deep linking
|
||
app.on('open-url', (event, url) => {
|
||
event.preventDefault();
|
||
handleCallback(url);
|
||
});
|
||
const handleCallback = (url) => {
|
||
try {
|
||
// 检查 URL 是否有效
|
||
if (!url.startsWith('koodo-reader://')) {
|
||
console.error('Invalid URL format:', url);
|
||
return;
|
||
}
|
||
|
||
// 解析 URL
|
||
const parsedUrl = new URL(url);
|
||
const code = parsedUrl.searchParams.get('code');
|
||
const state = parsedUrl.searchParams.get('state');
|
||
|
||
if (code && mainWin) {
|
||
mainWin.webContents.send('oauth-callback', { code, state });
|
||
}
|
||
} catch (error) {
|
||
console.error('Error handling callback URL:', error);
|
||
console.log('Problematic URL:', url);
|
||
}
|
||
}; |