forked from miao-moe/QZMusic_PC
fork(fix): Clone AMLL 并修复 BUG
- 将AMLL Clone到本以地进行修复和优化(emm虽然这很不优雅但是暂时无时间做子模块和Fork) - 修复在当前播放歌词行不可见的视口Seek会出现滚动偏移的问题
This commit is contained in:
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url'
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
import { QzpController } from './qzpController'
|
||||
import { startProxyServer, cleanupCache, getCacheDir, getCacheSize, setPersistCache, clearCacheNow } from './proxyServer'
|
||||
import { startProxyServer, cleanupCache, getCacheDir, getCacheSize, setPersistCache, clearCacheNow, refreshCacheDir } from './proxyServer'
|
||||
import { PluginSystem } from './pluginSystem'
|
||||
import { loadSettings, saveSettings, getSetting, AppSettings } from './settingsStore'
|
||||
|
||||
@@ -202,23 +202,8 @@ ipcMain.handle('cache:changeLocation', async (_, newPath: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Update Settings
|
||||
saveSettings({ cachePath: newPath })
|
||||
|
||||
// 4. Update Proxy Server if needed (it reads from settings or we notify it)
|
||||
// Ideally proxyServer should just use `getCacheDir()` which now reads from settings?
|
||||
// Wait, `getCacheDir` in proxyServer.ts probably uses hardcoded or internal variable.
|
||||
// We need to update proxyServer.ts logic too or restart it.
|
||||
// For now, let's assume getCacheDir() needs update or we restart proxy.
|
||||
// Actually, let's make sure proxyServer gets the new path.
|
||||
// We'll restart proxy to be safe or add a setPath method.
|
||||
// *Self-correction*: The simple way is to restart the proxy server function implies checking `getCacheDir` from settings.
|
||||
// Let's ensure proxyServer.ts `setCacheDir` exists or similar.
|
||||
|
||||
// RE-CHECK: `proxyServer.ts` isn't fully visible here.
|
||||
// I'll add a TODO/Warning in comments and handle proxy update via current imports if possible.
|
||||
// Since I can't `import { setCachePath } from './proxyServer'` yet (I haven't checked if it exists),
|
||||
// I will trust that the next step or automatic reload handles it, OR better: implement `updateCachePath` in proxyServer.
|
||||
refreshCacheDir()
|
||||
|
||||
return { success: true, message: 'Cache location updated', path: newPath }
|
||||
} catch (e: any) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,29 @@
|
||||
//@ts-ignore
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import { Socket } from 'net';
|
||||
import { EventEmitter } from 'events';
|
||||
import path from 'path';
|
||||
import { spawn, type ChildProcess } from 'node:child_process';
|
||||
import { Socket } from 'node:net';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import path from 'node:path';
|
||||
import { app } from 'electron';
|
||||
import { MessagePlugin } from "tdesign-vue-next";
|
||||
|
||||
export class QzpController extends EventEmitter {
|
||||
private process: ChildProcess | null = null;
|
||||
private socket: Socket | null = null;
|
||||
private ipcPath: string;
|
||||
private messageBuffer: string = '';
|
||||
private readonly ipcPath: string;
|
||||
private messageBuffer = '';
|
||||
private connectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private restartTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private pendingCommands: any[][] = [];
|
||||
private currentUrl: string | null = null;
|
||||
private lastKnownTime = 0;
|
||||
private desiredPause = true;
|
||||
private volume = 50;
|
||||
private isConnected = false;
|
||||
private isRestarting = false;
|
||||
private isShuttingDown = false;
|
||||
|
||||
private readonly connectRetryDelay = 250;
|
||||
private readonly maxConnectRetries = 20;
|
||||
private readonly maxPendingCommands = 50;
|
||||
private readonly restartDelay = 150;
|
||||
|
||||
constructor(ipcPath?: string) {
|
||||
super();
|
||||
@@ -32,138 +45,341 @@ export class QzpController extends EventEmitter {
|
||||
return path.join(appRoot, 'core', 'qzplayer.exe');
|
||||
}
|
||||
|
||||
start() {
|
||||
start(): void {
|
||||
if (this.process || this.connectTimer || this.isRestarting) return;
|
||||
|
||||
this.isShuttingDown = false;
|
||||
this.messageBuffer = '';
|
||||
|
||||
const playerPath = this.getCorePath();
|
||||
console.log('Starting QZPlayer from:', playerPath);
|
||||
|
||||
this.process = spawn(playerPath);
|
||||
try {
|
||||
const child = spawn(playerPath, [], {
|
||||
stdio: 'ignore',
|
||||
windowsHide: true,
|
||||
});
|
||||
this.process = child;
|
||||
|
||||
this.process.on('error', (err) => {
|
||||
console.error('Failed to start QZPlayer:', err);
|
||||
MessagePlugin.error(`启动播放核心时出现异常:${err}`).then();
|
||||
this.emit('error', err);
|
||||
});
|
||||
child.once('error', (err) => {
|
||||
if (this.process === child) this.process = null;
|
||||
console.error('Failed to start QZPlayer:', err);
|
||||
this.emitError(err);
|
||||
this.restart(`process error: ${err.message}`);
|
||||
});
|
||||
|
||||
this.process.on('exit', (code, signal) => {
|
||||
console.log(`QZPlayer exited with code ${code} and signal ${signal}`);
|
||||
//MessagePlugin.error(`播放核心异常退出!code:${code},signal:${signal}`).then();
|
||||
this.emit('exit', { code, signal });
|
||||
this.socket?.destroy();
|
||||
});
|
||||
child.once('exit', (code, signal) => {
|
||||
if (this.process === child) this.process = null;
|
||||
console.log(`QZPlayer exited with code ${code} and signal ${signal}`);
|
||||
this.emit('exit', { code, signal });
|
||||
this.destroySocket();
|
||||
this.restart(`process exited: code=${code}, signal=${signal}`);
|
||||
});
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error(String(err));
|
||||
console.error('Failed to spawn QZPlayer:', error);
|
||||
this.emitError(error);
|
||||
this.restart(`spawn failed: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.tryConnect();
|
||||
}
|
||||
|
||||
private tryConnect(retries = 10) {
|
||||
if (retries <= 0) {
|
||||
console.error('Could not connect to QZPlayer socket after multiple attempts.');
|
||||
return;
|
||||
}
|
||||
private tryConnect(retries = this.maxConnectRetries): void {
|
||||
if (this.isShuttingDown || this.isRestarting) return;
|
||||
|
||||
setTimeout(() => {
|
||||
this.socket = new Socket();
|
||||
this.clearConnectTimer();
|
||||
this.connectTimer = setTimeout(() => {
|
||||
this.connectTimer = null;
|
||||
if (this.isShuttingDown || this.isRestarting) return;
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
const socket = new Socket();
|
||||
let connected = false;
|
||||
let handledConnectFailure = false;
|
||||
this.socket = socket;
|
||||
|
||||
socket.once('connect', () => {
|
||||
connected = true;
|
||||
this.isConnected = true;
|
||||
this.messageBuffer = '';
|
||||
console.log('Connected to QZPlayer IPC socket');
|
||||
this.emit('ready');
|
||||
|
||||
this.send(['observe_property', 1, 'pause']);
|
||||
this.send(['observe_property', 2, 'time-pos']);
|
||||
this.send(['observe_property', 3, 'duration']);
|
||||
this.send(['observe_property', 4, 'idle-active']);
|
||||
this.send(['observe_property', 5, 'eof-reached']);
|
||||
this.send(['set_property', 'volume', 50])
|
||||
this.initPlayerState();
|
||||
this.restorePlaybackState();
|
||||
this.flushPendingCommands();
|
||||
});
|
||||
|
||||
this.socket.on('data', (data) => {
|
||||
socket.on('data', (data) => {
|
||||
this.handleData(data);
|
||||
});
|
||||
|
||||
this.socket.on('error', (_) => {
|
||||
this.socket?.destroy();
|
||||
this.tryConnect(retries - 1);
|
||||
socket.once('error', (err) => {
|
||||
if (!connected) {
|
||||
handledConnectFailure = true;
|
||||
this.destroySocket(socket);
|
||||
if (retries > 1) {
|
||||
this.tryConnect(retries - 1);
|
||||
} else {
|
||||
this.restart(`socket connect failed: ${err.message}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn('QZPlayer IPC socket error:', err);
|
||||
this.restart(`socket error: ${err.message}`);
|
||||
});
|
||||
|
||||
this.socket.connect(this.ipcPath);
|
||||
}, 500);
|
||||
socket.once('close', () => {
|
||||
if (this.socket === socket) {
|
||||
this.socket = null;
|
||||
this.isConnected = false;
|
||||
}
|
||||
|
||||
if (this.isShuttingDown || this.isRestarting) return;
|
||||
|
||||
if (connected) {
|
||||
this.restart('socket closed');
|
||||
} else if (!handledConnectFailure) {
|
||||
if (retries > 1) {
|
||||
this.tryConnect(retries - 1);
|
||||
} else {
|
||||
this.restart('socket closed before connect');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.connect(this.ipcPath);
|
||||
}, this.connectRetryDelay);
|
||||
}
|
||||
|
||||
private handleData(data: Buffer) {
|
||||
// Determine message boundaries by newline
|
||||
private initPlayerState(): void {
|
||||
this.send(['observe_property', 1, 'pause']);
|
||||
this.send(['observe_property', 2, 'time-pos']);
|
||||
this.send(['observe_property', 3, 'duration']);
|
||||
this.send(['observe_property', 4, 'idle-active']);
|
||||
this.send(['observe_property', 5, 'eof-reached']);
|
||||
this.send(['set_property', 'volume', this.volume]);
|
||||
}
|
||||
|
||||
private restorePlaybackState(): void {
|
||||
const hasPendingLoad = this.pendingCommands.some((command) => command[0] === 'loadfile');
|
||||
if (!this.currentUrl || hasPendingLoad) return;
|
||||
|
||||
const restoreTime = this.lastKnownTime;
|
||||
this.send(['loadfile', this.currentUrl]);
|
||||
if (restoreTime > 0) {
|
||||
this.send(['seek', restoreTime, 'absolute']);
|
||||
}
|
||||
this.send(['set_property', 'pause', this.desiredPause]);
|
||||
}
|
||||
|
||||
private restart(reason: string): void {
|
||||
if (this.isShuttingDown || this.isRestarting) return;
|
||||
|
||||
console.warn(`Restarting QZPlayer: ${reason}`);
|
||||
this.isRestarting = true;
|
||||
this.emit('restart', { reason });
|
||||
|
||||
this.clearConnectTimer();
|
||||
this.clearRestartTimer();
|
||||
this.destroySocket();
|
||||
this.killProcess();
|
||||
|
||||
this.restartTimer = setTimeout(() => {
|
||||
this.restartTimer = null;
|
||||
this.isRestarting = false;
|
||||
if (!this.isShuttingDown) {
|
||||
this.start();
|
||||
}
|
||||
}, this.restartDelay);
|
||||
}
|
||||
|
||||
private handleData(data: Buffer): void {
|
||||
const raw = data.toString();
|
||||
|
||||
this.messageBuffer += raw;
|
||||
const messages = this.messageBuffer.split('\n');
|
||||
|
||||
// The last part might be an incomplete message, save it back to buffer
|
||||
this.messageBuffer = messages.pop() || '';
|
||||
|
||||
for (const msg of messages) {
|
||||
if (!msg.trim()) continue;
|
||||
//console.log('[IPC]', msg); // User requested raw communication
|
||||
try {
|
||||
const json = JSON.parse(msg);
|
||||
this.trackPlayerState(json);
|
||||
this.emit('message', json);
|
||||
|
||||
// Handle specific events
|
||||
if (json.event) {
|
||||
this.emit('event', json);
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
console.error('Failed to parse QZPlayer message:', msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async send(command: any[]) {
|
||||
if (!this.socket || this.socket.destroyed) {
|
||||
async send(command: any[]): Promise<void> {
|
||||
this.trackOutgoingCommand(command);
|
||||
|
||||
if (!this.socket || this.socket.destroyed || !this.isConnected) {
|
||||
console.warn('QZPlayer socket not connected');
|
||||
this.queueCommand(command);
|
||||
if (!this.process && !this.isRestarting && !this.isShuttingDown) {
|
||||
this.start();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendNow(command);
|
||||
}
|
||||
|
||||
private sendNow(command: any[]): void {
|
||||
if (!this.socket || this.socket.destroyed || !this.isConnected) {
|
||||
this.queueCommand(command);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = JSON.stringify({ command });
|
||||
console.log('[QZPlayer TX]', payload); // User requested raw communication
|
||||
this.socket.write(payload + '\n');
|
||||
console.log('[QZPlayer TX]', payload);
|
||||
|
||||
try {
|
||||
this.socket.write(payload + '\n');
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error(String(err));
|
||||
this.queueCommand(command);
|
||||
this.restart(`socket write failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience methods
|
||||
async load(url: string) {
|
||||
async load(url: string): Promise<void> {
|
||||
return this.send(['loadfile', url]);
|
||||
}
|
||||
|
||||
async play() {
|
||||
async play(): Promise<void> {
|
||||
return this.send(['set_property', 'pause', false]);
|
||||
}
|
||||
|
||||
async pause() {
|
||||
async pause(): Promise<void> {
|
||||
return this.send(['set_property', 'pause', true]);
|
||||
}
|
||||
|
||||
async togglePause() {
|
||||
async togglePause(): Promise<void> {
|
||||
return this.send(['cycle', 'pause']);
|
||||
}
|
||||
|
||||
async stop() {
|
||||
async stop(): Promise<void> {
|
||||
return this.send(['stop']);
|
||||
}
|
||||
|
||||
async setVolume(vol: number) {
|
||||
async setVolume(vol: number): Promise<void> {
|
||||
return this.send(['set_property', 'volume', vol]);
|
||||
}
|
||||
|
||||
async seek(seconds: number) {
|
||||
async seek(seconds: number): Promise<void> {
|
||||
return this.send(['seek', seconds, 'absolute']);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.process) {
|
||||
console.log('Killing QZPlayer process...');
|
||||
this.process.kill();
|
||||
this.process = null;
|
||||
}
|
||||
if (this.socket) {
|
||||
this.socket.destroy();
|
||||
destroy(): void {
|
||||
this.isShuttingDown = true;
|
||||
this.isRestarting = false;
|
||||
this.clearConnectTimer();
|
||||
this.clearRestartTimer();
|
||||
this.destroySocket();
|
||||
this.killProcess();
|
||||
}
|
||||
|
||||
private destroySocket(socket = this.socket): void {
|
||||
if (!socket) return;
|
||||
|
||||
if (this.socket === socket) {
|
||||
this.socket = null;
|
||||
this.isConnected = false;
|
||||
}
|
||||
|
||||
socket.removeAllListeners('data');
|
||||
socket.removeAllListeners('error');
|
||||
socket.removeAllListeners('close');
|
||||
socket.destroy();
|
||||
}
|
||||
|
||||
private killProcess(): void {
|
||||
const child = this.process;
|
||||
this.process = null;
|
||||
if (!child) return;
|
||||
|
||||
console.log('Killing QZPlayer process...');
|
||||
child.removeAllListeners('error');
|
||||
child.removeAllListeners('exit');
|
||||
if (!child.killed) {
|
||||
child.kill('SIGKILL');
|
||||
}
|
||||
}
|
||||
|
||||
private clearConnectTimer(): void {
|
||||
if (this.connectTimer) {
|
||||
clearTimeout(this.connectTimer);
|
||||
this.connectTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private clearRestartTimer(): void {
|
||||
if (this.restartTimer) {
|
||||
clearTimeout(this.restartTimer);
|
||||
this.restartTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private queueCommand(command: any[]): void {
|
||||
if (command[0] === 'observe_property') return;
|
||||
this.pendingCommands.push(command);
|
||||
if (this.pendingCommands.length > this.maxPendingCommands) {
|
||||
this.pendingCommands.shift();
|
||||
}
|
||||
}
|
||||
|
||||
private flushPendingCommands(): void {
|
||||
const commands = this.pendingCommands.splice(0);
|
||||
for (const command of commands) {
|
||||
this.sendNow(command);
|
||||
}
|
||||
}
|
||||
|
||||
private trackPlayerState(json: any): void {
|
||||
if (json?.event !== 'property-change') return;
|
||||
|
||||
if (json.name === 'pause') {
|
||||
this.desiredPause = Boolean(json.data);
|
||||
} else if (json.name === 'time-pos' && typeof json.data === 'number') {
|
||||
this.lastKnownTime = json.data;
|
||||
}
|
||||
}
|
||||
|
||||
private trackOutgoingCommand(command: any[]): void {
|
||||
const [name, ...args] = command;
|
||||
|
||||
if (name === 'loadfile' && typeof args[0] === 'string') {
|
||||
this.currentUrl = args[0];
|
||||
this.lastKnownTime = 0;
|
||||
} else if (name === 'seek' && typeof args[0] === 'number') {
|
||||
this.lastKnownTime = args[0];
|
||||
} else if (name === 'stop') {
|
||||
this.currentUrl = null;
|
||||
this.lastKnownTime = 0;
|
||||
this.desiredPause = true;
|
||||
} else if (name === 'cycle' && args[0] === 'pause') {
|
||||
this.desiredPause = !this.desiredPause;
|
||||
} else if (name === 'set_property') {
|
||||
if (args[0] === 'pause') {
|
||||
this.desiredPause = Boolean(args[1]);
|
||||
} else if (args[0] === 'volume' && typeof args[1] === 'number') {
|
||||
this.volume = args[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private emitError(err: Error): void {
|
||||
if (this.listenerCount('error') > 0) {
|
||||
this.emit('error', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,9 +93,44 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lyric">
|
||||
<div class="lyric" :class="{ 'show-playlist': showPlaylistPanel }">
|
||||
<!-- Playlist Panel -->
|
||||
<Transition name="playlist-fade">
|
||||
<div v-if="showPlaylistPanel" class="playlist-panel">
|
||||
<div class="playlist-header">
|
||||
<span class="playlist-title">播放队列</span>
|
||||
<span class="playlist-count">{{ playerStore.playlist.length }} 首</span>
|
||||
</div>
|
||||
<div class="playlist-scroll">
|
||||
<div
|
||||
v-for="(song, index) in playerStore.playlist"
|
||||
:key="song.id"
|
||||
class="playlist-item"
|
||||
:class="{ active: index === playerStore.playlist.findIndex(s => s.id === playerStore.currentSong?.id) }"
|
||||
@click="playFromPlaylist(index)"
|
||||
>
|
||||
<div class="item-index">
|
||||
<span v-if="index === playerStore.playlist.findIndex(s => s.id === playerStore.currentSong?.id)" class="playing-indicator">
|
||||
<span class="bar"></span>
|
||||
<span class="bar"></span>
|
||||
<span class="bar"></span>
|
||||
</span>
|
||||
<span v-else>{{ index + 1 }}</span>
|
||||
</div>
|
||||
<img v-if="song.picUrl" :src="song.picUrl" class="item-cover" />
|
||||
<div v-else class="item-cover item-cover-placeholder"></div>
|
||||
<div class="item-info">
|
||||
<div class="item-name">{{ song.name }}</div>
|
||||
<div class="item-artist">{{ song.artist }}</div>
|
||||
</div>
|
||||
<div class="item-duration">{{ song.duration }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
<!-- Lyric Player -->
|
||||
<LyricPlayer
|
||||
v-if="isPlayerFullScreen"
|
||||
v-if="isPlayerFullScreen && !showPlaylistPanel"
|
||||
ref="lyricPlayerRef"
|
||||
:lyric-lines="toRaw(playerStore.lyrics.lines)"
|
||||
:current-time="playerStore.currentTime"
|
||||
@@ -113,6 +148,8 @@
|
||||
<div class="bottomControls">
|
||||
<ToggleIconButton
|
||||
type="playlist"
|
||||
:checked="showPlaylistPanel"
|
||||
@click="togglePlaylistPanel"
|
||||
/>
|
||||
<ToggleIconButton
|
||||
type="lyrics"
|
||||
@@ -160,6 +197,8 @@ const lyricPlayerRef = ref<LyricPlayerRef>()
|
||||
const bgRef = ref<BackgroundRenderRef>();
|
||||
|
||||
const showRemaining = ref(false);
|
||||
const showPlaylistPanel = ref(false);
|
||||
const lyricState = ref(false);
|
||||
|
||||
const formatTime = (miliseconds: number) => {
|
||||
const seconds = miliseconds / 1000;
|
||||
@@ -168,15 +207,20 @@ const formatTime = (miliseconds: number) => {
|
||||
return `${min}:${sec.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const syncLyricPlayerSeek = (time: number) => {
|
||||
const lyricPlayer = lyricPlayerRef.value?.lyricPlayer.value;
|
||||
lyricPlayer?.resetScroll();
|
||||
lyricPlayer?.setCurrentTime(time, true);
|
||||
};
|
||||
|
||||
const handleSeek = (val: number) => {
|
||||
playerStore.seek(val);
|
||||
lyricPlayerRef.value?.lyricPlayer.value?.setCurrentTime(val,true);
|
||||
syncLyricPlayerSeek(val);
|
||||
};
|
||||
|
||||
|
||||
const playerStore = usePlayerStore();
|
||||
const isPlayerFullScreen = computed(() => playerStore.isPlayerFullScreen);
|
||||
//const currentSong = computed(() => playerStore.currentSong);
|
||||
const isPlaying = computed(() => playerStore.isPlaying);
|
||||
const playMode = computed(() => playerStore.playMode);
|
||||
|
||||
@@ -300,10 +344,26 @@ const toggleFullScreen = () => {
|
||||
};
|
||||
|
||||
const jumpTime = (e: LyricLineMouseEvent) => {
|
||||
playerStore.seek(e.line.getLine().startTime)
|
||||
lyricPlayerRef.value?.lyricPlayer.value?.setCurrentTime(e.line.getLine().startTime,true);
|
||||
const time = e.line.getLine().startTime;
|
||||
playerStore.seek(time);
|
||||
syncLyricPlayerSeek(time);
|
||||
}
|
||||
|
||||
const togglePlaylistPanel = () => {
|
||||
const opening = !showPlaylistPanel.value;
|
||||
opening
|
||||
? (lyricState.value = playerStore.hideLyricView, playerStore.hideLyricView = false)
|
||||
: (playerStore.hideLyricView = lyricState.value);
|
||||
showPlaylistPanel.value = opening;
|
||||
};
|
||||
|
||||
const playFromPlaylist = (index: number) => {
|
||||
const song = playerStore.playlist[index];
|
||||
if (song) {
|
||||
playerStore.playSong(song);
|
||||
}
|
||||
};
|
||||
|
||||
// watch(()=>playerStore.currentTime,(t)=>{
|
||||
// console.log(toRaw(t))
|
||||
// })
|
||||
@@ -539,6 +599,10 @@ const jumpTime = (e: LyricLineMouseEvent) => {
|
||||
}
|
||||
}
|
||||
|
||||
.lyric.show-playlist {
|
||||
mask-image: linear-gradient(black 0%, black 90%, transparent);
|
||||
}
|
||||
|
||||
.bottomControls {
|
||||
grid-area: buttom-controls / 1 / buttom-controls / 4;
|
||||
gap: 2em;
|
||||
@@ -617,4 +681,199 @@ const jumpTime = (e: LyricLineMouseEvent) => {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Playlist Panel Styles */
|
||||
.playlist-panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
backdrop-filter: blur(30px);
|
||||
-webkit-backdrop-filter: blur(30px);
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
pointer-events: auto;
|
||||
margin-right: 15%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1600px), (max-height: 1000px) {
|
||||
.playlist-panel {
|
||||
margin-right: 8%;
|
||||
}
|
||||
}
|
||||
|
||||
.playlist-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px 16px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.playlist-title {
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.playlist-count {
|
||||
font-size: 0.9em;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.playlist-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px 12px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
|
||||
}
|
||||
|
||||
.playlist-scroll::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.playlist-scroll::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.playlist-scroll::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.playlist-scroll::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.playlist-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.playlist-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.playlist-item.active {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.playlist-item.active .item-name {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
.item-index {
|
||||
width: 28px;
|
||||
text-align: center;
|
||||
font-size: 0.85em;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.playlist-item.active .item-index {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.playing-indicator {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.playing-indicator .bar {
|
||||
width: 3px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 2px;
|
||||
animation: soundBars 0.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.playing-indicator .bar:nth-child(1) {
|
||||
height: 60%;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.playing-indicator .bar:nth-child(2) {
|
||||
height: 100%;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.playing-indicator .bar:nth-child(3) {
|
||||
height: 40%;
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
@keyframes soundBars {
|
||||
0%, 100% {
|
||||
transform: scaleY(0.5);
|
||||
}
|
||||
50% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
|
||||
.item-cover {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-cover-placeholder {
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.1), rgba(255,255,255,0.05));
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 0.95em;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.item-artist {
|
||||
font-size: 0.8em;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.item-duration {
|
||||
font-size: 0.85em;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
flex-shrink: 0;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Playlist Panel Transition */
|
||||
.playlist-fade-enter-active,
|
||||
.playlist-fade-leave-active {
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.playlist-fade-enter-from,
|
||||
.playlist-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -251,7 +251,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onBeforeMount, nextTick, watch } from 'vue';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { usePlayerStore } from '../stores/player';
|
||||
|
||||
const playerStore = usePlayerStore();
|
||||
@@ -323,17 +323,17 @@ const installPluginFromFile = async () => {
|
||||
if (window.electronAPI?.plugin?.install) {
|
||||
const result = await window.electronAPI.plugin.install();
|
||||
if (result.success) {
|
||||
MessagePlugin.success(result.message || '安装成功');
|
||||
ElMessage.success(result.message || '安装成功');
|
||||
await loadPlugins();
|
||||
} else {
|
||||
if (result.message !== 'canceled') { // Assuming 'canceled' might be a thing, or just show whatever message comes back
|
||||
MessagePlugin.error(result.message || '安装失败');
|
||||
ElMessage.error(result.message || '安装失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to install plugin', e);
|
||||
MessagePlugin.error('安装过程中发生错误');
|
||||
ElMessage.error('安装过程中发生错误');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,14 +433,14 @@ const changeCacheLocation = async () => {
|
||||
isChangingCache.value = true;
|
||||
const result = await window.electronAPI.changeCacheLocation(path);
|
||||
if (result.success) {
|
||||
MessagePlugin.success('缓存位置已修改');
|
||||
ElMessage.success('缓存位置已修改');
|
||||
await loadCacheInfo();
|
||||
} else {
|
||||
MessagePlugin.error(result.message || '修改失败');
|
||||
ElMessage.error(result.message || '修改失败');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
MessagePlugin.error('操作失败');
|
||||
ElMessage.error('操作失败');
|
||||
console.error(e);
|
||||
} finally {
|
||||
isChangingCache.value = false;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { createPinia } from 'pinia'
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
|
||||
import App from './App.vue'
|
||||
import 'tdesign-vue-next/es/style/index.css'
|
||||
import 'element-plus/dist/index.css'
|
||||
|
||||
const pinia = createPinia()
|
||||
const router = createRouter({
|
||||
@@ -41,4 +41,4 @@ const router = createRouter({
|
||||
const app = createApp(App)
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
app.mount('#app')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, shallowRef, watch } from 'vue';
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import type { Song } from '../types/song';
|
||||
import { parseLyric } from '../utils/lyricUtil'
|
||||
export enum PlayMode {
|
||||
@@ -191,7 +191,7 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
lyrics.value = { lines: parseLyric(rawLyric) }
|
||||
console.log(lyrics.value)
|
||||
} else {
|
||||
MessagePlugin.warning("当前插件不支持歌词获取").then()
|
||||
ElMessage.warning("当前插件不支持歌词获取")
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch lyrics:', e);
|
||||
@@ -263,11 +263,11 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
if (playErrorCount.value >= MAX_RETRY_COUNT) {
|
||||
window.electronAPI.qzplayer.pause().then();
|
||||
isPlaying.value = false;
|
||||
MessagePlugin.error('连续多次播放失败,已停止播放').then();
|
||||
ElMessage.error('连续多次播放失败,已停止播放');
|
||||
playErrorCount.value = 0;
|
||||
syncDummyAudioState(false);
|
||||
} else {
|
||||
MessagePlugin.warning(`播放失败,尝试播放下一首 (${playErrorCount.value}/${MAX_RETRY_COUNT})`).then();
|
||||
ElMessage.warning(`播放失败,尝试播放下一首 (${playErrorCount.value}/${MAX_RETRY_COUNT})`);
|
||||
next(false)
|
||||
}
|
||||
};
|
||||
@@ -369,4 +369,4 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
playFromList,
|
||||
hideLyricView
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user