From 31bfdcd1e3843597f878e35f15de091f236b4c4d Mon Sep 17 00:00:00 2001 From: cosven Date: Sat, 30 Nov 2024 18:00:33 +0800 Subject: [PATCH 1/3] [feat](gui) show videos on homepage --- feeluown/app/config.py | 5 ++-- feeluown/gui/pages/homepage.py | 33 ++++++++++++++++++++++----- feeluown/gui/widgets/img_card_list.py | 6 +++-- feeluown/library/provider_protocol.py | 11 +++++++++ 4 files changed, 45 insertions(+), 10 deletions(-) diff --git a/feeluown/app/config.py b/feeluown/app/config.py index ba51511e6..f60b93c03 100644 --- a/feeluown/app/config.py +++ b/feeluown/app/config.py @@ -24,9 +24,10 @@ def create_config() -> Config: type_=dict, default={ 'contents': [ - {'name': 'RecListDailySongs', 'provider': 'netease'}, + {'name': 'RecACollectionOfVideos', 'provider': 'bilibili'}, + #{'name': 'RecListDailySongs', 'provider': 'netease'}, {'name': 'RecListDailyPlaylists', 'provider': 'qqmusic'}, - {'name': 'RecACollectionOfSongs', 'provider': 'qqmusic'}, + #{'name': 'RecACollectionOfSongs', 'provider': 'qqmusic'}, ] }, desc='主页配置' diff --git a/feeluown/gui/pages/homepage.py b/feeluown/gui/pages/homepage.py index 23384fa11..a3169a52e 100644 --- a/feeluown/gui/pages/homepage.py +++ b/feeluown/gui/pages/homepage.py @@ -7,7 +7,7 @@ from feeluown.library import ( SupportsRecListDailyPlaylists, SupportsRecACollectionOfSongs, Collection, - SupportsRecListDailySongs, Provider + SupportsRecListDailySongs, Provider, SupportsRecACollectionOfVideos, ) from feeluown.utils.reader import create_reader from feeluown.utils.aio import run_fn, gather, run_afn @@ -182,20 +182,28 @@ async def render(self): class RecVideosPanel(Panel): - def __init__(self, app: 'GuiApp', provider): + def __init__(self, app: 'GuiApp', provider: SupportsRecACollectionOfVideos): self._app = app self._provider = provider self.video_list_view = video_list_view = VideoCardListView() - video_list_view.setItemDelegate(VideoCardListDelegate(video_list_view)) + video_list_view.setItemDelegate(VideoCardListDelegate( + video_list_view, + card_min_width=200, + )) pixmap = Panel.get_provider_pixmap(app, provider.identifier) - super().__init__('热门视频', video_list_view, pixmap) + super().__init__('瞅瞅', video_list_view, pixmap) video_list_view.play_video_needed.connect(self._app.playlist.play_model) async def render(self): videos = await run_fn(self._provider.rec_a_collection_of_videos) - model = VideoCardListModel.create(videos[:8], self._app) - self.video_list_view.setModel(model) + if videos: + # TODO: maybe show all videos + model = VideoCardListModel.create(videos[:8], self._app) + self.video_list_view.setModel(model) + else: + self.header.setText('暂无推荐视频') + self.video_list_view.hide() class View(QWidget, BgTransparentMixin): @@ -225,10 +233,23 @@ async def render(self): panel = self._handle_rec_a_collection_of_songs(content) if panel is not None: panels.append(panel) + elif name == 'RecACollectionOfVideos': + panel = self._handle_rec_a_collection_of_videos(content) + if panel is not None: + panels.append(panel) for panel in panels: self._layout.addWidget(panel) gather(*[panel.render() for panel in panels]) + def _handle_rec_a_collection_of_videos(self, content: dict) -> Optional[Panel]: + source = content['provider'] + provider = self._app.library.get(source) + if isinstance(provider, SupportsRecACollectionOfVideos): + return RecVideosPanel(self._app, provider) + logger.warning(f'Invalid homepage content: {content}, ' + f'provider {source} not found or not supported') + return None + def _handle_rec_list_daily_songs(self, content: dict) -> Optional[Panel]: source = content['provider'] provider = self._app.library.get(source) diff --git a/feeluown/gui/widgets/img_card_list.py b/feeluown/gui/widgets/img_card_list.py index e907b6af0..86736f024 100644 --- a/feeluown/gui/widgets/img_card_list.py +++ b/feeluown/gui/widgets/img_card_list.py @@ -186,7 +186,9 @@ def paint(self, painter, option, index): return with painter_save(painter): - painter.setRenderHint(QPainter.Antialiasing) + painter.setRenderHints(QPainter.Antialiasing | + QPainter.SmoothPixmapTransform | + QPainter.LosslessImageRendering) painter.translate(option.rect.x(), option.rect.y()) if not self.is_leftmost(index): @@ -234,7 +236,7 @@ def draw_img_or_color(self, painter, border_color, obj, draw_width, height): border_radius = 3 if self.as_circle: border_radius = draw_width // 2 - cover_rect = QRect(0, 0, draw_width, height) + cover_rect = QRectF(0, 0, draw_width, height) painter.drawRoundedRect(cover_rect, border_radius, border_radius) def draw_title(self, painter, index, text_rect): diff --git a/feeluown/library/provider_protocol.py b/feeluown/library/provider_protocol.py index 0c74ad1fb..99b52c866 100644 --- a/feeluown/library/provider_protocol.py +++ b/feeluown/library/provider_protocol.py @@ -419,6 +419,17 @@ def rec_a_collection_of_songs(self) -> Collection: """ +@runtime_checkable +class SupportsRecACollectionOfVideos(Protocol): + @abstractmethod + def rec_a_collection_of_videos(self) -> Collection: + """ + For example, providers may recommend a list of videos. + For different user, this API may return different result. + This API MAY return different result at different time. + """ + + @runtime_checkable class SupportsRecListDailyPlaylists(Protocol): @abstractmethod From be339c74fa0aad27d2d7921dbf8dd525ff90447d Mon Sep 17 00:00:00 2001 From: cosven Date: Sat, 30 Nov 2024 20:10:11 +0800 Subject: [PATCH 2/3] respect device_pixel_ratio when drawing image --- feeluown/app/config.py | 4 +- feeluown/gui/drawers.py | 61 +++++++++++++++------------ feeluown/gui/widgets/cover_label.py | 10 +++++ feeluown/gui/widgets/img_card_list.py | 2 +- 4 files changed, 46 insertions(+), 31 deletions(-) diff --git a/feeluown/app/config.py b/feeluown/app/config.py index f60b93c03..33583926b 100644 --- a/feeluown/app/config.py +++ b/feeluown/app/config.py @@ -25,9 +25,9 @@ def create_config() -> Config: default={ 'contents': [ {'name': 'RecACollectionOfVideos', 'provider': 'bilibili'}, - #{'name': 'RecListDailySongs', 'provider': 'netease'}, + # {'name': 'RecListDailySongs', 'provider': 'netease'}, {'name': 'RecListDailyPlaylists', 'provider': 'qqmusic'}, - #{'name': 'RecACollectionOfSongs', 'provider': 'qqmusic'}, + # {'name': 'RecACollectionOfSongs', 'provider': 'qqmusic'}, ] }, desc='主页配置' diff --git a/feeluown/gui/drawers.py b/feeluown/gui/drawers.py index be2cc9ca1..d5a3c7ba1 100644 --- a/feeluown/gui/drawers.py +++ b/feeluown/gui/drawers.py @@ -4,7 +4,7 @@ from PyQt5.QtCore import Qt, QRect, QPoint, QPointF from PyQt5.QtGui import ( QPainter, QBrush, QPixmap, QImage, QColor, QPolygonF, QPalette, - QPainterPath + QPainterPath, QGuiApplication, ) from PyQt5.QtWidgets import QWidget @@ -12,13 +12,17 @@ class SizedPixmapDrawer: - def __init__(self, img, rect: QRect, radius: int = 0): - """ - :param widget: a object which has width() and height() method. - """ + """ + Draw pixmap on a specific rect (on a fixed area). + + Note that if device_pixel_ratio is not properly set, the drawed image + quality may be poor. + """ + def __init__(self, img: Optional[QImage], rect: QRect, radius: int = 0): self._rect = rect self._img_old_width = rect.width() self._radius = radius + self._device_pixel_ratio = QGuiApplication.instance().devicePixelRatio() if img is None: self._color = random_solarized_color() @@ -27,8 +31,13 @@ def __init__(self, img, rect: QRect, radius: int = 0): else: self._img = img self._color = None - new_img = img.scaledToWidth(self._img_old_width, Qt.SmoothTransformation) + new_img = self._scale_image(img) self._pixmap = QPixmap(new_img) + self._pixmap.setDevicePixelRatio(self._device_pixel_ratio) + + def _scale_image(self, img: QImage) -> QImage: + return img.scaledToWidth(int(self._img_old_width * self._device_pixel_ratio), + Qt.SmoothTransformation) def get_radius(self): return self._radius if self._radius >= 1 else \ @@ -37,9 +46,6 @@ def get_radius(self): def get_rect(self): return self._rect - def maybe_update_pixmap(self): - pass - @classmethod def from_img_data(cls, img_data, *args, **kwargs): img = QImage() @@ -53,14 +59,11 @@ def get_pixmap(self) -> Optional[QPixmap]: return self._pixmap def draw(self, painter: QPainter): - painter.save() - painter.setRenderHint(QPainter.Antialiasing) - painter.setRenderHint(QPainter.SmoothPixmapTransform) - if self._pixmap is None: - self._draw_random_color(painter) - else: - self._draw_pixmap(painter) - painter.restore() + with painter_save(painter): + if self._pixmap is None: + self._draw_random_color(painter) + else: + self._draw_pixmap(painter) def _draw_random_color(self, painter: QPainter): brush = QBrush(self._color) @@ -76,23 +79,21 @@ def _draw_random_color(self, painter: QPainter): def _draw_pixmap(self, painter: QPainter): assert self._pixmap is not None - self.maybe_update_pixmap() brush = QBrush(self._pixmap) painter.setBrush(brush) painter.setPen(Qt.NoPen) radius = self.get_radius() - size = self._pixmap.size() + size = self._pixmap.size() / self._pixmap.devicePixelRatio() target_rect = self.get_rect() y = (size.height() - target_rect.height()) // 2 - painter.save() - painter.translate(target_rect.x(), target_rect.y()) - painter.translate(0, -y) - rect = QRect(0, 0, target_rect.width(), size.height()) - if radius == 0: - painter.drawRect(rect) - else: - painter.drawRoundedRect(rect, radius, radius) - painter.restore() + with painter_save(painter): + painter.translate(target_rect.x(), target_rect.y()) + painter.translate(0, -y) + rect = QRect(0, 0, target_rect.width(), size.height()) + if radius == 0: + painter.drawRect(rect) + else: + painter.drawRoundedRect(rect, radius, radius) class PixmapDrawer(SizedPixmapDrawer): @@ -120,6 +121,10 @@ def maybe_update_pixmap(self): Qt.SmoothTransformation) self._pixmap = QPixmap(new_img) + def _draw_pixmap(self, painter: QPainter): + self.maybe_update_pixmap() + super()._draw_pixmap(painter) + class AvatarIconDrawer: def __init__(self, length, padding, fg_color=None): diff --git a/feeluown/gui/widgets/cover_label.py b/feeluown/gui/widgets/cover_label.py index 2d5b05c61..093f1d2fc 100644 --- a/feeluown/gui/widgets/cover_label.py +++ b/feeluown/gui/widgets/cover_label.py @@ -84,3 +84,13 @@ async def show_cover(self, url, cover_uid): img = QImage() img.loadFromData(content) self.show_img(img) + + +if __name__ == '__main__': + from feeluown.gui.debug import simple_layout + + with simple_layout() as layout: + label = CoverLabel() + layout.addWidget(label) + label.resize(100, 100) + label.show_img(QImage('/Users/cosven/Desktop/test.png')) diff --git a/feeluown/gui/widgets/img_card_list.py b/feeluown/gui/widgets/img_card_list.py index 86736f024..1275cba5d 100644 --- a/feeluown/gui/widgets/img_card_list.py +++ b/feeluown/gui/widgets/img_card_list.py @@ -236,7 +236,7 @@ def draw_img_or_color(self, painter, border_color, obj, draw_width, height): border_radius = 3 if self.as_circle: border_radius = draw_width // 2 - cover_rect = QRectF(0, 0, draw_width, height) + cover_rect = QRect(0, 0, draw_width, height) painter.drawRoundedRect(cover_rect, border_radius, border_radius) def draw_title(self, painter, index, text_rect): From 22b5b7e4919b65a7dda3d2cfe9e2dd6b9dd10e2a Mon Sep 17 00:00:00 2001 From: cosven Date: Sat, 30 Nov 2024 23:20:49 +0800 Subject: [PATCH 3/3] show high quality pixmap --- feeluown/app/config.py | 6 +-- feeluown/gui/pages/homepage.py | 6 ++- feeluown/gui/widgets/cover_label.py | 2 +- feeluown/gui/widgets/img_card_list.py | 13 +++++-- feeluown/gui/widgets/song_minicard_list.py | 45 ++++++++++++++-------- 5 files changed, 46 insertions(+), 26 deletions(-) diff --git a/feeluown/app/config.py b/feeluown/app/config.py index 33583926b..cfc2fac88 100644 --- a/feeluown/app/config.py +++ b/feeluown/app/config.py @@ -24,10 +24,10 @@ def create_config() -> Config: type_=dict, default={ 'contents': [ - {'name': 'RecACollectionOfVideos', 'provider': 'bilibili'}, - # {'name': 'RecListDailySongs', 'provider': 'netease'}, - {'name': 'RecListDailyPlaylists', 'provider': 'qqmusic'}, + {'name': 'RecListDailySongs', 'provider': 'netease'}, # {'name': 'RecACollectionOfSongs', 'provider': 'qqmusic'}, + {'name': 'RecListDailyPlaylists', 'provider': 'qqmusic'}, + {'name': 'RecACollectionOfVideos', 'provider': 'bilibili'}, ] }, desc='主页配置' diff --git a/feeluown/gui/pages/homepage.py b/feeluown/gui/pages/homepage.py index a3169a52e..edbe764f4 100644 --- a/feeluown/gui/pages/homepage.py +++ b/feeluown/gui/pages/homepage.py @@ -196,11 +196,13 @@ def __init__(self, app: 'GuiApp', provider: SupportsRecACollectionOfVideos): video_list_view.play_video_needed.connect(self._app.playlist.play_model) async def render(self): - videos = await run_fn(self._provider.rec_a_collection_of_videos) + coll = await run_fn(self._provider.rec_a_collection_of_videos) + videos = coll.models if videos: # TODO: maybe show all videos model = VideoCardListModel.create(videos[:8], self._app) self.video_list_view.setModel(model) + self.header.setText(coll.name) else: self.header.setText('暂无推荐视频') self.video_list_view.hide() @@ -214,7 +216,7 @@ def __init__(self, app: 'GuiApp'): self._layout = QVBoxLayout(self) self._layout.setContentsMargins(20, 10, 20, 0) - self._layout.setSpacing(0) + self._layout.setSpacing(10) async def render(self): panels = [] diff --git a/feeluown/gui/widgets/cover_label.py b/feeluown/gui/widgets/cover_label.py index 093f1d2fc..a4778984f 100644 --- a/feeluown/gui/widgets/cover_label.py +++ b/feeluown/gui/widgets/cover_label.py @@ -93,4 +93,4 @@ async def show_cover(self, url, cover_uid): label = CoverLabel() layout.addWidget(label) label.resize(100, 100) - label.show_img(QImage('/Users/cosven/Desktop/test.png')) + label.show_img(QImage('/path/to/test.png')) diff --git a/feeluown/gui/widgets/img_card_list.py b/feeluown/gui/widgets/img_card_list.py index 1275cba5d..3daf7a60b 100644 --- a/feeluown/gui/widgets/img_card_list.py +++ b/feeluown/gui/widgets/img_card_list.py @@ -19,7 +19,7 @@ QRectF, QRect, QSize, QSortFilterProxyModel, pyqtSignal ) from PyQt5.QtGui import ( - QImage, QColor, QResizeEvent, + QImage, QColor, QResizeEvent, QGuiApplication, QBrush, QPainter, QTextOption, QFontMetrics ) from PyQt5.QtWidgets import ( @@ -162,6 +162,8 @@ def __init__(self, parent=None, self.as_circle = True self.w_h_ratio = 1.0 + self._device_pixel_ratio = QGuiApplication.instance().devicePixelRatio() + self.card_min_width = card_min_width self.card_spacing = card_spacing self.card_text_height = card_text_height @@ -227,10 +229,13 @@ def draw_img_or_color(self, painter, border_color, obj, draw_width, height): brush = QBrush(color) painter.setBrush(brush) else: - if obj.height() < obj.width(): - img = obj.scaledToHeight(height, Qt.SmoothTransformation) + if obj.width() / obj.height() > draw_width / height: + img = obj.scaledToHeight(int(height * self._device_pixel_ratio), + Qt.SmoothTransformation) else: - img = obj.scaledToWidth(draw_width, Qt.SmoothTransformation) + img = obj.scaledToWidth(int(draw_width * self._device_pixel_ratio), + Qt.SmoothTransformation) + img.setDevicePixelRatio(self._device_pixel_ratio) brush = QBrush(img) painter.setBrush(brush) border_radius = 3 diff --git a/feeluown/gui/widgets/song_minicard_list.py b/feeluown/gui/widgets/song_minicard_list.py index db4b384d4..d31b43f63 100644 --- a/feeluown/gui/widgets/song_minicard_list.py +++ b/feeluown/gui/widgets/song_minicard_list.py @@ -8,7 +8,7 @@ ) from PyQt5.QtGui import ( QPainter, QPixmap, QImage, QColor, QPalette, QBrush, - QFontMetrics, QTextOption, + QFontMetrics, QTextOption, QGuiApplication, ) from PyQt5.QtWidgets import ( QFrame, QListView, QStyle, QStyledItemDelegate @@ -144,6 +144,8 @@ def __init__( self.card_bottom_padding = card_padding[3] self.card_left_padding = card_padding[0] + self._device_pixel_ratio = QGuiApplication.instance().devicePixelRatio() + def item_sizehint(self) -> tuple: # HELP: listview needs about 20 spacing left on macOS width = max(self.view.width() - 20, self.card_min_width) @@ -158,8 +160,10 @@ def item_sizehint(self) -> tuple: (self.card_min_width + self.card_right_spacing) count = max(count, 1) item_width = (width - ((count + 1) * self.card_right_spacing)) // count - return (item_width, - self.card_height + self.card_top_padding + self.card_bottom_padding) + return ( + item_width, + self.card_height + self.card_top_padding + self.card_bottom_padding + ) def paint(self, painter, option, index): card_top_padding = self.card_top_padding @@ -194,7 +198,7 @@ def paint(self, painter, option, index): painter.restore() painter.save() - painter.translate(rect.x() + card_left_padding, rect.y() + card_top_padding) + painter.translate(rect.x() + card_left_padding, rect.y() + card_top_padding) if selected: text_color = option.palette.color(QPalette.HighlightedText) @@ -211,8 +215,9 @@ def paint(self, painter, option, index): # Draw image. painter.save() painter.translate(0, img_padding) - self.paint_pixmap(painter, non_text_color, obj, - cover_width, cover_height, border_radius) + self.paint_pixmap( + painter, non_text_color, obj, cover_width, cover_height, border_radius + ) painter.restore() # Draw text. @@ -225,16 +230,18 @@ def paint(self, painter, option, index): # Note this is not a bool object. is_enabled = option.state & QStyle.State_Enabled self.paint_text( - painter, is_enabled, title, subtitle, text_color, non_text_color, - text_width, card_height + painter, is_enabled, title, subtitle, text_color, non_text_color, text_width, + card_height ) painter.restore() painter.restore() - def paint_text(self, painter, is_enabled, title, subtitle, - text_color, non_text_color, text_width, text_height): - each_height = text_height//2 + def paint_text( + self, painter, is_enabled, title, subtitle, text_color, non_text_color, + text_width, text_height + ): + each_height = text_height // 2 text_option = QTextOption() text_option.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) @@ -259,8 +266,9 @@ def paint_text(self, painter, is_enabled, title, subtitle, painter.setPen(non_text_color) painter.drawText(subtitle_rect, elided_title, text_option) - def paint_pixmap(self, painter, border_color, decoration, - width, height, border_radius): + def paint_pixmap( + self, painter, border_color, decoration, width, height, border_radius + ): painter.setRenderHint(QPainter.Antialiasing) pen = painter.pen() pen.setColor(border_color) @@ -269,11 +277,16 @@ def paint_pixmap(self, painter, border_color, decoration, color = decoration brush = QBrush(color) painter.setBrush(brush) - else: + else: # QImage if decoration.height() < decoration.width(): - pixmap = decoration.scaledToHeight(height, Qt.SmoothTransformation) + pixmap = decoration.scaledToHeight( + int(height * self._device_pixel_ratio), Qt.SmoothTransformation + ) else: - pixmap = decoration.scaledToWidth(width, Qt.SmoothTransformation) + pixmap = decoration.scaledToWidth( + int(width * self._device_pixel_ratio), Qt.SmoothTransformation + ) + pixmap.setDevicePixelRatio(self._device_pixel_ratio) brush = QBrush(pixmap) painter.setBrush(brush) cover_rect = QRect(0, 0, width, height)