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

gui: add recommendation page #742

Merged
merged 3 commits into from
Jan 3, 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
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
Loading