feat: redesign play queue and per-track queue actions (1.2.3)
This commit is contained in:
1 parent
1fccf2bed8
commit
2a55d0dc2c
5 files changed
+295
-104
No files matched your search
@@ -12,7 +12,7 @@
|
||||
- 标准 / 较高 / 极高 / 无损音质切换
|
||||
- 逐行歌词、精确居中、点击歌词跳转
|
||||
- 全屏播放页、黑胶旋转、音量与播放进度控制
|
||||
- 播放栏显示当前播放歌单
|
||||
- 播放队列面板:下一首播放、删除单曲、清空保留当前
|
||||
- 应用内检查更新、自动下载、SHA-256 校验和重启安装
|
||||
|
||||
## 开发运行
|
||||
|
||||
+77
-79
@@ -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
@@ -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
@@ -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
@@ -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=""):
|
||||
|
||||
Reference in new issue
Block a user