Skip to content

Commit 4bf986b

Browse files
committed
✨ feat: 桌面歌词支持 TTML
1 parent ffb1fcc commit 4bf986b

1 file changed

Lines changed: 51 additions & 71 deletions

File tree

src/views/DesktopLyric/index.vue

Lines changed: 51 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,16 @@
8989
'end-with-space': text.word.endsWith(' ') || text.startTime === 0,
9090
}"
9191
>
92-
<span class="word" :style="{ color: lyricConfig.unplayedColor }">
93-
{{ text.word }}
94-
</span>
9592
<span
96-
class="filler"
97-
:style="[{ color: lyricConfig.playedColor }, getYrcStyle(text, line.index)]"
93+
class="word"
94+
:style="[
95+
{
96+
backgroundImage: `linear-gradient(to right, ${lyricConfig.playedColor} 50%, ${lyricConfig.unplayedColor} 50%)`,
97+
textShadow: 'none',
98+
filter: `drop-shadow(0 0 1px ${lyricConfig.shadowColor}) drop-shadow(0 0 2px ${lyricConfig.shadowColor})`,
99+
},
100+
getYrcStyle(text, line.index),
101+
]"
98102
>
99103
{{ text.word }}
100104
</span>
@@ -121,7 +125,7 @@
121125
</template>
122126

