mirror of
https://github.com/imsyy/SPlayer.git
synced 2025-11-25 03:14:57 +08:00
🌈 style: 优化部分样式
This commit is contained in:
@@ -27,7 +27,7 @@ const initFileIpc = (): void => {
|
|||||||
const filePath = resolve(dirPath).replace(/\\/g, "/");
|
const filePath = resolve(dirPath).replace(/\\/g, "/");
|
||||||
console.info(`📂 Fetching music files from: ${filePath}`);
|
console.info(`📂 Fetching music files from: ${filePath}`);
|
||||||
// 查找指定目录下的所有音乐文件
|
// 查找指定目录下的所有音乐文件
|
||||||
const musicFiles = await FastGlob("**/*.{mp3,wav,flac}", { cwd: filePath });
|
const musicFiles = await FastGlob("**/*.{mp3,wav,flac,aac,webm}", { cwd: filePath });
|
||||||
// 解析元信息
|
// 解析元信息
|
||||||
const metadataPromises = musicFiles.map(async (file) => {
|
const metadataPromises = musicFiles.map(async (file) => {
|
||||||
const filePath = join(dirPath, file);
|
const filePath = join(dirPath, file);
|
||||||
|
|||||||
1
src/assets/icons/Close.svg
Normal file
1
src/assets/icons/Close.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="m12 13.4l-4.9 4.9q-.275.275-.7.275t-.7-.275t-.275-.7t.275-.7l4.9-4.9l-4.9-4.9q-.275-.275-.275-.7t.275-.7t.7-.275t.7.275l4.9 4.9l4.9-4.9q.275-.275.7-.275t.7.275t.275.7t-.275.7L13.4 12l4.9 4.9q.275.275.275.7t-.275.7t-.7.275t-.7-.275z"/></svg>
|
||||||
|
After Width: | Height: | Size: 467 B |
4
src/assets/icons/Lock.svg
Normal file
4
src/assets/icons/Lock.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2M9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9zm9 14H6V10h12zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2s-2 .9-2 2s.9 2 2 2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 349 B |
1
src/assets/icons/LockOpen.svg
Normal file
1
src/assets/icons/LockOpen.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M6 20h12V10H6zm6-3q.825 0 1.413-.587T14 15t-.587-1.412T12 13t-1.412.588T10 15t.588 1.413T12 17m-6 3V10zm0 2q-.825 0-1.412-.587T4 20V10q0-.825.588-1.412T6 8h7V6q0-2.075 1.463-3.537T18 1q1.775 0 3.1 1.075t1.75 2.7q.125.425-.162.825T22 6q-.425 0-.7-.175t-.4-.575q-.275-.95-1.062-1.6T18 3q-1.25 0-2.125.875T15 6v2h3q.825 0 1.413.588T20 10v10q0 .825-.587 1.413T18 22z"/></svg>
|
||||||
|
After Width: | Height: | Size: 598 B |
4
src/assets/icons/TextSizeAdd.svg
Normal file
4
src/assets/icons/TextSizeAdd.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M8.5 7h2L16 21h-2.4l-1.1-3H6.3l-1.1 3H3zm-1.4 9h4.8L9.5 9.7zM22 5v2h-3v3h-2V7h-3V5h3V2h2v3z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 227 B |
3
src/assets/icons/TextSizeReduce.svg
Normal file
3
src/assets/icons/TextSizeReduce.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M10.5 7h-2L3 21h2.2l1.1-3h6.2l1.1 3H16zm-3.4 9l2.4-6.3l2.4 6.3zM22 7h-8V5h8z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 204 B |
@@ -1,7 +1,11 @@
|
|||||||
<!-- 全局图标 -->
|
<!-- 全局图标 -->
|
||||||
<template>
|
<template>
|
||||||
<n-icon v-if="name" :size="size" :color="color" :depth="depth">
|
<n-icon v-if="name" :size="size" :color="color" :depth="depth">
|
||||||
<div ref="svgContainer" class="svg-container" />
|
<div
|
||||||
|
ref="svgContainer"
|
||||||
|
:style="{ transform: offset ? `translateY(${offset}px)` : undefined }"
|
||||||
|
class="svg-container"
|
||||||
|
/>
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -10,6 +14,7 @@ const props = defineProps<{
|
|||||||
name: string;
|
name: string;
|
||||||
size?: string | number;
|
size?: string | number;
|
||||||
color?: string;
|
color?: string;
|
||||||
|
offset?: number;
|
||||||
depth?: 1 | 2 | 3 | 4 | 5;
|
depth?: 1 | 2 | 3 | 4 | 5;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<s-image
|
<s-image
|
||||||
v-else-if="settingStore.playerBackgroundType === 'blur'"
|
v-else-if="settingStore.playerBackgroundType === 'blur'"
|
||||||
:src="musicStore.songCover"
|
:src="musicStore.songCover"
|
||||||
|
:observe-visibility="false"
|
||||||
class="bg-img"
|
class="bg-img"
|
||||||
alt="cover"
|
alt="cover"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<s-image
|
<s-image
|
||||||
:key="musicStore.getSongCover()"
|
:key="musicStore.getSongCover()"
|
||||||
:src="musicStore.getSongCover('l')"
|
:src="musicStore.getSongCover('l')"
|
||||||
|
:observe-visibility="false"
|
||||||
class="cover-img"
|
class="cover-img"
|
||||||
/>
|
/>
|
||||||
<!-- 动态封面 -->
|
<!-- 动态封面 -->
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
:key="imgSrc"
|
:key="imgSrc"
|
||||||
:alt="alt || 'image'"
|
:alt="alt || 'image'"
|
||||||
:class="['cover', { loaded: isLoaded }]"
|
:class="['cover', { loaded: isLoaded }]"
|
||||||
|
:decoding="decodeAsync ? 'async' : 'auto'"
|
||||||
|
:loading="nativeLazy ? 'lazy' : 'eager'"
|
||||||
@load="imageLoaded"
|
@load="imageLoaded"
|
||||||
@error="imageError"
|
@error="imageError"
|
||||||
/>
|
/>
|
||||||
@@ -27,9 +29,21 @@ const props = withDefaults(
|
|||||||
src: string | undefined;
|
src: string | undefined;
|
||||||
defaultSrc?: string;
|
defaultSrc?: string;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
|
// 是否进行可视状态变化
|
||||||
|
observeVisibility?: boolean;
|
||||||
|
// 在不可视时是否释放图片以回收内存
|
||||||
|
releaseOnHide?: boolean;
|
||||||
|
// 是否使用浏览器异步解码
|
||||||
|
decodeAsync?: boolean;
|
||||||
|
// 是否使用原生懒加载
|
||||||
|
nativeLazy?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
defaultSrc: "/images/song.jpg?assest",
|
defaultSrc: "/images/song.jpg?assest",
|
||||||
|
observeVisibility: true,
|
||||||
|
releaseOnHide: false,
|
||||||
|
decodeAsync: true,
|
||||||
|
nativeLazy: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -49,31 +63,57 @@ const imgContainer = ref<HTMLImageElement>();
|
|||||||
|
|
||||||
// 是否加载完成
|
// 是否加载完成
|
||||||
const isLoaded = ref<boolean>(false);
|
const isLoaded = ref<boolean>(false);
|
||||||
|
// 可视状态上一次值,避免重复 emit
|
||||||
|
const lastShowState = ref<boolean | null>(null);
|
||||||
|
// 加载竞态 token,防止旧图片回调覆盖新状态
|
||||||
|
const loadToken = ref<number>(0);
|
||||||
|
const currentToken = ref<number>(0);
|
||||||
|
|
||||||
// 是否可视
|
// 是否可视
|
||||||
const isCanLook = useElementVisibility(imgContainer);
|
const isCanLook = useElementVisibility(imgContainer);
|
||||||
|
|
||||||
// 图片加载完成
|
// 图片加载完成
|
||||||
const imageLoaded = (e: Event) => {
|
const imageLoaded = (e: Event) => {
|
||||||
|
// 竞态保护:仅响应最新一次设置的图片
|
||||||
|
if (currentToken.value !== loadToken.value) return;
|
||||||
|
if (isLoaded.value) return;
|
||||||
isLoaded.value = true;
|
isLoaded.value = true;
|
||||||
// 加载完成
|
|
||||||
emit("load", e);
|
emit("load", e);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 图片加载失败
|
// 图片加载失败
|
||||||
const imageError = (e: Event) => {
|
const imageError = (e: Event) => {
|
||||||
|
// 竞态保护
|
||||||
|
if (currentToken.value !== loadToken.value) return;
|
||||||
isLoaded.value = false;
|
isLoaded.value = false;
|
||||||
imgSrc.value = props.defaultSrc;
|
// 避免默认图也反复触发导致死循环
|
||||||
// 加载失败
|
if (imgSrc.value !== props.defaultSrc) {
|
||||||
|
imgSrc.value = props.defaultSrc;
|
||||||
|
}
|
||||||
emit("error", e);
|
emit("error", e);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 可视状态变化
|
// 可视状态变化(可控)
|
||||||
watch(
|
watch(
|
||||||
isCanLook,
|
isCanLook,
|
||||||
(show) => {
|
(show) => {
|
||||||
emit("update:show", show);
|
if (!props.observeVisibility) return;
|
||||||
if (show) imgSrc.value = props.src;
|
// 去重:仅在状态变化时触发
|
||||||
|
if (lastShowState.value !== show) {
|
||||||
|
lastShowState.value = show;
|
||||||
|
emit("update:show", show);
|
||||||
|
}
|
||||||
|
if (show) {
|
||||||
|
// 进入可视区再加载,避免重复赋值
|
||||||
|
if (imgSrc.value !== props.src) {
|
||||||
|
loadToken.value += 1;
|
||||||
|
currentToken.value = loadToken.value;
|
||||||
|
imgSrc.value = props.src;
|
||||||
|
}
|
||||||
|
} else if (props.releaseOnHide) {
|
||||||
|
// 释放图片以回收内存
|
||||||
|
if (imgSrc.value !== undefined) imgSrc.value = undefined;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
@@ -83,13 +123,40 @@ watch(
|
|||||||
() => props.src,
|
() => props.src,
|
||||||
(val) => {
|
(val) => {
|
||||||
isLoaded.value = false;
|
isLoaded.value = false;
|
||||||
if (isCanLook.value) {
|
// 不同值时才进行赋值,减少重绘
|
||||||
imgSrc.value = val;
|
if (props.observeVisibility) {
|
||||||
|
if (isCanLook.value) {
|
||||||
|
if (imgSrc.value !== val) {
|
||||||
|
loadToken.value += 1;
|
||||||
|
currentToken.value = loadToken.value;
|
||||||
|
imgSrc.value = val;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (props.releaseOnHide) {
|
||||||
|
if (imgSrc.value !== undefined) imgSrc.value = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
imgSrc.value = undefined;
|
if (imgSrc.value !== val) {
|
||||||
|
loadToken.value += 1;
|
||||||
|
currentToken.value = loadToken.value;
|
||||||
|
imgSrc.value = val;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
try {
|
||||||
|
if (imgRef.value) imgRef.value.src = "";
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
imgSrc.value = undefined;
|
||||||
|
imgRef.value = undefined;
|
||||||
|
imgContainer.value = undefined;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -393,3 +393,38 @@ export const shuffleArray = <T>(arr: T[]): T[] => {
|
|||||||
}
|
}
|
||||||
return copy;
|
return copy;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在浏览器空闲时执行任务
|
||||||
|
* @param task 要执行的任务
|
||||||
|
*/
|
||||||
|
export const runIdle = (task: () => void) => {
|
||||||
|
try {
|
||||||
|
const ric = window?.requestIdleCallback as ((cb: () => void) => number) | undefined;
|
||||||
|
if (typeof ric === "function") {
|
||||||
|
ric(() => {
|
||||||
|
try {
|
||||||
|
task();
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
task();
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
task();
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { cloneDeep } from "lodash-es";
|
|||||||
import { useMusicStore, useStatusStore, useDataStore, useSettingStore } from "@/stores";
|
import { useMusicStore, useStatusStore, useDataStore, useSettingStore } from "@/stores";
|
||||||
import { resetSongLyric, parseLocalLyric, calculateLyricIndex } from "./lyric";
|
import { resetSongLyric, parseLocalLyric, calculateLyricIndex } from "./lyric";
|
||||||
import { calculateProgress } from "./time";
|
import { calculateProgress } from "./time";
|
||||||
import { isElectron, isDev, shuffleArray } from "./helper";
|
import { isElectron, isDev, shuffleArray, runIdle } from "./helper";
|
||||||
import { heartRateList } from "@/api/playlist";
|
import { heartRateList } from "@/api/playlist";
|
||||||
import { formatSongsList } from "./format";
|
import { formatSongsList } from "./format";
|
||||||
import { isLogin } from "./auth";
|
import { isLogin } from "./auth";
|
||||||
@@ -40,7 +40,6 @@ class Player {
|
|||||||
private analyser: AnalyserNode | null = null;
|
private analyser: AnalyserNode | null = null;
|
||||||
private dataArray: Uint8Array<ArrayBuffer> | null = null;
|
private dataArray: Uint8Array<ArrayBuffer> | null = null;
|
||||||
/** 其他数据 */
|
/** 其他数据 */
|
||||||
private testNumber: number = 0;
|
|
||||||
private message: MessageReactive | null = null;
|
private message: MessageReactive | null = null;
|
||||||
/** 预载下一首歌曲播放地址缓存(仅存 URL,不创建 Howl) */
|
/** 预载下一首歌曲播放地址缓存(仅存 URL,不创建 Howl) */
|
||||||
private nextPrefetch: { id: number; url: string | null; ublock: boolean } | null = null;
|
private nextPrefetch: { id: number; url: string | null; ublock: boolean } | null = null;
|
||||||
@@ -48,6 +47,8 @@ class Player {
|
|||||||
private playSessionId: number = 0;
|
private playSessionId: number = 0;
|
||||||
/** 是否正在切换歌曲 */
|
/** 是否正在切换歌曲 */
|
||||||
private switching: boolean = false;
|
private switching: boolean = false;
|
||||||
|
/** 当前曲目重试信息(按歌曲维度计数) */
|
||||||
|
private retryInfo: { songId: number; count: number } = { songId: 0, count: 0 };
|
||||||
constructor() {
|
constructor() {
|
||||||
// 创建播放器实例
|
// 创建播放器实例
|
||||||
this.player = new Howl({ src: [""], format: allowPlayFormat, autoplay: false });
|
this.player = new Howl({ src: [""], format: allowPlayFormat, autoplay: false });
|
||||||
@@ -56,14 +57,51 @@ class Player {
|
|||||||
// 挂载全局
|
// 挂载全局
|
||||||
window.$player = this;
|
window.$player = this;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 新建会话并返回会话 id
|
||||||
|
*/
|
||||||
|
private newSession(): number {
|
||||||
|
this.playSessionId += 1;
|
||||||
|
return this.playSessionId;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 检查传入会话是否过期
|
||||||
|
*/
|
||||||
|
private isStale(sessionId: number): boolean {
|
||||||
|
return sessionId !== this.playSessionId;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 保护执行:会话过期则早退
|
||||||
|
*/
|
||||||
|
private guard<T>(sessionId: number, fn: () => T): T | undefined {
|
||||||
|
if (this.isStale(sessionId)) return;
|
||||||
|
return fn();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 重置底层播放器与定时器(幂等)
|
||||||
|
*/
|
||||||
|
private resetPlayerCore() {
|
||||||
|
try {
|
||||||
|
this.player?.off();
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Howler.stop();
|
||||||
|
Howler.unload();
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
this.cleanupAllTimers();
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 处理播放状态
|
* 处理播放状态
|
||||||
*/
|
*/
|
||||||
private handlePlayStatus(sessionId?: number) {
|
private handlePlayStatus() {
|
||||||
// const musicStore = useMusicStore();
|
// const musicStore = useMusicStore();
|
||||||
const statusStore = useStatusStore();
|
const statusStore = useStatusStore();
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const currentSessionId = sessionId ?? this.playSessionId;
|
const currentSessionId = this.playSessionId;
|
||||||
// 清理定时器
|
// 清理定时器
|
||||||
clearInterval(this.playerInterval);
|
clearInterval(this.playerInterval);
|
||||||
// 更新播放状态
|
// 更新播放状态
|
||||||
@@ -171,12 +209,7 @@ class Player {
|
|||||||
* @param autoPlay 是否自动播放
|
* @param autoPlay 是否自动播放
|
||||||
* @param seek 播放位置
|
* @param seek 播放位置
|
||||||
*/
|
*/
|
||||||
private async createPlayer(
|
private async createPlayer(src: string, autoPlay: boolean = true, seek: number = 0) {
|
||||||
src: string,
|
|
||||||
autoPlay: boolean = true,
|
|
||||||
seek: number = 0,
|
|
||||||
sessionId?: number,
|
|
||||||
) {
|
|
||||||
// 获取数据
|
// 获取数据
|
||||||
const dataStore = useDataStore();
|
const dataStore = useDataStore();
|
||||||
const musicStore = useMusicStore();
|
const musicStore = useMusicStore();
|
||||||
@@ -184,23 +217,15 @@ class Player {
|
|||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
// 播放信息
|
// 播放信息
|
||||||
const { id, path, type } = musicStore.playSong;
|
const { id, path, type } = musicStore.playSong;
|
||||||
const currentSessionId = sessionId ?? this.playSessionId;
|
const currentSessionId = this.playSessionId;
|
||||||
// 检查会话是否过期
|
// 检查会话是否过期
|
||||||
if (currentSessionId !== this.playSessionId) {
|
if (currentSessionId !== this.playSessionId) {
|
||||||
console.log("🚫 Session expired, skipping player creation");
|
console.log("🚫 Session expired, skipping player creation");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 清理播放器(移除事件,停止并卸载)
|
// 统一重置底层播放器
|
||||||
try {
|
this.resetPlayerCore();
|
||||||
this.player.off();
|
// 二次检查会话
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
Howler.stop();
|
|
||||||
Howler.unload();
|
|
||||||
// 清理所有定时器
|
|
||||||
this.cleanupAllTimers();
|
|
||||||
// 再次检查会话是否过期(异步操作后)
|
|
||||||
if (currentSessionId !== this.playSessionId) {
|
if (currentSessionId !== this.playSessionId) {
|
||||||
console.log("🚫 Session expired after cleanup, aborting");
|
console.log("🚫 Session expired after cleanup, aborting");
|
||||||
return;
|
return;
|
||||||
@@ -217,27 +242,29 @@ class Player {
|
|||||||
rate: statusStore.playRate,
|
rate: statusStore.playRate,
|
||||||
});
|
});
|
||||||
// 播放器事件(绑定当前会话)
|
// 播放器事件(绑定当前会话)
|
||||||
this.playerEvent({ seek, sessionId: currentSessionId });
|
this.playerEvent({ seek });
|
||||||
// 播放设备
|
// 播放设备
|
||||||
if (!settingStore.showSpectrums) this.toggleOutputDevice();
|
if (!settingStore.showSpectrums) this.toggleOutputDevice();
|
||||||
// 自动播放(仅一次性触发)
|
// 自动播放(仅一次性触发)
|
||||||
if (autoPlay) await this.play();
|
if (autoPlay) await this.play();
|
||||||
// 获取歌曲附加信息 - 非电台和本地
|
// 获取歌曲附加信息 - 非电台和本地
|
||||||
if (type !== "radio" && !path) {
|
if (type !== "radio" && !path) {
|
||||||
getLyricData(id);
|
runIdle(() => getLyricData(id));
|
||||||
} else resetSongLyric();
|
} else resetSongLyric();
|
||||||
// 定时获取状态
|
// 定时获取状态
|
||||||
if (!this.playerInterval) this.handlePlayStatus(currentSessionId);
|
if (!this.playerInterval) this.handlePlayStatus();
|
||||||
// 新增播放历史
|
// 新增播放历史
|
||||||
if (type !== "radio") dataStore.setHistory(musicStore.playSong);
|
if (type !== "radio") dataStore.setHistory(musicStore.playSong);
|
||||||
// 获取歌曲封面主色
|
// 获取歌曲封面主色
|
||||||
if (!path) getCoverColor(musicStore.songCover);
|
if (!path) runIdle(() => getCoverColor(musicStore.songCover));
|
||||||
// 更新 MediaSession
|
// 更新 MediaSession
|
||||||
if (!path) this.updateMediaSession();
|
if (!path) runIdle(() => this.updateMediaSession());
|
||||||
// 开发模式
|
// 开发模式
|
||||||
if (isDev) window.player = this.player;
|
if (isDev) window.player = this.player;
|
||||||
// 异步预载下一首播放地址(不阻塞当前播放)
|
// 预载下一首播放地址
|
||||||
void this.prefetchNextSongUrl();
|
runIdle(() => {
|
||||||
|
void this.prefetchNextSongUrl();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 播放器事件
|
* 播放器事件
|
||||||
@@ -246,8 +273,6 @@ class Player {
|
|||||||
options: {
|
options: {
|
||||||
// 恢复进度
|
// 恢复进度
|
||||||
seek?: number;
|
seek?: number;
|
||||||
// 当前会话 id,用于忽略过期事件
|
|
||||||
sessionId?: number;
|
|
||||||
} = { seek: 0 },
|
} = { seek: 0 },
|
||||||
) {
|
) {
|
||||||
// 获取数据
|
// 获取数据
|
||||||
@@ -257,7 +282,7 @@ class Player {
|
|||||||
const playSongData = getPlaySongData();
|
const playSongData = getPlaySongData();
|
||||||
// 获取配置
|
// 获取配置
|
||||||
const { seek } = options;
|
const { seek } = options;
|
||||||
const currentSessionId = options.sessionId ?? this.playSessionId;
|
const currentSessionId = this.playSessionId;
|
||||||
// 初次加载
|
// 初次加载
|
||||||
this.player.once("load", () => {
|
this.player.once("load", () => {
|
||||||
if (currentSessionId !== this.playSessionId) return;
|
if (currentSessionId !== this.playSessionId) return;
|
||||||
@@ -284,6 +309,14 @@ class Player {
|
|||||||
}
|
}
|
||||||
// 更新状态
|
// 更新状态
|
||||||
statusStore.playLoading = false;
|
statusStore.playLoading = false;
|
||||||
|
// 重置当前曲目重试计数
|
||||||
|
try {
|
||||||
|
const current = getPlaySongData();
|
||||||
|
const sid = current?.type === "radio" ? current?.dj?.id : current?.id;
|
||||||
|
this.retryInfo = { songId: Number(sid || 0), count: 0 };
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
// ipc
|
// ipc
|
||||||
if (isElectron) {
|
if (isElectron) {
|
||||||
window.electron.ipcRenderer.send("play-song-change", getPlayerInfo());
|
window.electron.ipcRenderer.send("play-song-change", getPlayerInfo());
|
||||||
@@ -297,6 +330,14 @@ class Player {
|
|||||||
this.player.on("play", () => {
|
this.player.on("play", () => {
|
||||||
if (currentSessionId !== this.playSessionId) return;
|
if (currentSessionId !== this.playSessionId) return;
|
||||||
window.document.title = getPlayerInfo() || "SPlayer";
|
window.document.title = getPlayerInfo() || "SPlayer";
|
||||||
|
// 重置重试计数
|
||||||
|
try {
|
||||||
|
const current = getPlaySongData();
|
||||||
|
const sid = current?.type === "radio" ? current?.dj?.id : current?.id;
|
||||||
|
this.retryInfo = { songId: Number(sid || 0), count: 0 };
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
// ipc
|
// ipc
|
||||||
if (isElectron) {
|
if (isElectron) {
|
||||||
window.electron.ipcRenderer.send("play-status-change", true);
|
window.electron.ipcRenderer.send("play-status-change", true);
|
||||||
@@ -338,9 +379,15 @@ class Player {
|
|||||||
this.player.on("loaderror", (sourceid, err: unknown) => {
|
this.player.on("loaderror", (sourceid, err: unknown) => {
|
||||||
if (currentSessionId !== this.playSessionId) return;
|
if (currentSessionId !== this.playSessionId) return;
|
||||||
const code = typeof err === "number" ? err : undefined;
|
const code = typeof err === "number" ? err : undefined;
|
||||||
this.errorNext(code);
|
this.handlePlaybackError(code);
|
||||||
console.error("❌ song error:", sourceid, playSongData, err);
|
console.error("❌ song error:", sourceid, playSongData, err);
|
||||||
});
|
});
|
||||||
|
this.player.on("playerror", (sourceid, err: unknown) => {
|
||||||
|
if (currentSessionId !== this.playSessionId) return;
|
||||||
|
const code = typeof err === "number" ? err : undefined;
|
||||||
|
this.handlePlaybackError(code);
|
||||||
|
console.error("❌ song play error:", sourceid, playSongData, err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 初始化 MediaSession
|
* 初始化 MediaSession
|
||||||
@@ -440,27 +487,31 @@ class Player {
|
|||||||
updateSpectrumData();
|
updateSpectrumData();
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 播放错误
|
* 集中处理播放错误与重试策略
|
||||||
* 在播放错误时,播放下一首
|
|
||||||
*/
|
*/
|
||||||
private async errorNext(errCode?: number) {
|
private async handlePlaybackError(errCode?: number) {
|
||||||
const dataStore = useDataStore();
|
const dataStore = useDataStore();
|
||||||
// 次数加一
|
const playSongData = getPlaySongData();
|
||||||
this.testNumber++;
|
const currentSongId = playSongData?.type === "radio" ? playSongData.dj?.id : playSongData?.id;
|
||||||
if (this.testNumber > 5) {
|
// 初始化/切换曲目时重置计数
|
||||||
this.testNumber = 0;
|
if (!this.retryInfo.songId || this.retryInfo.songId !== Number(currentSongId || 0)) {
|
||||||
this.resetStatus();
|
this.retryInfo = { songId: Number(currentSongId || 0), count: 0 };
|
||||||
window.$message.error("当前重试次数过多,请稍后再试");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
// 错误 2 通常为网络地址过期
|
this.retryInfo.count += 1;
|
||||||
if (errCode === 2) {
|
// 错误码 2:资源过期或临时网络错误,允许较少次数的刷新
|
||||||
// 重载播放器
|
if (errCode === 2 && this.retryInfo.count <= 2) {
|
||||||
await this.initPlayer(true, this.getSeek());
|
await this.initPlayer(true, this.getSeek());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 播放下一曲
|
// 其它错误:最多 3 次
|
||||||
|
if (this.retryInfo.count <= 3) {
|
||||||
|
await this.initPlayer(true, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 超过次数:切到下一首或清空
|
||||||
|
this.retryInfo.count = 0;
|
||||||
if (dataStore.playList.length > 1) {
|
if (dataStore.playList.length > 1) {
|
||||||
|
window.$message.error("当前歌曲播放失败,已跳至下一首");
|
||||||
await this.nextOrPrev("next");
|
await this.nextOrPrev("next");
|
||||||
} else {
|
} else {
|
||||||
window.$message.error("当前列表暂无可播放歌曲");
|
window.$message.error("当前列表暂无可播放歌曲");
|
||||||
@@ -498,12 +549,12 @@ class Player {
|
|||||||
musicStore.playSong.cover = "/images/song.jpg?assest";
|
musicStore.playSong.cover = "/images/song.jpg?assest";
|
||||||
}
|
}
|
||||||
// 获取主色
|
// 获取主色
|
||||||
getCoverColor(musicStore.playSong.cover);
|
runIdle(() => getCoverColor(musicStore.playSong.cover));
|
||||||
// 获取歌词数据
|
// 获取歌词数据
|
||||||
const { lyric, format } = await window.electron.ipcRenderer.invoke("get-music-lyric", path);
|
const { lyric, format } = await window.electron.ipcRenderer.invoke("get-music-lyric", path);
|
||||||
parseLocalLyric(lyric, format);
|
parseLocalLyric(lyric, format);
|
||||||
// 更新媒体会话
|
// 更新媒体会话
|
||||||
this.updateMediaSession();
|
runIdle(() => this.updateMediaSession());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.$message.error("获取本地歌曲元信息失败");
|
window.$message.error("获取本地歌曲元信息失败");
|
||||||
console.error("Failed to parse local music info:", error);
|
console.error("Failed to parse local music info:", error);
|
||||||
@@ -540,7 +591,7 @@ class Player {
|
|||||||
const musicStore = useMusicStore();
|
const musicStore = useMusicStore();
|
||||||
const statusStore = useStatusStore();
|
const statusStore = useStatusStore();
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const sessionId = ++this.playSessionId;
|
const sessionId = this.newSession();
|
||||||
try {
|
try {
|
||||||
// 获取播放数据
|
// 获取播放数据
|
||||||
const playSongData = getPlaySongData();
|
const playSongData = getPlaySongData();
|
||||||
@@ -550,9 +601,12 @@ class Player {
|
|||||||
musicStore.playSong = playSongData;
|
musicStore.playSong = playSongData;
|
||||||
// 更改状态
|
// 更改状态
|
||||||
statusStore.playLoading = true;
|
statusStore.playLoading = true;
|
||||||
|
// 清理旧播放器与计时器
|
||||||
|
this.resetPlayerCore();
|
||||||
// 本地歌曲
|
// 本地歌曲
|
||||||
if (path) {
|
if (path) {
|
||||||
await this.createPlayer(`file://${path}`, autoPlay, seek, sessionId);
|
if (this.isStale(sessionId)) return;
|
||||||
|
await this.createPlayer(`file://${path}`, autoPlay, seek);
|
||||||
// 获取歌曲元信息
|
// 获取歌曲元信息
|
||||||
await this.parseLocalMusicInfo(path);
|
await this.parseLocalMusicInfo(path);
|
||||||
}
|
}
|
||||||
@@ -564,7 +618,8 @@ class Player {
|
|||||||
const cached = this.nextPrefetch;
|
const cached = this.nextPrefetch;
|
||||||
if (cached && cached.id === songId && cached.url) {
|
if (cached && cached.id === songId && cached.url) {
|
||||||
statusStore.playUblock = cached.ublock;
|
statusStore.playUblock = cached.ublock;
|
||||||
await this.createPlayer(cached.url, autoPlay, seek, sessionId);
|
if (this.isStale(sessionId)) return;
|
||||||
|
await this.createPlayer(cached.url, autoPlay, seek);
|
||||||
} else {
|
} else {
|
||||||
// 官方地址失败或仅为试听时再尝试解锁(Electron 且非电台且开启解灰)
|
// 官方地址失败或仅为试听时再尝试解锁(Electron 且非电台且开启解灰)
|
||||||
const canUnlock = isElectron && type !== "radio" && settingStore.useSongUnlock;
|
const canUnlock = isElectron && type !== "radio" && settingStore.useSongUnlock;
|
||||||
@@ -572,20 +627,23 @@ class Player {
|
|||||||
if (officialUrl && !isTrial) {
|
if (officialUrl && !isTrial) {
|
||||||
// 官方可播放且非试听
|
// 官方可播放且非试听
|
||||||
statusStore.playUblock = false;
|
statusStore.playUblock = false;
|
||||||
await this.createPlayer(officialUrl, autoPlay, seek, sessionId);
|
if (this.isStale(sessionId)) return;
|
||||||
|
await this.createPlayer(officialUrl, autoPlay, seek);
|
||||||
} else if (canUnlock) {
|
} else if (canUnlock) {
|
||||||
// 官方失败或为试听时尝试解锁
|
// 官方失败或为试听时尝试解锁
|
||||||
const unlockUrl = await getUnlockSongUrl(playSongData);
|
const unlockUrl = await getUnlockSongUrl(playSongData);
|
||||||
if (unlockUrl) {
|
if (unlockUrl) {
|
||||||
statusStore.playUblock = true;
|
statusStore.playUblock = true;
|
||||||
console.log("🎼 Song unlock successfully:", unlockUrl);
|
console.log("🎼 Song unlock successfully:", unlockUrl);
|
||||||
await this.createPlayer(unlockUrl, autoPlay, seek, sessionId);
|
if (this.isStale(sessionId)) return;
|
||||||
|
await this.createPlayer(unlockUrl, autoPlay, seek);
|
||||||
} else if (officialUrl) {
|
} else if (officialUrl) {
|
||||||
// 解锁失败,若允许试听则播放试听
|
// 解锁失败,若允许试听则播放试听
|
||||||
if (isTrial && settingStore.playSongDemo) {
|
if (isTrial && settingStore.playSongDemo) {
|
||||||
window.$message.warning("当前歌曲仅可试听,请开通会员后重试");
|
window.$message.warning("当前歌曲仅可试听,请开通会员后重试");
|
||||||
statusStore.playUblock = false;
|
statusStore.playUblock = false;
|
||||||
await this.createPlayer(officialUrl, autoPlay, seek, sessionId);
|
if (this.isStale(sessionId)) return;
|
||||||
|
await this.createPlayer(officialUrl, autoPlay, seek);
|
||||||
} else {
|
} else {
|
||||||
// 不允许试听
|
// 不允许试听
|
||||||
statusStore.playUblock = false;
|
statusStore.playUblock = false;
|
||||||
@@ -663,6 +721,7 @@ class Player {
|
|||||||
async pause(changeStatus: boolean = true) {
|
async pause(changeStatus: boolean = true) {
|
||||||
const statusStore = useStatusStore();
|
const statusStore = useStatusStore();
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
|
const localSession = this.playSessionId;
|
||||||
|
|
||||||
// 播放器未加载完成或不存在
|
// 播放器未加载完成或不存在
|
||||||
if (!this.player || this.player.state() !== "loaded") {
|
if (!this.player || this.player.state() !== "loaded") {
|
||||||
@@ -675,7 +734,7 @@ class Player {
|
|||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
this.player.fade(statusStore.playVolume, 0, settingStore.getFadeTime);
|
this.player.fade(statusStore.playVolume, 0, settingStore.getFadeTime);
|
||||||
this.player.once("fade", () => {
|
this.player.once("fade", () => {
|
||||||
this.player.pause();
|
this.guard(localSession, () => this.player.pause());
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
38
src/views/DesktopLyrics/index.d.ts
vendored
Normal file
38
src/views/DesktopLyrics/index.d.ts
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { LyricType } from "@/types/main";
|
||||||
|
|
||||||
|
/** 桌面歌词数据 */
|
||||||
|
export interface LyricData {
|
||||||
|
/** 播放歌曲名称 */
|
||||||
|
playName: string;
|
||||||
|
/** 播放状态 */
|
||||||
|
playStatus: boolean;
|
||||||
|
/** 播放进度 */
|
||||||
|
progress: number;
|
||||||
|
/** 歌词数据 */
|
||||||
|
lrcData: LyricType[];
|
||||||
|
yrcData: LyricType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 桌面歌词配置 */
|
||||||
|
export interface LyricConfig {
|
||||||
|
/** 是否锁定歌词 */
|
||||||
|
isLock: boolean;
|
||||||
|
/** 已播放颜色 */
|
||||||
|
playedColor: string;
|
||||||
|
/** 未播放颜色 */
|
||||||
|
unplayedColor: string;
|
||||||
|
/** 描边 */
|
||||||
|
stroke: string;
|
||||||
|
/** 描边宽度 */
|
||||||
|
strokeWidth: number;
|
||||||
|
/** 字体 */
|
||||||
|
fontFamily: string;
|
||||||
|
/** 字体大小 */
|
||||||
|
fontSize: number;
|
||||||
|
/** 行高 */
|
||||||
|
lineHeight: number;
|
||||||
|
/** 是否双行 */
|
||||||
|
isDoubleLine: boolean;
|
||||||
|
/** 文本排版位置 */
|
||||||
|
position: "left" | "center" | "right" | "both";
|
||||||
|
}
|
||||||
@@ -4,45 +4,71 @@
|
|||||||
{{ lyricData.playName }}
|
{{ lyricData.playName }}
|
||||||
{{ lyricConfig }}
|
{{ lyricConfig }}
|
||||||
456456
|
456456
|
||||||
<n-flex class="menu" align="center" justify="space-between">
|
<div class="header" align="center" justify="space-between">
|
||||||
<n-flex class="name">
|
<n-flex :wrap="false" align="center" justify="flex-start" size="small">
|
||||||
<div class="menu-btn">
|
<div class="menu-btn" title="返回应用">
|
||||||
<SvgIcon name="Logo" />
|
<SvgIcon name="Music" />
|
||||||
|
</div>
|
||||||
|
<div class="menu-btn" title="增加字体大小">
|
||||||
|
<SvgIcon :offset="-1" name="TextSizeAdd" />
|
||||||
|
</div>
|
||||||
|
<div class="menu-btn" title="减少字体大小">
|
||||||
|
<SvgIcon :offset="-1" name="TextSizeReduce" />
|
||||||
|
</div>
|
||||||
|
<span class="song-name">{{ lyricData.playName }}</span>
|
||||||
|
</n-flex>
|
||||||
|
<n-flex :wrap="false" align="center" justify="center" size="small">
|
||||||
|
<div class="menu-btn" title="上一曲">
|
||||||
|
<SvgIcon name="SkipPrev" />
|
||||||
|
</div>
|
||||||
|
<div class="menu-btn" :title="lyricData.playStatus ? '暂停' : '播放'">
|
||||||
|
<SvgIcon :name="lyricData.playStatus ? 'Pause' : 'Play'" />
|
||||||
|
</div>
|
||||||
|
<div class="menu-btn" title="下一曲">
|
||||||
|
<SvgIcon name="SkipNext" />
|
||||||
</div>
|
</div>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</n-flex>
|
<n-flex :wrap="false" align="center" justify="flex-end" size="small">
|
||||||
|
<div class="menu-btn" title="锁定">
|
||||||
|
<SvgIcon name="Lock" />
|
||||||
|
</div>
|
||||||
|
<div class="menu-btn" title="解锁">
|
||||||
|
<SvgIcon name="LockOpen" />
|
||||||
|
</div>
|
||||||
|
<div class="menu-btn" title="关闭">
|
||||||
|
<SvgIcon name="Close" />
|
||||||
|
</div>
|
||||||
|
</n-flex>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</n-config-provider>
|
</n-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { LyricType } from "@/types/main";
|
|
||||||
import { Position } from "@vueuse/core";
|
import { Position } from "@vueuse/core";
|
||||||
|
import { LyricConfig, LyricData } from ".";
|
||||||
|
|
||||||
// 桌面歌词数据
|
// 桌面歌词数据
|
||||||
const lyricData = reactive<{
|
const lyricData = reactive<LyricData>({
|
||||||
playName: string;
|
|
||||||
playStatus: boolean;
|
|
||||||
progress: number;
|
|
||||||
lrcData: LyricType[];
|
|
||||||
yrcData: LyricType[];
|
|
||||||
lyricIndex: number;
|
|
||||||
}>({
|
|
||||||
playName: "未知歌曲",
|
playName: "未知歌曲",
|
||||||
playStatus: false,
|
playStatus: false,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
lrcData: [],
|
lrcData: [],
|
||||||
yrcData: [],
|
yrcData: [],
|
||||||
lyricIndex: -1,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 桌面歌词配置
|
// 桌面歌词配置
|
||||||
const lyricConfig = reactive<{
|
const lyricConfig = reactive<LyricConfig>({
|
||||||
fontSize: number;
|
isLock: false,
|
||||||
lineHeight: number;
|
playedColor: "#fff",
|
||||||
}>({
|
unplayedColor: "#ccc",
|
||||||
|
stroke: "#000",
|
||||||
|
strokeWidth: 2,
|
||||||
|
fontFamily: "system-ui",
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
lineHeight: 48,
|
lineHeight: 48,
|
||||||
|
isDoubleLine: false,
|
||||||
|
position: "center",
|
||||||
});
|
});
|
||||||
|
|
||||||
// 桌面歌词元素
|
// 桌面歌词元素
|
||||||
@@ -57,6 +83,10 @@ const lyricDragMove = (position: Position) => {
|
|||||||
useDraggable(desktopLyricsRef, {
|
useDraggable(desktopLyricsRef, {
|
||||||
onMove: lyricDragMove,
|
onMove: lyricDragMove,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 接收歌词配置
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -68,16 +98,62 @@ useDraggable(desktopLyricsRef, {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: background-color 0.3s;
|
transition: background-color 0.3s;
|
||||||
cursor: move;
|
cursor: move;
|
||||||
|
.header {
|
||||||
|
opacity: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
// 子内容三等分grid
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
grid-gap: 12px;
|
||||||
|
> * {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.song-name {
|
||||||
|
font-size: 1em;
|
||||||
|
text-align: left;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.menu-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
will-change: transform;
|
||||||
|
transition:
|
||||||
|
background-color 0.3s,
|
||||||
|
transform 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
.n-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
&:not(.lock) {
|
&:not(.lock) {
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
.header {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- <style>
|
<style>
|
||||||
body {
|
body {
|
||||||
background-image: url("https://picsum.photos/1920/1080");
|
background-image: url("https://picsum.photos/1920/1080");
|
||||||
}
|
}
|
||||||
</style> -->
|
</style>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"]
|
"@/*": ["src/*"]
|
||||||
},
|
},
|
||||||
"types": ["node", "electron", "electron-vite/node", "./auto-imports.d.ts"],
|
"types": ["node", "electron", "electron-vite/node", "./auto-imports.d.ts", "./components.d.ts"],
|
||||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
"target": "es2022"
|
"target": "es2022"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user