def __init__(self, app, parent=None): super().__init__(parent) self._app = app self._renderer = None self._table = None # current visible table self._tables = [] self.toolbar = SongsTableToolbar() self.tabbar = TableTabBarV2() self.meta_widget = TableMetaWidget(parent=self) self.songs_table = SongsTableView(parent=self) self.albums_table = AlbumListView(parent=self) self.desc_widget = DescLabel(parent=self) self._tables.append(self.songs_table) self._tables.append(self.albums_table) self.songs_table.play_song_needed.connect( lambda song: asyncio.ensure_future(self.play_song(song))) self.songs_table.show_artist_needed.connect( lambda artist: self._app.browser.goto(model=artist)) self.songs_table.show_album_needed.connect( lambda album: self._app.browser.goto(model=album)) self.toolbar.play_all_needed.connect(self.play_all) self.songs_table.add_to_playlist_needed.connect( self._add_songs_to_playlist) self._setup_ui()
def __init__(self, app, parent=None): super().__init__(parent) self._app = app self._renderer = None self._table = None # current visible table self._tables = [] self._extra = None self.toolbar = SongsTableToolbar() self.tabbar = TableTabBarV2() self.meta_widget = TableMetaWidget(parent=self) self.songs_table = SongsTableView(parent=self) self.albums_table = AlbumListView(parent=self) self.artists_table = ArtistListView(parent=self) self.videos_table = VideoListView(parent=self) self.playlists_table = PlaylistListView(parent=self) self.comments_table = CommentListView(parent=self) self.desc_widget = DescLabel(parent=self) self._tables.append(self.songs_table) self._tables.append(self.albums_table) self._tables.append(self.artists_table) self._tables.append(self.playlists_table) self._tables.append(self.videos_table) self._tables.append(self.comments_table) self.songs_table.play_song_needed.connect( lambda song: asyncio.ensure_future(self.play_song(song))) self.videos_table.play_video_needed.connect( lambda video: aio.create_task(self.play_video(video))) def goto_model(model): self._app.browser.goto(model=model) for signal in [ self.songs_table.show_artist_needed, self.songs_table.show_album_needed, self.albums_table.show_album_needed, self.artists_table.show_artist_needed, self.playlists_table.show_playlist_needed, ]: signal.connect(goto_model) self.toolbar.play_all_needed.connect(self.play_all) self.songs_table.add_to_playlist_needed.connect( self._add_songs_to_playlist) self.songs_table.about_to_show_menu.connect( self._songs_table_about_to_show_menu) self.songs_table.activated.connect(lambda index: aio.create_task( self._on_songs_table_activated(index))) self._setup_ui()
def test_table_meta(qtbot): widget = TableMetaWidget(SongsTableToolbar()) qtbot.addWidget(widget) widget.title = '我喜欢的音乐' widget.subtitle = '嘿嘿' widget.creator = 'cosven' widget.updated_at = time.time() widget.desc = "<pre><code>print('hello world')</code><pre>"
def __init__(self, parent=None): super().__init__(parent=parent) self.title_label = QLabel(self) self.cover_label = QLabel(self) self.meta_label = QLabel(self) self.desc_container = DescriptionContainer(parent=self) self.toolbar = SongsTableToolbar(parent=self) self.title_label.setTextFormat(Qt.RichText) self.meta_label.setTextFormat(Qt.RichText) self._is_fullwindow = False self._setup_ui() self._refresh() self.desc_container.space_pressed.connect(self.toggle_full_window)
def __init__(self, app, parent=None): super().__init__(parent) self._app = app self.toolbar = SongsTableToolbar() self.meta_widget = TableMetaWidget(self.toolbar, parent=self) self.songs_table = SongsTableView(parent=self) self.albums_table = AlbumListView(parent=self) self.songs_table.play_song_needed.connect( lambda song: asyncio.ensure_future(self.play_song(song))) self.songs_table.show_artist_needed.connect( lambda artist: self._app.browser.goto(model=artist)) self.songs_table.show_album_needed.connect( lambda album: self._app.browser.goto(model=album)) self.meta_widget.toolbar.play_all_needed.connect(self.play_all) self.meta_widget.toggle_full_window_needed.connect( self.toggle_meta_full_window) self.hide() self._setup_ui() self._delegate = None
class TableContainer(QFrame, BgTransparentMixin): def __init__(self, app, parent=None): super().__init__(parent) self._app = app self._renderer = None self._table = None # current visible table self._tables = [] self._extra = None self.toolbar = SongsTableToolbar() self.tabbar = TableTabBarV2() self.meta_widget = TableMetaWidget(parent=self) self.songs_table = SongsTableView(parent=self) self.albums_table = AlbumListView(parent=self) self.artists_table = ArtistListView(parent=self) self.videos_table = VideoListView(parent=self) self.playlists_table = PlaylistListView(parent=self) self.desc_widget = DescLabel(parent=self) self._tables.append(self.songs_table) self._tables.append(self.albums_table) self._tables.append(self.artists_table) self._tables.append(self.playlists_table) self._tables.append(self.videos_table) self.songs_table.play_song_needed.connect( lambda song: asyncio.ensure_future(self.play_song(song))) self.videos_table.play_video_needed.connect( lambda video: aio.create_task(self.play_video(video))) def goto_model(model): self._app.browser.goto(model=model) for signal in [ self.songs_table.show_artist_needed, self.songs_table.show_album_needed, self.albums_table.show_album_needed, self.artists_table.show_artist_needed, self.playlists_table.show_playlist_needed, ]: signal.connect(goto_model) self.toolbar.play_all_needed.connect(self.play_all) self.songs_table.add_to_playlist_needed.connect( self._add_songs_to_playlist) self._setup_ui() def _setup_ui(self): self.current_table = None self.tabbar.hide() self.meta_widget.add_tabbar(self.tabbar) self.desc_widget.hide() self._layout = QVBoxLayout(self) self._layout.addWidget(self.meta_widget) self._layout.addWidget(self.toolbar) self._layout.addSpacing(10) self._layout.addWidget(self.desc_widget) for table in self._tables: self._layout.addWidget(table) self._layout.addStretch(0) self._layout.setContentsMargins(0, 0, 0, 0) self._layout.setSpacing(0) @property def current_extra(self): return self._extra @current_extra.setter def current_extra(self, extra): """(alpha)""" if self._extra is not None: self._layout.removeWidget(self._extra) self._extra.deleteLater() del self._extra self._extra = extra if self._extra is not None: self._layout.insertWidget(1, self._extra) @property def current_table(self): """current visible table, if no table is visible, return None""" return self._table @current_table.setter def current_table(self, table): """set table as current visible table show table and hide other tables, if table is None, hide all tables. """ for t in self._tables: if t != table: t.hide() if table is None: self.toolbar.hide() else: self.desc_widget.hide() table.show() if table is self.artists_table: self.toolbar.artists_mode() if table is self.albums_table: self.toolbar.albums_mode() if table is self.songs_table: self.toolbar.songs_mode() self._table = table async def set_renderer(self, renderer): """set ui renderer TODO: add lock for set_renderer """ if renderer is None: return # firstly, tear down everything # tear down last renderer if self._renderer is not None: await self._renderer.tearDown() self.meta_widget.hide() self.meta_widget.clear() self.tabbar.hide() self.tabbar.check_default() self.current_table = None self.current_extra = None # clean right_panel background image self._app.ui.right_panel.show_background_image(None) # disconnect songs_table signal signals = ( self.tabbar.show_contributed_albums_needed, self.tabbar.show_albums_needed, self.tabbar.show_songs_needed, self.tabbar.show_artists_needed, self.tabbar.show_playlists_needed, self.tabbar.show_desc_needed, ) for signal in signals: disconnect_slots_if_has(signal) # unbind some callback function self.songs_table.remove_song_func = None # secondly, prepare environment self.show() # thirdly, setup new renderer await renderer.setUp(self) self._renderer = renderer await self._renderer.render() async def play_song(self, song): self._app.player.play_song(song) async def play_video(self, video): media = await aio.run_in_executor(None, lambda: video.media) self._app.player.play(media) def play_all(self): task_name = 'play-all' task_spec = self._app.task_mgr.get_or_create(task_name) def reader_readall_cb(task): with suppress(ProviderIOError, asyncio.CancelledError): songs = task.result() self._app.player.play_songs(songs=songs) self.toolbar.enter_state_playall_end() model = self.songs_table.model() # FIXME: think about a more elegant way reader = model.sourceModel().songs_g if reader is not None: if reader.count is not None: task = task_spec.bind_blocking_io(reader.readall) self.toolbar.enter_state_playall_start() task.add_done_callback(reader_readall_cb) return songs = model.sourceModel().songs self._app.player.play_songs(songs=songs) async def show_model(self, model): model_type = ModelType(model.meta.model_type) if model_type == ModelType.album: renderer = AlbumRenderer(model) elif model_type == ModelType.artist: renderer = ArtistRenderer(model) elif model_type == ModelType.playlist: renderer = PlaylistRenderer(model) else: renderer = None await self.set_renderer(renderer) def show_collection(self, coll): renderer = SongsCollectionRenderer(coll) aio.create_task(self.set_renderer(renderer)) def show_songs(self, songs=None, songs_g=None): """(DEPRECATED) provided only for backward compatibility""" renderer = Renderer() task = aio.create_task(self.set_renderer(renderer)) task.add_done_callback( lambda _: renderer.show_songs(songs=songs, songs_g=songs_g)) def show_albums_coll(self, albums_g): aio.create_task(self.set_renderer(AlbumsCollectionRenderer(albums_g))) def show_artists_coll(self, artists_g): aio.create_task(self.set_renderer( ArtistsCollectionRenderer(artists_g))) def show_player_playlist(self): aio.create_task(self.set_renderer(PlayerPlaylistRenderer())) def search(self, text): if self.isVisible() and self.songs_table is not None: self.songs_table.filter_row(text) def _add_songs_to_playlist(self, songs): for song in songs: self._app.playlist.add(song)
class TableContainer(QFrame, BgTransparentMixin): def __init__(self, app, parent=None): super().__init__(parent) self._app = app self._renderer = None self._table = None # current visible table self._tables = [] self._extra = None self.toolbar = SongsTableToolbar() self.tabbar = TableTabBarV2() self.meta_widget = TableMetaWidget(parent=self) self.songs_table = SongsTableView(parent=self) self.albums_table = AlbumListView(parent=self) self.artists_table = ArtistListView(parent=self) self.videos_table = VideoListView(parent=self) self.playlists_table = PlaylistListView(parent=self) self.comments_table = CommentListView(parent=self) self.desc_widget = DescLabel(parent=self) self._tables.append(self.songs_table) self._tables.append(self.albums_table) self._tables.append(self.artists_table) self._tables.append(self.playlists_table) self._tables.append(self.videos_table) self._tables.append(self.comments_table) self.songs_table.play_song_needed.connect( lambda song: asyncio.ensure_future(self.play_song(song))) self.videos_table.play_video_needed.connect( lambda video: aio.create_task(self.play_video(video))) def goto_model(model): self._app.browser.goto(model=model) for signal in [ self.songs_table.show_artist_needed, self.songs_table.show_album_needed, self.albums_table.show_album_needed, self.artists_table.show_artist_needed, self.playlists_table.show_playlist_needed, ]: signal.connect(goto_model) self.toolbar.play_all_needed.connect(self.play_all) self.songs_table.add_to_playlist_needed.connect( self._add_songs_to_playlist) self.songs_table.about_to_show_menu.connect( self._songs_table_about_to_show_menu) self.songs_table.activated.connect(lambda index: aio.create_task( self._on_songs_table_activated(index))) self._setup_ui() def _setup_ui(self): self.current_table = None self.tabbar.hide() self.meta_widget.add_tabbar(self.tabbar) self.desc_widget.hide() self._layout = QVBoxLayout(self) self._layout.addWidget(self.meta_widget) self._layout.addWidget(self.toolbar) self._layout.addSpacing(10) self._layout.addWidget(self.desc_widget) for table in self._tables: self._layout.addWidget(table) self._layout.addStretch(0) self._layout.setContentsMargins(0, 0, 0, 0) self._layout.setSpacing(0) @property def current_extra(self): return self._extra @current_extra.setter def current_extra(self, extra): """(alpha)""" if self._extra is not None: self._layout.removeWidget(self._extra) self._extra.deleteLater() del self._extra self._extra = extra if self._extra is not None: self._layout.insertWidget(1, self._extra) @property def current_table(self): """current visible table, if no table is visible, return None""" return self._table @current_table.setter def current_table(self, table): """set table as current visible table show table and hide other tables, if table is None, hide all tables. """ for t in self._tables: if t != table: t.hide() if table is None: self.toolbar.hide() else: self.desc_widget.hide() table.show() if table is self.artists_table: self.toolbar.artists_mode() if table is self.albums_table: self.toolbar.albums_mode() if table is self.songs_table: self.toolbar.songs_mode() self._table = table async def set_renderer(self, renderer): """set ui renderer TODO: add lock for set_renderer """ if renderer is None: return # firstly, tear down everything # tear down last renderer if self._renderer is not None: await self._renderer.tearDown() self.meta_widget.hide() self.meta_widget.clear() self.tabbar.hide() self.tabbar.check_default() self.current_table = None self.current_extra = None # clean right_panel background image self._app.ui.right_panel.show_background_image(None) # disconnect songs_table signal signals = ( self.tabbar.show_contributed_albums_needed, self.tabbar.show_albums_needed, self.tabbar.show_songs_needed, self.tabbar.show_artists_needed, self.tabbar.show_playlists_needed, self.tabbar.show_desc_needed, ) for signal in signals: disconnect_slots_if_has(signal) # unbind some callback function self.songs_table.remove_song_func = None # secondly, prepare environment self.show() # thirdly, setup new renderer await renderer.setUp(self) self._renderer = renderer await self._renderer.render() async def play_song(self, song): self._app.player.play_song(song) async def play_video(self, video): media = await aio.run_in_executor(None, lambda: video.media) self._app.player.play(media) def play_all(self): task_name = 'play-all' task_spec = self._app.task_mgr.get_or_create(task_name) def reader_readall_cb(task): with suppress(ProviderIOError, asyncio.CancelledError): songs = task.result() self._app.player.play_songs(songs=songs) self.toolbar.enter_state_playall_end() model = self.songs_table.model() # FIXME: think about a more elegant way reader = model.sourceModel()._reader if reader is not None: if reader.count is not None: task = task_spec.bind_blocking_io(reader.readall) self.toolbar.enter_state_playall_start() task.add_done_callback(reader_readall_cb) return songs = model.sourceModel().songs self._app.player.play_songs(songs=songs) async def show_model(self, model): model = self._app.library.cast_model_to_v1(model) model_type = ModelType(model.meta.model_type) if model_type == ModelType.album: renderer = AlbumRenderer(model) elif model_type == ModelType.artist: renderer = ArtistRenderer(model) elif model_type == ModelType.playlist: renderer = PlaylistRenderer(model) else: renderer = None await self.set_renderer(renderer) def show_collection(self, coll): renderer = SongsCollectionRenderer(coll) aio.create_task(self.set_renderer(renderer)) def show_songs(self, songs=None, songs_g=None): """(DEPRECATED) provided only for backward compatibility""" warnings.warn('use readerer.show_songs please') renderer = Renderer() task = aio.create_task(self.set_renderer(renderer)) if songs is not None: reader = wrap(songs) else: reader = songs_g task.add_done_callback(lambda _: renderer.show_songs(reader=reader)) def show_albums_coll(self, albums_g): aio.create_task(self.set_renderer(AlbumsCollectionRenderer(albums_g))) def show_artists_coll(self, artists_g): aio.create_task(self.set_renderer( ArtistsCollectionRenderer(artists_g))) def show_player_playlist(self): aio.create_task(self.set_renderer(PlayerPlaylistRenderer())) def search(self, text): if self.isVisible() and self.songs_table is not None: self.songs_table.filter_row(text) def _add_songs_to_playlist(self, songs): for song in songs: self._app.playlist.add(song) def _songs_table_about_to_show_menu(self, ctx): add_action = ctx['add_action'] models = ctx['models'] if not models or models[0].meta.model_type != ModelType.song: return song = models[0] goto = self._app.browser.goto if self._app.library.check_flags_by_model(song, ProviderFlags.similar): add_action('相似歌曲', lambda *args: goto(model=song, path='/similar')) if self._app.library.check_flags_by_model(song, ProviderFlags.hot_comments): add_action('歌曲评论', lambda *args: goto(model=song, path='/hot_comments')) async def _on_songs_table_activated(self, index): """ QTableView should have no IO operations. """ from feeluown.widgets.songs import Column song = index.data(Qt.UserRole) if index.column() == Column.song: self.songs_table.play_song_needed.emit(song) else: try: song = await aio.run_in_executor( None, self._app.library.song_upgrade, song) except NotSupported: assert ModelFlags.v2 & song.meta.flags self._app.show_msg('资源提供放不支持该功能') logger.info( f'provider:{song.source} does not support song_get') song.state = ModelState.cant_upgrade except (ProviderIOError, RequestException) as e: # FIXME: we should only catch ProviderIOError here, # but currently, some plugins such fuo-qqmusic may raise # requests.RequestException logger.exception('upgrade song failed') self._app.show_msg(f'请求失败: {str(e)}') else: if index.column() == Column.artist: artists = song.artists if artists: if len(artists) > 1: self.songs_table.show_artists_by_index(index) else: self.songs_table.show_artist_needed.emit( artists[0]) elif index.column() == Column.album: self.songs_table.show_album_needed.emit(song.album) model = self.songs_table.model() topleft = model.index(index.row(), 0) bottomright = model.index(index.row(), 4) model.dataChanged.emit(topleft, bottomright, [])
class TableMetaWidget(QWidget): toggle_full_window_needed = pyqtSignal([bool]) class getset_property: def __init__(self, name, set_cb): self.name = name self.name_real = '_' + name self.set_cb = set_cb def __get__(self, instance, owner): if hasattr(instance, self.name_real): return getattr(instance, self.name_real) return None def __set__(self, instance, value): setattr(instance, self.name_real, value) if self.set_cb is not None: self.set_cb(instance) def __init__(self, parent=None): super().__init__(parent=parent) self.title_label = QLabel(self) self.cover_label = QLabel(self) self.meta_label = QLabel(self) self.desc_container = DescriptionContainer(parent=self) self.toolbar = SongsTableToolbar(parent=self) self.title_label.setTextFormat(Qt.RichText) self.meta_label.setTextFormat(Qt.RichText) self._is_fullwindow = False self._setup_ui() self._refresh() self.desc_container.space_pressed.connect(self.toggle_full_window) def _setup_ui(self): self.cover_label.setMinimumWidth(200) self.setMaximumHeight(180) self.title_label.setAlignment(Qt.AlignTop) self.meta_label.setAlignment(Qt.AlignTop) self._v_layout = QVBoxLayout(self) self._h_layout = QHBoxLayout() self._right_layout = QVBoxLayout() self._right_layout.addWidget(self.title_label) self._right_layout.addWidget(self.meta_label) self._right_layout.addWidget(self.desc_container) self._right_layout.addWidget(self.toolbar) self._right_layout.setAlignment(self.toolbar, Qt.AlignBottom) self._h_layout.addWidget(self.cover_label) self._h_layout.setAlignment(self.cover_label, Qt.AlignTop) self._h_layout.addLayout(self._right_layout) self._v_layout.addLayout(self._h_layout) self._h_layout.setContentsMargins(10, 10, 10, 10) self._h_layout.setSpacing(20) self._right_layout.setContentsMargins(0, 0, 0, 0) self._right_layout.setSpacing(5) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().setSpacing(0) def _refresh(self): self._refresh_title() self._refresh_meta_label() self._refresh_desc() self._refresh_cover() def _refresh_title(self): if self.title: self.title_label.show() self.title_label.setText('<h2>{}</h2>'.format(self.title)) else: self.title_label.hide() def _refresh_meta_label(self): creator = self.creator creator_part = '👤 <a href="fuo://local/users/{}">{}</a>'\ .format(creator, creator) if creator else '' if self.updated_at: updated_at = datetime.fromtimestamp(self.updated_at) updated_part = '🕒 更新于 <code style="font-size: small">{}</code>'\ .format(updated_at.strftime('%Y-%m-%d')) else: updated_part = '' if self.created_at: created_at = datetime.fromtimestamp(self.created_at) created_part = '🕛 创建于 <code style="font-size: small">{}</code>'\ .format(created_at.strftime('%Y-%m-%d')) else: created_part = '' if creator_part or updated_part or created_part: parts = [creator_part, created_part, updated_part] valid_parts = [p for p in parts if p] content = ' | '.join(valid_parts) text = '<span style="color: grey">{}</span>'.format(content) # TODO: add linkActivated callback for meta_label self.meta_label.setText(text) self.meta_label.show() else: self.meta_label.hide() def _refresh_desc(self): if self.desc: self.desc_container.show() self.desc_container.label.setText(self.desc) else: self.desc_container.hide() def _refresh_cover(self): if not self.cover: self.cover_label.hide() def _refresh_toolbar(self): if self.is_artist: self.toolbar.artist_mode() else: self.toolbar.songs_mode() def clear(self): self.title = None self.subtitle = None self.desc = None self.cover = None self.created_at = None self.updated_at = None self.creator = None self.is_artist = False def set_cover_pixmap(self, pixmap): self.cover_label.show() self.cover_label.setPixmap( pixmap.scaledToWidth(self.cover_label.width(), mode=Qt.SmoothTransformation)) title = getset_property('title', _refresh_title) subtitle = getset_property('subtitle', _refresh_title) desc = getset_property('desc', _refresh_desc) cover = getset_property('cover', _refresh_cover) created_at = getset_property('created_at', _refresh_meta_label) updated_at = getset_property('updated_at', _refresh_meta_label) creator = getset_property('creator', _refresh_meta_label) is_artist = getset_property('is_artist', _refresh_toolbar) def toggle_full_window(self): if self._is_fullwindow: self.toggle_full_window_needed.emit(False) self.setMaximumHeight(180) else: # generally, display height will be less than 4000px self.toggle_full_window_needed.emit(True) self.setMaximumHeight(4000) self._is_fullwindow = not self._is_fullwindow