Version 1
This commit is contained in:
33
linedance-app/app_logger.py
Normal file
33
linedance-app/app_logger.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
"""
|
||||||
|
app_logger.py — Central logging til fil i stedet for konsol.
|
||||||
|
P<EFBFBD> Windows uden konsol skrives alt til ~/.linedance/app.log
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
LOG_PATH = Path.home() / ".linedance" / "app.log"
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging():
|
||||||
|
LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
handlers = [logging.FileHandler(LOG_PATH, encoding="utf-8")]
|
||||||
|
# Kun tilføj konsol-handler hvis vi kører med konsol (development)
|
||||||
|
if sys.stdout and hasattr(sys.stdout, 'write'):
|
||||||
|
try:
|
||||||
|
sys.stdout.write("") # test om konsol virker
|
||||||
|
handlers.append(logging.StreamHandler(sys.stdout))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||||
|
datefmt="%H:%M:%S",
|
||||||
|
handlers=handlers,
|
||||||
|
force=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger("linedance")
|
||||||
@@ -64,7 +64,7 @@ exe = EXE(
|
|||||||
bootloader_ignore_signals=False,
|
bootloader_ignore_signals=False,
|
||||||
strip=False,
|
strip=False,
|
||||||
upx=False, # UPX kan give problemer med PyQt6 DLL-filer
|
upx=False, # UPX kan give problemer med PyQt6 DLL-filer
|
||||||
console=True, # Vis fejlbeskeder
|
console=False, # Ingen konsol-vindue
|
||||||
disable_windowed_traceback=False,
|
disable_windowed_traceback=False,
|
||||||
target_arch=None,
|
target_arch=None,
|
||||||
codesign_identity=None,
|
codesign_identity=None,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -17,36 +17,51 @@ from pathlib import Path
|
|||||||
DB_PATH = Path.home() / ".linedance" / "local.db"
|
DB_PATH = Path.home() / ".linedance" / "local.db"
|
||||||
|
|
||||||
_local = threading.local()
|
_local = threading.local()
|
||||||
|
_global_conn: sqlite3.Connection | None = None
|
||||||
|
|
||||||
|
|
||||||
def _get_conn() -> sqlite3.Connection:
|
def _get_conn() -> sqlite3.Connection:
|
||||||
"""Returnerer en thread-lokal forbindelse."""
|
"""Returnerer en global forbindelse i autocommit mode."""
|
||||||
if not hasattr(_local, "conn") or _local.conn is None:
|
global _global_conn
|
||||||
|
if _global_conn is None:
|
||||||
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
|
_global_conn = sqlite3.connect(str(DB_PATH), check_same_thread=False,
|
||||||
|
isolation_level=None) # autocommit
|
||||||
|
_global_conn.row_factory = sqlite3.Row
|
||||||
|
_global_conn.execute("PRAGMA journal_mode=WAL")
|
||||||
|
_global_conn.execute("PRAGMA foreign_keys=ON")
|
||||||
|
return _global_conn
|
||||||
|
|
||||||
|
|
||||||
|
def new_conn() -> sqlite3.Connection:
|
||||||
|
"""Åbn en frisk forbindelse til brug i tag_editor og dialogs."""
|
||||||
|
conn = sqlite3.connect(str(DB_PATH), check_same_thread=False)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
conn.execute("PRAGMA journal_mode=WAL") # bedre concurrent adgang
|
conn.execute("PRAGMA foreign_keys=OFF") # FK checker forhindrer level_id gem
|
||||||
conn.execute("PRAGMA foreign_keys=ON")
|
return conn
|
||||||
_local.conn = conn
|
|
||||||
return _local.conn
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def get_db():
|
def get_db():
|
||||||
|
"""Context manager der bruger app-forbindelsen i autocommit mode.
|
||||||
|
Hver statement committer med det samme — ingen eksplicit transaktion."""
|
||||||
conn = _get_conn()
|
conn = _get_conn()
|
||||||
try:
|
try:
|
||||||
yield conn
|
yield conn
|
||||||
conn.commit()
|
|
||||||
except Exception:
|
except Exception:
|
||||||
conn.rollback()
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def get_db_raw() -> sqlite3.Connection:
|
||||||
|
return _get_conn()
|
||||||
|
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
"""Opret alle tabeller hvis de ikke findes."""
|
"""Opret alle tabeller hvis de ikke findes."""
|
||||||
conn = _get_conn()
|
conn = _get_conn()
|
||||||
|
|
||||||
# Brug executescript direkte (ikke via context manager) da det auto-committer
|
# executescript committer automatisk og nulstiller isolation_level
|
||||||
|
# Kør det direkte på den underliggende connection
|
||||||
conn.executescript("""
|
conn.executescript("""
|
||||||
CREATE TABLE IF NOT EXISTS libraries (
|
CREATE TABLE IF NOT EXISTS libraries (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -148,23 +163,20 @@ def init_db():
|
|||||||
CREATE INDEX IF NOT EXISTS idx_song_dances ON song_dances(song_id);
|
CREATE INDEX IF NOT EXISTS idx_song_dances ON song_dances(song_id);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Kør migrations for ældre databaser (each separately)
|
# executescript slår foreign_keys fra — genaktiver
|
||||||
migrations = [
|
conn.execute("PRAGMA foreign_keys=ON")
|
||||||
"ALTER TABLE songs ADD COLUMN extra_tags TEXT NOT NULL DEFAULT '{}'",
|
|
||||||
"ALTER TABLE song_dances ADD COLUMN level_id INTEGER REFERENCES dance_levels(id)",
|
|
||||||
"ALTER TABLE dance_alternatives ADD COLUMN alt_dance_name TEXT NOT NULL DEFAULT ''",
|
|
||||||
"ALTER TABLE dance_alternatives ADD COLUMN level_id INTEGER REFERENCES dance_levels(id)",
|
|
||||||
"ALTER TABLE dance_alternatives ADD COLUMN source TEXT NOT NULL DEFAULT 'local'",
|
|
||||||
"ALTER TABLE dance_alternatives ADD COLUMN created_by TEXT NOT NULL DEFAULT ''",
|
|
||||||
]
|
|
||||||
for sql in migrations:
|
|
||||||
try:
|
|
||||||
conn.execute(sql)
|
|
||||||
conn.commit()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Seed standard-niveauer — KUN hvis tabellen er tom
|
# Tilføj db_version tabel hvis den ikke findes
|
||||||
|
conn.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS db_version (
|
||||||
|
version INTEGER PRIMARY KEY
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Kør versionsbaserede migrationer
|
||||||
|
_run_versioned_migrations(conn)
|
||||||
|
|
||||||
|
# Seed standard-niveauer
|
||||||
count = conn.execute("SELECT COUNT(*) FROM dance_levels").fetchone()[0]
|
count = conn.execute("SELECT COUNT(*) FROM dance_levels").fetchone()[0]
|
||||||
if count == 0:
|
if count == 0:
|
||||||
defaults = [
|
defaults = [
|
||||||
@@ -174,14 +186,49 @@ def init_db():
|
|||||||
(4, "Erfaren", "For dedikerede dansere"),
|
(4, "Erfaren", "For dedikerede dansere"),
|
||||||
(5, "Ekspert", "Konkurrenceniveau"),
|
(5, "Ekspert", "Konkurrenceniveau"),
|
||||||
]
|
]
|
||||||
conn.executemany(
|
for row in defaults:
|
||||||
|
conn.execute(
|
||||||
"INSERT OR IGNORE INTO dance_levels (sort_order, name, description) VALUES (?,?,?)",
|
"INSERT OR IGNORE INTO dance_levels (sort_order, name, description) VALUES (?,?,?)",
|
||||||
defaults
|
row
|
||||||
)
|
)
|
||||||
conn.commit()
|
|
||||||
print(f"Dans-niveauer seedet: {len(defaults)} niveauer")
|
|
||||||
else:
|
# ── Versionsbaserede migrationer ──────────────────────────────────────────────
|
||||||
print(f"Dans-niveauer: {count} niveauer i databasen")
|
# Tilføj aldrig gamle — tilføj kun nye versioner nederst.
|
||||||
|
|
||||||
|
MIGRATIONS: dict[int, list[str]] = {
|
||||||
|
1: [
|
||||||
|
"ALTER TABLE songs ADD COLUMN extra_tags TEXT NOT NULL DEFAULT '{}'",
|
||||||
|
"ALTER TABLE song_dances ADD COLUMN level_id INTEGER REFERENCES dance_levels(id)",
|
||||||
|
"ALTER TABLE dance_alternatives ADD COLUMN alt_dance_name TEXT NOT NULL DEFAULT ''",
|
||||||
|
"ALTER TABLE dance_alternatives ADD COLUMN level_id INTEGER REFERENCES dance_levels(id)",
|
||||||
|
"ALTER TABLE dance_alternatives ADD COLUMN source TEXT NOT NULL DEFAULT 'local'",
|
||||||
|
"ALTER TABLE dance_alternatives ADD COLUMN created_by TEXT NOT NULL DEFAULT ''",
|
||||||
|
],
|
||||||
|
# Eksempel på fremtidig migration:
|
||||||
|
# 2: ["ALTER TABLE songs ADD COLUMN mbid TEXT"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _run_versioned_migrations(conn):
|
||||||
|
"""Kør kun migrationer der ikke allerede er kørt vha. db_version tabel."""
|
||||||
|
row = conn.execute("SELECT version FROM db_version").fetchone()
|
||||||
|
current_version = row["version"] if row else 0
|
||||||
|
|
||||||
|
for version in sorted(MIGRATIONS.keys()):
|
||||||
|
if version <= current_version:
|
||||||
|
continue
|
||||||
|
for sql in MIGRATIONS[version]:
|
||||||
|
try:
|
||||||
|
conn.execute(sql)
|
||||||
|
except Exception:
|
||||||
|
pass # kolonnen eksisterer allerede
|
||||||
|
conn.execute(
|
||||||
|
"INSERT OR REPLACE INTO db_version (version) VALUES (?)", (version,)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -282,30 +329,49 @@ def upsert_song(song_data: dict) -> str:
|
|||||||
extra_tags_json,
|
extra_tags_json,
|
||||||
))
|
))
|
||||||
|
|
||||||
# Opdater danse hvis de er med i data
|
# Opdater danse hvis de er med i data — bevar level_id og alternativer
|
||||||
if "dances" in song_data:
|
if "dances" in song_data:
|
||||||
conn.execute("DELETE FROM song_dances WHERE song_id=?", (song_id,))
|
file_dances = []
|
||||||
for i, dance in enumerate(song_data["dances"], start=1):
|
|
||||||
# dance kan være str eller dict med {name, level_id}
|
|
||||||
if isinstance(dance, dict):
|
|
||||||
name = dance.get("name", "")
|
|
||||||
level_id = dance.get("level_id")
|
|
||||||
else:
|
|
||||||
name = dance
|
|
||||||
level_id = None
|
|
||||||
conn.execute(
|
|
||||||
"INSERT INTO song_dances (song_id, dance_name, dance_order, level_id) VALUES (?,?,?,?)",
|
|
||||||
(song_id, name, i, level_id),
|
|
||||||
)
|
|
||||||
# Registrer navne i ordbogen
|
|
||||||
try:
|
|
||||||
from local.local_db import register_dance_name as _reg
|
|
||||||
for dance in song_data["dances"]:
|
for dance in song_data["dances"]:
|
||||||
nm = dance.get("name", dance) if isinstance(dance, dict) else dance
|
if isinstance(dance, dict):
|
||||||
if nm:
|
file_dances.append(dance.get("name", ""))
|
||||||
_reg(nm)
|
else:
|
||||||
except Exception:
|
file_dances.append(dance)
|
||||||
pass
|
file_dances = [d for d in file_dances if d]
|
||||||
|
|
||||||
|
# Hent eksisterende danse med level_id og alternativer
|
||||||
|
existing = conn.execute(
|
||||||
|
"SELECT id, dance_name, dance_order, level_id FROM song_dances "
|
||||||
|
"WHERE song_id=? ORDER BY dance_order",
|
||||||
|
(song_id,)
|
||||||
|
).fetchall()
|
||||||
|
existing_map = {r["dance_name"].lower(): r for r in existing}
|
||||||
|
|
||||||
|
# Slet danse der ikke længere er i filen
|
||||||
|
file_lower = [d.lower() for d in file_dances]
|
||||||
|
for row in existing:
|
||||||
|
if row["dance_name"].lower() not in file_lower:
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM dance_alternatives WHERE song_dance_id=?", (row["id"],)
|
||||||
|
)
|
||||||
|
conn.execute("DELETE FROM song_dances WHERE id=?", (row["id"],))
|
||||||
|
|
||||||
|
# Tilføj eller opdater danse fra filen
|
||||||
|
for i, name in enumerate(file_dances, start=1):
|
||||||
|
ex = existing_map.get(name.lower())
|
||||||
|
if ex:
|
||||||
|
# Bevar level_id — opdater kun dance_order
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE song_dances SET dance_order=? WHERE id=?",
|
||||||
|
(i, ex["id"])
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Ny dans — ingen level_id endnu
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO song_dances (song_id, dance_name, dance_order, level_id) "
|
||||||
|
"VALUES (?,?,?,NULL)",
|
||||||
|
(song_id, name, i)
|
||||||
|
)
|
||||||
|
|
||||||
return song_id
|
return song_id
|
||||||
|
|
||||||
@@ -465,15 +531,41 @@ def clear_event_state():
|
|||||||
# ── Dans-navne ordbog ─────────────────────────────────────────────────────────
|
# ── Dans-navne ordbog ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def get_dance_name_suggestions(prefix: str, limit: int = 20) -> list[str]:
|
def get_dance_name_suggestions(prefix: str, limit: int = 20) -> list[str]:
|
||||||
"""Returnerer danse-navne der starter med prefix, sorteret efter popularitet."""
|
"""Returnerer dans-navne der starter med prefix fra alle kendte sources,
|
||||||
|
sorteret efter popularitet. Inkluderer navne fra song_dances og dance_alternatives."""
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
|
# Hent fra dance_names ordbog (primær kilde)
|
||||||
rows = conn.execute("""
|
rows = conn.execute("""
|
||||||
SELECT name FROM dance_names
|
SELECT name, use_count FROM dance_names
|
||||||
WHERE name LIKE ? COLLATE NOCASE
|
WHERE name LIKE ? COLLATE NOCASE
|
||||||
ORDER BY use_count DESC, name
|
ORDER BY use_count DESC, name
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
""", (f"{prefix}%", limit)).fetchall()
|
""", (f"{prefix}%", limit)).fetchall()
|
||||||
return [r["name"] for r in rows]
|
names = {r["name"]: r["use_count"] for r in rows}
|
||||||
|
|
||||||
|
# Supplér med navne direkte fra song_dances der ikke er i ordbogen
|
||||||
|
extra = conn.execute("""
|
||||||
|
SELECT DISTINCT dance_name as name FROM song_dances
|
||||||
|
WHERE dance_name LIKE ? COLLATE NOCASE
|
||||||
|
LIMIT ?
|
||||||
|
""", (f"{prefix}%", limit)).fetchall()
|
||||||
|
for r in extra:
|
||||||
|
if r["name"] not in names:
|
||||||
|
names[r["name"]] = 0
|
||||||
|
|
||||||
|
# Supplér med alternativ-danse
|
||||||
|
extra2 = conn.execute("""
|
||||||
|
SELECT DISTINCT alt_dance_name as name FROM dance_alternatives
|
||||||
|
WHERE alt_dance_name LIKE ? COLLATE NOCASE
|
||||||
|
LIMIT ?
|
||||||
|
""", (f"{prefix}%", limit)).fetchall()
|
||||||
|
for r in extra2:
|
||||||
|
if r["name"] not in names:
|
||||||
|
names[r["name"]] = 0
|
||||||
|
|
||||||
|
# Sorter: kendte navne med høj use_count først, derefter alfabetisk
|
||||||
|
return sorted(names.keys(),
|
||||||
|
key=lambda n: (-names[n], n.lower()))[:limit]
|
||||||
|
|
||||||
|
|
||||||
def register_dance_name(name: str, source: str = "local"):
|
def register_dance_name(name: str, source: str = "local"):
|
||||||
|
|||||||
Binary file not shown.
@@ -11,6 +11,7 @@ Sender signals til GUI:
|
|||||||
|
|
||||||
from PyQt6.QtCore import QObject, pyqtSignal, QTimer
|
from PyQt6.QtCore import QObject, pyqtSignal, QTimer
|
||||||
import random
|
import random
|
||||||
|
import math
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import vlc
|
import vlc
|
||||||
@@ -33,6 +34,7 @@ class Player(QObject):
|
|||||||
self._duration: int = 0
|
self._duration: int = 0
|
||||||
self._demo_mode = False
|
self._demo_mode = False
|
||||||
self._demo_stop_sec = 10
|
self._demo_stop_sec = 10
|
||||||
|
self._demo_fade_sec = 5
|
||||||
self._demo_fading = False
|
self._demo_fading = False
|
||||||
self._volume = 78
|
self._volume = 78
|
||||||
|
|
||||||
@@ -78,10 +80,15 @@ class Player(QObject):
|
|||||||
self._poll_timer.start()
|
self._poll_timer.start()
|
||||||
self.state_changed.emit("playing")
|
self.state_changed.emit("playing")
|
||||||
|
|
||||||
def play_demo(self, stop_at_sec: int = 10):
|
def play_demo(self, stop_at_sec: int = 10, fade_sec: int = 5):
|
||||||
"""Afspil fra start og stop automatisk ved stop_at_sec med 2 sek fade-out."""
|
"""
|
||||||
|
Afspil fra start, fade ud over fade_sec sekunder og stop.
|
||||||
|
Total afspilningstid = stop_at_sec + fade_sec.
|
||||||
|
fade_sec=0 giver ingen fade.
|
||||||
|
"""
|
||||||
self._demo_mode = True
|
self._demo_mode = True
|
||||||
self._demo_stop_sec = stop_at_sec
|
self._demo_stop_sec = stop_at_sec + fade_sec # total tid inkl. fade
|
||||||
|
self._demo_fade_sec = fade_sec
|
||||||
self._demo_fading = False
|
self._demo_fading = False
|
||||||
if VLC_AVAILABLE and self._media_player:
|
if VLC_AVAILABLE and self._media_player:
|
||||||
self._media_player.set_time(0)
|
self._media_player.set_time(0)
|
||||||
@@ -156,18 +163,16 @@ class Player(QObject):
|
|||||||
self.state_changed.emit("demo_ended")
|
self.state_changed.emit("demo_ended")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Demo fade-out — de sidste 2 sekunder
|
# Demo fade-out — de sidste _demo_fade_sec sekunder (0 = ingen fade)
|
||||||
FADE_SEC = 2.0
|
if self._demo_mode and VLC_AVAILABLE and self._media_player and self._demo_fade_sec > 0:
|
||||||
if self._demo_mode and VLC_AVAILABLE and self._media_player:
|
|
||||||
secs_left = self._demo_stop_sec - cur
|
secs_left = self._demo_stop_sec - cur
|
||||||
if secs_left <= FADE_SEC and secs_left > 0:
|
if secs_left <= self._demo_fade_sec and secs_left > 0:
|
||||||
# Fade fra fuld volumen til 0 over FADE_SEC sekunder
|
fade_fraction = secs_left / self._demo_fade_sec # 1.0 → 0.0
|
||||||
fade_fraction = secs_left / FADE_SEC # 1.0 → 0.0
|
log_fraction = math.log10(1 + fade_fraction * 9) / math.log10(10)
|
||||||
faded_vol = int(self._volume * fade_fraction)
|
faded_vol = int(self._volume * log_fraction)
|
||||||
self._media_player.audio_set_volume(max(0, faded_vol))
|
self._media_player.audio_set_volume(max(0, faded_vol))
|
||||||
self._demo_fading = True
|
self._demo_fading = True
|
||||||
elif not self._demo_fading:
|
elif not self._demo_fading:
|
||||||
# Ikke i fade-zone endnu — sørg for fuld volumen
|
|
||||||
self._media_player.audio_set_volume(self._volume)
|
self._media_player.audio_set_volume(self._volume)
|
||||||
|
|
||||||
# VU-meter: brug VLC's audio-amplitude hvis tilgængelig, ellers simulér
|
# VU-meter: brug VLC's audio-amplitude hvis tilgængelig, ellers simulér
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -51,6 +51,7 @@ class LibraryPanel(QWidget):
|
|||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._all_songs: list[dict] = []
|
self._all_songs: list[dict] = []
|
||||||
self._filtered: list[dict] = []
|
self._filtered: list[dict] = []
|
||||||
|
self._bpm_scan_running = False
|
||||||
self._search_timer = QTimer(self)
|
self._search_timer = QTimer(self)
|
||||||
self._search_timer.setSingleShot(True)
|
self._search_timer.setSingleShot(True)
|
||||||
self._search_timer.setInterval(150)
|
self._search_timer.setInterval(150)
|
||||||
@@ -70,6 +71,12 @@ class LibraryPanel(QWidget):
|
|||||||
header.addWidget(lbl)
|
header.addWidget(lbl)
|
||||||
header.addStretch()
|
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 = QPushButton("⚙ Mapper")
|
||||||
btn_manage.setFixedHeight(24)
|
btn_manage.setFixedHeight(24)
|
||||||
btn_manage.setToolTip("Tilføj, fjern og scan musikbiblioteker")
|
btn_manage.setToolTip("Tilføj, fjern og scan musikbiblioteker")
|
||||||
@@ -172,7 +179,6 @@ class LibraryPanel(QWidget):
|
|||||||
dance_levels = song.get("dance_levels", [])
|
dance_levels = song.get("dance_levels", [])
|
||||||
missing = song.get("file_missing", False)
|
missing = song.get("file_missing", False)
|
||||||
|
|
||||||
# Byg dans-streng med niveau hvis tilgængeligt
|
|
||||||
dance_parts = []
|
dance_parts = []
|
||||||
for i, d in enumerate(dances):
|
for i, d in enumerate(dances):
|
||||||
lvl = dance_levels[i] if i < len(dance_levels) else ""
|
lvl = dance_levels[i] if i < len(dance_levels) else ""
|
||||||
@@ -183,13 +189,97 @@ class LibraryPanel(QWidget):
|
|||||||
bpm = song.get("bpm", 0)
|
bpm = song.get("bpm", 0)
|
||||||
bpm_str = f"{bpm} BPM" if bpm else "? BPM"
|
bpm_str = f"{bpm} BPM" if bpm else "? BPM"
|
||||||
line2 = f" {song.get('artist','—')} · {bpm_str} · {song.get('file_format','').upper()}{dance_str}"
|
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)
|
item.setData(Qt.ItemDataRole.UserRole, song)
|
||||||
if missing:
|
row_widget.adjustSize()
|
||||||
item.setForeground(QColor("#5a6070"))
|
hint = row_widget.sizeHint()
|
||||||
elif q and any(q in d.lower() for d in dances):
|
hint.setHeight(max(hint.height(), 52))
|
||||||
item.setForeground(QColor("#e8a020"))
|
item.setSizeHint(hint)
|
||||||
self._list.addItem(item)
|
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 ────────────────────────────────────────────────────────────
|
# ── Handlinger ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ class ProgressBar(QWidget):
|
|||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._fraction = 0.0
|
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.setFixedHeight(10)
|
||||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
|
|
||||||
@@ -34,8 +35,9 @@ class ProgressBar(QWidget):
|
|||||||
self._fraction = max(0.0, min(1.0, f))
|
self._fraction = max(0.0, min(1.0, f))
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def set_demo_marker(self, f: float):
|
def set_demo_marker(self, demo_f: float, fade_f: float = 0.0):
|
||||||
self._demo_fraction = max(0.0, min(1.0, f))
|
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()
|
self.update()
|
||||||
|
|
||||||
def paintEvent(self, event):
|
def paintEvent(self, event):
|
||||||
@@ -46,6 +48,11 @@ class ProgressBar(QWidget):
|
|||||||
fill_w = int(w * self._fraction)
|
fill_w = int(w * self._fraction)
|
||||||
if fill_w > 0:
|
if fill_w > 0:
|
||||||
p.fillRect(0, 0, fill_w, h, QColor("#e8a020"))
|
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:
|
if self._demo_fraction > 0:
|
||||||
mx = int(w * self._demo_fraction)
|
mx = int(w * self._demo_fraction)
|
||||||
p.fillRect(mx - 1, 0, 2, h, QColor("#3b8fd4"))
|
p.fillRect(mx - 1, 0, 2, h, QColor("#3b8fd4"))
|
||||||
@@ -81,6 +88,7 @@ class MainWindow(QMainWindow):
|
|||||||
self._settings = load_settings()
|
self._settings = load_settings()
|
||||||
self._dark_theme = self._settings.get("dark_theme", True)
|
self._dark_theme = self._settings.get("dark_theme", True)
|
||||||
self._demo_seconds = self._settings.get("demo_seconds", 10)
|
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._connect_player_signals()
|
||||||
self._build_menu()
|
self._build_menu()
|
||||||
@@ -306,12 +314,12 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
self._vol_slider = QSlider(Qt.Orientation.Horizontal)
|
self._vol_slider = QSlider(Qt.Orientation.Horizontal)
|
||||||
self._vol_slider.setRange(0, 100)
|
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.setFixedWidth(100)
|
||||||
self._vol_slider.valueChanged.connect(self._on_volume)
|
self._vol_slider.valueChanged.connect(self._on_volume)
|
||||||
layout.addWidget(self._vol_slider)
|
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")
|
self._lbl_vol.setObjectName("vol_val")
|
||||||
layout.addWidget(self._lbl_vol)
|
layout.addWidget(self._lbl_vol)
|
||||||
|
|
||||||
@@ -401,7 +409,7 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._set_status(f"DB fejl: {e}")
|
self._set_status(f"DB fejl: {e}")
|
||||||
print(f"DB init fejl: {e}")
|
pass
|
||||||
|
|
||||||
def start_scan(self):
|
def start_scan(self):
|
||||||
"""Start fuld scanning af alle biblioteker i baggrundstråd."""
|
"""Start fuld scanning af alle biblioteker i baggrundstråd."""
|
||||||
@@ -463,7 +471,7 @@ class MainWindow(QMainWindow):
|
|||||||
count = len(songs)
|
count = len(songs)
|
||||||
self._set_status(f"Bibliotek: {count} sang{'e' if count != 1 else ''}", 3000)
|
self._set_status(f"Bibliotek: {count} sang{'e' if count != 1 else ''}", 3000)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Bibliotek reload fejl: {e}")
|
pass
|
||||||
|
|
||||||
def add_library_path(self, path: str):
|
def add_library_path(self, path: str):
|
||||||
try:
|
try:
|
||||||
@@ -483,6 +491,7 @@ class MainWindow(QMainWindow):
|
|||||||
if dialog.exec():
|
if dialog.exec():
|
||||||
self._settings = dialog.get_values()
|
self._settings = dialog.get_values()
|
||||||
self._demo_seconds = self._settings.get("demo_seconds", 10)
|
self._demo_seconds = self._settings.get("demo_seconds", 10)
|
||||||
|
self._demo_fade_seconds = self._settings.get("demo_fade_seconds", 5)
|
||||||
# Opdater tema hvis ændret
|
# Opdater tema hvis ændret
|
||||||
new_dark = self._settings.get("dark_theme", True)
|
new_dark = self._settings.get("dark_theme", True)
|
||||||
if new_dark != self._dark_theme:
|
if new_dark != self._dark_theme:
|
||||||
@@ -498,7 +507,7 @@ class MainWindow(QMainWindow):
|
|||||||
if hasattr(self, "_current_song") and self._current_song:
|
if hasattr(self, "_current_song") and self._current_song:
|
||||||
dur = self._current_song.get("duration_sec", 0)
|
dur = self._current_song.get("duration_sec", 0)
|
||||||
if dur > 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)
|
self._set_status("Indstillinger gemt", 2000)
|
||||||
|
|
||||||
def _auto_login(self):
|
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)
|
self._set_status(f"Synkroniseret {len(levels)} niveauer og {len(names)} dans-navne", 4000)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Dans-sync fejl: {e}")
|
pass
|
||||||
|
|
||||||
def _go_offline(self):
|
def _go_offline(self):
|
||||||
self._api_url = self._api_token = self._api_username = None
|
self._api_url = self._api_token = self._api_username = None
|
||||||
@@ -744,7 +753,7 @@ class MainWindow(QMainWindow):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if dur > 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(f"Indlæst: {song.get('title','—')}", 3000)
|
self._set_status(f"Indlæst: {song.get('title','—')}", 3000)
|
||||||
|
|
||||||
@@ -787,7 +796,10 @@ class MainWindow(QMainWindow):
|
|||||||
else:
|
else:
|
||||||
self._demo_active = True
|
self._demo_active = True
|
||||||
self._btn_demo.setChecked(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("⏸")
|
self._btn_play.setText("⏸")
|
||||||
|
|
||||||
def _prev_song(self):
|
def _prev_song(self):
|
||||||
@@ -853,33 +865,34 @@ class MainWindow(QMainWindow):
|
|||||||
self._load_song(next_song)
|
self._load_song(next_song)
|
||||||
self._set_status(f"Klar: {next_song.get('title','')} — tryk ▶ for at starte")
|
self._set_status(f"Klar: {next_song.get('title','')} — tryk ▶ for at starte")
|
||||||
else:
|
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_title.setText("— Danseliste afsluttet —")
|
||||||
self._lbl_meta.setText("")
|
self._lbl_meta.setText("")
|
||||||
self._lbl_dances.setText("")
|
self._lbl_dances.setText("")
|
||||||
self._set_status("Danselisten er afsluttet")
|
self._set_status("Danselisten er afsluttet")
|
||||||
|
|
||||||
def _sync_event_status_to_playlist(self):
|
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:
|
try:
|
||||||
from local.local_db import get_db
|
pl_id = self._playlist_panel.get_named_playlist_id()
|
||||||
songs = self._playlist_panel.get_songs()
|
if not pl_id:
|
||||||
statuses = self._playlist_panel.get_statuses()
|
|
||||||
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
|
return
|
||||||
# Opdater status for hver sang i listen
|
statuses = self._playlist_panel.get_statuses()
|
||||||
for i, (song, status) in enumerate(zip(songs, statuses)):
|
from local.local_db import get_db
|
||||||
conn.execute("""
|
with get_db() as conn:
|
||||||
UPDATE playlist_songs SET status=?
|
for position, status in enumerate(statuses, start=1):
|
||||||
WHERE playlist_id=? AND song_id=?
|
conn.execute(
|
||||||
""", (status, pl["id"], song.get("id")))
|
"UPDATE playlist_songs SET status=? "
|
||||||
|
"WHERE playlist_id=? AND position=?",
|
||||||
|
(status, pl_id, position)
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Event-status sync fejl: {e}")
|
pass
|
||||||
|
|
||||||
def _on_state_changed(self, state: str):
|
def _on_state_changed(self, state: str):
|
||||||
if state == "playing":
|
if state == "playing":
|
||||||
@@ -900,6 +913,9 @@ class MainWindow(QMainWindow):
|
|||||||
def _on_volume(self, value: int):
|
def _on_volume(self, value: int):
|
||||||
self._lbl_vol.setText(str(value))
|
self._lbl_vol.setText(str(value))
|
||||||
self._player.set_volume(value)
|
self._player.set_volume(value)
|
||||||
|
from ui.settings_dialog import save_settings
|
||||||
|
self._settings["volume"] = value
|
||||||
|
save_settings(self._settings)
|
||||||
|
|
||||||
# ── Tema ──────────────────────────────────────────────────────────────────
|
# ── Tema ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class PlaylistPanel(QWidget):
|
|||||||
self._current_idx = -1
|
self._current_idx = -1
|
||||||
self._song_ended = False
|
self._song_ended = False
|
||||||
self._active_playlist_id: int | None = None
|
self._active_playlist_id: int | None = None
|
||||||
|
self._named_playlist_id: int | None = None # den indlæste/gemte navngivne liste
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
self.setAcceptDrops(True)
|
self.setAcceptDrops(True)
|
||||||
# Autogem-timer — venter 800ms efter sidst ændring
|
# Autogem-timer — venter 800ms efter sidst ændring
|
||||||
@@ -229,7 +230,7 @@ class PlaylistPanel(QWidget):
|
|||||||
from local.local_db import save_event_state
|
from local.local_db import save_event_state
|
||||||
save_event_state(self._current_idx, self._statuses)
|
save_event_state(self._current_idx, self._statuses)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Event-state gem fejl: {e}")
|
pass
|
||||||
|
|
||||||
def _trigger_event_state_save(self):
|
def _trigger_event_state_save(self):
|
||||||
self._event_state_timer.start()
|
self._event_state_timer.start()
|
||||||
@@ -250,9 +251,12 @@ class PlaylistPanel(QWidget):
|
|||||||
self._refresh()
|
self._refresh()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Event-state gendan fejl: {e}")
|
pass
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def get_named_playlist_id(self) -> int | None:
|
||||||
|
return self._named_playlist_id
|
||||||
|
|
||||||
def next_playable_idx(self) -> int | None:
|
def next_playable_idx(self) -> int | None:
|
||||||
"""Find første sang fra toppen der ikke er 'skipped' eller 'played'."""
|
"""Find første sang fra toppen der ikke er 'skipped' eller 'played'."""
|
||||||
for i in range(len(self._songs)):
|
for i in range(len(self._songs)):
|
||||||
@@ -286,7 +290,7 @@ class PlaylistPanel(QWidget):
|
|||||||
self.playlist_changed.emit()
|
self.playlist_changed.emit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._lbl_autosave.setText(f"⚠ gemfejl")
|
self._lbl_autosave.setText(f"⚠ gemfejl")
|
||||||
print(f"Autogem fejl: {e}")
|
pass
|
||||||
|
|
||||||
def restore_active_playlist(self):
|
def restore_active_playlist(self):
|
||||||
"""Indlæs den sidst aktive liste ved opstart."""
|
"""Indlæs den sidst aktive liste ved opstart."""
|
||||||
@@ -324,7 +328,7 @@ class PlaylistPanel(QWidget):
|
|||||||
self._lbl_autosave.setText("✓ gendannet")
|
self._lbl_autosave.setText("✓ gendannet")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Gendan aktiv liste fejl: {e}")
|
pass
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# ── Ny / Gem som / Hent ───────────────────────────────────────────────────
|
# ── Ny / Gem som / Hent ───────────────────────────────────────────────────
|
||||||
@@ -362,6 +366,7 @@ class PlaylistPanel(QWidget):
|
|||||||
for i, song in enumerate(self._songs, start=1):
|
for i, song in enumerate(self._songs, start=1):
|
||||||
if song.get("id"):
|
if song.get("id"):
|
||||||
add_song_to_playlist(pl_id, song["id"], position=i)
|
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._title_label.setText(f"DANSELISTE — {name.upper()}")
|
||||||
self._lbl_autosave.setText(f"✓ gemt som \"{name}\"")
|
self._lbl_autosave.setText(f"✓ gemt som \"{name}\"")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -400,11 +405,12 @@ class PlaylistPanel(QWidget):
|
|||||||
from local.local_db import get_db
|
from local.local_db import get_db
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
songs_raw = conn.execute("""
|
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
|
JOIN songs s ON s.id = ps.song_id
|
||||||
WHERE ps.playlist_id=? ORDER BY ps.position
|
WHERE ps.playlist_id=? ORDER BY ps.position
|
||||||
""", (pl_id,)).fetchall()
|
""", (pl_id,)).fetchall()
|
||||||
songs = []
|
songs = []
|
||||||
|
statuses = []
|
||||||
for row in songs_raw:
|
for row in songs_raw:
|
||||||
dances = conn.execute(
|
dances = conn.execute(
|
||||||
"SELECT dance_name FROM song_dances WHERE song_id=? ORDER BY dance_order",
|
"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"]),
|
"file_missing": bool(row["file_missing"]),
|
||||||
"dances": [d["dance_name"] for d in dances],
|
"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:
|
except Exception as e:
|
||||||
QMessageBox.warning(self, "Fejl", f"Kunne ikke indlæse listen: {e}")
|
QMessageBox.warning(self, "Fejl", f"Kunne ikke indlæse listen: {e}")
|
||||||
|
|
||||||
|
|||||||
@@ -13,19 +13,22 @@ from PyQt6.QtCore import Qt, QSettings
|
|||||||
|
|
||||||
SETTINGS_KEY_THEME = "appearance/dark_theme"
|
SETTINGS_KEY_THEME = "appearance/dark_theme"
|
||||||
SETTINGS_KEY_DEMO_SEC = "playback/demo_seconds"
|
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_MAIL_PATH = "mail/custom_path"
|
||||||
SETTINGS_KEY_AUTO_LOGIN = "online/auto_login"
|
SETTINGS_KEY_AUTO_LOGIN = "online/auto_login"
|
||||||
SETTINGS_KEY_USERNAME = "online/username"
|
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:
|
def load_settings() -> dict:
|
||||||
"""Indlæs alle indstillinger med fornuftige standardværdier."""
|
|
||||||
s = QSettings("LineDance", "Player")
|
s = QSettings("LineDance", "Player")
|
||||||
return {
|
return {
|
||||||
"dark_theme": s.value(SETTINGS_KEY_THEME, True, type=bool),
|
"dark_theme": s.value(SETTINGS_KEY_THEME, True, type=bool),
|
||||||
"demo_seconds": s.value(SETTINGS_KEY_DEMO_SEC, 10, type=int),
|
"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_client": s.value(SETTINGS_KEY_MAIL_CLIENT, "auto"),
|
||||||
"mail_path": s.value(SETTINGS_KEY_MAIL_PATH, ""),
|
"mail_path": s.value(SETTINGS_KEY_MAIL_PATH, ""),
|
||||||
"auto_login": s.value(SETTINGS_KEY_AUTO_LOGIN, False, type=bool),
|
"auto_login": s.value(SETTINGS_KEY_AUTO_LOGIN, False, type=bool),
|
||||||
@@ -38,6 +41,8 @@ def save_settings(values: dict):
|
|||||||
s = QSettings("LineDance", "Player")
|
s = QSettings("LineDance", "Player")
|
||||||
s.setValue(SETTINGS_KEY_THEME, values.get("dark_theme", True))
|
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_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_CLIENT, values.get("mail_client", "auto"))
|
||||||
s.setValue(SETTINGS_KEY_MAIL_PATH, values.get("mail_path", ""))
|
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_AUTO_LOGIN, values.get("auto_login", False))
|
||||||
@@ -117,9 +122,21 @@ class SettingsDialog(QDialog):
|
|||||||
self._spin_demo.setFixedWidth(140)
|
self._spin_demo.setFixedWidth(140)
|
||||||
grp_layout.addRow("Forspil-længde:", self._spin_demo)
|
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(
|
note = QLabel(
|
||||||
"Forspillet afspiller begyndelsen af sangen så arrangøren kan bekræfte\n"
|
"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.setObjectName("result_count")
|
||||||
note.setWordWrap(True)
|
note.setWordWrap(True)
|
||||||
@@ -224,6 +241,7 @@ class SettingsDialog(QDialog):
|
|||||||
v = self._values
|
v = self._values
|
||||||
self._chk_dark.setChecked(v.get("dark_theme", True))
|
self._chk_dark.setChecked(v.get("dark_theme", True))
|
||||||
self._spin_demo.setValue(v.get("demo_seconds", 10))
|
self._spin_demo.setValue(v.get("demo_seconds", 10))
|
||||||
|
self._spin_fade.setValue(v.get("demo_fade_seconds", 5))
|
||||||
|
|
||||||
# Mail
|
# Mail
|
||||||
client = v.get("mail_client", "auto")
|
client = v.get("mail_client", "auto")
|
||||||
@@ -248,6 +266,7 @@ class SettingsDialog(QDialog):
|
|||||||
values = {
|
values = {
|
||||||
"dark_theme": self._chk_dark.isChecked(),
|
"dark_theme": self._chk_dark.isChecked(),
|
||||||
"demo_seconds": self._spin_demo.value(),
|
"demo_seconds": self._spin_demo.value(),
|
||||||
|
"demo_fade_seconds": self._spin_fade.value(),
|
||||||
"mail_client": self._mail_combo.currentData(),
|
"mail_client": self._mail_combo.currentData(),
|
||||||
"mail_path": self._mail_path.text().strip(),
|
"mail_path": self._mail_path.text().strip(),
|
||||||
"auto_login": self._chk_auto_login.isChecked(),
|
"auto_login": self._chk_auto_login.isChecked(),
|
||||||
|
|||||||
@@ -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:
|
Danse gemmes til MP3-filen via mutagen.
|
||||||
Mine danse | Fællesskabets danse
|
Niveau og alternativ-danse gemmes til SQLite.
|
||||||
Mine alternativer | Fællesskabets alternativer
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
||||||
QPushButton, QListWidget, QListWidgetItem, QFrame,
|
QPushButton, QComboBox, QWidget, QMessageBox, QGroupBox,
|
||||||
QSplitter, QWidget, QMessageBox, QComboBox, QCompleter,
|
QScrollArea, QFrame, QGridLayout,
|
||||||
QGridLayout, QGroupBox,
|
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt, QTimer, QStringListModel, pyqtSignal
|
from PyQt6.QtCore import Qt, QTimer, QStringListModel
|
||||||
from PyQt6.QtGui import QColor
|
from PyQt6.QtWidgets import QCompleter
|
||||||
|
|
||||||
|
|
||||||
class AutoCompleteLineEdit(QLineEdit):
|
# ── Autoudfyld søgefelt ───────────────────────────────────────────────────────
|
||||||
"""QLineEdit med autoudfyld fra dans-navne databasen."""
|
|
||||||
|
|
||||||
def __init__(self, placeholder: str = "", parent=None):
|
class AutoLineEdit(QLineEdit):
|
||||||
|
def __init__(self, placeholder="", parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setPlaceholderText(placeholder)
|
self.setPlaceholderText(placeholder)
|
||||||
self._completer_model = QStringListModel()
|
self._model = QStringListModel()
|
||||||
self._completer = QCompleter(self._completer_model, self)
|
comp = QCompleter(self._model, self)
|
||||||
self._completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
|
comp.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
|
||||||
self._completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
|
comp.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
|
||||||
self._completer.setMaxVisibleItems(12)
|
comp.setMaxVisibleItems(10)
|
||||||
self.setCompleter(self._completer)
|
self.setCompleter(comp)
|
||||||
self._timer = QTimer(self)
|
t = QTimer(self)
|
||||||
self._timer.setSingleShot(True)
|
t.setSingleShot(True)
|
||||||
self._timer.setInterval(150)
|
t.setInterval(200)
|
||||||
self._timer.timeout.connect(self._update_suggestions)
|
t.timeout.connect(self._suggest)
|
||||||
self.textChanged.connect(lambda _: self._timer.start())
|
self.textChanged.connect(lambda _: t.start())
|
||||||
|
self._timer = t
|
||||||
|
|
||||||
def _update_suggestions(self):
|
def _suggest(self):
|
||||||
prefix = self.text().strip()
|
prefix = self.text().strip()
|
||||||
if len(prefix) < 1:
|
if not prefix:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
from local.local_db import get_dance_name_suggestions
|
from local.local_db import get_dance_name_suggestions
|
||||||
names = get_dance_name_suggestions(prefix, limit=20)
|
self._model.setStringList(get_dance_name_suggestions(prefix))
|
||||||
self._completer_model.setStringList(names)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class DanceRow(QWidget):
|
# ── Niveau dropdown ───────────────────────────────────────────────────────────
|
||||||
"""Én dans med navn og niveau-dropdown."""
|
|
||||||
removed = pyqtSignal()
|
|
||||||
|
|
||||||
def __init__(self, dance_name: str = "", level_id: int | None = None,
|
def make_level_combo(levels: list, current_id=None) -> QComboBox:
|
||||||
levels: list = [], readonly: bool = False, parent=None):
|
cb = QComboBox()
|
||||||
super().__init__(parent)
|
cb.addItem("— intet niveau —", None)
|
||||||
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:
|
for lvl in levels:
|
||||||
self._level_combo.addItem(lvl["name"], lvl["id"])
|
cb.addItem(lvl["name"], lvl["id"])
|
||||||
self._level_data.append(lvl["id"])
|
if current_id is not None:
|
||||||
if level_id is not None:
|
for i in range(cb.count()):
|
||||||
for i, lid in enumerate(self._level_data):
|
if cb.itemData(i) == current_id:
|
||||||
if lid == level_id:
|
cb.setCurrentIndex(i)
|
||||||
self._level_combo.setCurrentIndex(i)
|
|
||||||
break
|
break
|
||||||
self._level_combo.setFixedWidth(130)
|
cb.setFixedWidth(130)
|
||||||
self._level_combo.setEnabled(not readonly)
|
return cb
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
class AltRow(QWidget):
|
# ── Hoved-dialog ─────────────────────────────────────────────────────────────
|
||||||
"""É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 ""
|
|
||||||
|
|
||||||
|
|
||||||
class TagEditorDialog(QDialog):
|
class TagEditorDialog(QDialog):
|
||||||
def __init__(self, song: dict, parent=None):
|
def __init__(self, song: dict, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._song = song
|
self._song = song
|
||||||
self._levels = []
|
self._levels = []
|
||||||
self._my_dance_rows: list[DanceRow] = []
|
self._dances = [] # list of {name, level_id, db_id}
|
||||||
self._my_alt_rows: list[AltRow] = []
|
self._alts = [] # list of {name, level_id, note}
|
||||||
self.setWindowTitle(f"Rediger tags — {song.get('title','')}")
|
|
||||||
self.setMinimumSize(860, 620)
|
self.setWindowTitle(f"Rediger tags — {song.get('title', '')}")
|
||||||
|
self.setMinimumSize(720, 500)
|
||||||
|
self.resize(820, 580)
|
||||||
|
|
||||||
self._load_levels()
|
self._load_levels()
|
||||||
|
self._load_existing()
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
self._load_data()
|
|
||||||
|
# ── Indlæsning ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _load_levels(self):
|
def _load_levels(self):
|
||||||
try:
|
try:
|
||||||
from local.local_db import get_dance_levels
|
from local.local_db import get_dance_levels
|
||||||
self._levels = [dict(r) for r in get_dance_levels()]
|
self._levels = [dict(r) for r in get_dance_levels()]
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
pass # log fejl
|
||||||
self._levels = []
|
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):
|
def _build_ui(self):
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
layout.setContentsMargins(16, 16, 16, 16)
|
layout.setContentsMargins(12, 12, 12, 12)
|
||||||
layout.setSpacing(10)
|
layout.setSpacing(8)
|
||||||
|
|
||||||
# ── Sang-info ─────────────────────────────────────────────────────────
|
# Sang-info
|
||||||
info = QFrame()
|
info = QFrame()
|
||||||
info.setObjectName("track_display")
|
info.setObjectName("track_display")
|
||||||
info_layout = QHBoxLayout(info)
|
il = QHBoxLayout(info)
|
||||||
info_layout.setContentsMargins(10, 8, 10, 8)
|
il.setContentsMargins(10, 8, 10, 8)
|
||||||
title_col = QVBoxLayout()
|
lbl_t = QLabel(self._song.get("title", "—"))
|
||||||
lbl_title = QLabel(self._song.get("title", "—"))
|
lbl_t.setObjectName("track_title")
|
||||||
lbl_title.setObjectName("track_title")
|
il.addWidget(lbl_t, stretch=1)
|
||||||
title_col.addWidget(lbl_title)
|
fmt = self._song.get("file_format", "").lower()
|
||||||
meta = f"{self._song.get('artist','')} · {self._song.get('bpm',0)} BPM · {self._song.get('file_format','').upper()}"
|
can_write = fmt in ("mp3", "flac", "ogg", "opus", "m4a")
|
||||||
lbl_meta = QLabel(meta)
|
lbl_w = QLabel("✓ Danse skrives til filen" if can_write
|
||||||
lbl_meta.setObjectName("track_meta")
|
else "⚠ Dette format understøtter ikke fil-skrivning")
|
||||||
title_col.addWidget(lbl_meta)
|
lbl_w.setObjectName("result_count")
|
||||||
can_write = self._song.get("file_format","").lower() in ("mp3","flac","ogg","opus","m4a")
|
il.addWidget(lbl_w)
|
||||||
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)
|
|
||||||
layout.addWidget(info)
|
layout.addWidget(info)
|
||||||
|
|
||||||
# ── Fire paneler i 2x2 grid ───────────────────────────────────────────
|
# To kolonner
|
||||||
grid = QWidget()
|
cols = QHBoxLayout()
|
||||||
grid_layout = QGridLayout(grid)
|
cols.setSpacing(12)
|
||||||
grid_layout.setSpacing(8)
|
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)
|
# Knapper
|
||||||
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 ───────────────────────────────────────────────────────────
|
|
||||||
btn_row = QHBoxLayout()
|
btn_row = QHBoxLayout()
|
||||||
btn_row.addStretch()
|
btn_row.addStretch()
|
||||||
btn_cancel = QPushButton("Annuller")
|
btn_cancel = QPushButton("Annuller")
|
||||||
@@ -243,202 +166,262 @@ class TagEditorDialog(QDialog):
|
|||||||
btn_row.addWidget(btn_save)
|
btn_row.addWidget(btn_save)
|
||||||
layout.addLayout(btn_row)
|
layout.addLayout(btn_row)
|
||||||
|
|
||||||
# ── Mine danse ────────────────────────────────────────────────────────────
|
def _build_dances_panel(self) -> QGroupBox:
|
||||||
|
grp = QGroupBox("Danse")
|
||||||
def _build_my_dances_panel(self) -> QGroupBox:
|
|
||||||
grp = QGroupBox("Mine danse")
|
|
||||||
layout = QVBoxLayout(grp)
|
layout = QVBoxLayout(grp)
|
||||||
layout.setSpacing(4)
|
|
||||||
|
|
||||||
self._my_dances_container = QVBoxLayout()
|
# Scroll-område til eksisterende danse
|
||||||
layout.addLayout(self._my_dances_container)
|
scroll = QScrollArea()
|
||||||
layout.addStretch()
|
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()
|
add_row = QHBoxLayout()
|
||||||
self._new_dance_input = AutoCompleteLineEdit("Ny dans...", self)
|
self._new_alt = AutoLineEdit("Nyt alternativ...", self)
|
||||||
self._new_dance_input.returnPressed.connect(self._add_my_dance)
|
self._new_alt.returnPressed.connect(self._on_add_alt)
|
||||||
add_row.addWidget(self._new_dance_input)
|
add_row.addWidget(self._new_alt)
|
||||||
btn_add = QPushButton("+ Tilføj")
|
btn = QPushButton("+ Tilføj")
|
||||||
btn_add.clicked.connect(self._add_my_dance)
|
btn.setFixedWidth(70)
|
||||||
add_row.addWidget(btn_add)
|
btn.clicked.connect(self._on_add_alt)
|
||||||
|
add_row.addWidget(btn)
|
||||||
layout.addLayout(add_row)
|
layout.addLayout(add_row)
|
||||||
|
|
||||||
return grp
|
return grp
|
||||||
|
|
||||||
def _add_my_dance(self, name: str = "", level_id=None):
|
def _add_alt_row(self, name="", level_id=None, note=""):
|
||||||
n = name or self._new_dance_input.text().strip()
|
row_widget = QWidget()
|
||||||
if not n:
|
row_layout = QHBoxLayout(row_widget)
|
||||||
return
|
row_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
row = DanceRow(n, level_id, self._levels, readonly=False, parent=self)
|
row_layout.setSpacing(4)
|
||||||
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 _remove_dance_row(self, row: DanceRow):
|
lbl = QLabel("→")
|
||||||
self._my_dance_rows.remove(row)
|
lbl.setObjectName("track_meta")
|
||||||
self._my_dances_container.removeWidget(row)
|
row_layout.addWidget(lbl)
|
||||||
row.deleteLater()
|
|
||||||
|
|
||||||
# ── 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:
|
level_cb = make_level_combo(self._levels, level_id)
|
||||||
grp = QGroupBox("Fællesskabets danse")
|
row_layout.addWidget(level_cb)
|
||||||
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
|
|
||||||
|
|
||||||
# ── 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:
|
btn_rm = QPushButton("✕")
|
||||||
grp = QGroupBox("Mine alternativ-danse")
|
btn_rm.setFixedSize(24, 24)
|
||||||
layout = QVBoxLayout(grp)
|
row_layout.addWidget(btn_rm)
|
||||||
layout.setSpacing(4)
|
|
||||||
self._my_alts_container = QVBoxLayout()
|
|
||||||
layout.addLayout(self._my_alts_container)
|
|
||||||
layout.addStretch()
|
|
||||||
|
|
||||||
add_row = QHBoxLayout()
|
idx = self._alt_layout.count() - 1
|
||||||
self._new_alt_input = AutoCompleteLineEdit("Alternativ dansenavn...", self)
|
self._alt_layout.insertWidget(idx, row_widget)
|
||||||
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
|
|
||||||
|
|
||||||
def _add_my_alt(self, name: str = "", level_id=None, note: str = ""):
|
entry = {"widget": row_widget, "name": name_edit,
|
||||||
n = name or self._new_alt_input.text().strip()
|
"level": level_cb, "note": note_edit}
|
||||||
if not n:
|
self._alt_rows.append(entry)
|
||||||
return
|
btn_rm.clicked.connect(lambda: self._remove_alt_row(entry))
|
||||||
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()
|
|
||||||
|
|
||||||
def _remove_alt_row(self, row: AltRow):
|
def _remove_alt_row(self, entry):
|
||||||
self._my_alt_rows.remove(row)
|
self._alt_rows.remove(entry)
|
||||||
self._my_alts_container.removeWidget(row)
|
entry["widget"].deleteLater()
|
||||||
row.deleteLater()
|
|
||||||
|
|
||||||
# ── Fællesskabets alternativer ────────────────────────────────────────────
|
def _on_add_alt(self):
|
||||||
|
name = self._new_alt.text().strip()
|
||||||
def _build_community_alts_panel(self) -> QGroupBox:
|
if name:
|
||||||
grp = QGroupBox("Fællesskabets alternativ-danse")
|
self._add_alt_row(name)
|
||||||
layout = QVBoxLayout(grp)
|
self._new_alt.clear()
|
||||||
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}")
|
|
||||||
|
|
||||||
# ── Gem ───────────────────────────────────────────────────────────────────
|
# ── Gem ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _save(self):
|
def _save(self):
|
||||||
|
import uuid
|
||||||
song_id = self._song.get("id")
|
song_id = self._song.get("id")
|
||||||
local_path = self._song.get("local_path", "")
|
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:
|
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
|
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()]
|
|
||||||
|
|
||||||
dance_ids = []
|
conn = new_conn()
|
||||||
with get_db() as conn:
|
|
||||||
# Slet eksisterende danse og alternativer
|
# Slet gammelt
|
||||||
old_dances = conn.execute(
|
old = conn.execute(
|
||||||
"SELECT id FROM song_dances WHERE song_id=?", (song_id,)
|
"SELECT id FROM song_dances WHERE song_id=?", (song_id,)
|
||||||
).fetchall()
|
).fetchall()
|
||||||
for od in old_dances:
|
for o in old:
|
||||||
conn.execute("DELETE FROM dance_alternatives WHERE song_dance_id=?", (od["id"],))
|
conn.execute(
|
||||||
|
"DELETE FROM dance_alternatives WHERE song_dance_id=?",
|
||||||
|
(o["id"],)
|
||||||
|
)
|
||||||
conn.execute("DELETE FROM song_dances WHERE song_id=?", (song_id,))
|
conn.execute("DELETE FROM song_dances WHERE song_id=?", (song_id,))
|
||||||
|
|
||||||
# Indsæt nye danse og hent IDs
|
# Indsæt danse
|
||||||
for i, (name, level_id) in enumerate(dances, start=1):
|
dance_ids = []
|
||||||
|
for i, (name, level_id) in enumerate(dances, 1):
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO song_dances (song_id, dance_name, dance_order, level_id) "
|
"INSERT INTO song_dances "
|
||||||
"VALUES (?,?,?,?)",
|
"(song_id, dance_name, dance_order, level_id) VALUES (?,?,?,?)",
|
||||||
(song_id, name, i, level_id)
|
(song_id, name, i, level_id)
|
||||||
)
|
)
|
||||||
new_id = conn.execute(
|
row = conn.execute(
|
||||||
"SELECT id FROM song_dances WHERE song_id=? AND dance_order=?",
|
"SELECT id FROM song_dances "
|
||||||
(song_id, i)
|
"WHERE song_id=? AND dance_order=?", (song_id, i)
|
||||||
).fetchone()["id"]
|
).fetchone()
|
||||||
dance_ids.append(new_id)
|
dance_ids.append(row["id"])
|
||||||
register_dance_name(name)
|
|
||||||
|
|
||||||
# Indsæt alternativer knyttet til første dans
|
# Opdater dance_names
|
||||||
if dance_ids and self._my_alt_rows:
|
existing = conn.execute(
|
||||||
first_dance_id = dance_ids[0]
|
"SELECT id FROM dance_names WHERE name=? COLLATE NOCASE",
|
||||||
for row in self._my_alt_rows:
|
(name,)
|
||||||
name = row.get_name()
|
).fetchone()
|
||||||
if name:
|
if existing:
|
||||||
import uuid as _uuid
|
conn.execute(
|
||||||
conn.execute("""
|
"UPDATE dance_names SET use_count=use_count+1 WHERE id=?",
|
||||||
INSERT INTO dance_alternatives
|
(existing["id"],)
|
||||||
(id, song_dance_id, alt_dance_name, level_id, note, source)
|
)
|
||||||
VALUES (?,?,?,?,?,'local')
|
else:
|
||||||
""", (str(_uuid.uuid4()), first_dance_id,
|
conn.execute(
|
||||||
name, row.get_level_id(), row.get_note()))
|
"INSERT INTO dance_names (name, source, use_count) "
|
||||||
register_dance_name(name)
|
"VALUES (?,?,1)", (name, "local")
|
||||||
|
)
|
||||||
|
|
||||||
# Skriv til fil
|
# Indsæt alternativer på første dans
|
||||||
if local_path and can_write_dances(local_path):
|
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")
|
||||||
|
)
|
||||||
|
|
||||||
|
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]
|
dance_names = [n for n, _ in dances]
|
||||||
ok = write_dances(local_path, dance_names)
|
if not write_dances(local_path, dance_names):
|
||||||
if not ok:
|
QMessageBox.warning(
|
||||||
QMessageBox.warning(self, "Advarsel",
|
self, "Advarsel",
|
||||||
"Tags gemt i database, men kunne ikke skrives til filen.")
|
"Gemt i database, men kunne ikke skrive til filen."
|
||||||
|
)
|
||||||
|
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
||||||
except Exception as e:
|
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