279 lines
10 KiB
Python
279 lines
10 KiB
Python
"""
|
|
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)
|