Files
SubMiner/src/core/services/stats-window-runtime.ts
T
sudacode 81b941fe8c fix(jellyfin): fix overlay toggle sync, redirect reload, and AppImage bi
- Sync visible-overlay state back to plugin via script messages to avoid toggle/hide drift
- Collapse duplicate toggle events within 250ms to prevent hide-then-show on single keypress
- Preserve manual hide across Jellyfin path-changing redirects even when media-title drops
- Rearm managed subtitle defaults on path-changing redirects
- Route toggleVisibleOverlay session binding through plugin toggle instead of app-side IPC
- Show Linux/Hyprland overlay passively (showInactive) to avoid stealing mpv keyboard focus
- Fix AppImage binary resolution to prefer $APPIMAGE env over mounted inner binary
- Add stats window layer management so delete/update dialogs appear above stats window
- Fix Jellyfin remote progress sync during Linux websocket reconnect windows
2026-05-23 01:45:09 -07:00

202 lines
5.6 KiB
TypeScript

import type {
BrowserWindow,
BrowserWindowConstructorOptions,
MessageBoxSyncOptions,
} from 'electron';
import type { WindowGeometry } from '../../types';
const DEFAULT_STATS_WINDOW_WIDTH = 900;
const DEFAULT_STATS_WINDOW_HEIGHT = 700;
export const STATS_WINDOW_TITLE = 'SubMiner Stats';
type StatsWindowLevelController = Pick<BrowserWindow, 'setAlwaysOnTop' | 'moveTop'> &
Partial<Pick<BrowserWindow, 'setVisibleOnAllWorkspaces' | 'setFullScreenable'>>;
type VisibleStatsWindowLevelController = StatsWindowLevelController &
Pick<BrowserWindow, 'isDestroyed' | 'isVisible'>;
type VisibleStatsWindowDialogLayerController = Pick<
BrowserWindow,
'isDestroyed' | 'isVisible' | 'setAlwaysOnTop'
>;
type StatsNativeConfirmDialogWindow = Pick<BrowserWindow, 'isDestroyed'>;
type StatsNativeConfirmDialogPresenter<WindowT> = {
showWithParent: (window: WindowT, options: MessageBoxSyncOptions) => number;
showWithoutParent: (options: MessageBoxSyncOptions) => number;
};
type StatsWindowBoundsController = Pick<BrowserWindow, 'getBounds' | 'getContentBounds'>;
type StatsWindowPresentationController = Pick<BrowserWindow, 'show' | 'focus'> &
Partial<Pick<BrowserWindow, 'showInactive'>>;
function isBareToggleKeyInput(input: Electron.Input, toggleKey: string): boolean {
return (
input.type === 'keyDown' &&
input.code === toggleKey &&
!input.control &&
!input.alt &&
!input.meta &&
!input.shift &&
!input.isAutoRepeat
);
}
export function shouldHideStatsWindowForInput(input: Electron.Input, toggleKey: string): boolean {
return (
(input.type === 'keyDown' && input.key === 'Escape') || isBareToggleKeyInput(input, toggleKey)
);
}
export function buildStatsWindowOptions(options: {
preloadPath: string;
bounds?: WindowGeometry | null;
}): BrowserWindowConstructorOptions {
return {
title: STATS_WINDOW_TITLE,
x: options.bounds?.x,
y: options.bounds?.y,
width: options.bounds?.width ?? DEFAULT_STATS_WINDOW_WIDTH,
height: options.bounds?.height ?? DEFAULT_STATS_WINDOW_HEIGHT,
frame: false,
transparent: false,
alwaysOnTop: true,
resizable: false,
skipTaskbar: true,
hasShadow: false,
focusable: true,
acceptFirstMouse: true,
fullscreenable: false,
backgroundColor: '#24273a',
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: options.preloadPath,
sandbox: true,
},
};
}
export function resolveStatsWindowOuterBoundsForContent(
window: StatsWindowBoundsController,
target: WindowGeometry,
): WindowGeometry {
const outer = window.getBounds();
const content = window.getContentBounds();
const leftInset = content.x - outer.x;
const topInset = content.y - outer.y;
const rightInset = outer.x + outer.width - (content.x + content.width);
const bottomInset = outer.y + outer.height - (content.y + content.height);
const insets = [leftInset, topInset, rightInset, bottomInset];
if (insets.some((inset) => !Number.isFinite(inset) || inset < 0)) {
return target;
}
return {
x: target.x - leftInset,
y: target.y - topInset,
width: target.width + leftInset + rightInset,
height: target.height + topInset + bottomInset,
};
}
export function promoteStatsWindowLevel(
window: StatsWindowLevelController,
platform: NodeJS.Platform = process.platform,
): void {
if (platform === 'darwin') {
window.setAlwaysOnTop(true, 'screen-saver', 2);
window.setVisibleOnAllWorkspaces?.(true, { visibleOnFullScreen: true });
window.setFullScreenable?.(false);
window.moveTop();
return;
}
if (platform === 'win32') {
window.setAlwaysOnTop(true, 'screen-saver', 2);
window.moveTop();
return;
}
window.setAlwaysOnTop(true);
window.moveTop();
}
export function promoteVisibleStatsWindowAboveOverlay(
window: VisibleStatsWindowLevelController,
options: {
platform?: NodeJS.Platform;
promoteHyprlandWindow?: () => void;
} = {},
): boolean {
if (window.isDestroyed() || !window.isVisible()) {
return false;
}
promoteStatsWindowLevel(window, options.platform);
options.promoteHyprlandWindow?.();
return true;
}
export function demoteVisibleStatsWindowBelowDialogs(
window: VisibleStatsWindowDialogLayerController,
): boolean {
if (window.isDestroyed() || !window.isVisible()) {
return false;
}
window.setAlwaysOnTop(false);
return true;
}
export function buildStatsNativeConfirmDialogOptions(message: string): MessageBoxSyncOptions {
return {
type: 'warning',
message,
buttons: ['Delete', 'Cancel'],
defaultId: 1,
cancelId: 1,
noLink: true,
};
}
export function showStatsNativeConfirmDialog<WindowT extends StatsNativeConfirmDialogWindow>(
window: WindowT | null,
message: string,
presenter: StatsNativeConfirmDialogPresenter<WindowT>,
): boolean {
const options = buildStatsNativeConfirmDialogOptions(message);
const response =
window && !window.isDestroyed()
? presenter.showWithParent(window, options)
: presenter.showWithoutParent(options);
return response === 0;
}
export function presentStatsWindow(
window: StatsWindowPresentationController,
platform: NodeJS.Platform = process.platform,
): void {
if (platform === 'darwin') {
if (window.showInactive) {
window.showInactive();
} else {
window.show();
}
return;
}
window.show();
window.focus();
}
export function buildStatsWindowLoadFileOptions(apiBaseUrl?: string): {
query: Record<string, string>;
} {
return {
query: {
overlay: '1',
...(apiBaseUrl ? { apiBase: apiBaseUrl } : {}),
},
};
}