""" 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