Skip to content

Commit

Permalink
gui: add page home (#813)
Browse files Browse the repository at this point in the history
1. Renderring daily recommended playlists/songs from all providers in homepage.
2. fix #821

Add this option to enable new homepage
```python
config.ENABLE_NEW_HOMEPAGE = True
```
  • Loading branch information
cosven committed Apr 14, 2024
1 parent b19fcbd commit ae5910c
Show file tree
Hide file tree
Showing 13 changed files with 243 additions and 26 deletions.
1 change: 1 addition & 0 deletions feeluown/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def create_config() -> Config:
config.deffield('ENABLE_WEB_SERVER', default=False, type_=bool)
config.deffield('MODE', default=0x0000, desc='CLI or GUI 模式')
config.deffield('THEME', default='auto', desc='auto/light/dark')
config.deffield('ENABLE_NEW_HOMEPAGE', default=False, type_=bool)
config.deffield('MPV_AUDIO_DEVICE', default='auto', desc='MPV 播放设备')
config.deffield('COLLECTIONS_DIR', desc='本地收藏所在目录')
config.deffield('FORCE_MAC_HOTKEY', desc='强制开启 macOS 全局快捷键功能',
Expand Down
13 changes: 4 additions & 9 deletions feeluown/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import logging
import os
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Dict, Iterable, List

Expand All @@ -12,7 +11,7 @@
from feeluown.consts import COLLECTIONS_DIR
from feeluown.utils.dispatch import Signal
from feeluown.library import resolve, reverse, ResolverNotFound, \
ResolveFailed, ModelState
ResolveFailed, ModelState, CollectionType
from feeluown.utils.utils import elfhash

logger = logging.getLogger(__name__)
Expand All @@ -33,14 +32,10 @@ class CollectionAlreadyExists(Exception):
pass


class CollectionType(Enum):
sys_library = 16
sys_pool = 13

mixed = 8


class Collection:
"""
TODO: This collection should be moved into local provider.
"""

def __init__(self, fpath):
# TODO: 以后考虑添加 identifier 字段,identifier
Expand Down
2 changes: 2 additions & 0 deletions feeluown/gui/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ def initialize(self):
from feeluown.gui.pages.recommendation_daily_songs import \
render as render_rec_daily_songs
from feeluown.gui.pages.my_fav import render as render_my_fav
from feeluown.gui.pages.homepage import render as render_homepage

model_prefix = f'{MODEL_PAGE_PREFIX}<provider>'

Expand All @@ -214,6 +215,7 @@ async def dummy_render(req, *args, **kwargs):
('/recently_played', render_recently_played),
('/search', render_search),
('/rec', render_rec),
('/homepage', render_homepage),
('/rec/daily_songs', render_rec_daily_songs),
('/my_fav', render_my_fav),
]
Expand Down
5 changes: 3 additions & 2 deletions feeluown/gui/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,9 @@ def adjust_height(self):
# qt triggers fetchMore when user scrolls down to bottom.
index = self._last_visible_index()
rect = self.visualRect(index)
height = self.sizeHint().height() - int(rect.height() * 1.5) - \
self._reserved
height = self.sizeHint().height()
if self._no_scroll_v is False:
height = height - int(rect.height() * 1.5) - self._reserved
self.setFixedHeight(max(height, self.min_height()))
else:
self.setFixedHeight(self._row_height * self._fixed_row_count)
Expand Down
143 changes: 143 additions & 0 deletions feeluown/gui/pages/homepage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import logging
from typing import TYPE_CHECKING

from PyQt5.QtWidgets import QWidget, QVBoxLayout

from feeluown.library import SupportsRecListDailyPlaylists, SupportsRecACollectionOfSongs
from feeluown.utils.reader import create_reader
from feeluown.utils.aio import run_fn, as_completed
from feeluown.gui.widgets.header import LargeHeader
from feeluown.gui.widgets.img_card_list import (
PlaylistCardListView,
PlaylistCardListModel,
PlaylistFilterProxyModel,
PlaylistCardListDelegate,
)
from feeluown.gui.widgets.song_minicard_list import (
SongMiniCardListView,
SongMiniCardListDelegate,
SongMiniCardListModel,
)
from feeluown.gui.page_containers.scroll_area import ScrollArea
from feeluown.gui.helpers import fetch_cover_wrapper, BgTransparentMixin

if TYPE_CHECKING:
from feeluown.app.gui_app import GuiApp

logger = logging.getLogger(__name__)


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

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


class View(QWidget, BgTransparentMixin):

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

self.header_playlist_list = LargeHeader('一些歌单')
self.header_songs_list = LargeHeader('随便听听')
self.playlist_list_view = PlaylistCardListView(no_scroll_v=True)
self.playlist_list_view.setItemDelegate(
PlaylistCardListDelegate(
self.playlist_list_view,
card_min_width=150,
)
)
self.songs_list_view = SongMiniCardListView(no_scroll_v=True)
self.songs_list_view.setItemDelegate(
SongMiniCardListDelegate(self.songs_list_view, )
)

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

self.playlist_list_view.show_playlist_needed.connect(
lambda model: self._app.browser.goto(model=model)
)
self.songs_list_view.play_song_needed.connect(self._app.playlist.play_model)

def _setup_ui(self):
self._layout.setContentsMargins(20, 10, 20, 0)
self._layout.setSpacing(0)
self._layout.addWidget(self.header_playlist_list)
self._layout.addSpacing(10)
self._layout.addWidget(self.playlist_list_view)
self._layout.addSpacing(10)
self._layout.addWidget(self.header_songs_list)
self._layout.addSpacing(10)
self._layout.addWidget(self.songs_list_view)
self._layout.addStretch(0)

async def _get_daily_playlists(self):
providers = self._app.library.list()
playlists_list = []
for coro in as_completed([
run_fn(provider.rec_list_daily_playlists) for provider in providers
if isinstance(provider, SupportsRecListDailyPlaylists)
]):
try:
playlists_ = await coro
except: # noqa
logger.exception('get recommended daily playlists failed')
else:
playlists_list.append(playlists_)

playlists = []
finished = [False] * len(playlists_list)
while True:
for i, playlists_ in enumerate(playlists_list):
try:
playlist = playlists_.pop(0)
except IndexError:
finished[i] = True
else:
playlists.append(playlist)
if all(finished):
break
return playlists

async def _get_rec_songs(self):
providers = self._app.library.list()
titles = []
songs = []
for coro in as_completed([
run_fn(provider.rec_a_collection) for provider in providers
if isinstance(provider, SupportsRecACollectionOfSongs)
]):
try:
title, songs_ = await coro
except: # noqa
logger.exception('get rec songs failed')
else:
songs.extend(songs_)
titles.append(title)
return titles, songs

async def render(self):
playlists = await self._get_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)

