diff --git a/plugins/radiant-lyrics-luna/src/index.ts b/plugins/radiant-lyrics-luna/src/index.ts index 4323854..27f74ea 100644 --- a/plugins/radiant-lyrics-luna/src/index.ts +++ b/plugins/radiant-lyrics-luna/src/index.ts @@ -1515,6 +1515,8 @@ let tickLoopUnload: LunaUnload | null = null; let isActive = false; let savedTidalClasses: string[] | null = null; let tidalFollowObserver: MutationObserver | null = null; +let tidalFollowLunaUnload: (() => void) | null = null; +let tidalFollowRebuildPending = false; let nativeLyricsOverlayInstalled = false; let originalReduxGetState: (() => ReturnType) | null = null; @@ -1938,6 +1940,7 @@ let scrollSynced = true; let userScrollListener: (() => void) | null = null; let syncButtonListener: (() => void) | null = null; let syncButtonEl: HTMLElement | null = null; +let syncButtonObserverUnload: (() => void) | null = null; // scroll bounce animation state let scrollAnimIsAnimating = false; @@ -2962,6 +2965,10 @@ const buildTidalLines = ( }); lineDiv.appendChild(lineSpan); + lineDiv.addEventListener("click", () => { + tidalSpan.click(); + resync(); + }); wbwContainer.appendChild(lineDiv); lines.push({ el: lineDiv, @@ -2979,6 +2986,11 @@ const buildTidalLines = ( }; const stopTidalFollowLoop = (): void => { + tidalFollowRebuildPending = false; + if (tidalFollowLunaUnload) { + tidalFollowLunaUnload(); + tidalFollowLunaUnload = null; + } if (tidalFollowObserver) { tidalFollowObserver.disconnect(); tidalFollowObserver = null; @@ -3213,6 +3225,11 @@ const updateTidalFollowActiveLine = (): void => { applyInactiveBlurState(activeIndex); + // Retry hooking the sync button when desynced (no tick loop in line-tidal) + if (!scrollSynced && !syncButtonEl) { + hookSyncButton(); + } + if (activeIndex !== prevPrimary) { const newLine = lines[activeIndex]; const scrollParent = findScroller(newLine.el); @@ -3244,24 +3261,53 @@ const updateTidalFollowActiveLine = (): void => { } }; -const startTidalFollowLoop = (): void => { - stopTidalFollowLoop(); +const rebuildTidalSpanAttrObservers = (): void => { + if (tidalFollowObserver) { + tidalFollowObserver.disconnect(); + tidalFollowObserver = null; + } + const lyricsContainer = findLyricsContainer(); if (!lyricsContainer) return; + const tidalSpans = lyricsContainer.querySelectorAll( + 'span[data-test="lyrics-line"]', + ); + if (tidalSpans.length === 0) return; + tidalFollowObserver = new MutationObserver(() => { updateTidalFollowActiveLine(); }); - tidalFollowObserver.observe(lyricsContainer, { - subtree: true, - childList: true, - attributes: true, - attributeFilter: ["class"], - }); + + for (const span of tidalSpans) { + tidalFollowObserver.observe(span, { + attributes: true, + attributeFilter: ["class"], + }); + } updateTidalFollowActiveLine(); }; +const startTidalFollowLoop = (): void => { + stopTidalFollowLoop(); + + rebuildTidalSpanAttrObservers(); + + tidalFollowLunaUnload = observe( + unloads, + 'span[data-test="lyrics-line"]', + () => { + if (tidalFollowRebuildPending) return; + tidalFollowRebuildPending = true; + setTimeout(() => { + tidalFollowRebuildPending = false; + rebuildTidalSpanAttrObservers(); + }, 0); + }, + ); +}; + // watch for re-renders const watchForRerender = (): void => { unwatchRerender(); @@ -3369,13 +3415,10 @@ const findScroller = (el: HTMLElement): HTMLElement => { while (parent) { if (boundary && !boundary.contains(parent)) break; const style = window.getComputedStyle(parent); - if ( - style.overflowY === "auto" || - style.overflowY === "scroll" || - style.overflow === "auto" || - style.overflow === "scroll" || - parent.scrollHeight > parent.clientHeight + 1 - ) { + const oy = style.overflowY; + const o = style.overflow; + const scrollable = oy === "auto" || oy === "scroll" || o === "auto" || o === "scroll"; + if (scrollable || ((oy === "hidden" || o === "hidden") && parent.scrollHeight > parent.clientHeight + 1)) { return parent; } parent = parent.parentElement; @@ -3507,19 +3550,37 @@ const unhookUserScroll = (): void => { }; // Hook lyric scroll sync button -const hookSyncButton = (): void => { - unhookSyncButton(); - const btn = document.querySelector( - 'div[class*="_syncButton"] button', - ) as HTMLElement; - if (!btn) return; +const SYNC_BTN_SELECTOR = 'div[class*="_syncButton"] button'; + +const attachSyncButtonHandler = (btn: HTMLElement): void => { syncButtonEl = btn; const handler = () => resync(false); btn.addEventListener("click", handler); syncButtonListener = () => btn.removeEventListener("click", handler); }; +const hookSyncButton = (): void => { + unhookSyncButton(); + const btn = document.querySelector(SYNC_BTN_SELECTOR) as HTMLElement; + if (btn) { + attachSyncButtonHandler(btn); + return; + } + syncButtonObserverUnload = observe( + unloads, + SYNC_BTN_SELECTOR, + (el) => { + if (syncButtonEl) return; + attachSyncButtonHandler(el); + }, + ); +}; + const unhookSyncButton = (): void => { + if (syncButtonObserverUnload) { + syncButtonObserverUnload(); + syncButtonObserverUnload = null; + } if (syncButtonListener) { syncButtonListener(); syncButtonListener = null;