Files
SubMiner/src/core/services/field-grouping-overlay.ts
T

153 lines
5.7 KiB
TypeScript

import { KikuFieldGroupingChoice, KikuFieldGroupingRequestData } from '../../types';
import { IPC_CHANNELS } from '../../shared/ipc/contracts';
import { createFieldGroupingCallbackRuntime, sendToVisibleOverlayRuntime } from './overlay-bridge';
interface WindowLike {
isDestroyed: () => boolean;
webContents: {
send: (channel: string, payload?: unknown) => void;
};
}
const KIKU_FIELD_GROUPING_MODAL_OPEN_TIMEOUT_MS = 1500;
const KIKU_FIELD_GROUPING_MODAL_RETRY_WARNING =
'Kiku field grouping modal did not acknowledge modal open on first attempt; retrying dedicated modal window.';
export interface FieldGroupingOverlayRuntimeOptions<T extends string> {
getMainWindow: () => WindowLike | null;
getVisibleOverlayVisible: () => boolean;
setVisibleOverlayVisible: (visible: boolean) => void;
getResolver: () => ((choice: KikuFieldGroupingChoice) => void) | null;
setResolver: (resolver: ((choice: KikuFieldGroupingChoice) => void) | null) => void;
getRestoreVisibleOverlayOnModalClose: () => Set<T>;
waitForModalOpen?: (modal: T, timeoutMs: number) => Promise<boolean>;
handleOverlayModalClosed?: (modal: T) => void;
logWarn?: (message: string) => void;
/**
* Prepare the overlay runtime and (re)create the visible overlay window before opening the
* modal — the same prerequisites every other modal runs via `openOverlayHostedModal`. Without
* them the field grouping modal can open with no sibling overlay window present, and on
* Hyprland it then fails to sit above / take focus over fullscreen mpv the way the other
* modals do.
*/
ensureOverlayStartupPrereqs?: () => void;
ensureOverlayWindowsReadyForVisibilityActions?: () => void;
sendToVisibleOverlay?: (
channel: string,
payload?: unknown,
runtimeOptions?: { restoreOnModalClose?: T; preferModalWindow?: boolean },
) => boolean;
}
export function createFieldGroupingOverlayRuntime<T extends string>(
options: FieldGroupingOverlayRuntimeOptions<T>,
): {
sendToVisibleOverlay: (
channel: string,
payload?: unknown,
runtimeOptions?: { restoreOnModalClose?: T; preferModalWindow?: boolean },
) => boolean;
createFieldGroupingCallback: () => (
data: KikuFieldGroupingRequestData,
) => Promise<KikuFieldGroupingChoice>;
} {
const sendToVisibleOverlay = (
channel: string,
payload?: unknown,
runtimeOptions?: { restoreOnModalClose?: T; preferModalWindow?: boolean },
): boolean => {
if (options.sendToVisibleOverlay) {
const wasVisible = options.getVisibleOverlayVisible();
const sent = options.sendToVisibleOverlay(channel, payload, runtimeOptions);
if (sent && !wasVisible && !options.getVisibleOverlayVisible()) {
options.setVisibleOverlayVisible(true);
}
return sent;
}
return sendToVisibleOverlayRuntime({
mainWindow: options.getMainWindow() as never,
visibleOverlayVisible: options.getVisibleOverlayVisible(),
setVisibleOverlayVisible: options.setVisibleOverlayVisible,
channel,
payload,
restoreOnModalClose: runtimeOptions?.restoreOnModalClose,
restoreVisibleOverlayOnModalClose: options.getRestoreVisibleOverlayOnModalClose(),
});
};
const sendKikuFieldGroupingRequest = async (
data: KikuFieldGroupingRequestData,
): Promise<boolean> => {
const kikuModal = 'kiku' as T;
const sendOpen = (): boolean => {
// Match every other modal's open path (openOverlayHostedModal): ensure the overlay runtime
// and visible overlay window exist before handing off to the dedicated modal window.
options.ensureOverlayStartupPrereqs?.();
options.ensureOverlayWindowsReadyForVisibilityActions?.();
return sendToVisibleOverlay('kiku:field-grouping-request', data, {
restoreOnModalClose: kikuModal,
preferModalWindow: true,
});
};
if (!options.waitForModalOpen) {
return sendOpen();
}
if (!sendOpen()) {
return false;
}
if (await options.waitForModalOpen(kikuModal, KIKU_FIELD_GROUPING_MODAL_OPEN_TIMEOUT_MS)) {
return true;
}
options.logWarn?.(KIKU_FIELD_GROUPING_MODAL_RETRY_WARNING);
if (!sendOpen()) {
options.handleOverlayModalClosed?.(kikuModal);
return false;
}
const opened = await options.waitForModalOpen(
kikuModal,
KIKU_FIELD_GROUPING_MODAL_OPEN_TIMEOUT_MS,
);
if (!opened) {
options.handleOverlayModalClosed?.(kikuModal);
}
return opened;
};
const dismissModalUi = (): void => {
const kikuModal = 'kiku' as T;
// Best-effort: tell the renderer hosting the modal to close its dialog. When the modal
// lives in the dedicated modal window this is redundant with the teardown below, but it
// also covers the case where the request was routed into the visible overlay.
sendToVisibleOverlay(IPC_CHANNELS.event.kikuFieldGroupingCancel, undefined, {
preferModalWindow: true,
});
// Reliable teardown of main-side modal state (restore set, main-overlay passthrough,
// dedicated modal window). This is what recovers the frozen overlay when a grouping
// request times out or fails to reach a visible modal.
options.handleOverlayModalClosed?.(kikuModal);
};
const createFieldGroupingCallback = (): ((
data: KikuFieldGroupingRequestData,
) => Promise<KikuFieldGroupingChoice>) => {
return createFieldGroupingCallbackRuntime({
getVisibleOverlayVisible: options.getVisibleOverlayVisible,
setVisibleOverlayVisible: options.setVisibleOverlayVisible,
getResolver: options.getResolver,
setResolver: options.setResolver,
sendToVisibleOverlay,
sendKikuFieldGroupingRequest,
dismissModalUi,
});
};
return {
sendToVisibleOverlay,
createFieldGroupingCallback,
};
}