mirror of
https://github.com/antebudimir/feishin.git
synced 2026-01-02 10:53:33 +00:00
Lint all files
This commit is contained in:
parent
22af76b4d6
commit
30e52ebb54
334 changed files with 76519 additions and 75932 deletions
|
|
@ -1,10 +1,10 @@
|
|||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import {
|
||||
useCurrentStatus,
|
||||
useCurrentTime,
|
||||
useLyricsSettings,
|
||||
usePlayerType,
|
||||
useSeeked,
|
||||
useCurrentStatus,
|
||||
useCurrentTime,
|
||||
useLyricsSettings,
|
||||
usePlayerType,
|
||||
useSeeked,
|
||||
} from '/@/renderer/store';
|
||||
import { PlaybackType, PlayerStatus } from '/@/renderer/types';
|
||||
import { LyricLine } from '/@/renderer/features/lyrics/lyric-line';
|
||||
|
|
@ -16,303 +16,310 @@ import styled from 'styled-components';
|
|||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||
|
||||
const SynchronizedLyricsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 10vh 0 6vh;
|
||||
overflow: scroll;
|
||||
transform: translateY(-2rem);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 10vh 0 6vh;
|
||||
overflow: scroll;
|
||||
transform: translateY(-2rem);
|
||||
|
||||
mask-image: linear-gradient(
|
||||
180deg,
|
||||
transparent 5%,
|
||||
rgba(0, 0, 0, 100%) 20%,
|
||||
rgba(0, 0, 0, 100%) 85%,
|
||||
transparent 95%
|
||||
);
|
||||
mask-image: linear-gradient(
|
||||
180deg,
|
||||
transparent 5%,
|
||||
rgba(0, 0, 0, 100%) 20%,
|
||||
rgba(0, 0, 0, 100%) 85%,
|
||||
transparent 95%
|
||||
);
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
padding: 5vh 0;
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
padding: 5vh 0;
|
||||
}
|
||||
`;
|
||||
|
||||
interface SynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
|
||||
lyrics: SynchronizedLyricsArray;
|
||||
lyrics: SynchronizedLyricsArray;
|
||||
}
|
||||
|
||||
export const SynchronizedLyrics = ({
|
||||
artist,
|
||||
lyrics,
|
||||
name,
|
||||
remote,
|
||||
source,
|
||||
artist,
|
||||
lyrics,
|
||||
name,
|
||||
remote,
|
||||
source,
|
||||
}: SynchronizedLyricsProps) => {
|
||||
const playersRef = PlayersRef;
|
||||
const status = useCurrentStatus();
|
||||
const playerType = usePlayerType();
|
||||
const now = useCurrentTime();
|
||||
const settings = useLyricsSettings();
|
||||
const playersRef = PlayersRef;
|
||||
const status = useCurrentStatus();
|
||||
const playerType = usePlayerType();
|
||||
const now = useCurrentTime();
|
||||
const settings = useLyricsSettings();
|
||||
|
||||
const seeked = useSeeked();
|
||||
const seeked = useSeeked();
|
||||
|
||||
// A reference to the timeout handler
|
||||
const lyricTimer = useRef<ReturnType<typeof setTimeout>>();
|
||||
// A reference to the timeout handler
|
||||
const lyricTimer = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
// A reference to the lyrics. This is necessary for the
|
||||
// timers, which are not part of react necessarily, to always
|
||||
// have the most updated values
|
||||
const lyricRef = useRef<SynchronizedLyricsArray>();
|
||||
// A reference to the lyrics. This is necessary for the
|
||||
// timers, which are not part of react necessarily, to always
|
||||
// have the most updated values
|
||||
const lyricRef = useRef<SynchronizedLyricsArray>();
|
||||
|
||||
// A constantly increasing value, used to tell timers that may be out of date
|
||||
// whether to proceed or stop
|
||||
const timerEpoch = useRef(0);
|
||||
// A constantly increasing value, used to tell timers that may be out of date
|
||||
// whether to proceed or stop
|
||||
const timerEpoch = useRef(0);
|
||||
|
||||
const delayMsRef = useRef(settings.delayMs);
|
||||
const followRef = useRef(settings.follow);
|
||||
const delayMsRef = useRef(settings.delayMs);
|
||||
const followRef = useRef(settings.follow);
|
||||
|
||||
const getCurrentLyric = (timeInMs: number) => {
|
||||
if (lyricRef.current) {
|
||||
const activeLyrics = lyricRef.current;
|
||||
for (let idx = 0; idx < activeLyrics.length; idx += 1) {
|
||||
if (timeInMs <= activeLyrics[idx][0]) {
|
||||
return idx === 0 ? idx : idx - 1;
|
||||
}
|
||||
}
|
||||
const getCurrentLyric = (timeInMs: number) => {
|
||||
if (lyricRef.current) {
|
||||
const activeLyrics = lyricRef.current;
|
||||
for (let idx = 0; idx < activeLyrics.length; idx += 1) {
|
||||
if (timeInMs <= activeLyrics[idx][0]) {
|
||||
return idx === 0 ? idx : idx - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return activeLyrics.length - 1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
};
|
||||
|
||||
const getCurrentTime = useCallback(async () => {
|
||||
if (isElectron() && playerType !== PlaybackType.WEB) {
|
||||
if (mpvPlayer) {
|
||||
return mpvPlayer.getCurrentTime();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (playersRef.current === undefined) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const player = (playersRef.current.player1 ?? playersRef.current.player2).getInternalPlayer();
|
||||
|
||||
// If it is null, this probably means we added a new song while the lyrics tab is open
|
||||
// and the queue was previously empty
|
||||
if (!player) return 0;
|
||||
|
||||
return player.currentTime;
|
||||
}, [playerType, playersRef]);
|
||||
|
||||
const setCurrentLyric = useCallback((timeInMs: number, epoch?: number, targetIndex?: number) => {
|
||||
const start = performance.now();
|
||||
let nextEpoch: number;
|
||||
|
||||
if (epoch === undefined) {
|
||||
timerEpoch.current = (timerEpoch.current + 1) % 10000;
|
||||
nextEpoch = timerEpoch.current;
|
||||
} else if (epoch !== timerEpoch.current) {
|
||||
return;
|
||||
} else {
|
||||
nextEpoch = epoch;
|
||||
}
|
||||
|
||||
let index: number;
|
||||
|
||||
if (targetIndex === undefined) {
|
||||
index = getCurrentLyric(timeInMs);
|
||||
} else {
|
||||
index = targetIndex;
|
||||
}
|
||||
|
||||
// Directly modify the dom instead of using react to prevent rerender
|
||||
document
|
||||
.querySelectorAll('.synchronized-lyrics .active')
|
||||
.forEach((node) => node.classList.remove('active'));
|
||||
|
||||
if (index === -1) {
|
||||
lyricRef.current = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = document.getElementById('sychronized-lyrics-scroll-container') as HTMLElement;
|
||||
const currentLyric = document.querySelector(`#lyric-${index}`) as HTMLElement;
|
||||
const offsetTop = currentLyric?.offsetTop - doc?.clientHeight / 2 ?? 0;
|
||||
|
||||
if (currentLyric === null) {
|
||||
lyricRef.current = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
currentLyric.classList.add('active');
|
||||
|
||||
if (followRef.current) {
|
||||
doc?.scroll({ behavior: 'smooth', top: offsetTop });
|
||||
}
|
||||
|
||||
if (index !== lyricRef.current!.length - 1) {
|
||||
const nextTime = lyricRef.current![index + 1][0];
|
||||
|
||||
const elapsed = performance.now() - start;
|
||||
|
||||
lyricTimer.current = setTimeout(() => {
|
||||
setCurrentLyric(nextTime, nextEpoch, index + 1);
|
||||
}, nextTime - timeInMs - elapsed);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Copy the follow settings into a ref that can be accessed in the timeout
|
||||
followRef.current = settings.follow;
|
||||
}, [settings.follow]);
|
||||
|
||||
useEffect(() => {
|
||||
// This handler is used to handle when lyrics change. It is in some sense the
|
||||
// 'primary' handler for parsing lyrics, as unlike the other callbacks, it will
|
||||
// ALSO remove listeners on close. Use the promisified getCurrentTime(), because
|
||||
// we don't want to be dependent on npw, which may not be precise
|
||||
lyricRef.current = lyrics;
|
||||
|
||||
if (status === PlayerStatus.PLAYING) {
|
||||
let rejected = false;
|
||||
|
||||
getCurrentTime()
|
||||
.then((timeInSec: number) => {
|
||||
if (rejected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setCurrentLyric(timeInSec * 1000 - delayMsRef.current);
|
||||
|
||||
return true;
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
return () => {
|
||||
// Case 1: cleanup happens before we hear back from
|
||||
// the main process. In this case, when the promise resolves, ignore the result
|
||||
rejected = true;
|
||||
|
||||
// Case 2: Cleanup happens after we hear back from main process but
|
||||
// (potentially) before the next lyric. In this case, clear the timer.
|
||||
// Do NOT do this for other cleanup functions, as it should only be done
|
||||
// when switching to a new song (or an empty one)
|
||||
if (lyricTimer.current) clearTimeout(lyricTimer.current);
|
||||
};
|
||||
}
|
||||
|
||||
return () => {};
|
||||
}, [getCurrentTime, lyrics, playerType, setCurrentLyric, status]);
|
||||
|
||||
useEffect(() => {
|
||||
// This handler is used to deal with changes to the current delay. If the offset
|
||||
// changes, we should immediately stop the current listening set and calculate
|
||||
// the correct one using the new offset. Afterwards, timing can be calculated like normal
|
||||
const changed = delayMsRef.current !== settings.delayMs;
|
||||
|
||||
if (!changed) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
if (lyricTimer.current) {
|
||||
clearTimeout(lyricTimer.current);
|
||||
}
|
||||
|
||||
let rejected = false;
|
||||
|
||||
delayMsRef.current = settings.delayMs;
|
||||
|
||||
getCurrentTime()
|
||||
.then((timeInSec: number) => {
|
||||
if (rejected) {
|
||||
return false;
|
||||
return activeLyrics.length - 1;
|
||||
}
|
||||
|
||||
setCurrentLyric(timeInSec * 1000 - delayMsRef.current);
|
||||
|
||||
return true;
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
return () => {
|
||||
// In the event this ends earlier, just kill the promise. Cleanup of
|
||||
// timeouts is otherwise handled by another handler
|
||||
rejected = true;
|
||||
return -1;
|
||||
};
|
||||
}, [getCurrentTime, setCurrentLyric, settings.delayMs]);
|
||||
|
||||
useEffect(() => {
|
||||
// This handler is used specifically for dealing with seeking. In this case,
|
||||
// we assume that now is the accurate time
|
||||
if (status !== PlayerStatus.PLAYING) {
|
||||
if (lyricTimer.current) {
|
||||
clearTimeout(lyricTimer.current);
|
||||
}
|
||||
const getCurrentTime = useCallback(async () => {
|
||||
if (isElectron() && playerType !== PlaybackType.WEB) {
|
||||
if (mpvPlayer) {
|
||||
return mpvPlayer.getCurrentTime();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
if (!seeked) {
|
||||
return;
|
||||
}
|
||||
if (playersRef.current === undefined) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (lyricTimer.current) {
|
||||
clearTimeout(lyricTimer.current);
|
||||
}
|
||||
const player = (
|
||||
playersRef.current.player1 ?? playersRef.current.player2
|
||||
).getInternalPlayer();
|
||||
|
||||
setCurrentLyric(now * 1000 - delayMsRef.current);
|
||||
}, [now, seeked, setCurrentLyric, status]);
|
||||
// If it is null, this probably means we added a new song while the lyrics tab is open
|
||||
// and the queue was previously empty
|
||||
if (!player) return 0;
|
||||
|
||||
useEffect(() => {
|
||||
// Guaranteed cleanup; stop the timer, and just in case also increment
|
||||
// the epoch to instruct any dangling timers to stop
|
||||
if (lyricTimer.current) {
|
||||
clearTimeout(lyricTimer.current);
|
||||
}
|
||||
return player.currentTime;
|
||||
}, [playerType, playersRef]);
|
||||
|
||||
timerEpoch.current += 1;
|
||||
}, []);
|
||||
const setCurrentLyric = useCallback(
|
||||
(timeInMs: number, epoch?: number, targetIndex?: number) => {
|
||||
const start = performance.now();
|
||||
let nextEpoch: number;
|
||||
|
||||
const hideScrollbar = () => {
|
||||
const doc = document.getElementById('sychronized-lyrics-scroll-container') as HTMLElement;
|
||||
doc.classList.add('hide-scrollbar');
|
||||
};
|
||||
if (epoch === undefined) {
|
||||
timerEpoch.current = (timerEpoch.current + 1) % 10000;
|
||||
nextEpoch = timerEpoch.current;
|
||||
} else if (epoch !== timerEpoch.current) {
|
||||
return;
|
||||
} else {
|
||||
nextEpoch = epoch;
|
||||
}
|
||||
|
||||
const showScrollbar = () => {
|
||||
const doc = document.getElementById('sychronized-lyrics-scroll-container') as HTMLElement;
|
||||
doc.classList.remove('hide-scrollbar');
|
||||
};
|
||||
let index: number;
|
||||
|
||||
return (
|
||||
<SynchronizedLyricsContainer
|
||||
className="synchronized-lyrics overlay-scrollbar"
|
||||
id="sychronized-lyrics-scroll-container"
|
||||
onMouseEnter={showScrollbar}
|
||||
onMouseLeave={hideScrollbar}
|
||||
>
|
||||
{source && (
|
||||
<LyricLine
|
||||
className="lyric-credit"
|
||||
text={`Provided by ${source}`}
|
||||
/>
|
||||
)}
|
||||
{remote && (
|
||||
<LyricLine
|
||||
className="lyric-credit"
|
||||
text={`"${name} by ${artist}"`}
|
||||
/>
|
||||
)}
|
||||
{lyrics.map(([, text], idx) => (
|
||||
<LyricLine
|
||||
key={idx}
|
||||
className="lyric-line synchronized"
|
||||
id={`lyric-${idx}`}
|
||||
text={text}
|
||||
/>
|
||||
))}
|
||||
</SynchronizedLyricsContainer>
|
||||
);
|
||||
if (targetIndex === undefined) {
|
||||
index = getCurrentLyric(timeInMs);
|
||||
} else {
|
||||
index = targetIndex;
|
||||
}
|
||||
|
||||
// Directly modify the dom instead of using react to prevent rerender
|
||||
document
|
||||
.querySelectorAll('.synchronized-lyrics .active')
|
||||
.forEach((node) => node.classList.remove('active'));
|
||||
|
||||
if (index === -1) {
|
||||
lyricRef.current = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = document.getElementById(
|
||||
'sychronized-lyrics-scroll-container',
|
||||
) as HTMLElement;
|
||||
const currentLyric = document.querySelector(`#lyric-${index}`) as HTMLElement;
|
||||
const offsetTop = currentLyric?.offsetTop - doc?.clientHeight / 2 ?? 0;
|
||||
|
||||
if (currentLyric === null) {
|
||||
lyricRef.current = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
currentLyric.classList.add('active');
|
||||
|
||||
if (followRef.current) {
|
||||
doc?.scroll({ behavior: 'smooth', top: offsetTop });
|
||||
}
|
||||
|
||||
if (index !== lyricRef.current!.length - 1) {
|
||||
const nextTime = lyricRef.current![index + 1][0];
|
||||
|
||||
const elapsed = performance.now() - start;
|
||||
|
||||
lyricTimer.current = setTimeout(() => {
|
||||
setCurrentLyric(nextTime, nextEpoch, index + 1);
|
||||
}, nextTime - timeInMs - elapsed);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Copy the follow settings into a ref that can be accessed in the timeout
|
||||
followRef.current = settings.follow;
|
||||
}, [settings.follow]);
|
||||
|
||||
useEffect(() => {
|
||||
// This handler is used to handle when lyrics change. It is in some sense the
|
||||
// 'primary' handler for parsing lyrics, as unlike the other callbacks, it will
|
||||
// ALSO remove listeners on close. Use the promisified getCurrentTime(), because
|
||||
// we don't want to be dependent on npw, which may not be precise
|
||||
lyricRef.current = lyrics;
|
||||
|
||||
if (status === PlayerStatus.PLAYING) {
|
||||
let rejected = false;
|
||||
|
||||
getCurrentTime()
|
||||
.then((timeInSec: number) => {
|
||||
if (rejected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setCurrentLyric(timeInSec * 1000 - delayMsRef.current);
|
||||
|
||||
return true;
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
return () => {
|
||||
// Case 1: cleanup happens before we hear back from
|
||||
// the main process. In this case, when the promise resolves, ignore the result
|
||||
rejected = true;
|
||||
|
||||
// Case 2: Cleanup happens after we hear back from main process but
|
||||
// (potentially) before the next lyric. In this case, clear the timer.
|
||||
// Do NOT do this for other cleanup functions, as it should only be done
|
||||
// when switching to a new song (or an empty one)
|
||||
if (lyricTimer.current) clearTimeout(lyricTimer.current);
|
||||
};
|
||||
}
|
||||
|
||||
return () => {};
|
||||
}, [getCurrentTime, lyrics, playerType, setCurrentLyric, status]);
|
||||
|
||||
useEffect(() => {
|
||||
// This handler is used to deal with changes to the current delay. If the offset
|
||||
// changes, we should immediately stop the current listening set and calculate
|
||||
// the correct one using the new offset. Afterwards, timing can be calculated like normal
|
||||
const changed = delayMsRef.current !== settings.delayMs;
|
||||
|
||||
if (!changed) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
if (lyricTimer.current) {
|
||||
clearTimeout(lyricTimer.current);
|
||||
}
|
||||
|
||||
let rejected = false;
|
||||
|
||||
delayMsRef.current = settings.delayMs;
|
||||
|
||||
getCurrentTime()
|
||||
.then((timeInSec: number) => {
|
||||
if (rejected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setCurrentLyric(timeInSec * 1000 - delayMsRef.current);
|
||||
|
||||
return true;
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
return () => {
|
||||
// In the event this ends earlier, just kill the promise. Cleanup of
|
||||
// timeouts is otherwise handled by another handler
|
||||
rejected = true;
|
||||
};
|
||||
}, [getCurrentTime, setCurrentLyric, settings.delayMs]);
|
||||
|
||||
useEffect(() => {
|
||||
// This handler is used specifically for dealing with seeking. In this case,
|
||||
// we assume that now is the accurate time
|
||||
if (status !== PlayerStatus.PLAYING) {
|
||||
if (lyricTimer.current) {
|
||||
clearTimeout(lyricTimer.current);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
if (!seeked) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lyricTimer.current) {
|
||||
clearTimeout(lyricTimer.current);
|
||||
}
|
||||
|
||||
setCurrentLyric(now * 1000 - delayMsRef.current);
|
||||
}, [now, seeked, setCurrentLyric, status]);
|
||||
|
||||
useEffect(() => {
|
||||
// Guaranteed cleanup; stop the timer, and just in case also increment
|
||||
// the epoch to instruct any dangling timers to stop
|
||||
if (lyricTimer.current) {
|
||||
clearTimeout(lyricTimer.current);
|
||||
}
|
||||
|
||||
timerEpoch.current += 1;
|
||||
}, []);
|
||||
|
||||
const hideScrollbar = () => {
|
||||
const doc = document.getElementById('sychronized-lyrics-scroll-container') as HTMLElement;
|
||||
doc.classList.add('hide-scrollbar');
|
||||
};
|
||||
|
||||
const showScrollbar = () => {
|
||||
const doc = document.getElementById('sychronized-lyrics-scroll-container') as HTMLElement;
|
||||
doc.classList.remove('hide-scrollbar');
|
||||
};
|
||||
|
||||
return (
|
||||
<SynchronizedLyricsContainer
|
||||
className="synchronized-lyrics overlay-scrollbar"
|
||||
id="sychronized-lyrics-scroll-container"
|
||||
onMouseEnter={showScrollbar}
|
||||
onMouseLeave={hideScrollbar}
|
||||
>
|
||||
{source && (
|
||||
<LyricLine
|
||||
className="lyric-credit"
|
||||
text={`Provided by ${source}`}
|
||||
/>
|
||||
)}
|
||||
{remote && (
|
||||
<LyricLine
|
||||
className="lyric-credit"
|
||||
text={`"${name} by ${artist}"`}
|
||||
/>
|
||||
)}
|
||||
{lyrics.map(([, text], idx) => (
|
||||
<LyricLine
|
||||
key={idx}
|
||||
className="lyric-line synchronized"
|
||||
id={`lyric-${idx}`}
|
||||
text={text}
|
||||
/>
|
||||
))}
|
||||
</SynchronizedLyricsContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue