""" playlist_panel.py — Danseliste med event-overblik, drag-and-drop og højreklik. """ from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QListWidget, QListWidgetItem, QLabel, QHBoxLayout, QPushButton, QMenu, QAbstractItemView, QMessageBox, ) from PyQt6.QtCore import Qt, pyqtSignal, QMimeData from PyQt6.QtGui import QColor, QFont, QDragEnterEvent, QDropEvent class PlaylistPanel(QWidget): song_selected = pyqtSignal(int) # dobbeltklik → indlæs sang status_changed = pyqtSignal(int, str) # (indeks, ny_status) song_dropped = pyqtSignal(dict) # sang droppet fra bibliotek 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._build_ui() self.setAcceptDrops(True) def _build_ui(self): layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) # Header header = QHBoxLayout() header.setContentsMargins(10, 6, 10, 6) self._title_label = QLabel("DANSELISTE") self._title_label.setObjectName("section_title") header.addWidget(self._title_label) header.addStretch() layout.addLayout(header) # Event-kontrol-linje ctrl = QHBoxLayout() ctrl.setContentsMargins(8, 4, 8, 4) ctrl.setSpacing(6) self._btn_start = QPushButton("▶ START EVENT") self._btn_start.setObjectName("btn_start_event") self._btn_start.setFixedHeight(28) self._btn_start.setToolTip("Nulstil alle statusser og start eventet fra top") 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) layout.addLayout(ctrl) # Kolonneheader col_header = QHBoxLayout() col_header.setContentsMargins(10, 2, 10, 2) for text, stretch in [("#", 0), ("Titel / Dans", 1), ("Status", 0)]: lbl = QLabel(text) lbl.setObjectName("result_count") if stretch: col_header.addWidget(lbl, stretch=1) else: lbl.setFixedWidth(30 if text == "#" else 50) col_header.addWidget(lbl) layout.addLayout(col_header) # Liste self._list = QListWidget() self._list.setObjectName("playlist_list") self._list.setDragDropMode(QAbstractItemView.DragDropMode.DropOnly) 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) 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 data = mime.data("application/x-linedance-song").data() song = json.loads(data.decode("utf-8")) 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() # ── Data ────────────────────────────────────────────────────────────────── def load_songs(self, songs: list[dict], reset_statuses: bool = True): self._songs = list(songs) if reset_statuses: self._statuses = ["pending"] * len(songs) self._current_idx = -1 self._song_ended = False self._refresh() 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() 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) # ── Event-styring ───────────────────────────────────────────────────────── 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 = False self._refresh() # ── Højreklik-menu ──────────────────────────────────────────────────────── 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 menu = QMenu(self) menu.setStyleSheet("QMenu { padding: 4px; } QMenu::item { padding: 6px 20px; }") 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() 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() elif action == act_unplay: self._statuses[idx] = "pending" self.status_changed.emit(idx, "pending") self._refresh() elif action == act_played: self._statuses[idx] = "played" self.status_changed.emit(idx, "played") self._refresh() 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() # ── Render ──────────────────────────────────────────────────────────────── def _refresh(self): self._list.clear() played_count = sum(1 for s in self._statuses if s == "played") self._lbl_progress.setText(f"{played_count} / {len(self._songs)} afspillet") for i, song in enumerate(self._songs): is_current = (i == self._current_idx and not self._song_ended) is_next = (self._song_ended and i == self._current_idx + 1) if is_current: status = "playing" elif is_next: status = "next" else: status = self._statuses[i] icon = self.STATUS_ICON.get(status, " ") color = self.STATUS_COLOR.get(status, "#5a6070") dances = " / ".join(song.get("dances", [])) or "ingen dans tagget" text = f"{i+1:>2}. {song.get('title','—')}\n {song.get('artist','')} · {dances}" item = QListWidgetItem(f"{icon} {text}") item.setData(Qt.ItemDataRole.UserRole, i) # Farver if status == "playing": item.setForeground(QColor("#e8a020")) font = item.font() font.setBold(True) item.setFont(font) elif status == "next": item.setForeground(QColor("#3b8fd4")) font = item.font() font.setBold(True) item.setFont(font) elif status == "played": item.setForeground(QColor("#2ecc71")) elif status == "skipped": item.setForeground(QColor("#e74c3c")) else: item.setForeground(QColor("#9aa0b0")) self._list.addItem(item) def set_playlist_name(self, name: str): self._title_label.setText(f"DANSELISTE — {name.upper()}") 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)