Files
peoplelib/preload.js
T
lofyerandfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> 3ccd044527 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>
2026-08-03 12:13:02 +08:00

204 lines
9.7 KiB
JavaScript

const { contextBridge, ipcRenderer } = require('electron');
let downloadSeq = 0;
function downloadFile(url, suggestName, entryId, extraHeaders, meta, onProgress) {
const requestId = `dl_${Date.now().toString(36)}_${(++downloadSeq).toString(36)}`;
const listener = (_event, data) => {
if (!data || data.requestId !== requestId || typeof onProgress !== 'function') return;
try { onProgress(data); } catch (e) { /* 渲染层进度回调异常不影响下载 */ }
};
if (typeof onProgress === 'function') ipcRenderer.on('download:progress', listener);
return ipcRenderer
.invoke('download:file', url, suggestName, entryId, extraHeaders, meta, requestId)
.finally(() => ipcRenderer.removeListener('download:progress', listener));
}
function captureReaderRect(rect) {
const value = rect && typeof rect === 'object' ? rect : {};
const area = {
x: Number(value.x),
y: Number(value.y),
width: Number(value.width),
height: Number(value.height)
};
const documentArea = document.getElementById('docArea');
const bounds = documentArea && documentArea.getBoundingClientRect();
if (
!bounds
|| !Object.values(area).every(Number.isFinite)
|| area.x < bounds.left - 1
|| area.y < bounds.top - 1
|| area.x + area.width > bounds.right + 1
|| area.y + area.height > bounds.bottom + 1
) {
return Promise.resolve({ ok: false, error: '只能截取阅读正文区域' });
}
return ipcRenderer.invoke('reader:captureRect', area);
}
contextBridge.exposeInMainWorld('api', {
sources: {
list: () => ipcRenderer.invoke('sources:list'),
browse: (sourceId, page) => ipcRenderer.invoke('source:list', sourceId, page),
search: (sourceId, keyword, page) => ipcRenderer.invoke('source:search', sourceId, keyword, page),
detail: (sourceId, postId) => ipcRenderer.invoke('source:detail', sourceId, postId),
download: (sourceId, postId) => ipcRenderer.invoke('source:download', sourceId, postId)
},
library: {
list: () => ipcRenderer.invoke('library:list'),
get: (id) => ipcRenderer.invoke('library:get', id),
listShelves: () => ipcRenderer.invoke('library:listShelves'),
listTags: () => ipcRenderer.invoke('library:listTags'),
addShelf: (input) => ipcRenderer.invoke('library:addShelf', input),
updateShelf: (id, patch) => ipcRenderer.invoke('library:updateShelf', id, patch),
removeShelf: (id) => ipcRenderer.invoke('library:removeShelf', id),
addTag: (input) => ipcRenderer.invoke('library:addTag', input),
updateTag: (id, patch) => ipcRenderer.invoke('library:updateTag', id, patch),
removeTag: (id) => ipcRenderer.invoke('library:removeTag', id),
findBySource: (sourceId, postId) => ipcRenderer.invoke('library:findBySource', sourceId, postId),
add: (item) => ipcRenderer.invoke('library:add', item),
update: (id, patch) => ipcRenderer.invoke('library:update', id, patch),
remove: (id, options) => ipcRenderer.invoke('library:remove', id, options),
getDir: () => ipcRenderer.invoke('library:getDir'),
pickDir: () => ipcRenderer.invoke('library:pickDir'),
setDir: (dir, migrate) => ipcRenderer.invoke('library:setDir', dir, migrate),
pickLocal: (kind) => ipcRenderer.invoke('dialog:pickLocal', kind),
importLocal: (selectionId, options) => (
ipcRenderer.invoke('library:importLocal', selectionId, options)
),
scan: () => ipcRenderer.invoke('library:scan'),
onChanged: (cb) => {
const h = () => cb();
ipcRenderer.on('library:changed', h);
return () => ipcRenderer.removeListener('library:changed', h);
}
},
settings: {
get: (key, def) => ipcRenderer.invoke('settings:get', key, def),
set: (key, value) => ipcRenderer.invoke('settings:set', key, value)
},
ui: {
getTheme: () => ipcRenderer.invoke('ui:getTheme'),
setTheme: (theme) => ipcRenderer.invoke('ui:setTheme', theme),
onThemeChanged: (cb) => {
const h = (_event, theme) => cb(theme);
ipcRenderer.on('ui:themeChanged', h);
return () => ipcRenderer.removeListener('ui:themeChanged', h);
}
},
downloadFile,
zlib: {
hasCreds: () => ipcRenderer.invoke('zlib:hasCreds'),
login: (email, password) => ipcRenderer.invoke('zlib:login', email, password),
logout: () => ipcRenderer.invoke('zlib:logout')
},
semanticScholar: {
keyStatus: () => ipcRenderer.invoke('semanticScholar:keyStatus'),
setKey: (key) => ipcRenderer.invoke('semanticScholar:setKey', key),
clearKey: () => ipcRenderer.invoke('semanticScholar:clearKey')
},
proxy: {
get: () => ipcRenderer.invoke('proxy:get'),
set: (url) => ipcRenderer.invoke('proxy:set', url)
},
reader: {
ready: () => ipcRenderer.invoke('reader:ready'),
open: (entryId, fileIndex) => ipcRenderer.invoke('reader:open', entryId, fileIndex),
openAt: (entryId, fileIndex, documentKey, locator) => (
ipcRenderer.invoke('reader:openAt', entryId, fileIndex, documentKey, locator)
),
meta: (entryId, fileIndex) => ipcRenderer.invoke('reader:meta', entryId, fileIndex),
bytes: (entryId, fileIndex) => ipcRenderer.invoke('reader:bytes', entryId, fileIndex),
rangeOpen: (entryId, fileIndex) => ipcRenderer.invoke('reader:rangeOpen', entryId, fileIndex),
rangeRead: (sessionId, begin, end) => (
ipcRenderer.invoke('reader:rangeRead', sessionId, begin, end)
),
rangeClose: (sessionId) => ipcRenderer.invoke('reader:rangeClose', sessionId),
captureRect: (rect) => captureReaderRect(rect),
openExternal: (entryId, fileIndex) => (
ipcRenderer.invoke('reader:openExternal', entryId, fileIndex)
),
getState: (entryId, documentKey) => ipcRenderer.invoke('reader:getState', entryId, documentKey),
setProgress: (entryId, documentKey, locator, percent) => (
ipcRenderer.invoke('reader:setProgress', entryId, documentKey, locator, percent)
),
addBookmark: (entryId, mark) => ipcRenderer.invoke('reader:addBookmark', entryId, mark),
removeBookmark: (entryId, markId) => ipcRenderer.invoke('reader:removeBookmark', entryId, markId),
addNote: (entryId, note) => ipcRenderer.invoke('reader:addNote', entryId, note),
addStandaloneNote: (note) => ipcRenderer.invoke('reader:addStandaloneNote', note),
updateNote: (entryId, noteId, patch) => ipcRenderer.invoke('reader:updateNote', entryId, noteId, patch),
removeNote: (entryId, noteId) => ipcRenderer.invoke('reader:removeNote', entryId, noteId),
listNotes: (filters) => ipcRenderer.invoke('reader:listNotes', filters),
getNoteCounts: () => ipcRenderer.invoke('reader:getNoteCounts'),
listCollections: () => ipcRenderer.invoke('reader:listCollections'),
addCollection: (input) => ipcRenderer.invoke('reader:addCollection', input),
updateCollection: (id, patch) => ipcRenderer.invoke('reader:updateCollection', id, patch),
removeCollection: (id) => ipcRenderer.invoke('reader:removeCollection', id),
pickNotePdf: () => ipcRenderer.invoke('reader:pickNotePdf'),
notePdfBytes: (ref) => ipcRenderer.invoke('reader:notePdfBytes', ref),
saveNotePdf: (bytes, suggestedName) => (
ipcRenderer.invoke('reader:saveNotePdf', bytes, suggestedName)
),
getAnnotations: (entryId, fileIndex) => ipcRenderer.invoke('reader:getAnnotations', entryId, fileIndex),
setAnnotationPage: (entryId, fileIndex, page, data) => ipcRenderer.invoke('reader:setAnnotationPage', entryId, fileIndex, page, data),
onOpenEntry: (cb) => {
const h = (_e, data) => cb(data);
ipcRenderer.on('reader:openEntry', h);
return () => ipcRenderer.removeListener('reader:openEntry', h);
},
onCloseEntry: (cb) => {
const h = (_e, entryId) => cb(entryId);
ipcRenderer.on('reader:closeEntry', h);
return () => ipcRenderer.removeListener('reader:closeEntry', h);
},
onPurgeEntry: (cb) => {
const h = async (_e, data) => {
try { await cb(data); } finally {
if (data && data.requestId) ipcRenderer.send('reader:purgeReady', data.requestId);
}
};
ipcRenderer.on('reader:purgeEntry', h);
return () => ipcRenderer.removeListener('reader:purgeEntry', h);
},
onPrepareClose: (cb) => {
const h = async () => {
try { await cb(); } finally { ipcRenderer.send('reader:shutdownReady'); }
};
ipcRenderer.on('reader:prepareClose', h);
return () => ipcRenderer.removeListener('reader:prepareClose', h);
},
onNotesChanged: (cb) => {
const h = (_e, data) => cb(data);
ipcRenderer.on('reader:notesChanged', h);
return () => ipcRenderer.removeListener('reader:notesChanged', h);
}
},
ai: {
status: () => ipcRenderer.invoke('ai:status'),
save: (cfg) => ipcRenderer.invoke('ai:save', cfg),
clear: () => ipcRenderer.invoke('ai:clear'),
run: (payload) => ipcRenderer.invoke('ai:run', payload),
cancel: (runId) => ipcRenderer.invoke('ai:cancel', runId),
onChanged: (cb) => {
const h = (_e, data) => cb(data);
ipcRenderer.on('ai:changed', h);
return () => ipcRenderer.removeListener('ai:changed', h);
},
// 返回取消订阅函数:阅读器窗口关闭时要能解绑,否则监听器会越积越多
onDelta: (cb) => {
const h = (_e, data) => cb(data);
ipcRenderer.on('ai:delta', h);
return () => ipcRenderer.removeListener('ai:delta', h);
}
},
openPath: (p) => ipcRenderer.invoke('shell:openPath', p),
showItem: (p) => ipcRenderer.invoke('shell:showItem', p),
openExternal: (url) => ipcRenderer.invoke('shell:openExternal', url),
copy: (text) => ipcRenderer.invoke('copy', text),
getVersion: () => ipcRenderer.invoke('app:version'),
checkUpdate: () => ipcRenderer.invoke('app:checkUpdate'),
minimize: () => ipcRenderer.send('win:minimize'),
maximize: () => ipcRenderer.send('win:maximize'),
close: () => ipcRenderer.send('win:close')
});