titles, songs = await self._get_rec_songs()
songs_model = SongMiniCardListModel(
create_reader(songs),
fetch_image=fetch_cover_wrapper(self._app),
)
self.songs_list_view.setModel(songs_model)
if titles:
self.header_songs_list.setText(titles[0])
24 changes: 17 additions & 7 deletions feeluown/gui/uimain/page_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from PyQt5.QtCore import Qt, QRect, QSize, QEasingCurve, QEvent
from PyQt5.QtGui import QPainter, QBrush, QColor, QLinearGradient, QPalette
from PyQt5.QtWidgets import QFrame, QVBoxLayout, QStackedLayout
from PyQt5.QtWidgets import QAbstractScrollArea, QFrame, QVBoxLayout, QStackedLayout

from feeluown.utils import aio
from feeluown.library import ModelType
Expand Down Expand Up @@ -55,11 +55,7 @@ def height_for_itemview(self):
height -= w.height()
return height

def wheelEvent(self, e):
super().wheelEvent(e)
self._app.ui.bottom_panel.update()

def eventFilter(self, obj, event):
def eventFilter(self, _, event):
if event.type() == QEvent.Resize:
self.maybe_resize_itemview()
return False
Expand Down Expand Up @@ -122,9 +118,19 @@ def set_body(self, widget):
if w not in (self.scrollarea, ):
self._stacked_layout.removeWidget(w)

widget.installEventFilter(self)
if isinstance(widget, QAbstractScrollArea):
widget.verticalScrollBar().installEventFilter(self)

self._stacked_layout.addWidget(widget)
self._stacked_layout.setCurrentWidget(widget)

def eventFilter(self, _, event):
# Refresh when the body is scrolled.
if event.type() == QEvent.Wheel:
self.update()
return False

def show_collection(self, coll, model_type):

def _show_pure_albums_coll(coll):
Expand Down Expand Up @@ -176,7 +182,11 @@ def paintEvent(self, e):
if isinstance(current_widget, VFillableBg):
draw_height += current_widget.fillable_bg_height()

scrolled = self.scrollarea.verticalScrollBar().value()
widget = self._stacked_layout.currentWidget()
if isinstance(widget, QAbstractScrollArea):
scrolled = widget.verticalScrollBar().value()
else:
scrolled = 0
max_scroll_height = draw_height - self.bottom_panel.height()

