Skip to content

Commit

Permalink
gui: add recommendation page (#742)
Browse files Browse the repository at this point in the history
1. daily recommended songs
2. daily recommended playlists
3. show rank(排行榜) button (but not implemented)
  • Loading branch information
cosven authored Jan 3, 2024
1 parent 300251c commit 893d790
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 8 deletions.
5 changes: 5 additions & 0 deletions feeluown/gui/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@ def initialize(self):
from feeluown.gui.pages.song_explore import render as render_song_explore
from feeluown.gui.pages.provider_home import render as render_provider_home
from feeluown.gui.pages.recently_played import render as render_recently_played
from feeluown.gui.pages.recommendation import render as render_rec
from feeluown.gui.pages.recommendation_daily_songs import \
render as render_rec_daily_songs

model_prefix = f'{MODEL_PAGE_PREFIX}<provider>'

Expand All @@ -209,6 +212,8 @@ async def dummy_render(req, *args, **kwargs):
('/providers/<identifier>', render_provider_home),
('/recently_played', render_recently_played),
('/search', render_search),
('/rec', render_rec),
('/rec/daily_songs', render_rec_daily_songs),
]
for url, renderer in urlpatterns:
self.route(url)(renderer)
45 changes: 45 additions & 0 deletions feeluown/gui/drawers.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,48 @@ def paint(self, painter):
painter.drawLine(self._body_bottom_left, self._body_bottom_right)
painter.drawLine(self._body_top_left, self._body_bottom_left)
painter.drawLine(self._body_top_right, self._body_bottom_right)


class CalendarIconDrawer:
def __init__(self, length, padding):
self._body_x = self._body_y = padding
self._body_width = length - 2 * padding
self._radius = 3
self._h_line_y = self._body_y + self._body_width // 4

def paint(self, painter: QPainter):
pen = painter.pen()
pen.setWidthF(1.5)
painter.setPen(pen)
body_rect = QRect(self._body_x, self._body_x, self._body_width, self._body_width)
painter.drawRoundedRect(body_rect, self._radius, self._radius)
painter.drawLine(QPoint(self._body_x, self._h_line_y),
QPoint(self._body_x + self._body_width, self._h_line_y))


class RankIconDrawer:
def __init__(self, length, padding):
body = length - 2*padding
body_2 = body // 2
body_8 = body // 8
body_3 = body // 3
_top_right_x = length - padding
_top_right_y = padding + body_8
_bottom_left_y = padding + body - body_8

self.p1 = QPoint(padding, _bottom_left_y)
self.p2 = QPoint(padding + body_3, padding + body_3)
self.p3 = QPoint(padding + body_2, padding + body_3 * 2)
self.p4 = QPoint(_top_right_x, _top_right_y)
self.p5 = QPoint(_top_right_x - body_3, _top_right_y)
self.p6 = QPoint(_top_right_x, _top_right_y + body_3)

def paint(self, painter: QPainter):
pen = painter.pen()
pen.setWidthF(1.5)
painter.setPen(pen)
painter.drawLine(self.p1, self.p2)
painter.drawLine(self.p2, self.p3)
painter.drawLine(self.p3, self.p4)
painter.drawLine(self.p4, self.p5)
painter.drawLine(self.p4, self.p6)
98 changes: 98 additions & 0 deletions feeluown/gui/pages/recommendation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from typing import TYPE_CHECKING

from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout

from feeluown.utils.reader import create_reader
from feeluown.utils.aio import run_fn
from feeluown.gui.widgets.header import LargeHeader, MidHeader
from feeluown.gui.widgets.img_card_list import (
PlaylistCardListView, PlaylistCardListModel, PlaylistFilterProxyModel,
PlaylistCardListDelegate,
)

from feeluown.library import SupportsRecListDailyPlaylists, SupportsRecListDailySongs

from feeluown.gui.widgets import CalendarButton, RankButton
from feeluown.gui.helpers import fetch_cover_wrapper


if TYPE_CHECKING:
from feeluown.app.gui_app import GuiApp


async def render(req, **kwargs):
app: 'GuiApp' = req.ctx['app']

view = View(app)
app.ui.right_panel.set_body(view)
await view.render()


class View(QWidget):
def __init__(self, app: 'GuiApp'):
super().__init__(parent=None)
self._app = app

self.header_title = LargeHeader()
self.header_playlist_list = MidHeader()
self.playlist_list_view = PlaylistCardListView(fixed_row_count=1)
self.playlist_list_view.setItemDelegate(
PlaylistCardListDelegate(self.playlist_list_view,
card_min_width=100,))
self.daily_songs_btn = CalendarButton('每日推荐', parent=self)
self.rank_btn = RankButton(parent=self)
self.daily_songs_btn.setMinimumWidth(150)
self.rank_btn.setMinimumWidth(150)

