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,76 @@
## 0.5.1 (2026-05-17)
### Patch Changes
- **fix:** 修复含对唱时歌词错误提前导致的多行高亮 ([#521](https://github.com/amll-dev/applemusic-like-lyrics/pull/521))
- **refactor:** 引入歌词组来包装主歌词和背景人声 & 前置背景人声 ([#531](https://github.com/amll-dev/applemusic-like-lyrics/pull/531))
- **chore:** 更正 package.json 协议声明 ([#534](https://github.com/amll-dev/applemusic-like-lyrics/pull/534))
仓库根目录的 LICENSE 文件为 AGPL v3.0 协议,但是 package.json 中的 `license` 字段为 `GPL-3.0`。经与原开发者确认package.json 中的 `license` 字段有误。仓库与其所有产出的 npm 包均应为 AGPL v3 only 协议SPDX: `AGPL-3.0-only`。因此,更正各包 `package.json``license` 字段为 `AGPL-3.0-only`
- **chore(core):** 优化类型定义 ([#519](https://github.com/amll-dev/applemusic-like-lyrics/pull/519))
### Contributors
- apoint123 [@apoint123](https://github.com/apoint123)
- Linho [@Linho1219](https://github.com/Linho1219)
## 0.5.0 (2026-05-12)
### Minor Changes
- **refactor:** 整理核心播放器代码结构,将抽象接口部分集中到统一目录 ([#508](https://github.com/amll-dev/applemusic-like-lyrics/pull/508))
- **refactor:** 整理核心播放器抽象类中时间线、滚动与单行布局部分的结构与状态管理 ([#509](https://github.com/amll-dev/applemusic-like-lyrics/pull/509))
### Patch Changes
- **fix:** 修复 setCurrentTime 在提供 isSeek 标志时,实际排版未遵守标志导致布局异常漂移的问题 ([#509](https://github.com/amll-dev/applemusic-like-lyrics/pull/509))
- **fix:** 修复在同一行时间内拖拽进度条时逐字动画不同步的问题 ([#509](https://github.com/amll-dev/applemusic-like-lyrics/pull/509))
- **fix:** 修复暂停状态下点击行跳转时仍播放逐字动画的问题 ([#509](https://github.com/amll-dev/applemusic-like-lyrics/pull/509))
### Contributors
- Linho [@Linho1219](https://github.com/Linho1219)
## 0.4.2 (2026-05-01)
### Patch Changes
- **feat(core):** 平衡行长度时优先在标点处换行 ([#503](https://github.com/amll-dev/applemusic-like-lyrics/pull/503))
- **fix:** 修复背景行注音高度错误 ([#497](https://github.com/amll-dev/applemusic-like-lyrics/pull/497))
- **fix(core):** 修正平衡行长度时的行宽度计算 ([#502](https://github.com/amll-dev/applemusic-like-lyrics/pull/502))
### Contributors
- apoint123 [@apoint123](https://github.com/apoint123)
- Linho [@Linho1219](https://github.com/Linho1219)
## 0.4.1 (2026-04-23)
### Patch Changes
- **fix:** 在各绑定中暴露歌词优化选项 ([#492](https://github.com/amll-dev/applemusic-like-lyrics/pull/492))
- **fix(vue):** 修复掩码模式错误的类型 ([#496](https://github.com/amll-dev/applemusic-like-lyrics/pull/496))
- **refactor(core):** 重构平均行长度实现 ([#494](https://github.com/amll-dev/applemusic-like-lyrics/pull/494))
### Contributors
- apoint123 [@apoint123](https://github.com/apoint123)
## 0.4.0 (2026-04-14)
### Minor Changes
- **chore:** 移除 canvas 歌词渲染器 ([#476](https://github.com/amll-dev/applemusic-like-lyrics/pull/476))
### Patch Changes
- **refactor:** 重构核心库测试组织模式 ([3db83c93](https://github.com/amll-dev/applemusic-like-lyrics/commit/3db83c93))
- **docs:** 修正 optimize-lyric.ts 和 OptimizeLyricOptions 里 cleanUnintentionalOverlaps 的文档和注释 ([75a8c0bb](https://github.com/amll-dev/applemusic-like-lyrics/commit/75a8c0bb))
- **chore:** 更换工具链 ([#476](https://github.com/amll-dev/applemusic-like-lyrics/pull/476))
- **chore:** 在项目范围内启用 isolatedDeclarations ([#480](https://github.com/amll-dev/applemusic-like-lyrics/pull/480))
### Contributors
- apoint123 [@apoint123](https://github.com/apoint123)
- Linho [@Linho1219](https://github.com/Linho1219)
- MoYingJi [@MoYingJi](https://github.com/MoYingJi)

View File

@@ -0,0 +1,49 @@
# AMLL for React
[English](./README.md) / 简体中文
> 警告:此为个人项目,且尚未完成开发,可能仍有大量问题,所以请勿直接用于生产环境!
![AMLL-React](https://img.shields.io/badge/React-%23149eca?label=Apple%20Music-like%20Lyrics&labelColor=%23FB5C74)
[![npm](https://img.shields.io/npm/dt/%40applemusic-like-lyrics/react)](https://www.npmjs.com/package/@applemusic-like-lyrics/react)
[![npm](https://img.shields.io/npm/v/%40applemusic-like-lyrics%2Freact)](https://www.npmjs.com/package/@applemusic-like-lyrics/react)
AMLL 组件库的 React 绑定,你可以通过此库来更加方便地使用 AMLL 歌词组件。
详情可以访问 [Core 核心组件的 README.md](../core/README-CN.md)。
## 安装
安装使用的依赖(如果以下列出的依赖包没有安装的话需要自行安装):
```bash
npm install @pixi/app @pixi/core @pixi/display @pixi/filter-blur @pixi/filter-bulge-pinch @pixi/filter-color-matrix @pixi/sprite jss jss-preset-default # 使用 npm
yarn add @pixi/app @pixi/core @pixi/display @pixi/filter-blur @pixi/filter-bulge-pinch @pixi/filter-color-matrix @pixi/sprite jss jss-preset-default # 使用 yarn
```
安装 React 绑定需要使用的依赖(如果以下列出的依赖包没有安装的话需要自行安装):
```bash
npm install react react-dom # 使用 npm
yarn add react react-dom # 使用 yarn
```
安装本体框架:
```bash
npm install @applemusic-like-lyrics/react # 使用 npm
yarn add @applemusic-like-lyrics/react # 使用 yarn
```
## 使用方式摘要
详细的 API 文档请参考 [./docs/modules.md](./docs/modules.md)
一个测试用途的程序可以在 [../playground/react/src/test.tsx](../playground/react/src/test.tsx) 里找到。
```tsx
import { LyricPlayer } from "@applemusic-like-lyrics/react";
const App = () => {
const [currentTime, setCurrentTime] = useState(0);
const [lyricLines, setLyricLines] = useState<LyricLine[]>([]);
return <LyricPlayer lyricLines={lyricLines} currentTime={currentTime} />
};
```

View File

@@ -0,0 +1,49 @@
# AMLL for React
English / [简体中文](./README-CN.md)
> Warning: This is a personal project and is still under development. There may still be many issues, so please do not use it directly in production environments!
![AMLL-React](https://img.shields.io/badge/React-%23149eca?label=Apple%20Music-like%20Lyrics&labelColor=%23FB5C74)
[![npm](https://img.shields.io/npm/dt/%40applemusic-like-lyrics/react)](https://www.npmjs.com/package/@applemusic-like-lyrics/react)
[![npm](https://img.shields.io/npm/v/%40applemusic-like-lyrics%2Freact)](https://www.npmjs.com/package/@applemusic-like-lyrics/react)
React binding for the AMLL component library, which allows you to use AMLL lyric components more conveniently.
For more details, please visit [Core component README.md](../core/README.md).
## Installation
Install the required dependencies (if the dependencies listed below are not installed, you need to install them yourself):
```bash
npm install @pixi/app @pixi/core @pixi/display @pixi/filter-blur @pixi/filter-bulge-pinch @pixi/filter-color-matrix @pixi/sprite jss jss-preset-default # using npm
yarn add @pixi/app @pixi/core @pixi/display @pixi/filter-blur @pixi/filter-bulge-pinch @pixi/filter-color-matrix @pixi/sprite jss jss-preset-default # using yarn
```
Install the dependencies required for React binding (if the dependencies listed below are not installed, you need to install them yourself):
```bash
npm install react react-dom # using npm
yarn add react react-dom # using yarn
```
Install the framework:
```bash
npm install @applemusic-like-lyrics/react # using npm
yarn add @applemusic-like-lyrics/react # using yarn
```
## Usage Summary
For detailed API documentation, please refer to [./docs/modules.md](./docs/modules.md)
A test program can be found in [../playground/react/src/test.tsx](../playground/react/src/test.tsx).
```tsx
import { LyricPlayer } from "@applemusic-like-lyrics/react";
const App = () => {
const [currentTime, setCurrentTime] = useState(0);
const [lyricLines, setLyricLines] = useState<LyricLine[]>([]);
return <LyricPlayer lyricLines={lyricLines} currentTime={currentTime} />
};
```

View File

@@ -0,0 +1,73 @@
{
"name": "@applemusic-like-lyrics/react",
"version": "0.5.1",
"description": "AMLL 组件库的 React 绑定",
"repository": {
"url": "https://github.com/amll-dev/applemusic-like-lyrics.git",
"directory": "packages/react",
"type": "git"
},
"license": "AGPL-3.0-only",
"nx": {
"tags": [
"library"
],
"targets": {
"nx-release-publish": {
"executor": "@nx/js:release-publish",
"dependsOn": []
}
}
},
"type": "module",
"files": [
"dist"
],
"scripts": {
"build:docs": "typedoc --plugin typedoc-plugin-markdown --out docs src/index.ts",
"typecheck": "tsgo -b",
"build-only": "tsdown",
"build": "run-p typecheck \"build-only {@}\" --",
"build:dev": "tsdown",
"fmt": "biome format --write ./src",
"dev": "nx run @applemusic-like-lyrics/playground-react:dev"
},
"main": "./dist/amll-react.cjs",
"module": "./dist/amll-react.mjs",
"typings": "./dist/amll-react.d.mts",
"exports": {
".": {
"import": {
"types": "./dist/amll-react.d.mts",
"default": "./dist/amll-react.mjs"
},
"require": {
"types": "./dist/amll-react.d.cts",
"default": "./dist/amll-react.cjs"
}
}
},
"devDependencies": {
"@biomejs/biome": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"tsdown": "catalog:",
"typedoc": "catalog:",
"typedoc-plugin-markdown": "catalog:"
},
"dependencies": {
"@applemusic-like-lyrics/core": "workspace:^"
},
"peerDependencies": {
"@pixi/app": "*",
"@pixi/core": "*",
"@pixi/display": "*",
"@pixi/filter-blur": "*",
"@pixi/filter-color-matrix": "*",
"@pixi/sprite": "*",
"react": "*",
"react-dom": "*"
}
}

View File

@@ -0,0 +1,219 @@
import {
type AbstractBaseRenderer,
type BaseRenderer,
BackgroundRender as CoreBackgroundRender,
MeshGradientRenderer,
} from "@applemusic-like-lyrics/core";
import {
type ForwardRefExoticComponent,
forwardRef,
type HTMLProps,
type RefAttributes,
useEffect,
useImperativeHandle,
useRef,
} from "react";
export {
BaseRenderer,
MeshGradientRenderer,
PixiRenderer,
} from "@applemusic-like-lyrics/core";
/**
* 背景渲染组件的属性
*/
export interface BackgroundRenderProps {
/**
* 设置背景专辑资源
*/
album?: string | HTMLImageElement | HTMLVideoElement;
/**
* 设置专辑资源是否为视频
*/
albumIsVideo?: boolean;
/**
* 设置当前背景动画帧率,如果为 `undefined` 则默认为 `30`
*/
fps?: number;
/**
* 设置当前播放状态,如果为 `undefined` 则默认为 `true`
*/
playing?: boolean;
/**
* 设置当前动画流动速度,如果为 `undefined` 则默认为 `2`
*/
flowSpeed?: number;
/**
* 设置背景是否根据“是否有歌词”这个特征调整自身效果,例如有歌词时会变得更加活跃
*
* 部分渲染器会根据这个特征调整自身效果
*
* 如果不确定是否需要赋值或无法知晓是否包含歌词,请传入 true 或不做任何处理(默认值为 true
*/
hasLyric?: boolean;
/**
* 设置低频的音量大小,范围在 80hz-120hz 之间为宜,取值范围在 [0.0-1.0] 之间
*
* 部分渲染器会根据音量大小调整背景效果(例如根据鼓点跳动)
*
* 如果无法获取到类似的数据,请传入 undefined 或 1.0 作为默认值,或不做任何处理(默认值即 1.0
*/
lowFreqVolume?: number;
/**
* 设置当前渲染缩放比例,如果为 `undefined` 则默认为 `0.5`
*/
renderScale?: number;
/**
* 是否启用静态模式,即图片在更换后就会保持静止状态并禁用更新,以节省性能
* 默认为 `false`
*/
staticMode?: boolean;
/**
* 设置渲染器,如果为 `undefined` 则默认为 `PixiRenderer`
* 默认渲染器有可能会随着版本更新而更换
*/
renderer?: {
new (...args: ConstructorParameters<typeof BaseRenderer>): BaseRenderer;
};
}
/**
* 背景渲染组件的引用
*/
export interface BackgroundRenderRef {
/**
* 背景渲染实例引用
*/
bgRender?: AbstractBaseRenderer;
/**
* 将背景渲染实例的元素包裹起来的 DIV 元素实例
*/
wrapperEl: HTMLDivElement | null;
}
/**
* 流体背景渲染组件,通过提供图片链接可以显示出酷似 Apple Music 的流体背景效果
*/
export const BackgroundRender: ForwardRefExoticComponent<
Omit<HTMLProps<HTMLDivElement> & BackgroundRenderProps, "ref"> &
RefAttributes<BackgroundRenderRef>
> = forwardRef<
BackgroundRenderRef,
HTMLProps<HTMLDivElement> & BackgroundRenderProps
>(
(
{
album,
albumIsVideo,
fps,
playing,
flowSpeed,
renderScale,
staticMode,
lowFreqVolume,
hasLyric,
renderer,
style,
...props
},
ref,
) => {
const coreBGRenderRef = useRef<AbstractBaseRenderer>(null);
const wrapperRef = useRef<HTMLDivElement>(null);
const lastRendererRef = useRef<{
new (canvas: HTMLCanvasElement): BaseRenderer;
}>(null);
const curRenderer = renderer ?? MeshGradientRenderer;
useEffect(() => {
if (
lastRendererRef.current !== curRenderer ||
coreBGRenderRef.current === undefined
) {
lastRendererRef.current = curRenderer;
coreBGRenderRef.current?.dispose();
coreBGRenderRef.current = CoreBackgroundRender.new(curRenderer);
}
}, [curRenderer]);
useEffect(() => {
if (curRenderer && album)
coreBGRenderRef.current?.setAlbum(album, albumIsVideo);
}, [curRenderer, album, albumIsVideo]);
useEffect(() => {
if (curRenderer && fps) coreBGRenderRef.current?.setFPS(fps);
}, [curRenderer, fps]);
useEffect(() => {
if (!curRenderer) return;
if (playing === undefined) {
coreBGRenderRef.current?.resume();
} else if (playing) {
coreBGRenderRef.current?.resume();
} else {
coreBGRenderRef.current?.pause();
}
}, [curRenderer, playing]);
useEffect(() => {
if (!curRenderer) return;
if (flowSpeed) coreBGRenderRef.current?.setFlowSpeed(flowSpeed);
}, [curRenderer, flowSpeed]);
useEffect(() => {
if (!curRenderer) return;
coreBGRenderRef.current?.setStaticMode(staticMode ?? false);
}, [curRenderer, staticMode]);
useEffect(() => {
if (curRenderer && renderScale)
coreBGRenderRef.current?.setRenderScale(renderScale ?? 0.5);
}, [curRenderer, renderScale]);
useEffect(() => {
if (curRenderer && lowFreqVolume)
coreBGRenderRef.current?.setLowFreqVolume(lowFreqVolume ?? 1.0);
}, [curRenderer, lowFreqVolume]);
useEffect(() => {
if (curRenderer && hasLyric !== undefined)
coreBGRenderRef.current?.setHasLyric(hasLyric ?? true);
}, [curRenderer, hasLyric]);
// biome-ignore lint/correctness/useExhaustiveDependencies: coreBGRenderRef.current
useEffect(() => {
if (coreBGRenderRef.current) {
const el = coreBGRenderRef.current.getElement();
el.style.width = "100%";
el.style.height = "100%";
el.style.minHeight = "0";
el.style.minWidth = "0";
el.style.overflow = "hidden";
wrapperRef.current?.appendChild(el);
}
}, [coreBGRenderRef.current]);
// biome-ignore lint/correctness/useExhaustiveDependencies: wrapperRef.current, coreBGRenderRef.current
useImperativeHandle(
ref,
() => ({
wrapperEl: wrapperRef.current,
bgRender: coreBGRenderRef.current as AbstractBaseRenderer,
}),
[wrapperRef.current, coreBGRenderRef.current],
);
return (
<div
style={{
display: "contents",
...style,
}}
{...props}
ref={wrapperRef}
/>
);
},
);

View File

@@ -0,0 +1,2 @@
export * from "./bg-render";
export * from "./lyric-player";

View File

@@ -0,0 +1,385 @@
import type {
LyricLine,
LyricLineMouseEvent,
LyricPlayerBase,
OptimizeLyricOptions,
spring,
} from "@applemusic-like-lyrics/core";
import {
LyricPlayer as DefaultLyricPlayer,
MaskObsceneWordsMode,
} from "@applemusic-like-lyrics/core";
import {
type ForwardRefExoticComponent,
forwardRef,
type HTMLProps,
type RefAttributes,
useEffect,
useImperativeHandle,
useLayoutEffect,
useRef,
useState,
} from "react";
import { createPortal } from "react-dom";
/**
* 歌词播放组件的属性
*/
export interface LyricPlayerProps {
/**
* 是否禁用歌词播放组件,默认为 `false`,歌词组件启用后将会开始逐帧更新歌词的动画效果,并对传入的其他参数变更做出反馈。
*
* 如果禁用了歌词组件动画,你也可以通过引用取得原始渲染组件实例,手动逐帧调用其 `update` 函数来更新动画效果。
*/
disabled?: boolean;
/**
* 是否演出部分效果,目前会控制播放间奏点的动画的播放暂停与否,默认为 `true`
*/
playing?: boolean;
/**
* 设置歌词行的对齐方式,如果为 `undefined` 则默认为 `center`
*
* - 设置成 `top` 的话将会向目标歌词行的顶部对齐
* - 设置成 `bottom` 的话将会向目标歌词行的底部对齐
* - 设置成 `center` 的话将会向目标歌词行的垂直中心对齐
*/
alignAnchor?: "top" | "bottom" | "center";
/**
* 设置默认的歌词行对齐位置,相对于整个歌词播放组件的大小位置,如果为 `undefined`
* 则默认为 `0.5`
*
* 可以设置一个 `[0.0-1.0]` 之间的任意数字,代表组件高度由上到下的比例位置
*/
alignPosition?: number;
/**
* 设置是否使用物理弹簧算法实现歌词动画效果,默认启用
*
* 如果启用,则会通过弹簧算法实时处理歌词位置,但是需要性能足够强劲的电脑方可流畅运行
*
* 如果不启用,则会回退到基于 `transition` 的过渡效果,对低性能的机器比较友好,但是效果会比较单一
*/
enableSpring?: boolean;
/**
* 设置是否启用歌词行的模糊效果,默认为 `true`
*/
enableBlur?: boolean;
/**
* 设置是否使用物理弹簧算法实现歌词动画效果,默认启用
*
* 如果启用,则会通过弹簧算法实时处理歌词位置,但是需要性能足够强劲的电脑方可流畅运行
*
* 如果不启用,则会回退到基于 `transition` 的过渡效果,对低性能的机器比较友好,但是效果会比较单一
*/
enableScale?: boolean;
/**
* 设置是否隐藏已经播放过的歌词行,默认不隐藏
*/
hidePassedLines?: boolean;
/**
* 设置歌词中不雅用语的掩码模式,默认为 `MaskObsceneWordsMode.Disabled`,即不掩码
*/
maskObsceneWordsMode?: MaskObsceneWordsMode;
/**
* 设置不雅用语掩码使用的字符,默认为 `*`
*/
maskObsceneWordChar?: string;
/**
* 设置歌词优化选项
*/
optimizeOptions?: OptimizeLyricOptions;
/**
* 设置当前播放歌词,要注意传入后这个数组内的信息不得修改,否则会发生错误
*/
lyricLines?: LyricLine[];
/**
* 设置当前播放进度,单位为毫秒且**必须是整数**,此时将会更新内部的歌词进度信息
* 内部会根据调用间隔和播放进度自动决定如何滚动和显示歌词,所以这个的调用频率越快越准确越好
*/
currentTime?: number;
isSeeking?: boolean;
/**
* 设置文字动画的渐变宽度,单位以歌词行的主文字字体大小的倍数为单位,默认为 0.5,即一个全角字符的一半宽度
*
* 如果要模拟 Apple Music for Android 的效果,可以设置为 1
*
* 如果要模拟 Apple Music for iPad 的效果,可以设置为 0.5
*
* 如果想要近乎禁用渐变效果,可以设置成非常接近 0 的小数(例如 `0.0001` ),但是**不可以为 0**
*/
wordFadeWidth?: number;
/**
* 设置所有歌词行在横坐标上的弹簧属性,包括重量、弹力和阻力。
*
* @param params 需要设置的弹簧属性,提供的属性将会覆盖原来的属性,未提供的属性将会保持原样
*/
linePosXSpringParams?: Partial<spring.SpringParams>;
/**
* 设置所有歌词行在​纵坐标上的弹簧属性,包括重量、弹力和阻力。
*
* @param params 需要设置的弹簧属性,提供的属性将会覆盖原来的属性,未提供的属性将会保持原样
*/
linePosYSpringParams?: Partial<spring.SpringParams>;
/**
* 设置所有歌词行在​缩放大小上的弹簧属性,包括重量、弹力和阻力。
*
* @param params 需要设置的弹簧属性,提供的属性将会覆盖原来的属性,未提供的属性将会保持原样
*/
lineScaleSpringParams?: Partial<spring.SpringParams>;
/**
* 在一个特殊的底栏元素内加入指定元素,默认是空白的,可以往内部添加任意元素
*
* 这个元素始终在歌词的底部,可以用于显示歌曲创作者等信息
*/
bottomLine?: Parameters<typeof createPortal>[0];
/**
* 需要用于创建歌词播放组件的类实例
*/
lyricPlayer?: {
new (
...args: ConstructorParameters<typeof LyricPlayerBase>
): LyricPlayerBase;
};
/**
* 当某个歌词行被左键点击时触发的事件
* @param line 歌词行的事件对象,可以访问到对应的歌词行信息和歌词行索引
*/
onLyricLineClick?: (line: LyricLineMouseEvent) => void;
/**
* 当某个歌词行被右键点击时触发的事件
* @param line 歌词行的事件对象,可以访问到对应的歌词行信息和歌词行索引
*/
onLyricLineContextMenu?: (line: LyricLineMouseEvent) => void;
}
/**
* 歌词播放组件的引用
*/
export interface LyricPlayerRef {
/**
* 歌词播放实例
*/
lyricPlayer?: LyricPlayerBase;
/**
* 将歌词播放实例的元素包裹起来的 DIV 元素实例
*/
wrapperEl: HTMLDivElement | null;
}
/**
* 歌词播放组件,本框架的核心组件
*
* 尽可能贴切 Apple Music for iPad 的歌词效果设计,且做了力所能及的优化措施
*/
export const LyricPlayer: ForwardRefExoticComponent<
Omit<HTMLProps<HTMLDivElement> & LyricPlayerProps, "ref"> &
RefAttributes<LyricPlayerRef>
> = forwardRef<LyricPlayerRef, HTMLProps<HTMLDivElement> & LyricPlayerProps>(
(
{
disabled,
playing,
alignAnchor,
alignPosition,
enableSpring,
enableBlur,
enableScale,
maskObsceneWordsMode,
maskObsceneWordChar,
hidePassedLines,
optimizeOptions,
lyricLines,
currentTime,
isSeeking,
wordFadeWidth,
linePosXSpringParams,
linePosYSpringParams,
lineScaleSpringParams,
bottomLine,
lyricPlayer,
onLyricLineClick,
onLyricLineContextMenu,
...props
},
ref,
) => {
// const corePlayerRef = useRef<LyricPlayerBase>();
const [corePlayer, setCorePlayer] = useState<LyricPlayerBase>();
const wrapperRef = useRef<HTMLDivElement>(null);
const currentTimeRef = useRef(currentTime);
useLayoutEffect(() => {
const newPlayer = new (lyricPlayer ?? DefaultLyricPlayer)();
setCorePlayer(newPlayer);
wrapperRef.current?.appendChild(newPlayer.getElement());
return () => {
newPlayer?.dispose();
setCorePlayer(undefined);
};
}, [lyricPlayer]);
useLayoutEffect(() => {
if (optimizeOptions !== undefined) {
corePlayer?.setOptimizeOptions(optimizeOptions);
}
if (lyricLines !== undefined) {
corePlayer?.setLyricLines(lyricLines, currentTimeRef.current);
if (currentTimeRef.current !== undefined) {
corePlayer?.setCurrentTime(currentTimeRef.current, true);
}
corePlayer?.update();
} else {
corePlayer?.setLyricLines([]);
corePlayer?.update();
}
}, [corePlayer, lyricLines, optimizeOptions]);
useEffect(() => {
if (!disabled) {
let canceled = false;
let lastTime = -1;
const onFrame = (time: number) => {
if (canceled) return;
if (lastTime === -1) {
lastTime = time;
}
corePlayer?.update(time - lastTime);
lastTime = time;
requestAnimationFrame(onFrame);
};
corePlayer?.calcLayout();
requestAnimationFrame(onFrame);
return () => {
canceled = true;
};
}
return;
}, [corePlayer, disabled]);
useEffect(() => {
if (playing !== undefined) {
if (playing) {
corePlayer?.resume();
} else {
corePlayer?.pause();
}
} else corePlayer?.resume();
}, [corePlayer, playing]);
useEffect(() => {
if (alignAnchor !== undefined) corePlayer?.setAlignAnchor(alignAnchor);
}, [corePlayer, alignAnchor]);
useEffect(() => {
if (hidePassedLines !== undefined)
corePlayer?.setHidePassedLines(hidePassedLines);
}, [corePlayer, hidePassedLines]);
useEffect(() => {
if (alignPosition !== undefined)
corePlayer?.setAlignPosition(alignPosition);
}, [corePlayer, alignPosition]);
useEffect(() => {
if (enableSpring !== undefined) corePlayer?.setEnableSpring(enableSpring);
else corePlayer?.setEnableSpring(true);
}, [corePlayer, enableSpring]);
useEffect(() => {
if (enableScale !== undefined) corePlayer?.setEnableScale(enableScale);
else corePlayer?.setEnableScale(true);
}, [corePlayer, enableScale]);
useEffect(() => {
corePlayer?.setEnableBlur(enableBlur ?? true);
}, [corePlayer, enableBlur]);
useLayoutEffect(() => {
if (currentTime !== undefined) {
corePlayer?.setCurrentTime(currentTime, isSeeking);
currentTimeRef.current = currentTime;
} else {
corePlayer?.setCurrentTime(0);
currentTimeRef.current = 0;
}
}, [corePlayer, currentTime, isSeeking]);
useEffect(() => {
corePlayer?.setIsSeeking(!!isSeeking);
}, [corePlayer, isSeeking]);
useEffect(() => {
corePlayer?.setWordFadeWidth(wordFadeWidth);
}, [corePlayer, wordFadeWidth]);
useEffect(() => {
if (linePosXSpringParams !== undefined)
corePlayer?.setLinePosXSpringParams(linePosXSpringParams);
}, [corePlayer, linePosXSpringParams]);
useEffect(() => {
if (linePosYSpringParams !== undefined)
corePlayer?.setLinePosYSpringParams(linePosYSpringParams);
}, [corePlayer, linePosYSpringParams]);
useEffect(() => {
if (lineScaleSpringParams !== undefined)
corePlayer?.setLineScaleSpringParams(lineScaleSpringParams);
}, [corePlayer, lineScaleSpringParams]);
useEffect(() => {
if (maskObsceneWordsMode !== undefined) {
corePlayer?.setMaskObsceneWords(maskObsceneWordsMode);
} else {
corePlayer?.setMaskObsceneWords(MaskObsceneWordsMode.Disabled);
}
}, [corePlayer, maskObsceneWordsMode]);
useEffect(() => {
if (maskObsceneWordChar !== undefined) {
corePlayer?.setMaskObsceneWordChar(maskObsceneWordChar);
}
}, [corePlayer, maskObsceneWordChar]);
useEffect(() => {
if (onLyricLineClick) {
const handler = (e: Event) =>
onLyricLineClick(e as LyricLineMouseEvent);
corePlayer?.addEventListener("line-click", handler);
return () => corePlayer?.removeEventListener("line-click", handler);
}
return;
}, [corePlayer, onLyricLineClick]);
useEffect(() => {
if (onLyricLineContextMenu) {
const handler = (e: Event) =>
onLyricLineContextMenu(e as LyricLineMouseEvent);
corePlayer?.addEventListener("line-contextmenu", handler);
return () =>
corePlayer?.removeEventListener("line-contextmenu", handler);
}
return;
}, [corePlayer, onLyricLineContextMenu]);
useImperativeHandle(
ref,
() => ({
wrapperEl: wrapperRef.current,
lyricPlayer: corePlayer,
}),
[corePlayer],
);
return (
<>
<div {...props} ref={wrapperRef} />
{corePlayer?.getBottomLineElement() && bottomLine
? createPortal(bottomLine, corePlayer?.getBottomLineElement())
: null}
</>
);
},
);

View File

@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src"]
}

View File

@@ -0,0 +1,7 @@
import { defineConfig } from "tsdown";
import { baseConfig } from "../../tsdown.base.ts";
export default defineConfig({
...baseConfig,
entry: { "amll-react": "./src/index.ts" },
});