Files
lintunes/lintunes/gui/preferences_dialog.py
T
trav a1eb8aec8b Visualizer: gray-mode color slider + mode persists across launches
- New prefs: visualizer_mode (on/dim/off, saved on every click) and
  color_visualizer_gray (None = keep the derived alternateBase look).
- VisualizerWidget takes prefs (optional, so existing call sites/tests
  keep working); restores its mode at startup and validates junk values.
- New "Visualizer (gray mode):" row in the Preferences color sliders;
  theme.default_gray mirrors the widget's 0.88-lightness derivation so
  an untouched slider shows the current appearance.
- TransportBar.refresh_theme() repaints the visualizer so slider drags
  preview live.
2026-07-02 16:52:59 -04:00

198 lines
7.4 KiB
Python

from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QGroupBox, QLabel, QSlider,
QRadioButton, QButtonGroup, QLineEdit, QPushButton, QCheckBox,
QFormLayout,
)
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtGui import QPixmap, QColor, QIcon
from lintunes import theme
from lintunes.theme import HIGHLIGHT_COLORS
_SCALES = ["small", "medium", "large"]
# (preference key, row label) for the per-area grayscale sliders.
_COLOR_ROWS = [
("color_background", "Background:"),
("color_button", "Buttons:"),
("color_now_playing_bg", "Now-playing panel:"),
("color_now_playing_text", "Now-playing text:"),
("color_stripe", "Stripes:"),
("color_visualizer_gray", "Visualizer (gray mode):"),
]
def _swatch(color_hex: str) -> QIcon:
pixmap = QPixmap(18, 18)
pixmap.fill(QColor(color_hex))
return QIcon(pixmap)
class PreferencesDialog(QDialog):
def __init__(self, prefs, lastfm, parent=None):
super().__init__(parent)
self._prefs = prefs
self._lastfm = lastfm
self.setWindowTitle("Preferences")
self.setMinimumWidth(420)
# Color sliders preview live (set_live emits without writing); persist
# once the user pauses so a drag doesn't hammer the disk.
self._color_save_timer = QTimer(self)
self._color_save_timer.setSingleShot(True)
self._color_save_timer.setInterval(400)
self._color_save_timer.timeout.connect(self._prefs.save)
layout = QVBoxLayout(self)
layout.addWidget(self._build_general_group())
layout.addWidget(self._build_colors_group())
layout.addWidget(self._build_lastfm_group())
layout.addStretch()
lastfm.login_finished.connect(self._on_login_finished)
self._refresh_lastfm_state()
# ---- General ----
def _build_general_group(self) -> QGroupBox:
group = QGroupBox("General")
form = QFormLayout(group)
# UI size: 3-position slider
size_row = QHBoxLayout()
self._size_slider = QSlider(Qt.Orientation.Horizontal)
self._size_slider.setRange(0, 2)
self._size_slider.setPageStep(1)
self._size_slider.setTickPosition(QSlider.TickPosition.TicksBelow)
self._size_slider.setTickInterval(1)
self._size_slider.setFixedWidth(160)
current_scale = self._prefs.get("ui_scale", "medium")
self._size_slider.setValue(
_SCALES.index(current_scale) if current_scale in _SCALES else 1)
self._size_label = QLabel(current_scale.capitalize())
self._size_slider.valueChanged.connect(self._on_size_changed)
size_row.addWidget(QLabel("Small"))
size_row.addWidget(self._size_slider)
size_row.addWidget(QLabel("Large"))
size_row.addSpacing(8)
size_row.addWidget(self._size_label)
size_row.addStretch()
form.addRow("UI size:", size_row)
# Highlight color swatches
color_row = QHBoxLayout()
self._color_group = QButtonGroup(self)
for name, hex_value in HIGHLIGHT_COLORS.items():
radio = QRadioButton(name)
radio.setIcon(_swatch(hex_value))
radio.setChecked(self._prefs.get("highlight") == name)
radio.toggled.connect(
lambda checked, n=name: checked and self._prefs.set("highlight", n))
self._color_group.addButton(radio)
color_row.addWidget(radio)
color_row.addStretch()
form.addRow("Highlight:", color_row)
return group
def _on_size_changed(self, value: int):
scale = _SCALES[value]
self._size_label.setText(scale.capitalize())
self._prefs.set("ui_scale", scale)
# ---- Colors ----
def _build_colors_group(self) -> QGroupBox:
group = QGroupBox("Colors")
form = QFormLayout(group)
note = QLabel("Each slider runs black → white and updates instantly.")
note.setWordWrap(True)
form.addRow(note)
self._color_sliders = {}
for key, label in _COLOR_ROWS:
slider = QSlider(Qt.Orientation.Horizontal)
slider.setRange(0, 255)
slider.setValue(theme.gray_value(self._prefs, key)) # before connect
slider.valueChanged.connect(
lambda value, k=key: self._on_color_changed(k, value))
self._color_sliders[key] = slider
form.addRow(label, slider)
return group
def _on_color_changed(self, key: str, value: int):
self._prefs.set_live(key, value) # live preview, no disk write yet
self._color_save_timer.start() # persist once the drag settles
# ---- Last.fm ----
def _build_lastfm_group(self) -> QGroupBox:
group = QGroupBox("Last.fm")
form = QFormLayout(group)
hint = QLabel(
'Needs a free API account: '
'<a href="https://www.last.fm/api/account/create">'
'last.fm/api/account/create</a>')
hint.setOpenExternalLinks(True)
form.addRow(hint)
lf = self._prefs.lastfm
self._api_key = QLineEdit(lf.get("api_key", ""))
self._api_secret = QLineEdit(lf.get("api_secret", ""))
self._api_secret.setEchoMode(QLineEdit.EchoMode.Password)
self._username = QLineEdit(lf.get("username", ""))
self._password = QLineEdit()
self._password.setEchoMode(QLineEdit.EchoMode.Password)
form.addRow("API key:", self._api_key)
form.addRow("Shared secret:", self._api_secret)
form.addRow("Username:", self._username)
form.addRow("Password:", self._password)
button_row = QHBoxLayout()
self._login_button = QPushButton("Log in")
self._login_button.clicked.connect(self._on_login_clicked)
self._status_label = QLabel("")
button_row.addWidget(self._login_button)
button_row.addWidget(self._status_label)
button_row.addStretch()
form.addRow("", button_row)
self._scrobble_check = QCheckBox("Enable scrobbling")
self._scrobble_check.setChecked(lf.get("scrobble_enabled", False))
self._scrobble_check.toggled.connect(
lambda on: self._prefs.update_lastfm(scrobble_enabled=on))
form.addRow("", self._scrobble_check)
return group
def _on_login_clicked(self):
if self._lastfm.is_logged_in():
self._lastfm.logout()
self._refresh_lastfm_state()
return
api_key = self._api_key.text().strip()
secret = self._api_secret.text().strip()
username = self._username.text().strip()
password = self._password.text()
if not all((api_key, secret, username, password)):
self._status_label.setText("Fill in all four fields first")
return
self._login_button.setEnabled(False)
self._status_label.setText("Logging in…")
self._lastfm.login(api_key, secret, username, password)
def _on_login_finished(self, success: bool, message: str):
self._status_label.setText(message)
self._login_button.setEnabled(True)
if success:
self._password.clear()
self._refresh_lastfm_state()
def _refresh_lastfm_state(self):
logged_in = self._lastfm.is_logged_in()
self._login_button.setText("Log out" if logged_in else "Log in")
self._scrobble_check.setEnabled(logged_in)
if logged_in and not self._status_label.text():
self._status_label.setText(
f"Logged in as {self._prefs.lastfm.get('username', '')}")