# Draw the whole background with QPalette.Base color.
Expand Down
7 changes: 6 additions & 1 deletion feeluown/gui/uimain/sidebar.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,12 @@ def __init__(self, app: 'GuiApp', parent=None):
self.fav_btn.setDisabled(True)
self.discovery_btn.setToolTip('当前资源提供方未知')

self.home_btn.clicked.connect(self.show_library)
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))
Expand Down
6 changes: 3 additions & 3 deletions feeluown/gui/widgets/header.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@


class BaseHeader(QLabel):
def __init__(self, font_size, *args, **kwargs):
def __init__(self, *args, font_size=13, **kwargs):
super().__init__(*args, **kwargs)

font = self.font()
Expand All @@ -13,7 +13,7 @@ def __init__(self, font_size, *args, **kwargs):

class LargeHeader(BaseHeader):
def __init__(self, *args, **kwargs):
super().__init__(20, *args, **kwargs)
super().__init__(*args, font_size=20, **kwargs)

font = self.font()
font.setWeight(QFont.DemiBold)
Expand All @@ -22,4 +22,4 @@ def __init__(self, *args, **kwargs):

class MidHeader(BaseHeader):
def __init__(self, *args, **kwargs):
super().__init__(16, *args, **kwargs)
super().__init__(*args, font_size=16, **kwargs)
1 change: 1 addition & 0 deletions feeluown/library/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@
Resolver, reverse, resolve, ResolverNotFound, ResolveFailed,
parse_line, NS_TYPE_MAP,
)
from .collection import Collection, CollectionType
38 changes: 38 additions & 0 deletions feeluown/library/collection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from enum import Enum
from dataclasses import dataclass
from typing import List

from .models import BaseModel


class CollectionType(Enum):
"""Collection type enumeration"""

# These two values are only used in local collection.
sys_library = 16
sys_pool = 13

only_songs = 1
only_artists = 2
only_albums = 3
only_playlists = 4
only_lyrics = 5
only_videos = 6

only_users = 17
only_comments = 18

mixed = 32


@dataclass
class Collection:
"""
Differences between a collection and a playlist
- A collection has no identifier in general.
- A collection may have songs, albums and artists.
"""
name: str
type_: CollectionType
models: List[BaseModel]
description: str = ''
13 changes: 13 additions & 0 deletions feeluown/library/provider_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
PlaylistModel, UserModel, ModelType, BriefArtistModel, BriefSongModel,
LyricModel, BriefVideoModel,
)
from .collection import Collection

from .flags import Flags as PF

Expand Down Expand Up @@ -384,6 +385,18 @@ def rec_list_daily_songs(self) -> List[SongModel]:
pass


@runtime_checkable
class SupportsRecACollectionOfSongs(Protocol):
@abstractmethod
def rec_a_collection_of_songs(self) -> Collection:
"""
For example, providers may provider a list of songs,
and the title looks like “大家都在听” / “红心歌曲”.
For different user, this API may return different result.
"""


@runtime_checkable
class SupportsRecListDailyPlaylists(Protocol):
@abstractmethod
Expand Down
10 changes: 6 additions & 4 deletions feeluown/player/lyric.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,12 @@ def parse_lyric_text(content: str) -> Dict[int, str]:
"""
Reference: https://github.com/osdlyrics/osdlyrics/blob/master/python/lrc.py
>>> parse_lyric_text("[00:00.00] 作曲 : 周杰伦\\n[00:01.00] 作词 : 周杰伦\\n")
OrderedDict([(0, ' 作曲 : 周杰伦'), (1000, ' 作词 : 周杰伦')])
>>> parse_lyric_text("[01:30][00:01:10][01:00]再等直至再吻到你")
OrderedDict([(60000, '再等直至再吻到你'), (70000, '再等直至再吻到你'), (90000, '再等直至再吻到你')])
>>> r = parse_lyric_text("[00:00.00] 作曲 : 周杰伦\\n[00:01.00] 作词 : 周杰伦\\n")
>>> list(r.items())[0]
(0, ' 作曲 : 周杰伦')
>>> r = parse_lyric_text("[01:30][00:01:10][01:00]再等直至再吻到你")
>>> list(r.items())[-1]
(90000, '再等直至再吻到你')
"""
def to_mileseconds(time_str):
mileseconds = 0
Expand Down
Loading

0 comments on commit ae5910c

Please sign in to comment.