fix: address claude review feedback on overlay refactor

This commit is contained in:
2026-02-26 18:47:51 -08:00
parent 75442a4648
commit a03388a38f
28 changed files with 95 additions and 197 deletions

View File

@@ -43,7 +43,7 @@ export function buildCoreConfigOptionRegistry(
kind: 'boolean', kind: 'boolean',
defaultValue: defaultConfig.bind_visible_overlay_to_mpv_sub_visibility, defaultValue: defaultConfig.bind_visible_overlay_to_mpv_sub_visibility,
description: description:
'Link visible overlay toggles to MPV subtitle visibility (primary and secondary).', 'Link visible overlay toggles to MPV primary subtitle visibility.',
}, },
]; ];
} }

View File

@@ -212,10 +212,6 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
deps.toggleDevTools(); deps.toggleDevTools();
}); });
ipc.handle(IPC_CHANNELS.request.getOverlayVisibility, () => {
return deps.getVisibleOverlayVisibility();
});
ipc.on(IPC_CHANNELS.command.toggleOverlay, () => { ipc.on(IPC_CHANNELS.command.toggleOverlay, () => {
deps.toggleVisibleOverlay(); deps.toggleVisibleOverlay();
}); });

View File

@@ -131,23 +131,7 @@ test('dispatchMpvProtocolMessage enforces sub-visibility hidden when overlay sup
); );
assert.deepEqual(state.commands.pop(), { assert.deepEqual(state.commands.pop(), {
command: ['set_property', 'sub-visibility', 'no'], command: ['set_property', 'sub-visibility', false],
});
});
test('dispatchMpvProtocolMessage enforces secondary sub-visibility hidden when overlay suppression is enabled', async () => {
const { deps, state } = createDeps({
shouldBindVisibleOverlayToMpvSubVisibility: () => true,
isVisibleOverlayVisible: () => true,
});
await dispatchMpvProtocolMessage(
{ event: 'property-change', name: 'secondary-sub-visibility', data: 'yes' },
deps,
);
assert.deepEqual(state.commands.pop(), {
command: ['set_property', 'secondary-sub-visibility', 'no'],
}); });
}); });

View File

