""" main_window.py — Linedance afspiller hovedvindue. """ from PyQt6.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QSlider, QLabel, QFrame, QSplitter, QSizePolicy, QMenuBar, QMenu, QStatusBar, QFileDialog, QMessageBox, ) from PyQt6.QtCore import Qt, QTimer from PyQt6.QtGui import QAction from ui.vu_meter import VUMeter from ui.playlist_panel import PlaylistPanel from ui.library_panel import LibraryPanel from ui.themes import apply_theme from ui.scan_worker import ScanWorker from ui.login_dialog import LoginDialog, API_URL from ui.playlist_manager import PlaylistManagerDialog from ui.settings_dialog import SettingsDialog, load_settings from player.player import Player class ProgressBar(QWidget): def __init__(self, parent=None): super().__init__(parent) self._fraction = 0.0 self._demo_fraction = 0.0 self.setFixedHeight(10) self.setCursor(Qt.CursorShape.PointingHandCursor) def set_fraction(self, f: float): self._fraction = max(0.0, min(1.0, f)) self.update() def set_demo_marker(self, f: float): self._demo_fraction = max(0.0, min(1.0, f)) self.update() def paintEvent(self, event): from PyQt6.QtGui import QPainter, QColor p = QPainter(self) w, h = self.width(), self.height() p.fillRect(0, 0, w, h, QColor("#2c3038")) fill_w = int(w * self._fraction) if fill_w > 0: p.fillRect(0, 0, fill_w, h, QColor("#e8a020")) if self._demo_fraction > 0: mx = int(w * self._demo_fraction) p.fillRect(mx - 1, 0, 2, h, QColor("#3b8fd4")) p.end() def mousePressEvent(self, event): if event.button() == Qt.MouseButton.LeftButton: fraction = event.position().x() / self.width() mw = self.window() if hasattr(mw, "_on_seek"): mw._on_seek(fraction) class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("LineDance Player") self.setMinimumSize(1000, 680) self.resize(1600, 820) self._dark_theme = True self._player = Player(self) self._current_idx = -1 self._song_ended = False self._demo_active = False self._watcher = None self._scan_worker = None self._api_url: str | None = None self._api_token: str | None = None self._api_username: str | None = None # Indlæs indstillinger self._settings = load_settings() self._dark_theme = self._settings.get("dark_theme", True) self._demo_seconds = self._settings.get("demo_seconds", 10) self._connect_player_signals() self._build_menu() self._build_ui() self._build_statusbar() apply_theme(self._app_ref(), dark=self._dark_theme) self._theme_btn.setText("☀ LYS TEMA" if self._dark_theme else "● MØRKT TEMA") # Gendan gemt vinduestørrelse og splitter-position self._restore_window_state() # Start DB og scanning ved opstart QTimer.singleShot(200, self._init_local_db) # Auto-login hvis aktiveret i indstillinger if self._settings.get("auto_login") and self._settings.get("password"): QTimer.singleShot(800, self._auto_login) def _app_ref(self): from PyQt6.QtWidgets import QApplication return QApplication.instance() def _connect_player_signals(self): self._player.position_changed.connect(self._on_position) self._player.time_changed.connect(self._on_time) self._player.levels_changed.connect(self._on_levels) self._player.song_ended.connect(self._on_song_ended) self._player.state_changed.connect(self._on_state_changed) # ── Menu ────────────────────────────────────────────────────────────────── def _build_menu(self): menubar = self.menuBar() # ── Filer ───────────────────────────────────────────────────────────── file_menu = menubar.addMenu("Filer") self._act_go_online = QAction("Gå online...", self) self._act_go_online.setShortcut("Ctrl+L") self._act_go_online.triggered.connect(self._go_online) file_menu.addAction(self._act_go_online) self._act_go_offline = QAction("Gå offline", self) self._act_go_offline.triggered.connect(self._go_offline) self._act_go_offline.setEnabled(False) file_menu.addAction(self._act_go_offline) file_menu.addSeparator() act_settings = QAction("Indstillinger...", self) act_settings.setShortcut("Ctrl+,") act_settings.triggered.connect(self._open_settings) file_menu.addAction(act_settings) file_menu.addSeparator() act_quit = QAction("Afslut", self) act_quit.setShortcut("Ctrl+Q") act_quit.triggered.connect(self.close) file_menu.addAction(act_quit) # ── Ingen Danseliste- eller Visning-menu ────────────────────────────── # Ny/Gem/Hent ligger direkte i danseliste-panelet # Tema-skift ligger i topbar-knappen # Mapper og scan ligger i ⚙ Mapper dialogen # Gem reference til scan-action (bruges stadig internt) self._act_scan = QAction("Scan", self) self._act_scan.triggered.connect(self.start_scan) # ── Statuslinje ─────────────────────────────────────────────────────────── def _build_statusbar(self): self._statusbar = QStatusBar() self.setStatusBar(self._statusbar) self._statusbar.showMessage("Klar") def _set_status(self, text: str, timeout_ms: int = 0): """Vis besked i statuslinjen. timeout_ms=0 = permanent.""" self._statusbar.showMessage(text, timeout_ms) # ── UI byggeri ──────────────────────────────────────────────────────────── def _build_ui(self): root = QWidget() root.setObjectName("root") self.setCentralWidget(root) main_layout = QVBoxLayout(root) main_layout.setContentsMargins(10, 6, 10, 10) main_layout.setSpacing(4) main_layout.addWidget(self._build_topbar()) main_layout.addWidget(self._build_now_playing()) main_layout.addWidget(self._build_progress()) main_layout.addWidget(self._build_transport()) main_layout.addWidget(self._build_panels(), stretch=1) def _build_topbar(self) -> QFrame: bar = QFrame() bar.setObjectName("topbar") layout = QHBoxLayout(bar) layout.setContentsMargins(12, 6, 12, 6) logo = QLabel("LINEDANCE PLAYER") logo.setObjectName("logo") logo.setTextFormat(Qt.TextFormat.RichText) layout.addWidget(logo) layout.addStretch() self._conn_label = QLabel("● OFFLINE") self._conn_label.setObjectName("conn_label") layout.addWidget(self._conn_label) self._theme_btn = QPushButton("☀ LYS TEMA") self._theme_btn.setFixedHeight(26) self._theme_btn.clicked.connect(self._toggle_theme) layout.addWidget(self._theme_btn) return bar def _build_now_playing(self) -> QFrame: frame = QFrame() frame.setObjectName("now_playing_frame") layout = QHBoxLayout(frame) layout.setContentsMargins(12, 10, 12, 10) track_frame = QFrame() track_frame.setObjectName("track_display") track_layout = QVBoxLayout(track_frame) track_layout.setContentsMargins(10, 8, 10, 8) track_layout.setSpacing(3) self._lbl_title = QLabel("—") self._lbl_title.setObjectName("track_title") track_layout.addWidget(self._lbl_title) self._lbl_meta = QLabel("—") self._lbl_meta.setObjectName("track_meta") track_layout.addWidget(self._lbl_meta) self._lbl_dances = QLabel("") self._lbl_dances.setObjectName("track_meta") self._lbl_dances.setWordWrap(True) track_layout.addWidget(self._lbl_dances) layout.addWidget(track_frame, stretch=1) self._vu = VUMeter() layout.addWidget(self._vu) return frame def _build_progress(self) -> QFrame: frame = QFrame() frame.setObjectName("progress_frame") layout = QHBoxLayout(frame) layout.setContentsMargins(12, 6, 12, 6) layout.setSpacing(8) self._lbl_cur = QLabel("0:00") self._lbl_cur.setObjectName("track_meta") self._lbl_cur.setFixedWidth(36) layout.addWidget(self._lbl_cur) self._progress = ProgressBar(self) self._progress.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed ) layout.addWidget(self._progress, stretch=1) self._lbl_tot = QLabel("0:00") self._lbl_tot.setObjectName("track_meta") self._lbl_tot.setFixedWidth(36) self._lbl_tot.setAlignment(Qt.AlignmentFlag.AlignRight) layout.addWidget(self._lbl_tot) return frame def _build_transport(self) -> QFrame: frame = QFrame() frame.setObjectName("transport_frame") layout = QHBoxLayout(frame) layout.setContentsMargins(14, 10, 14, 10) layout.setSpacing(8) def btn(text, name=None, size=52, checkable=False): b = QPushButton(text) if name: b.setObjectName(name) b.setFixedSize(size, size) if checkable: b.setCheckable(True) return b self._btn_prev = btn("⏮", size=52) self._btn_play = btn("▶", "btn_play", size=72) self._btn_stop = btn("⏹", "btn_stop", size=52) self._btn_next = btn("⏭", size=52) self._btn_demo = btn(f"▶\n{self._demo_seconds} SEK", "btn_demo", size=64, checkable=True) self._btn_prev.clicked.connect(self._prev_song) self._btn_play.clicked.connect(self._toggle_play) self._btn_stop.clicked.connect(self._stop) self._btn_next.clicked.connect(self._next_song) self._btn_demo.clicked.connect(self._toggle_demo) layout.addWidget(self._btn_prev) layout.addWidget(self._btn_play) layout.addWidget(self._btn_stop) layout.addWidget(self._btn_next) sep1 = QFrame() sep1.setFrameShape(QFrame.Shape.VLine) sep1.setFixedWidth(1) layout.addWidget(sep1) layout.addWidget(self._btn_demo) layout.addStretch() lbl_vol = QLabel("VOL") lbl_vol.setObjectName("vol_label") layout.addWidget(lbl_vol) self._vol_slider = QSlider(Qt.Orientation.Horizontal) self._vol_slider.setRange(0, 100) self._vol_slider.setValue(78) self._vol_slider.setFixedWidth(100) self._vol_slider.valueChanged.connect(self._on_volume) layout.addWidget(self._vol_slider) self._lbl_vol = QLabel("78") self._lbl_vol.setObjectName("vol_val") layout.addWidget(self._lbl_vol) return frame def _build_panels(self) -> QSplitter: self._splitter = QSplitter(Qt.Orientation.Horizontal) self._playlist_panel = PlaylistPanel() self._playlist_panel.song_selected.connect(self._load_song_by_idx) self._playlist_panel.song_dropped.connect(self._on_song_dropped) self._playlist_panel.event_started.connect(self._on_event_started) self._playlist_panel.next_song_ready.connect(self._load_song) self._library_panel = LibraryPanel() self._library_panel.song_selected.connect(self._on_library_song_selected) self._library_panel.add_to_playlist.connect(self._add_song_to_playlist) self._library_panel.scan_requested.connect(self.start_scan) self._library_panel.edit_tags_requested.connect(self._open_tag_editor) self._library_panel.send_mail_requested.connect(self._send_mail) self._splitter.addWidget(self._playlist_panel) self._splitter.addWidget(self._library_panel) self._splitter.setSizes([700, 900]) return self._splitter def _restore_window_state(self): from PyQt6.QtCore import QSettings, QByteArray settings = QSettings("LineDance", "Player") geom = settings.value("window/geometry") if geom: self.restoreGeometry(geom) splitter_state = settings.value("window/splitter") if splitter_state and hasattr(self, "_splitter"): self._splitter.restoreState(splitter_state) def _save_window_state(self): from PyQt6.QtCore import QSettings settings = QSettings("LineDance", "Player") settings.setValue("window/geometry", self.saveGeometry()) if hasattr(self, "_splitter"): settings.setValue("window/splitter", self._splitter.saveState()) # ── Lokal DB + scanning ─────────────────────────────────────────────────── def _init_local_db(self): try: import sys, os sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) from local.local_db import init_db from local.file_watcher import get_watcher init_db() # Brug et Qt signal til thread-safe reload fra watcher-tråden from PyQt6.QtCore import QMetaObject, Q_ARG def on_file_change(event_type, path, song_id): QTimer.singleShot(0, self._reload_library) self._watcher = get_watcher(on_change=on_file_change) self._watcher.start() # Indlæs hvad vi allerede kender fra SQLite self._reload_library() # Gendan sidst aktive danseliste restored = self._playlist_panel.restore_active_playlist() # Gendan event-fremgang hvis liste blev gendannet if restored: if self._playlist_panel.restore_event_state(): # Indlæs den sang vi var nået til idx = self._playlist_panel._current_idx song = self._playlist_panel.get_song(idx) if song: self._current_idx = idx self._load_song(song) self._set_status( f"Event genoptaget ved: {song.get('title','')} — tryk ▶ for at fortsætte", 6000, ) # Kør automatisk scanning ved opstart self._set_status("Starter scanning af biblioteker...") QTimer.singleShot(100, self.start_scan) except Exception as e: self._set_status(f"DB fejl: {e}") print(f"DB init fejl: {e}") def start_scan(self): """Start fuld scanning af alle biblioteker i baggrundstråd.""" if self._scan_worker and self._scan_worker.isRunning(): return # Scanning kører allerede if not self._watcher: self._set_status("Ingen biblioteker at scanne — tilføj en mappe først") return self._library_panel.set_scanning(True, "Forbereder scanning...") self._act_scan.setEnabled(False) self._scan_worker = ScanWorker(self._watcher, parent=self) self._scan_worker.status_update.connect(self._on_scan_status) self._scan_worker.scan_done.connect(self._on_scan_done) self._scan_worker.start() def _on_scan_status(self, text: str): self._set_status(text) self._library_panel.update_scan_status(text) def _on_scan_done(self, count: int): self._library_panel.set_scanning(False) self._act_scan.setEnabled(True) msg = f"Scanning færdig — {count} filer gennemgået" self._set_status(msg, timeout_ms=5000) # Genindlæs biblioteket QTimer.singleShot(200, self._reload_library) def _reload_library(self): try: from local.local_db import search_songs, get_db songs_raw = search_songs("", limit=5000) songs = [] for row in songs_raw: with get_db() as conn: dances = conn.execute( "SELECT dance_name FROM song_dances " "WHERE song_id=? ORDER BY dance_order", (row["id"],) ).fetchall() songs.append({ "id": row["id"], "title": row["title"], "artist": row["artist"], "album": row["album"], "bpm": row["bpm"], "duration_sec": row["duration_sec"], "local_path": row["local_path"], "file_format": row["file_format"], "file_missing": bool(row["file_missing"]), "dances": [d["dance_name"] for d in dances], }) self._library_panel.load_songs(songs) count = len(songs) self._set_status(f"Bibliotek: {count} sang{'e' if count != 1 else ''}", 3000) except Exception as e: print(f"Bibliotek reload fejl: {e}") def add_library_path(self, path: str): try: if not self._watcher: self._set_status("Watcher ikke klar endnu — prøv igen om et øjeblik", 3000) return self._watcher.add_library(path) self._set_status(f"Tilføjet: {path} — scanner...") # Genindlæs bibliotekslisten og start scan QTimer.singleShot(500, self._reload_library) QTimer.singleShot(1000, self.start_scan) except Exception as e: self._set_status(f"Fejl ved tilføjelse: {e}") def _open_settings(self): dialog = SettingsDialog(parent=self) if dialog.exec(): self._settings = dialog.get_values() self._demo_seconds = self._settings.get("demo_seconds", 10) # Opdater tema hvis ændret new_dark = self._settings.get("dark_theme", True) if new_dark != self._dark_theme: self._dark_theme = new_dark apply_theme(self._app_ref(), dark=self._dark_theme) self._theme_btn.setText( "☀ LYS TEMA" if self._dark_theme else "● MØRKT TEMA" ) self._vu.set_dark(self._dark_theme) # Opdater demo-knap tekst self._btn_demo.setText(f"▶\n{self._demo_seconds} SEK") # Opdater demo-markør hvis en sang er indlæst if hasattr(self, "_current_song") and self._current_song: dur = self._current_song.get("duration_sec", 0) if dur > 0: self._progress.set_demo_marker(min(self._demo_seconds / dur, 1.0)) self._set_status("Indstillinger gemt", 2000) def _auto_login(self): """Forsøg automatisk login med gemte oplysninger.""" username = self._settings.get("username", "") password = self._settings.get("password", "") if not username or not password: return try: import urllib.request, urllib.parse, json data = urllib.parse.urlencode({"username": username, "password": password}).encode() req = urllib.request.Request( f"{API_URL}/auth/login", data=data, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST", ) with urllib.request.urlopen(req, timeout=8) as resp: body = json.loads(resp.read()) self._api_token = body.get("access_token") self._api_url = API_URL self._api_username = username self._set_online_state(True) self._set_status(f"Automatisk logget ind som {username}", 4000) # Synkroniser dans-niveauer og navne QTimer.singleShot(500, self._sync_dance_data) except Exception: self._set_status("Auto-login fejlede — kør Filer → Gå online manuelt", 5000) def _go_online(self): dialog = LoginDialog(self) if dialog.exec(): url, username, token = dialog.get_credentials() self._api_url = url self._api_token = token self._api_username = username self._set_online_state(True) self._set_status(f"Online som {username}", 5000) QTimer.singleShot(500, self._sync_dance_data) def _sync_dance_data(self): """Synkroniser dans-niveauer og navne fra API.""" if not self._api_token: return try: import urllib.request, json headers = {"Authorization": f"Bearer {self._api_token}"} # Hent niveauer req = urllib.request.Request(f"{API_URL}/dances/levels", headers=headers) with urllib.request.urlopen(req, timeout=8) as resp: levels = json.loads(resp.read()) from local.local_db import sync_dance_levels_from_api sync_dance_levels_from_api(levels) # Hent populære dans-navne req = urllib.request.Request(f"{API_URL}/dances/names?limit=500", headers=headers) with urllib.request.urlopen(req, timeout=8) as resp: names = json.loads(resp.read()) from local.local_db import sync_dance_names_from_api sync_dance_names_from_api(names) self._set_status(f"Synkroniseret {len(levels)} niveauer og {len(names)} dans-navne", 4000) except Exception as e: print(f"Dans-sync fejl: {e}") def _go_offline(self): self._api_url = self._api_token = self._api_username = None self._set_online_state(False) self._set_status("Offline — arbejder lokalt", 3000) def _set_online_state(self, online: bool): self._act_go_online.setEnabled(not online) self._act_go_offline.setEnabled(online) if online: name = self._api_username or "?" self._conn_label.setText(f"● ONLINE ({name})") self._conn_label.setStyleSheet("color: #2ecc71;") else: self._conn_label.setText("● OFFLINE") self._conn_label.setStyleSheet("color: #5a6070;") def _new_playlist(self): self._stop() self._playlist_panel.load_songs([]) self._playlist_panel.set_playlist_name("Ny liste") self._set_status("Ny danseliste oprettet", 2000) def _open_playlist_manager(self): dialog = PlaylistManagerDialog( current_songs=self._playlist_panel.get_songs(), parent=self, ) dialog.playlist_loaded.connect(self._on_playlist_loaded) dialog.exec() def _on_playlist_loaded(self, name: str, songs: list[dict]): self._stop() self._playlist_panel.load_songs(songs) self._playlist_panel.set_playlist_name(name) self._set_status(f"Indlæst: {name} ({len(songs)} sange)", 3000) def _open_tag_editor(self, song: dict): from ui.tag_editor import TagEditorDialog dialog = TagEditorDialog(song, parent=self) if dialog.exec(): # Genindlæs biblioteket så ændringer vises QTimer.singleShot(200, self._reload_library) def _send_mail(self, song: dict): import subprocess, sys, shutil, urllib.parse from pathlib import Path path = song.get("local_path", "") title = song.get("title", "") artist = song.get("artist", "") if not path or not Path(path).exists(): self._set_status("Filen blev ikke fundet — kan ikke sende mail", 4000) return # ── Auto-detekter mailklient ─────────────────────────────────────────── def try_thunderbird() -> bool: """Thunderbird: thunderbird -compose attachment='file:///sti'""" candidates = [] if sys.platform == "win32": import winreg for base in (winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER): try: key = winreg.OpenKey(base, r"SOFTWARE\Mozilla\Mozilla Thunderbird") inst, _ = winreg.QueryValueEx(key, "Install Directory") candidates.append(str(Path(inst) / "thunderbird.exe")) except Exception: pass candidates += [ r"C:\Program Files\Mozilla Thunderbird\thunderbird.exe", r"C:\Program Files (x86)\Mozilla Thunderbird\thunderbird.exe", ] elif sys.platform == "darwin": candidates = [ "/Applications/Thunderbird.app/Contents/MacOS/thunderbird", ] else: candidates = [shutil.which("thunderbird") or "", "/usr/bin/thunderbird", "/usr/local/bin/thunderbird", "/snap/bin/thunderbird"] tb = next((c for c in candidates if c and Path(c).exists()), None) if not tb: return False file_uri = Path(path).as_uri() subject = f"Linedance sang: {title} — {artist}" compose = ( f"subject='{subject}'," f"attachment='{file_uri}'" ) subprocess.Popen([tb, "-compose", compose]) return True def try_outlook() -> bool: """Outlook: outlook.exe /a 'filsti' (kun Windows)""" if sys.platform != "win32": return False candidates = [ shutil.which("outlook") or "", r"C:\Program Files\Microsoft Office\root\Office16\OUTLOOK.EXE", r"C:\Program Files (x86)\Microsoft Office\root\Office16\OUTLOOK.EXE", r"C:\Program Files\Microsoft Office\Office16\OUTLOOK.EXE", ] ol = next((c for c in candidates if c and Path(c).exists()), None) if not ol: return False subprocess.Popen([ol, "/a", path]) return True def fallback_mailto(): """Ingen vedhæftning — åbn standard-mailprogram via mailto:""" subject = urllib.parse.quote(f"Linedance sang: {title} — {artist}") body = urllib.parse.quote( f"Sang: {title}\nArtist: {artist}\nFil: {path}\n\n" f"(Vedhæft filen manuelt fra ovenstående sti)" ) mailto = f"mailto:?subject={subject}&body={body}" if sys.platform == "win32": import os; os.startfile(mailto) elif sys.platform == "darwin": subprocess.Popen(["open", mailto]) else: subprocess.Popen(["xdg-open", mailto]) # ── Prøv i rækkefølge ───────────────────────────────────────────────── if try_thunderbird(): self._set_status(f"Thunderbird åbnet med {Path(path).name} vedh.", 4000) elif try_outlook(): self._set_status(f"Outlook åbnet med {Path(path).name} vedh.", 4000) else: fallback_mailto() self._set_status( f"Ingen kendt mailklient fundet — åbnet mailto: (uden vedhæftning)", 5000 ) def _on_event_started(self): """Start event — indlæs første sang i afspilleren klar til afspilning.""" first = self._playlist_panel.get_song(0) if not first: return self._stop() self._current_idx = 0 self._song_ended = False self._load_song(first) self._set_status("Event klar — tryk ▶ for at starte", 5000) def _on_song_dropped(self, song: dict): self._set_status(f"Tilføjet: {song.get('title','')}", 2000) def _menu_add_folder(self): folder = QFileDialog.getExistingDirectory(self, "Vælg musikmappe") if folder: self.add_library_path(folder) # ── Afspilning ──────────────────────────────────────────────────────────── def _load_song(self, song: dict): self._current_song = song self._song_ended = False self._demo_active = False self._btn_demo.setChecked(False) dur = song.get("duration_sec", 0) self._player.load(song.get("local_path", ""), dur) self._lbl_title.setText(song.get("title", "—")) bpm = song.get("bpm", 0) fmt_dur = f"{dur//60}:{dur%60:02d}" self._lbl_meta.setText(f"{song.get('artist','')} · {bpm} BPM · {fmt_dur}") dances = song.get("dances", []) self._lbl_dances.setText( " · ".join(f"[{d}]" for d in dances) if dances else "ingen danse tagget" ) if dur > 0: self._progress.set_demo_marker(min(self._demo_seconds / dur, 1.0)) self._set_status(f"Indlæst: {song.get('title','—')}", 3000) def _load_song_by_idx(self, idx: int): song = self._playlist_panel.get_song(idx) if not song: return self._current_idx = idx self._load_song(song) self._playlist_panel.set_current(idx) def _toggle_play(self): if self._demo_active: self._player.stop() self._demo_active = False self._btn_demo.setChecked(False) self._btn_play.setText("▶") return if self._player.is_playing(): self._player.pause() else: self._song_ended = False self._player.play() self._btn_play.setText("⏸") def _stop(self): self._player.stop() self._song_ended = False self._demo_active = False self._btn_demo.setChecked(False) self._btn_play.setText("▶") self._vu.reset() def _toggle_demo(self): if self._demo_active: self._player.stop() self._demo_active = False self._btn_demo.setChecked(False) self._btn_play.setText("▶") else: self._demo_active = True self._btn_demo.setChecked(True) self._player.play_demo(stop_at_sec=self._demo_seconds) self._btn_play.setText("⏸") def _prev_song(self): if self._current_idx > 0: self._stop() self._load_song_by_idx(self._current_idx - 1) def _next_song(self): if self._current_idx < self._playlist_panel.count() - 1: self._stop() self._playlist_panel.mark_played(self._current_idx) self._load_song_by_idx(self._current_idx + 1) def _play_next(self): self._song_ended = False self._player.play() self._btn_play.setText("⏸") def _on_library_song_selected(self, song: dict): self._load_song(song) self._player.play() self._btn_play.setText("⏸") def _add_song_to_playlist(self, song: dict): songs = [self._playlist_panel.get_song(i) for i in range(self._playlist_panel.count())] songs = [s for s in songs if s] songs.append(song) self._playlist_panel.load_songs(songs) self._set_status(f"Tilføjet til danseliste: {song.get('title','')}", 2000) # ── Player signals ──────────────────────────────────────────────────────── def _on_position(self, fraction: float): self._progress.set_fraction(fraction) def _on_time(self, cur: int, tot: int): self._lbl_cur.setText(f"{cur//60}:{cur%60:02d}") self._lbl_tot.setText(f"{tot//60}:{tot%60:02d}") def _on_levels(self, left: float, right: float): self._vu.set_levels(left, right) def _on_song_ended(self): self._song_ended = True self._demo_active = False self._btn_demo.setChecked(False) self._btn_play.setText("▶") self._vu.reset() # Markér den afspillede sang prev_idx = self._current_idx self._playlist_panel.mark_played(prev_idx) # Synkroniser event-status til den gemte navngivne liste self._sync_event_status_to_playlist() # Find næste afspilbare sang — fra 0 hvis ingen sang var i gang search_from = max(0, prev_idx + 1) ni = self._playlist_panel.next_playable_idx(search_from) next_song = self._playlist_panel.get_song(ni) if ni is not None else None if next_song: self._current_idx = ni self._playlist_panel.set_next_ready(ni) self._load_song(next_song) self._set_status(f"Klar: {next_song.get('title','')} — tryk ▶ for at starte") else: self._lbl_title.setText("— Danseliste afsluttet —") self._lbl_meta.setText("") self._lbl_dances.setText("") self._set_status("Danselisten er afsluttet") def _sync_event_status_to_playlist(self): """Gem event-fremgang i den aktive navngivne liste.""" try: from local.local_db import get_db songs = self._playlist_panel.get_songs() statuses = self._playlist_panel.get_statuses() with get_db() as conn: # Find den aktive liste (ikke __aktiv__) pl = conn.execute( "SELECT id FROM playlists WHERE name != '__aktiv__' " "ORDER BY created_at DESC LIMIT 1" ).fetchone() if not pl: return # Opdater status for hver sang i listen for i, (song, status) in enumerate(zip(songs, statuses)): conn.execute(""" UPDATE playlist_songs SET status=? WHERE playlist_id=? AND song_id=? """, (status, pl["id"], song.get("id"))) except Exception as e: print(f"Event-status sync fejl: {e}") def _on_state_changed(self, state: str): if state == "playing": self._btn_play.setText("⏸") elif state in ("paused", "stopped"): self._btn_play.setText("▶") if state == "stopped" and not self._song_ended: self._vu.reset() elif state == "demo_ended": self._demo_active = False self._btn_demo.setChecked(False) self._btn_play.setText("▶") self._vu.reset() def _on_seek(self, fraction: float): self._player.set_position(fraction) def _on_volume(self, value: int): self._lbl_vol.setText(str(value)) self._player.set_volume(value) # ── Tema ────────────────────────────────────────────────────────────────── def _toggle_theme(self): self._dark_theme = not self._dark_theme apply_theme(self._app_ref(), dark=self._dark_theme) self._theme_btn.setText( "● MØRKT TEMA" if not self._dark_theme else "☀ LYS TEMA" ) self._vu.set_dark(self._dark_theme) # ── Luk ─────────────────────────────────────────────────────────────────── def closeEvent(self, event): self._save_window_state() self._player.stop() if self._scan_worker and self._scan_worker.isRunning(): self._scan_worker.quit() self._scan_worker.wait(2000) try: if self._watcher: self._watcher.stop() except Exception: pass event.accept()