Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6f21871
chore: 引入 @applemusic-like-lyrics 与相关依赖
ITManCHINA Jun 18, 2026
c84fc8d
feat: 实现 cleanTTMLTranslations 用于 TTML 多语言歌词预过滤
ITManCHINA Jun 18, 2026
d68fc4a
refactor: 在 parseContent 中集成 AMLL 解析器适配 TTML 与 YRC
ITManCHINA Jun 18, 2026
0b9d7ec
feat: 新增基于 PixiJS 的流体渐变背景组件 BackgroundRender
ITManCHINA Jun 18, 2026
050c04a
feat: 播放器背景 PlayerBackground 接入流体动画背景支持
ITManCHINA Jun 18, 2026
3e66952
feat: 加入 AMLL 歌词播放组件
ITManCHINA Jun 18, 2026
2bce8df
feat: 全屏播放器 index.vue 支持条件分流渲染歌词引擎
ITManCHINA Jun 18, 2026
6d01f0d
feat: 扩展设置类型与持久化歌词渲染引擎、重构歌词设置,并新增全屏歌词分类
ITManCHINA Jun 18, 2026
29af466
refactor: 重构歌词渲染引擎设置
ITManCHINA Jun 18, 2026
78a2470
chore: 扩展设置类型以支持 AMLL 专属物理回弹与缩放参数
ITManCHINA Jun 18, 2026
e2e951f
feat: 在 setting store 中初始化并持久化 AMLL 弹簧参数
ITManCHINA Jun 18, 2026
792b5f4
feat: 设置项支持 visible 条件渲染
ITManCHINA Jun 18, 2026
8de0fbf
refactor: 重构全屏歌词设置 Schema 使专属选项按引擎显隐且支持嵌套展开
ITManCHINA Jun 18, 2026
f2b73cf
feat: AMLL 适配自适应字号,补充物理弹簧微调参数
ITManCHINA Jun 18, 2026
be1884a
chore: 补全 AMLL 专属弹簧微调设置项的中英文多语言翻译
ITManCHINA Jun 18, 2026
6191bf5
refactor: 补全扁平化 AMLL 弹簧设置分区并补齐翻译
ITManCHINA Jun 18, 2026
d044e61
refactor: 优化缩进与格式化
ITManCHINA Jun 18, 2026
c4e6674
feat: 补充 AMLL 的音译设置
ITManCHINA Jun 18, 2026
f772fa7
feat: 新增 extractLyricAuthors 支持提取多位歌词作者信息
ITManCHINA Jun 19, 2026
dc407d6
feat: media store 新增 lyricAuthors 数组以支持多作者状态管理
ITManCHINA Jun 19, 2026
b123087
feat: 全屏播放器底栏支持使用 v-for 渲染多位作者与超链接
ITManCHINA Jun 19, 2026
431852f
feat: 复用默认歌词组件,为 AMLL 组件支持底栏歌词作者信息
ITManCHINA Jun 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ declare module 'vue' {
AbLoopDialog: typeof import('./src/components/modals/AbLoopDialog.vue')['default']
AboutSettings: typeof import('./src/components/settings/custom/AboutSettings.vue')['default']
AmllDbServerConfig: typeof import('./src/components/settings/custom/AmllDbServerConfig.vue')['default']
AMLLLyrics: typeof import('./src/components/player/Lyrics/AMLLLyrics.vue')['default']
AppBackground: typeof import('./src/components/AppBackground.vue')['default']
AutoCloseDialog: typeof import('./src/components/modals/AutoCloseDialog.vue')['default']
BackgroundImagePicker: typeof import('./src/components/settings/custom/BackgroundImagePicker.vue')['default']
BackgroundRender: typeof import('./src/components/player/FullPlayer/BackgroundRender.vue')['default']
BottomSpectrum: typeof import('./src/components/player/FullPlayer/BottomSpectrum.vue')['default']
ComboboxAnchor: typeof import('reka-ui')['ComboboxAnchor']
ComboboxContent: typeof import('reka-ui')['ComboboxContent']
Expand Down
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,17 @@
"docs:preview": "vitepress preview docs"
},
"dependencies": {
"@applemusic-like-lyrics/core": "^0.5.1",
"@applemusic-like-lyrics/lyric": "^1.0.1",
"@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0",
"@pixi/app": "^7.4.3",
"@pixi/core": "^7.4.3",
"@pixi/display": "^7.4.3",
"@pixi/filter-blur": "^7.4.3",
"@pixi/filter-bulge-pinch": "^5.1.1",
"@pixi/filter-color-matrix": "^7.4.3",
"@pixi/sprite": "^7.4.3",
"@hono/node-server": "^2.0.2",
"@material/material-color-utilities": "^0.4.0",
"@vueuse/core": "^14.2.1",
Expand Down
326 changes: 326 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

229 changes: 229 additions & 0 deletions src/components/player/FullPlayer/BackgroundRender.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
<script setup lang="ts">
import { ref, shallowRef, watch, onMounted, onBeforeUnmount, onUnmounted } from "vue";
import { useRafFn } from "@vueuse/core";
import {
type AbstractBaseRenderer,
type BaseRenderer,
BackgroundRender as CoreBackgroundRender,
MeshGradientRenderer,
} from "@applemusic-like-lyrics/core";
import { getFftFrame } from "@/services/playback";

export interface BackgroundRenderProps {
/** 专辑封面资源 URL */
album?: string;
/** 是否处于播放状态,默认为 true */
playing?: boolean;
/** 动画流动速度,默认为 2 */
flowSpeed?: number;
/** 是否有歌词,默认为 true */
hasLyric?: boolean;
/** 帧率,默认为 30 */
fps?: number;
/** 渲染缩放比例,默认为 0.5 */
renderScale?: number;
/** 渲染器类,默认为 MeshGradientRenderer */
renderer?: new (...args: ConstructorParameters<typeof BaseRenderer>) => BaseRenderer;
}

const props = withDefaults(defineProps<BackgroundRenderProps>(), {
playing: true,
flowSpeed: 2,
hasLyric: true,
fps: 30,
renderScale: 0.5,
renderer: () => MeshGradientRenderer,
});

const wrapperRef = ref<HTMLDivElement | null>(null);

// 外部渲染器实例引用
const bgRenderRef = shallowRef<AbstractBaseRenderer>();

/**
* 统一同步更新属性状态到底层渲染器
*/
const updateRendererState = () => {
const renderer = bgRenderRef.value;
if (!renderer) return;

if (props.album) {
renderer.setAlbum(props.album, false);
}
renderer.setFPS(props.fps);
renderer.setFlowSpeed(props.flowSpeed);
renderer.setRenderScale(props.renderScale);
renderer.setHasLyric(props.hasLyric);

if (props.playing) {
renderer.resume();
} else {
renderer.pause();
}
};

// 低频平滑后音量
let smoothedVolume = 0;

/**
* 从最新 FFT 帧数据计算低频音量能量值 [0.0 - 1.0]
*/
const updateLowFreqVolume = () => {
const data = getFftFrame();
if (!data || data.length === 0) return;

// 提取低频部分 (前 4 个 bin,约 0 - 150Hz)
const lowBins = data.slice(0, 4);
const sum = lowBins.reduce((acc, val) => acc + val, 0);
const avg = sum / lowBins.length;

// 映射与幂扩展动态范围
const threshold = 0.05;
const normalized = Math.max(0, (avg - threshold) / (1.0 - threshold));
const rawValue = Math.pow(normalized, 1.5);

// EMA 平滑处理,提供自然的过渡律动
const smoothFactor = 0.2;
smoothedVolume = smoothedVolume + smoothFactor * (rawValue - smoothedVolume);

bgRenderRef.value?.setLowFreqVolume(smoothedVolume);
};

// 使用 VueUse 提供的 useRafFn 帧刷新函数,避免内存占用并配合 playing 状态自动暂停
const { resume: resumeFftLoop, pause: pauseFftLoop } = useRafFn(updateLowFreqVolume, {
immediate: false,
});

/**
* 开始捕获 FFT 频谱数据
*/
const startFftCapture = () => {
if (window.api?.player?.setFftEnabled) {
window.api.player.setFftEnabled(true);
}
resumeFftLoop();
};

/**
* 停止捕获 FFT 频谱数据
*/
const stopFftCapture = () => {
pauseFftLoop();
if (window.api?.player?.setFftEnabled) {
window.api.player.setFftEnabled(false);
}
};

onMounted(() => {
if (wrapperRef.value) {
// 初始化 AMLL 底层渲染器
bgRenderRef.value = CoreBackgroundRender.new(props.renderer);

// 设置 Canvas 自适应容器并附着 DOM
const el = bgRenderRef.value.getElement();
el.style.width = "100%";
el.style.height = "100%";
el.style.display = "block";
wrapperRef.value.appendChild(el);

updateRendererState();

if (props.playing) {
startFftCapture();
}
}
});

let disposeTimer: ReturnType<typeof setTimeout> | null = null;

onBeforeUnmount(() => {
stopFftCapture();

const renderer = bgRenderRef.value;
if (renderer) {
renderer.pause();
bgRenderRef.value = undefined;

// 延迟 500ms 销毁底层 Canvas 与 WebGL 上下文,以配合过渡动画,避免发生闪烁和内存泄漏
disposeTimer = setTimeout(() => {
renderer.dispose();
disposeTimer = null;
}, 500);
}
});

onUnmounted(() => {
if (disposeTimer) {
clearTimeout(disposeTimer);
disposeTimer = null;
}
});

// 属性变化监听
watch(
() => props.album,
(val) => {
if (val && bgRenderRef.value) {
bgRenderRef.value.setAlbum(val, false);
}
},
);

watch(
() => props.playing,
(isPlaying) => {
if (bgRenderRef.value) {
if (isPlaying) {
bgRenderRef.value.resume();
startFftCapture();
} else {
bgRenderRef.value.pause();
stopFftCapture();
}
}
},
);

watch(
() => props.flowSpeed,
(val) => {
bgRenderRef.value?.setFlowSpeed(val);
},
);

watch(
() => props.renderScale,
(val) => {
bgRenderRef.value?.setRenderScale(val);
},
);

watch(
() => props.hasLyric,
(val) => {
bgRenderRef.value?.setHasLyric(val);
},
);

defineExpose({
bgRender: bgRenderRef,
wrapperEl: wrapperRef,
});
</script>

<template>
<div ref="wrapperRef" class="background-render-wrapper" aria-hidden="true" />
</template>

<style scoped>
.background-render-wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
z-index: 0;
pointer-events: none;
}
</style>
18 changes: 16 additions & 2 deletions src/components/player/FullPlayer/PlayerBackground.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { useThemeStore } from "@/stores/theme";
import { useMediaStore } from "@/stores/media";
import { useStatusStore } from "@/stores/status";
import DEFAULT_COVER from "@/assets/images/song.jpg";
import BackgroundRender from "./BackgroundRender.vue";

