diff --git a/Makefile b/Makefile index 372e7c44f..9218056a5 100644 --- a/Makefile +++ b/Makefile @@ -38,6 +38,16 @@ mypy: mypy ${MYPY_PKGS} mypy --check-untyped-defs ${MYPY_STRICT_PKGS} +# mypy has several issues: +# 1. mypy behaves inconsistently between local and CI environments. +# 2. mypy checks other files even if only one file is specified. +# +# So give pyright a try. +PYRIGHT_PKGS= +PYRIGHT_PKGS+=feeluown/gui/uimain/sidebar.py +pyright: + pyright ${PYRIGHT_PKGS} + flake8: flake8 feeluown/ tests/ diff --git a/feeluown/gui/drawers.py b/feeluown/gui/drawers.py index 78248edbe..6b38b345b 100644 --- a/feeluown/gui/drawers.py +++ b/feeluown/gui/drawers.py @@ -419,11 +419,11 @@ def draw(self, painter: QPainter, palette: QPalette): class FireIconDrawer: - def __init__(self, width, padding): - self.width = width + def __init__(self, length, padding): + self.length = length self.padding = padding - def draw(self, painter): + def paint(self, painter): # flake8: noqa: E501 # The following path is copied from a SVG file. path = QPainterPath() @@ -450,19 +450,21 @@ def draw(self, painter): path.closeSubpath() # Calculate scaling factor - scale_factor = 1024 / (self.width - 2 * self.padding) + scale_factor = 1024 / (self.length - 2 * self.padding) painter_sf = 1 / scale_factor # Scale and translate the path transform = painter.transform() transform.scale(painter_sf, painter_sf) transform.translate(self.padding * scale_factor, self.padding * scale_factor) - painter.setTransform(transform) - # Set the brush and pen - pen = painter.pen() - pen.setWidthF(1.5 * scale_factor) - painter.setPen(pen) + with painter_save(painter): + painter.setTransform(transform) + + # Set the brush and pen + pen = painter.pen() + pen.setWidthF(1.5 * scale_factor) + painter.setPen(pen) - # Draw the fire shape - painter.drawPath(path) + # Draw the fire shape + painter.drawPath(path) diff --git a/feeluown/gui/uimain/sidebar.py b/feeluown/gui/uimain/sidebar.py index 373ac298c..b113a61df 100644 --- a/feeluown/gui/uimain/sidebar.py +++ b/feeluown/gui/uimain/sidebar.py @@ -3,7 +3,8 @@ from PyQt5.QtCore import QSize, Qt from PyQt5.QtWidgets import QFrame, QLabel, QVBoxLayout, QSizePolicy, QScrollArea, \ - QHBoxLayout, QFormLayout, QDialog, QLineEdit, QDialogButtonBox, QMessageBox + QHBoxLayout, QFormLayout, QDialog, QLineEdit, QDialogButtonBox, QMessageBox, \ + QWidget from feeluown.excs import ProviderIOError, NoUserLoggedIn from feeluown.library import ( @@ -13,7 +14,7 @@ ) from feeluown.collection import CollectionAlreadyExists, CollectionType from feeluown.utils import aio -from feeluown.utils.reader import create_reader +from feeluown.utils.reader import create_reader, Reader from feeluown.utils.aio import run_fn from feeluown.gui.components import Avatar, CollectionListView from feeluown.gui.widgets import ( @@ -107,67 +108,71 @@ async def show_provider_current_user_playlists(self, provider): playlists = await run_fn(provider.current_user_list_playlists) reader = await run_fn(provider.current_user_fav_create_playlists_rd) - fav_playlists = create_reader(reader).readall() + reader = create_reader(reader) + assert isinstance(reader, Reader) + fav_playlists = reader.readall() self._app.pl_uimgr.add(playlists) self._app.pl_uimgr.add(fav_playlists, is_fav=True) -class _LeftPanel(QFrame): +LVC = ListViewContainer + +class ProviderPanel(QWidget): + """ + A panel shows provider-specific contents. + """ def __init__(self, app: 'GuiApp', parent=None): super().__init__(parent) self._app = app - self.home_btn = HomeButton(height=30, parent=self) self.discovery_btn = DiscoveryButton(height=30, padding=0.2, parent=self) self.fav_btn = StarButton('我的收藏', height=30, parent=self) self.fold_top_btn = TriagleButton(length=14, padding=0.2) self.fold_top_btn.setCheckable(True) - self.collections_header = QLabel('本地收藏集', self) - self.collections_header.setToolTip('我们可以在本地建立『收藏集』来收藏自己喜欢的音乐资源\n\n' - '每个收藏集都以一个独立 .fuo 文件的存在,' - '将鼠标悬浮在收藏集上,可以查看文件所在路径。\n' - '新建 fuo 文件,则可以新建收藏集,文件名即是收藏集的名字。\n\n' - '手动编辑 fuo 文件即可编辑收藏集中的音乐资源,也可以在界面上拖拽来增删歌曲。') + self.playlists_header = QLabel('歌单列表', self) self.my_music_header = QLabel('我的音乐', self) + self._layout = QVBoxLayout(self) self.playlists_view = PlaylistsView(self) self.my_music_view = MyMusicView(self) - self.collections_view = CollectionListView(self._app) - - self.collections_con = ListViewContainer(self.collections_header, - self.collections_view) - self.playlists_con = ListViewContainer(self.playlists_header, - self.playlists_view) - self.my_music_con = ListViewContainer(self.my_music_header, self.my_music_view) - self.playlists_view.setModel(self._app.pl_uimgr.model) self.my_music_view.setModel(self._app.mymusic_uimgr.model) - self._top_separator = Separator(self._app) - self._layout = QVBoxLayout(self) - self._avatar_layout = QHBoxLayout() + self.playlists_con = LVC(self.playlists_header, self.playlists_view) + self.my_music_con = LVC(self.my_music_header, self.my_music_view) + self.playlists_view.show_playlist.connect( + lambda pl: self._app.browser.goto(model=pl)) + self.playlists_view.remove_playlist.connect(self._remove_playlist) + self.playlists_con.create_btn.clicked.connect(self._create_playlist) + self._app.current_pvd_ui_mgr.changed.connect( + self.on_current_pvd_ui_changed) + self.discovery_btn.clicked.connect( + lambda: self._app.browser.goto(page='/rec')) + self.fav_btn.clicked.connect( + lambda: self._app.browser.goto(page='/my_fav')) + + self.setup_ui() + + def setup_ui(self): self._layout.setSpacing(0) - self._layout.setContentsMargins(16, 10, 16, 0) - self._layout.addWidget(self.home_btn) - self._layout.addWidget(self.collections_con) - self._layout.addWidget(self._top_separator) - self._layout.addLayout(self._avatar_layout) + self._layout.setContentsMargins(0, 0, 0, 0) + + self.playlists_view.setFrameShape(QFrame.NoFrame) + self.my_music_view.setFrameShape(QFrame.NoFrame) + + self._avatar_layout = QHBoxLayout() self._avatar_layout.addWidget(Avatar(self._app, height=48)) self._avatar_layout.addWidget(self.fold_top_btn) + + self._layout.addLayout(self._avatar_layout) self._layout.addWidget(self.discovery_btn) self._layout.addWidget(self.fav_btn) self._layout.addWidget(self.my_music_con) self._layout.addWidget(self.playlists_con) - self._layout.addStretch(0) - self.playlists_view.setFrameShape(QFrame.NoFrame) - self.my_music_view.setFrameShape(QFrame.NoFrame) - self.collections_view.setFrameShape(QFrame.NoFrame) - self.setFrameShape(QFrame.NoFrame) - self.collections_con.create_btn.show() # 让各个音乐库来决定是否显示这些组件 self.playlists_con.hide() self.my_music_con.hide() @@ -176,67 +181,14 @@ def __init__(self, app: 'GuiApp', parent=None): self.discovery_btn.setToolTip('当前资源提供方未知') self.fold_top_btn.setToolTip('折叠/打开“主页和本地收藏集”功能') - if self._app.config.ENABLE_NEW_HOMEPAGE is True: - self.home_btn.clicked.connect( - lambda: self._app.browser.goto(page='/homepage')) - else: - self.home_btn.clicked.connect(self.show_library) - - self.discovery_btn.clicked.connect(self.show_pool) - self.playlists_view.show_playlist.connect( - lambda pl: self._app.browser.goto(model=pl)) - self.collections_view.show_collection.connect( - lambda coll: self._app.browser.goto(page=f'/colls/{coll.identifier}')) - self.collections_view.remove_collection.connect(self._remove_coll) - self.playlists_view.remove_playlist.connect(self._remove_playlist) - self.collections_con.create_btn.clicked.connect( - self.popup_collection_adding_dialog) - self.playlists_con.create_btn.clicked.connect(self._create_playlist) - self._app.current_pvd_ui_mgr.changed.connect( - self.on_current_pvd_ui_changed) - self.discovery_btn.clicked.connect( - lambda: self._app.browser.goto(page='/rec')) - self.fav_btn.clicked.connect( - lambda: self._app.browser.goto(page='/my_fav')) - self.fold_top_btn.clicked.connect(self._toggle_top_layout) - - def _toggle_top_layout(self, checked): - widgets = [self._top_separator, self.collections_con, self.home_btn] - if checked: - self.fold_top_btn.set_direction('down') - for w in widgets: - w.hide() + def on_current_pvd_ui_changed(self, pvd_ui, _): + if pvd_ui: + self.discovery_btn.setEnabled(True) + self.fav_btn.setEnabled(True) + self.discovery_btn.setToolTip(f'点击进入 {pvd_ui.provider.name} 推荐页') else: - self.fold_top_btn.set_direction('up') - for w in widgets: - w.show() - - def popup_collection_adding_dialog(self): - dialog = QDialog(self) - # Set WA_DeleteOnClose so that the dialog can be deleted (from self.children). - dialog.setAttribute(Qt.WA_DeleteOnClose) - layout = QFormLayout(dialog) - id_edit = QLineEdit(dialog) - title_edit = QLineEdit(dialog) - layout.addRow('ID', id_edit) - layout.addRow('标题', title_edit) - button_box = QDialogButtonBox(QDialogButtonBox.Cancel | QDialogButtonBox.Save) - layout.addRow('', button_box) - button_box.accepted.connect(dialog.accept) - button_box.rejected.connect(dialog.reject) - - def create_collection_and_reload(): - fname = id_edit.text() - title = title_edit.text() - try: - self._app.coll_mgr.create(fname, title) - except CollectionAlreadyExists: - QMessageBox.warning(self, '警告', f"收藏集 '{fname}' 已存在") - else: - self._app.coll_mgr.refresh() - - dialog.accepted.connect(create_collection_and_reload) - dialog.open() + self.discovery_btn.setEnabled(False) + self.fav_btn.setEnabled(False) def _create_playlist(self): provider_ui = self._app.current_pvd_ui_mgr.get() @@ -282,14 +234,6 @@ async def do(): dialog.accepted.connect(create_playlist_and_reload) dialog.open() - def show_library(self): - coll_library = self._app.coll_mgr.get_coll_library() - self._app.browser.goto(page=f'/colls/{coll_library.identifier}') - - def show_pool(self): - coll = self._app.coll_mgr.get(CollectionType.sys_pool) - self._app.browser.goto(page=f'/colls/{coll.identifier}') - def _remove_playlist(self, playlist): async def do(): @@ -307,6 +251,102 @@ async def do(): box.accepted.connect(lambda: aio.run_afn(do)) box.open() + +class _LeftPanel(QFrame): + + def __init__(self, app: 'GuiApp', parent=None): + super().__init__(parent) + self._app = app + + self.home_btn = HomeButton(height=30, parent=self) + self.collections_header = QLabel('本地收藏集', self) + self.collections_header.setToolTip('我们可以在本地建立『收藏集』来收藏自己喜欢的音乐资源\n\n' + '每个收藏集都以一个独立 .fuo 文件的存在,' + '将鼠标悬浮在收藏集上,可以查看文件所在路径。\n' + '新建 fuo 文件,则可以新建收藏集,文件名即是收藏集的名字。\n\n' + '手动编辑 fuo 文件即可编辑收藏集中的音乐资源,也可以在界面上拖拽来增删歌曲。') + self.collections_view = CollectionListView(self._app) + self.collections_con = ListViewContainer(self.collections_header, + self.collections_view) + self._top_separator = Separator(self._app) + self.provider_bar = ProviderPanel(self._app) + + # For backward compatibility. + self.playlists_con = self.provider_bar.playlists_con + self.my_music_con = self.provider_bar.my_music_con + self.playlists_header = self.provider_bar.playlists_header + + self._layout = QVBoxLayout(self) + + self._layout.setSpacing(0) + self._layout.setContentsMargins(16, 10, 16, 0) + self._layout.addWidget(self.home_btn) + self._layout.addWidget(self.collections_con) + self._layout.addWidget(self._top_separator) + self._layout.addWidget(self.provider_bar) + self._layout.addStretch(0) + + self.collections_view.show_collection.connect( + lambda coll: self._app.browser.goto(page=f'/colls/{coll.identifier}')) + self.collections_view.remove_collection.connect(self._remove_coll) + self.collections_con.create_btn.clicked.connect( + self.popup_collection_adding_dialog) + self.collections_view.setFrameShape(QFrame.NoFrame) + self.setFrameShape(QFrame.NoFrame) + self.collections_con.create_btn.show() + self.provider_bar.fold_top_btn.clicked.connect(self._toggle_top_layout) + if self._app.config.ENABLE_NEW_HOMEPAGE is True: + self.home_btn.clicked.connect( + lambda: self._app.browser.goto(page='/homepage')) + else: + self.home_btn.clicked.connect(self.show_library) + + def _toggle_top_layout(self, checked): + widgets = [self._top_separator, self.collections_con, self.home_btn] + if checked: + self.provider_bar.fold_top_btn.set_direction('down') + for w in widgets: + w.hide() + else: + self.provider_bar.fold_top_btn.set_direction('up') + for w in widgets: + w.show() + + def popup_collection_adding_dialog(self): + dialog = QDialog(self) + # Set WA_DeleteOnClose so that the dialog can be deleted (from self.children). + dialog.setAttribute(Qt.WA_DeleteOnClose) + layout = QFormLayout(dialog) + id_edit = QLineEdit(dialog) + title_edit = QLineEdit(dialog) + layout.addRow('ID', id_edit) + layout.addRow('标题', title_edit) + button_box = QDialogButtonBox(QDialogButtonBox.Cancel | QDialogButtonBox.Save) + layout.addRow('', button_box) + button_box.accepted.connect(dialog.accept) + button_box.rejected.connect(dialog.reject) + + def create_collection_and_reload(): + fname = id_edit.text() + title = title_edit.text() + try: + self._app.coll_mgr.create(fname, title) + except CollectionAlreadyExists: + QMessageBox.warning(self, '警告', f"收藏集 '{fname}' 已存在") + else: + self._app.coll_mgr.refresh() + + dialog.accepted.connect(create_collection_and_reload) + dialog.open() + + def show_library(self): + coll_library = self._app.coll_mgr.get_coll_library() + self._app.browser.goto(page=f'/colls/{coll_library.identifier}') + + def show_pool(self): + coll = self._app.coll_mgr.get(CollectionType.sys_pool) + self._app.browser.goto(page=f'/colls/{coll.identifier}') + def _remove_coll(self, coll): def do(): @@ -317,12 +357,3 @@ def do(): QMessageBox.Yes | QMessageBox.No, self) box.accepted.connect(do) box.open() - - def on_current_pvd_ui_changed(self, pvd_ui, _): - if pvd_ui: - self.discovery_btn.setEnabled(True) - self.fav_btn.setEnabled(True) - self.discovery_btn.setToolTip(f'点击进入 {pvd_ui.provider.name} 推荐页') - else: - self.discovery_btn.setEnabled(False) - self.fav_btn.setEnabled(False) diff --git a/feeluown/gui/widgets/__init__.py b/feeluown/gui/widgets/__init__.py index 0c392c262..3dea0451f 100644 --- a/feeluown/gui/widgets/__init__.py +++ b/feeluown/gui/widgets/__init__.py @@ -6,5 +6,5 @@ PlusButton, TriagleButton, DiscoveryButton, SelfPaintAbstractIconTextButton, CalendarButton, RankButton, StarButton, PlayPauseButton, PlayNextButton, PlayPreviousButton, - MVButton, VolumeButton, + MVButton, VolumeButton, HotButton, ) diff --git a/feeluown/gui/widgets/selfpaint_btn.py b/feeluown/gui/widgets/selfpaint_btn.py index 3ec03b6e4..e8db69c8f 100644 --- a/feeluown/gui/widgets/selfpaint_btn.py +++ b/feeluown/gui/widgets/selfpaint_btn.py @@ -344,6 +344,15 @@ def draw_icon(self, painter): self.star_icon.paint(painter) +class HotButton(SelfPaintAbstractIconTextButton): + def __init__(self, text='热门', *args, **kwargs): + super().__init__(text, *args, **kwargs) + self.hot_icon = FireIconDrawer(self.height(), self._padding) + + def draw_icon(self, painter): + self.hot_icon.paint(painter) + + class PlayButton(SelfPaintAbstractSquareButton): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -517,18 +526,6 @@ def paintEvent(self, _): self.drawer.draw(painter, self.palette()) -class FireButton(SelfPaintAbstractSquareButton): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.drawer = FireIconDrawer(self.width(), self._padding) - - def paintEvent(self, _): - painter = QPainter(self) - painter.setRenderHint(QPainter.Antialiasing) - self.paint_round_bg_when_hover(painter) - self.drawer.draw(painter) - - if __name__ == '__main__': from PyQt5.QtWidgets import QHBoxLayout, QVBoxLayout @@ -563,5 +560,5 @@ def paintEvent(self, _): volume_button = VolumeButton(length=length) volume_button.set_volume(60) l2.addWidget(volume_button) - l2.addWidget(FireButton(length=100)) + l2.addWidget(HotButton(height=100)) l2.addStretch(0)