mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
b989508ece
|
|||
|
978cb8c401
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "subminer",
|
"name": "subminer",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"description": "All-in-one sentence mining overlay with AnkiConnect and dictionary integration",
|
"description": "All-in-one sentence mining overlay with AnkiConnect and dictionary integration",
|
||||||
"packageManager": "bun@1.3.5",
|
"packageManager": "bun@1.3.5",
|
||||||
"main": "dist/main-entry.js",
|
"main": "dist/main-entry.js",
|
||||||
|
|||||||
@@ -233,5 +233,6 @@ test('registerIpcHandlers ignores malformed fire-and-forget payloads', () => {
|
|||||||
|
|
||||||
handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'not-a-modal');
|
handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'not-a-modal');
|
||||||
handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'subsync');
|
handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'subsync');
|
||||||
assert.deepEqual(modals, ['subsync']);
|
handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'kiku');
|
||||||
|
assert.deepEqual(modals, ['subsync', 'kiku']);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ export function createFieldGroupingCallbackRuntime<T extends string>(options: {
|
|||||||
getResolver: options.getResolver,
|
getResolver: options.getResolver,
|
||||||
setResolver: options.setResolver,
|
setResolver: options.setResolver,
|
||||||
sendRequestToVisibleOverlay: (data) =>
|
sendRequestToVisibleOverlay: (data) =>
|
||||||
options.sendToVisibleOverlay('kiku:field-grouping-request', data),
|
options.sendToVisibleOverlay('kiku:field-grouping-request', data, {
|
||||||
|
restoreOnModalClose: 'kiku' as T,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ test('overlay manager initializes with empty windows and hidden overlays', () =>
|
|||||||
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.getSecondaryWindow(), null);
|
||||||
|
assert.equal(manager.getModalWindow(), 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(), []);
|
||||||
@@ -27,14 +28,19 @@ test('overlay manager stores window references and returns stable window order',
|
|||||||
const secondaryWindow = {
|
const secondaryWindow = {
|
||||||
isDestroyed: () => false,
|
isDestroyed: () => false,
|
||||||
} as unknown as Electron.BrowserWindow;
|
} as unknown as Electron.BrowserWindow;
|
||||||
|
const modalWindow = {
|
||||||
|
isDestroyed: () => false,
|
||||||
|
} as unknown as Electron.BrowserWindow;
|
||||||
|
|
||||||
manager.setMainWindow(visibleWindow);
|
manager.setMainWindow(visibleWindow);
|
||||||
manager.setInvisibleWindow(invisibleWindow);
|
manager.setInvisibleWindow(invisibleWindow);
|
||||||
manager.setSecondaryWindow(secondaryWindow);
|
manager.setSecondaryWindow(secondaryWindow);
|
||||||
|
manager.setModalWindow(modalWindow);
|
||||||
|
|
||||||
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.getSecondaryWindow(), secondaryWindow);
|
||||||
|
assert.equal(manager.getModalWindow(), modalWindow);
|
||||||
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, secondaryWindow]);
|
assert.deepEqual(manager.getOverlayWindows(), [visibleWindow, invisibleWindow, secondaryWindow]);
|
||||||
@@ -51,6 +57,9 @@ test('overlay manager excludes destroyed windows', () => {
|
|||||||
manager.setSecondaryWindow({
|
manager.setSecondaryWindow({
|
||||||
isDestroyed: () => true,
|
isDestroyed: () => true,
|
||||||
} as unknown as Electron.BrowserWindow);
|
} as unknown as Electron.BrowserWindow);
|
||||||
|
manager.setModalWindow({
|
||||||
|
isDestroyed: () => false,
|
||||||
|
} as unknown as Electron.BrowserWindow);
|
||||||
|
|
||||||
assert.equal(manager.getOverlayWindows().length, 1);
|
assert.equal(manager.getOverlayWindows().length, 1);
|
||||||
});
|
});
|
||||||
@@ -93,6 +102,10 @@ test('overlay manager broadcasts to non-destroyed windows', () => {
|
|||||||
manager.setMainWindow(aliveWindow);
|
manager.setMainWindow(aliveWindow);
|
||||||
manager.setInvisibleWindow(deadWindow);
|
manager.setInvisibleWindow(deadWindow);
|
||||||
manager.setSecondaryWindow(secondaryWindow);
|
manager.setSecondaryWindow(secondaryWindow);
|
||||||
|
manager.setModalWindow({
|
||||||
|
isDestroyed: () => false,
|
||||||
|
webContents: { send: () => {} },
|
||||||
|
} as unknown as Electron.BrowserWindow);
|
||||||
manager.broadcastToOverlayWindows('x', 1, 'a');
|
manager.broadcastToOverlayWindows('x', 1, 'a');
|
||||||
|
|
||||||
assert.deepEqual(calls, [
|
assert.deepEqual(calls, [
|
||||||
@@ -123,9 +136,17 @@ test('overlay manager applies bounds by layer', () => {
|
|||||||
invisibleCalls.push(bounds);
|
invisibleCalls.push(bounds);
|
||||||
},
|
},
|
||||||
} as unknown as Electron.BrowserWindow;
|
} as unknown as Electron.BrowserWindow;
|
||||||
|
const modalCalls: Electron.Rectangle[] = [];
|
||||||
|
const modalWindow = {
|
||||||
|
isDestroyed: () => false,
|
||||||
|
setBounds: (bounds: Electron.Rectangle) => {
|
||||||
|
modalCalls.push(bounds);
|
||||||
|
},
|
||||||
|
} as unknown as Electron.BrowserWindow;
|
||||||
manager.setMainWindow(visibleWindow);
|
manager.setMainWindow(visibleWindow);
|
||||||
manager.setInvisibleWindow(invisibleWindow);
|
manager.setInvisibleWindow(invisibleWindow);
|
||||||
manager.setSecondaryWindow(secondaryWindow);
|
manager.setSecondaryWindow(secondaryWindow);
|
||||||
|
manager.setModalWindow(modalWindow);
|
||||||
|
|
||||||
manager.setOverlayWindowBounds('visible', {
|
manager.setOverlayWindowBounds('visible', {
|
||||||
x: 10,
|
x: 10,
|
||||||
@@ -145,12 +166,19 @@ test('overlay manager applies bounds by layer', () => {
|
|||||||
width: 10,
|
width: 10,
|
||||||
height: 11,
|
height: 11,
|
||||||
});
|
});
|
||||||
|
manager.setModalWindowBounds({
|
||||||
|
x: 80,
|
||||||
|
y: 90,
|
||||||
|
width: 100,
|
||||||
|
height: 110,
|
||||||
|
});
|
||||||
|
|
||||||
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, [
|
assert.deepEqual(invisibleCalls, [
|
||||||
{ x: 1, y: 2, width: 3, height: 4 },
|
{ x: 1, y: 2, width: 3, height: 4 },
|
||||||
{ x: 8, y: 9, width: 10, height: 11 },
|
{ x: 8, y: 9, width: 10, height: 11 },
|
||||||
]);
|
]);
|
||||||
|
assert.deepEqual(modalCalls, [{ x: 80, y: 90, width: 100, height: 110 }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('runtime-option and debug broadcasts use expected channels', () => {
|
test('runtime-option and debug broadcasts use expected channels', () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BrowserWindow } from 'electron';
|
import type { BrowserWindow } from 'electron';
|
||||||
import { RuntimeOptionState, WindowGeometry } from '../../types';
|
import { RuntimeOptionState, WindowGeometry } from '../../types';
|
||||||
import { updateOverlayWindowBounds } from './overlay-window';
|
import { updateOverlayWindowBounds } from './overlay-window';
|
||||||
|
|
||||||
@@ -11,9 +11,12 @@ export interface OverlayManager {
|
|||||||
setInvisibleWindow: (window: BrowserWindow | null) => void;
|
setInvisibleWindow: (window: BrowserWindow | null) => void;
|
||||||
getSecondaryWindow: () => BrowserWindow | null;
|
getSecondaryWindow: () => BrowserWindow | null;
|
||||||
setSecondaryWindow: (window: BrowserWindow | null) => void;
|
setSecondaryWindow: (window: BrowserWindow | null) => void;
|
||||||
|
getModalWindow: () => BrowserWindow | null;
|
||||||
|
setModalWindow: (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;
|
setSecondaryWindowBounds: (geometry: WindowGeometry) => void;
|
||||||
|
setModalWindowBounds: (geometry: WindowGeometry) => void;
|
||||||
getVisibleOverlayVisible: () => boolean;
|
getVisibleOverlayVisible: () => boolean;
|
||||||
setVisibleOverlayVisible: (visible: boolean) => void;
|
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||||
getInvisibleOverlayVisible: () => boolean;
|
getInvisibleOverlayVisible: () => boolean;
|
||||||
@@ -26,6 +29,7 @@ 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 secondaryWindow: BrowserWindow | null = null;
|
||||||
|
let modalWindow: BrowserWindow | null = null;
|
||||||
let visibleOverlayVisible = false;
|
let visibleOverlayVisible = false;
|
||||||
let invisibleOverlayVisible = false;
|
let invisibleOverlayVisible = false;
|
||||||
|
|
||||||
@@ -42,6 +46,10 @@ export function createOverlayManager(): OverlayManager {
|
|||||||
setSecondaryWindow: (window) => {
|
setSecondaryWindow: (window) => {
|
||||||
secondaryWindow = window;
|
secondaryWindow = window;
|
||||||
},
|
},
|
||||||
|
getModalWindow: () => modalWindow,
|
||||||
|
setModalWindow: (window) => {
|
||||||
|
modalWindow = 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);
|
||||||
@@ -49,6 +57,9 @@ export function createOverlayManager(): OverlayManager {
|
|||||||
setSecondaryWindowBounds: (geometry) => {
|
setSecondaryWindowBounds: (geometry) => {
|
||||||
updateOverlayWindowBounds(geometry, secondaryWindow);
|
updateOverlayWindowBounds(geometry, secondaryWindow);
|
||||||
},
|
},
|
||||||
|
setModalWindowBounds: (geometry) => {
|
||||||
|
updateOverlayWindowBounds(geometry, modalWindow);
|
||||||
|
},
|
||||||
getVisibleOverlayVisible: () => visibleOverlayVisible,
|
getVisibleOverlayVisible: () => visibleOverlayVisible,
|
||||||
setVisibleOverlayVisible: (visible) => {
|
setVisibleOverlayVisible: (visible) => {
|
||||||
visibleOverlayVisible = visible;
|
visibleOverlayVisible = visible;
|
||||||
|
|||||||
@@ -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' | 'secondary';
|
export type OverlayWindowKind = 'visible' | 'invisible' | 'secondary' | 'modal';
|
||||||
|
|
||||||
export function updateOverlayWindowBounds(
|
export function updateOverlayWindowBounds(
|
||||||
geometry: WindowGeometry,
|
geometry: WindowGeometry,
|
||||||
@@ -71,6 +71,7 @@ export function createOverlayWindow(
|
|||||||
resizable: false,
|
resizable: false,
|
||||||
hasShadow: false,
|
hasShadow: false,
|
||||||
focusable: true,
|
focusable: true,
|
||||||
|
acceptFirstMouse: true,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, '..', '..', 'preload.js'),
|
preload: path.join(__dirname, '..', '..', 'preload.js'),
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
@@ -115,6 +116,7 @@ export function createOverlayWindow(
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.webContents.on('before-input-event', (event, input) => {
|
window.webContents.on('before-input-event', (event, input) => {
|
||||||
|
if (kind === 'modal') return;
|
||||||
if (!options.isOverlayVisible(kind)) return;
|
if (!options.isOverlayVisible(kind)) return;
|
||||||
if (!options.tryHandleOverlayShortcutLocalFallback(input)) return;
|
if (!options.tryHandleOverlayShortcutLocalFallback(input)) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
53
src/main.ts
53
src/main.ts
@@ -567,6 +567,26 @@ process.on('SIGTERM', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const overlayManager = createOverlayManager();
|
const overlayManager = createOverlayManager();
|
||||||
|
let overlayModalInputExclusive = false;
|
||||||
|
let syncOverlayShortcutsForModal: (isActive: boolean) => void = () => {};
|
||||||
|
|
||||||
|
const handleModalInputStateChange = (isActive: boolean): void => {
|
||||||
|
if (overlayModalInputExclusive === isActive) return;
|
||||||
|
overlayModalInputExclusive = isActive;
|
||||||
|
if (isActive) {
|
||||||
|
const modalWindow = overlayManager.getModalWindow();
|
||||||
|
if (modalWindow && !modalWindow.isDestroyed()) {
|
||||||
|
modalWindow.setIgnoreMouseEvents(false);
|
||||||
|
modalWindow.setAlwaysOnTop(true, 'screen-saver', 1);
|
||||||
|
modalWindow.focus();
|
||||||
|
if (!modalWindow.webContents.isFocused()) {
|
||||||
|
modalWindow.webContents.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
syncOverlayShortcutsForModal(isActive);
|
||||||
|
};
|
||||||
|
|
||||||
const buildOverlayContentMeasurementStoreMainDepsHandler =
|
const buildOverlayContentMeasurementStoreMainDepsHandler =
|
||||||
createBuildOverlayContentMeasurementStoreMainDepsHandler({
|
createBuildOverlayContentMeasurementStoreMainDepsHandler({
|
||||||
now: () => Date.now(),
|
now: () => Date.now(),
|
||||||
@@ -575,6 +595,10 @@ const buildOverlayContentMeasurementStoreMainDepsHandler =
|
|||||||
const buildOverlayModalRuntimeMainDepsHandler = createBuildOverlayModalRuntimeMainDepsHandler({
|
const buildOverlayModalRuntimeMainDepsHandler = createBuildOverlayModalRuntimeMainDepsHandler({
|
||||||
getMainWindow: () => overlayManager.getMainWindow(),
|
getMainWindow: () => overlayManager.getMainWindow(),
|
||||||
getInvisibleWindow: () => overlayManager.getInvisibleWindow(),
|
getInvisibleWindow: () => overlayManager.getInvisibleWindow(),
|
||||||
|
getModalWindow: () => overlayManager.getModalWindow(),
|
||||||
|
createModalWindow: () => createModalWindow(),
|
||||||
|
getModalGeometry: () => getCurrentOverlayGeometry(),
|
||||||
|
setModalWindowBounds: (geometry) => overlayManager.setModalWindowBounds(geometry),
|
||||||
});
|
});
|
||||||
const overlayContentMeasurementStoreMainDeps = buildOverlayContentMeasurementStoreMainDepsHandler();
|
const overlayContentMeasurementStoreMainDeps = buildOverlayContentMeasurementStoreMainDepsHandler();
|
||||||
const overlayContentMeasurementStore = createOverlayContentMeasurementStore(
|
const overlayContentMeasurementStore = createOverlayContentMeasurementStore(
|
||||||
@@ -582,6 +606,9 @@ const overlayContentMeasurementStore = createOverlayContentMeasurementStore(
|
|||||||
);
|
);
|
||||||
const overlayModalRuntime = createOverlayModalRuntimeService(
|
const overlayModalRuntime = createOverlayModalRuntimeService(
|
||||||
buildOverlayModalRuntimeMainDepsHandler(),
|
buildOverlayModalRuntimeMainDepsHandler(),
|
||||||
|
{
|
||||||
|
onModalStateChange: (isActive: boolean) => handleModalInputStateChange(isActive),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
const appState = createAppState({
|
const appState = createAppState({
|
||||||
mpvSocketPath: getDefaultSocketPath(),
|
mpvSocketPath: getDefaultSocketPath(),
|
||||||
@@ -789,6 +816,13 @@ const overlayShortcutsRuntime = createOverlayShortcutsRuntimeService(
|
|||||||
},
|
},
|
||||||
})(),
|
})(),
|
||||||
);
|
);
|
||||||
|
syncOverlayShortcutsForModal = (isActive: boolean): void => {
|
||||||
|
if (isActive) {
|
||||||
|
overlayShortcutsRuntime.unregisterOverlayShortcuts();
|
||||||
|
} else {
|
||||||
|
overlayShortcutsRuntime.syncOverlayShortcuts();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const buildConfigHotReloadMessageMainDepsHandler = createBuildConfigHotReloadMessageMainDepsHandler(
|
const buildConfigHotReloadMessageMainDepsHandler = createBuildConfigHotReloadMessageMainDepsHandler(
|
||||||
{
|
{
|
||||||
@@ -2216,6 +2250,7 @@ function applyOverlayRegions(layer: 'visible' | 'invisible', geometry: WindowGeo
|
|||||||
const regions = splitOverlayGeometryForSecondaryBar(geometry);
|
const regions = splitOverlayGeometryForSecondaryBar(geometry);
|
||||||
overlayManager.setOverlayWindowBounds(layer, regions.primary);
|
overlayManager.setOverlayWindowBounds(layer, regions.primary);
|
||||||
overlayManager.setSecondaryWindowBounds(regions.secondary);
|
overlayManager.setSecondaryWindowBounds(regions.secondary);
|
||||||
|
overlayManager.setModalWindowBounds(geometry);
|
||||||
syncSecondaryOverlayWindowVisibility();
|
syncSecondaryOverlayWindowVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2276,10 +2311,20 @@ async function ensureYomitanExtensionLoaded(): Promise<Extension | null> {
|
|||||||
return yomitanExtensionRuntime.ensureYomitanExtensionLoaded();
|
return yomitanExtensionRuntime.ensureYomitanExtensionLoaded();
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOverlayWindow(kind: 'visible' | 'invisible' | 'secondary'): BrowserWindow {
|
function createOverlayWindow(kind: 'visible' | 'invisible' | 'secondary' | 'modal'): BrowserWindow {
|
||||||
return createOverlayWindowHandler(kind);
|
return createOverlayWindowHandler(kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createModalWindow(): BrowserWindow {
|
||||||
|
const existingWindow = overlayManager.getModalWindow();
|
||||||
|
if (existingWindow && !existingWindow.isDestroyed()) {
|
||||||
|
return existingWindow;
|
||||||
|
}
|
||||||
|
const window = createModalWindowHandler();
|
||||||
|
overlayManager.setModalWindowBounds(getCurrentOverlayGeometry());
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
|
||||||
function createSecondaryWindow(): BrowserWindow {
|
function createSecondaryWindow(): BrowserWindow {
|
||||||
const existingWindow = overlayManager.getSecondaryWindow();
|
const existingWindow = overlayManager.getSecondaryWindow();
|
||||||
if (existingWindow && !existingWindow.isDestroyed()) {
|
if (existingWindow && !existingWindow.isDestroyed()) {
|
||||||
@@ -2742,6 +2787,7 @@ const {
|
|||||||
createMainWindow: createMainWindowHandler,
|
createMainWindow: createMainWindowHandler,
|
||||||
createInvisibleWindow: createInvisibleWindowHandler,
|
createInvisibleWindow: createInvisibleWindowHandler,
|
||||||
createSecondaryWindow: createSecondaryWindowHandler,
|
createSecondaryWindow: createSecondaryWindowHandler,
|
||||||
|
createModalWindow: createModalWindowHandler,
|
||||||
} = createOverlayWindowRuntimeHandlers<BrowserWindow>({
|
} = createOverlayWindowRuntimeHandlers<BrowserWindow>({
|
||||||
createOverlayWindowDeps: {
|
createOverlayWindowDeps: {
|
||||||
createOverlayWindowCore: (kind, options) => createOverlayWindowCore(kind, options),
|
createOverlayWindowCore: (kind, options) => createOverlayWindowCore(kind, options),
|
||||||
@@ -2763,14 +2809,17 @@ const {
|
|||||||
overlayManager.setMainWindow(null);
|
overlayManager.setMainWindow(null);
|
||||||
} else if (windowKind === 'invisible') {
|
} else if (windowKind === 'invisible') {
|
||||||
overlayManager.setInvisibleWindow(null);
|
overlayManager.setInvisibleWindow(null);
|
||||||
} else {
|
} else if (windowKind === 'secondary') {
|
||||||
overlayManager.setSecondaryWindow(null);
|
overlayManager.setSecondaryWindow(null);
|
||||||
|
} else {
|
||||||
|
overlayManager.setModalWindow(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),
|
setSecondaryWindow: (window) => overlayManager.setSecondaryWindow(window),
|
||||||
|
setModalWindow: (window) => overlayManager.setModalWindow(window),
|
||||||
});
|
});
|
||||||
const {
|
const {
|
||||||
resolveTrayIconPath: resolveTrayIconPathHandler,
|
resolveTrayIconPath: resolveTrayIconPathHandler,
|
||||||
|
|||||||
218
src/main/overlay-runtime.test.ts
Normal file
218
src/main/overlay-runtime.test.ts
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { createOverlayModalRuntimeService } from './overlay-runtime';
|
||||||
|
|
||||||
|
type MockWindow = {
|
||||||
|
destroyed: boolean;
|
||||||
|
visible: boolean;
|
||||||
|
focused: boolean;
|
||||||
|
ignoreMouseEvents: boolean;
|
||||||
|
webContentsFocused: boolean;
|
||||||
|
showCount: number;
|
||||||
|
hideCount: number;
|
||||||
|
sent: unknown[][];
|
||||||
|
loading: boolean;
|
||||||
|
loadCallbacks: Array<() => void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function createMockWindow(): MockWindow & {
|
||||||
|
isDestroyed: () => boolean;
|
||||||
|
isVisible: () => boolean;
|
||||||
|
isFocused: () => boolean;
|
||||||
|
setIgnoreMouseEvents: (ignore: boolean) => void;
|
||||||
|
getShowCount: () => number;
|
||||||
|
getHideCount: () => number;
|
||||||
|
show: () => void;
|
||||||
|
hide: () => void;
|
||||||
|
focus: () => void;
|
||||||
|
webContents: {
|
||||||
|
focused: boolean;
|
||||||
|
isLoading: () => boolean;
|
||||||
|
send: (channel: string, payload?: unknown) => void;
|
||||||
|
isFocused: () => boolean;
|
||||||
|
once: (event: 'did-finish-load', cb: () => void) => void;
|
||||||
|
focus: () => void;
|
||||||
|
};
|
||||||
|
} {
|
||||||
|
const state: MockWindow = {
|
||||||
|
destroyed: false,
|
||||||
|
visible: false,
|
||||||
|
focused: false,
|
||||||
|
ignoreMouseEvents: false,
|
||||||
|
webContentsFocused: false,
|
||||||
|
showCount: 0,
|
||||||
|
hideCount: 0,
|
||||||
|
sent: [],
|
||||||
|
loading: false,
|
||||||
|
loadCallbacks: [],
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isDestroyed: () => state.destroyed,
|
||||||
|
isVisible: () => state.visible,
|
||||||
|
isFocused: () => state.focused,
|
||||||
|
setIgnoreMouseEvents: (ignore: boolean) => {
|
||||||
|
state.ignoreMouseEvents = ignore;
|
||||||
|
},
|
||||||
|
getShowCount: () => state.showCount,
|
||||||
|
getHideCount: () => state.hideCount,
|
||||||
|
show: () => {
|
||||||
|
state.visible = true;
|
||||||
|
state.showCount += 1;
|
||||||
|
},
|
||||||
|
hide: () => {
|
||||||
|
state.visible = false;
|
||||||
|
state.hideCount += 1;
|
||||||
|
},
|
||||||
|
focus: () => {
|
||||||
|
state.focused = true;
|
||||||
|
},
|
||||||
|
webContents: {
|
||||||
|
isLoading: () => state.loading,
|
||||||
|
send: (channel, payload) => {
|
||||||
|
if (payload === undefined) {
|
||||||
|
state.sent.push([channel]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.sent.push([channel, payload]);
|
||||||
|
},
|
||||||
|
focused: false,
|
||||||
|
isFocused: () => state.webContentsFocused,
|
||||||
|
once: (_event, cb) => {
|
||||||
|
state.loadCallbacks.push(cb);
|
||||||
|
},
|
||||||
|
focus: () => {
|
||||||
|
state.webContentsFocused = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('sendToActiveOverlayWindow targets modal window with full geometry and tracks close restore', () => {
|
||||||
|
const window = createMockWindow();
|
||||||
|
const calls: string[] = [];
|
||||||
|
const runtime = createOverlayModalRuntimeService({
|
||||||
|
getMainWindow: () => null,
|
||||||
|
getInvisibleWindow: () => null,
|
||||||
|
getModalWindow: () => window as never,
|
||||||
|
createModalWindow: () => {
|
||||||
|
calls.push('create-modal-window');
|
||||||
|
return window as never;
|
||||||
|
},
|
||||||
|
getModalGeometry: () => ({ x: 10, y: 20, width: 300, height: 200 }),
|
||||||
|
setModalWindowBounds: (geometry) => {
|
||||||
|
calls.push(`bounds:${geometry.x},${geometry.y},${geometry.width},${geometry.height}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const sent = runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||||
|
restoreOnModalClose: 'runtime-options',
|
||||||
|
});
|
||||||
|
assert.equal(sent, true);
|
||||||
|
assert.equal(runtime.getRestoreVisibleOverlayOnModalClose().has('runtime-options'), true);
|
||||||
|
assert.deepEqual(calls, ['bounds:10,20,300,200']);
|
||||||
|
assert.equal(window.getShowCount(), 1);
|
||||||
|
assert.equal(window.isFocused(), true);
|
||||||
|
assert.deepEqual(window.sent, [['runtime-options:open']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sendToActiveOverlayWindow creates modal window lazily when absent', () => {
|
||||||
|
const window = createMockWindow();
|
||||||
|
let modalWindow: ReturnType<typeof createMockWindow> | null = null;
|
||||||
|
const runtime = createOverlayModalRuntimeService({
|
||||||
|
getMainWindow: () => null,
|
||||||
|
getInvisibleWindow: () => null,
|
||||||
|
getModalWindow: () => modalWindow as never,
|
||||||
|
createModalWindow: () => {
|
||||||
|
modalWindow = window;
|
||||||
|
return modalWindow as never;
|
||||||
|
},
|
||||||
|
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
||||||
|
setModalWindowBounds: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
runtime.sendToActiveOverlayWindow('jimaku:open', undefined, { restoreOnModalClose: 'jimaku' }),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.deepEqual(window.sent, [['jimaku:open']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handleOverlayModalClosed hides modal window only after all pending modals close', () => {
|
||||||
|
const window = createMockWindow();
|
||||||
|
const runtime = createOverlayModalRuntimeService({
|
||||||
|
getMainWindow: () => null,
|
||||||
|
getInvisibleWindow: () => null,
|
||||||
|
getModalWindow: () => window as never,
|
||||||
|
createModalWindow: () => window as never,
|
||||||
|
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
||||||
|
setModalWindowBounds: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||||
|
restoreOnModalClose: 'runtime-options',
|
||||||
|
});
|
||||||
|
runtime.sendToActiveOverlayWindow('subsync:open-manual', { sourceTracks: [] }, {
|
||||||
|
restoreOnModalClose: 'subsync',
|
||||||
|
});
|
||||||
|
|
||||||
|
runtime.handleOverlayModalClosed('runtime-options');
|
||||||
|
assert.equal(window.getHideCount(), 0);
|
||||||
|
|
||||||
|
runtime.handleOverlayModalClosed('subsync');
|
||||||
|
assert.equal(window.getHideCount(), 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('modal runtime notifies callers when modal input state becomes active/inactive', () => {
|
||||||
|
const window = createMockWindow();
|
||||||
|
const state: boolean[] = [];
|
||||||
|
const runtime = createOverlayModalRuntimeService(
|
||||||
|
{
|
||||||
|
getMainWindow: () => null,
|
||||||
|
getInvisibleWindow: () => null,
|
||||||
|
getModalWindow: () => window as never,
|
||||||
|
createModalWindow: () => window as never,
|
||||||
|
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
||||||
|
setModalWindowBounds: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onModalStateChange: (active: boolean): void => {
|
||||||
|
state.push(active);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||||
|
restoreOnModalClose: 'runtime-options',
|
||||||
|
});
|
||||||
|
runtime.sendToActiveOverlayWindow('subsync:open-manual', { sourceTracks: [] }, {
|
||||||
|
restoreOnModalClose: 'subsync',
|
||||||
|
});
|
||||||
|
assert.deepEqual(state, [true]);
|
||||||
|
|
||||||
|
runtime.handleOverlayModalClosed('runtime-options');
|
||||||
|
assert.deepEqual(state, [true]);
|
||||||
|
|
||||||
|
runtime.handleOverlayModalClosed('subsync');
|
||||||
|
assert.deepEqual(state, [true, false]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handleOverlayModalClosed hides modal window for single kiku modal', () => {
|
||||||
|
const window = createMockWindow();
|
||||||
|
const runtime = createOverlayModalRuntimeService({
|
||||||
|
getMainWindow: () => null,
|
||||||
|
getInvisibleWindow: () => null,
|
||||||
|
getModalWindow: () => window as never,
|
||||||
|
createModalWindow: () => window as never,
|
||||||
|
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
||||||
|
setModalWindowBounds: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
runtime.sendToActiveOverlayWindow('kiku:field-grouping-open', { test: true }, {
|
||||||
|
restoreOnModalClose: 'kiku',
|
||||||
|
});
|
||||||
|
runtime.handleOverlayModalClosed('kiku');
|
||||||
|
|
||||||
|
assert.equal(window.getHideCount(), 1);
|
||||||
|
assert.equal(runtime.getRestoreVisibleOverlayOnModalClose().size, 0);
|
||||||
|
});
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
import type { BrowserWindow } from 'electron';
|
import type { BrowserWindow } from 'electron';
|
||||||
|
import type { WindowGeometry } from '../types';
|
||||||
|
|
||||||
type OverlayHostedModal = 'runtime-options' | 'subsync' | 'jimaku';
|
type OverlayHostedModal = 'runtime-options' | 'subsync' | 'jimaku' | 'kiku';
|
||||||
type OverlayHostLayer = 'visible' | 'invisible';
|
type OverlayHostLayer = 'visible' | 'invisible';
|
||||||
|
|
||||||
export interface OverlayWindowResolver {
|
export interface OverlayWindowResolver {
|
||||||
getMainWindow: () => BrowserWindow | null;
|
getMainWindow: () => BrowserWindow | null;
|
||||||
getInvisibleWindow: () => BrowserWindow | null;
|
getInvisibleWindow: () => BrowserWindow | null;
|
||||||
|
getModalWindow: () => BrowserWindow | null;
|
||||||
|
createModalWindow: () => BrowserWindow | null;
|
||||||
|
getModalGeometry: () => WindowGeometry;
|
||||||
|
setModalWindowBounds: (geometry: WindowGeometry) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OverlayModalRuntime {
|
export interface OverlayModalRuntime {
|
||||||
@@ -19,9 +24,34 @@ export interface OverlayModalRuntime {
|
|||||||
getRestoreVisibleOverlayOnModalClose: () => Set<OverlayHostedModal>;
|
getRestoreVisibleOverlayOnModalClose: () => Set<OverlayHostedModal>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createOverlayModalRuntimeService(deps: OverlayWindowResolver): OverlayModalRuntime {
|
export interface OverlayModalRuntimeOptions {
|
||||||
|
onModalStateChange?: (isActive: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createOverlayModalRuntimeService(
|
||||||
|
deps: OverlayWindowResolver,
|
||||||
|
options: OverlayModalRuntimeOptions = {},
|
||||||
|
): OverlayModalRuntime {
|
||||||
const restoreVisibleOverlayOnModalClose = new Set<OverlayHostedModal>();
|
const restoreVisibleOverlayOnModalClose = new Set<OverlayHostedModal>();
|
||||||
const overlayModalAutoShownLayer = new Map<OverlayHostedModal, OverlayHostLayer>();
|
let modalActive = false;
|
||||||
|
|
||||||
|
const notifyModalStateChange = (nextState: boolean): void => {
|
||||||
|
if (modalActive === nextState) return;
|
||||||
|
modalActive = nextState;
|
||||||
|
options.onModalStateChange?.(nextState);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveModalWindow = (): BrowserWindow | null => {
|
||||||
|
const existingWindow = deps.getModalWindow();
|
||||||
|
if (existingWindow && !existingWindow.isDestroyed()) {
|
||||||
|
return existingWindow;
|
||||||
|
}
|
||||||
|
const createdWindow = deps.createModalWindow();
|
||||||
|
if (!createdWindow || createdWindow.isDestroyed()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return createdWindow;
|
||||||
|
};
|
||||||
|
|
||||||
const getTargetOverlayWindow = (): {
|
const getTargetOverlayWindow = (): {
|
||||||
window: BrowserWindow;
|
window: BrowserWindow;
|
||||||
@@ -41,6 +71,15 @@ export function createOverlayModalRuntimeService(deps: OverlayWindowResolver): O
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showModalWindow = (window: BrowserWindow): void => {
|
||||||
|
window.show();
|
||||||
|
window.setIgnoreMouseEvents(false);
|
||||||
|
window.focus();
|
||||||
|
if (!window.webContents.isFocused()) {
|
||||||
|
window.webContents.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const showOverlayWindowForModal = (window: BrowserWindow, layer: OverlayHostLayer): void => {
|
const showOverlayWindowForModal = (window: BrowserWindow, layer: OverlayHostLayer): void => {
|
||||||
if (layer === 'invisible' && typeof window.showInactive === 'function') {
|
if (layer === 'invisible' && typeof window.showInactive === 'function') {
|
||||||
window.showInactive();
|
window.showInactive();
|
||||||
@@ -57,39 +96,66 @@ export function createOverlayModalRuntimeService(deps: OverlayWindowResolver): O
|
|||||||
payload?: unknown,
|
payload?: unknown,
|
||||||
runtimeOptions?: { restoreOnModalClose?: OverlayHostedModal },
|
runtimeOptions?: { restoreOnModalClose?: OverlayHostedModal },
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const target = getTargetOverlayWindow();
|
|
||||||
if (!target) return false;
|
|
||||||
|
|
||||||
const { window: targetWindow, layer } = target;
|
|
||||||
const wasVisible = targetWindow.isVisible();
|
|
||||||
const restoreOnModalClose = runtimeOptions?.restoreOnModalClose;
|
const restoreOnModalClose = runtimeOptions?.restoreOnModalClose;
|
||||||
|
|
||||||
const sendNow = (): void => {
|
const sendNow = (window: BrowserWindow): void => {
|
||||||
if (payload === undefined) {
|
if (payload === undefined) {
|
||||||
targetWindow.webContents.send(channel);
|
window.webContents.send(channel);
|
||||||
} else {
|
} else {
|
||||||
targetWindow.webContents.send(channel, payload);
|
window.webContents.send(channel, payload);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!wasVisible) {
|
if (restoreOnModalClose) {
|
||||||
showOverlayWindowForModal(targetWindow, layer);
|
const modalWindow = resolveModalWindow();
|
||||||
}
|
if (!modalWindow) return false;
|
||||||
if (!wasVisible && restoreOnModalClose) {
|
|
||||||
|
deps.setModalWindowBounds(deps.getModalGeometry());
|
||||||
|
const wasVisible = modalWindow.isVisible();
|
||||||
|
const wasModalActive = restoreVisibleOverlayOnModalClose.size > 0;
|
||||||
restoreVisibleOverlayOnModalClose.add(restoreOnModalClose);
|
restoreVisibleOverlayOnModalClose.add(restoreOnModalClose);
|
||||||
overlayModalAutoShownLayer.set(restoreOnModalClose, layer);
|
if (!wasModalActive) {
|
||||||
|
notifyModalStateChange(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetWindow.webContents.isLoading()) {
|
if (!wasVisible) {
|
||||||
targetWindow.webContents.once('did-finish-load', () => {
|
showModalWindow(modalWindow);
|
||||||
if (targetWindow && !targetWindow.isDestroyed() && !targetWindow.webContents.isLoading()) {
|
} else if (!modalWindow.isFocused()) {
|
||||||
sendNow();
|
showModalWindow(modalWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modalWindow.webContents.isLoading()) {
|
||||||
|
modalWindow.webContents.once('did-finish-load', () => {
|
||||||
|
if (modalWindow && !modalWindow.isDestroyed() && !modalWindow.webContents.isLoading()) {
|
||||||
|
sendNow(modalWindow);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendNow();
|
sendNow(modalWindow);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = getTargetOverlayWindow();
|
||||||
|
if (!target) return false;
|
||||||
|
|
||||||
|
const { window: targetWindow, layer } = target;
|
||||||
|
const wasVisible = targetWindow.isVisible();
|
||||||
|
if (!wasVisible) {
|
||||||
|
showOverlayWindowForModal(targetWindow, layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetWindow.webContents.isLoading()) {
|
||||||
|
targetWindow.webContents.once('did-finish-load', () => {
|
||||||
|
if (targetWindow && !targetWindow.isDestroyed() && !targetWindow.webContents.isLoading()) {
|
||||||
|
sendNow(targetWindow);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendNow(targetWindow);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -102,24 +168,13 @@ export function createOverlayModalRuntimeService(deps: OverlayWindowResolver): O
|
|||||||
const handleOverlayModalClosed = (modal: OverlayHostedModal): void => {
|
const handleOverlayModalClosed = (modal: OverlayHostedModal): void => {
|
||||||
if (!restoreVisibleOverlayOnModalClose.has(modal)) return;
|
if (!restoreVisibleOverlayOnModalClose.has(modal)) return;
|
||||||
restoreVisibleOverlayOnModalClose.delete(modal);
|
restoreVisibleOverlayOnModalClose.delete(modal);
|
||||||
const layer = overlayModalAutoShownLayer.get(modal);
|
const modalWindow = deps.getModalWindow();
|
||||||
overlayModalAutoShownLayer.delete(modal);
|
if (!modalWindow || modalWindow.isDestroyed()) return;
|
||||||
if (!layer) return;
|
if (restoreVisibleOverlayOnModalClose.size === 0) {
|
||||||
const shouldKeepLayerVisible = [...restoreVisibleOverlayOnModalClose].some(
|
notifyModalStateChange(false);
|
||||||
(pendingModal) => overlayModalAutoShownLayer.get(pendingModal) === layer,
|
|
||||||
);
|
|
||||||
if (shouldKeepLayerVisible) return;
|
|
||||||
|
|
||||||
if (layer === 'visible') {
|
|
||||||
const mainWindow = deps.getMainWindow();
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
||||||
mainWindow.hide();
|
|
||||||
}
|
}
|
||||||
return;
|
if (restoreVisibleOverlayOnModalClose.size === 0) {
|
||||||
}
|
modalWindow.hide();
|
||||||
const invisibleWindow = deps.getInvisibleWindow();
|
|
||||||
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
|
|
||||||
invisibleWindow.hide();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,23 @@ test('overlay content measurement store main deps builder maps callbacks', () =>
|
|||||||
test('overlay modal runtime main deps builder maps window resolvers', () => {
|
test('overlay modal runtime main deps builder maps window resolvers', () => {
|
||||||
const mainWindow = { id: 'main' };
|
const mainWindow = { id: 'main' };
|
||||||
const invisibleWindow = { id: 'invisible' };
|
const invisibleWindow = { id: 'invisible' };
|
||||||
|
const modalWindow = { id: 'modal' };
|
||||||
|
const calls: string[] = [];
|
||||||
const deps = createBuildOverlayModalRuntimeMainDepsHandler({
|
const deps = createBuildOverlayModalRuntimeMainDepsHandler({
|
||||||
getMainWindow: () => mainWindow as never,
|
getMainWindow: () => mainWindow as never,
|
||||||
getInvisibleWindow: () => invisibleWindow as never,
|
getInvisibleWindow: () => invisibleWindow as never,
|
||||||
|
getModalWindow: () => modalWindow as never,
|
||||||
|
createModalWindow: () => modalWindow as never,
|
||||||
|
getModalGeometry: () => ({ x: 1, y: 2, width: 3, height: 4 }),
|
||||||
|
setModalWindowBounds: (geometry) =>
|
||||||
|
calls.push(`modal-bounds:${geometry.x},${geometry.y},${geometry.width},${geometry.height}`),
|
||||||
})();
|
})();
|
||||||
|
|
||||||
assert.equal(deps.getMainWindow(), mainWindow);
|
assert.equal(deps.getMainWindow(), mainWindow);
|
||||||
assert.equal(deps.getInvisibleWindow(), invisibleWindow);
|
assert.equal(deps.getInvisibleWindow(), invisibleWindow);
|
||||||
|
assert.equal(deps.getModalWindow(), modalWindow);
|
||||||
|
assert.equal(deps.createModalWindow(), modalWindow);
|
||||||
|
assert.deepEqual(deps.getModalGeometry(), { x: 1, y: 2, width: 3, height: 4 });
|
||||||
|
deps.setModalWindowBounds({ x: 10, y: 20, width: 30, height: 40 });
|
||||||
|
assert.deepEqual(calls, ['modal-bounds:10,20,30,40']);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,9 +14,15 @@ export function createBuildOverlayContentMeasurementStoreMainDepsHandler(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createBuildOverlayModalRuntimeMainDepsHandler(deps: OverlayWindowResolver) {
|
export function createBuildOverlayModalRuntimeMainDepsHandler(
|
||||||
|
deps: OverlayWindowResolver,
|
||||||
|
) {
|
||||||
return (): OverlayWindowResolver => ({
|
return (): OverlayWindowResolver => ({
|
||||||
getMainWindow: () => deps.getMainWindow(),
|
getMainWindow: () => deps.getMainWindow(),
|
||||||
getInvisibleWindow: () => deps.getInvisibleWindow(),
|
getInvisibleWindow: () => deps.getInvisibleWindow(),
|
||||||
|
getModalWindow: () => deps.getModalWindow(),
|
||||||
|
createModalWindow: () => deps.createModalWindow(),
|
||||||
|
getModalGeometry: () => deps.getModalGeometry(),
|
||||||
|
setModalWindowBounds: (geometry) => deps.setModalWindowBounds(geometry),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import test from 'node:test';
|
|||||||
import {
|
import {
|
||||||
createBuildCreateInvisibleWindowMainDepsHandler,
|
createBuildCreateInvisibleWindowMainDepsHandler,
|
||||||
createBuildCreateMainWindowMainDepsHandler,
|
createBuildCreateMainWindowMainDepsHandler,
|
||||||
|
createBuildCreateModalWindowMainDepsHandler,
|
||||||
createBuildCreateOverlayWindowMainDepsHandler,
|
createBuildCreateOverlayWindowMainDepsHandler,
|
||||||
createBuildCreateSecondaryWindowMainDepsHandler,
|
createBuildCreateSecondaryWindowMainDepsHandler,
|
||||||
} from './overlay-window-factory-main-deps';
|
} from './overlay-window-factory-main-deps';
|
||||||
@@ -47,5 +48,12 @@ test('overlay window factory main deps builders return mapped handlers', () => {
|
|||||||
const secondaryDeps = buildSecondaryDeps();
|
const secondaryDeps = buildSecondaryDeps();
|
||||||
secondaryDeps.setSecondaryWindow(null);
|
secondaryDeps.setSecondaryWindow(null);
|
||||||
|
|
||||||
assert.deepEqual(calls, ['set-main', 'set-invisible', 'set-secondary']);
|
const buildModalDeps = createBuildCreateModalWindowMainDepsHandler({
|
||||||
|
createOverlayWindow: () => ({ id: 'modal' }),
|
||||||
|
setModalWindow: () => calls.push('set-modal'),
|
||||||
|
});
|
||||||
|
const modalDeps = buildModalDeps();
|
||||||
|
modalDeps.setModalWindow(null);
|
||||||
|
|
||||||
|
assert.deepEqual(calls, ['set-main', 'set-invisible', 'set-secondary', 'set-modal']);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
export function createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps: {
|
export function createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps: {
|
||||||
createOverlayWindowCore: (
|
createOverlayWindowCore: (
|
||||||
kind: 'visible' | 'invisible' | 'secondary',
|
kind: 'visible' | 'invisible' | 'secondary' | 'modal',
|
||||||
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' | 'secondary') => boolean;
|
isOverlayVisible: (windowKind: 'visible' | 'invisible' | 'secondary' | 'modal') => boolean;
|
||||||
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
|
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
|
||||||
onWindowClosed: (windowKind: 'visible' | 'invisible' | 'secondary') => void;
|
onWindowClosed: (windowKind: 'visible' | 'invisible' | 'secondary' | 'modal') => 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' | 'secondary') => boolean;
|
isOverlayVisible: (windowKind: 'visible' | 'invisible' | 'secondary' | 'modal') => boolean;
|
||||||
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
|
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
|
||||||
onWindowClosed: (windowKind: 'visible' | 'invisible' | 'secondary') => void;
|
onWindowClosed: (windowKind: 'visible' | 'invisible' | 'secondary' | 'modal') => 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' | 'secondary') => TWindow;
|
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary' | 'modal') => 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' | 'secondary') => TWindow;
|
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary' | 'modal') => TWindow;
|
||||||
setInvisibleWindow: (window: TWindow | null) => void;
|
setInvisibleWindow: (window: TWindow | null) => void;
|
||||||
}) {
|
}) {
|
||||||
return () => ({
|
return () => ({
|
||||||
@@ -55,7 +55,7 @@ export function createBuildCreateInvisibleWindowMainDepsHandler<TWindow>(deps: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createBuildCreateSecondaryWindowMainDepsHandler<TWindow>(deps: {
|
export function createBuildCreateSecondaryWindowMainDepsHandler<TWindow>(deps: {
|
||||||
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary') => TWindow;
|
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary' | 'modal') => TWindow;
|
||||||
setSecondaryWindow: (window: TWindow | null) => void;
|
setSecondaryWindow: (window: TWindow | null) => void;
|
||||||
}) {
|
}) {
|
||||||
return () => ({
|
return () => ({
|
||||||
@@ -63,3 +63,13 @@ export function createBuildCreateSecondaryWindowMainDepsHandler<TWindow>(deps: {
|
|||||||
setSecondaryWindow: deps.setSecondaryWindow,
|
setSecondaryWindow: deps.setSecondaryWindow,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createBuildCreateModalWindowMainDepsHandler<TWindow>(deps: {
|
||||||
|
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary' | 'modal') => TWindow;
|
||||||
|
setModalWindow: (window: TWindow | null) => void;
|
||||||
|
}) {
|
||||||
|
return () => ({
|
||||||
|
createOverlayWindow: deps.createOverlayWindow,
|
||||||
|
setModalWindow: deps.setModalWindow,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
|
|||||||
import {
|
import {
|
||||||
createCreateInvisibleWindowHandler,
|
createCreateInvisibleWindowHandler,
|
||||||
createCreateMainWindowHandler,
|
createCreateMainWindowHandler,
|
||||||
|
createCreateModalWindowHandler,
|
||||||
createCreateOverlayWindowHandler,
|
createCreateOverlayWindowHandler,
|
||||||
createCreateSecondaryWindowHandler,
|
createCreateSecondaryWindowHandler,
|
||||||
} from './overlay-window-factory';
|
} from './overlay-window-factory';
|
||||||
@@ -80,3 +81,18 @@ test('create secondary window handler stores secondary window', () => {
|
|||||||
assert.equal(createSecondaryWindow(), secondaryWindow);
|
assert.equal(createSecondaryWindow(), secondaryWindow);
|
||||||
assert.deepEqual(calls, ['create:secondary', 'set:secondary']);
|
assert.deepEqual(calls, ['create:secondary', 'set:secondary']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('create modal window handler stores modal window', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const modalWindow = { id: 'modal' };
|
||||||
|
const createModalWindow = createCreateModalWindowHandler({
|
||||||
|
createOverlayWindow: (kind) => {
|
||||||
|
calls.push(`create:${kind}`);
|
||||||
|
return modalWindow;
|
||||||
|
},
|
||||||
|
setModalWindow: (window) => calls.push(`set:${(window as { id: string }).id}`),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(createModalWindow(), modalWindow);
|
||||||
|
assert.deepEqual(calls, ['create:modal', 'set:modal']);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
type OverlayWindowKind = 'visible' | 'invisible' | 'secondary';
|
type OverlayWindowKind = 'visible' | 'invisible' | 'secondary' | 'modal';
|
||||||
|
|
||||||
export function createCreateOverlayWindowHandler<TWindow>(deps: {
|
export function createCreateOverlayWindowHandler<TWindow>(deps: {
|
||||||
createOverlayWindowCore: (
|
createOverlayWindowCore: (
|
||||||
@@ -69,3 +69,14 @@ export function createCreateSecondaryWindowHandler<TWindow>(deps: {
|
|||||||
return window;
|
return window;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createCreateModalWindowHandler<TWindow>(deps: {
|
||||||
|
createOverlayWindow: (kind: OverlayWindowKind) => TWindow;
|
||||||
|
setModalWindow: (window: TWindow | null) => void;
|
||||||
|
}) {
|
||||||
|
return (): TWindow => {
|
||||||
|
const window = deps.createOverlayWindow('modal');
|
||||||
|
deps.setModalWindow(window);
|
||||||
|
return window;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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 secondaryWindow: { kind: string } | null = null;
|
||||||
|
let modalWindow: { kind: string } | null = null;
|
||||||
let debugEnabled = false;
|
let debugEnabled = false;
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
|
|
||||||
@@ -32,6 +33,9 @@ test('overlay window runtime handlers compose create/main/invisible handlers', (
|
|||||||
setSecondaryWindow: (window) => {
|
setSecondaryWindow: (window) => {
|
||||||
secondaryWindow = window;
|
secondaryWindow = window;
|
||||||
},
|
},
|
||||||
|
setModalWindow: (window) => {
|
||||||
|
modalWindow = window;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(runtime.createOverlayWindow('visible'), { kind: 'visible' });
|
assert.deepEqual(runtime.createOverlayWindow('visible'), { kind: 'visible' });
|
||||||
@@ -46,6 +50,8 @@ test('overlay window runtime handlers compose create/main/invisible handlers', (
|
|||||||
|
|
||||||
assert.deepEqual(runtime.createSecondaryWindow(), { kind: 'secondary' });
|
assert.deepEqual(runtime.createSecondaryWindow(), { kind: 'secondary' });
|
||||||
assert.deepEqual(secondaryWindow, { kind: 'secondary' });
|
assert.deepEqual(secondaryWindow, { kind: 'secondary' });
|
||||||
|
assert.deepEqual(runtime.createModalWindow(), { kind: 'modal' });
|
||||||
|
assert.deepEqual(modalWindow, { kind: 'modal' });
|
||||||
|
|
||||||
assert.equal(debugEnabled, false);
|
assert.equal(debugEnabled, false);
|
||||||
assert.deepEqual(calls, []);
|
assert.deepEqual(calls, []);
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
createCreateInvisibleWindowHandler,
|
createCreateInvisibleWindowHandler,
|
||||||
createCreateMainWindowHandler,
|
createCreateMainWindowHandler,
|
||||||
|
createCreateModalWindowHandler,
|
||||||
createCreateOverlayWindowHandler,
|
createCreateOverlayWindowHandler,
|
||||||
createCreateSecondaryWindowHandler,
|
createCreateSecondaryWindowHandler,
|
||||||
} from './overlay-window-factory';
|
} from './overlay-window-factory';
|
||||||
import {
|
import {
|
||||||
createBuildCreateInvisibleWindowMainDepsHandler,
|
createBuildCreateInvisibleWindowMainDepsHandler,
|
||||||
createBuildCreateMainWindowMainDepsHandler,
|
createBuildCreateMainWindowMainDepsHandler,
|
||||||
|
createBuildCreateModalWindowMainDepsHandler,
|
||||||
createBuildCreateOverlayWindowMainDepsHandler,
|
createBuildCreateOverlayWindowMainDepsHandler,
|
||||||
createBuildCreateSecondaryWindowMainDepsHandler,
|
createBuildCreateSecondaryWindowMainDepsHandler,
|
||||||
} from './overlay-window-factory-main-deps';
|
} from './overlay-window-factory-main-deps';
|
||||||
@@ -20,6 +22,7 @@ export function createOverlayWindowRuntimeHandlers<TWindow>(deps: {
|
|||||||
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;
|
setSecondaryWindow: (window: TWindow | null) => void;
|
||||||
|
setModalWindow: (window: TWindow | null) => void;
|
||||||
}) {
|
}) {
|
||||||
const createOverlayWindow = createCreateOverlayWindowHandler<TWindow>(
|
const createOverlayWindow = createCreateOverlayWindowHandler<TWindow>(
|
||||||
createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps.createOverlayWindowDeps)(),
|
createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps.createOverlayWindowDeps)(),
|
||||||
@@ -42,11 +45,18 @@ export function createOverlayWindowRuntimeHandlers<TWindow>(deps: {
|
|||||||
setSecondaryWindow: (window) => deps.setSecondaryWindow(window),
|
setSecondaryWindow: (window) => deps.setSecondaryWindow(window),
|
||||||
})(),
|
})(),
|
||||||
);
|
);
|
||||||
|
const createModalWindow = createCreateModalWindowHandler<TWindow>(
|
||||||
|
createBuildCreateModalWindowMainDepsHandler<TWindow>({
|
||||||
|
createOverlayWindow: (kind) => createOverlayWindow(kind),
|
||||||
|
setModalWindow: (window) => deps.setModalWindow(window),
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createOverlayWindow,
|
createOverlayWindow,
|
||||||
createMainWindow,
|
createMainWindow,
|
||||||
createInvisibleWindow,
|
createInvisibleWindow,
|
||||||
createSecondaryWindow,
|
createSecondaryWindow,
|
||||||
|
createModalWindow,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ const overlayLayerFromArg = overlayLayerArg?.slice('--overlay-layer='.length);
|
|||||||
const overlayLayer =
|
const overlayLayer =
|
||||||
overlayLayerFromArg === 'visible' ||
|
overlayLayerFromArg === 'visible' ||
|
||||||
overlayLayerFromArg === 'invisible' ||
|
overlayLayerFromArg === 'invisible' ||
|
||||||
overlayLayerFromArg === 'secondary'
|
overlayLayerFromArg === 'secondary' ||
|
||||||
|
overlayLayerFromArg === 'modal'
|
||||||
? overlayLayerFromArg
|
? overlayLayerFromArg
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -253,7 +254,7 @@ const electronAPI: ElectronAPI = {
|
|||||||
},
|
},
|
||||||
appendClipboardVideoToQueue: (): Promise<ClipboardAppendResult> =>
|
appendClipboardVideoToQueue: (): Promise<ClipboardAppendResult> =>
|
||||||
ipcRenderer.invoke(IPC_CHANNELS.request.appendClipboardVideoToQueue),
|
ipcRenderer.invoke(IPC_CHANNELS.request.appendClipboardVideoToQueue),
|
||||||
notifyOverlayModalClosed: (modal: 'runtime-options' | 'subsync' | 'jimaku') => {
|
notifyOverlayModalClosed: (modal: 'runtime-options' | 'subsync' | 'jimaku' | 'kiku') => {
|
||||||
ipcRenderer.send(IPC_CHANNELS.command.overlayModalClosed, modal);
|
ipcRenderer.send(IPC_CHANNELS.command.overlayModalClosed, modal);
|
||||||
},
|
},
|
||||||
reportOverlayContentBounds: (measurement: OverlayContentMeasurement) => {
|
reportOverlayContentBounds: (measurement: OverlayContentMeasurement) => {
|
||||||
|
|||||||
@@ -190,3 +190,38 @@ test('resolvePlatformInfo supports secondary layer and disables mouse-ignore tog
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('resolvePlatformInfo supports modal 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: () => 'modal',
|
||||||
|
},
|
||||||
|
location: { search: '' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis, 'navigator', {
|
||||||
|
configurable: true,
|
||||||
|
value: {
|
||||||
|
platform: 'MacIntel',
|
||||||
|
userAgent: 'Mozilla/5.0 (Macintosh)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const info = resolvePlatformInfo();
|
||||||
|
assert.equal(info.overlayLayer, 'modal');
|
||||||
|
assert.equal(info.isModalLayer, 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' | 'secondary';
|
overlayLayer: 'visible' | 'invisible' | 'secondary' | 'modal';
|
||||||
};
|
};
|
||||||
|
|
||||||
type NormalizedRendererError = {
|
type NormalizedRendererError = {
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ export function createKikuModal(
|
|||||||
|
|
||||||
setKikuPreviewError(null);
|
setKikuPreviewError(null);
|
||||||
ctx.dom.kikuPreviewJson.textContent = '';
|
ctx.dom.kikuPreviewJson.textContent = '';
|
||||||
|
window.electronAPI.notifyOverlayModalClosed('kiku');
|
||||||
|
|
||||||
ctx.state.kikuPendingChoice = null;
|
ctx.state.kikuPendingChoice = null;
|
||||||
ctx.state.kikuPreviewCompactData = null;
|
ctx.state.kikuPreviewCompactData = null;
|
||||||
|
|||||||
@@ -567,6 +567,16 @@ body.layer-secondary #secondarySubContainer {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.layer-modal #subtitleContainer,
|
||||||
|
body.layer-modal #secondarySubContainer {
|
||||||
|
display: none !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.layer-modal #overlay {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
#secondarySubRoot {
|
#secondarySubRoot {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
export type OverlayLayer = 'visible' | 'invisible' | 'secondary';
|
export type OverlayLayer = 'visible' | 'invisible' | 'secondary' | 'modal';
|
||||||
|
|
||||||
export type PlatformInfo = {
|
export type PlatformInfo = {
|
||||||
overlayLayer: OverlayLayer;
|
overlayLayer: OverlayLayer;
|
||||||
isInvisibleLayer: boolean;
|
isInvisibleLayer: boolean;
|
||||||
isSecondaryLayer: boolean;
|
isSecondaryLayer: boolean;
|
||||||
|
isModalLayer: boolean;
|
||||||
isLinuxPlatform: boolean;
|
isLinuxPlatform: boolean;
|
||||||
isMacOSPlatform: boolean;
|
isMacOSPlatform: boolean;
|
||||||
shouldToggleMouseIgnore: boolean;
|
shouldToggleMouseIgnore: boolean;
|
||||||
@@ -16,7 +17,10 @@ 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 === 'secondary'
|
queryLayer === 'visible' ||
|
||||||
|
queryLayer === 'invisible' ||
|
||||||
|
queryLayer === 'secondary' ||
|
||||||
|
queryLayer === 'modal'
|
||||||
? queryLayer
|
? queryLayer
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -24,12 +28,14 @@ export function resolvePlatformInfo(): PlatformInfo {
|
|||||||
overlayLayerFromQuery ??
|
overlayLayerFromQuery ??
|
||||||
(overlayLayerFromPreload === 'visible' ||
|
(overlayLayerFromPreload === 'visible' ||
|
||||||
overlayLayerFromPreload === 'invisible' ||
|
overlayLayerFromPreload === 'invisible' ||
|
||||||
overlayLayerFromPreload === 'secondary'
|
overlayLayerFromPreload === 'secondary' ||
|
||||||
|
overlayLayerFromPreload === 'modal'
|
||||||
? overlayLayerFromPreload
|
? overlayLayerFromPreload
|
||||||
: 'visible');
|
: 'visible');
|
||||||
|
|
||||||
const isInvisibleLayer = overlayLayer === 'invisible';
|
const isInvisibleLayer = overlayLayer === 'invisible';
|
||||||
const isSecondaryLayer = overlayLayer === 'secondary';
|
const isSecondaryLayer = overlayLayer === 'secondary';
|
||||||
|
const isModalLayer = overlayLayer === 'modal';
|
||||||
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);
|
||||||
@@ -38,9 +44,10 @@ export function resolvePlatformInfo(): PlatformInfo {
|
|||||||
overlayLayer,
|
overlayLayer,
|
||||||
isInvisibleLayer,
|
isInvisibleLayer,
|
||||||
isSecondaryLayer,
|
isSecondaryLayer,
|
||||||
|
isModalLayer,
|
||||||
isLinuxPlatform,
|
isLinuxPlatform,
|
||||||
isMacOSPlatform,
|
isMacOSPlatform,
|
||||||
shouldToggleMouseIgnore: !isLinuxPlatform && !isSecondaryLayer,
|
shouldToggleMouseIgnore: !isLinuxPlatform && !isSecondaryLayer && !isModalLayer,
|
||||||
invisiblePositionEditToggleCode: 'KeyP',
|
invisiblePositionEditToggleCode: 'KeyP',
|
||||||
invisiblePositionStepPx: 1,
|
invisiblePositionStepPx: 1,
|
||||||
invisiblePositionStepFastPx: 4,
|
invisiblePositionStepFastPx: 4,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { OverlayContentMeasurement, RuntimeOptionId, RuntimeOptionValue } from '../../types';
|
import type { OverlayContentMeasurement, RuntimeOptionId, RuntimeOptionValue } from '../../types';
|
||||||
|
|
||||||
export const OVERLAY_HOSTED_MODALS = ['runtime-options', 'subsync', 'jimaku'] as const;
|
export const OVERLAY_HOSTED_MODALS = ['runtime-options', 'subsync', 'jimaku', 'kiku'] as const;
|
||||||
export type OverlayHostedModal = (typeof OVERLAY_HOSTED_MODALS)[number];
|
export type OverlayHostedModal = (typeof OVERLAY_HOSTED_MODALS)[number];
|
||||||
|
|
||||||
export const IPC_CHANNELS = {
|
export const IPC_CHANNELS = {
|
||||||
|
|||||||
@@ -728,7 +728,7 @@ export interface SubtitleHoverTokenPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ElectronAPI {
|
export interface ElectronAPI {
|
||||||
getOverlayLayer: () => 'visible' | 'invisible' | 'secondary' | null;
|
getOverlayLayer: () => 'visible' | 'invisible' | 'secondary' | 'modal' | 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;
|
||||||
@@ -780,7 +780,7 @@ export interface ElectronAPI {
|
|||||||
onOpenRuntimeOptions: (callback: () => void) => void;
|
onOpenRuntimeOptions: (callback: () => void) => void;
|
||||||
onOpenJimaku: (callback: () => void) => void;
|
onOpenJimaku: (callback: () => void) => void;
|
||||||
appendClipboardVideoToQueue: () => Promise<ClipboardAppendResult>;
|
appendClipboardVideoToQueue: () => Promise<ClipboardAppendResult>;
|
||||||
notifyOverlayModalClosed: (modal: 'runtime-options' | 'subsync' | 'jimaku') => void;
|
notifyOverlayModalClosed: (modal: 'runtime-options' | 'subsync' | 'jimaku' | 'kiku') => void;
|
||||||
reportOverlayContentBounds: (measurement: OverlayContentMeasurement) => void;
|
reportOverlayContentBounds: (measurement: OverlayContentMeasurement) => void;
|
||||||
reportHoveredSubtitleToken: (tokenIndex: number | null) => void;
|
reportHoveredSubtitleToken: (tokenIndex: number | null) => void;
|
||||||
onConfigHotReload: (callback: (payload: ConfigHotReloadPayload) => void) => void;
|
onConfigHotReload: (callback: (payload: ConfigHotReloadPayload) => void) => void;
|
||||||
|
|||||||
Reference in New Issue
Block a user