This commit is contained in:
2026-04-09 21:54:18 +02:00
commit ad33255b88
8906 changed files with 1437726 additions and 0 deletions

View File

@@ -0,0 +1,278 @@
"""
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)