feat: redesign play queue and per-track queue actions (1.2.3)

This commit is contained in:
yuncishu committed 2026-10-31 22:38:59 +08:00
1 parent 1fccf2bed8
commit 2a55d0dc2c
5 files changed
+295 -104

No files matched your search

+1 -1
View File
@@ -12,7 +12,7 @@
- 标准 / 较高 / 极高 / 无损音质切换 - 标准 / 较高 / 极高 / 无损音质切换
- 逐行歌词、精确居中、点击歌词跳转 - 逐行歌词、精确居中、点击歌词跳转
- 全屏播放页、黑胶旋转、音量与播放进度控制 - 全屏播放页、黑胶旋转、音量与播放进度控制
- 播放栏显示当前播放歌单 - 播放队列面板:下一首播放、删除单曲、清空保留当前
- 应用内检查更新、自动下载、SHA-256 校验和重启安装 - 应用内检查更新、自动下载、SHA-256 校验和重启安装
## 开发运行 ## 开发运行
+77 -79
View File
@@ -7,11 +7,11 @@ import sys
import requests import requests
from PySide6.QtCore import Qt, QUrl, QSize, QTimer, QRunnable, QThreadPool, Signal, QObject from PySide6.QtCore import Qt, QUrl, QSize, QTimer, QRunnable, QThreadPool, Signal, QObject
from PySide6.QtGui import QPixmap, QIcon, QColor from PySide6.QtGui import QPixmap, QIcon, QCursor
from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QLineEdit, QStackedWidget, QListWidget, QListWidgetItem, QSlider, QComboBox, QLineEdit, QStackedWidget, QSlider, QComboBox, QToolTip,
QGridLayout, QSizePolicy, QProgressBar, QDialog, QGridLayout, QSizePolicy, QProgressBar, QDialog,
) )
@@ -22,7 +22,7 @@ from version import APP_NAME, APP_VERSION
from theme import QSS, ACCENT from theme import QSS, ACCENT
from widgets import ( from widgets import (
make_icon, rounded_pixmap, DiscWidget, ClickSlider, LyricView, TrackTable, icon_button, make_icon, rounded_pixmap, DiscWidget, ClickSlider, LyricView, TrackTable, icon_button,
CoverBackdrop, CoverBackdrop, QueuePopup,
) )
@@ -108,8 +108,6 @@ class MainWindow(QMainWindow):
self.queue = [] self.queue = []
self.q_index = -1 self.q_index = -1
self.current = None self.current = None
self.stack_context_name = "发现音乐"
self.current_playlist_name = ""
self.mode = "list" self.mode = "list"
self.quality = "exhigh" self.quality = "exhigh"
self.volume = 78 self.volume = 78
@@ -230,6 +228,7 @@ class MainWindow(QMainWindow):
self.stack.addWidget(self.charts_page) self.stack.addWidget(self.charts_page)
self.table = TrackTable() self.table = TrackTable()
self.table.play_requested.connect(self._play_from_list) self.table.play_requested.connect(self._play_from_list)
self.table.next_requested.connect(self._queue_next)
self.table.verticalScrollBar().valueChanged.connect(self._load_visible_covers) self.table.verticalScrollBar().valueChanged.connect(self._load_visible_covers)
self.stack.addWidget(self.table) self.stack.addWidget(self.table)
self.empty_page = self._build_empty_page() self.empty_page = self._build_empty_page()
@@ -242,6 +241,11 @@ class MainWindow(QMainWindow):
self.fs.setParent(root) self.fs.setParent(root)
self.fs.hide() self.fs.hide()
self.queue_popup = QueuePopup(self)
self.queue_popup.play_requested.connect(self._play_index)
self.queue_popup.remove_requested.connect(self._remove_queue_item)
self.queue_popup.clear_requested.connect(self._clear_queue)
def _build_sidebar(self): def _build_sidebar(self):
side = QWidget(objectName="side") side = QWidget(objectName="side")
side.setFixedWidth(224) side.setFixedWidth(224)
@@ -281,26 +285,7 @@ class MainWindow(QMainWindow):
layout.addWidget(button) layout.addWidget(button)
self.nav_btns[key] = button self.nav_btns[key] = button
queue_header = QHBoxLayout() layout.addStretch()
queue_header.setContentsMargins(12, 28, 6, 8)
queue_header.addWidget(QLabel("当前队列", objectName="sideSection"))
queue_header.addStretch()
clear = QPushButton("清空", objectName="clearBtn")
clear.setCursor(Qt.PointingHandCursor)
clear.clicked.connect(self._clear_queue)
queue_header.addWidget(clear)
layout.addLayout(queue_header)
self.queue_list = QListWidget(objectName="queue")
self.queue_list.setCursor(Qt.PointingHandCursor)
self.queue_list.viewport().setCursor(Qt.PointingHandCursor)
self.queue_list.itemClicked.connect(self._on_queue_click)
layout.addWidget(self.queue_list, 1)
self.queue_empty = QLabel("播放的歌曲会出现在这里", objectName="queueEmpty")
self.queue_empty.setContentsMargins(12, 4, 8, 8)
layout.addWidget(self.queue_empty)
layout.addSpacing(8)
version_row = QHBoxLayout() version_row = QHBoxLayout()
version_row.setContentsMargins(10, 8, 2, 0) version_row.setContentsMargins(10, 8, 2, 0)
self.version_label = QLabel("v%s" % APP_VERSION, objectName="versionLabel") self.version_label = QLabel("v%s" % APP_VERSION, objectName="versionLabel")
@@ -468,11 +453,8 @@ class MainWindow(QMainWindow):
layout.addLayout(center, 1) layout.addLayout(center, 1)
right_widget = QWidget() right_widget = QWidget()
right_widget.setFixedWidth(290) right_widget.setFixedWidth(324)
right_column = QVBoxLayout(right_widget) right = QHBoxLayout(right_widget)
right_column.setContentsMargins(0, 1, 0, 0)
right_column.setSpacing(4)
right = QHBoxLayout()
right.setContentsMargins(0, 0, 0, 0) right.setContentsMargins(0, 0, 0, 0)
right.setSpacing(7) right.setSpacing(7)
right.addStretch() right.addStretch()
@@ -502,12 +484,9 @@ class MainWindow(QMainWindow):
self.vol_slider.setToolTip("音量") self.vol_slider.setToolTip("音量")
self.vol_slider.valueChanged.connect(self._on_volume) self.vol_slider.valueChanged.connect(self._on_volume)
right.addWidget(self.vol_slider) right.addWidget(self.vol_slider)
right_column.addLayout(right) self.btn_queue = icon_button("bars", 34, 18, tooltip="播放队列")
self.bar_playlist = QLabel("歌单 · 未播放", objectName="barPlaylist") self.btn_queue.clicked.connect(self._toggle_queue_popup)
self.bar_playlist.setAlignment(Qt.AlignRight | Qt.AlignVCenter) right.addWidget(self.btn_queue)
self.bar_playlist.setFixedHeight(14)
self.bar_playlist.setToolTip("当前播放队列来源")
right_column.addWidget(self.bar_playlist)
layout.addWidget(right_widget) layout.addWidget(right_widget)
return bar return bar
@@ -629,20 +608,17 @@ class MainWindow(QMainWindow):
self.page_eyebrow.setText("LIBRARY / DISCOVER") self.page_eyebrow.setText("LIBRARY / DISCOVER")
self.page_title.setText("发现音乐") self.page_title.setText("发现音乐")
self.page_meta.setText("网易云音乐官方榜单") self.page_meta.setText("网易云音乐官方榜单")
self.stack_context_name = "发现音乐"
self.stack.setCurrentWidget(self.charts_page) self.stack.setCurrentWidget(self.charts_page)
elif key == "search": elif key == "search":
self.page_eyebrow.setText("LIBRARY / SEARCH") self.page_eyebrow.setText("LIBRARY / SEARCH")
self.page_title.setText("搜索") self.page_title.setText("搜索")
self.page_meta.setText("在网易云音乐曲库中寻找声音") self.page_meta.setText("在网易云音乐曲库中寻找声音")
self.stack_context_name = "搜索"
self._show_empty("搜索一首歌", "输入歌曲、歌手或专辑名称,按回车开始") self._show_empty("搜索一首歌", "输入歌曲、歌手或专辑名称,按回车开始")
self.search_input.setFocus() self.search_input.setFocus()
else: else:
self.page_eyebrow.setText("LIBRARY / HISTORY") self.page_eyebrow.setText("LIBRARY / HISTORY")
self.page_title.setText("最近播放") self.page_title.setText("最近播放")
self.page_meta.setText("保存在这台电脑上的播放记录") self.page_meta.setText("保存在这台电脑上的播放记录")
self.stack_context_name = "最近播放"
if not self.recent: if not self.recent:
self._show_empty("还没有播放记录", "播放过的歌曲会自动保存在这里") self._show_empty("还没有播放记录", "播放过的歌曲会自动保存在这里")
else: else:
@@ -788,7 +764,6 @@ class MainWindow(QMainWindow):
token = self.view_token token = self.view_token
self.page_eyebrow.setText("CHARTS / OFFICIAL") self.page_eyebrow.setText("CHARTS / OFFICIAL")
self.page_title.setText(name) self.page_title.setText(name)
self.stack_context_name = name
self.btn_back.setVisible(True) self.btn_back.setVisible(True)
self.btn_refresh.setVisible(False) self.btn_refresh.setVisible(False)
self.btn_play_all.setVisible(False) self.btn_play_all.setVisible(False)
@@ -877,7 +852,6 @@ class MainWindow(QMainWindow):
) )
return return
self.page_title.setText(playlist.get("name") or "导入歌单") self.page_title.setText(playlist.get("name") or "导入歌单")
self.stack_context_name = playlist.get("name") or "导入歌单"
total = playlist.get("trackCount") or len(songs) total = playlist.get("trackCount") or len(songs)
extra = " · 当前导入前 %d 首" % len(songs) if total > len(songs) else "" extra = " · 当前导入前 %d 首" % len(songs) if total > len(songs) else ""
self.page_meta.setText("共 %d 首%s · 来自网易云音乐" % (total, extra)) self.page_meta.setText("共 %d 首%s · 来自网易云音乐" % (total, extra))
@@ -923,7 +897,6 @@ class MainWindow(QMainWindow):
self.view_token += 1 self.view_token += 1
token = self.view_token token = self.view_token
self.page_meta.setText("正在搜索「%s」…" % keyword) self.page_meta.setText("正在搜索「%s」…" % keyword)
self.stack_context_name = "搜索 · %s" % keyword
self.btn_play_all.setVisible(False) self.btn_play_all.setVisible(False)
self._show_empty("正在搜索", "在整个曲库中寻找「%s」" % keyword, busy=True) self._show_empty("正在搜索", "在整个曲库中寻找「%s」" % keyword, busy=True)
@@ -950,41 +923,79 @@ class MainWindow(QMainWindow):
# ---------- 队列 ---------- # ---------- 队列 ----------
def _render_queue(self): def _render_queue(self):
self.queue_list.clear() if hasattr(self, "queue_popup") and self.queue_popup.isVisible():
for i, song in enumerate(self.queue): current_id = self.current.get("id") if self.current else None
item = QListWidgetItem("%s\n%s" % (song.get("name", ""), core.artists_of(song))) self.queue_popup.set_queue(self.queue, self.q_index, current_id)
item.setData(Qt.UserRole, i)
item.setToolTip(song.get("name", ""))
if i == self.q_index:
item.setForeground(QColor("#f27770"))
font = item.font()
font.setBold(True)
item.setFont(font)
self.queue_list.addItem(item)
self.queue_empty.setVisible(not self.queue)
if 0 <= self.q_index < self.queue_list.count():
self.queue_list.scrollToItem(self.queue_list.item(self.q_index))
def _on_queue_click(self, item): def _toggle_queue_popup(self):
index = item.data(Qt.UserRole) if self.queue_popup.isVisible():
if index is not None and 0 <= int(index) < len(self.queue): self.queue_popup.hide()
self._play_index(int(index)) return
current_id = self.current.get("id") if self.current else None
self.queue_popup.set_queue(self.queue, self.q_index, current_id)
self.queue_popup.show_above(self.btn_queue)
def _remove_queue_item(self, index):
if not (0 <= index < len(self.queue)):
return
was_current = index == self.q_index
self.queue.pop(index)
if was_current:
self.q_index = index - 1
elif index < self.q_index:
self.q_index -= 1
if not self.queue:
self.q_index = -1
elif self.q_index >= len(self.queue):
self.q_index = len(self.queue) - 1
self._render_queue()
def _clear_queue(self): def _clear_queue(self):
self.queue = [] if self.current is not None:
self.q_index = -1 self.queue = [self.current]
self.q_index = 0
else:
self.queue = []
self.q_index = -1
self._render_queue() self._render_queue()
def _queue_next(self, song):
if not song:
return
if self.current and song.get("id") == self.current.get("id"):
QToolTip.showText(QCursor.pos(), "这首歌正在播放")
return
existing = next(
(i for i, item in enumerate(self.queue) if item.get("id") == song.get("id")),
None,
)
if existing is not None:
self.queue.pop(existing)
if existing < self.q_index:
self.q_index -= 1
insert_at = self.q_index + 1 if self.q_index >= 0 else 0
insert_at = max(0, min(insert_at, len(self.queue)))
self.queue.insert(insert_at, dict(song))
self._render_queue()
QToolTip.showText(QCursor.pos(), "已设为下一首播放")
# ---------- 播放 ---------- # ---------- 播放 ----------
def _play_from_list(self, songs, index): def _play_from_list(self, songs, index):
self.queue = list(songs) if not (0 <= index < len(songs)):
self._set_current_playlist(self.stack_context_name) return
self._play_index(index) song = dict(songs[index])
existing = next(
(i for i, item in enumerate(self.queue) if item.get("id") == song.get("id")),
None,
)
if existing is None:
self.queue.append(song)
existing = len(self.queue) - 1
self._play_index(existing)
def _play_all(self): def _play_all(self):
if self.stack_songs: if self.stack_songs:
self.queue = list(self.stack_songs) self.queue = list(self.stack_songs)
self._set_current_playlist(self.stack_context_name)
self._play_index(0) self._play_index(0)
def _play_index(self, index): def _play_index(self, index):
@@ -1147,19 +1158,6 @@ class MainWindow(QMainWindow):
lambda error: self.bar_artist.setText("音质切换失败")) lambda error: self.bar_artist.setText("音质切换失败"))
# ---------- 当前歌曲与封面 ---------- # ---------- 当前歌曲与封面 ----------
def _update_bar_playlist(self):
label = self.current_playlist_name or "未播放"
raw = "当前歌单:" + label
width = max(80, self.bar_playlist.width() - 2)
shown = self.bar_playlist.fontMetrics().elidedText(
"歌单 · " + label, Qt.ElideRight, width)
self.bar_playlist.setText(shown)
self.bar_playlist.setToolTip(raw)
def _set_current_playlist(self, name):
self.current_playlist_name = (name or "").strip() or "未命名歌单"
self._update_bar_playlist()
def _update_bar(self): def _update_bar(self):
song = self.current song = self.current
if not song: if not song:
+35 -13
View File
@@ -29,15 +29,6 @@ QPushButton#nav {
} }
QPushButton#nav:hover { color: #f3f3f4; background: #28292e; } QPushButton#nav:hover { color: #f3f3f4; background: #28292e; }
QPushButton#nav[active="true"] { color: #ffffff; background: #323338; font-weight: 600; } QPushButton#nav[active="true"] { color: #ffffff; background: #323338; font-weight: 600; }
QPushButton#clearBtn { color: #777981; border: none; background: transparent; font-size: 11px; }
QPushButton#clearBtn:hover { color: #f0f0f1; }
QListWidget#queue { background: transparent; border: none; outline: none; }
QListWidget#queue::item {
color: #9698a0; border: none; border-radius: 7px; padding: 0 8px; height: 44px;
}
QListWidget#queue::item:hover { background: #28292e; color: #e7e7e9; }
QListWidget#queue::item:selected { background: #303137; color: #f3f3f4; }
#queueEmpty { color: #62646c; font-size: 11px; }
#versionLabel { color: #666871; font-size: 9.5px; letter-spacing: .4px; } #versionLabel { color: #666871; font-size: 9.5px; letter-spacing: .4px; }
QPushButton#versionBtn { QPushButton#versionBtn {
color: #8b8d95; background: transparent; border: none; border-radius: 5px; color: #8b8d95; background: transparent; border: none; border-radius: 5px;
@@ -110,10 +101,6 @@ QProgressBar#emptyProgress::chunk { background: #e6493f; border-radius: 2px; }
#barName { font-size: 13px; font-weight: 650; color: #2c2a27; } #barName { font-size: 13px; font-weight: 650; color: #2c2a27; }
#barArtist { font-size: 11px; color: #918d85; } #barArtist { font-size: 11px; color: #918d85; }
#barHint { color: #b0aba3; font-size: 10px; } #barHint { color: #b0aba3; font-size: 10px; }
#barPlaylist {
color: #aaa49b; font-size: 9.5px; letter-spacing: .02em;
background: transparent;
}
#timeLabel { color: #9b978f; font-size: 10px; font-family: "Segoe UI", sans-serif; } #timeLabel { color: #9b978f; font-size: 10px; font-family: "Segoe UI", sans-serif; }
QSlider::groove:horizontal { height: 4px; background: #e8e4de; border-radius: 2px; } QSlider::groove:horizontal { height: 4px; background: #e8e4de; border-radius: 2px; }
QSlider::sub-page:horizontal { height: 4px; background: #e6493f; border-radius: 2px; } QSlider::sub-page:horizontal { height: 4px; background: #e6493f; border-radius: 2px; }
@@ -136,6 +123,41 @@ QComboBox QAbstractItemView {
selection-background-color: #eeeae4; selection-color: #242322; selection-background-color: #eeeae4; selection-color: #242322;
} }
/* 播放队列弹层 */
#queueCard { background:#fffefa; border:1px solid #e4dfd8; border-radius:14px; }
#queueTitle { color:#242322; font-size:15px; font-weight:700; }
#queueCount { color:#aaa49b; font-size:10px; }
QPushButton#queueClear {
color:#8d8880; background:transparent; border:1px solid #e6e1da;
border-radius:6px; padding:0 9px; min-height:25px; font-size:10.5px;
}
QPushButton#queueClear:hover { color:#c83b32; border-color:#e7c5c0; background:#fdf5f3; }
QPushButton#queueClear:disabled { color:#c9c4bc; border-color:#efebe5; }
QScrollArea#queueScroll { background:transparent; border:none; }
QScrollArea#queueScroll > QWidget > QWidget { background:transparent; }
QWidget#queueBody { background:transparent; }
QWidget#queueRow { background:transparent; border-radius:8px; }
QWidget#queueRow:hover { background:#f4f0ea; }
QWidget#queueRow[current="true"] { background:#fdf1ef; }
QPushButton#queueRemove {
background:transparent; border:none; border-radius:6px; padding:0;
}
QPushButton#queueRemove:hover { background:#efe7e1; }
#queueEmpty { color:#aaa49b; font-size:11px; padding:54px 0; }
QPushButton#trackMore {
background:transparent; border:none; border-radius:6px; padding:0;
}
QPushButton#trackMore:hover { background:#efebe5; }
QMenu {
background:#fffefa; border:1px solid #e1dcd4; border-radius:8px; padding:5px;
}
QMenu::item {
color:#4f4b46; background:transparent; padding:8px 20px 8px 14px;
border-radius:5px; font-size:12px;
}
QMenu::item:selected { background:#f4f0ea; color:#242322; }
QMenu::separator { height:1px; background:#eeeae3; margin:4px 8px; }
/* 导入歌单 */ /* 导入歌单 */
#importCard { background:#fffefa; border:1px solid #e4dfd8; border-radius:16px; } #importCard { background:#fffefa; border:1px solid #e4dfd8; border-radius:16px; }
#importTitle { color:#22211f; font-size:19px; font-weight:700; } #importTitle { color:#22211f; font-size:19px; font-weight:700; }
+1 -1
View File
@@ -1,7 +1,7 @@
"""应用版本与更新地址。""" """应用版本与更新地址。"""
APP_NAME = "网易云播放器" APP_NAME = "网易云播放器"
APP_VERSION = "1.2.2" APP_VERSION = "1.2.3"
BUILD_DATE = "2026-10-31" BUILD_DATE = "2026-10-31"
UPDATE_ENDPOINT = "https://music.yuncishu.xyz/api/app/update" UPDATE_ENDPOINT = "https://music.yuncishu.xyz/api/app/update"
DOWNLOAD_PAGE = "https://music.yuncishu.xyz/download/" DOWNLOAD_PAGE = "https://music.yuncishu.xyz/download/"
+181 -10
View File
@@ -2,7 +2,7 @@
import bisect import bisect
from PySide6.QtCore import ( from PySide6.QtCore import (
Qt, QSize, QRectF, QByteArray, QTimer, QElapsedTimer, Qt, QSize, QPoint, QRectF, QByteArray, QTimer, QElapsedTimer,
QPropertyAnimation, QEasingCurve, Signal, QPropertyAnimation, QEasingCurve, Signal,
) )
from PySide6.QtGui import QPixmap, QIcon, QColor, QFont, QPainter, QPainterPath, QImage, QLinearGradient from PySide6.QtGui import QPixmap, QIcon, QColor, QFont, QPainter, QPainterPath, QImage, QLinearGradient
@@ -10,7 +10,7 @@ from PySide6.QtSvg import QSvgRenderer
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QWidget, QLabel, QPushButton, QSlider, QStyle, QScrollArea, QVBoxLayout, QHBoxLayout, QWidget, QLabel, QPushButton, QSlider, QStyle, QScrollArea, QVBoxLayout, QHBoxLayout,
QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView, QSizePolicy, QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView, QSizePolicy,
QGraphicsScene, QGraphicsBlurEffect, QGraphicsScene, QGraphicsBlurEffect, QFrame, QGraphicsDropShadowEffect, QMenu,
) )
import core import core
@@ -35,6 +35,8 @@ ICONS = {
"clock": _S % '<circle cx="12" cy="12" r="7.5" stroke="COLOR" stroke-width="1.65" fill="none"/><path d="M12 7.5V12l3.2 2" stroke="COLOR" stroke-width="1.65" fill="none" stroke-linecap="round"/>', "clock": _S % '<circle cx="12" cy="12" r="7.5" stroke="COLOR" stroke-width="1.65" fill="none"/><path d="M12 7.5V12l3.2 2" stroke="COLOR" stroke-width="1.65" fill="none" stroke-linecap="round"/>',
"back": _S % '<path d="M15.5 5l-7 7 7 7" stroke="COLOR" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/>', "back": _S % '<path d="M15.5 5l-7 7 7 7" stroke="COLOR" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/>',
"plus": _S % '<path d="M12 5v14M5 12h14" stroke="COLOR" stroke-width="1.8" fill="none" stroke-linecap="round"/>', "plus": _S % '<path d="M12 5v14M5 12h14" stroke="COLOR" stroke-width="1.8" fill="none" stroke-linecap="round"/>',
"bars": _S % '<path d="M4.5 7h15M4.5 12h15M4.5 17h15" stroke="COLOR" stroke-width="1.8" fill="none" stroke-linecap="round"/>',
"more": _S % '<circle cx="6" cy="12" r="1.6" fill="COLOR"/><circle cx="12" cy="12" r="1.6" fill="COLOR"/><circle cx="18" cy="12" r="1.6" fill="COLOR"/>',
"playlist": _S % '<path d="M4.5 6.5h10M4.5 11.5h10M4.5 16.5h6.5" stroke="COLOR" stroke-width="1.6" fill="none" stroke-linecap="round"/><path d="M17 11v7.2" stroke="COLOR" stroke-width="1.6" stroke-linecap="round"/><circle cx="15.2" cy="18.2" r="1.9" fill="COLOR"/>', "playlist": _S % '<path d="M4.5 6.5h10M4.5 11.5h10M4.5 16.5h6.5" stroke="COLOR" stroke-width="1.6" fill="none" stroke-linecap="round"/><path d="M17 11v7.2" stroke="COLOR" stroke-width="1.6" stroke-linecap="round"/><circle cx="15.2" cy="18.2" r="1.9" fill="COLOR"/>',
"expand": _S % '<path d="M8.5 4.5h-4v4M15.5 4.5h4v4M19.5 15.5v4h-4M4.5 15.5v4h4" stroke="COLOR" stroke-width="1.7" fill="none" stroke-linecap="round" stroke-linejoin="round"/>', "expand": _S % '<path d="M8.5 4.5h-4v4M15.5 4.5h4v4M19.5 15.5v4h-4M4.5 15.5v4h4" stroke="COLOR" stroke-width="1.7" fill="none" stroke-linecap="round" stroke-linejoin="round"/>',
"music": _S % '<path d="M9 18.2V6.5l10-2v11.3M9 9.3l10-2" stroke="COLOR" stroke-width="1.7" fill="none" stroke-linecap="round" stroke-linejoin="round"/><circle cx="6.5" cy="18.2" r="2.5" fill="COLOR"/><circle cx="16.5" cy="15.8" r="2.5" fill="COLOR"/>', "music": _S % '<path d="M9 18.2V6.5l10-2v11.3M9 9.3l10-2" stroke="COLOR" stroke-width="1.7" fill="none" stroke-linecap="round" stroke-linejoin="round"/><circle cx="6.5" cy="18.2" r="2.5" fill="COLOR"/><circle cx="16.5" cy="15.8" r="2.5" fill="COLOR"/>',
@@ -404,12 +406,160 @@ class LyricView(QScrollArea):
super().wheelEvent(event) super().wheelEvent(event)
class TrackTable(QTableWidget): class QueueRow(QWidget):
play_requested = Signal(list, int) play_requested = Signal(int)
COLS = ["", "歌曲", "专辑", "时长"] remove_requested = Signal(int)
def __init__(self, index, song, current=False, parent=None):
super().__init__(parent)
self.index = index
self.setObjectName("queueRow")
self.setProperty("current", "true" if current else "false")
self.setAttribute(Qt.WA_StyledBackground, True)
self.setFixedHeight(60)
self.setCursor(Qt.PointingHandCursor)
row = QHBoxLayout(self)
row.setContentsMargins(10, 4, 8, 4)
row.setSpacing(11)
number = QLabel("▶" if current else "%02d" % (index + 1), objectName="queueIndex")
number.setFixedWidth(24)
number.setAlignment(Qt.AlignCenter)
number.setStyleSheet("color:%s; font-size:%spx; font-weight:600; background:transparent;" % (
"#e6493f" if current else "#aaa59c", 9 if current else 10))
row.addWidget(number)
text = QVBoxLayout()
text.setSpacing(2)
title = QLabel(song.get("name", ""), objectName="queueSong")
title.setStyleSheet("color:%s; font-size:12.5px; font-weight:%s; background:transparent;" % (
"#e6493f" if current else "#2c2a27", "600" if current else "500"))
artist = QLabel(core.artists_of(song), objectName="queueArtist")
artist.setStyleSheet("color:%s; font-size:10px; background:transparent;" % (
"#c16b64" if current else "#9b968e"))
text.addWidget(title)
text.addWidget(artist)
row.addLayout(text, 1)
remove = QPushButton()
remove.setObjectName("queueRemove")
remove.setFixedSize(26, 26)
remove.setCursor(Qt.PointingHandCursor)
remove.setIcon(make_icon("close", "#aaa49b", 12))
remove.setIconSize(QSize(12, 12))
remove.setToolTip("从队列移除")
remove.clicked.connect(lambda: self.remove_requested.emit(self.index))
row.addWidget(remove)
for label in (number, title, artist):
label.setAttribute(Qt.WA_TransparentForMouseEvents)
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton:
self.play_requested.emit(self.index)
super().mouseReleaseEvent(event)
class QueuePopup(QFrame):
play_requested = Signal(int)
remove_requested = Signal(int)
clear_requested = Signal()
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(0, 4, parent) super().__init__(parent, Qt.Popup | Qt.FramelessWindowHint | Qt.NoDropShadowWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
self.setFixedWidth(380)
self.queue = []
self.current_index = -1
self._build()
def _build(self):
outer = QVBoxLayout(self)
outer.setContentsMargins(10, 10, 10, 10)
card = QFrame(objectName="queueCard")
shadow = QGraphicsDropShadowEffect(card)
shadow.setBlurRadius(32)
shadow.setOffset(0, 10)
shadow.setColor(QColor(28, 27, 31, 55))
card.setGraphicsEffect(shadow)
outer.addWidget(card)
layout = QVBoxLayout(card)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
header = QHBoxLayout()
header.setContentsMargins(18, 15, 14, 9)
header.setSpacing(8)
title = QLabel("播放队列", objectName="queueTitle")
header.addWidget(title)
self.count = QLabel("0 首", objectName="queueCount")
header.addWidget(self.count)
header.addStretch()
self.clear_btn = QPushButton("清空", objectName="queueClear")
self.clear_btn.setCursor(Qt.PointingHandCursor)
self.clear_btn.setToolTip("清空队列,保留当前播放的歌曲")
self.clear_btn.clicked.connect(self.clear_requested)
header.addWidget(self.clear_btn)
layout.addLayout(header)
self.scroll = QScrollArea(objectName="queueScroll")
self.scroll.setWidgetResizable(True)
self.scroll.setFrameShape(QFrame.NoFrame)
self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
body = QWidget(objectName="queueBody")
self.rows_layout = QVBoxLayout(body)
self.rows_layout.setContentsMargins(8, 2, 8, 10)
self.rows_layout.setSpacing(1)
self.scroll.setWidget(body)
layout.addWidget(self.scroll, 1)
def set_queue(self, queue, current_index=-1, current_id=None):
self.queue = list(queue or [])
self.current_index = current_index
while self.rows_layout.count():
item = self.rows_layout.takeAt(0)
if item.widget():
item.widget().deleteLater()
self.count.setText("%d 首" % len(self.queue))
self.clear_btn.setEnabled(bool(self.queue))
scroll_index = -1
if not self.queue:
empty = QLabel("播放队列是空的", objectName="queueEmpty")
empty.setAlignment(Qt.AlignCenter)
self.rows_layout.addWidget(empty)
self.rows_layout.addStretch()
else:
if current_id is not None:
scroll_index = next(
(i for i, song in enumerate(self.queue) if song.get("id") == current_id), -1)
for i, song in enumerate(self.queue):
row = QueueRow(i, song, song.get("id") == current_id)
row.play_requested.connect(self.play_requested)
row.remove_requested.connect(self.remove_requested)
self.rows_layout.addWidget(row)
self.rows_layout.addStretch()
visible_rows = max(1, min(len(self.queue), 7))
self.setFixedHeight(104 + visible_rows * 60)
if scroll_index >= 0:
QTimer.singleShot(0, lambda: self.scroll.verticalScrollBar().setValue(
max(0, (scroll_index - 2) * 60)))
def show_above(self, anchor):
screen = anchor.screen()
geo = screen.availableGeometry()
pos = anchor.mapToGlobal(QPoint(anchor.width(), 0))
x = min(max(geo.left() + 8, pos.x() - self.width()), geo.right() - self.width() - 8)
y = pos.y() - self.height() - 10
if y < geo.top() + 8:
y = geo.top() + 8
self.move(x, y)
self.show()
self.raise_()
class TrackTable(QTableWidget):
play_requested = Signal(list, int)
next_requested = Signal(dict)
COLS = ["", "歌曲", "专辑", "时长", ""]
def __init__(self, parent=None):
super().__init__(0, 5, parent)
self.songs = [] self.songs = []
self.playing_id = None self.playing_id = None
self.covers = {} self.covers = {}
@@ -445,6 +595,8 @@ class TrackTable(QTableWidget):
self.setColumnWidth(2, 190) self.setColumnWidth(2, 190)
header.setSectionResizeMode(3, QHeaderView.Fixed) header.setSectionResizeMode(3, QHeaderView.Fixed)
self.setColumnWidth(3, 72) self.setColumnWidth(3, 72)
header.setSectionResizeMode(4, QHeaderView.Fixed)
self.setColumnWidth(4, 42)
for i, alignment in enumerate((Qt.AlignCenter, Qt.AlignLeft | Qt.AlignVCenter, for i, alignment in enumerate((Qt.AlignCenter, Qt.AlignLeft | Qt.AlignVCenter,
Qt.AlignLeft | Qt.AlignVCenter, Qt.AlignRight | Qt.AlignVCenter)): Qt.AlignLeft | Qt.AlignVCenter, Qt.AlignRight | Qt.AlignVCenter)):
item = self.horizontalHeaderItem(i) item = self.horizontalHeaderItem(i)
@@ -513,8 +665,21 @@ class TrackTable(QTableWidget):
duration.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter) duration.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
duration.setForeground(QColor("#aaa59c")) duration.setForeground(QColor("#aaa59c"))
self.setItem(i, 3, duration) self.setItem(i, 3, duration)
more = QTableWidgetItem()
more.setIcon(make_icon("more", "#9d988f", 16))
more.setTextAlignment(Qt.AlignCenter)
more.setToolTip("更多操作")
self.setItem(i, 4, more)
self._paint_row_state(i) self._paint_row_state(i)
self.verticalScrollBar().setValue(0) self.verticalScrollBar().setValue(0)
self.setIconSize(QSize(16, 16))
def _show_more_menu(self, song, position):
menu = QMenu(self)
action = menu.addAction("下一首播放")
action.triggered.connect(lambda: self.next_requested.emit(dict(song)))
menu.exec(position)
menu.deleteLater()
def _paint_row_state(self, row, hover=None): def _paint_row_state(self, row, hover=None):
if not (0 <= row < self.rowCount()): if not (0 <= row < self.rowCount()):
@@ -523,13 +688,14 @@ class TrackTable(QTableWidget):
and self.songs[row].get("id") == self.playing_id) and self.songs[row].get("id") == self.playing_id)
hot = self._hover_row if hover is None else hover hot = self._hover_row if hover is None else hover
color = QColor("#fdf3f1" if current else ("#f5f2ed" if row == hot else "#fffefa")) color = QColor("#fdf3f1" if current else ("#f5f2ed" if row == hot else "#fffefa"))
for col in (0, 2, 3): for col in (0, 1, 2, 3, 4):
item = self.item(row, col) item = self.item(row, col)
if item: if item:
item.setBackground(color) item.setBackground(color)
cell = self.cellWidget(row, 1) cell = self.cellWidget(row, 1)
if cell: if cell:
cell.setStyleSheet("background:%s;" % color.name()) cell.setAttribute(Qt.WA_StyledBackground, False)
cell.setStyleSheet("background:transparent;")
def mouseMoveEvent(self, event): def mouseMoveEvent(self, event):
row = self.indexAt(event.position().toPoint()).row() row = self.indexAt(event.position().toPoint()).row()
@@ -586,8 +752,13 @@ class TrackTable(QTableWidget):
def _on_click(self, index): def _on_click(self, index):
row = index.row() row = index.row()
if 0 <= row < len(self.songs): if not (0 <= row < len(self.songs)):
self.play_requested.emit(self.songs, row) return
if index.column() == 4:
pos = self.viewport().mapToGlobal(self.visualRect(index).bottomLeft())
self._show_more_menu(self.songs[row], pos)
return
self.play_requested.emit(self.songs, row)
def icon_button(name, size=36, icon_size=18, color="#5f5b55", tooltip=""): def icon_button(name, size=36, icon_size=18, color="#5f5b55", tooltip=""):