mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-14 14:40:08 +02:00
fix scroll follow pause on manual scroll
This commit is contained in:
@@ -157,9 +157,7 @@ const collectLineParts = (lineElement: HTMLElement): PartData[] => {
|
||||
};
|
||||
|
||||
const collectOverlayParts = (lineElement: HTMLElement): PartData[] => {
|
||||
const words = lineElement.querySelectorAll<HTMLElement>(
|
||||
'.karaoke-overlay-word[data-duration]',
|
||||
);
|
||||
const words = lineElement.querySelectorAll<HTMLElement>('.karaoke-overlay-word[data-duration]');
|
||||
|
||||
return Array.from(words).map((element) => ({
|
||||
animationStartTimeMs: Number.POSITIVE_INFINITY,
|
||||
@@ -263,11 +261,7 @@ const setupPartAnimation = (part: PartData, interpolatedTimeSec: number, now: nu
|
||||
part.isAnimating = true;
|
||||
};
|
||||
|
||||
const animateWordParts = (
|
||||
parts: PartData[],
|
||||
interpolatedTimeSec: number,
|
||||
now: number,
|
||||
): void => {
|
||||
const animateWordParts = (parts: PartData[], interpolatedTimeSec: number, now: number): void => {
|
||||
for (const part of parts) {
|
||||
const partEnd = part.time + part.duration;
|
||||
|
||||
@@ -703,8 +697,7 @@ export const tickLyricsAnimation = (state: AnimEngineState, opts: TickOptions):
|
||||
|
||||
const ss = state.scroll;
|
||||
const scrollPausedByUser = ss.scrollResumeTime >= now;
|
||||
const canAutoscroll =
|
||||
follow && (!scrollPausedByUser || ss.pendingScroll || ss.scrollPos === -1);
|
||||
const canAutoscroll = follow && !scrollPausedByUser;
|
||||
|
||||
if (canAutoscroll) {
|
||||
if (activeElems.length === 0 && lines.length > 0) {
|
||||
@@ -760,14 +753,20 @@ export const tickLyricsAnimation = (state: AnimEngineState, opts: TickOptions):
|
||||
|
||||
if (ss.wasUserScrolling && ss.scrollResumeTime < now) {
|
||||
ss.wasUserScrolling = false;
|
||||
ss.pendingScroll = true;
|
||||
}
|
||||
|
||||
return activeLineIndex;
|
||||
};
|
||||
|
||||
export const handleLyricsUserScroll = (state: AnimEngineState, pauseDurationMs = 3000): void => {
|
||||
state.scroll.wasUserScrolling = true;
|
||||
state.scroll.scrollResumeTime = Date.now() + pauseDurationMs;
|
||||
const ss = state.scroll;
|
||||
cancelActiveLyricsScroll(ss);
|
||||
ss.programmaticScrollUntil = 0;
|
||||
ss.skipScrolls = 0;
|
||||
ss.skipScrollsDecayTimes = [];
|
||||
ss.wasUserScrolling = true;
|
||||
ss.scrollResumeTime = Date.now() + pauseDurationMs;
|
||||
};
|
||||
|
||||
export const resumeLyricsAutoscroll = (state: AnimEngineState): void => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import isElectron from 'is-electron';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import {
|
||||
animateLyricsScrollTo,
|
||||
@@ -22,6 +22,8 @@ const utils = isElectron() ? window.api.utils : null;
|
||||
const mpris = isElectron() && utils?.isLinux() ? window.api.mpris : null;
|
||||
|
||||
export const LYRICS_SCROLL_CONTAINER_ID = 'sychronized-lyrics-scroll-container';
|
||||
export const MANUAL_SCROLL_PAUSE_MS = 2000;
|
||||
const MANUAL_SCROLL_DRIFT_PX = 3;
|
||||
|
||||
export const useSynchronizedLyricsBase = (settingsKey = 'default', offsetMs?: number) => {
|
||||
const playbackType = usePlaybackType();
|
||||
@@ -129,6 +131,19 @@ export const useSynchronizedLyricsBase = (settingsKey = 'default', offsetMs?: nu
|
||||
resumeLyricsAutoscroll(scrollAnimStateRef.current);
|
||||
}, []);
|
||||
|
||||
const pauseManualScrollFollow = useCallback(() => {
|
||||
userScrollingRef.current = true;
|
||||
handleLyricsUserScroll(scrollAnimStateRef.current, MANUAL_SCROLL_PAUSE_MS);
|
||||
|
||||
if (scrollTimeoutRef.current) {
|
||||
clearTimeout(scrollTimeoutRef.current);
|
||||
}
|
||||
|
||||
scrollTimeoutRef.current = setTimeout(() => {
|
||||
userScrollingRef.current = false;
|
||||
}, MANUAL_SCROLL_PAUSE_MS);
|
||||
}, []);
|
||||
|
||||
const containerStyle = useMemo(
|
||||
() =>
|
||||
({
|
||||
@@ -170,13 +185,37 @@ export const useSynchronizedLyricsBase = (settingsKey = 'default', offsetMs?: nu
|
||||
delayMsRef.current = newOffset;
|
||||
}, [offsetMs]);
|
||||
|
||||
useEffect(() => {
|
||||
const container =
|
||||
containerRef.current ||
|
||||
(document.getElementById(LYRICS_SCROLL_CONTAINER_ID) as HTMLElement);
|
||||
if (!container) return;
|
||||
useLayoutEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleWheel = (event: WheelEvent) => {
|
||||
if (event.deltaX === 0 && event.deltaY === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
pauseManualScrollFollow();
|
||||
};
|
||||
|
||||
const handleTouchStart = () => {
|
||||
pauseManualScrollFollow();
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
const scrollState = scrollAnimStateRef.current.scroll;
|
||||
const isProgrammatic = Date.now() < scrollState.programmaticScrollUntil;
|
||||
|
||||
if (
|
||||
!isProgrammatic &&
|
||||
scrollState.scrollPos >= 0 &&
|
||||
Math.abs(container.scrollTop - scrollState.scrollPos) > MANUAL_SCROLL_DRIFT_PX
|
||||
) {
|
||||
pauseManualScrollFollow();
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldSkipLyricsScrollEvent(scrollAnimStateRef.current)) {
|
||||
return;
|
||||
}
|
||||
@@ -193,22 +232,18 @@ export const useSynchronizedLyricsBase = (settingsKey = 'default', offsetMs?: nu
|
||||
return;
|
||||
}
|
||||
|
||||
userScrollingRef.current = true;
|
||||
handleLyricsUserScroll(scrollAnimStateRef.current);
|
||||
|
||||
if (scrollTimeoutRef.current) {
|
||||
clearTimeout(scrollTimeoutRef.current);
|
||||
}
|
||||
|
||||
scrollTimeoutRef.current = setTimeout(() => {
|
||||
userScrollingRef.current = false;
|
||||
}, 3000);
|
||||
pauseManualScrollFollow();
|
||||
};
|
||||
|
||||
container.addEventListener('wheel', handleWheel, { passive: true });
|
||||
container.addEventListener('touchstart', handleTouchStart, { passive: true });
|
||||
container.addEventListener('scroll', handleScroll, { passive: true });
|
||||
|
||||
return () => {
|
||||
container.removeEventListener('wheel', handleWheel);
|
||||
container.removeEventListener('touchstart', handleTouchStart);
|
||||
container.removeEventListener('scroll', handleScroll);
|
||||
|
||||
if (scrollTimeoutRef.current) {
|
||||
clearTimeout(scrollTimeoutRef.current);
|
||||
}
|
||||
@@ -217,7 +252,7 @@ export const useSynchronizedLyricsBase = (settingsKey = 'default', offsetMs?: nu
|
||||
clearTimeout(programmaticScrollTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}, [pauseManualScrollFollow]);
|
||||
|
||||
return {
|
||||
containerRef,
|
||||
|
||||
@@ -150,7 +150,9 @@ export const SynchronizedKaraokeLyrics = ({
|
||||
eventCreationTime: options?.eventCreationTime ?? Date.now(),
|
||||
forceResync: options?.forceResync ?? false,
|
||||
});
|
||||
lastSyncedTimeRef.current = timeInMs;
|
||||
const eventCreationTime = options?.eventCreationTime ?? Date.now();
|
||||
const interpolatedOffsetMs = isPlaying ? Date.now() - eventCreationTime : 0;
|
||||
lastSyncedTimeRef.current = timeInMs + interpolatedOffsetMs;
|
||||
},
|
||||
[rebuildLyricsData, reset, tick],
|
||||
);
|
||||
@@ -267,6 +269,8 @@ export const SynchronizedKaraokeLyrics = ({
|
||||
}
|
||||
|
||||
if (isSeek) {
|
||||
resumeAutoscroll();
|
||||
resumeEngineAutoscroll();
|
||||
syncAtTime(timeInMs, true, {
|
||||
eventCreationTime: playbackAnchorRef.current.eventCreationTime,
|
||||
forceReset: true,
|
||||
@@ -276,7 +280,7 @@ export const SynchronizedKaraokeLyrics = ({
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [delayMsRef, syncAtTime, updatePlaybackAnchor]);
|
||||
}, [delayMsRef, resumeAutoscroll, resumeEngineAutoscroll, syncAtTime, updatePlaybackAnchor]);
|
||||
|
||||
const getOverlayText = (
|
||||
overlayLyrics: null | SynchronizedLyricsData | undefined,
|
||||
|
||||
@@ -126,6 +126,8 @@ export const SynchronizedLyrics = ({
|
||||
const timeInMs = timestamp * 1000 + delayMsRef.current;
|
||||
|
||||
if (Math.abs(timeInMs - lastSyncedTimeRef.current) > SEEK_DETECT_THRESHOLD_MS) {
|
||||
resumeAutoscroll();
|
||||
resumeEngineAutoscroll();
|
||||
syncAtTime(timeInMs, true, true);
|
||||
} else {
|
||||
syncAtTime(timeInMs, true);
|
||||
@@ -135,7 +137,7 @@ export const SynchronizedLyrics = ({
|
||||
};
|
||||
|
||||
rafRef.current = requestAnimationFrame(runTick);
|
||||
}, [delayMsRef, stopRaf, syncAtTime]);
|
||||
}, [delayMsRef, resumeAutoscroll, resumeEngineAutoscroll, stopRaf, syncAtTime]);
|
||||
|
||||
const syncFromCurrentTimestamp = useCallback(() => {
|
||||
const timestamp = useTimestampStoreBase.getState().timestamp;
|
||||
@@ -205,12 +207,14 @@ export const SynchronizedLyrics = ({
|
||||
}
|
||||
|
||||
if (Math.abs(timeInMs - lastSyncedTimeRef.current) > SEEK_DETECT_THRESHOLD_MS) {
|
||||
resumeAutoscroll();
|
||||
resumeEngineAutoscroll();
|
||||
syncAtTime(timeInMs, true, true);
|
||||
}
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [delayMsRef, syncAtTime]);
|
||||
}, [delayMsRef, resumeAutoscroll, resumeEngineAutoscroll, syncAtTime]);
|
||||
|
||||
const handleContainerClick = useCallback(
|
||||
(event: React.MouseEvent<HTMLDivElement>) => {
|
||||
|
||||
Reference in New Issue
Block a user