mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 18:22:42 -08:00
refactor: remove invisible subtitle overlay code
This commit is contained in:
@@ -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 },
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
function makeShortcuts(overrides: Partial<ConfiguredShortcuts> = {}): ConfiguredShortcuts {
|
||||
return {
|
||||
toggleVisibleOverlayGlobal: null,
|
||||
toggleInvisibleOverlayGlobal: null,
|
||||
copySubtitle: null,
|
||||
copySubtitleMultiple: null,
|
||||
updateLastCardFromClipboard: null,
|
||||
|
||||
265
src/core/services/overlay-visibility.test.ts
Normal file
265
src/core/services/overlay-visibility.test.ts
Normal 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...']);
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user