mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-06 19:57:26 -08:00
feat(renderer): add keyboard-driven yomitan navigation and popup controls
This commit is contained in:
@@ -2,6 +2,7 @@ import { BrowserWindow } from 'electron';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { WindowGeometry } from '../../types';
|
import { WindowGeometry } from '../../types';
|
||||||
import { createLogger } from '../../logger';
|
import { createLogger } from '../../logger';
|
||||||
|
import { IPC_CHANNELS } from '../../shared/ipc/contracts';
|
||||||
|
|
||||||
const logger = createLogger('main:overlay-window');
|
const logger = createLogger('main:overlay-window');
|
||||||
const overlayWindowLayerByInstance = new WeakMap<BrowserWindow, OverlayWindowKind>();
|
const overlayWindowLayerByInstance = new WeakMap<BrowserWindow, OverlayWindowKind>();
|
||||||
@@ -24,6 +25,24 @@ function loadOverlayWindowLayer(window: BrowserWindow, layer: OverlayWindowKind)
|
|||||||
|
|
||||||
export type OverlayWindowKind = 'visible' | 'modal';
|
export type OverlayWindowKind = 'visible' | 'modal';
|
||||||
|
|
||||||
|
function isLookupWindowToggleInput(input: Electron.Input): boolean {
|
||||||
|
if (input.type !== 'keyDown') return false;
|
||||||
|
if (input.alt) return false;
|
||||||
|
if (!input.control && !input.meta) return false;
|
||||||
|
if (input.shift) return false;
|
||||||
|
const normalizedKey = typeof input.key === 'string' ? input.key.toLowerCase() : '';
|
||||||
|
return input.code === 'KeyY' || normalizedKey === 'y';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isKeyboardModeToggleInput(input: Electron.Input): boolean {
|
||||||
|
if (input.type !== 'keyDown') return false;
|
||||||
|
if (input.alt) return false;
|
||||||
|
if (!input.control && !input.meta) return false;
|
||||||
|
if (!input.shift) return false;
|
||||||
|
const normalizedKey = typeof input.key === 'string' ? input.key.toLowerCase() : '';
|
||||||
|
return input.code === 'KeyY' || normalizedKey === 'y';
|
||||||
|
}
|
||||||
|
|
||||||
export function updateOverlayWindowBounds(
|
export function updateOverlayWindowBounds(
|
||||||
geometry: WindowGeometry,
|
geometry: WindowGeometry,
|
||||||
window: BrowserWindow | null,
|
window: BrowserWindow | null,
|
||||||
@@ -118,6 +137,16 @@ export function createOverlayWindow(
|
|||||||
window.webContents.on('before-input-event', (event, input) => {
|
window.webContents.on('before-input-event', (event, input) => {
|
||||||
if (kind === 'modal') return;
|
if (kind === 'modal') return;
|
||||||
if (!window.isVisible()) return;
|
if (!window.isVisible()) return;
|
||||||
|
if (isKeyboardModeToggleInput(input)) {
|
||||||
|
event.preventDefault();
|
||||||
|
window.webContents.send(IPC_CHANNELS.event.keyboardModeToggleRequested);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isLookupWindowToggleInput(input)) {
|
||||||
|
event.preventDefault();
|
||||||
|
window.webContents.send(IPC_CHANNELS.event.lookupWindowToggleRequested);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!options.tryHandleOverlayShortcutLocalFallback(input)) return;
|
if (!options.tryHandleOverlayShortcutLocalFallback(input)) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -118,6 +118,12 @@ function createQueuedIpcListenerWithPayload<T>(
|
|||||||
|
|
||||||
const onOpenRuntimeOptionsEvent = createQueuedIpcListener(IPC_CHANNELS.event.runtimeOptionsOpen);
|
const onOpenRuntimeOptionsEvent = createQueuedIpcListener(IPC_CHANNELS.event.runtimeOptionsOpen);
|
||||||
const onOpenJimakuEvent = createQueuedIpcListener(IPC_CHANNELS.event.jimakuOpen);
|
const onOpenJimakuEvent = createQueuedIpcListener(IPC_CHANNELS.event.jimakuOpen);
|
||||||
|
const onKeyboardModeToggleRequestedEvent = createQueuedIpcListener(
|
||||||
|
IPC_CHANNELS.event.keyboardModeToggleRequested,
|
||||||
|
);
|
||||||
|
const onLookupWindowToggleRequestedEvent = createQueuedIpcListener(
|
||||||
|
IPC_CHANNELS.event.lookupWindowToggleRequested,
|
||||||
|
);
|
||||||
const onSubsyncManualOpenEvent = createQueuedIpcListenerWithPayload<SubsyncManualPayload>(
|
const onSubsyncManualOpenEvent = createQueuedIpcListenerWithPayload<SubsyncManualPayload>(
|
||||||
IPC_CHANNELS.event.subsyncOpenManual,
|
IPC_CHANNELS.event.subsyncOpenManual,
|
||||||
(payload) => payload as SubsyncManualPayload,
|
(payload) => payload as SubsyncManualPayload,
|
||||||
@@ -282,6 +288,8 @@ const electronAPI: ElectronAPI = {
|
|||||||
},
|
},
|
||||||
onOpenRuntimeOptions: onOpenRuntimeOptionsEvent,
|
onOpenRuntimeOptions: onOpenRuntimeOptionsEvent,
|
||||||
onOpenJimaku: onOpenJimakuEvent,
|
onOpenJimaku: onOpenJimakuEvent,
|
||||||
|
onKeyboardModeToggleRequested: onKeyboardModeToggleRequestedEvent,
|
||||||
|
onLookupWindowToggleRequested: onLookupWindowToggleRequestedEvent,
|
||||||
appendClipboardVideoToQueue: (): Promise<ClipboardAppendResult> =>
|
appendClipboardVideoToQueue: (): Promise<ClipboardAppendResult> =>
|
||||||
ipcRenderer.invoke(IPC_CHANNELS.request.appendClipboardVideoToQueue),
|
ipcRenderer.invoke(IPC_CHANNELS.request.appendClipboardVideoToQueue),
|
||||||
notifyOverlayModalClosed: (modal: 'runtime-options' | 'subsync' | 'jimaku' | 'kiku') => {
|
notifyOverlayModalClosed: (modal: 'runtime-options' | 'subsync' | 'jimaku' | 'kiku') => {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
YOMITAN_POPUP_IFRAME_SELECTOR,
|
YOMITAN_POPUP_IFRAME_SELECTOR,
|
||||||
hasYomitanPopupIframe,
|
hasYomitanPopupIframe,
|
||||||
isYomitanPopupIframe,
|
isYomitanPopupIframe,
|
||||||
|
isYomitanPopupVisible,
|
||||||
} from './yomitan-popup.js';
|
} from './yomitan-popup.js';
|
||||||
import { scrollActiveRuntimeOptionIntoView } from './modals/runtime-options.js';
|
import { scrollActiveRuntimeOptionIntoView } from './modals/runtime-options.js';
|
||||||
import { resolvePlatformInfo } from './utils/platform.js';
|
import { resolvePlatformInfo } from './utils/platform.js';
|
||||||
@@ -283,6 +284,43 @@ test('hasYomitanPopupIframe queries for modern + legacy selector', () => {
|
|||||||
assert.equal(selector, YOMITAN_POPUP_IFRAME_SELECTOR);
|
assert.equal(selector, YOMITAN_POPUP_IFRAME_SELECTOR);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('isYomitanPopupVisible requires visible iframe geometry', () => {
|
||||||
|
const previousWindow = (globalThis as { window?: unknown }).window;
|
||||||
|
let selector = '';
|
||||||
|
const visibleFrame = {
|
||||||
|
getBoundingClientRect: () => ({ width: 320, height: 180 }),
|
||||||
|
} as unknown as HTMLIFrameElement;
|
||||||
|
const hiddenFrame = {
|
||||||
|
getBoundingClientRect: () => ({ width: 320, height: 180 }),
|
||||||
|
} as unknown as HTMLIFrameElement;
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis, 'window', {
|
||||||
|
configurable: true,
|
||||||
|
value: {
|
||||||
|
getComputedStyle: (element: Element) => {
|
||||||
|
if (element === hiddenFrame) {
|
||||||
|
return { visibility: 'hidden', display: 'block', opacity: '1' } as CSSStyleDeclaration;
|
||||||
|
}
|
||||||
|
return { visibility: 'visible', display: 'block', opacity: '1' } as CSSStyleDeclaration;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const root = {
|
||||||
|
querySelectorAll: (value: string) => {
|
||||||
|
selector = value;
|
||||||
|
return [hiddenFrame, visibleFrame];
|
||||||
|
},
|
||||||
|
} as unknown as ParentNode;
|
||||||
|
|
||||||
|
assert.equal(isYomitanPopupVisible(root), true);
|
||||||
|
assert.equal(selector, YOMITAN_POPUP_IFRAME_SELECTOR);
|
||||||
|
} finally {
|
||||||
|
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('scrollActiveRuntimeOptionIntoView scrolls active runtime option with nearest block', () => {
|
test('scrollActiveRuntimeOptionIntoView scrolls active runtime option with nearest block', () => {
|
||||||
const calls: Array<{ block?: ScrollLogicalPosition }> = [];
|
const calls: Array<{ block?: ScrollLogicalPosition }> = [];
|
||||||
const activeItem = {
|
const activeItem = {
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import type { Keybinding } from '../../types';
|
import type { Keybinding } from '../../types';
|
||||||
import type { RendererContext } from '../context';
|
import type { RendererContext } from '../context';
|
||||||
import { hasYomitanPopupIframe, isYomitanPopupIframe } from '../yomitan-popup.js';
|
import {
|
||||||
|
YOMITAN_POPUP_HIDDEN_EVENT,
|
||||||
|
YOMITAN_POPUP_COMMAND_EVENT,
|
||||||
|
isYomitanPopupVisible,
|
||||||
|
isYomitanPopupIframe,
|
||||||
|
} from '../yomitan-popup.js';
|
||||||
|
|
||||||
export function createKeyboardHandlers(
|
export function createKeyboardHandlers(
|
||||||
ctx: RendererContext,
|
ctx: RendererContext,
|
||||||
@@ -20,6 +25,7 @@ export function createKeyboardHandlers(
|
|||||||
) {
|
) {
|
||||||
// Timeout for the modal chord capture window (e.g. Y followed by H/K).
|
// Timeout for the modal chord capture window (e.g. Y followed by H/K).
|
||||||
const CHORD_TIMEOUT_MS = 1000;
|
const CHORD_TIMEOUT_MS = 1000;
|
||||||
|
const KEYBOARD_SELECTED_WORD_CLASS = 'keyboard-selected';
|
||||||
|
|
||||||
const CHORD_MAP = new Map<
|
const CHORD_MAP = new Map<
|
||||||
string,
|
string,
|
||||||
@@ -55,6 +61,293 @@ export function createKeyboardHandlers(
|
|||||||
return parts.join('+');
|
return parts.join('+');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dispatchYomitanPopupKeydown(
|
||||||
|
key: string,
|
||||||
|
code: string,
|
||||||
|
modifiers: Array<'alt' | 'ctrl' | 'shift' | 'meta'>,
|
||||||
|
repeat: boolean,
|
||||||
|
) {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent(YOMITAN_POPUP_COMMAND_EVENT, {
|
||||||
|
detail: {
|
||||||
|
type: 'forwardKeyDown',
|
||||||
|
key,
|
||||||
|
code,
|
||||||
|
modifiers,
|
||||||
|
repeat,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dispatchYomitanPopupVisibility(visible: boolean) {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent(YOMITAN_POPUP_COMMAND_EVENT, {
|
||||||
|
detail: {
|
||||||
|
type: 'setVisible',
|
||||||
|
visible,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dispatchYomitanPopupMineSelected() {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent(YOMITAN_POPUP_COMMAND_EVENT, {
|
||||||
|
detail: {
|
||||||
|
type: 'mineSelected',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dispatchYomitanFrontendScanSelectedText() {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent(YOMITAN_POPUP_COMMAND_EVENT, {
|
||||||
|
detail: {
|
||||||
|
type: 'scanSelectedText',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPrimaryModifierPressed(e: KeyboardEvent): boolean {
|
||||||
|
return e.ctrlKey || e.metaKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isKeyboardDrivenModeToggle(e: KeyboardEvent): boolean {
|
||||||
|
const isYKey = e.code === 'KeyY' || e.key.toLowerCase() === 'y';
|
||||||
|
return isPrimaryModifierPressed(e) && !e.altKey && e.shiftKey && isYKey && !e.repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLookupWindowToggle(e: KeyboardEvent): boolean {
|
||||||
|
const isYKey = e.code === 'KeyY' || e.key.toLowerCase() === 'y';
|
||||||
|
return isPrimaryModifierPressed(e) && !e.altKey && !e.shiftKey && isYKey && !e.repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSubtitleWordNodes(): HTMLElement[] {
|
||||||
|
return Array.from(ctx.dom.subtitleRoot.querySelectorAll<HTMLElement>('.word[data-token-index]'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncKeyboardTokenSelection(): void {
|
||||||
|
const wordNodes = getSubtitleWordNodes();
|
||||||
|
for (const wordNode of wordNodes) {
|
||||||
|
wordNode.classList.remove(KEYBOARD_SELECTED_WORD_CLASS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.state.keyboardDrivenModeEnabled || wordNodes.length === 0) {
|
||||||
|
ctx.state.keyboardSelectedWordIndex = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedIndex = Math.min(
|
||||||
|
Math.max(ctx.state.keyboardSelectedWordIndex ?? 0, 0),
|
||||||
|
wordNodes.length - 1,
|
||||||
|
);
|
||||||
|
ctx.state.keyboardSelectedWordIndex = selectedIndex;
|
||||||
|
const selectedWordNode = wordNodes[selectedIndex];
|
||||||
|
if (selectedWordNode) {
|
||||||
|
selectedWordNode.classList.add(KEYBOARD_SELECTED_WORD_CLASS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setKeyboardDrivenModeEnabled(enabled: boolean): void {
|
||||||
|
ctx.state.keyboardDrivenModeEnabled = enabled;
|
||||||
|
if (!enabled) {
|
||||||
|
ctx.state.keyboardSelectedWordIndex = null;
|
||||||
|
}
|
||||||
|
syncKeyboardTokenSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleKeyboardDrivenMode(): void {
|
||||||
|
setKeyboardDrivenModeEnabled(!ctx.state.keyboardDrivenModeEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveKeyboardSelection(delta: -1 | 1): boolean {
|
||||||
|
const wordNodes = getSubtitleWordNodes();
|
||||||
|
if (wordNodes.length === 0) {
|
||||||
|
ctx.state.keyboardSelectedWordIndex = null;
|
||||||
|
syncKeyboardTokenSelection();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIndex = ctx.state.keyboardSelectedWordIndex ?? 0;
|
||||||
|
const nextIndex = Math.min(Math.max(currentIndex + delta, 0), wordNodes.length - 1);
|
||||||
|
ctx.state.keyboardSelectedWordIndex = nextIndex;
|
||||||
|
syncKeyboardTokenSelection();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScanModifierState = {
|
||||||
|
shiftKey?: boolean;
|
||||||
|
ctrlKey?: boolean;
|
||||||
|
altKey?: boolean;
|
||||||
|
metaKey?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function emitSyntheticScanEvents(
|
||||||
|
target: Element,
|
||||||
|
clientX: number,
|
||||||
|
clientY: number,
|
||||||
|
modifiers: ScanModifierState = {},
|
||||||
|
): void {
|
||||||
|
if (typeof PointerEvent !== 'undefined') {
|
||||||
|
const pointerEventInit = {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
composed: true,
|
||||||
|
clientX,
|
||||||
|
clientY,
|
||||||
|
pointerType: 'mouse',
|
||||||
|
isPrimary: true,
|
||||||
|
button: 0,
|
||||||
|
buttons: 0,
|
||||||
|
shiftKey: modifiers.shiftKey ?? false,
|
||||||
|
ctrlKey: modifiers.ctrlKey ?? false,
|
||||||
|
altKey: modifiers.altKey ?? false,
|
||||||
|
metaKey: modifiers.metaKey ?? false,
|
||||||
|
} satisfies PointerEventInit;
|
||||||
|
|
||||||
|
target.dispatchEvent(new PointerEvent('pointerover', pointerEventInit));
|
||||||
|
target.dispatchEvent(new PointerEvent('pointermove', pointerEventInit));
|
||||||
|
target.dispatchEvent(new PointerEvent('pointerdown', { ...pointerEventInit, buttons: 1 }));
|
||||||
|
target.dispatchEvent(new PointerEvent('pointerup', pointerEventInit));
|
||||||
|
}
|
||||||
|
|
||||||
|
const mouseEventInit = {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
composed: true,
|
||||||
|
clientX,
|
||||||
|
clientY,
|
||||||
|
button: 0,
|
||||||
|
buttons: 0,
|
||||||
|
shiftKey: modifiers.shiftKey ?? false,
|
||||||
|
ctrlKey: modifiers.ctrlKey ?? false,
|
||||||
|
altKey: modifiers.altKey ?? false,
|
||||||
|
metaKey: modifiers.metaKey ?? false,
|
||||||
|
} satisfies MouseEventInit;
|
||||||
|
target.dispatchEvent(new MouseEvent('mousemove', mouseEventInit));
|
||||||
|
target.dispatchEvent(new MouseEvent('mousedown', { ...mouseEventInit, buttons: 1 }));
|
||||||
|
target.dispatchEvent(new MouseEvent('mouseup', mouseEventInit));
|
||||||
|
target.dispatchEvent(new MouseEvent('click', mouseEventInit));
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitLookupScanFallback(target: Element, clientX: number, clientY: number): void {
|
||||||
|
emitSyntheticScanEvents(target, clientX, clientY, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectWordNodeText(wordNode: HTMLElement): void {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (!selection) return;
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(wordNode);
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
ctx.dom.subtitleRoot.classList.add('has-selection');
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerLookupForSelectedWord(): boolean {
|
||||||
|
const wordNodes = getSubtitleWordNodes();
|
||||||
|
if (wordNodes.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedIndex = Math.min(
|
||||||
|
Math.max(ctx.state.keyboardSelectedWordIndex ?? 0, 0),
|
||||||
|
wordNodes.length - 1,
|
||||||
|
);
|
||||||
|
ctx.state.keyboardSelectedWordIndex = selectedIndex;
|
||||||
|
const selectedWordNode = wordNodes[selectedIndex];
|
||||||
|
if (!selectedWordNode) return false;
|
||||||
|
|
||||||
|
syncKeyboardTokenSelection();
|
||||||
|
selectWordNodeText(selectedWordNode);
|
||||||
|
|
||||||
|
const rect = selectedWordNode.getBoundingClientRect();
|
||||||
|
const clientX = rect.left + rect.width / 2;
|
||||||
|
const clientY = rect.top + rect.height / 2;
|
||||||
|
|
||||||
|
dispatchYomitanFrontendScanSelectedText();
|
||||||
|
// Fallback only if the explicit scan path did not open popup quickly.
|
||||||
|
setTimeout(() => {
|
||||||
|
if (ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Dispatch directly on the selected token span; when overlay pointer-events are disabled,
|
||||||
|
// elementFromPoint may resolve to the underlying video surface instead.
|
||||||
|
emitLookupScanFallback(selectedWordNode, clientX, clientY);
|
||||||
|
}, 60);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyboardModeToggleRequested(): void {
|
||||||
|
toggleKeyboardDrivenMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLookupWindowToggleRequested(): void {
|
||||||
|
if (ctx.state.yomitanPopupVisible) {
|
||||||
|
dispatchYomitanPopupVisibility(false);
|
||||||
|
if (ctx.state.keyboardDrivenModeEnabled) {
|
||||||
|
queueMicrotask(() => {
|
||||||
|
restoreOverlayKeyboardFocus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
triggerLookupForSelectedWord();
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreOverlayKeyboardFocus(): void {
|
||||||
|
void window.electronAPI.focusMainWindow();
|
||||||
|
window.focus();
|
||||||
|
ctx.dom.overlay.focus({ preventScroll: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyboardDrivenModeNavigation(e: KeyboardEvent): boolean {
|
||||||
|
if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = e.code;
|
||||||
|
if (key === 'ArrowLeft' || key === 'ArrowUp' || key === 'KeyH' || key === 'KeyK') {
|
||||||
|
return moveKeyboardSelection(-1);
|
||||||
|
}
|
||||||
|
if (key === 'ArrowRight' || key === 'ArrowDown' || key === 'KeyL' || key === 'KeyJ') {
|
||||||
|
return moveKeyboardSelection(1);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleYomitanPopupKeybind(e: KeyboardEvent): boolean {
|
||||||
|
if (e.repeat) return false;
|
||||||
|
const modifierOnlyCodes = new Set([
|
||||||
|
'ShiftLeft',
|
||||||
|
'ShiftRight',
|
||||||
|
'ControlLeft',
|
||||||
|
'ControlRight',
|
||||||
|
'AltLeft',
|
||||||
|
'AltRight',
|
||||||
|
'MetaLeft',
|
||||||
|
'MetaRight',
|
||||||
|
]);
|
||||||
|
if (modifierOnlyCodes.has(e.code)) return false;
|
||||||
|
|
||||||
|
if (!e.ctrlKey && !e.metaKey && !e.altKey && e.code === 'KeyM') {
|
||||||
|
dispatchYomitanPopupMineSelected();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modifiers: Array<'alt' | 'ctrl' | 'shift' | 'meta'> = [];
|
||||||
|
if (e.altKey) modifiers.push('alt');
|
||||||
|
if (e.ctrlKey) modifiers.push('ctrl');
|
||||||
|
if (e.shiftKey) modifiers.push('shift');
|
||||||
|
if (e.metaKey) modifiers.push('meta');
|
||||||
|
|
||||||
|
dispatchYomitanPopupKeydown(e.key, e.code, modifiers, e.repeat);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function resolveSessionHelpChordBinding(): {
|
function resolveSessionHelpChordBinding(): {
|
||||||
bindingKey: 'KeyH' | 'KeyK';
|
bindingKey: 'KeyH' | 'KeyK';
|
||||||
fallbackUsed: boolean;
|
fallbackUsed: boolean;
|
||||||
@@ -106,9 +399,42 @@ export function createKeyboardHandlers(
|
|||||||
|
|
||||||
async function setupMpvInputForwarding(): Promise<void> {
|
async function setupMpvInputForwarding(): Promise<void> {
|
||||||
updateKeybindings(await window.electronAPI.getKeybindings());
|
updateKeybindings(await window.electronAPI.getKeybindings());
|
||||||
|
syncKeyboardTokenSelection();
|
||||||
|
|
||||||
|
const subtitleMutationObserver = new MutationObserver(() => {
|
||||||
|
syncKeyboardTokenSelection();
|
||||||
|
});
|
||||||
|
subtitleMutationObserver.observe(ctx.dom.subtitleRoot, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener(YOMITAN_POPUP_HIDDEN_EVENT, () => {
|
||||||
|
if (!ctx.state.keyboardDrivenModeEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
restoreOverlayKeyboardFocus();
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
if (hasYomitanPopupIframe(document)) return;
|
if (isKeyboardDrivenModeToggle(e)) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleKeyboardModeToggleRequested();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLookupWindowToggle(e)) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleLookupWindowToggleRequested();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document)) {
|
||||||
|
if (handleYomitanPopupKeybind(e)) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (ctx.state.runtimeOptionsModalOpen) {
|
if (ctx.state.runtimeOptionsModalOpen) {
|
||||||
options.handleRuntimeOptionsKeydown(e);
|
options.handleRuntimeOptionsKeydown(e);
|
||||||
@@ -131,6 +457,11 @@ export function createKeyboardHandlers(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ctx.state.keyboardDrivenModeEnabled && handleKeyboardDrivenModeNavigation(e)) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (ctx.state.chordPending) {
|
if (ctx.state.chordPending) {
|
||||||
const modifierKeys = [
|
const modifierKeys = [
|
||||||
'ShiftLeft',
|
'ShiftLeft',
|
||||||
@@ -211,5 +542,8 @@ export function createKeyboardHandlers(
|
|||||||
return {
|
return {
|
||||||
setupMpvInputForwarding,
|
setupMpvInputForwarding,
|
||||||
updateKeybindings,
|
updateKeybindings,
|
||||||
|
syncKeyboardTokenSelection,
|
||||||
|
handleKeyboardModeToggleRequested,
|
||||||
|
handleLookupWindowToggleRequested,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { ModalStateReader, RendererContext } from '../context';
|
|||||||
import {
|
import {
|
||||||
YOMITAN_POPUP_HIDDEN_EVENT,
|
YOMITAN_POPUP_HIDDEN_EVENT,
|
||||||
YOMITAN_POPUP_SHOWN_EVENT,
|
YOMITAN_POPUP_SHOWN_EVENT,
|
||||||
hasYomitanPopupIframe,
|
isYomitanPopupVisible,
|
||||||
isYomitanPopupIframe,
|
isYomitanPopupIframe,
|
||||||
} from '../yomitan-popup.js';
|
} from '../yomitan-popup.js';
|
||||||
|
|
||||||
@@ -79,6 +79,7 @@ export function createMouseHandlers(
|
|||||||
|
|
||||||
function enablePopupInteraction(): void {
|
function enablePopupInteraction(): void {
|
||||||
yomitanPopupVisible = true;
|
yomitanPopupVisible = true;
|
||||||
|
ctx.state.yomitanPopupVisible = true;
|
||||||
ctx.dom.overlay.classList.add('interactive');
|
ctx.dom.overlay.classList.add('interactive');
|
||||||
if (ctx.platform.shouldToggleMouseIgnore) {
|
if (ctx.platform.shouldToggleMouseIgnore) {
|
||||||
window.electronAPI.setIgnoreMouseEvents(false);
|
window.electronAPI.setIgnoreMouseEvents(false);
|
||||||
@@ -89,12 +90,14 @@ export function createMouseHandlers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function disablePopupInteractionIfIdle(): void {
|
function disablePopupInteractionIfIdle(): void {
|
||||||
if (typeof document !== 'undefined' && hasYomitanPopupIframe(document)) {
|
if (typeof document !== 'undefined' && isYomitanPopupVisible(document)) {
|
||||||
yomitanPopupVisible = true;
|
yomitanPopupVisible = true;
|
||||||
|
ctx.state.yomitanPopupVisible = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
yomitanPopupVisible = false;
|
yomitanPopupVisible = false;
|
||||||
|
ctx.state.yomitanPopupVisible = false;
|
||||||
popupPauseRequestId += 1;
|
popupPauseRequestId += 1;
|
||||||
maybeResumeYomitanPopupPause();
|
maybeResumeYomitanPopupPause();
|
||||||
maybeResumeHoverPause();
|
maybeResumeHoverPause();
|
||||||
@@ -202,7 +205,8 @@ export function createMouseHandlers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setupYomitanObserver(): void {
|
function setupYomitanObserver(): void {
|
||||||
yomitanPopupVisible = hasYomitanPopupIframe(document);
|
yomitanPopupVisible = isYomitanPopupVisible(document);
|
||||||
|
ctx.state.yomitanPopupVisible = yomitanPopupVisible;
|
||||||
void maybePauseForYomitanPopup();
|
void maybePauseForYomitanPopup();
|
||||||
|
|
||||||
window.addEventListener(YOMITAN_POPUP_SHOWN_EVENT, () => {
|
window.addEventListener(YOMITAN_POPUP_SHOWN_EVENT, () => {
|
||||||
|
|||||||
@@ -140,6 +140,16 @@ function truncateForErrorLog(text: string): string {
|
|||||||
return `${normalized.slice(0, 177)}...`;
|
return `${normalized.slice(0, 177)}...`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSubtitleTextForPreview(data: SubtitleData | string): string {
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
if (data && typeof data.text === 'string') {
|
||||||
|
return data.text;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function getActiveModal(): string | null {
|
function getActiveModal(): string | null {
|
||||||
if (ctx.state.jimakuModalOpen) return 'jimaku';
|
if (ctx.state.jimakuModalOpen) return 'jimaku';
|
||||||
if (ctx.state.kikuModalOpen) return 'kiku';
|
if (ctx.state.kikuModalOpen) return 'kiku';
|
||||||
@@ -245,6 +255,20 @@ function registerModalOpenHandlers(): void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function registerKeyboardCommandHandlers(): void {
|
||||||
|
window.electronAPI.onKeyboardModeToggleRequested(() => {
|
||||||
|
runGuarded('keyboard-mode-toggle:requested', () => {
|
||||||
|
keyboardHandlers.handleKeyboardModeToggleRequested();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electronAPI.onLookupWindowToggleRequested(() => {
|
||||||
|
runGuarded('lookup-window-toggle:requested', () => {
|
||||||
|
keyboardHandlers.handleLookupWindowToggleRequested();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function runGuarded(action: string, fn: () => void): void {
|
function runGuarded(action: string, fn: () => void): void {
|
||||||
try {
|
try {
|
||||||
fn();
|
fn();
|
||||||
@@ -262,6 +286,7 @@ function runGuardedAsync(action: string, fn: () => Promise<void> | void): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
registerModalOpenHandlers();
|
registerModalOpenHandlers();
|
||||||
|
registerKeyboardCommandHandlers();
|
||||||
|
|
||||||
async function init(): Promise<void> {
|
async function init(): Promise<void> {
|
||||||
document.body.classList.add(`layer-${ctx.platform.overlayLayer}`);
|
document.body.classList.add(`layer-${ctx.platform.overlayLayer}`);
|
||||||
@@ -271,11 +296,7 @@ async function init(): Promise<void> {
|
|||||||
|
|
||||||
window.electronAPI.onSubtitle((data: SubtitleData) => {
|
window.electronAPI.onSubtitle((data: SubtitleData) => {
|
||||||
runGuarded('subtitle:update', () => {
|
runGuarded('subtitle:update', () => {
|
||||||
if (typeof data === 'string') {
|
lastSubtitlePreview = truncateForErrorLog(getSubtitleTextForPreview(data));
|
||||||
lastSubtitlePreview = truncateForErrorLog(data);
|
|
||||||
} else if (data && typeof data.text === 'string') {
|
|
||||||
lastSubtitlePreview = truncateForErrorLog(data.text);
|
|
||||||
}
|
|
||||||
subtitleRenderer.renderSubtitle(data);
|
subtitleRenderer.renderSubtitle(data);
|
||||||
measurementReporter.schedule();
|
measurementReporter.schedule();
|
||||||
});
|
});
|
||||||
@@ -288,8 +309,13 @@ async function init(): Promise<void> {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialSubtitle = await window.electronAPI.getCurrentSubtitleRaw();
|
let initialSubtitle: SubtitleData | string = '';
|
||||||
lastSubtitlePreview = truncateForErrorLog(initialSubtitle);
|
try {
|
||||||
|
initialSubtitle = await window.electronAPI.getCurrentSubtitle();
|
||||||
|
} catch {
|
||||||
|
initialSubtitle = await window.electronAPI.getCurrentSubtitleRaw();
|
||||||
|
}
|
||||||
|
lastSubtitlePreview = truncateForErrorLog(getSubtitleTextForPreview(initialSubtitle));
|
||||||
subtitleRenderer.renderSubtitle(initialSubtitle);
|
subtitleRenderer.renderSubtitle(initialSubtitle);
|
||||||
measurementReporter.schedule();
|
measurementReporter.schedule();
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ export type RendererState = {
|
|||||||
keybindingsMap: Map<string, (string | number)[]>;
|
keybindingsMap: Map<string, (string | number)[]>;
|
||||||
chordPending: boolean;
|
chordPending: boolean;
|
||||||
chordTimeout: ReturnType<typeof setTimeout> | null;
|
chordTimeout: ReturnType<typeof setTimeout> | null;
|
||||||
|
keyboardDrivenModeEnabled: boolean;
|
||||||
|
keyboardSelectedWordIndex: number | null;
|
||||||
|
yomitanPopupVisible: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createRendererState(): RendererState {
|
export function createRendererState(): RendererState {
|
||||||
@@ -143,5 +146,8 @@ export function createRendererState(): RendererState {
|
|||||||
keybindingsMap: new Map(),
|
keybindingsMap: new Map(),
|
||||||
chordPending: false,
|
chordPending: false,
|
||||||
chordTimeout: null,
|
chordTimeout: null,
|
||||||
|
keyboardDrivenModeEnabled: false,
|
||||||
|
keyboardSelectedWordIndex: null,
|
||||||
|
yomitanPopupVisible: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -340,6 +340,15 @@ body.settings-modal-open #subtitleContainer {
|
|||||||
-webkit-text-fill-color: currentColor !important;
|
-webkit-text-fill-color: currentColor !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#subtitleRoot .word.keyboard-selected {
|
||||||
|
outline: 2px solid rgba(135, 201, 255, 0.92);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 2px rgba(12, 18, 28, 0.68),
|
||||||
|
0 0 18px rgba(120, 188, 255, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
#subtitleRoot .word[data-frequency-rank]::before {
|
#subtitleRoot .word[data-frequency-rank]::before {
|
||||||
content: attr(data-frequency-rank);
|
content: attr(data-frequency-rank);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -363,7 +372,8 @@ body.settings-modal-open #subtitleContainer {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#subtitleRoot .word[data-frequency-rank]:hover::before {
|
#subtitleRoot .word[data-frequency-rank]:hover::before,
|
||||||
|
#subtitleRoot .word.keyboard-selected[data-frequency-rank]::before {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateX(-50%) translateY(0);
|
transform: translateX(-50%) translateY(0);
|
||||||
}
|
}
|
||||||
@@ -390,7 +400,8 @@ body.settings-modal-open #subtitleContainer {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#subtitleRoot .word[data-jlpt-level]:hover::after {
|
#subtitleRoot .word[data-jlpt-level]:hover::after,
|
||||||
|
#subtitleRoot .word.keyboard-selected[data-jlpt-level]::after {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateX(-50%) translateY(0);
|
transform: translateX(-50%) translateY(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -409,6 +409,11 @@ test('JLPT CSS rules use underline-only styling in renderer stylesheet', () => {
|
|||||||
'#subtitleRoot .word[data-frequency-rank]:hover::before',
|
'#subtitleRoot .word[data-frequency-rank]:hover::before',
|
||||||
);
|
);
|
||||||
assert.match(frequencyTooltipHoverBlock, /opacity:\s*1;/);
|
assert.match(frequencyTooltipHoverBlock, /opacity:\s*1;/);
|
||||||
|
const frequencyTooltipKeyboardSelectedBlock = extractClassBlock(
|
||||||
|
cssText,
|
||||||
|
'#subtitleRoot .word.keyboard-selected[data-frequency-rank]::before',
|
||||||
|
);
|
||||||
|
assert.match(frequencyTooltipKeyboardSelectedBlock, /opacity:\s*1;/);
|
||||||
|
|
||||||
const jlptTooltipBaseBlock = extractClassBlock(
|
const jlptTooltipBaseBlock = extractClassBlock(
|
||||||
cssText,
|
cssText,
|
||||||
@@ -424,6 +429,11 @@ test('JLPT CSS rules use underline-only styling in renderer stylesheet', () => {
|
|||||||
'#subtitleRoot .word[data-jlpt-level]:hover::after',
|
'#subtitleRoot .word[data-jlpt-level]:hover::after',
|
||||||
);
|
);
|
||||||
assert.match(jlptTooltipHoverBlock, /opacity:\s*1;/);
|
assert.match(jlptTooltipHoverBlock, /opacity:\s*1;/);
|
||||||
|
const jlptTooltipKeyboardSelectedBlock = extractClassBlock(
|
||||||
|
cssText,
|
||||||
|
'#subtitleRoot .word.keyboard-selected[data-jlpt-level]::after',
|
||||||
|
);
|
||||||
|
assert.match(jlptTooltipKeyboardSelectedBlock, /opacity:\s*1;/);
|
||||||
|
|
||||||
assert.match(
|
assert.match(
|
||||||
cssText,
|
cssText,
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
export const YOMITAN_POPUP_IFRAME_SELECTOR = 'iframe.yomitan-popup, iframe[id^="yomitan-popup"]';
|
export const YOMITAN_POPUP_IFRAME_SELECTOR = 'iframe.yomitan-popup, iframe[id^="yomitan-popup"]';
|
||||||
export const YOMITAN_POPUP_SHOWN_EVENT = 'yomitan-popup-shown';
|
export const YOMITAN_POPUP_SHOWN_EVENT = 'yomitan-popup-shown';
|
||||||
export const YOMITAN_POPUP_HIDDEN_EVENT = 'yomitan-popup-hidden';
|
export const YOMITAN_POPUP_HIDDEN_EVENT = 'yomitan-popup-hidden';
|
||||||
|
export const YOMITAN_POPUP_MOUSE_ENTER_EVENT = 'yomitan-popup-mouse-enter';
|
||||||
|
export const YOMITAN_POPUP_MOUSE_LEAVE_EVENT = 'yomitan-popup-mouse-leave';
|
||||||
|
export const YOMITAN_POPUP_COMMAND_EVENT = 'subminer-yomitan-popup-command';
|
||||||
|
|
||||||
export function isYomitanPopupIframe(element: Element | null): boolean {
|
export function isYomitanPopupIframe(element: Element | null): boolean {
|
||||||
if (!element) return false;
|
if (!element) return false;
|
||||||
@@ -14,3 +17,19 @@ export function isYomitanPopupIframe(element: Element | null): boolean {
|
|||||||
export function hasYomitanPopupIframe(root: ParentNode = document): boolean {
|
export function hasYomitanPopupIframe(root: ParentNode = document): boolean {
|
||||||
return root.querySelector(YOMITAN_POPUP_IFRAME_SELECTOR) !== null;
|
return root.querySelector(YOMITAN_POPUP_IFRAME_SELECTOR) !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isYomitanPopupVisible(root: ParentNode = document): boolean {
|
||||||
|
const popupIframes = root.querySelectorAll<HTMLIFrameElement>(YOMITAN_POPUP_IFRAME_SELECTOR);
|
||||||
|
for (const iframe of popupIframes) {
|
||||||
|
const rect = iframe.getBoundingClientRect();
|
||||||
|
if (rect.width <= 0 || rect.height <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const styles = window.getComputedStyle(iframe);
|
||||||
|
if (styles.visibility === 'hidden' || styles.display === 'none' || styles.opacity === '0') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ export const IPC_CHANNELS = {
|
|||||||
runtimeOptionsChanged: 'runtime-options:changed',
|
runtimeOptionsChanged: 'runtime-options:changed',
|
||||||
runtimeOptionsOpen: 'runtime-options:open',
|
runtimeOptionsOpen: 'runtime-options:open',
|
||||||
jimakuOpen: 'jimaku:open',
|
jimakuOpen: 'jimaku:open',
|
||||||
|
keyboardModeToggleRequested: 'keyboard-mode-toggle:requested',
|
||||||
|
lookupWindowToggleRequested: 'lookup-window-toggle:requested',
|
||||||
configHotReload: 'config:hot-reload',
|
configHotReload: 'config:hot-reload',
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -843,6 +843,8 @@ export interface ElectronAPI {
|
|||||||
onRuntimeOptionsChanged: (callback: (options: RuntimeOptionState[]) => void) => void;
|
onRuntimeOptionsChanged: (callback: (options: RuntimeOptionState[]) => void) => void;
|
||||||
onOpenRuntimeOptions: (callback: () => void) => void;
|
onOpenRuntimeOptions: (callback: () => void) => void;
|
||||||
onOpenJimaku: (callback: () => void) => void;
|
onOpenJimaku: (callback: () => void) => void;
|
||||||
|
onKeyboardModeToggleRequested: (callback: () => void) => void;
|
||||||
|
onLookupWindowToggleRequested: (callback: () => void) => void;
|
||||||
appendClipboardVideoToQueue: () => Promise<ClipboardAppendResult>;
|
appendClipboardVideoToQueue: () => Promise<ClipboardAppendResult>;
|
||||||
notifyOverlayModalClosed: (modal: 'runtime-options' | 'subsync' | 'jimaku' | 'kiku') => void;
|
notifyOverlayModalClosed: (modal: 'runtime-options' | 'subsync' | 'jimaku' | 'kiku') => void;
|
||||||
notifyOverlayModalOpened: (modal: 'runtime-options' | 'subsync' | 'jimaku' | 'kiku') => void;
|
notifyOverlayModalOpened: (modal: 'runtime-options' | 'subsync' | 'jimaku' | 'kiku') => void;
|
||||||
|
|||||||
37
vendor/yomitan/js/app/frontend.js
vendored
37
vendor/yomitan/js/app/frontend.js
vendored
@@ -28,6 +28,40 @@ import {TextSourceGenerator} from '../dom/text-source-generator.js';
|
|||||||
import {TextSourceRange} from '../dom/text-source-range.js';
|
import {TextSourceRange} from '../dom/text-source-range.js';
|
||||||
import {TextScanner} from '../language/text-scanner.js';
|
import {TextScanner} from '../language/text-scanner.js';
|
||||||
|
|
||||||
|
const SUBMINER_FRONTEND_COMMAND_EVENT = 'subminer-yomitan-popup-command';
|
||||||
|
const subminerFrontendInstances = new Set();
|
||||||
|
let subminerFrontendCommandBridgeRegistered = false;
|
||||||
|
|
||||||
|
function getActiveFrontendForSubminerCommand() {
|
||||||
|
/** @type {?Frontend} */
|
||||||
|
let fallback = null;
|
||||||
|
for (const frontend of subminerFrontendInstances) {
|
||||||
|
if (frontend._textScanner?.isEnabled?.()) {
|
||||||
|
return frontend;
|
||||||
|
}
|
||||||
|
if (fallback === null) {
|
||||||
|
fallback = frontend;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerSubminerFrontendCommandBridge() {
|
||||||
|
if (subminerFrontendCommandBridgeRegistered) { return; }
|
||||||
|
subminerFrontendCommandBridgeRegistered = true;
|
||||||
|
|
||||||
|
window.addEventListener(SUBMINER_FRONTEND_COMMAND_EVENT, (event) => {
|
||||||
|
const frontend = getActiveFrontendForSubminerCommand();
|
||||||
|
if (frontend === null) { return; }
|
||||||
|
const detail = event.detail;
|
||||||
|
if (typeof detail !== 'object' || detail === null) { return; }
|
||||||
|
|
||||||
|
if (detail.type === 'scanSelectedText') {
|
||||||
|
frontend._onApiScanSelectedText();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the main class responsible for scanning and handling webpage content.
|
* This is the main class responsible for scanning and handling webpage content.
|
||||||
*/
|
*/
|
||||||
@@ -158,6 +192,9 @@ export class Frontend {
|
|||||||
* Prepares the instance for use.
|
* Prepares the instance for use.
|
||||||
*/
|
*/
|
||||||
async prepare() {
|
async prepare() {
|
||||||
|
registerSubminerFrontendCommandBridge();
|
||||||
|
subminerFrontendInstances.add(this);
|
||||||
|
|
||||||
await this.updateOptions();
|
await this.updateOptions();
|
||||||
try {
|
try {
|
||||||
const {zoomFactor} = await this._application.api.getZoom();
|
const {zoomFactor} = await this._application.api.getZoom();
|
||||||
|
|||||||
84
vendor/yomitan/js/app/popup.js
vendored
84
vendor/yomitan/js/app/popup.js
vendored
@@ -28,6 +28,85 @@ import {loadStyle} from '../dom/style-util.js';
|
|||||||
import {checkPopupPreviewURL} from '../pages/settings/popup-preview-controller.js';
|
import {checkPopupPreviewURL} from '../pages/settings/popup-preview-controller.js';
|
||||||
import {ThemeController} from './theme-controller.js';
|
import {ThemeController} from './theme-controller.js';
|
||||||
|
|
||||||
|
const SUBMINER_POPUP_COMMAND_EVENT = 'subminer-yomitan-popup-command';
|
||||||
|
const subminerPopupInstances = new Set();
|
||||||
|
let subminerPopupCommandBridgeRegistered = false;
|
||||||
|
|
||||||
|
function getActivePopupForSubminerCommand() {
|
||||||
|
/** @type {?Popup} */
|
||||||
|
let fallback = null;
|
||||||
|
for (const popup of subminerPopupInstances) {
|
||||||
|
if (!popup.isVisibleSync()) { continue; }
|
||||||
|
fallback = popup;
|
||||||
|
if (popup.isPointerOverSelfOrChildren()) {
|
||||||
|
return popup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerSubminerPopupCommandBridge() {
|
||||||
|
if (subminerPopupCommandBridgeRegistered) { return; }
|
||||||
|
subminerPopupCommandBridgeRegistered = true;
|
||||||
|
window.addEventListener(SUBMINER_POPUP_COMMAND_EVENT, (event) => {
|
||||||
|
const popup = getActivePopupForSubminerCommand();
|
||||||
|
if (popup === null) { return; }
|
||||||
|
const detail = event.detail;
|
||||||
|
if (typeof detail !== 'object' || detail === null) { return; }
|
||||||
|
|
||||||
|
if (detail.type === 'simulateHotkey') {
|
||||||
|
const key = detail.key;
|
||||||
|
const rawModifiers = detail.modifiers;
|
||||||
|
if (typeof key !== 'string' || !Array.isArray(rawModifiers)) { return; }
|
||||||
|
const modifiers = rawModifiers.filter((modifier) => (
|
||||||
|
modifier === 'alt' ||
|
||||||
|
modifier === 'ctrl' ||
|
||||||
|
modifier === 'shift' ||
|
||||||
|
modifier === 'meta'
|
||||||
|
));
|
||||||
|
void popup._invokeSafe('displaySimulateHotkey', {key, modifiers});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detail.type === 'forwardKeyDown') {
|
||||||
|
const code = detail.code;
|
||||||
|
const key = detail.key;
|
||||||
|
const rawModifiers = detail.modifiers;
|
||||||
|
if (typeof code !== 'string' || typeof key !== 'string' || !Array.isArray(rawModifiers)) { return; }
|
||||||
|
const modifiers = rawModifiers.filter((modifier) => (
|
||||||
|
modifier === 'alt' ||
|
||||||
|
modifier === 'ctrl' ||
|
||||||
|
modifier === 'shift' ||
|
||||||
|
modifier === 'meta'
|
||||||
|
));
|
||||||
|
void popup._invokeSafe('displayForwardKeyDown', {
|
||||||
|
key,
|
||||||
|
code,
|
||||||
|
modifiers,
|
||||||
|
repeat: detail.repeat === true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detail.type === 'mineSelected') {
|
||||||
|
void popup._invokeSafe('displayMineSelected', void 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detail.type === 'cycleAudioSource') {
|
||||||
|
const direction = detail.direction === -1 ? -1 : 1;
|
||||||
|
void popup._invokeSafe('displayAudioCycleSource', {direction});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detail.type === 'setVisible') {
|
||||||
|
if (detail.visible === false) {
|
||||||
|
popup.hide(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is the container which hosts the display of search results.
|
* This class is the container which hosts the display of search results.
|
||||||
* @augments EventDispatcher<import('popup').Events>
|
* @augments EventDispatcher<import('popup').Events>
|
||||||
@@ -209,6 +288,8 @@ export class Popup extends EventDispatcher {
|
|||||||
* Prepares the popup for use.
|
* Prepares the popup for use.
|
||||||
*/
|
*/
|
||||||
prepare() {
|
prepare() {
|
||||||
|
registerSubminerPopupCommandBridge();
|
||||||
|
subminerPopupInstances.add(this);
|
||||||
this._frame.addEventListener('mouseover', this._onFrameMouseOver.bind(this));
|
this._frame.addEventListener('mouseover', this._onFrameMouseOver.bind(this));
|
||||||
this._frame.addEventListener('mouseout', this._onFrameMouseOut.bind(this));
|
this._frame.addEventListener('mouseout', this._onFrameMouseOut.bind(this));
|
||||||
this._frame.addEventListener('mousedown', (e) => e.stopPropagation());
|
this._frame.addEventListener('mousedown', (e) => e.stopPropagation());
|
||||||
@@ -471,6 +552,7 @@ export class Popup extends EventDispatcher {
|
|||||||
*/
|
*/
|
||||||
_onFrameMouseOver() {
|
_onFrameMouseOver() {
|
||||||
this._isPointerOverPopup = true;
|
this._isPointerOverPopup = true;
|
||||||
|
window.dispatchEvent(new CustomEvent('yomitan-popup-mouse-enter'));
|
||||||
|
|
||||||
this.stopHideDelayed();
|
this.stopHideDelayed();
|
||||||
this.trigger('mouseOver', {});
|
this.trigger('mouseOver', {});
|
||||||
@@ -486,6 +568,7 @@ export class Popup extends EventDispatcher {
|
|||||||
*/
|
*/
|
||||||
_onFrameMouseOut() {
|
_onFrameMouseOut() {
|
||||||
this._isPointerOverPopup = false;
|
this._isPointerOverPopup = false;
|
||||||
|
window.dispatchEvent(new CustomEvent('yomitan-popup-mouse-leave'));
|
||||||
|
|
||||||
this.trigger('mouseOut', {});
|
this.trigger('mouseOut', {});
|
||||||
|
|
||||||
@@ -836,6 +919,7 @@ export class Popup extends EventDispatcher {
|
|||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_onExtensionUnloaded() {
|
_onExtensionUnloaded() {
|
||||||
|
subminerPopupInstances.delete(this);
|
||||||
this._invokeWindow('displayExtensionUnloaded', void 0);
|
this._invokeWindow('displayExtensionUnloaded', void 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
109
vendor/yomitan/js/display/display-audio.js
vendored
109
vendor/yomitan/js/display/display-audio.js
vendored
@@ -69,6 +69,10 @@ export class DisplayAudio {
|
|||||||
]);
|
]);
|
||||||
/** @type {?boolean} */
|
/** @type {?boolean} */
|
||||||
this._enableDefaultAudioSources = null;
|
this._enableDefaultAudioSources = null;
|
||||||
|
/** @type {?number} */
|
||||||
|
this._audioCycleSourceIndex = null;
|
||||||
|
/** @type {Map<number, number>} */
|
||||||
|
this._audioCycleAudioInfoIndexMap = new Map();
|
||||||
/** @type {(event: MouseEvent) => void} */
|
/** @type {(event: MouseEvent) => void} */
|
||||||
this._onAudioPlayButtonClickBind = this._onAudioPlayButtonClick.bind(this);
|
this._onAudioPlayButtonClickBind = this._onAudioPlayButtonClick.bind(this);
|
||||||
/** @type {(event: MouseEvent) => void} */
|
/** @type {(event: MouseEvent) => void} */
|
||||||
@@ -96,6 +100,7 @@ export class DisplayAudio {
|
|||||||
]);
|
]);
|
||||||
this._display.registerDirectMessageHandlers([
|
this._display.registerDirectMessageHandlers([
|
||||||
['displayAudioClearAutoPlayTimer', this._onMessageClearAutoPlayTimer.bind(this)],
|
['displayAudioClearAutoPlayTimer', this._onMessageClearAutoPlayTimer.bind(this)],
|
||||||
|
['displayAudioCycleSource', this._onMessageCycleAudioSource.bind(this)],
|
||||||
]);
|
]);
|
||||||
/* eslint-enable @stylistic/no-multi-spaces */
|
/* eslint-enable @stylistic/no-multi-spaces */
|
||||||
this._display.on('optionsUpdated', this._onOptionsUpdated.bind(this));
|
this._display.on('optionsUpdated', this._onOptionsUpdated.bind(this));
|
||||||
@@ -186,6 +191,8 @@ export class DisplayAudio {
|
|||||||
/** @type {Map<string, import('display-audio').AudioSource[]>} */
|
/** @type {Map<string, import('display-audio').AudioSource[]>} */
|
||||||
const nameMap = new Map();
|
const nameMap = new Map();
|
||||||
this._audioSources.length = 0;
|
this._audioSources.length = 0;
|
||||||
|
this._audioCycleSourceIndex = null;
|
||||||
|
this._audioCycleAudioInfoIndexMap.clear();
|
||||||
for (const {type, url, voice} of sources) {
|
for (const {type, url, voice} of sources) {
|
||||||
this._addAudioSourceInfo(type, url, voice, true, nameMap);
|
this._addAudioSourceInfo(type, url, voice, true, nameMap);
|
||||||
requiredAudioSources.delete(type);
|
requiredAudioSources.delete(type);
|
||||||
@@ -204,6 +211,8 @@ export class DisplayAudio {
|
|||||||
_onContentClear() {
|
_onContentClear() {
|
||||||
this._entriesToken = {};
|
this._entriesToken = {};
|
||||||
this._cache.clear();
|
this._cache.clear();
|
||||||
|
this._audioCycleSourceIndex = null;
|
||||||
|
this._audioCycleAudioInfoIndexMap.clear();
|
||||||
this.clearAutoPlayTimer();
|
this.clearAutoPlayTimer();
|
||||||
this._eventListeners.removeAllEventListeners();
|
this._eventListeners.removeAllEventListeners();
|
||||||
}
|
}
|
||||||
@@ -273,6 +282,73 @@ export class DisplayAudio {
|
|||||||
this.clearAutoPlayTimer();
|
this.clearAutoPlayTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{direction?: number}} details
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async _onMessageCycleAudioSource({direction}) {
|
||||||
|
/** @type {import('display-audio').AudioSource[]} */
|
||||||
|
const configuredSources = this._audioSources.filter((source) => source.isInOptions);
|
||||||
|
const sources = configuredSources.length > 0 ? configuredSources : this._audioSources;
|
||||||
|
if (sources.length === 0) { return false; }
|
||||||
|
|
||||||
|
const dictionaryEntryIndex = this._display.selectedIndex;
|
||||||
|
const headwordIndex = 0;
|
||||||
|
const headword = this._getHeadword(dictionaryEntryIndex, headwordIndex);
|
||||||
|
if (headword === null) { return false; }
|
||||||
|
const {term, reading} = headword;
|
||||||
|
|
||||||
|
const primaryCardAudio = this._getPrimaryCardAudio(term, reading);
|
||||||
|
let source = null;
|
||||||
|
if (primaryCardAudio !== null) {
|
||||||
|
source = sources.find((item) => item.index === primaryCardAudio.index) ?? null;
|
||||||
|
}
|
||||||
|
if (source === null) {
|
||||||
|
const fallbackIndex = (
|
||||||
|
this._audioCycleSourceIndex !== null &&
|
||||||
|
this._audioCycleSourceIndex >= 0 &&
|
||||||
|
this._audioCycleSourceIndex < sources.length
|
||||||
|
) ? this._audioCycleSourceIndex : 0;
|
||||||
|
source = sources[fallbackIndex] ?? null;
|
||||||
|
this._audioCycleSourceIndex = fallbackIndex;
|
||||||
|
}
|
||||||
|
if (source === null) { return false; }
|
||||||
|
|
||||||
|
const infoList = await this._getSourceAudioInfoList(source, term, reading);
|
||||||
|
const infoListLength = infoList.length;
|
||||||
|
if (infoListLength === 0) { return false; }
|
||||||
|
|
||||||
|
const step = direction === -1 ? -1 : 1;
|
||||||
|
let currentSubIndex = this._audioCycleAudioInfoIndexMap.get(source.index);
|
||||||
|
if (
|
||||||
|
typeof currentSubIndex !== 'number' &&
|
||||||
|
primaryCardAudio !== null &&
|
||||||
|
primaryCardAudio.index === source.index &&
|
||||||
|
primaryCardAudio.subIndex !== null
|
||||||
|
) {
|
||||||
|
currentSubIndex = primaryCardAudio.subIndex;
|
||||||
|
}
|
||||||
|
if (typeof currentSubIndex !== 'number') {
|
||||||
|
currentSubIndex = step > 0 ? -1 : infoListLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < infoListLength; ++i) {
|
||||||
|
currentSubIndex = (currentSubIndex + step + infoListLength) % infoListLength;
|
||||||
|
const {valid} = await this._playAudio(
|
||||||
|
dictionaryEntryIndex,
|
||||||
|
headwordIndex,
|
||||||
|
[source],
|
||||||
|
currentSubIndex,
|
||||||
|
);
|
||||||
|
if (valid) {
|
||||||
|
this._audioCycleAudioInfoIndexMap.set(source.index, currentSubIndex);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('settings').AudioSourceType} type
|
* @param {import('settings').AudioSourceType} type
|
||||||
* @param {string} url
|
* @param {string} url
|
||||||
@@ -691,6 +767,39 @@ export class DisplayAudio {
|
|||||||
return infoList.map((info) => ({info, audioPromise: null, audioResolved: false, audio: null}));
|
return infoList.map((info) => ({info, audioPromise: null, audioResolved: false, audio: null}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('display-audio').AudioSource} source
|
||||||
|
* @param {string} term
|
||||||
|
* @param {string} reading
|
||||||
|
* @returns {Promise<import('display-audio').AudioInfoList>}
|
||||||
|
*/
|
||||||
|
async _getSourceAudioInfoList(source, term, reading) {
|
||||||
|
const cacheItem = this._getCacheItem(term, reading, true);
|
||||||
|
if (typeof cacheItem === 'undefined') { return []; }
|
||||||
|
const {sourceMap} = cacheItem;
|
||||||
|
|
||||||
|
let cacheUpdated = false;
|
||||||
|
let sourceInfo = sourceMap.get(source.index);
|
||||||
|
if (typeof sourceInfo === 'undefined') {
|
||||||
|
const infoListPromise = this._getTermAudioInfoList(source, term, reading);
|
||||||
|
sourceInfo = {infoListPromise, infoList: null};
|
||||||
|
sourceMap.set(source.index, sourceInfo);
|
||||||
|
cacheUpdated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {infoList} = sourceInfo;
|
||||||
|
if (infoList === null) {
|
||||||
|
infoList = await sourceInfo.infoListPromise;
|
||||||
|
sourceInfo.infoList = infoList;
|
||||||
|
cacheUpdated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cacheUpdated) {
|
||||||
|
this._updateOpenMenu();
|
||||||
|
}
|
||||||
|
return infoList;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} dictionaryEntryIndex
|
* @param {number} dictionaryEntryIndex
|
||||||
* @param {number} headwordIndex
|
* @param {number} headwordIndex
|
||||||
|
|||||||
54
vendor/yomitan/js/display/display.js
vendored
54
vendor/yomitan/js/display/display.js
vendored
@@ -224,6 +224,9 @@ export class Display extends EventDispatcher {
|
|||||||
['displaySetContentScale', this._onMessageSetContentScale.bind(this)],
|
['displaySetContentScale', this._onMessageSetContentScale.bind(this)],
|
||||||
['displayConfigure', this._onMessageConfigure.bind(this)],
|
['displayConfigure', this._onMessageConfigure.bind(this)],
|
||||||
['displayVisibilityChanged', this._onMessageVisibilityChanged.bind(this)],
|
['displayVisibilityChanged', this._onMessageVisibilityChanged.bind(this)],
|
||||||
|
['displaySimulateHotkey', this._onMessageSimulateHotkey.bind(this)],
|
||||||
|
['displayForwardKeyDown', this._onMessageForwardKeyDown.bind(this)],
|
||||||
|
['displayMineSelected', this._onMessageMineSelected.bind(this)],
|
||||||
]);
|
]);
|
||||||
this.registerWindowMessageHandlers([
|
this.registerWindowMessageHandlers([
|
||||||
['displayExtensionUnloaded', this._onMessageExtensionUnloaded.bind(this)],
|
['displayExtensionUnloaded', this._onMessageExtensionUnloaded.bind(this)],
|
||||||
@@ -785,6 +788,57 @@ export class Display extends EventDispatcher {
|
|||||||
this.trigger('frameVisibilityChange', {value});
|
this.trigger('frameVisibilityChange', {value});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{key: string, modifiers: unknown[]}} details
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
_onMessageSimulateHotkey({key, modifiers}) {
|
||||||
|
if (typeof key !== 'string' || !Array.isArray(modifiers)) { return false; }
|
||||||
|
const normalizedModifiers = modifiers.filter((modifier) => (
|
||||||
|
modifier === 'alt' ||
|
||||||
|
modifier === 'ctrl' ||
|
||||||
|
modifier === 'shift' ||
|
||||||
|
modifier === 'meta'
|
||||||
|
));
|
||||||
|
return this._hotkeyHandler.simulate(key, normalizedModifiers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{key: string, code: string, modifiers: unknown[], repeat?: boolean}} details
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
_onMessageForwardKeyDown({key, code, modifiers, repeat = false}) {
|
||||||
|
if (typeof key !== 'string' || typeof code !== 'string' || !Array.isArray(modifiers)) { return false; }
|
||||||
|
const normalizedModifiers = modifiers.filter((modifier) => (
|
||||||
|
modifier === 'alt' ||
|
||||||
|
modifier === 'ctrl' ||
|
||||||
|
modifier === 'shift' ||
|
||||||
|
modifier === 'meta'
|
||||||
|
));
|
||||||
|
const eventInit = {
|
||||||
|
key,
|
||||||
|
code,
|
||||||
|
repeat,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
composed: true,
|
||||||
|
altKey: normalizedModifiers.includes('alt'),
|
||||||
|
ctrlKey: normalizedModifiers.includes('ctrl'),
|
||||||
|
shiftKey: normalizedModifiers.includes('shift'),
|
||||||
|
metaKey: normalizedModifiers.includes('meta'),
|
||||||
|
};
|
||||||
|
document.dispatchEvent(new KeyboardEvent('keydown', eventInit));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
_onMessageMineSelected() {
|
||||||
|
document.dispatchEvent(new CustomEvent('subminer-display-mine-selected'));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/** @type {import('display').WindowApiHandler<'displayExtensionUnloaded'>} */
|
/** @type {import('display').WindowApiHandler<'displayExtensionUnloaded'>} */
|
||||||
_onMessageExtensionUnloaded() {
|
_onMessageExtensionUnloaded() {
|
||||||
this._application.webExtension.triggerUnloaded();
|
this._application.webExtension.triggerUnloaded();
|
||||||
|
|||||||
44
vendor/yomitan/js/display/popup-main.js
vendored
44
vendor/yomitan/js/display/popup-main.js
vendored
@@ -47,6 +47,50 @@ await Application.main(true, async (application) => {
|
|||||||
const displayResizer = new DisplayResizer(display);
|
const displayResizer = new DisplayResizer(display);
|
||||||
displayResizer.prepare();
|
displayResizer.prepare();
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (event) => {
|
||||||
|
if (event.defaultPrevented || event.repeat) { return; }
|
||||||
|
if (event.ctrlKey || event.metaKey || event.altKey) { return; }
|
||||||
|
|
||||||
|
const target = /** @type {?Element} */ (event.target instanceof Element ? event.target : null);
|
||||||
|
if (target !== null) {
|
||||||
|
if (target.closest('input, textarea, select, [contenteditable="true"]')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = event.code;
|
||||||
|
if (code === 'KeyJ' || code === 'KeyK') {
|
||||||
|
const scanningOptions = display.getOptions()?.scanning;
|
||||||
|
const scale = Number.isFinite(scanningOptions?.reducedMotionScrollingScale)
|
||||||
|
? scanningOptions.reducedMotionScrollingScale
|
||||||
|
: 1;
|
||||||
|
display._scrollByPopupHeight(code === 'KeyJ' ? 1 : -1, scale);
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code === 'KeyM') {
|
||||||
|
displayAnki._hotkeySaveAnkiNoteForSelectedEntry('0');
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code === 'KeyP') {
|
||||||
|
void displayAudio.playAudio(display.selectedIndex, 0);
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code === 'BracketLeft' || code === 'BracketRight') {
|
||||||
|
displayAudio._onMessageCycleAudioSource({direction: code === 'BracketLeft' ? 1 : -1});
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('subminer-display-mine-selected', () => {
|
||||||
|
displayAnki._hotkeySaveAnkiNoteForSelectedEntry('0');
|
||||||
|
});
|
||||||
|
|
||||||
display.initializeState();
|
display.initializeState();
|
||||||
|
|
||||||
document.documentElement.dataset.loaded = 'true';
|
document.documentElement.dataset.loaded = 'true';
|
||||||
|
|||||||
Reference in New Issue
Block a user