diff --git a/gui.py b/gui.py index ac81bf5..9e1ebae 100644 --- a/gui.py +++ b/gui.py @@ -972,7 +972,8 @@ class PlaybackBar(QWidget): "QPushButton:hover { color: #ffffff; }" ), } - btn_base = icon_style["normal"] + icon_style["hover"] + self._btn_base = icon_style["normal"] + icon_style["hover"] + btn_base = self._btn_base self._loop_btn = QPushButton("↻") self._loop_btn.setFixedSize(btn_size, btn_size) @@ -986,13 +987,17 @@ class PlaybackBar(QWidget): self._prev_btn = QPushButton("⏮") self._prev_btn.setFixedSize(btn_size, btn_size) - self._prev_btn.setToolTip("Restart / Previous") + self._prev_btn.setToolTip("Previous") self._prev_btn.setStyleSheet(btn_base) self._prev_btn.clicked.connect(self._restart) + self._prev_btn.setEnabled(False) + self._play_btn = QPushButton("▶") self._play_btn.setFixedSize(40, 40) self._play_btn.setToolTip("Play / Pause") + + self._play_btn.setEnabled(False) self._play_btn.setStyleSheet( "QPushButton { background: #ffffff; border: none;" " border-radius: 20px; color: #0a0814;" @@ -1008,6 +1013,8 @@ class PlaybackBar(QWidget): self._next_btn.setStyleSheet(btn_base) self._next_btn.clicked.connect(self._next) + self._next_btn.setEnabled(False) + self._stop_btn = QPushButton("⏹") self._stop_btn.setFixedSize(btn_size, btn_size) self._stop_btn.setToolTip("Stop") @@ -1017,6 +1024,8 @@ class PlaybackBar(QWidget): ) self._stop_btn.clicked.connect(self._stop) + self._stop_btn.setEnabled(False) + btn_row.addWidget(self._loop_btn) btn_row.addWidget(self._prev_btn) btn_row.addStretch() @@ -1024,6 +1033,19 @@ class PlaybackBar(QWidget): btn_row.addStretch() btn_row.addWidget(self._next_btn) btn_row.addWidget(self._stop_btn) + + self._disabled_style = """ + QPushButton { background: transparent; border: none; + color: #44445a; font-size: 15pt; } + """ + self._disabled_extra = """ + QPushButton { background: transparent; border: none; + color: #44445a; font-size: 15pt; } + QPushButton:hover { color: #44445a; } + """ + self._disabled_loop_checked = """ + QPushButton:checked { color: #44445a; } + """ center_layout.addLayout(btn_row) # ── Progress bar ── @@ -1263,8 +1285,34 @@ class PlaybackBar(QWidget): self._player.set_loop(enabled) self._loop_btn.setChecked(enabled) + def set_buttons_enabled(self, enabled: bool) -> None: + """Enable or disable playback control buttons.""" + if enabled: + normal = self._btn_base + loop_checked = "QPushButton:checked { color: #a78bfa; }" + else: + normal = self._disabled_extra + loop_checked = self._disabled_loop_checked + self._prev_btn.setEnabled(enabled) + self._prev_btn.setStyleSheet(normal) + self._play_btn.setEnabled(enabled) + self._next_btn.setEnabled(enabled) + self._next_btn.setStyleSheet(normal) + self._stop_btn.setEnabled(enabled) + self._stop_btn.setStyleSheet(normal) + self._loop_btn.setEnabled(enabled) + if enabled: + self._loop_btn.setStyleSheet(self._btn_base + loop_checked) + else: + self._loop_btn.setStyleSheet(self._disabled_style + loop_checked) + self._vol_slider.setEnabled(enabled) + self._vol_btn.setEnabled(enabled) + def _restart(self) -> None: - self._player.seek(0) + if self._player.has_previous() and self._player.get_position() < 3000: + self._player.previous() + else: + self._player.seek(0) def _next(self) -> None: self._player.skip() @@ -2214,6 +2262,7 @@ class TunettiWindow(QMainWindow): self.player.playback_state_changed.connect(self._on_playback_state_changed) self.player.error.connect(self._on_player_error) self.player.seeked.connect(self._on_player_seeked) + self.player.song_loaded.connect(self._on_song_loaded) # ── Visualizer audio buffer input ── # Connect the player's audio buffer output to our visualizer @@ -2714,6 +2763,10 @@ class TunettiWindow(QMainWindow): log.error("Player error: %s", err.get("error", "unknown")) self._playback.show_error(err.get("error", "Playback error")) + @Slot(bool) + def _on_song_loaded(self, loaded: bool) -> None: + self._playback.set_buttons_enabled(loaded) + @Slot(int) def _on_player_seeked(self, position_ms: int) -> None: """Recalculate Discord RPC start timestamp after a seek.""" diff --git a/player.py b/player.py index 074aa88..dcf72ca 100644 --- a/player.py +++ b/player.py @@ -355,6 +355,7 @@ class AudioPlayer(QObject): """ loading = Signal(str) + song_loaded = Signal(bool) song_started = Signal(dict) song_ended = Signal(str) paused = Signal(dict) @@ -383,6 +384,7 @@ class AudioPlayer(QObject): # ── Queue & state ───────────────────────────────────────────── self._queue: list[dict] = [] self._current: Optional[dict] = None + self._history: list[dict] = [] self._loop_mode = False # ── Download worker (single persistent thread) ──────────────── @@ -447,20 +449,42 @@ class AudioPlayer(QObject): def skip(self) -> None: """Skip to the next song.""" - # Cancel any in-progress download (the skip is our new intent). + # Push current song to history before moving on. + if self._current is not None: + self._history.append(self._current) self._cancel_active_downloads() self._player.stop() self._advance() + def previous(self) -> None: + """Go back to the previous song, if available.""" + if not self._history: + return + prev = self._history.pop() + self._cancel_active_downloads() + self._player.stop() + self._queue.insert(0, prev) + self._try_next_song() + + def has_previous(self) -> bool: + """Whether a previous song is available.""" + return len(self._history) > 0 + + def is_loaded(self) -> bool: + """Whether any song is currently loaded (playing or paused).""" + return self._current is not None + def stop_playback(self) -> None: """Soft stop: halt playback, clear queue, cancel downloads.""" self._cancel_active_downloads() self._cleanup_temp_files() self._queue.clear() self._prefetch_cache.clear() + self._history.clear() self._loop_mode = False self._current = None self._player.stop() + self.song_loaded.emit(False) self.playback_state_changed.emit("stopped") @Slot() @@ -523,6 +547,9 @@ class AudioPlayer(QObject): def get_loop(self) -> bool: return self._loop_mode + def get_position(self) -> int: + return self._player.position() + def set_loop(self, enabled: bool) -> None: self._loop_mode = enabled @@ -658,6 +685,7 @@ class AudioPlayer(QObject): self._player.setSource(QUrl.fromLocalFile(local_path)) self._player.play() self.song_started.emit(song) + self.song_loaded.emit(True) # Kick off prefetch for the next queued song. self._start_prefetch()