diff --git a/feeluown/collection.py b/feeluown/collection.py index 76a5a8117a..2c48fac4fa 100644 --- a/feeluown/collection.py +++ b/feeluown/collection.py @@ -18,8 +18,10 @@ logger = logging.getLogger(__name__) COLL_LIBRARY_IDENTIFIER = 'library' +COLL_POOL_IDENTIFIER = 'pool' # for backward compat, we should never change these filenames LIBRARY_FILENAME = f'{COLL_LIBRARY_IDENTIFIER}.fuo' +POOL_FILENAME = f'{COLL_POOL_IDENTIFIER}.fuo' DEPRECATED_FUO_FILENAMES = ( 'Songs.fuo', 'Albums.fuo', 'Artists.fuo', 'Videos.fuo' ) @@ -33,6 +35,7 @@ class CollectionAlreadyExists(Exception): class CollectionType(Enum): sys_library = 16 + sys_pool = 13 mixed = 8 @@ -70,6 +73,8 @@ def load(self): self.name = name if name == COLL_LIBRARY_IDENTIFIER: self.type = CollectionType.sys_library + elif name == COLL_POOL_IDENTIFIER: + self.type = CollectionType.sys_pool else: self.type = CollectionType.mixed @@ -127,6 +132,11 @@ def create_empty(cls, fpath, title=''): f.write(tomlkit.dumps(doc)) f.write(TOML_DELIMLF) + coll = cls(fpath) + coll._loads_metadata(doc) + coll.type = CollectionType.mixed + return coll + def add(self, model): """add model to collection @@ -213,10 +223,13 @@ def __init__(self, app): self._library = app.library self.default_dir = COLLECTIONS_DIR - self._id_coll_mapping: Dict[str, Collection] = {} + self._id_coll_mapping: Dict[int, Collection] = {} + self._sys_colls = {} def get(self, identifier): - return self._id_coll_mapping.get(int(identifier), None) + if identifier in (CollectionType.sys_pool, CollectionType.sys_library): + return self._sys_colls[identifier] + return self._id_coll_mapping[int(identifier)] def get_coll_library(self): for coll in self._id_coll_mapping.values(): @@ -259,12 +272,24 @@ def _get_dirs(self, ): def scan(self): colls: List[Collection] = [] - library_coll = None for coll in self._scan(): if coll.type == CollectionType.sys_library: - library_coll = coll - continue - colls.append(coll) + self._sys_colls[CollectionType.sys_library] = coll + elif coll.type == CollectionType.sys_pool: + self._sys_colls[CollectionType.sys_pool] = coll + else: + colls.append(coll) + + if CollectionType.sys_pool not in self._sys_colls: + pool_fpath = os.path.join(self.default_dir, POOL_FILENAME) + assert not os.path.exists(pool_fpath) + logger.info('Generating collection pool.') + coll = Collection.create_empty(pool_fpath, '想听') + self._sys_colls[CollectionType.sys_pool] = coll + + pool_coll = self._sys_colls[CollectionType.sys_pool] + library_coll = self._sys_colls[CollectionType.sys_library] + colls.insert(0, pool_coll) colls.insert(0, library_coll) for collection in colls: coll_id = collection.identifier diff --git a/feeluown/gui/components/collections.py b/feeluown/gui/components/collections.py index 614a07c052..f323b61b99 100644 --- a/feeluown/gui/components/collections.py +++ b/feeluown/gui/components/collections.py @@ -100,7 +100,7 @@ def data(self, index, role=Qt.DisplayRole): item = self._items[row] if role == Qt.DisplayRole: icon = '◎ ' - if item.type == CollectionType.sys_library: + if item.type in (CollectionType.sys_library, CollectionType.sys_pool): icon = '◉ ' return icon + item.name if role == Qt.ToolTipRole: diff --git a/feeluown/gui/uimain/sidebar.py b/feeluown/gui/uimain/sidebar.py index c556062475..2ff41d94fe 100644 --- a/feeluown/gui/uimain/sidebar.py +++ b/feeluown/gui/uimain/sidebar.py @@ -5,9 +5,9 @@ from PyQt5.QtWidgets import QFrame, QLabel, QVBoxLayout, QSizePolicy, QScrollArea, \ QHBoxLayout, QFormLayout, QDialog, QLineEdit, QDialogButtonBox, QMessageBox -from feeluown.collection import CollectionAlreadyExists +from feeluown.collection import CollectionAlreadyExists, CollectionType from feeluown.gui.widgets import ( - RecentlyPlayedButton, HomeButton, PlusButton, TriagleButton, + DiscoveryButton, HomeButton, PlusButton, TriagleButton, ) from feeluown.gui.widgets.playlists import PlaylistsView from feeluown.gui.components import CollectionListView @@ -94,8 +94,8 @@ def __init__(self, app: 'GuiApp', parent=None): self._app = app self.home_btn = HomeButton(height=30, parent=self) - self.recently_played_btn = RecentlyPlayedButton(height=30, parent=self) - self.collections_header = QLabel('本地收藏', self) + self.discovery_btn = DiscoveryButton(height=30, padding=0.2, parent=self) + self.collections_header = QLabel('本地收藏集', self) self.collections_header.setToolTip( '我们可以在本地建立『收藏集』来收藏自己喜欢的音乐资源\n\n' '每个收藏集都以一个独立 .fuo 文件的存在,' @@ -129,9 +129,9 @@ def __init__(self, app: 'GuiApp', parent=None): self._layout.addLayout(self._top_layout) self._layout.addLayout(self._sub_layout) - self._top_layout.addWidget(self.home_btn) - self._top_layout.addWidget(self.recently_played_btn) self._top_layout.setContentsMargins(15, 16, 16, 0) + self._top_layout.addWidget(self.home_btn) + self._top_layout.addWidget(self.discovery_btn) self._sub_layout.setContentsMargins(16, 8, 16, 0) self._sub_layout.addWidget(self.collections_con) self._sub_layout.addWidget(self.my_music_con) @@ -148,8 +148,7 @@ def __init__(self, app: 'GuiApp', parent=None): self.my_music_con.hide() self.home_btn.clicked.connect(self.show_library) - self.recently_played_btn.clicked.connect( - lambda: self._app.browser.goto(page='/recently_played')) + self.discovery_btn.clicked.connect(self.show_pool) self.playlists_view.show_playlist.connect( lambda pl: self._app.browser.goto(model=pl)) self.collections_view.show_collection.connect( @@ -189,6 +188,10 @@ def show_library(self): coll_library = self._app.coll_mgr.get_coll_library() self._app.browser.goto(page=f'/colls/{coll_library.identifier}') + def show_pool(self): + coll = self._app.coll_mgr.get(CollectionType.sys_pool) + self._app.browser.goto(page=f'/colls/{coll.identifier}') + def remove_coll(self, coll): def do(): self._app.coll_mgr.remove(coll) diff --git a/feeluown/gui/widgets/__init__.py b/feeluown/gui/widgets/__init__.py index 9579b9b97b..437a349604 100644 --- a/feeluown/gui/widgets/__init__.py +++ b/feeluown/gui/widgets/__init__.py @@ -3,5 +3,5 @@ from .selfpaint_btn import ( # noqa SelfPaintAbstractSquareButton, RecentlyPlayedButton, HomeButton, LeftArrowButton, RightArrowButton, SearchButton, SettingsButton, - PlusButton, TriagleButton + PlusButton, TriagleButton, DiscoveryButton, ) diff --git a/feeluown/gui/widgets/selfpaint_btn.py b/feeluown/gui/widgets/selfpaint_btn.py index 9f4919ef39..3720c1def7 100644 --- a/feeluown/gui/widgets/selfpaint_btn.py +++ b/feeluown/gui/widgets/selfpaint_btn.py @@ -1,6 +1,6 @@ -from PyQt5.QtCore import QPoint, Qt, QRect, QRectF +from PyQt5.QtCore import QPoint, Qt, QRect, QRectF, QTimer, QPointF from PyQt5.QtWidgets import QPushButton, QStyle, QStyleOptionButton -from PyQt5.QtGui import QPainter, QPalette +from PyQt5.QtGui import QPainter, QPalette, QPainterPath from feeluown.gui.drawers import HomeIconDrawer, PlusIconDrawer, TriangleIconDrawer from feeluown.gui.helpers import darker_or_lighter @@ -32,10 +32,10 @@ def paint_border_bg_when_hover(self, painter, radius=3): class SelfPaintAbstractIconTextButton(SelfPaintAbstractButton): - def __init__(self, text, height=30, padding=0.25, parent=None): + def __init__(self, text='', height=30, padding=0.25, parent=None): super().__init__(parent=parent) - self._padding = int(height * padding) if padding < 1 else padding + self._padding: int = int(height * padding if padding < 1 else padding) self._text_width = self.fontMetrics().horizontalAdvance(text) self._text = text @@ -200,8 +200,8 @@ def paintEvent(self, _): class RecentlyPlayedButton(SelfPaintAbstractIconTextButton): - def __init__(self, *args, **kwargs): - super().__init__('最近播放', *args, **kwargs) + def __init__(self, text='最近播放', **kwargs): + super().__init__(text, **kwargs) def draw_icon(self, painter): pen_width = 1.5 @@ -225,6 +225,47 @@ def draw_icon(self, painter): painter.drawPoint(QPoint(self._padding, center)) +class DiscoveryButton(SelfPaintAbstractIconTextButton): + def __init__(self, text='发现', **kwargs): + super().__init__(text=text, **kwargs) + + self._timer = QTimer(self) + + length = self.height() + self._half = length // 2 + self._v1 = self._half - self._padding + self._v2 = self._v1 / 2.5 + + self._triagle = QPainterPath(QPointF(-self._v2, 0)) + self._triagle.lineTo(QPointF(0, self._v1)) + self._triagle.lineTo(QPointF(0, self._v2)) + self._rotate = 0 + self._rotate_mod = 360 + + self._timer.timeout.connect(self.on_timeout) + self._timer.start(30) + + def on_timeout(self): + self._rotate = (self._rotate + 2) % self._rotate_mod + self.update() + + def draw_icon(self, painter: QPainter): + opt = QStyleOptionButton() + self.initStyleOption(opt) + + pen = painter.pen() + pen.setWidthF(1.5) + painter.setPen(pen) + + painter.save() + painter.translate(self._half, self._half) + painter.rotate(self._rotate) + for ratio in range(4): + painter.rotate(90*ratio) + painter.drawPath(self._triagle) + painter.restore() + + class HomeButton(SelfPaintAbstractIconTextButton): def __init__(self, *args, **kwargs): super().__init__('主页', *args, **kwargs) @@ -248,5 +289,6 @@ def draw_icon(self, painter): layout.addWidget(SettingsButton(length=length)) layout.addWidget(RecentlyPlayedButton(height=length)) layout.addWidget(HomeButton(height=length)) + layout.addWidget(DiscoveryButton(height=length)) layout.addWidget(TriagleButton(length=length, direction='up')) diff --git a/tests/test_collection.py b/tests/test_collection.py index fb48439752..4e0cb5d6d3 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -1,5 +1,6 @@ from feeluown.models.uri import ResolveFailed, ResolverNotFound, reverse -from feeluown.collection import Collection, CollectionManager, LIBRARY_FILENAME +from feeluown.collection import Collection, CollectionManager, LIBRARY_FILENAME, \ + POOL_FILENAME def test_collection_load(tmp_path, song, mocker): @@ -160,9 +161,10 @@ def new_collection(path): coll1 = new_collection(tmp_path / '1.fuo') coll2 = new_collection(tmp_path / '2.fuo') coll_library = new_collection(tmp_path / LIBRARY_FILENAME) + coll_pool = new_collection(tmp_path / POOL_FILENAME) coll_mgr = CollectionManager(app_mock) mocker.patch.object(CollectionManager, '_scan', - return_value=[coll1, coll_library, coll2]) + return_value=[coll1, coll_library, coll_pool, coll2]) coll_mgr.scan() - assert list(coll_mgr.listall()) == [coll_library, coll1, coll2] + assert list(coll_mgr.listall()) == [coll_library, coll_pool, coll1, coll2]