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 SongsTableToolbar(QWidget): play_all_needed = pyqtSignal() filter_albums_needed = pyqtSignal([list]) filter_text_changed = pyqtSignal([str]) def __init__(self, parent=None): super().__init__(parent) self._tmp_buttons = [] self.play_all_btn = TextButton('播放全部', self) self.play_all_btn.clicked.connect(self.play_all_needed.emit) self.play_all_btn.setObjectName('play_all') # album filters self.filter_albums_combobox = QComboBox(self) self.filter_albums_combobox.addItems( ['所有专辑', '标准', '单曲与EP', '现场', '合辑']) self.filter_albums_combobox.currentIndexChanged.connect( self.on_albums_filter_changed) # 8 works on macOS, don't know if it works on various Linux DEs self.filter_albums_combobox.setMinimumContentsLength(8) self.filter_albums_combobox.hide() self._setup_ui() def albums_mode(self): self._before_change_mode() self.filter_albums_combobox.show() def songs_mode(self): self._before_change_mode() self.play_all_btn.show() def artists_mode(self): self._before_change_mode() def enter_state_playall_start(self): self.play_all_btn.setEnabled(False) # currently, this is called only when feeluown is fetching songs, # so when we enter state_playall_start, we set play all btn text # to this. self.play_all_btn.setText('获取所有歌曲...') def enter_state_playall_end(self): self.play_all_btn.setText('获取所有歌曲...done') self.play_all_btn.setEnabled(True) self.play_all_btn.setText('播放全部') def add_tmp_button(self, button): """Append text button""" if button not in self._tmp_buttons: # FIXME(cosven): the button inserted isn't aligned with other buttons self._layout.insertWidget(len(self._tmp_buttons) + 1, button) self._tmp_buttons.append(button) def _setup_ui(self): self._layout = QHBoxLayout(self) # left margin of meta widget is 30, we align with it # bottom margin of meta widget is 15, we should be larger than that self._layout.setContentsMargins(30, 15, 30, 10) self._layout.addWidget(self.play_all_btn) self._layout.addStretch(0) self._layout.addWidget(self.filter_albums_combobox) def _before_change_mode(self): """filter all filter buttons""" for button in self._tmp_buttons: self._layout.removeWidget(button) button.close() self._tmp_buttons.clear() self.filter_albums_combobox.hide() self.play_all_btn.hide() def on_albums_filter_changed(self, index): # ['所有', '专辑', '单曲与EP', '现场', '合辑'] if index == 0: types = [] elif index == 1: types = [AlbumType.standard] elif index == 2: types = [AlbumType.single, AlbumType.ep] elif index == 3: types = [AlbumType.live] else: types = [AlbumType.compilation, AlbumType.retrospective] self.filter_albums_needed.emit(types)