fix(plugin): keep loading OSD off on warm playlist advance (#317)

* fix(plugin): keep loading OSD off on warm playlist advance

- Track overlay_content_ready_seen when the app sends subminer-overlay-loading-ready
- Skip restarting the "Overlay loading" spinner for an overlay that already reported ready, so it no longer times out into "Overlay did not become ready" (#315)
- Reset the flag on stop, restart, start failure and shutdown
- Add start-gate tests for warm and not-yet-ready playlist advances

* fix(plugin): only report overlay ready once content has loaded

Hide paths dismissed the loading OSD with subminer-overlay-loading-ready,
which the plugin now remembers to skip the spinner on later playlist files.
A hide before the overlay had content would wrongly mark it ready. The app
now sends ready only when the overlay window content is ready, and a new
stop-only subminer-overlay-loading-dismissed message otherwise.

* fix(plugin): resync overlay readiness on dismissal and mpv connect

A dismissal now clears the plugin's readiness flag, since the app sends it
only while its overlay window has no content (e.g. a replaced window).
On mpv (re)connect the app resends subminer-overlay-loading-ready when the
overlay already has content, so a ready signal lost while mpv was away, or
sent before this mpv's plugin existed, still reaches the plugin.

* refactor(main): pass overlay readiness handler directly
This commit is contained in:
sudacode authored and GitHub committed 2026-11-02 21:47:32 -08:00
1 parent b99409bbcd
commit 848a6c4021
13 files changed
+309 -9

No files matched your search

@@ -0,0 +1,60 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import type { BrowserWindow } from 'electron';
import { OVERLAY_WINDOW_CONTENT_READY_FLAG } from '../../core/services/overlay-window-flags';
import type { MpvIpcClient } from '../../core/services';
import type { ResolvedConfig } from '../../types';
import { createOverlayNotificationsRuntime } from './overlay-notifications-runtime';
function createRuntimeHarness(contentReady: boolean) {
const sent: unknown[][] = [];
const overlayWindow = {
isDestroyed: () => false,
[OVERLAY_WINDOW_CONTENT_READY_FLAG]: contentReady,
} as unknown as BrowserWindow;
const mpvClient = {
send: ({ command }: { command: unknown[] }) => {
sent.push(command);
},
} as unknown as MpvIpcClient;
const runtime = createOverlayNotificationsRuntime({
getResolvedConfig: () => ({}) as ResolvedConfig,
getMainOverlayWindow: () => overlayWindow,
getVisibleOverlayVisible: () => false,
broadcastToOverlayWindows: () => {},
showMpvOsd: () => {},
getMpvClient: () => mpvClient,
getAnkiIntegration: () => null,
getRuntimeOptionsManager: () => null,
});
const scriptMessages = () =>
sent.filter((command) => command[0] === 'script-message').map((command) => command[1]);
return { runtime, scriptMessages };
}
test('dismissing the loading OSD before overlay content is ready only stops the plugin spinner', () => {
const { runtime, scriptMessages } = createRuntimeHarness(false);
runtime.dismissOverlayLoadingStatusNotification();
assert.deepEqual(scriptMessages(), ['subminer-overlay-loading-dismissed']);
});
test('dismissing the loading OSD once overlay content is ready reports readiness to the plugin', () => {
const { runtime, scriptMessages } = createRuntimeHarness(true);
runtime.dismissOverlayLoadingStatusNotification();
assert.deepEqual(scriptMessages(), ['subminer-overlay-loading-ready']);
});
test('reporting overlay readiness after an mpv connect resends ready only for loaded content', () => {
const ready = createRuntimeHarness(true);
ready.runtime.reportOverlayContentReadyToPlugin();
assert.deepEqual(ready.scriptMessages(), ['subminer-overlay-loading-ready']);
const loading = createRuntimeHarness(false);
loading.runtime.reportOverlayContentReadyToPlugin();
assert.deepEqual(loading.scriptMessages(), []);
});
@@ -61,6 +61,7 @@ export function createOverlayNotificationsRuntime(deps: OverlayNotificationsRunt
showYoutubeFlowStatusNotification: (message: string) => void;
showOverlayLoadingStatusNotification: () => void;
dismissOverlayLoadingStatusNotification: () => void;
reportOverlayContentReadyToPlugin: () => void;
maybeStartOverlayLoadingOsd: (mediaPath?: string | null) => void;
} {
function isVisibleOverlayContentReady(): boolean {
@@ -229,15 +230,39 @@ export function createOverlayNotificationsRuntime(deps: OverlayNotificationsRunt
getOverlayLoadingOsdController().start();
}
function isMainOverlayContentReady(): boolean {
const overlayWindow = deps.getMainOverlayWindow();
return Boolean(overlayWindow && isOverlayWindowContentReady(overlayWindow));
}
function sendPluginOverlayLoadingMessage(
message: 'subminer-overlay-loading-ready' | 'subminer-overlay-loading-dismissed',
): void {
sendMpvCommandRuntime(deps.getMpvClient(), ['script-message', message]);
}
// Hide paths also dismiss, often before the overlay has content. Only a content-ready window
// reports `subminer-overlay-loading-ready`: the plugin remembers it and skips the spinner on
// later playlist files, so other dismissals send the stop-only message, which also makes the
// plugin forget readiness from a replaced window.
function dismissOverlayLoadingStatusNotification(): void {
getOverlayLoadingOsdController().stop();
sendMpvCommandRuntime(deps.getMpvClient(), [
'script-message',
'subminer-overlay-loading-ready',
]);
sendPluginOverlayLoadingMessage(
isMainOverlayContentReady()
? 'subminer-overlay-loading-ready'
: 'subminer-overlay-loading-dismissed',
);
dismissOverlayNotification('overlay-loading-status');
}
// Called when mpv (re)connects: a ready message sent while mpv was away, or before this mpv's
// plugin existed, was lost. Never sends the dismissal, since nothing is being hidden.
function reportOverlayContentReadyToPlugin(): void {
if (isMainOverlayContentReady()) {
sendPluginOverlayLoadingMessage('subminer-overlay-loading-ready');
}
}
const maybeStartOverlayLoadingOsd = createMaybeStartOverlayLoadingOsdHandler({
getVisibleOverlayRequested: () => deps.getVisibleOverlayVisible(),
isOverlayContentReady: () => isVisibleOverlayContentReady(),
@@ -261,6 +286,7 @@ export function createOverlayNotificationsRuntime(deps: OverlayNotificationsRunt
showYoutubeFlowStatusNotification,
showOverlayLoadingStatusNotification,
dismissOverlayLoadingStatusNotification,
reportOverlayContentReadyToPlugin,
maybeStartOverlayLoadingOsd,
};
}