class PlayerControlPanel(QFrame): def __init__(self, app, parent=None): super().__init__(parent) self._app = app class IconButton(QPushButton): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._playback_modes = list(PlaybackMode.__members__.values()) self._pm_alias_map = { PlaybackMode.one_loop: '单曲循环', PlaybackMode.sequential: '顺序播放', PlaybackMode.loop: '循环播放', PlaybackMode.random: '随机播放', } # initialize sub widgets self._layout = QHBoxLayout(self) self.previous_btn = IconButton(self) self.pp_btn = IconButton(self) self.next_btn = IconButton(self) #: playback mode switch button self.pms_btn = TextButton(self) self.volume_btn = VolumeButton(self) self.playlist_btn = IconButton(parent=self) #: mark song as favorite button self.like_btn = QPushButton(self) self.mv_btn = TextButton('MV', self) self.lyric_btn = TextButton('词', self) self.download_btn = QPushButton(self) self.toggle_video_btn = TextButton('△', self) # toggle picture-in-picture button self.toggle_pip_btn = TextButton('◲', self) self.lyric_window = LyricWindow() self.lyric_window.hide() self.previous_btn.setObjectName('previous_btn') self.pp_btn.setObjectName('pp_btn') self.next_btn.setObjectName('next_btn') self.playlist_btn.setObjectName('playlist_btn') self.volume_btn.setObjectName('volume_btn') self.pms_btn.setObjectName('pms_btn') self.download_btn.setObjectName('download_btn') self.like_btn.setObjectName('like_btn') self.mv_btn.setObjectName('mv_btn') self.lyric_btn.setObjectName('lyric_btn') self.toggle_video_btn.setObjectName('toggle_video_btn') self.toggle_pip_btn.setObjectName('toggle_pip_btn') self.progress_slider = ProgressSlider(app=app, parent=self) self.pms_btn.setToolTip('修改播放模式') self.volume_btn.setToolTip('调整音量') self.playlist_btn.setToolTip('显示当前播放列表') self.mv_btn.setToolTip('播放 MV') self.download_btn.setToolTip('下载歌曲(未实现,欢迎 PR)') self.like_btn.setToolTip('收藏歌曲(未实现,欢迎 PR)') self.pp_btn.setCheckable(True) self.like_btn.setCheckable(True) self.download_btn.setCheckable(True) self.toggle_video_btn.hide() self.toggle_pip_btn.hide() self.song_title_label = SongBriefLabel(self._app) self.song_source_label = QLabel('歌曲来源', parent=self) self.song_title_label.setAlignment(Qt.AlignCenter) self.duration_label = DurationLabel(app, parent=self) self.position_label = ProgressLabel(app, parent=self) self.song_source_label.setObjectName('song_source_label') self.next_btn.clicked.connect(self._app.playlist.next) self.previous_btn.clicked.connect(self._app.playlist.previous) self.pp_btn.clicked.connect(self._app.player.toggle) self.pms_btn.clicked.connect(self._switch_playback_mode) self.lyric_btn.clicked.connect(self._toggle_lyric_window) self.volume_btn.change_volume_needed.connect( lambda volume: setattr(self._app.player, 'volume', volume)) player = self._app.player player.state_changed.connect(self._on_player_state_changed, aioqueue=True) player.playlist.playback_mode_changed.connect( self.on_playback_mode_changed, aioqueue=True) player.playlist.song_changed.connect(self.on_player_song_changed, aioqueue=True) player.media_changed.connect(self.on_player_media_changed, aioqueue=True) player.volume_changed.connect(self.volume_btn.on_volume_changed) self._app.live_lyric.sentence_changed.connect( self.lyric_window.set_sentence) self.lyric_window.play_previous_needed.connect( player.playlist.previous) self.lyric_window.play_next_needed.connect(player.playlist.next) self._update_pms_btn_text() self._setup_ui() def _setup_ui(self): # set widget layout self.song_source_label.setFixedHeight(20) self.progress_slider.setFixedHeight(20) # half of parent height self.position_label.setFixedWidth(45) self.duration_label.setFixedWidth(45) # on macOS, we should set AlignVCenter flag self.position_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) self.like_btn.setFixedSize(15, 15) self.download_btn.setFixedSize(15, 15) self.mv_btn.setFixedHeight(16) self.lyric_btn.setFixedHeight(16) self.progress_slider.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self._sub_layout = QVBoxLayout() self._sub_top_layout = QHBoxLayout() # add space to make top layout align with progress slider self._sub_top_layout.addSpacing(3) self._sub_top_layout.addWidget(self.song_source_label) self._sub_top_layout.addSpacing(5) self._sub_top_layout.addWidget(self.song_title_label) self._sub_top_layout.addSpacing(5) self._sub_top_layout.addStretch(0) self._sub_top_layout.addWidget(self.like_btn) self._sub_top_layout.addSpacing(8) self._sub_top_layout.addWidget(self.mv_btn) self._sub_top_layout.addSpacing(8) self._sub_top_layout.addWidget(self.lyric_btn) self._sub_top_layout.addSpacing(8) self._sub_top_layout.addWidget(self.download_btn) self._sub_top_layout.addSpacing(3) self._sub_layout.addSpacing(3) self._sub_layout.addLayout(self._sub_top_layout) self._sub_layout.addWidget(self.progress_slider) self._layout.addSpacing(20) self._layout.addWidget(self.previous_btn) self._layout.addSpacing(8) self._layout.addWidget(self.pp_btn) self._layout.addSpacing(8) self._layout.addWidget(self.next_btn) self._layout.addSpacing(26) self._layout.addWidget(self.volume_btn) # 18 = 200(left_panel_width) - 4 * 30(btn) - 20 - 8 - 8 -26 self._layout.addSpacing(18) self._layout.addStretch(0) self._layout.addWidget(self.position_label) self._layout.addSpacing(7) self._layout.addLayout(self._sub_layout) self._layout.setStretchFactor(self._sub_layout, 1) self._layout.addSpacing(7) self._layout.addWidget(self.duration_label) self._layout.addStretch(0) self._layout.addSpacing(18) self._layout.addWidget(self.pms_btn) self._layout.addSpacing(8) self._layout.addWidget(self.playlist_btn) self._layout.addSpacing(8) self._layout.addWidget(self.toggle_video_btn) self._layout.addSpacing(8) self._layout.addWidget(self.toggle_pip_btn) self._layout.addSpacing(18) self._layout.setSpacing(0) self._layout.setContentsMargins(0, 0, 0, 0) def _switch_playback_mode(self): playlist = self._app.player.playlist pm_total = len(self._playback_modes) pm_idx = self._playback_modes.index(playlist.playback_mode) if pm_idx < pm_total - 1: pm_idx += 1 else: pm_idx = 0 playlist.playback_mode = self._playback_modes[pm_idx] def on_playback_mode_changed(self, playback_mode): self._update_pms_btn_text() def _update_pms_btn_text(self): playback_mode = self._app.player.playlist.playback_mode alias = self._pm_alias_map[playback_mode] self.pms_btn.setText(alias) def on_player_song_changed(self, song): if song is None: self.song_source_label.setText('歌曲来源') self.song_title_label.setText('No song is playing.') return source_name_map = { p.identifier: p.name for p in self._app.library.list() } font_metrics = QFontMetrics(QApplication.font()) text = '{} - {}'.format(song.title_display, song.artists_name_display) # width -> three button + source label + text <= progress slider # three button: 63, source label: 150 elided_text = font_metrics.elidedText( text, Qt.ElideRight, self.progress_slider.width() - 200) self.song_source_label.setText(source_name_map[song.source]) self.song_title_label.setText(elided_text) loop = asyncio.get_event_loop() loop.create_task(self.update_mv_btn_status(song)) def on_player_media_changed(self, media): if media is not None and media.type_ == MediaType.audio: metadata = media.metadata if metadata.bitrate: text = self.song_source_label.text() bitrate_text = str(metadata.bitrate) + 'kbps' self.song_source_label.setText('{} - {}'.format( text, bitrate_text)) async def update_mv_btn_status(self, song): try: mv = await async_run(lambda: song.mv) except ProviderIOError: logger.exception('fetch song mv info failed') self.mv_btn.setEnabled(False) else: if mv: self.mv_btn.setToolTip(mv.name) self.mv_btn.setEnabled(True) else: self.mv_btn.setEnabled(False) def _on_player_state_changed(self, state): self.pp_btn.setChecked(state == State.playing) def _toggle_lyric_window(self): if self.lyric_window.isVisible(): self.lyric_window.hide() else: self.lyric_window.show()
class PlayerControlPanel(QFrame): def __init__(self, app, parent=None): super().__init__(parent) self._app = app class IconButton(QPushButton): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._playback_modes = list(PlaybackMode.__members__.values()) self._pm_alias_map = { PlaybackMode.one_loop: '单曲循环', PlaybackMode.sequential: '顺序播放', PlaybackMode.loop: '循环播放', PlaybackMode.random: '随机播放', } self.lyric_window = LyricWindow() self.lyric_window.hide() # initialize sub widgets self._layout = QHBoxLayout(self) self.previous_btn = IconButton(self) self.pp_btn = IconButton(self) self.next_btn = IconButton(self) #: playback mode switch button self.pms_btn = TextButton(self) self.volume_btn = VolumeButton(self) self.playlist_btn = PlaylistButton(self._app, self) #: mark song as favorite button self.like_btn = LikeButton(self._app, self) self.mv_btn = TextButton('MV', self) self.toggle_lyric_btn = LyricButton(self._app, self) self.download_btn = QPushButton(self) self.toggle_watch_btn = WatchButton(self._app, self) self.toggle_video_btn = TextButton('△', self) # toggle picture-in-picture button self.toggle_pip_btn = TextButton('◲', self) self.previous_btn.setObjectName('previous_btn') self.pp_btn.setObjectName('pp_btn') self.next_btn.setObjectName('next_btn') self.playlist_btn.setObjectName('playlist_btn') self.volume_btn.setObjectName('volume_btn') self.pms_btn.setObjectName('pms_btn') self.download_btn.setObjectName('download_btn') self.like_btn.setObjectName('like_btn') self.mv_btn.setObjectName('mv_btn') self.toggle_lyric_btn.setObjectName('toggle_lyric_btn') self.toggle_video_btn.setObjectName('toggle_video_btn') self.toggle_pip_btn.setObjectName('toggle_pip_btn') self.progress_slider = ProgressSlider(app=app, parent=self) self.pms_btn.setToolTip('修改播放模式') self.volume_btn.setToolTip('调整音量') self.playlist_btn.setToolTip('显示当前播放列表') self.mv_btn.setToolTip('播放 MV') self.download_btn.setToolTip('下载歌曲(未实现,欢迎 PR)') self.pp_btn.setCheckable(True) self.download_btn.setCheckable(True) self.toggle_video_btn.hide() self.toggle_pip_btn.hide() self.song_title_label = SongBriefLabel(self._app) self.song_title_label.setAlignment(Qt.AlignCenter) self.song_source_label = SourceLabel(self._app, parent=self) self.duration_label = DurationLabel(app, parent=self) self.position_label = ProgressLabel(app, parent=self) # we should enable focus since we want to have shortcut keys self.setFocusPolicy(Qt.StrongFocus) self.song_source_label.setObjectName('song_source_label') self.next_btn.clicked.connect(self._app.playlist.next) self.previous_btn.clicked.connect(self._app.playlist.previous) self.pp_btn.clicked.connect(self._app.player.toggle) self.pms_btn.clicked.connect(self._switch_playback_mode) self.volume_btn.change_volume_needed.connect( lambda volume: setattr(self._app.player, 'volume', volume)) player = self._app.player playlist = self._app.playlist playlist.playback_mode_changed.connect(self.on_playback_mode_changed, aioqueue=True) playlist.song_changed.connect(self.on_player_song_changed, aioqueue=True) player.state_changed.connect(self._on_player_state_changed, aioqueue=True) player.metadata_changed.connect(self.on_metadata_changed, aioqueue=True) player.volume_changed.connect(self.volume_btn.on_volume_changed) self._app.live_lyric.sentence_changed.connect( self.lyric_window.set_sentence) self.lyric_window.play_previous_needed.connect(playlist.previous) self.lyric_window.play_next_needed.connect(playlist.next) self._update_pms_btn_text() self._setup_ui() def _setup_ui(self): # set widget layout self.song_source_label.setFixedHeight(20) self.progress_slider.setFixedHeight(20) # half of parent height self.position_label.setFixedWidth(50) self.duration_label.setFixedWidth(50) # on macOS, we should set AlignVCenter flag self.position_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) self.like_btn.setFixedSize(15, 15) self.download_btn.setFixedSize(15, 15) self.download_btn.hide() self.mv_btn.setFixedHeight(16) self.toggle_lyric_btn.setFixedHeight(16) self.toggle_watch_btn.setFixedHeight(16) self.progress_slider.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self._sub_layout = QVBoxLayout() self._sub_top_layout = QHBoxLayout() # add space to make top layout align with progress slider self._sub_top_layout.addSpacing(3) self._sub_top_layout.addWidget(self.song_source_label) self._sub_top_layout.addSpacing(5) self._sub_top_layout.addWidget(self.song_title_label) self._sub_top_layout.addSpacing(5) self._sub_top_layout.addWidget(self.like_btn) self._sub_top_layout.addSpacing(8) self._sub_top_layout.addWidget(self.mv_btn) self._sub_top_layout.addSpacing(8) self._sub_top_layout.addWidget(self.toggle_lyric_btn) self._sub_top_layout.addSpacing(8) self._sub_top_layout.addWidget(self.toggle_watch_btn) # self._sub_top_layout.addSpacing(8) # self._sub_top_layout.addWidget(self.download_btn) self._sub_top_layout.addSpacing(3) self._sub_layout.addSpacing(3) self._sub_layout.addLayout(self._sub_top_layout) self._sub_layout.addWidget(self.progress_slider) self._layout.addSpacing(20) self._layout.addWidget(self.previous_btn) self._layout.addSpacing(8) self._layout.addWidget(self.pp_btn) self._layout.addSpacing(8) self._layout.addWidget(self.next_btn) self._layout.addSpacing(26) self._layout.addWidget(self.volume_btn) # 18 = 200(left_panel_width) - 4 * 30(btn) - 20 - 8 - 8 -26 self._layout.addSpacing(18) self._layout.addStretch(0) self._layout.addWidget(self.position_label) self._layout.addSpacing(7) self._layout.addLayout(self._sub_layout) self._layout.setStretchFactor(self._sub_layout, 1) self._layout.addSpacing(7) self._layout.addWidget(self.duration_label) self._layout.addStretch(0) self._layout.addSpacing(18) self._layout.addWidget(self.pms_btn) self._layout.addSpacing(8) self._layout.addWidget(self.playlist_btn) self._layout.addSpacing(8) self._layout.addWidget(self.toggle_video_btn) self._layout.addSpacing(8) self._layout.addWidget(self.toggle_pip_btn) self._layout.addSpacing(18) self._layout.setSpacing(0) self._layout.setContentsMargins(0, 0, 0, 0) def _switch_playback_mode(self): playlist = self._app.playlist pm_total = len(self._playback_modes) pm_idx = self._playback_modes.index(playlist.playback_mode) if pm_idx < pm_total - 1: pm_idx += 1 else: pm_idx = 0 playlist.playback_mode = self._playback_modes[pm_idx] def on_playback_mode_changed(self, _): self._update_pms_btn_text() def _update_pms_btn_text(self): playback_mode = self._app.playlist.playback_mode alias = self._pm_alias_map[playback_mode] self.pms_btn.setText(alias) def on_player_song_changed(self, song): task_spec = self._app.task_mgr.get_or_create('update-mv-btn-status') task_spec.bind_coro(self.update_mv_btn_status(song)) def on_metadata_changed(self, metadata): if not metadata: self.song_source_label.setText('歌曲来源') self.song_title_label.setText('') return # Set main text. text = metadata.get('title', '') if text: artists = metadata.get('artists', []) if artists: # FIXME: use _get_artists_name text += f" - {','.join(artists)}" self.song_title_label.setText(text) # Set source name. source = metadata.get('source', '') default = '未知来源' if source: source_name_map = { p.identifier: p.name for p in self._app.library.list() } name = source_name_map.get(source, default) self.song_source_label.setText(name) else: self.song_source_label.setText(default) # Set audio bitrate info if available. media = self._app.player.current_media if media is not None and media.type_ == MediaType.audio: props = media.props if props.bitrate: text = self.song_source_label.text() self.song_source_label.setText(f'{text} - {props.bitrate}kbps') async def update_mv_btn_status(self, song): if song is None: self.mv_btn.setEnabled(False) return try: mv = await aio.run_fn(self._app.library.song_get_mv, song) except ProviderIOError: logger.exception('fetch song mv info failed') self.mv_btn.setEnabled(False) else: if mv is None: self.mv_btn.setEnabled(False) else: self.mv_btn.setToolTip(mv.title) self.mv_btn.setEnabled(True) def _on_player_state_changed(self, state): self.pp_btn.setChecked(state == State.playing)