feat: 内置阅读器、批注笔记与 AI 助手,发布 1.3.0
新增 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>
This commit is contained in:
co-authored by
factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent
b8c8d24107
commit
3ccd044527
@@ -0,0 +1,167 @@
|
||||
// 阅读器独立窗口的生命周期管理。
|
||||
// 全局只保留一个阅读器窗口,书籍通过窗口内标签切换,避免同一 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
|
||||
};
|
||||
Reference in New Issue
Block a user