class TableContainer(QFrame): 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 def _setup_ui(self): self.setAutoFillBackground(False) self._layout = QVBoxLayout(self) self._layout.addWidget(self.meta_widget) self._layout.addWidget(self.songs_table) self._layout.addWidget(self.albums_table) self._layout.setContentsMargins(0, 0, 0, 0) self._layout.setSpacing(0) async def set_delegate(self, delegate): """set ui delegate TODO: add lock for set_delegate """ if delegate is None: return # firstly, tear down everything # tear down last delegate if self._delegate is not None: await self._delegate.tearDown() self.meta_widget.clear() self.songs_table.hide() self.albums_table.hide() # disconnect songs_table signal signals = ( self.songs_table.song_deleted, self.meta_widget.toolbar.show_albums_needed, self.meta_widget.toolbar.show_songs_needed, self.albums_table.show_album_needed, ) for signal in signals: try: signal.disconnect() except TypeError: pass # secondly, prepare environment self.show() # thirdly, setup new delegate await delegate.setUp(self) self._delegate = delegate await self._delegate.render() async def play_song(self, song): self._app.player.play_song(song) def play_all(self): task_name = 'play-all' task_spec = self._app.task_mgr.get_or_create(task_name) def songs_g_readall_cb(task): try: songs = task.result() except asyncio.CancelledError: pass except ProviderIOError as e: self._app.show_msg('[play-all] read songs failed: {}'.format( str(e))) else: self._app.player.play_songs(songs=songs) finally: self.meta_widget.toolbar.enter_state_playall_end() model = self.songs_table.model() songs_g = model.songs_g if songs_g is not None and songs_g.allow_random_read: task = task_spec.bind_blocking_io(songs_g.readall) self.meta_widget.toolbar.enter_state_playall_start() task.add_done_callback(songs_g_readall_cb) return songs = model.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: delegate = AlbumDelegate(model) elif model_type == ModelType.artist: delegate = ArtistDelegate(model) elif model_type == ModelType.playlist: delegate = PlaylistDelegate(model) else: delegate = None await self.set_delegate(delegate) def show_collection(self, coll): delegate = CollectionDelegate(coll) aio.create_task(self.set_delegate(delegate)) def show_songs(self, songs=None, songs_g=None): """(DEPRECATED) provided only for backward compatibility""" delegate = Delegate() task = aio.create_task(self.set_delegate(delegate)) task.add_done_callback( lambda _: delegate.show_songs(songs=songs, songs_g=songs_g)) def show_albums(self, albums_g): delegate = Delegate() task = aio.create_task(self.set_delegate(delegate)) task.add_done_callback(lambda _: delegate.show_albums(albums_g)) def show_player_playlist(self): aio.create_task(self.set_delegate(PlayerPlaylistDelegate())) def search(self, text): if self.isVisible() and self.songs_table is not None: self.songs_table.filter_row(text) def toggle_meta_full_window(self, fullwindow_needed): if fullwindow_needed: self.songs_table.hide() else: self.songs_table.show()
class SongsTableContainer(QFrame): def __init__(self, app, parent=None): super().__init__(parent) self._app = app self.songs_table = SongsTableView(self) self._toolbar = TableToolbar(self) self._cover_label = QLabel(self) self._desc_container_folded = True self._desc_container = DescriptionContainer(self) self._top_container = QWidget(self) self._cover_container = QWidget(self._top_container) 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._desc_container.space_pressed.connect( self.toggle_desc_container_fold) self._toolbar.toggle_desc_needed.connect( self.toggle_desc_container_fold) self._toolbar.play_all_needed.connect(self.play_all) self.hide() self._setup_ui() def _setup_ui(self): self._left_sub_layout = QVBoxLayout(self._cover_container) self._top_layout = QHBoxLayout(self._top_container) self._layout = QVBoxLayout(self) self._cover_label.setMinimumWidth(200) self._right_sub_layout = QVBoxLayout() self._right_sub_layout.addWidget(self._desc_container) self._right_sub_layout.addWidget(self._toolbar) self._left_sub_layout.addWidget(self._cover_label) self._left_sub_layout.addStretch(0) # 根据 Qt 文档中所说,在大部分平台中,ContentMargin 为 11 self._left_sub_layout.setContentsMargins(0, 0, 11, 0) self._left_sub_layout.setSpacing(0) self._top_layout.addWidget(self._cover_container) self._top_layout.addLayout(self._right_sub_layout) self._top_layout.setStretch(1, 1) self.setAutoFillBackground(False) self._layout.setContentsMargins(0, 0, 0, 0) self._layout.setSpacing(0) self._layout.addWidget(self._top_container) self._layout.addWidget(self.songs_table) self._top_container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) # FIXME: 更好的计算宽度和高度 # 目前是假设知道自己初始化高度大约是 530px # 之后可以考虑按比例来计算 self.overview_height = 180 self._top_container.setMaximumHeight(self.overview_height) self._songs_table_height = 530 - self.overview_height self.songs_table.setMinimumHeight(self._songs_table_height) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) async def play_song(self, song): await async_run(lambda: song.url) self._app.player.play_song(song) def play_all(self): songs = self.songs_table.model().songs self._app.player.playlist.clear() for song in songs: self._app.player.playlist.add(song) self._app.player.play_next() async def show_model(self, model): model_type = ModelType(model._meta.model_type) if model_type == ModelType.album: func = self.show_album elif model_type == ModelType.artist: func = self.show_artist elif model_type == ModelType.playlist: func = self.show_playlist else: def func(model): pass # seems silly await func(model) def show_player_playlist(self, songs): self.show_songs(songs) self.songs_table.song_deleted.connect( lambda song: self._app.playlist.remove(song)) def set_desc(self, desc): self._desc_container.show() self._desc_container.set_html(desc) async def show_playlist(self, playlist): self._top_container.show() loop = asyncio.get_event_loop() if playlist.meta.allow_create_songs_g: songs_g = playlist.create_songs_g() self._show_songs(songs_g=songs_g) else: songs = await async_run(lambda: playlist.songs, loop=loop) self._show_songs(songs) desc = '<h2>{}</h2>\n{}'.format(playlist.name, playlist.desc or '') self.set_desc(desc) if playlist.cover: loop.create_task(self.show_cover(playlist.cover)) def remove_song(song): model = self.songs_table.model() row = model.songs.index(song) msg = 'remove {} from {}'.format(song, playlist) with self._app.create_action(msg) as action: rv = playlist.remove(song.identifier) if rv: model.removeRow(row) else: action.failed() self.songs_table.song_deleted.connect(lambda song: remove_song(song)) async def show_artist(self, artist): self._top_container.show() loop = asyncio.get_event_loop() songs = songs_g = None if artist.meta.allow_create_songs_g: songs_g = artist.create_songs_g() else: songs = await async_run(lambda: artist.songs) if songs_g is not None: self._show_songs(songs_g=songs_g) else: self._show_songs(songs=songs) desc = await async_run(lambda: artist.desc) self.set_desc(desc or '<h2>{}</h2>'.format(artist.name)) if artist.cover: loop.create_task(self.show_cover(artist.cover)) async def show_album(self, album): self._top_container.show() loop = asyncio.get_event_loop() songs = await async_run(lambda: album.songs) self._show_songs(songs) desc = await async_run(lambda: album.desc) self.set_desc(desc or '<h2>{}</h2>'.format(album.name)) if album.cover: loop.create_task(self.show_cover(album.cover)) def show_collection(self, collection): self._top_container.hide() self.show_songs(collection.models) self.songs_table.song_deleted.connect(collection.remove) async def show_url(self, url): model = self._app.protocol.get_model(url) if model.meta.model_type == ModelType.song: self._app.player.play_song(model) else: # TODO: add artist/album/user support self._app.show_msg('暂时只支持歌曲,不支持其它歌曲资源') async def show_cover(self, cover): # FIXME: cover_hash may not work properly someday cover_uid = cover.split('/', -1)[-1] content = await self._app.img_mgr.get(cover, cover_uid) img = QImage() img.loadFromData(content) pixmap = QPixmap(img) if not pixmap.isNull(): self.set_cover(pixmap) self.update() def _show_songs(self, songs=None, songs_g=None): try: self.songs_table.song_deleted.disconnect() except TypeError: # no connections at all pass self.show() self.songs_table.show() songs = songs or [] logger.debug('Show songs in table, total: %d', len(songs)) source_name_map = { p.identifier: p.name for p in self._app.library.list() } if songs_g is not None: # 优先使用生成器 self.songs_table.setModel( SongsTableModel(source_name_map=source_name_map, songs_g=songs_g, parent=self.songs_table)) else: self.songs_table.setModel( SongsTableModel(songs=songs, source_name_map=source_name_map, parent=self.songs_table)) self.songs_table.scrollToTop() def show_songs(self, songs): self._show_songs(songs) self._top_container.show() self.hide_desc() self.hide_cover() def set_cover(self, pixmap): self._cover_container.show() self._cover_label.setPixmap( pixmap.scaledToWidth(self._cover_label.width(), mode=Qt.SmoothTransformation)) def toggle_desc_container_fold(self): # TODO: add toggle animation? if self._desc_container_folded: self._top_container.setMaximumHeight(4000) self.songs_table.hide() self._desc_container_folded = False else: self._top_container.setMaximumHeight(self.overview_height) self.songs_table.show() self._desc_container_folded = True def search(self, text): if self.isVisible() and self.songs_table is not None: self.songs_table.filter_row(text) def hide_cover(self): self._cover_container.hide() def hide_desc(self): self._desc_container.hide()
class SongsTableContainer(QFrame): def __init__(self, app, parent=None): super().__init__(parent) self._app = app self.meta_widget = TableMetaWidget(parent=self) self.songs_table = SongsTableView(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() def _setup_ui(self): self.setAutoFillBackground(False) self._layout = QVBoxLayout(self) self._layout.addWidget(self.meta_widget) self._layout.addWidget(self.songs_table) self._layout.setContentsMargins(0, 0, 0, 0) self._layout.setSpacing(0) async def play_song(self, song): await async_run(lambda: song.url) self._app.player.play_song(song) def play_all(self): songs = self.songs_table.model().songs self._app.player.playlist.clear() for song in songs: self._app.player.playlist.add(song) self._app.player.play_next() async def show_model(self, model): model_type = ModelType(model._meta.model_type) if model_type == ModelType.album: func = self.show_album elif model_type == ModelType.artist: func = self.show_artist elif model_type == ModelType.playlist: func = self.show_playlist else: def func(model): pass # seems silly await func(model) def show_player_playlist(self, songs): self.show_songs(songs) self.songs_table.song_deleted.connect( lambda song: self._app.playlist.remove(song)) async def show_playlist(self, playlist): loop = asyncio.get_event_loop() if playlist.meta.allow_create_songs_g: songs_g = playlist.create_songs_g() self._show_songs(songs_g=songs_g) else: songs = await async_run(lambda: playlist.songs, loop=loop) self._show_songs(songs) self.meta_widget.clear() self.meta_widget.title = playlist.name desc = await async_run(lambda: playlist.desc) self.meta_widget.desc = desc if playlist.cover: loop.create_task(self.show_cover(playlist.cover)) def remove_song(song): model = self.songs_table.model() row = model.songs.index(song) msg = 'remove {} from {}'.format(song, playlist) with self._app.create_action(msg) as action: rv = playlist.remove(song.identifier) if rv: model.removeRow(row) else: action.failed() self.songs_table.song_deleted.connect(lambda song: remove_song(song)) async def show_artist(self, artist): loop = asyncio.get_event_loop() songs = songs_g = None if artist.meta.allow_create_songs_g: songs_g = artist.create_songs_g() else: songs = await async_run(lambda: artist.songs) if songs_g is not None: self._show_songs(songs_g=songs_g) else: self._show_songs(songs=songs) desc = await async_run(lambda: artist.desc) self.meta_widget.clear() self.meta_widget.title = artist.name self.meta_widget.desc = desc if artist.cover: loop.create_task(self.show_cover(artist.cover)) async def show_album(self, album): loop = asyncio.get_event_loop() songs = await async_run(lambda: album.songs) self.meta_widget.clear() self._show_songs(songs) desc = await async_run(lambda: album.desc) self.meta_widget.title = album.name self.meta_widget.desc = desc if album.cover: loop.create_task(self.show_cover(album.cover)) def show_collection(self, collection): self.meta_widget.clear() self.meta_widget.title = collection.name self.meta_widget.updated_at = collection.updated_at self.meta_widget.created_at = collection.created_at self._show_songs(collection.models) self.songs_table.song_deleted.connect(collection.remove) async def show_url(self, url): model = self._app.protocol.get_model(url) if model.meta.model_type == ModelType.song: self._app.player.play_song(model) else: # TODO: add artist/album/user support self._app.show_msg('暂时只支持歌曲,不支持其它歌曲资源') async def show_cover(self, cover): # FIXME: cover_hash may not work properly someday cover_uid = cover.split('/', -1)[-1] content = await self._app.img_mgr.get(cover, cover_uid) img = QImage() img.loadFromData(content) pixmap = QPixmap(img) if not pixmap.isNull(): self.set_cover(pixmap) self.update() def _show_songs(self, songs=None, songs_g=None): try: self.songs_table.song_deleted.disconnect() except TypeError: # no connections at all pass self.show() self.songs_table.show() songs = songs or [] logger.debug('Show songs in table, total: %d', len(songs)) source_name_map = { p.identifier: p.name for p in self._app.library.list() } if songs_g is not None: # 优先使用生成器 self.songs_table.setModel( SongsTableModel(source_name_map=source_name_map, songs_g=songs_g, parent=self.songs_table)) else: self.songs_table.setModel( SongsTableModel(songs=songs, source_name_map=source_name_map, parent=self.songs_table)) self.songs_table.scrollToTop() def show_songs(self, songs=None, songs_g=None): self.meta_widget.clear() self._show_songs(songs=songs, songs_g=songs_g) def search(self, text): if self.isVisible() and self.songs_table is not None: self.songs_table.filter_row(text) def set_cover(self, pixmap): self.meta_widget.set_cover_pixmap(pixmap) def toggle_meta_full_window(self, fullwindow_needed): if fullwindow_needed: self.songs_table.hide() else: self.songs_table.show()