Version 1
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -41,9 +41,9 @@ class DraggableLibraryList(QListWidget):
|
||||
|
||||
|
||||
class LibraryPanel(QWidget):
|
||||
song_selected = pyqtSignal(dict)
|
||||
add_to_playlist = pyqtSignal(dict)
|
||||
scan_requested = pyqtSignal()
|
||||
song_selected = pyqtSignal(dict)
|
||||
add_to_playlist = pyqtSignal(dict)
|
||||
scan_requested = pyqtSignal()
|
||||
edit_tags_requested = pyqtSignal(dict)
|
||||
send_mail_requested = pyqtSignal(dict)
|
||||
|
||||
@@ -51,6 +51,7 @@ class LibraryPanel(QWidget):
|
||||
super().__init__(parent)
|
||||
self._all_songs: list[dict] = []
|
||||
self._filtered: list[dict] = []
|
||||
self._bpm_scan_running = False
|
||||
self._search_timer = QTimer(self)
|
||||
self._search_timer.setSingleShot(True)
|
||||
self._search_timer.setInterval(150)
|
||||
@@ -70,6 +71,12 @@ class LibraryPanel(QWidget):
|
||||
header.addWidget(lbl)
|
||||
header.addStretch()
|
||||
|
||||
self._btn_bpm_scan = QPushButton("♩ BPM alle")
|
||||
self._btn_bpm_scan.setFixedHeight(24)
|
||||
self._btn_bpm_scan.setToolTip("Analysér BPM på alle sange uden BPM (kører i baggrunden)")
|
||||
self._btn_bpm_scan.clicked.connect(self._start_bulk_bpm_scan)
|
||||
header.addWidget(self._btn_bpm_scan)
|
||||
|
||||
btn_manage = QPushButton("⚙ Mapper")
|
||||
btn_manage.setFixedHeight(24)
|
||||
btn_manage.setToolTip("Tilføj, fjern og scan musikbiblioteker")
|
||||
@@ -172,7 +179,6 @@ class LibraryPanel(QWidget):
|
||||
dance_levels = song.get("dance_levels", [])
|
||||
missing = song.get("file_missing", False)
|
||||
|
||||
# Byg dans-streng med niveau hvis tilgængeligt
|
||||
dance_parts = []
|
||||
for i, d in enumerate(dances):
|
||||
lvl = dance_levels[i] if i < len(dance_levels) else ""
|
||||
@@ -183,13 +189,97 @@ class LibraryPanel(QWidget):
|
||||
bpm = song.get("bpm", 0)
|
||||
bpm_str = f"{bpm} BPM" if bpm else "? BPM"
|
||||
line2 = f" {song.get('artist','—')} · {bpm_str} · {song.get('file_format','').upper()}{dance_str}"
|
||||
item = QListWidgetItem(f"{line1}\n{line2}")
|
||||
|
||||
row_widget = QWidget()
|
||||
row_widget.setStyleSheet("background: transparent;")
|
||||
row_layout = QHBoxLayout(row_widget)
|
||||
row_layout.setContentsMargins(2, 2, 2, 2)
|
||||
row_layout.setSpacing(8)
|
||||
|
||||
lbl = QLabel(f"{line1}\n{line2}")
|
||||
lbl.setWordWrap(False)
|
||||
row_layout.addWidget(lbl, stretch=1)
|
||||
|
||||
btn_danse = QPushButton("Danse")
|
||||
btn_danse.setFixedHeight(30)
|
||||
btn_danse.setFixedWidth(70)
|
||||
btn_danse.setToolTip("Rediger dans-tags")
|
||||
btn_danse.setStyleSheet(
|
||||
"QPushButton { background: #e8a020; color: #111; border-radius: 4px; "
|
||||
"font-weight: bold; font-size: 12px; border: none; }"
|
||||
"QPushButton:hover { background: #f0b030; }"
|
||||
)
|
||||
btn_danse.clicked.connect(lambda _, s=song: self.edit_tags_requested.emit(s))
|
||||
row_layout.addWidget(btn_danse)
|
||||
|
||||
item = QListWidgetItem()
|
||||
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"))
|
||||
row_widget.adjustSize()
|
||||
hint = row_widget.sizeHint()
|
||||
hint.setHeight(max(hint.height(), 52))
|
||||
item.setSizeHint(hint)
|
||||
self._list.addItem(item)
|
||||
self._list.setItemWidget(item, row_widget)
|
||||
|
||||
def _start_bulk_bpm_scan(self):
|
||||
"""Start BPM-analyse på alle sange uden BPM i baggrundstråd med lav prioritet."""
|
||||
if self._bpm_scan_running:
|
||||
return
|
||||
songs_without_bpm = [s for s in self._all_songs
|
||||
if not s.get("bpm") and not s.get("file_missing")]
|
||||
if not songs_without_bpm:
|
||||
self._btn_bpm_scan.setText("♩ Alle har BPM")
|
||||
return
|
||||
|
||||
self._bpm_scan_running = True
|
||||
self._btn_bpm_scan.setText(f"♩ Scanner 0/{len(songs_without_bpm)}...")
|
||||
self._btn_bpm_scan.setEnabled(False)
|
||||
|
||||
from PyQt6.QtCore import QThread, pyqtSignal as _sig
|
||||
|
||||
class BulkBpmWorker(QThread):
|
||||
progress = _sig(int, int, str) # done, total, title
|
||||
finished = _sig()
|
||||
|
||||
def __init__(self, songs):
|
||||
super().__init__()
|
||||
self._songs = songs
|
||||
|
||||
def run(self):
|
||||
from local.tag_reader import analyze_and_save_bpm
|
||||
total = len(self._songs)
|
||||
for i, song in enumerate(self._songs, start=1):
|
||||
if self.isInterruptionRequested():
|
||||
break
|
||||
try:
|
||||
bpm = analyze_and_save_bpm(song["local_path"], song["id"])
|
||||
if bpm:
|
||||
song["bpm"] = int(round(bpm))
|
||||
except Exception:
|
||||
pass
|
||||
self.progress.emit(i, total, song.get("title", ""))
|
||||
self.finished.emit()
|
||||
|
||||
self._bulk_bpm_worker = BulkBpmWorker(songs_without_bpm)
|
||||
|
||||
def on_progress(done, total, title):
|
||||
self._btn_bpm_scan.setText(f"♩ {done}/{total}...")
|
||||
# Opdater sangen i listen
|
||||
for s in self._all_songs:
|
||||
if s.get("title") == title and s.get("bpm"):
|
||||
break
|
||||
self._do_search()
|
||||
|
||||
def on_finished():
|
||||
self._bpm_scan_running = False
|
||||
self._btn_bpm_scan.setEnabled(True)
|
||||
self._btn_bpm_scan.setText("♩ BPM alle")
|
||||
self._do_search()
|
||||
|
||||
self._bulk_bpm_worker.progress.connect(on_progress)
|
||||
self._bulk_bpm_worker.finished.connect(on_finished)
|
||||
self._bulk_bpm_worker.start()
|
||||
self._bulk_bpm_worker.setPriority(QThread.Priority.LowestPriority)
|
||||
|
||||
# ── Handlinger ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -26,7 +26,8 @@ class ProgressBar(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._fraction = 0.0
|
||||
self._demo_fraction = 0.0
|
||||
self._demo_fraction = 0.0 # hvor musikken stopper (blå)
|
||||
self._demo_fade_fraction = 0.0 # hvor fade slutter (grå)
|
||||
self.setFixedHeight(10)
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
|
||||
@@ -34,8 +35,9 @@ class ProgressBar(QWidget):
|
||||
self._fraction = max(0.0, min(1.0, f))
|
||||
self.update()
|
||||
|
||||
def set_demo_marker(self, f: float):
|
||||
self._demo_fraction = max(0.0, min(1.0, f))
|
||||
def set_demo_marker(self, demo_f: float, fade_f: float = 0.0):
|
||||
self._demo_fraction = max(0.0, min(1.0, demo_f))
|
||||
self._demo_fade_fraction = max(0.0, min(1.0, fade_f))
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
@@ -46,6 +48,11 @@ class ProgressBar(QWidget):
|
||||
fill_w = int(w * self._fraction)
|
||||
if fill_w > 0:
|
||||
p.fillRect(0, 0, fill_w, h, QColor("#e8a020"))
|
||||
# Fade-slut markør (grå) — vises bag demo-markøren
|
||||
if self._demo_fade_fraction > 0:
|
||||
fx = int(w * self._demo_fade_fraction)
|
||||
p.fillRect(fx - 1, 0, 2, h, QColor("#6a7080"))
|
||||
# Demo-stop markør (blå)
|
||||
if self._demo_fraction > 0:
|
||||
mx = int(w * self._demo_fraction)
|
||||
p.fillRect(mx - 1, 0, 2, h, QColor("#3b8fd4"))
|
||||
@@ -81,6 +88,7 @@ class MainWindow(QMainWindow):
|
||||
self._settings = load_settings()
|
||||
self._dark_theme = self._settings.get("dark_theme", True)
|
||||
self._demo_seconds = self._settings.get("demo_seconds", 10)
|
||||
self._demo_fade_seconds = self._settings.get("demo_fade_seconds", 5)
|
||||
|
||||
self._connect_player_signals()
|
||||
self._build_menu()
|
||||
@@ -306,12 +314,12 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self._vol_slider = QSlider(Qt.Orientation.Horizontal)
|
||||
self._vol_slider.setRange(0, 100)
|
||||
self._vol_slider.setValue(78)
|
||||
self._vol_slider.setValue(self._settings.get("volume", 78))
|
||||
self._vol_slider.setFixedWidth(100)
|
||||
self._vol_slider.valueChanged.connect(self._on_volume)
|
||||
layout.addWidget(self._vol_slider)
|
||||
|
||||
self._lbl_vol = QLabel("78")
|
||||
self._lbl_vol = QLabel(str(self._settings.get("volume", 78)))
|
||||
self._lbl_vol.setObjectName("vol_val")
|
||||
layout.addWidget(self._lbl_vol)
|
||||
|
||||
@@ -401,7 +409,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
except Exception as e:
|
||||
self._set_status(f"DB fejl: {e}")
|
||||
print(f"DB init fejl: {e}")
|
||||
pass
|
||||
|
||||
def start_scan(self):
|
||||
"""Start fuld scanning af alle biblioteker i baggrundstråd."""
|
||||
@@ -463,7 +471,7 @@ class MainWindow(QMainWindow):
|
||||
count = len(songs)
|
||||
self._set_status(f"Bibliotek: {count} sang{'e' if count != 1 else ''}", 3000)
|
||||
except Exception as e:
|
||||
print(f"Bibliotek reload fejl: {e}")
|
||||
pass
|
||||
|
||||
def add_library_path(self, path: str):
|
||||
try:
|
||||
@@ -483,6 +491,7 @@ class MainWindow(QMainWindow):
|
||||
if dialog.exec():
|
||||
self._settings = dialog.get_values()
|
||||
self._demo_seconds = self._settings.get("demo_seconds", 10)
|
||||
self._demo_fade_seconds = self._settings.get("demo_fade_seconds", 5)
|
||||
# Opdater tema hvis ændret
|
||||
new_dark = self._settings.get("dark_theme", True)
|
||||
if new_dark != self._dark_theme:
|
||||
@@ -498,7 +507,7 @@ class MainWindow(QMainWindow):
|
||||
if hasattr(self, "_current_song") and self._current_song:
|
||||
dur = self._current_song.get("duration_sec", 0)
|
||||
if dur > 0:
|
||||
self._progress.set_demo_marker(min(self._demo_seconds / dur, 1.0))
|
||||
self._progress.set_demo_marker(min(self._demo_seconds / dur, 1.0), min((self._demo_seconds + self._demo_fade_seconds) / dur, 1.0))
|
||||
self._set_status("Indstillinger gemt", 2000)
|
||||
|
||||
def _auto_login(self):
|
||||
@@ -562,7 +571,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self._set_status(f"Synkroniseret {len(levels)} niveauer og {len(names)} dans-navne", 4000)
|
||||
except Exception as e:
|
||||
print(f"Dans-sync fejl: {e}")
|
||||
pass
|
||||
|
||||
def _go_offline(self):
|
||||
self._api_url = self._api_token = self._api_username = None
|
||||
@@ -744,7 +753,7 @@ class MainWindow(QMainWindow):
|
||||
)
|
||||
|
||||
if dur > 0:
|
||||
self._progress.set_demo_marker(min(self._demo_seconds / dur, 1.0))
|
||||
self._progress.set_demo_marker(min(self._demo_seconds / dur, 1.0), min((self._demo_seconds + self._demo_fade_seconds) / dur, 1.0))
|
||||
|
||||
self._set_status(f"Indlæst: {song.get('title','—')}", 3000)
|
||||
|
||||
@@ -787,7 +796,10 @@ class MainWindow(QMainWindow):
|
||||
else:
|
||||
self._demo_active = True
|
||||
self._btn_demo.setChecked(True)
|
||||
self._player.play_demo(stop_at_sec=self._demo_seconds)
|
||||
self._player.play_demo(
|
||||
stop_at_sec=self._demo_seconds,
|
||||
fade_sec=self._demo_fade_seconds,
|
||||
)
|
||||
self._btn_play.setText("⏸")
|
||||
|
||||
def _prev_song(self):
|
||||
@@ -853,33 +865,34 @@ class MainWindow(QMainWindow):
|
||||
self._load_song(next_song)
|
||||
self._set_status(f"Klar: {next_song.get('title','')} — tryk ▶ for at starte")
|
||||
else:
|
||||
# Danseliste afsluttet — nulstil liste-markering og synkroniser
|
||||
self._current_idx = -1
|
||||
self._playlist_panel._current_idx = -1
|
||||
self._playlist_panel._song_ended = False
|
||||
self._playlist_panel._refresh()
|
||||
self._sync_event_status_to_playlist()
|
||||
self._lbl_title.setText("— Danseliste afsluttet —")
|
||||
self._lbl_meta.setText("")
|
||||
self._lbl_dances.setText("")
|
||||
self._set_status("Danselisten er afsluttet")
|
||||
|
||||
def _sync_event_status_to_playlist(self):
|
||||
"""Gem event-fremgang i den aktive navngivne liste."""
|
||||
"""Gem event-fremgang (afspillet/sprunget over) til den navngivne liste."""
|
||||
try:
|
||||
from local.local_db import get_db
|
||||
songs = self._playlist_panel.get_songs()
|
||||
pl_id = self._playlist_panel.get_named_playlist_id()
|
||||
if not pl_id:
|
||||
return
|
||||
statuses = self._playlist_panel.get_statuses()
|
||||
from local.local_db import get_db
|
||||
with get_db() as conn:
|
||||
# Find den aktive liste (ikke __aktiv__)
|
||||
pl = conn.execute(
|
||||
"SELECT id FROM playlists WHERE name != '__aktiv__' "
|
||||
"ORDER BY created_at DESC LIMIT 1"
|
||||
).fetchone()
|
||||
if not pl:
|
||||
return
|
||||
# Opdater status for hver sang i listen
|
||||
for i, (song, status) in enumerate(zip(songs, statuses)):
|
||||
conn.execute("""
|
||||
UPDATE playlist_songs SET status=?
|
||||
WHERE playlist_id=? AND song_id=?
|
||||
""", (status, pl["id"], song.get("id")))
|
||||
for position, status in enumerate(statuses, start=1):
|
||||
conn.execute(
|
||||
"UPDATE playlist_songs SET status=? "
|
||||
"WHERE playlist_id=? AND position=?",
|
||||
(status, pl_id, position)
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Event-status sync fejl: {e}")
|
||||
pass
|
||||
|
||||
def _on_state_changed(self, state: str):
|
||||
if state == "playing":
|
||||
@@ -900,6 +913,9 @@ class MainWindow(QMainWindow):
|
||||
def _on_volume(self, value: int):
|
||||
self._lbl_vol.setText(str(value))
|
||||
self._player.set_volume(value)
|
||||
from ui.settings_dialog import save_settings
|
||||
self._settings["volume"] = value
|
||||
save_settings(self._settings)
|
||||
|
||||
# ── Tema ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ class PlaylistPanel(QWidget):
|
||||
self._current_idx = -1
|
||||
self._song_ended = False
|
||||
self._active_playlist_id: int | None = None
|
||||
self._named_playlist_id: int | None = None # den indlæste/gemte navngivne liste
|
||||
self._build_ui()
|
||||
self.setAcceptDrops(True)
|
||||
# Autogem-timer — venter 800ms efter sidst ændring
|
||||
@@ -229,7 +230,7 @@ class PlaylistPanel(QWidget):
|
||||
from local.local_db import save_event_state
|
||||
save_event_state(self._current_idx, self._statuses)
|
||||
except Exception as e:
|
||||
print(f"Event-state gem fejl: {e}")
|
||||
pass
|
||||
|
||||
def _trigger_event_state_save(self):
|
||||
self._event_state_timer.start()
|
||||
@@ -250,9 +251,12 @@ class PlaylistPanel(QWidget):
|
||||
self._refresh()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Event-state gendan fejl: {e}")
|
||||
pass
|
||||
return False
|
||||
|
||||
def get_named_playlist_id(self) -> int | None:
|
||||
return self._named_playlist_id
|
||||
|
||||
def next_playable_idx(self) -> int | None:
|
||||
"""Find første sang fra toppen der ikke er 'skipped' eller 'played'."""
|
||||
for i in range(len(self._songs)):
|
||||
@@ -286,7 +290,7 @@ class PlaylistPanel(QWidget):
|
||||
self.playlist_changed.emit()
|
||||
except Exception as e:
|
||||
self._lbl_autosave.setText(f"⚠ gemfejl")
|
||||
print(f"Autogem fejl: {e}")
|
||||
pass
|
||||
|
||||
def restore_active_playlist(self):
|
||||
"""Indlæs den sidst aktive liste ved opstart."""
|
||||
@@ -324,7 +328,7 @@ class PlaylistPanel(QWidget):
|
||||
self._lbl_autosave.setText("✓ gendannet")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Gendan aktiv liste fejl: {e}")
|
||||
pass
|
||||
return False
|
||||
|
||||
# ── Ny / Gem som / Hent ───────────────────────────────────────────────────
|
||||
@@ -362,6 +366,7 @@ class PlaylistPanel(QWidget):
|
||||
for i, song in enumerate(self._songs, start=1):
|
||||
if song.get("id"):
|
||||
add_song_to_playlist(pl_id, song["id"], position=i)
|
||||
self._named_playlist_id = pl_id
|
||||
self._title_label.setText(f"DANSELISTE — {name.upper()}")
|
||||
self._lbl_autosave.setText(f"✓ gemt som \"{name}\"")
|
||||
except Exception as e:
|
||||
@@ -400,11 +405,12 @@ class PlaylistPanel(QWidget):
|
||||
from local.local_db import get_db
|
||||
with get_db() as conn:
|
||||
songs_raw = conn.execute("""
|
||||
SELECT s.*, ps.position FROM playlist_songs ps
|
||||
SELECT s.*, ps.position, ps.status FROM playlist_songs ps
|
||||
JOIN songs s ON s.id = ps.song_id
|
||||
WHERE ps.playlist_id=? ORDER BY ps.position
|
||||
""", (pl_id,)).fetchall()
|
||||
songs = []
|
||||
statuses = []
|
||||
for row in songs_raw:
|
||||
dances = conn.execute(
|
||||
"SELECT dance_name FROM song_dances WHERE song_id=? ORDER BY dance_order",
|
||||
@@ -418,7 +424,16 @@ class PlaylistPanel(QWidget):
|
||||
"file_missing": bool(row["file_missing"]),
|
||||
"dances": [d["dance_name"] for d in dances],
|
||||
})
|
||||
self.load_songs(songs, name=pl_name)
|
||||
statuses.append(row["status"] or "pending")
|
||||
self._songs = songs
|
||||
self._statuses = statuses
|
||||
self._current_idx = -1
|
||||
self._song_ended = False
|
||||
self._named_playlist_id = pl_id
|
||||
self._title_label.setText(f"DANSELISTE — {pl_name.upper()}")
|
||||
self._lbl_autosave.setText("✓ gendannet")
|
||||
self._refresh()
|
||||
self._trigger_autosave()
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, "Fejl", f"Kunne ikke indlæse listen: {e}")
|
||||
|
||||
|
||||
@@ -13,36 +13,41 @@ from PyQt6.QtCore import Qt, QSettings
|
||||
|
||||
SETTINGS_KEY_THEME = "appearance/dark_theme"
|
||||
SETTINGS_KEY_DEMO_SEC = "playback/demo_seconds"
|
||||
SETTINGS_KEY_MAIL_CLIENT = "mail/client" # "auto"|"thunderbird"|"outlook"|"mailto"
|
||||
SETTINGS_KEY_DEMO_FADE = "playback/demo_fade_seconds"
|
||||
SETTINGS_KEY_VOLUME = "playback/volume"
|
||||
SETTINGS_KEY_MAIL_CLIENT = "mail/client"
|
||||
SETTINGS_KEY_MAIL_PATH = "mail/custom_path"
|
||||
SETTINGS_KEY_AUTO_LOGIN = "online/auto_login"
|
||||
SETTINGS_KEY_USERNAME = "online/username"
|
||||
SETTINGS_KEY_PASSWORD = "online/password" # gemt i klartekst — ikke ideelt, men funktionelt
|
||||
SETTINGS_KEY_PASSWORD = "online/password"
|
||||
|
||||
|
||||
def load_settings() -> dict:
|
||||
"""Indlæs alle indstillinger med fornuftige standardværdier."""
|
||||
s = QSettings("LineDance", "Player")
|
||||
return {
|
||||
"dark_theme": s.value(SETTINGS_KEY_THEME, True, type=bool),
|
||||
"demo_seconds": s.value(SETTINGS_KEY_DEMO_SEC, 10, type=int),
|
||||
"mail_client": s.value(SETTINGS_KEY_MAIL_CLIENT, "auto"),
|
||||
"mail_path": s.value(SETTINGS_KEY_MAIL_PATH, ""),
|
||||
"auto_login": s.value(SETTINGS_KEY_AUTO_LOGIN, False, type=bool),
|
||||
"username": s.value(SETTINGS_KEY_USERNAME, ""),
|
||||
"password": s.value(SETTINGS_KEY_PASSWORD, ""),
|
||||
"dark_theme": s.value(SETTINGS_KEY_THEME, True, type=bool),
|
||||
"demo_seconds": s.value(SETTINGS_KEY_DEMO_SEC, 10, type=int),
|
||||
"demo_fade_seconds": s.value(SETTINGS_KEY_DEMO_FADE, 5, type=int),
|
||||
"volume": s.value(SETTINGS_KEY_VOLUME, 78, type=int),
|
||||
"mail_client": s.value(SETTINGS_KEY_MAIL_CLIENT, "auto"),
|
||||
"mail_path": s.value(SETTINGS_KEY_MAIL_PATH, ""),
|
||||
"auto_login": s.value(SETTINGS_KEY_AUTO_LOGIN, False, type=bool),
|
||||
"username": s.value(SETTINGS_KEY_USERNAME, ""),
|
||||
"password": s.value(SETTINGS_KEY_PASSWORD, ""),
|
||||
}
|
||||
|
||||
|
||||
def save_settings(values: dict):
|
||||
s = QSettings("LineDance", "Player")
|
||||
s.setValue(SETTINGS_KEY_THEME, values.get("dark_theme", True))
|
||||
s.setValue(SETTINGS_KEY_DEMO_SEC, values.get("demo_seconds", 10))
|
||||
s.setValue(SETTINGS_KEY_MAIL_CLIENT, values.get("mail_client", "auto"))
|
||||
s.setValue(SETTINGS_KEY_MAIL_PATH, values.get("mail_path", ""))
|
||||
s.setValue(SETTINGS_KEY_AUTO_LOGIN, values.get("auto_login", False))
|
||||
s.setValue(SETTINGS_KEY_USERNAME, values.get("username", ""))
|
||||
s.setValue(SETTINGS_KEY_PASSWORD, values.get("password", ""))
|
||||
s.setValue(SETTINGS_KEY_THEME, values.get("dark_theme", True))
|
||||
s.setValue(SETTINGS_KEY_DEMO_SEC, values.get("demo_seconds", 10))
|
||||
s.setValue(SETTINGS_KEY_DEMO_FADE, values.get("demo_fade_seconds", 5))
|
||||
s.setValue(SETTINGS_KEY_VOLUME, values.get("volume", 78))
|
||||
s.setValue(SETTINGS_KEY_MAIL_CLIENT, values.get("mail_client", "auto"))
|
||||
s.setValue(SETTINGS_KEY_MAIL_PATH, values.get("mail_path", ""))
|
||||
s.setValue(SETTINGS_KEY_AUTO_LOGIN, values.get("auto_login", False))
|
||||
s.setValue(SETTINGS_KEY_USERNAME, values.get("username", ""))
|
||||
s.setValue(SETTINGS_KEY_PASSWORD, values.get("password", ""))
|
||||
|
||||
|
||||
class SettingsDialog(QDialog):
|
||||
@@ -117,9 +122,21 @@ class SettingsDialog(QDialog):
|
||||
self._spin_demo.setFixedWidth(140)
|
||||
grp_layout.addRow("Forspil-længde:", self._spin_demo)
|
||||
|
||||
self._spin_fade = QSpinBox()
|
||||
self._spin_fade.setRange(0, 15)
|
||||
self._spin_fade.setSuffix(" sekunder (0 = ingen fade)")
|
||||
self._spin_fade.setFixedWidth(220)
|
||||
self._spin_fade.setToolTip(
|
||||
"Fade-out tilføjes til forspillets længde.\n"
|
||||
"F.eks. 10 sek forspil + 5 sek fade = 15 sek total.\n"
|
||||
"Sæt til 0 for ingen fade."
|
||||
)
|
||||
grp_layout.addRow("Fade-ud:", self._spin_fade)
|
||||
|
||||
note = QLabel(
|
||||
"Forspillet afspiller begyndelsen af sangen så arrangøren kan bekræfte\n"
|
||||
"at det er den rigtige sang og dans inden eventet starter."
|
||||
"at det er den rigtige sang og dans inden eventet starter.\n"
|
||||
"Fade-ud tilføjes oven i forspillets længde og fades logaritmisk."
|
||||
)
|
||||
note.setObjectName("result_count")
|
||||
note.setWordWrap(True)
|
||||
@@ -224,6 +241,7 @@ class SettingsDialog(QDialog):
|
||||
v = self._values
|
||||
self._chk_dark.setChecked(v.get("dark_theme", True))
|
||||
self._spin_demo.setValue(v.get("demo_seconds", 10))
|
||||
self._spin_fade.setValue(v.get("demo_fade_seconds", 5))
|
||||
|
||||
# Mail
|
||||
client = v.get("mail_client", "auto")
|
||||
@@ -246,13 +264,14 @@ class SettingsDialog(QDialog):
|
||||
|
||||
def _save_and_close(self):
|
||||
values = {
|
||||
"dark_theme": self._chk_dark.isChecked(),
|
||||
"demo_seconds": self._spin_demo.value(),
|
||||
"mail_client": self._mail_combo.currentData(),
|
||||
"mail_path": self._mail_path.text().strip(),
|
||||
"auto_login": self._chk_auto_login.isChecked(),
|
||||
"username": self._user_input.text().strip(),
|
||||
"password": self._pass_input.text(),
|
||||
"dark_theme": self._chk_dark.isChecked(),
|
||||
"demo_seconds": self._spin_demo.value(),
|
||||
"demo_fade_seconds": self._spin_fade.value(),
|
||||
"mail_client": self._mail_combo.currentData(),
|
||||
"mail_path": self._mail_path.text().strip(),
|
||||
"auto_login": self._chk_auto_login.isChecked(),
|
||||
"username": self._user_input.text().strip(),
|
||||
"password": self._pass_input.text(),
|
||||
}
|
||||
save_settings(values)
|
||||
self._values = values
|
||||
|
||||
@@ -1,237 +1,160 @@
|
||||
"""
|
||||
tag_editor.py — Rediger danse og alternativ-danse med niveau og autoudfyld.
|
||||
tag_editor.py — Simpel og robust dans-tag editor.
|
||||
|
||||
Fire sektioner:
|
||||
Mine danse | Fællesskabets danse
|
||||
Mine alternativer | Fællesskabets alternativer
|
||||
Danse gemmes til MP3-filen via mutagen.
|
||||
Niveau og alternativ-danse gemmes til SQLite.
|
||||
"""
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
||||
QPushButton, QListWidget, QListWidgetItem, QFrame,
|
||||
QSplitter, QWidget, QMessageBox, QComboBox, QCompleter,
|
||||
QGridLayout, QGroupBox,
|
||||
QPushButton, QComboBox, QWidget, QMessageBox, QGroupBox,
|
||||
QScrollArea, QFrame, QGridLayout,
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QTimer, QStringListModel, pyqtSignal
|
||||
from PyQt6.QtGui import QColor
|
||||
from PyQt6.QtCore import Qt, QTimer, QStringListModel
|
||||
from PyQt6.QtWidgets import QCompleter
|
||||
|
||||
|
||||
class AutoCompleteLineEdit(QLineEdit):
|
||||
"""QLineEdit med autoudfyld fra dans-navne databasen."""
|
||||
# ── Autoudfyld søgefelt ───────────────────────────────────────────────────────
|
||||
|
||||
def __init__(self, placeholder: str = "", parent=None):
|
||||
class AutoLineEdit(QLineEdit):
|
||||
def __init__(self, placeholder="", parent=None):
|
||||
super().__init__(parent)
|
||||
self.setPlaceholderText(placeholder)
|
||||
self._completer_model = QStringListModel()
|
||||
self._completer = QCompleter(self._completer_model, self)
|
||||
self._completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
|
||||
self._completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
|
||||
self._completer.setMaxVisibleItems(12)
|
||||
self.setCompleter(self._completer)
|
||||
self._timer = QTimer(self)
|
||||
self._timer.setSingleShot(True)
|
||||
self._timer.setInterval(150)
|
||||
self._timer.timeout.connect(self._update_suggestions)
|
||||
self.textChanged.connect(lambda _: self._timer.start())
|
||||
self._model = QStringListModel()
|
||||
comp = QCompleter(self._model, self)
|
||||
comp.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
|
||||
comp.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
|
||||
comp.setMaxVisibleItems(10)
|
||||
self.setCompleter(comp)
|
||||
t = QTimer(self)
|
||||
t.setSingleShot(True)
|
||||
t.setInterval(200)
|
||||
t.timeout.connect(self._suggest)
|
||||
self.textChanged.connect(lambda _: t.start())
|
||||
self._timer = t
|
||||
|
||||
def _update_suggestions(self):
|
||||
def _suggest(self):
|
||||
prefix = self.text().strip()
|
||||
if len(prefix) < 1:
|
||||
if not prefix:
|
||||
return
|
||||
try:
|
||||
from local.local_db import get_dance_name_suggestions
|
||||
names = get_dance_name_suggestions(prefix, limit=20)
|
||||
self._completer_model.setStringList(names)
|
||||
self._model.setStringList(get_dance_name_suggestions(prefix))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class DanceRow(QWidget):
|
||||
"""Én dans med navn og niveau-dropdown."""
|
||||
removed = pyqtSignal()
|
||||
# ── Niveau dropdown ───────────────────────────────────────────────────────────
|
||||
|
||||
def __init__(self, dance_name: str = "", level_id: int | None = None,
|
||||
levels: list = [], readonly: bool = False, parent=None):
|
||||
super().__init__(parent)
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 2, 0, 2)
|
||||
layout.setSpacing(6)
|
||||
|
||||
if readonly:
|
||||
self._name_lbl = QLabel(dance_name)
|
||||
self._name_lbl.setObjectName("track_meta")
|
||||
layout.addWidget(self._name_lbl, stretch=1)
|
||||
else:
|
||||
self._name_edit = AutoCompleteLineEdit("Dansenavn...", self)
|
||||
self._name_edit.setText(dance_name)
|
||||
layout.addWidget(self._name_edit, stretch=1)
|
||||
|
||||
self._level_combo = QComboBox()
|
||||
self._level_combo.addItem("— intet niveau —", None)
|
||||
self._level_data = [None]
|
||||
for lvl in levels:
|
||||
self._level_combo.addItem(lvl["name"], lvl["id"])
|
||||
self._level_data.append(lvl["id"])
|
||||
if level_id is not None:
|
||||
for i, lid in enumerate(self._level_data):
|
||||
if lid == level_id:
|
||||
self._level_combo.setCurrentIndex(i)
|
||||
break
|
||||
self._level_combo.setFixedWidth(130)
|
||||
self._level_combo.setEnabled(not readonly)
|
||||
layout.addWidget(self._level_combo)
|
||||
|
||||
if not readonly:
|
||||
btn_rm = QPushButton("✕")
|
||||
btn_rm.setFixedSize(24, 24)
|
||||
btn_rm.clicked.connect(self.removed.emit)
|
||||
layout.addWidget(btn_rm)
|
||||
|
||||
def get_name(self) -> str:
|
||||
if hasattr(self, "_name_edit"):
|
||||
return self._name_edit.text().strip()
|
||||
return self._name_lbl.text()
|
||||
|
||||
def get_level_id(self) -> int | None:
|
||||
return self._level_combo.currentData()
|
||||
def make_level_combo(levels: list, current_id=None) -> QComboBox:
|
||||
cb = QComboBox()
|
||||
cb.addItem("— intet niveau —", None)
|
||||
for lvl in levels:
|
||||
cb.addItem(lvl["name"], lvl["id"])
|
||||
if current_id is not None:
|
||||
for i in range(cb.count()):
|
||||
if cb.itemData(i) == current_id:
|
||||
cb.setCurrentIndex(i)
|
||||
break
|
||||
cb.setFixedWidth(130)
|
||||
return cb
|
||||
|
||||
|
||||
class AltRow(QWidget):
|
||||
"""Én alternativ-dans med navn, niveau og note."""
|
||||
removed = pyqtSignal()
|
||||
copy_to_mine = pyqtSignal(str, object, str) # name, level_id, note
|
||||
|
||||
def __init__(self, alt_name: str = "", level_id: int | None = None,
|
||||
note: str = "", levels: list = [],
|
||||
readonly: bool = False, source: str = "local",
|
||||
rating: float = 0, rating_count: int = 0, parent=None):
|
||||
super().__init__(parent)
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 2, 0, 2)
|
||||
layout.setSpacing(6)
|
||||
|
||||
if readonly:
|
||||
lbl = QLabel(f"→ {alt_name}")
|
||||
lbl.setObjectName("track_meta")
|
||||
layout.addWidget(lbl, stretch=1)
|
||||
if rating_count > 0:
|
||||
stars = "★" * round(rating) + "☆" * (5 - round(rating))
|
||||
lbl_r = QLabel(f"{stars} ({rating_count})")
|
||||
lbl_r.setObjectName("result_count")
|
||||
layout.addWidget(lbl_r)
|
||||
else:
|
||||
prefix_lbl = QLabel("→")
|
||||
prefix_lbl.setObjectName("track_meta")
|
||||
layout.addWidget(prefix_lbl)
|
||||
self._name_edit = AutoCompleteLineEdit("Alternativ dansenavn...", self)
|
||||
self._name_edit.setText(alt_name)
|
||||
layout.addWidget(self._name_edit, stretch=1)
|
||||
|
||||
self._level_combo = QComboBox()
|
||||
self._level_combo.addItem("— niveau —", None)
|
||||
self._level_data = [None]
|
||||
for lvl in levels:
|
||||
self._level_combo.addItem(lvl["name"], lvl["id"])
|
||||
self._level_data.append(lvl["id"])
|
||||
if level_id is not None:
|
||||
for i, lid in enumerate(self._level_data):
|
||||
if lid == level_id:
|
||||
self._level_combo.setCurrentIndex(i)
|
||||
break
|
||||
self._level_combo.setFixedWidth(120)
|
||||
self._level_combo.setEnabled(not readonly)
|
||||
layout.addWidget(self._level_combo)
|
||||
|
||||
if readonly:
|
||||
btn_copy = QPushButton("← Kopier")
|
||||
btn_copy.setFixedHeight(22)
|
||||
btn_copy.clicked.connect(
|
||||
lambda: self.copy_to_mine.emit(alt_name, self._level_combo.currentData(), note)
|
||||
)
|
||||
layout.addWidget(btn_copy)
|
||||
else:
|
||||
self._note_edit = QLineEdit()
|
||||
self._note_edit.setPlaceholderText("note...")
|
||||
self._note_edit.setText(note)
|
||||
self._note_edit.setFixedWidth(100)
|
||||
layout.addWidget(self._note_edit)
|
||||
btn_rm = QPushButton("✕")
|
||||
btn_rm.setFixedSize(24, 24)
|
||||
btn_rm.clicked.connect(self.removed.emit)
|
||||
layout.addWidget(btn_rm)
|
||||
|
||||
def get_name(self) -> str:
|
||||
if hasattr(self, "_name_edit"):
|
||||
return self._name_edit.text().strip()
|
||||
return ""
|
||||
|
||||
def get_level_id(self) -> int | None:
|
||||
return self._level_combo.currentData()
|
||||
|
||||
def get_note(self) -> str:
|
||||
if hasattr(self, "_note_edit"):
|
||||
return self._note_edit.text().strip()
|
||||
return ""
|
||||
|
||||
# ── Hoved-dialog ─────────────────────────────────────────────────────────────
|
||||
|
||||
class TagEditorDialog(QDialog):
|
||||
def __init__(self, song: dict, parent=None):
|
||||
super().__init__(parent)
|
||||
self._song = song
|
||||
self._levels = []
|
||||
self._my_dance_rows: list[DanceRow] = []
|
||||
self._my_alt_rows: list[AltRow] = []
|
||||
self.setWindowTitle(f"Rediger tags — {song.get('title','')}")
|
||||
self.setMinimumSize(860, 620)
|
||||
self._song = song
|
||||
self._levels = []
|
||||
self._dances = [] # list of {name, level_id, db_id}
|
||||
self._alts = [] # list of {name, level_id, note}
|
||||
|
||||
self.setWindowTitle(f"Rediger tags — {song.get('title', '')}")
|
||||
self.setMinimumSize(720, 500)
|
||||
self.resize(820, 580)
|
||||
|
||||
self._load_levels()
|
||||
self._load_existing()
|
||||
self._build_ui()
|
||||
self._load_data()
|
||||
|
||||
# ── Indlæsning ────────────────────────────────────────────────────────────
|
||||
|
||||
def _load_levels(self):
|
||||
try:
|
||||
from local.local_db import get_dance_levels
|
||||
self._levels = [dict(r) for r in get_dance_levels()]
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
pass # log fejl
|
||||
self._levels = []
|
||||
|
||||
def _load_existing(self):
|
||||
"""Indlæs eksisterende danse og alternativer fra DB."""
|
||||
try:
|
||||
from local.local_db import new_conn
|
||||
conn = new_conn()
|
||||
song_id = self._song.get("id")
|
||||
|
||||
rows = conn.execute(
|
||||
"SELECT id, dance_name, level_id FROM song_dances "
|
||||
"WHERE song_id=? ORDER BY dance_order",
|
||||
(song_id,)
|
||||
).fetchall()
|
||||
for row in rows:
|
||||
|
||||
for row in rows:
|
||||
alts = conn.execute(
|
||||
"SELECT alt_dance_name, level_id, note FROM dance_alternatives "
|
||||
"WHERE song_dance_id=? AND source='local'",
|
||||
(row["id"],)
|
||||
).fetchall()
|
||||
self._dances.append({
|
||||
"name": row["dance_name"],
|
||||
"level_id": row["level_id"],
|
||||
"db_id": row["id"],
|
||||
})
|
||||
for alt in alts:
|
||||
self._alts.append({
|
||||
"name": alt["alt_dance_name"],
|
||||
"level_id": alt["level_id"],
|
||||
"note": alt["note"] or "",
|
||||
})
|
||||
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
pass # log fejl
|
||||
|
||||
# ── UI ────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _build_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(16, 16, 16, 16)
|
||||
layout.setSpacing(10)
|
||||
layout.setContentsMargins(12, 12, 12, 12)
|
||||
layout.setSpacing(8)
|
||||
|
||||
# ── Sang-info ─────────────────────────────────────────────────────────
|
||||
# Sang-info
|
||||
info = QFrame()
|
||||
info.setObjectName("track_display")
|
||||
info_layout = QHBoxLayout(info)
|
||||
info_layout.setContentsMargins(10, 8, 10, 8)
|
||||
title_col = QVBoxLayout()
|
||||
lbl_title = QLabel(self._song.get("title", "—"))
|
||||
lbl_title.setObjectName("track_title")
|
||||
title_col.addWidget(lbl_title)
|
||||
meta = f"{self._song.get('artist','')} · {self._song.get('bpm',0)} BPM · {self._song.get('file_format','').upper()}"
|
||||
lbl_meta = QLabel(meta)
|
||||
lbl_meta.setObjectName("track_meta")
|
||||
title_col.addWidget(lbl_meta)
|
||||
can_write = self._song.get("file_format","").lower() in ("mp3","flac","ogg","opus","m4a")
|
||||
lbl_write = QLabel("✓ Tags skrives til filen" if can_write else "⚠ Tags gemmes kun i database")
|
||||
lbl_write.setObjectName("result_count")
|
||||
title_col.addWidget(lbl_write)
|
||||
info_layout.addLayout(title_col, stretch=1)
|
||||
il = QHBoxLayout(info)
|
||||
il.setContentsMargins(10, 8, 10, 8)
|
||||
lbl_t = QLabel(self._song.get("title", "—"))
|
||||
lbl_t.setObjectName("track_title")
|
||||
il.addWidget(lbl_t, stretch=1)
|
||||
fmt = self._song.get("file_format", "").lower()
|
||||
can_write = fmt in ("mp3", "flac", "ogg", "opus", "m4a")
|
||||
lbl_w = QLabel("✓ Danse skrives til filen" if can_write
|
||||
else "⚠ Dette format understøtter ikke fil-skrivning")
|
||||
lbl_w.setObjectName("result_count")
|
||||
il.addWidget(lbl_w)
|
||||
layout.addWidget(info)
|
||||
|
||||
# ── Fire paneler i 2x2 grid ───────────────────────────────────────────
|
||||
grid = QWidget()
|
||||
grid_layout = QGridLayout(grid)
|
||||
grid_layout.setSpacing(8)
|
||||
# To kolonner
|
||||
cols = QHBoxLayout()
|
||||
cols.setSpacing(12)
|
||||
cols.addWidget(self._build_dances_panel())
|
||||
cols.addWidget(self._build_alts_panel())
|
||||
layout.addLayout(cols, stretch=1)
|
||||
|
||||
grid_layout.addWidget(self._build_my_dances_panel(), 0, 0)
|
||||
grid_layout.addWidget(self._build_community_dances_panel(), 0, 1)
|
||||
grid_layout.addWidget(self._build_my_alts_panel(), 1, 0)
|
||||
grid_layout.addWidget(self._build_community_alts_panel(), 1, 1)
|
||||
|
||||
layout.addWidget(grid, stretch=1)
|
||||
|
||||
# ── Knapper ───────────────────────────────────────────────────────────
|
||||
# Knapper
|
||||
btn_row = QHBoxLayout()
|
||||
btn_row.addStretch()
|
||||
btn_cancel = QPushButton("Annuller")
|
||||
@@ -243,202 +166,262 @@ class TagEditorDialog(QDialog):
|
||||
btn_row.addWidget(btn_save)
|
||||
layout.addLayout(btn_row)
|
||||
|
||||
# ── Mine danse ────────────────────────────────────────────────────────────
|
||||
|
||||
def _build_my_dances_panel(self) -> QGroupBox:
|
||||
grp = QGroupBox("Mine danse")
|
||||
def _build_dances_panel(self) -> QGroupBox:
|
||||
grp = QGroupBox("Danse")
|
||||
layout = QVBoxLayout(grp)
|
||||
layout.setSpacing(4)
|
||||
|
||||
self._my_dances_container = QVBoxLayout()
|
||||
layout.addLayout(self._my_dances_container)
|
||||
layout.addStretch()
|
||||
# Scroll-område til eksisterende danse
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||||
container = QWidget()
|
||||
self._dance_layout = QVBoxLayout(container)
|
||||
self._dance_layout.setSpacing(4)
|
||||
self._dance_layout.addStretch()
|
||||
scroll.setWidget(container)
|
||||
layout.addWidget(scroll, stretch=1)
|
||||
|
||||
# Udfyld med eksisterende
|
||||
self._dance_rows = []
|
||||
for d in self._dances:
|
||||
self._add_dance_row(d["name"], d["level_id"])
|
||||
|
||||
# Tilføj-linje
|
||||
add_row = QHBoxLayout()
|
||||
self._new_dance = AutoLineEdit("Ny dans...", self)
|
||||
self._new_dance.returnPressed.connect(self._on_add_dance)
|
||||
add_row.addWidget(self._new_dance)
|
||||
btn = QPushButton("+ Tilføj")
|
||||
btn.setFixedWidth(70)
|
||||
btn.clicked.connect(self._on_add_dance)
|
||||
add_row.addWidget(btn)
|
||||
layout.addLayout(add_row)
|
||||
|
||||
return grp
|
||||
|
||||
def _add_dance_row(self, name="", level_id=None):
|
||||
row_widget = QWidget()
|
||||
row_layout = QHBoxLayout(row_widget)
|
||||
row_layout.setContentsMargins(0, 0, 0, 0)
|
||||
row_layout.setSpacing(4)
|
||||
|
||||
name_edit = AutoLineEdit("Dans...", self)
|
||||
name_edit.setText(name)
|
||||
row_layout.addWidget(name_edit, stretch=1)
|
||||
|
||||
level_cb = make_level_combo(self._levels, level_id)
|
||||
row_layout.addWidget(level_cb)
|
||||
|
||||
btn_rm = QPushButton("✕")
|
||||
btn_rm.setFixedSize(24, 24)
|
||||
row_layout.addWidget(btn_rm)
|
||||
|
||||
# Indsæt FØR stretch
|
||||
idx = self._dance_layout.count() - 1
|
||||
self._dance_layout.insertWidget(idx, row_widget)
|
||||
|
||||
entry = {"widget": row_widget, "name": name_edit, "level": level_cb}
|
||||
self._dance_rows.append(entry)
|
||||
btn_rm.clicked.connect(lambda: self._remove_dance_row(entry))
|
||||
|
||||
def _remove_dance_row(self, entry):
|
||||
self._dance_rows.remove(entry)
|
||||
entry["widget"].deleteLater()
|
||||
|
||||
def _on_add_dance(self):
|
||||
name = self._new_dance.text().strip()
|
||||
if name:
|
||||
self._add_dance_row(name)
|
||||
self._new_dance.clear()
|
||||
|
||||
def _build_alts_panel(self) -> QGroupBox:
|
||||
grp = QGroupBox("Alternativ-danse")
|
||||
layout = QVBoxLayout(grp)
|
||||
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||||
container = QWidget()
|
||||
self._alt_layout = QVBoxLayout(container)
|
||||
self._alt_layout.setSpacing(4)
|
||||
self._alt_layout.addStretch()
|
||||
scroll.setWidget(container)
|
||||
layout.addWidget(scroll, stretch=1)
|
||||
|
||||
self._alt_rows = []
|
||||
for a in self._alts:
|
||||
self._add_alt_row(a["name"], a["level_id"], a["note"])
|
||||
|
||||
add_row = QHBoxLayout()
|
||||
self._new_dance_input = AutoCompleteLineEdit("Ny dans...", self)
|
||||
self._new_dance_input.returnPressed.connect(self._add_my_dance)
|
||||
add_row.addWidget(self._new_dance_input)
|
||||
btn_add = QPushButton("+ Tilføj")
|
||||
btn_add.clicked.connect(self._add_my_dance)
|
||||
add_row.addWidget(btn_add)
|
||||
self._new_alt = AutoLineEdit("Nyt alternativ...", self)
|
||||
self._new_alt.returnPressed.connect(self._on_add_alt)
|
||||
add_row.addWidget(self._new_alt)
|
||||
btn = QPushButton("+ Tilføj")
|
||||
btn.setFixedWidth(70)
|
||||
btn.clicked.connect(self._on_add_alt)
|
||||
add_row.addWidget(btn)
|
||||
layout.addLayout(add_row)
|
||||
|
||||
return grp
|
||||
|
||||
def _add_my_dance(self, name: str = "", level_id=None):
|
||||
n = name or self._new_dance_input.text().strip()
|
||||
if not n:
|
||||
return
|
||||
row = DanceRow(n, level_id, self._levels, readonly=False, parent=self)
|
||||
row.removed.connect(lambda r=row: self._remove_dance_row(r))
|
||||
self._my_dance_rows.append(row)
|
||||
self._my_dances_container.addWidget(row)
|
||||
self._new_dance_input.clear()
|
||||
def _add_alt_row(self, name="", level_id=None, note=""):
|
||||
row_widget = QWidget()
|
||||
row_layout = QHBoxLayout(row_widget)
|
||||
row_layout.setContentsMargins(0, 0, 0, 0)
|
||||
row_layout.setSpacing(4)
|
||||
|
||||
def _remove_dance_row(self, row: DanceRow):
|
||||
self._my_dance_rows.remove(row)
|
||||
self._my_dances_container.removeWidget(row)
|
||||
row.deleteLater()
|
||||
lbl = QLabel("→")
|
||||
lbl.setObjectName("track_meta")
|
||||
row_layout.addWidget(lbl)
|
||||
|
||||
# ── Fællesskabets danse ───────────────────────────────────────────────────
|
||||
name_edit = AutoLineEdit("Dans...", self)
|
||||
name_edit.setText(name)
|
||||
row_layout.addWidget(name_edit, stretch=1)
|
||||
|
||||
def _build_community_dances_panel(self) -> QGroupBox:
|
||||
grp = QGroupBox("Fællesskabets danse")
|
||||
layout = QVBoxLayout(grp)
|
||||
self._community_dances_container = QVBoxLayout()
|
||||
layout.addLayout(self._community_dances_container)
|
||||
layout.addStretch()
|
||||
lbl = QLabel("Kræver online forbindelse")
|
||||
lbl.setObjectName("result_count")
|
||||
layout.addWidget(lbl)
|
||||
return grp
|
||||
level_cb = make_level_combo(self._levels, level_id)
|
||||
row_layout.addWidget(level_cb)
|
||||
|
||||
# ── Mine alternativer ─────────────────────────────────────────────────────
|
||||
note_edit = QLineEdit()
|
||||
note_edit.setPlaceholderText("note...")
|
||||
note_edit.setText(note)
|
||||
note_edit.setFixedWidth(80)
|
||||
row_layout.addWidget(note_edit)
|
||||
|
||||
def _build_my_alts_panel(self) -> QGroupBox:
|
||||
grp = QGroupBox("Mine alternativ-danse")
|
||||
layout = QVBoxLayout(grp)
|
||||
layout.setSpacing(4)
|
||||
self._my_alts_container = QVBoxLayout()
|
||||
layout.addLayout(self._my_alts_container)
|
||||
layout.addStretch()
|
||||
btn_rm = QPushButton("✕")
|
||||
btn_rm.setFixedSize(24, 24)
|
||||
row_layout.addWidget(btn_rm)
|
||||
|
||||
add_row = QHBoxLayout()
|
||||
self._new_alt_input = AutoCompleteLineEdit("Alternativ dansenavn...", self)
|
||||
self._new_alt_input.returnPressed.connect(self._add_my_alt)
|
||||
add_row.addWidget(self._new_alt_input)
|
||||
btn_add = QPushButton("+ Tilføj")
|
||||
btn_add.clicked.connect(self._add_my_alt)
|
||||
add_row.addWidget(btn_add)
|
||||
layout.addLayout(add_row)
|
||||
return grp
|
||||
idx = self._alt_layout.count() - 1
|
||||
self._alt_layout.insertWidget(idx, row_widget)
|
||||
|
||||
def _add_my_alt(self, name: str = "", level_id=None, note: str = ""):
|
||||
n = name or self._new_alt_input.text().strip()
|
||||
if not n:
|
||||
return
|
||||
row = AltRow(n, level_id, note, self._levels, readonly=False, parent=self)
|
||||
row.removed.connect(lambda r=row: self._remove_alt_row(r))
|
||||
self._my_alt_rows.append(row)
|
||||
self._my_alts_container.addWidget(row)
|
||||
self._new_alt_input.clear()
|
||||
entry = {"widget": row_widget, "name": name_edit,
|
||||
"level": level_cb, "note": note_edit}
|
||||
self._alt_rows.append(entry)
|
||||
btn_rm.clicked.connect(lambda: self._remove_alt_row(entry))
|
||||
|
||||
def _remove_alt_row(self, row: AltRow):
|
||||
self._my_alt_rows.remove(row)
|
||||
self._my_alts_container.removeWidget(row)
|
||||
row.deleteLater()
|
||||
def _remove_alt_row(self, entry):
|
||||
self._alt_rows.remove(entry)
|
||||
entry["widget"].deleteLater()
|
||||
|
||||
# ── Fællesskabets alternativer ────────────────────────────────────────────
|
||||
|
||||
def _build_community_alts_panel(self) -> QGroupBox:
|
||||
grp = QGroupBox("Fællesskabets alternativ-danse")
|
||||
layout = QVBoxLayout(grp)
|
||||
self._community_alts_container = QVBoxLayout()
|
||||
layout.addLayout(self._community_alts_container)
|
||||
layout.addStretch()
|
||||
lbl = QLabel("Kræver online forbindelse")
|
||||
lbl.setObjectName("result_count")
|
||||
layout.addWidget(lbl)
|
||||
return grp
|
||||
|
||||
# ── Indlæs eksisterende data ──────────────────────────────────────────────
|
||||
|
||||
def _load_data(self):
|
||||
try:
|
||||
from local.local_db import get_db, get_alternatives_for_dance
|
||||
song_id = self._song.get("id")
|
||||
with get_db() as conn:
|
||||
dances = conn.execute(
|
||||
"SELECT id, dance_name, dance_order, level_id FROM song_dances "
|
||||
"WHERE song_id=? ORDER BY dance_order",
|
||||
(song_id,)
|
||||
).fetchall()
|
||||
|
||||
for d in dances:
|
||||
self._add_my_dance(d["dance_name"], d["level_id"])
|
||||
# Indlæs alternativer for denne dans
|
||||
alts = get_alternatives_for_dance(d["id"])
|
||||
for alt in alts:
|
||||
if alt["source"] == "local":
|
||||
self._add_my_alt(
|
||||
alt["alt_dance_name"],
|
||||
alt["level_id"],
|
||||
alt["note"],
|
||||
)
|
||||
else:
|
||||
# Community-alternativ
|
||||
row = AltRow(
|
||||
alt["alt_dance_name"], alt["level_id"],
|
||||
alt["note"], self._levels,
|
||||
readonly=True, source="community",
|
||||
parent=self,
|
||||
)
|
||||
row.copy_to_mine.connect(self._add_my_alt)
|
||||
self._community_alts_container.addWidget(row)
|
||||
except Exception as e:
|
||||
print(f"Tag editor load fejl: {e}")
|
||||
def _on_add_alt(self):
|
||||
name = self._new_alt.text().strip()
|
||||
if name:
|
||||
self._add_alt_row(name)
|
||||
self._new_alt.clear()
|
||||
|
||||
# ── Gem ───────────────────────────────────────────────────────────────────
|
||||
|
||||
def _save(self):
|
||||
song_id = self._song.get("id")
|
||||
import uuid
|
||||
song_id = self._song.get("id")
|
||||
local_path = self._song.get("local_path", "")
|
||||
|
||||
# Saml data fra UI
|
||||
dances = []
|
||||
for row in self._dance_rows:
|
||||
name = row["name"].text().strip()
|
||||
if name:
|
||||
dances.append((name, row["level"].currentData()))
|
||||
|
||||
alts = []
|
||||
for row in self._alt_rows:
|
||||
name = row["name"].text().strip()
|
||||
if name:
|
||||
alts.append((name, row["level"].currentData(),
|
||||
row["note"].text().strip()))
|
||||
|
||||
try:
|
||||
from local.local_db import get_db, register_dance_name, add_alternative
|
||||
from local.local_db import new_conn
|
||||
from local.tag_reader import write_dances, can_write_dances
|
||||
import uuid
|
||||
|
||||
# Saml danse fra UI
|
||||
dances = [(r.get_name(), r.get_level_id())
|
||||
for r in self._my_dance_rows if r.get_name()]
|
||||
|
||||
conn = new_conn()
|
||||
|
||||
# Slet gammelt
|
||||
old = conn.execute(
|
||||
"SELECT id FROM song_dances WHERE song_id=?", (song_id,)
|
||||
).fetchall()
|
||||
for o in old:
|
||||
conn.execute(
|
||||
"DELETE FROM dance_alternatives WHERE song_dance_id=?",
|
||||
(o["id"],)
|
||||
)
|
||||
conn.execute("DELETE FROM song_dances WHERE song_id=?", (song_id,))
|
||||
|
||||
# Indsæt danse
|
||||
dance_ids = []
|
||||
with get_db() as conn:
|
||||
# Slet eksisterende danse og alternativer
|
||||
old_dances = conn.execute(
|
||||
"SELECT id FROM song_dances WHERE song_id=?", (song_id,)
|
||||
).fetchall()
|
||||
for od in old_dances:
|
||||
conn.execute("DELETE FROM dance_alternatives WHERE song_dance_id=?", (od["id"],))
|
||||
conn.execute("DELETE FROM song_dances WHERE song_id=?", (song_id,))
|
||||
for i, (name, level_id) in enumerate(dances, 1):
|
||||
conn.execute(
|
||||
"INSERT INTO song_dances "
|
||||
"(song_id, dance_name, dance_order, level_id) VALUES (?,?,?,?)",
|
||||
(song_id, name, i, level_id)
|
||||
)
|
||||
row = conn.execute(
|
||||
"SELECT id FROM song_dances "
|
||||
"WHERE song_id=? AND dance_order=?", (song_id, i)
|
||||
).fetchone()
|
||||
dance_ids.append(row["id"])
|
||||
|
||||
# Indsæt nye danse og hent IDs
|
||||
for i, (name, level_id) in enumerate(dances, start=1):
|
||||
# Opdater dance_names
|
||||
existing = conn.execute(
|
||||
"SELECT id FROM dance_names WHERE name=? COLLATE NOCASE",
|
||||
(name,)
|
||||
).fetchone()
|
||||
if existing:
|
||||
conn.execute(
|
||||
"INSERT INTO song_dances (song_id, dance_name, dance_order, level_id) "
|
||||
"VALUES (?,?,?,?)",
|
||||
(song_id, name, i, level_id)
|
||||
"UPDATE dance_names SET use_count=use_count+1 WHERE id=?",
|
||||
(existing["id"],)
|
||||
)
|
||||
else:
|
||||
conn.execute(
|
||||
"INSERT INTO dance_names (name, source, use_count) "
|
||||
"VALUES (?,?,1)", (name, "local")
|
||||
)
|
||||
new_id = conn.execute(
|
||||
"SELECT id FROM song_dances WHERE song_id=? AND dance_order=?",
|
||||
(song_id, i)
|
||||
).fetchone()["id"]
|
||||
dance_ids.append(new_id)
|
||||
register_dance_name(name)
|
||||
|
||||
# Indsæt alternativer knyttet til første dans
|
||||
if dance_ids and self._my_alt_rows:
|
||||
first_dance_id = dance_ids[0]
|
||||
for row in self._my_alt_rows:
|
||||
name = row.get_name()
|
||||
if name:
|
||||
import uuid as _uuid
|
||||
conn.execute("""
|
||||
INSERT INTO dance_alternatives
|
||||
(id, song_dance_id, alt_dance_name, level_id, note, source)
|
||||
VALUES (?,?,?,?,?,'local')
|
||||
""", (str(_uuid.uuid4()), first_dance_id,
|
||||
name, row.get_level_id(), row.get_note()))
|
||||
register_dance_name(name)
|
||||
# Indsæt alternativer på første dans
|
||||
if dance_ids and alts:
|
||||
fid = dance_ids[0]
|
||||
for alt_name, alt_level, alt_note in alts:
|
||||
conn.execute(
|
||||
"INSERT INTO dance_alternatives "
|
||||
"(id, song_dance_id, alt_dance_name, level_id, note, source) "
|
||||
"VALUES (?,?,?,?,?,'local')",
|
||||
(str(uuid.uuid4()), fid, alt_name, alt_level, alt_note)
|
||||
)
|
||||
existing = conn.execute(
|
||||
"SELECT id FROM dance_names WHERE name=? COLLATE NOCASE",
|
||||
(alt_name,)
|
||||
).fetchone()
|
||||
if existing:
|
||||
conn.execute(
|
||||
"UPDATE dance_names SET use_count=use_count+1 WHERE id=?",
|
||||
(existing["id"],)
|
||||
)
|
||||
else:
|
||||
conn.execute(
|
||||
"INSERT INTO dance_names (name, source, use_count) "
|
||||
"VALUES (?,?,1)", (alt_name, "local")
|
||||
)
|
||||
|
||||
# Skriv til fil
|
||||
if local_path and can_write_dances(local_path):
|
||||
dance_names = [n for n, _ in dances]
|
||||
ok = write_dances(local_path, dance_names)
|
||||
if not ok:
|
||||
QMessageBox.warning(self, "Advarsel",
|
||||
"Tags gemt i database, men kunne ikke skrives til filen.")
|
||||
conn.commit()
|
||||
"SELECT COUNT(*) FROM song_dances WHERE song_id=?", (song_id,)
|
||||
conn.close()
|
||||
|
||||
# Skriv danse til filen
|
||||
if local_path:
|
||||
from local.tag_reader import write_dances, can_write_dances
|
||||
if can_write_dances(local_path):
|
||||
dance_names = [n for n, _ in dances]
|
||||
if not write_dances(local_path, dance_names):
|
||||
QMessageBox.warning(
|
||||
self, "Advarsel",
|
||||
"Gemt i database, men kunne ikke skrive til filen."
|
||||
)
|
||||
|
||||
self.accept()
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, "Fejl", f"Kunne ikke gemme tags: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
QMessageBox.warning(self, "Fejl", f"Kunne ikke gemme: {e}")
|
||||
|
||||
Reference in New Issue
Block a user