Five grayscale (black->white) sliders in Preferences that update live:
outer-chrome background (QPalette.Window), the rounded transport button
boxes, the now-playing panel, the now-playing text, and the row stripes
(QPalette.AlternateBase). Defaults are None ("inherit current") so nothing
changes until the user drags. Decouples the button boxes from the stripe
color (they used to share palette alternate-base). Live preview via
Preferences.set_live (no disk write); persisted ~0.4s after the drag.
90 lines
3.0 KiB
Python
90 lines
3.0 KiB
Python
import copy
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from PyQt6.QtCore import QObject, pyqtSignal
|
|
|
|
|
|
DEFAULTS = {
|
|
"ui_scale": "medium", # small | medium | large
|
|
"highlight": "blue", # see theme.HIGHLIGHT_COLORS
|
|
"now_playing_font": None, # None=prefer Century Gothic, ""=use app
|
|
# default, "Family"=use app
|
|
"volume": 0.8, # logical 0..1 master output volume
|
|
# Per-area grayscale overrides (0=black .. 255=white). None = inherit the
|
|
# current/default look for that surface, so nothing changes until the user
|
|
# drags a slider in Preferences.
|
|
"color_background": None, # outer chrome (QPalette.Window)
|
|
"color_button": None, # rounded transport button boxes
|
|
"color_now_playing_bg": None, # panel behind the now-playing text
|
|
"color_now_playing_text": None, # the now-playing title/artist text
|
|
"color_stripe": None, # alternating row stripes (QPalette.AlternateBase)
|
|
"lastfm": {
|
|
"api_key": "",
|
|
"api_secret": "",
|
|
"username": "",
|
|
"session_key": "",
|
|
"scrobble_enabled": False,
|
|
},
|
|
}
|
|
|
|
|
|
class Preferences(QObject):
|
|
"""App preferences persisted to <data_dir>/preferences.json (so they ride
|
|
the same syncthing share as the library)."""
|
|
|
|
changed = pyqtSignal()
|
|
|
|
def __init__(self, data_dir: Path, parent=None):
|
|
super().__init__(parent)
|
|
self._path = Path(data_dir) / "preferences.json"
|
|
self._data = copy.deepcopy(DEFAULTS)
|
|
if self._path.exists():
|
|
try:
|
|
with open(self._path, "r", encoding="utf-8") as f:
|
|
stored = json.load(f)
|
|
_deep_merge(self._data, stored)
|
|
except (ValueError, OSError):
|
|
pass
|
|
|
|
def get(self, key: str, default=None):
|
|
return self._data.get(key, default)
|
|
|
|
def set(self, key: str, value):
|
|
if self._data.get(key) == value:
|
|
return
|
|
self._data[key] = value
|
|
self.save()
|
|
self.changed.emit()
|
|
|
|
def set_live(self, key: str, value):
|
|
"""Update a preference and notify listeners *without* writing to disk —
|
|
for live drag-preview (e.g. the color sliders). Call save() to persist
|
|
once the user stops dragging."""
|
|
self._data[key] = value
|
|
self.changed.emit()
|
|
|
|
def update_lastfm(self, **fields):
|
|
self._data["lastfm"].update(fields)
|
|
self.save()
|
|
self.changed.emit()
|
|
|
|
@property
|
|
def lastfm(self) -> dict:
|
|
return self._data["lastfm"]
|
|
|
|
def save(self):
|
|
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
tmp = self._path.with_suffix(".json.tmp")
|
|
with open(tmp, "w", encoding="utf-8") as f:
|
|
json.dump(self._data, f, indent=2)
|
|
tmp.replace(self._path)
|
|
|
|
|
|
def _deep_merge(base: dict, override: dict):
|
|
for key, value in override.items():
|
|
if isinstance(value, dict) and isinstance(base.get(key), dict):
|
|
_deep_merge(base[key], value)
|
|
else:
|
|
base[key] = value
|