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
+154
-4
@@ -1,5 +1,41 @@
|
||||
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'),
|
||||
@@ -11,21 +47,46 @@ contextBridge.exposeInMainWorld('api', {
|
||||
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, deleteFiles) => ipcRenderer.invoke('library:remove', id, deleteFiles),
|
||||
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) => ipcRenderer.on('library:changed', () => cb())
|
||||
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)
|
||||
},
|
||||
downloadFile: (url, suggestName, entryId, extraHeaders, meta) => ipcRenderer.invoke('download:file', url, suggestName, entryId, extraHeaders, meta),
|
||||
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),
|
||||
@@ -40,7 +101,96 @@ contextBridge.exposeInMainWorld('api', {
|
||||
get: () => ipcRenderer.invoke('proxy:get'),
|
||||
set: (url) => ipcRenderer.invoke('proxy:set', url)
|
||||
},
|
||||
pickFile: () => ipcRenderer.invoke('dialog:pickFile'),
|
||||
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),
|
||||
|
||||
Reference in New Issue
Block a user