mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-11-10 04:46:07 -08:00
Compare commits
3
Commits
bc7e82fee6
...
79e5535823
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79e5535823
|
||
|
|
5140b5e9be
|
||
|
|
c2dac537da
|
No files matched your search
@@ -1,4 +1,4 @@
|
||||
type: added
|
||||
area: overlay
|
||||
|
||||
- The duplicate card (field grouping) dialog has a controller layout: the D-pad picks the original or new card, `A` continues and confirms the merge, `B` cancels or goes back from the preview, `X` turns "Delete duplicate" on or off, `Y` switches Compact / Full, and the D-pad scrolls the preview. Each control shows its shortcut, and the layout can be changed per controller in the new **Duplicate card (field grouping)** group of `Alt+C`.
|
||||
- The duplicate card (field grouping) dialog has a controller layout: the D-pad picks the original or new card, `A` continues and confirms the merge, `B` cancels or goes back from the preview, `X` turns "Delete duplicate" on or off, `Y` switches Compact / Full, and the D-pad scrolls the preview. Each control shows its shortcut while a controller is connected, and the layout can be changed per controller in the new **Duplicate card (field grouping)** group of `Alt+C`.
|
||||
@@ -1,5 +1,5 @@
|
||||
type: added
|
||||
area: overlay
|
||||
|
||||
- Added a controller layout for the media timing review dialog covering every button, with L1 / R1 + D-pad combos for the clip start and end, and D-pad actions that repeat while held. Each button shows its controller shortcut while controller support is on, and the first press in a newly opened dialog already counts. The controller's usual actions pause while the dialog is open, including in the hidden subtitle overlay behind it.
|
||||
- Added a controller layout for the media timing review dialog covering every button, with L1 / R1 + D-pad combos for the clip start and end, and D-pad actions that repeat while held. Each button shows its controller shortcut while a controller is connected, and the first press in a newly opened dialog already counts. The controller's usual actions pause while the dialog is open, including in the hidden subtitle overlay behind it.
|
||||
- Added `controller.faceButtonLabels` (`xbox` or `nintendo`) so fixed controller layouts and the `Alt+C` button names follow the letters printed on the pad. Use `nintendo` for controllers with A on the right and B at the bottom.
|
||||
@@ -162,7 +162,7 @@ When you cancel, you can go back to editing, keep the original timing, create th
|
||||
|
||||
#### With a controller
|
||||
|
||||
With [controller support](/usage#controller-support) on, the dialog has its own controller layout, and each button shows its controller shortcut. It works with or without keyboard-only mode, and the controller's usual actions pause until the dialog closes. Buttons that were already held when the dialog opened, such as the mine button, wait for a fresh press.
|
||||
With [controller support](/usage#controller-support) on, the dialog has its own controller layout, and each button shows its controller shortcut once SubMiner has seen a controller, which takes one button press after connecting it. It works with or without keyboard-only mode, and the controller's usual actions pause until the dialog closes. Buttons that were already held when the dialog opened, such as the mine button, wait for a fresh press.
|
||||
|
||||
`A`, `B`, `X` and `Y` mean the letters printed on your controller. Pick your controller's **Button Style** in `Alt+C`: choose Nintendo if A is on the right and B at the bottom (Switch-style and many 8BitDo pads). On PlayStation, `A` is ╳, `B` ○, `X` □ and `Y` △, and the shortcuts on screen show those symbols.
|
||||
|
||||
@@ -267,7 +267,7 @@ The manual dialog cancels itself after 90 seconds. Identical entries are not ded
|
||||
|
||||
### With a controller {#field-grouping-with-a-controller}
|
||||
|
||||
With [controller support](/usage#controller-support) on, the dialog has its own controller layout, and each control shows its controller shortcut. Like the [media timing review](#with-a-controller), it works with or without keyboard-only mode, `A`/`B`/`X`/`Y` are the printed letters (set by **Button Style** in `Alt+C`), and a button already held when the dialog opens waits for a fresh press.
|
||||
With [controller support](/usage#controller-support) on, the dialog has its own controller layout, and each control shows its controller shortcut once SubMiner has seen a controller. Like the [media timing review](#with-a-controller), it works with or without keyboard-only mode, `A`/`B`/`X`/`Y` are the printed letters (set by **Button Style** in `Alt+C`), and a button already held when the dialog opens waits for a fresh press.
|
||||
|
||||
| Controller | Choosing the card | Merge preview |
|
||||
| ------------------ | --------------------------------- | ----------------------- |
|
||||
|
||||
@@ -8,6 +8,9 @@ const http = require('node:http');
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const parser = require(path.join(root, 'dist/core/services/tokenizer/yomitan-parser-runtime.js'));
|
||||
const { writeStoredZip } = require(path.join(root, 'dist/shared/stored-zip.js'));
|
||||
const { HACHIDORI_POPUP_OPEN_SELECTOR } = require(
|
||||
path.join(root, 'dist/renderer/yomitan-popup.js'),
|
||||
);
|
||||
const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-hachidori-parser-'));
|
||||
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
@@ -43,6 +46,58 @@ function fixture(name, title, bankName, entries, frequencyMode = 'rank-based') {
|
||||
return zipPath;
|
||||
}
|
||||
|
||||
// The renderer's popup auto-pause and key routing read this marker because the
|
||||
// reader's shadow root is closed. Drives the real reader with real input, since
|
||||
// stubbed DOM tests cannot notice an upstream merge that changes what it exposes.
|
||||
async function assertPopupOpenMarkerFollowsPane(targetSession) {
|
||||
const server = http.createServer((_request, response) => {
|
||||
response.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
response.end(
|
||||
'<!doctype html><p id="word" style="font-size:48px;margin:300px 0 0 100px">食べた</p>',
|
||||
);
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||
const page = new BrowserWindow({
|
||||
width: 900,
|
||||
height: 600,
|
||||
show: true,
|
||||
webPreferences: { session: targetSession, contextIsolation: true, nodeIntegration: false },
|
||||
});
|
||||
try {
|
||||
await page.loadURL(`http://127.0.0.1:${server.address().port}/`);
|
||||
const { webContents } = page;
|
||||
const word = await webContents.executeJavaScript(
|
||||
`(() => { const { x, y, height } = document.getElementById('word').getBoundingClientRect(); return { x, y, height }; })()`,
|
||||
);
|
||||
const repeatUntilMarker = async (expected, act) => {
|
||||
const query = `document.querySelector(${JSON.stringify(HACHIDORI_POPUP_OPEN_SELECTOR)}) !== null`;
|
||||
for (let attempt = 0; attempt < 100; attempt += 1) {
|
||||
// Content scripts load asynchronously, so keep acting until the reader reacts.
|
||||
act(attempt);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
if ((await webContents.executeJavaScript(query)) === expected) return;
|
||||
}
|
||||
assert.fail(`Hachidori popup-open marker never became ${expected}`);
|
||||
};
|
||||
page.focus();
|
||||
webContents.focus();
|
||||
await repeatUntilMarker(true, (attempt) =>
|
||||
webContents.sendInputEvent({
|
||||
type: 'mouseMove',
|
||||
x: Math.round(word.x + 24) + (attempt % 2),
|
||||
y: Math.round(word.y + word.height / 2),
|
||||
}),
|
||||
);
|
||||
await repeatUntilMarker(false, () => {
|
||||
webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Escape' });
|
||||
webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Escape' });
|
||||
});
|
||||
} finally {
|
||||
page.destroy();
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
}
|
||||
}
|
||||
|
||||
app
|
||||
.whenReady()
|
||||
.then(async () => {
|
||||
@@ -211,6 +266,7 @@ app
|
||||
assert.deepEqual(unmatched, []);
|
||||
assert.equal(await parser.getYomitanCurrentAnkiDeckName(deps, logger), 'Test Mining');
|
||||
assert.equal((await targetSession.extensions.getAllExtensions()).length, 1);
|
||||
await assertPopupOpenMarkerFollowsPane(targetSession);
|
||||
assert.equal(
|
||||
await parser.syncYomitanDefaultAnkiServer('http://127.0.0.1:8765', deps, logger),
|
||||
true,
|
||||
|
||||
@@ -157,6 +157,22 @@ function setup(t: TestContext, options: { platform?: NodeJS.Platform; visible?:
|
||||
Object.defineProperty(process, 'platform', { value: options.platform, configurable: true });
|
||||
t.after(() => Object.defineProperty(process, 'platform', { value: originalPlatform }));
|
||||
}
|
||||
if (options.platform === 'linux') {
|
||||
// Fullscreen recreation only runs outside Hyprland and Sway, so pin an X11
|
||||
// session rather than inheriting the compositor the tests happen to run under.
|
||||
for (const key of [
|
||||
'HYPRLAND_INSTANCE_SIGNATURE',
|
||||
'SWAYSOCK',
|
||||
'XDG_CURRENT_DESKTOP',
|
||||
'XDG_SESSION_DESKTOP',
|
||||
]) {
|
||||
const original = process.env[key];
|
||||
delete process.env[key];
|
||||
t.after(() => {
|
||||
if (original !== undefined) process.env[key] = original;
|
||||
});
|
||||
}
|
||||
}
|
||||
const recorder = createRecorder();
|
||||
const state: FixtureState = {
|
||||
visible: options.visible ?? false,
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { createControllerHintVisibility } from './controller-hint-visibility.js';
|
||||
|
||||
function createHarness(answers: Array<string | null | Error>) {
|
||||
const pending = [...answers];
|
||||
let requests = 0;
|
||||
const visibility = createControllerHintVisibility({
|
||||
getOverlayControllerId: async () => {
|
||||
requests += 1;
|
||||
const answer = pending.shift() ?? null;
|
||||
if (answer instanceof Error) throw answer;
|
||||
return answer;
|
||||
},
|
||||
});
|
||||
const frame = async (now: number, activeGamepadId: string | null = null) => {
|
||||
const visible = visibility.update({ enabled: true, dialogOpen: true, activeGamepadId, now });
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
return visible;
|
||||
};
|
||||
return { visibility, frame, getRequests: () => requests };
|
||||
}
|
||||
|
||||
test('hints stay hidden while no window can see a controller', async () => {
|
||||
const { frame } = createHarness([null, null]);
|
||||
|
||||
assert.equal(await frame(0), false);
|
||||
assert.equal(await frame(16), false);
|
||||
assert.equal(await frame(2_000), false);
|
||||
});
|
||||
|
||||
test('a controller the dialog window can see shows hints without asking the overlay', async () => {
|
||||
const { frame, getRequests } = createHarness([]);
|
||||
|
||||
assert.equal(await frame(0, 'pad-1'), true);
|
||||
assert.equal(getRequests(), 0);
|
||||
});
|
||||
|
||||
test('a fresh dialog window shows hints for the controller the overlay reports', async () => {
|
||||
const { frame } = createHarness(['pad-1']);
|
||||
|
||||
assert.equal(await frame(0), false);
|
||||
assert.equal(await frame(16), true);
|
||||
});
|
||||
|
||||
test('hints hide on the next refresh after the overlay loses the controller', async () => {
|
||||
const { frame, getRequests } = createHarness(['pad-1', null]);
|
||||
|
||||
await frame(0);
|
||||
assert.equal(await frame(500), true);
|
||||
assert.equal(getRequests(), 1);
|
||||
|
||||
await frame(1_000);
|
||||
assert.equal(getRequests(), 2);
|
||||
assert.equal(await frame(1_016), false);
|
||||
});
|
||||
|
||||
test('a failed request keeps the last answer and retries on the next refresh', async () => {
|
||||
const { frame, getRequests } = createHarness(['pad-1', new Error('ipc closed'), null]);
|
||||
|
||||
await frame(0);
|
||||
await frame(1_000);
|
||||
assert.equal(await frame(1_016), true);
|
||||
|
||||
await frame(2_000);
|
||||
assert.equal(getRequests(), 3);
|
||||
assert.equal(await frame(2_016), false);
|
||||
});
|
||||
|
||||
test('nothing is requested while controller support is off or no dialog is open', () => {
|
||||
const { visibility, getRequests } = createHarness([]);
|
||||
|
||||
assert.equal(
|
||||
visibility.update({ enabled: false, dialogOpen: true, activeGamepadId: 'pad-1', now: 0 }),
|
||||
false,
|
||||
);
|
||||
visibility.update({ enabled: true, dialogOpen: false, activeGamepadId: null, now: 0 });
|
||||
assert.equal(getRequests(), 0);
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
const OVERLAY_CONTROLLER_REFRESH_MS = 1_000;
|
||||
|
||||
/**
|
||||
* Decides whether dialogs show their controller shortcut tags. A fresh dialog window cannot
|
||||
* see a pad until a button is pressed in it, so an open dialog also asks main which controller
|
||||
* the overlay is reading. Called every frame; requests are limited to one per refresh interval.
|
||||
*/
|
||||
export function createControllerHintVisibility(options: {
|
||||
getOverlayControllerId: () => Promise<string | null>;
|
||||
}) {
|
||||
let overlayHasController = false;
|
||||
let nextRefreshAt = Number.NEGATIVE_INFINITY;
|
||||
|
||||
function refreshOverlayController(now: number): void {
|
||||
if (now < nextRefreshAt) return;
|
||||
nextRefreshAt = now + OVERLAY_CONTROLLER_REFRESH_MS;
|
||||
options.getOverlayControllerId().then(
|
||||
(gamepadId) => {
|
||||
overlayHasController = gamepadId !== null;
|
||||
},
|
||||
() => {
|
||||
// Keep the last answer; the next refresh tries again.
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
update(state: {
|
||||
enabled: boolean;
|
||||
dialogOpen: boolean;
|
||||
activeGamepadId: string | null;
|
||||
now: number;
|
||||
}): boolean {
|
||||
if (!state.enabled) return false;
|
||||
if (state.activeGamepadId !== null) return true;
|
||||
if (state.dialogOpen) refreshOverlayController(state.now);
|
||||
return overlayHasController;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import test from 'node:test';
|
||||
|
||||
import { createKeyboardHandlers } from './keyboard.js';
|
||||
import { createRendererState } from '../state.js';
|
||||
import { HACHIDORI_HOST_SELECTOR } from '../yomitan-popup.js';
|
||||
import { HACHIDORI_HOST_SELECTOR, HACHIDORI_POPUP_OPEN_SELECTOR } from '../yomitan-popup.js';
|
||||
import type { CompiledSessionBinding } from '../../types';
|
||||
import type { MpvInputBindingsSnapshot } from '../../types/session-bindings';
|
||||
import { DEFAULT_KEYBINDINGS, SPECIAL_COMMANDS } from '../../config/definitions';
|
||||
@@ -156,9 +156,6 @@ function installKeyboardTestGlobals() {
|
||||
|
||||
let popupVisible = false;
|
||||
let hachidoriPaneOpen = false;
|
||||
const hachidoriHost = {
|
||||
shadowRoot: { querySelectorAll: () => [{ hidden: false }] },
|
||||
};
|
||||
|
||||
const popupIframe = {
|
||||
tagName: 'IFRAME',
|
||||
@@ -293,8 +290,8 @@ function installKeyboardTestGlobals() {
|
||||
documentListeners.set(type, listeners);
|
||||
},
|
||||
querySelectorAll: (selector: string) => {
|
||||
if (selector === HACHIDORI_HOST_SELECTOR) {
|
||||
return hachidoriPaneOpen ? [hachidoriHost] : [];
|
||||
if (selector === HACHIDORI_POPUP_OPEN_SELECTOR) {
|
||||
return hachidoriPaneOpen ? [{}] : [];
|
||||
}
|
||||
if (popupVisible) {
|
||||
return [popupIframe];
|
||||
|
||||
@@ -5,9 +5,8 @@ import { SUBTITLE_DEFAULT_CONFIG } from '../../config/definitions/defaults-subti
|
||||
import { createRendererState } from '../state';
|
||||
import { createMouseHandlers } from './mouse.js';
|
||||
import {
|
||||
HACHIDORI_HOST_SELECTOR,
|
||||
HACHIDORI_POPUP_HIDDEN_EVENT,
|
||||
HACHIDORI_POPUP_SELECTOR,
|
||||
HACHIDORI_POPUP_OPEN_SELECTOR,
|
||||
HACHIDORI_POPUP_SHOWN_EVENT,
|
||||
YOMITAN_LOOKUP_EVENT,
|
||||
YOMITAN_POPUP_HIDDEN_EVENT,
|
||||
@@ -110,23 +109,11 @@ function createMouseHarness(
|
||||
};
|
||||
const ignoreCalls: IgnoreCall[] = [];
|
||||
const mpvCommands: Array<(string | number)[]> = [];
|
||||
const popupPane = {
|
||||
get hidden() {
|
||||
return !harness.popupPaneOpen;
|
||||
},
|
||||
};
|
||||
const popupHost = {
|
||||
tagName: options.hachidori ? 'HACHIDORI-HOST' : 'DIV',
|
||||
getAttribute: (name: string) =>
|
||||
name === 'data-subminer-yomitan-popup-visible' ? String(harness.popupHostVisible) : null,
|
||||
shadowRoot: {
|
||||
querySelectorAll: (selector: string) =>
|
||||
selector === HACHIDORI_POPUP_SELECTOR ? [popupPane] : [],
|
||||
},
|
||||
};
|
||||
const hostSelectors = options.hachidori
|
||||
? [HACHIDORI_HOST_SELECTOR, YOMITAN_POPUP_HOST_SELECTOR]
|
||||
: [YOMITAN_POPUP_HOST_SELECTOR];
|
||||
|
||||
const harness = {
|
||||
ctx,
|
||||
@@ -183,7 +170,10 @@ function createMouseHarness(
|
||||
if (selector === YOMITAN_POPUP_VISIBLE_HOST_SELECTOR) {
|
||||
return harness.popupHostVisible ? [popupHost] : [];
|
||||
}
|
||||
return hostSelectors.includes(selector) &&
|
||||
if (selector === HACHIDORI_POPUP_OPEN_SELECTOR) {
|
||||
return harness.popupPaneOpen ? [popupHost] : [];
|
||||
}
|
||||
return selector === YOMITAN_POPUP_HOST_SELECTOR &&
|
||||
(harness.popupHostVisible || harness.popupHostAttached)
|
||||
? [popupHost]
|
||||
: [];
|
||||
@@ -529,6 +519,31 @@ test('Hachidori hover popup pauses when shown', async (t) => {
|
||||
assert.deepEqual(h.mpvCommands, [PAUSE]);
|
||||
});
|
||||
|
||||
test('leaving the subtitle keeps playback paused while a Hachidori popup is open', async (t) => {
|
||||
let paused = false;
|
||||
const h = createMouseHarness(t, {
|
||||
hoverPause: true,
|
||||
popupPause: true,
|
||||
hachidori: true,
|
||||
getPlaybackPaused: async () => paused,
|
||||
});
|
||||
h.handlers.setupYomitanObserver();
|
||||
|
||||
await h.handlers.handleMouseEnter();
|
||||
paused = true;
|
||||
h.popupPaneOpen = true;
|
||||
h.popupHostVisible = true;
|
||||
h.fire('window', HACHIDORI_POPUP_SHOWN_EVENT);
|
||||
await waitForNextTick();
|
||||
await h.handlers.handleMouseLeave();
|
||||
assert.deepEqual(h.mpvCommands, [PAUSE]);
|
||||
|
||||
h.popupPaneOpen = false;
|
||||
h.popupHostVisible = false;
|
||||
h.fire('window', HACHIDORI_POPUP_HIDDEN_EVENT);
|
||||
assert.deepEqual(h.mpvCommands, [PAUSE, RESUME]);
|
||||
});
|
||||
|
||||
test('popup shown reclaims overlay focus on macOS and captures click-away', (t) => {
|
||||
const h = createMouseHarness(t, { platform: 'macos' });
|
||||
h.handlers.setupYomitanObserver();
|
||||
|
||||
@@ -29,6 +29,7 @@ import type {
|
||||
import type { OverlayHostedModal } from '../shared/ipc/contracts';
|
||||
import { createKeyboardHandlers } from './handlers/keyboard.js';
|
||||
import { createGamepadController } from './handlers/gamepad-controller.js';
|
||||
import { createControllerHintVisibility } from './controller-hint-visibility.js';
|
||||
import { createControllerPressReplay } from './handlers/controller-press-replay.js';
|
||||
import { createMouseHandlers } from './handlers/mouse.js';
|
||||
import { createControllerStatusIndicator } from './controller-status-indicator.js';
|
||||
@@ -398,6 +399,9 @@ let lastSecondarySubtitlePreview = '';
|
||||
let overlayErrorToastTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
let controllerAnimationFrameId: number | null = null;
|
||||
const controllerPressReplay = createControllerPressReplay();
|
||||
const controllerHintVisibility = createControllerHintVisibility({
|
||||
getOverlayControllerId: () => window.electronAPI.getActiveControllerId(),
|
||||
});
|
||||
|
||||
function truncateForErrorLog(text: string): string {
|
||||
const normalized = text.replace(/\s+/g, ' ').trim();
|
||||
@@ -453,10 +457,13 @@ function applyControllerSnapshot(snapshot: {
|
||||
ctx.state.controllerRawButtons = snapshot.rawButtons;
|
||||
controllerSelectModal.updateDevices();
|
||||
controllerDebugModal.updateSnapshot();
|
||||
// Shown whenever controller support is on: a fresh dialog window cannot see the pad
|
||||
// until its first press, so "connected" would hide the hints exactly when needed.
|
||||
const controllerConfig = ctx.state.controllerConfig;
|
||||
const hintsVisible = controllerConfig?.enabled === true;
|
||||
const hintsVisible = controllerHintVisibility.update({
|
||||
enabled: controllerConfig?.enabled === true,
|
||||
dialogOpen: ctx.state.mediaTimingReviewModalOpen || ctx.state.kikuModalOpen,
|
||||
activeGamepadId: snapshot.activeGamepadId,
|
||||
now: performance.now(),
|
||||
});
|
||||
mediaTimingReviewModal.setControllerHintsVisible(hintsVisible);
|
||||
kikuModal.setControllerHintsVisible(hintsVisible);
|
||||
if (
|
||||
|
||||
@@ -2045,7 +2045,7 @@ body:focus-visible,
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Controller hints: shown beside each control while controller support is on. */
|
||||
/* Controller hints: shown beside each control while a controller is connected. */
|
||||
.media-timing-review-modal.controller-hints-visible button[data-controller-hint]::after,
|
||||
#kikuFieldGroupingModal.controller-hints-visible [data-controller-hint]::after {
|
||||
content: attr(data-controller-hint);
|
||||
|
||||
@@ -12,11 +12,13 @@ export const YOMITAN_LOOKUP_EVENT = 'subminer-yomitan-lookup';
|
||||
export const PRIMARY_SUB_VISIBLE_ON_YOMITAN_POPUP_CLASS = 'primary-sub-visible-on-yomitan-popup';
|
||||
// Hachidori's shown/hidden pair means "the reader needs mouse events", which
|
||||
// also covers a left press on subtitle text that may start a selection. Its
|
||||
// popup panes live in the host's open shadow root.
|
||||
// popup panes live in a closed shadow root, so the fork's subminer-host.js
|
||||
// publishes whether one is on screen as an attribute on the host.
|
||||
export const HACHIDORI_POPUP_SHOWN_EVENT = 'hachidori-popup-shown';
|
||||
export const HACHIDORI_POPUP_HIDDEN_EVENT = 'hachidori-popup-hidden';
|
||||
export const HACHIDORI_HOST_SELECTOR = 'hachidori-host';
|
||||
export const HACHIDORI_POPUP_SELECTOR = '.gsm-hoshidicts-popup';
|
||||
export const HACHIDORI_POPUP_OPEN_SELECTOR =
|
||||
'hachidori-host[data-subminer-hachidori-popup-open="true"]';
|
||||
|
||||
export type DictionaryReader = 'yomitan' | 'hachidori';
|
||||
|
||||
@@ -114,12 +116,7 @@ function queryPopupElements<T extends Element>(
|
||||
* attaches its host on the first lookup, so no host means no popup.
|
||||
*/
|
||||
export function isHachidoriPopupOpen(root: ParentNode | null | undefined = document): boolean {
|
||||
const hosts = queryPopupElements<HTMLElement>(root, HACHIDORI_HOST_SELECTOR);
|
||||
return hosts.some((host) =>
|
||||
queryPopupElements<HTMLElement>(host.shadowRoot, HACHIDORI_POPUP_SELECTOR).some(
|
||||
(pane) => !pane.hidden,
|
||||
),
|
||||
);
|
||||
return queryPopupElements(root, HACHIDORI_POPUP_OPEN_SELECTOR).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Vendored
+1
-1
Submodule vendor/hachidori updated: 323d0143dd...0792c09e05.
Reference in new issue
Block a user