Add volume slider to the transport bar

A slim horizontal master-volume slider in its own slot between the
visualizer and the timeline (TASKS.md). Perceptual->linear gain so it
ramps like iTunes; persists to preferences.json (debounced so it doesn't
thrash the theme). Clicking jumps to the spot via a new ClickJumpSlider
base extracted from SeekSlider.
This commit is contained in:
2026-06-26 21:21:00 -04:00
parent 8a07e280b9
commit 6a9027fb9b
4 changed files with 174 additions and 5 deletions
+48 -4
View File
@@ -49,6 +49,14 @@ QFrame#nowPlayingBox {
TITLE_PT_BUMP = 2
def _clamp_volume(value) -> float:
"""Coerce a stored preference to a logical 0..1 volume."""
try:
return max(0.0, min(1.0, float(value)))
except (TypeError, ValueError):
return 0.8
class FlashButton(QToolButton):
"""Transport button: dark-gray glyph that shows the highlight color
while pressed (a quick flash on click, sustained while held). A checkable
@@ -101,9 +109,10 @@ def _box(*widgets, margins=(10, 3, 10, 3), hug=False, height=None) -> QFrame:
return frame
class SeekSlider(QSlider):
"""Timeline slider: clicking anywhere jumps the handle (and playhead)
straight to the clicked spot instead of paging; dragging scrubs."""
class ClickJumpSlider(QSlider):
"""Horizontal slider where clicking anywhere jumps the handle straight to
the clicked spot (instead of paging one step toward it); dragging tracks
the cursor. Shared by the timeline and the volume slider."""
def __init__(self, parent=None):
super().__init__(Qt.Orientation.Horizontal, parent)
@@ -113,7 +122,8 @@ class SeekSlider(QSlider):
self.minimum(), self.maximum(), int(x), self.width())
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton and self.maximum() > 0:
if (event.button() == Qt.MouseButton.LeftButton
and self.maximum() > self.minimum()):
self.setSliderDown(True) # emits sliderPressed
self.setSliderPosition(self._value_at(event.position().x()))
event.accept()
@@ -135,6 +145,11 @@ class SeekSlider(QSlider):
super().mouseReleaseEvent(event)
class SeekSlider(ClickJumpSlider):
"""Timeline slider: clicking anywhere jumps the handle (and playhead)
straight to the clicked spot instead of paging; dragging scrubs."""
class BpmButton(QPushButton):
"""Tap-tempo button: tap along to the music; when the cursor leaves,
the shown bpm is saved to the playing track 3 seconds later."""
@@ -231,6 +246,27 @@ class TransportBar(QWidget):
layout.addWidget(self._visualizer)
layout.addSpacing(10)
# Master output volume: a slim horizontal slider in its own slot
# between the visualizer and the timeline. The HBox centers it
# vertically, so nothing stacks above or below it (iTunes-style).
self._volume_slider = ClickJumpSlider()
self._volume_slider.setRange(0, 100)
self._volume_slider.setFixedWidth(96)
self._volume_slider.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self._volume_slider.setToolTip("Volume")
initial_volume = _clamp_volume(self._prefs.get("volume", 0.8))
self._volume_slider.setValue(round(initial_volume * 100))
player.set_volume(initial_volume)
# Persisting volume re-applies the theme (Preferences.changed), so save
# only after the user stops sliding — audio still tracks every tick.
self._volume_save_timer = QTimer(self)
self._volume_save_timer.setSingleShot(True)
self._volume_save_timer.setInterval(400)
self._volume_save_timer.timeout.connect(self._save_volume)
self._volume_slider.valueChanged.connect(self._on_volume_changed)
layout.addWidget(self._volume_slider)
layout.addSpacing(10)
center = QVBoxLayout()
center.setSpacing(0)
@@ -302,6 +338,14 @@ class TransportBar(QWidget):
button.refresh_icons()
self._apply_now_playing_font()
def _on_volume_changed(self, value: int):
"""Slider moved: update audio live, defer the (theme-reapplying) save."""
self._player.set_volume(value / 100)
self._volume_save_timer.start()
def _save_volume(self):
self._prefs.set("volume", self._volume_slider.value() / 100)
def _apply_now_playing_font(self):
"""Set the now-playing labels to Century Gothic (or the user's chosen
substitute), black text; title slightly larger than artist/album.