mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-25 12:55:18 -07:00
81b941fe8c
- 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
202 lines
5.6 KiB
TypeScript
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 } : {}),
|
|
},
|
|
};
|
|
}
|