""" 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 sync_requested = pyqtSignal() # bed main_window om at køre sync 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_share = QPushButton("↗ Del...") btn_share.clicked.connect(self._share_selected) btn_row.addWidget(btn_share) btn_shared = QPushButton("🌐 Hent delte") btn_shared.clicked.connect(self._fetch_shared) btn_row.addWidget(btn_shared) 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 delete_playlist delete_playlist(pl["id"]) self._load_data() # Signal til main_window om at køre sync self.sync_requested.emit() except Exception as e: QMessageBox.warning(self, "Fejl", f"Kunne ikke slette: {e}") def _share_selected(self): """Åbn del-dialog for den valgte playliste.""" item = self._list.currentItem() if not item: QMessageBox.information(self, "Del", "Vælg en playliste først.") return pl = item.data(Qt.ItemDataRole.UserRole) if not isinstance(pl, dict): return # Hent server-info fra settings try: from ui.settings_dialog import load_settings s = load_settings() server_url = s.get("server_url", "") token = self._get_token() if not token: QMessageBox.warning(self, "Ikke logget ind", "Du skal være logget ind for at dele.") return # Find server-ID for playlisten server_id = pl.get("api_project_id") if not server_id: QMessageBox.warning(self, "Ikke synkroniseret", "Synkroniser playlisten til serveren først\n" "(Filer → Synkroniser nu).") return from ui.share_dialog import ShareDialog dlg = ShareDialog(server_id, pl["name"], server_url, token, parent=self) dlg.exec() except Exception as e: QMessageBox.warning(self, "Fejl", str(e)) def _get_token(self) -> str | None: """Hent JWT token fra main_window.""" mw = self.parent() while mw and not hasattr(mw, "_api_token"): mw = mw.parent() return getattr(mw, "_api_token", None) if mw else None def _fetch_shared(self): """Hent playlister der er delt med mig fra serveren.""" try: from ui.settings_dialog import load_settings s = load_settings() server_url = s.get("server_url", "").rstrip("/") token = self._get_token() if not token: QMessageBox.warning(self, "Ikke logget ind", "Du skal være logget ind for at hente delte lister.") return import urllib.request, json req = urllib.request.Request( f"{server_url}/sharing/playlists/shared-with-me", headers={"Authorization": f"Bearer {token}"} ) with urllib.request.urlopen(req, timeout=10) as resp: shared = json.loads(resp.read()) if not shared: QMessageBox.information(self, "Ingen delte lister", "Ingen playlister er delt med dig.") return # Vis valgdialog from PyQt6.QtWidgets import QInputDialog options = [ f"{p['name']} (af {p['owner']}, {p['song_count']} sange, {p['permission']})" for p in shared ] choice, ok = QInputDialog.getItem( self, "Hent delt playliste", "Vælg en playliste at hente:", options, 0, False ) if not ok: return idx = options.index(choice) chosen = shared[idx] # Hent indholdet req2 = urllib.request.Request( f"{server_url}/sharing/playlists/{chosen['project_id']}", headers={"Authorization": f"Bearer {token}"} ) with urllib.request.urlopen(req2, timeout=10) as resp: pl_data = json.loads(resp.read()) self._import_shared_playlist(pl_data, server_url, token, permission=chosen.get("permission", "view")) except Exception as e: QMessageBox.warning(self, "Fejl", f"Kunne ikke hente: {e}") def _import_shared_playlist(self, pl_data: dict, server_url: str, token: str, permission: str = "view"): """Importer en delt playliste som en linket liste.""" import sqlite3 from local.local_db import DB_PATH, get_db, add_song_to_playlist name = pl_data["name"] server_id = pl_data["id"] conn = sqlite3.connect(str(DB_PATH)) conn.row_factory = sqlite3.Row # Tjek om listen allerede er linket existing = conn.execute( "SELECT id FROM playlists WHERE api_project_id=?", (server_id,) ).fetchone() conn.close() if existing: # Opdater eksisterende pl_id = existing["id"] with get_db() as c: c.execute("DELETE FROM playlist_songs WHERE playlist_id=?", (pl_id,)) else: # Opret ny linket playliste with get_db() as c: c.execute( "INSERT INTO playlists (name, api_project_id, is_linked, server_permission) " "VALUES (?, ?, 1, ?)", (name, server_id, permission) ) pl_id = c.execute("SELECT last_insert_rowid()").fetchone()[0] # Indsæt sange med sang-matching matched = 0 with get_db() as c: for song_data in pl_data.get("songs", []): local = c.execute( "SELECT id FROM songs WHERE title=? AND artist=? AND file_missing=0", (song_data["title"], song_data["artist"]) ).fetchone() if local: c.execute( "INSERT INTO playlist_songs " "(playlist_id, song_id, position, status, is_workshop, dance_override) " "VALUES (?,?,?,?,?,?)", (pl_id, local["id"], song_data["position"], song_data.get("status", "pending"), 1 if song_data.get("is_workshop") else 0, song_data.get("dance_override") or "") ) matched += 1 self._load_data() self.playlist_selected.emit(pl_id, name) perm_text = {"view": "se", "copy": "kopiere", "edit": "redigere"}.get( permission, permission ) QMessageBox.information( self, "Linket", f"'{name}' er nu linket til server-listen.\n" f"Du har rettighed til at {perm_text} listen.\n\n" f"{matched} af {len(pl_data.get('songs', []))} sange fundet lokalt." )