refactor: remove invisible subtitle overlay code

This commit is contained in:
2026-02-26 16:40:46 -08:00
parent 643f8eb958
commit 74554a30f0
119 changed files with 691 additions and 2946 deletions

View File

@@ -10,14 +10,11 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
stop: false,
toggle: false,
toggleVisibleOverlay: false,
toggleInvisibleOverlay: false,
settings: false,
show: false,
hide: false,
showVisibleOverlay: false,
hideVisibleOverlay: false,
showInvisibleOverlay: false,
hideInvisibleOverlay: false,
copySubtitle: false,
copySubtitleMultiple: false,
mineSentence: false,
@@ -94,18 +91,12 @@ function createDeps(overrides: Partial<CliCommandServiceDeps> = {}) {
toggleVisibleOverlay: () => {
calls.push('toggleVisibleOverlay');
},
toggleInvisibleOverlay: () => {
calls.push('toggleInvisibleOverlay');
},
openYomitanSettingsDelayed: (delayMs) => {
calls.push(`openYomitanSettingsDelayed:${delayMs}`);
},
setVisibleOverlayVisible: (visible) => {
calls.push(`setVisibleOverlayVisible:${visible}`);
},
setInvisibleOverlayVisible: (visible) => {
calls.push(`setInvisibleOverlayVisible:${visible}`);
},
copyCurrentSubtitle: () => {
calls.push('copyCurrentSubtitle');
},
@@ -339,10 +330,6 @@ test('handleCliCommand handles visibility and utility command dispatches', () =>
args: Partial<CliArgs>;
expected: string;
}> = [
{
args: { toggleInvisibleOverlay: true },
expected: 'toggleInvisibleOverlay',
},
{ args: { settings: true }, expected: 'openYomitanSettingsDelayed:1000' },
{
args: { showVisibleOverlay: true },
@@ -352,14 +339,6 @@ test('handleCliCommand handles visibility and utility command dispatches', () =>
args: { hideVisibleOverlay: true },
expected: 'setVisibleOverlayVisible:false',
},
{
args: { showInvisibleOverlay: true },
expected: 'setInvisibleOverlayVisible:true',
},
{
args: { hideInvisibleOverlay: true },
expected: 'setInvisibleOverlayVisible:false',
},
{ args: { copySubtitle: true }, expected: 'copyCurrentSubtitle' },
{
args: { copySubtitleMultiple: true },

View File

@@ -16,10 +16,8 @@ export interface CliCommandServiceDeps {
isOverlayRuntimeInitialized: () => boolean;
initializeOverlayRuntime: () => void;
toggleVisibleOverlay: () => void;
toggleInvisibleOverlay: () => void;
openYomitanSettingsDelayed: (delayMs: number) => void;
setVisibleOverlayVisible: (visible: boolean) => void;
setInvisibleOverlayVisible: (visible: boolean) => void;
copyCurrentSubtitle: () => void;
startPendingMultiCopy: (timeoutMs: number) => void;
mineSentenceCard: () => Promise<void>;
@@ -93,9 +91,7 @@ interface OverlayCliRuntime {
isInitialized: () => boolean;
initialize: () => void;
toggleVisible: () => void;
toggleInvisible: () => void;
setVisible: (visible: boolean) => void;
setInvisible: (visible: boolean) => void;
}
interface MiningCliRuntime {
@@ -180,14 +176,12 @@ export function createCliCommandDepsRuntime(
isOverlayRuntimeInitialized: options.overlay.isInitialized,
initializeOverlayRuntime: options.overlay.initialize,
toggleVisibleOverlay: options.overlay.toggleVisible,
toggleInvisibleOverlay: options.overlay.toggleInvisible,
openYomitanSettingsDelayed: (delayMs) => {
options.schedule(() => {
options.ui.openYomitanSettings();
}, delayMs);
},
setVisibleOverlayVisible: options.overlay.setVisible,
setInvisibleOverlayVisible: options.overlay.setInvisible,
copyCurrentSubtitle: options.mining.copyCurrentSubtitle,
startPendingMultiCopy: options.mining.startPendingMultiCopy,
mineSentenceCard: options.mining.mineSentenceCard,
@@ -242,14 +236,11 @@ export function handleCliCommand(
args.stop ||
args.toggle ||
args.toggleVisibleOverlay ||
args.toggleInvisibleOverlay ||
args.settings ||
args.show ||
args.hide ||
args.showVisibleOverlay ||
args.hideVisibleOverlay ||
args.showInvisibleOverlay ||
args.hideInvisibleOverlay ||
args.copySubtitle ||
args.copySubtitleMultiple ||
args.mineSentence ||
@@ -286,10 +277,7 @@ export function handleCliCommand(
}
const shouldStart =
args.start ||
args.toggle ||
args.toggleVisibleOverlay ||
args.toggleInvisibleOverlay;
args.start || args.toggle || args.toggleVisibleOverlay;
const needsOverlayRuntime = commandNeedsOverlayRuntime(args);
const shouldInitializeOverlayRuntime = needsOverlayRuntime || args.start;
@@ -325,18 +313,12 @@ export function handleCliCommand(
if (args.toggle || args.toggleVisibleOverlay) {
deps.toggleVisibleOverlay();
} else if (args.toggleInvisibleOverlay) {
deps.toggleInvisibleOverlay();
} else if (args.settings) {
deps.openYomitanSettingsDelayed(1000);
} else if (args.show || args.showVisibleOverlay) {
deps.setVisibleOverlayVisible(true);
} else if (args.hide || args.hideVisibleOverlay) {
deps.setVisibleOverlayVisible(false);
} else if (args.showInvisibleOverlay) {
deps.setInvisibleOverlayVisible(true);
} else if (args.hideInvisibleOverlay) {
deps.setInvisibleOverlayVisible(false);
} else if (args.copySubtitle) {
deps.copyCurrentSubtitle();
} else if (args.copySubtitleMultiple) {

View File

@@ -19,11 +19,9 @@ test('createFieldGroupingOverlayRuntime sends overlay messages and sets restore
},
}),
getVisibleOverlayVisible: () => visible,
getInvisibleOverlayVisible: () => false,
setVisibleOverlayVisible: (next) => {
visible = next;
},
setInvisibleOverlayVisible: () => {},
getResolver: () => null,
setResolver: () => {},
getRestoreVisibleOverlayOnModalClose: () => restore,
@@ -44,9 +42,7 @@ test('createFieldGroupingOverlayRuntime callback cancels when send fails', async
const runtime = createFieldGroupingOverlayRuntime<'runtime-options' | 'subsync'>({
getMainWindow: () => null,
getVisibleOverlayVisible: () => false,
getInvisibleOverlayVisible: () => false,
setVisibleOverlayVisible: () => {},
setInvisibleOverlayVisible: () => {},
getResolver: () => resolver,
setResolver: (next: ((choice: KikuFieldGroupingChoice) => void) | null) => {
resolver = next;
@@ -87,12 +83,10 @@ test('createFieldGroupingOverlayRuntime callback restores hidden visible overlay
const runtime = createFieldGroupingOverlayRuntime<'runtime-options' | 'subsync'>({
getMainWindow: () => null,
getVisibleOverlayVisible: () => visible,
getInvisibleOverlayVisible: () => false,
setVisibleOverlayVisible: (nextVisible) => {
visible = nextVisible;
visibilityTransitions.push(nextVisible);
},
setInvisibleOverlayVisible: () => {},
getResolver: () => resolver as ((choice: KikuFieldGroupingChoice) => void) | null,
setResolver: (nextResolver: ((choice: KikuFieldGroupingChoice) => void) | null) => {
resolver = nextResolver;

View File

@@ -11,9 +11,7 @@ interface WindowLike {
export interface FieldGroupingOverlayRuntimeOptions<T extends string> {
getMainWindow: () => WindowLike | null;
getVisibleOverlayVisible: () => boolean;
getInvisibleOverlayVisible: () => boolean;
setVisibleOverlayVisible: (visible: boolean) => void;
setInvisibleOverlayVisible: (visible: boolean) => void;
getResolver: () => ((choice: KikuFieldGroupingChoice) => void) | null;
setResolver: (resolver: ((choice: KikuFieldGroupingChoice) => void) | null) => void;
getRestoreVisibleOverlayOnModalClose: () => Set<T>;
@@ -65,9 +63,7 @@ export function createFieldGroupingOverlayRuntime<T extends string>(
) => Promise<KikuFieldGroupingChoice>) => {
return createFieldGroupingCallbackRuntime({
getVisibleOverlayVisible: options.getVisibleOverlayVisible,
getInvisibleOverlayVisible: options.getInvisibleOverlayVisible,
setVisibleOverlayVisible: options.setVisibleOverlayVisible,
setInvisibleOverlayVisible: options.setInvisibleOverlayVisible,
getResolver: options.getResolver,
setResolver: options.setResolver,
sendToVisibleOverlay,

View File

@@ -2,9 +2,7 @@ import { KikuFieldGroupingChoice, KikuFieldGroupingRequestData } from '../../typ
export function createFieldGroupingCallback(options: {
getVisibleOverlayVisible: () => boolean;
getInvisibleOverlayVisible: () => boolean;
setVisibleOverlayVisible: (visible: boolean) => void;
setInvisibleOverlayVisible: (visible: boolean) => void;
getResolver: () => ((choice: KikuFieldGroupingChoice) => void) | null;
setResolver: (resolver: ((choice: KikuFieldGroupingChoice) => void) | null) => void;
sendRequestToVisibleOverlay: (data: KikuFieldGroupingRequestData) => boolean;
@@ -22,7 +20,6 @@ export function createFieldGroupingCallback(options: {
}
const previousVisibleOverlay = options.getVisibleOverlayVisible();
const previousInvisibleOverlay = options.getInvisibleOverlayVisible();
let settled = false;
const finish = (choice: KikuFieldGroupingChoice): void => {
@@ -36,9 +33,6 @@ export function createFieldGroupingCallback(options: {
if (!previousVisibleOverlay && options.getVisibleOverlayVisible()) {
options.setVisibleOverlayVisible(false);
}
if (options.getInvisibleOverlayVisible() !== previousInvisibleOverlay) {
options.setInvisibleOverlayVisible(previousInvisibleOverlay);
}
};
options.setResolver(finish);

View File

@@ -36,10 +36,8 @@ function createFakeIpcRegistrar(): {
test('createIpcDepsRuntime wires AniList handlers', async () => {
const calls: string[] = [];
const deps = createIpcDepsRuntime({
getInvisibleWindow: () => null,
getMainWindow: () => null,
getVisibleOverlayVisibility: () => false,
getInvisibleOverlayVisibility: () => false,
onOverlayModalClosed: () => {},
openYomitanSettings: () => {},
quitApp: () => {},
@@ -47,7 +45,6 @@ test('createIpcDepsRuntime wires AniList handlers', async () => {
tokenizeCurrentSubtitle: async () => null,
getCurrentSubtitleRaw: () => '',
getCurrentSubtitleAss: () => '',
getMpvSubtitleRenderMetrics: () => null,
getSubtitlePosition: () => null,
getSubtitleStyle: () => null,
saveSubtitlePosition: () => {},
@@ -64,7 +61,6 @@ test('createIpcDepsRuntime wires AniList handlers', async () => {
setRuntimeOption: () => ({ ok: true }),
cycleRuntimeOption: () => ({ ok: true }),
reportOverlayContentBounds: () => {},
reportHoveredSubtitleToken: () => {},
getAnilistStatus: () => ({ tokenStatus: 'resolved' }),
clearAnilistToken: () => {
calls.push('clearAnilistToken');
@@ -101,20 +97,15 @@ test('registerIpcHandlers rejects malformed runtime-option payloads', async () =
const cycles: Array<{ id: string; direction: 1 | -1 }> = [];
registerIpcHandlers(
{
getInvisibleWindow: () => null,
isVisibleOverlayVisible: () => false,
setInvisibleIgnoreMouseEvents: () => {},
onOverlayModalClosed: () => {},
openYomitanSettings: () => {},
quitApp: () => {},
toggleDevTools: () => {},
getVisibleOverlayVisibility: () => false,
toggleVisibleOverlay: () => {},
getInvisibleOverlayVisibility: () => false,
tokenizeCurrentSubtitle: async () => null,
getCurrentSubtitleRaw: () => '',
getCurrentSubtitleAss: () => '',
getMpvSubtitleRenderMetrics: () => null,
getSubtitlePosition: () => null,
getSubtitleStyle: () => null,
saveSubtitlePosition: () => {},
@@ -138,7 +129,6 @@ test('registerIpcHandlers rejects malformed runtime-option payloads', async () =
return { ok: true };
},
reportOverlayContentBounds: () => {},
reportHoveredSubtitleToken: () => {},
getAnilistStatus: () => ({}),
clearAnilistToken: () => {},
openAnilistSetup: () => {},
@@ -176,25 +166,24 @@ test('registerIpcHandlers rejects malformed runtime-option payloads', async () =
test('registerIpcHandlers ignores malformed fire-and-forget payloads', () => {
const { registrar, handlers } = createFakeIpcRegistrar();
const saves: unknown[] = [];
const modals: unknown[] = [];
const closedModals: unknown[] = [];
const openedModals: unknown[] = [];
registerIpcHandlers(
{
getInvisibleWindow: () => null,
isVisibleOverlayVisible: () => false,
setInvisibleIgnoreMouseEvents: () => {},
onOverlayModalClosed: (modal) => {
modals.push(modal);
closedModals.push(modal);
},
onOverlayModalOpened: (modal) => {
openedModals.push(modal);
},
openYomitanSettings: () => {},
quitApp: () => {},
toggleDevTools: () => {},
getVisibleOverlayVisibility: () => false,
toggleVisibleOverlay: () => {},
getInvisibleOverlayVisibility: () => false,
tokenizeCurrentSubtitle: async () => null,
getCurrentSubtitleRaw: () => '',
getCurrentSubtitleAss: () => '',
getMpvSubtitleRenderMetrics: () => null,
getSubtitlePosition: () => null,
getSubtitleStyle: () => null,
saveSubtitlePosition: (position) => {
@@ -214,7 +203,6 @@ test('registerIpcHandlers ignores malformed fire-and-forget payloads', () => {
setRuntimeOption: () => ({ ok: true }),
cycleRuntimeOption: () => ({ ok: true }),
reportOverlayContentBounds: () => {},
reportHoveredSubtitleToken: () => {},
getAnilistStatus: () => ({}),
clearAnilistToken: () => {},
openAnilistSetup: () => {},
@@ -228,11 +216,16 @@ test('registerIpcHandlers ignores malformed fire-and-forget payloads', () => {
handlers.on.get(IPC_CHANNELS.command.saveSubtitlePosition)!({}, { yPercent: 'bad' });
handlers.on.get(IPC_CHANNELS.command.saveSubtitlePosition)!({}, { yPercent: 42 });
assert.deepEqual(saves, [
{ yPercent: 42, invisibleOffsetXPx: undefined, invisibleOffsetYPx: undefined },
{ yPercent: 42 },
]);
handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'not-a-modal');
handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'subsync');
handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'kiku');
assert.deepEqual(modals, ['subsync', 'kiku']);
assert.deepEqual(closedModals, ['subsync', 'kiku']);
handlers.on.get(IPC_CHANNELS.command.overlayModalOpened)!({}, 'bad');
handlers.on.get(IPC_CHANNELS.command.overlayModalOpened)!({}, 'subsync');
handlers.on.get(IPC_CHANNELS.command.overlayModalOpened)!({}, 'runtime-options');
assert.deepEqual(openedModals, ['subsync', 'runtime-options']);
});

View File

@@ -19,20 +19,16 @@ import {
} from '../../shared/ipc/validators';
export interface IpcServiceDeps {
getInvisibleWindow: () => WindowLike | null;
isVisibleOverlayVisible: () => boolean;
setInvisibleIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => void;
onOverlayModalClosed: (modal: OverlayHostedModal) => void;
onOverlayModalOpened?: (modal: OverlayHostedModal) => void;
openYomitanSettings: () => void;
quitApp: () => void;
toggleDevTools: () => void;
getVisibleOverlayVisibility: () => boolean;
toggleVisibleOverlay: () => void;
getInvisibleOverlayVisibility: () => boolean;
tokenizeCurrentSubtitle: () => Promise<unknown>;
getCurrentSubtitleRaw: () => string;
getCurrentSubtitleAss: () => string;
getMpvSubtitleRenderMetrics: () => unknown;
getSubtitlePosition: () => unknown;
getSubtitleStyle: () => unknown;
saveSubtitlePosition: (position: SubtitlePosition) => void;
@@ -54,7 +50,6 @@ export interface IpcServiceDeps {
setRuntimeOption: (id: RuntimeOptionId, value: RuntimeOptionValue) => unknown;
cycleRuntimeOption: (id: RuntimeOptionId, direction: 1 | -1) => unknown;
reportOverlayContentBounds: (payload: unknown) => void;
reportHoveredSubtitleToken: (tokenIndex: number | null) => void;
getAnilistStatus: () => unknown;
clearAnilistToken: () => void;
openAnilistSetup: () => void;
@@ -91,18 +86,16 @@ interface IpcMainRegistrar {
}
export interface IpcDepsRuntimeOptions {
getInvisibleWindow: () => WindowLike | null;
getMainWindow: () => WindowLike | null;
getVisibleOverlayVisibility: () => boolean;
getInvisibleOverlayVisibility: () => boolean;
onOverlayModalClosed: (modal: OverlayHostedModal) => void;
onOverlayModalOpened?: (modal: OverlayHostedModal) => void;
openYomitanSettings: () => void;
quitApp: () => void;
toggleVisibleOverlay: () => void;
tokenizeCurrentSubtitle: () => Promise<unknown>;
getCurrentSubtitleRaw: () => string;
getCurrentSubtitleAss: () => string;
getMpvSubtitleRenderMetrics: () => unknown;
getSubtitlePosition: () => unknown;
getSubtitleStyle: () => unknown;
saveSubtitlePosition: (position: SubtitlePosition) => void;
@@ -119,7 +112,6 @@ export interface IpcDepsRuntimeOptions {
setRuntimeOption: (id: RuntimeOptionId, value: RuntimeOptionValue) => unknown;
cycleRuntimeOption: (id: RuntimeOptionId, direction: 1 | -1) => unknown;
reportOverlayContentBounds: (payload: unknown) => void;
reportHoveredSubtitleToken: (tokenIndex: number | null) => void;
getAnilistStatus: () => unknown;
clearAnilistToken: () => void;
openAnilistSetup: () => void;
@@ -130,14 +122,8 @@ export interface IpcDepsRuntimeOptions {
export function createIpcDepsRuntime(options: IpcDepsRuntimeOptions): IpcServiceDeps {
return {
getInvisibleWindow: () => options.getInvisibleWindow(),
isVisibleOverlayVisible: options.getVisibleOverlayVisibility,
setInvisibleIgnoreMouseEvents: (ignore, eventsOptions) => {
const invisibleWindow = options.getInvisibleWindow();
if (!invisibleWindow || invisibleWindow.isDestroyed()) return;
invisibleWindow.setIgnoreMouseEvents(ignore, eventsOptions);
},
onOverlayModalClosed: options.onOverlayModalClosed,
onOverlayModalOpened: options.onOverlayModalOpened,
openYomitanSettings: options.openYomitanSettings,
quitApp: options.quitApp,
toggleDevTools: () => {
@@ -147,11 +133,9 @@ export function createIpcDepsRuntime(options: IpcDepsRuntimeOptions): IpcService
},
getVisibleOverlayVisibility: options.getVisibleOverlayVisibility,
toggleVisibleOverlay: options.toggleVisibleOverlay,
getInvisibleOverlayVisibility: options.getInvisibleOverlayVisibility,
tokenizeCurrentSubtitle: options.tokenizeCurrentSubtitle,
getCurrentSubtitleRaw: options.getCurrentSubtitleRaw,
getCurrentSubtitleAss: options.getCurrentSubtitleAss,
getMpvSubtitleRenderMetrics: options.getMpvSubtitleRenderMetrics,
getSubtitlePosition: options.getSubtitlePosition,
getSubtitleStyle: options.getSubtitleStyle,
saveSubtitlePosition: options.saveSubtitlePosition,
@@ -182,7 +166,6 @@ export function createIpcDepsRuntime(options: IpcDepsRuntimeOptions): IpcService
setRuntimeOption: options.setRuntimeOption,
cycleRuntimeOption: options.cycleRuntimeOption,
reportOverlayContentBounds: options.reportOverlayContentBounds,
reportHoveredSubtitleToken: options.reportHoveredSubtitleToken,
getAnilistStatus: options.getAnilistStatus,
clearAnilistToken: options.clearAnilistToken,
openAnilistSetup: options.openAnilistSetup,
@@ -200,17 +183,7 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
const parsedOptions = parseOptionalForwardingOptions(options);
const senderWindow = BrowserWindow.fromWebContents((event as IpcMainEvent).sender);
if (senderWindow && !senderWindow.isDestroyed()) {
const invisibleWindow = deps.getInvisibleWindow();
if (
senderWindow === invisibleWindow &&
deps.isVisibleOverlayVisible() &&
invisibleWindow &&
!invisibleWindow.isDestroyed()
) {
deps.setInvisibleIgnoreMouseEvents(true, { forward: true });
} else {
senderWindow.setIgnoreMouseEvents(ignore, parsedOptions);
}
senderWindow.setIgnoreMouseEvents(ignore, parsedOptions);
}
},
);
@@ -220,6 +193,12 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
if (!parsedModal) return;
deps.onOverlayModalClosed(parsedModal);
});
ipc.on(IPC_CHANNELS.command.overlayModalOpened, (_event: unknown, modal: unknown) => {
const parsedModal = parseOverlayHostedModal(modal);
if (!parsedModal) return;
if (!deps.onOverlayModalOpened) return;
deps.onOverlayModalOpened(parsedModal);
});
ipc.on(IPC_CHANNELS.command.openYomitanSettings, () => {
deps.openYomitanSettings();
@@ -245,10 +224,6 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
return deps.getVisibleOverlayVisibility();
});
ipc.handle(IPC_CHANNELS.request.getInvisibleOverlayVisibility, () => {
return deps.getInvisibleOverlayVisibility();
});
ipc.handle(IPC_CHANNELS.request.getCurrentSubtitle, async () => {
return await deps.tokenizeCurrentSubtitle();
});
@@ -261,10 +236,6 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
return deps.getCurrentSubtitleAss();
});
ipc.handle(IPC_CHANNELS.request.getMpvSubtitleRenderMetrics, () => {
return deps.getMpvSubtitleRenderMetrics();
});
ipc.handle(IPC_CHANNELS.request.getSubtitlePosition, () => {
return deps.getSubtitlePosition();
});
@@ -358,17 +329,6 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
deps.reportOverlayContentBounds(payload);
});
ipc.on('subtitle-token-hover:set', (_event: unknown, tokenIndex: unknown) => {
if (tokenIndex === null) {
deps.reportHoveredSubtitleToken(null);
return;
}
if (!Number.isInteger(tokenIndex) || (tokenIndex as number) < 0) {
return;
}
deps.reportHoveredSubtitleToken(tokenIndex as number);
});
ipc.handle(IPC_CHANNELS.request.getAnilistStatus, () => {
return deps.getAnilistStatus();
});

View File

@@ -33,13 +33,51 @@ test('sendToVisibleOverlayRuntime restores visibility flag when opening hidden o
assert.deepEqual(sent, [['runtime-options:open']]);
});
test('sendToVisibleOverlayRuntime waits for overlay page before sending open command', () => {
const sent: unknown[][] = [];
const restoreSet = new Set<'runtime-options' | 'subsync'>();
let loading = true;
let currentURL = '';
const finishCallbacks: Array<() => void> = [];
const ok = sendToVisibleOverlayRuntime({
mainWindow: {
isDestroyed: () => false,
webContents: {
isLoading: () => loading,
getURL: () => currentURL,
send: (...args: unknown[]) => {
sent.push(args);
},
once: (_event: string, callback: () => void) => {
finishCallbacks.push(callback);
},
},
} as unknown as Electron.BrowserWindow,
visibleOverlayVisible: false,
setVisibleOverlayVisible: () => {},
channel: 'runtime-options:open',
restoreOnModalClose: 'runtime-options',
restoreVisibleOverlayOnModalClose: restoreSet,
});
assert.equal(ok, true);
assert.deepEqual(sent, []);
assert.equal(restoreSet.has('runtime-options'), true);
loading = false;
currentURL = 'file:///overlay/index.html?layer=visible';
assert.ok(finishCallbacks[0]);
finishCallbacks[0]!();
assert.deepEqual(sent, [['runtime-options:open']]);
});
test('createFieldGroupingCallbackRuntime cancels when overlay request cannot be sent', async () => {
let resolver: ((choice: KikuFieldGroupingChoice) => void) | null = null;
const callback = createFieldGroupingCallbackRuntime<'runtime-options' | 'subsync'>({
getVisibleOverlayVisible: () => false,
getInvisibleOverlayVisible: () => false,
setVisibleOverlayVisible: () => {},
setInvisibleOverlayVisible: () => {},
getResolver: () => resolver,
setResolver: (next) => {
resolver = next;

View File

@@ -26,27 +26,32 @@ export function sendToVisibleOverlayRuntime<T extends string>(options: {
options.mainWindow!.webContents.send(options.channel, options.payload);
}
};
if (options.mainWindow.webContents.isLoading()) {
const getURL = options.mainWindow.webContents.getURL;
const currentURL =
typeof getURL === 'function' ? getURL.call(options.mainWindow.webContents) : 'ready';
const isReady =
!options.mainWindow.webContents.isLoading() &&
currentURL !== '' &&
currentURL !== 'about:blank';
if (!isReady) {
options.mainWindow.webContents.once('did-finish-load', () => {
if (
options.mainWindow &&
!options.mainWindow.isDestroyed() &&
!options.mainWindow.webContents.isLoading()
) {
if (!options.mainWindow || options.mainWindow.isDestroyed()) return;
if (!options.mainWindow.webContents.isLoading()) {
sendNow();
}
});
return true;
}
sendNow();
return true;
}
export function createFieldGroupingCallbackRuntime<T extends string>(options: {
getVisibleOverlayVisible: () => boolean;
getInvisibleOverlayVisible: () => boolean;
setVisibleOverlayVisible: (visible: boolean) => void;
setInvisibleOverlayVisible: (visible: boolean) => void;
getResolver: () => ((choice: KikuFieldGroupingChoice) => void) | null;
setResolver: (resolver: ((choice: KikuFieldGroupingChoice) => void) | null) => void;
sendToVisibleOverlay: (
@@ -57,9 +62,7 @@ export function createFieldGroupingCallbackRuntime<T extends string>(options: {
}): (data: KikuFieldGroupingRequestData) => Promise<KikuFieldGroupingChoice> {
return createFieldGroupingCallback({
getVisibleOverlayVisible: options.getVisibleOverlayVisible,
getInvisibleOverlayVisible: options.getInvisibleOverlayVisible,
setVisibleOverlayVisible: options.setVisibleOverlayVisible,
setInvisibleOverlayVisible: options.setInvisibleOverlayVisible,
getResolver: options.getResolver,
setResolver: options.setResolver,
sendRequestToVisibleOverlay: (data) =>

View File

@@ -28,7 +28,7 @@ test('sanitizeOverlayContentMeasurement accepts valid payload with null rect', (
test('sanitizeOverlayContentMeasurement rejects invalid ranges', () => {
const measurement = sanitizeOverlayContentMeasurement(
{
layer: 'invisible',
layer: 'visible',
measuredAtMs: 100,
viewport: { width: 0, height: 1080 },
contentRect: { x: 0, y: 0, width: 100, height: 20 },
@@ -39,7 +39,7 @@ test('sanitizeOverlayContentMeasurement rejects invalid ranges', () => {
assert.equal(measurement, null);
});
test('overlay measurement store keeps latest payload per layer', () => {
test('overlay measurement store keeps latest payload for visible layer', () => {
const store = createOverlayContentMeasurementStore({
now: () => 1000,
warn: () => {
@@ -53,17 +53,9 @@ test('overlay measurement store keeps latest payload per layer', () => {
viewport: { width: 1280, height: 720 },
contentRect: { x: 50, y: 60, width: 400, height: 80 },
});
const invisible = store.report({
layer: 'invisible',
measuredAtMs: 910,
viewport: { width: 1280, height: 720 },
contentRect: { x: 20, y: 30, width: 300, height: 40 },
});
assert.equal(visible?.layer, 'visible');
assert.equal(invisible?.layer, 'invisible');
assert.equal(store.getLatestByLayer('visible')?.contentRect?.width, 400);
assert.equal(store.getLatestByLayer('invisible')?.contentRect?.height, 40);
});
test('overlay measurement store rate-limits invalid payload warnings', () => {

View File

@@ -28,7 +28,7 @@ export function sanitizeOverlayContentMeasurement(
} | null;
};
if (candidate.layer !== 'visible' && candidate.layer !== 'invisible') {
if (candidate.layer !== 'visible') {
return null;
}
@@ -112,7 +112,6 @@ export function createOverlayContentMeasurementStore(options?: {
const warn = options?.warn ?? ((message: string) => logger.warn(message));
const latestByLayer: OverlayMeasurementStore = {
visible: null,
invisible: null,
};
let droppedInvalid = 0;

View File

@@ -10,16 +10,11 @@ import {
export function initializeOverlayRuntime(options: {
backendOverride: string | null;
getInitialInvisibleOverlayVisibility: () => boolean;
createMainWindow: () => void;
createInvisibleWindow: () => void;
registerGlobalShortcuts: () => void;
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void;
isVisibleOverlayVisible: () => boolean;
isInvisibleOverlayVisible: () => boolean;
updateVisibleOverlayVisibility: () => void;
updateInvisibleOverlayVisibility: () => void;
getOverlayWindows: () => BrowserWindow[];
syncOverlayShortcuts: () => void;
setWindowTracker: (tracker: BaseWindowTracker | null) => void;
@@ -38,12 +33,8 @@ export function initializeOverlayRuntime(options: {
data: KikuFieldGroupingRequestData,
) => Promise<KikuFieldGroupingChoice>;
getKnownWordCacheStatePath: () => string;
}): {
invisibleOverlayVisible: boolean;
} {
}): void {
options.createMainWindow();
options.createInvisibleWindow();
const invisibleOverlayVisible = options.getInitialInvisibleOverlayVisibility();
options.registerGlobalShortcuts();
const windowTracker = createWindowTracker(options.backendOverride, options.getMpvSocketPath());
@@ -51,17 +42,12 @@ export function initializeOverlayRuntime(options: {
if (windowTracker) {
windowTracker.onGeometryChange = (geometry: WindowGeometry) => {
options.updateVisibleOverlayBounds(geometry);
options.updateInvisibleOverlayBounds(geometry);
};
windowTracker.onWindowFound = (geometry: WindowGeometry) => {
options.updateVisibleOverlayBounds(geometry);
options.updateInvisibleOverlayBounds(geometry);
if (options.isVisibleOverlayVisible()) {
options.updateVisibleOverlayVisibility();
}
if (options.isInvisibleOverlayVisible()) {
options.updateInvisibleOverlayVisibility();
}
};
windowTracker.onWindowLost = () => {
for (const window of options.getOverlayWindows()) {
@@ -101,7 +87,4 @@ export function initializeOverlayRuntime(options: {
}
options.updateVisibleOverlayVisibility();
options.updateInvisibleOverlayVisibility();
return { invisibleOverlayVisible };
}

View File

@@ -10,7 +10,6 @@ import {
function makeShortcuts(overrides: Partial<ConfiguredShortcuts> = {}): ConfiguredShortcuts {
return {
toggleVisibleOverlayGlobal: null,
toggleInvisibleOverlayGlobal: null,
copySubtitle: null,
copySubtitleMultiple: null,
updateLastCardFromClipboard: null,

View File

@@ -0,0 +1,265 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { setVisibleOverlayVisible, updateVisibleOverlayVisibility } from './overlay-visibility';
type WindowTrackerStub = {
isTracking: () => boolean;
getGeometry: () => { x: number; y: number; width: number; height: number } | null;
};
function createMainWindowRecorder() {
const calls: string[] = [];
const window = {
isDestroyed: () => false,
hide: () => {
calls.push('hide');
},
show: () => {
calls.push('show');
},
focus: () => {
calls.push('focus');
},
setIgnoreMouseEvents: () => {
calls.push('mouse-ignore');
},
};
return { window, calls };
}
test('macOS keeps visible overlay hidden while tracker is not ready and emits one loading OSD', () => {
const { window, calls } = createMainWindowRecorder();
let trackerWarning = false;
const osdMessages: string[] = [];
const tracker: WindowTrackerStub = {
isTracking: () => false,
getGeometry: () => null,
};
const run = () =>
updateVisibleOverlayVisibility({
visibleOverlayVisible: true,
mainWindow: window as never,
windowTracker: tracker as never,
trackerNotReadyWarningShown: trackerWarning,
setTrackerNotReadyWarningShown: (shown: boolean) => {
trackerWarning = shown;
},
updateVisibleOverlayBounds: () => {
calls.push('update-bounds');
},
ensureOverlayWindowLevel: () => {
calls.push('ensure-level');
},
syncPrimaryOverlayWindowLayer: () => {
calls.push('sync-layer');
},
enforceOverlayLayerOrder: () => {
calls.push('enforce-order');
},
syncOverlayShortcuts: () => {
calls.push('sync-shortcuts');
},
isMacOSPlatform: true,
showOverlayLoadingOsd: (message: string) => {
osdMessages.push(message);
},
} as never);
run();
run();
assert.equal(trackerWarning, true);
assert.deepEqual(osdMessages, ['Overlay loading...']);
assert.ok(calls.includes('hide'));
assert.ok(!calls.includes('show'));
});
test('non-macOS keeps fallback visible overlay behavior when tracker is not ready', () => {
const { window, calls } = createMainWindowRecorder();
let trackerWarning = false;
const tracker: WindowTrackerStub = {
isTracking: () => false,
getGeometry: () => null,
};
updateVisibleOverlayVisibility({
visibleOverlayVisible: true,
mainWindow: window as never,
windowTracker: tracker as never,
trackerNotReadyWarningShown: trackerWarning,
setTrackerNotReadyWarningShown: (shown: boolean) => {
trackerWarning = shown;
},
updateVisibleOverlayBounds: () => {
calls.push('update-bounds');
},
ensureOverlayWindowLevel: () => {
calls.push('ensure-level');
},
syncPrimaryOverlayWindowLayer: () => {
calls.push('sync-layer');
},
enforceOverlayLayerOrder: () => {
calls.push('enforce-order');
},
syncOverlayShortcuts: () => {
calls.push('sync-shortcuts');
},
isMacOSPlatform: false,
showOverlayLoadingOsd: () => {
calls.push('osd');
},
resolveFallbackBounds: () => ({ x: 12, y: 24, width: 640, height: 360 }),
} as never);
assert.equal(trackerWarning, true);
assert.ok(calls.includes('update-bounds'));
assert.ok(calls.includes('show'));
assert.ok(calls.includes('focus'));
assert.ok(!calls.includes('osd'));
});
test('macOS keeps visible overlay hidden while tracker is not initialized yet', () => {
const { window, calls } = createMainWindowRecorder();
let trackerWarning = false;
const osdMessages: string[] = [];
updateVisibleOverlayVisibility({
visibleOverlayVisible: true,
mainWindow: window as never,
windowTracker: null,
trackerNotReadyWarningShown: trackerWarning,
setTrackerNotReadyWarningShown: (shown: boolean) => {
trackerWarning = shown;
},
updateVisibleOverlayBounds: () => {
calls.push('update-bounds');
},
ensureOverlayWindowLevel: () => {
calls.push('ensure-level');
},
syncPrimaryOverlayWindowLayer: () => {
calls.push('sync-layer');
},
enforceOverlayLayerOrder: () => {
calls.push('enforce-order');
},
syncOverlayShortcuts: () => {
calls.push('sync-shortcuts');
},
isMacOSPlatform: true,
showOverlayLoadingOsd: (message: string) => {
osdMessages.push(message);
},
} as never);
assert.equal(trackerWarning, true);
assert.deepEqual(osdMessages, ['Overlay loading...']);
assert.ok(calls.includes('hide'));
assert.ok(!calls.includes('show'));
assert.ok(!calls.includes('update-bounds'));
});
test('setVisibleOverlayVisible does not mutate mpv subtitle visibility directly', () => {
const calls: string[] = [];
setVisibleOverlayVisible({
visible: true,
setVisibleOverlayVisibleState: (visible) => {
calls.push(`state:${visible}`);
},
updateVisibleOverlayVisibility: () => {
calls.push('update');
},
});
assert.deepEqual(calls, ['state:true', 'update']);
});
test('macOS loading OSD can show again after overlay is hidden and retried', () => {
const { window, calls } = createMainWindowRecorder();
const osdMessages: string[] = [];
let trackerWarning = false;
updateVisibleOverlayVisibility({
visibleOverlayVisible: true,
mainWindow: window as never,
windowTracker: null,
trackerNotReadyWarningShown: trackerWarning,
setTrackerNotReadyWarningShown: (shown: boolean) => {
trackerWarning = shown;
calls.push(`warn:${shown ? 'yes' : 'no'}`);
},
updateVisibleOverlayBounds: () => {
calls.push('update-bounds');
},
ensureOverlayWindowLevel: () => {
calls.push('ensure-level');
},
syncPrimaryOverlayWindowLayer: () => {
calls.push('sync-layer');
},
enforceOverlayLayerOrder: () => {
calls.push('enforce-order');
},
syncOverlayShortcuts: () => {
calls.push('sync-shortcuts');
},
isMacOSPlatform: true,
showOverlayLoadingOsd: (message: string) => {
osdMessages.push(message);
},
} as never);
updateVisibleOverlayVisibility({
visibleOverlayVisible: false,
mainWindow: window as never,
windowTracker: null,
trackerNotReadyWarningShown: trackerWarning,
setTrackerNotReadyWarningShown: (shown: boolean) => {
trackerWarning = shown;
calls.push(`warn:${shown ? 'yes' : 'no'}`);
},
updateVisibleOverlayBounds: () => {},
ensureOverlayWindowLevel: () => {},
syncPrimaryOverlayWindowLayer: () => {},
enforceOverlayLayerOrder: () => {},
syncOverlayShortcuts: () => {},
isMacOSPlatform: true,
showOverlayLoadingOsd: () => {},
} as never);
updateVisibleOverlayVisibility({
visibleOverlayVisible: true,
mainWindow: window as never,
windowTracker: null,
trackerNotReadyWarningShown: trackerWarning,
setTrackerNotReadyWarningShown: (shown: boolean) => {
trackerWarning = shown;
calls.push(`warn:${shown ? 'yes' : 'no'}`);
},
updateVisibleOverlayBounds: () => {
calls.push('update-bounds');
},
ensureOverlayWindowLevel: () => {
calls.push('ensure-level');
},
syncPrimaryOverlayWindowLayer: () => {
calls.push('sync-layer');
},
enforceOverlayLayerOrder: () => {
calls.push('enforce-order');
},
syncOverlayShortcuts: () => {
calls.push('sync-shortcuts');
},
isMacOSPlatform: true,
showOverlayLoadingOsd: (message: string) => {
osdMessages.push(message);
},
} as never);
assert.deepEqual(osdMessages, ['Overlay loading...', 'Overlay loading...']);
});

View File

@@ -1,4 +1,4 @@
import { BrowserWindow, screen } from 'electron';
import type { BrowserWindow } from 'electron';
import { BaseWindowTracker } from '../../window-trackers';
import { WindowGeometry } from '../../types';
@@ -10,14 +10,19 @@ export function updateVisibleOverlayVisibility(args: {
setTrackerNotReadyWarningShown: (shown: boolean) => void;
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
ensureOverlayWindowLevel: (window: BrowserWindow) => void;
syncPrimaryOverlayWindowLayer: (layer: 'visible') => void;
enforceOverlayLayerOrder: () => void;
syncOverlayShortcuts: () => void;
isMacOSPlatform?: boolean;
showOverlayLoadingOsd?: (message: string) => void;
resolveFallbackBounds: () => WindowGeometry;
}): void {
if (!args.mainWindow || args.mainWindow.isDestroyed()) {
return;
}
if (!args.visibleOverlayVisible) {
args.setTrackerNotReadyWarningShown(false);
args.mainWindow.hide();
args.syncOverlayShortcuts();
return;
@@ -29,6 +34,8 @@ export function updateVisibleOverlayVisibility(args: {
if (geometry) {
args.updateVisibleOverlayBounds(geometry);
}
args.syncPrimaryOverlayWindowLayer('visible');
args.mainWindow.setIgnoreMouseEvents(false);
args.ensureOverlayWindowLevel(args.mainWindow);
args.mainWindow.show();
args.mainWindow.focus();
@@ -38,7 +45,18 @@ export function updateVisibleOverlayVisibility(args: {
}
if (!args.windowTracker) {
if (args.isMacOSPlatform) {
if (!args.trackerNotReadyWarningShown) {
args.setTrackerNotReadyWarningShown(true);
args.showOverlayLoadingOsd?.('Overlay loading...');
}
args.mainWindow.hide();
args.syncOverlayShortcuts();
return;
}
args.setTrackerNotReadyWarningShown(false);
args.syncPrimaryOverlayWindowLayer('visible');
args.mainWindow.setIgnoreMouseEvents(false);
args.ensureOverlayWindowLevel(args.mainWindow);
args.mainWindow.show();
args.mainWindow.focus();
@@ -49,16 +67,21 @@ export function updateVisibleOverlayVisibility(args: {
if (!args.trackerNotReadyWarningShown) {
args.setTrackerNotReadyWarningShown(true);
if (args.isMacOSPlatform) {
args.showOverlayLoadingOsd?.('Overlay loading...');
}
}
const cursorPoint = screen.getCursorScreenPoint();
const display = screen.getDisplayNearestPoint(cursorPoint);
const fallbackBounds = display.workArea;
args.updateVisibleOverlayBounds({
x: fallbackBounds.x,
y: fallbackBounds.y,
width: fallbackBounds.width,
height: fallbackBounds.height,
});
if (args.isMacOSPlatform) {
args.mainWindow.hide();
args.syncOverlayShortcuts();
return;
}
const fallbackBounds = args.resolveFallbackBounds();
args.updateVisibleOverlayBounds(fallbackBounds);
args.syncPrimaryOverlayWindowLayer('visible');
args.mainWindow.setIgnoreMouseEvents(false);
args.ensureOverlayWindowLevel(args.mainWindow);
args.mainWindow.show();
args.mainWindow.focus();
@@ -66,111 +89,11 @@ export function updateVisibleOverlayVisibility(args: {
args.syncOverlayShortcuts();
}
export function updateInvisibleOverlayVisibility(args: {
invisibleWindow: BrowserWindow | null;
visibleOverlayVisible: boolean;
invisibleOverlayVisible: boolean;
windowTracker: BaseWindowTracker | null;
updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void;
ensureOverlayWindowLevel: (window: BrowserWindow) => void;
enforceOverlayLayerOrder: () => void;
syncOverlayShortcuts: () => void;
}): void {
if (!args.invisibleWindow || args.invisibleWindow.isDestroyed()) {
return;
}
if (args.visibleOverlayVisible) {
args.invisibleWindow.hide();
args.syncOverlayShortcuts();
return;
}
const showInvisibleWithoutFocus = (): void => {
args.ensureOverlayWindowLevel(args.invisibleWindow!);
if (typeof args.invisibleWindow!.showInactive === 'function') {
args.invisibleWindow!.showInactive();
} else {
args.invisibleWindow!.show();
}
args.enforceOverlayLayerOrder();
};
if (!args.invisibleOverlayVisible) {
args.invisibleWindow.hide();
args.syncOverlayShortcuts();
return;
}
if (args.windowTracker && args.windowTracker.isTracking()) {
const geometry = args.windowTracker.getGeometry();
if (geometry) {
args.updateInvisibleOverlayBounds(geometry);
}
showInvisibleWithoutFocus();
args.syncOverlayShortcuts();
return;
}
if (!args.windowTracker) {
showInvisibleWithoutFocus();
args.syncOverlayShortcuts();
return;
}
const cursorPoint = screen.getCursorScreenPoint();
const display = screen.getDisplayNearestPoint(cursorPoint);
const fallbackBounds = display.workArea;
args.updateInvisibleOverlayBounds({
x: fallbackBounds.x,
y: fallbackBounds.y,
width: fallbackBounds.width,
height: fallbackBounds.height,
});
showInvisibleWithoutFocus();
args.syncOverlayShortcuts();
}
export function syncInvisibleOverlayMousePassthrough(options: {
hasInvisibleWindow: () => boolean;
setIgnoreMouseEvents: (ignore: boolean, extra?: { forward: boolean }) => void;
visibleOverlayVisible: boolean;
invisibleOverlayVisible: boolean;
}): void {
if (!options.hasInvisibleWindow()) return;
if (options.visibleOverlayVisible) {
options.setIgnoreMouseEvents(true, { forward: true });
} else if (options.invisibleOverlayVisible) {
options.setIgnoreMouseEvents(false);
}
}
export function setVisibleOverlayVisible(options: {
visible: boolean;
setVisibleOverlayVisibleState: (visible: boolean) => void;
updateVisibleOverlayVisibility: () => void;
updateInvisibleOverlayVisibility: () => void;
syncInvisibleOverlayMousePassthrough: () => void;
shouldBindVisibleOverlayToMpvSubVisibility: () => boolean;
isMpvConnected: () => boolean;
setMpvSubVisibility: (visible: boolean) => void;
}): void {
options.setVisibleOverlayVisibleState(options.visible);
options.updateVisibleOverlayVisibility();
options.updateInvisibleOverlayVisibility();
options.syncInvisibleOverlayMousePassthrough();
if (options.shouldBindVisibleOverlayToMpvSubVisibility() && options.isMpvConnected()) {
options.setMpvSubVisibility(!options.visible);
}
}
export function setInvisibleOverlayVisible(options: {
visible: boolean;
setInvisibleOverlayVisibleState: (visible: boolean) => void;
updateInvisibleOverlayVisibility: () => void;
syncInvisibleOverlayMousePassthrough: () => void;
}): void {
options.setInvisibleOverlayVisibleState(options.visible);
options.updateInvisibleOverlayVisibility();
options.syncInvisibleOverlayMousePassthrough();
}

View File

@@ -1,7 +1,6 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
getInitialInvisibleOverlayVisibility,
isAutoUpdateEnabledRuntime,
shouldAutoInitializeOverlayRuntimeFromConfig,
shouldBindVisibleOverlayToMpvSubVisibility,
@@ -10,9 +9,6 @@ import {
const BASE_CONFIG = {
auto_start_overlay: false,
bind_visible_overlay_to_mpv_sub_visibility: true,
invisibleOverlay: {
startupVisibility: 'platform-default' as const,
},
ankiConnect: {
behavior: {
autoUpdateNewCards: true,
@@ -20,26 +16,7 @@ const BASE_CONFIG = {
},
};
test('getInitialInvisibleOverlayVisibility handles visibility + platform', () => {
assert.equal(
getInitialInvisibleOverlayVisibility(
{ ...BASE_CONFIG, invisibleOverlay: { startupVisibility: 'visible' } },
'linux',
),
true,
);
assert.equal(
getInitialInvisibleOverlayVisibility(
{ ...BASE_CONFIG, invisibleOverlay: { startupVisibility: 'hidden' } },
'darwin',
),
false,
);
assert.equal(getInitialInvisibleOverlayVisibility(BASE_CONFIG, 'linux'), false);
assert.equal(getInitialInvisibleOverlayVisibility(BASE_CONFIG, 'darwin'), true);
});
test('shouldAutoInitializeOverlayRuntimeFromConfig respects auto start and visible startup', () => {
test('shouldAutoInitializeOverlayRuntimeFromConfig respects auto start', () => {
assert.equal(shouldAutoInitializeOverlayRuntimeFromConfig(BASE_CONFIG), false);
assert.equal(
shouldAutoInitializeOverlayRuntimeFromConfig({
@@ -48,13 +25,6 @@ test('shouldAutoInitializeOverlayRuntimeFromConfig respects auto start and visib
}),
true,
);
assert.equal(
shouldAutoInitializeOverlayRuntimeFromConfig({
...BASE_CONFIG,
invisibleOverlay: { startupVisibility: 'visible' },
}),
true,
);
});
test('shouldBindVisibleOverlayToMpvSubVisibility returns config value', () => {

View File

@@ -5,14 +5,12 @@ const logger = createLogger('main:shortcut');
export interface GlobalShortcutConfig {
toggleVisibleOverlayGlobal: string | null | undefined;
toggleInvisibleOverlayGlobal: string | null | undefined;
openJimaku?: string | null | undefined;
}
export interface RegisterGlobalShortcutsServiceOptions {
shortcuts: GlobalShortcutConfig;
onToggleVisibleOverlay: () => void;
onToggleInvisibleOverlay: () => void;
onOpenYomitanSettings: () => void;
onOpenJimaku?: () => void;
isDev: boolean;
@@ -21,9 +19,7 @@ export interface RegisterGlobalShortcutsServiceOptions {
export function registerGlobalShortcuts(options: RegisterGlobalShortcutsServiceOptions): void {
const visibleShortcut = options.shortcuts.toggleVisibleOverlayGlobal;
const invisibleShortcut = options.shortcuts.toggleInvisibleOverlayGlobal;
const normalizedVisible = visibleShortcut?.replace(/\s+/g, '').toLowerCase();
const normalizedInvisible = invisibleShortcut?.replace(/\s+/g, '').toLowerCase();
const normalizedJimaku = options.shortcuts.openJimaku?.replace(/\s+/g, '').toLowerCase();
const normalizedSettings = 'alt+shift+y';
@@ -38,31 +34,10 @@ export function registerGlobalShortcuts(options: RegisterGlobalShortcutsServiceO
}
}
if (invisibleShortcut && normalizedInvisible && normalizedInvisible !== normalizedVisible) {
const toggleInvisibleRegistered = globalShortcut.register(invisibleShortcut, () => {
options.onToggleInvisibleOverlay();
});
if (!toggleInvisibleRegistered) {
logger.warn(
`Failed to register global shortcut toggleInvisibleOverlayGlobal: ${invisibleShortcut}`,
);
}
} else if (
invisibleShortcut &&
normalizedInvisible &&
normalizedInvisible === normalizedVisible
) {
logger.warn(
'Skipped registering toggleInvisibleOverlayGlobal because it collides with toggleVisibleOverlayGlobal',
);
}
if (options.shortcuts.openJimaku && options.onOpenJimaku) {
if (
normalizedJimaku &&
(normalizedJimaku === normalizedVisible ||
normalizedJimaku === normalizedInvisible ||
normalizedJimaku === normalizedSettings)
(normalizedJimaku === normalizedVisible || normalizedJimaku === normalizedSettings)
) {
logger.warn(
'Skipped registering openJimaku because it collides with another global shortcut',

View File

@@ -10,14 +10,11 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
stop: false,
toggle: false,
toggleVisibleOverlay: false,
toggleInvisibleOverlay: false,
settings: false,
show: false,
hide: false,
showVisibleOverlay: false,
hideVisibleOverlay: false,
showInvisibleOverlay: false,
hideInvisibleOverlay: false,
copySubtitle: false,
copySubtitleMultiple: false,
mineSentence: false,

View File

@@ -19,9 +19,6 @@ interface RuntimeAutoUpdateOptionManagerLike {
export interface RuntimeConfigLike {
auto_start_overlay?: boolean;
bind_visible_overlay_to_mpv_sub_visibility: boolean;
invisibleOverlay: {
startupVisibility: 'visible' | 'hidden' | 'platform-default';
};
ankiConnect?: {
behavior?: {
autoUpdateNewCards?: boolean;
@@ -155,21 +152,8 @@ function getStartupCriticalConfigErrors(config: AppReadyConfigLike): string[] {
return errors;
}
export function getInitialInvisibleOverlayVisibility(
config: RuntimeConfigLike,
platform: NodeJS.Platform,
): boolean {
const visibility = config.invisibleOverlay.startupVisibility;
if (visibility === 'visible') return true;
if (visibility === 'hidden') return false;
if (platform === 'linux') return false;
return true;
}
export function shouldAutoInitializeOverlayRuntimeFromConfig(config: RuntimeConfigLike): boolean {
if (config.auto_start_overlay === true) return true;
if (config.invisibleOverlay.startupVisibility === 'visible') return true;
return false;
return config.auto_start_overlay === true;
}
export function shouldBindVisibleOverlayToMpvSubVisibility(config: RuntimeConfigLike): boolean {

View File

@@ -101,20 +101,7 @@ export function loadSubtitlePosition(
const data = fs.readFileSync(positionPath, 'utf-8');
const parsed = JSON.parse(data) as Partial<SubtitlePosition>;
if (parsed && typeof parsed.yPercent === 'number' && Number.isFinite(parsed.yPercent)) {
const position: SubtitlePosition = { yPercent: parsed.yPercent };
if (
typeof parsed.invisibleOffsetXPx === 'number' &&
Number.isFinite(parsed.invisibleOffsetXPx)
) {
position.invisibleOffsetXPx = parsed.invisibleOffsetXPx;
}
if (
typeof parsed.invisibleOffsetYPx === 'number' &&
Number.isFinite(parsed.invisibleOffsetYPx)
) {
position.invisibleOffsetYPx = parsed.invisibleOffsetYPx;
}
return position;
return { yPercent: parsed.yPercent };
}
return options.fallbackPosition;
} catch (err) {