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,221 @@
"""
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,
)
from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QMimeData, QByteArray
from PyQt6.QtGui import QColor, QDrag
class DraggableLibraryList(QListWidget):
"""QListWidget der understøtter drag-start med sang-data som mime."""
def __init__(self, parent=None):
super().__init__(parent)
self.setDragEnabled(True)
self.setDragDropMode(QAbstractItemView.DragDropMode.DragOnly)
self.setDefaultDropAction(Qt.DropAction.CopyAction)
def startDrag(self, supported_actions):
item = self.currentItem()
if not item:
return
song = item.data(Qt.ItemDataRole.UserRole)
if not song:
return
import json
data = json.dumps(song).encode("utf-8")
mime = QMimeData()
mime.setData("application/x-linedance-song", QByteArray(data))
mime.setText(song.get("title", ""))
drag = QDrag(self)
drag.setMimeData(mime)
drag.exec(Qt.DropAction.CopyAction)
class LibraryPanel(QWidget):
song_selected = pyqtSignal(dict)
add_to_playlist = pyqtSignal(dict)
scan_requested = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self._all_songs: list[dict] = []
self._filtered: list[dict] = []
self._search_timer = QTimer(self)
self._search_timer.setSingleShot(True)
self._search_timer.setInterval(150)
self._search_timer.timeout.connect(self._do_search)
self._build_ui()
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)
lbl = QLabel("BIBLIOTEK")
lbl.setObjectName("section_title")
header.addWidget(lbl)
header.addStretch()
self._btn_scan = QPushButton("⟳ SCAN")
self._btn_scan.setFixedHeight(24)
self._btn_scan.setToolTip("Scan alle biblioteksmapper for nye og ændrede filer")
self._btn_scan.clicked.connect(self._on_scan_clicked)
header.addWidget(self._btn_scan)
btn_add = QPushButton("+ MAPPE")
btn_add.setFixedHeight(24)
btn_add.clicked.connect(self._add_folder)
header.addWidget(btn_add)
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...")
self._search.textChanged.connect(self._on_search_changed)
layout.addWidget(self._search)
# Resultat-tæller + drag-hint
hint_row = QHBoxLayout()
hint_row.setContentsMargins(8, 2, 8, 2)
self._count_label = QLabel("0 sange")
self._count_label.setObjectName("result_count")
hint_row.addWidget(self._count_label)
hint_row.addStretch()
drag_hint = QLabel("træk til danseliste →")
drag_hint.setObjectName("result_count")
hint_row.addWidget(drag_hint)
layout.addLayout(hint_row)
# Liste — draggable
self._list = DraggableLibraryList()
self._list.setObjectName("library_list")
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)
# ── Scanning ──────────────────────────────────────────────────────────────
def _on_scan_clicked(self):
self.scan_requested.emit()
def set_scanning(self, scanning: bool, status_text: str = ""):
if scanning:
self._btn_scan.setEnabled(False)
self._btn_scan.setText("⟳ SCANNER...")
self._scan_bar.show()
self._scan_label.setText(status_text or "Starter...")
self._scan_label.show()
else:
self._btn_scan.setEnabled(True)
self._btn_scan.setText("⟳ SCAN")
self._scan_bar.hide()
self._scan_label.hide()
def update_scan_status(self, text: str):
self._scan_label.setText(text)
# ── Sange ─────────────────────────────────────────────────────────────────
def load_songs(self, songs: list[dict]):
self._all_songs = songs
self._do_search()
# ── Søgning ───────────────────────────────────────────────────────────────
def _on_search_changed(self):
self._search_timer.start()
def _do_search(self):
q = self._search.text().strip().lower()
self._filtered = [s for s in self._all_songs if self._matches(s, q)] if q else list(self._all_songs)
total = len(self._all_songs)
found = len(self._filtered)
q_text = self._search.text().strip()
self._count_label.setText(
f"{found} resultat{'er' if found != 1 else ''} for \"{q_text}\"" if q_text
else f"{total} sang{'e' if total != 1 else ''}"
)
self._render()
def _matches(self, song: dict, q: str) -> bool:
return any(q in f.lower() for f in [
song.get("title", ""), song.get("artist", ""),
song.get("album", ""), song.get("file_format", ""),
] + song.get("dances", []))
def _render(self):
self._list.clear()
q = self._search.text().strip().lower()
for song in self._filtered:
dances = song.get("dances", [])
dance_str = " · " + " / ".join(dances) if dances else ""
missing = song.get("file_missing", False)
line1 = ("" if missing else "") + song.get("title", "")
line2 = f" {song.get('artist','')} · {song.get('bpm',0)} BPM · {song.get('file_format','').upper()}{dance_str}"
item = QListWidgetItem(f"{line1}\n{line2}")
item.setData(Qt.ItemDataRole.UserRole, song)
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)
# ── Handlinger ────────────────────────────────────────────────────────────
def _on_double_click(self, item: QListWidgetItem):
song = item.data(Qt.ItemDataRole.UserRole)
if song:
self.song_selected.emit(song)
def _show_context_menu(self, pos):
from PyQt6.QtWidgets import QMenu
item = self._list.itemAt(pos)
if not item:
return
song = item.data(Qt.ItemDataRole.UserRole)
if not song:
return
menu = QMenu(self)
act_add = menu.addAction("Tilføj til danseliste")
act_play = menu.addAction("Afspil")
action = menu.exec(self._list.mapToGlobal(pos))
if action == act_add:
self.add_to_playlist.emit(song)
elif action == act_play:
self.song_selected.emit(song)
def _add_folder(self):
from PyQt6.QtWidgets import QFileDialog
folder = QFileDialog.getExistingDirectory(self, "Vælg musikmappe")
if folder:
mw = self.window()
if hasattr(mw, "add_library_path"):
mw.add_library_path(folder)