mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-16 13:55:51 -07:00
feat(anime): add anime browser powered by Aniyomi extensions
- Add `subminer anime` / `--anime` and a tray entry to open a browser that searches installed Aniyomi extension sources, shows cover art and episodes, and plays into mpv with overlay/mining attached - Add an Extensions tab to add repos and install/update/remove sources, and per-source settings for sources needing config - Support searching all sources at once with streaming, per-source results and status - Prefer Japanese audio/subtitle tracks from the source and keep the primary subtitle slot reserved for Japanese - Fix window/tray/Dock handling so the browser and mpv can be switched between without quitting the app or losing the Dock icon - Add anime.repos, anime.extensionsDir, anime.preferredQuality config keys (no bundled repos or discovery)
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { IPC_CHANNELS } from '../../shared/ipc/contracts';
|
||||
import type { AnimeBrowserRuntime } from './anime-browser-runtime';
|
||||
import type { AnimeBrowserPlayRequest } from '../../types/anime-browser';
|
||||
|
||||
export interface AnimeBrowserIpcDeps {
|
||||
// Structurally typed so tests can pass a fake without importing Electron.
|
||||
ipcMain: {
|
||||
handle(channel: string, listener: (event: unknown, ...args: unknown[]) => unknown): unknown;
|
||||
};
|
||||
runtime: AnimeBrowserRuntime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bridge the renderer to the anime runtime. Page arguments arrive as `unknown`
|
||||
* from the renderer, so they are coerced here rather than trusted.
|
||||
*/
|
||||
export function registerAnimeBrowserIpcHandlers(deps: AnimeBrowserIpcDeps): void {
|
||||
const channels = IPC_CHANNELS.request;
|
||||
const { runtime } = deps;
|
||||
const handle = (
|
||||
channel: string,
|
||||
listener: (event: unknown, ...args: unknown[]) => unknown,
|
||||
): void => {
|
||||
deps.ipcMain.handle(channel, listener);
|
||||
};
|
||||
|
||||
handle(channels.animeBrowserGetSnapshot, () => runtime.getSnapshot());
|
||||
handle(channels.animeBrowserEnsureBridge, () => runtime.ensureBridge());
|
||||
handle(channels.animeBrowserSelectSource, (_event, sourceId) =>
|
||||
runtime.selectSource(String(sourceId)),
|
||||
);
|
||||
handle(channels.animeBrowserSearch, (_event, query, page) =>
|
||||
runtime.search(String(query ?? ''), toPage(page)),
|
||||
);
|
||||
handle(channels.animeBrowserGetPopular, (_event, page) => runtime.getPopular(toPage(page)));
|
||||
handle(channels.animeBrowserGetDetails, (_event, animeUrl, sourceId) =>
|
||||
runtime.getDetails(String(animeUrl), toOptionalId(sourceId)),
|
||||
);
|
||||
handle(channels.animeBrowserGetEpisodes, (_event, animeUrl, sourceId) =>
|
||||
runtime.getEpisodes(String(animeUrl), toOptionalId(sourceId)),
|
||||
);
|
||||
handle(channels.animeBrowserListAvailableExtensions, () => runtime.listAvailableExtensions());
|
||||
handle(channels.animeBrowserInstallExtension, (_event, pkg) =>
|
||||
runtime.installExtension(String(pkg)),
|
||||
);
|
||||
handle(channels.animeBrowserRemoveExtension, (_event, pkg) =>
|
||||
runtime.removeExtension(String(pkg)),
|
||||
);
|
||||
handle(channels.animeBrowserRescanExtensions, () => runtime.rescanExtensions());
|
||||
handle(channels.animeBrowserAddRepo, (_event, url) => runtime.addRepo(String(url)));
|
||||
handle(channels.animeBrowserRemoveRepo, (_event, url) => runtime.removeRepo(String(url)));
|
||||
handle(channels.animeBrowserPlayEpisode, (_event, request) =>
|
||||
runtime.playEpisode(request as AnimeBrowserPlayRequest),
|
||||
);
|
||||
handle(channels.animeBrowserGetPreferences, (_event, sourceId) =>
|
||||
runtime.getPreferences(String(sourceId)),
|
||||
);
|
||||
handle(channels.animeBrowserSetPreference, (_event, sourceId, key, value) =>
|
||||
runtime.setPreference(String(sourceId), String(key), value as string | string[] | boolean),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A source id the renderer may omit. Absent means "use the current selection",
|
||||
* so an empty value must stay undefined rather than becoming the string "".
|
||||
*/
|
||||
function toOptionalId(value: unknown): string | undefined {
|
||||
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
/** Bridge pages are 1-based; anything unusable falls back to the first page. */
|
||||
function toPage(value: unknown): number {
|
||||
const page = Number(value);
|
||||
return Number.isFinite(page) && page >= 1 ? Math.floor(page) : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user