forked from miao-moe/QZMusic_PC
fork(fix): Clone AMLL 并修复 BUG
- 将AMLL Clone到本以地进行修复和优化(emm虽然这很不优雅但是暂时无时间做子模块和Fork) - 修复在当前播放歌词行不可见的视口Seek会出现滚动偏移的问题
This commit is contained in:
4
amll-local/packages/playground/react-full/env.d.ts
vendored
Normal file
4
amll-local/packages/playground/react-full/env.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module "@radix-ui/themes/styles.css" {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
19
amll-local/packages/playground/react-full/index.html
Normal file
19
amll-local/packages/playground/react-full/index.html
Normal 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>
|
||||
39
amll-local/packages/playground/react-full/package.json
Normal file
39
amll-local/packages/playground/react-full/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
33
amll-local/packages/playground/react-full/prebuilt.html
Normal file
33
amll-local/packages/playground/react-full/prebuilt.html
Normal 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>
|
||||
245
amll-local/packages/playground/react-full/src/test-prebuilt.tsx
Normal file
245
amll-local/packages/playground/react-full/src/test-prebuilt.tsx
Normal 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>,
|
||||
);
|
||||
167
amll-local/packages/playground/react-full/src/test.tsx
Normal file
167
amll-local/packages/playground/react-full/src/test.tsx
Normal 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>,
|
||||
);
|
||||
9
amll-local/packages/playground/react-full/tsconfig.json
Normal file
9
amll-local/packages/playground/react-full/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"include": ["src", "env.d.ts"],
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@react-full/*": ["../../react-full/src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
29
amll-local/packages/playground/react-full/vite.config.ts
Normal file
29
amll-local/packages/playground/react-full/vite.config.ts
Normal 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 },
|
||||
}),
|
||||
],
|
||||
});
|
||||
Reference in New Issue
Block a user