mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-05-17 18:47:29 +08:00
🦄 refactor: 重构整个项目 优化打包 修改后台服务为本地运行 添加更新版本检测功能
This commit is contained in:
@@ -0,0 +1,803 @@
|
||||
<template>
|
||||
<div
|
||||
class="lyric-window"
|
||||
:class="[lyricSetting.theme, { lyric_lock: lyricSetting.isLock }]"
|
||||
@mousedown="handleMouseDown"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<div class="drag-overlay"></div>
|
||||
<!-- 顶部控制栏 -->
|
||||
<div class="control-bar" :class="{ 'control-bar-show': showControls }">
|
||||
<div class="font-size-controls">
|
||||
<n-button-group>
|
||||
<n-button quaternary size="small" :disabled="fontSize <= 12" @click="decreaseFontSize">
|
||||
<i class="ri-subtract-line"></i>
|
||||
</n-button>
|
||||
<n-button quaternary size="small" :disabled="fontSize >= 48" @click="increaseFontSize">
|
||||
<i class="ri-add-line"></i>
|
||||
</n-button>
|
||||
</n-button-group>
|
||||
<div>{{ staticData.playMusic.name }}</div>
|
||||
</div>
|
||||
<!-- 添加播放控制按钮 -->
|
||||
<div class="play-controls">
|
||||
<div class="control-button" @click="handlePrev">
|
||||
<i class="ri-skip-back-fill"></i>
|
||||
</div>
|
||||
<div class="control-button play-button" @click="handlePlayPause">
|
||||
<i :class="dynamicData.isPlay ? 'ri-pause-fill' : 'ri-play-fill'"></i>
|
||||
</div>
|
||||
<div class="control-button" @click="handleNext">
|
||||
<i class="ri-skip-forward-fill"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-buttons">
|
||||
<div class="control-button" @click="checkTheme">
|
||||
<i v-if="lyricSetting.theme === 'light'" class="ri-sun-line"></i>
|
||||
<i v-else class="ri-moon-line"></i>
|
||||
</div>
|
||||
<!-- <div class="control-button" @click="handleTop">
|
||||
<i class="ri-pushpin-line" :class="{ active: lyricSetting.isTop }"></i>
|
||||
</div> -->
|
||||
<div id="lyric-lock" class="control-button" @click="handleLock">
|
||||
<i v-if="lyricSetting.isLock" class="ri-lock-line"></i>
|
||||
<i v-else class="ri-lock-unlock-line"></i>
|
||||
</div>
|
||||
<div class="control-button" @click="handleClose">
|
||||
<i class="ri-close-line"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 歌词显示区域 -->
|
||||
<div ref="containerRef" class="lyric-container">
|
||||
<div class="lyric-scroll">
|
||||
<div class="lyric-wrapper" :style="wrapperStyle">
|
||||
<template v-if="staticData.lrcArray?.length > 0">
|
||||
<div
|
||||
v-for="(line, index) in staticData.lrcArray"
|
||||
:key="index"
|
||||
class="lyric-line"
|
||||
:style="lyricLineStyle"
|
||||
:class="{
|
||||
'lyric-line-current': index === currentIndex,
|
||||
'lyric-line-passed': index < currentIndex,
|
||||
'lyric-line-next': index === currentIndex + 1
|
||||
}"
|
||||
>
|
||||
<div class="lyric-text" :style="{ fontSize: `${fontSize}px` }">
|
||||
<span class="lyric-text-inner" :style="getLyricStyle(index)">
|
||||
{{ line.text || '' }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="line.trText"
|
||||
class="lyric-translation"
|
||||
:style="{ fontSize: `${fontSize * 0.6}px` }"
|
||||
>
|
||||
{{ line.trText }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="lyric-empty">无歌词</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
import { SongResult } from '@/type/music';
|
||||
|
||||
defineOptions({
|
||||
name: 'Lyric'
|
||||
});
|
||||
const windowData = window as any;
|
||||
const containerRef = ref<HTMLElement | null>(null);
|
||||
const containerHeight = ref(0);
|
||||
const lineHeight = ref(60);
|
||||
const currentIndex = ref(0);
|
||||
// 字体大小控制
|
||||
const fontSize = ref(24); // 默认字体大小
|
||||
const fontSizeStep = 2; // 每次整的步长
|
||||
const animationFrameId = ref<number | null>(null);
|
||||
const lastUpdateTime = ref(performance.now());
|
||||
|
||||
// 静态数据
|
||||
const staticData = ref<{
|
||||
lrcArray: Array<{ text: string; trText: string }>;
|
||||
lrcTimeArray: number[];
|
||||
allTime: number;
|
||||
playMusic: SongResult;
|
||||
}>({
|
||||
lrcArray: [],
|
||||
lrcTimeArray: [],
|
||||
allTime: 0,
|
||||
playMusic: {} as SongResult
|
||||
});
|
||||
|
||||
// 动态数据
|
||||
const dynamicData = ref({
|
||||
nowTime: 0,
|
||||
startCurrentTime: 0,
|
||||
nextTime: 0,
|
||||
isPlay: true
|
||||
});
|
||||
|
||||
const lyricSetting = ref({
|
||||
...(localStorage.getItem('lyricData')
|
||||
? JSON.parse(localStorage.getItem('lyricData') || '')
|
||||
: {
|
||||
isTop: false,
|
||||
theme: 'dark',
|
||||
isLock: false
|
||||
})
|
||||
});
|
||||
|
||||
let hideControlsTimer: number | null = null;
|
||||
|
||||
const isHovering = ref(false);
|
||||
|
||||
// 计算是否栏
|
||||
const showControls = computed(() => {
|
||||
if (lyricSetting.value.isLock) {
|
||||
return isHovering.value;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// 清除隐藏定时器
|
||||
const clearHideTimer = () => {
|
||||
if (hideControlsTimer) {
|
||||
clearTimeout(hideControlsTimer);
|
||||
hideControlsTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理鼠标进入窗口
|
||||
const handleMouseEnter = () => {
|
||||
if (lyricSetting.value.isLock) {
|
||||
isHovering.value = true;
|
||||
windowData.electron.ipcRenderer.send('set-ignore-mouse', true);
|
||||
} else {
|
||||
windowData.electron.ipcRenderer.send('set-ignore-mouse', false);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理鼠标离开窗口
|
||||
const handleMouseLeave = () => {
|
||||
if (!lyricSetting.value.isLock) return;
|
||||
isHovering.value = false;
|
||||
windowData.electron.ipcRenderer.send('set-ignore-mouse', false);
|
||||
};
|
||||
|
||||
// 监听锁定状态变化
|
||||
watch(
|
||||
() => lyricSetting.value.isLock,
|
||||
(newLock: boolean) => {
|
||||
if (newLock) {
|
||||
isHovering.value = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化时,如果是锁定状态,确保控制栏隐藏
|
||||
if (lyricSetting.value.isLock) {
|
||||
isHovering.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
clearHideTimer();
|
||||
});
|
||||
|
||||
// 计算歌词滚动位置
|
||||
const wrapperStyle = computed(() => {
|
||||
if (!containerHeight.value) {
|
||||
return {
|
||||
transform: 'translateY(0)',
|
||||
transition: 'none'
|
||||
};
|
||||
}
|
||||
|
||||
// 计算容器中心点
|
||||
const containerCenter = containerHeight.value / 2;
|
||||
|
||||
// 计算当前行到顶部的距离(包含padding)
|
||||
const currentLineTop =
|
||||
currentIndex.value * lineHeight.value + containerHeight.value * 0.2 + lineHeight.value; // 加上顶部padding
|
||||
|
||||
// 计算偏移量,使当前行居中
|
||||
const targetOffset = containerCenter - currentLineTop;
|
||||
|
||||
// 计算内容总高度(包含padding)
|
||||
const contentHeight =
|
||||
staticData.value.lrcArray.length * lineHeight.value + containerHeight.value * 0.4; // 上下padding各20vh
|
||||
|
||||
// 计算最小和最大偏移量
|
||||
const minOffset = -(contentHeight - containerHeight.value);
|
||||
const maxOffset = 0;
|
||||
|
||||
// 限制偏移量在合理范围内
|
||||
const finalOffset = Math.min(maxOffset, Math.max(minOffset, targetOffset));
|
||||
|
||||
return {
|
||||
transform: `translateY(${finalOffset}px)`,
|
||||
transition: 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)'
|
||||
};
|
||||
});
|
||||
|
||||
const lyricLineStyle = computed(() => ({
|
||||
height: `${lineHeight.value}px`
|
||||
}));
|
||||
// 更新容器高度和行高
|
||||
const updateContainerHeight = () => {
|
||||
if (!containerRef.value) return;
|
||||
|
||||
// 更新容器高度
|
||||
containerHeight.value = containerRef.value.clientHeight;
|
||||
|
||||
// 计算基础行高(字体大小的2.5倍)
|
||||
const baseLineHeight = fontSize.value * 2.5;
|
||||
|
||||
// 计算最大允许行高(容器高度的1/4)
|
||||
const maxAllowedHeight = containerHeight.value / 3;
|
||||
|
||||
// 设置行高(不小于40px,不大于最大允许高度)
|
||||
lineHeight.value = Math.min(maxAllowedHeight, Math.max(40, baseLineHeight));
|
||||
};
|
||||
|
||||
// 处理字体大小变化
|
||||
const handleFontSizeChange = async () => {
|
||||
// 先保存字体大小
|
||||
saveFontSize();
|
||||
|
||||
// 更新容器高度和行高
|
||||
updateContainerHeight();
|
||||
};
|
||||
|
||||
// 增加字体大小
|
||||
const increaseFontSize = async () => {
|
||||
if (fontSize.value < 48) {
|
||||
fontSize.value += fontSizeStep;
|
||||
await handleFontSizeChange();
|
||||
}
|
||||
};
|
||||
|
||||
// 减小字体大小
|
||||
const decreaseFontSize = async () => {
|
||||
if (fontSize.value > 12) {
|
||||
fontSize.value -= fontSizeStep;
|
||||
await handleFontSizeChange();
|
||||
}
|
||||
};
|
||||
|
||||
// 保存字体大小到本地存储
|
||||
const saveFontSize = () => {
|
||||
localStorage.setItem('lyricFontSize', fontSize.value.toString());
|
||||
};
|
||||
|
||||
// 监听容器大小变化
|
||||
onMounted(() => {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
updateContainerHeight();
|
||||
});
|
||||
|
||||
if (containerRef.value) {
|
||||
resizeObserver.observe(containerRef.value);
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
resizeObserver.disconnect();
|
||||
});
|
||||
});
|
||||
// 实际播放时间
|
||||
const actualTime = ref(0);
|
||||
|
||||
// 计算当前行的进度
|
||||
const currentProgress = computed(() => {
|
||||
const { startCurrentTime, nextTime } = dynamicData.value;
|
||||
if (!startCurrentTime || !nextTime) return 0;
|
||||
|
||||
const duration = nextTime - startCurrentTime;
|
||||
const elapsed = actualTime.value - startCurrentTime;
|
||||
return Math.min(Math.max(elapsed / duration, 0), 1);
|
||||
});
|
||||
|
||||
// 获取歌词样式
|
||||
const getLyricStyle = (index: number) => {
|
||||
if (index !== currentIndex.value) return {};
|
||||
|
||||
const progress = currentProgress.value * 100;
|
||||
return {
|
||||
background: `linear-gradient(to right, var(--highlight-color) ${progress}%, var(--text-color) ${progress}%)`,
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
transition: 'all 0.1s linear'
|
||||
};
|
||||
};
|
||||
|
||||
// 时间偏移量(毫秒)
|
||||
const TIME_OFFSET = 400;
|
||||
|
||||
// 更新动画
|
||||
const updateProgress = () => {
|
||||
if (!dynamicData.value.isPlay) {
|
||||
if (animationFrameId.value) {
|
||||
cancelAnimationFrame(animationFrameId.value);
|
||||
animationFrameId.value = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算实际时间,添加偏移量
|
||||
const timeDiff = (performance.now() - lastUpdateTime.value) / 1000;
|
||||
actualTime.value = dynamicData.value.nowTime + timeDiff + TIME_OFFSET / 1000;
|
||||
|
||||
// 继续动画
|
||||
animationFrameId.value = requestAnimationFrame(updateProgress);
|
||||
};
|
||||
|
||||
// 记录上次更新时间
|
||||
|
||||
// 监听据更新
|
||||
watch(
|
||||
() => dynamicData.value,
|
||||
(newData: any) => {
|
||||
// 更新最后更新时间
|
||||
lastUpdateTime.value = performance.now();
|
||||
|
||||
// 更新实际时间,包含偏移量
|
||||
actualTime.value = newData.nowTime + TIME_OFFSET / 1000;
|
||||
|
||||
// 如果正在播放且没有动画,启动动画
|
||||
if (newData.isPlay && !animationFrameId.value) {
|
||||
updateProgress();
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 监听播放状态变化
|
||||
watch(
|
||||
() => dynamicData.value.isPlay,
|
||||
(isPlaying: boolean) => {
|
||||
if (isPlaying) {
|
||||
lastUpdateTime.value = performance.now();
|
||||
updateProgress();
|
||||
} else if (animationFrameId.value) {
|
||||
cancelAnimationFrame(animationFrameId.value);
|
||||
animationFrameId.value = null;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 修改数据更新处
|
||||
const handleDataUpdate = (parsedData: {
|
||||
nowTime: number;
|
||||
startCurrentTime: number;
|
||||
nextTime: number;
|
||||
isPlay: boolean;
|
||||
nowIndex: number;
|
||||
lrcArray: Array<{ text: string; trText: string }>;
|
||||
lrcTimeArray: number[];
|
||||
allTime: number;
|
||||
playMusic: SongResult;
|
||||
}) => {
|
||||
// 确保数据存在且格式正确
|
||||
if (!parsedData) {
|
||||
console.error('Invalid update data received:', parsedData);
|
||||
return;
|
||||
}
|
||||
// 更新静态数据
|
||||
staticData.value = {
|
||||
lrcArray: parsedData.lrcArray || [],
|
||||
lrcTimeArray: parsedData.lrcTimeArray || [],
|
||||
allTime: parsedData.allTime || 0,
|
||||
playMusic: parsedData.playMusic || {}
|
||||
};
|
||||
|
||||
// 更新动态数据
|
||||
dynamicData.value = {
|
||||
nowTime: parsedData.nowTime || 0,
|
||||
startCurrentTime: parsedData.startCurrentTime || 0,
|
||||
nextTime: parsedData.nextTime || 0,
|
||||
isPlay: parsedData.isPlay
|
||||
};
|
||||
|
||||
// 更新索引
|
||||
if (typeof parsedData.nowIndex === 'number') {
|
||||
currentIndex.value = parsedData.nowIndex;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 加载保存的字体大小
|
||||
const savedFontSize = localStorage.getItem('lyricFontSize');
|
||||
if (savedFontSize) {
|
||||
fontSize.value = Number(savedFontSize);
|
||||
lineHeight.value = fontSize.value * 2.5;
|
||||
}
|
||||
|
||||
// 初始化容器高度
|
||||
updateContainerHeight();
|
||||
window.addEventListener('resize', updateContainerHeight);
|
||||
|
||||
// 监听歌词数据
|
||||
windowData.electron.ipcRenderer.on('receive-lyric', (_, data) => {
|
||||
try {
|
||||
const parsedData = JSON.parse(data);
|
||||
handleDataUpdate(parsedData);
|
||||
} catch (error) {
|
||||
console.error('Error parsing lyric data:', error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateContainerHeight);
|
||||
});
|
||||
|
||||
const checkTheme = () => {
|
||||
if (lyricSetting.value.theme === 'light') {
|
||||
lyricSetting.value.theme = 'dark';
|
||||
} else {
|
||||
lyricSetting.value.theme = 'light';
|
||||
}
|
||||
};
|
||||
|
||||
// const handleTop = () => {
|
||||
// lyricSetting.value.isTop = !lyricSetting.value.isTop;
|
||||
// windowData.electron.ipcRenderer.send('top-lyric', lyricSetting.value.isTop);
|
||||
// };
|
||||
|
||||
const handleLock = () => {
|
||||
lyricSetting.value.isLock = !lyricSetting.value.isLock;
|
||||
windowData.electron.ipcRenderer.send('set-ignore-mouse', lyricSetting.value.isLock);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
windowData.electron.ipcRenderer.send('close-lyric');
|
||||
};
|
||||
|
||||
watch(
|
||||
() => lyricSetting.value,
|
||||
(newValue: any) => {
|
||||
localStorage.setItem('lyricData', JSON.stringify(newValue));
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 添��拖动相关变量
|
||||
const isDragging = ref(false);
|
||||
const startPosition = ref({ x: 0, y: 0 });
|
||||
|
||||
// 处理鼠标按下事件
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
// 如果点击的是控制按钮区域或窗口被锁定,不处理拖动
|
||||
if (
|
||||
lyricSetting.value.isLock ||
|
||||
(e.target as HTMLElement).closest('.control-buttons') ||
|
||||
(e.target as HTMLElement).closest('.font-size-controls')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 只响应鼠标左键
|
||||
if (e.button !== 0) return;
|
||||
|
||||
isDragging.value = true;
|
||||
startPosition.value = { x: e.screenX, y: e.screenY };
|
||||
|
||||
// 添加全局鼠标事件监听
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!isDragging.value) return;
|
||||
|
||||
const deltaX = e.screenX - startPosition.value.x;
|
||||
const deltaY = e.screenY - startPosition.value.y;
|
||||
|
||||
// 发送移动事件到主进程
|
||||
windowData.electron.ipcRenderer.send('lyric-drag-move', { deltaX, deltaY });
|
||||
startPosition.value = { x: e.screenX, y: e.screenY };
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (!isDragging.value) return;
|
||||
isDragging.value = false;
|
||||
|
||||
// 移除事件监听
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
// 添加全局事件监听
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
isDragging.value = false;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
const lyricLock = document.getElementById('lyric-lock');
|
||||
if (lyricLock) {
|
||||
lyricLock.onmouseenter = () => {
|
||||
if (lyricSetting.value.isLock) {
|
||||
windowData.electron.ipcRenderer.send('set-ignore-mouse', false);
|
||||
}
|
||||
};
|
||||
lyricLock.onmouseleave = () => {
|
||||
if (lyricSetting.value.isLock) {
|
||||
windowData.electron.ipcRenderer.send('set-ignore-mouse', true);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 添加播放控制相关的函数
|
||||
const handlePlayPause = () => {
|
||||
windowData.electron.ipcRenderer.send('control-back', 'playpause');
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
windowData.electron.ipcRenderer.send('control-back', 'prev');
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
windowData.electron.ipcRenderer.send('control-back', 'next');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.lyric-window {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
user-select: none;
|
||||
transition: background-color 0.2s ease;
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
.control-bar {
|
||||
&-show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
--text-color: #ffffff;
|
||||
--text-secondary: rgba(255, 255, 255, 0.6);
|
||||
--highlight-color: #1db954;
|
||||
--control-bg: rgba(124, 124, 124, 0.3);
|
||||
}
|
||||
|
||||
&.light {
|
||||
--text-color: #333333;
|
||||
--text-secondary: rgba(51, 51, 51, 0.6);
|
||||
--highlight-color: #1db954;
|
||||
--control-bg: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.control-bar {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
padding: 0 20px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
visibility 0.2s ease;
|
||||
z-index: 100;
|
||||
|
||||
.font-size-controls {
|
||||
-webkit-app-region: no-drag;
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.play-controls {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
.play-button {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
i {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.control-button {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
color: var(--text-color);
|
||||
transition: all 0.2s ease;
|
||||
&:hover {
|
||||
background: var(--control-bg);
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
text-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
|
||||
|
||||
&.active {
|
||||
color: var(--highlight-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lyric-container {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.lyric-scroll {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
mask-image: linear-gradient(to bottom, transparent 0%, black 20%, black 80%, transparent 100%);
|
||||
}
|
||||
|
||||
.lyric-wrapper {
|
||||
will-change: transform;
|
||||
padding: 20vh 0;
|
||||
transform-origin: center center;
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.lyric-line {
|
||||
padding: 4px 20px;
|
||||
text-align: center;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&.lyric-line-current {
|
||||
transform: scale(1.05);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.lyric-line-passed {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.lyric-text {
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
color: var(--text-color);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
text-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
|
||||
transition: all 0.2s ease;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.lyric-translation {
|
||||
color: var(--text-secondary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
text-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
|
||||
transition: font-size 0.2s ease;
|
||||
line-height: 1.4; // 添加行高比例
|
||||
}
|
||||
|
||||
.lyric-empty {
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
font-size: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: transparent !important;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
|
||||
'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
.lyric-content {
|
||||
transition: font-size 0.2s ease;
|
||||
}
|
||||
|
||||
.lyric-line-current {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.control-bar {
|
||||
.control-buttons {
|
||||
.control-button {
|
||||
&:not(:has(.ri-lock-line)):not(:has(.ri-lock-unlock-line)) {
|
||||
.lyric_lock & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lyric_lock & .font-size-controls {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.lyric_lock & .play-controls {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.lyric_lock {
|
||||
background: transparent;
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#lyric-lock {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 72px;
|
||||
background: var(--control-bg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user