forked from miao-moe/QZMusic_PC
fork(fix): Clone AMLL 并修复 BUG
- 将AMLL Clone到本以地进行修复和优化(emm虽然这很不优雅但是暂时无时间做子模块和Fork) - 修复在当前播放歌词行不可见的视口Seek会出现滚动偏移的问题
This commit is contained in:
@@ -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