feat(subtitles): improve mpv hovered-token highlighting flow

- add subtitleStyle.hoverTokenColor config default + validation

- normalize hover color payloads and propagate configured color to mpv runtime

- refresh invisible overlay tokenization with current subtitle text and tighten hover overlay cleanup hooks

- record TASK-98 and subagent coordination updates
This commit is contained in:
2026-02-21 22:20:56 -08:00
parent 430c4e7120
commit 01f01f18e3
17 changed files with 244 additions and 15 deletions

View File

@@ -74,7 +74,18 @@ test('buildHoveredTokenPayload normalizes metadata and strips empty tokens', ()
assert.equal(payload.tokens[0]?.text, '昨日');
assert.equal(payload.tokens[0]?.index, 0);
assert.equal(payload.tokens[1]?.index, 1);
assert.equal(payload.colors.hover, 'E7C06A');
assert.equal(payload.colors.hover, 'C6A0F6');
});
test('buildHoveredTokenPayload normalizes hover color override', () => {
const payload = buildHoveredTokenPayload({
subtitle: SUBTITLE,
hoveredTokenIndex: 1,
revision: 7,
hoverColor: '#c6a0f6',
});
assert.equal(payload.colors.hover, 'C6A0F6');
});
test('buildHoveredTokenMessageCommand sends script-message-to subminer payload', () => {
@@ -111,6 +122,7 @@ test('createApplyHoveredTokenOverlayHandler sends clear payload when hovered tok
getCurrentSubtitleData: () => SUBTITLE,
getHoveredTokenIndex: () => null,
getHoveredSubtitleRevision: () => 3,
getHoverTokenColor: () => null,
});
apply();
@@ -134,6 +146,7 @@ test('createApplyHoveredTokenOverlayHandler sends highlight payload when hover i
getCurrentSubtitleData: () => SUBTITLE,
getHoveredTokenIndex: () => 0,
getHoveredSubtitleRevision: () => 3,
getHoverTokenColor: () => '#c6a0f6',
});
apply();
@@ -142,6 +155,7 @@ test('createApplyHoveredTokenOverlayHandler sends highlight payload when hover i
assert.equal(parsed.hoveredTokenIndex, 0);
assert.equal(parsed.subtitle, '昨日は雨だった。');
assert.equal(parsed.tokens.length, 4);
assert.equal(parsed.colors.hover, 'C6A0F6');
assert.equal(commands[0]?.[0], 'script-message-to');
assert.equal(commands[0]?.[1], HOVER_SCRIPT_NAME);
});

View File

@@ -3,7 +3,7 @@ import type { SubtitleData } from '../../types';
export const HOVER_SCRIPT_NAME = 'subminer';
export const HOVER_TOKEN_MESSAGE = 'subminer-hover-token';
const DEFAULT_HOVER_TOKEN_COLOR = 'E7C06A';
const DEFAULT_HOVER_TOKEN_COLOR = 'C6A0F6';
const DEFAULT_TOKEN_COLOR = 'FFFFFF';
export type HoverPayloadToken = {
@@ -28,8 +28,17 @@ type HoverTokenInput = {
subtitle: SubtitleData | null;
hoveredTokenIndex: number | null;
revision: number;
hoverColor?: string | null;
};
function normalizeHexColor(color: string | null | undefined, fallback: string): string {
if (typeof color !== 'string') {
return fallback;
}
const normalized = color.trim().replace(/^#/, '').toUpperCase();
return /^[0-9A-F]{6}$/.test(normalized) ? normalized : fallback;
}
function sanitizeSubtitleText(text: string): string {
return text
.replace(/\\N/g, '\n')
@@ -51,7 +60,7 @@ function hasHoveredToken(subtitle: SubtitleData | null, hoveredTokenIndex: numbe
}
export function buildHoveredTokenPayload(input: HoverTokenInput): HoverTokenPayload {
const { subtitle, hoveredTokenIndex, revision } = input;
const { subtitle, hoveredTokenIndex, revision, hoverColor } = input;
const tokens: HoverPayloadToken[] = [];
@@ -83,7 +92,7 @@ export function buildHoveredTokenPayload(input: HoverTokenInput): HoverTokenPayl
tokens,
colors: {
base: DEFAULT_TOKEN_COLOR,
hover: DEFAULT_HOVER_TOKEN_COLOR,
hover: normalizeHexColor(hoverColor, DEFAULT_HOVER_TOKEN_COLOR),
},
};
}
@@ -105,6 +114,7 @@ export function createApplyHoveredTokenOverlayHandler(deps: {
getCurrentSubtitleData: () => SubtitleData | null;
getHoveredTokenIndex: () => number | null;
getHoveredSubtitleRevision: () => number;
getHoverTokenColor: () => string | null;
}) {
return (): void => {
const mpvClient = deps.getMpvClient();
@@ -115,10 +125,12 @@ export function createApplyHoveredTokenOverlayHandler(deps: {
const subtitle = deps.getCurrentSubtitleData();
const hoveredTokenIndex = deps.getHoveredTokenIndex();
const revision = deps.getHoveredSubtitleRevision();
const hoverColor = deps.getHoverTokenColor();
const payload = buildHoveredTokenPayload({
subtitle: subtitle && hasHoveredToken(subtitle, hoveredTokenIndex) ? subtitle : null,
hoveredTokenIndex: hoveredTokenIndex,
revision,
hoverColor,
});
mpvClient.send({ command: buildHoveredTokenMessageCommand(payload) });