mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-27 00:55:16 -07:00
fix(jellyfin): show overlay, inject plugin, and fix stats title on playback (#77)
* fix(jellyfin): show overlay, inject plugin, and fix stats title on playb - Show visible overlay automatically during Jellyfin playback so subtitleStyle applies - Inject bundled mpv plugin on auto-launch so keybindings work without overlay focus - Group Jellyfin playback stats under item metadata (jellyfin://host/item/id) instead of stream URLs so episodes merge with matching local titles - Mark ffsubsync unavailable in subsync modal for remote media paths - Drain queued second-instance commands even when onReady throws * fix(overlay): stabilize macOS focus handoff and sidebar Yomitan pause - Keep overlay visible during macOS foreground probe after overlay blur - Hold sidebar hover-pause while a Yomitan lookup popup remains open * fix(jellyfin): fix discovery loop, device identity, tray state, and Disc - Derive device identity from OS hostname; remove legacy configurable client/device fields - Prevent discovery playback from reloading active item, misreporting pause state, and duplicate overlay restores - Restart stale tray discovery sessions without re-login when server drops SubMiner cast target - Sync tray discovery checkbox state on Linux after CLI/startup/remote-session changes - Stop Discord presence falling back to stream URLs; prime title before tokenized stream loads - Fix picker library discovery when log level is above info - Fix config.example.jsonc trailing commas and array formatting * docs(release): trim and consolidate prerelease notes for 0.15.0 - Remove breaking changes section and several redundant bullet points - Consolidate per-platform updater notes into a single entry - Normalize em-dash separators to hyphens in section headers * fix(config): remove trailing commas from config.example.jsonc - Strip trailing commas throughout both config.example.jsonc copies - Reformat inline arrays to multi-line for JSON strictness - Update Jellyfin subtitle preload and playback launch tests and impl * fix(tokenizer): preserve known-word highlight when POS filters suppress - Known-word cache matches now set isKnown=true even for tokens excluded by POS filters - POS exclusion gate suppresses N+1, frequency, and JLPT only; known status is computed before the gate - Jellyfin subtitle preload continues after cleanup failures instead of aborting - Update config docs and option description to document the known-word bypass behavior * fix(jellyfin): send explicit hide/show overlay instead of toggle - Track overlay visibility in plugin state; y-t uses explicit hide/show commands when state is known - Prevent paused Jellyfin playback from resuming on overlay hide - Fix subtitle cache cleanup to only remove dirs after successful cleanup * fix(jellyfin): fix remote progress sync, seek reporting, and startup sto - arm active playback before loadfile with loadedMediaPath: null to suppress premature stop events - force immediate progress report on seek-like position jumps at the mpv time-pos level - send positionTicks and failed=false in reportStopped payload - remove EventName from HTTP timeline payloads (websocket-only field) - add startup grace window to drop stop events before media finishes loading * fix(jellyfin): fix overlay toggle sync, redirect reload, and AppImage bi - Sync visible-overlay state back to plugin via script messages to avoid toggle/hide drift - Collapse duplicate toggle events within 250ms to prevent hide-then-show on single keypress - Preserve manual hide across Jellyfin path-changing redirects even when media-title drops - Rearm managed subtitle defaults on path-changing redirects - Route toggleVisibleOverlay session binding through plugin toggle instead of app-side IPC - Show Linux/Hyprland overlay passively (showInactive) to avoid stealing mpv keyboard focus - Fix AppImage binary resolution to prefer $APPIMAGE env over mounted inner binary - Add stats window layer management so delete/update dialogs appear above stats window - Fix Jellyfin remote progress sync during Linux websocket reconnect windows * Fix CodeRabbit review feedback * fix(jellyfin): subtitle timing, resume progress, and overlay sync - Add per-stream subtitle delay persistence and auto timeline-offset correction - Strip server-selected subtitle stream from mpv load URL; suppress plugin subtitle rearm and auto-start during app-managed preload - Fix resume position lost when mpv resets on stop; use last known position for final progress/stopped reports - Keep Play vs Resume distinct to avoid early seek race on normal play - Fix discovery resume when remote play sends StartPositionTicks=0 despite saved progress - Deduplicate show/hide overlay commands using recorded visibility state - Rewrite docs-site Jellyfin page around cast-to-device UX * test: update lifecycle cleanup assertion * fix: clear aborted playback state, fix overlay passthrough, and guard du - Reset app_managed_playback_pending on lifecycle cleanup to prevent state leak into next item - Record visible overlay action only after command succeeds, not before - Non-native passive overlay now always click-through on re-show (fix isNonNativePassiveOverlay ordering) - Defer activeParsedSubtitleMediaPath assignment until after prefetch completes - Move autoplay gate release into the hide branch of toggleVisibleOverlay - Clear active Jellyfin playback when stopping media that never loaded - Reset managed subtitle delay and delay key when no external tracks are available - Await async removeDir in subtitle cache cleanup - Guard duplicate delete clicks in MediaDetailView and SessionsTab with refs - Escape key in DeleteConfirmDialog now calls stopPropagation and stopImmediatePropagation
This commit is contained in:
@@ -1008,6 +1008,38 @@ test('visible-layer y-t dispatches mpv plugin toggle while overlay owns focus',
|
||||
}
|
||||
});
|
||||
|
||||
test('visible-layer configured overlay toggle dispatches mpv plugin toggle', async () => {
|
||||
const { handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||
|
||||
try {
|
||||
await handlers.setupMpvInputForwarding();
|
||||
handlers.updateSessionBindings([
|
||||
{
|
||||
sourcePath: 'shortcuts.toggleVisibleOverlayGlobal',
|
||||
originalKey: 'Alt+Shift+O',
|
||||
key: { code: 'KeyO', modifiers: ['alt', 'shift'] },
|
||||
actionType: 'session-action',
|
||||
actionId: 'toggleVisibleOverlay',
|
||||
},
|
||||
] as never);
|
||||
|
||||
testGlobals.dispatchKeydown({ key: 'O', code: 'KeyO', altKey: true, shiftKey: true });
|
||||
|
||||
assert.equal(
|
||||
testGlobals.mpvCommands.some(
|
||||
(command) => command[0] === 'script-message' && command[1] === 'subminer-toggle',
|
||||
),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
testGlobals.sessionActions.some((action) => action.actionId === 'toggleVisibleOverlay'),
|
||||
false,
|
||||
);
|
||||
} finally {
|
||||
testGlobals.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('refreshConfiguredShortcuts updates hot-reloaded stats and watched keys', async () => {
|
||||
const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||
|
||||
|
||||
@@ -207,6 +207,11 @@ export function createKeyboardHandlers(
|
||||
return;
|
||||
}
|
||||
|
||||
if (binding.actionType === 'session-action' && binding.actionId === 'toggleVisibleOverlay') {
|
||||
window.electronAPI.sendMpvCommand(['script-message', 'subminer-toggle']);
|
||||
return;
|
||||
}
|
||||
|
||||
if (binding.actionType === 'session-action' && binding.actionId === 'openControllerSelect') {
|
||||
options.openControllerSelectModal?.();
|
||||
return;
|
||||
|
||||
@@ -83,6 +83,7 @@ function createTestHarness(runSubsyncManual: () => Promise<{ ok: boolean; messag
|
||||
|
||||
const subsyncEngineFfsubsync = {
|
||||
checked: false,
|
||||
disabled: false,
|
||||
addEventListener: engineFfsubsyncEvents.addEventListener,
|
||||
dispatch: engineFfsubsyncEvents.dispatch,
|
||||
};
|
||||
@@ -194,6 +195,7 @@ test('manual subsync failure closes during run, then reopens modal with error',
|
||||
harness.modal.wireDomEvents();
|
||||
harness.modal.openSubsyncModal({
|
||||
sourceTracks: [{ id: 2, label: 'External #2 - eng' }],
|
||||
ffsubsyncAvailable: true,
|
||||
});
|
||||
|
||||
harness.runButton.dispatch('click');
|
||||
@@ -224,3 +226,47 @@ test('manual subsync failure closes during run, then reopens modal with error',
|
||||
harness.restoreGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
test('subsync modal disables ffsubsync when payload marks it unavailable', () => {
|
||||
const harness = createTestHarness(async () => ({ ok: true, message: 'ok' }));
|
||||
|
||||
try {
|
||||
harness.modal.openSubsyncModal({
|
||||
sourceTracks: [{ id: 2, label: 'External #2 - eng' }],
|
||||
ffsubsyncAvailable: false,
|
||||
});
|
||||
|
||||
assert.equal(harness.ctx.dom.subsyncEngineAlass.checked, true);
|
||||
assert.equal(harness.ctx.dom.subsyncEngineFfsubsync.checked, false);
|
||||
assert.equal(harness.ctx.dom.subsyncEngineFfsubsync.disabled, true);
|
||||
assert.equal(harness.ctx.dom.subsyncStatus.textContent, 'Choose alass source, then run.');
|
||||
} finally {
|
||||
harness.restoreGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
test('subsync modal ignores enter submission when no sync engine is available', async () => {
|
||||
let runCalls = 0;
|
||||
const harness = createTestHarness(async () => {
|
||||
runCalls += 1;
|
||||
return { ok: true, message: 'ok' };
|
||||
});
|
||||
|
||||
try {
|
||||
harness.modal.openSubsyncModal({
|
||||
sourceTracks: [],
|
||||
ffsubsyncAvailable: false,
|
||||
});
|
||||
|
||||
harness.modal.handleSubsyncKeydown({
|
||||
key: 'Enter',
|
||||
preventDefault: () => {},
|
||||
} as KeyboardEvent);
|
||||
await flushMicrotasks();
|
||||
|
||||
assert.equal(runCalls, 0);
|
||||
assert.equal(harness.ctx.state.subsyncModalOpen, true);
|
||||
} finally {
|
||||
harness.restoreGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,6 +8,8 @@ export function createSubsyncModal(
|
||||
syncSettingsModalSubtitleSuppression: () => void;
|
||||
},
|
||||
) {
|
||||
let ffsubsyncAvailable = true;
|
||||
|
||||
function setSubsyncStatus(message: string, isError = false): void {
|
||||
ctx.dom.subsyncStatus.textContent = message;
|
||||
ctx.dom.subsyncStatus.classList.toggle('error', isError);
|
||||
@@ -46,20 +48,26 @@ export function createSubsyncModal(
|
||||
|
||||
function openSubsyncModal(payload: SubsyncManualPayload): void {
|
||||
ctx.state.subsyncSubmitting = false;
|
||||
ctx.dom.subsyncRunButton.disabled = false;
|
||||
ctx.state.subsyncSourceTracks = payload.sourceTracks;
|
||||
ffsubsyncAvailable = payload.ffsubsyncAvailable;
|
||||
|
||||
const hasSources = ctx.state.subsyncSourceTracks.length > 0;
|
||||
ctx.dom.subsyncEngineAlass.checked = hasSources;
|
||||
ctx.dom.subsyncEngineFfsubsync.checked = !hasSources;
|
||||
ctx.dom.subsyncEngineFfsubsync.checked = !hasSources && ffsubsyncAvailable;
|
||||
ctx.dom.subsyncEngineFfsubsync.disabled = !ffsubsyncAvailable;
|
||||
ctx.dom.subsyncRunButton.disabled = !hasSources && !ffsubsyncAvailable;
|
||||
|
||||
renderSubsyncSourceTracks();
|
||||
updateSubsyncSourceVisibility();
|
||||
|
||||
setSubsyncStatus(
|
||||
hasSources
|
||||
? 'Choose engine and source, then run.'
|
||||
: 'No source subtitles available for alass. Use ffsubsync.',
|
||||
!ffsubsyncAvailable && hasSources
|
||||
? 'Choose alass source, then run.'
|
||||
: !ffsubsyncAvailable
|
||||
? 'No source subtitles available for alass.'
|
||||
: hasSources
|
||||
? 'Choose engine and source, then run.'
|
||||
: 'No source subtitles available for alass. Use ffsubsync.',
|
||||
false,
|
||||
);
|
||||
|
||||
@@ -77,7 +85,7 @@ export function createSubsyncModal(
|
||||
sourceTrackId: number | null,
|
||||
message: string,
|
||||
): void {
|
||||
openSubsyncModal({ sourceTracks });
|
||||
openSubsyncModal({ sourceTracks, ffsubsyncAvailable });
|
||||
|
||||
if (engine === 'alass' && sourceTracks.length > 0) {
|
||||
ctx.dom.subsyncEngineAlass.checked = true;
|
||||
@@ -85,7 +93,7 @@ export function createSubsyncModal(
|
||||
if (Number.isFinite(sourceTrackId)) {
|
||||
ctx.dom.subsyncSourceSelect.value = String(sourceTrackId);
|
||||
}
|
||||
} else {
|
||||
} else if (ffsubsyncAvailable) {
|
||||
ctx.dom.subsyncEngineAlass.checked = false;
|
||||
ctx.dom.subsyncEngineFfsubsync.checked = true;
|
||||
}
|
||||
@@ -97,8 +105,16 @@ export function createSubsyncModal(
|
||||
|
||||
async function runSubsyncManualFromModal(): Promise<void> {
|
||||
if (ctx.state.subsyncSubmitting) return;
|
||||
if (ctx.dom.subsyncRunButton.disabled) return;
|
||||
|
||||
const engine = ctx.dom.subsyncEngineAlass.checked ? 'alass' : 'ffsubsync';
|
||||
const useAlass = ctx.dom.subsyncEngineAlass.checked;
|
||||
const useFfsubsync = ctx.dom.subsyncEngineFfsubsync.checked;
|
||||
if (!useAlass && !useFfsubsync) {
|
||||
setSubsyncStatus('No sync engine available for current media.', true);
|
||||
return;
|
||||
}
|
||||
|
||||
const engine = useAlass ? 'alass' : 'ffsubsync';
|
||||
const sourceTrackId =
|
||||
engine === 'alass' && ctx.dom.subsyncSourceSelect.value
|
||||
? Number.parseInt(ctx.dom.subsyncSourceSelect.value, 10)
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
createSubtitleSidebarModal,
|
||||
findActiveSubtitleCueIndex,
|
||||
} from './subtitle-sidebar.js';
|
||||
import { YOMITAN_POPUP_HIDDEN_EVENT, YOMITAN_POPUP_SHOWN_EVENT } from '../yomitan-popup.js';
|
||||
|
||||
function createClassList(initialTokens: string[] = []) {
|
||||
const tokens = new Set(initialTokens);
|
||||
@@ -1542,6 +1543,137 @@ test('subtitle sidebar hover pause ignores playback-state IPC failures', async (
|
||||
}
|
||||
});
|
||||
|
||||
test('subtitle sidebar keeps hover pause while a Yomitan lookup popup remains open', async () => {
|
||||
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
|
||||
const previousWindow = globals.window;
|
||||
const previousDocument = globals.document;
|
||||
const mpvCommands: Array<Array<string | number>> = [];
|
||||
const contentListeners = new Map<string, Array<() => Promise<void> | void>>();
|
||||
const windowListeners = new Map<string, Array<() => Promise<void> | void>>();
|
||||
|
||||
const snapshot: SubtitleSidebarSnapshot = {
|
||||
cues: [{ startTime: 1, endTime: 2, text: 'first' }],
|
||||
currentSubtitle: {
|
||||
text: 'first',
|
||||
startTime: 1,
|
||||
endTime: 2,
|
||||
},
|
||||
currentTimeSec: 1.1,
|
||||
config: {
|
||||
enabled: true,
|
||||
autoOpen: false,
|
||||
layout: 'overlay',
|
||||
toggleKey: 'Backslash',
|
||||
pauseVideoOnHover: true,
|
||||
autoScroll: true,
|
||||
maxWidth: 420,
|
||||
opacity: 0.92,
|
||||
backgroundColor: 'rgba(54, 58, 79, 0.88)',
|
||||
textColor: '#cad3f5',
|
||||
fontFamily: '"Iosevka Aile", sans-serif',
|
||||
fontSize: 17,
|
||||
timestampColor: '#a5adcb',
|
||||
activeLineColor: '#f5bde6',
|
||||
activeLineBackgroundColor: 'rgba(138, 173, 244, 0.22)',
|
||||
hoverLineBackgroundColor: 'rgba(54, 58, 79, 0.84)',
|
||||
},
|
||||
};
|
||||
|
||||
Object.defineProperty(globalThis, 'window', {
|
||||
configurable: true,
|
||||
value: {
|
||||
addEventListener: (type: string, listener: () => Promise<void> | void) => {
|
||||
const bucket = windowListeners.get(type) ?? [];
|
||||
bucket.push(listener);
|
||||
windowListeners.set(type, bucket);
|
||||
},
|
||||
removeEventListener: () => {},
|
||||
electronAPI: {
|
||||
getSubtitleSidebarSnapshot: async () => snapshot,
|
||||
getPlaybackPaused: async () => false,
|
||||
sendMpvCommand: (command: Array<string | number>) => {
|
||||
mpvCommands.push(command);
|
||||
},
|
||||
} as unknown as ElectronAPI,
|
||||
},
|
||||
});
|
||||
Object.defineProperty(globalThis, 'document', {
|
||||
configurable: true,
|
||||
value: {
|
||||
createElement: () => createCueRow(),
|
||||
body: {
|
||||
classList: createClassList(),
|
||||
},
|
||||
documentElement: {
|
||||
style: {
|
||||
setProperty: () => {},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const state = createRendererState();
|
||||
state.autoPauseVideoOnYomitanPopup = true;
|
||||
const ctx = {
|
||||
dom: {
|
||||
overlay: { classList: createClassList() },
|
||||
subtitleSidebarModal: {
|
||||
classList: createClassList(['hidden']),
|
||||
setAttribute: () => {},
|
||||
style: { setProperty: () => {} },
|
||||
addEventListener: () => {},
|
||||
},
|
||||
subtitleSidebarContent: {
|
||||
classList: createClassList(),
|
||||
getBoundingClientRect: () => ({ width: 420 }),
|
||||
addEventListener: (type: string, listener: () => Promise<void> | void) => {
|
||||
const bucket = contentListeners.get(type) ?? [];
|
||||
bucket.push(listener);
|
||||
contentListeners.set(type, bucket);
|
||||
},
|
||||
},
|
||||
subtitleSidebarClose: { addEventListener: () => {} },
|
||||
subtitleSidebarStatus: { textContent: '' },
|
||||
subtitleSidebarList: createListStub(),
|
||||
},
|
||||
state,
|
||||
};
|
||||
|
||||
const modal = createSubtitleSidebarModal(ctx as never, {
|
||||
modalStateReader: { isAnyModalOpen: () => false },
|
||||
});
|
||||
modal.wireDomEvents();
|
||||
|
||||
await modal.openSubtitleSidebarModal();
|
||||
mpvCommands.length = 0;
|
||||
await contentListeners.get('mouseenter')?.[0]?.();
|
||||
|
||||
assert.deepEqual(mpvCommands, [['set_property', 'pause', 'yes']]);
|
||||
|
||||
for (const listener of windowListeners.get(YOMITAN_POPUP_SHOWN_EVENT) ?? []) {
|
||||
await listener();
|
||||
}
|
||||
await contentListeners.get('mouseleave')?.[0]?.();
|
||||
|
||||
assert.deepEqual(mpvCommands, [['set_property', 'pause', 'yes']]);
|
||||
assert.equal(state.subtitleSidebarPausedByHover, true);
|
||||
|
||||
for (const listener of windowListeners.get(YOMITAN_POPUP_HIDDEN_EVENT) ?? []) {
|
||||
await listener();
|
||||
}
|
||||
|
||||
assert.deepEqual(mpvCommands, [
|
||||
['set_property', 'pause', 'yes'],
|
||||
['set_property', 'pause', 'no'],
|
||||
]);
|
||||
assert.equal(state.subtitleSidebarPausedByHover, false);
|
||||
} finally {
|
||||
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
|
||||
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
|
||||
}
|
||||
});
|
||||
|
||||
test('subtitle sidebar embedded layout reserves and releases mpv right margin', async () => {
|
||||
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
|
||||
const previousWindow = globals.window;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { SubtitleCue, SubtitleData, SubtitleSidebarSnapshot } from '../../types';
|
||||
import type { ModalStateReader, RendererContext } from '../context';
|
||||
import { syncOverlayMouseIgnoreState } from '../overlay-mouse-ignore.js';
|
||||
import {
|
||||
YOMITAN_POPUP_HIDDEN_EVENT,
|
||||
YOMITAN_POPUP_SHOWN_EVENT,
|
||||
isYomitanPopupVisible,
|
||||
} from '../yomitan-popup.js';
|
||||
|
||||
const MANUAL_SCROLL_HOLD_MS = 1500;
|
||||
const ACTIVE_CUE_LOOKAHEAD_SEC = 0.18;
|
||||
@@ -194,6 +199,8 @@ export function createSubtitleSidebarModal(
|
||||
let disposeDomEvents: (() => void) | null = null;
|
||||
let subtitleSidebarHovered = false;
|
||||
let subtitleSidebarFocusedWithin = false;
|
||||
let subtitleSidebarYomitanPopupVisible = false;
|
||||
let subtitleSidebarPauseHeldByYomitanPopup = false;
|
||||
|
||||
function restoreEmbeddedSidebarPassthrough(): void {
|
||||
syncOverlayMouseIgnoreState(ctx);
|
||||
@@ -323,18 +330,65 @@ export function createSubtitleSidebarModal(
|
||||
return `Jump to subtitle at ${formatCueTimestamp(cue.startTime)}`;
|
||||
}
|
||||
|
||||
function isYomitanPopupVisibleForSidebar(): boolean {
|
||||
if (subtitleSidebarYomitanPopupVisible || ctx.state.yomitanPopupVisible) {
|
||||
return true;
|
||||
}
|
||||
if (typeof document === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
return isYomitanPopupVisible(document);
|
||||
}
|
||||
|
||||
function shouldHoldSidebarPauseForYomitanPopup(): boolean {
|
||||
return (
|
||||
ctx.state.autoPauseVideoOnYomitanPopup &&
|
||||
ctx.state.subtitleSidebarPausedByHover &&
|
||||
isYomitanPopupVisibleForSidebar()
|
||||
);
|
||||
}
|
||||
|
||||
function resumeSubtitleSidebarHoverPause(): void {
|
||||
subtitleSidebarHoverRequestId += 1;
|
||||
if (!ctx.state.subtitleSidebarPausedByHover) {
|
||||
subtitleSidebarPauseHeldByYomitanPopup = false;
|
||||
restoreEmbeddedSidebarPassthrough();
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldHoldSidebarPauseForYomitanPopup()) {
|
||||
subtitleSidebarPauseHeldByYomitanPopup = true;
|
||||
restoreEmbeddedSidebarPassthrough();
|
||||
return;
|
||||
}
|
||||
|
||||
subtitleSidebarPauseHeldByYomitanPopup = false;
|
||||
ctx.state.subtitleSidebarPausedByHover = false;
|
||||
window.electronAPI.sendMpvCommand(['set_property', 'pause', 'no']);
|
||||
restoreEmbeddedSidebarPassthrough();
|
||||
}
|
||||
|
||||
function handleYomitanPopupShown(): void {
|
||||
subtitleSidebarYomitanPopupVisible = true;
|
||||
if (ctx.state.autoPauseVideoOnYomitanPopup && ctx.state.subtitleSidebarPausedByHover) {
|
||||
subtitleSidebarPauseHeldByYomitanPopup = true;
|
||||
}
|
||||
}
|
||||
|
||||
function handleYomitanPopupHidden(): void {
|
||||
subtitleSidebarYomitanPopupVisible = false;
|
||||
if (!subtitleSidebarPauseHeldByYomitanPopup) {
|
||||
return;
|
||||
}
|
||||
|
||||
subtitleSidebarPauseHeldByYomitanPopup = false;
|
||||
if (ctx.state.isOverSubtitleSidebar) {
|
||||
restoreEmbeddedSidebarPassthrough();
|
||||
return;
|
||||
}
|
||||
resumeSubtitleSidebarHoverPause();
|
||||
}
|
||||
|
||||
function maybeAutoScrollActiveCue(
|
||||
previousActiveCueIndex: number,
|
||||
behavior: ScrollBehavior = 'smooth',
|
||||
@@ -660,8 +714,12 @@ export function createSubtitleSidebarModal(
|
||||
syncEmbeddedSidebarLayout();
|
||||
};
|
||||
window.addEventListener('resize', resizeHandler);
|
||||
window.addEventListener(YOMITAN_POPUP_SHOWN_EVENT, handleYomitanPopupShown);
|
||||
window.addEventListener(YOMITAN_POPUP_HIDDEN_EVENT, handleYomitanPopupHidden);
|
||||
disposeDomEvents = () => {
|
||||
window.removeEventListener('resize', resizeHandler);
|
||||
window.removeEventListener(YOMITAN_POPUP_SHOWN_EVENT, handleYomitanPopupShown);
|
||||
window.removeEventListener(YOMITAN_POPUP_HIDDEN_EVENT, handleYomitanPopupHidden);
|
||||
disposeDomEvents = null;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user