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 校验和重启安装
## 开发运行
+77 -79
View File
@@ -7,11 +7,11 @@ import sys
import requests
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.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QLineEdit, QStackedWidget, QListWidget, QListWidgetItem, QSlider, QComboBox,
QLineEdit, QStackedWidget, QSlider, QComboBox, QToolTip,
QGridLayout, QSizePolicy, QProgressBar, QDialog,
)
@@ -22,7 +22,7 @@ from version import APP_NAME, APP_VERSION
from theme import QSS, ACCENT
from widgets import (
make_icon, rounded_pixmap, DiscWidget, ClickSlider, LyricView, TrackTable, icon_button,
CoverBackdrop,
CoverBackdrop, QueuePopup,
)
@@ -108,8 +108,6 @@ class MainWindow(QMainWindow):
self.queue = []
self.q_index = -1
self.current = None
self.stack_context_name = "发现音乐"
self.current_playlist_name = ""
self.mode = "list"
self.quality = "exhigh"
self.volume = 78
@@ -230,6 +228,7 @@ class MainWindow(QMainWindow):
self.stack.addWidget(self.charts_page)
self.table = TrackTable()
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.stack.addWidget(self.table)
self.empty_page = self._build_empty_page()
@@ -242,6 +241,11 @@ class MainWindow(QMainWindow):
self.fs.setParent(root)
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):
side = QWidget(objectName="side")
side.setFixedWidth(224)
@@ -281,26 +285,7 @@ class MainWindow(QMainWindow):
layout.addWidget(button)
self.nav_btns[key] = button
queue_header = QHBoxLayout()
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)
layout.addStretch()
version_row = QHBoxLayout()
version_row.setContentsMargins(10, 8, 2, 0)
self.version_label = QLabel("v%s" % APP_VERSION, objectName="versionLabel")
@@ -468,11 +453,8 @@ class MainWindow(QMainWindow):
layout.addLayout(center, 1)
right_widget = QWidget()
right_widget.setFixedWidth(290)
right_column = QVBoxLayout(right_widget)
right_column.setContentsMargins(0, 1, 0, 0)
right_column.setSpacing(4)
right = QHBoxLayout()
right_widget.setFixedWidth(324)
right = QHBoxLayout(right_widget)
right.setContentsMargins(0, 0, 0, 0)
right.setSpacing(7)
right.addStretch()
@@ -502,12 +484,9 @@ class MainWindow(QMainWindow):
self.vol_slider.setToolTip("音量")
self.vol_slider.valueChanged.connect(self._on_volume)
right.addWidget(self.vol_slider)
right_column.addLayout(right)
self.bar_playlist = QLabel("歌单 · 未播放", objectName="barPlaylist")
self.bar_playlist.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.bar_playlist.setFixedHeight(14)
self.bar_playlist.setToolTip("当前播放队列来源")
right_column.addWidget(self.bar_playlist)
self.btn_queue = icon_button("bars", 34, 18, tooltip="播放队列")
self.btn_queue.clicked.connect(self._toggle_queue_popup)
right.addWidget(self.btn_queue)
layout.addWidget(right_widget)
return bar
@@ -629,20 +608,17 @@ class MainWindow(QMainWindow):
self.page_eyebrow.setText("LIBRARY / DISCOVER")
self.page_title.setText("发现音乐")
self.page_meta.setText("网易云音乐官方榜单")
self.stack_context_name = "发现音乐"
self.stack.setCurrentWidget(self.charts_page)
elif key == "search":
self.page_eyebrow.setText("LIBRARY / SEARCH")
self.page_title.setText("搜索")
self.page_meta.setText("在网易云音乐曲库中寻找声音")
self.stack_context_name = "搜索"
self._show_empty("搜索一首歌", "输入歌曲、歌手或专辑名称,按回车开始")
self.search_input.setFocus()
else:
self.page_eyebrow.setText("LIBRARY / HISTORY")
self.page_title.setText("最近播放")
self.page_meta.setText("保存在这台电脑上的播放记录")
self.stack_context_name = "最近播放"
if not self.recent:
self._show_empty("还没有播放记录", "播放过的歌曲会自动保存在这里")
else:
@@ -788,7 +764,6 @@ class MainWindow(QMainWindow):
token = self.view_token
self.page_eyebrow.setText("CHARTS / OFFICIAL")
self.page_title.setText(name)
self.stack_context_name = name
self.btn_back.setVisible(True)
self.btn_refresh.setVisible(False)
self.btn_play_all.setVisible(False)
@@ -877,7 +852,6 @@ class MainWindow(QMainWindow):
)
return
self.page_title.setText(playlist.get("name") or "导入歌单")
self.stack_context_name = playlist.get("name") or "导入歌单"
total = playlist.get("trackCount") or len(songs)
extra = " · 当前导入前 %d 首" % len(songs) if total > len(songs) else ""
self.page_meta.setText("共 %d 首%s · 来自网易云音乐" % (total, extra))
@@ -923,7 +897,6 @@ class MainWindow(QMainWindow):
self.view_token += 1
token = self.view_token
self.page_meta.setText("正在搜索「%s」…" % keyword)
self.stack_context_name = "搜索 · %s" % keyword
self.btn_play_all.setVisible(False)
self._show_empty("正在搜索", "在整个曲库中寻找「%s」" % keyword, busy=True)
@@ -950,41 +923,79 @@ class MainWindow(QMainWindow):
# ---------- 队列 ----------
def _render_queue(self):
self.queue_list.clear()
for i, song in enumerate(self.queue):
item = QListWidgetItem("%s\n%s" % (song.get("name", ""), core.artists_of(song)))
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))
if hasattr(self, "queue_popup") and self.queue_popup.isVisible():
current_id = self.current.get("id") if self.current else None
self.queue_popup.set_queue(self.queue, self.q_index, current_id)
def _on_queue_click(self, item):
index = item.data(Qt.UserRole)
if index is not None and 0 <= int(index) < len(self.queue):
self._play_index(int(index))
def _toggle_queue_popup(self):
if self.queue_popup.isVisible():
self.queue_popup.hide()
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):
self.queue = []
self.q_index = -1
if self.current is not None:
self.queue = [self.current]
self.q_index = 0
else:
self.queue = []
self.q_index = -1
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):
self.queue = list(songs)
self._set_current_playlist(self.stack_context_name)
self._play_index(index)
if not (0 <= index < len(songs)):
return
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):
if self.stack_songs:
self.queue = list(self.stack_songs)
self._set_current_playlist(self.stack_context_name)
self._play_index(0)
def _play_index(self, index):
@@ -1147,19 +1158,6 @@ class MainWindow(QMainWindow):
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):
song = self.current
if not song:
+35 -13
View File
@@ -29,15 +29,6 @@ QPushButton#nav {
}
QPushButton#nav:hover { color: #f3f3f4; background: #28292e; }
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; }
QPushButton#versionBtn {
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; }
#barArtist { font-size: 11px; color: #918d85; }
#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; }
QSlider::groove:horizontal { height: 4px; background: #e8e4de; 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;
}
/* 播放队列弹层 */
#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; }
#importTitle { color:#22211f; font-size:19px; font-weight:700; }
+1 -1
View File
@@ -1,7 +1,7 @@
"""应用版本与更新地址。"""
APP_NAME = "网易云播放器"
APP_VERSION = "1.2.2"
APP_VERSION = "1.2.3"
BUILD_DATE = "2026-10-31"
UPDATE_ENDPOINT = "https://music.yuncishu.xyz/api/app/update"
DOWNLOAD_PAGE = "https://music.yuncishu.xyz/download/"
+181 -10
View File
@@ -2,7 +2,7 @@
import bisect
from PySide6.QtCore import (
Qt, QSize, QRectF, QByteArray, QTimer, QElapsedTimer,
Qt, QSize, QPoint, QRectF, QByteArray, QTimer, QElapsedTimer,
QPropertyAnimation, QEasingCurve, Signal,
)
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 (
QWidget, QLabel, QPushButton, QSlider, QStyle, QScrollArea, QVBoxLayout, QHBoxLayout,
QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView, QSizePolicy,
QGraphicsScene, QGraphicsBlurEffect,
QGraphicsScene, QGraphicsBlurEffect, QFrame, QGraphicsDropShadowEffect, QMenu,
)
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"/>',
"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"/>',
"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"/>',
"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"/>',
@@ -404,12 +406,160 @@ class LyricView(QScrollArea):
super().wheelEvent(event)
class TrackTable(QTableWidget):
play_requested = Signal(list, int)
COLS = ["", "歌曲", "专辑", "时长"]
class QueueRow(QWidget):
play_requested = Signal(int)
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):
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.playing_id = None
self.covers = {}
@@ -445,6 +595,8 @@ class TrackTable(QTableWidget):
self.setColumnWidth(2, 190)
header.setSectionResizeMode(3, QHeaderView.Fixed)
self.setColumnWidth(3, 72)
header.setSectionResizeMode(4, QHeaderView.Fixed)
self.setColumnWidth(4, 42)
for i, alignment in enumerate((Qt.AlignCenter, Qt.AlignLeft | Qt.AlignVCenter,
Qt.AlignLeft | Qt.AlignVCenter, Qt.AlignRight | Qt.AlignVCenter)):
item = self.horizontalHeaderItem(i)
@@ -513,8 +665,21 @@ class TrackTable(QTableWidget):
duration.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
duration.setForeground(QColor("#aaa59c"))
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.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):
if not (0 <= row < self.rowCount()):
@@ -523,13 +688,14 @@ class TrackTable(QTableWidget):
and self.songs[row].get("id") == self.playing_id)
hot = self._hover_row if hover is None else hover
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)
if item:
item.setBackground(color)
cell = self.cellWidget(row, 1)
if cell:
cell.setStyleSheet("background:%s;" % color.name())
cell.setAttribute(Qt.WA_StyledBackground, False)
cell.setStyleSheet("background:transparent;")
def mouseMoveEvent(self, event):
row = self.indexAt(event.position().toPoint()).row()
@@ -586,8 +752,13 @@ class TrackTable(QTableWidget):
def _on_click(self, index):
row = index.row()
if 0 <= row < len(self.songs):
self.play_requested.emit(self.songs, row)
if not (0 <= row < len(self.songs)):
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=""):