feat(subtitle-sidebar): add sidebar runtime and modal plumbing

This commit is contained in:
2026-03-20 23:00:26 -07:00
parent bb54898747
commit ea86f4e504
28 changed files with 2013 additions and 57 deletions

View File

@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
import { createIpcDepsRuntime, registerIpcHandlers, type IpcServiceDeps } from './ipc';
import { IPC_CHANNELS } from '../../shared/ipc/contracts';
import type { SubtitleSidebarSnapshot } from '../../types';
interface FakeIpcRegistrar {
on: Map<string, (event: unknown, ...args: unknown[]) => void>;
@@ -77,6 +78,30 @@ function createControllerConfigFixture() {
};
}
function createSubtitleSidebarSnapshotFixture(): SubtitleSidebarSnapshot {
return {
cues: [],
currentSubtitle: { text: '', startTime: null, endTime: null },
config: {
enabled: false,
layout: 'overlay',
toggleKey: 'Backslash',
pauseVideoOnHover: false,
autoScroll: true,
maxWidth: 420,
opacity: 0.92,
backgroundColor: 'rgba(54, 58, 79, 0.88)',
textColor: '#cad3f5',
fontFamily: '"M PLUS 1", "Noto Sans CJK JP", sans-serif',
fontSize: 16,
timestampColor: '#a5adcb',
activeLineColor: '#f5bde6',
activeLineBackgroundColor: 'rgba(138, 173, 244, 0.22)',
hoverLineBackgroundColor: 'rgba(54, 58, 79, 0.84)',
},
};
}
function createRegisterIpcDeps(overrides: Partial<IpcServiceDeps> = {}): IpcServiceDeps {
return {
onOverlayModalClosed: () => {},
@@ -88,6 +113,7 @@ function createRegisterIpcDeps(overrides: Partial<IpcServiceDeps> = {}): IpcServ
tokenizeCurrentSubtitle: async () => null,
getCurrentSubtitleRaw: () => '',
getCurrentSubtitleAss: () => '',
getSubtitleSidebarSnapshot: async () => createSubtitleSidebarSnapshotFixture(),
getPlaybackPaused: () => false,
getSubtitlePosition: () => null,
getSubtitleStyle: () => null,
@@ -173,6 +199,7 @@ test('createIpcDepsRuntime wires AniList handlers', async () => {
tokenizeCurrentSubtitle: async () => null,
getCurrentSubtitleRaw: () => '',
getCurrentSubtitleAss: () => '',
getSubtitleSidebarSnapshot: async () => createSubtitleSidebarSnapshotFixture(),
getPlaybackPaused: () => true,
getSubtitlePosition: () => null,
getSubtitleStyle: () => null,
@@ -269,6 +296,7 @@ test('registerIpcHandlers rejects malformed runtime-option payloads', async () =
cycles.push({ id, direction });
return { ok: true };
},
getSubtitleSidebarSnapshot: async () => createSubtitleSidebarSnapshotFixture(),
reportOverlayContentBounds: () => {},
getAnilistStatus: () => ({}),
clearAnilistToken: () => {},
@@ -320,6 +348,24 @@ test('registerIpcHandlers rejects malformed runtime-option payloads', async () =
);
});
test('registerIpcHandlers exposes subtitle sidebar snapshot request', async () => {
const { registrar, handlers } = createFakeIpcRegistrar();
const snapshot = createSubtitleSidebarSnapshotFixture();
snapshot.cues = [{ startTime: 1, endTime: 2, text: 'line-1' }];
snapshot.config.enabled = true;
registerIpcHandlers(
createRegisterIpcDeps({
getSubtitleSidebarSnapshot: async () => snapshot,
}),
registrar,
);
const handler = handlers.handle.get(IPC_CHANNELS.request.getSubtitleSidebarSnapshot);
assert.ok(handler);
assert.deepEqual(await handler!({}), snapshot);
});
test('registerIpcHandlers forwards yomitan lookup tracking commands to immersion tracker', () => {
const { registrar, handlers } = createFakeIpcRegistrar();
const calls: string[] = [];
@@ -530,6 +576,7 @@ test('registerIpcHandlers ignores malformed fire-and-forget payloads', () => {
tokenizeCurrentSubtitle: async () => null,
getCurrentSubtitleRaw: () => '',
getCurrentSubtitleAss: () => '',
getSubtitleSidebarSnapshot: async () => createSubtitleSidebarSnapshotFixture(),
getPlaybackPaused: () => false,
getSubtitlePosition: () => null,
getSubtitleStyle: () => null,
@@ -596,6 +643,7 @@ test('registerIpcHandlers awaits saveControllerPreference through request-respon
tokenizeCurrentSubtitle: async () => null,
getCurrentSubtitleRaw: () => '',
getCurrentSubtitleAss: () => '',
getSubtitleSidebarSnapshot: async () => createSubtitleSidebarSnapshotFixture(),
getPlaybackPaused: () => false,
getSubtitlePosition: () => null,
getSubtitleStyle: () => null,
@@ -667,6 +715,7 @@ test('registerIpcHandlers rejects malformed controller preference payloads', asy
tokenizeCurrentSubtitle: async () => null,
getCurrentSubtitleRaw: () => '',
getCurrentSubtitleAss: () => '',
getSubtitleSidebarSnapshot: async () => createSubtitleSidebarSnapshotFixture(),
getPlaybackPaused: () => false,
getSubtitlePosition: () => null,
getSubtitleStyle: () => null,

View File

@@ -6,6 +6,7 @@ import type {
ResolvedControllerConfig,
RuntimeOptionId,
RuntimeOptionValue,
SubtitleSidebarSnapshot,
SubtitlePosition,
SubsyncManualRunRequest,
SubsyncResult,
@@ -37,6 +38,7 @@ export interface IpcServiceDeps {
tokenizeCurrentSubtitle: () => Promise<unknown>;
getCurrentSubtitleRaw: () => string;
getCurrentSubtitleAss: () => string;
getSubtitleSidebarSnapshot?: () => Promise<SubtitleSidebarSnapshot>;
getPlaybackPaused: () => boolean | null;
getSubtitlePosition: () => unknown;
getSubtitleStyle: () => unknown;
@@ -143,6 +145,7 @@ export interface IpcDepsRuntimeOptions {
tokenizeCurrentSubtitle: () => Promise<unknown>;
getCurrentSubtitleRaw: () => string;
getCurrentSubtitleAss: () => string;
getSubtitleSidebarSnapshot?: () => Promise<SubtitleSidebarSnapshot>;
getPlaybackPaused: () => boolean | null;
getSubtitlePosition: () => unknown;
getSubtitleStyle: () => unknown;
@@ -190,6 +193,7 @@ export function createIpcDepsRuntime(options: IpcDepsRuntimeOptions): IpcService
tokenizeCurrentSubtitle: options.tokenizeCurrentSubtitle,
getCurrentSubtitleRaw: options.getCurrentSubtitleRaw,
getCurrentSubtitleAss: options.getCurrentSubtitleAss,
getSubtitleSidebarSnapshot: options.getSubtitleSidebarSnapshot,
getPlaybackPaused: options.getPlaybackPaused,
getSubtitlePosition: options.getSubtitlePosition,
getSubtitleStyle: options.getSubtitleStyle,
@@ -321,6 +325,13 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
return deps.getCurrentSubtitleAss();
});
ipc.handle(IPC_CHANNELS.request.getSubtitleSidebarSnapshot, async () => {
if (!deps.getSubtitleSidebarSnapshot) {
throw new Error('Subtitle sidebar snapshot is unavailable.');
}
return await deps.getSubtitleSidebarSnapshot();
});
ipc.handle(IPC_CHANNELS.request.getPlaybackPaused, () => {
return deps.getPlaybackPaused();
});

View File

@@ -183,7 +183,13 @@ export function parseSubtitleCues(content: string, filename: string): SubtitleCu
cues = parseAssCues(content);
break;
default:
return [];
cues = [];
}
if (cues.length === 0) {
const assCues = parseAssCues(content);
const srtCues = parseSrtCues(content);
cues = assCues.length >= srtCues.length ? assCues : srtCues;
}
cues.sort((a, b) => a.startTime - b.startTime);