I gang
This commit is contained in:
278
ui/playlist_panel.py
Normal file
278
ui/playlist_panel.py
Normal 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)
|
||||
Reference in New Issue
Block a user