En del opdateringer

This commit is contained in:
2026-04-19 00:58:48 +02:00
parent efe3739626
commit e4ab9caab6
14 changed files with 3412 additions and 189 deletions

View File

@@ -1,5 +1,5 @@
"""
dance_picker_dialog.py — Dialog til at vælge eller skrive en dans med autoudfyld.
dance_picker_dialog.py — Dialog til at vælge dans og koreograf med autoudfyld.
"""
from PyQt6.QtWidgets import (
@@ -10,16 +10,18 @@ from PyQt6.QtCore import Qt, QTimer
class DancePickerDialog(QDialog):
def __init__(self, current_dance: str = "", song_title: str = "", parent=None):
def __init__(self, current_dance: str = "", current_choreo: str = "",
song_title: str = "", parent=None):
super().__init__(parent)
self._chosen = current_dance
self._chosen_dance = current_dance
self._chosen_choreo = current_choreo
self.setWindowTitle("Vælg dans")
self.setMinimumWidth(380)
self.setFixedWidth(420)
self._build_ui(current_dance, song_title)
self._load_suggestions("")
self.setMinimumWidth(400)
self.setFixedWidth(440)
self._build_ui(current_dance, current_choreo, song_title)
self._load_dance_suggestions("")
def _build_ui(self, current_dance: str, song_title: str):
def _build_ui(self, current_dance: str, current_choreo: str, song_title: str):
layout = QVBoxLayout(self)
layout.setContentsMargins(12, 12, 12, 12)
layout.setSpacing(8)
@@ -30,37 +32,65 @@ class DancePickerDialog(QDialog):
lbl.setWordWrap(True)
layout.addWidget(lbl)
lbl2 = QLabel("Vælg eller skriv dans-navn:")
# ── Dans ──────────────────────────────────────────────────────────────
lbl2 = QLabel("Dans:")
lbl2.setObjectName("track_meta")
layout.addWidget(lbl2)
# Søgefelt med autoudfyld
self._edit = QLineEdit()
self._edit.setText(current_dance)
self._edit.setPlaceholderText("Skriv dans-navn...")
self._edit.selectAll()
self._edit.textChanged.connect(self._on_text_changed)
self._edit.returnPressed.connect(self._on_accept)
layout.addWidget(self._edit)
self._edit_dance = QLineEdit()
self._edit_dance.setText(current_dance)
self._edit_dance.setPlaceholderText("Skriv dans-navn...")
self._edit_dance.selectAll()
self._edit_dance.textChanged.connect(self._on_dance_text_changed)
self._edit_dance.returnPressed.connect(lambda: self._edit_choreo.setFocus())
layout.addWidget(self._edit_dance)
# Liste med forslag
self._suggestion_list = QListWidget()
self._suggestion_list.setMaximumHeight(180)
self._suggestion_list.itemDoubleClicked.connect(self._on_item_selected)
self._suggestion_list.itemClicked.connect(
lambda item: self._edit.setText(item.text())
self._dance_list = QListWidget()
self._dance_list.setMaximumHeight(160)
self._dance_list.itemDoubleClicked.connect(self._on_dance_selected)
self._dance_list.itemClicked.connect(
lambda item: self._edit_dance.setText(
item.data(Qt.ItemDataRole.UserRole) or item.text().split(" / ")[0]
)
)
layout.addWidget(self._suggestion_list)
layout.addWidget(self._dance_list)
# Debounce timer
self._timer = QTimer(self)
self._timer.setSingleShot(True)
self._timer.setInterval(200)
self._timer.timeout.connect(
lambda: self._load_suggestions(self._edit.text().strip())
# ── Koreograf ─────────────────────────────────────────────────────────
lbl3 = QLabel("Koreograf (valgfri):")
lbl3.setObjectName("track_meta")
layout.addWidget(lbl3)
self._edit_choreo = QLineEdit()
self._edit_choreo.setText(current_choreo)
self._edit_choreo.setPlaceholderText("Koreografens navn...")
self._edit_choreo.textChanged.connect(self._on_choreo_text_changed)
self._edit_choreo.returnPressed.connect(self._on_accept)
layout.addWidget(self._edit_choreo)
self._choreo_list = QListWidget()
self._choreo_list.setMaximumHeight(100)
self._choreo_list.itemDoubleClicked.connect(self._on_choreo_selected)
self._choreo_list.itemClicked.connect(
lambda item: self._edit_choreo.setText(item.text())
)
layout.addWidget(self._choreo_list)
# ── Debounce timere ───────────────────────────────────────────────────
self._dance_timer = QTimer(self)
self._dance_timer.setSingleShot(True)
self._dance_timer.setInterval(200)
self._dance_timer.timeout.connect(
lambda: self._load_dance_suggestions(self._edit_dance.text().strip())
)
# Knapper
self._choreo_timer = QTimer(self)
self._choreo_timer.setSingleShot(True)
self._choreo_timer.setInterval(200)
self._choreo_timer.timeout.connect(
lambda: self._load_choreo_suggestions(self._edit_choreo.text().strip())
)
# ── Knapper ───────────────────────────────────────────────────────────
btn_row = QHBoxLayout()
btn_row.addStretch()
btn_cancel = QPushButton("Annuller")
@@ -72,34 +102,62 @@ class DancePickerDialog(QDialog):
btn_row.addWidget(btn_ok)
layout.addLayout(btn_row)
self._edit.setFocus()
self._edit_dance.setFocus()
def _on_text_changed(self, text: str):
self._timer.start()
def _on_dance_text_changed(self):
self._dance_timer.start()
def _load_suggestions(self, prefix: str):
def _on_choreo_text_changed(self):
self._choreo_timer.start()
def _load_dance_suggestions(self, prefix: str):
try:
from local.local_db import get_dance_suggestions
suggestions = get_dance_suggestions(prefix or "", limit=20)
self._suggestion_list.clear()
self._dance_list.clear()
for s in suggestions:
label = f"{s['name']} / {s['level_name']}" if s.get("level_name") else s["name"]
if s.get("choreographer"):
label += f" ({s['choreographer']})"
item = QListWidgetItem(label)
item.setData(Qt.ItemDataRole.UserRole, s["name"])
self._suggestion_list.addItem(item)
item.setData(Qt.ItemDataRole.UserRole + 1, s.get("choreographer", ""))
self._dance_list.addItem(item)
except Exception:
pass
def _on_item_selected(self, item: QListWidgetItem):
name = item.data(Qt.ItemDataRole.UserRole) or item.text().split(" / ")[0]
self._edit.setText(name)
self._chosen = name
def _load_choreo_suggestions(self, prefix: str):
try:
from local.local_db import get_choreographer_suggestions
suggestions = get_choreographer_suggestions(prefix or "", limit=15)
self._choreo_list.clear()
for name in suggestions:
self._choreo_list.addItem(QListWidgetItem(name))
except Exception:
pass
def _on_dance_selected(self, item: QListWidgetItem):
name = item.data(Qt.ItemDataRole.UserRole) or item.text().split(" / ")[0]
choreo = item.data(Qt.ItemDataRole.UserRole + 1) or ""
self._edit_dance.setText(name)
if choreo and not self._edit_choreo.text().strip():
self._edit_choreo.setText(choreo)
self._chosen_dance = name
self._chosen_choreo = self._edit_choreo.text().strip()
self.accept()
def _on_choreo_selected(self, item: QListWidgetItem):
self._edit_choreo.setText(item.text())
self._choreo_list.clear()
def _on_accept(self):
self._chosen = self._edit.text().strip()
if self._chosen:
self._chosen_dance = self._edit_dance.text().strip()
self._chosen_choreo = self._edit_choreo.text().strip()
if self._chosen_dance:
self.accept()
def get_dance(self) -> str:
return self._chosen
return self._chosen_dance
def get_choreo(self) -> str:
return self._chosen_choreo

View File

@@ -292,12 +292,16 @@ class LibraryPanel(QWidget):
def _matches(self, song: dict, q: str, incl_alt: bool = False) -> bool:
fields = [
song.get("title", ""), song.get("artist", ""),
song.get("album", ""), song.get("file_format", ""),
] + song.get("dances", [])
song.get("title", ""),
song.get("artist", ""),
song.get("album", ""),
song.get("file_format", ""),
] + song.get("dances", []) \
+ song.get("dance_choreographers", []) \
+ song.get("dance_levels", [])
if incl_alt:
fields += song.get("alt_dances", [])
return any(q in f.lower() for f in fields)
return any(q in f.lower() for f in fields if f)
def _render(self):
self._list.clear()

View File

@@ -162,6 +162,15 @@ class MainWindow(QMainWindow):
act_quit.triggered.connect(self.close)
file_menu.addAction(act_quit)
# ── Danse ─────────────────────────────────────────────────────────────
dance_menu = menubar.addMenu("Danse")
act_new_dance = QAction("Opret dans...", self)
act_new_dance.setShortcut("Ctrl+D")
act_new_dance.setToolTip("Opret en dans i databasen uden at knytte den til musik")
act_new_dance.triggered.connect(self._create_dance_dialog)
dance_menu.addAction(act_new_dance)
# ── Ingen Danseliste- eller Visning-menu ──────────────────────────────
# Ny/Gem/Hent ligger direkte i danseliste-panelet
# Tema-skift ligger i topbar-knappen
@@ -178,6 +187,16 @@ class MainWindow(QMainWindow):
self.setStatusBar(self._statusbar)
self._statusbar.showMessage("Klar")
# Versionsnummer permanent til højre
try:
from main import APP_VERSION
except Exception:
APP_VERSION = "0.8.1"
version_lbl = QLabel(f"v{APP_VERSION}")
version_lbl.setObjectName("result_count")
version_lbl.setContentsMargins(0, 0, 8, 0)
self._statusbar.addPermanentWidget(version_lbl)
def _set_status(self, text: str, timeout_ms: int = 0):
"""Vis besked i statuslinjen. timeout_ms=0 = permanent."""
self._statusbar.showMessage(text, timeout_ms)
@@ -481,9 +500,10 @@ class MainWindow(QMainWindow):
SELECT s.id, s.title, s.artist, s.album, s.bpm,
s.duration_sec, s.local_path, s.file_format,
s.file_missing,
GROUP_CONCAT(d.name, ',') AS dance_names,
GROUP_CONCAT(COALESCE(dl.name,''), ',') AS dance_levels,
GROUP_CONCAT(DISTINCT ad.name) AS alt_dance_names
GROUP_CONCAT(d.name, ',') AS dance_names,
GROUP_CONCAT(COALESCE(dl.name,''), ',') AS dance_levels,
GROUP_CONCAT(COALESCE(d.choreographer,''), ',') AS dance_choreographers,
GROUP_CONCAT(DISTINCT ad.name) AS alt_dance_names
FROM songs s
LEFT JOIN song_dances sd ON sd.song_id = s.id
LEFT JOIN dances d ON d.id = sd.dance_id
@@ -498,22 +518,24 @@ class MainWindow(QMainWindow):
songs = []
for row in rows:
dances = row["dance_names"].split(",") if row["dance_names"] else []
levels = row["dance_levels"].split(",") if row["dance_levels"] else []
alt_dances = row["alt_dance_names"].split(",") if row["alt_dance_names"] else []
dances = row["dance_names"].split(",") if row["dance_names"] else []
levels = row["dance_levels"].split(",") if row["dance_levels"] else []
choreos = row["dance_choreographers"].split(",") if row["dance_choreographers"] else []
alt_dances = row["alt_dance_names"].split(",") if row["alt_dance_names"] else []
songs.append({
"id": row["id"],
"title": row["title"],
"artist": row["artist"],
"album": row["album"],
"bpm": row["bpm"],
"duration_sec": row["duration_sec"],
"local_path": row["local_path"],
"file_format": row["file_format"],
"file_missing": bool(row["file_missing"]),
"dances": dances,
"dance_levels": levels,
"alt_dances": alt_dances,
"id": row["id"],
"title": row["title"],
"artist": row["artist"],
"album": row["album"],
"bpm": row["bpm"],
"duration_sec": row["duration_sec"],
"local_path": row["local_path"],
"file_format": row["file_format"],
"file_missing": bool(row["file_missing"]),
"dances": dances,
"dance_levels": levels,
"dance_choreographers": choreos,
"alt_dances": alt_dances,
})
self._library_loaded.emit(songs)
except Exception:
@@ -652,6 +674,66 @@ class MainWindow(QMainWindow):
except Exception as e:
self._set_status(f"Fejl ved tilføjelse: {e}")
def _create_dance_dialog(self):
"""Opret en dans i databasen — fritliggende, uden tilknytning til musik."""
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QFormLayout, QLineEdit,
QComboBox, QDialogButtonBox, QMessageBox
)
try:
from local.local_db import get_dance_levels, get_or_create_dance
except Exception as e:
QMessageBox.warning(self, "Fejl", str(e))
return
levels = [dict(r) for r in get_dance_levels()]
dlg = QDialog(self)
dlg.setWindowTitle("Opret dans")
dlg.setFixedWidth(380)
layout = QVBoxLayout(dlg)
layout.setSpacing(8)
layout.setContentsMargins(14, 14, 14, 14)
form = QFormLayout()
form.setSpacing(8)
name_edit = QLineEdit()
name_edit.setPlaceholderText("f.eks. Cowboy Cha Cha")
form.addRow("Dans-navn:", name_edit)
level_cb = QComboBox()
level_cb.addItem("— intet niveau —", None)
for lvl in levels:
level_cb.addItem(lvl["name"], lvl["id"])
form.addRow("Niveau:", level_cb)
choreo_edit = QLineEdit()
choreo_edit.setPlaceholderText("Koreografens navn (valgfri)")
form.addRow("Koreograf:", choreo_edit)
layout.addLayout(form)
btns = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok |
QDialogButtonBox.StandardButton.Cancel
)
btns.accepted.connect(dlg.accept)
btns.rejected.connect(dlg.reject)
layout.addWidget(btns)
name_edit.setFocus()
if dlg.exec():
name = name_edit.text().strip()
choreo = choreo_edit.text().strip()
level = level_cb.currentData()
if name:
try:
get_or_create_dance(name, level, choreographer=choreo)
self._set_status(f'Dans "{name}" oprettet', 3000)
except Exception as e:
QMessageBox.warning(self, "Fejl", f"Kunne ikke oprette dans:\n{e}")
def _open_settings(self):
dialog = SettingsDialog(parent=self)
if dialog.exec():

