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,4 @@
declare module "@radix-ui/themes/styles.css" {
const content: string;
export default content;
}

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<title>AMLL React Wrapper Test</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="src/test.tsx" type="module" defer></script>
<style>
:root {
font-family:
-apple-system, BlinkMacSystemFont, "SF Pro Display", "PingFang SC", system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
</style>
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@@ -0,0 +1,39 @@
{
"name": "@applemusic-like-lyrics/playground-react-full",
"private": true,
"version": "0.0.0",
"type": "module",
"nx": {
"tags": [
"playground"
]
},
"scripts": {
"dev": "vite dev"
},
"devDependencies": {
"@applemusic-like-lyrics/core": "workspace:^",
"@applemusic-like-lyrics/lyric": "workspace:^",
"@applemusic-like-lyrics/react": "workspace:^",
"@applemusic-like-lyrics/react-full": "workspace:^",
"@pixi/app": "^7.4.3",
"@pixi/core": "^7.4.3",
"@pixi/display": "^7.4.3",
"@pixi/filter-blur": "^7.4.3",
"@pixi/filter-color-matrix": "^7.4.3",
"@pixi/sprite": "^7.4.3",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@vitejs/plugin-react": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"vite": "catalog:",
"vite-plugin-svgr": "^5.2.0"
},
"dependencies": {
"@radix-ui/themes": "^3.3.0",
"@types/jsmediatags": "^3.9.6",
"jotai": "^2.19.1",
"jsmediatags": "^3.9.7"
}
}

View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<title>AMLL React Wrapper Test</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root {
font-family:
-apple-system, BlinkMacSystemFont, "SF Pro Display", "PingFang SC", system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
body {
margin: 0;
background: black;
}
.radix-themes {
--default-font-family: "";
--heading-font-family: "";
--code-font-family: "";
--strong-font-family: "";
--em-font-family: "";
--quote-font-family: "";
}
</style>
<script src="src/test-prebuilt.tsx" type="module" defer></script>
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@@ -0,0 +1,245 @@
import { ContextMenu, Theme } from "@radix-ui/themes";
import { Provider, useStore } from "jotai";
import {
type FC,
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import { createRoot } from "react-dom/client";
import { PrebuiltLyricPlayer } from "@react-full/components/PrebuiltLyricPlayer";
import "@radix-ui/themes/styles.css";
import type { LyricLine } from "@applemusic-like-lyrics/core";
import {
parseLrc,
parseLys,
parseQrc,
parseTTML,
parseYrc,
type LyricLine as RawLyricLine,
} from "@applemusic-like-lyrics/lyric";
import { onRequestOpenMenuAtom } from "@react-full/states/callbacks";
import { hideLyricViewAtom } from "@react-full/states/configAtoms";
import {
musicAlbumNameAtom,
musicArtistsAtom,
musicCoverAtom,
musicLyricLinesAtom,
musicNameAtom,
} from "@react-full/states/dataAtoms";
import jsmediatags from "jsmediatags";
const mapLyric = (
line: RawLyricLine,
_i: number,
_lines: RawLyricLine[],
): LyricLine => ({
words: line.words.map((w) => ({ ...w, obscene: false })),
startTime: line.words[0]?.startTime ?? 0,
endTime:
line.words[line.words.length - 1]?.endTime ?? Number.POSITIVE_INFINITY,
translatedLyric: "",
romanLyric: "",
isBG: false,
isDuet: false,
});
const App: FC = () => {
const [hideLyric, setHideLyric] = useState(false);
const audioRef = useRef<HTMLAudioElement>(null);
const store = useStore();
useEffect(() => {
store.set(musicLyricLinesAtom, [
{
words: [
{
word: "Test",
startTime: 0,
endTime: 1000,
romanWord: "",
},
],
startTime: 0,
endTime: 1000,
translatedLyric: "",
romanLyric: "",
isBG: false,
isDuet: false,
},
]);
}, [store]);
useEffect(() => {
store.set(hideLyricViewAtom, hideLyric);
}, [hideLyric, store]);
const onRequestOpenMenu = useCallback(() => {
const inputEl = document.createElement("input");
inputEl.type = "file";
inputEl.accept = "audio/*";
inputEl.onchange = (e) => {
const files = (e.target as HTMLInputElement).files;
if (!files) return;
const file = files[0];
jsmediatags.read(file, {
onSuccess(tag) {
console.log("tag read", tag);
const title: string = tag.tags?.title ?? file.name ?? "未知歌曲";
const album: string = tag.tags?.album ?? "未知专辑";
const artist: string = tag.tags?.artist ?? "未知作者";
// const lyrics: string = tag.tags?.lyrics ?? "";
store.set(musicNameAtom, title);
store.set(musicAlbumNameAtom, album);
store.set(musicArtistsAtom, [{ name: artist, id: "unknown" }]);
if ("picture" in tag.tags && tag.tags.picture) {
const { data, format } = tag.tags.picture;
let base64String = "";
for (let i = 0; i < data.length; i++) {
base64String += String.fromCharCode(data[i]);
}
const imgUrl = `data:${format};base64,${window.btoa(base64String)}`;
store.set(musicCoverAtom, imgUrl);
}
},
onError(error) {
console.log(error);
},
});
};
inputEl.click();
}, [store]);
const loadLyric = useCallback(
(format: "yrc" | "qrc" | "ttml" | "lrc" | "lys" | "") => {
let accept: string;
switch (format) {
case "yrc":
accept = ".yrc";
break;
case "qrc":
accept = ".qrc";
break;
case "ttml":
accept = ".ttml";
break;
case "lrc":
accept = ".lrc";
break;
case "lys":
accept = ".lys";
break;
default:
accept = "";
store.set(musicLyricLinesAtom, []);
return;
}
const inputEl = document.createElement("input");
inputEl.type = "file";
inputEl.accept = accept;
inputEl.onchange = async (e) => {
const files = (e.target as HTMLInputElement).files;
if (!files) return;
const file = files[0];
const raw = await file.text();
let lines: RawLyricLine[];
switch (format) {
case "yrc":
lines = parseYrc(raw);
break;
case "qrc":
lines = parseQrc(raw);
break;
case "ttml":
lines = parseTTML(raw).lines;
break;
case "lrc":
lines = parseLrc(raw);
break;
case "lys":
lines = parseLys(raw);
break;
}
store.set(musicLyricLinesAtom, lines.map(mapLyric));
};
inputEl.click();
},
[store],
);
useLayoutEffect(() => {}, []);
useLayoutEffect(() => {
store.set(onRequestOpenMenuAtom, {
onEmit: onRequestOpenMenu,
});
}, [store, onRequestOpenMenu]);
return (
<>
<audio ref={audioRef} />
<ContextMenu.Root>
<ContextMenu.Trigger>
<PrebuiltLyricPlayer
style={{
position: "fixed",
width: "100%",
maxWidth: "100vw",
overflow: "hidden",
height: "100vh",
backgroundColor: "#222",
}}
/>
</ContextMenu.Trigger>
<ContextMenu.Content size="1">
<ContextMenu.Label>AMLL React </ContextMenu.Label>
<ContextMenu.Item onSelect={onRequestOpenMenu}>
</ContextMenu.Item>
<ContextMenu.CheckboxItem
checked={!hideLyric}
onCheckedChange={(e) => setHideLyric(!e)}
>
</ContextMenu.CheckboxItem>
<ContextMenu.Sub>
<ContextMenu.SubTrigger></ContextMenu.SubTrigger>
<ContextMenu.SubContent>
<ContextMenu.Item onClick={() => loadLyric("")}>
</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item onClick={() => loadLyric("ttml")}>
TTML
</ContextMenu.Item>
<ContextMenu.Item onClick={() => loadLyric("lys")}>
Lyricify Syllable
</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item onClick={() => loadLyric("lrc")}>
LyRiC
</ContextMenu.Item>
<ContextMenu.Item onClick={() => loadLyric("yrc")}>
YRC
</ContextMenu.Item>
<ContextMenu.Item onClick={() => loadLyric("qrc")}>
QRC
</ContextMenu.Item>
</ContextMenu.SubContent>
</ContextMenu.Sub>
</ContextMenu.Content>
</ContextMenu.Root>
</>
);
};
createRoot(document.getElementById("root") as HTMLElement).render(
<Provider>
<Theme appearance="dark">
<App />
</Theme>
</Provider>,
);

View File

@@ -0,0 +1,167 @@
import { Provider, useStore } from "jotai";
import { type FC, useEffect, useState } from "react";
import { createRoot } from "react-dom/client";
import { PrebuiltLyricPlayer } from "@react-full/components/PrebuiltLyricPlayer";
import { HorizontalLayout } from "@react-full/layout/horizontal";
import { VerticalLayout } from "@react-full/layout/vertical";
import { hideLyricViewAtom } from "@react-full/states/configAtoms";
import { musicLyricLinesAtom } from "@react-full/states/dataAtoms";
const App: FC = () => {
const [hideLyric, setHideLyric] = useState(false);
const store = useStore();
useEffect(() => {
store.set(musicLyricLinesAtom, [
{
words: [
{
word: "Test",
startTime: 0,
endTime: 1000,
romanWord: "",
},
],
startTime: 0,
endTime: 1000,
translatedLyric: "",
romanLyric: "",
isBG: false,
isDuet: false,
},
]);
}, [store]);
useEffect(() => {
store.set(hideLyricViewAtom, hideLyric);
}, [hideLyric, store]);
return (
<>
<h1>AMLL React Framework gallery</h1>
<a href="/prebuilt">Go to Prebuilt Player</a>
<h2>Prebuilt Player</h2>
<PrebuiltLyricPlayer
style={{
width: "100%",
maxWidth: "100vw",
overflow: "hidden",
height: "100vh",
backgroundColor: "#222",
}}
/>
<h2>Horizontal Layout</h2>
<label>
<input
type="checkbox"
id="showLyric"
checked={hideLyric}
onChange={(v) => setHideLyric(!!v.target.checked)}
/>
Hide lyric
</label>
<div>{hideLyric ? "Lyric is hidden" : "Lyric is shown"}</div>
<div
style={{
width: "100%",
maxWidth: "100vw",
overflow: "hidden",
height: "80vh",
backgroundColor: "black",
}}
>
<HorizontalLayout
style={{
width: "100%",
height: "100%",
}}
thumbSlot={
<div style={{ background: "red", width: "100%", height: "100%" }}>
Thumb slot
</div>
}
coverSlot={
<div style={{ background: "green", width: "100%", height: "100%" }}>
Cover slot
</div>
}
controlsSlot={
<div
style={{ background: "orange", width: "100%", height: "100%" }}
>
Controls slot
</div>
}
lyricSlot={
<div style={{ background: "pink", width: "100%", height: "100%" }}>
Lyric slot
</div>
}
hideLyric={hideLyric}
/>
</div>
<h2>Vertical Layout</h2>
<label>
<input
type="checkbox"
id="showLyric"
checked={hideLyric}
onChange={(v) => setHideLyric(!!v.target.checked)}
/>
Hide lyric
</label>
<div
style={{
backgroundColor: "black",
width: "40vw",
maxWidth: "800px",
maxHeight: "80vh",
flex: "0",
}}
>
<VerticalLayout
style={{
width: "100%",
height: "100%",
}}
thumbSlot={
<div style={{ background: "red", width: "100%", height: "100%" }}>
Thumb slot
</div>
}
coverSlot={
<div style={{ background: "green", width: "100%", height: "100%" }}>
Cover slot
</div>
}
smallControlsSlot={
<div
style={{ background: "orange", width: "100%", height: "100%" }}
>
Small Controls slot
</div>
}
bigControlsSlot={
<div style={{ background: "aqua", width: "100%", height: "100%" }}>
Big Controls slot
</div>
}
lyricSlot={
<div style={{ background: "pink", width: "100%", height: "100%" }}>
Lyric slot
</div>
}
hideLyric={hideLyric}
/>
</div>
</>
);
};
createRoot(document.getElementById("root") as HTMLElement).render(
<Provider>
<App />
</Provider>,
);

View File

@@ -0,0 +1,9 @@
{
"extends": "../../../tsconfig.base.json",
"include": ["src", "env.d.ts"],
"compilerOptions": {
"paths": {
"@react-full/*": ["../../react-full/src/*"]
}
}
}

View File

@@ -0,0 +1,29 @@
import path from "node:path";
import { defineConfig } from "vite";
import svgr from "vite-plugin-svgr";
export default defineConfig({
resolve: {
alias: {
"@applemusic-like-lyrics/core/style.css": path.resolve(
__dirname,
"../../core/src/styles/index.css",
),
"@applemusic-like-lyrics/react": path.resolve(
__dirname,
"../../react/src",
),
"@applemusic-like-lyrics/core": path.resolve(__dirname, "../../core/src"),
"@applemusic-like-lyrics/lyric": path.resolve(
__dirname,
"../../lyric/src",
),
"@react-full": path.resolve(__dirname, "../../react-full/src"),
},
},
plugins: [
svgr({
svgrOptions: { ref: true },
}),
],
});