Files
LinedanceAfspiller/linedance-app/ui/playlist_panel.py
2026-04-12 10:25:41 +02:00

757 lines
32 KiB
Python
Raw 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.
"""
playlist_panel.py — Danseliste med Ny/Gem/Hent knapper, autogem og event-overblik.
"""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QListWidget, QListWidgetItem,
QLabel, QHBoxLayout, QPushButton, QMenu, QAbstractItemView,
QMessageBox,
)
from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QByteArray
from PyQt6.QtGui import QColor, QDragEnterEvent, QDropEvent
ACTIVE_PLAYLIST_NAME = "__aktiv__" # fast navn til autogem-listen
class PlaylistPanel(QWidget):
song_selected = pyqtSignal(int)
status_changed = pyqtSignal(int, str)
song_dropped = pyqtSignal(dict)
playlist_changed = pyqtSignal()
event_started = pyqtSignal()
next_song_ready = pyqtSignal(dict) # udsendes når næste sang ændres — main_window indlæser den # udsendes af Start event — main_window indlæser første sang # udsendes ved enhver ændring → trigger autogem
STATUS_ICON = {"pending": " ", "playing": "", "played": "", "skipped": "", "next": ""}
STATUS_COLOR = {"pending": "#5a6070", "playing": "#e8a020", "played": "#2ecc71", "skipped": "#e74c3c", "next": "#3b8fd4"}
def __init__(self, parent=None):
super().__init__(parent)
self._songs: list[dict] = []
self._statuses: list[str] = []
self._current_idx = -1
self._song_ended = False
self._active_playlist_id: int | None = None
self._named_playlist_id: int | None = None # den indlæste/gemte navngivne liste
self._build_ui()
self.setAcceptDrops(True)
# Autogem-timer — venter 800ms efter sidst ændring
self._autosave_timer = QTimer(self)
self._autosave_timer.setSingleShot(True)
self._autosave_timer.setInterval(800)
self._autosave_timer.timeout.connect(self._autosave)
# Event-state gem — hurtig, kritisk for genopstart efter strømsvigt
self._event_state_timer = QTimer(self)
self._event_state_timer.setSingleShot(True)
self._event_state_timer.setInterval(300)
self._event_state_timer.timeout.connect(self._save_event_state)
def _build_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# ── Header med titel ──────────────────────────────────────────────────
header = QHBoxLayout()
header.setContentsMargins(10, 6, 10, 6)
self._title_label = QLabel("DANSELISTE")
self._title_label.setObjectName("section_title")
header.addWidget(self._title_label)
layout.addLayout(header)
# ── Ny / Gem / Hent knapper ───────────────────────────────────────────
toolbar = QHBoxLayout()
toolbar.setContentsMargins(8, 2, 8, 4)
toolbar.setSpacing(4)
btn_new = QPushButton("✚ Ny")
btn_new.setFixedHeight(26)
btn_new.setToolTip("Opret en ny tom danseliste")
btn_new.clicked.connect(self._new_playlist)
toolbar.addWidget(btn_new)
btn_save = QPushButton("💾 Gem som...")
btn_save.setFixedHeight(26)
btn_save.setToolTip("Gem aktuel liste med et navn")
btn_save.clicked.connect(self._save_as)
toolbar.addWidget(btn_save)
self._btn_save_current = QPushButton("💾 Gem")
self._btn_save_current.setFixedHeight(26)
self._btn_save_current.setToolTip("Gem ændringer til den indlæste liste")
self._btn_save_current.clicked.connect(self._save_current)
self._btn_save_current.setEnabled(False)
toolbar.addWidget(self._btn_save_current)
btn_load = QPushButton("📂 Hent...")
btn_load.setFixedHeight(26)
btn_load.setToolTip("Hent en tidligere gemt danseliste")
btn_load.clicked.connect(self._load_dialog)
toolbar.addWidget(btn_load)
toolbar.addStretch()
self._lbl_autosave = QLabel("")
self._lbl_autosave.setObjectName("result_count")
toolbar.addWidget(self._lbl_autosave)
layout.addLayout(toolbar)
# ── Event-kontrol ─────────────────────────────────────────────────────
ctrl = QHBoxLayout()
ctrl.setContentsMargins(8, 2, 8, 4)
ctrl.setSpacing(6)
self._btn_start = QPushButton("▶ START EVENT")
self._btn_start.setFixedHeight(28)
self._btn_start.setToolTip("Nulstil alle statusser og gør klar til event")
self._btn_start.clicked.connect(self._start_event)
ctrl.addWidget(self._btn_start)
ctrl.addStretch()
self._lbl_progress = QLabel("0 / 0")
self._lbl_progress.setObjectName("result_count")
ctrl.addWidget(self._lbl_progress)
btn_info = QPushButton("")
btn_info.setFixedSize(24, 28)
btn_info.setToolTip("Danseliste-info: samlet tid, pause-tid m.m.")
btn_info.clicked.connect(self._show_playlist_info)
ctrl.addWidget(btn_info)
layout.addLayout(ctrl)
# Pause-tid per dans (skjult men kan vises via info)
try:
from ui.settings_dialog import load_settings
s = load_settings()
self._pause_seconds = s.get("between_seconds", 60)
self._workshop_seconds = s.get("workshop_minutes", 10) * 60
except Exception:
self._pause_seconds = 60
self._workshop_seconds = 600
# ── Liste ─────────────────────────────────────────────────────────────
self._list = QListWidget()
self._list.setObjectName("playlist_list")
self._list.setDragDropMode(QAbstractItemView.DragDropMode.DragDrop)
self._list.setDefaultDropAction(Qt.DropAction.MoveAction)
self._list.setAcceptDrops(True)
self._list.itemDoubleClicked.connect(self._on_double_click)
self._list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self._list.customContextMenuRequested.connect(self._show_context_menu)
self._list.model().rowsMoved.connect(self._on_rows_moved)
layout.addWidget(self._list)
# ── Drag & drop ───────────────────────────────────────────────────────────
def dragEnterEvent(self, event: QDragEnterEvent):
if event.mimeData().hasFormat("application/x-linedance-song"):
event.acceptProposedAction()
else:
event.ignore()
def dropEvent(self, event: QDropEvent):
mime = event.mimeData()
if mime.hasFormat("application/x-linedance-song"):
import json
song = json.loads(mime.data("application/x-linedance-song").data().decode())
self._append_song(song)
self.song_dropped.emit(song)
event.acceptProposedAction()
def _append_song(self, song: dict):
self._songs.append(song)
self._statuses.append("pending")
self._refresh()
self._trigger_autosave()
# ── Data API ──────────────────────────────────────────────────────────────
def load_songs(self, songs: list[dict], reset_statuses: bool = True, name: str = ""):
self._songs = list(songs)
if reset_statuses:
self._statuses = ["pending"] * len(songs)
self._current_idx = -1
self._song_ended = False
if name:
self._title_label.setText(f"DANSELISTE — {name.upper()}")
self._refresh()
self._trigger_autosave()
def set_current(self, idx: int, song_ended: bool = False):
self._current_idx = idx
self._song_ended = song_ended
if 0 <= idx < len(self._statuses) and not song_ended:
self._statuses[idx] = "playing"
self._refresh()
self._scroll_to(idx)
def mark_played(self, idx: int):
if 0 <= idx < len(self._statuses):
self._statuses[idx] = "played"
self._refresh()
self._trigger_autosave()
self._trigger_event_state_save()
def set_next_ready(self, idx: int):
"""Sæt næste sang klar — uden at overskrive skipped/played statusser."""
self._current_idx = idx
self._song_ended = False
# Ændr KUN status hvis den er pending — rør ikke skipped/played
if 0 <= idx < len(self._statuses):
if self._statuses[idx] not in ("skipped", "played"):
self._statuses[idx] = "pending"
self._refresh()
self._scroll_to(idx)
def get_song(self, idx: int) -> dict | None:
return self._songs[idx] if 0 <= idx < len(self._songs) else None
def get_songs(self) -> list[dict]:
return list(self._songs)
def get_statuses(self) -> list[str]:
return list(self._statuses)
def count(self) -> int:
return len(self._songs)
def set_playlist_name(self, name: str):
self._title_label.setText(f"DANSELISTE — {name.upper()}")
# ── Drag-flytning ─────────────────────────────────────────────────────────
def _on_rows_moved(self, parent, start, end, dest, dest_row):
"""Opdater _songs og _statuses når en sang flyttes via drag."""
# Husk hvilken sang der er aktiv
current_song_id = None
if 0 <= self._current_idx < len(self._songs):
current_song_id = self._songs[self._current_idx].get("id")
new_songs = []
new_statuses = []
for i in range(self._list.count()):
old_idx = self._list.item(i).data(Qt.ItemDataRole.UserRole)
if old_idx is not None and 0 <= old_idx < len(self._songs):
new_songs.append(self._songs[old_idx])
new_statuses.append(self._statuses[old_idx])
self._songs = new_songs
self._statuses = new_statuses
# Gendan current_idx til den sang der stadig spiller
if current_song_id:
for i, s in enumerate(self._songs):
if s.get("id") == current_song_id:
self._current_idx = i
break
else:
self._current_idx = -1
self._song_ended = False
self._refresh()
self._trigger_autosave()
# Emit IKKE next_song_ready — afspilning fortsætter uforstyrret
# ── Event-state ───────────────────────────────────────────────────────────
def _save_event_state(self):
"""Gem current_idx og statuses — overlever strømsvigt."""
try:
from local.local_db import save_event_state
save_event_state(self._current_idx, self._statuses)
except Exception as e:
pass
def _trigger_event_state_save(self):
self._event_state_timer.start()
def restore_event_state(self) -> bool:
"""Gendan gemt event-fremgang. Returnerer True hvis gendannet."""
try:
from local.local_db import load_event_state
result = load_event_state()
if not result:
return False
idx, statuses = result
if len(statuses) != len(self._songs):
return False # listen er ændret siden sidst
self._statuses = statuses
self._current_idx = idx
self._song_ended = False
self._refresh()
return True
except Exception as e:
pass
return False
def get_named_playlist_id(self) -> int | None:
return self._named_playlist_id
def next_playable_idx(self) -> int | None:
"""Find første sang fra toppen der ikke er 'skipped' eller 'played'."""
for i in range(len(self._songs)):
if self._statuses[i] not in ("skipped", "played"):
return i
return None
# ── Autogem ───────────────────────────────────────────────────────────────
def _trigger_autosave(self):
"""Start/nulstil debounce-timer — gemmer 800ms efter sidst ændring."""
self._autosave_timer.start()
self._lbl_autosave.setText("● ikke gemt")
def _autosave(self):
"""Gem til den faste 'Aktiv liste' i SQLite."""
try:
from local.local_db import get_db, create_playlist, add_song_to_playlist
with get_db() as conn:
# Slet den gamle aktive liste
conn.execute(
"DELETE FROM playlists WHERE name=?", (ACTIVE_PLAYLIST_NAME,)
)
# Opret ny
pl_id = create_playlist(ACTIVE_PLAYLIST_NAME)
self._active_playlist_id = pl_id
for i, song in enumerate(self._songs, start=1):
if song.get("id"):
add_song_to_playlist(pl_id, song["id"], position=i)
self._lbl_autosave.setText("✓ gemt")
self.playlist_changed.emit()
except Exception as e:
self._lbl_autosave.setText(f"⚠ gemfejl")
pass
def _save_named_playlist_id(self, pl_id: int | None):
"""Gem hvilken navngiven liste der er aktiv — til brug ved næste opstart."""
from PyQt6.QtCore import QSettings
s = QSettings("LineDance", "Player")
if pl_id:
s.setValue("session/named_playlist_id", pl_id)
else:
s.remove("session/named_playlist_id")
def restore_active_playlist(self):
"""Gendan senest aktive navngivne liste med event-fremgang ved opstart."""
try:
from PyQt6.QtCore import QSettings
s = QSettings("LineDance", "Player")
pl_id = s.value("session/named_playlist_id", None, type=int)
if not pl_id:
return False
import sqlite3
from local.local_db import DB_PATH
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
# Verificer at listen stadig eksisterer
pl = conn.execute(
"SELECT id, name FROM playlists WHERE id=?", (pl_id,)
).fetchone()
if not pl:
conn.close()
return False
# Hent sange med status, workshop og dans-override
songs_raw = conn.execute("""
SELECT s.*, ps.position, ps.status,
ps.is_workshop, ps.dance_override
FROM playlist_songs ps
JOIN songs s ON s.id = ps.song_id
WHERE ps.playlist_id=? ORDER BY ps.position
""", (pl_id,)).fetchall()
songs = []
statuses = []
for row in songs_raw:
dances = conn.execute("""
SELECT d.name FROM song_dances sd
JOIN dances d ON d.id = sd.dance_id
WHERE sd.song_id=? ORDER BY sd.dance_order
""", (row["id"],)).fetchall()
dance_names = [d["name"] for d in dances]
override = row["dance_override"] or ""
active_dance = override if override else (dance_names[0] if dance_names else "")
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": dance_names,
"active_dance": active_dance,
"is_workshop": bool(row["is_workshop"]),
})
statuses.append(row["status"] or "pending")
conn.close()
if songs:
self._songs = songs
self._statuses = statuses
self._named_playlist_id = pl_id
self._current_idx = -1
self._song_ended = False
self._btn_save_current.setEnabled(True)
self._btn_save_current.setToolTip(f"Gem ændringer til '{pl['name']}'")
self._title_label.setText(f"DANSELISTE — {pl['name'].upper()}")
self._lbl_autosave.setText("✓ gendannet")
self._refresh()
# Find næste uafspillede og sæt den klar
ni = self.next_playable_idx()
if ni is not None:
self._current_idx = ni
self._refresh()
self.next_song_ready.emit(self._songs[ni])
return True
except Exception:
pass
return False
# ── Ny / Gem som / Hent ───────────────────────────────────────────────────
def _new_playlist(self):
if self._songs:
reply = QMessageBox.question(
self, "Ny danseliste",
"Ryd den aktuelle liste og start forfra?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply != QMessageBox.StandardButton.Yes:
return
self._songs = []
self._statuses = []
self._current_idx = -1
self._song_ended = False
self._named_playlist_id = None
self._btn_save_current.setEnabled(False)
self._btn_save_current.setToolTip("Gem ændringer til den indlæste liste")
self._save_named_playlist_id(None)
self._title_label.setText("DANSELISTE — NY")
self._refresh()
self._trigger_autosave()
def _save_as(self):
if not self._songs:
QMessageBox.information(self, "Gem", "Danselisten er tom.")
return
from ui.playlist_browser import PlaylistBrowserDialog
current_name = self._title_label.text().replace("DANSELISTE — ", "").replace("DANSELISTE", "").strip()
dialog = PlaylistBrowserDialog(
mode="save",
current_songs=self._songs,
current_name=current_name,
parent=self.window()
)
def on_saved(pl_id, name):
self._named_playlist_id = pl_id
self._title_label.setText(f"DANSELISTE — {name.upper()}")
self._lbl_autosave.setText(f"✓ gemt som \"{name}\"")
self._btn_save_current.setEnabled(True)
self._btn_save_current.setToolTip(f"Gem ændringer til '{name}'")
self._save_named_playlist_id(pl_id)
dialog.playlist_selected.connect(on_saved)
dialog.exec()
def _save_current(self):
"""Gem ændringer tilbage til den aktuelt indlæste navngivne liste."""
if not self._named_playlist_id:
return
if not self._songs:
QMessageBox.information(self, "Gem", "Danselisten er tom.")
return
try:
from local.local_db import get_db
with get_db() as conn:
conn.execute(
"DELETE FROM playlist_songs WHERE playlist_id=?",
(self._named_playlist_id,)
)
for i, song in enumerate(self._songs, start=1):
if song.get("id"):
status = self._statuses[i-1] if i-1 < len(self._statuses) else "pending"
conn.execute(
"INSERT INTO playlist_songs "
"(playlist_id, song_id, position, status) VALUES (?,?,?,?)",
(self._named_playlist_id, song["id"], i, status)
)
self._lbl_autosave.setText("✓ gemt")
except Exception as e:
QMessageBox.warning(self, "Fejl", f"Kunne ikke gemme: {e}")
def _load_dialog(self):
from ui.playlist_browser import PlaylistBrowserDialog
dialog = PlaylistBrowserDialog(mode="load", parent=self.window())
dialog.playlist_selected.connect(self._load_playlist_by_id)
dialog.exec()
def _load_playlist_by_id(self, pl_id: int, pl_name: str):
try:
from local.local_db import get_db
with get_db() as conn:
songs_raw = conn.execute("""
SELECT s.*, ps.position, ps.status,
ps.is_workshop, ps.dance_override
FROM playlist_songs ps
JOIN songs s ON s.id = ps.song_id
WHERE ps.playlist_id=? ORDER BY ps.position
""", (pl_id,)).fetchall()
songs = []
statuses = []
for row in songs_raw:
dances = conn.execute("""
SELECT d.name FROM song_dances sd
JOIN dances d ON d.id = sd.dance_id
WHERE sd.song_id=? ORDER BY sd.dance_order
""", (row["id"],)).fetchall()
dance_names = [d["name"] for d in dances]
# dance_override bestemmer hvilken dans der vises
override = row["dance_override"] or ""
active_dance = override if override else (dance_names[0] if dance_names else "")
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": dance_names,
"active_dance": active_dance,
"is_workshop": bool(row["is_workshop"]),
})
statuses.append(row["status"] or "pending")
self._songs = songs
self._statuses = statuses
self._current_idx = -1
self._song_ended = False
self._named_playlist_id = pl_id
self._title_label.setText(f"DANSELISTE — {pl_name.upper()}")
self._lbl_autosave.setText("✓ gendannet")
self._btn_save_current.setEnabled(True)
self._btn_save_current.setToolTip(f"Gem ændringer til '{pl_name}'")
self._save_named_playlist_id(pl_id)
self._refresh()
self._trigger_autosave()
except Exception as e:
QMessageBox.warning(self, "Fejl", f"Kunne ikke indlæse listen: {e}")
# ── Start event ───────────────────────────────────────────────────────────
def _show_playlist_info(self):
"""Åbn/luk det flydende danseliste-info vindue."""
try:
if hasattr(self, "_info_window") and self._info_window \
and self._info_window.isVisible():
self._info_window.close()
self._info_window = None
return
except RuntimeError:
self._info_window = None
# Opdater defaults fra indstillinger ved åbning
try:
from ui.settings_dialog import load_settings
s = load_settings()
self._pause_seconds = s.get("between_seconds", 60)
self._workshop_seconds = s.get("workshop_minutes", 10) * 60
except Exception:
pass
from ui.playlist_info_dialog import PlaylistInfoWindow
from PyQt6.QtWidgets import QApplication
main = self.window()
self._info_window = PlaylistInfoWindow(self, parent=main)
QApplication.instance().aboutToQuit.connect(self._info_window.close)
if main:
geo = main.geometry()
self._info_window.move(geo.right() + 10, geo.top() + 100)
self._info_window.show()
def _change_dance(self, idx: int, song: dict):
"""Lad brugeren vælge/skrive hvilken dans der vises for dette nummer."""
from ui.dance_picker_dialog import DancePickerDialog
current = song.get("active_dance", "")
if not current:
dances = song.get("dances", [])
current = dances[0] if dances else ""
dlg = DancePickerDialog(
current_dance=current,
song_title=song.get("title", ""),
parent=self.window()
)
if dlg.exec():
chosen = dlg.get_dance()
if chosen:
song["active_dance"] = chosen
self._refresh()
self._sync_dance_to_db(idx, song)
def _sync_dance_to_db(self, idx: int, song: dict):
"""Gem dance_override til playlist_songs."""
if not self._named_playlist_id:
return
try:
from local.local_db import get_db
with get_db() as conn:
conn.execute(
"UPDATE playlist_songs SET dance_override=? "
"WHERE playlist_id=? AND position=?",
(song.get("active_dance", ""), self._named_playlist_id, idx + 1)
)
except Exception:
pass
def _sync_ws_to_db(self, idx: int, song: dict):
"""Gem is_workshop til playlist_songs — både navngiven og aktiv liste."""
pl_ids = []
if self._named_playlist_id:
pl_ids.append(self._named_playlist_id)
if self._active_playlist_id:
pl_ids.append(self._active_playlist_id)
if not pl_ids:
return
try:
from local.local_db import get_db
with get_db() as conn:
for pl_id in pl_ids:
conn.execute(
"UPDATE playlist_songs SET is_workshop=? "
"WHERE playlist_id=? AND position=?",
(1 if song.get("is_workshop") else 0, pl_id, idx + 1)
)
except Exception:
pass
def _on_pause_changed(self, seconds: int):
self._pause_seconds = seconds
def _start_event(self):
if not self._songs:
return
reply = QMessageBox.question(
self, "Start event",
"Dette nulstiller alle statusser i danselisten.\nFortsæt?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply == QMessageBox.StandardButton.Yes:
self._statuses = ["pending"] * len(self._songs)
self._current_idx = -1
self._song_ended = True
try:
from local.local_db import clear_event_state
clear_event_state()
except Exception:
pass
self._refresh()
self._scroll_to(0)
self.event_started.emit()
# ── Højreklik ─────────────────────────────────────────────────────────────
def _show_context_menu(self, pos):
item = self._list.itemAt(pos)
if not item:
return
idx = item.data(Qt.ItemDataRole.UserRole)
if idx is None:
return
song = self._songs[idx] if 0 <= idx < len(self._songs) else None
menu = QMenu(self)
act_play = menu.addAction("▶ Afspil denne")
menu.addSeparator()
act_skip = menu.addAction("— Spring over")
act_unplay = menu.addAction("↺ Sæt til ikke afspillet")
act_played = menu.addAction("✓ Sæt til afspillet")
menu.addSeparator()
# Dans-valg
act_dance = menu.addAction("💃 Vælg dans...")
# Workshop toggle
is_ws = song.get("is_workshop", False) if song else False
act_ws = menu.addAction("🎓 Fjern workshop" if is_ws else "🎓 Markér som workshop")
menu.addSeparator()
act_dance_info = menu.addAction(" Dans-info...")
menu.addSeparator()
act_remove = menu.addAction("✕ Fjern fra liste")
action = menu.exec(self._list.mapToGlobal(pos))
if action == act_play:
self.song_selected.emit(idx)
elif action == act_skip:
self._statuses[idx] = "skipped"
self.status_changed.emit(idx, "skipped")
self._refresh(); self._trigger_autosave(); self._trigger_event_state_save()
elif action == act_unplay:
self._statuses[idx] = "pending"
self.status_changed.emit(idx, "pending")
self._refresh(); self._trigger_autosave(); self._trigger_event_state_save()
elif action == act_played:
self._statuses[idx] = "played"
self.status_changed.emit(idx, "played")
self._refresh(); self._trigger_autosave(); self._trigger_event_state_save()
elif action == act_dance and song:
self._change_dance(idx, song)
elif action == act_ws and song:
song["is_workshop"] = not song.get("is_workshop", False)
self._sync_ws_to_db(idx, song)
self._refresh()
self.playlist_changed.emit()
elif action == act_dance_info:
if song:
from ui.dance_info_dialog import DanceInfoDialog
dlg = DanceInfoDialog(song, parent=self.window())
dlg.exec()
elif action == act_remove:
self._songs.pop(idx)
self._statuses.pop(idx)
if self._current_idx >= idx:
self._current_idx = max(-1, self._current_idx - 1)
self._refresh(); self._trigger_autosave()
# ── Render ────────────────────────────────────────────────────────────────
def _refresh(self):
self._list.clear()
played = sum(1 for s in self._statuses if s == "played")
self._lbl_progress.setText(f"{played} / {len(self._songs)} afspillet")
for i, song in enumerate(self._songs):
is_current = (i == self._current_idx and not self._song_ended)
status = "playing" if is_current else self._statuses[i]
icon = self.STATUS_ICON.get(status, " ")
# Vis active_dance (override eller første dans) eller alle danse
active = song.get("active_dance", "")
if not active:
dances = song.get("dances", [])
active = dances[0] if dances else "ingen dans tagget"
ws_tag = " 🎓" if song.get("is_workshop") else ""
text = (f"{i+1:>2}. {song.get('title','')}{ws_tag}\n"
f" {song.get('artist','')} · {active}")
item = QListWidgetItem(f"{icon} {text}")
item.setData(Qt.ItemDataRole.UserRole, i)
if status == "playing":
item.setForeground(QColor(self.STATUS_COLOR["playing"]))
f = item.font(); f.setBold(True); item.setFont(f)
elif status == "played":
item.setForeground(QColor("#2ecc71"))
elif status == "skipped":
item.setForeground(QColor("#e74c3c"))
else:
item.setForeground(QColor("#9aa0b0"))
self._list.addItem(item)
def _scroll_to(self, idx: int):
if 0 <= idx < self._list.count():
self._list.scrollToItem(
self._list.item(idx), QListWidget.ScrollHint.PositionAtCenter)
def _on_double_click(self, item: QListWidgetItem):
idx = item.data(Qt.ItemDataRole.UserRole)
if idx is not None:
self.song_selected.emit(idx)