const media = useMediaStore();
const settings = useSettingsStore();
const theme = useThemeStore();
const status = useStatusStore();

const bgType = computed(() => settings.player.playerBgType);
const bgType = computed(() => settings.player.playerBgType as string);

// 封面颜色(纯色模式)
const coverColor = computed(() => {
Expand Down Expand Up @@ -95,6 +96,17 @@ onBeforeUnmount(() => {
/>
</div>

<!-- 流体背景 -->
<div
v-else-if="bgType === 'animation'"
class="absolute inset-0 overflow-hidden -z-1 bg-animation-wrap"
>
<BackgroundRender
:album="media.track?.cover || media.track?.coverOriginal || DEFAULT_COVER"
:playing="status.isPlaying"
/>
</div>

<!-- 纯色背景 -->
<div v-else class="absolute inset-0 overflow-hidden -z-1 bg-solid-wrap">
<Transition name="fade">
Expand All @@ -110,11 +122,13 @@ onBeforeUnmount(() => {
<style scoped>
/* 公共:遮罩层 */
.bg-blur-wrap::after,
.bg-solid-wrap::after {
.bg-solid-wrap::after,
.bg-animation-wrap::after {
content: "";
position: absolute;
inset: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1;
}

/* 模糊模式 */
Expand Down
Loading