Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat](gui) show video in homepage and show high-quality image #889

Merged
merged 3 commits into from
Dec 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion feeluown/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ def create_config() -> Config:
default={
'contents': [
{'name': 'RecListDailySongs', 'provider': 'netease'},
# {'name': 'RecACollectionOfSongs', 'provider': 'qqmusic'},
{'name': 'RecListDailyPlaylists', 'provider': 'qqmusic'},
{'name': 'RecACollectionOfSongs', 'provider': 'qqmusic'},
{'name': 'RecACollectionOfVideos', 'provider': 'bilibili'},
]
},
desc='主页配置'
Expand Down
61 changes: 33 additions & 28 deletions feeluown/gui/drawers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,25 @@
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

from feeluown.gui.helpers import random_solarized_color, painter_save, IS_MACOS


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()
Expand All @@ -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 \
Expand All @@ -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()
Expand All @@ -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)
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
39 changes: 31 additions & 8 deletions feeluown/gui/pages/homepage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -182,20 +182,30 @@ 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)
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()


class View(QWidget, BgTransparentMixin):
Expand All @@ -206,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 = []
Expand All @@ -225,10 +235,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)
Expand Down
10 changes: 10 additions & 0 deletions feeluown/gui/widgets/cover_label.py
Original file line number Diff line number Diff line change
Expand Up @@ -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('/path/to/test.png'))
17 changes: 12 additions & 5 deletions feeluown/gui/widgets/img_card_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand All @@ -186,7 +188,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):
Expand Down Expand Up @@ -225,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
Expand Down
45 changes: 29 additions & 16 deletions feeluown/gui/widgets/song_minicard_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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.
Expand All @@ -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)

Expand All @@ -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)
Expand All @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions feeluown/library/provider_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading