新增 PDF/EPUB/MOBI/AZW 内置阅读器,PDF 分段读取支持超大文件, 批注、读书与画布笔记、封面生成与本地导入。AI 助手支持三种协议、 图像上下文与安全 Markdown 渲染,上下文范围改为 选中/当前页/全文, 页面与全文无需选中文本即可发送,全文会提示可能超出模型限制。 便携版输出目录固定为 PeopleLib-windows-x64,不再随版本号变化, 避免升级后 data/ 被遗留在旧目录。 Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
168 lines
4.5 KiB
JavaScript
168 lines
4.5 KiB
JavaScript
// 阅读器独立窗口的生命周期管理。
|
|
// 全局只保留一个阅读器窗口,书籍通过窗口内标签切换,避免同一 PDF 出现两个并发编辑器。
|
|
|
|
const path = require('path');
|
|
const { pathToFileURL } = require('url');
|
|
const { BrowserWindow } = require('electron');
|
|
|
|
let readerWindow = null;
|
|
let rendererReady = false;
|
|
let pendingMessages = [];
|
|
let closeAllowed = false;
|
|
let closePending = false;
|
|
let closeTimer = null;
|
|
|
|
function alive(win) {
|
|
return !!win && !win.isDestroyed();
|
|
}
|
|
|
|
function get() {
|
|
if (alive(readerWindow)) return readerWindow;
|
|
readerWindow = null;
|
|
return null;
|
|
}
|
|
|
|
function sendEntry(win, channel, payload) {
|
|
if (!rendererReady) {
|
|
pendingMessages.push([channel, payload]);
|
|
return;
|
|
}
|
|
win.webContents.send(channel, payload);
|
|
}
|
|
|
|
function markReady(webContents) {
|
|
const win = get();
|
|
if (!win || win.webContents.id !== webContents.id) return false;
|
|
rendererReady = true;
|
|
const messages = pendingMessages;
|
|
pendingMessages = [];
|
|
for (const [channel, payload] of messages) {
|
|
if (alive(win)) win.webContents.send(channel, payload);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function isReady(win) {
|
|
return alive(win) && win === get() && rendererReady;
|
|
}
|
|
|
|
function open(entryId, rootDir, fileIndex, locator, uiTheme = 'dark') {
|
|
const existing = get();
|
|
if (existing) {
|
|
if (existing.isMinimized()) existing.restore();
|
|
const payload = {
|
|
entryId: String(entryId),
|
|
fileIndex: Number.isInteger(fileIndex) ? fileIndex : null
|
|
};
|
|
if (locator && typeof locator === 'object') payload.locator = locator;
|
|
sendEntry(existing, 'reader:openEntry', payload);
|
|
existing.focus();
|
|
return existing;
|
|
}
|
|
|
|
readerWindow = new BrowserWindow({
|
|
width: 1180,
|
|
height: 900,
|
|
minWidth: 760,
|
|
minHeight: 540,
|
|
frame: false,
|
|
backgroundColor: '#141414',
|
|
icon: path.join(
|
|
rootDir,
|
|
'icons',
|
|
'dist',
|
|
uiTheme === 'light' ? 'book-ai-light.ico' : 'book-ai-dark.ico'
|
|
),
|
|
title: 'PeopleLib',
|
|
webPreferences: {
|
|
preload: path.join(rootDir, 'preload.js'),
|
|
contextIsolation: true,
|
|
nodeIntegration: false,
|
|
// 重排图书正文来自不可信来源,即使已净化也不给它任何 Node 能力
|
|
sandbox: false,
|
|
spellcheck: false
|
|
}
|
|
});
|
|
rendererReady = false;
|
|
pendingMessages = [];
|
|
closeAllowed = false;
|
|
closePending = false;
|
|
if (closeTimer) clearTimeout(closeTimer);
|
|
closeTimer = null;
|
|
|
|
readerWindow.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
|
|
readerWindow.webContents.on('will-navigate', (event, url) => {
|
|
const expected = pathToFileURL(path.join(rootDir, 'src', 'ui', 'reader.html')).href;
|
|
if (!String(url).startsWith(expected)) event.preventDefault();
|
|
});
|
|
|
|
const query = { entryId: String(entryId) };
|
|
if (Number.isInteger(fileIndex)) query.fileIndex = String(fileIndex);
|
|
if (locator && typeof locator === 'object') query.locator = JSON.stringify(locator);
|
|
readerWindow.loadFile(path.join(rootDir, 'src', 'ui', 'reader.html'), { query });
|
|
|
|
readerWindow.on('close', (event) => {
|
|
if (closeAllowed || !alive(readerWindow)) return;
|
|
event.preventDefault();
|
|
if (closePending) return;
|
|
closePending = true;
|
|
sendEntry(readerWindow, 'reader:prepareClose', null);
|
|
closeTimer = setTimeout(() => {
|
|
const win = get();
|
|
if (win) win.destroy();
|
|
}, 10000);
|
|
});
|
|
readerWindow.on('closed', () => {
|
|
readerWindow = null;
|
|
rendererReady = false;
|
|
pendingMessages = [];
|
|
closeAllowed = false;
|
|
closePending = false;
|
|
if (closeTimer) clearTimeout(closeTimer);
|
|
closeTimer = null;
|
|
});
|
|
return readerWindow;
|
|
}
|
|
|
|
function closeFor(entryId) {
|
|
const win = get();
|
|
if (win) sendEntry(win, 'reader:closeEntry', String(entryId));
|
|
}
|
|
|
|
function purgeFor(entryId, requestId) {
|
|
const win = get();
|
|
if (win) {
|
|
sendEntry(win, 'reader:purgeEntry', {
|
|
entryId: String(entryId),
|
|
requestId: String(requestId)
|
|
});
|
|
}
|
|
return !!win;
|
|
}
|
|
|
|
function shutdownReady(webContents) {
|
|
const win = get();
|
|
if (!win || win.webContents.id !== webContents.id || !closePending) return false;
|
|
if (closeTimer) clearTimeout(closeTimer);
|
|
closeTimer = null;
|
|
closeAllowed = true;
|
|
closePending = false;
|
|
win.close();
|
|
return true;
|
|
}
|
|
|
|
function fromWebContents(wc) {
|
|
const win = get();
|
|
return win && win.webContents.id === wc.id ? 'reader' : null;
|
|
}
|
|
|
|
function all() {
|
|
const win = get();
|
|
return win ? [win] : [];
|
|
}
|
|
|
|
module.exports = {
|
|
open, get, closeFor, purgeFor, fromWebContents, all,
|
|
markReady, isReady, shutdownReady
|
|
};
|