mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-07 07:21:35 -07:00
fix: Kiku field grouping, frequency particles, sidebar media, Yomitan popup visibility (#91)
This commit is contained in:
@@ -113,6 +113,88 @@ test('findActiveSubtitleCueIndex prefers current subtitle timing over near-futur
|
||||
assert.equal(findActiveSubtitleCueIndex(cues, { text: 'previous', startTime: 231 }, 233, 0), 0);
|
||||
});
|
||||
|
||||
test('subtitle sidebar mining context resolves selected row cue timing', () => {
|
||||
const globals = globalThis as typeof globalThis & {
|
||||
Element?: unknown;
|
||||
Node?: unknown;
|
||||
window?: unknown;
|
||||
};
|
||||
const previousElement = globals.Element;
|
||||
const previousNode = globals.Node;
|
||||
const previousWindow = globals.window;
|
||||
|
||||
class FakeNode {
|
||||
parentElement: FakeElement | null = null;
|
||||
}
|
||||
class FakeElement extends FakeNode {
|
||||
dataset: Record<string, string> = {};
|
||||
|
||||
closest(selector: string) {
|
||||
return selector === '.subtitle-sidebar-item' ? this : null;
|
||||
}
|
||||
}
|
||||
|
||||
const row = new FakeElement();
|
||||
row.dataset.index = '1';
|
||||
const textNode = new FakeNode();
|
||||
textNode.parentElement = row;
|
||||
|
||||
Object.defineProperty(globalThis, 'Node', { configurable: true, value: FakeNode });
|
||||
Object.defineProperty(globalThis, 'Element', { configurable: true, value: FakeElement });
|
||||
Object.defineProperty(globalThis, 'window', {
|
||||
configurable: true,
|
||||
value: {
|
||||
getSelection: () => ({ anchorNode: textNode, focusNode: null }),
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const state = createRendererState();
|
||||
state.subtitleSidebarModalOpen = true;
|
||||
state.subtitleSidebarCues = [
|
||||
{ startTime: 1, endTime: 2, text: 'current line' },
|
||||
{ startTime: 3, endTime: 5, text: 'sidebar previous line' },
|
||||
];
|
||||
const modal = createSubtitleSidebarModal(
|
||||
{
|
||||
dom: {
|
||||
overlay: { classList: createClassList() },
|
||||
subtitleSidebarModal: {
|
||||
classList: createClassList(),
|
||||
setAttribute: () => {},
|
||||
style: { setProperty: () => {} },
|
||||
addEventListener: () => {},
|
||||
},
|
||||
subtitleSidebarContent: {
|
||||
classList: createClassList(),
|
||||
getBoundingClientRect: () => ({ width: 420 }),
|
||||
style: { setProperty: () => {} },
|
||||
},
|
||||
subtitleSidebarClose: { addEventListener: () => {} },
|
||||
subtitleSidebarStatus: { textContent: '' },
|
||||
subtitleSidebarList: createListStub(),
|
||||
},
|
||||
state,
|
||||
} as never,
|
||||
{
|
||||
modalStateReader: { isAnyModalOpen: () => false },
|
||||
},
|
||||
);
|
||||
|
||||
const context = modal.getSubtitleSidebarMiningContext();
|
||||
|
||||
assert.equal(context?.source, 'subtitle-sidebar');
|
||||
assert.equal(context?.text, 'sidebar previous line');
|
||||
assert.equal(context?.startTime, 3);
|
||||
assert.equal(context?.endTime, 5);
|
||||
assert.equal(typeof context?.capturedAtMs, 'number');
|
||||
} finally {
|
||||
Object.defineProperty(globalThis, 'Element', { configurable: true, value: previousElement });
|
||||
Object.defineProperty(globalThis, 'Node', { configurable: true, value: previousNode });
|
||||
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
|
||||
}
|
||||
});
|
||||
|
||||
test('applySidebarCssDeclarations clears declarations removed by config reload', () => {
|
||||
const removed: string[] = [];
|
||||
const style = {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import type { SubtitleCue, SubtitleData, SubtitleSidebarSnapshot } from '../../types';
|
||||
import type {
|
||||
SubtitleCue,
|
||||
SubtitleData,
|
||||
SubtitleMiningContext,
|
||||
SubtitleSidebarSnapshot,
|
||||
} from '../../types';
|
||||
import type { ModalStateReader, RendererContext } from '../context';
|
||||
import { syncOverlayMouseIgnoreState } from '../overlay-mouse-ignore.js';
|
||||
import {
|
||||
@@ -201,6 +206,7 @@ export function createSubtitleSidebarModal(
|
||||
let subtitleSidebarFocusedWithin = false;
|
||||
let subtitleSidebarYomitanPopupVisible = false;
|
||||
let subtitleSidebarPauseHeldByYomitanPopup = false;
|
||||
let lastSubtitleSidebarLookupCueIndex = -1;
|
||||
|
||||
function restoreEmbeddedSidebarPassthrough(): void {
|
||||
syncOverlayMouseIgnoreState(ctx);
|
||||
@@ -213,9 +219,75 @@ export function createSubtitleSidebarModal(
|
||||
function clearSidebarInteractionState(): void {
|
||||
subtitleSidebarHovered = false;
|
||||
subtitleSidebarFocusedWithin = false;
|
||||
lastSubtitleSidebarLookupCueIndex = -1;
|
||||
syncSidebarInteractionState();
|
||||
}
|
||||
|
||||
function findCueIndexFromNode(node: Node | null): number | null {
|
||||
if (!node || typeof Element === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
const element = node instanceof Element ? node : node.parentElement;
|
||||
const row = element?.closest<HTMLElement>('.subtitle-sidebar-item') ?? null;
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
const index = Number.parseInt(row.dataset.index ?? '', 10);
|
||||
if (!Number.isInteger(index) || index < 0 || index >= ctx.state.subtitleSidebarCues.length) {
|
||||
return null;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
function rememberLookupCueFromTarget(target: EventTarget | null): void {
|
||||
if (typeof Node === 'undefined') {
|
||||
return;
|
||||
}
|
||||
if (!(target instanceof Node)) {
|
||||
return;
|
||||
}
|
||||
const index = findCueIndexFromNode(target);
|
||||
if (index === null) {
|
||||
return;
|
||||
}
|
||||
lastSubtitleSidebarLookupCueIndex = index;
|
||||
}
|
||||
|
||||
function getSubtitleSidebarMiningContext(): SubtitleMiningContext | null {
|
||||
if (!ctx.state.subtitleSidebarModalOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selection = window.getSelection?.() ?? null;
|
||||
const selectionIndex =
|
||||
findCueIndexFromNode(selection?.anchorNode ?? null) ??
|
||||
findCueIndexFromNode(selection?.focusNode ?? null);
|
||||
const index =
|
||||
selectionIndex ??
|
||||
(lastSubtitleSidebarLookupCueIndex >= 0 ? lastSubtitleSidebarLookupCueIndex : null);
|
||||
if (index === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cue = ctx.state.subtitleSidebarCues[index];
|
||||
if (
|
||||
!cue ||
|
||||
!Number.isFinite(cue.startTime) ||
|
||||
!Number.isFinite(cue.endTime) ||
|
||||
cue.endTime <= cue.startTime
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
source: 'subtitle-sidebar',
|
||||
text: cue.text,
|
||||
startTime: cue.startTime,
|
||||
endTime: cue.endTime,
|
||||
capturedAtMs: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function setStatus(message: string): void {
|
||||
ctx.dom.subtitleSidebarStatus.textContent = message;
|
||||
}
|
||||
@@ -653,6 +725,12 @@ export function createSubtitleSidebarModal(
|
||||
ctx.dom.subtitleSidebarList.addEventListener('wheel', () => {
|
||||
ctx.state.subtitleSidebarManualScrollUntilMs = nowForUiTiming() + MANUAL_SCROLL_HOLD_MS;
|
||||
});
|
||||
ctx.dom.subtitleSidebarList.addEventListener('pointerover', (event) => {
|
||||
rememberLookupCueFromTarget(event.target);
|
||||
});
|
||||
ctx.dom.subtitleSidebarList.addEventListener('focusin', (event) => {
|
||||
rememberLookupCueFromTarget(event.target);
|
||||
});
|
||||
ctx.dom.subtitleSidebarContent.addEventListener('mouseenter', async () => {
|
||||
subtitleSidebarHovered = true;
|
||||
syncSidebarInteractionState();
|
||||
@@ -677,6 +755,9 @@ export function createSubtitleSidebarModal(
|
||||
});
|
||||
ctx.dom.subtitleSidebarContent.addEventListener('mouseleave', () => {
|
||||
subtitleSidebarHovered = false;
|
||||
if (!subtitleSidebarFocusedWithin) {
|
||||
lastSubtitleSidebarLookupCueIndex = -1;
|
||||
}
|
||||
syncSidebarInteractionState();
|
||||
if (ctx.state.isOverSubtitleSidebar) {
|
||||
restoreEmbeddedSidebarPassthrough();
|
||||
@@ -700,6 +781,7 @@ export function createSubtitleSidebarModal(
|
||||
}
|
||||
|
||||
subtitleSidebarFocusedWithin = false;
|
||||
lastSubtitleSidebarLookupCueIndex = -1;
|
||||
syncSidebarInteractionState();
|
||||
if (ctx.state.isOverSubtitleSidebar) {
|
||||
restoreEmbeddedSidebarPassthrough();
|
||||
@@ -736,5 +818,6 @@ export function createSubtitleSidebarModal(
|
||||
},
|
||||
handleSubtitleUpdated,
|
||||
seekToCue,
|
||||
getSubtitleSidebarMiningContext,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user