@@ -223,15 +223,7 @@ export async function dispatchMpvProtocolMessage(
deps.isVisibleOverlayVisible() && deps.isVisibleOverlayVisible() &&
asBoolean(msg.data, false) asBoolean(msg.data, false)
) { ) {
deps.sendCommand({ command: ['set_property', 'sub-visibility', 'no'] }); deps.sendCommand({ command: ['set_property', 'sub-visibility', false] });
}
} else if (msg.name === 'secondary-sub-visibility') {
if (
deps.shouldBindVisibleOverlayToMpvSubVisibility?.() &&
deps.isVisibleOverlayVisible() &&
asBoolean(msg.data, false)
) {
deps.sendCommand({ command: ['set_property', 'secondary-sub-visibility', 'no'] });
} }
} else if (msg.name === 'sub-use-margins') { } else if (msg.name === 'sub-use-margins') {
deps.emitSubtitleMetricsChange({ deps.emitSubtitleMetricsChange({

View File

@@ -474,7 +474,7 @@ export class MpvIpcClient implements MpvClient {
setSubVisibility(visible: boolean): void { setSubVisibility(visible: boolean): void {
this.send({ this.send({
command: ['set_property', 'sub-visibility', visible ? 'yes' : 'no'], command: ['set_property', 'sub-visibility', visible],
}); });
} }

View File

@@ -15,7 +15,7 @@ export function updateVisibleOverlayVisibility(args: {
syncOverlayShortcuts: () => void; syncOverlayShortcuts: () => void;
isMacOSPlatform?: boolean; isMacOSPlatform?: boolean;
showOverlayLoadingOsd?: (message: string) => void; showOverlayLoadingOsd?: (message: string) => void;
resolveFallbackBounds: () => WindowGeometry; resolveFallbackBounds?: () => WindowGeometry;
}): void { }): void {
if (!args.mainWindow || args.mainWindow.isDestroyed()) { if (!args.mainWindow || args.mainWindow.isDestroyed()) {
return; return;
@@ -78,7 +78,9 @@ export function updateVisibleOverlayVisibility(args: {
return; return;
} }
const fallbackBounds = args.resolveFallbackBounds(); const fallbackBounds = args.resolveFallbackBounds?.();
if (!fallbackBounds) return;
args.updateVisibleOverlayBounds(fallbackBounds); args.updateVisibleOverlayBounds(fallbackBounds);
args.syncPrimaryOverlayWindowLayer('visible'); args.syncPrimaryOverlayWindowLayer('visible');
args.mainWindow.setIgnoreMouseEvents(false); args.mainWindow.setIgnoreMouseEvents(false);

View File

@@ -354,7 +354,6 @@ import {
runStartupBootstrapRuntime, runStartupBootstrapRuntime,
saveSubtitlePosition as saveSubtitlePositionCore, saveSubtitlePosition as saveSubtitlePositionCore,
sendMpvCommandRuntime, sendMpvCommandRuntime,
setMpvSecondarySubVisibilityRuntime,
setMpvSubVisibilityRuntime, setMpvSubVisibilityRuntime,
setOverlayDebugVisualizationEnabledRuntime, setOverlayDebugVisualizationEnabledRuntime,
syncOverlayWindowLayer, syncOverlayWindowLayer,
@@ -731,10 +730,6 @@ const ensureOverlayMpvSubtitlesHidden = createEnsureOverlayMpvSubtitlesHiddenHan
setSavedSubVisibility: (visible) => { setSavedSubVisibility: (visible) => {
appState.overlaySavedMpvSubVisibility = visible; appState.overlaySavedMpvSubVisibility = visible;
}, },
getSavedSecondarySubVisibility: () => appState.overlaySavedSecondaryMpvSubVisibility,
setSavedSecondarySubVisibility: (visible) => {
appState.overlaySavedSecondaryMpvSubVisibility = visible;
},
getRevision: () => appState.overlayMpvSubVisibilityRevision, getRevision: () => appState.overlayMpvSubVisibilityRevision,
setRevision: (revision) => { setRevision: (revision) => {
appState.overlayMpvSubVisibilityRevision = revision; appState.overlayMpvSubVisibilityRevision = revision;
@@ -742,9 +737,6 @@ const ensureOverlayMpvSubtitlesHidden = createEnsureOverlayMpvSubtitlesHiddenHan
setMpvSubVisibility: (visible) => { setMpvSubVisibility: (visible) => {
setMpvSubVisibilityRuntime(appState.mpvClient, visible); setMpvSubVisibilityRuntime(appState.mpvClient, visible);
}, },
setMpvSecondarySubVisibility: (visible) => {
setMpvSecondarySubVisibilityRuntime(appState.mpvClient, visible);
},
logWarn: (message, error) => { logWarn: (message, error) => {
logger.warn(message, error); logger.warn(message, error);
}, },
@@ -754,10 +746,6 @@ const restoreOverlayMpvSubtitles = createRestoreOverlayMpvSubtitlesHandler({
setSavedSubVisibility: (visible) => { setSavedSubVisibility: (visible) => {
appState.overlaySavedMpvSubVisibility = visible; appState.overlaySavedMpvSubVisibility = visible;
}, },
getSavedSecondarySubVisibility: () => appState.overlaySavedSecondaryMpvSubVisibility,
setSavedSecondarySubVisibility: (visible) => {
appState.overlaySavedSecondaryMpvSubVisibility = visible;
},
getRevision: () => appState.overlayMpvSubVisibilityRevision, getRevision: () => appState.overlayMpvSubVisibilityRevision,
setRevision: (revision) => { setRevision: (revision) => {
appState.overlayMpvSubVisibilityRevision = revision; appState.overlayMpvSubVisibilityRevision = revision;
@@ -767,16 +755,12 @@ const restoreOverlayMpvSubtitles = createRestoreOverlayMpvSubtitlesHandler({
setMpvSubVisibility: (visible) => { setMpvSubVisibility: (visible) => {
setMpvSubVisibilityRuntime(appState.mpvClient, visible); setMpvSubVisibilityRuntime(appState.mpvClient, visible);
}, },
setMpvSecondarySubVisibility: (visible) => {
setMpvSecondarySubVisibilityRuntime(appState.mpvClient, visible);
},
}); });
function shouldSuppressMpvSubtitlesForOverlay(): boolean { function shouldSuppressMpvSubtitlesForOverlay(): boolean {
return ( return (
appState.secondarySubMode === 'visible' || overlayManager.getVisibleOverlayVisible() &&
(overlayManager.getVisibleOverlayVisible() && configDerivedRuntime.shouldBindVisibleOverlayToMpvSubVisibility()
configDerivedRuntime.shouldBindVisibleOverlayToMpvSubVisibility())
); );
} }
@@ -1884,8 +1868,8 @@ const {
destroyTray: () => destroyTray(), destroyTray: () => destroyTray(),
stopConfigHotReload: () => configHotReloadRuntime.stop(), stopConfigHotReload: () => configHotReloadRuntime.stop(),
restorePreviousSecondarySubVisibility: () => restorePreviousSecondarySubVisibility(), restorePreviousSecondarySubVisibility: () => restorePreviousSecondarySubVisibility(),
restoreMpvSubVisibilityForInvisibleOverlay: () => { restoreMpvSubVisibility: () => {
restoreOverlayMpvSubtitles({ respectVisibleOverlayBinding: false }); restoreOverlayMpvSubtitles();
}, },
unregisterAllGlobalShortcuts: () => globalShortcut.unregisterAll(), unregisterAllGlobalShortcuts: () => globalShortcut.unregisterAll(),
stopSubtitleWebsocket: () => subtitleWsService.stop(), stopSubtitleWebsocket: () => subtitleWsService.stop(),
@@ -2181,8 +2165,8 @@ const {
updateCurrentMediaPath: (path) => { updateCurrentMediaPath: (path) => {
mediaRuntime.updateCurrentMediaPath(path); mediaRuntime.updateCurrentMediaPath(path);
}, },
restoreMpvSubVisibilityForInvisibleOverlay: () => { restoreMpvSubVisibility: () => {
restoreOverlayMpvSubtitles({ respectVisibleOverlayBinding: false }); restoreOverlayMpvSubtitles();
}, },
getCurrentAnilistMediaKey: () => getCurrentAnilistMediaKey(), getCurrentAnilistMediaKey: () => getCurrentAnilistMediaKey(),
resetAnilistMediaTracking: (mediaKey) => { resetAnilistMediaTracking: (mediaKey) => {
@@ -2529,7 +2513,6 @@ const cycleSecondarySubMode = createCycleSecondarySubModeRuntimeHandler({
function setSecondarySubMode(mode: SecondarySubMode): void { function setSecondarySubMode(mode: SecondarySubMode): void {
appState.secondarySubMode = mode; appState.secondarySubMode = mode;
syncOverlayMpvSubtitleSuppression();
} }
function handleCycleSecondarySubMode(): void { function handleCycleSecondarySubMode(): void {

View File

@@ -50,7 +50,7 @@ function createMockWindow(): MockWindow & {
url: 'file:///overlay/index.html?layer=modal', url: 'file:///overlay/index.html?layer=modal',
loadCallbacks: [], loadCallbacks: [],
}; };
return { const window = {
...state, ...state,
isDestroyed: () => state.destroyed, isDestroyed: () => state.destroyed,
isVisible: () => state.visible, isVisible: () => state.visible,
@@ -92,6 +92,29 @@ function createMockWindow(): MockWindow & {
}, },
}, },
}; };
Object.defineProperty(window, 'loading', {
get: () => state.loading,
set: (value: boolean) => {
state.loading = value;
},
});
Object.defineProperty(window, 'url', {
get: () => state.url,
set: (value: string) => {
state.url = value;
},
});
Object.defineProperty(window, 'ignoreMouseEvents', {
get: () => state.ignoreMouseEvents,
set: (value: boolean) => {
state.ignoreMouseEvents = value;
},
});
return window;
} }
test('sendToActiveOverlayWindow targets modal window with full geometry and tracks close restore', () => { test('sendToActiveOverlayWindow targets modal window with full geometry and tracks close restore', () => {

View File

@@ -1,6 +1,8 @@
import type { BrowserWindow } from 'electron'; import type { BrowserWindow } from 'electron';
import type { WindowGeometry } from '../types'; import type { WindowGeometry } from '../types';
const MODAL_REVEAL_FALLBACK_DELAY_MS = 250;
type OverlayHostedModal = 'runtime-options' | 'subsync' | 'jimaku' | 'kiku'; type OverlayHostedModal = 'runtime-options' | 'subsync' | 'jimaku' | 'kiku';
export interface OverlayWindowResolver { export interface OverlayWindowResolver {
@@ -67,14 +69,7 @@ export function createOverlayModalRuntimeService(
if (window.webContents.isLoading()) { if (window.webContents.isLoading()) {
return false; return false;
} }
return window.webContents.getURL() !== '' && window.webContents.getURL() !== 'about:blank';
const getURL = window.webContents.getURL;
if (typeof getURL !== 'function') {
return true;
}
const currentURL = getURL.call(window.webContents);
return currentURL !== '' && currentURL !== 'about:blank';
}; };
const sendOrQueueForWindow = ( const sendOrQueueForWindow = (
@@ -142,7 +137,7 @@ export function createOverlayModalRuntimeService(
return; return;
} }
showModalWindow(targetWindow, { passThroughMouseEvents: true }); showModalWindow(targetWindow, { passThroughMouseEvents: true });
}, 250); }, MODAL_REVEAL_FALLBACK_DELAY_MS);
}; };
const sendToActiveOverlayWindow = ( const sendToActiveOverlayWindow = (
@@ -208,8 +203,6 @@ export function createOverlayModalRuntimeService(
if (restoreVisibleOverlayOnModalClose.size === 0) { if (restoreVisibleOverlayOnModalClose.size === 0) {
clearPendingModalWindowReveal(); clearPendingModalWindowReveal();
notifyModalStateChange(false); notifyModalStateChange(false);
}
if (restoreVisibleOverlayOnModalClose.size === 0) {
modalWindow.hide(); modalWindow.hide();
} }
}; };

View File

@@ -12,7 +12,7 @@ test('on will quit cleanup handler runs all cleanup steps', () => {
destroyTray: () => calls.push('destroy-tray'), destroyTray: () => calls.push('destroy-tray'),
stopConfigHotReload: () => calls.push('stop-config'), stopConfigHotReload: () => calls.push('stop-config'),
restorePreviousSecondarySubVisibility: () => calls.push('restore-sub'), restorePreviousSecondarySubVisibility: () => calls.push('restore-sub'),
restoreMpvSubVisibilityForInvisibleOverlay: () => calls.push('restore-mpv-sub'), restoreMpvSubVisibility: () => calls.push('restore-mpv-sub'),
unregisterAllGlobalShortcuts: () => calls.push('unregister-shortcuts'), unregisterAllGlobalShortcuts: () => calls.push('unregister-shortcuts'),
stopSubtitleWebsocket: () => calls.push('stop-ws'), stopSubtitleWebsocket: () => calls.push('stop-ws'),
stopTexthookerService: () => calls.push('stop-texthooker'), stopTexthookerService: () => calls.push('stop-texthooker'),

View File

@@ -2,7 +2,7 @@ export function createOnWillQuitCleanupHandler(deps: {
destroyTray: () => void; destroyTray: () => void;
stopConfigHotReload: () => void; stopConfigHotReload: () => void;
restorePreviousSecondarySubVisibility: () => void; restorePreviousSecondarySubVisibility: () => void;
restoreMpvSubVisibilityForInvisibleOverlay: () => void; restoreMpvSubVisibility: () => void;
unregisterAllGlobalShortcuts: () => void; unregisterAllGlobalShortcuts: () => void;
stopSubtitleWebsocket: () => void; stopSubtitleWebsocket: () => void;
stopTexthookerService: () => void; stopTexthookerService: () => void;
@@ -26,7 +26,7 @@ export function createOnWillQuitCleanupHandler(deps: {
deps.destroyTray(); deps.destroyTray();
deps.stopConfigHotReload(); deps.stopConfigHotReload();
deps.restorePreviousSecondarySubVisibility(); deps.restorePreviousSecondarySubVisibility();
deps.restoreMpvSubVisibilityForInvisibleOverlay(); deps.restoreMpvSubVisibility();
deps.unregisterAllGlobalShortcuts(); deps.unregisterAllGlobalShortcuts();
deps.stopSubtitleWebsocket(); deps.stopSubtitleWebsocket();
deps.stopTexthookerService(); deps.stopTexthookerService();

View File

@@ -14,7 +14,7 @@ test('cleanup deps builder returns handlers that guard optional runtime objects'
destroyTray: () => calls.push('destroy-tray'), destroyTray: () => calls.push('destroy-tray'),
stopConfigHotReload: () => calls.push('stop-config'), stopConfigHotReload: () => calls.push('stop-config'),
restorePreviousSecondarySubVisibility: () => calls.push('restore-sub'), restorePreviousSecondarySubVisibility: () => calls.push('restore-sub'),
restoreMpvSubVisibilityForInvisibleOverlay: () => calls.push('restore-mpv-sub'), restoreMpvSubVisibility: () => calls.push('restore-mpv-sub'),
unregisterAllGlobalShortcuts: () => calls.push('unregister-shortcuts'), unregisterAllGlobalShortcuts: () => calls.push('unregister-shortcuts'),
stopSubtitleWebsocket: () => calls.push('stop-ws'), stopSubtitleWebsocket: () => calls.push('stop-ws'),
stopTexthookerService: () => calls.push('stop-texthooker'), stopTexthookerService: () => calls.push('stop-texthooker'),
@@ -73,7 +73,7 @@ test('cleanup deps builder skips destroyed yomitan window', () => {
destroyTray: () => {}, destroyTray: () => {},
stopConfigHotReload: () => {}, stopConfigHotReload: () => {},
restorePreviousSecondarySubVisibility: () => {}, restorePreviousSecondarySubVisibility: () => {},
restoreMpvSubVisibilityForInvisibleOverlay: () => {}, restoreMpvSubVisibility: () => {},
unregisterAllGlobalShortcuts: () => {}, unregisterAllGlobalShortcuts: () => {},
stopSubtitleWebsocket: () => {}, stopSubtitleWebsocket: () => {},
stopTexthookerService: () => {}, stopTexthookerService: () => {},

View File

@@ -21,7 +21,7 @@ export function createBuildOnWillQuitCleanupDepsHandler(deps: {
destroyTray: () => void; destroyTray: () => void;
stopConfigHotReload: () => void; stopConfigHotReload: () => void;
restorePreviousSecondarySubVisibility: () => void; restorePreviousSecondarySubVisibility: () => void;
restoreMpvSubVisibilityForInvisibleOverlay: () => void; restoreMpvSubVisibility: () => void;
unregisterAllGlobalShortcuts: () => void; unregisterAllGlobalShortcuts: () => void;
stopSubtitleWebsocket: () => void; stopSubtitleWebsocket: () => void;
stopTexthookerService: () => void; stopTexthookerService: () => void;
@@ -52,8 +52,8 @@ export function createBuildOnWillQuitCleanupDepsHandler(deps: {
destroyTray: () => deps.destroyTray(), destroyTray: () => deps.destroyTray(),
stopConfigHotReload: () => deps.stopConfigHotReload(), stopConfigHotReload: () => deps.stopConfigHotReload(),
restorePreviousSecondarySubVisibility: () => deps.restorePreviousSecondarySubVisibility(), restorePreviousSecondarySubVisibility: () => deps.restorePreviousSecondarySubVisibility(),
restoreMpvSubVisibilityForInvisibleOverlay: () => restoreMpvSubVisibility: () =>
deps.restoreMpvSubVisibilityForInvisibleOverlay(), deps.restoreMpvSubVisibility(),
unregisterAllGlobalShortcuts: () => deps.unregisterAllGlobalShortcuts(), unregisterAllGlobalShortcuts: () => deps.unregisterAllGlobalShortcuts(),
stopSubtitleWebsocket: () => deps.stopSubtitleWebsocket(), stopSubtitleWebsocket: () => deps.stopSubtitleWebsocket(),
stopTexthookerService: () => deps.stopTexthookerService(), stopTexthookerService: () => deps.stopTexthookerService(),

View File

@@ -75,7 +75,7 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
onSubtitleChange: () => {}, onSubtitleChange: () => {},
refreshDiscordPresence: () => {}, refreshDiscordPresence: () => {},
updateCurrentMediaPath: () => {}, updateCurrentMediaPath: () => {},
restoreMpvSubVisibilityForInvisibleOverlay: () => {}, restoreMpvSubVisibility: () => {},
getCurrentAnilistMediaKey: () => null, getCurrentAnilistMediaKey: () => null,
resetAnilistMediaTracking: () => {}, resetAnilistMediaTracking: () => {},
maybeProbeAnilistDuration: () => {}, maybeProbeAnilistDuration: () => {},

View File

@@ -16,7 +16,7 @@ test('composeStartupLifecycleHandlers returns callable startup lifecycle handler
destroyTray: () => {}, destroyTray: () => {},
stopConfigHotReload: () => {}, stopConfigHotReload: () => {},
restorePreviousSecondarySubVisibility: () => {}, restorePreviousSecondarySubVisibility: () => {},
restoreMpvSubVisibilityForInvisibleOverlay: () => {}, restoreMpvSubVisibility: () => {},
unregisterAllGlobalShortcuts: () => {}, unregisterAllGlobalShortcuts: () => {},
stopSubtitleWebsocket: () => {}, stopSubtitleWebsocket: () => {},
stopTexthookerService: () => {}, stopTexthookerService: () => {},

View File

@@ -51,7 +51,7 @@ test('media path change handler reports stop for empty path and probes media key
const handler = createHandleMpvMediaPathChangeHandler({ const handler = createHandleMpvMediaPathChangeHandler({
updateCurrentMediaPath: (path) => calls.push(`path:${path}`), updateCurrentMediaPath: (path) => calls.push(`path:${path}`),
reportJellyfinRemoteStopped: () => calls.push('stopped'), reportJellyfinRemoteStopped: () => calls.push('stopped'),
restoreMpvSubVisibilityForInvisibleOverlay: () => calls.push('restore-mpv-sub'), restoreMpvSubVisibility: () => calls.push('restore-mpv-sub'),
getCurrentAnilistMediaKey: () => 'show:1', getCurrentAnilistMediaKey: () => 'show:1',
resetAnilistMediaTracking: (mediaKey) => calls.push(`reset:${String(mediaKey)}`), resetAnilistMediaTracking: (mediaKey) => calls.push(`reset:${String(mediaKey)}`),
maybeProbeAnilistDuration: (mediaKey) => calls.push(`probe:${mediaKey}`), maybeProbeAnilistDuration: (mediaKey) => calls.push(`probe:${mediaKey}`),

View File

@@ -33,7 +33,7 @@ export function createHandleMpvSecondarySubtitleChangeHandler(deps: {
export function createHandleMpvMediaPathChangeHandler(deps: { export function createHandleMpvMediaPathChangeHandler(deps: {
updateCurrentMediaPath: (path: string) => void; updateCurrentMediaPath: (path: string) => void;
reportJellyfinRemoteStopped: () => void; reportJellyfinRemoteStopped: () => void;
restoreMpvSubVisibilityForInvisibleOverlay: () => void; restoreMpvSubVisibility: () => void;
getCurrentAnilistMediaKey: () => string | null; getCurrentAnilistMediaKey: () => string | null;
resetAnilistMediaTracking: (mediaKey: string | null) => void; resetAnilistMediaTracking: (mediaKey: string | null) => void;
maybeProbeAnilistDuration: (mediaKey: string) => void; maybeProbeAnilistDuration: (mediaKey: string) => void;
@@ -45,7 +45,7 @@ export function createHandleMpvMediaPathChangeHandler(deps: {
deps.updateCurrentMediaPath(path); deps.updateCurrentMediaPath(path);
if (!path) { if (!path) {
deps.reportJellyfinRemoteStopped(); deps.reportJellyfinRemoteStopped();
deps.restoreMpvSubVisibilityForInvisibleOverlay(); deps.restoreMpvSubVisibility();
} }
const mediaKey = deps.getCurrentAnilistMediaKey(); const mediaKey = deps.getCurrentAnilistMediaKey();
deps.resetAnilistMediaTracking(mediaKey); deps.resetAnilistMediaTracking(mediaKey);

View File

@@ -36,7 +36,7 @@ test('main mpv event binder wires callbacks through to runtime deps', () => {
broadcastSecondarySubtitle: (text) => calls.push(`broadcast-secondary:${text}`), broadcastSecondarySubtitle: (text) => calls.push(`broadcast-secondary:${text}`),
updateCurrentMediaPath: (path) => calls.push(`media-path:${path}`), updateCurrentMediaPath: (path) => calls.push(`media-path:${path}`),
restoreMpvSubVisibilityForInvisibleOverlay: () => calls.push('restore-mpv-sub'), restoreMpvSubVisibility: () => calls.push('restore-mpv-sub'),
getCurrentAnilistMediaKey: () => 'media-key', getCurrentAnilistMediaKey: () => 'media-key',
resetAnilistMediaTracking: (key) => calls.push(`reset-media:${String(key)}`), resetAnilistMediaTracking: (key) => calls.push(`reset-media:${String(key)}`),
maybeProbeAnilistDuration: (mediaKey) => calls.push(`probe:${mediaKey}`), maybeProbeAnilistDuration: (mediaKey) => calls.push(`probe:${mediaKey}`),

View File

@@ -43,7 +43,7 @@ export function createBindMpvMainEventHandlersHandler(deps: {
broadcastSecondarySubtitle: (text: string) => void; broadcastSecondarySubtitle: (text: string) => void;
updateCurrentMediaPath: (path: string) => void; updateCurrentMediaPath: (path: string) => void;
restoreMpvSubVisibilityForInvisibleOverlay: () => void; restoreMpvSubVisibility: () => void;
getCurrentAnilistMediaKey: () => string | null; getCurrentAnilistMediaKey: () => string | null;
resetAnilistMediaTracking: (mediaKey: string | null) => void; resetAnilistMediaTracking: (mediaKey: string | null) => void;
maybeProbeAnilistDuration: (mediaKey: string) => void; maybeProbeAnilistDuration: (mediaKey: string) => void;
@@ -97,8 +97,8 @@ export function createBindMpvMainEventHandlersHandler(deps: {
const handleMpvMediaPathChange = createHandleMpvMediaPathChangeHandler({ const handleMpvMediaPathChange = createHandleMpvMediaPathChangeHandler({
updateCurrentMediaPath: (path) => deps.updateCurrentMediaPath(path), updateCurrentMediaPath: (path) => deps.updateCurrentMediaPath(path),
reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(), reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(),
restoreMpvSubVisibilityForInvisibleOverlay: () => restoreMpvSubVisibility: () =>
deps.restoreMpvSubVisibilityForInvisibleOverlay(), deps.restoreMpvSubVisibility(),
getCurrentAnilistMediaKey: () => deps.getCurrentAnilistMediaKey(), getCurrentAnilistMediaKey: () => deps.getCurrentAnilistMediaKey(),
resetAnilistMediaTracking: (mediaKey) => deps.resetAnilistMediaTracking(mediaKey), resetAnilistMediaTracking: (mediaKey) => deps.resetAnilistMediaTracking(mediaKey),
maybeProbeAnilistDuration: (mediaKey) => deps.maybeProbeAnilistDuration(mediaKey), maybeProbeAnilistDuration: (mediaKey) => deps.maybeProbeAnilistDuration(mediaKey),

View File

@@ -41,7 +41,7 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
calls.push(`broadcast:${channel}:${String(payload)}`), calls.push(`broadcast:${channel}:${String(payload)}`),
onSubtitleChange: (text) => calls.push(`subtitle-change:${text}`), onSubtitleChange: (text) => calls.push(`subtitle-change:${text}`),
updateCurrentMediaPath: (path) => calls.push(`path:${path}`), updateCurrentMediaPath: (path) => calls.push(`path:${path}`),
restoreMpvSubVisibilityForInvisibleOverlay: () => calls.push('restore-mpv-sub'), restoreMpvSubVisibility: () => calls.push('restore-mpv-sub'),
getCurrentAnilistMediaKey: () => 'media-key', getCurrentAnilistMediaKey: () => 'media-key',
resetAnilistMediaTracking: (mediaKey) => calls.push(`reset:${mediaKey}`), resetAnilistMediaTracking: (mediaKey) => calls.push(`reset:${mediaKey}`),
maybeProbeAnilistDuration: (mediaKey) => calls.push(`probe:${mediaKey}`), maybeProbeAnilistDuration: (mediaKey) => calls.push(`probe:${mediaKey}`),
@@ -75,7 +75,7 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
deps.broadcastSubtitleAss('ass'); deps.broadcastSubtitleAss('ass');
deps.broadcastSecondarySubtitle('sec'); deps.broadcastSecondarySubtitle('sec');
deps.updateCurrentMediaPath('/tmp/video'); deps.updateCurrentMediaPath('/tmp/video');
deps.restoreMpvSubVisibilityForInvisibleOverlay(); deps.restoreMpvSubVisibility();
assert.equal(deps.getCurrentAnilistMediaKey(), 'media-key'); assert.equal(deps.getCurrentAnilistMediaKey(), 'media-key');
deps.resetAnilistMediaTracking('media-key'); deps.resetAnilistMediaTracking('media-key');
deps.maybeProbeAnilistDuration('media-key'); deps.maybeProbeAnilistDuration('media-key');

View File

@@ -27,7 +27,7 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
broadcastToOverlayWindows: (channel: string, payload: unknown) => void; broadcastToOverlayWindows: (channel: string, payload: unknown) => void;
onSubtitleChange: (text: string) => void; onSubtitleChange: (text: string) => void;
updateCurrentMediaPath: (path: string) => void; updateCurrentMediaPath: (path: string) => void;
restoreMpvSubVisibilityForInvisibleOverlay: () => void; restoreMpvSubVisibility: () => void;
getCurrentAnilistMediaKey: () => string | null; getCurrentAnilistMediaKey: () => string | null;
resetAnilistMediaTracking: (mediaKey: string | null) => void; resetAnilistMediaTracking: (mediaKey: string | null) => void;
maybeProbeAnilistDuration: (mediaKey: string) => void; maybeProbeAnilistDuration: (mediaKey: string) => void;
@@ -71,8 +71,8 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
broadcastSecondarySubtitle: (text: string) => broadcastSecondarySubtitle: (text: string) =>
deps.broadcastToOverlayWindows('secondary-subtitle:set', text), deps.broadcastToOverlayWindows('secondary-subtitle:set', text),
updateCurrentMediaPath: (path: string) => deps.updateCurrentMediaPath(path), updateCurrentMediaPath: (path: string) => deps.updateCurrentMediaPath(path),
restoreMpvSubVisibilityForInvisibleOverlay: () => restoreMpvSubVisibility: () =>
deps.restoreMpvSubVisibilityForInvisibleOverlay(), deps.restoreMpvSubVisibility(),
getCurrentAnilistMediaKey: () => deps.getCurrentAnilistMediaKey(), getCurrentAnilistMediaKey: () => deps.getCurrentAnilistMediaKey(),
resetAnilistMediaTracking: (mediaKey: string | null) => resetAnilistMediaTracking: (mediaKey: string | null) =>
deps.resetAnilistMediaTracking(mediaKey), deps.resetAnilistMediaTracking(mediaKey),

View File

@@ -8,14 +8,12 @@ import {
type VisibilityState = { type VisibilityState = {
savedSubVisibility: boolean | null; savedSubVisibility: boolean | null;
savedSecondarySubVisibility: boolean | null;
revision: number; revision: number;
}; };
test('ensure overlay mpv subtitle suppression captures previous visibility then hides subtitles', async () => { test('ensure overlay mpv subtitle suppression captures previous visibility then hides subtitles', async () => {
const state: VisibilityState = { const state: VisibilityState = {
savedSubVisibility: null, savedSubVisibility: null,
savedSecondarySubVisibility: null,
revision: 0, revision: 0,
}; };
const calls: boolean[] = []; const calls: boolean[] = [];
@@ -29,10 +27,6 @@ test('ensure overlay mpv subtitle suppression captures previous visibility then
setSavedSubVisibility: (visible) => { setSavedSubVisibility: (visible) => {
state.savedSubVisibility = visible; state.savedSubVisibility = visible;
}, },
getSavedSecondarySubVisibility: () => state.savedSecondarySubVisibility,
setSavedSecondarySubVisibility: (visible) => {
state.savedSecondarySubVisibility = visible;
},
getRevision: () => state.revision, getRevision: () => state.revision,
setRevision: (revision) => { setRevision: (revision) => {
state.revision = revision; state.revision = revision;
@@ -40,24 +34,19 @@ test('ensure overlay mpv subtitle suppression captures previous visibility then
setMpvSubVisibility: (visible) => { setMpvSubVisibility: (visible) => {
calls.push(visible); calls.push(visible);
}, },
setMpvSecondarySubVisibility: (visible) => {
calls.push(visible);
},
logWarn: () => {}, logWarn: () => {},
}); });
await ensureHidden(); await ensureHidden();
assert.equal(state.savedSubVisibility, false); assert.equal(state.savedSubVisibility, false);
assert.equal(state.savedSecondarySubVisibility, false);
assert.equal(state.revision, 1); assert.equal(state.revision, 1);
assert.deepEqual(calls, [false, false]); assert.deepEqual(calls, [false]);
}); });
test('restore overlay mpv subtitle suppression restores saved visibility', () => { test('restore overlay mpv subtitle suppression restores saved visibility', () => {
const state: VisibilityState = { const state: VisibilityState = {
savedSubVisibility: false, savedSubVisibility: false,
savedSecondarySubVisibility: true,
revision: 4, revision: 4,
}; };
const calls: boolean[] = []; const calls: boolean[] = [];
@@ -67,10 +56,6 @@ test('restore overlay mpv subtitle suppression restores saved visibility', () =>
setSavedSubVisibility: (visible) => { setSavedSubVisibility: (visible) => {
state.savedSubVisibility = visible; state.savedSubVisibility = visible;
}, },
getSavedSecondarySubVisibility: () => state.savedSecondarySubVisibility,
setSavedSecondarySubVisibility: (visible) => {
state.savedSecondarySubVisibility = visible;
},
getRevision: () => state.revision, getRevision: () => state.revision,
setRevision: (revision) => { setRevision: (revision) => {
state.revision = revision; state.revision = revision;
@@ -80,23 +65,18 @@ test('restore overlay mpv subtitle suppression restores saved visibility', () =>
setMpvSubVisibility: (visible) => { setMpvSubVisibility: (visible) => {
calls.push(visible); calls.push(visible);
}, },
setMpvSecondarySubVisibility: (visible) => {
calls.push(visible);
},
}); });
restore(); restore();
assert.equal(state.savedSubVisibility, null); assert.equal(state.savedSubVisibility, null);
assert.equal(state.savedSecondarySubVisibility, null);
assert.equal(state.revision, 5); assert.equal(state.revision, 5);
assert.deepEqual(calls, [false, true]); assert.deepEqual(calls, [false]);
}); });
test('restore keeps mpv subtitles hidden when visible-overlay binding still requires suppression', () => { test('restore keeps mpv subtitles hidden when visible-overlay binding still requires suppression', () => {
const state: VisibilityState = { const state: VisibilityState = {
savedSubVisibility: true, savedSubVisibility: true,
savedSecondarySubVisibility: true,
revision: 9, revision: 9,
}; };
const calls: boolean[] = []; const calls: boolean[] = [];
@@ -106,10 +86,6 @@ test('restore keeps mpv subtitles hidden when visible-overlay binding still requ
setSavedSubVisibility: (visible) => { setSavedSubVisibility: (visible) => {
state.savedSubVisibility = visible; state.savedSubVisibility = visible;
}, },
getSavedSecondarySubVisibility: () => state.savedSecondarySubVisibility,
setSavedSecondarySubVisibility: (visible) => {
state.savedSecondarySubVisibility = visible;
},
getRevision: () => state.revision, getRevision: () => state.revision,
setRevision: (revision) => { setRevision: (revision) => {
state.revision = revision; state.revision = revision;
@@ -119,23 +95,18 @@ test('restore keeps mpv subtitles hidden when visible-overlay binding still requ
setMpvSubVisibility: (visible) => { setMpvSubVisibility: (visible) => {
calls.push(visible); calls.push(visible);
}, },
setMpvSecondarySubVisibility: (visible) => {
calls.push(visible);
},
}); });
restore(); restore();
assert.equal(state.savedSubVisibility, true); assert.equal(state.savedSubVisibility, true);
assert.equal(state.savedSecondarySubVisibility, true);
assert.equal(state.revision, 10); assert.equal(state.revision, 10);
assert.deepEqual(calls, [false, false]); assert.deepEqual(calls, [false]);
}); });
test('restore defers mpv subtitle restore while mpv is disconnected', () => { test('restore defers mpv subtitle restore while mpv is disconnected', () => {
const state: VisibilityState = { const state: VisibilityState = {
savedSubVisibility: true, savedSubVisibility: true,
savedSecondarySubVisibility: false,
revision: 2, revision: 2,
}; };
const calls: boolean[] = []; const calls: boolean[] = [];
@@ -145,10 +116,6 @@ test('restore defers mpv subtitle restore while mpv is disconnected', () => {
setSavedSubVisibility: (visible) => { setSavedSubVisibility: (visible) => {
state.savedSubVisibility = visible; state.savedSubVisibility = visible;
}, },
getSavedSecondarySubVisibility: () => state.savedSecondarySubVisibility,
setSavedSecondarySubVisibility: (visible) => {
state.savedSecondarySubVisibility = visible;
},
getRevision: () => state.revision, getRevision: () => state.revision,
setRevision: (revision) => { setRevision: (revision) => {
state.revision = revision; state.revision = revision;
@@ -158,9 +125,6 @@ test('restore defers mpv subtitle restore while mpv is disconnected', () => {
setMpvSubVisibility: (visible) => { setMpvSubVisibility: (visible) => {
calls.push(visible); calls.push(visible);
}, },
setMpvSecondarySubVisibility: (visible) => {
calls.push(visible);
},
}); });
restore(); restore();

View File

@@ -3,10 +3,6 @@ type MpvVisibilityClient = {
requestProperty: (name: string) => Promise<unknown>; requestProperty: (name: string) => Promise<unknown>;
}; };
type RestoreOptions = {
respectVisibleOverlayBinding?: boolean;
};
function parseSubVisibility(value: unknown): boolean { function parseSubVisibility(value: unknown): boolean {
if (typeof value === 'string') { if (typeof value === 'string') {
const normalized = value.trim().toLowerCase(); const normalized = value.trim().toLowerCase();
@@ -33,12 +29,9 @@ export function createEnsureOverlayMpvSubtitlesHiddenHandler(deps: {
getMpvClient: () => MpvVisibilityClient | null; getMpvClient: () => MpvVisibilityClient | null;
getSavedSubVisibility: () => boolean | null; getSavedSubVisibility: () => boolean | null;
setSavedSubVisibility: (visible: boolean | null) => void; setSavedSubVisibility: (visible: boolean | null) => void;
getSavedSecondarySubVisibility: () => boolean | null;
setSavedSecondarySubVisibility: (visible: boolean | null) => void;
getRevision: () => number; getRevision: () => number;
setRevision: (revision: number) => void; setRevision: (revision: number) => void;
setMpvSubVisibility: (visible: boolean) => void; setMpvSubVisibility: (visible: boolean) => void;
setMpvSecondarySubVisibility: (visible: boolean) => void;
logWarn: (message: string, error: unknown) => void; logWarn: (message: string, error: unknown) => void;
}) { }) {
return async (): Promise<void> => { return async (): Promise<void> => {
@@ -50,9 +43,17 @@ export function createEnsureOverlayMpvSubtitlesHiddenHandler(deps: {
return; return;
} }
if (deps.getSavedSubVisibility() === null) { const shouldCaptureSavedVisibility = deps.getSavedSubVisibility() === null;
const savedVisibilityPromise = shouldCaptureSavedVisibility
? mpvClient.requestProperty('sub-visibility')
: null;
// Hide immediately on overlay toggle; capture/restore logic is handled separately.
deps.setMpvSubVisibility(false);
if (shouldCaptureSavedVisibility && savedVisibilityPromise) {
try { try {
const currentSubVisibility = await mpvClient.requestProperty('sub-visibility'); const currentSubVisibility = await savedVisibilityPromise;
if (revision !== deps.getRevision()) { if (revision !== deps.getRevision()) {
return; return;
} }
@@ -68,64 +69,28 @@ export function createEnsureOverlayMpvSubtitlesHiddenHandler(deps: {
deps.setSavedSubVisibility(true); deps.setSavedSubVisibility(true);
} }
} }
if (deps.getSavedSecondarySubVisibility() === null) {
try {
const currentSecondarySubVisibility = await mpvClient.requestProperty('secondary-sub-visibility');
if (revision !== deps.getRevision()) {
return;
}
deps.setSavedSecondarySubVisibility(parseSubVisibility(currentSecondarySubVisibility));
} catch (error) {
if (revision !== deps.getRevision()) {
return;
}
deps.logWarn(
'[overlay] Failed to capture secondary mpv sub-visibility; falling back to visible restore',
error,
);
deps.setSavedSecondarySubVisibility(true);
}
}
if (revision !== deps.getRevision()) {
return;
}
deps.setMpvSubVisibility(false);
deps.setMpvSecondarySubVisibility(false);
}; };
} }
export function createRestoreOverlayMpvSubtitlesHandler(deps: { export function createRestoreOverlayMpvSubtitlesHandler(deps: {
getSavedSubVisibility: () => boolean | null; getSavedSubVisibility: () => boolean | null;
setSavedSubVisibility: (visible: boolean | null) => void; setSavedSubVisibility: (visible: boolean | null) => void;
getSavedSecondarySubVisibility: () => boolean | null;
setSavedSecondarySubVisibility: (visible: boolean | null) => void;
getRevision: () => number; getRevision: () => number;
setRevision: (revision: number) => void; setRevision: (revision: number) => void;
isMpvConnected: () => boolean; isMpvConnected: () => boolean;
shouldKeepSuppressedFromVisibleOverlayBinding: () => boolean; shouldKeepSuppressedFromVisibleOverlayBinding: () => boolean;
setMpvSubVisibility: (visible: boolean) => void; setMpvSubVisibility: (visible: boolean) => void;
setMpvSecondarySubVisibility: (visible: boolean) => void;
}) { }) {
return (options?: RestoreOptions): void => { return (): void => {
deps.setRevision(deps.getRevision() + 1); deps.setRevision(deps.getRevision() + 1);
const savedVisibility = deps.getSavedSubVisibility(); const savedVisibility = deps.getSavedSubVisibility();
const respectVisibleOverlayBinding = options?.respectVisibleOverlayBinding ?? true; if (deps.shouldKeepSuppressedFromVisibleOverlayBinding()) {
if (
respectVisibleOverlayBinding &&
deps.shouldKeepSuppressedFromVisibleOverlayBinding()
) {
deps.setMpvSubVisibility(false); deps.setMpvSubVisibility(false);
deps.setMpvSecondarySubVisibility(false);
return; return;
} }
const hasSecondarySavedVisibility = deps.getSavedSecondarySubVisibility() !== null; if (savedVisibility === null) {
if (savedVisibility === null && !hasSecondarySavedVisibility) {
return; return;
} }
@@ -136,12 +101,7 @@ export function createRestoreOverlayMpvSubtitlesHandler(deps: {
if (savedVisibility !== null) { if (savedVisibility !== null) {
deps.setMpvSubVisibility(savedVisibility); deps.setMpvSubVisibility(savedVisibility);
} }
const savedSecondaryVisibility = deps.getSavedSecondarySubVisibility();
if (savedSecondaryVisibility !== null) {
deps.setMpvSecondarySubVisibility(savedSecondaryVisibility);
}
deps.setSavedSubVisibility(null); deps.setSavedSubVisibility(null);
deps.setSavedSecondarySubVisibility(null);
}; };
} }

View File

@@ -172,7 +172,6 @@ export interface AppState {
lastSecondarySubToggleAtMs: number; lastSecondarySubToggleAtMs: number;
previousSecondarySubVisibility: boolean | null; previousSecondarySubVisibility: boolean | null;
overlaySavedMpvSubVisibility: boolean | null; overlaySavedMpvSubVisibility: boolean | null;
overlaySavedSecondaryMpvSubVisibility: boolean | null;
overlayMpvSubVisibilityRevision: number; overlayMpvSubVisibilityRevision: number;
mpvSubtitleRenderMetrics: MpvSubtitleRenderMetrics; mpvSubtitleRenderMetrics: MpvSubtitleRenderMetrics;
shortcutsRegistered: boolean; shortcutsRegistered: boolean;
@@ -247,7 +246,6 @@ export function createAppState(values: AppStateInitialValues): AppState {
lastSecondarySubToggleAtMs: 0, lastSecondarySubToggleAtMs: 0,
previousSecondarySubVisibility: null, previousSecondarySubVisibility: null,
overlaySavedMpvSubVisibility: null, overlaySavedMpvSubVisibility: null,
overlaySavedSecondaryMpvSubVisibility: null,
overlayMpvSubVisibilityRevision: 0, overlayMpvSubVisibilityRevision: 0,
mpvSubtitleRenderMetrics: { mpvSubtitleRenderMetrics: {
...DEFAULT_MPV_SUBTITLE_RENDER_METRICS, ...DEFAULT_MPV_SUBTITLE_RENDER_METRICS,

View File

@@ -154,7 +154,7 @@ const electronAPI: ElectronAPI = {
}, },
getOverlayVisibility: (): Promise<boolean> => getOverlayVisibility: (): Promise<boolean> =>
ipcRenderer.invoke(IPC_CHANNELS.request.getOverlayVisibility), ipcRenderer.invoke(IPC_CHANNELS.request.getVisibleOverlayVisibility),
getCurrentSubtitle: (): Promise<SubtitleData> => getCurrentSubtitle: (): Promise<SubtitleData> =>
ipcRenderer.invoke(IPC_CHANNELS.request.getCurrentSubtitle), ipcRenderer.invoke(IPC_CHANNELS.request.getCurrentSubtitle),
getCurrentSubtitleRaw: (): Promise<string> => getCurrentSubtitleRaw: (): Promise<string> =>

View File

@@ -7,7 +7,7 @@ import type { MergedToken } from '../types';
import { PartOfSpeech } from '../types.js'; import { PartOfSpeech } from '../types.js';
import { import {
alignTokensToSourceText, alignTokensToSourceText,
buildInvisibleTokenHoverRanges, buildSubtitleTokenHoverRanges,
computeWordClass, computeWordClass,
normalizeSubtitle, normalizeSubtitle,
sanitizeSubtitleHoverTokenColor, sanitizeSubtitleHoverTokenColor,
@@ -266,26 +266,26 @@ test('alignTokensToSourceText avoids duplicate tail when later token surface doe
); );
}); });
test('buildInvisibleTokenHoverRanges tracks token offsets across text separators', () => { test('buildSubtitleTokenHoverRanges tracks token offsets across text separators', () => {
const tokens = [ const tokens = [
createToken({ surface: 'キリキリと' }), createToken({ surface: 'キリキリと' }),
createToken({ surface: 'かかってこい' }), createToken({ surface: 'かかってこい' }),
]; ];
const ranges = buildInvisibleTokenHoverRanges(tokens, 'キリキリと\nかかってこい'); const ranges = buildSubtitleTokenHoverRanges(tokens, 'キリキリと\nかかってこい');
assert.deepEqual(ranges, [ assert.deepEqual(ranges, [
{ start: 0, end: 5, tokenIndex: 0 }, { start: 0, end: 5, tokenIndex: 0 },
{ start: 6, end: 12, tokenIndex: 1 }, { start: 6, end: 12, tokenIndex: 1 },
]); ]);
}); });
test('buildInvisibleTokenHoverRanges ignores unmatched token surfaces', () => { test('buildSubtitleTokenHoverRanges ignores unmatched token surfaces', () => {
const tokens = [ const tokens = [
createToken({ surface: '君たちが潰した拠点に' }), createToken({ surface: '君たちが潰した拠点に' }),
createToken({ surface: '教団の主力は1人もいない' }), createToken({ surface: '教団の主力は1人もいない' }),
]; ];
const ranges = buildInvisibleTokenHoverRanges(tokens, '君たちが潰した拠点に\n教団の主力は人もいない'); const ranges = buildSubtitleTokenHoverRanges(tokens, '君たちが潰した拠点に\n教団の主力は人もいない');
assert.deepEqual(ranges, [{ start: 0, end: 10, tokenIndex: 0 }]); assert.deepEqual(ranges, [{ start: 0, end: 10, tokenIndex: 0 }]);
}); });

View File

@@ -9,7 +9,7 @@ type FrequencyRenderSettings = {
bandedColors: [string, string, string, string, string]; bandedColors: [string, string, string, string, string];
}; };
export type InvisibleTokenHoverRange = { export type SubtitleTokenHoverRange = {
start: number; start: number;
end: number; end: number;
tokenIndex: number; tokenIndex: number;
@@ -37,6 +37,8 @@ export function normalizeSubtitle(text: string, trim = true, collapseLineBreaks
} }
const HEX_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/; const HEX_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
const SAFE_CSS_COLOR_PATTERN =
/^(?:#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|(?:rgba?|hsla?)\([^)]*\)|var\([^)]*\)|[a-zA-Z]+)$/;
function sanitizeHexColor(value: unknown, fallback: string): string { function sanitizeHexColor(value: unknown, fallback: string): string {
return typeof value === 'string' && HEX_COLOR_PATTERN.test(value.trim()) return typeof value === 'string' && HEX_COLOR_PATTERN.test(value.trim())
@@ -58,7 +60,9 @@ function sanitizeSubtitleHoverTokenBackgroundColor(value: unknown): string {
return 'rgba(54, 58, 79, 0.84)'; return 'rgba(54, 58, 79, 0.84)';
} }
const trimmed = value.trim(); const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : 'rgba(54, 58, 79, 0.84)'; return trimmed.length > 0 && SAFE_CSS_COLOR_PATTERN.test(trimmed)
? trimmed
: 'rgba(54, 58, 79, 0.84)';
} }
const DEFAULT_FREQUENCY_RENDER_SETTINGS: FrequencyRenderSettings = { const DEFAULT_FREQUENCY_RENDER_SETTINGS: FrequencyRenderSettings = {
@@ -293,16 +297,16 @@ export function alignTokensToSourceText(
return segments; return segments;
} }
export function buildInvisibleTokenHoverRanges( export function buildSubtitleTokenHoverRanges(
tokens: MergedToken[], tokens: MergedToken[],
sourceText: string, sourceText: string,
): InvisibleTokenHoverRange[] { ): SubtitleTokenHoverRange[] {
if (tokens.length === 0 || sourceText.length === 0) { if (tokens.length === 0 || sourceText.length === 0) {
return []; return [];
} }
const segments = alignTokensToSourceText(tokens, sourceText); const segments = alignTokensToSourceText(tokens, sourceText);
const ranges: InvisibleTokenHoverRange[] = []; const ranges: SubtitleTokenHoverRange[] = [];
let cursor = 0; let cursor = 0;
for (const segment of segments) { for (const segment of segments) {

View File

@@ -22,7 +22,6 @@ export const IPC_CHANNELS = {
overlayModalOpened: 'overlay:modal-opened', overlayModalOpened: 'overlay:modal-opened',
}, },
request: { request: {
getOverlayVisibility: 'get-overlay-visibility',
getVisibleOverlayVisibility: 'get-visible-overlay-visibility', getVisibleOverlayVisibility: 'get-visible-overlay-visibility',
getCurrentSubtitle: 'get-current-subtitle', getCurrentSubtitle: 'get-current-subtitle',
getCurrentSubtitleRaw: 'get-current-subtitle-raw', getCurrentSubtitleRaw: 'get-current-subtitle-raw',