mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-18 00:21:41 -07:00
fix(overlay): keep macOS modal windows on fullscreen Spaces (#200)
This commit is contained in:
@@ -53,7 +53,7 @@ const MPV_SUBTITLE_PROPERTY_OBSERVATIONS: string[] = [
|
||||
'sub-scale-by-window',
|
||||
'osd-height',
|
||||
'osd-dimensions',
|
||||
'sub-text-ass',
|
||||
'sub-text/ass',
|
||||
'sub-border-size',
|
||||
'sub-shadow-offset',
|
||||
'sub-ass-override',
|
||||
@@ -74,7 +74,7 @@ const MPV_INITIAL_PROPERTY_REQUESTS: Array<MpvProtocolCommand> = [
|
||||
request_id: MPV_REQUEST_ID_SUBTEXT,
|
||||
},
|
||||
{
|
||||
command: ['get_property', 'sub-text-ass'],
|
||||
command: ['get_property', 'sub-text/ass'],
|
||||
request_id: MPV_REQUEST_ID_SUBTEXT_ASS,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -129,6 +129,28 @@ test('dispatchMpvProtocolMessage emits subtitle text on property change', async
|
||||
assert.deepEqual(state.events, [{ text: '字幕', isOverlayVisible: false }]);
|
||||
});
|
||||
|
||||
test('dispatchMpvProtocolMessage emits ASS subtitle text from the current mpv property', async () => {
|
||||
const { deps, state } = createDeps();
|
||||
|
||||
await dispatchMpvProtocolMessage(
|
||||
{ event: 'property-change', name: 'sub-text/ass', data: '{\\b1}字幕' },
|
||||
deps,
|
||||
);
|
||||
|
||||
assert.deepEqual(state.events, [{ text: '{\\b1}字幕' }]);
|
||||
});
|
||||
|
||||
test('dispatchMpvProtocolMessage emits ASS subtitle text from the legacy mpv property', async () => {
|
||||
const { deps, state } = createDeps();
|
||||
|
||||
await dispatchMpvProtocolMessage(
|
||||
{ event: 'property-change', name: 'sub-text-ass', data: '{\\b1}字幕' },
|
||||
deps,
|
||||
);
|
||||
|
||||
assert.deepEqual(state.events, [{ text: '{\\b1}字幕' }]);
|
||||
});
|
||||
|
||||
test('dispatchMpvProtocolMessage emits subtitle track changes', async () => {
|
||||
const { deps, state } = createDeps({
|
||||
emitSubtitleTrackChange: (payload) => state.events.push(payload),
|
||||
|
||||
@@ -248,7 +248,7 @@ export async function dispatchMpvProtocolMessage(
|
||||
isOverlayVisible: overlayVisible,
|
||||
});
|
||||
deps.setCurrentSubText(nextSubText);
|
||||
} else if (msg.name === 'sub-text-ass') {
|
||||
} else if (msg.name === 'sub-text/ass' || msg.name === 'sub-text-ass') {
|
||||
deps.emitSubtitleAssChange({ text: (msg.data as string) || '' });
|
||||
} else if (msg.name === 'sub-start') {
|
||||
deps.setCurrentSubStart((msg.data as number) || 0);
|
||||
|
||||
@@ -505,6 +505,17 @@ test('MpvIpcClient reconnect replays property subscriptions and initial state re
|
||||
(command as { command: unknown[] }).command[1] === 1 &&
|
||||
(command as { command: unknown[] }).command[2] === 'sub-text',
|
||||
);
|
||||
const hasAssSubtitleSubscription = commands.some(
|
||||
(command) =>
|
||||
Array.isArray((command as { command: unknown[] }).command) &&
|
||||
(command as { command: unknown[] }).command[0] === 'observe_property' &&
|
||||
(command as { command: unknown[] }).command[2] === 'sub-text/ass',
|
||||
);
|
||||
const hasDeprecatedAssSubtitleProperty = commands.some(
|
||||
(command) =>
|
||||
Array.isArray((command as { command: unknown[] }).command) &&
|
||||
(command as { command: unknown[] }).command.includes('sub-text-ass'),
|
||||
);
|
||||
const hasPathRequest = commands.some(
|
||||
(command) =>
|
||||
Array.isArray((command as { command: unknown[] }).command) &&
|
||||
@@ -514,6 +525,8 @@ test('MpvIpcClient reconnect replays property subscriptions and initial state re
|
||||
|
||||
assert.equal(hasSecondaryVisibilityReset, true);
|
||||
assert.equal(hasTrackSubscription, true);
|
||||
assert.equal(hasAssSubtitleSubscription, true);
|
||||
assert.equal(hasDeprecatedAssSubtitleProperty, false);
|
||||
assert.equal(hasPathRequest, true);
|
||||
});
|
||||
|
||||
|
||||
@@ -15,6 +15,32 @@ test('overlay window config explicitly disables renderer sandbox for preload com
|
||||
assert.equal(options.webPreferences?.backgroundThrottling, false);
|
||||
});
|
||||
|
||||
test('macOS modal overlay uses a fullscreen auxiliary panel without changing the passive overlay', () => {
|
||||
const visibleOptions = buildOverlayWindowOptions('visible', {
|
||||
isDev: false,
|
||||
platform: 'darwin',
|
||||
yomitanSession: null,
|
||||
});
|
||||
const modalOptions = buildOverlayWindowOptions('modal', {
|
||||
isDev: false,
|
||||
platform: 'darwin',
|
||||
yomitanSession: null,
|
||||
});
|
||||
|
||||
assert.equal(visibleOptions.type, undefined);
|
||||
assert.equal(modalOptions.type, 'panel');
|
||||
});
|
||||
|
||||
test('non-macOS modal overlay remains a regular window', () => {
|
||||
const options = buildOverlayWindowOptions('modal', {
|
||||
isDev: false,
|
||||
platform: 'linux',
|
||||
yomitanSession: null,
|
||||
});
|
||||
|
||||
assert.equal(options.type, undefined);
|
||||
});
|
||||
|
||||
test('Linux visible overlay window allows compositor resize for mpv-sized placement', () => {
|
||||
const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export const OVERLAY_WINDOW_CONTENT_READY_FLAG = '__subminerOverlayContentReady';
|
||||
export const OVERLAY_WINDOW_DOCUMENT_LOADED_FLAG = '__subminerOverlayDocumentLoaded';
|
||||
|
||||
@@ -12,15 +12,17 @@ export function buildOverlayWindowOptions(
|
||||
options: {
|
||||
isDev: boolean;
|
||||
linuxX11FullscreenOverlay?: boolean;
|
||||
platform?: NodeJS.Platform;
|
||||
yomitanSession?: Session | null;
|
||||
},
|
||||
): BrowserWindowConstructorOptions {
|
||||
const showNativeDebugFrame = process.platform === 'win32' && options.isDev;
|
||||
const isLinuxVisibleOverlay = process.platform === 'linux' && kind === 'visible';
|
||||
const platform = options.platform ?? process.platform;
|
||||
const showNativeDebugFrame = platform === 'win32' && options.isDev;
|
||||
const isLinuxVisibleOverlay = platform === 'linux' && kind === 'visible';
|
||||
const isLinuxFullscreenOverlay =
|
||||
isLinuxVisibleOverlay && options.linuxX11FullscreenOverlay === true;
|
||||
const shouldStartAlwaysOnTop =
|
||||
!(process.platform === 'win32' && kind === 'visible') &&
|
||||
!(platform === 'win32' && kind === 'visible') &&
|
||||
(!isLinuxVisibleOverlay || isLinuxFullscreenOverlay);
|
||||
const shouldAllowCompositorResize = isLinuxVisibleOverlay && !isLinuxFullscreenOverlay;
|
||||
|
||||
@@ -41,7 +43,10 @@ export function buildOverlayWindowOptions(
|
||||
hasShadow: false,
|
||||
focusable: !isLinuxFullscreenOverlay,
|
||||
acceptFirstMouse: true,
|
||||
...(process.platform === 'win32' ? { thickFrame: showNativeDebugFrame } : {}),
|
||||
// A macOS panel is a fullscreen auxiliary window, so modal surfaces stay on the
|
||||
// active mpv Space instead of opening on SubMiner's last regular desktop.
|
||||
...(platform === 'darwin' && kind === 'modal' ? { type: 'panel' as const } : {}),
|
||||
...(platform === 'win32' ? { thickFrame: showNativeDebugFrame } : {}),
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, '..', '..', 'preload.js'),
|
||||
contextIsolation: true,
|
||||
|
||||
@@ -16,7 +16,10 @@ import {
|
||||
} from './hyprland-window-placement';
|
||||
import { buildOverlayWindowOptions, OVERLAY_WINDOW_TITLES } from './overlay-window-options';
|
||||
import { normalizeOverlayWindowBoundsForPlatform } from './overlay-window-bounds';
|
||||
import { OVERLAY_WINDOW_CONTENT_READY_FLAG } from './overlay-window-flags';
|
||||
import {
|
||||
OVERLAY_WINDOW_CONTENT_READY_FLAG,
|
||||
OVERLAY_WINDOW_DOCUMENT_LOADED_FLAG,
|
||||
} from './overlay-window-flags';
|
||||
export { OVERLAY_WINDOW_CONTENT_READY_FLAG } from './overlay-window-flags';
|
||||
|
||||
const logger = createLogger('main:overlay-window');
|
||||
@@ -133,6 +136,9 @@ export function createOverlayWindow(
|
||||
(window as BrowserWindow & { [OVERLAY_WINDOW_CONTENT_READY_FLAG]?: boolean })[
|
||||
OVERLAY_WINDOW_CONTENT_READY_FLAG
|
||||
] = false;
|
||||
(window as BrowserWindow & { [OVERLAY_WINDOW_DOCUMENT_LOADED_FLAG]?: boolean })[
|
||||
OVERLAY_WINDOW_DOCUMENT_LOADED_FLAG
|
||||
] = false;
|
||||
|
||||
if (!(process.platform === 'win32' && kind === 'visible')) {
|
||||
options.ensureOverlayWindowLevel(window);
|
||||
@@ -144,11 +150,20 @@ export function createOverlayWindow(
|
||||
});
|
||||
|
||||
window.webContents.on('did-finish-load', () => {
|
||||
(window as BrowserWindow & { [OVERLAY_WINDOW_DOCUMENT_LOADED_FLAG]?: boolean })[
|
||||
OVERLAY_WINDOW_DOCUMENT_LOADED_FLAG
|
||||
] = true;
|
||||
window.setTitle(OVERLAY_WINDOW_TITLES[kind]);
|
||||
options.onRuntimeOptionsChanged();
|
||||
options.onWindowDidFinishLoad?.();
|
||||
});
|
||||
|
||||
window.webContents.on('did-start-loading', () => {
|
||||
(window as BrowserWindow & { [OVERLAY_WINDOW_DOCUMENT_LOADED_FLAG]?: boolean })[
|
||||
OVERLAY_WINDOW_DOCUMENT_LOADED_FLAG
|
||||
] = false;
|
||||
});
|
||||
|
||||
window.webContents.on('page-title-updated', (event) => {
|
||||
event.preventDefault();
|
||||
window.setTitle(OVERLAY_WINDOW_TITLES[kind]);
|
||||
|
||||
@@ -57,7 +57,9 @@ export function shouldHideStatsWindowForInput(input: Electron.Input, toggleKey:
|
||||
export function buildStatsWindowOptions(options: {
|
||||
preloadPath: string;
|
||||
bounds?: WindowGeometry | null;
|
||||
platform?: NodeJS.Platform;
|
||||
}): BrowserWindowConstructorOptions {
|
||||
const platform = options.platform ?? process.platform;
|
||||
return {
|
||||
title: STATS_WINDOW_TITLE,
|
||||
x: options.bounds?.x,
|
||||
@@ -73,6 +75,9 @@ export function buildStatsWindowOptions(options: {
|
||||
focusable: true,
|
||||
acceptFirstMouse: true,
|
||||
fullscreenable: false,
|
||||
// Panels join fullscreen Spaces on macOS without moving the user back to the
|
||||
// desktop where SubMiner last owned a regular application window.
|
||||
...(platform === 'darwin' ? { type: 'panel' as const } : {}),
|
||||
backgroundColor: '#24273a',
|
||||
show: false,
|
||||
webPreferences: {
|
||||
@@ -84,6 +89,12 @@ export function buildStatsWindowOptions(options: {
|
||||
};
|
||||
}
|
||||
|
||||
export function shouldPresentStatsWindowAfterLoad(
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): boolean {
|
||||
return platform === 'darwin';
|
||||
}
|
||||
|
||||
export function resolveStatsWindowOuterBoundsForContent(
|
||||
window: StatsWindowBoundsController,
|
||||
target: WindowGeometry,
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
scheduleStatsWindowPostShowReconciles,
|
||||
showStatsNativeConfirmDialog,
|
||||
shouldHideStatsWindowForInput,
|
||||
shouldPresentStatsWindowAfterLoad,
|
||||
} from './stats-window-runtime';
|
||||
|
||||
test('buildStatsWindowOptions uses tracked overlay bounds and preload-friendly web preferences', () => {
|
||||
@@ -40,6 +41,30 @@ test('buildStatsWindowOptions uses tracked overlay bounds and preload-friendly w
|
||||
assert.equal(options.webPreferences?.sandbox, true);
|
||||
});
|
||||
|
||||
test('buildStatsWindowOptions uses a fullscreen auxiliary panel on macOS', () => {
|
||||
const options = buildStatsWindowOptions({
|
||||
preloadPath: '/tmp/preload-stats.js',
|
||||
platform: 'darwin',
|
||||
});
|
||||
|
||||
assert.equal(options.type, 'panel');
|
||||
});
|
||||
|
||||
test('buildStatsWindowOptions remains a regular window off macOS', () => {
|
||||
const options = buildStatsWindowOptions({
|
||||
preloadPath: '/tmp/preload-stats.js',
|
||||
platform: 'linux',
|
||||
});
|
||||
|
||||
assert.equal(options.type, undefined);
|
||||
});
|
||||
|
||||
test('stats panels present after document load on macOS', () => {
|
||||
assert.equal(shouldPresentStatsWindowAfterLoad('darwin'), true);
|
||||
assert.equal(shouldPresentStatsWindowAfterLoad('linux'), false);
|
||||
assert.equal(shouldPresentStatsWindowAfterLoad('win32'), false);
|
||||
});
|
||||
|
||||
test('shouldHideStatsWindowForInput matches Escape and configured bare toggle key', () => {
|
||||
assert.equal(
|
||||
shouldHideStatsWindowForInput(
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
scheduleStatsWindowPostShowReconciles,
|
||||
showStatsNativeConfirmDialog,
|
||||
shouldHideStatsWindowForInput,
|
||||
shouldPresentStatsWindowAfterLoad,
|
||||
STATS_WINDOW_TITLE,
|
||||
} from './stats-window-runtime.js';
|
||||
import { ensureHyprlandWindowFloatingByTitle } from './hyprland-window-placement.js';
|
||||
@@ -209,10 +210,15 @@ export function toggleStatsOverlay(options: StatsWindowOptions): void {
|
||||
options.onVisibilityChanged?.(false);
|
||||
}
|
||||
});
|
||||
statsWindow.once('ready-to-show', () => {
|
||||
const showInitialStatsWindow = () => {
|
||||
if (!statsWindow) return;
|
||||
showStatsWindow(statsWindow, options);
|
||||
});
|
||||
};
|
||||
if (shouldPresentStatsWindowAfterLoad()) {
|
||||
statsWindow.webContents.once('did-finish-load', showInitialStatsWindow);
|
||||
} else {
|
||||
statsWindow.once('ready-to-show', showInitialStatsWindow);
|
||||
}
|
||||
|
||||
statsWindow.on('blur', () => {
|
||||
if (!statsWindow || statsWindow.isDestroyed() || !statsWindow.isVisible()) {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* layer that keeps the two views consistent by construction.
|
||||
* 2. Otherwise (embedded track nobody parsed, a source whose timings mpv has shifted)
|
||||
* fall back to timing alone. No authoring metadata is available live -- mpv delivers
|
||||
* `sub-text-ass` after `sub-start`/`sub-end`, so any ASS text read here belongs to the
|
||||
* `sub-text/ass` after `sub-start`/`sub-end`, so any ASS text read here belongs to the
|
||||
* previous event -- which puts this layer in the same position as the SRT path in
|
||||
* `subtitle-cue-dedup`, and it uses that path's deliberately strict bounds.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user