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-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` |
|
| `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-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: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: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-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();
|
const manager = createOverlayManager();
|
||||||
assert.equal(manager.getMainWindow(), null);
|
assert.equal(manager.getMainWindow(), null);
|
||||||
assert.equal(manager.getInvisibleWindow(), null);
|
assert.equal(manager.getInvisibleWindow(), null);
|
||||||
|
assert.equal(manager.getSecondaryWindow(), null);
|
||||||
assert.equal(manager.getVisibleOverlayVisible(), false);
|
assert.equal(manager.getVisibleOverlayVisible(), false);
|
||||||
assert.equal(manager.getInvisibleOverlayVisible(), false);
|
assert.equal(manager.getInvisibleOverlayVisible(), false);
|
||||||
assert.deepEqual(manager.getOverlayWindows(), []);
|
assert.deepEqual(manager.getOverlayWindows(), []);
|
||||||
@@ -23,15 +24,20 @@ test('overlay manager stores window references and returns stable window order',
|
|||||||
const invisibleWindow = {
|
const invisibleWindow = {
|
||||||
isDestroyed: () => false,
|
isDestroyed: () => false,
|
||||||
} as unknown as Electron.BrowserWindow;
|
} as unknown as Electron.BrowserWindow;
|
||||||
|
const secondaryWindow = {
|
||||||
|
isDestroyed: () => false,
|
||||||
|
} as unknown as Electron.BrowserWindow;
|
||||||
|
|
||||||
manager.setMainWindow(visibleWindow);
|
manager.setMainWindow(visibleWindow);
|
||||||
manager.setInvisibleWindow(invisibleWindow);
|
manager.setInvisibleWindow(invisibleWindow);
|
||||||
|
manager.setSecondaryWindow(secondaryWindow);
|
||||||
|
|
||||||
assert.equal(manager.getMainWindow(), visibleWindow);
|
assert.equal(manager.getMainWindow(), visibleWindow);
|
||||||
assert.equal(manager.getInvisibleWindow(), invisibleWindow);
|
assert.equal(manager.getInvisibleWindow(), invisibleWindow);
|
||||||
|
assert.equal(manager.getSecondaryWindow(), secondaryWindow);
|
||||||
assert.equal(manager.getOverlayWindow('visible'), visibleWindow);
|
assert.equal(manager.getOverlayWindow('visible'), visibleWindow);
|
||||||
assert.equal(manager.getOverlayWindow('invisible'), invisibleWindow);
|
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', () => {
|
test('overlay manager excludes destroyed windows', () => {
|
||||||
@@ -42,6 +48,9 @@ test('overlay manager excludes destroyed windows', () => {
|
|||||||
manager.setInvisibleWindow({
|
manager.setInvisibleWindow({
|
||||||
isDestroyed: () => false,
|
isDestroyed: () => false,
|
||||||
} as unknown as Electron.BrowserWindow);
|
} as unknown as Electron.BrowserWindow);
|
||||||
|
manager.setSecondaryWindow({
|
||||||
|
isDestroyed: () => true,
|
||||||
|
} as unknown as Electron.BrowserWindow);
|
||||||
|
|
||||||
assert.equal(manager.getOverlayWindows().length, 1);
|
assert.equal(manager.getOverlayWindows().length, 1);
|
||||||
});
|
});
|
||||||
@@ -72,12 +81,24 @@ test('overlay manager broadcasts to non-destroyed windows', () => {
|
|||||||
send: (..._args: unknown[]) => {},
|
send: (..._args: unknown[]) => {},
|
||||||
},
|
},
|
||||||
} as unknown as Electron.BrowserWindow;
|
} 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.setMainWindow(aliveWindow);
|
||||||
manager.setInvisibleWindow(deadWindow);
|
manager.setInvisibleWindow(deadWindow);
|
||||||
|
manager.setSecondaryWindow(secondaryWindow);
|
||||||
manager.broadcastToOverlayWindows('x', 1, 'a');
|
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', () => {
|
test('overlay manager applies bounds by layer', () => {
|
||||||
@@ -96,8 +117,15 @@ test('overlay manager applies bounds by layer', () => {
|
|||||||
invisibleCalls.push(bounds);
|
invisibleCalls.push(bounds);
|
||||||
},
|
},
|
||||||
} as unknown as Electron.BrowserWindow;
|
} 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.setMainWindow(visibleWindow);
|
||||||
manager.setInvisibleWindow(invisibleWindow);
|
manager.setInvisibleWindow(invisibleWindow);
|
||||||
|
manager.setSecondaryWindow(secondaryWindow);
|
||||||
|
|
||||||
manager.setOverlayWindowBounds('visible', {
|
manager.setOverlayWindowBounds('visible', {
|
||||||
x: 10,
|
x: 10,
|
||||||
@@ -111,9 +139,18 @@ test('overlay manager applies bounds by layer', () => {
|
|||||||
width: 3,
|
width: 3,
|
||||||
height: 4,
|
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(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', () => {
|
test('runtime-option and debug broadcasts use expected channels', () => {
|
||||||
|
|||||||
@@ -9,8 +9,11 @@ export interface OverlayManager {
|
|||||||
setMainWindow: (window: BrowserWindow | null) => void;
|
setMainWindow: (window: BrowserWindow | null) => void;
|
||||||
getInvisibleWindow: () => BrowserWindow | null;
|
getInvisibleWindow: () => BrowserWindow | null;
|
||||||
setInvisibleWindow: (window: BrowserWindow | null) => void;
|
setInvisibleWindow: (window: BrowserWindow | null) => void;
|
||||||
|
getSecondaryWindow: () => BrowserWindow | null;
|
||||||
|
setSecondaryWindow: (window: BrowserWindow | null) => void;
|
||||||
getOverlayWindow: (layer: OverlayLayer) => BrowserWindow | null;
|
getOverlayWindow: (layer: OverlayLayer) => BrowserWindow | null;
|
||||||
setOverlayWindowBounds: (layer: OverlayLayer, geometry: WindowGeometry) => void;
|
setOverlayWindowBounds: (layer: OverlayLayer, geometry: WindowGeometry) => void;
|
||||||
|
setSecondaryWindowBounds: (geometry: WindowGeometry) => void;
|
||||||
getVisibleOverlayVisible: () => boolean;
|
getVisibleOverlayVisible: () => boolean;
|
||||||
setVisibleOverlayVisible: (visible: boolean) => void;
|
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||||
getInvisibleOverlayVisible: () => boolean;
|
getInvisibleOverlayVisible: () => boolean;
|
||||||
@@ -22,6 +25,7 @@ export interface OverlayManager {
|
|||||||
export function createOverlayManager(): OverlayManager {
|
export function createOverlayManager(): OverlayManager {
|
||||||
let mainWindow: BrowserWindow | null = null;
|
let mainWindow: BrowserWindow | null = null;
|
||||||
let invisibleWindow: BrowserWindow | null = null;
|
let invisibleWindow: BrowserWindow | null = null;
|
||||||
|
let secondaryWindow: BrowserWindow | null = null;
|
||||||
let visibleOverlayVisible = false;
|
let visibleOverlayVisible = false;
|
||||||
let invisibleOverlayVisible = false;
|
let invisibleOverlayVisible = false;
|
||||||
|
|
||||||
@@ -34,10 +38,17 @@ export function createOverlayManager(): OverlayManager {
|
|||||||
setInvisibleWindow: (window) => {
|
setInvisibleWindow: (window) => {
|
||||||
invisibleWindow = window;
|
invisibleWindow = window;
|
||||||
},
|
},
|
||||||
|
getSecondaryWindow: () => secondaryWindow,
|
||||||
|
setSecondaryWindow: (window) => {
|
||||||
|
secondaryWindow = window;
|
||||||
|
},
|
||||||
getOverlayWindow: (layer) => (layer === 'visible' ? mainWindow : invisibleWindow),
|
getOverlayWindow: (layer) => (layer === 'visible' ? mainWindow : invisibleWindow),
|
||||||
setOverlayWindowBounds: (layer, geometry) => {
|
setOverlayWindowBounds: (layer, geometry) => {
|
||||||
updateOverlayWindowBounds(geometry, layer === 'visible' ? mainWindow : invisibleWindow);
|
updateOverlayWindowBounds(geometry, layer === 'visible' ? mainWindow : invisibleWindow);
|
||||||
},
|
},
|
||||||
|
setSecondaryWindowBounds: (geometry) => {
|
||||||
|
updateOverlayWindowBounds(geometry, secondaryWindow);
|
||||||
|
},
|
||||||
getVisibleOverlayVisible: () => visibleOverlayVisible,
|
getVisibleOverlayVisible: () => visibleOverlayVisible,
|
||||||
setVisibleOverlayVisible: (visible) => {
|
setVisibleOverlayVisible: (visible) => {
|
||||||
visibleOverlayVisible = visible;
|
visibleOverlayVisible = visible;
|
||||||
@@ -54,6 +65,9 @@ export function createOverlayManager(): OverlayManager {
|
|||||||
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
|
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
|
||||||
windows.push(invisibleWindow);
|
windows.push(invisibleWindow);
|
||||||
}
|
}
|
||||||
|
if (secondaryWindow && !secondaryWindow.isDestroyed()) {
|
||||||
|
windows.push(secondaryWindow);
|
||||||
|
}
|
||||||
return windows;
|
return windows;
|
||||||
},
|
},
|
||||||
broadcastToOverlayWindows: (channel, ...args) => {
|
broadcastToOverlayWindows: (channel, ...args) => {
|
||||||
@@ -64,6 +78,9 @@ export function createOverlayManager(): OverlayManager {
|
|||||||
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
|
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
|
||||||
windows.push(invisibleWindow);
|
windows.push(invisibleWindow);
|
||||||
}
|
}
|
||||||
|
if (secondaryWindow && !secondaryWindow.isDestroyed()) {
|
||||||
|
windows.push(secondaryWindow);
|
||||||
|
}
|
||||||
for (const window of windows) {
|
for (const window of windows) {
|
||||||
window.webContents.send(channel, ...args);
|
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');
|
const logger = createLogger('main:overlay-window');
|
||||||
|
|
||||||
export type OverlayWindowKind = 'visible' | 'invisible';
|
export type OverlayWindowKind = 'visible' | 'invisible' | 'secondary';
|
||||||
|
|
||||||
export function updateOverlayWindowBounds(
|
export function updateOverlayWindowBounds(
|
||||||
geometry: WindowGeometry,
|
geometry: WindowGeometry,
|
||||||
@@ -87,7 +87,7 @@ export function createOverlayWindow(
|
|||||||
|
|
||||||
window
|
window
|
||||||
.loadFile(htmlPath, {
|
.loadFile(htmlPath, {
|
||||||
query: { layer: kind === 'visible' ? 'visible' : 'invisible' },
|
query: { layer: kind },
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
logger.error('Failed to load HTML file:', err);
|
logger.error('Failed to load HTML file:', err);
|
||||||
|
|||||||
85
src/main.ts
85
src/main.ts
@@ -27,6 +27,7 @@ import {
|
|||||||
nativeImage,
|
nativeImage,
|
||||||
Tray,
|
Tray,
|
||||||
dialog,
|
dialog,
|
||||||
|
screen,
|
||||||
} from 'electron';
|
} from 'electron';
|
||||||
|
|
||||||
protocol.registerSchemesAsPrivileged([
|
protocol.registerSchemesAsPrivileged([
|
||||||
@@ -329,6 +330,7 @@ import {
|
|||||||
triggerFieldGrouping as triggerFieldGroupingCore,
|
triggerFieldGrouping as triggerFieldGroupingCore,
|
||||||
updateLastCardFromClipboard as updateLastCardFromClipboardCore,
|
updateLastCardFromClipboard as updateLastCardFromClipboardCore,
|
||||||
} from './core/services';
|
} from './core/services';
|
||||||
|
import { splitOverlayGeometryForSecondaryBar } from './core/services/overlay-window-geometry';
|
||||||
import { createAnilistUpdateQueue } from './core/services/anilist/anilist-update-queue';
|
import { createAnilistUpdateQueue } from './core/services/anilist/anilist-update-queue';
|
||||||
import {
|
import {
|
||||||
guessAnilistMediaInfo,
|
guessAnilistMediaInfo,
|
||||||
@@ -814,6 +816,7 @@ const buildConfigHotReloadAppliedMainDepsHandler = createBuildConfigHotReloadApp
|
|||||||
},
|
},
|
||||||
setSecondarySubMode: (mode) => {
|
setSecondarySubMode: (mode) => {
|
||||||
appState.secondarySubMode = mode;
|
appState.secondarySubMode = mode;
|
||||||
|
syncSecondaryOverlayWindowVisibility();
|
||||||
},
|
},
|
||||||
broadcastToOverlayWindows: (channel, payload) => {
|
broadcastToOverlayWindows: (channel, payload) => {
|
||||||
broadcastToOverlayWindows(channel, payload);
|
broadcastToOverlayWindows(channel, payload);
|
||||||
@@ -1848,6 +1851,7 @@ const { reloadConfig: reloadConfigHandler, appReadyRuntimeRunner } = composeAppR
|
|||||||
},
|
},
|
||||||
setSecondarySubMode: (mode: SecondarySubMode) => {
|
setSecondarySubMode: (mode: SecondarySubMode) => {
|
||||||
appState.secondarySubMode = mode;
|
appState.secondarySubMode = mode;
|
||||||
|
syncSecondaryOverlayWindowVisibility();
|
||||||
},
|
},
|
||||||
defaultSecondarySubMode: 'hover',
|
defaultSecondarySubMode: 'hover',
|
||||||
defaultWebsocketPort: DEFAULT_CONFIG.websocket.port,
|
defaultWebsocketPort: DEFAULT_CONFIG.websocket.port,
|
||||||
@@ -2167,10 +2171,57 @@ function updateMpvSubtitleRenderMetrics(patch: Partial<MpvSubtitleRenderMetrics>
|
|||||||
updateMpvSubtitleRenderMetricsHandler(patch);
|
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 =
|
const buildUpdateVisibleOverlayBoundsMainDepsHandler =
|
||||||
createBuildUpdateVisibleOverlayBoundsMainDepsHandler({
|
createBuildUpdateVisibleOverlayBoundsMainDepsHandler({
|
||||||
setOverlayWindowBounds: (layer, geometry) =>
|
setOverlayWindowBounds: (layer, geometry) => applyOverlayRegions(layer, geometry),
|
||||||
overlayManager.setOverlayWindowBounds(layer, geometry),
|
|
||||||
});
|
});
|
||||||
const updateVisibleOverlayBoundsMainDeps = buildUpdateVisibleOverlayBoundsMainDepsHandler();
|
const updateVisibleOverlayBoundsMainDeps = buildUpdateVisibleOverlayBoundsMainDepsHandler();
|
||||||
const updateVisibleOverlayBounds = createUpdateVisibleOverlayBoundsHandler(
|
const updateVisibleOverlayBounds = createUpdateVisibleOverlayBoundsHandler(
|
||||||
@@ -2179,8 +2230,7 @@ const updateVisibleOverlayBounds = createUpdateVisibleOverlayBoundsHandler(
|
|||||||
|
|
||||||
const buildUpdateInvisibleOverlayBoundsMainDepsHandler =
|
const buildUpdateInvisibleOverlayBoundsMainDepsHandler =
|
||||||
createBuildUpdateInvisibleOverlayBoundsMainDepsHandler({
|
createBuildUpdateInvisibleOverlayBoundsMainDepsHandler({
|
||||||
setOverlayWindowBounds: (layer, geometry) =>
|
setOverlayWindowBounds: (layer, geometry) => applyOverlayRegions(layer, geometry),
|
||||||
overlayManager.setOverlayWindowBounds(layer, geometry),
|
|
||||||
});
|
});
|
||||||
const updateInvisibleOverlayBoundsMainDeps = buildUpdateInvisibleOverlayBoundsMainDepsHandler();
|
const updateInvisibleOverlayBoundsMainDeps = buildUpdateInvisibleOverlayBoundsMainDepsHandler();
|
||||||
const updateInvisibleOverlayBounds = createUpdateInvisibleOverlayBoundsHandler(
|
const updateInvisibleOverlayBounds = createUpdateInvisibleOverlayBoundsHandler(
|
||||||
@@ -2226,12 +2276,24 @@ async function ensureYomitanExtensionLoaded(): Promise<Extension | null> {
|
|||||||
return yomitanExtensionRuntime.ensureYomitanExtensionLoaded();
|
return yomitanExtensionRuntime.ensureYomitanExtensionLoaded();
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOverlayWindow(kind: 'visible' | 'invisible'): BrowserWindow {
|
function createOverlayWindow(kind: 'visible' | 'invisible' | 'secondary'): BrowserWindow {
|
||||||
return createOverlayWindowHandler(kind);
|
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 {
|
function createMainWindow(): BrowserWindow {
|
||||||
return createMainWindowHandler();
|
const window = createMainWindowHandler();
|
||||||
|
createSecondaryWindow();
|
||||||
|
return window;
|
||||||
}
|
}
|
||||||
function createInvisibleWindow(): BrowserWindow {
|
function createInvisibleWindow(): BrowserWindow {
|
||||||
return createInvisibleWindowHandler();
|
return createInvisibleWindowHandler();
|
||||||
@@ -2339,6 +2401,7 @@ const cycleSecondarySubMode = createCycleSecondarySubModeRuntimeHandler({
|
|||||||
getSecondarySubMode: () => appState.secondarySubMode,
|
getSecondarySubMode: () => appState.secondarySubMode,
|
||||||
setSecondarySubMode: (mode: SecondarySubMode) => {
|
setSecondarySubMode: (mode: SecondarySubMode) => {
|
||||||
appState.secondarySubMode = mode;
|
appState.secondarySubMode = mode;
|
||||||
|
syncSecondaryOverlayWindowVisibility();
|
||||||
},
|
},
|
||||||
getLastSecondarySubToggleAtMs: () => appState.lastSecondarySubToggleAtMs,
|
getLastSecondarySubToggleAtMs: () => appState.lastSecondarySubToggleAtMs,
|
||||||
setLastSecondarySubToggleAtMs: (timestampMs: number) => {
|
setLastSecondarySubToggleAtMs: (timestampMs: number) => {
|
||||||
@@ -2678,6 +2741,7 @@ const {
|
|||||||
createOverlayWindow: createOverlayWindowHandler,
|
createOverlayWindow: createOverlayWindowHandler,
|
||||||
createMainWindow: createMainWindowHandler,
|
createMainWindow: createMainWindowHandler,
|
||||||
createInvisibleWindow: createInvisibleWindowHandler,
|
createInvisibleWindow: createInvisibleWindowHandler,
|
||||||
|
createSecondaryWindow: createSecondaryWindowHandler,
|
||||||
} = createOverlayWindowRuntimeHandlers<BrowserWindow>({
|
} = createOverlayWindowRuntimeHandlers<BrowserWindow>({
|
||||||
createOverlayWindowDeps: {
|
createOverlayWindowDeps: {
|
||||||
createOverlayWindowCore: (kind, options) => createOverlayWindowCore(kind, options),
|
createOverlayWindowCore: (kind, options) => createOverlayWindowCore(kind, options),
|
||||||
@@ -2689,19 +2753,24 @@ const {
|
|||||||
isOverlayVisible: (windowKind) =>
|
isOverlayVisible: (windowKind) =>
|
||||||
windowKind === 'visible'
|
windowKind === 'visible'
|
||||||
? overlayManager.getVisibleOverlayVisible()
|
? overlayManager.getVisibleOverlayVisible()
|
||||||
: overlayManager.getInvisibleOverlayVisible(),
|
: windowKind === 'invisible'
|
||||||
|
? overlayManager.getInvisibleOverlayVisible()
|
||||||
|
: false,
|
||||||
tryHandleOverlayShortcutLocalFallback: (input) =>
|
tryHandleOverlayShortcutLocalFallback: (input) =>
|
||||||
overlayShortcutsRuntime.tryHandleOverlayShortcutLocalFallback(input),
|
overlayShortcutsRuntime.tryHandleOverlayShortcutLocalFallback(input),
|
||||||
onWindowClosed: (windowKind) => {
|
onWindowClosed: (windowKind) => {
|
||||||
if (windowKind === 'visible') {
|
if (windowKind === 'visible') {
|
||||||
overlayManager.setMainWindow(null);
|
overlayManager.setMainWindow(null);
|
||||||
} else {
|
} else if (windowKind === 'invisible') {
|
||||||
overlayManager.setInvisibleWindow(null);
|
overlayManager.setInvisibleWindow(null);
|
||||||
|
} else {
|
||||||
|
overlayManager.setSecondaryWindow(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setMainWindow: (window) => overlayManager.setMainWindow(window),
|
setMainWindow: (window) => overlayManager.setMainWindow(window),
|
||||||
setInvisibleWindow: (window) => overlayManager.setInvisibleWindow(window),
|
setInvisibleWindow: (window) => overlayManager.setInvisibleWindow(window),
|
||||||
|
setSecondaryWindow: (window) => overlayManager.setSecondaryWindow(window),
|
||||||
});
|
});
|
||||||
const {
|
const {
|
||||||
resolveTrayIconPath: resolveTrayIconPathHandler,
|
resolveTrayIconPath: resolveTrayIconPathHandler,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
createBuildCreateInvisibleWindowMainDepsHandler,
|
createBuildCreateInvisibleWindowMainDepsHandler,
|
||||||
createBuildCreateMainWindowMainDepsHandler,
|
createBuildCreateMainWindowMainDepsHandler,
|
||||||
createBuildCreateOverlayWindowMainDepsHandler,
|
createBuildCreateOverlayWindowMainDepsHandler,
|
||||||
|
createBuildCreateSecondaryWindowMainDepsHandler,
|
||||||
} from './overlay-window-factory-main-deps';
|
} from './overlay-window-factory-main-deps';
|
||||||
|
|
||||||
test('overlay window factory main deps builders return mapped handlers', () => {
|
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();
|
const invisibleDeps = buildInvisibleDeps();
|
||||||
invisibleDeps.setInvisibleWindow(null);
|
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: {
|
export function createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps: {
|
||||||
createOverlayWindowCore: (
|
createOverlayWindowCore: (
|
||||||
kind: 'visible' | 'invisible',
|
kind: 'visible' | 'invisible' | 'secondary',
|
||||||
options: {
|
options: {
|
||||||
isDev: boolean;
|
isDev: boolean;
|
||||||
overlayDebugVisualizationEnabled: boolean;
|
overlayDebugVisualizationEnabled: boolean;
|
||||||
ensureOverlayWindowLevel: (window: TWindow) => void;
|
ensureOverlayWindowLevel: (window: TWindow) => void;
|
||||||
onRuntimeOptionsChanged: () => void;
|
onRuntimeOptionsChanged: () => void;
|
||||||
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
|
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
|
||||||
isOverlayVisible: (windowKind: 'visible' | 'invisible') => boolean;
|
isOverlayVisible: (windowKind: 'visible' | 'invisible' | 'secondary') => boolean;
|
||||||
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
|
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
|
||||||
onWindowClosed: (windowKind: 'visible' | 'invisible') => void;
|
onWindowClosed: (windowKind: 'visible' | 'invisible' | 'secondary') => void;
|
||||||
},
|
},
|
||||||
) => TWindow;
|
) => TWindow;
|
||||||
isDev: boolean;
|
isDev: boolean;
|
||||||
@@ -17,9 +17,9 @@ export function createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps: {
|
|||||||
ensureOverlayWindowLevel: (window: TWindow) => void;
|
ensureOverlayWindowLevel: (window: TWindow) => void;
|
||||||
onRuntimeOptionsChanged: () => void;
|
onRuntimeOptionsChanged: () => void;
|
||||||
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
|
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
|
||||||
isOverlayVisible: (windowKind: 'visible' | 'invisible') => boolean;
|
isOverlayVisible: (windowKind: 'visible' | 'invisible' | 'secondary') => boolean;
|
||||||
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
|
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
|
||||||
onWindowClosed: (windowKind: 'visible' | 'invisible') => void;
|
onWindowClosed: (windowKind: 'visible' | 'invisible' | 'secondary') => void;
|
||||||
}) {
|
}) {
|
||||||
return () => ({
|
return () => ({
|
||||||
createOverlayWindowCore: deps.createOverlayWindowCore,
|
createOverlayWindowCore: deps.createOverlayWindowCore,
|
||||||
@@ -35,7 +35,7 @@ export function createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createBuildCreateMainWindowMainDepsHandler<TWindow>(deps: {
|
export function createBuildCreateMainWindowMainDepsHandler<TWindow>(deps: {
|
||||||
createOverlayWindow: (kind: 'visible' | 'invisible') => TWindow;
|
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary') => TWindow;
|
||||||
setMainWindow: (window: TWindow | null) => void;
|
setMainWindow: (window: TWindow | null) => void;
|
||||||
}) {
|
}) {
|
||||||
return () => ({
|
return () => ({
|
||||||
@@ -45,7 +45,7 @@ export function createBuildCreateMainWindowMainDepsHandler<TWindow>(deps: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createBuildCreateInvisibleWindowMainDepsHandler<TWindow>(deps: {
|
export function createBuildCreateInvisibleWindowMainDepsHandler<TWindow>(deps: {
|
||||||
createOverlayWindow: (kind: 'visible' | 'invisible') => TWindow;
|
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary') => TWindow;
|
||||||
setInvisibleWindow: (window: TWindow | null) => void;
|
setInvisibleWindow: (window: TWindow | null) => void;
|
||||||
}) {
|
}) {
|
||||||
return () => ({
|
return () => ({
|
||||||
@@ -53,3 +53,13 @@ export function createBuildCreateInvisibleWindowMainDepsHandler<TWindow>(deps: {
|
|||||||
setInvisibleWindow: deps.setInvisibleWindow,
|
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,
|
createCreateInvisibleWindowHandler,
|
||||||
createCreateMainWindowHandler,
|
createCreateMainWindowHandler,
|
||||||
createCreateOverlayWindowHandler,
|
createCreateOverlayWindowHandler,
|
||||||
|
createCreateSecondaryWindowHandler,
|
||||||
} from './overlay-window-factory';
|
} from './overlay-window-factory';
|
||||||
|
|
||||||
test('create overlay window handler forwards options and kind', () => {
|
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.equal(createInvisibleWindow(), invisibleWindow);
|
||||||
assert.deepEqual(calls, ['create:invisible', 'set:invisible']);
|
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: {
|
export function createCreateOverlayWindowHandler<TWindow>(deps: {
|
||||||
createOverlayWindowCore: (
|
createOverlayWindowCore: (
|
||||||
@@ -58,3 +58,14 @@ export function createCreateInvisibleWindowHandler<TWindow>(deps: {
|
|||||||
return window;
|
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', () => {
|
test('overlay window runtime handlers compose create/main/invisible handlers', () => {
|
||||||
let mainWindow: { kind: string } | null = null;
|
let mainWindow: { kind: string } | null = null;
|
||||||
let invisibleWindow: { kind: string } | null = null;
|
let invisibleWindow: { kind: string } | null = null;
|
||||||
|
let secondaryWindow: { kind: string } | null = null;
|
||||||
let debugEnabled = false;
|
let debugEnabled = false;
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
|
|
||||||
@@ -28,10 +29,14 @@ test('overlay window runtime handlers compose create/main/invisible handlers', (
|
|||||||
setInvisibleWindow: (window) => {
|
setInvisibleWindow: (window) => {
|
||||||
invisibleWindow = window;
|
invisibleWindow = window;
|
||||||
},
|
},
|
||||||
|
setSecondaryWindow: (window) => {
|
||||||
|
secondaryWindow = window;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(runtime.createOverlayWindow('visible'), { kind: 'visible' });
|
assert.deepEqual(runtime.createOverlayWindow('visible'), { kind: 'visible' });
|
||||||
assert.deepEqual(runtime.createOverlayWindow('invisible'), { kind: 'invisible' });
|
assert.deepEqual(runtime.createOverlayWindow('invisible'), { kind: 'invisible' });
|
||||||
|
assert.deepEqual(runtime.createOverlayWindow('secondary'), { kind: 'secondary' });
|
||||||
|
|
||||||
assert.deepEqual(runtime.createMainWindow(), { kind: 'visible' });
|
assert.deepEqual(runtime.createMainWindow(), { kind: 'visible' });
|
||||||
assert.deepEqual(mainWindow, { 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(runtime.createInvisibleWindow(), { kind: 'invisible' });
|
||||||
assert.deepEqual(invisibleWindow, { kind: 'invisible' });
|
assert.deepEqual(invisibleWindow, { kind: 'invisible' });
|
||||||
|
|
||||||
|
assert.deepEqual(runtime.createSecondaryWindow(), { kind: 'secondary' });
|
||||||
|
assert.deepEqual(secondaryWindow, { kind: 'secondary' });
|
||||||
|
|
||||||
assert.equal(debugEnabled, false);
|
assert.equal(debugEnabled, false);
|
||||||
assert.deepEqual(calls, []);
|
assert.deepEqual(calls, []);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import {
|
|||||||
createCreateInvisibleWindowHandler,
|
createCreateInvisibleWindowHandler,
|
||||||
createCreateMainWindowHandler,
|
createCreateMainWindowHandler,
|
||||||
createCreateOverlayWindowHandler,
|
createCreateOverlayWindowHandler,
|
||||||
|
createCreateSecondaryWindowHandler,
|
||||||
} from './overlay-window-factory';
|
} from './overlay-window-factory';
|
||||||
import {
|
import {
|
||||||
createBuildCreateInvisibleWindowMainDepsHandler,
|
createBuildCreateInvisibleWindowMainDepsHandler,
|
||||||
createBuildCreateMainWindowMainDepsHandler,
|
createBuildCreateMainWindowMainDepsHandler,
|
||||||
createBuildCreateOverlayWindowMainDepsHandler,
|
createBuildCreateOverlayWindowMainDepsHandler,
|
||||||
|
createBuildCreateSecondaryWindowMainDepsHandler,
|
||||||
} from './overlay-window-factory-main-deps';
|
} from './overlay-window-factory-main-deps';
|
||||||
|
|
||||||
type CreateOverlayWindowMainDeps<TWindow> = Parameters<
|
type CreateOverlayWindowMainDeps<TWindow> = Parameters<
|
||||||
@@ -17,6 +19,7 @@ export function createOverlayWindowRuntimeHandlers<TWindow>(deps: {
|
|||||||
createOverlayWindowDeps: CreateOverlayWindowMainDeps<TWindow>;
|
createOverlayWindowDeps: CreateOverlayWindowMainDeps<TWindow>;
|
||||||
setMainWindow: (window: TWindow | null) => void;
|
setMainWindow: (window: TWindow | null) => void;
|
||||||
setInvisibleWindow: (window: TWindow | null) => void;
|
setInvisibleWindow: (window: TWindow | null) => void;
|
||||||
|
setSecondaryWindow: (window: TWindow | null) => void;
|
||||||
}) {
|
}) {
|
||||||
const createOverlayWindow = createCreateOverlayWindowHandler<TWindow>(
|
const createOverlayWindow = createCreateOverlayWindowHandler<TWindow>(
|
||||||
createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps.createOverlayWindowDeps)(),
|
createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps.createOverlayWindowDeps)(),
|
||||||
@@ -33,10 +36,17 @@ export function createOverlayWindowRuntimeHandlers<TWindow>(deps: {
|
|||||||
setInvisibleWindow: (window) => deps.setInvisibleWindow(window),
|
setInvisibleWindow: (window) => deps.setInvisibleWindow(window),
|
||||||
})(),
|
})(),
|
||||||
);
|
);
|
||||||
|
const createSecondaryWindow = createCreateSecondaryWindowHandler<TWindow>(
|
||||||
|
createBuildCreateSecondaryWindowMainDepsHandler<TWindow>({
|
||||||
|
createOverlayWindow: (kind) => createOverlayWindow(kind),
|
||||||
|
setSecondaryWindow: (window) => deps.setSecondaryWindow(window),
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createOverlayWindow,
|
createOverlayWindow,
|
||||||
createMainWindow,
|
createMainWindow,
|
||||||
createInvisibleWindow,
|
createInvisibleWindow,
|
||||||
|
createSecondaryWindow,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,9 @@ import { IPC_CHANNELS } from './shared/ipc/contracts';
|
|||||||
const overlayLayerArg = process.argv.find((arg) => arg.startsWith('--overlay-layer='));
|
const overlayLayerArg = process.argv.find((arg) => arg.startsWith('--overlay-layer='));
|
||||||
const overlayLayerFromArg = overlayLayerArg?.slice('--overlay-layer='.length);
|
const overlayLayerFromArg = overlayLayerArg?.slice('--overlay-layer='.length);
|
||||||
const overlayLayer =
|
const overlayLayer =
|
||||||
overlayLayerFromArg === 'visible' || overlayLayerFromArg === 'invisible'
|
overlayLayerFromArg === 'visible' ||
|
||||||
|
overlayLayerFromArg === 'invisible' ||
|
||||||
|
overlayLayerFromArg === 'secondary'
|
||||||
? overlayLayerFromArg
|
? overlayLayerFromArg
|
||||||
: null;
|
: 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;
|
isOverlayInteractive: boolean;
|
||||||
isOverSubtitle: boolean;
|
isOverSubtitle: boolean;
|
||||||
invisiblePositionEditMode: boolean;
|
invisiblePositionEditMode: boolean;
|
||||||
overlayLayer: 'visible' | 'invisible';
|
overlayLayer: 'visible' | 'invisible' | 'secondary';
|
||||||
};
|
};
|
||||||
|
|
||||||
type NormalizedRendererError = {
|
type NormalizedRendererError = {
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import type { RendererContext } from './context';
|
|||||||
|
|
||||||
const MEASUREMENT_DEBOUNCE_MS = 80;
|
const MEASUREMENT_DEBOUNCE_MS = 80;
|
||||||
|
|
||||||
|
function isMeasurableOverlayLayer(layer: string): layer is 'visible' | 'invisible' {
|
||||||
|
return layer === 'visible' || layer === 'invisible';
|
||||||
|
}
|
||||||
|
|
||||||
function round2(value: number): number {
|
function round2(value: number): number {
|
||||||
return Math.round(value * 100) / 100;
|
return Math.round(value * 100) / 100;
|
||||||
}
|
}
|
||||||
@@ -78,6 +82,10 @@ export function createOverlayContentMeasurementReporter(ctx: RendererContext) {
|
|||||||
let debounceTimer: number | null = null;
|
let debounceTimer: number | null = null;
|
||||||
|
|
||||||
function emitNow(): void {
|
function emitNow(): void {
|
||||||
|
if (!isMeasurableOverlayLayer(ctx.platform.overlayLayer)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const measurement: OverlayContentMeasurement = {
|
const measurement: OverlayContentMeasurement = {
|
||||||
layer: ctx.platform.overlayLayer,
|
layer: ctx.platform.overlayLayer,
|
||||||
measuredAtMs: Date.now(),
|
measuredAtMs: Date.now(),
|
||||||
|
|||||||
@@ -533,6 +533,40 @@ body.layer-invisible.invisible-position-edit #subtitleRoot .c {
|
|||||||
pointer-events: auto;
|
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 {
|
#secondarySubRoot {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
@@ -548,6 +582,10 @@ body.layer-invisible.invisible-position-edit #subtitleRoot .c {
|
|||||||
cursor: text;
|
cursor: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.layer-secondary #secondarySubRoot {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
#secondarySubRoot:empty {
|
#secondarySubRoot:empty {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -591,6 +629,11 @@ body.settings-modal-open #secondarySubContainer {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.layer-secondary #secondarySubContainer.secondary-sub-hover {
|
||||||
|
padding: 8px 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
iframe[id^='yomitan-popup'] {
|
iframe[id^='yomitan-popup'] {
|
||||||
pointer-events: auto !important;
|
pointer-events: auto !important;
|
||||||
z-index: 2147483647 !important;
|
z-index: 2147483647 !important;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
export type OverlayLayer = 'visible' | 'invisible';
|
export type OverlayLayer = 'visible' | 'invisible' | 'secondary';
|
||||||
|
|
||||||
export type PlatformInfo = {
|
export type PlatformInfo = {
|
||||||
overlayLayer: OverlayLayer;
|
overlayLayer: OverlayLayer;
|
||||||
isInvisibleLayer: boolean;
|
isInvisibleLayer: boolean;
|
||||||
|
isSecondaryLayer: boolean;
|
||||||
isLinuxPlatform: boolean;
|
isLinuxPlatform: boolean;
|
||||||
isMacOSPlatform: boolean;
|
isMacOSPlatform: boolean;
|
||||||
shouldToggleMouseIgnore: boolean;
|
shouldToggleMouseIgnore: boolean;
|
||||||
@@ -15,15 +16,20 @@ export function resolvePlatformInfo(): PlatformInfo {
|
|||||||
const overlayLayerFromPreload = window.electronAPI.getOverlayLayer();
|
const overlayLayerFromPreload = window.electronAPI.getOverlayLayer();
|
||||||
const queryLayer = new URLSearchParams(window.location.search).get('layer');
|
const queryLayer = new URLSearchParams(window.location.search).get('layer');
|
||||||
const overlayLayerFromQuery: OverlayLayer | null =
|
const overlayLayerFromQuery: OverlayLayer | null =
|
||||||
queryLayer === 'visible' || queryLayer === 'invisible' ? queryLayer : null;
|
queryLayer === 'visible' || queryLayer === 'invisible' || queryLayer === 'secondary'
|
||||||
|
? queryLayer
|
||||||
|
: null;
|
||||||
|
|
||||||
const overlayLayer: OverlayLayer =
|
const overlayLayer: OverlayLayer =
|
||||||
overlayLayerFromQuery ??
|
overlayLayerFromQuery ??
|
||||||
(overlayLayerFromPreload === 'visible' || overlayLayerFromPreload === 'invisible'
|
(overlayLayerFromPreload === 'visible' ||
|
||||||
|
overlayLayerFromPreload === 'invisible' ||
|
||||||
|
overlayLayerFromPreload === 'secondary'
|
||||||
? overlayLayerFromPreload
|
? overlayLayerFromPreload
|
||||||
: 'visible');
|
: 'visible');
|
||||||
|
|
||||||
const isInvisibleLayer = overlayLayer === 'invisible';
|
const isInvisibleLayer = overlayLayer === 'invisible';
|
||||||
|
const isSecondaryLayer = overlayLayer === 'secondary';
|
||||||
const isLinuxPlatform = navigator.platform.toLowerCase().includes('linux');
|
const isLinuxPlatform = navigator.platform.toLowerCase().includes('linux');
|
||||||
const isMacOSPlatform =
|
const isMacOSPlatform =
|
||||||
navigator.platform.toLowerCase().includes('mac') || /mac/i.test(navigator.userAgent);
|
navigator.platform.toLowerCase().includes('mac') || /mac/i.test(navigator.userAgent);
|
||||||
@@ -31,9 +37,10 @@ export function resolvePlatformInfo(): PlatformInfo {
|
|||||||
return {
|
return {
|
||||||
overlayLayer,
|
overlayLayer,
|
||||||
isInvisibleLayer,
|
isInvisibleLayer,
|
||||||
|
isSecondaryLayer,
|
||||||
isLinuxPlatform,
|
isLinuxPlatform,
|
||||||
isMacOSPlatform,
|
isMacOSPlatform,
|
||||||
shouldToggleMouseIgnore: !isLinuxPlatform,
|
shouldToggleMouseIgnore: !isLinuxPlatform && !isSecondaryLayer,
|
||||||
invisiblePositionEditToggleCode: 'KeyP',
|
invisiblePositionEditToggleCode: 'KeyP',
|
||||||
invisiblePositionStepPx: 1,
|
invisiblePositionStepPx: 1,
|
||||||
invisiblePositionStepFastPx: 4,
|
invisiblePositionStepFastPx: 4,
|
||||||
|
|||||||
@@ -728,7 +728,7 @@ export interface SubtitleHoverTokenPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ElectronAPI {
|
export interface ElectronAPI {
|
||||||
getOverlayLayer: () => 'visible' | 'invisible' | null;
|
getOverlayLayer: () => 'visible' | 'invisible' | 'secondary' | null;
|
||||||
onSubtitle: (callback: (data: SubtitleData) => void) => void;
|
onSubtitle: (callback: (data: SubtitleData) => void) => void;
|
||||||
onVisibility: (callback: (visible: boolean) => void) => void;
|
onVisibility: (callback: (visible: boolean) => void) => void;
|
||||||
onSubtitlePosition: (callback: (position: SubtitlePosition | null) => void) => void;
|
onSubtitlePosition: (callback: (position: SubtitlePosition | null) => void) => void;
|
||||||
|
|||||||
Reference in New Issue
Block a user