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