Næste version
This commit is contained in:
346
linedance-app/ui/playlist_browser.py
Normal file
346
linedance-app/ui/playlist_browser.py
Normal file
@@ -0,0 +1,346 @@
|
||||
"""
|
||||
playlist_browser.py — Dialog til at hente og gemme danselister med tag-organisering.
|
||||
|
||||
Viser en liste over alle gemte danselister med:
|
||||
- Navn, dato, antal sange
|
||||
- Tag-filtrering i venstre side
|
||||
- Gem ny liste med tags
|
||||
"""
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
||||
QPushButton, QListWidget, QListWidgetItem, QWidget,
|
||||
QSplitter, QFrame, QMessageBox, QInputDialog,
|
||||
)
|
||||
from PyQt6.QtCore import Qt, pyqtSignal
|
||||
from PyQt6.QtGui import QColor
|
||||
|
||||
|
||||
class PlaylistBrowserDialog(QDialog):
|
||||
"""Kombineret gem/hent dialog til danselister."""
|
||||
|
||||
playlist_selected = pyqtSignal(int, str) # playlist_id, name
|
||||
|
||||
def __init__(self, mode: str = "load", current_songs: list = None,
|
||||
current_name: str = "", parent=None):
|
||||
super().__init__(parent)
|
||||
self._mode = mode # "load" eller "save"
|
||||
self._current_songs = current_songs or []
|
||||
self._current_name = current_name
|
||||
self._all_playlists = []
|
||||
self._active_tag = None
|
||||
|
||||
title = "Gem danseliste" if mode == "save" else "Hent danseliste"
|
||||
self.setWindowTitle(title)
|
||||
self.setMinimumSize(700, 480)
|
||||
self.resize(780, 520)
|
||||
|
||||
self._build_ui()
|
||||
self._load_data()
|
||||
|
||||
def _build_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(12, 12, 12, 12)
|
||||
layout.setSpacing(8)
|
||||
|
||||
# Gem-felter (kun i save-mode)
|
||||
if self._mode == "save":
|
||||
save_frame = QFrame()
|
||||
save_frame.setObjectName("track_display")
|
||||
save_layout = QVBoxLayout(save_frame)
|
||||
save_layout.setContentsMargins(10, 8, 10, 8)
|
||||
|
||||
row1 = QHBoxLayout()
|
||||
row1.addWidget(QLabel("Navn:"))
|
||||
self._name_input = QLineEdit()
|
||||
self._name_input.setText(self._current_name)
|
||||
self._name_input.setPlaceholderText("Navn på danselisten...")
|
||||
row1.addWidget(self._name_input)
|
||||
save_layout.addLayout(row1)
|
||||
|
||||
row2 = QHBoxLayout()
|
||||
row2.addWidget(QLabel("Tags:"))
|
||||
self._tags_input = QLineEdit()
|
||||
self._tags_input.setPlaceholderText("stævne, øvning, workshop (komma-separeret)")
|
||||
self._tags_input.textChanged.connect(self._suggest_tags)
|
||||
row2.addWidget(self._tags_input)
|
||||
save_layout.addLayout(row2)
|
||||
|
||||
# Tag-forslag
|
||||
self._tag_suggestions = QListWidget()
|
||||
self._tag_suggestions.setMaximumHeight(80)
|
||||
self._tag_suggestions.hide()
|
||||
self._tag_suggestions.itemClicked.connect(self._add_tag_suggestion)
|
||||
save_layout.addWidget(self._tag_suggestions)
|
||||
|
||||
lbl = QLabel(f"{len(self._current_songs)} sange vil blive gemt")
|
||||
lbl.setObjectName("result_count")
|
||||
save_layout.addWidget(lbl)
|
||||
|
||||
layout.addWidget(save_frame)
|
||||
|
||||
# Splitter: tags til venstre, lister til højre
|
||||
splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||
|
||||
# ── Venstre: tag-filtrering ──
|
||||
tag_panel = QWidget()
|
||||
tag_layout = QVBoxLayout(tag_panel)
|
||||
tag_layout.setContentsMargins(0, 0, 0, 0)
|
||||
tag_layout.setSpacing(4)
|
||||
lbl_tags = QLabel("FILTRÉR PÅ TAG")
|
||||
lbl_tags.setObjectName("section_title")
|
||||
tag_layout.addWidget(lbl_tags)
|
||||
self._tag_list = QListWidget()
|
||||
self._tag_list.currentItemChanged.connect(self._on_tag_selected)
|
||||
tag_layout.addWidget(self._tag_list)
|
||||
tag_panel.setMaximumWidth(180)
|
||||
splitter.addWidget(tag_panel)
|
||||
|
||||
# ── Højre: danseliste-oversigt ──
|
||||
list_panel = QWidget()
|
||||
list_layout = QVBoxLayout(list_panel)
|
||||
list_layout.setContentsMargins(0, 0, 0, 0)
|
||||
list_layout.setSpacing(4)
|
||||
|
||||
# Søgefelt
|
||||
self._search = QLineEdit()
|
||||
self._search.setPlaceholderText("Søg i navn...")
|
||||
self._search.textChanged.connect(self._filter)
|
||||
list_layout.addWidget(self._search)
|
||||
|
||||
self._count_label = QLabel("")
|
||||
self._count_label.setObjectName("result_count")
|
||||
list_layout.addWidget(self._count_label)
|
||||
|
||||
self._list = QListWidget()
|
||||
self._list.itemDoubleClicked.connect(self._on_double_click)
|
||||
list_layout.addWidget(self._list)
|
||||
splitter.addWidget(list_panel)
|
||||
|
||||
splitter.setSizes([160, 580])
|
||||
layout.addWidget(splitter, stretch=1)
|
||||
|
||||
# Knapper
|
||||
btn_row = QHBoxLayout()
|
||||
|
||||
if self._mode == "load":
|
||||
btn_delete = QPushButton("🗑 Slet valgte")
|
||||
btn_delete.clicked.connect(self._delete_selected)
|
||||
btn_row.addWidget(btn_delete)
|
||||
btn_tags = QPushButton("🏷 Rediger tags")
|
||||
btn_tags.clicked.connect(self._edit_tags)
|
||||
btn_row.addWidget(btn_tags)
|
||||
|
||||
btn_row.addStretch()
|
||||
btn_cancel = QPushButton("Annuller")
|
||||
btn_cancel.clicked.connect(self.reject)
|
||||
btn_row.addWidget(btn_cancel)
|
||||
|
||||
if self._mode == "save":
|
||||
btn_ok = QPushButton("💾 Gem")
|
||||
btn_ok.setObjectName("btn_play")
|
||||
btn_ok.clicked.connect(self._save)
|
||||
else:
|
||||
btn_ok = QPushButton("📂 Hent valgte")
|
||||
btn_ok.setObjectName("btn_play")
|
||||
btn_ok.clicked.connect(self._load_selected)
|
||||
btn_row.addWidget(btn_ok)
|
||||
|
||||
layout.addLayout(btn_row)
|
||||
|
||||
def _load_data(self):
|
||||
try:
|
||||
from local.local_db import get_playlists, get_all_playlist_tags
|
||||
self._all_playlists = [dict(r) for r in get_playlists()]
|
||||
|
||||
# Udfyld tag-liste
|
||||
self._tag_list.clear()
|
||||
all_item = QListWidgetItem("Alle lister")
|
||||
all_item.setData(Qt.ItemDataRole.UserRole, None)
|
||||
self._tag_list.addItem(all_item)
|
||||
|
||||
for tag in get_all_playlist_tags():
|
||||
item = QListWidgetItem(f"# {tag}")
|
||||
item.setData(Qt.ItemDataRole.UserRole, tag)
|
||||
self._tag_list.addItem(item)
|
||||
|
||||
self._tag_list.setCurrentRow(0)
|
||||
self._render(self._all_playlists)
|
||||
except Exception as e:
|
||||
print(f"Playlist browser load fejl: {e}")
|
||||
|
||||
def _on_tag_selected(self, current, previous):
|
||||
if not current:
|
||||
return
|
||||
self._active_tag = current.data(Qt.ItemDataRole.UserRole)
|
||||
self._filter()
|
||||
|
||||
def _suggest_tags(self, text: str):
|
||||
"""Vis forslag til det sidst indtastede tag."""
|
||||
if not hasattr(self, '_tag_suggestions'):
|
||||
return
|
||||
parts = text.split(",")
|
||||
prefix = parts[-1].strip().lower()
|
||||
if not prefix:
|
||||
self._tag_suggestions.hide()
|
||||
return
|
||||
try:
|
||||
from local.local_db import get_all_playlist_tags
|
||||
all_tags = get_all_playlist_tags()
|
||||
matches = [t for t in all_tags
|
||||
if t.startswith(prefix) and t not in
|
||||
[p.strip().lower() for p in parts[:-1]]]
|
||||
if matches:
|
||||
self._tag_suggestions.clear()
|
||||
for t in matches[:5]:
|
||||
self._tag_suggestions.addItem(t)
|
||||
self._tag_suggestions.show()
|
||||
else:
|
||||
self._tag_suggestions.hide()
|
||||
except Exception:
|
||||
self._tag_suggestions.hide()
|
||||
|
||||
def _add_tag_suggestion(self, item):
|
||||
"""Tilføj et foreslået tag til tekstfeltet."""
|
||||
parts = self._tags_input.text().split(",")
|
||||
parts[-1] = " " + item.text()
|
||||
self._tags_input.setText(",".join(parts) + ", ")
|
||||
self._tag_suggestions.hide()
|
||||
self._tags_input.setFocus()
|
||||
|
||||
def _edit_tags(self):
|
||||
"""Rediger tags på den valgte liste."""
|
||||
item = self._list.currentItem()
|
||||
if not item:
|
||||
return
|
||||
pl = item.data(Qt.ItemDataRole.UserRole)
|
||||
if not pl or not isinstance(pl, dict):
|
||||
return
|
||||
from PyQt6.QtWidgets import QInputDialog
|
||||
current = pl.get("tags", "")
|
||||
new_tags, ok = QInputDialog.getText(
|
||||
self, "Rediger tags", "Tags (komma-separeret):", text=current
|
||||
)
|
||||
if ok:
|
||||
try:
|
||||
from local.local_db import update_playlist_tags
|
||||
update_playlist_tags(pl["id"], new_tags.strip())
|
||||
self._load_data()
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, "Fejl", f"Kunne ikke opdatere tags: {e}")
|
||||
|
||||
def _filter(self):
|
||||
query = self._search.text().strip().lower()
|
||||
tag = self._active_tag
|
||||
|
||||
filtered = self._all_playlists
|
||||
if tag:
|
||||
filtered = [
|
||||
p for p in filtered
|
||||
if tag in [t.strip().lower() for t in p.get("tags", "").split(",")]
|
||||
]
|
||||
if query:
|
||||
filtered = [p for p in filtered if query in p["name"].lower()]
|
||||
|
||||
self._render(filtered)
|
||||
|
||||
def _render(self, playlists: list):
|
||||
self._list.clear()
|
||||
self._count_label.setText(f"{len(playlists)} liste{'r' if len(playlists) != 1 else ''}")
|
||||
|
||||
for pl in playlists:
|
||||
date = pl.get("created_at", "")[:10]
|
||||
count = pl.get("song_count", 0)
|
||||
tags = pl.get("tags", "")
|
||||
tag_str = f" [{tags}]" if tags else ""
|
||||
|
||||
item = QListWidgetItem(
|
||||
f"{pl['name']}\n"
|
||||
f" {date} · {count} sange{tag_str}"
|
||||
)
|
||||
item.setData(Qt.ItemDataRole.UserRole, pl)
|
||||
self._list.addItem(item)
|
||||
|
||||
def _on_double_click(self, item: QListWidgetItem):
|
||||
if self._mode == "load":
|
||||
self._load_selected()
|
||||
else:
|
||||
self._save()
|
||||
|
||||
def _load_selected(self):
|
||||
item = self._list.currentItem()
|
||||
if not item:
|
||||
QMessageBox.information(self, "Vælg", "Vælg en liste først.")
|
||||
return
|
||||
pl = item.data(Qt.ItemDataRole.UserRole)
|
||||
self.playlist_selected.emit(pl["id"], pl["name"])
|
||||
self.accept()
|
||||
|
||||
def _save(self):
|
||||
name = self._name_input.text().strip()
|
||||
if not name:
|
||||
QMessageBox.warning(self, "Navn mangler", "Angiv et navn til danselisten.")
|
||||
self._name_input.setFocus()
|
||||
return
|
||||
tags = self._tags_input.text().strip()
|
||||
|
||||
# Tjek om navn allerede eksisterer
|
||||
existing = [p for p in self._all_playlists
|
||||
if p["name"].lower() == name.lower()]
|
||||
if existing:
|
||||
reply = QMessageBox.question(
|
||||
self, "Navn eksisterer allerede",
|
||||
f"Der findes allerede en liste med navnet '{name}'.\n"
|
||||
f"Vil du overskrive den?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
)
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
try:
|
||||
from local.local_db import get_db, add_song_to_playlist
|
||||
pl_id = existing[0]["id"]
|
||||
with get_db() as conn:
|
||||
conn.execute(
|
||||
"DELETE FROM playlist_songs WHERE playlist_id=?", (pl_id,)
|
||||
)
|
||||
if tags:
|
||||
conn.execute(
|
||||
"UPDATE playlists SET tags=? WHERE id=?", (tags, pl_id)
|
||||
)
|
||||
for i, song in enumerate(self._current_songs, start=1):
|
||||
if song.get("id"):
|
||||
add_song_to_playlist(pl_id, song["id"], position=i)
|
||||
self.playlist_selected.emit(pl_id, name)
|
||||
self.accept()
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, "Fejl", f"Kunne ikke overskrive: {e}")
|
||||
return
|
||||
|
||||
try:
|
||||
from local.local_db import create_playlist, add_song_to_playlist
|
||||
pl_id = create_playlist(name, tags=tags)
|
||||
for i, song in enumerate(self._current_songs, start=1):
|
||||
if song.get("id"):
|
||||
add_song_to_playlist(pl_id, song["id"], position=i)
|
||||
self.playlist_selected.emit(pl_id, name)
|
||||
self.accept()
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, "Fejl", f"Kunne ikke gemme: {e}")
|
||||
|
||||
def _delete_selected(self):
|
||||
item = self._list.currentItem()
|
||||
if not item:
|
||||
return
|
||||
pl = item.data(Qt.ItemDataRole.UserRole)
|
||||
reply = QMessageBox.question(
|
||||
self, "Slet danseliste",
|
||||
f"Slet '{pl['name']}'?\n\nDette kan ikke fortrydes.",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
)
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
try:
|
||||
from local.local_db import get_db
|
||||
with get_db() as conn:
|
||||
conn.execute("DELETE FROM playlists WHERE id=?", (pl["id"],))
|
||||
self._load_data()
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, "Fejl", f"Kunne ikke slette: {e}")
|
||||
Reference in New Issue
Block a user