fork(fix): Clone AMLL 并修复 BUG

- 将AMLL Clone到本以地进行修复和优化(emm虽然这很不优雅但是暂时无时间做子模块和Fork)
- 修复在当前播放歌词行不可见的视口Seek会出现滚动偏移的问题
This commit is contained in:
lqtmcstudio
2026-06-07 00:02:14 +08:00
parent 783d2c3dee
commit 72f4510dc8
458 changed files with 86075 additions and 1665 deletions

View File

@@ -0,0 +1,165 @@
import type { LyricLine } from "@applemusic-like-lyrics/core";
import {
parseTTML,
type LyricLine as RawLyricLine,
} from "@applemusic-like-lyrics/lyric";
import {
BackgroundRender,
LyricPlayer,
type LyricPlayerRef,
} from "@applemusic-like-lyrics/react";
import type { FC } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
const mapTTMLLyric = (line: RawLyricLine): LyricLine => ({
...line,
words: line.words.map((word) => ({ obscene: false, ...word })),
});
export const App: FC = () => {
const audioRef = useRef<HTMLAudioElement>(null);
const [audioUrl, setAudioUrl] = useState("");
const [albumUrl, setAlbumUrl] = useState("");
const [albumIsVideo, setAlbumIsVideo] = useState(false);
const lyricPlayerRef = useRef<LyricPlayerRef>(null);
const [lyricLines, setLyricLines] = useState<LyricLine[]>([]);
const onClickOpenAudio = useCallback(() => {
const input = document.createElement("input");
input.type = "file";
input.accept = "audio/*";
input.onchange = () => {
const file = input.files?.[0];
if (file) {
setAudioUrl((old) => {
if (old.trim().length > 0) {
URL.revokeObjectURL(old);
}
return URL.createObjectURL(file);
});
}
};
input.click();
}, []);
const onClickOpenAlbumImage = useCallback(() => {
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*,video/*";
input.onchange = () => {
const file = input.files?.[0];
if (file) {
setAlbumIsVideo(file.type.startsWith("video/"));
setAlbumUrl((old) => {
if (old.trim().length > 0) {
URL.revokeObjectURL(old);
}
return URL.createObjectURL(file);
});
}
};
input.click();
}, []);
const onClickOpenTTMLLyric = useCallback(() => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".ttml,text/*";
input.onchange = async () => {
const file = input.files?.[0];
if (file) {
const text = await file.text();
setLyricLines(parseTTML(text).lines.map(mapTTMLLyric));
}
};
input.click();
}, []);
useEffect(() => {
if (audioRef.current) {
let lastTime = -1;
const onFrame = (time: number) => {
if (audioRef.current && !audioRef.current.paused) {
if (lastTime === -1) {
lastTime = time;
}
lyricPlayerRef.current?.lyricPlayer?.update(time - lastTime);
lastTime = time;
lyricPlayerRef.current?.lyricPlayer?.setCurrentTime(
(audioRef.current.currentTime * 1000) | 0,
);
requestAnimationFrame(onFrame);
}
};
const onPlay = () => onFrame(0);
audioRef.current.addEventListener("play", onPlay);
return () => {
audioRef.current?.removeEventListener("play", onPlay);
};
}
return;
}, []);
useEffect(() => {
if (lyricPlayerRef.current) {
// biome-ignore lint/suspicious/noExplicitAny: 调试用途,暴露到 Window
(window as any).lyricPlayer = lyricPlayerRef.current;
}
}, []);
return (
<>
<BackgroundRender
style={{
position: "absolute",
top: "0",
left: "0",
width: "100%",
height: "100%",
}}
album={albumUrl}
albumIsVideo={albumIsVideo}
/>
<LyricPlayer
style={{
position: "absolute",
top: "0",
left: "0",
width: "100%",
height: "100%",
maxWidth: "100%",
maxHeight: "100%",
contain: "paint layout",
overflow: "hidden",
mixBlendMode: "plus-lighter",
}}
ref={lyricPlayerRef}
alignAnchor="center"
lyricLines={lyricLines}
/>
<div
style={{
position: "absolute",
right: "0",
bottom: "0",
backgroundColor: "#0004",
margin: "1rem",
padding: "1rem",
borderRadius: "0.5rem",
color: "white",
display: "flex",
flexDirection: "column",
gap: "0.5rem",
}}
>
<div>AMLL React </div>
<div></div>
<div>使 Core </div>
<button type="button" onClick={onClickOpenAudio}>
</button>
<button type="button" onClick={onClickOpenAlbumImage}>
/
</button>
<button type="button" onClick={onClickOpenTTMLLyric}>
</button>
<audio controls ref={audioRef} src={audioUrl} preload="auto" />
</div>
</>
);
};

View File

@@ -0,0 +1,9 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./test-app";
createRoot(document.getElementById("root") as HTMLElement).render(
<StrictMode>
<App />
</StrictMode>,
);