123127
<script setup lang="ts">
124-
import { useRafFn } from "@vueuse/core";
128+
import { useRafFn, useTimeoutFn, useThrottleFn } from "@vueuse/core";
125129
import { LyricLine, LyricWord } from "@applemusic-like-lyrics/lyric";
126130
import { LyricConfig, LyricData, RenderLine } from "@/types/desktop-lyric";
127131
import defaultDesktopLyricConfig from "@/assets/data/lyricConfig";
@@ -164,24 +168,22 @@ const desktopLyricRef = ref<HTMLElement>();
164168
165169
// hover 状态控制
166170
const isHovered = ref<boolean>(false);
167-
let hoverTimer: ReturnType<typeof setTimeout> | null = null;
171+
172+
const { start: startHoverTimer } = useTimeoutFn(
173+
() => {
174+
isHovered.value = false;
175+
},
176+
1000,
177+
{ immediate: false },
178+
);
168179
169180
/**
170181
* 处理鼠标移动,更新 hover 状态
171182
*/
172183
const handleMouseMove = () => {
173184
// 设置 hover 状态(锁定和非锁定状态都响应)
174185
isHovered.value = true;
175-
// 清除之前的定时器
176-
if (hoverTimer) {
177-
clearTimeout(hoverTimer);
178-
hoverTimer = null;
179-
}
180-
// 设置新的定时器,延迟后移除 hover 状态
181-
hoverTimer = setTimeout(() => {
182-
isHovered.value = false;
183-
hoverTimer = null;
184-
}, 1000);
186+
startHoverTimer();
185187
};
186188
187189
/**
@@ -334,7 +336,7 @@ const renderLyricLines = computed<RenderLine[]>(() => {
334336
*/
335337
const getYrcStyle = (wordData: LyricWord, lyricIndex: number) => {
336338
const currentLine = lyricData.yrcData?.[lyricIndex];
337-
if (!currentLine) return { WebkitMaskPositionX: "100%" };
339+
if (!currentLine) return { backgroundPositionX: "100%" };
338340
const seekSec = playSeekMs.value;
339341
const startSec = currentLine.startTime || 0;
340342
const endSec = currentLine.endTime || 0;
@@ -343,14 +345,12 @@ const getYrcStyle = (wordData: LyricWord, lyricIndex: number) => {
343345
344346
if (!isLineActive) {
345347
const hasPlayed = seekSec >= (wordData.endTime || 0);
346-
return { WebkitMaskPositionX: hasPlayed ? "0%" : "100%" };
348+
return { backgroundPositionX: hasPlayed ? "0%" : "100%" };
347349
}
348350
const durationSec = Math.max((wordData.endTime || 0) - (wordData.startTime || 0), 0.001);
349351
const progress = Math.max(Math.min((seekSec - (wordData.startTime || 0)) / durationSec, 1), 0);
350352
return {
351-
transitionDuration: `0s, 0s, 0.35s`,
352-
transitionDelay: `0ms`,
353-
WebkitMaskPositionX: `${100 - progress * 100}%`,
353+
backgroundPositionX: `${100 - progress * 100}%`,
354354
};
355355
};
356356
@@ -409,6 +409,11 @@ const dragState = reactive({
409409
startWinY: 0,
410410
winWidth: 0,
411411
winHeight: 0,
412+
// 缓存屏幕边界
413+
minX: -99999,
414+
minY: -99999,
415+
maxX: 99999,
416+
maxY: 99999,
412417
});
413418
414419
/**
@@ -436,6 +441,14 @@ const startDrag = async (event: MouseEvent) => {
436441
const { width, height } = await window.api.store.get("lyric");
437442
const safeWidth = Number(width) > 0 ? Number(width) : 800;
438443
const safeHeight = Number(height) > 0 ? Number(height) : 136;
444+
// 如果开启了限制边界,在拖拽开始时预先获取一次屏幕范围
445+
if (lyricConfig.limitBounds) {
446+
const bounds = await window.electron.ipcRenderer.invoke("get-virtual-screen-bounds");
447+
dragState.minX = bounds.minX ?? -99999;
448+
dragState.minY = bounds.minY ?? -99999;
449+
dragState.maxX = bounds.maxX ?? 99999;
450+
dragState.maxY = bounds.maxY ?? 99999;
451+
}
439452
window.electron.ipcRenderer.send("toggle-fixed-max-size", {
440453
width: safeWidth,
441454
height: safeHeight,
@@ -456,19 +469,20 @@ const startDrag = async (event: MouseEvent) => {
456469
* 桌面歌词拖动移动
457470
* @param event 鼠标事件
458471
*/
459-
const onDocMouseMove = async (event: MouseEvent) => {
472+
const onDocMouseMove = useThrottleFn((event: MouseEvent) => {
460473
if (!dragState.isDragging || lyricConfig.isLock) return;
461474
const screenX = event?.screenX ?? 0;
462475
const screenY = event?.screenY ?? 0;
463476
let newWinX = Math.round(dragState.startWinX + (screenX - dragState.startX));
464477
let newWinY = Math.round(dragState.startWinY + (screenY - dragState.startY));
465-
// 是否限制在屏幕边界(支持多屏)
478+
// 是否限制在屏幕边界(支持多屏)- 使用缓存的边界数据同步计算
466479
if (lyricConfig.limitBounds) {
467-
const { minX, minY, maxX, maxY } = await window.electron.ipcRenderer.invoke(
468-
"get-virtual-screen-bounds",
480+
newWinX = Math.round(
481+
Math.max(dragState.minX, Math.min(dragState.maxX - dragState.winWidth, newWinX)),
482+
);
483+
newWinY = Math.round(
484+
Math.max(dragState.minY, Math.min(dragState.maxY - dragState.winHeight, newWinY)),
469485
);
470-
newWinX = Math.round(Math.max(minX as number, Math.min(maxX - dragState.winWidth, newWinX)));
471-
newWinY = Math.round(Math.max(minY as number, Math.min(maxY - dragState.winHeight, newWinY)));
472486
}
473487
window.electron.ipcRenderer.send(
474488
"move-window",
@@ -477,7 +491,7 @@ const onDocMouseMove = async (event: MouseEvent) => {
477491
dragState.winWidth,
478492
dragState.winHeight,
479493
);
480-
};
494+
}, 16);
481495
482496
/**
483497
* 桌面歌词拖动结束
@@ -650,11 +664,6 @@ onBeforeUnmount(() => {
650664
// 解绑事件
651665
document.removeEventListener("mousedown", onDocMouseDown);
652666
document.removeEventListener("mousemove", handleMouseMove);
653-
// 清理定时器
654-
if (hoverTimer) {
655-
clearTimeout(hoverTimer);
656-
hoverTimer = null;
657-
}
658667
if (dragState.isDragging) onDocMouseUp();
659668
});
660669
</script>
@@ -739,6 +748,7 @@ onBeforeUnmount(() => {
739748
.lyric-line {
740749
width: 100%;
741750
line-height: normal;
751+
padding: 4px 0;
742752
overflow: hidden;
743753
text-overflow: ellipsis;
744754
white-space: nowrap;
@@ -761,34 +771,14 @@ onBeforeUnmount(() => {
761771
position: relative;
762772
display: inline-block;
763773
.word {
764-
opacity: 1;
765774
display: inline-block;
766-
}
767-
.filler {
768-
opacity: 0;
769-
position: absolute;
770-
left: 0;
771-
top: 0;
772-
will-change: -webkit-mask-position-x, transform, opacity;
773-
mask-image: linear-gradient(
774-
to right,
775-
rgb(0, 0, 0) 45.4545454545%,
776-
rgba(0, 0, 0, 0) 54.5454545455%
777-
);
778-
mask-size: 220% 100%;
779-
mask-repeat: no-repeat;
780-
-webkit-mask-image: linear-gradient(
781-
to right,
782-
rgb(0, 0, 0) 45.4545454545%,
783-
rgba(0, 0, 0, 0) 54.5454545455%
784-
);
785-
-webkit-mask-size: 220% 100%;
786-
-webkit-mask-repeat: no-repeat;
787-
transition:
788-
opacity 0.3s,
789-
filter 0.3s,
790-
margin 0.3s,
791-
padding 0.3s !important;
775+
background-clip: text;
776+
-webkit-background-clip: text;
777+
color: transparent;
778+
background-size: 200% 100%;
779+
background-repeat: no-repeat;
780+
background-position-x: 100%;
781+
will-change: background-position-x;
792782
}
793783
&.end-with-space {
794784
margin-right: 5vh;
@@ -797,16 +787,6 @@ onBeforeUnmount(() => {
797787
}
798788
}
799789
}
800-
&.active {
801-
.content-text {
802-
.filler {
803-
opacity: 1;
804-
-webkit-mask-position-x: 0%;
805-
transition-property: -webkit-mask-position-x, transform, opacity;
806-
transition-timing-function: linear, ease, ease;
807-
}
808-
}
809-
}
810790
}
811791
}
812792
&.center {

0 commit comments

Comments
 (0)