mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
feat(overlay): split secondary subtitles into dedicated top window
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
---
|
||||
id: TASK-110
|
||||
title: Split overlay into top secondary bar and bottom primary region
|
||||
status: Done
|
||||
assignee:
|
||||
- codex
|
||||
created_date: '2026-02-23 02:16'
|
||||
updated_date: '2026-02-23 02:29'
|
||||
labels:
|
||||
- overlay
|
||||
- subtitle
|
||||
- ux
|
||||
dependencies: []
|
||||
priority: high
|
||||
ordinal: 110000
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
|
||||
Implement a 3-window overlay layout:
|
||||
|
||||
- Dedicated secondary subtitle window anchored to top of mpv bounds (max 20% of height).
|
||||
- Visible overlay window constrained to remaining lower region.
|
||||
- Invisible overlay window constrained to remaining lower region.
|
||||
|
||||
Secondary subtitle bar must stay independently anchored while visible/invisible overlays swap.
|
||||
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
<!-- AC:BEGIN -->
|
||||
|
||||
- [x] #1 Runtime creates/manages a third overlay window dedicated to secondary subtitles.
|
||||
- [x] #2 Secondary window stays anchored to top region capped at 20% of tracked mpv bounds.
|
||||
- [x] #3 Visible and invisible overlay windows are constrained to remaining lower region.
|
||||
- [x] #4 Secondary subtitle rendering/mode updates reach dedicated top window without duplicate top bars in primary windows.
|
||||
- [x] #5 Focused runtime/core tests cover geometry split + window wiring regressions.
|
||||
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
|
||||
- Added new `secondary` overlay window kind and runtime factory wiring, plus manager ownership (`get/setSecondaryWindow`) and bounds setter.
|
||||
- Added geometry splitter `splitOverlayGeometryForSecondaryBar` (20% top secondary, 80% bottom primary), integrated into `updateVisibleOverlayBounds` / `updateInvisibleOverlayBounds` flow.
|
||||
- Main runtime now creates secondary window alongside primary overlays and syncs secondary window visibility with `secondarySubMode`.
|
||||
- Renderer now recognizes `layer=secondary`; secondary bar is hidden on primary layers, and primary subtitle/modals are hidden on secondary layer.
|
||||
- Secondary layer no longer reports overlay content measurements to avoid invalid payloads.
|
||||
- Validation:
|
||||
- `bun run tsc --noEmit`
|
||||
- `bun test src/main/runtime/overlay-window-factory.test.ts src/main/runtime/overlay-window-factory-main-deps.test.ts src/main/runtime/overlay-window-runtime-handlers.test.ts src/renderer/error-recovery.test.ts src/core/services/overlay-window.test.ts`
|
||||
- `bun run build`
|
||||
- `node --test dist/core/services/overlay-manager.test.js`
|
||||
|
||||
<!-- SECTION:NOTES:END -->
|
||||
@@ -87,3 +87,4 @@ Read first. Keep concise.
|
||||
| `opencode-task103-jellyfin-main-composer-20260222T221152Z-n3p7` | `opencode-task103-jellyfin-main-composer` | `Implement TASK-103 Jellyfin runtime wiring extraction from main.ts into composer module(s), tests, docs, and required validations (no commit).` | `in_progress` | `docs/subagents/agents/opencode-task103-jellyfin-main-composer-20260222T221152Z-n3p7.md` | `2026-02-22T22:11:52Z` |
|
||||
| `opencode-task109-discord-presence-20260223T011027Z-j9r4` | `opencode-task109-discord-presence` | `Finalize TASK-109 Discord Rich Presence with plan-first workflow and backlog closure.` | `in_progress` | `docs/subagents/agents/opencode-task109-discord-presence-20260223T011027Z-j9r4.md` | `2026-02-23T01:15:39Z` |
|
||||
| `codex-task88-yomitan-flow-20260223T012755Z-x4m2` | `codex-task88-yomitan-flow` | `Execute TASK-88 remove MeCab fallback tokenizer and simplify Yomitan token flow via plan-first workflow (no commit).` | `handoff` | `docs/subagents/agents/codex-task88-yomitan-flow-20260223T012755Z-x4m2.md` | `2026-02-23T01:44:16Z` |
|
||||
| `codex-overlay-three-window-layout-20260223T021606Z-9z2t` | `codex-overlay-three-window-layout` | `Implement top-anchored secondary subtitle overlay window (20%) plus swappable primary overlay region (80%).` | `handoff` | `docs/subagents/agents/codex-overlay-three-window-layout-20260223T021606Z-9z2t.md` | `2026-02-23T02:29:51Z` |
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# Agent Session: codex-overlay-three-window-layout-20260223T021606Z-9z2t
|
||||
|
||||
- alias: `codex-overlay-three-window-layout`
|
||||
- started_utc: `2026-02-23T02:16:06Z`
|
||||
- status: `handoff`
|
||||
- mission: `Implement top-anchored secondary subtitle overlay window (20%) plus swappable primary overlay region (80%).`
|
||||
- linked_backlog: `TASK-110`
|
||||
|
||||
## Intent
|
||||
|
||||
- Convert overlay runtime from 2 fullscreen windows to 3 windows:
|
||||
- `secondary` window anchored top (max 20% height).
|
||||
- `visible` / `invisible` windows constrained to remaining 80%.
|
||||
- Keep secondary subtitle bar independent from visible/invisible swapping.
|
||||
|
||||
## Planned Files
|
||||
|
||||
- `src/core/services/overlay-window.ts`
|
||||
- `src/core/services/overlay-runtime-init.ts`
|
||||
- `src/core/services/overlay-manager.ts`
|
||||
- `src/main.ts`
|
||||
- `src/preload.ts`
|
||||
- `src/types.ts`
|
||||
- `src/renderer/utils/platform.ts`
|
||||
- `src/renderer/renderer.ts`
|
||||
- `src/renderer/style.css`
|
||||
- `src/main/runtime/overlay-window-factory*.ts`
|
||||
- `src/main/runtime/overlay-window-runtime-handlers*.ts`
|
||||
- `src/main/runtime/overlay-runtime-options*.ts`
|
||||
- focused tests under `src/main/runtime/*` + `src/core/services/*`
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Secondary bar window should remain independently present while visible/invisible overlays toggle.
|
||||
- Top bar height fixed at 20% of tracked mpv bounds (clamped to valid integer px).
|
||||
- Existing secondary mode semantics (`hidden`/`visible`/`hover`) remain; hover behavior stays functional.
|
||||
|
||||
## Phase Log
|
||||
|
||||
- `2026-02-23T02:16:06Z` plan: read overlay/runtime wiring, implement secondary window + geometry split, run focused tests.
|
||||
- `2026-02-23T02:24:10Z` implementation: added `secondary` overlay window kind/wiring, overlay-manager secondary window ownership, and geometry split helper (`top 20%` secondary + `bottom 80%` primary).
|
||||
- `2026-02-23T02:27:40Z` renderer updates: added `secondary` layer detection, disabled measurement reports for secondary layer, and CSS layer split (secondary hidden in primary layers; subtitle/modals hidden in secondary layer).
|
||||
- `2026-02-23T02:29:51Z` validation: `bun run tsc --noEmit`; `bun test src/main/runtime/overlay-window-factory.test.ts src/main/runtime/overlay-window-factory-main-deps.test.ts src/main/runtime/overlay-window-runtime-handlers.test.ts src/renderer/error-recovery.test.ts src/core/services/overlay-window.test.ts`; `bun run build`; `node --test dist/core/services/overlay-manager.test.js`.
|
||||
|
||||
## Files Touched
|
||||
|
||||
- `src/main.ts`
|
||||
- `src/core/services/overlay-window.ts`
|
||||
- `src/core/services/overlay-window-geometry.ts`
|
||||
- `src/core/services/overlay-window.test.ts`
|
||||
- `src/core/services/overlay-manager.ts`
|
||||
- `src/core/services/overlay-manager.test.ts`
|
||||
- `src/main/runtime/overlay-window-factory.ts`
|
||||
- `src/main/runtime/overlay-window-factory-main-deps.ts`
|
||||
- `src/main/runtime/overlay-window-runtime-handlers.ts`
|
||||
- `src/main/runtime/overlay-window-factory.test.ts`
|
||||
- `src/main/runtime/overlay-window-factory-main-deps.test.ts`
|
||||
- `src/main/runtime/overlay-window-runtime-handlers.test.ts`
|
||||
- `src/preload.ts`
|
||||
- `src/types.ts`
|
||||
- `src/renderer/utils/platform.ts`
|
||||
- `src/renderer/overlay-content-measurement.ts`
|
||||
- `src/renderer/error-recovery.ts`
|
||||
- `src/renderer/style.css`
|
||||
- `backlog/tasks/task-110 - Split-overlay-into-top-secondary-bar-and-bottom-primary-region.md`
|
||||
|
||||
## Handoff
|
||||
|
||||
- Implemented requested 3-window behavior:
|
||||
- dedicated top secondary overlay window anchored by mpv geometry split,
|
||||
- visible/invisible overlays constrained to lower region.
|
||||
- Secondary mode state now syncs window visibility (`hidden` hides window; non-hidden shows + interactive).
|
||||
- Remaining caution: source-level `overlay-manager.test.ts` still requires dist/node path (Bun ESM + electron named export limitation), but dist test lane passes.
|
||||
@@ -150,3 +150,5 @@ Shared notes. Append-only.
|
||||
- [2026-02-23T01:15:39Z] [opencode-task109-discord-presence-20260223T011027Z-j9r4|opencode-task109-discord-presence] user feedback from real Discord session: status resumed to Playing with noticeable delay; tuned default `discordPresence.updateIntervalMs` from 15000 to 3000 in defaults/docs/examples and updated focused config expectations; reran focused config + discord presence tests green.
|
||||
- [2026-02-23T01:27:55Z] [codex-task88-yomitan-flow-20260223T012755Z-x4m2|codex-task88-yomitan-flow] starting TASK-88 via Backlog MCP + writing-plans/executing-plans; expected overlap in tokenizer modules (`src/core/services/tokenizer*`, Yomitan flow wiring/tests); will keep scope to MeCab fallback removal and token flow simplification.
|
||||
- [2026-02-23T01:44:16Z] [codex-task88-yomitan-flow-20260223T012755Z-x4m2|codex-task88-yomitan-flow] completed TASK-88 implementation pass: removed MeCab fallback branch from `tokenizeSubtitle`, restricted parser-selection to `scanning-parser` candidates, refreshed tokenizer regressions for Yomitan-only flow, updated usage/troubleshooting docs, and verified tokenizer+subtitle suites/build/docs-build green.
|
||||
- [2026-02-23T02:16:06Z] [codex-overlay-three-window-layout-20260223T021606Z-9z2t|codex-overlay-three-window-layout] overlap note: implementing user-requested 3-window overlay layout (`secondary` top bar + `visible|invisible` bottom region) in `src/core/services/overlay-window.ts`, `src/core/services/overlay-runtime-init.ts`, `src/main.ts`, renderer layer handling/CSS, and focused runtime/core tests.
|
||||
- [2026-02-23T02:29:51Z] [codex-overlay-three-window-layout-20260223T021606Z-9z2t|codex-overlay-three-window-layout] completed TASK-110 implementation: added `secondary` overlay window kind + manager ownership, enforced top-20%/bottom-80% bounds split in main runtime updates, hid secondary bar in primary renderer layers, and validated via focused tests + tsc + build + dist overlay-manager test.
|
||||
|
||||
@@ -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);
|
||||
|
||||
85
src/main.ts
85
src/main.ts
@@ -27,6 +27,7 @@ import {
|
||||
nativeImage,
|
||||
Tray,
|
||||
dialog,
|
||||
screen,
|
||||
} from 'electron';
|
||||
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
@@ -329,6 +330,7 @@ import {
|
||||
triggerFieldGrouping as triggerFieldGroupingCore,
|
||||
updateLastCardFromClipboard as updateLastCardFromClipboardCore,
|
||||
} from './core/services';
|
||||
import { splitOverlayGeometryForSecondaryBar } from './core/services/overlay-window-geometry';
|
||||
import { createAnilistUpdateQueue } from './core/services/anilist/anilist-update-queue';
|
||||
import {
|
||||
guessAnilistMediaInfo,
|
||||
@@ -814,6 +816,7 @@ const buildConfigHotReloadAppliedMainDepsHandler = createBuildConfigHotReloadApp
|
||||
},
|
||||
setSecondarySubMode: (mode) => {
|
||||
appState.secondarySubMode = mode;
|
||||
syncSecondaryOverlayWindowVisibility();
|
||||
},
|
||||
broadcastToOverlayWindows: (channel, payload) => {
|
||||
broadcastToOverlayWindows(channel, payload);
|
||||
@@ -1848,6 +1851,7 @@ const { reloadConfig: reloadConfigHandler, appReadyRuntimeRunner } = composeAppR
|
||||
},
|
||||
setSecondarySubMode: (mode: SecondarySubMode) => {
|
||||
appState.secondarySubMode = mode;
|
||||
syncSecondaryOverlayWindowVisibility();
|
||||
},
|
||||
defaultSecondarySubMode: 'hover',
|
||||
defaultWebsocketPort: DEFAULT_CONFIG.websocket.port,
|
||||
@@ -2167,10 +2171,57 @@ function updateMpvSubtitleRenderMetrics(patch: Partial<MpvSubtitleRenderMetrics>
|
||||
updateMpvSubtitleRenderMetricsHandler(patch);
|
||||
}
|
||||
|
||||
let lastOverlayWindowGeometry: WindowGeometry | null = null;
|
||||
|
||||
function getOverlayGeometryFallback(): WindowGeometry {
|
||||
const cursorPoint = screen.getCursorScreenPoint();
|
||||
const display = screen.getDisplayNearestPoint(cursorPoint);
|
||||
const bounds = display.workArea;
|
||||
return {
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
width: bounds.width,
|
||||
height: bounds.height,
|
||||
};
|
||||
}
|
||||
|
||||
function getCurrentOverlayGeometry(): WindowGeometry {
|
||||
if (lastOverlayWindowGeometry) return lastOverlayWindowGeometry;
|
||||
const trackerGeometry = appState.windowTracker?.getGeometry();
|
||||
if (trackerGeometry) return trackerGeometry;
|
||||
return getOverlayGeometryFallback();
|
||||
}
|
||||
|
||||
function syncSecondaryOverlayWindowVisibility(): void {
|
||||
const secondaryWindow = overlayManager.getSecondaryWindow();
|
||||
if (!secondaryWindow || secondaryWindow.isDestroyed()) return;
|
||||
|
||||
if (appState.secondarySubMode === 'hidden') {
|
||||
secondaryWindow.setIgnoreMouseEvents(true, { forward: true });
|
||||
secondaryWindow.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
secondaryWindow.setIgnoreMouseEvents(false);
|
||||
ensureOverlayWindowLevel(secondaryWindow);
|
||||
if (typeof secondaryWindow.showInactive === 'function') {
|
||||
secondaryWindow.showInactive();
|
||||
} else {
|
||||
secondaryWindow.show();
|
||||
}
|
||||
}
|
||||
|
||||
function applyOverlayRegions(layer: 'visible' | 'invisible', geometry: WindowGeometry): void {
|
||||
lastOverlayWindowGeometry = geometry;
|
||||
const regions = splitOverlayGeometryForSecondaryBar(geometry);
|
||||
overlayManager.setOverlayWindowBounds(layer, regions.primary);
|
||||
overlayManager.setSecondaryWindowBounds(regions.secondary);
|
||||
syncSecondaryOverlayWindowVisibility();
|
||||
}
|
||||
|
||||
const buildUpdateVisibleOverlayBoundsMainDepsHandler =
|
||||
createBuildUpdateVisibleOverlayBoundsMainDepsHandler({
|
||||
setOverlayWindowBounds: (layer, geometry) =>
|
||||
overlayManager.setOverlayWindowBounds(layer, geometry),
|
||||
setOverlayWindowBounds: (layer, geometry) => applyOverlayRegions(layer, geometry),
|
||||
});
|
||||
const updateVisibleOverlayBoundsMainDeps = buildUpdateVisibleOverlayBoundsMainDepsHandler();
|
||||
const updateVisibleOverlayBounds = createUpdateVisibleOverlayBoundsHandler(
|
||||
@@ -2179,8 +2230,7 @@ const updateVisibleOverlayBounds = createUpdateVisibleOverlayBoundsHandler(
|
||||
|
||||
const buildUpdateInvisibleOverlayBoundsMainDepsHandler =
|
||||
createBuildUpdateInvisibleOverlayBoundsMainDepsHandler({
|
||||
setOverlayWindowBounds: (layer, geometry) =>
|
||||
overlayManager.setOverlayWindowBounds(layer, geometry),
|
||||
setOverlayWindowBounds: (layer, geometry) => applyOverlayRegions(layer, geometry),
|
||||
});
|
||||
const updateInvisibleOverlayBoundsMainDeps = buildUpdateInvisibleOverlayBoundsMainDepsHandler();
|
||||
const updateInvisibleOverlayBounds = createUpdateInvisibleOverlayBoundsHandler(
|
||||
@@ -2226,12 +2276,24 @@ async function ensureYomitanExtensionLoaded(): Promise<Extension | null> {
|
||||
return yomitanExtensionRuntime.ensureYomitanExtensionLoaded();
|
||||
}
|
||||
|
||||
function createOverlayWindow(kind: 'visible' | 'invisible'): BrowserWindow {
|
||||
function createOverlayWindow(kind: 'visible' | 'invisible' | 'secondary'): BrowserWindow {
|
||||
return createOverlayWindowHandler(kind);
|
||||
}
|
||||
|
||||
function createSecondaryWindow(): BrowserWindow {
|
||||
const existingWindow = overlayManager.getSecondaryWindow();
|
||||
if (existingWindow && !existingWindow.isDestroyed()) {
|
||||
return existingWindow;
|
||||
}
|
||||
const window = createSecondaryWindowHandler();
|
||||
applyOverlayRegions('visible', getCurrentOverlayGeometry());
|
||||
return window;
|
||||
}
|
||||
|
||||
function createMainWindow(): BrowserWindow {
|
||||
return createMainWindowHandler();
|
||||
const window = createMainWindowHandler();
|
||||
createSecondaryWindow();
|
||||
return window;
|
||||
}
|
||||
function createInvisibleWindow(): BrowserWindow {
|
||||
return createInvisibleWindowHandler();
|
||||
@@ -2339,6 +2401,7 @@ const cycleSecondarySubMode = createCycleSecondarySubModeRuntimeHandler({
|
||||
getSecondarySubMode: () => appState.secondarySubMode,
|
||||
setSecondarySubMode: (mode: SecondarySubMode) => {
|
||||
appState.secondarySubMode = mode;
|
||||
syncSecondaryOverlayWindowVisibility();
|
||||
},
|
||||
getLastSecondarySubToggleAtMs: () => appState.lastSecondarySubToggleAtMs,
|
||||
setLastSecondarySubToggleAtMs: (timestampMs: number) => {
|
||||
@@ -2678,6 +2741,7 @@ const {
|
||||
createOverlayWindow: createOverlayWindowHandler,
|
||||
createMainWindow: createMainWindowHandler,
|
||||
createInvisibleWindow: createInvisibleWindowHandler,
|
||||
createSecondaryWindow: createSecondaryWindowHandler,
|
||||
} = createOverlayWindowRuntimeHandlers<BrowserWindow>({
|
||||
createOverlayWindowDeps: {
|
||||
createOverlayWindowCore: (kind, options) => createOverlayWindowCore(kind, options),
|
||||
@@ -2689,19 +2753,24 @@ const {
|
||||
isOverlayVisible: (windowKind) =>
|
||||
windowKind === 'visible'
|
||||
? overlayManager.getVisibleOverlayVisible()
|
||||
: overlayManager.getInvisibleOverlayVisible(),
|
||||
: windowKind === 'invisible'
|
||||
? overlayManager.getInvisibleOverlayVisible()
|
||||
: false,
|
||||
tryHandleOverlayShortcutLocalFallback: (input) =>
|
||||
overlayShortcutsRuntime.tryHandleOverlayShortcutLocalFallback(input),
|
||||
onWindowClosed: (windowKind) => {
|
||||
if (windowKind === 'visible') {
|
||||
overlayManager.setMainWindow(null);
|
||||
} else {
|
||||
} else if (windowKind === 'invisible') {
|
||||
overlayManager.setInvisibleWindow(null);
|
||||
} else {
|
||||
overlayManager.setSecondaryWindow(null);
|
||||
}
|
||||
},
|
||||
},
|
||||
setMainWindow: (window) => overlayManager.setMainWindow(window),
|
||||
setInvisibleWindow: (window) => overlayManager.setInvisibleWindow(window),
|
||||
setSecondaryWindow: (window) => overlayManager.setSecondaryWindow(window),
|
||||
});
|
||||
const {
|
||||
resolveTrayIconPath: resolveTrayIconPathHandler,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
createBuildCreateInvisibleWindowMainDepsHandler,
|
||||
createBuildCreateMainWindowMainDepsHandler,
|
||||
createBuildCreateOverlayWindowMainDepsHandler,
|
||||
createBuildCreateSecondaryWindowMainDepsHandler,
|
||||
} from './overlay-window-factory-main-deps';
|
||||
|
||||
test('overlay window factory main deps builders return mapped handlers', () => {
|
||||
@@ -39,5 +40,12 @@ test('overlay window factory main deps builders return mapped handlers', () => {
|
||||
const invisibleDeps = buildInvisibleDeps();
|
||||
invisibleDeps.setInvisibleWindow(null);
|
||||
|
||||
assert.deepEqual(calls, ['set-main', 'set-invisible']);
|
||||
const buildSecondaryDeps = createBuildCreateSecondaryWindowMainDepsHandler({
|
||||
createOverlayWindow: () => ({ id: 'secondary' }),
|
||||
setSecondaryWindow: () => calls.push('set-secondary'),
|
||||
});
|
||||
const secondaryDeps = buildSecondaryDeps();
|
||||
secondaryDeps.setSecondaryWindow(null);
|
||||
|
||||
assert.deepEqual(calls, ['set-main', 'set-invisible', 'set-secondary']);
|
||||
});
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
export function createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps: {
|
||||
createOverlayWindowCore: (
|
||||
kind: 'visible' | 'invisible',
|
||||
kind: 'visible' | 'invisible' | 'secondary',
|
||||
options: {
|
||||
isDev: boolean;
|
||||
overlayDebugVisualizationEnabled: boolean;
|
||||
ensureOverlayWindowLevel: (window: TWindow) => void;
|
||||
onRuntimeOptionsChanged: () => void;
|
||||
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
|
||||
isOverlayVisible: (windowKind: 'visible' | 'invisible') => boolean;
|
||||
isOverlayVisible: (windowKind: 'visible' | 'invisible' | 'secondary') => boolean;
|
||||
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
|
||||
onWindowClosed: (windowKind: 'visible' | 'invisible') => void;
|
||||
onWindowClosed: (windowKind: 'visible' | 'invisible' | 'secondary') => void;
|
||||
},
|
||||
) => TWindow;
|
||||
isDev: boolean;
|
||||
@@ -17,9 +17,9 @@ export function createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps: {
|
||||
ensureOverlayWindowLevel: (window: TWindow) => void;
|
||||
onRuntimeOptionsChanged: () => void;
|
||||
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
|
||||
isOverlayVisible: (windowKind: 'visible' | 'invisible') => boolean;
|
||||
isOverlayVisible: (windowKind: 'visible' | 'invisible' | 'secondary') => boolean;
|
||||
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
|
||||
onWindowClosed: (windowKind: 'visible' | 'invisible') => void;
|
||||
onWindowClosed: (windowKind: 'visible' | 'invisible' | 'secondary') => void;
|
||||
}) {
|
||||
return () => ({
|
||||
createOverlayWindowCore: deps.createOverlayWindowCore,
|
||||
@@ -35,7 +35,7 @@ export function createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps: {
|
||||
}
|
||||
|
||||
export function createBuildCreateMainWindowMainDepsHandler<TWindow>(deps: {
|
||||
createOverlayWindow: (kind: 'visible' | 'invisible') => TWindow;
|
||||
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary') => TWindow;
|
||||
setMainWindow: (window: TWindow | null) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
@@ -45,7 +45,7 @@ export function createBuildCreateMainWindowMainDepsHandler<TWindow>(deps: {
|
||||
}
|
||||
|
||||
export function createBuildCreateInvisibleWindowMainDepsHandler<TWindow>(deps: {
|
||||
createOverlayWindow: (kind: 'visible' | 'invisible') => TWindow;
|
||||
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary') => TWindow;
|
||||
setInvisibleWindow: (window: TWindow | null) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
@@ -53,3 +53,13 @@ export function createBuildCreateInvisibleWindowMainDepsHandler<TWindow>(deps: {
|
||||
setInvisibleWindow: deps.setInvisibleWindow,
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildCreateSecondaryWindowMainDepsHandler<TWindow>(deps: {
|
||||
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary') => TWindow;
|
||||
setSecondaryWindow: (window: TWindow | null) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
createOverlayWindow: deps.createOverlayWindow,
|
||||
setSecondaryWindow: deps.setSecondaryWindow,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
createCreateInvisibleWindowHandler,
|
||||
createCreateMainWindowHandler,
|
||||
createCreateOverlayWindowHandler,
|
||||
createCreateSecondaryWindowHandler,
|
||||
} from './overlay-window-factory';
|
||||
|
||||
test('create overlay window handler forwards options and kind', () => {
|
||||
@@ -64,3 +65,18 @@ test('create invisible window handler stores invisible window', () => {
|
||||
assert.equal(createInvisibleWindow(), invisibleWindow);
|
||||
assert.deepEqual(calls, ['create:invisible', 'set:invisible']);
|
||||
});
|
||||
|
||||
test('create secondary window handler stores secondary window', () => {
|
||||
const calls: string[] = [];
|
||||
const secondaryWindow = { id: 'secondary' };
|
||||
const createSecondaryWindow = createCreateSecondaryWindowHandler({
|
||||
createOverlayWindow: (kind) => {
|
||||
calls.push(`create:${kind}`);
|
||||
return secondaryWindow;
|
||||
},
|
||||
setSecondaryWindow: (window) => calls.push(`set:${(window as { id: string }).id}`),
|
||||
});
|
||||
|
||||
assert.equal(createSecondaryWindow(), secondaryWindow);
|
||||
assert.deepEqual(calls, ['create:secondary', 'set:secondary']);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
type OverlayWindowKind = 'visible' | 'invisible';
|
||||
type OverlayWindowKind = 'visible' | 'invisible' | 'secondary';
|
||||
|
||||
export function createCreateOverlayWindowHandler<TWindow>(deps: {
|
||||
createOverlayWindowCore: (
|
||||
@@ -58,3 +58,14 @@ export function createCreateInvisibleWindowHandler<TWindow>(deps: {
|
||||
return window;
|
||||
};
|
||||
}
|
||||
|
||||
export function createCreateSecondaryWindowHandler<TWindow>(deps: {
|
||||
createOverlayWindow: (kind: OverlayWindowKind) => TWindow;
|
||||
setSecondaryWindow: (window: TWindow | null) => void;
|
||||
}) {
|
||||
return (): TWindow => {
|
||||
const window = deps.createOverlayWindow('secondary');
|
||||
deps.setSecondaryWindow(window);
|
||||
return window;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createOverlayWindowRuntimeHandlers } from './overlay-window-runtime-han
|
||||
test('overlay window runtime handlers compose create/main/invisible handlers', () => {
|
||||
let mainWindow: { kind: string } | null = null;
|
||||
let invisibleWindow: { kind: string } | null = null;
|
||||
let secondaryWindow: { kind: string } | null = null;
|
||||
let debugEnabled = false;
|
||||
const calls: string[] = [];
|
||||
|
||||
@@ -28,10 +29,14 @@ test('overlay window runtime handlers compose create/main/invisible handlers', (
|
||||
setInvisibleWindow: (window) => {
|
||||
invisibleWindow = window;
|
||||
},
|
||||
setSecondaryWindow: (window) => {
|
||||
secondaryWindow = window;
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(runtime.createOverlayWindow('visible'), { kind: 'visible' });
|
||||
assert.deepEqual(runtime.createOverlayWindow('invisible'), { kind: 'invisible' });
|
||||
assert.deepEqual(runtime.createOverlayWindow('secondary'), { kind: 'secondary' });
|
||||
|
||||
assert.deepEqual(runtime.createMainWindow(), { kind: 'visible' });
|
||||
assert.deepEqual(mainWindow, { kind: 'visible' });
|
||||
@@ -39,6 +44,9 @@ test('overlay window runtime handlers compose create/main/invisible handlers', (
|
||||
assert.deepEqual(runtime.createInvisibleWindow(), { kind: 'invisible' });
|
||||
assert.deepEqual(invisibleWindow, { kind: 'invisible' });
|
||||
|
||||
assert.deepEqual(runtime.createSecondaryWindow(), { kind: 'secondary' });
|
||||
assert.deepEqual(secondaryWindow, { kind: 'secondary' });
|
||||
|
||||
assert.equal(debugEnabled, false);
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
|
||||
@@ -2,11 +2,13 @@ import {
|
||||
createCreateInvisibleWindowHandler,
|
||||
createCreateMainWindowHandler,
|
||||
createCreateOverlayWindowHandler,
|
||||
createCreateSecondaryWindowHandler,
|
||||
} from './overlay-window-factory';
|
||||
import {
|
||||
createBuildCreateInvisibleWindowMainDepsHandler,
|
||||
createBuildCreateMainWindowMainDepsHandler,
|
||||
createBuildCreateOverlayWindowMainDepsHandler,
|
||||
createBuildCreateSecondaryWindowMainDepsHandler,
|
||||
} from './overlay-window-factory-main-deps';
|
||||
|
||||
type CreateOverlayWindowMainDeps<TWindow> = Parameters<
|
||||
@@ -17,6 +19,7 @@ export function createOverlayWindowRuntimeHandlers<TWindow>(deps: {
|
||||
createOverlayWindowDeps: CreateOverlayWindowMainDeps<TWindow>;
|
||||
setMainWindow: (window: TWindow | null) => void;
|
||||
setInvisibleWindow: (window: TWindow | null) => void;
|
||||
setSecondaryWindow: (window: TWindow | null) => void;
|
||||
}) {
|
||||
const createOverlayWindow = createCreateOverlayWindowHandler<TWindow>(
|
||||
createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps.createOverlayWindowDeps)(),
|
||||
@@ -33,10 +36,17 @@ export function createOverlayWindowRuntimeHandlers<TWindow>(deps: {
|
||||
setInvisibleWindow: (window) => deps.setInvisibleWindow(window),
|
||||
})(),
|
||||
);
|
||||
const createSecondaryWindow = createCreateSecondaryWindowHandler<TWindow>(
|
||||
createBuildCreateSecondaryWindowMainDepsHandler<TWindow>({
|
||||
createOverlayWindow: (kind) => createOverlayWindow(kind),
|
||||
setSecondaryWindow: (window) => deps.setSecondaryWindow(window),
|
||||
})(),
|
||||
);
|
||||
|
||||
return {
|
||||
createOverlayWindow,
|
||||
createMainWindow,
|
||||
createInvisibleWindow,
|
||||
createSecondaryWindow,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -55,7 +55,9 @@ import { IPC_CHANNELS } from './shared/ipc/contracts';
|
||||
const overlayLayerArg = process.argv.find((arg) => arg.startsWith('--overlay-layer='));
|
||||
const overlayLayerFromArg = overlayLayerArg?.slice('--overlay-layer='.length);
|
||||
const overlayLayer =
|
||||
overlayLayerFromArg === 'visible' || overlayLayerFromArg === 'invisible'
|
||||
overlayLayerFromArg === 'visible' ||
|
||||
overlayLayerFromArg === 'invisible' ||
|
||||
overlayLayerFromArg === 'secondary'
|
||||
? overlayLayerFromArg
|
||||
: null;
|
||||
|
||||
|
||||
@@ -155,3 +155,38 @@ test('resolvePlatformInfo prefers query layer over preload layer', () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('resolvePlatformInfo supports secondary layer and disables mouse-ignore toggles', () => {
|
||||
const previousWindow = (globalThis as { window?: unknown }).window;
|
||||
const previousNavigator = (globalThis as { navigator?: unknown }).navigator;
|
||||
|
||||
Object.defineProperty(globalThis, 'window', {
|
||||
configurable: true,
|
||||
value: {
|
||||
electronAPI: {
|
||||
getOverlayLayer: () => 'secondary',
|
||||
},
|
||||
location: { search: '' },
|
||||
},
|
||||
});
|
||||
Object.defineProperty(globalThis, 'navigator', {
|
||||
configurable: true,
|
||||
value: {
|
||||
platform: 'MacIntel',
|
||||
userAgent: 'Mozilla/5.0 (Macintosh)',
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const info = resolvePlatformInfo();
|
||||
assert.equal(info.overlayLayer, 'secondary');
|
||||
assert.equal(info.isSecondaryLayer, true);
|
||||
assert.equal(info.shouldToggleMouseIgnore, false);
|
||||
} finally {
|
||||
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
|
||||
Object.defineProperty(globalThis, 'navigator', {
|
||||
configurable: true,
|
||||
value: previousNavigator,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ export type RendererRecoverySnapshot = {
|
||||
isOverlayInteractive: boolean;
|
||||
isOverSubtitle: boolean;
|
||||
invisiblePositionEditMode: boolean;
|
||||
overlayLayer: 'visible' | 'invisible';
|
||||
overlayLayer: 'visible' | 'invisible' | 'secondary';
|
||||
};
|
||||
|
||||
type NormalizedRendererError = {
|
||||
|
||||
@@ -3,6 +3,10 @@ import type { RendererContext } from './context';
|
||||
|
||||
const MEASUREMENT_DEBOUNCE_MS = 80;
|
||||
|
||||
function isMeasurableOverlayLayer(layer: string): layer is 'visible' | 'invisible' {
|
||||
return layer === 'visible' || layer === 'invisible';
|
||||
}
|
||||
|
||||
function round2(value: number): number {
|
||||
return Math.round(value * 100) / 100;
|
||||
}
|
||||
@@ -78,6 +82,10 @@ export function createOverlayContentMeasurementReporter(ctx: RendererContext) {
|
||||
let debounceTimer: number | null = null;
|
||||
|
||||
function emitNow(): void {
|
||||
if (!isMeasurableOverlayLayer(ctx.platform.overlayLayer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const measurement: OverlayContentMeasurement = {
|
||||
layer: ctx.platform.overlayLayer,
|
||||
measuredAtMs: Date.now(),
|
||||
|
||||
@@ -533,6 +533,40 @@ body.layer-invisible.invisible-position-edit #subtitleRoot .c {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
body.layer-visible #secondarySubContainer,
|
||||
body.layer-invisible #secondarySubContainer {
|
||||
display: none !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
body.layer-secondary #subtitleContainer,
|
||||
body.layer-secondary .modal,
|
||||
body.layer-secondary .overlay-error-toast {
|
||||
display: none !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
body.layer-secondary #overlay {
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
body.layer-secondary #secondarySubContainer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
transform: none;
|
||||
max-width: 100%;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#secondarySubRoot {
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
@@ -548,6 +582,10 @@ body.layer-invisible.invisible-position-edit #subtitleRoot .c {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
body.layer-secondary #secondarySubRoot {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#secondarySubRoot:empty {
|
||||
display: none;
|
||||
}
|
||||
@@ -591,6 +629,11 @@ body.settings-modal-open #secondarySubContainer {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
body.layer-secondary #secondarySubContainer.secondary-sub-hover {
|
||||
padding: 8px 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
iframe[id^='yomitan-popup'] {
|
||||
pointer-events: auto !important;
|
||||
z-index: 2147483647 !important;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
export type OverlayLayer = 'visible' | 'invisible';
|
||||
export type OverlayLayer = 'visible' | 'invisible' | 'secondary';
|
||||
|
||||
export type PlatformInfo = {
|
||||
overlayLayer: OverlayLayer;
|
||||
isInvisibleLayer: boolean;
|
||||
isSecondaryLayer: boolean;
|
||||
isLinuxPlatform: boolean;
|
||||
isMacOSPlatform: boolean;
|
||||
shouldToggleMouseIgnore: boolean;
|
||||
@@ -15,15 +16,20 @@ export function resolvePlatformInfo(): PlatformInfo {
|
||||
const overlayLayerFromPreload = window.electronAPI.getOverlayLayer();
|
||||
const queryLayer = new URLSearchParams(window.location.search).get('layer');
|
||||
const overlayLayerFromQuery: OverlayLayer | null =
|
||||
queryLayer === 'visible' || queryLayer === 'invisible' ? queryLayer : null;
|
||||
queryLayer === 'visible' || queryLayer === 'invisible' || queryLayer === 'secondary'
|
||||
? queryLayer
|
||||
: null;
|
||||
|
||||
const overlayLayer: OverlayLayer =
|
||||
overlayLayerFromQuery ??
|
||||
(overlayLayerFromPreload === 'visible' || overlayLayerFromPreload === 'invisible'
|
||||
(overlayLayerFromPreload === 'visible' ||
|
||||
overlayLayerFromPreload === 'invisible' ||
|
||||
overlayLayerFromPreload === 'secondary'
|
||||
? overlayLayerFromPreload
|
||||
: 'visible');
|
||||
|
||||
const isInvisibleLayer = overlayLayer === 'invisible';
|
||||
const isSecondaryLayer = overlayLayer === 'secondary';
|
||||
const isLinuxPlatform = navigator.platform.toLowerCase().includes('linux');
|
||||
const isMacOSPlatform =
|
||||
navigator.platform.toLowerCase().includes('mac') || /mac/i.test(navigator.userAgent);
|
||||
@@ -31,9 +37,10 @@ export function resolvePlatformInfo(): PlatformInfo {
|
||||
return {
|
||||
overlayLayer,
|
||||
isInvisibleLayer,
|
||||
isSecondaryLayer,
|
||||
isLinuxPlatform,
|
||||
isMacOSPlatform,
|
||||
shouldToggleMouseIgnore: !isLinuxPlatform,
|
||||
shouldToggleMouseIgnore: !isLinuxPlatform && !isSecondaryLayer,
|
||||
invisiblePositionEditToggleCode: 'KeyP',
|
||||
invisiblePositionStepPx: 1,
|
||||
invisiblePositionStepFastPx: 4,
|
||||
|
||||
@@ -728,7 +728,7 @@ export interface SubtitleHoverTokenPayload {
|
||||
}
|
||||
|
||||
export interface ElectronAPI {
|
||||
getOverlayLayer: () => 'visible' | 'invisible' | null;
|
||||
getOverlayLayer: () => 'visible' | 'invisible' | 'secondary' | null;
|
||||
onSubtitle: (callback: (data: SubtitleData) => void) => void;
|
||||
onVisibility: (callback: (visible: boolean) => void) => void;
|
||||
onSubtitlePosition: (callback: (position: SubtitlePosition | null) => void) => void;
|
||||
|
||||
Reference in New Issue
Block a user