feat: 集成漫画源并支持在线阅读

This commit is contained in:
lofyer
2026-08-08 19:32:47 +08:00
parent c2292da442
commit 87dcc307e6
30 changed files with 3681 additions and 58 deletions
+69 -2
View File
@@ -108,6 +108,8 @@ const annotations = require('./src/reader/annotations');
const noteAssets = require('./src/reader/note-assets');
const readerWindow = require('./src/reader/window');
const noteWindow = require('./src/reader/note-window');
const mangaOnlineWindow = require('./src/manga-online/window');
const mangaOnlineSessions = require('./src/manga-online/session');
const rangeSessions = require('./src/reader/range-sessions');
const aiConfig = require('./src/reader/ai-config');
const aiClient = require('./src/reader/ai-client');
@@ -276,7 +278,7 @@ noteWindow.setChangeListener(notifyNoteWindowsChanged);
function applyWindowIcons(theme) {
currentUiTheme = theme === 'light' ? 'light' : 'dark';
const icon = iconForTheme(currentUiTheme);
const windows = [mainWindow, ...readerWindow.all(), ...noteWindow.all()];
const windows = [mainWindow, ...readerWindow.all(), ...noteWindow.all(), ...mangaOnlineWindow.all()];
for (const win of windows) {
if (!win || win.isDestroyed()) continue;
try { win.setIcon(icon); } catch (e) { /* 平台不支持动态图标时保留创建时图标 */ }
@@ -284,7 +286,7 @@ function applyWindowIcons(theme) {
}
function notifyUiThemeChanged() {
const windows = [mainWindow, ...readerWindow.all(), ...noteWindow.all()];
const windows = [mainWindow, ...readerWindow.all(), ...noteWindow.all(), ...mangaOnlineWindow.all()];
for (const win of windows) {
if (win && !win.isDestroyed()) {
win.webContents.send('ui:themeChanged', currentUiTheme);
@@ -314,6 +316,7 @@ app.on('before-quit', () => {
for (const download of downloadSessions.values()) discardDownloadSession(download);
coverGenerator.close();
rangeSessions.closeAll().catch(() => {});
mangaOnlineSessions.reset();
});
// fn 同步抛出时也必须变成 { ok:false },否则 invoke 直接 reject
@@ -332,6 +335,70 @@ ipcMain.handle('source:search', (_e, sourceId, keyword, page) => wrap(() => sour
ipcMain.handle('source:detail', (_e, sourceId, postId) => wrap(() => sources.getSource(sourceId).detail(postId)));
ipcMain.handle('source:download', (_e, sourceId, postId) => wrap(() => sources.getSource(sourceId).download(postId)));
// 按章节下载的源不复用 download:file;后者只处理单个直链文件。
ipcMain.handle('source:chapters', (_e, sourceId, postId, page, options) => wrap(() => {
const source = sources.getSource(sourceId);
if (typeof source.chapters !== 'function') throw new Error('该数据源不支持章节列表');
return source.chapters(postId, page, options || {});
}));
ipcMain.handle('source:readOnline', (event, sourceId, input) => wrap(() => {
if (!mainWindow || mainWindow.isDestroyed() || event.sender.id !== mainWindow.webContents.id) {
throw new Error('只有主窗口可以打开在线漫画');
}
const source = sources.getSource(sourceId);
const win = mangaOnlineWindow.create(__dirname, currentUiTheme);
const ownerId = win.webContents.id;
win.webContents.once('destroyed', () => mangaOnlineSessions.closeOwner(ownerId));
try {
const value = mangaOnlineSessions.create(ownerId, source, input || {});
mangaOnlineWindow.load(__dirname, value.sessionId);
return value;
} catch (error) {
win.destroy();
throw error;
}
}));
ipcMain.handle('source:downloadChapter', async (event, sourceId, payload, requestId) => {
const progressId = typeof requestId === 'string' ? requestId.slice(0, 100) : '';
const sendProgress = (done, total) => {
if (!progressId || event.sender.isDestroyed()) return;
event.sender.send('source:chapterProgress', { requestId: progressId, done, total });
};
return wrap(async () => {
const source = sources.getSource(sourceId);
if (typeof source.downloadChapter !== 'function') throw new Error('该数据源不支持章节下载');
// library.add()/attachFile() 内部已经在 commit() 里触发变更通知,这里不需要再发一次
const result = await source.downloadChapter(library, payload || {}, sendProgress);
return { entryId: result.entry ? result.entry.id : null, chapterLabel: result.chapterLabel, created: result.created };
});
});
function requireMangaOnlineSender(event) {
if (!mangaOnlineWindow.fromWebContents(event.sender)) throw new Error('只有在线漫画阅读器可以读取内容');
return event.sender.id;
}
ipcMain.handle('mangaOnline:meta', (event, sessionId) => wrap(() => (
mangaOnlineSessions.meta(requireMangaOnlineSender(event), sessionId)
)));
ipcMain.handle('mangaOnline:chapters', (event, sessionId, page) => wrap(() => (
mangaOnlineSessions.chapters(requireMangaOnlineSender(event), sessionId, page)
)));
ipcMain.handle('mangaOnline:chapterManifest', (event, sessionId, chapterId) => wrap(() => (
mangaOnlineSessions.chapterManifest(requireMangaOnlineSender(event), sessionId, chapterId)
)));
ipcMain.handle('mangaOnline:image', (event, sessionId, manifestId, index) => wrap(() => (
mangaOnlineSessions.image(
requireMangaOnlineSender(event),
sessionId,
manifestId,
index
)
)));
ipcMain.handle('mangaOnline:close', (event, sessionId) => wrap(() => (
mangaOnlineSessions.close(requireMangaOnlineSender(event), sessionId)
)));
// 代理配置:全局生效,影响所有数据源的 HTTP 请求与文件下载
ipcMain.handle('proxy:get', () => wrap(() => getProxy()));
ipcMain.handle('proxy:set', (_e, url) => {