mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-22 12:11:27 -07:00
600 lines
19 KiB
TypeScript
600 lines
19 KiB
TypeScript
import type {
|
|
SubtitleCue,
|
|
SubtitleData,
|
|
SubtitleSidebarSnapshot,
|
|
} from '../../types';
|
|
import type { ModalStateReader, RendererContext } from '../context';
|
|
import { syncOverlayMouseIgnoreState } from '../overlay-mouse-ignore.js';
|
|
|
|
const MANUAL_SCROLL_HOLD_MS = 1500;
|
|
const ACTIVE_CUE_LOOKAHEAD_SEC = 0.18;
|
|
const CLICK_SEEK_OFFSET_SEC = 0.08;
|
|
const SNAPSHOT_POLL_INTERVAL_MS = 80;
|
|
const EMBEDDED_SIDEBAR_MIN_WIDTH_PX = 240;
|
|
const EMBEDDED_SIDEBAR_MAX_RATIO = 0.45;
|
|
function subtitleCueListsEqual(a: SubtitleCue[], b: SubtitleCue[]): boolean {
|
|
if (a.length !== b.length) {
|
|
return false;
|
|
}
|
|
|
|
for (let i = 0; i < a.length; i += 1) {
|
|
const left = a[i]!;
|
|
const right = b[i]!;
|
|
if (
|
|
left.startTime !== right.startTime ||
|
|
left.endTime !== right.endTime ||
|
|
left.text !== right.text
|
|
) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function normalizeCueText(text: string): string {
|
|
return text.replace(/\r\n/g, '\n').trim();
|
|
}
|
|
|
|
function formatCueTimestamp(seconds: number): string {
|
|
const totalSeconds = Math.max(0, Math.floor(seconds));
|
|
const hours = Math.floor(totalSeconds / 3600);
|
|
const mins = Math.floor((totalSeconds % 3600) / 60);
|
|
const secs = totalSeconds % 60;
|
|
if (hours > 0) {
|
|
return [
|
|
String(hours).padStart(2, '0'),
|
|
String(mins).padStart(2, '0'),
|
|
String(secs).padStart(2, '0'),
|
|
].join(':');
|
|
}
|
|
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
|
}
|
|
|
|
export function findActiveSubtitleCueIndex(
|
|
cues: SubtitleCue[],
|
|
current: { text: string; startTime?: number | null } | null,
|
|
currentTimeSec: number | null = null,
|
|
preferredCueIndex: number = -1,
|
|
): number {
|
|
if (cues.length === 0) {
|
|
return -1;
|
|
}
|
|
|
|
if (typeof currentTimeSec === 'number' && Number.isFinite(currentTimeSec)) {
|
|
const activeOrUpcomingCue = cues.findIndex(
|
|
(cue) =>
|
|
cue.endTime > currentTimeSec &&
|
|
cue.startTime <= currentTimeSec + ACTIVE_CUE_LOOKAHEAD_SEC,
|
|
);
|
|
if (activeOrUpcomingCue >= 0) {
|
|
return activeOrUpcomingCue;
|
|
}
|
|
|
|
const nextCue = cues.findIndex((cue) => cue.endTime > currentTimeSec);
|
|
if (nextCue >= 0) {
|
|
return nextCue;
|
|
}
|
|
}
|
|
|
|
if (!current) {
|
|
return -1;
|
|
}
|
|
|
|
if (typeof current.startTime === 'number' && Number.isFinite(current.startTime)) {
|
|
const timingMatch = cues.findIndex(
|
|
(cue) => current.startTime! >= cue.startTime && current.startTime! < cue.endTime,
|
|
);
|
|
if (timingMatch >= 0) {
|
|
return timingMatch;
|
|
}
|
|
}
|
|
|
|
const normalizedText = normalizeCueText(current.text);
|
|
if (!normalizedText) {
|
|
return -1;
|
|
}
|
|
|
|
const matchingIndices: number[] = [];
|
|
for (const [index, cue] of cues.entries()) {
|
|
if (normalizeCueText(cue.text) === normalizedText) {
|
|
matchingIndices.push(index);
|
|
}
|
|
}
|
|
|
|
if (matchingIndices.length === 0) {
|
|
return -1;
|
|
}
|
|
|
|
const hasTiming =
|
|
typeof current.startTime === 'number' && Number.isFinite(current.startTime);
|
|
|
|
if (preferredCueIndex >= 0) {
|
|
if (!hasTiming && currentTimeSec === null) {
|
|
const forwardMatches = matchingIndices.filter((index) => index >= preferredCueIndex);
|
|
if (forwardMatches.length > 0) {
|
|
return forwardMatches[0]!;
|
|
}
|
|
if (matchingIndices.includes(preferredCueIndex)) {
|
|
return preferredCueIndex;
|
|
}
|
|
return matchingIndices[matchingIndices.length - 1] ?? -1;
|
|
}
|
|
|
|
let nearestIndex = matchingIndices[0]!;
|
|
let nearestDistance = Math.abs(nearestIndex - preferredCueIndex);
|
|
for (const matchIndex of matchingIndices) {
|
|
const distance = Math.abs(matchIndex - preferredCueIndex);
|
|
if (distance < nearestDistance) {
|
|
nearestIndex = matchIndex;
|
|
nearestDistance = distance;
|
|
}
|
|
}
|
|
return nearestIndex;
|
|
}
|
|
|
|
return matchingIndices[0]!;
|
|
}
|
|
|
|
export function createSubtitleSidebarModal(
|
|
ctx: RendererContext,
|
|
options: {
|
|
modalStateReader: Pick<ModalStateReader, 'isAnyModalOpen'>;
|
|
},
|
|
) {
|
|
let snapshotPollInterval: ReturnType<typeof setTimeout> | null = null;
|
|
let lastAppliedVideoMarginRatio: number | null = null;
|
|
let subtitleSidebarHoverRequestId = 0;
|
|
let disposeDomEvents: (() => void) | null = null;
|
|
|
|
function restoreEmbeddedSidebarPassthrough(): void {
|
|
syncOverlayMouseIgnoreState(ctx);
|
|
}
|
|
|
|
function setStatus(message: string): void {
|
|
ctx.dom.subtitleSidebarStatus.textContent = message;
|
|
}
|
|
|
|
function getReservedSidebarWidthPx(): number {
|
|
const config = ctx.state.subtitleSidebarConfig;
|
|
if (!config || config.layout !== 'embedded' || !ctx.state.subtitleSidebarModalOpen) {
|
|
return 0;
|
|
}
|
|
|
|
const measuredWidth = ctx.dom.subtitleSidebarContent.getBoundingClientRect().width;
|
|
if (Number.isFinite(measuredWidth) && measuredWidth > 0) {
|
|
return measuredWidth;
|
|
}
|
|
|
|
return Math.max(EMBEDDED_SIDEBAR_MIN_WIDTH_PX, config.maxWidth);
|
|
}
|
|
|
|
function syncEmbeddedSidebarLayout(): void {
|
|
const config = ctx.state.subtitleSidebarConfig;
|
|
const wantsEmbedded = Boolean(
|
|
config && config.layout === 'embedded' && ctx.state.subtitleSidebarModalOpen,
|
|
);
|
|
|
|
if (wantsEmbedded) {
|
|
ctx.dom.subtitleSidebarContent.classList.add('subtitle-sidebar-content-embedded');
|
|
ctx.dom.subtitleSidebarModal.classList.add('subtitle-sidebar-modal-embedded');
|
|
document.body.classList.add('subtitle-sidebar-embedded-open');
|
|
} else {
|
|
ctx.dom.subtitleSidebarContent.classList.remove('subtitle-sidebar-content-embedded');
|
|
ctx.dom.subtitleSidebarModal.classList.remove('subtitle-sidebar-modal-embedded');
|
|
document.body.classList.remove('subtitle-sidebar-embedded-open');
|
|
}
|
|
const reservedWidthPx = wantsEmbedded ? getReservedSidebarWidthPx() : 0;
|
|
const embedded = wantsEmbedded && reservedWidthPx > 0;
|
|
document.documentElement.style.setProperty(
|
|
'--subtitle-sidebar-reserved-width',
|
|
`${Math.max(0, Math.round(reservedWidthPx))}px`,
|
|
);
|
|
|
|
const viewportWidth = window.innerWidth;
|
|
const ratio =
|
|
embedded && Number.isFinite(viewportWidth) && viewportWidth > 0
|
|
? Math.min(EMBEDDED_SIDEBAR_MAX_RATIO, reservedWidthPx / viewportWidth)
|
|
: 0;
|
|
|
|
if (
|
|
lastAppliedVideoMarginRatio !== null &&
|
|
Math.abs(ratio - lastAppliedVideoMarginRatio) < 0.0001
|
|
) {
|
|
return;
|
|
}
|
|
|
|
lastAppliedVideoMarginRatio = ratio;
|
|
window.electronAPI.sendMpvCommand([
|
|
'set_property',
|
|
'video-margin-ratio-right',
|
|
Number(ratio.toFixed(4)),
|
|
]);
|
|
window.electronAPI.sendMpvCommand([
|
|
'set_property',
|
|
'osd-align-x',
|
|
'left',
|
|
]);
|
|
window.electronAPI.sendMpvCommand([
|
|
'set_property',
|
|
'osd-align-y',
|
|
'top',
|
|
]);
|
|
window.electronAPI.sendMpvCommand([
|
|
'set_property',
|
|
'user-data/osc/margins',
|
|
JSON.stringify({
|
|
l: 0,
|
|
r: Number(ratio.toFixed(4)),
|
|
t: 0,
|
|
b: 0,
|
|
}),
|
|
]);
|
|
if (ratio === 0) {
|
|
window.electronAPI.sendMpvCommand(['set_property', 'video-pan-x', 0]);
|
|
}
|
|
}
|
|
|
|
function applyConfig(snapshot: SubtitleSidebarSnapshot): void {
|
|
ctx.state.subtitleSidebarConfig = snapshot.config;
|
|
ctx.state.subtitleSidebarToggleKey = snapshot.config.toggleKey;
|
|
ctx.state.subtitleSidebarPauseVideoOnHover = snapshot.config.pauseVideoOnHover;
|
|
ctx.state.subtitleSidebarAutoScroll = snapshot.config.autoScroll;
|
|
const style = ctx.dom.subtitleSidebarModal.style;
|
|
style.setProperty('--subtitle-sidebar-max-width', `${snapshot.config.maxWidth}px`);
|
|
style.setProperty('--subtitle-sidebar-opacity', String(snapshot.config.opacity));
|
|
style.setProperty('--subtitle-sidebar-background-color', snapshot.config.backgroundColor);
|
|
style.setProperty('--subtitle-sidebar-text-color', snapshot.config.textColor);
|
|
style.setProperty('--subtitle-sidebar-font-family', snapshot.config.fontFamily);
|
|
style.setProperty('--subtitle-sidebar-font-size', `${snapshot.config.fontSize}px`);
|
|
style.setProperty('--subtitle-sidebar-timestamp-color', snapshot.config.timestampColor);
|
|
style.setProperty('--subtitle-sidebar-active-line-color', snapshot.config.activeLineColor);
|
|
style.setProperty(
|
|
'--subtitle-sidebar-active-background-color',
|
|
snapshot.config.activeLineBackgroundColor,
|
|
);
|
|
style.setProperty(
|
|
'--subtitle-sidebar-hover-background-color',
|
|
snapshot.config.hoverLineBackgroundColor,
|
|
);
|
|
}
|
|
|
|
function seekToCue(cue: SubtitleCue): void {
|
|
const targetTime = Math.min(cue.endTime - 0.01, cue.startTime + CLICK_SEEK_OFFSET_SEC);
|
|
window.electronAPI.sendMpvCommand([
|
|
'seek',
|
|
Math.max(cue.startTime, targetTime),
|
|
'absolute+exact',
|
|
]);
|
|
}
|
|
|
|
function getCueRowLabel(cue: SubtitleCue): string {
|
|
return `Jump to subtitle at ${formatCueTimestamp(cue.startTime)}`;
|
|
}
|
|
|
|
function resumeSubtitleSidebarHoverPause(): void {
|
|
subtitleSidebarHoverRequestId += 1;
|
|
if (!ctx.state.subtitleSidebarPausedByHover) {
|
|
restoreEmbeddedSidebarPassthrough();
|
|
return;
|
|
}
|
|
|
|
ctx.state.subtitleSidebarPausedByHover = false;
|
|
window.electronAPI.sendMpvCommand(['set_property', 'pause', 'no']);
|
|
restoreEmbeddedSidebarPassthrough();
|
|
}
|
|
|
|
function maybeAutoScrollActiveCue(
|
|
previousActiveCueIndex: number,
|
|
behavior: ScrollBehavior = 'smooth',
|
|
force = false,
|
|
): void {
|
|
if (
|
|
!ctx.state.subtitleSidebarAutoScroll ||
|
|
ctx.state.subtitleSidebarActiveCueIndex < 0 ||
|
|
(!force && ctx.state.subtitleSidebarActiveCueIndex === previousActiveCueIndex) ||
|
|
Date.now() < ctx.state.subtitleSidebarManualScrollUntilMs
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const list = ctx.dom.subtitleSidebarList;
|
|
const active = list.children[ctx.state.subtitleSidebarActiveCueIndex] as HTMLElement | undefined;
|
|
if (!active) {
|
|
return;
|
|
}
|
|
|
|
const targetScrollTop =
|
|
active.offsetTop - (list.clientHeight - active.clientHeight) / 2;
|
|
const nextScrollTop = Math.max(0, targetScrollTop);
|
|
if (previousActiveCueIndex < 0) {
|
|
list.scrollTop = nextScrollTop;
|
|
return;
|
|
}
|
|
|
|
list.scrollTo({
|
|
top: nextScrollTop,
|
|
behavior,
|
|
});
|
|
}
|
|
|
|
function renderCueList(): void {
|
|
ctx.dom.subtitleSidebarList.innerHTML = '';
|
|
for (const [index, cue] of ctx.state.subtitleSidebarCues.entries()) {
|
|
const row = document.createElement('li');
|
|
row.className = 'subtitle-sidebar-item';
|
|
row.classList.toggle('active', index === ctx.state.subtitleSidebarActiveCueIndex);
|
|
row.dataset.index = String(index);
|
|
row.tabIndex = 0;
|
|
row.setAttribute('role', 'button');
|
|
row.setAttribute('aria-label', getCueRowLabel(cue));
|
|
row.addEventListener('keydown', (event: KeyboardEvent) => {
|
|
if (event.key !== 'Enter' && event.key !== ' ') {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
seekToCue(cue);
|
|
});
|
|
|
|
const timestamp = document.createElement('div');
|
|
timestamp.className = 'subtitle-sidebar-timestamp';
|
|
timestamp.textContent = formatCueTimestamp(cue.startTime);
|
|
|
|
const text = document.createElement('div');
|
|
text.className = 'subtitle-sidebar-text';
|
|
text.textContent = cue.text;
|
|
|
|
row.appendChild(timestamp);
|
|
row.appendChild(text);
|
|
|
|
ctx.dom.subtitleSidebarList.appendChild(row);
|
|
}
|
|
}
|
|
|
|
function syncActiveCueClasses(previousActiveCueIndex: number): void {
|
|
if (previousActiveCueIndex >= 0) {
|
|
const previous = ctx.dom.subtitleSidebarList.children[previousActiveCueIndex] as
|
|
| HTMLElement
|
|
| undefined;
|
|
previous?.classList.remove('active');
|
|
}
|
|
|
|
if (ctx.state.subtitleSidebarActiveCueIndex >= 0) {
|
|
const current = ctx.dom.subtitleSidebarList.children[ctx.state.subtitleSidebarActiveCueIndex] as
|
|
| HTMLElement
|
|
| undefined;
|
|
current?.classList.add('active');
|
|
}
|
|
}
|
|
|
|
function updateActiveCue(
|
|
current: { text: string; startTime?: number | null } | null,
|
|
currentTimeSec: number | null = null,
|
|
): void {
|
|
const previousActiveCueIndex = ctx.state.subtitleSidebarActiveCueIndex;
|
|
ctx.state.subtitleSidebarActiveCueIndex = findActiveSubtitleCueIndex(
|
|
ctx.state.subtitleSidebarCues,
|
|
current,
|
|
currentTimeSec,
|
|
previousActiveCueIndex,
|
|
);
|
|
if (ctx.state.subtitleSidebarModalOpen) {
|
|
syncActiveCueClasses(previousActiveCueIndex);
|
|
maybeAutoScrollActiveCue(previousActiveCueIndex);
|
|
}
|
|
}
|
|
|
|
async function refreshSnapshot(): Promise<SubtitleSidebarSnapshot> {
|
|
const snapshot = await window.electronAPI.getSubtitleSidebarSnapshot();
|
|
applyConfig(snapshot);
|
|
if (!snapshot.config.enabled) {
|
|
resumeSubtitleSidebarHoverPause();
|
|
ctx.state.subtitleSidebarCues = [];
|
|
ctx.state.subtitleSidebarModalOpen = false;
|
|
ctx.dom.subtitleSidebarModal.classList.add('hidden');
|
|
ctx.dom.subtitleSidebarModal.setAttribute('aria-hidden', 'true');
|
|
stopSnapshotPolling();
|
|
updateActiveCue(null, snapshot.currentTimeSec ?? null);
|
|
setStatus('Subtitle sidebar disabled in config.');
|
|
syncEmbeddedSidebarLayout();
|
|
if (!ctx.state.isOverSubtitle && !options.modalStateReader.isAnyModalOpen()) {
|
|
ctx.dom.overlay.classList.remove('interactive');
|
|
}
|
|
restoreEmbeddedSidebarPassthrough();
|
|
return snapshot;
|
|
}
|
|
|
|
const cuesChanged = !subtitleCueListsEqual(ctx.state.subtitleSidebarCues, snapshot.cues);
|
|
if (cuesChanged) {
|
|
ctx.state.subtitleSidebarCues = snapshot.cues;
|
|
if (ctx.state.subtitleSidebarModalOpen) {
|
|
renderCueList();
|
|
}
|
|
}
|
|
updateActiveCue(snapshot.currentSubtitle, snapshot.currentTimeSec ?? null);
|
|
syncEmbeddedSidebarLayout();
|
|
return snapshot;
|
|
}
|
|
|
|
function startSnapshotPolling(): void {
|
|
stopSnapshotPolling();
|
|
|
|
const pollOnce = async (): Promise<void> => {
|
|
try {
|
|
await refreshSnapshot();
|
|
} catch {
|
|
// Keep polling; a transient IPC failure should not stop updates.
|
|
}
|
|
|
|
if (!ctx.state.subtitleSidebarModalOpen) {
|
|
snapshotPollInterval = null;
|
|
return;
|
|
}
|
|
|
|
snapshotPollInterval = setTimeout(pollOnce, SNAPSHOT_POLL_INTERVAL_MS);
|
|
};
|
|
|
|
snapshotPollInterval = setTimeout(pollOnce, SNAPSHOT_POLL_INTERVAL_MS);
|
|
}
|
|
|
|
function stopSnapshotPolling(): void {
|
|
if (!snapshotPollInterval) {
|
|
return;
|
|
}
|
|
clearTimeout(snapshotPollInterval);
|
|
snapshotPollInterval = null;
|
|
}
|
|
|
|
async function openSubtitleSidebarModal(): Promise<void> {
|
|
const snapshot = await refreshSnapshot();
|
|
if (!snapshot.config.enabled) {
|
|
setStatus('Subtitle sidebar disabled in config.');
|
|
return;
|
|
}
|
|
|
|
ctx.dom.subtitleSidebarList.innerHTML = '';
|
|
if (snapshot.cues.length === 0) {
|
|
setStatus('No parsed subtitle cues available.');
|
|
} else {
|
|
setStatus(`${snapshot.cues.length} parsed subtitle lines`);
|
|
}
|
|
|
|
ctx.state.subtitleSidebarModalOpen = true;
|
|
ctx.state.isOverSubtitleSidebar = false;
|
|
ctx.dom.subtitleSidebarModal.classList.remove('hidden');
|
|
ctx.dom.subtitleSidebarModal.setAttribute('aria-hidden', 'false');
|
|
renderCueList();
|
|
syncActiveCueClasses(-1);
|
|
maybeAutoScrollActiveCue(-1, 'auto', true);
|
|
startSnapshotPolling();
|
|
syncEmbeddedSidebarLayout();
|
|
restoreEmbeddedSidebarPassthrough();
|
|
}
|
|
|
|
async function autoOpenSubtitleSidebarOnStartup(): Promise<void> {
|
|
const snapshot = await refreshSnapshot();
|
|
if (!snapshot.config.enabled || !snapshot.config.autoOpen || ctx.state.subtitleSidebarModalOpen) {
|
|
return;
|
|
}
|
|
await openSubtitleSidebarModal();
|
|
}
|
|
|
|
function closeSubtitleSidebarModal(): void {
|
|
if (!ctx.state.subtitleSidebarModalOpen) {
|
|
return;
|
|
}
|
|
resumeSubtitleSidebarHoverPause();
|
|
ctx.state.isOverSubtitleSidebar = false;
|
|
ctx.state.subtitleSidebarModalOpen = false;
|
|
ctx.dom.subtitleSidebarModal.classList.add('hidden');
|
|
ctx.dom.subtitleSidebarModal.setAttribute('aria-hidden', 'true');
|
|
stopSnapshotPolling();
|
|
syncEmbeddedSidebarLayout();
|
|
if (!ctx.state.isOverSubtitle && !options.modalStateReader.isAnyModalOpen()) {
|
|
ctx.dom.overlay.classList.remove('interactive');
|
|
}
|
|
restoreEmbeddedSidebarPassthrough();
|
|
}
|
|
|
|
async function toggleSubtitleSidebarModal(): Promise<void> {
|
|
if (ctx.state.subtitleSidebarModalOpen) {
|
|
closeSubtitleSidebarModal();
|
|
return;
|
|
}
|
|
await openSubtitleSidebarModal();
|
|
}
|
|
|
|
function handleSubtitleUpdated(data: SubtitleData): void {
|
|
if (ctx.state.subtitleSidebarModalOpen) {
|
|
return;
|
|
}
|
|
|
|
updateActiveCue(
|
|
{ text: data.text, startTime: data.startTime },
|
|
data.startTime ?? null,
|
|
);
|
|
}
|
|
|
|
function wireDomEvents(): void {
|
|
if (disposeDomEvents) {
|
|
return;
|
|
}
|
|
|
|
ctx.dom.subtitleSidebarClose.addEventListener('click', () => {
|
|
closeSubtitleSidebarModal();
|
|
});
|
|
ctx.dom.subtitleSidebarList.addEventListener('click', (event) => {
|
|
const target = event.target;
|
|
if (!(target instanceof Element)) {
|
|
return;
|
|
}
|
|
const row = target.closest<HTMLElement>('.subtitle-sidebar-item');
|
|
if (!row) {
|
|
return;
|
|
}
|
|
const index = Number.parseInt(row.dataset.index ?? '', 10);
|
|
if (!Number.isInteger(index) || index < 0 || index >= ctx.state.subtitleSidebarCues.length) {
|
|
return;
|
|
}
|
|
const cue = ctx.state.subtitleSidebarCues[index];
|
|
if (!cue) {
|
|
return;
|
|
}
|
|
seekToCue(cue);
|
|
});
|
|
ctx.dom.subtitleSidebarList.addEventListener('wheel', () => {
|
|
ctx.state.subtitleSidebarManualScrollUntilMs = Date.now() + MANUAL_SCROLL_HOLD_MS;
|
|
});
|
|
ctx.dom.subtitleSidebarModal.addEventListener('mouseenter', async () => {
|
|
ctx.state.isOverSubtitleSidebar = true;
|
|
restoreEmbeddedSidebarPassthrough();
|
|
if (!ctx.state.subtitleSidebarPauseVideoOnHover || ctx.state.subtitleSidebarPausedByHover) {
|
|
return;
|
|
}
|
|
const requestId = ++subtitleSidebarHoverRequestId;
|
|
let paused: boolean | null | undefined;
|
|
try {
|
|
paused = await window.electronAPI.getPlaybackPaused();
|
|
} catch {
|
|
paused = undefined;
|
|
}
|
|
if (requestId !== subtitleSidebarHoverRequestId) {
|
|
return;
|
|
}
|
|
if (paused === false) {
|
|
window.electronAPI.sendMpvCommand(['set_property', 'pause', 'yes']);
|
|
ctx.state.subtitleSidebarPausedByHover = true;
|
|
}
|
|
});
|
|
ctx.dom.subtitleSidebarModal.addEventListener('mouseleave', () => {
|
|
ctx.state.isOverSubtitleSidebar = false;
|
|
resumeSubtitleSidebarHoverPause();
|
|
});
|
|
const resizeHandler = () => {
|
|
if (!ctx.state.subtitleSidebarModalOpen) {
|
|
return;
|
|
}
|
|
syncEmbeddedSidebarLayout();
|
|
};
|
|
window.addEventListener('resize', resizeHandler);
|
|
disposeDomEvents = () => {
|
|
window.removeEventListener('resize', resizeHandler);
|
|
disposeDomEvents = null;
|
|
};
|
|
}
|
|
|
|
return {
|
|
autoOpenSubtitleSidebarOnStartup,
|
|
openSubtitleSidebarModal,
|
|
closeSubtitleSidebarModal,
|
|
toggleSubtitleSidebarModal,
|
|
refreshSubtitleSidebarSnapshot: refreshSnapshot,
|
|
wireDomEvents,
|
|
disposeDomEvents: () => {
|
|
disposeDomEvents?.();
|
|
},
|
|
handleSubtitleUpdated,
|
|
seekToCue,
|
|
};
|
|
}
|