Add shuffle and repeat controls with end-of-playlist fix
This commit is contained in:
parent
aaaf354c9d
commit
8861e55f32
71
src/app.py
71
src/app.py
@ -6,6 +6,7 @@ Provides a GUI for converting, managing and transferring music to iPod Nano devi
|
||||
|
||||
import os
|
||||
import sys
|
||||
import random
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
@ -54,6 +55,9 @@ class MainWindow(QMainWindow):
|
||||
self._saved_playing: bool = False
|
||||
self._pending_seek_ms: int = 0
|
||||
|
||||
self._shuffle_active: bool = False
|
||||
self._repeat_mode: str = "no_repeat"
|
||||
|
||||
self._setup_player()
|
||||
self._setup_ui()
|
||||
self.library_tab.set_playlist_manager(self.playlist_manager)
|
||||
@ -88,6 +92,8 @@ class MainWindow(QMainWindow):
|
||||
self.player_header.prev_requested.connect(self._on_prev)
|
||||
self.player_header.play_pause_requested.connect(self._on_play_pause)
|
||||
self.player_header.next_requested.connect(self._on_next)
|
||||
self.player_header.shuffle_requested.connect(self._on_shuffle_toggle)
|
||||
self.player_header.repeat_requested.connect(self._on_repeat_cycle)
|
||||
self.player_header.position_changed_by_user.connect(self._on_user_seek_frac)
|
||||
self.player_header.volume_changed.connect(self._on_volume_changed)
|
||||
main_layout.addWidget(self.player_header)
|
||||
@ -239,6 +245,11 @@ class MainWindow(QMainWindow):
|
||||
self._saved_track_path = config.get("Playback", "last_track_path", fallback="")
|
||||
self._saved_position_ms = config.get_int("Playback", "last_position_ms", fallback=0)
|
||||
self._saved_playing = config.get_boolean("Playback", "last_playing", fallback=False)
|
||||
self._shuffle_active = config.get_boolean("Playback", "shuffle_active", fallback=False)
|
||||
self._repeat_mode = config.get("Playback", "repeat_mode", fallback="no_repeat")
|
||||
|
||||
self.player_header.set_shuffle_active(self._shuffle_active)
|
||||
self.player_header.set_repeat_mode(self._repeat_mode)
|
||||
|
||||
self.audio_output.setVolume(self._saved_volume / 100.0)
|
||||
self.player_header.set_volume(self._saved_volume)
|
||||
@ -247,6 +258,8 @@ class MainWindow(QMainWindow):
|
||||
config = self.config_loader
|
||||
self.settings_tab.save_settings(config)
|
||||
config.set("Playback", "last_volume", str(self.player_header.volume_slider.value()))
|
||||
config.set("Playback", "shuffle_active", str(self._shuffle_active).lower())
|
||||
config.set("Playback", "repeat_mode", self._repeat_mode)
|
||||
if self.current_track_path and os.path.exists(self.current_track_path):
|
||||
config.set("Playback", "last_track_path", self.current_track_path)
|
||||
position = self.player.position()
|
||||
@ -353,10 +366,41 @@ class MainWindow(QMainWindow):
|
||||
idx = max(0, self.library_tab.current_playback_index - 1)
|
||||
self.library_tab.play_track_at_index(idx)
|
||||
|
||||
def _get_next_index(self):
|
||||
playable = self.library_tab.playable_row_indices()
|
||||
if not playable:
|
||||
return None
|
||||
|
||||
current = self.library_tab.current_playback_index
|
||||
|
||||
if self._shuffle_active:
|
||||
candidates = [r for r in playable if r != current]
|
||||
if not candidates:
|
||||
candidates = playable
|
||||
return random.choice(candidates)
|
||||
|
||||
try:
|
||||
pos = playable.index(current)
|
||||
except ValueError:
|
||||
pos = -1
|
||||
|
||||
if pos >= 0 and pos < len(playable) - 1:
|
||||
return playable[pos + 1]
|
||||
|
||||
if self._repeat_mode == "repeat_all" and playable:
|
||||
return playable[0]
|
||||
|
||||
return None
|
||||
|
||||
def _stop_playback(self):
|
||||
self.player.stop()
|
||||
self.library_tab.on_playing_changed(False)
|
||||
self.player_header.set_playing(False)
|
||||
|
||||
def _on_next(self):
|
||||
rows = self.library_tab.library_table.rowCount()
|
||||
idx = min(rows - 1, self.library_tab.current_playback_index + 1)
|
||||
self.library_tab.play_track_at_index(idx)
|
||||
idx = self._get_next_index()
|
||||
if idx is not None:
|
||||
self.library_tab.play_track_at_index(idx)
|
||||
|
||||
def _seek_forward(self):
|
||||
if self.player.duration() <= 0:
|
||||
@ -396,10 +440,29 @@ class MainWindow(QMainWindow):
|
||||
pos = self.player.position()
|
||||
self.player_header.set_position(pos, duration)
|
||||
|
||||
def _on_shuffle_toggle(self):
|
||||
self._shuffle_active = not self._shuffle_active
|
||||
self.player_header.set_shuffle_active(self._shuffle_active)
|
||||
|
||||
def _on_repeat_cycle(self):
|
||||
cycle = {"no_repeat": "repeat_all", "repeat_all": "repeat_one", "repeat_one": "no_repeat"}
|
||||
self._repeat_mode = cycle[self._repeat_mode]
|
||||
self.player_header.set_repeat_mode(self._repeat_mode)
|
||||
|
||||
def _on_media_status(self, status):
|
||||
if status == QMediaPlayer.MediaStatus.EndOfMedia:
|
||||
self.library_tab.on_track_ended()
|
||||
self._on_next()
|
||||
|
||||
if self._repeat_mode == "repeat_one":
|
||||
self.player.setPosition(0)
|
||||
self.player.play()
|
||||
return
|
||||
|
||||
next_idx = self._get_next_index()
|
||||
if next_idx is not None:
|
||||
self.library_tab.play_track_at_index(next_idx)
|
||||
else:
|
||||
self._stop_playback()
|
||||
elif status == QMediaPlayer.MediaStatus.NoMedia:
|
||||
self.library_tab.on_playing_changed(False)
|
||||
self.player_header.reset_info()
|
||||
|
||||
@ -174,6 +174,10 @@ class ConfigLoader:
|
||||
self.config.set('Playback', 'last_volume', '80')
|
||||
if not self.config.has_option('Playback', 'last_playing'):
|
||||
self.config.set('Playback', 'last_playing', 'false')
|
||||
if not self.config.has_option('Playback', 'shuffle_active'):
|
||||
self.config.set('Playback', 'shuffle_active', 'false')
|
||||
if not self.config.has_option('Playback', 'repeat_mode'):
|
||||
self.config.set('Playback', 'repeat_mode', 'no_repeat')
|
||||
|
||||
def get(self, section: str, option: str, fallback: Any = None) -> Any:
|
||||
"""
|
||||
|
||||
@ -256,6 +256,13 @@ class LibraryTab(QWidget):
|
||||
|
||||
layout.addLayout(toolbar)
|
||||
|
||||
def playable_row_indices(self) -> list[int]:
|
||||
indices = []
|
||||
for row in range(self.library_table.rowCount()):
|
||||
if not self.library_table.isRowHidden(row):
|
||||
indices.append(row)
|
||||
return indices
|
||||
|
||||
def play_track_at_index(self, index: int, start_position_ms: int = 0, auto_play: bool = True):
|
||||
if index < 0 or index >= self.library_table.rowCount():
|
||||
return
|
||||
|
||||
@ -22,6 +22,8 @@ class PlayerHeader(QGroupBox):
|
||||
prev_requested = pyqtSignal()
|
||||
play_pause_requested = pyqtSignal()
|
||||
next_requested = pyqtSignal()
|
||||
shuffle_requested = pyqtSignal()
|
||||
repeat_requested = pyqtSignal()
|
||||
position_changed_by_user = pyqtSignal(int)
|
||||
volume_changed = pyqtSignal(int)
|
||||
|
||||
@ -52,9 +54,21 @@ class PlayerHeader(QGroupBox):
|
||||
self.next_btn.setToolTip("Next")
|
||||
self.next_btn.clicked.connect(self.next_requested)
|
||||
|
||||
self.shuffle_btn = QPushButton("\u21C4")
|
||||
self.shuffle_btn.setFixedWidth(36)
|
||||
self.shuffle_btn.setToolTip("Shuffle")
|
||||
self.shuffle_btn.clicked.connect(self.shuffle_requested)
|
||||
|
||||
self.repeat_btn = QPushButton("\u21BB")
|
||||
self.repeat_btn.setFixedWidth(36)
|
||||
self.repeat_btn.setToolTip("Repeat: Off")
|
||||
self.repeat_btn.clicked.connect(self.repeat_requested)
|
||||
|
||||
controls_row.addWidget(self.shuffle_btn)
|
||||
controls_row.addWidget(self.prev_btn)
|
||||
controls_row.addWidget(self.play_btn)
|
||||
controls_row.addWidget(self.next_btn)
|
||||
controls_row.addWidget(self.repeat_btn)
|
||||
|
||||
self.time_label_start = QLabel("0:00")
|
||||
self.time_label_start.setFixedWidth(40)
|
||||
@ -102,6 +116,28 @@ class PlayerHeader(QGroupBox):
|
||||
now_playing_layout.addWidget(self.now_playing_label, stretch=1)
|
||||
layout.addLayout(now_playing_layout)
|
||||
|
||||
def set_shuffle_active(self, active: bool):
|
||||
if active:
|
||||
self.shuffle_btn.setStyleSheet("color: palette(highlight); font-weight: bold;")
|
||||
self.shuffle_btn.setToolTip("Shuffle: On")
|
||||
else:
|
||||
self.shuffle_btn.setStyleSheet("")
|
||||
self.shuffle_btn.setToolTip("Shuffle: Off")
|
||||
|
||||
def set_repeat_mode(self, mode: str):
|
||||
if mode == "repeat_all":
|
||||
self.repeat_btn.setText("\u21BB")
|
||||
self.repeat_btn.setStyleSheet("color: palette(highlight); font-weight: bold;")
|
||||
self.repeat_btn.setToolTip("Repeat: All")
|
||||
elif mode == "repeat_one":
|
||||
self.repeat_btn.setText("\u21BB\u00B9")
|
||||
self.repeat_btn.setStyleSheet("color: palette(highlight); font-weight: bold;")
|
||||
self.repeat_btn.setToolTip("Repeat: One")
|
||||
else:
|
||||
self.repeat_btn.setText("\u21BB")
|
||||
self.repeat_btn.setStyleSheet("")
|
||||
self.repeat_btn.setToolTip("Repeat: Off")
|
||||
|
||||
def _on_slider_user_action(self):
|
||||
self.position_changed_by_user.emit(self.position_slider.value())
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user