Files
LinedanceAfspiller/linedance-app/ui/settings_dialog.py
2026-04-14 20:20:46 +02:00

530 lines
23 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
settings_dialog.py — Indstillinger for LineDance Player.
Gemmes via QSettings og læses ved opstart.
"""
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QComboBox, QSpinBox, QCheckBox, QFrame,
QTabWidget, QWidget, QFileDialog, QGroupBox, QFormLayout,
)
from PyQt6.QtCore import Qt, QSettings
SETTINGS_KEY_THEME = "appearance/dark_theme"
SETTINGS_KEY_DEMO_SEC = "playback/demo_seconds"
SETTINGS_KEY_DEMO_FADE = "playback/demo_fade_seconds"
SETTINGS_KEY_VOLUME = "playback/volume"
SETTINGS_KEY_MAIL_CLIENT = "mail/client"
SETTINGS_KEY_MAIL_PATH = "mail/custom_path"
SETTINGS_KEY_AUTO_LOGIN = "online/auto_login"
SETTINGS_KEY_USERNAME = "online/username"
SETTINGS_KEY_PASSWORD = "online/password"
SETTINGS_KEY_SERVER_URL = "online/server_url"
SETTINGS_KEY_LANGUAGE = "appearance/language"
SETTINGS_KEY_BETWEEN_SEC = "playback/between_seconds"
SETTINGS_KEY_WORKSHOP_MIN = "playback/workshop_minutes"
SETTINGS_KEY_MAIN_DEVICE = "playback/audio_device_main"
SETTINGS_KEY_PREV_DEVICE = "playback/audio_device_preview"
SETTINGS_KEY_AFTER_SONG = "playback/after_song_mode"
SETTINGS_KEY_AFTER_DELAY = "playback/after_song_delay"
SETTINGS_KEY_ACOUSTID = "playback/acoustid_enabled"
SETTINGS_KEY_ACOUSTID_KEY = "playback/acoustid_api_key"
def load_settings() -> dict:
s = QSettings("LineDance", "Player")
return {
"dark_theme": s.value(SETTINGS_KEY_THEME, True, type=bool),
"demo_seconds": s.value(SETTINGS_KEY_DEMO_SEC, 10, type=int),
"demo_fade_seconds": s.value(SETTINGS_KEY_DEMO_FADE, 5, type=int),
"volume": s.value(SETTINGS_KEY_VOLUME, 78, type=int),
"mail_client": s.value(SETTINGS_KEY_MAIL_CLIENT, "auto"),
"mail_path": s.value(SETTINGS_KEY_MAIL_PATH, ""),
"auto_login": s.value(SETTINGS_KEY_AUTO_LOGIN, False, type=bool),
"username": s.value(SETTINGS_KEY_USERNAME, ""),
"password": s.value(SETTINGS_KEY_PASSWORD, ""),
"server_url": s.value(SETTINGS_KEY_SERVER_URL, "http://localhost:8000"),
"language": s.value(SETTINGS_KEY_LANGUAGE, "da"),
"between_seconds": s.value(SETTINGS_KEY_BETWEEN_SEC, 60, type=int),
"workshop_minutes": s.value(SETTINGS_KEY_WORKSHOP_MIN, 10, type=int),
"audio_device_main": s.value(SETTINGS_KEY_MAIN_DEVICE, ""),
"audio_device_preview":s.value(SETTINGS_KEY_PREV_DEVICE, ""),
"after_song_mode": s.value(SETTINGS_KEY_AFTER_SONG, "manual"),
"after_song_delay": s.value(SETTINGS_KEY_AFTER_DELAY, 2, type=int),
"acoustid_enabled": s.value(SETTINGS_KEY_ACOUSTID, False, type=bool),
"acoustid_api_key": s.value(SETTINGS_KEY_ACOUSTID_KEY, ""),
}
def save_settings(values: dict):
s = QSettings("LineDance", "Player")
s.setValue(SETTINGS_KEY_THEME, values.get("dark_theme", True))
s.setValue(SETTINGS_KEY_DEMO_SEC, values.get("demo_seconds", 10))
s.setValue(SETTINGS_KEY_DEMO_FADE, values.get("demo_fade_seconds", 5))
s.setValue(SETTINGS_KEY_VOLUME, values.get("volume", 78))
s.setValue(SETTINGS_KEY_MAIL_CLIENT, values.get("mail_client", "auto"))
s.setValue(SETTINGS_KEY_MAIL_PATH, values.get("mail_path", ""))
s.setValue(SETTINGS_KEY_AUTO_LOGIN, values.get("auto_login", False))
s.setValue(SETTINGS_KEY_USERNAME, values.get("username", ""))
s.setValue(SETTINGS_KEY_PASSWORD, values.get("password", ""))
s.setValue(SETTINGS_KEY_SERVER_URL, values.get("server_url", "http://localhost:8000"))
s.setValue(SETTINGS_KEY_LANGUAGE, values.get("language", "da"))
s.setValue(SETTINGS_KEY_BETWEEN_SEC, values.get("between_seconds", 60))
s.setValue(SETTINGS_KEY_WORKSHOP_MIN,values.get("workshop_minutes", 10))
s.setValue(SETTINGS_KEY_MAIN_DEVICE, values.get("audio_device_main", ""))
s.setValue(SETTINGS_KEY_PREV_DEVICE, values.get("audio_device_preview", ""))
s.setValue(SETTINGS_KEY_AFTER_SONG, values.get("after_song_mode", "manual"))
s.setValue(SETTINGS_KEY_AFTER_DELAY, values.get("after_song_delay", 2))
s.setValue(SETTINGS_KEY_ACOUSTID, values.get("acoustid_enabled", False))
s.setValue(SETTINGS_KEY_ACOUSTID_KEY, values.get("acoustid_api_key", ""))
class SettingsDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Indstillinger")
self.setMinimumWidth(480)
self.setModal(True)
self._values = load_settings()
self._build_ui()
self._populate()
def _build_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(16, 16, 16, 16)
layout.setSpacing(12)
tabs = QTabWidget()
tabs.setStyleSheet("""
QTabBar::tab {
padding: 6px 14px;
font-size: 13px;
color: #9aa0b0;
background: #1e2128;
border: none;
min-width: 80px;
}
QTabBar::tab:selected {
color: #e0e4f0;
background: #2a2d36;
border-bottom: 2px solid #e8a020;
}
QTabBar::tab:hover {
color: #e0e4f0;
background: #252830;
}
""")
tabs.addTab(self._build_appearance_tab(), "Udseende")
tabs.addTab(self._build_playback_tab(), "Afspilning")
tabs.addTab(self._build_mail_tab(), "Mail")
tabs.addTab(self._build_online_tab(), "Online")
tabs.addTab(self._build_language_tab(), "Sprog")
layout.addWidget(tabs)
# Knapper
btn_row = QHBoxLayout()
btn_row.addStretch()
btn_cancel = QPushButton("Annuller")
btn_cancel.clicked.connect(self.reject)
btn_row.addWidget(btn_cancel)
btn_save = QPushButton("💾 Gem indstillinger")
btn_save.setObjectName("btn_play")
btn_save.setDefault(True)
btn_save.clicked.connect(self._save_and_close)
btn_row.addWidget(btn_save)
layout.addLayout(btn_row)
# ── Fane: Udseende ────────────────────────────────────────────────────────
def _build_appearance_tab(self) -> QWidget:
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setSpacing(12)
grp = QGroupBox("Standard tema")
grp_layout = QVBoxLayout(grp)
self._chk_dark = QCheckBox("Start med mørkt tema")
grp_layout.addWidget(self._chk_dark)
note = QLabel("Du kan altid skifte tema mens programmet kører via topbar-knappen.")
note.setObjectName("result_count")
note.setWordWrap(True)
grp_layout.addWidget(note)
layout.addWidget(grp)
layout.addStretch()
return tab
# ── Fane: Afspilning ──────────────────────────────────────────────────────
def _build_playback_tab(self) -> QWidget:
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setSpacing(12)
grp = QGroupBox("Forspil (▶ N SEK knappen)")
grp_layout = QFormLayout(grp)
self._spin_demo = QSpinBox()
self._spin_demo.setRange(3, 60)
self._spin_demo.setSuffix(" sekunder")
self._spin_demo.setFixedWidth(140)
grp_layout.addRow("Forspil-længde:", self._spin_demo)
self._spin_fade = QSpinBox()
self._spin_fade.setRange(0, 15)
self._spin_fade.setSuffix(" sekunder (0 = ingen fade)")
self._spin_fade.setFixedWidth(220)
self._spin_fade.setToolTip(
"Fade-out tilføjes til forspillets længde.\n"
"F.eks. 10 sek forspil + 5 sek fade = 15 sek total.\n"
"Sæt til 0 for ingen fade."
)
grp_layout.addRow("Fade-ud:", self._spin_fade)
note = QLabel(
"Forspillet afspiller begyndelsen af sangen så arrangøren kan bekræfte\n"
"at det er den rigtige sang og dans inden eventet starter.\n"
"Fade-ud tilføjes oven i forspillets længde og fades logaritmisk."
)
note.setObjectName("result_count")
note.setWordWrap(True)
grp_layout.addRow(note)
layout.addWidget(grp)
grp2 = QGroupBox("Danseliste-tider ( info-vinduet)")
grp2_layout = QFormLayout(grp2)
self._spin_between = QSpinBox()
self._spin_between.setRange(0, 600)
self._spin_between.setSuffix(" sekunder")
self._spin_between.setFixedWidth(140)
grp2_layout.addRow("Tid mellem musikstykker:", self._spin_between)
self._spin_workshop = QSpinBox()
self._spin_workshop.setRange(0, 120)
self._spin_workshop.setSuffix(" minutter")
self._spin_workshop.setFixedWidth(140)
grp2_layout.addRow("Tid per workshop:", self._spin_workshop)
layout.addWidget(grp2)
# Reaktion når sang slutter
from PyQt6.QtWidgets import QRadioButton, QButtonGroup
grp3 = QGroupBox("Når en sang slutter")
grp3_layout = QVBoxLayout(grp3)
grp3_layout.setSpacing(8)
self._radio_manual = QRadioButton("Manuel — marker næste klar, vent på ▶")
self._radio_auto_demo = QRadioButton("Auto-demo — afspil demo af næste sang automatisk")
self._radio_auto_play = QRadioButton("Auto-play — start næste sang automatisk")
self._radio_demo_then_play = QRadioButton("Auto-demo → auto-play — demo, pause, så spiller sangen automatisk")
self._after_song_group = QButtonGroup(self)
self._after_song_group.addButton(self._radio_manual, 0)
self._after_song_group.addButton(self._radio_auto_demo, 1)
self._after_song_group.addButton(self._radio_auto_play, 2)
self._after_song_group.addButton(self._radio_demo_then_play, 3)
grp3_layout.addWidget(self._radio_manual)
grp3_layout.addWidget(self._radio_auto_demo)
grp3_layout.addWidget(self._radio_auto_play)
grp3_layout.addWidget(self._radio_demo_then_play)
delay_row = QHBoxLayout()
delay_row.addWidget(QLabel(" Pause før næste starter:"))
self._spin_after_delay = QSpinBox()
self._spin_after_delay.setRange(0, 30)
self._spin_after_delay.setSuffix(" sekunder")
self._spin_after_delay.setFixedWidth(160)
self._spin_after_delay.setToolTip(
"Bruges til auto-demo og auto-play.\n"
"Antal sekunder der ventes inden næste sang starter."
)
delay_row.addWidget(self._spin_after_delay)
delay_row.addStretch()
grp3_layout.addLayout(delay_row)
layout.addWidget(grp3)
grp3 = QGroupBox("Lydenheder")
grp3_layout = QFormLayout(grp3)
from player.player import Player as _Player
devices = _Player.get_audio_devices()
device_items = [("Standard", "")] + [(d["name"], d["id"]) for d in devices]
self._combo_main_device = QComboBox()
self._combo_preview_device = QComboBox()
for name, did in device_items:
self._combo_main_device.addItem(name, did)
self._combo_preview_device.addItem(name, did)
grp3_layout.addRow("Hoved-afspiller (sal):", self._combo_main_device)
grp3_layout.addRow("Preview (høretelefoner):", self._combo_preview_device)
note3 = QLabel("Preview-afspilleren bruges til at lytte til sange i biblioteket\nudenom at afbryde den sang der spiller i salen.")
note3.setObjectName("result_count")
note3.setWordWrap(True)
grp3_layout.addRow(note3)
layout.addWidget(grp3)
# AcoustID fingerprinting
grp4 = QGroupBox("AcoustID fingerprinting (valgfri)")
grp4_layout = QVBoxLayout(grp4)
grp4_layout.setSpacing(6)
self._chk_acoustid = QCheckBox("Kør AcoustID fingerprinting i baggrunden")
self._chk_acoustid.setToolTip(
"Analyserer sange uden MBID og slår dem op i AcoustID-databasen.\n"
"Kræver fpcalc (Chromaprint) installeret.\n"
"Startes automatisk 10 sekunder efter opstart."
)
grp4_layout.addWidget(self._chk_acoustid)
key_row = QHBoxLayout()
key_row.addWidget(QLabel("API-nøgle:"))
self._acoustid_key = QLineEdit()
self._acoustid_key.setPlaceholderText("Hent gratis på acoustid.org/api-key")
key_row.addWidget(self._acoustid_key)
grp4_layout.addLayout(key_row)
note4 = QLabel(
"fpcalc skal installeres separat:\n"
" Linux: sudo apt install libchromaprint-tools\n"
" Windows: download fra acoustid.org/chromaprint"
)
note4.setObjectName("result_count")
note4.setWordWrap(True)
grp4_layout.addWidget(note4)
layout.addWidget(grp4)
layout.addStretch()
return tab
# ── Fane: Mail ────────────────────────────────────────────────────────────
def _build_mail_tab(self) -> QWidget:
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setSpacing(12)
grp = QGroupBox("Mailklient")
grp_layout = QFormLayout(grp)
self._mail_combo = QComboBox()
self._mail_combo.addItem("Auto-detekter (Thunderbird → Outlook → mailto:)", "auto")
self._mail_combo.addItem("Thunderbird", "thunderbird")
self._mail_combo.addItem("Outlook (Windows)", "outlook")
self._mail_combo.addItem("Brugerdefineret sti", "custom")
self._mail_combo.addItem("Kun mailto: (ingen vedhæftning)", "mailto")
self._mail_combo.currentIndexChanged.connect(self._on_mail_combo_changed)
grp_layout.addRow("Klient:", self._mail_combo)
path_row = QHBoxLayout()
self._mail_path = QLineEdit()
self._mail_path.setPlaceholderText("/usr/bin/thunderbird eller C:\\...\\thunderbird.exe")
path_row.addWidget(self._mail_path)
btn_browse = QPushButton("...")
btn_browse.setFixedWidth(32)
btn_browse.clicked.connect(self._browse_mail_path)
path_row.addWidget(btn_browse)
self._mail_path_row_widget = QWidget()
self._mail_path_row_widget.setLayout(path_row)
grp_layout.addRow("Sti:", self._mail_path_row_widget)
note = QLabel(
"Med Thunderbird og Outlook åbnes et nyt compose-vindue med filen vedhæftet.\n"
"mailto: åbner standard-mailprogrammet men uden automatisk vedhæftning."
)
note.setObjectName("result_count")
note.setWordWrap(True)
grp_layout.addRow(note)
layout.addWidget(grp)
layout.addStretch()
return tab
def _on_mail_combo_changed(self, idx: int):
is_custom = self._mail_combo.currentData() == "custom"
self._mail_path_row_widget.setVisible(is_custom)
def _browse_mail_path(self):
path, _ = QFileDialog.getOpenFileName(self, "Vælg mailklient")
if path:
self._mail_path.setText(path)
# ── Fane: Online ──────────────────────────────────────────────────────────
def _build_online_tab(self) -> QWidget:
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setSpacing(12)
# Server URL
grp_server = QGroupBox("Server")
grp_server_layout = QFormLayout(grp_server)
self._server_url = QLineEdit()
self._server_url.setPlaceholderText("http://localhost:8000")
grp_server_layout.addRow("API-adresse:", self._server_url)
note_server = QLabel("Adressen på LineDance API-serveren.")
note_server.setObjectName("result_count")
grp_server_layout.addRow(note_server)
layout.addWidget(grp_server)
# Login
grp = QGroupBox("Konto")
grp_layout = QFormLayout(grp)
btn_register = QPushButton("✚ Opret ny konto...")
btn_register.clicked.connect(self._open_register)
grp_layout.addRow(btn_register)
self._chk_auto_login = QCheckBox("Log automatisk ind når programmet starter")
self._chk_auto_login.stateChanged.connect(self._on_auto_login_changed)
grp_layout.addRow(self._chk_auto_login)
self._user_input = QLineEdit()
self._user_input.setPlaceholderText("brugernavn eller e-mail")
grp_layout.addRow("Brugernavn:", self._user_input)
self._pass_input = QLineEdit()
self._pass_input.setEchoMode(QLineEdit.EchoMode.Password)
self._pass_input.setPlaceholderText("••••••••")
grp_layout.addRow("Kodeord:", self._pass_input)
note = QLabel(
"⚠ Kodeordet gemmes lokalt på denne computer.\n"
"Brug kun dette på en personlig maskine."
)
note.setObjectName("result_count")
note.setWordWrap(True)
grp_layout.addRow(note)
layout.addWidget(grp)
layout.addStretch()
return tab
def _open_register(self):
from ui.register_dialog import RegisterDialog
server_url = self._server_url.text().strip() or "http://localhost:8000"
dlg = RegisterDialog(server_url=server_url, parent=self)
dlg.exec()
def _build_language_tab(self) -> QWidget:
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setSpacing(12)
grp = QGroupBox("Sprog")
grp_layout = QFormLayout(grp)
self._lang_combo = QComboBox()
self._lang_combo.addItem("Dansk", "da")
self._lang_combo.addItem("English", "en")
grp_layout.addRow("Programsprog:", self._lang_combo)
note = QLabel("Sproget anvendes næste gang programmet startes.")
note.setObjectName("result_count")
note.setWordWrap(True)
grp_layout.addRow(note)
layout.addWidget(grp)
layout.addStretch()
return tab
def _on_auto_login_changed(self, state: int):
enabled = state == Qt.CheckState.Checked.value
self._user_input.setEnabled(enabled)
self._pass_input.setEnabled(enabled)
# ── Populer fra gemte værdier ─────────────────────────────────────────────
def _populate(self):
v = self._values
self._chk_dark.setChecked(v.get("dark_theme", True))
self._spin_demo.setValue(v.get("demo_seconds", 10))
self._spin_fade.setValue(v.get("demo_fade_seconds", 5))
self._spin_between.setValue(v.get("between_seconds", 60))
self._spin_workshop.setValue(v.get("workshop_minutes", 10))
# Sprog
lang = v.get("language", "da")
for i in range(self._lang_combo.count()):
if self._lang_combo.itemData(i) == lang:
self._lang_combo.setCurrentIndex(i)
break
# Mail
client = v.get("mail_client", "auto")
for i in range(self._mail_combo.count()):
if self._mail_combo.itemData(i) == client:
self._mail_combo.setCurrentIndex(i)
break
self._mail_path.setText(v.get("mail_path", ""))
self._on_mail_combo_changed(self._mail_combo.currentIndex())
# Online
auto = v.get("auto_login", False)
self._chk_auto_login.setChecked(auto)
self._user_input.setText(v.get("username", ""))
self._pass_input.setText(v.get("password", ""))
self._server_url.setText(v.get("server_url", "http://localhost:8000"))
self._user_input.setEnabled(auto)
self._pass_input.setEnabled(auto)
# Lydenheder
main_dev = v.get("audio_device_main", "")
preview_dev = v.get("audio_device_preview", "")
for i in range(self._combo_main_device.count()):
if self._combo_main_device.itemData(i) == main_dev:
self._combo_main_device.setCurrentIndex(i)
break
for i in range(self._combo_preview_device.count()):
if self._combo_preview_device.itemData(i) == preview_dev:
self._combo_preview_device.setCurrentIndex(i)
break
# Reaktion når sang slutter
mode = v.get("after_song_mode", "manual")
if mode == "auto_demo":
self._radio_auto_demo.setChecked(True)
elif mode == "auto_play":
self._radio_auto_play.setChecked(True)
elif mode == "demo_then_play":
self._radio_demo_then_play.setChecked(True)
else:
self._radio_manual.setChecked(True)
self._spin_after_delay.setValue(v.get("after_song_delay", 2))
self._chk_acoustid.setChecked(v.get("acoustid_enabled", False))
self._acoustid_key.setText(v.get("acoustid_api_key", ""))
# ── Gem ───────────────────────────────────────────────────────────────────
def _save_and_close(self):
values = {
"dark_theme": self._chk_dark.isChecked(),
"demo_seconds": self._spin_demo.value(),
"demo_fade_seconds": self._spin_fade.value(),
"between_seconds": self._spin_between.value(),
"workshop_minutes": self._spin_workshop.value(),
"mail_client": self._mail_combo.currentData(),
"mail_path": self._mail_path.text().strip(),
"auto_login": self._chk_auto_login.isChecked(),
"username": self._user_input.text().strip(),
"password": self._pass_input.text(),
"server_url": self._server_url.text().strip() or "http://localhost:8000",
"language": self._lang_combo.currentData(),
"audio_device_main": self._combo_main_device.currentData() or "",
"audio_device_preview":self._combo_preview_device.currentData() or "",
"after_song_mode": (
"auto_demo" if self._radio_auto_demo.isChecked() else
"auto_play" if self._radio_auto_play.isChecked() else
"demo_then_play" if self._radio_demo_then_play.isChecked() else
"manual"
),
"after_song_delay": self._spin_after_delay.value(),
"acoustid_enabled": self._chk_acoustid.isChecked(),
"acoustid_api_key": self._acoustid_key.text().strip(),
}
save_settings(values)
self._values = values
self.accept()
def get_values(self) -> dict:
return self._values