-
-
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatTime(playerStore.currentTime) }}
+
+
+ {{ showRemaining ? `-${formatTime(playerStore.duration - playerStore.currentTime)}` : formatTime(playerStore.duration) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
{{ currentSong?.name || 'No Music' }}
-
{{ currentSong?.artist }}
-
-
-
+
+
diff --git a/src/renderer/src/components/player/BouncingSlider.vue b/src/renderer/src/components/player/BouncingSlider.vue
new file mode 100644
index 0000000..63e4926
--- /dev/null
+++ b/src/renderer/src/components/player/BouncingSlider.vue
@@ -0,0 +1,223 @@
+
+
+
+
+
+
+
diff --git a/src/renderer/src/components/player/ControlThumb.vue b/src/renderer/src/components/player/ControlThumb.vue
new file mode 100644
index 0000000..5af7c2e
--- /dev/null
+++ b/src/renderer/src/components/player/ControlThumb.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/renderer/src/components/player/Cover.vue b/src/renderer/src/components/player/Cover.vue
new file mode 100644
index 0000000..4241b56
--- /dev/null
+++ b/src/renderer/src/components/player/Cover.vue
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
diff --git a/src/renderer/src/components/player/MediaButton.vue b/src/renderer/src/components/player/MediaButton.vue
new file mode 100644
index 0000000..2c713a8
--- /dev/null
+++ b/src/renderer/src/components/player/MediaButton.vue
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
diff --git a/src/renderer/src/components/player/MusicInfo.vue b/src/renderer/src/components/player/MusicInfo.vue
new file mode 100644
index 0000000..bf722c9
--- /dev/null
+++ b/src/renderer/src/components/player/MusicInfo.vue
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
diff --git a/src/renderer/src/components/PlayerBar.vue b/src/renderer/src/components/player/PlayerBar.vue
similarity index 99%
rename from src/renderer/src/components/PlayerBar.vue
rename to src/renderer/src/components/player/PlayerBar.vue
index 10b5d1e..fdaccb9 100644
--- a/src/renderer/src/components/PlayerBar.vue
+++ b/src/renderer/src/components/player/PlayerBar.vue
@@ -103,7 +103,7 @@
+
+
diff --git a/src/renderer/src/components/player/ToggleIconButton.vue b/src/renderer/src/components/player/ToggleIconButton.vue
new file mode 100644
index 0000000..c9c4ac7
--- /dev/null
+++ b/src/renderer/src/components/player/ToggleIconButton.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
diff --git a/src/renderer/src/components/player/VolumeControl.vue b/src/renderer/src/components/player/VolumeControl.vue
new file mode 100644
index 0000000..80e7128
--- /dev/null
+++ b/src/renderer/src/components/player/VolumeControl.vue
@@ -0,0 +1,191 @@
+
+
+
+
+
+
+
diff --git a/src/renderer/src/env.d.ts b/src/renderer/src/env.d.ts
new file mode 100644
index 0000000..53758af
--- /dev/null
+++ b/src/renderer/src/env.d.ts
@@ -0,0 +1,14 @@
+///
+///
+
+declare module '*.svg' {
+ import { Component } from 'vue';
+ const content: Component;
+ export default content;
+}
+
+declare module '*.svg?component' {
+ import { Component } from 'vue';
+ const content: Component;
+ export default content;
+}
diff --git a/src/renderer/src/layout/MainLayout.vue b/src/renderer/src/layout/MainLayout.vue
index 2a7787d..5e3db62 100644
--- a/src/renderer/src/layout/MainLayout.vue
+++ b/src/renderer/src/layout/MainLayout.vue
@@ -19,7 +19,7 @@
import { computed } from 'vue';
import Sidebar from '../components/Sidebar.vue';
import TopBar from '../components/TopBar.vue';
-import PlayerBar from '../components/PlayerBar.vue';
+import PlayerBar from '../components/player/PlayerBar.vue';
import { usePlayerStore } from '../stores/player';
const playerStore = usePlayerStore();
diff --git a/src/renderer/src/shims.d.ts b/src/renderer/src/shims.d.ts
new file mode 100644
index 0000000..7d50869
--- /dev/null
+++ b/src/renderer/src/shims.d.ts
@@ -0,0 +1,14 @@
+declare module '*.svg' {
+ const content: string;
+ export default content;
+}
+
+declare module '*.png' {
+ const content: string;
+ export default content;
+}
+
+declare module '*.jpg' {
+ const content: string;
+ export default content;
+}
diff --git a/src/renderer/src/stores/player.ts b/src/renderer/src/stores/player.ts
index 7822ca8..2a76370 100644
--- a/src/renderer/src/stores/player.ts
+++ b/src/renderer/src/stores/player.ts
@@ -1,8 +1,8 @@
import { defineStore } from 'pinia';
-import { ref, watch } from 'vue';
+import { ref, shallowRef, watch } from 'vue';
import { MessagePlugin } from 'tdesign-vue-next';
import type { Song } from '../types/song';
-
+import { parseLyric } from '../utils/lyricUtil'
export enum PlayMode {
List = 'list',
Single = 'single',
@@ -44,6 +44,7 @@ export const usePlayerStore = defineStore('player', () => {
// UI State
const isPlayerFullScreen = ref(false);
+ const hideLyricView = ref(false);
// Playlist State
const savedPlaylist = localStorage.getItem('qz-player-playlist');
@@ -61,7 +62,7 @@ export const usePlayerStore = defineStore('player', () => {
const hasRetriedWithFreshUrl = ref(false);
// Lyrics State
- const lyrics = ref<{ lines: any[] }>({ lines: [] });
+ const lyrics = shallowRef<{ lines: any[] }>({ lines: [] });
// --- Helpers ---
const activateDummyAudio = async () => {
@@ -139,7 +140,7 @@ export const usePlayerStore = defineStore('player', () => {
await activateDummyAudio();
updateMediaSession(song);
- // fetchLyrics(song);
+ fetchLyrics(song);
// Get URL (Cache -> Network)
let playUrl = song.url;
@@ -184,13 +185,14 @@ export const usePlayerStore = defineStore('player', () => {
lyrics.value = { lines: [] }; // Reset
if (!song || !song.id) return;
try {
- // Check if plugin API exists
- // if (window.electronAPI?.plugin?.getLyric) {
- // const rawLyric = await window.electronAPI.plugin.getLyric(song.source || 'kw', song.id.toString());
- // console.log(rawLyric)
- // } else {
- // MessagePlugin.warning("当前插件不支持歌词获取").then()
- // }
+ //Check if plugin API exists
+ if (window.electronAPI?.plugin?.getLyric) {
+ const rawLyric = await window.electronAPI.plugin.getLyric(song.source || 'kw', song.id.toString());
+ lyrics.value = { lines: parseLyric(rawLyric) }
+ console.log(lyrics.value)
+ } else {
+ MessagePlugin.warning("当前插件不支持歌词获取").then()
+ }
} catch (e) {
console.error('Failed to fetch lyrics:', e);
}
@@ -335,7 +337,7 @@ export const usePlayerStore = defineStore('player', () => {
if (restoredSong) {
// Use playSong with autoPlay=false to load the song into the engine
setTimeout(() => {
- playSong(restoredSong, false);
+ playSong(restoredSong, true);
fetchLyrics(restoredSong);
}, 0);
}
@@ -364,6 +366,7 @@ export const usePlayerStore = defineStore('player', () => {
lyrics,
fetchLyrics,
addListMode,
- playFromList
+ playFromList,
+ hideLyricView
};
});
\ No newline at end of file
diff --git a/src/renderer/src/styles/main.css b/src/renderer/src/styles/main.css
index 4426550..5d89f6e 100644
--- a/src/renderer/src/styles/main.css
+++ b/src/renderer/src/styles/main.css
@@ -1,8 +1,9 @@
@import 'variables.css';
-* {
+* , *::before, *::after {
margin: 0;
padding: 0;
+ box-sizing: border-box;
}
body {
diff --git a/src/renderer/src/utils/lyricUtil.ts b/src/renderer/src/utils/lyricUtil.ts
new file mode 100644
index 0000000..980e496
--- /dev/null
+++ b/src/renderer/src/utils/lyricUtil.ts
@@ -0,0 +1,57 @@
+import {LyricLine, parseLrc, parseQrc, parseTTML, parseYrc} from "@applemusic-like-lyrics/lyric";
+const sanitizeLyricLines = (lines: LyricLine[]): LyricLine[] => {
+ const defaultLineDuration = 3000
+ const toFiniteNumber = (v: any, fallback: number) => {
+ const n = typeof v === 'number' ? v : Number(v)
+ return Number.isFinite(n) ? n : fallback
+ }
+ const cleaned: LyricLine[] = []
+ for (const rawLine of lines || []) {
+ const rawWords = Array.isArray((rawLine as any).words) ? (rawLine as any).words : []
+ const fixedWords: any[] = []
+ let prevEnd = -1
+ for (const rawWord of rawWords) {
+ const rawStart = toFiniteNumber(rawWord?.startTime, Number.NaN)
+ const rawEnd = toFiniteNumber(rawWord?.endTime, Number.NaN)
+ if (!Number.isFinite(rawStart)) continue
+ let startTime = Math.max(0, rawStart)
+ if (startTime < prevEnd) startTime = prevEnd
+ let endTime = Number.isFinite(rawEnd) ? rawEnd : startTime + 1
+ if (endTime <= startTime) endTime = startTime + 1
+ prevEnd = endTime
+ fixedWords.push({ ...rawWord, startTime, endTime })
+ }
+ if (fixedWords.length === 0) continue
+
+ const firstWordStart = fixedWords[0].startTime
+ const lastWordEnd = fixedWords[fixedWords.length - 1].endTime
+ let startTime = toFiniteNumber((rawLine as any).startTime, firstWordStart)
+ startTime = Math.max(0, startTime)
+ let endTime = toFiniteNumber((rawLine as any).endTime, lastWordEnd)
+ if (!Number.isFinite(endTime) || endTime <= startTime) endTime = startTime + defaultLineDuration
+ if (endTime < lastWordEnd) endTime = lastWordEnd
+
+ cleaned.push({ ...(rawLine as any), startTime, endTime, words: fixedWords })
+ }
+ cleaned.sort((a: any, b: any) => (a?.startTime ?? 0) - (b?.startTime ?? 0))
+ return cleaned
+}
+interface LyricData {
+ ttml?: string,
+ yrc?: string,
+ lrc?: string,
+ qrc?: string
+}
+export function parseLyric(lyric: LyricData):LyricLine[] {
+ let parsed:LyricLine[] = []
+ if (lyric.ttml != undefined) {
+ parsed = parseTTML(lyric.ttml).lines;
+ } else if (lyric.yrc != undefined) {
+ parsed = parseYrc(lyric.yrc);
+ } else if (lyric.lrc != undefined) {
+ parsed = parseLrc(lyric.lrc);
+ } else if (lyric.qrc != undefined) {
+ parsed = parseQrc(lyric.qrc)
+ }
+ return sanitizeLyricLines(parsed);
+}
\ No newline at end of file
diff --git a/tsconfig.web.json b/tsconfig.web.json
new file mode 100644
index 0000000..c5fa8b4
--- /dev/null
+++ b/tsconfig.web.json
@@ -0,0 +1,19 @@
+{
+ "extends": "@electron-toolkit/tsconfig/tsconfig.web.json",
+ "include": [
+ "src/renderer/src/env.d.ts",
+ "src/renderer/src/**/*",
+ "src/renderer/src/**/*.vue",
+ "src/preload/*.d.ts"
+ ],
+ "compilerOptions": {
+ "composite": true,
+ "baseUrl": ".",
+ "paths": {
+ "@renderer/*": [
+ "src/renderer/src/*"
+ ]
+ },
+ },
+
+}