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), 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'), 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'), 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') });