self.header_title.setText('发现音乐')
self.header_playlist_list.setText('个性化推荐')
self.rank_btn.setDisabled(True)
self.rank_btn.setToolTip('未实现,欢迎 PR!')

self._layout = QVBoxLayout(self)
self._setup_ui()

self.playlist_list_view.show_playlist_needed.connect(
lambda model: self._app.browser.goto(model=model))
self.daily_songs_btn.clicked.connect(
lambda: self._app.browser.goto(page='/rec/daily_songs'))
self.rank_btn.clicked.connect(
lambda: self._app.show_msg('未实现,欢迎 PR!'))

def _setup_ui(self):
self._h_layout = QHBoxLayout()
self._h_layout.addWidget(self.daily_songs_btn)
self._h_layout.addSpacing(10)
self._h_layout.addWidget(self.rank_btn)
self._h_layout.addStretch(0)

self._layout.setContentsMargins(20, 10, 20, 0)
self._layout.setSpacing(0)
self._layout.addWidget(self.header_title)
self._layout.addSpacing(10)
self._layout.addLayout(self._h_layout)
self._layout.addSpacing(30)
self._layout.addWidget(self.header_playlist_list)
self._layout.addSpacing(10)
self._layout.addWidget(self.playlist_list_view)
self._layout.addStretch(0)

async def render(self):
pvd_ui = self._app.current_pvd_ui_mgr.get()
if pvd_ui is None:
self._app.show_msg('wtf!')
return

provider = pvd_ui.provider
if isinstance(provider, SupportsRecListDailyPlaylists):
playlists = await run_fn(provider.rec_list_daily_playlists)
model = PlaylistCardListModel(
create_reader(playlists),
fetch_cover_wrapper(self._app),
{p.identifier: p.name for p in self._app.library.list()})
filter_model = PlaylistFilterProxyModel()
filter_model.setSourceModel(model)
self.playlist_list_view.setModel(filter_model)

if not isinstance(provider, SupportsRecListDailySongs):
self.daily_songs_btn.setDisabled(True)
33 changes: 33 additions & 0 deletions feeluown/gui/pages/recommendation_daily_songs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from typing import TYPE_CHECKING

from feeluown.library import SupportsRecListDailySongs
from feeluown.gui.page_containers.table import TableContainer, Renderer
from feeluown.gui.page_containers.scroll_area import ScrollArea
from feeluown.utils.aio import run_fn
from feeluown.utils.reader import create_reader
from .template import render_error_message


if TYPE_CHECKING:
from feeluown.app.gui_app import GuiApp


async def render(req, **_):
app: 'GuiApp' = req.ctx['app']
pvd_ui = app.current_pvd_ui_mgr.get()
if pvd_ui is None:
return await render_error_message(app, '当前资源提供方未知,无法浏览该页面')

provider = pvd_ui.provider

scroll_area = ScrollArea()
body = TableContainer(app, scroll_area)
scroll_area.setWidget(body)
app.ui.right_panel.set_body(scroll_area)
if isinstance(provider, SupportsRecListDailySongs):
songs = await run_fn(provider.rec_list_daily_songs)
renderer = Renderer()
await body.set_renderer(renderer)
renderer.show_songs(create_reader(songs))
renderer.meta_widget.show()
renderer.meta_widget.title = '每日推荐歌曲'
14 changes: 14 additions & 0 deletions feeluown/gui/pages/template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from typing import TYPE_CHECKING

from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QLabel

if TYPE_CHECKING:
from feeluown.app.gui_app import GuiApp


async def render_error_message(app: 'GuiApp', msg: str):
label = QLabel(f"<span style='color: red;'>错误提示:{msg}<span>")
label.setTextFormat(Qt.RichText)
label.setAlignment(Qt.AlignCenter)
app.ui.page_view.set_body(label)
12 changes: 7 additions & 5 deletions feeluown/gui/uimain/sidebar.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@
PlusButton,
TriagleButton,
)
from feeluown.gui.provider_ui import UISupportsDiscovery

from feeluown.gui.widgets.playlists import PlaylistsView
from feeluown.gui.components import CollectionListView
from feeluown.gui.widgets.my_music import MyMusicView
from feeluown.gui.helpers import disconnect_slots_if_has

if TYPE_CHECKING:
from feeluown.app.gui_app import GuiApp
Expand Down Expand Up @@ -156,6 +155,8 @@ def __init__(self, app: 'GuiApp', parent=None):
# 让各个音乐库来决定是否显示这些组件
self.playlists_con.hide()
self.my_music_con.hide()
self.discovery_btn.setDisabled(True)
self.discovery_btn.setToolTip('当前资源提供方未知')

