Fix overlay modal dispatch and restore behavior

This commit is contained in:
2026-02-14 00:35:30 -08:00
parent 1d2c404273
commit 9b24e09d48
3 changed files with 151 additions and 13 deletions

View File

@@ -176,6 +176,12 @@ if (process.platform === "linux") {
}
const DEFAULT_TEXTHOOKER_PORT = 5174;
const DEFAULT_MPV_LOG_FILE = path.join(
os.homedir(),
".cache",
"SubMiner",
"mp.log",
);
function resolveConfigDir(): string {
const xdgConfigHome = process.env.XDG_CONFIG_HOME?.trim();
const baseDirs = Array.from(
@@ -212,6 +218,7 @@ function resolveConfigDir(): string {
const CONFIG_DIR = resolveConfigDir();
const USER_DATA_PATH = CONFIG_DIR;
const DEFAULT_MPV_LOG_PATH = process.env.SUBMINER_MPV_LOG?.trim() || DEFAULT_MPV_LOG_FILE;
const configService = new ConfigService(CONFIG_DIR);
const isDev =
process.argv.includes("--dev") || process.argv.includes("--debug");
@@ -270,6 +277,7 @@ let currentSubAssText = "";
let windowTracker: BaseWindowTracker | null = null;
let subtitlePosition: SubtitlePosition | null = null;
let currentMediaPath: string | null = null;
let currentMediaTitle: string | null = null;
let pendingSubtitlePosition: SubtitlePosition | null = null;
let mecabTokenizer: MecabTokenizer | null = null;
let keybindings: Keybinding[] = [];
@@ -297,8 +305,10 @@ const overlayContentMeasurementStore = createOverlayContentMeasurementStoreServi
console.warn(message);
},
});
type OverlayHostedModal = "runtime-options" | "subsync";
type OverlayHostedModal = "runtime-options" | "subsync" | "jimaku";
type OverlayHostLayer = "visible" | "invisible";
const restoreVisibleOverlayOnModalClose = new Set<OverlayHostedModal>();
const overlayModalAutoShownLayer = new Map<OverlayHostedModal, OverlayHostLayer>();
function getFieldGroupingResolver(): ((choice: KikuFieldGroupingChoice) => void) | null {
return fieldGroupingResolver;
@@ -328,8 +338,11 @@ const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntimeService<Ove
getResolver: () => getFieldGroupingResolver(),
setResolver: (resolver) => setFieldGroupingResolver(resolver),
getRestoreVisibleOverlayOnModalClose: () => restoreVisibleOverlayOnModalClose,
sendToVisibleOverlay: (channel, payload) => {
sendToActiveOverlayWindow(channel, payload);
return true;
},
});
const sendToVisibleOverlay = fieldGroupingOverlayRuntime.sendToVisibleOverlay;
const createFieldGroupingCallback =
fieldGroupingOverlayRuntime.createFieldGroupingCallback;
@@ -357,6 +370,79 @@ function broadcastRuntimeOptionsChanged(): void {
);
}
function getTargetOverlayWindow(): {
window: BrowserWindow;
layer: OverlayHostLayer;
} | null {
const visibleMainWindow = overlayManager.getMainWindow();
const invisibleWindow = overlayManager.getInvisibleWindow();
if (visibleMainWindow && !visibleMainWindow.isDestroyed()) {
return { window: visibleMainWindow, layer: "visible" };
}
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
return { window: invisibleWindow, layer: "invisible" };
}
return null;
}
function showOverlayWindowForModal(window: BrowserWindow, layer: OverlayHostLayer): void {
if (layer === "invisible" && typeof window.showInactive === "function") {
window.showInactive();
} else {
window.show();
}
if (!window.isFocused()) {
window.focus();
}
}
function sendToActiveOverlayWindow(
channel: string,
payload?: unknown,
runtimeOptions?: { restoreOnModalClose?: OverlayHostedModal },
): void {
const target = getTargetOverlayWindow();
if (!target) return;
const { window: targetWindow, layer } = target;
const wasVisible = targetWindow.isVisible();
const restoreOnModalClose = runtimeOptions?.restoreOnModalClose;
const sendNow = (): void => {
if (payload === undefined) {
targetWindow.webContents.send(channel);
} else {
targetWindow.webContents.send(channel, payload);
}
};
if (!wasVisible) {
showOverlayWindowForModal(targetWindow, layer);
}
if (!wasVisible && restoreOnModalClose) {
restoreVisibleOverlayOnModalClose.add(restoreOnModalClose);
overlayModalAutoShownLayer.set(restoreOnModalClose, layer);
}
if (targetWindow.webContents.isLoading()) {
targetWindow.webContents.once("did-finish-load", () => {
if (
targetWindow &&
!targetWindow.isDestroyed() &&
!targetWindow.webContents.isLoading()
) {
sendNow();
}
});
return;
}
sendNow();
}
function setOverlayDebugVisualizationEnabled(enabled: boolean): void {
setOverlayDebugVisualizationEnabledRuntimeService(
overlayDebugVisualizationEnabled,
@@ -368,7 +454,11 @@ function setOverlayDebugVisualizationEnabled(enabled: boolean): void {
);
}
function openRuntimeOptionsPalette(): void { sendToVisibleOverlay("runtime-options:open", undefined, { restoreOnModalClose: "runtime-options" }); }
function openRuntimeOptionsPalette(): void {
sendToActiveOverlayWindow("runtime-options:open", undefined, {
restoreOnModalClose: "runtime-options",
});
}
function getResolvedConfig() { return configService.getConfig(); }
@@ -437,6 +527,9 @@ function saveSubtitlePosition(position: SubtitlePosition): void {
}
function updateCurrentMediaPath(mediaPath: unknown): void {
if (typeof mediaPath !== "string" || !isRemoteMediaPath(mediaPath)) {
currentMediaTitle = null;
}
updateCurrentMediaPathService({
mediaPath,
currentMediaPath,
@@ -458,6 +551,21 @@ function updateCurrentMediaPath(mediaPath: unknown): void {
});
}
function updateCurrentMediaTitle(mediaTitle: unknown): void {
if (typeof mediaTitle === "string") {
const sanitized = mediaTitle.trim();
currentMediaTitle = sanitized.length > 0 ? sanitized : null;
return;
}
currentMediaTitle = null;
}
function resolveMediaPathForJimaku(mediaPath: string | null): string | null {
return mediaPath && isRemoteMediaPath(mediaPath) && currentMediaTitle
? currentMediaTitle
: mediaPath;
}
let subsyncInProgress = false;
let initialArgs: CliArgs;
let mpvSocketPath = getDefaultSocketPath();
@@ -552,6 +660,9 @@ const startupState = runStartupBootstrapRuntimeService({
updateCurrentMediaPath: (mediaPath) => {
updateCurrentMediaPath(mediaPath);
},
updateCurrentMediaTitle: (mediaTitle) => {
updateCurrentMediaTitle(mediaTitle);
},
updateMpvSubtitleRenderMetrics: (patch) => {
updateMpvSubtitleRenderMetrics(patch);
},
@@ -944,7 +1055,9 @@ function getOverlayShortcutRuntimeHandlers() {
openRuntimeOptionsPalette();
},
openJimaku: () => {
sendToVisibleOverlay("jimaku:open");
sendToActiveOverlayWindow("jimaku:open", undefined, {
restoreOnModalClose: "jimaku",
});
},
markAudioCard: () => markLastCardAsAudioCard(),
copySubtitleMultiple: (timeoutMs) => {
@@ -994,6 +1107,7 @@ function cycleSecondarySubMode(): void {
}
function showMpvOsd(text: string): void {
appendToMpvLog(`[OSD] ${text}`);
showMpvOsdRuntimeService(
mpvClient,
text,
@@ -1003,6 +1117,19 @@ function showMpvOsd(text: string): void {
);
}
function appendToMpvLog(message: string): void {
try {
fs.mkdirSync(path.dirname(DEFAULT_MPV_LOG_PATH), { recursive: true });
fs.appendFileSync(
DEFAULT_MPV_LOG_PATH,
`[${new Date().toISOString()}] ${message}\n`,
{ encoding: "utf8" },
);
} catch {
// best-effort logging
}
}
const numericShortcutRuntime = createNumericShortcutRuntimeService({
globalShortcut,
showMpvOsd: (text) => showMpvOsd(text),
@@ -1022,7 +1149,7 @@ function getSubsyncRuntimeDeps() {
},
showMpvOsd: (text: string) => showMpvOsd(text),
openManualPicker: (payload: SubsyncManualPayload) => {
sendToVisibleOverlay("subsync:open-manual", payload, {
sendToActiveOverlayWindow("subsync:open-manual", payload, {
restoreOnModalClose: "subsync",
});
},
@@ -1270,8 +1397,24 @@ function toggleOverlay(): void { toggleVisibleOverlay(); }
function handleOverlayModalClosed(modal: OverlayHostedModal): void {
if (!restoreVisibleOverlayOnModalClose.has(modal)) return;
restoreVisibleOverlayOnModalClose.delete(modal);
if (restoreVisibleOverlayOnModalClose.size === 0) {
setVisibleOverlayVisible(false);
const layer = overlayModalAutoShownLayer.get(modal);
overlayModalAutoShownLayer.delete(modal);
if (!layer) return;
const shouldKeepLayerVisible = [...restoreVisibleOverlayOnModalClose].some(
(pendingModal) => overlayModalAutoShownLayer.get(pendingModal) === layer,
);
if (shouldKeepLayerVisible) return;
if (layer === "visible") {
const mainWindow = overlayManager.getMainWindow();
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.hide();
}
return;
}
const invisibleWindow = overlayManager.getInvisibleWindow();
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
invisibleWindow.hide();
}
}
@@ -1379,7 +1522,7 @@ registerAnkiJimakuIpcRuntimeService(
broadcastRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
getFieldGroupingResolver: () => getFieldGroupingResolver(),
setFieldGroupingResolver: (resolver) => setFieldGroupingResolver(resolver),
parseMediaInfo: (mediaPath) => parseMediaInfo(mediaPath),
parseMediaInfo: (mediaPath) => parseMediaInfo(resolveMediaPathForJimaku(mediaPath)),
getCurrentMediaPath: () => currentMediaPath,
jimakuFetchJson: (endpoint, query) => jimakuFetchJson(endpoint, query),
getJimakuMaxEntryResults: () => getJimakuMaxEntryResults(),