mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-11 16:19:27 -07:00
fix: address remaining CodeRabbit PR feedback
This commit is contained in:
@@ -177,7 +177,7 @@
|
|||||||
"openSessionHelp": "CommandOrControl+Shift+H", // Open session help setting.
|
"openSessionHelp": "CommandOrControl+Shift+H", // Open session help setting.
|
||||||
"openControllerSelect": "Alt+C", // Open controller select setting.
|
"openControllerSelect": "Alt+C", // Open controller select setting.
|
||||||
"openControllerDebug": "Alt+Shift+C", // Open controller debug setting.
|
"openControllerDebug": "Alt+Shift+C", // Open controller debug setting.
|
||||||
"toggleSubtitleSidebar": "\\" // Toggle subtitle sidebar setting.
|
"toggleSubtitleSidebar": "Backslash" // Toggle subtitle sidebar setting.
|
||||||
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
@@ -540,7 +540,7 @@ See `config.example.jsonc` for detailed configuration options.
|
|||||||
"openControllerSelect": "Alt+C",
|
"openControllerSelect": "Alt+C",
|
||||||
"openControllerDebug": "Alt+Shift+C",
|
"openControllerDebug": "Alt+Shift+C",
|
||||||
"openJimaku": "Ctrl+Shift+J",
|
"openJimaku": "Ctrl+Shift+J",
|
||||||
"toggleSubtitleSidebar": "\\",
|
"toggleSubtitleSidebar": "Backslash",
|
||||||
"multiCopyTimeoutMs": 3000
|
"multiCopyTimeoutMs": 3000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -564,7 +564,7 @@ See `config.example.jsonc` for detailed configuration options.
|
|||||||
| `openControllerSelect` | string \| `null` | Opens the controller config/remap modal (default: `"Alt+C"`) |
|
| `openControllerSelect` | string \| `null` | Opens the controller config/remap modal (default: `"Alt+C"`) |
|
||||||
| `openControllerDebug` | string \| `null` | Opens the controller debug modal (default: `"Alt+Shift+C"`) |
|
| `openControllerDebug` | string \| `null` | Opens the controller debug modal (default: `"Alt+Shift+C"`) |
|
||||||
| `openJimaku` | string \| `null` | Opens the Jimaku search modal (default: `"Ctrl+Shift+J"`) |
|
| `openJimaku` | string \| `null` | Opens the Jimaku search modal (default: `"Ctrl+Shift+J"`) |
|
||||||
| `toggleSubtitleSidebar` | string \| `null` | Dispatches the subtitle sidebar toggle action (default: `"\\"`). `subtitleSidebar.toggleKey` remains the primary bare-key setting. |
|
| `toggleSubtitleSidebar` | string \| `null` | Dispatches the subtitle sidebar toggle action (default: `"Backslash"`). `subtitleSidebar.toggleKey` remains the primary bare-key setting. |
|
||||||
|
|
||||||
**See `config.example.jsonc`** for the complete list of shortcut configuration options.
|
**See `config.example.jsonc`** for the complete list of shortcut configuration options.
|
||||||
|
|
||||||
|
|||||||
@@ -177,7 +177,7 @@
|
|||||||
"openSessionHelp": "CommandOrControl+Shift+H", // Open session help setting.
|
"openSessionHelp": "CommandOrControl+Shift+H", // Open session help setting.
|
||||||
"openControllerSelect": "Alt+C", // Open controller select setting.
|
"openControllerSelect": "Alt+C", // Open controller select setting.
|
||||||
"openControllerDebug": "Alt+Shift+C", // Open controller debug setting.
|
"openControllerDebug": "Alt+Shift+C", // Open controller debug setting.
|
||||||
"toggleSubtitleSidebar": "\\" // Toggle subtitle sidebar setting.
|
"toggleSubtitleSidebar": "Backslash" // Toggle subtitle sidebar setting.
|
||||||
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ test('loads defaults when config is missing', () => {
|
|||||||
assert.equal(config.startupWarmups.yomitanExtension, true);
|
assert.equal(config.startupWarmups.yomitanExtension, true);
|
||||||
assert.equal(config.startupWarmups.subtitleDictionaries, true);
|
assert.equal(config.startupWarmups.subtitleDictionaries, true);
|
||||||
assert.equal(config.startupWarmups.jellyfinRemoteSession, true);
|
assert.equal(config.startupWarmups.jellyfinRemoteSession, true);
|
||||||
|
assert.equal(config.shortcuts.toggleSubtitleSidebar, 'Backslash');
|
||||||
assert.equal(config.discordPresence.enabled, true);
|
assert.equal(config.discordPresence.enabled, true);
|
||||||
assert.equal(config.discordPresence.updateIntervalMs, 3_000);
|
assert.equal(config.discordPresence.updateIntervalMs, 3_000);
|
||||||
assert.equal(config.subtitleStyle.backgroundColor, 'rgb(30, 32, 48, 0.88)');
|
assert.equal(config.subtitleStyle.backgroundColor, 'rgb(30, 32, 48, 0.88)');
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export const CORE_DEFAULT_CONFIG: Pick<
|
|||||||
openSessionHelp: 'CommandOrControl+Shift+H',
|
openSessionHelp: 'CommandOrControl+Shift+H',
|
||||||
openControllerSelect: 'Alt+C',
|
openControllerSelect: 'Alt+C',
|
||||||
openControllerDebug: 'Alt+Shift+C',
|
openControllerDebug: 'Alt+Shift+C',
|
||||||
toggleSubtitleSidebar: '\\',
|
toggleSubtitleSidebar: 'Backslash',
|
||||||
},
|
},
|
||||||
secondarySub: {
|
secondarySub: {
|
||||||
secondarySubLanguages: [],
|
secondarySubLanguages: [],
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ export function handleCliCommand(
|
|||||||
osdLabel: string,
|
osdLabel: string,
|
||||||
): void => {
|
): void => {
|
||||||
runAsyncWithOsd(
|
runAsyncWithOsd(
|
||||||
() => deps.dispatchSessionAction?.(request) ?? Promise.resolve(),
|
() => deps.dispatchSessionAction(request),
|
||||||
deps,
|
deps,
|
||||||
logLabel,
|
logLabel,
|
||||||
osdLabel,
|
osdLabel,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import { OVERLAY_WINDOW_CONTENT_READY_FLAG } from './overlay-window-flags';
|
||||||
import { setVisibleOverlayVisible, updateVisibleOverlayVisibility } from './overlay-visibility';
|
import { setVisibleOverlayVisible, updateVisibleOverlayVisibility } from './overlay-visibility';
|
||||||
|
|
||||||
type WindowTrackerStub = {
|
type WindowTrackerStub = {
|
||||||
@@ -15,7 +16,9 @@ function createMainWindowRecorder() {
|
|||||||
let visible = false;
|
let visible = false;
|
||||||
let focused = false;
|
let focused = false;
|
||||||
let opacity = 1;
|
let opacity = 1;
|
||||||
|
let contentReady = true;
|
||||||
const window = {
|
const window = {
|
||||||
|
webContents: {},
|
||||||
isDestroyed: () => false,
|
isDestroyed: () => false,
|
||||||
isVisible: () => visible,
|
isVisible: () => visible,
|
||||||
isFocused: () => focused,
|
isFocused: () => focused,
|
||||||
@@ -50,11 +53,24 @@ function createMainWindowRecorder() {
|
|||||||
calls.push('move-top');
|
calls.push('move-top');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
(
|
||||||
|
window as {
|
||||||
|
[OVERLAY_WINDOW_CONTENT_READY_FLAG]?: boolean;
|
||||||
|
}
|
||||||
|
)[OVERLAY_WINDOW_CONTENT_READY_FLAG] = contentReady;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
window,
|
window,
|
||||||
calls,
|
calls,
|
||||||
getOpacity: () => opacity,
|
getOpacity: () => opacity,
|
||||||
|
setContentReady: (nextContentReady: boolean) => {
|
||||||
|
contentReady = nextContentReady;
|
||||||
|
(
|
||||||
|
window as {
|
||||||
|
[OVERLAY_WINDOW_CONTENT_READY_FLAG]?: boolean;
|
||||||
|
}
|
||||||
|
)[OVERLAY_WINDOW_CONTENT_READY_FLAG] = contentReady;
|
||||||
|
},
|
||||||
setFocused: (nextFocused: boolean) => {
|
setFocused: (nextFocused: boolean) => {
|
||||||
focused = nextFocused;
|
focused = nextFocused;
|
||||||
},
|
},
|
||||||
@@ -285,6 +301,54 @@ test('Windows visible overlay restores opacity after the deferred reveal delay',
|
|||||||
assert.ok(calls.includes('opacity:1'));
|
assert.ok(calls.includes('opacity:1'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Windows visible overlay waits for content-ready before first reveal', () => {
|
||||||
|
const { window, calls, setContentReady } = createMainWindowRecorder();
|
||||||
|
const tracker: WindowTrackerStub = {
|
||||||
|
isTracking: () => true,
|
||||||
|
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
|
||||||
|
};
|
||||||
|
setContentReady(false);
|
||||||
|
|
||||||
|
const run = () =>
|
||||||
|
updateVisibleOverlayVisibility({
|
||||||
|
visibleOverlayVisible: true,
|
||||||
|
mainWindow: window as never,
|
||||||
|
windowTracker: tracker as never,
|
||||||
|
trackerNotReadyWarningShown: false,
|
||||||
|
setTrackerNotReadyWarningShown: () => {},
|
||||||
|
updateVisibleOverlayBounds: () => {
|
||||||
|
calls.push('update-bounds');
|
||||||
|
},
|
||||||
|
ensureOverlayWindowLevel: () => {
|
||||||
|
calls.push('ensure-level');
|
||||||
|
},
|
||||||
|
syncWindowsOverlayToMpvZOrder: () => {
|
||||||
|
calls.push('sync-windows-z-order');
|
||||||
|
},
|
||||||
|
syncPrimaryOverlayWindowLayer: () => {
|
||||||
|
calls.push('sync-layer');
|
||||||
|
},
|
||||||
|
enforceOverlayLayerOrder: () => {
|
||||||
|
calls.push('enforce-order');
|
||||||
|
},
|
||||||
|
syncOverlayShortcuts: () => {
|
||||||
|
calls.push('sync-shortcuts');
|
||||||
|
},
|
||||||
|
isMacOSPlatform: false,
|
||||||
|
isWindowsPlatform: true,
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
run();
|
||||||
|
|
||||||
|
assert.ok(!calls.includes('show-inactive'));
|
||||||
|
assert.ok(!calls.includes('show'));
|
||||||
|
|
||||||
|
setContentReady(true);
|
||||||
|
run();
|
||||||
|
|
||||||
|
assert.ok(calls.includes('show-inactive'));
|
||||||
|
});
|
||||||
|
|
||||||
test('tracked Windows overlay refresh rebinds while already visible', () => {
|
test('tracked Windows overlay refresh rebinds while already visible', () => {
|
||||||
const { window, calls } = createMainWindowRecorder();
|
const { window, calls } = createMainWindowRecorder();
|
||||||
const tracker: WindowTrackerStub = {
|
const tracker: WindowTrackerStub = {
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ function resolveCount(count: number | undefined): number {
|
|||||||
return Math.min(9, Math.max(1, normalized));
|
return Math.min(9, Math.max(1, normalized));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assertUnreachableSessionAction(actionId: never): never {
|
||||||
|
throw new Error(`Unhandled session action: ${String(actionId)}`);
|
||||||
|
}
|
||||||
|
|
||||||
export async function dispatchSessionAction(
|
export async function dispatchSessionAction(
|
||||||
request: SessionActionDispatchRequest,
|
request: SessionActionDispatchRequest,
|
||||||
deps: SessionActionExecutorDeps,
|
deps: SessionActionExecutorDeps,
|
||||||
@@ -121,5 +125,7 @@ export async function dispatchSessionAction(
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
return assertUnreachableSessionAction(request.actionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1544,8 +1544,8 @@ const buildConfigHotReloadAppliedMainDepsHandler = createBuildConfigHotReloadApp
|
|||||||
setKeybindings: (keybindings) => {
|
setKeybindings: (keybindings) => {
|
||||||
appState.keybindings = keybindings;
|
appState.keybindings = keybindings;
|
||||||
},
|
},
|
||||||
setSessionBindings: (sessionBindings) => {
|
setSessionBindings: (sessionBindings, sessionBindingWarnings) => {
|
||||||
persistSessionBindings(sessionBindings);
|
persistSessionBindings(sessionBindings, sessionBindingWarnings);
|
||||||
},
|
},
|
||||||
refreshGlobalAndOverlayShortcuts: () => {
|
refreshGlobalAndOverlayShortcuts: () => {
|
||||||
refreshGlobalAndOverlayShortcuts();
|
refreshGlobalAndOverlayShortcuts();
|
||||||
@@ -3631,7 +3631,7 @@ function ensureOverlayStartupPrereqs(): void {
|
|||||||
if (appState.keybindings.length === 0) {
|
if (appState.keybindings.length === 0) {
|
||||||
appState.keybindings = resolveKeybindings(getResolvedConfig(), DEFAULT_KEYBINDINGS);
|
appState.keybindings = resolveKeybindings(getResolvedConfig(), DEFAULT_KEYBINDINGS);
|
||||||
refreshCurrentSessionBindings();
|
refreshCurrentSessionBindings();
|
||||||
} else if (appState.sessionBindings.length === 0) {
|
} else if (!appState.sessionBindingsInitialized) {
|
||||||
refreshCurrentSessionBindings();
|
refreshCurrentSessionBindings();
|
||||||
}
|
}
|
||||||
if (!appState.mpvClient) {
|
if (!appState.mpvClient) {
|
||||||
@@ -4261,6 +4261,7 @@ function persistSessionBindings(
|
|||||||
});
|
});
|
||||||
writeSessionBindingsArtifact(CONFIG_DIR, artifact);
|
writeSessionBindingsArtifact(CONFIG_DIR, artifact);
|
||||||
appState.sessionBindings = bindings;
|
appState.sessionBindings = bindings;
|
||||||
|
appState.sessionBindingsInitialized = true;
|
||||||
if (appState.mpvClient?.connected) {
|
if (appState.mpvClient?.connected) {
|
||||||
sendMpvCommandRuntime(appState.mpvClient, [
|
sendMpvCommandRuntime(appState.mpvClient, [
|
||||||
'script-message',
|
'script-message',
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ function createMockWindow(): MockWindow & {
|
|||||||
hide: () => void;
|
hide: () => void;
|
||||||
destroy: () => void;
|
destroy: () => void;
|
||||||
focus: () => void;
|
focus: () => void;
|
||||||
|
emitDidFinishLoad: () => void;
|
||||||
|
emitReadyToShow: () => void;
|
||||||
once: (event: 'ready-to-show', cb: () => void) => void;
|
once: (event: 'ready-to-show', cb: () => void) => void;
|
||||||
webContents: {
|
webContents: {
|
||||||
focused: boolean;
|
focused: boolean;
|
||||||
@@ -89,6 +91,14 @@ function createMockWindow(): MockWindow & {
|
|||||||
focus: () => {
|
focus: () => {
|
||||||
state.focused = true;
|
state.focused = true;
|
||||||
},
|
},
|
||||||
|
emitDidFinishLoad: () => {
|
||||||
|
const callback = state.loadCallbacks.shift();
|
||||||
|
callback?.();
|
||||||
|
},
|
||||||
|
emitReadyToShow: () => {
|
||||||
|
const callback = state.readyToShowCallbacks.shift();
|
||||||
|
callback?.();
|
||||||
|
},
|
||||||
once: (_event: 'ready-to-show', cb: () => void) => {
|
once: (_event: 'ready-to-show', cb: () => void) => {
|
||||||
state.readyToShowCallbacks.push(cb);
|
state.readyToShowCallbacks.push(cb);
|
||||||
},
|
},
|
||||||
@@ -269,16 +279,13 @@ test('sendToActiveOverlayWindow waits for blank modal URL before sending open co
|
|||||||
|
|
||||||
assert.equal(sent, true);
|
assert.equal(sent, true);
|
||||||
assert.deepEqual(window.sent, []);
|
assert.deepEqual(window.sent, []);
|
||||||
|
|
||||||
assert.equal(window.loadCallbacks.length, 1);
|
|
||||||
assert.equal(window.readyToShowCallbacks.length, 1);
|
|
||||||
window.loading = false;
|
window.loading = false;
|
||||||
window.url = 'file:///overlay/index.html?layer=modal';
|
window.url = 'file:///overlay/index.html?layer=modal';
|
||||||
window.loadCallbacks[0]!();
|
window.emitDidFinishLoad();
|
||||||
assert.deepEqual(window.sent, []);
|
assert.deepEqual(window.sent, []);
|
||||||
|
|
||||||
window.contentReady = true;
|
window.contentReady = true;
|
||||||
window.readyToShowCallbacks[0]!();
|
window.emitReadyToShow();
|
||||||
|
|
||||||
runtime.notifyOverlayModalOpened('runtime-options');
|
runtime.notifyOverlayModalOpened('runtime-options');
|
||||||
assert.deepEqual(window.sent, [['runtime-options:open']]);
|
assert.deepEqual(window.sent, [['runtime-options:open']]);
|
||||||
@@ -549,6 +556,7 @@ test('handleOverlayModalClosed destroys modal window for single kiku modal', ()
|
|||||||
|
|
||||||
test('modal fallback reveal skips showing window when content is not ready', async () => {
|
test('modal fallback reveal skips showing window when content is not ready', async () => {
|
||||||
const window = createMockWindow();
|
const window = createMockWindow();
|
||||||
|
let scheduledReveal: (() => void) | null = null;
|
||||||
const runtime = createOverlayModalRuntimeService({
|
const runtime = createOverlayModalRuntimeService({
|
||||||
getMainWindow: () => null,
|
getMainWindow: () => null,
|
||||||
getModalWindow: () => window as never,
|
getModalWindow: () => window as never,
|
||||||
@@ -557,6 +565,14 @@ test('modal fallback reveal skips showing window when content is not ready', asy
|
|||||||
},
|
},
|
||||||
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
||||||
setModalWindowBounds: () => {},
|
setModalWindowBounds: () => {},
|
||||||
|
}, {
|
||||||
|
scheduleRevealFallback: (callback) => {
|
||||||
|
scheduledReveal = callback;
|
||||||
|
return { scheduled: true } as never;
|
||||||
|
},
|
||||||
|
clearRevealFallback: () => {
|
||||||
|
scheduledReveal = null;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
window.loading = true;
|
window.loading = true;
|
||||||
@@ -568,10 +584,11 @@ test('modal fallback reveal skips showing window when content is not ready', asy
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(sent, true);
|
assert.equal(sent, true);
|
||||||
|
if (scheduledReveal === null) {
|
||||||
await new Promise<void>((resolve) => {
|
throw new Error('expected reveal callback');
|
||||||
setTimeout(resolve, 260);
|
}
|
||||||
});
|
const runScheduledReveal: () => void = scheduledReveal;
|
||||||
|
runScheduledReveal();
|
||||||
|
|
||||||
assert.equal(window.getShowCount(), 0);
|
assert.equal(window.getShowCount(), 0);
|
||||||
|
|
||||||
@@ -599,14 +616,11 @@ test('sendToActiveOverlayWindow waits for modal ready-to-show before delivering
|
|||||||
|
|
||||||
assert.equal(sent, true);
|
assert.equal(sent, true);
|
||||||
assert.deepEqual(window.sent, []);
|
assert.deepEqual(window.sent, []);
|
||||||
assert.equal(window.loadCallbacks.length, 1);
|
window.emitDidFinishLoad();
|
||||||
assert.equal(window.readyToShowCallbacks.length, 1);
|
|
||||||
|
|
||||||
window.loadCallbacks[0]!();
|
|
||||||
assert.deepEqual(window.sent, []);
|
assert.deepEqual(window.sent, []);
|
||||||
|
|
||||||
window.contentReady = true;
|
window.contentReady = true;
|
||||||
window.readyToShowCallbacks[0]!();
|
window.emitReadyToShow();
|
||||||
assert.deepEqual(window.sent, [['runtime-options:open']]);
|
assert.deepEqual(window.sent, [['runtime-options:open']]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -617,8 +631,7 @@ test('modal reopen creates a fresh window after close destroys the previous one'
|
|||||||
|
|
||||||
const runtime = createOverlayModalRuntimeService({
|
const runtime = createOverlayModalRuntimeService({
|
||||||
getMainWindow: () => null,
|
getMainWindow: () => null,
|
||||||
getModalWindow: () =>
|
getModalWindow: () => currentModal as never,
|
||||||
currentModal && !currentModal.isDestroyed() ? (currentModal as never) : null,
|
|
||||||
createModalWindow: () => {
|
createModalWindow: () => {
|
||||||
currentModal = secondWindow;
|
currentModal = secondWindow;
|
||||||
return secondWindow as never;
|
return secondWindow as never;
|
||||||
@@ -653,8 +666,7 @@ test('modal reopen after close-destroy notifies state change on fresh window lif
|
|||||||
const runtime = createOverlayModalRuntimeService(
|
const runtime = createOverlayModalRuntimeService(
|
||||||
{
|
{
|
||||||
getMainWindow: () => null,
|
getMainWindow: () => null,
|
||||||
getModalWindow: () =>
|
getModalWindow: () => currentModal as never,
|
||||||
currentModal && !currentModal.isDestroyed() ? (currentModal as never) : null,
|
|
||||||
createModalWindow: () => {
|
createModalWindow: () => {
|
||||||
currentModal = secondWindow;
|
currentModal = secondWindow;
|
||||||
return secondWindow as never;
|
return secondWindow as never;
|
||||||
|
|||||||
@@ -50,8 +50,15 @@ export interface OverlayModalRuntime {
|
|||||||
getRestoreVisibleOverlayOnModalClose: () => Set<OverlayHostedModal>;
|
getRestoreVisibleOverlayOnModalClose: () => Set<OverlayHostedModal>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RevealFallbackHandle = NonNullable<Parameters<typeof globalThis.clearTimeout>[0]>;
|
||||||
|
|
||||||
export interface OverlayModalRuntimeOptions {
|
export interface OverlayModalRuntimeOptions {
|
||||||
onModalStateChange?: (isActive: boolean) => void;
|
onModalStateChange?: (isActive: boolean) => void;
|
||||||
|
scheduleRevealFallback?: (
|
||||||
|
callback: () => void,
|
||||||
|
delayMs: number,
|
||||||
|
) => RevealFallbackHandle;
|
||||||
|
clearRevealFallback?: (timeout: RevealFallbackHandle) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createOverlayModalRuntimeService(
|
export function createOverlayModalRuntimeService(
|
||||||
@@ -65,7 +72,14 @@ export function createOverlayModalRuntimeService(
|
|||||||
let mainWindowHiddenByModal = false;
|
let mainWindowHiddenByModal = false;
|
||||||
let modalWindowPrimedForImmediateShow = false;
|
let modalWindowPrimedForImmediateShow = false;
|
||||||
let pendingModalWindowReveal: BrowserWindow | null = null;
|
let pendingModalWindowReveal: BrowserWindow | null = null;
|
||||||
let pendingModalWindowRevealTimeout: ReturnType<typeof setTimeout> | null = null;
|
let pendingModalWindowRevealTimeout: RevealFallbackHandle | null = null;
|
||||||
|
const scheduleRevealFallback = (
|
||||||
|
callback: () => void,
|
||||||
|
delayMs: number,
|
||||||
|
): RevealFallbackHandle =>
|
||||||
|
(options.scheduleRevealFallback ?? globalThis.setTimeout)(callback, delayMs);
|
||||||
|
const clearRevealFallback = (timeout: RevealFallbackHandle): void =>
|
||||||
|
(options.clearRevealFallback ?? globalThis.clearTimeout)(timeout);
|
||||||
|
|
||||||
const notifyModalStateChange = (nextState: boolean): void => {
|
const notifyModalStateChange = (nextState: boolean): void => {
|
||||||
if (modalActive === nextState) return;
|
if (modalActive === nextState) return;
|
||||||
@@ -207,7 +221,7 @@ export function createOverlayModalRuntimeService(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
clearTimeout(pendingModalWindowRevealTimeout);
|
clearRevealFallback(pendingModalWindowRevealTimeout);
|
||||||
pendingModalWindowRevealTimeout = null;
|
pendingModalWindowRevealTimeout = null;
|
||||||
pendingModalWindowReveal = null;
|
pendingModalWindowReveal = null;
|
||||||
};
|
};
|
||||||
@@ -266,7 +280,7 @@ export function createOverlayModalRuntimeService(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingModalWindowRevealTimeout = setTimeout(() => {
|
pendingModalWindowRevealTimeout = scheduleRevealFallback(() => {
|
||||||
const targetWindow = pendingModalWindowReveal;
|
const targetWindow = pendingModalWindowReveal;
|
||||||
clearPendingModalWindowReveal();
|
clearPendingModalWindowReveal();
|
||||||
if (!targetWindow || targetWindow.isDestroyed() || targetWindow.isVisible()) {
|
if (!targetWindow || targetWindow.isDestroyed() || targetWindow.isVisible()) {
|
||||||
|
|||||||
@@ -11,10 +11,14 @@ test('createConfigHotReloadAppliedHandler runs all hot-reload effects', () => {
|
|||||||
const config = deepCloneConfig(DEFAULT_CONFIG);
|
const config = deepCloneConfig(DEFAULT_CONFIG);
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
const ankiPatches: Array<{ enabled: boolean }> = [];
|
const ankiPatches: Array<{ enabled: boolean }> = [];
|
||||||
|
const sessionBindingWarnings: string[][] = [];
|
||||||
|
|
||||||
const applyHotReload = createConfigHotReloadAppliedHandler({
|
const applyHotReload = createConfigHotReloadAppliedHandler({
|
||||||
setKeybindings: () => calls.push('set:keybindings'),
|
setKeybindings: () => calls.push('set:keybindings'),
|
||||||
setSessionBindings: () => calls.push('set:session-bindings'),
|
setSessionBindings: (_sessionBindings, warnings) => {
|
||||||
|
calls.push('set:session-bindings');
|
||||||
|
sessionBindingWarnings.push(warnings.map((warning) => warning.message));
|
||||||
|
},
|
||||||
refreshGlobalAndOverlayShortcuts: () => calls.push('refresh:shortcuts'),
|
refreshGlobalAndOverlayShortcuts: () => calls.push('refresh:shortcuts'),
|
||||||
setSecondarySubMode: (mode) => calls.push(`set:secondary:${mode}`),
|
setSecondarySubMode: (mode) => calls.push(`set:secondary:${mode}`),
|
||||||
broadcastToOverlayWindows: (channel, payload) =>
|
broadcastToOverlayWindows: (channel, payload) =>
|
||||||
@@ -44,6 +48,12 @@ test('createConfigHotReloadAppliedHandler runs all hot-reload effects', () => {
|
|||||||
assert.ok(calls.some((entry) => entry.startsWith('broadcast:secondary-subtitle:mode:')));
|
assert.ok(calls.some((entry) => entry.startsWith('broadcast:secondary-subtitle:mode:')));
|
||||||
assert.ok(calls.includes('broadcast:config:hot-reload:object'));
|
assert.ok(calls.includes('broadcast:config:hot-reload:object'));
|
||||||
assert.deepEqual(ankiPatches, [{ enabled: config.ankiConnect.ai.enabled }]);
|
assert.deepEqual(ankiPatches, [{ enabled: config.ankiConnect.ai.enabled }]);
|
||||||
|
assert.equal(sessionBindingWarnings.length, 1);
|
||||||
|
assert.ok(
|
||||||
|
sessionBindingWarnings[0]?.some((message) =>
|
||||||
|
message.includes('Rename shortcuts.toggleVisibleOverlayGlobal'),
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('createConfigHotReloadAppliedHandler skips optional effects when no hot fields', () => {
|
test('createConfigHotReloadAppliedHandler skips optional effects when no hot fields', () => {
|
||||||
@@ -70,6 +80,34 @@ test('createConfigHotReloadAppliedHandler skips optional effects when no hot fie
|
|||||||
assert.deepEqual(calls, ['set:keybindings', 'set:session-bindings']);
|
assert.deepEqual(calls, ['set:keybindings', 'set:session-bindings']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('createConfigHotReloadAppliedHandler forwards compiled session-binding warnings', () => {
|
||||||
|
const config = deepCloneConfig(DEFAULT_CONFIG);
|
||||||
|
config.shortcuts.openSessionHelp = 'Ctrl+?';
|
||||||
|
const warnings: string[][] = [];
|
||||||
|
|
||||||
|
const applyHotReload = createConfigHotReloadAppliedHandler({
|
||||||
|
setKeybindings: () => {},
|
||||||
|
setSessionBindings: (_sessionBindings, sessionBindingWarnings) => {
|
||||||
|
warnings.push(sessionBindingWarnings.map((warning) => warning.message));
|
||||||
|
},
|
||||||
|
refreshGlobalAndOverlayShortcuts: () => {},
|
||||||
|
setSecondarySubMode: () => {},
|
||||||
|
broadcastToOverlayWindows: () => {},
|
||||||
|
applyAnkiRuntimeConfigPatch: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
applyHotReload(
|
||||||
|
{
|
||||||
|
hotReloadFields: ['shortcuts'],
|
||||||
|
restartRequiredFields: [],
|
||||||
|
},
|
||||||
|
config,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(warnings.length, 1);
|
||||||
|
assert.ok(warnings[0]?.some((message) => message.includes('Unsupported accelerator key token')));
|
||||||
|
});
|
||||||
|
|
||||||
test('createConfigHotReloadMessageHandler mirrors message to OSD and desktop notification', () => {
|
test('createConfigHotReloadMessageHandler mirrors message to OSD and desktop notification', () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
const handleMessage = createConfigHotReloadMessageHandler({
|
const handleMessage = createConfigHotReloadMessageHandler({
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import type { ConfigHotReloadPayload, ResolvedConfig, SecondarySubMode } from '.
|
|||||||
|
|
||||||
type ConfigHotReloadAppliedDeps = {
|
type ConfigHotReloadAppliedDeps = {
|
||||||
setKeybindings: (keybindings: ConfigHotReloadPayload['keybindings']) => void;
|
setKeybindings: (keybindings: ConfigHotReloadPayload['keybindings']) => void;
|
||||||
setSessionBindings: (sessionBindings: ConfigHotReloadPayload['sessionBindings']) => void;
|
setSessionBindings: (
|
||||||
|
sessionBindings: ConfigHotReloadPayload['sessionBindings'],
|
||||||
|
sessionBindingWarnings: ConfigHotReloadPayload['sessionBindingWarnings'],
|
||||||
|
) => void;
|
||||||
refreshGlobalAndOverlayShortcuts: () => void;
|
refreshGlobalAndOverlayShortcuts: () => void;
|
||||||
setSecondarySubMode: (mode: SecondarySubMode) => void;
|
setSecondarySubMode: (mode: SecondarySubMode) => void;
|
||||||
broadcastToOverlayWindows: (channel: string, payload: unknown) => void;
|
broadcastToOverlayWindows: (channel: string, payload: unknown) => void;
|
||||||
@@ -37,7 +40,7 @@ export function resolveSubtitleStyleForRenderer(config: ResolvedConfig) {
|
|||||||
|
|
||||||
export function buildConfigHotReloadPayload(config: ResolvedConfig): ConfigHotReloadPayload {
|
export function buildConfigHotReloadPayload(config: ResolvedConfig): ConfigHotReloadPayload {
|
||||||
const keybindings = resolveKeybindings(config, DEFAULT_KEYBINDINGS);
|
const keybindings = resolveKeybindings(config, DEFAULT_KEYBINDINGS);
|
||||||
const { bindings: sessionBindings } = compileSessionBindings({
|
const { bindings: sessionBindings, warnings: sessionBindingWarnings } = compileSessionBindings({
|
||||||
keybindings,
|
keybindings,
|
||||||
shortcuts: resolveConfiguredShortcuts(config, DEFAULT_CONFIG),
|
shortcuts: resolveConfiguredShortcuts(config, DEFAULT_CONFIG),
|
||||||
platform:
|
platform:
|
||||||
@@ -51,6 +54,7 @@ export function buildConfigHotReloadPayload(config: ResolvedConfig): ConfigHotRe
|
|||||||
return {
|
return {
|
||||||
keybindings,
|
keybindings,
|
||||||
sessionBindings,
|
sessionBindings,
|
||||||
|
sessionBindingWarnings,
|
||||||
subtitleStyle: resolveSubtitleStyleForRenderer(config),
|
subtitleStyle: resolveSubtitleStyleForRenderer(config),
|
||||||
subtitleSidebar: config.subtitleSidebar,
|
subtitleSidebar: config.subtitleSidebar,
|
||||||
secondarySubMode: config.secondarySub.defaultMode,
|
secondarySubMode: config.secondarySub.defaultMode,
|
||||||
@@ -61,7 +65,7 @@ export function createConfigHotReloadAppliedHandler(deps: ConfigHotReloadApplied
|
|||||||
return (diff: ConfigHotReloadDiff, config: ResolvedConfig): void => {
|
return (diff: ConfigHotReloadDiff, config: ResolvedConfig): void => {
|
||||||
const payload = buildConfigHotReloadPayload(config);
|
const payload = buildConfigHotReloadPayload(config);
|
||||||
deps.setKeybindings(payload.keybindings);
|
deps.setKeybindings(payload.keybindings);
|
||||||
deps.setSessionBindings(payload.sessionBindings);
|
deps.setSessionBindings(payload.sessionBindings, payload.sessionBindingWarnings);
|
||||||
|
|
||||||
if (diff.hotReloadFields.includes('shortcuts')) {
|
if (diff.hotReloadFields.includes('shortcuts')) {
|
||||||
deps.refreshGlobalAndOverlayShortcuts();
|
deps.refreshGlobalAndOverlayShortcuts();
|
||||||
|
|||||||
@@ -86,9 +86,13 @@ test('config hot reload message main deps builder maps notifications', () => {
|
|||||||
|
|
||||||
test('config hot reload applied main deps builder maps callbacks', () => {
|
test('config hot reload applied main deps builder maps callbacks', () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
|
const warningCounts: number[] = [];
|
||||||
const buildDeps = createBuildConfigHotReloadAppliedMainDepsHandler({
|
const buildDeps = createBuildConfigHotReloadAppliedMainDepsHandler({
|
||||||
setKeybindings: () => calls.push('keybindings'),
|
setKeybindings: () => calls.push('keybindings'),
|
||||||
setSessionBindings: () => calls.push('session-bindings'),
|
setSessionBindings: (_sessionBindings, warnings) => {
|
||||||
|
calls.push('session-bindings');
|
||||||
|
warningCounts.push(warnings.length);
|
||||||
|
},
|
||||||
refreshGlobalAndOverlayShortcuts: () => calls.push('refresh-shortcuts'),
|
refreshGlobalAndOverlayShortcuts: () => calls.push('refresh-shortcuts'),
|
||||||
setSecondarySubMode: () => calls.push('set-secondary'),
|
setSecondarySubMode: () => calls.push('set-secondary'),
|
||||||
broadcastToOverlayWindows: (channel) => calls.push(`broadcast:${channel}`),
|
broadcastToOverlayWindows: (channel) => calls.push(`broadcast:${channel}`),
|
||||||
@@ -97,7 +101,7 @@ test('config hot reload applied main deps builder maps callbacks', () => {
|
|||||||
|
|
||||||
const deps = buildDeps();
|
const deps = buildDeps();
|
||||||
deps.setKeybindings([]);
|
deps.setKeybindings([]);
|
||||||
deps.setSessionBindings([]);
|
deps.setSessionBindings([], []);
|
||||||
deps.refreshGlobalAndOverlayShortcuts();
|
deps.refreshGlobalAndOverlayShortcuts();
|
||||||
deps.setSecondarySubMode('hover');
|
deps.setSecondarySubMode('hover');
|
||||||
deps.broadcastToOverlayWindows('config:hot-reload', {});
|
deps.broadcastToOverlayWindows('config:hot-reload', {});
|
||||||
@@ -110,6 +114,7 @@ test('config hot reload applied main deps builder maps callbacks', () => {
|
|||||||
'broadcast:config:hot-reload',
|
'broadcast:config:hot-reload',
|
||||||
'apply-anki',
|
'apply-anki',
|
||||||
]);
|
]);
|
||||||
|
assert.deepEqual(warningCounts, [0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('config hot reload runtime main deps builder maps runtime callbacks', () => {
|
test('config hot reload runtime main deps builder maps runtime callbacks', () => {
|
||||||
|
|||||||
@@ -62,7 +62,10 @@ export function createBuildConfigHotReloadMessageMainDepsHandler(
|
|||||||
|
|
||||||
export function createBuildConfigHotReloadAppliedMainDepsHandler(deps: {
|
export function createBuildConfigHotReloadAppliedMainDepsHandler(deps: {
|
||||||
setKeybindings: (keybindings: ConfigHotReloadPayload['keybindings']) => void;
|
setKeybindings: (keybindings: ConfigHotReloadPayload['keybindings']) => void;
|
||||||
setSessionBindings: (sessionBindings: ConfigHotReloadPayload['sessionBindings']) => void;
|
setSessionBindings: (
|
||||||
|
sessionBindings: ConfigHotReloadPayload['sessionBindings'],
|
||||||
|
sessionBindingWarnings: ConfigHotReloadPayload['sessionBindingWarnings'],
|
||||||
|
) => void;
|
||||||
refreshGlobalAndOverlayShortcuts: () => void;
|
refreshGlobalAndOverlayShortcuts: () => void;
|
||||||
setSecondarySubMode: (mode: SecondarySubMode) => void;
|
setSecondarySubMode: (mode: SecondarySubMode) => void;
|
||||||
broadcastToOverlayWindows: (channel: string, payload: unknown) => void;
|
broadcastToOverlayWindows: (channel: string, payload: unknown) => void;
|
||||||
@@ -73,8 +76,10 @@ export function createBuildConfigHotReloadAppliedMainDepsHandler(deps: {
|
|||||||
return () => ({
|
return () => ({
|
||||||
setKeybindings: (keybindings: ConfigHotReloadPayload['keybindings']) =>
|
setKeybindings: (keybindings: ConfigHotReloadPayload['keybindings']) =>
|
||||||
deps.setKeybindings(keybindings),
|
deps.setKeybindings(keybindings),
|
||||||
setSessionBindings: (sessionBindings: ConfigHotReloadPayload['sessionBindings']) =>
|
setSessionBindings: (
|
||||||
deps.setSessionBindings(sessionBindings),
|
sessionBindings: ConfigHotReloadPayload['sessionBindings'],
|
||||||
|
sessionBindingWarnings: ConfigHotReloadPayload['sessionBindingWarnings'],
|
||||||
|
) => deps.setSessionBindings(sessionBindings, sessionBindingWarnings),
|
||||||
refreshGlobalAndOverlayShortcuts: () => deps.refreshGlobalAndOverlayShortcuts(),
|
refreshGlobalAndOverlayShortcuts: () => deps.refreshGlobalAndOverlayShortcuts(),
|
||||||
setSecondarySubMode: (mode: SecondarySubMode) => deps.setSecondarySubMode(mode),
|
setSecondarySubMode: (mode: SecondarySubMode) => deps.setSecondarySubMode(mode),
|
||||||
broadcastToOverlayWindows: (channel: string, payload: unknown) =>
|
broadcastToOverlayWindows: (channel: string, payload: unknown) =>
|
||||||
|
|||||||
@@ -113,3 +113,12 @@ test('applyStartupState preserves cleared startup-only runtime flags', () => {
|
|||||||
|
|
||||||
assert.equal(appState.initialArgs?.settings, true);
|
assert.equal(appState.initialArgs?.settings, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('createAppState starts with session bindings marked uninitialized', () => {
|
||||||
|
const appState = createAppState({
|
||||||
|
mpvSocketPath: '/tmp/mpv.sock',
|
||||||
|
texthookerPort: 4000,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(appState.sessionBindingsInitialized, false);
|
||||||
|
});
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ export interface AppState {
|
|||||||
mecabTokenizer: MecabTokenizer | null;
|
mecabTokenizer: MecabTokenizer | null;
|
||||||
keybindings: Keybinding[];
|
keybindings: Keybinding[];
|
||||||
sessionBindings: CompiledSessionBinding[];
|
sessionBindings: CompiledSessionBinding[];
|
||||||
|
sessionBindingsInitialized: boolean;
|
||||||
subtitleTimingTracker: SubtitleTimingTracker | null;
|
subtitleTimingTracker: SubtitleTimingTracker | null;
|
||||||
immersionTracker: ImmersionTrackerService | null;
|
immersionTracker: ImmersionTrackerService | null;
|
||||||
ankiIntegration: AnkiIntegration | null;
|
ankiIntegration: AnkiIntegration | null;
|
||||||
@@ -255,6 +256,7 @@ export function createAppState(values: AppStateInitialValues): AppState {
|
|||||||
mecabTokenizer: null,
|
mecabTokenizer: null,
|
||||||
keybindings: [],
|
keybindings: [],
|
||||||
sessionBindings: [],
|
sessionBindings: [],
|
||||||
|
sessionBindingsInitialized: false,
|
||||||
subtitleTimingTracker: null,
|
subtitleTimingTracker: null,
|
||||||
immersionTracker: null,
|
immersionTracker: null,
|
||||||
ankiIntegration: null,
|
ankiIntegration: null,
|
||||||
|
|||||||
@@ -944,6 +944,29 @@ test('keyboard mode: configured stats toggle works even while popup is open', as
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('refreshConfiguredShortcuts updates refreshed stats and mark-watched keys', async () => {
|
||||||
|
const { handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await handlers.setupMpvInputForwarding();
|
||||||
|
|
||||||
|
testGlobals.setStatsToggleKey('KeyG');
|
||||||
|
testGlobals.setMarkWatchedKey('KeyM');
|
||||||
|
await handlers.refreshConfiguredShortcuts();
|
||||||
|
|
||||||
|
const beforeMarkWatchedCalls = testGlobals.markActiveVideoWatchedCalls();
|
||||||
|
|
||||||
|
testGlobals.dispatchKeydown({ key: 'g', code: 'KeyG' });
|
||||||
|
testGlobals.dispatchKeydown({ key: 'm', code: 'KeyM' });
|
||||||
|
await wait(10);
|
||||||
|
|
||||||
|
assert.equal(testGlobals.statsToggleOverlayCalls(), 1);
|
||||||
|
assert.equal(testGlobals.markActiveVideoWatchedCalls(), beforeMarkWatchedCalls + 1);
|
||||||
|
} finally {
|
||||||
|
testGlobals.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('youtube picker: unhandled keys still dispatch mpv keybindings', async () => {
|
test('youtube picker: unhandled keys still dispatch mpv keybindings', async () => {
|
||||||
const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness();
|
const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||||
|
|
||||||
|
|||||||
@@ -80,12 +80,27 @@ export function createKeyboardHandlers(
|
|||||||
return parts.join('+');
|
return parts.join('+');
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateConfiguredShortcuts(shortcuts: Required<ShortcutsConfig>): void {
|
function updateConfiguredShortcuts(
|
||||||
|
shortcuts: Required<ShortcutsConfig>,
|
||||||
|
statsToggleKey?: string,
|
||||||
|
markWatchedKey?: string,
|
||||||
|
): void {
|
||||||
ctx.state.sessionActionTimeoutMs = shortcuts.multiCopyTimeoutMs;
|
ctx.state.sessionActionTimeoutMs = shortcuts.multiCopyTimeoutMs;
|
||||||
|
if (typeof statsToggleKey === 'string' && statsToggleKey.length > 0) {
|
||||||
|
ctx.state.statsToggleKey = statsToggleKey;
|
||||||
|
}
|
||||||
|
if (typeof markWatchedKey === 'string' && markWatchedKey.length > 0) {
|
||||||
|
ctx.state.markWatchedKey = markWatchedKey;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshConfiguredShortcuts(): Promise<void> {
|
async function refreshConfiguredShortcuts(): Promise<void> {
|
||||||
updateConfiguredShortcuts(await window.electronAPI.getConfiguredShortcuts());
|
const [shortcuts, statsToggleKey, markWatchedKey] = await Promise.all([
|
||||||
|
window.electronAPI.getConfiguredShortcuts(),
|
||||||
|
window.electronAPI.getStatsToggleKey(),
|
||||||
|
window.electronAPI.getMarkWatchedKey(),
|
||||||
|
]);
|
||||||
|
updateConfiguredShortcuts(shortcuts, statsToggleKey, markWatchedKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSessionBindings(bindings: CompiledSessionBinding[]): void {
|
function updateSessionBindings(bindings: CompiledSessionBinding[]): void {
|
||||||
@@ -912,9 +927,7 @@ export function createKeyboardHandlers(
|
|||||||
window.electronAPI.getMarkWatchedKey(),
|
window.electronAPI.getMarkWatchedKey(),
|
||||||
]);
|
]);
|
||||||
updateSessionBindings(sessionBindings);
|
updateSessionBindings(sessionBindings);
|
||||||
updateConfiguredShortcuts(shortcuts);
|
updateConfiguredShortcuts(shortcuts, statsToggleKey, markWatchedKey);
|
||||||
ctx.state.statsToggleKey = statsToggleKey;
|
|
||||||
ctx.state.markWatchedKey = markWatchedKey;
|
|
||||||
syncKeyboardTokenSelection();
|
syncKeyboardTokenSelection();
|
||||||
|
|
||||||
const subtitleMutationObserver = new MutationObserver(() => {
|
const subtitleMutationObserver = new MutationObserver(() => {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
CompiledSessionBinding,
|
CompiledSessionBinding,
|
||||||
SessionActionId,
|
SessionActionId,
|
||||||
SessionActionPayload,
|
SessionActionPayload,
|
||||||
|
SessionBindingWarning,
|
||||||
} from './session-bindings';
|
} from './session-bindings';
|
||||||
import type {
|
import type {
|
||||||
JimakuApiResponse,
|
JimakuApiResponse,
|
||||||
@@ -327,6 +328,7 @@ export interface ClipboardAppendResult {
|
|||||||
export interface ConfigHotReloadPayload {
|
export interface ConfigHotReloadPayload {
|
||||||
keybindings: Keybinding[];
|
keybindings: Keybinding[];
|
||||||
sessionBindings: CompiledSessionBinding[];
|
sessionBindings: CompiledSessionBinding[];
|
||||||
|
sessionBindingWarnings: SessionBindingWarning[];
|
||||||
subtitleStyle: SubtitleStyleConfig | null;
|
subtitleStyle: SubtitleStyleConfig | null;
|
||||||
subtitleSidebar: Required<SubtitleSidebarConfig>;
|
subtitleSidebar: Required<SubtitleSidebarConfig>;
|
||||||
secondarySubMode: SecondarySubMode;
|
secondarySubMode: SecondarySubMode;
|
||||||
|
|||||||
@@ -79,11 +79,11 @@ export abstract class BaseWindowTracker {
|
|||||||
this.updateTargetWindowFocused(focused);
|
this.updateTargetWindowFocused(focused);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updateGeometry(newGeometry: WindowGeometry | null): void {
|
protected updateGeometry(newGeometry: WindowGeometry | null, initialFocused = true): void {
|
||||||
if (newGeometry) {
|
if (newGeometry) {
|
||||||
if (!this.windowFound) {
|
if (!this.windowFound) {
|
||||||
this.windowFound = true;
|
this.windowFound = true;
|
||||||
this.updateTargetWindowFocused(true);
|
this.updateTargetWindowFocused(initialFocused);
|
||||||
if (this.onWindowFound) this.onWindowFound(newGeometry);
|
if (this.onWindowFound) this.onWindowFound(newGeometry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,22 @@ test('WindowsWindowTracker updates geometry from poll output', () => {
|
|||||||
assert.equal(tracker.isTargetWindowFocused(), true);
|
assert.equal(tracker.isTargetWindowFocused(), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('WindowsWindowTracker preserves an unfocused initial match', () => {
|
||||||
|
const tracker = new WindowsWindowTracker(undefined, {
|
||||||
|
pollMpvWindows: () => mpvVisible({ x: 10, y: 20, width: 1280, height: 720, focused: false }),
|
||||||
|
});
|
||||||
|
|
||||||
|
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
|
||||||
|
|
||||||
|
assert.deepEqual(tracker.getGeometry(), {
|
||||||
|
x: 10,
|
||||||
|
y: 20,
|
||||||
|
width: 1280,
|
||||||
|
height: 720,
|
||||||
|
});
|
||||||
|
assert.equal(tracker.isTargetWindowFocused(), false);
|
||||||
|
});
|
||||||
|
|
||||||
test('WindowsWindowTracker clears geometry for poll misses', () => {
|
test('WindowsWindowTracker clears geometry for poll misses', () => {
|
||||||
const tracker = new WindowsWindowTracker(undefined, {
|
const tracker = new WindowsWindowTracker(undefined, {
|
||||||
pollMpvWindows: () => mpvNotFound,
|
pollMpvWindows: () => mpvNotFound,
|
||||||
|
|||||||
@@ -154,8 +154,8 @@ export class WindowsWindowTracker extends BaseWindowTracker {
|
|||||||
this.resetTrackingLossState();
|
this.resetTrackingLossState();
|
||||||
this.targetWindowMinimized = false;
|
this.targetWindowMinimized = false;
|
||||||
this.currentTargetWindowHwnd = best.hwnd;
|
this.currentTargetWindowHwnd = best.hwnd;
|
||||||
|
this.updateGeometry(best.geometry, best.focused);
|
||||||
this.updateTargetWindowFocused(best.focused);
|
this.updateTargetWindowFocused(best.focused);
|
||||||
this.updateGeometry(best.geometry);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user