self.home_btn.clicked.connect(self.show_library)
self.discovery_btn.clicked.connect(self.show_pool)
Expand All @@ -170,6 +171,8 @@ def __init__(self, app: 'GuiApp', parent=None):
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'))

def popup_collection_adding_dialog(self):
dialog = QDialog(self)
Expand Down Expand Up @@ -279,6 +282,5 @@ def do():
box.open()

def on_current_pvd_ui_changed(self, pvd_ui, _):
disconnect_slots_if_has(self.discovery_btn)
if isinstance(pvd_ui, UISupportsDiscovery):
self.discovery_btn.clicked.connect(pvd_ui.discovery)
self.discovery_btn.setEnabled(True)
self.discovery_btn.setToolTip(f'点击进入 {pvd_ui.provider.name} 推荐页')
1 change: 1 addition & 0 deletions feeluown/gui/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
SelfPaintAbstractSquareButton, RecentlyPlayedButton,
HomeButton, LeftArrowButton, RightArrowButton, SearchButton, SettingsButton,
PlusButton, TriagleButton, DiscoveryButton,
SelfPaintAbstractIconTextButton, CalendarButton, RankButton,
)
2 changes: 1 addition & 1 deletion feeluown/gui/widgets/header.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ def __init__(self, *args, **kwargs):

class MidHeader(BaseHeader):
def __init__(self, *args, **kwargs):
super().__init__(15, *args, **kwargs)
super().__init__(16, *args, **kwargs)
25 changes: 24 additions & 1 deletion feeluown/gui/widgets/selfpaint_btn.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
from PyQt5.QtWidgets import QPushButton, QStyle, QStyleOptionButton
from PyQt5.QtGui import QPainter, QPalette, QPainterPath

from feeluown.gui.drawers import HomeIconDrawer, PlusIconDrawer, TriangleIconDrawer
from feeluown.gui.drawers import (
HomeIconDrawer, PlusIconDrawer, TriangleIconDrawer, CalendarIconDrawer,
RankIconDrawer,
)
from feeluown.gui.helpers import darker_or_lighter


Expand Down Expand Up @@ -275,6 +278,24 @@ def draw_icon(self, painter):
self.home_icon.paint(painter)


class CalendarButton(SelfPaintAbstractIconTextButton):
def __init__(self, text='日历', *args, **kwargs):
super().__init__(text, *args, **kwargs)
self.calendar_icon = CalendarIconDrawer(self.height(), self._padding)

def draw_icon(self, painter):
self.calendar_icon.paint(painter)


class RankButton(SelfPaintAbstractIconTextButton):
def __init__(self, text='排行榜', *args, **kwargs):
super().__init__(text, *args, **kwargs)
self.rank_icon = RankIconDrawer(self.height(), self._padding)

def draw_icon(self, painter):
self.rank_icon.paint(painter)


if __name__ == '__main__':
from feeluown.gui.debug import simple_layout

Expand All @@ -292,3 +313,5 @@ def draw_icon(self, painter):
layout.addWidget(DiscoveryButton(height=length))

layout.addWidget(TriagleButton(length=length, direction='up'))
layout.addWidget(CalendarButton(height=length))
layout.addWidget(RankButton(height=length))
29 changes: 28 additions & 1 deletion feeluown/library/provider_protocol.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from typing import runtime_checkable, Protocol, List, Tuple, Optional, Dict
from abc import abstractmethod

from feeluown.media import Quality, Media
from .models import (
BriefCommentModel, SongModel, VideoModel, AlbumModel, ArtistModel,
Expand Down Expand Up @@ -41,6 +40,10 @@

'SupportsVideoGet',
'SupportsVideoMultiQuality',

'SupportsRecListDailySongs',
'SupportsRecListDailyAlbums',
'SupportsRecListDailyPlaylists',
)


Expand Down Expand Up @@ -344,3 +347,27 @@ def get_current_user(self) -> UserModel:
:raises NoUserLoggedIn: there is no logged in user.
"""


#
# Protocols for recommendation.
#
@runtime_checkable
class SupportsRecListDailySongs(Protocol):
@abstractmethod
def rec_list_daily_songs(self) -> List[SongModel]:
pass


@runtime_checkable
class SupportsRecListDailyPlaylists(Protocol):
@abstractmethod
def rec_list_daily_playlists(self) -> List[PlaylistModel]:
pass


@runtime_checkable
class SupportsRecListDailyAlbums(Protocol):
@abstractmethod
def rec_list_daily_albums(self) -> List[AlbumModel]:
pass

0 comments on commit 893d790

Please sign in to comment.