>();
+
+ const estimateElapsedTime = useCallback(() => {
+ const now = new Date().getTime();
+ return (now - previousTimestamp.current) / 1000;
+ }, []);
+
+ const getCurrentLyric = useCallback(
+ (timeInMs: number) => {
+ for (let idx = 0; idx < lyrics.length; idx += 1) {
+ if (timeInMs <= lyrics[idx][0]) {
+ return idx === 0 ? idx : idx - 1;
+ }
+ }
+ return lyrics.length - 1;
+ },
+ [lyrics],
+ );
+
+ const doSetNextTimeout = useCallback(
+ (idx: number, currentTimeMs: number) => {
+ if (timeout.current) {
+ clearTimeout(timeout.current);
+ }
+
+ document
+ .querySelector(`#lyric-${idx}`)
+ ?.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ setIndex(idx);
+
+ if (idx !== lyrics.length - 1) {
+ const nextTimeMs = lyrics[idx + 1][0];
+ const nextTime = nextTimeMs - currentTimeMs;
+
+ timeout.current = setTimeout(() => {
+ doSetNextTimeout(idx + 1, nextTimeMs);
+ }, nextTime);
+ } else {
+ timeout.current = undefined;
+ }
+ },
+ [lyrics],
+ );
+
+ const handleTimeChange = useCallback(() => {
+ const elapsedJs = estimateElapsedTime();
+ const elapsedPlayer = now - lastTimeUpdate.current;
+
+ lastTimeUpdate.current = now;
+ previousTimestamp.current = new Date().getTime();
+
+ if (Math.abs(elapsedJs - elapsedPlayer) >= CLOSE_ENOUGH_TIME_DIFF_SEC) {
+ if (timeout.current) {
+ clearTimeout(timeout.current);
+ }
+
+ const currentTimeMs = now * 1000;
+ const idx = getCurrentLyric(currentTimeMs);
+ doSetNextTimeout(idx, currentTimeMs);
+ }
+ }, [doSetNextTimeout, estimateElapsedTime, getCurrentLyric, now]);
+
+ useEffect(() => {
+ if (status !== PlayerStatus.PLAYING) {
+ if (timeout.current) {
+ clearTimeout(timeout.current);
+ timeout.current = undefined;
+ }
+
+ return () => {};
+ }
+
+ const changeTimeout = setTimeout(() => {
+ handleTimeChange();
+ }, 100);
+
+ return () => clearTimeout(changeTimeout);
+ }, [handleTimeChange, status]);
+
+ return (
+
+ {lyrics.map(([, text], idx) => (
+
+ ))}
+
+ );
+};
diff --git a/src/renderer/features/lyrics/unsynchronized-lyrics.tsx b/src/renderer/features/lyrics/unsynchronized-lyrics.tsx
new file mode 100644
index 00000000..b2783f76
--- /dev/null
+++ b/src/renderer/features/lyrics/unsynchronized-lyrics.tsx
@@ -0,0 +1,25 @@
+import { useMemo } from 'react';
+import { LyricLine } from '/@/renderer/features/lyrics/lyric-line';
+
+interface UnsynchronizedLyricsProps {
+ lyrics: string;
+}
+
+export const UnsynchronizedLyrics = ({ lyrics }: UnsynchronizedLyricsProps) => {
+ const lines = useMemo(() => {
+ return lyrics.split('\n');
+ }, [lyrics]);
+
+ return (
+
+ {lines.map((text, idx) => (
+
+ ))}
+
+ );
+};
diff --git a/src/renderer/features/player/components/full-screen-player-queue.tsx b/src/renderer/features/player/components/full-screen-player-queue.tsx
index 42c438c5..2ab95961 100644
--- a/src/renderer/features/player/components/full-screen-player-queue.tsx
+++ b/src/renderer/features/player/components/full-screen-player-queue.tsx
@@ -9,6 +9,7 @@ import {
useFullScreenPlayerStore,
useFullScreenPlayerStoreActions,
} from '/@/renderer/store/full-screen-player.store';
+import { Lyrics } from '/@/renderer/features/lyrics/lyrics';
const QueueContainer = styled.div`
position: relative;
@@ -26,6 +27,12 @@ const QueueContainer = styled.div`
}
`;
+const LyricsContainer = styled.div`
+ height: 100%;
+ overflow: scroll;
+ text-align: center;
+`;
+
const ActiveTabIndicator = styled(motion.div)`
position: absolute;
bottom: 0;
@@ -115,17 +122,9 @@ export const FullScreenPlayerQueue = () => {
) : activeTab === 'lyrics' ? (
-
-
-
-
- COMING SOON
-
-
-
+
+
+
) : null}
);