View File

@@ -732,15 +732,19 @@ class PlaylistPanel(QWidget):
if not current:
dances = song.get("dances", [])
current = dances[0] if dances else ""
current_choreo = song.get("active_choreo", "")
dlg = DancePickerDialog(
current_dance=current,
current_choreo=current_choreo,
song_title=song.get("title", ""),
parent=self.window()
)
if dlg.exec():
chosen = dlg.get_dance()
choreo = dlg.get_choreo()
if chosen:
song["active_dance"] = chosen
song["active_dance"] = chosen
song["active_choreo"] = choreo
self._refresh()
self._sync_dance_to_db(idx, song)

View File

@@ -47,8 +47,8 @@ class TagEditorDialog(QDialog):
self._alts = [] # fra DB: {dance_id, name, level_id, level_name, note}
self.setWindowTitle(f"Rediger tags — {song.get('title', '')}")
self.setMinimumSize(720, 500)
self.resize(820, 580)
self.setMinimumSize(860, 520)
self.resize(980, 600)
self._load_levels()
self._load_existing()
@@ -97,11 +97,11 @@ class TagEditorDialog(QDialog):
hint.setWordWrap(True)
layout.addWidget(hint)
# To kolonner
# To kolonner — hoveddanse får mere plads
cols = QHBoxLayout()
cols.setSpacing(12)
cols.addWidget(self._build_dances_panel())
cols.addWidget(self._build_alts_panel())
cols.addWidget(self._build_dances_panel(), stretch=3)
cols.addWidget(self._build_alts_panel(), stretch=2)
layout.addLayout(cols, stretch=1)
btn_row = QHBoxLayout()
@@ -132,11 +132,11 @@ class TagEditorDialog(QDialog):
layout.addWidget(scroll, stretch=1)
self._dance_rows = []
for d in self._dances:
self._add_dance_row(d["name"], d["level_id"])
self._add_dance_row(d["name"], d["level_id"], d.get("choreographer", ""))
# Søgefelt
self._new_dance = QLineEdit()
self._new_dance.setPlaceholderText(_("tags.new_dance"))
self._new_dance.setPlaceholderText("Søg dans eller koreograf...")
self._new_dance.textChanged.connect(self._on_dance_search)
self._new_dance.returnPressed.connect(self._on_add_dance)
layout.addWidget(self._new_dance)
@@ -162,7 +162,7 @@ class TagEditorDialog(QDialog):
self._load_dance_suggestions("", self._dance_suggestions)
return grp
def _add_dance_row(self, name="", level_id=None):
def _add_dance_row(self, name="", level_id=None, choreographer=""):
try:
from translations import _, translate_level
except Exception:
@@ -175,14 +175,13 @@ class TagEditorDialog(QDialog):
edit = DanceLineEdit("Dans...", self)
edit.setText(name)
row_layout.addWidget(edit, stretch=1)
row_layout.addWidget(edit, stretch=2)
# Niveau-dropdown
level_cb = QComboBox()
level_cb.addItem(_("tags.no_level"), None)
for lvl in self._levels:
level_cb.addItem(translate_level(lvl["name"]), lvl["id"])
# Sæt til det rigtige niveau
if level_id is not None:
for i in range(level_cb.count()):
if level_cb.itemData(i) == level_id:
@@ -191,24 +190,102 @@ class TagEditorDialog(QDialog):
level_cb.setFixedWidth(130)
row_layout.addWidget(level_cb)
# Når autoudfyld vælger — opdater dropdown
def on_dance_selected(dance_info, cb=level_cb):
# Koreograf-felt med autocomplete
choreo_edit = QLineEdit()
choreo_edit.setText(choreographer)
choreo_edit.setPlaceholderText("Koreograf...")
choreo_edit.setFixedWidth(140)
choreo_edit.textChanged.connect(
lambda txt, ce=choreo_edit: self._show_choreo_suggestions(txt, ce)
)
row_layout.addWidget(choreo_edit)
# Når autoudfyld vælger — opdater dropdown og koreograf
def on_dance_selected(dance_info, cb=level_cb, ce=choreo_edit):
if dance_info.get("level_id") is not None:
for i in range(cb.count()):
if cb.itemData(i) == dance_info["level_id"]:
cb.setCurrentIndex(i)
break
if dance_info.get("choreographer") and not ce.text().strip():
ce.setText(dance_info["choreographer"])
edit.dance_selected.connect(on_dance_selected)
btn_info = QPushButton("•••")
btn_info.setFixedSize(36, 24)
btn_info.setToolTip("Åbn dans-info (link, video, noter)")
btn_info.setObjectName("btn_info_row")
btn_info.style().unpolish(btn_info)
btn_info.style().polish(btn_info)
row_layout.addWidget(btn_info)
btn_rm = QPushButton("")
btn_rm.setFixedSize(24, 24)
btn_rm.setFixedSize(32, 24)
btn_rm.setToolTip("Fjern dans")
btn_rm.setObjectName("btn_rm_row")
btn_rm.style().unpolish(btn_rm)
btn_rm.style().polish(btn_rm)
row_layout.addWidget(btn_rm)
idx = self._dance_layout.count() - 1
self._dance_layout.insertWidget(idx, row_widget)
entry = {"widget": row_widget, "edit": edit, "level": level_cb}
entry = {"widget": row_widget, "edit": edit, "level": level_cb, "choreo": choreo_edit}
self._dance_rows.append(entry)
btn_rm.clicked.connect(lambda: self._remove_dance_row(entry))
btn_info.clicked.connect(lambda: self._open_dance_info(entry))
def _create_dance(self):
"""Opret en ny dans i databasen uden at knytte den til musik."""
from PyQt6.QtWidgets import QDialog, QFormLayout, QDialogButtonBox
from PyQt6.QtCore import Qt
dlg = QDialog(self)
dlg.setWindowTitle("Opret dans")
dlg.setFixedWidth(360)
layout = QVBoxLayout(dlg)
layout.setSpacing(8)
form = QFormLayout()
name_edit = QLineEdit()
name_edit.setPlaceholderText("f.eks. Cowboy Cha Cha")
form.addRow("Dans-navn:", name_edit)
from PyQt6.QtWidgets import QComboBox
level_cb = QComboBox()
level_cb.addItem("— intet niveau —", None)
for lvl in self._levels:
level_cb.addItem(lvl["name"], lvl["id"])
form.addRow("Niveau:", level_cb)
choreo_edit = QLineEdit()
choreo_edit.setPlaceholderText("Koreografens navn (valgfri)")
form.addRow("Koreograf:", choreo_edit)
layout.addLayout(form)
btns = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok |
QDialogButtonBox.StandardButton.Cancel
)
btns.accepted.connect(dlg.accept)
btns.rejected.connect(dlg.reject)
layout.addWidget(btns)
name_edit.setFocus()
if dlg.exec():
name = name_edit.text().strip()
choreo = choreo_edit.text().strip()
level = level_cb.currentData()
if name:
try:
from local.local_db import get_or_create_dance
get_or_create_dance(name, level, choreographer=choreo)
# Opdater forslagslisten
self._load_dance_suggestions("", self._dance_suggestions)
self._load_existing_dance_suggestions("", self._alt_suggestions)
self._new_dance.setPlaceholderText(f'"{name}" oprettet ✓')
except Exception as e:
QMessageBox.warning(self, "Fejl", f"Kunne ikke oprette dans:\n{e}")
def _remove_dance_row(self, entry):
self._dance_rows.remove(entry)
@@ -217,9 +294,52 @@ class TagEditorDialog(QDialog):
def _on_dance_search(self):
self._dance_search_timer.start()
def _load_existing_dance_suggestions(self, prefix: str, list_widget):
"""Kun eksisterende danse fra DB — ingen nye kan oprettes herfra."""
try:
from local.local_db import get_dance_suggestions
suggestions = get_dance_suggestions(prefix, limit=20)
list_widget.clear()
for s in suggestions:
label = f"{s['name']} / {s['level_name']}" if s.get("level_name") else s["name"]
item = QListWidgetItem(label)
item.setData(Qt.ItemDataRole.UserRole, s.get("level_id"))
item.setData(Qt.ItemDataRole.UserRole + 1, s["name"])
item.setData(Qt.ItemDataRole.UserRole + 2, s.get("choreographer", ""))
list_widget.addItem(item)
except Exception:
pass
def _on_alt_search(self):
self._alt_search_timer.start()
def _open_dance_info(self, entry: dict):
"""Åbn dans-info dialog for den dans der er i denne række."""
name = entry["edit"].text().strip()
if not name:
entry["edit"].setFocus()
entry["edit"].setPlaceholderText("Skriv dans-navn først...")
return
from ui.dance_info_dialog import DanceInfoDialog
dlg = DanceInfoDialog(song=self._song, parent=self)
dlg.exec()
# Opdater koreograf-feltet fra DB bagefter
try:
from local.local_db import get_db
with get_db() as conn:
row = conn.execute(
"SELECT choreographer FROM dances "
"WHERE name=? COLLATE NOCASE LIMIT 1", (name,)
).fetchone()
if row and row["choreographer"]:
entry["choreo"].setText(row["choreographer"])
except Exception:
pass
def _show_choreo_suggestions(self, prefix: str, source_edit: 'QLineEdit'):
"""Vis autocomplete popup for koreograf direkte under feltet."""
pass # Simpel løsning: autocomplete via QCompleter nedenfor
def _load_dance_suggestions(self, prefix: str, list_widget):
try:
from local.local_db import get_dance_suggestions
@@ -227,19 +347,23 @@ class TagEditorDialog(QDialog):
list_widget.clear()
for s in suggestions:
label = f"{s['name']} / {s['level_name']}" if s.get("level_name") else s["name"]
if s.get("choreographer"):
label += f" · {s['choreographer']}"
item = QListWidgetItem(label)
item.setData(Qt.ItemDataRole.UserRole, s.get("level_id"))
item.setData(Qt.ItemDataRole.UserRole, s.get("level_id"))
item.setData(Qt.ItemDataRole.UserRole + 1, s["name"])
item.setData(Qt.ItemDataRole.UserRole + 2, s.get("choreographer", ""))
list_widget.addItem(item)
except Exception:
pass
def _add_from_suggestion(self, item, panel: str):
"""Tilføj dans fra forslags-listen ved klik."""
name = item.data(Qt.ItemDataRole.UserRole + 1) or item.text().split(" / ")[0]
level_id = item.data(Qt.ItemDataRole.UserRole)
name = item.data(Qt.ItemDataRole.UserRole + 1) or item.text().split(" / ")[0]
level_id = item.data(Qt.ItemDataRole.UserRole)
choreographer = item.data(Qt.ItemDataRole.UserRole + 2) or ""
if panel == "dance":
self._add_dance_row(name, level_id)
self._add_dance_row(name, level_id, choreographer)
self._new_dance.clear()
self._new_dance.setFocus()
self._load_dance_suggestions("", self._dance_suggestions)
@@ -273,6 +397,8 @@ class TagEditorDialog(QDialog):
from translations import _
grp = QGroupBox(_("tags.alts"))
layout = QVBoxLayout(grp)
# Eksisterende alternativ-rækker
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.Shape.NoFrame)
@@ -286,8 +412,13 @@ class TagEditorDialog(QDialog):
for a in self._alts:
self._add_alt_row(a["name"], a["level_id"], a.get("note", ""))
# Søgefelt — kun eksisterende danse
hint = QLabel("Søg blandt eksisterende danse:")
hint.setObjectName("result_count")
layout.addWidget(hint)
self._new_alt = QLineEdit()
self._new_alt.setPlaceholderText(_("tags.new_alt"))
self._new_alt.setPlaceholderText("Søg dans...")
self._new_alt.textChanged.connect(self._on_alt_search)
self._new_alt.returnPressed.connect(self._on_add_alt)
layout.addWidget(self._new_alt)
@@ -304,11 +435,11 @@ class TagEditorDialog(QDialog):
self._alt_search_timer.setSingleShot(True)
self._alt_search_timer.setInterval(150)
self._alt_search_timer.timeout.connect(
lambda: self._load_dance_suggestions(
lambda: self._load_existing_dance_suggestions(
self._new_alt.text().strip(), self._alt_suggestions
)
)
self._load_dance_suggestions("", self._alt_suggestions)
self._load_existing_dance_suggestions("", self._alt_suggestions)
return grp
def _add_alt_row(self, name="", level_id=None, note=""):
@@ -326,44 +457,34 @@ class TagEditorDialog(QDialog):
lbl.setObjectName("track_meta")
row_layout.addWidget(lbl)
edit = DanceLineEdit("Dans...", self)
edit.setText(name)
row_layout.addWidget(edit, stretch=1)
# Vis dans-navn — ikke redigerbart, kun valgt fra listen
lbl_name = QLabel(name)
lbl_name.setObjectName("track_title")
row_layout.addWidget(lbl_name, stretch=1)
# Niveau-dropdown
level_cb = QComboBox()
level_cb.addItem(_("tags.no_level"), None)
# Niveau (read-only label)
level_name = ""
for lvl in self._levels:
level_cb.addItem(translate_level(lvl["name"]), lvl["id"])
if level_id is not None:
for i in range(level_cb.count()):
if level_cb.itemData(i) == level_id:
level_cb.setCurrentIndex(i)
break
level_cb.setFixedWidth(130)
row_layout.addWidget(level_cb)
def on_dance_selected(dance_info, cb=level_cb):
if dance_info.get("level_id") is not None:
for i in range(cb.count()):
if cb.itemData(i) == dance_info["level_id"]:
cb.setCurrentIndex(i)
break
edit.dance_selected.connect(on_dance_selected)
note_edit = QLineEdit()
note_edit.setPlaceholderText(_("tags.note"))
note_edit.setText(note)
note_edit.setFixedWidth(80)
row_layout.addWidget(note_edit)
if lvl["id"] == level_id:
level_name = lvl["name"]
break
if level_name:
lbl_level = QLabel(level_name)
lbl_level.setObjectName("result_count")
lbl_level.setFixedWidth(110)
row_layout.addWidget(lbl_level)
btn_rm = QPushButton("")
btn_rm.setFixedSize(24, 24)
btn_rm.setFixedSize(32, 24)
btn_rm.setToolTip("Fjern alternativ-dans")
btn_rm.setObjectName("btn_rm_row")
btn_rm.style().unpolish(btn_rm)
btn_rm.style().polish(btn_rm)
row_layout.addWidget(btn_rm)
idx = self._alt_layout.count() - 1
self._alt_layout.insertWidget(idx, row_widget)
entry = {"widget": row_widget, "edit": edit, "level": level_cb, "note": note_edit}
entry = {"widget": row_widget, "name": name, "level_id": level_id}
self._alt_rows.append(entry)
btn_rm.clicked.connect(lambda: self._remove_alt_row(entry))
@@ -372,12 +493,10 @@ class TagEditorDialog(QDialog):
entry["widget"].deleteLater()
def _on_add_alt(self):
text = self._new_alt.text().strip()
if text:
name, level_id = self._parse_name_level(text)
self._add_alt_row(name, level_id)
self._new_alt.clear()
self._load_dance_suggestions("", self._alt_suggestions)
"""Alternativ-danse kan kun tilføjes fra forslagslisten, ikke som fri tekst."""
if self._alt_suggestions.count() > 0:
self._alt_suggestions.setCurrentRow(0)
self._add_from_suggestion(self._alt_suggestions.currentItem(), "alt")
def _save(self):
song_id = self._song.get("id")
@@ -387,24 +506,25 @@ class TagEditorDialog(QDialog):
from local.local_db import new_conn, get_or_create_dance
from local.tag_reader import write_dances, can_write_dances
# Saml data fra UI — niveau kommer fra dropdown, ikke fra tekstfeltet
# Saml data fra UI
dances = []
for row in self._dance_rows:
name = row["edit"].text().strip()
if name:
dances.append({
"name": name,
"level_id": row["level"].currentData(),
"name": name,
"level_id": row["level"].currentData(),
"choreographer": row["choreo"].text().strip(),
})
alts = []
for row in self._alt_rows:
name = row["edit"].text().strip()
name = row.get("name", "")
if name:
alts.append({
"name": name,
"level_id": row["level"].currentData(),
"note": row["note"].text().strip(),
"level_id": row.get("level_id"),
"note": "",
})
conn = new_conn()
@@ -415,7 +535,8 @@ class TagEditorDialog(QDialog):
# Indsæt hoveddanse
for i, d in enumerate(dances, 1):
dance_id = get_or_create_dance(d["name"], d["level_id"], conn)
dance_id = get_or_create_dance(d["name"], d["level_id"], conn,
choreographer=d.get("choreographer", ""))
conn.execute(
"INSERT OR IGNORE INTO song_dances (song_id, dance_id, dance_order) "
"VALUES (?,?,?)",

View File

@@ -79,6 +79,24 @@ QPushButton#btn_demo:hover, QPushButton#btn_demo:checked {
color: #111214;
border-color: #3b8fd4;
}
QPushButton#btn_info_row {
color: #3b8fd4;
border-color: #3b8fd4;
padding: 2px 6px;
}
QPushButton#btn_info_row:hover {
background-color: rgba(59,143,212,0.15);
border-color: #3b8fd4;
}
QPushButton#btn_rm_row {
color: #e74c3c;
border-color: #e74c3c;
padding: 2px 6px;
}
QPushButton#btn_rm_row:hover {
background-color: rgba(231,76,60,0.15);
border-color: #e74c3c;
}
/* Slider */
QSlider::groove:horizontal {
@@ -328,6 +346,22 @@ QPushButton#btn_play_small {
QPushButton#btn_play_small:hover {
background-color: #a05808;
}
QPushButton#btn_info_row {
color: #1a6fb0;
border-color: #1a6fb0;
padding: 2px 6px;
}
QPushButton#btn_info_row:hover {
background-color: rgba(26,111,176,0.12);
}
QPushButton#btn_rm_row {
color: #c0392b;
border-color: #c0392b;
padding: 2px 6px;
}
QPushButton#btn_rm_row:hover {
background-color: rgba(192,57,43,0.12);
}
QListWidget {
background-color: #d8dae0;
color: #1a1c22;