mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
feat(overlay): split secondary subtitles into dedicated top window
This commit is contained in:
@@ -10,6 +10,7 @@ test('overlay manager initializes with empty windows and hidden overlays', () =>
|
||||
const manager = createOverlayManager();
|
||||
assert.equal(manager.getMainWindow(), null);
|
||||
assert.equal(manager.getInvisibleWindow(), null);
|
||||
assert.equal(manager.getSecondaryWindow(), null);
|
||||
assert.equal(manager.getVisibleOverlayVisible(), false);
|
||||
assert.equal(manager.getInvisibleOverlayVisible(), false);
|
||||
assert.deepEqual(manager.getOverlayWindows(), []);
|
||||
@@ -23,15 +24,20 @@ test('overlay manager stores window references and returns stable window order',
|
||||
const invisibleWindow = {
|
||||
isDestroyed: () => false,
|
||||
} as unknown as Electron.BrowserWindow;
|
||||
const secondaryWindow = {
|
||||
isDestroyed: () => false,
|
||||
} as unknown as Electron.BrowserWindow;
|
||||
|
||||
manager.setMainWindow(visibleWindow);
|
||||
manager.setInvisibleWindow(invisibleWindow);
|
||||
manager.setSecondaryWindow(secondaryWindow);
|
||||
|
||||
assert.equal(manager.getMainWindow(), visibleWindow);
|
||||
assert.equal(manager.getInvisibleWindow(), invisibleWindow);
|
||||
assert.equal(manager.getSecondaryWindow(), secondaryWindow);
|
||||
assert.equal(manager.getOverlayWindow('visible'), visibleWindow);
|
||||
assert.equal(manager.getOverlayWindow('invisible'), invisibleWindow);
|
||||
assert.deepEqual(manager.getOverlayWindows(), [visibleWindow, invisibleWindow]);
|
||||
assert.deepEqual(manager.getOverlayWindows(), [visibleWindow, invisibleWindow, secondaryWindow]);
|
||||
});
|
||||
|
||||
test('overlay manager excludes destroyed windows', () => {
|
||||
@@ -42,6 +48,9 @@ test('overlay manager excludes destroyed windows', () => {
|
||||
manager.setInvisibleWindow({
|
||||
isDestroyed: () => false,
|
||||
} as unknown as Electron.BrowserWindow);
|
||||
manager.setSecondaryWindow({
|
||||
isDestroyed: () => true,
|
||||
} as unknown as Electron.BrowserWindow);
|
||||
|
||||
assert.equal(manager.getOverlayWindows().length, 1);
|
||||
});
|
||||
@@ -72,12 +81,24 @@ test('overlay manager broadcasts to non-destroyed windows', () => {
|
||||
send: (..._args: unknown[]) => {},
|
||||
},
|
||||
} as unknown as Electron.BrowserWindow;
|
||||
const secondaryWindow = {
|
||||
isDestroyed: () => false,
|
||||
webContents: {
|
||||
send: (...args: unknown[]) => {
|
||||
calls.push(args);
|
||||
},
|
||||
},
|
||||
} as unknown as Electron.BrowserWindow;
|
||||
|
||||
manager.setMainWindow(aliveWindow);
|
||||
manager.setInvisibleWindow(deadWindow);
|
||||
manager.setSecondaryWindow(secondaryWindow);
|
||||
manager.broadcastToOverlayWindows('x', 1, 'a');
|
||||
|
||||
assert.deepEqual(calls, [['x', 1, 'a']]);
|
||||
assert.deepEqual(calls, [
|
||||
['x', 1, 'a'],
|
||||
['x', 1, 'a'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('overlay manager applies bounds by layer', () => {
|
||||
@@ -96,8 +117,15 @@ test('overlay manager applies bounds by layer', () => {
|
||||
invisibleCalls.push(bounds);
|
||||
},
|
||||
} as unknown as Electron.BrowserWindow;
|
||||
const secondaryWindow = {
|
||||
isDestroyed: () => false,
|
||||
setBounds: (bounds: Electron.Rectangle) => {
|
||||
invisibleCalls.push(bounds);
|
||||
},
|
||||
} as unknown as Electron.BrowserWindow;
|
||||
manager.setMainWindow(visibleWindow);
|
||||
manager.setInvisibleWindow(invisibleWindow);
|
||||
manager.setSecondaryWindow(secondaryWindow);
|
||||
|
||||
manager.setOverlayWindowBounds('visible', {
|
||||
x: 10,
|
||||
@@ -111,9 +139,18 @@ test('overlay manager applies bounds by layer', () => {
|
||||
width: 3,
|
||||
height: 4,
|
||||
});
|
||||
manager.setSecondaryWindowBounds({
|
||||
x: 8,
|
||||
y: 9,
|
||||
width: 10,
|
||||
height: 11,
|
||||
});
|
||||
|
||||
assert.deepEqual(visibleCalls, [{ x: 10, y: 20, width: 30, height: 40 }]);
|
||||
assert.deepEqual(invisibleCalls, [{ x: 1, y: 2, width: 3, height: 4 }]);
|
||||
assert.deepEqual(invisibleCalls, [
|
||||
{ x: 1, y: 2, width: 3, height: 4 },
|
||||
{ x: 8, y: 9, width: 10, height: 11 },
|
||||
]);
|
||||
});
|
||||
|
||||
test('runtime-option and debug broadcasts use expected channels', () => {
|
||||
|
||||
@@ -9,8 +9,11 @@ export interface OverlayManager {
|
||||
setMainWindow: (window: BrowserWindow | null) => void;
|
||||
getInvisibleWindow: () => BrowserWindow | null;
|
||||
setInvisibleWindow: (window: BrowserWindow | null) => void;
|
||||
getSecondaryWindow: () => BrowserWindow | null;
|
||||
setSecondaryWindow: (window: BrowserWindow | null) => void;
|
||||
getOverlayWindow: (layer: OverlayLayer) => BrowserWindow | null;
|
||||
setOverlayWindowBounds: (layer: OverlayLayer, geometry: WindowGeometry) => void;
|
||||
setSecondaryWindowBounds: (geometry: WindowGeometry) => void;
|
||||
getVisibleOverlayVisible: () => boolean;
|
||||
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||
getInvisibleOverlayVisible: () => boolean;
|
||||
@@ -22,6 +25,7 @@ export interface OverlayManager {
|
||||
export function createOverlayManager(): OverlayManager {
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let invisibleWindow: BrowserWindow | null = null;
|
||||
let secondaryWindow: BrowserWindow | null = null;
|
||||
let visibleOverlayVisible = false;
|
||||
let invisibleOverlayVisible = false;
|
||||
|
||||
@@ -34,10 +38,17 @@ export function createOverlayManager(): OverlayManager {
|
||||
setInvisibleWindow: (window) => {
|
||||
invisibleWindow = window;
|
||||
},
|
||||
getSecondaryWindow: () => secondaryWindow,
|
||||
setSecondaryWindow: (window) => {
|
||||
secondaryWindow = window;
|
||||
},
|
||||
getOverlayWindow: (layer) => (layer === 'visible' ? mainWindow : invisibleWindow),
|
||||
setOverlayWindowBounds: (layer, geometry) => {
|
||||
updateOverlayWindowBounds(geometry, layer === 'visible' ? mainWindow : invisibleWindow);
|
||||
},
|
||||
setSecondaryWindowBounds: (geometry) => {
|
||||
updateOverlayWindowBounds(geometry, secondaryWindow);
|
||||
},
|
||||
getVisibleOverlayVisible: () => visibleOverlayVisible,
|
||||
setVisibleOverlayVisible: (visible) => {
|
||||
visibleOverlayVisible = visible;
|
||||
@@ -54,6 +65,9 @@ export function createOverlayManager(): OverlayManager {
|
||||
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
|
||||
windows.push(invisibleWindow);
|
||||
}
|
||||
if (secondaryWindow && !secondaryWindow.isDestroyed()) {
|
||||
windows.push(secondaryWindow);
|
||||
}
|
||||
return windows;
|
||||
},
|
||||
broadcastToOverlayWindows: (channel, ...args) => {
|
||||
@@ -64,6 +78,9 @@ export function createOverlayManager(): OverlayManager {
|
||||
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
|
||||
windows.push(invisibleWindow);
|
||||
}
|
||||
if (secondaryWindow && !secondaryWindow.isDestroyed()) {
|
||||
windows.push(secondaryWindow);
|
||||
}
|
||||
for (const window of windows) {
|
||||
window.webContents.send(channel, ...args);
|
||||
}
|
||||
|
||||
41
src/core/services/overlay-window-geometry.ts
Normal file
41
src/core/services/overlay-window-geometry.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { WindowGeometry } from '../../types';
|
||||
|
||||
export const SECONDARY_OVERLAY_MAX_HEIGHT_RATIO = 0.2;
|
||||
|
||||
function toInteger(value: number): number {
|
||||
return Number.isFinite(value) ? Math.round(value) : 0;
|
||||
}
|
||||
|
||||
function clampPositive(value: number): number {
|
||||
return Math.max(1, toInteger(value));
|
||||
}
|
||||
|
||||
export function splitOverlayGeometryForSecondaryBar(geometry: WindowGeometry): {
|
||||
secondary: WindowGeometry;
|
||||
primary: WindowGeometry;
|
||||
} {
|
||||
const x = toInteger(geometry.x);
|
||||
const y = toInteger(geometry.y);
|
||||
const width = clampPositive(geometry.width);
|
||||
const totalHeight = clampPositive(geometry.height);
|
||||
|
||||
const secondaryHeight = clampPositive(
|
||||
Math.min(totalHeight, Math.round(totalHeight * SECONDARY_OVERLAY_MAX_HEIGHT_RATIO)),
|
||||
);
|
||||
const primaryHeight = clampPositive(totalHeight - secondaryHeight);
|
||||
|
||||
return {
|
||||
secondary: {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height: secondaryHeight,
|
||||
},
|
||||
primary: {
|
||||
x,
|
||||
y: y + secondaryHeight,
|
||||
width,
|
||||
height: primaryHeight,
|
||||
},
|
||||
};
|
||||
}
|
||||
37
src/core/services/overlay-window.test.ts
Normal file
37
src/core/services/overlay-window.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { splitOverlayGeometryForSecondaryBar } from './overlay-window-geometry';
|
||||
|
||||
test('splitOverlayGeometryForSecondaryBar returns 20/80 top-bottom regions', () => {
|
||||
const regions = splitOverlayGeometryForSecondaryBar({
|
||||
x: 100,
|
||||
y: 50,
|
||||
width: 1200,
|
||||
height: 900,
|
||||
});
|
||||
|
||||
assert.deepEqual(regions.secondary, {
|
||||
x: 100,
|
||||
y: 50,
|
||||
width: 1200,
|
||||
height: 180,
|
||||
});
|
||||
assert.deepEqual(regions.primary, {
|
||||
x: 100,
|
||||
y: 230,
|
||||
width: 1200,
|
||||
height: 720,
|
||||
});
|
||||
});
|
||||
|
||||
test('splitOverlayGeometryForSecondaryBar keeps positive sizes for tiny heights', () => {
|
||||
const regions = splitOverlayGeometryForSecondaryBar({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 300,
|
||||
height: 1,
|
||||
});
|
||||
|
||||
assert.ok(regions.secondary.height >= 1);
|
||||
assert.ok(regions.primary.height >= 1);
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import { createLogger } from '../../logger';
|
||||
|
||||
const logger = createLogger('main:overlay-window');
|
||||
|
||||
export type OverlayWindowKind = 'visible' | 'invisible';
|
||||
export type OverlayWindowKind = 'visible' | 'invisible' | 'secondary';
|
||||
|
||||
export function updateOverlayWindowBounds(
|
||||
geometry: WindowGeometry,
|
||||
@@ -87,7 +87,7 @@ export function createOverlayWindow(
|
||||
|
||||
window
|
||||
.loadFile(htmlPath, {
|
||||
query: { layer: kind === 'visible' ? 'visible' : 'invisible' },
|
||||
query: { layer: kind },
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error('Failed to load HTML file:', err);
|
||||
|
||||
Reference in New Issue
Block a user