Files
peoplelib/preload.js
T
lofyer 488de94289 feat: 支持自定义 Z-Library 镜像并发布 v2.1.5
在 Z-Library 账户设置内增加镜像站点编辑入口,一行一个 HTTPS 地址,
自定义地址经 origin 规范化后优先于内置列表。镜像配置与凭据分开处理,
退出登录后仍保留;移除当前会话依赖的镜像时清理旧会话和 Cookie。

版本更新到 2.1.5,并补充存储、校验、IPC、界面与真实 Electron 保存测试。
2026-08-09 15:25:32 +08:00

286 lines
13 KiB
JavaScript

const { contextBridge, ipcRenderer } = require('electron');
let downloadSeq = 0;
function invokeWithProgress(progressChannel, invokeChannel, requestId, args, onProgress) {
const listener = (_event, data) => {
if (!data || data.requestId !== requestId || typeof onProgress !== 'function') return;
try { onProgress(data); } catch (e) { /* 渲染层进度回调异常不影响下载 */ }
};
if (typeof onProgress === 'function') ipcRenderer.on(progressChannel, listener);
return ipcRenderer
.invoke(invokeChannel, ...args, requestId)
.finally(() => ipcRenderer.removeListener(progressChannel, listener));
}
function runDownload(requestId, url, suggestName, entryId, extraHeaders, meta, onProgress) {
return invokeWithProgress(
'download:progress',
'download:file',
requestId,
[url, suggestName, entryId, extraHeaders, meta],
onProgress
);
}
function downloadFile(url, suggestName, entryId, extraHeaders, meta, onProgress) {
const requestId = `dl_${Date.now().toString(36)}_${(++downloadSeq).toString(36)}`;
return runDownload(requestId, url, suggestName, entryId, extraHeaders, meta, onProgress);
}
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);
}
let chapterDownloadSeq = 0;
function runChapterDownload(sourceId, payload, onProgress) {
const requestId = `chapter_${Date.now().toString(36)}_${(++chapterDownloadSeq).toString(36)}`;
return invokeWithProgress(
'source:chapterProgress',
'source:downloadChapter',
requestId,
[sourceId, payload],
onProgress
);
}
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),
chapters: (sourceId, postId, page, options) => (
ipcRenderer.invoke('source:chapters', sourceId, postId, page, options)
),
readOnline: (sourceId, input) => ipcRenderer.invoke('source:readOnline', sourceId, input),
downloadChapter: (sourceId, payload, onProgress) => runChapterDownload(sourceId, payload, onProgress)
},
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),
updateMany: (patches) => ipcRenderer.invoke('library:updateMany', patches),
remove: (id, options) => ipcRenderer.invoke('library:remove', id, options),
removeMany: (ids, options) => ipcRenderer.invoke('library:removeMany', ids, 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'),
removeMissing: () => ipcRenderer.invoke('library:removeMissing'),
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,
downloads: {
run: runDownload,
pause: (requestId) => ipcRenderer.invoke('download:pause', requestId),
delete: (requestId) => ipcRenderer.invoke('download:delete', requestId)
},
zlib: {
hasCreds: () => ipcRenderer.invoke('zlib:hasCreds'),
login: (email, password) => ipcRenderer.invoke('zlib:login', email, password),
logout: () => ipcRenderer.invoke('zlib:logout'),
getMirrors: () => ipcRenderer.invoke('zlib:getMirrors'),
setMirrors: (list) => ipcRenderer.invoke('zlib:setMirrors', list)
},
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'),
entryClosed: () => ipcRenderer.invoke('reader:entryClosed'),
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'),
getAnnotationCounts: () => ipcRenderer.invoke('reader:getAnnotationCounts'),
orphanReport: () => ipcRenderer.invoke('reader:orphanReport'),
purgeOrphans: (scope) => ipcRenderer.invoke('reader:purgeOrphans', scope),
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);
}
},
notes: {
openWindow: (entryId, noteId) => ipcRenderer.invoke('notes:openWindow', entryId, noteId),
getOne: (entryId, noteId) => ipcRenderer.invoke('notes:getOne', entryId, noteId),
openWindows: () => ipcRenderer.invoke('notes:openWindows'),
tabsChanged: (tabs) => ipcRenderer.invoke('notes:tabsChanged', tabs),
shutdownReady: () => ipcRenderer.invoke('notes:shutdownReady'),
cancelClose: () => ipcRenderer.invoke('notes:cancelClose'),
onWindowsChanged: (cb) => {
const h = (_e, data) => cb(data);
ipcRenderer.on('notes:windowsChanged', h);
return () => ipcRenderer.removeListener('notes:windowsChanged', h);
},
onOpenTab: (cb) => {
const h = (_e, data) => cb(data);
ipcRenderer.on('notes:openTab', h);
return () => ipcRenderer.removeListener('notes:openTab', h);
},
onCloseTab: (cb) => {
const h = (_e, data) => cb(data);
ipcRenderer.on('notes:closeTab', h);
return () => ipcRenderer.removeListener('notes:closeTab', h);
},
onPrepareClose: (cb) => {
const h = () => cb();
ipcRenderer.on('notes:prepareClose', h);
return () => ipcRenderer.removeListener('notes:prepareClose', 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),
sessions: {
list: (filters) => ipcRenderer.invoke('ai:sessionList', filters),
create: (input) => ipcRenderer.invoke('ai:sessionCreate', input),
rename: (sessionId, title) => ipcRenderer.invoke('ai:sessionRename', sessionId, title),
setPinned: (sessionId, pinned) => ipcRenderer.invoke('ai:sessionPin', sessionId, pinned),
remove: (sessionId) => ipcRenderer.invoke('ai:sessionRemove', sessionId),
clear: (sessionId) => ipcRenderer.invoke('ai:sessionClear', sessionId),
messages: (sessionId, options) => ipcRenderer.invoke('ai:sessionMessages', sessionId, options)
},
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')
});