feat(dictionary): add Hachidori backend support (#266)

This commit is contained in:
sudacode authored and GitHub committed 2026-10-24 21:08:18 -07:00
1 parent e690ffba07
commit cbaa4626e1
143 files changed
+5564 -305

No files matched your search

+33 -21
View File
@@ -4,9 +4,12 @@ import { createMpvInputForwarding } from './mpv-input-forwarding';
import { MPV_MOUSE_BUTTON_BY_BUTTON, wheelEventToMpvWheel } from '../../shared/mpv-input-bindings';
import { dispatchConfiguredMpvCommand } from '../utils/mpv-command-dispatch';
import {
YOMITAN_POPUP_HIDDEN_EVENT,
YOMITAN_POPUP_SHOWN_EVENT,
registerDictionaryPopupVisibilityListener,
HACHIDORI_HOST_SELECTOR,
YOMITAN_POPUP_COMMAND_EVENT,
YOMITAN_POPUP_HOST_SELECTOR,
isHachidoriPopupOpen,
isLookupPopupOpen,
isYomitanPopupVisible,
isYomitanPopupIframe,
} from '../yomitan-popup.js';
@@ -80,6 +83,8 @@ export function createKeyboardHandlers(
if (target.closest('.modal')) return true;
if (ctx.dom.subtitleContainer.contains(target)) return true;
if (isYomitanPopupIframe(target)) return true;
// Hachidori's popup lives in a shadow root, so its events arrive retargeted to the host.
if (target.closest(`${YOMITAN_POPUP_HOST_SELECTOR}, ${HACHIDORI_HOST_SELECTOR}`)) return true;
if (target.closest && target.closest('iframe.yomitan-popup, iframe[id^="yomitan-popup"]'))
return true;
return false;
@@ -106,11 +111,13 @@ export function createKeyboardHandlers(
}
// Overlay UI that handles its own mouse input (menus, sidebar, notifications, controls).
// Hachidori's popup lives in the overlay document, unlike Yomitan's iframe, so its
// wheel events reach these handlers retargeted to its host.
function isOverlayControlTarget(target: EventTarget | null): boolean {
if (!(target instanceof Element)) return false;
return Boolean(
target.closest(
'.modal, .notification-history, .overlay-notification-stack, button, a, input, select, textarea',
`.modal, .notification-history, .overlay-notification-stack, button, a, input, select, textarea, ${YOMITAN_POPUP_HOST_SELECTOR}, ${HACHIDORI_HOST_SELECTOR}`,
),
);
}
@@ -122,8 +129,7 @@ export function createKeyboardHandlers(
!ctx.state.playlistBrowserModalOpen &&
!ctx.state.youtubePickerModalOpen &&
!ctx.state.subtitleSidebarModalOpen &&
!ctx.state.yomitanPopupVisible &&
!isYomitanPopupVisible(document) &&
!isLookupPopupOpen(ctx.state, document) &&
!isInteractiveTarget(target)
);
}
@@ -515,8 +521,7 @@ export function createKeyboardHandlers(
pendingSelectionAnchorAfterSubtitleSeek = null;
resetSelectionToStartOnNextSubtitleSync = false;
const shouldRefreshLookup =
pendingLookupRefreshAfterSubtitleSeek &&
(ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document));
pendingLookupRefreshAfterSubtitleSeek && isLookupPopupOpen(ctx.state, document);
pendingLookupRefreshAfterSubtitleSeek = false;
if (shouldRefreshLookup) {
queueMicrotask(() => {
@@ -708,7 +713,7 @@ export function createKeyboardHandlers(
if (lookupScanFallbackTimer !== null) clearTimeout(lookupScanFallbackTimer);
lookupScanFallbackTimer = setTimeout(() => {
lookupScanFallbackTimer = null;
if (ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document)) {
if (isLookupPopupOpen(ctx.state, document)) {
return;
}
// Dispatch directly on the selected token span; when overlay pointer-events are disabled,
@@ -738,15 +743,22 @@ export function createKeyboardHandlers(
}
function handleLookupWindowToggleRequested(): void {
if (ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document)) {
if (isLookupPopupOpen(ctx.state, document)) {
closeLookupWindow();
return;
}
triggerLookupForSelectedWord();
}
// Closes any popup state: an open Hachidori pane (its attention may already be
// released) and attention without a pane, so callers like the Kiku modal
// opener leave nothing behind.
function closeLookupWindow(): boolean {
if (!ctx.state.yomitanPopupVisible && !isYomitanPopupVisible(document)) {
if (
!ctx.state.yomitanPopupVisible &&
!isYomitanPopupVisible(document) &&
!isHachidoriPopupOpen(document)
) {
return false;
}
@@ -770,7 +782,7 @@ export function createKeyboardHandlers(
return false;
}
const popupVisible = ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document);
const popupVisible = isLookupPopupOpen(ctx.state, document);
const result = moveKeyboardSelection(delta);
if (result === 'no-words') {
seekAdjacentSubtitleAndQueueSelection(delta, popupVisible);
@@ -791,7 +803,7 @@ export function createKeyboardHandlers(
code: string,
repeat: boolean = true,
): boolean {
if (!ctx.state.yomitanPopupVisible && !isYomitanPopupVisible(document)) {
if (!isLookupPopupOpen(ctx.state, document)) {
return false;
}
dispatchYomitanPopupKeydown(key, code, [], repeat);
@@ -799,7 +811,7 @@ export function createKeyboardHandlers(
}
function mineSelectedFromController(): boolean {
if (!ctx.state.yomitanPopupVisible && !isYomitanPopupVisible(document)) {
if (!isLookupPopupOpen(ctx.state, document)) {
return false;
}
dispatchYomitanPopupMineSelected();
@@ -807,7 +819,7 @@ export function createKeyboardHandlers(
}
function cyclePopupAudioSourceForController(direction: -1 | 1): boolean {
if (!ctx.state.yomitanPopupVisible && !isYomitanPopupVisible(document)) {
if (!isLookupPopupOpen(ctx.state, document)) {
return false;
}
dispatchYomitanPopupCycleAudioSource(direction);
@@ -815,7 +827,7 @@ export function createKeyboardHandlers(
}
function playCurrentAudioForController(): boolean {
if (!ctx.state.yomitanPopupVisible && !isYomitanPopupVisible(document)) {
if (!isLookupPopupOpen(ctx.state, document)) {
return false;
}
dispatchYomitanPopupPlayCurrentAudio();
@@ -823,7 +835,7 @@ export function createKeyboardHandlers(
}
function scrollPopupByController(deltaX: number, deltaY: number): boolean {
if (!ctx.state.yomitanPopupVisible && !isYomitanPopupVisible(document)) {
if (!isLookupPopupOpen(ctx.state, document)) {
return false;
}
dispatchYomitanPopupScrollBy(deltaX, deltaY);
@@ -850,7 +862,7 @@ export function createKeyboardHandlers(
if (!ctx.state.keyboardDrivenModeEnabled) {
return;
}
if (!ctx.state.yomitanPopupVisible && !isYomitanPopupVisible(document)) {
if (!isLookupPopupOpen(ctx.state, document)) {
return;
}
restoreOverlayKeyboardFocus();
@@ -895,7 +907,7 @@ export function createKeyboardHandlers(
}
const key = e.code;
const popupVisible = ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document);
const popupVisible = isLookupPopupOpen(ctx.state, document);
if (key === 'ArrowLeft' || key === 'KeyH') {
const result = moveKeyboardSelection(-1);
if (result === 'start-boundary' || result === 'no-words') {
@@ -1086,7 +1098,7 @@ export function createKeyboardHandlers(
subtree: true,
});
window.addEventListener(YOMITAN_POPUP_HIDDEN_EVENT, () => {
registerDictionaryPopupVisibilityListener('hidden', () => {
clearNativeSubtitleSelection();
if (!ctx.state.keyboardDrivenModeEnabled) {
syncKeyboardTokenSelection();
@@ -1094,7 +1106,7 @@ export function createKeyboardHandlers(
}
restoreOverlayKeyboardFocus();
});
window.addEventListener(YOMITAN_POPUP_SHOWN_EVENT, () => {
registerDictionaryPopupVisibilityListener('shown', () => {
if (!ctx.state.keyboardDrivenModeEnabled) {
return;
}
@@ -1251,7 +1263,7 @@ export function createKeyboardHandlers(
return;
}
if (ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document)) {
if (isLookupPopupOpen(ctx.state, document)) {
if (handleYomitanPopupKeybind(e)) {
e.preventDefault();
return;