Næste version

This commit is contained in:
2026-04-12 10:25:41 +02:00
parent b678787236
commit 57f3c913b4
18 changed files with 2690 additions and 458 deletions

View File

@@ -4,15 +4,47 @@ library_panel.py — Musikbibliotek med søgning og drag-and-drop til danseliste
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QListWidget, QListWidgetItem,
QLineEdit, QLabel, QHBoxLayout, QPushButton, QProgressBar,
QAbstractItemView,
QLineEdit, QLabel, QHBoxLayout, QPushButton,
QAbstractItemView, QStyledItemDelegate,
)
from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QMimeData, QByteArray
from PyQt6.QtGui import QColor, QDrag
from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QMimeData, QByteArray, QRect
from PyQt6.QtGui import QColor, QDrag, QPainter, QBrush, QPen, QFont
class DanseButtonDelegate(QStyledItemDelegate):
"""Tegner en orange 'Danse' label i højre side af hvert list-item."""
BTN_W = 54
BTN_H = 22
BTN_MARGIN = 8
def paint(self, painter: QPainter, option, index):
super().paint(painter, option, index)
rect = option.rect
btn_rect = QRect(
rect.right() - self.BTN_W - self.BTN_MARGIN,
rect.top() + (rect.height() - self.BTN_H) // 2,
self.BTN_W,
self.BTN_H,
)
painter.save()
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
painter.setBrush(QBrush(QColor("#e8a020")))
painter.setPen(Qt.PenStyle.NoPen)
painter.drawRoundedRect(btn_rect, 4, 4)
painter.setPen(QPen(QColor("#111111")))
font = QFont()
font.setPointSize(8)
font.setBold(True)
painter.setFont(font)
painter.drawText(btn_rect, Qt.AlignmentFlag.AlignCenter, "Danse")
painter.restore()
class DraggableLibraryList(QListWidget):
"""QListWidget der understøtter drag-start med sang-data som mime."""
"""QListWidget med drag, dobbeltklik og klik på højre side for dans-tags."""
danse_clicked = __import__('PyQt6.QtCore', fromlist=['pyqtSignal']).pyqtSignal(dict)
def __init__(self, parent=None):
super().__init__(parent)
@@ -20,6 +52,28 @@ class DraggableLibraryList(QListWidget):
self.setDragDropMode(QAbstractItemView.DragDropMode.DragOnly)
self.setDefaultDropAction(Qt.DropAction.CopyAction)
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
item = self.itemAt(event.pos())
if item and event.pos().x() > self.viewport().width() - 75:
song = item.data(Qt.ItemDataRole.UserRole)
if song:
self.danse_clicked.emit(song)
return
super().mousePressEvent(event)
def mouseDoubleClickEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
item = self.itemAt(event.pos())
if item:
# Dobbeltklik i højre 75px = Danse, ellers song_selected
if event.pos().x() > self.viewport().width() - 75:
song = item.data(Qt.ItemDataRole.UserRole)
if song:
self.danse_clicked.emit(song)
return
super().mouseDoubleClickEvent(event)
def startDrag(self, supported_actions):
item = self.currentItem()
if not item:
@@ -71,34 +125,13 @@ class LibraryPanel(QWidget):
header.addWidget(lbl)
header.addStretch()
self._btn_bpm_scan = QPushButton("♩ BPM alle")
self._btn_bpm_scan.setFixedHeight(24)
self._btn_bpm_scan.setToolTip("Analysér BPM på alle sange uden BPM (kører i baggrunden)")
self._btn_bpm_scan.clicked.connect(self._start_bulk_bpm_scan)
header.addWidget(self._btn_bpm_scan)
btn_manage = QPushButton("⚙ Mapper")
btn_manage.setFixedHeight(24)
btn_manage.setFixedHeight(28)
btn_manage.setToolTip("Tilføj, fjern og scan musikbiblioteker")
btn_manage.clicked.connect(self._manage_libraries)
header.addWidget(btn_manage)
layout.addLayout(header)
# Scan status
self._scan_bar = QProgressBar()
self._scan_bar.setObjectName("scan_bar")
self._scan_bar.setTextVisible(True)
self._scan_bar.setFormat("Scanner...")
self._scan_bar.setFixedHeight(16)
self._scan_bar.setRange(0, 0)
self._scan_bar.hide()
layout.addWidget(self._scan_bar)
self._scan_label = QLabel("")
self._scan_label.setObjectName("result_count")
self._scan_label.hide()
layout.addWidget(self._scan_label)
# Søgefelt
self._search = QLineEdit()
self._search.setPlaceholderText("Søg i titel, artist, album, dans...")
@@ -121,8 +154,10 @@ class LibraryPanel(QWidget):
self._list = DraggableLibraryList()
self._list.setObjectName("library_list")
self._list.itemDoubleClicked.connect(self._on_double_click)
self._list.danse_clicked.connect(self.edit_tags_requested)
self._list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self._list.customContextMenuRequested.connect(self._show_context_menu)
self._list.setItemDelegate(DanseButtonDelegate(self._list))
layout.addWidget(self._list)
# ── Scanning ──────────────────────────────────────────────────────────────
@@ -131,16 +166,10 @@ class LibraryPanel(QWidget):
self.scan_requested.emit()
def set_scanning(self, scanning: bool, status_text: str = ""):
if scanning:
self._scan_bar.show()
self._scan_label.setText(status_text or "Starter...")
self._scan_label.show()
else:
self._scan_bar.hide()
self._scan_label.hide()
pass # Status vises i statuslinjen
def update_scan_status(self, text: str):
self._scan_label.setText(text)
pass # Status vises i statuslinjen
# ── Sange ─────────────────────────────────────────────────────────────────
@@ -185,41 +214,34 @@ class LibraryPanel(QWidget):
dance_parts.append(f"{d} / {lvl}" if lvl else d)
dance_str = " · " + " | ".join(dance_parts) if dance_parts else ""
line1 = ("" if missing else "") + song.get("title", "")
bpm = song.get("bpm", 0)
def _render(self):
self._list.clear()
q = self._search.text().strip().lower()
for song in self._filtered:
dances = song.get("dances", [])
dance_levels = song.get("dance_levels", [])
missing = song.get("file_missing", False)
dance_parts = []
for i, d in enumerate(dances):
lvl = dance_levels[i] if i < len(dance_levels) else ""
dance_parts.append(f"{d} / {lvl}" if lvl else d)
dance_str = " · " + " | ".join(dance_parts) if dance_parts else ""
prefix = "" if missing else ""
bpm = song.get("bpm", 0)
bpm_str = f"{bpm} BPM" if bpm else "? BPM"
line2 = f" {song.get('artist','')} · {bpm_str} · {song.get('file_format','').upper()}{dance_str}"
line1 = prefix + song.get("title", "")
line2 = f" {song.get('artist','')} · {bpm_str} · {song.get('file_format','').upper()}{dance_str}"
row_widget = QWidget()
row_widget.setStyleSheet("background: transparent;")
row_layout = QHBoxLayout(row_widget)
row_layout.setContentsMargins(2, 2, 2, 2)
row_layout.setSpacing(8)
lbl = QLabel(f"{line1}\n{line2}")
lbl.setWordWrap(False)
row_layout.addWidget(lbl, stretch=1)
btn_danse = QPushButton("Danse")
btn_danse.setFixedHeight(30)
btn_danse.setFixedWidth(70)
btn_danse.setToolTip("Rediger dans-tags")
btn_danse.setStyleSheet(
"QPushButton { background: #e8a020; color: #111; border-radius: 4px; "
"font-weight: bold; font-size: 12px; border: none; }"
"QPushButton:hover { background: #f0b030; }"
)
btn_danse.clicked.connect(lambda _, s=song: self.edit_tags_requested.emit(s))
row_layout.addWidget(btn_danse)
item = QListWidgetItem()
item = QListWidgetItem(f"{line1}\n{line2}")
item.setData(Qt.ItemDataRole.UserRole, song)
row_widget.adjustSize()
hint = row_widget.sizeHint()
hint.setHeight(max(hint.height(), 52))
item.setSizeHint(hint)
item.setSizeHint(__import__('PyQt6.QtCore', fromlist=['QSize']).QSize(0, 52))
if missing:
item.setForeground(QColor("#5a6070"))
elif q and any(q in d.lower() for d in dances):
item.setForeground(QColor("#e8a020"))
self._list.addItem(item)
self._list.setItemWidget(item, row_widget)
def _start_bulk_bpm_scan(self):
"""Start BPM-analyse på alle sange uden BPM i baggrundstråd med lav prioritet."""
@@ -301,6 +323,7 @@ class LibraryPanel(QWidget):
act_play = menu.addAction("Afspil")
menu.addSeparator()
act_tags = menu.addAction("✎ Rediger dans-tags...")
act_info = menu.addAction(" Dans-info...")
act_bpm = menu.addAction("♩ Analysér BPM")
menu.addSeparator()
send_menu = menu.addMenu("Send til")
@@ -312,6 +335,10 @@ class LibraryPanel(QWidget):
self.song_selected.emit(song)
elif action == act_tags:
self.edit_tags_requested.emit(song)
elif action == act_info:
from ui.dance_info_dialog import DanceInfoDialog
dlg = DanceInfoDialog(song, parent=self.window())
dlg.exec()
elif action == act_bpm:
self._analyze_bpm(song)
elif action == act_mail: