feat(anime): queue episodes to play next across anime

- Episode rows gain Play/Queue actions (and matching context-menu items); queued rows show their place in line, with a queue count and Clear queue in the episode header
- Queue lives in the main process (`anime-browser-queue.ts`) so it survives the browser window closing and advances on mpv's end-file even when nobody is watching; streams resolve at play time so a signed URL cannot expire while queued
- Holds mpv's keep-open off while the queue waits and restores it once empty; queueing with nothing playing just plays immediately
- Adds anime-browser-queue and episode-queue unit tests, IPC channels/contracts, and doc updates
This commit is contained in:
2026-08-15 21:44:51 -07:00
parent 51fc9034f7
commit 3ca7dcd664
19 changed files with 838 additions and 7 deletions
+14
View File
@@ -10,6 +10,7 @@ import type {
AnimeBrowserSetWatchedRequest,
AnimeBrowserPlayRequest,
AnimeBrowserPlayResult,
AnimeBrowserQueueState,
AnimeBrowserSearchResult,
AnimeBrowserSearchUpdate,
AnimeBrowserSnapshot,
@@ -44,6 +45,14 @@ const animeBrowserAPI: AnimeBrowserAPI = {
ipcRenderer.invoke(request.animeBrowserSetWatched, watchedRequest),
playEpisode: (playRequest: AnimeBrowserPlayRequest): Promise<AnimeBrowserPlayResult> =>
ipcRenderer.invoke(request.animeBrowserPlayEpisode, playRequest),
queueEpisode: (playRequest: AnimeBrowserPlayRequest): Promise<AnimeBrowserQueueState> =>
ipcRenderer.invoke(request.animeBrowserQueueEpisode, playRequest),
dequeueEpisode: (sourceId: string, episodeUrl: string): Promise<AnimeBrowserQueueState> =>
ipcRenderer.invoke(request.animeBrowserDequeueEpisode, sourceId, episodeUrl),
clearQueue: (): Promise<AnimeBrowserQueueState> =>
ipcRenderer.invoke(request.animeBrowserClearQueue),
getQueue: (): Promise<AnimeBrowserQueueState> => ipcRenderer.invoke(request.animeBrowserGetQueue),
isPlaying: (): Promise<boolean> => ipcRenderer.invoke(request.animeBrowserIsPlaying),
getPreferences: (sourceId: string): Promise<SourcePreferenceView[]> =>
ipcRenderer.invoke(request.animeBrowserGetPreferences, sourceId),
setPreference: (
@@ -72,6 +81,11 @@ const animeBrowserAPI: AnimeBrowserAPI = {
ipcRenderer.on(IPC_CHANNELS.event.animeBrowserSearchUpdate, handler);
return () => ipcRenderer.removeListener(IPC_CHANNELS.event.animeBrowserSearchUpdate, handler);
},
onQueueState: (listener: (state: AnimeBrowserQueueState) => void): (() => void) => {
const handler = (_event: unknown, state: AnimeBrowserQueueState): void => listener(state);
ipcRenderer.on(IPC_CHANNELS.event.animeBrowserQueueState, handler);
return () => ipcRenderer.removeListener(IPC_CHANNELS.event.animeBrowserQueueState, handler);
},
};
contextBridge.exposeInMainWorld('animeBrowserAPI', animeBrowserAPI);