345 lines
13 KiB
Python
345 lines
13 KiB
Python
"""
|
|
alt_dance_picker_dialog.py — Vælg alternativ dans til en sang i playlisten.
|
|
|
|
Tre sektioner:
|
|
🟢 Mine egne alternativ-danse med min rating
|
|
🟡 Community alternativ-danse med community + min rating
|
|
Alle andre danse
|
|
"""
|
|
|
|
from PyQt6.QtWidgets import (
|
|
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
|
QPushButton, QListWidget, QListWidgetItem, QWidget,
|
|
)
|
|
from PyQt6.QtCore import Qt, QTimer, pyqtSignal
|
|
from PyQt6.QtGui import QColor
|
|
|
|
STAR_FULL = "★"
|
|
STAR_EMPTY = "☆"
|
|
GREEN = "#27ae60"
|
|
YELLOW = "#e8a020"
|
|
MUTED = "#5a6070"
|
|
|
|
|
|
class StarRatingWidget(QWidget):
|
|
"""Klikbar stjerne-rating widget til brug i lister."""
|
|
rating_changed = pyqtSignal(int) # 1-5
|
|
|
|
def __init__(self, rating=None, max_stars=5, color=YELLOW, parent=None):
|
|
# YELLOW er ikke defineret endnu ved import — bruges som string nedenfor
|
|
super().__init__(parent)
|
|
self._rating = rating
|
|
self._max = max_stars
|
|
self._color = color
|
|
self._btns = []
|
|
layout = QHBoxLayout(self)
|
|
layout.setContentsMargins(2, 0, 2, 0)
|
|
layout.setSpacing(1)
|
|
for i in range(1, max_stars + 1):
|
|
btn = QPushButton("★" if rating and i <= rating else "☆")
|
|
btn.setFixedSize(18, 18)
|
|
btn.setStyleSheet(f"""
|
|
QPushButton {{
|
|
font-size: 13px; border: none; background: none; padding: 0;
|
|
color: {color if rating and i <= rating else '#5a6070'};
|
|
}}
|
|
QPushButton:hover {{ color: {color}; }}
|
|
""")
|
|
btn.clicked.connect(lambda checked, r=i: self._on_click(r))
|
|
layout.addWidget(btn)
|
|
self._btns.append(btn)
|
|
|
|
def _on_click(self, r):
|
|
self._rating = r
|
|
for i, btn in enumerate(self._btns):
|
|
filled = i < r
|
|
btn.setText("★" if filled else "☆")
|
|
btn.setStyleSheet(f"""
|
|
QPushButton {{
|
|
font-size: 13px; border: none; background: none; padding: 0;
|
|
color: {self._color if filled else '#5a6070'};
|
|
}}
|
|
QPushButton:hover {{ color: {self._color}; }}
|
|
""")
|
|
self.rating_changed.emit(r)
|
|
|
|
def get_rating(self):
|
|
return self._rating
|
|
|
|
|
|
def make_stars(rating, max_stars=5):
|
|
if not rating:
|
|
return STAR_EMPTY * max_stars
|
|
full = min(max_stars, round(float(rating)))
|
|
return STAR_FULL * full + STAR_EMPTY * (max_stars - full)
|
|
|
|
|
|
class AltDancePickerDialog(QDialog):
|
|
def __init__(self, song: dict, parent=None):
|
|
super().__init__(parent)
|
|
self._song = song
|
|
self._chosen_dance = ""
|
|
self._chosen_rating = None
|
|
self._cleared = False
|
|
self.setWindowTitle("Vælg alternativ dans")
|
|
self.setMinimumWidth(600)
|
|
self.setMinimumHeight(520)
|
|
self._build_ui()
|
|
self._load_suggestions("")
|
|
|
|
def _build_ui(self):
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(12, 12, 12, 12)
|
|
layout.setSpacing(8)
|
|
|
|
# Sang-info
|
|
title = self._song.get("title", "?")
|
|
artist = self._song.get("artist", "")
|
|
lbl = QLabel(f"{title} · {artist}" if artist else title)
|
|
lbl.setObjectName("track_title")
|
|
lbl.setWordWrap(True)
|
|
layout.addWidget(lbl)
|
|
|
|
# Søgefelt
|
|
self._edit = QLineEdit()
|
|
self._edit.setPlaceholderText("Søg dans-navn...")
|
|
self._edit.textChanged.connect(self._on_text_changed)
|
|
self._edit.returnPressed.connect(self._on_accept)
|
|
layout.addWidget(self._edit)
|
|
|
|
# Forslagsliste
|
|
self._list = QListWidget()
|
|
self._list.setMinimumHeight(320)
|
|
self._list.itemClicked.connect(self._on_item_clicked)
|
|
self._list.itemDoubleClicked.connect(self._on_selected)
|
|
layout.addWidget(self._list)
|
|
|
|
# Info-label
|
|
self._info_lbl = QLabel("")
|
|
self._info_lbl.setObjectName("result_count")
|
|
self._info_lbl.setWordWrap(True)
|
|
layout.addWidget(self._info_lbl)
|
|
|
|
# Debounce timer
|
|
self._timer = QTimer(self)
|
|
self._timer.setSingleShot(True)
|
|
self._timer.setInterval(150)
|
|
self._timer.timeout.connect(
|
|
lambda: self._load_suggestions(self._edit.text().strip())
|
|
)
|
|
|
|
# Knapper
|
|
btn_row = QHBoxLayout()
|
|
btn_none = QPushButton("✕ Ingen alternativ")
|
|
btn_none.clicked.connect(self._on_clear)
|
|
btn_row.addWidget(btn_none)
|
|
btn_row.addStretch()
|
|
btn_cancel = QPushButton("Annuller")
|
|
btn_cancel.clicked.connect(self.reject)
|
|
btn_row.addWidget(btn_cancel)
|
|
btn_ok = QPushButton("✓ Vælg")
|
|
btn_ok.setObjectName("btn_play")
|
|
btn_ok.clicked.connect(self._on_accept)
|
|
btn_row.addWidget(btn_ok)
|
|
layout.addLayout(btn_row)
|
|
|
|
self._edit.setFocus()
|
|
|
|
def _on_text_changed(self):
|
|
self._timer.start()
|
|
|
|
def _make_sep(self, text):
|
|
sep = QListWidgetItem(text)
|
|
sep.setForeground(QColor(MUTED))
|
|
sep.setFlags(Qt.ItemFlag.ItemIsEnabled)
|
|
sep.setData(Qt.ItemDataRole.UserRole, None)
|
|
return sep
|
|
|
|
def _load_suggestions(self, prefix):
|
|
try:
|
|
from local.local_db import (
|
|
get_alt_dances_for_song_with_ratings,
|
|
get_community_alts_for_song,
|
|
get_dance_suggestions,
|
|
)
|
|
self._list.clear()
|
|
song_id = self._song.get("id", "")
|
|
|
|
# ── Mine egne alternativ-danse ──
|
|
own_alts = get_alt_dances_for_song_with_ratings(song_id)
|
|
own_names = {a["name"].lower() for a in own_alts}
|
|
matching_own = [a for a in own_alts
|
|
if not prefix or prefix.lower() in a["name"].lower()]
|
|
if matching_own:
|
|
self._list.addItem(self._make_sep(
|
|
f"── 🟢 Mine alternativ-danse ──"
|
|
))
|
|
for a in matching_own:
|
|
my_r = a.get("user_rating")
|
|
my_s = make_stars(my_r)
|
|
name = a["name"]
|
|
level = a.get("level_name", "")
|
|
disp = f"{name} / {level}" if level else name
|
|
# Venstre: navn, højre: mine stjerner
|
|
label = f"🟢 {disp:<40} {my_s}"
|
|
item = QListWidgetItem()
|
|
item.setSizeHint(__import__('PyQt6.QtCore', fromlist=['QSize']).QSize(0, 34))
|
|
item.setData(Qt.ItemDataRole.UserRole, {
|
|
"name": name, "level": level,
|
|
"choreo": a.get("choreographer", ""),
|
|
"my_rating": my_r, "comm_rating": None,
|
|
"dance_id": a["id"], "is_own": True,
|
|
})
|
|
self._list.addItem(item)
|
|
# Widget med navn + klikbare stjerner
|
|
w = QWidget()
|
|
wl = QHBoxLayout(w)
|
|
wl.setContentsMargins(4, 0, 4, 0)
|
|
wl.setSpacing(6)
|
|
lbl_name = QLabel(f"🟢 {disp}")
|
|
lbl_name.setStyleSheet(f"color: {GREEN};")
|
|
wl.addWidget(lbl_name, stretch=1)
|
|
stars_w = StarRatingWidget(my_r, color=GREEN)
|
|
stars_w.rating_changed.connect(
|
|
lambda r, song_id=self._song.get("id",""), d_id=a["id"]:
|
|
self._save_rating(song_id, d_id, r)
|
|
)
|
|
wl.addWidget(stars_w)
|
|
self._list.setItemWidget(item, w)
|
|
|
|
# ── Community alternativ-danse ──
|
|
comm_alts = get_community_alts_for_song(song_id)
|
|
matching_comm = [c for c in comm_alts
|
|
if (not prefix or prefix.lower() in c["name"].lower())
|
|
and c["name"].lower() not in own_names]
|
|
if matching_comm:
|
|
self._list.addItem(self._make_sep("── 🟡 Community ──"))
|
|
for c in matching_comm:
|
|
comm_r = c.get("avg_rating")
|
|
my_r = c.get("my_rating")
|
|
from PyQt6.QtCore import QSize
|
|
name = c["name"]
|
|
level = c.get("level_name", "")
|
|
disp = f"{name} / {level}" if level else name
|
|
item = QListWidgetItem()
|
|
item.setSizeHint(QSize(0, 34))
|
|
item.setData(Qt.ItemDataRole.UserRole, {
|
|
"name": name, "level": level,
|
|
"choreo": c.get("choreographer", ""),
|
|
"my_rating": my_r, "comm_rating": comm_r,
|
|
"dance_id": c["id"], "is_community": True,
|
|
})
|
|
self._list.addItem(item)
|
|
# Widget: navn + community stjerner (ikke klikbare) + mine (klikbare)
|
|
w = QWidget()
|
|
wl = QHBoxLayout(w)
|
|
wl.setContentsMargins(4, 0, 4, 0)
|
|
wl.setSpacing(6)
|
|
lbl_name = QLabel(f"🟡 {disp}")
|
|
lbl_name.setStyleSheet(f"color: {YELLOW};")
|
|
wl.addWidget(lbl_name, stretch=1)
|
|
# Community rating — read-only label
|
|
comm_lbl = QLabel(make_stars(comm_r) if comm_r else "☆☆☆☆☆")
|
|
comm_lbl.setStyleSheet(f"color: {YELLOW}; font-size: 13px;")
|
|
comm_lbl.setToolTip(f"Community: {comm_r:.1f}/5" if comm_r else "Ingen community rating")
|
|
wl.addWidget(comm_lbl)
|
|
# Min rating — klikbar
|
|
my_stars_w = StarRatingWidget(my_r, color=GREEN)
|
|
my_stars_w.rating_changed.connect(
|
|
lambda r, song_id=self._song.get("id",""), d_id=c["id"]:
|
|
self._save_rating(song_id, d_id, r)
|
|
)
|
|
wl.addWidget(my_stars_w)
|
|
self._list.setItemWidget(item, w)
|
|
|
|
# ── Alle danse ──
|
|
suggestions = get_dance_suggestions(prefix or "", limit=20)
|
|
if suggestions:
|
|
self._list.addItem(self._make_sep("── Alle danse ──"))
|
|
for s in suggestions:
|
|
s = dict(s)
|
|
name = s["name"]
|
|
is_own = name.lower() in own_names
|
|
is_comm = any(c["name"].lower() == name.lower() for c in comm_alts)
|
|
icon = "🟢 " if is_own else "🟡 " if is_comm else " "
|
|
color = GREEN if is_own else YELLOW if is_comm else "#eceef4"
|
|
disp = name
|
|
if s.get("level_name"):
|
|
disp += f" / {s['level_name']}"
|
|
if s.get("choreographer"):
|
|
disp += f" · {s['choreographer']}"
|
|
item = QListWidgetItem(f"{icon}{disp}")
|
|
item.setForeground(QColor(color))
|
|
item.setData(Qt.ItemDataRole.UserRole, {
|
|
"name": name,
|
|
"level": s.get("level_name", ""),
|
|
"choreo": s.get("choreographer", ""),
|
|
"my_rating": None, "comm_rating": None,
|
|
"dance_id": s.get("id"),
|
|
})
|
|
self._list.addItem(item)
|
|
|
|
except Exception as e:
|
|
import logging
|
|
logging.getLogger(__name__).warning(
|
|
f"AltDancePicker fejl: {e}", exc_info=True
|
|
)
|
|
|
|
def _on_item_clicked(self, item):
|
|
data = item.data(Qt.ItemDataRole.UserRole)
|
|
if not data:
|
|
return
|
|
self._chosen_dance = data.get("name", "")
|
|
self._edit.setText(self._chosen_dance)
|
|
parts = []
|
|
if data.get("level"):
|
|
parts.append(data["level"])
|
|
if data.get("choreo"):
|
|
parts.append(data["choreo"])
|
|
info = " · ".join(parts)
|
|
comm_r = data.get("comm_rating")
|
|
my_r = data.get("my_rating")
|
|
if comm_r:
|
|
info += f" 🟡 Community: {make_stars(comm_r)} ({comm_r:.1f})"
|
|
if my_r:
|
|
info += f" 🟢 Min: {make_stars(my_r)}"
|
|
self._info_lbl.setText(info)
|
|
|
|
def _on_selected(self, item):
|
|
data = item.data(Qt.ItemDataRole.UserRole)
|
|
if not data:
|
|
return
|
|
self._on_item_clicked(item)
|
|
self._on_accept()
|
|
|
|
def _on_accept(self):
|
|
self._chosen_dance = self._edit.text().strip()
|
|
self.accept()
|
|
|
|
def _save_rating(self, song_id: str, dance_id: int, rating: int):
|
|
"""Gem rating direkte fra stjerne-widget i listen."""
|
|
try:
|
|
from local.local_db import get_db
|
|
with get_db() as conn:
|
|
conn.execute(
|
|
"UPDATE song_alt_dances SET user_rating=? WHERE song_id=? AND dance_id=?",
|
|
(rating, song_id, dance_id)
|
|
)
|
|
except Exception as e:
|
|
import logging
|
|
logging.getLogger(__name__).warning(f"save_rating fejl: {e}")
|
|
|
|
def _on_clear(self):
|
|
self._chosen_dance = ""
|
|
self._chosen_rating = None
|
|
self._cleared = True
|
|
self.accept()
|
|
|
|
def get_dance(self) -> str:
|
|
return self._chosen_dance
|
|
|
|
def get_rating(self):
|
|
return self._chosen_rating
|
|
|
|
def was_cleared(self) -> bool:
|
|
return self._cleared |